1 | //************************************ bs::framework - Copyright 2018 Marko Pintera **************************************// |
2 | //*********** Licensed under the MIT license. See LICENSE.md for full terms. This notice is not to be removed. ***********// |
3 | #include "GUI/BsGUIDropDownMenu.h" |
4 | #include "GUI/BsGUIPanel.h" |
5 | #include "GUI/BsGUILayoutY.h" |
6 | #include "GUI/BsGUILayoutX.h" |
7 | #include "GUI/BsGUITexture.h" |
8 | #include "GUI/BsGUILabel.h" |
9 | #include "GUI/BsGUIButton.h" |
10 | #include "GUI/BsGUISpace.h" |
11 | #include "GUI/BsGUIContent.h" |
12 | #include "GUI/BsGUISkin.h" |
13 | #include "RenderAPI/BsViewport.h" |
14 | #include "GUI/BsGUIListBox.h" |
15 | #include "GUI/BsGUIDropDownBoxManager.h" |
16 | #include "Scene/BsSceneObject.h" |
17 | #include "GUI/BsGUIDropDownHitBox.h" |
18 | #include "GUI/BsGUIDropDownContent.h" |
19 | #include "Renderer/BsCamera.h" |
20 | #include "Debug/BsDebug.h" |
21 | |
22 | using namespace std::placeholders; |
23 | |
24 | namespace bs |
25 | { |
26 | const UINT32 GUIDropDownMenu:: = 250; |
27 | |
28 | GUIDropDownDataEntry GUIDropDownDataEntry::separator() |
29 | { |
30 | GUIDropDownDataEntry data; |
31 | data.mType = Type::Separator; |
32 | data.mCallback = nullptr; |
33 | |
34 | return data; |
35 | } |
36 | |
37 | GUIDropDownDataEntry GUIDropDownDataEntry::button(const String& label, std::function<void()> callback, const String& shortcutTag) |
38 | { |
39 | GUIDropDownDataEntry data; |
40 | data.mLabel = label; |
41 | data.mType = Type::Entry; |
42 | data.mCallback = callback; |
43 | data.mShortcutTag = shortcutTag; |
44 | |
45 | return data; |
46 | } |
47 | |
48 | GUIDropDownDataEntry GUIDropDownDataEntry::(const String& label, const GUIDropDownData& data) |
49 | { |
50 | GUIDropDownDataEntry dataEntry; |
51 | dataEntry.mLabel = label; |
52 | dataEntry.mType = Type::SubMenu; |
53 | dataEntry.mChildData = data; |
54 | |
55 | return dataEntry; |
56 | } |
57 | |
58 | GUIDropDownMenu::GUIDropDownMenu(const HSceneObject& parent, const DROP_DOWN_BOX_DESC& desc, GUIDropDownType type) |
59 | :CGUIWidget(parent, desc.camera), mRootMenu(nullptr), mFrontHitBox(nullptr), mBackHitBox(nullptr), mCaptureHitBox(nullptr) |
60 | { |
61 | String stylePrefix = "" ; |
62 | switch(type) |
63 | { |
64 | case GUIDropDownType::ContextMenu: |
65 | stylePrefix = "ContextMenu" ; |
66 | break; |
67 | case GUIDropDownType::ListBox: |
68 | case GUIDropDownType::MultiListBox: |
69 | stylePrefix = "ListBox" ; |
70 | break; |
71 | case GUIDropDownType::MenuBar: |
72 | stylePrefix = "MenuBar" ; |
73 | break; |
74 | } |
75 | |
76 | mScrollUpStyle = stylePrefix + "ScrollUpBtn" ; |
77 | mScrollDownStyle = stylePrefix + "ScrollDownBtn" ; |
78 | mBackgroundStyle = stylePrefix + "Frame" ; |
79 | mContentStyle = stylePrefix + "Content" ; |
80 | mSideBackgroundStyle = stylePrefix + "SidebarBg" ; |
81 | mHandleStyle = stylePrefix + "Handle" ; |
82 | |
83 | setDepth(0); // Needs to be in front of everything |
84 | setSkin(desc.skin); |
85 | |
86 | mFrontHitBox = GUIDropDownHitBox::create(false, false); |
87 | mFrontHitBox->onFocusLost.connect(std::bind(&GUIDropDownMenu::dropDownFocusLost, this)); |
88 | mFrontHitBox->setFocus(true); |
89 | GUILayoutData hitboxLayoutData = mFrontHitBox->_getLayoutData(); |
90 | hitboxLayoutData.setWidgetDepth(0); |
91 | hitboxLayoutData.setPanelDepth(std::numeric_limits<INT16>::min()); |
92 | mFrontHitBox->_setLayoutData(hitboxLayoutData); |
93 | mFrontHitBox->_changeParentWidget(_getInternal()); |
94 | mFrontHitBox->_markLayoutAsDirty(); |
95 | |
96 | mBackHitBox = GUIDropDownHitBox::create(false, true); |
97 | GUILayoutData backHitboxLayoutData = mBackHitBox->_getLayoutData(); |
98 | backHitboxLayoutData.setWidgetDepth(0); |
99 | backHitboxLayoutData.setPanelDepth(std::numeric_limits<INT16>::max()); |
100 | mBackHitBox->_setLayoutData(backHitboxLayoutData); |
101 | mBackHitBox->_changeParentWidget(_getInternal()); |
102 | mBackHitBox->_markLayoutAsDirty(); |
103 | |
104 | SPtr<Viewport> viewport = desc.camera->getViewport(); |
105 | |
106 | Rect2I targetBounds(0, 0, viewport->getPixelArea().width, viewport->getPixelArea().height); |
107 | Vector<Rect2I> captureBounds; |
108 | targetBounds.cut(desc.additionalBounds, captureBounds); |
109 | |
110 | mCaptureHitBox = GUIDropDownHitBox::create(true, false); |
111 | mCaptureHitBox->setBounds(captureBounds); |
112 | GUILayoutData captureHitboxLayoutData = mCaptureHitBox->_getLayoutData(); |
113 | captureHitboxLayoutData.setWidgetDepth(0); |
114 | captureHitboxLayoutData.setPanelDepth(std::numeric_limits<INT16>::max()); |
115 | mCaptureHitBox->_setLayoutData(captureHitboxLayoutData); |
116 | mCaptureHitBox->_changeParentWidget(_getInternal()); |
117 | mCaptureHitBox->_markLayoutAsDirty(); |
118 | |
119 | mAdditionalCaptureBounds = desc.additionalBounds; |
120 | |
121 | Rect2I availableBounds = viewport->getPixelArea(); |
122 | mRootMenu = bs_new<DropDownSubMenu>(this, nullptr, desc.placement, availableBounds, desc.dropDownData, type, 0); |
123 | } |
124 | |
125 | GUIDropDownMenu::() |
126 | { |
127 | |
128 | } |
129 | |
130 | void GUIDropDownMenu::() |
131 | { |
132 | GUIElement::destroy(mFrontHitBox); |
133 | GUIElement::destroy(mBackHitBox); |
134 | GUIElement::destroy(mCaptureHitBox); |
135 | bs_delete(mRootMenu); |
136 | mRootMenu = nullptr; |
137 | |
138 | CGUIWidget::onDestroyed(); |
139 | } |
140 | |
141 | void GUIDropDownMenu::() |
142 | { |
143 | mRootMenu->closeSubMenu(); |
144 | GUIDropDownBoxManager::instance().closeDropDownBox(); |
145 | } |
146 | |
147 | void GUIDropDownMenu::(DropDownSubMenu* ) |
148 | { |
149 | Vector<Rect2I> bounds; |
150 | while(subMenu != nullptr) |
151 | { |
152 | bounds.push_back(subMenu->getVisibleBounds()); |
153 | |
154 | subMenu = subMenu->mParent; |
155 | } |
156 | |
157 | mBackHitBox->setBounds(bounds); |
158 | |
159 | for (auto& additionalBound : mAdditionalCaptureBounds) |
160 | bounds.push_back(additionalBound); |
161 | |
162 | mFrontHitBox->setBounds(bounds); |
163 | } |
164 | |
165 | void GUIDropDownMenu::(DropDownSubMenu* ) |
166 | { |
167 | Vector<Rect2I> bounds; |
168 | while(subMenu != nullptr) |
169 | { |
170 | bounds.push_back(subMenu->getVisibleBounds()); |
171 | |
172 | subMenu = subMenu->mParent; |
173 | } |
174 | |
175 | mBackHitBox->setBounds(bounds); |
176 | |
177 | for (auto& additionalBound : mAdditionalCaptureBounds) |
178 | bounds.push_back(additionalBound); |
179 | |
180 | mFrontHitBox->setBounds(bounds); |
181 | } |
182 | |
183 | GUIDropDownMenu::DropDownSubMenu::(GUIDropDownMenu* owner, DropDownSubMenu* parent, |
184 | const DropDownAreaPlacement& placement, const Rect2I& availableBounds, const GUIDropDownData& dropDownData, |
185 | GUIDropDownType type, UINT32 depthOffset) |
186 | : mOwner(owner), mType(type), mData(dropDownData), mPage(0), x(0), y(0), width(0), height(0) |
187 | , mDepthOffset(depthOffset), mOpenedUpward(false), mContent(nullptr), mBackgroundFrame(nullptr) |
188 | , mBackgroundPanel(nullptr), mContentPanel(nullptr), mContentLayout(nullptr), mSidebarPanel(nullptr) |
189 | , mParent(parent), mSubMenu(nullptr) |
190 | { |
191 | mAvailableBounds = availableBounds; |
192 | |
193 | const GUIElementStyle* backgroundStyle = mOwner->getSkin().getStyle(mOwner->mBackgroundStyle); |
194 | const GUIElementStyle* = mOwner->getSkin().getStyle(mOwner->mSideBackgroundStyle); |
195 | |
196 | // Create content GUI element |
197 | mContent = GUIDropDownContent::create(this, dropDownData, mOwner->mContentStyle); |
198 | mContent->setKeyboardFocus(true); |
199 | |
200 | // Content area |
201 | mContentPanel = mOwner->getPanel()->addNewElement<GUIPanel>(); |
202 | mContentPanel->setWidth(width); |
203 | mContentPanel->setHeight(height); |
204 | mContentPanel->setDepthRange(100 - depthOffset * 2 - 1); |
205 | |
206 | // Background frame |
207 | mBackgroundPanel = mOwner->getPanel()->addNewElement<GUIPanel>(); |
208 | mBackgroundPanel->setWidth(width); |
209 | mBackgroundPanel->setHeight(height); |
210 | mBackgroundPanel->setDepthRange(100 - depthOffset * 2); |
211 | |
212 | GUILayout* backgroundLayout = mBackgroundPanel->addNewElement<GUILayoutX>(); |
213 | |
214 | mBackgroundFrame = GUITexture::create(TextureScaleMode::StretchToFit, mOwner->mBackgroundStyle); |
215 | backgroundLayout->addElement(mBackgroundFrame); |
216 | |
217 | mContentLayout = mContentPanel->addNewElement<GUILayoutY>(); |
218 | mContentLayout->addElement(mContent); // Note: It's important this is added to the layout before we |
219 | // use it for size calculations, in order for its skin to be assigned |
220 | |
221 | UINT32 dropDownBoxWidth = DROP_DOWN_BOX_WIDTH + sideBarStyle->width; |
222 | |
223 | UINT32 maxNeededHeight = backgroundStyle->margins.top + backgroundStyle->margins.bottom; |
224 | UINT32 numElements = (UINT32)dropDownData.entries.size(); |
225 | for (UINT32 i = 0; i < numElements; i++) |
226 | maxNeededHeight += mContent->getElementHeight(i); |
227 | |
228 | DropDownAreaPlacement::HorzDir horzDir; |
229 | DropDownAreaPlacement::VertDir vertDir; |
230 | Rect2I placementBounds = placement.getOptimalBounds(dropDownBoxWidth, maxNeededHeight, availableBounds, horzDir, vertDir); |
231 | |
232 | mOpenedUpward = vertDir == DropDownAreaPlacement::VertDir::Up; |
233 | |
234 | UINT32 actualY = placementBounds.y; |
235 | if (mOpenedUpward) |
236 | y = placementBounds.y + placementBounds.height; |
237 | else |
238 | y = placementBounds.y; |
239 | |
240 | x = placementBounds.x; |
241 | width = placementBounds.width; |
242 | height = placementBounds.height; |
243 | |
244 | mContentPanel->setPosition(x, actualY); |
245 | mBackgroundPanel->setPosition(x, actualY); |
246 | |
247 | updateGUIElements(); |
248 | |
249 | mOwner->notifySubMenuOpened(this); |
250 | } |
251 | |
252 | GUIDropDownMenu::DropDownSubMenu::() |
253 | { |
254 | closeSubMenu(); |
255 | |
256 | mOwner->notifySubMenuClosed(this); |
257 | |
258 | GUIElement::destroy(mContent); |
259 | |
260 | GUIElement::destroy(mBackgroundFrame); |
261 | |
262 | GUILayout::destroy(mBackgroundPanel); |
263 | GUILayout::destroy(mContentPanel); |
264 | |
265 | if (mSidebarPanel != nullptr) |
266 | GUIPanel::destroy(mSidebarPanel); |
267 | } |
268 | |
269 | Vector<GUIDropDownMenu::DropDownSubMenu::PageInfo> GUIDropDownMenu::DropDownSubMenu::() const |
270 | { |
271 | const GUIElementStyle* backgroundStyle = mOwner->getSkin().getStyle(mOwner->mBackgroundStyle); |
272 | |
273 | INT32 numElements = (INT32)mData.entries.size(); |
274 | |
275 | PageInfo curPageInfo; |
276 | curPageInfo.start = 0; |
277 | curPageInfo.end = 0; |
278 | curPageInfo.idx = 0; |
279 | curPageInfo.height = backgroundStyle->margins.top + backgroundStyle->margins.bottom; |
280 | |
281 | Vector<PageInfo> pageInfos; |
282 | for (INT32 i = 0; i < numElements; i++) |
283 | { |
284 | curPageInfo.height += mContent->getElementHeight((UINT32)i); |
285 | curPageInfo.end++; |
286 | |
287 | if (curPageInfo.height > height) |
288 | { |
289 | // Remove last few elements until we fit again |
290 | while (curPageInfo.height > height && i >= 0) |
291 | { |
292 | curPageInfo.height -= mContent->getElementHeight((UINT32)i); |
293 | curPageInfo.end--; |
294 | |
295 | i--; |
296 | } |
297 | |
298 | // Nothing fits, break out of infinite loop |
299 | if (curPageInfo.start >= curPageInfo.end) |
300 | break; |
301 | |
302 | pageInfos.push_back(curPageInfo); |
303 | |
304 | curPageInfo.start = curPageInfo.end; |
305 | curPageInfo.height = backgroundStyle->margins.top + backgroundStyle->margins.bottom; |
306 | |
307 | curPageInfo.idx++; |
308 | } |
309 | } |
310 | |
311 | if (curPageInfo.start < curPageInfo.end) |
312 | pageInfos.push_back(curPageInfo); |
313 | |
314 | return pageInfos; |
315 | } |
316 | |
317 | void GUIDropDownMenu::DropDownSubMenu::() |
318 | { |
319 | // Remove all elements from content layout |
320 | while(mContentLayout->getNumChildren() > 0) |
321 | mContentLayout->removeElementAt(mContentLayout->getNumChildren() - 1); |
322 | |
323 | mContentLayout->addElement(mContent); // Note: Needs to be added first so that size calculations have proper skin to work with |
324 | |
325 | const GUIElementStyle* backgroundStyle = mOwner->getSkin().getStyle(mOwner->mBackgroundStyle); |
326 | const GUIElementStyle* = mOwner->getSkin().getStyle(mOwner->mSideBackgroundStyle); |
327 | const GUIElementStyle* scrollUpStyle = mOwner->getSkin().getStyle(mOwner->mScrollUpStyle); |
328 | const GUIElementStyle* scrollDownStyle = mOwner->getSkin().getStyle(mOwner->mScrollDownStyle); |
329 | |
330 | Vector<PageInfo> pageInfos = getPageInfos(); |
331 | |
332 | UINT32 pageStart = 0, pageEnd = 0; |
333 | UINT32 pageHeight = 0; |
334 | UINT32 pageCount = (UINT32)pageInfos.size(); |
335 | if (pageCount > mPage) |
336 | { |
337 | pageStart = pageInfos[mPage].start; |
338 | pageEnd = pageInfos[mPage].end; |
339 | pageHeight = pageInfos[mPage].height; |
340 | } |
341 | |
342 | INT32 actualY = y; |
343 | |
344 | if (mOpenedUpward) |
345 | actualY -= (INT32)pageHeight; |
346 | |
347 | // Add sidebar if needed |
348 | UINT32 contentOffset = 0; |
349 | if (pageInfos.size() > 1) |
350 | { |
351 | UINT32 = pageHeight - 2; |
352 | contentOffset = sideBarStyle->width; |
353 | |
354 | if (mSidebarPanel == nullptr) |
355 | { |
356 | mSidebarPanel = mOwner->getPanel()->addNewElement<GUIPanel>(); |
357 | |
358 | mScrollUpBtn = GUIButton::create(HString("" ), mOwner->mScrollUpStyle); |
359 | mScrollUpBtn->onClick.connect(std::bind(&DropDownSubMenu::scrollUp, this)); |
360 | |
361 | GUIElementOptions scrollUpBtnOptions = mScrollUpBtn->getOptionFlags(); |
362 | scrollUpBtnOptions.unset(GUIElementOption::AcceptsKeyFocus); |
363 | |
364 | mScrollUpBtn->setOptionFlags(scrollUpBtnOptions); |
365 | |
366 | mScrollDownBtn = GUIButton::create(HString("" ), mOwner->mScrollDownStyle); |
367 | mScrollDownBtn->onClick.connect(std::bind(&DropDownSubMenu::scrollDown, this)); |
368 | |
369 | GUIElementOptions scrollDownBtnOptions = mScrollDownBtn->getOptionFlags(); |
370 | scrollDownBtnOptions.unset(GUIElementOption::AcceptsKeyFocus); |
371 | |
372 | mScrollDownBtn->setOptionFlags(scrollDownBtnOptions); |
373 | |
374 | mHandle = GUITexture::create(mOwner->mHandleStyle); |
375 | GUITexture* background = GUITexture::create(mOwner->mSideBackgroundStyle); |
376 | background->_setElementDepth(2); |
377 | |
378 | mSidebarPanel->addElement(background); |
379 | mSidebarPanel->addElement(mScrollUpBtn); |
380 | mSidebarPanel->addElement(mScrollDownBtn); |
381 | mSidebarPanel->addElement(mHandle); |
382 | } |
383 | |
384 | mScrollUpBtn->setPosition(1, 1); |
385 | mScrollDownBtn->setPosition(1, sidebarHeight - 1 - scrollDownStyle->height); |
386 | |
387 | UINT32 maxHandleSize = std::max(0, (INT32)sidebarHeight - (INT32)scrollDownStyle->height - (INT32)scrollUpStyle->height - 2); |
388 | UINT32 handleSize = maxHandleSize / pageCount; |
389 | |
390 | INT32 handlePos = 1 + scrollUpStyle->height + mPage * handleSize; |
391 | |
392 | mHandle->setPosition(1, handlePos); |
393 | mHandle->setHeight(handleSize); |
394 | |
395 | mSidebarPanel->setPosition(x, actualY); |
396 | mSidebarPanel->setWidth(sideBarStyle->width); |
397 | mSidebarPanel->setHeight(sidebarHeight); |
398 | } |
399 | else |
400 | { |
401 | if (mSidebarPanel != nullptr) |
402 | { |
403 | GUIPanel::destroy(mSidebarPanel); |
404 | mSidebarPanel = nullptr; |
405 | } |
406 | } |
407 | |
408 | mContent->setRange(pageStart, pageEnd); |
409 | |
410 | if (mSubMenu == nullptr) |
411 | mContent->setKeyboardFocus(true); |
412 | |
413 | // Resize and reposition areas |
414 | mBackgroundPanel->setWidth(width - contentOffset); |
415 | mBackgroundPanel->setHeight(pageHeight); |
416 | mBackgroundPanel->setPosition(x + contentOffset, actualY); |
417 | |
418 | mVisibleBounds = Rect2I(x, actualY, width, pageHeight); |
419 | |
420 | UINT32 contentWidth = (UINT32)std::max(0, (INT32)width - (INT32)backgroundStyle->margins.left - (INT32)backgroundStyle->margins.right - (INT32)contentOffset); |
421 | UINT32 contentHeight = (UINT32)std::max(0, (INT32)pageHeight - (INT32)backgroundStyle->margins.top - (INT32)backgroundStyle->margins.bottom); |
422 | |
423 | mContentPanel->setWidth(contentWidth); |
424 | mContentPanel->setHeight(contentHeight); |
425 | mContentPanel->setPosition(x + contentOffset + backgroundStyle->margins.left, actualY + backgroundStyle->margins.top); |
426 | } |
427 | |
428 | void GUIDropDownMenu::DropDownSubMenu::() |
429 | { |
430 | mPage++; |
431 | if (mPage == (UINT32)getPageInfos().size()) |
432 | mPage = 0; |
433 | |
434 | updateGUIElements(); |
435 | |
436 | closeSubMenu(); |
437 | } |
438 | |
439 | void GUIDropDownMenu::DropDownSubMenu::() |
440 | { |
441 | if (mPage > 0) |
442 | mPage--; |
443 | else |
444 | mPage = (UINT32)getPageInfos().size() - 1; |
445 | |
446 | updateGUIElements(); |
447 | closeSubMenu(); |
448 | } |
449 | |
450 | void GUIDropDownMenu::DropDownSubMenu::() |
451 | { |
452 | mPage = 0; |
453 | updateGUIElements(); |
454 | |
455 | closeSubMenu(); |
456 | } |
457 | |
458 | void GUIDropDownMenu::DropDownSubMenu::() |
459 | { |
460 | mPage = (UINT32)(getPageInfos().size() - 1); |
461 | updateGUIElements(); |
462 | |
463 | closeSubMenu(); |
464 | } |
465 | |
466 | void GUIDropDownMenu::DropDownSubMenu::() |
467 | { |
468 | if(mSubMenu != nullptr) |
469 | { |
470 | bs_delete(mSubMenu); |
471 | mSubMenu = nullptr; |
472 | |
473 | mContent->setKeyboardFocus(true); |
474 | } |
475 | } |
476 | |
477 | void GUIDropDownMenu::DropDownSubMenu::(UINT32 idx, const Rect2I& bounds) |
478 | { |
479 | closeSubMenu(); |
480 | |
481 | if (!mData.entries[idx].isSubMenu()) |
482 | { |
483 | auto callback = mData.entries[idx].getCallback(); |
484 | if (callback != nullptr) |
485 | callback(); |
486 | |
487 | if (mType != GUIDropDownType::MultiListBox) |
488 | GUIDropDownBoxManager::instance().closeDropDownBox(); |
489 | } |
490 | else |
491 | { |
492 | mContent->setKeyboardFocus(false); |
493 | |
494 | mSubMenu = bs_new<DropDownSubMenu>(mOwner, this, DropDownAreaPlacement::aroundBoundsVert(bounds), |
495 | mAvailableBounds, mData.entries[idx].getSubMenuData(), mType, mDepthOffset + 1); |
496 | } |
497 | } |
498 | |
499 | void GUIDropDownMenu::DropDownSubMenu::() |
500 | { |
501 | if (mParent != nullptr) |
502 | mParent->closeSubMenu(); |
503 | else // We're the last sub-menu, close the whole thing |
504 | GUIDropDownBoxManager::instance().closeDropDownBox(); |
505 | } |
506 | |
507 | void GUIDropDownMenu::DropDownSubMenu::(UINT32 idx) |
508 | { |
509 | closeSubMenu(); |
510 | } |
511 | } |