BsGUIDropDownBox.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. #include "BsGUIDropDownBox.h"
  2. #include "BsGUIArea.h"
  3. #include "BsGUILayoutY.h"
  4. #include "BsGUILayoutX.h"
  5. #include "BsGUITexture.h"
  6. #include "BsGUILabel.h"
  7. #include "BsGUIButton.h"
  8. #include "BsGUISpace.h"
  9. #include "BsGUIContent.h"
  10. #include "BsGUISkin.h"
  11. #include "BsViewport.h"
  12. #include "BsGUIListBox.h"
  13. #include "BsGUIDropDownBoxManager.h"
  14. #include "BsSceneObject.h"
  15. #include "BsGUIDropDownHitBox.h"
  16. #include "BsGUIDropDownContent.h"
  17. using namespace std::placeholders;
  18. namespace BansheeEngine
  19. {
  20. const UINT32 GUIDropDownBox::DROP_DOWN_BOX_WIDTH = 250;
  21. GUIDropDownDataEntry GUIDropDownDataEntry::separator()
  22. {
  23. GUIDropDownDataEntry data;
  24. data.mType = Type::Separator;
  25. data.mCallback = nullptr;
  26. return data;
  27. }
  28. GUIDropDownDataEntry GUIDropDownDataEntry::button(const WString& label, std::function<void()> callback, const WString& shortcutTag)
  29. {
  30. GUIDropDownDataEntry data;
  31. data.mLabel = label;
  32. data.mType = Type::Entry;
  33. data.mCallback = callback;
  34. data.mShortcutTag = shortcutTag;
  35. return data;
  36. }
  37. GUIDropDownDataEntry GUIDropDownDataEntry::subMenu(const WString& label, const GUIDropDownData& data)
  38. {
  39. GUIDropDownDataEntry dataEntry;
  40. dataEntry.mLabel = label;
  41. dataEntry.mType = Type::SubMenu;
  42. dataEntry.mChildData = data;
  43. return dataEntry;
  44. }
  45. GUIDropDownAreaPlacement GUIDropDownAreaPlacement::aroundPosition(const Vector2I& position)
  46. {
  47. GUIDropDownAreaPlacement instance;
  48. instance.mType = Type::Position;
  49. instance.mPosition = position;
  50. return instance;
  51. }
  52. GUIDropDownAreaPlacement GUIDropDownAreaPlacement::aroundBoundsVert(const Rect2I& bounds)
  53. {
  54. GUIDropDownAreaPlacement instance;
  55. instance.mType = Type::BoundsVert;
  56. instance.mBounds = bounds;
  57. return instance;
  58. }
  59. GUIDropDownAreaPlacement GUIDropDownAreaPlacement::aroundBoundsHorz(const Rect2I& bounds)
  60. {
  61. GUIDropDownAreaPlacement instance;
  62. instance.mType = Type::BoundsHorz;
  63. instance.mBounds = bounds;
  64. return instance;
  65. }
  66. GUIDropDownBox::GUIDropDownBox(const HSceneObject& parent, Viewport* target, const GUIDropDownAreaPlacement& placement,
  67. const GUIDropDownData& dropDownData, const HGUISkin& skin, GUIDropDownType type)
  68. :GUIWidget(parent, target), mRootMenu(nullptr), mHitBox(nullptr), mCaptureHitBox(nullptr)
  69. {
  70. String stylePrefix = "";
  71. switch(type)
  72. {
  73. case GUIDropDownType::ContextMenu:
  74. stylePrefix = "ContextMenu";
  75. break;
  76. case GUIDropDownType::ListBox:
  77. stylePrefix = "ListBox";
  78. break;
  79. case GUIDropDownType::MenuBar:
  80. stylePrefix = "MenuBar";
  81. break;
  82. }
  83. mScrollUpStyle = stylePrefix + "ScrollUpBtn";
  84. mScrollDownStyle = stylePrefix + "ScrollDownBtn";
  85. mBackgroundStyle = stylePrefix + "Frame";
  86. mContentStyle = stylePrefix + "Content";
  87. mScrollUpBtnArrow = skin->getStyle(stylePrefix + "ScrollUpBtnArrow")->normal.texture;
  88. mScrollDownBtnArrow = skin->getStyle(stylePrefix + "ScrollDownBtnArrow")->normal.texture;
  89. setDepth(0); // Needs to be in front of everything
  90. setSkin(skin);
  91. mHitBox = GUIDropDownHitBox::create(false);
  92. mHitBox->onFocusLost.connect(std::bind(&GUIDropDownBox::dropDownFocusLost, this));
  93. mHitBox->setFocus(true);
  94. mHitBox->_setWidgetDepth(0);
  95. mHitBox->_setAreaDepth(0);
  96. mHitBox->_changeParentWidget(this);
  97. mCaptureHitBox = GUIDropDownHitBox::create(true);
  98. mCaptureHitBox->setBounds(Rect2I(0, 0, target->getWidth(), target->getHeight()));
  99. mCaptureHitBox->_setWidgetDepth(0);
  100. mCaptureHitBox->_setAreaDepth(200);
  101. mCaptureHitBox->_changeParentWidget(this);
  102. Rect2I availableBounds(target->getX(), target->getY(), target->getWidth(), target->getHeight());
  103. mRootMenu = bs_new<DropDownSubMenu>(this, nullptr, placement, availableBounds, dropDownData, type, 0);
  104. }
  105. GUIDropDownBox::~GUIDropDownBox()
  106. {
  107. }
  108. void GUIDropDownBox::onDestroyed()
  109. {
  110. GUIElement::destroy(mHitBox);
  111. GUIElement::destroy(mCaptureHitBox);
  112. bs_delete(mRootMenu);
  113. GUIWidget::onDestroyed();
  114. }
  115. void GUIDropDownBox::dropDownFocusLost()
  116. {
  117. mRootMenu->closeSubMenu();
  118. GUIDropDownBoxManager::instance().closeDropDownBox();
  119. }
  120. void GUIDropDownBox::notifySubMenuOpened(DropDownSubMenu* subMenu)
  121. {
  122. Vector<Rect2I> bounds;
  123. while(subMenu != nullptr)
  124. {
  125. bounds.push_back(subMenu->getVisibleBounds());
  126. subMenu = subMenu->mSubMenu;
  127. }
  128. mHitBox->setBounds(bounds);
  129. }
  130. void GUIDropDownBox::notifySubMenuClosed(DropDownSubMenu* subMenu)
  131. {
  132. Vector<Rect2I> bounds;
  133. while(subMenu != nullptr)
  134. {
  135. bounds.push_back(subMenu->getVisibleBounds());
  136. subMenu = subMenu->mSubMenu;
  137. }
  138. mHitBox->setBounds(bounds);
  139. }
  140. GUIDropDownBox::DropDownSubMenu::DropDownSubMenu(GUIDropDownBox* owner, DropDownSubMenu* parent, const GUIDropDownAreaPlacement& placement,
  141. const Rect2I& availableBounds, const GUIDropDownData& dropDownData, GUIDropDownType type, UINT32 depthOffset)
  142. :mOwner(owner), mParent(parent), mPage(0), mBackgroundFrame(nullptr), mBackgroundArea(nullptr), mContentArea(nullptr),
  143. mContentLayout(nullptr), mScrollUpBtn(nullptr), mScrollDownBtn(nullptr), x(0), y(0), width(0), height(0),
  144. mType(type), mSubMenu(nullptr), mData(dropDownData), mOpenedUpward(false), mDepthOffset(depthOffset), mContent(nullptr)
  145. {
  146. mAvailableBounds = availableBounds;
  147. const GUIElementStyle* scrollUpStyle = mOwner->getSkin().getStyle(mOwner->mScrollUpStyle);
  148. const GUIElementStyle* scrollDownStyle = mOwner->getSkin().getStyle(mOwner->mScrollDownStyle);
  149. const GUIElementStyle* backgroundStyle = mOwner->getSkin().getStyle(mOwner->mBackgroundStyle);
  150. Rect2I dropDownListBounds = placement.getBounds();
  151. int potentialLeftStart = 0;
  152. int potentialRightStart = 0;
  153. int potentialTopStart = 0;
  154. int potentialBottomStart = 0;
  155. switch(placement.getType())
  156. {
  157. case GUIDropDownAreaPlacement::Type::Position:
  158. potentialLeftStart = potentialRightStart = placement.getPosition().x;
  159. potentialTopStart = potentialBottomStart = placement.getPosition().y;
  160. break;
  161. case GUIDropDownAreaPlacement::Type::BoundsHorz:
  162. potentialRightStart = placement.getBounds().x;
  163. potentialLeftStart = placement.getBounds().x + placement.getBounds().width;
  164. potentialBottomStart = placement.getBounds().y + placement.getBounds().height;
  165. potentialTopStart = placement.getBounds().y;
  166. break;
  167. case GUIDropDownAreaPlacement::Type::BoundsVert:
  168. potentialRightStart = placement.getBounds().x + placement.getBounds().width;
  169. potentialLeftStart = placement.getBounds().x;
  170. potentialBottomStart = placement.getBounds().y;
  171. potentialTopStart = placement.getBounds().y + placement.getBounds().height;
  172. break;
  173. }
  174. // Create content GUI element
  175. mContent = GUIDropDownContent::create(this, dropDownData, mOwner->mContentStyle);
  176. mContent->setKeyboardFocus(true);
  177. // Content area
  178. mContentArea = GUIArea::create(*mOwner, 0, 0, width, height);
  179. mContentArea->setDepth(100 - depthOffset * 2 - 1);
  180. mContentLayout = mContentArea->getLayout().addNewElement<GUILayoutY>();
  181. // Background frame
  182. mBackgroundArea = GUIArea::create(*mOwner, 0, 0, width, height);
  183. mBackgroundArea->setDepth(100 - depthOffset * 2);
  184. mBackgroundFrame = GUITexture::create(GUIImageScaleMode::StretchToFit, mOwner->mBackgroundStyle);
  185. mBackgroundArea->getLayout().addElement(mBackgroundFrame);
  186. mContentLayout->addElement(mContent); // Note: It's important this is added to the layout before we
  187. // use it for size calculations, in order for its skin to be assigned
  188. // Determine x position and whether to align to left or right side of the drop down list
  189. UINT32 availableRightwardWidth = (UINT32)std::max(0, (availableBounds.x + availableBounds.width) - potentialRightStart);
  190. UINT32 availableLeftwardWidth = (UINT32)std::max(0, potentialLeftStart - availableBounds.x);
  191. //// Prefer right if possible
  192. if(DROP_DOWN_BOX_WIDTH <= availableRightwardWidth)
  193. {
  194. x = potentialRightStart;
  195. width = DROP_DOWN_BOX_WIDTH;
  196. }
  197. else
  198. {
  199. if(availableRightwardWidth >= availableLeftwardWidth)
  200. {
  201. x = potentialRightStart;
  202. width = std::min(DROP_DOWN_BOX_WIDTH, availableRightwardWidth);
  203. }
  204. else
  205. {
  206. x = potentialLeftStart - std::min(DROP_DOWN_BOX_WIDTH, availableLeftwardWidth);
  207. width = std::min(DROP_DOWN_BOX_WIDTH, availableLeftwardWidth);
  208. }
  209. }
  210. // Determine y position and whether to open upward or downward
  211. UINT32 availableDownwardHeight = (UINT32)std::max(0, (availableBounds.y + availableBounds.height) - potentialBottomStart);
  212. UINT32 availableUpwardHeight = (UINT32)std::max(0, potentialTopStart - availableBounds.y);
  213. //// Prefer down if possible
  214. UINT32 helperElementHeight = scrollUpStyle->height + scrollDownStyle->height +
  215. backgroundStyle->margins.top + backgroundStyle->margins.bottom;
  216. UINT32 maxNeededHeight = helperElementHeight;
  217. UINT32 numElements = (UINT32)dropDownData.entries.size();
  218. for(UINT32 i = 0; i < numElements; i++)
  219. maxNeededHeight += mContent->getElementHeight(i);
  220. height = 0;
  221. if(maxNeededHeight <= availableDownwardHeight)
  222. {
  223. y = potentialBottomStart;
  224. height = availableDownwardHeight;
  225. mOpenedUpward = false;
  226. }
  227. else
  228. {
  229. if(availableDownwardHeight >= availableUpwardHeight)
  230. {
  231. y = potentialBottomStart;
  232. height = availableDownwardHeight;
  233. mOpenedUpward = false;
  234. }
  235. else
  236. {
  237. y = potentialTopStart;
  238. height = availableUpwardHeight;
  239. mOpenedUpward = true;
  240. }
  241. }
  242. INT32 actualY = y;
  243. if(mOpenedUpward)
  244. actualY -= (INT32)std::min(maxNeededHeight, availableUpwardHeight);
  245. mContentArea->setPosition(x, actualY);
  246. mBackgroundArea->setPosition(x, actualY);
  247. updateGUIElements();
  248. mOwner->notifySubMenuOpened(this);
  249. }
  250. GUIDropDownBox::DropDownSubMenu::~DropDownSubMenu()
  251. {
  252. closeSubMenu();
  253. mOwner->notifySubMenuClosed(this);
  254. GUIElement::destroy(mContent);
  255. if (mScrollUpBtn != nullptr)
  256. GUIElement::destroy(mScrollUpBtn);
  257. if (mScrollDownBtn != nullptr)
  258. GUIElement::destroy(mScrollDownBtn);
  259. GUIElement::destroy(mBackgroundFrame);
  260. GUIArea::destroy(mBackgroundArea);
  261. GUIArea::destroy(mContentArea);
  262. }
  263. Vector<GUIDropDownBox::DropDownSubMenu::PageInfo> GUIDropDownBox::DropDownSubMenu::getPageInfos() const
  264. {
  265. const GUIElementStyle* scrollUpStyle = mOwner->getSkin().getStyle(mOwner->mScrollUpStyle);
  266. const GUIElementStyle* scrollDownStyle = mOwner->getSkin().getStyle(mOwner->mScrollDownStyle);
  267. const GUIElementStyle* backgroundStyle = mOwner->getSkin().getStyle(mOwner->mBackgroundStyle);
  268. INT32 numElements = (INT32)mData.entries.size();
  269. PageInfo curPageInfo;
  270. curPageInfo.start = 0;
  271. curPageInfo.end = 0;
  272. curPageInfo.idx = 0;
  273. curPageInfo.height = backgroundStyle->margins.top + backgroundStyle->margins.bottom;
  274. Vector<PageInfo> pageInfos;
  275. for (INT32 i = 0; i < numElements; i++)
  276. {
  277. curPageInfo.height += mContent->getElementHeight((UINT32)i);
  278. curPageInfo.end++;
  279. if (curPageInfo.height > height)
  280. {
  281. curPageInfo.height += scrollDownStyle->height;
  282. // Remove last few elements until we fit again
  283. while (curPageInfo.height > height && i >= 0)
  284. {
  285. curPageInfo.height -= mContent->getElementHeight((UINT32)i);
  286. curPageInfo.end--;
  287. i--;
  288. }
  289. // Nothing fits, break out of infinite loop
  290. if (curPageInfo.start >= curPageInfo.end)
  291. break;
  292. pageInfos.push_back(curPageInfo);
  293. curPageInfo.start = curPageInfo.end;
  294. curPageInfo.height = backgroundStyle->margins.top + backgroundStyle->margins.bottom;
  295. curPageInfo.height += scrollUpStyle->height;
  296. curPageInfo.idx++;
  297. }
  298. }
  299. if (curPageInfo.start < curPageInfo.end)
  300. pageInfos.push_back(curPageInfo);
  301. return pageInfos;
  302. }
  303. void GUIDropDownBox::DropDownSubMenu::updateGUIElements()
  304. {
  305. // Remove all elements from content layout
  306. while(mContentLayout->getNumChildren() > 0)
  307. mContentLayout->removeElementAt(mContentLayout->getNumChildren() - 1);
  308. mContentLayout->addElement(mContent); // Note: Needs to be added first so that size calculations have proper skin to work with
  309. const GUIElementStyle* scrollUpStyle = mOwner->getSkin().getStyle(mOwner->mScrollUpStyle);
  310. const GUIElementStyle* scrollDownStyle = mOwner->getSkin().getStyle(mOwner->mScrollDownStyle);
  311. const GUIElementStyle* backgroundStyle = mOwner->getSkin().getStyle(mOwner->mBackgroundStyle);
  312. // Determine if we need scroll up and/or down buttons, number of visible elements and actual height
  313. bool needsScrollUp = mPage > 0;
  314. UINT32 pageStart = 0, pageEnd = 0;
  315. UINT32 pageHeight = 0;
  316. Vector<PageInfo> pageInfos = getPageInfos();
  317. if (pageInfos.size() > mPage)
  318. {
  319. pageStart = pageInfos[mPage].start;
  320. pageEnd = pageInfos[mPage].end;
  321. pageHeight = pageInfos[mPage].height;
  322. }
  323. UINT32 numElements = (UINT32)mData.entries.size();
  324. bool needsScrollDown = pageEnd != numElements;
  325. // Add scroll up button
  326. if(needsScrollUp)
  327. {
  328. if(mScrollUpBtn == nullptr)
  329. {
  330. mScrollUpBtn = GUIButton::create(GUIContent(HString(L""), mOwner->mScrollUpBtnArrow), mOwner->mScrollUpStyle);
  331. mScrollUpBtn->onClick.connect(std::bind(&DropDownSubMenu::scrollUp, this));
  332. }
  333. mContentLayout->insertElement(0, mScrollUpBtn);
  334. }
  335. else
  336. {
  337. if(mScrollUpBtn != nullptr)
  338. {
  339. GUIElement::destroy(mScrollUpBtn);
  340. mScrollUpBtn = nullptr;
  341. }
  342. }
  343. mContent->setRange(pageStart, pageEnd);
  344. // Add scroll down button
  345. if(needsScrollDown)
  346. {
  347. if(mScrollDownBtn == nullptr)
  348. {
  349. mScrollDownBtn = GUIButton::create(GUIContent(HString(L""), mOwner->mScrollDownBtnArrow), mOwner->mScrollDownStyle);
  350. mScrollDownBtn->onClick.connect(std::bind(&DropDownSubMenu::scrollDown, this));
  351. }
  352. mContentLayout->addElement(mScrollDownBtn);
  353. }
  354. else
  355. {
  356. if(mScrollDownBtn != nullptr)
  357. {
  358. GUIElement::destroy(mScrollDownBtn);
  359. mScrollDownBtn = nullptr;
  360. }
  361. }
  362. // Resize and reposition areas
  363. INT32 actualY = y;
  364. if(mOpenedUpward)
  365. actualY -= (INT32)pageHeight;
  366. mBackgroundArea->setSize(width, pageHeight);
  367. mBackgroundArea->setPosition(x, actualY);
  368. mVisibleBounds = Rect2I(x, actualY, width, pageHeight);
  369. UINT32 contentWidth = (UINT32)std::max(0, (INT32)width - (INT32)backgroundStyle->margins.left - (INT32)backgroundStyle->margins.right);
  370. UINT32 contentHeight = (UINT32)std::max(0, (INT32)pageHeight - (INT32)backgroundStyle->margins.top - (INT32)backgroundStyle->margins.bottom);
  371. mContentArea->setSize(contentWidth, contentHeight);
  372. mContentArea->setPosition(x + backgroundStyle->margins.left, actualY + backgroundStyle->margins.top);
  373. }
  374. void GUIDropDownBox::DropDownSubMenu::scrollDown()
  375. {
  376. mPage++;
  377. if (mPage == (UINT32)getPageInfos().size())
  378. mPage = 0;
  379. updateGUIElements();
  380. closeSubMenu();
  381. }
  382. void GUIDropDownBox::DropDownSubMenu::scrollUp()
  383. {
  384. if (mPage > 0)
  385. mPage--;
  386. else
  387. mPage = (UINT32)getPageInfos().size() - 1;
  388. updateGUIElements();
  389. closeSubMenu();
  390. }
  391. void GUIDropDownBox::DropDownSubMenu::scrollToTop()
  392. {
  393. mPage = 0;
  394. updateGUIElements();
  395. closeSubMenu();
  396. }
  397. void GUIDropDownBox::DropDownSubMenu::scrollToBottom()
  398. {
  399. mPage = (UINT32)(getPageInfos().size() - 1);
  400. updateGUIElements();
  401. closeSubMenu();
  402. }
  403. void GUIDropDownBox::DropDownSubMenu::closeSubMenu()
  404. {
  405. if(mSubMenu != nullptr)
  406. {
  407. bs_delete(mSubMenu);
  408. mSubMenu = nullptr;
  409. mContent->setKeyboardFocus(true);
  410. }
  411. }
  412. void GUIDropDownBox::DropDownSubMenu::elementActivated(UINT32 idx, const Rect2I& bounds)
  413. {
  414. closeSubMenu();
  415. if (!mData.entries[idx].isSubMenu())
  416. {
  417. auto callback = mData.entries[idx].getCallback();
  418. if (callback != nullptr)
  419. callback();
  420. GUIDropDownBoxManager::instance().closeDropDownBox();
  421. }
  422. else
  423. {
  424. mContent->setKeyboardFocus(false);
  425. mSubMenu = bs_new<DropDownSubMenu>(mOwner, this, GUIDropDownAreaPlacement::aroundBoundsVert(bounds),
  426. mAvailableBounds, mData.entries[idx].getSubMenuData(), mType, mDepthOffset + 1);
  427. }
  428. }
  429. void GUIDropDownBox::DropDownSubMenu::close()
  430. {
  431. if (mParent != nullptr)
  432. mParent->closeSubMenu();
  433. else // We're the last sub-menu, close the whole thing
  434. GUIDropDownBoxManager::instance().closeDropDownBox();
  435. }
  436. void GUIDropDownBox::DropDownSubMenu::elementSelected(UINT32 idx)
  437. {
  438. closeSubMenu();
  439. }
  440. }