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
22using namespace std::placeholders;
23
24namespace bs
25{
26 const UINT32 GUIDropDownMenu::DROP_DOWN_BOX_WIDTH = 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::subMenu(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::~GUIDropDownMenu()
126 {
127
128 }
129
130 void GUIDropDownMenu::onDestroyed()
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::dropDownFocusLost()
142 {
143 mRootMenu->closeSubMenu();
144 GUIDropDownBoxManager::instance().closeDropDownBox();
145 }
146
147 void GUIDropDownMenu::notifySubMenuOpened(DropDownSubMenu* subMenu)
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::notifySubMenuClosed(DropDownSubMenu* subMenu)
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::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* sideBarStyle = 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::~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::getPageInfos() 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::updateGUIElements()
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* sideBarStyle = 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 sidebarHeight = 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::scrollDown()
429 {
430 mPage++;
431 if (mPage == (UINT32)getPageInfos().size())
432 mPage = 0;
433
434 updateGUIElements();
435
436 closeSubMenu();
437 }
438
439 void GUIDropDownMenu::DropDownSubMenu::scrollUp()
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::scrollToTop()
451 {
452 mPage = 0;
453 updateGUIElements();
454
455 closeSubMenu();
456 }
457
458 void GUIDropDownMenu::DropDownSubMenu::scrollToBottom()
459 {
460 mPage = (UINT32)(getPageInfos().size() - 1);
461 updateGUIElements();
462
463 closeSubMenu();
464 }
465
466 void GUIDropDownMenu::DropDownSubMenu::closeSubMenu()
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::elementActivated(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::close()
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::elementSelected(UINT32 idx)
508 {
509 closeSubMenu();
510 }
511}