BsGUIDropDownBox.cpp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. #include "BsGUIDropDownBox.h"
  2. #include "BsGUIArea.h"
  3. #include "BsGUILayout.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 GUISkin& 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. GUIElement::destroy(mHitBox);
  108. GUIElement::destroy(mCaptureHitBox);
  109. bs_delete(mRootMenu);
  110. }
  111. void GUIDropDownBox::dropDownFocusLost()
  112. {
  113. mRootMenu->closeSubMenu();
  114. GUIDropDownBoxManager::instance().closeDropDownBox();
  115. }
  116. void GUIDropDownBox::notifySubMenuOpened(DropDownSubMenu* subMenu)
  117. {
  118. Vector<Rect2I> bounds;
  119. while(subMenu != nullptr)
  120. {
  121. bounds.push_back(subMenu->getVisibleBounds());
  122. subMenu = subMenu->mSubMenu;
  123. }
  124. mHitBox->setBounds(bounds);
  125. }
  126. void GUIDropDownBox::notifySubMenuClosed(DropDownSubMenu* subMenu)
  127. {
  128. Vector<Rect2I> bounds;
  129. while(subMenu != nullptr)
  130. {
  131. bounds.push_back(subMenu->getVisibleBounds());
  132. subMenu = subMenu->mSubMenu;
  133. }
  134. mHitBox->setBounds(bounds);
  135. }
  136. GUIDropDownBox::DropDownSubMenu::DropDownSubMenu(GUIDropDownBox* owner, DropDownSubMenu* parent, const GUIDropDownAreaPlacement& placement,
  137. const Rect2I& availableBounds, const GUIDropDownData& dropDownData, GUIDropDownType type, UINT32 depthOffset)
  138. :mOwner(owner), mParent(parent), mPage(0), mBackgroundFrame(nullptr), mBackgroundArea(nullptr), mContentArea(nullptr),
  139. mContentLayout(nullptr), mScrollUpBtn(nullptr), mScrollDownBtn(nullptr), x(0), y(0), width(0), height(0),
  140. mType(type), mSubMenu(nullptr), mData(dropDownData), mOpenedUpward(false), mDepthOffset(depthOffset), mContent(nullptr)
  141. {
  142. mAvailableBounds = availableBounds;
  143. const GUIElementStyle* scrollUpStyle = mOwner->getSkin().getStyle(mOwner->mScrollUpStyle);
  144. const GUIElementStyle* scrollDownStyle = mOwner->getSkin().getStyle(mOwner->mScrollDownStyle);
  145. const GUIElementStyle* backgroundStyle = mOwner->getSkin().getStyle(mOwner->mBackgroundStyle);
  146. Rect2I dropDownListBounds = placement.getBounds();
  147. int potentialLeftStart = 0;
  148. int potentialRightStart = 0;
  149. int potentialTopStart = 0;
  150. int potentialBottomStart = 0;
  151. switch(placement.getType())
  152. {
  153. case GUIDropDownAreaPlacement::Type::Position:
  154. potentialLeftStart = potentialRightStart = placement.getPosition().x;
  155. potentialTopStart = potentialBottomStart = placement.getPosition().y;
  156. break;
  157. case GUIDropDownAreaPlacement::Type::BoundsHorz:
  158. potentialRightStart = placement.getBounds().x;
  159. potentialLeftStart = placement.getBounds().x + placement.getBounds().width;
  160. potentialBottomStart = placement.getBounds().y + placement.getBounds().height;
  161. potentialTopStart = placement.getBounds().y;
  162. break;
  163. case GUIDropDownAreaPlacement::Type::BoundsVert:
  164. potentialRightStart = placement.getBounds().x + placement.getBounds().width;
  165. potentialLeftStart = placement.getBounds().x;
  166. potentialBottomStart = placement.getBounds().y;
  167. potentialTopStart = placement.getBounds().y + placement.getBounds().height;
  168. break;
  169. }
  170. // Create content GUI element
  171. mContent = GUIDropDownContent::create(this, dropDownData, mOwner->mContentStyle);
  172. mContent->setKeyboardFocus(true);
  173. // Determine x position and whether to align to left or right side of the drop down list
  174. UINT32 availableRightwardWidth = (UINT32)std::max(0, (availableBounds.x + availableBounds.width) - potentialRightStart);
  175. UINT32 availableLeftwardWidth = (UINT32)std::max(0, potentialLeftStart - availableBounds.x);
  176. //// Prefer right if possible
  177. if(DROP_DOWN_BOX_WIDTH <= availableRightwardWidth)
  178. {
  179. x = potentialRightStart;
  180. width = DROP_DOWN_BOX_WIDTH;
  181. }
  182. else
  183. {
  184. if(availableRightwardWidth >= availableLeftwardWidth)
  185. {
  186. x = potentialRightStart;
  187. width = std::min(DROP_DOWN_BOX_WIDTH, availableRightwardWidth);
  188. }
  189. else
  190. {
  191. x = potentialLeftStart - std::min(DROP_DOWN_BOX_WIDTH, availableLeftwardWidth);
  192. width = std::min(DROP_DOWN_BOX_WIDTH, availableLeftwardWidth);
  193. }
  194. }
  195. // Determine y position and whether to open upward or downward
  196. UINT32 availableDownwardHeight = (UINT32)std::max(0, (availableBounds.y + availableBounds.height) - potentialBottomStart);
  197. UINT32 availableUpwardHeight = (UINT32)std::max(0, potentialTopStart - availableBounds.y);
  198. //// Prefer down if possible
  199. UINT32 helperElementHeight = scrollUpStyle->height + scrollDownStyle->height +
  200. backgroundStyle->margins.top + backgroundStyle->margins.bottom;
  201. UINT32 maxNeededHeight = helperElementHeight;
  202. UINT32 numElements = (UINT32)dropDownData.entries.size();
  203. for(UINT32 i = 0; i < numElements; i++)
  204. maxNeededHeight += mContent->getElementHeight(i);
  205. height = 0;
  206. if(maxNeededHeight <= availableDownwardHeight)
  207. {
  208. y = potentialBottomStart;
  209. height = availableDownwardHeight;
  210. mOpenedUpward = false;
  211. }
  212. else
  213. {
  214. if(availableDownwardHeight >= availableUpwardHeight)
  215. {
  216. y = potentialBottomStart;
  217. height = availableDownwardHeight;
  218. mOpenedUpward = false;
  219. }
  220. else
  221. {
  222. y = potentialTopStart;
  223. height = availableUpwardHeight;
  224. mOpenedUpward = true;
  225. }
  226. }
  227. INT32 actualY = y;
  228. if(mOpenedUpward)
  229. actualY -= (INT32)std::min(maxNeededHeight, availableUpwardHeight);
  230. // Content area
  231. mContentArea = GUIArea::create(*mOwner, x, actualY, width, height);
  232. mContentArea->setDepth(100 - depthOffset * 2 - 1);
  233. mContentLayout = &mContentArea->getLayout().addLayoutY();
  234. // Background frame
  235. mBackgroundArea = GUIArea::create(*mOwner, x, actualY, width, height);
  236. mBackgroundArea->setDepth(100 - depthOffset * 2);
  237. mBackgroundFrame = GUITexture::create(GUIImageScaleMode::StretchToFit, mOwner->mBackgroundStyle);
  238. mBackgroundArea->getLayout().addElement(mBackgroundFrame);
  239. updateGUIElements();
  240. mOwner->notifySubMenuOpened(this);
  241. }
  242. GUIDropDownBox::DropDownSubMenu::~DropDownSubMenu()
  243. {
  244. closeSubMenu();
  245. mOwner->notifySubMenuClosed(this);
  246. GUIElement::destroy(mContent);
  247. if(mScrollUpBtn != nullptr)
  248. GUIElement::destroy(mScrollUpBtn);
  249. if(mScrollDownBtn != nullptr)
  250. GUIElement::destroy(mScrollDownBtn);
  251. GUIElement::destroy(mBackgroundFrame);
  252. GUIArea::destroy(mBackgroundArea);
  253. GUIArea::destroy(mContentArea);
  254. }
  255. void GUIDropDownBox::DropDownSubMenu::updateGUIElements()
  256. {
  257. // Remove all elements from content layout
  258. while(mContentLayout->getNumChildren() > 0)
  259. mContentLayout->removeChildAt(mContentLayout->getNumChildren() - 1);
  260. const GUIElementStyle* scrollUpStyle = mOwner->getSkin().getStyle(mOwner->mScrollUpStyle);
  261. const GUIElementStyle* scrollDownStyle = mOwner->getSkin().getStyle(mOwner->mScrollDownStyle);
  262. const GUIElementStyle* backgroundStyle = mOwner->getSkin().getStyle(mOwner->mBackgroundStyle);
  263. // Determine if we need scroll up and/or down buttons, number of visible elements and actual height
  264. bool needsScrollUp = mPage > 0;
  265. UINT32 numElements = (UINT32)mData.entries.size();
  266. UINT32 usedHeight = backgroundStyle->margins.top + backgroundStyle->margins.bottom;
  267. UINT32 pageStart = 0, pageEnd = 0;
  268. UINT32 curPage = 0;
  269. bool needsScrollDown = false;
  270. for(UINT32 i = 0; i < numElements; i++)
  271. {
  272. usedHeight += mContent->getElementHeight(i);
  273. pageEnd++;
  274. if(usedHeight > height)
  275. {
  276. usedHeight += scrollDownStyle->height;
  277. // Remove last few elements until we fit again
  278. while(usedHeight > height && i >= 0)
  279. {
  280. usedHeight -= mContent->getElementHeight(i);
  281. pageEnd--;
  282. i--;
  283. }
  284. // We found our page and are done
  285. if(curPage == mPage)
  286. {
  287. needsScrollDown = i != (numElements - 1);
  288. break;
  289. }
  290. // Nothing fits, break out of infinite loop
  291. if(pageStart == pageEnd)
  292. {
  293. needsScrollDown = i != (numElements - 1);
  294. break;
  295. }
  296. pageStart = pageEnd;
  297. usedHeight = backgroundStyle->margins.top + backgroundStyle->margins.bottom;
  298. usedHeight += scrollUpStyle->height;
  299. curPage++;
  300. }
  301. }
  302. // Add scroll up button
  303. if(needsScrollUp)
  304. {
  305. if(mScrollUpBtn == nullptr)
  306. {
  307. mScrollUpBtn = GUIButton::create(GUIContent(HString(L""), mOwner->mScrollUpBtnArrow), mOwner->mScrollUpStyle);
  308. mScrollUpBtn->onClick.connect(std::bind(&DropDownSubMenu::scrollUp, this));
  309. }
  310. mContentLayout->addElement(mScrollUpBtn);
  311. }
  312. else
  313. {
  314. if(mScrollUpBtn != nullptr)
  315. {
  316. GUIElement::destroy(mScrollUpBtn);
  317. mScrollUpBtn = nullptr;
  318. }
  319. }
  320. mContent->setRange(pageStart, pageEnd);
  321. mContentLayout->addElement(mContent);
  322. // Add scroll down button
  323. if(needsScrollDown)
  324. {
  325. if(mScrollDownBtn == nullptr)
  326. {
  327. mScrollDownBtn = GUIButton::create(GUIContent(HString(L""), mOwner->mScrollDownBtnArrow), mOwner->mScrollDownStyle);
  328. mScrollDownBtn->onClick.connect(std::bind(&DropDownSubMenu::scrollDown, this));
  329. }
  330. mContentLayout->addElement(mScrollDownBtn);
  331. }
  332. else
  333. {
  334. if(mScrollDownBtn != nullptr)
  335. {
  336. GUIElement::destroy(mScrollDownBtn);
  337. mScrollDownBtn = nullptr;
  338. }
  339. }
  340. // Resize and reposition areas
  341. INT32 actualY = y;
  342. if(mOpenedUpward)
  343. actualY -= (INT32)usedHeight;
  344. mBackgroundArea->setSize(width, usedHeight);
  345. mBackgroundArea->setPosition(x, actualY);
  346. mVisibleBounds = Rect2I(x, actualY, width, usedHeight);
  347. UINT32 contentWidth = (UINT32)std::max(0, (INT32)width - (INT32)backgroundStyle->margins.left - (INT32)backgroundStyle->margins.right);
  348. UINT32 contentHeight = (UINT32)std::max(0, (INT32)usedHeight - (INT32)backgroundStyle->margins.top - (INT32)backgroundStyle->margins.bottom);
  349. mContentArea->setSize(contentWidth, contentHeight);
  350. mContentArea->setPosition(x + backgroundStyle->margins.left, actualY + backgroundStyle->margins.top);
  351. }
  352. void GUIDropDownBox::DropDownSubMenu::scrollDown()
  353. {
  354. mPage++;
  355. updateGUIElements();
  356. closeSubMenu();
  357. }
  358. void GUIDropDownBox::DropDownSubMenu::scrollUp()
  359. {
  360. if(mPage > 0)
  361. {
  362. mPage--;
  363. updateGUIElements();
  364. }
  365. closeSubMenu();
  366. }
  367. void GUIDropDownBox::DropDownSubMenu::closeSubMenu()
  368. {
  369. if(mSubMenu != nullptr)
  370. {
  371. bs_delete(mSubMenu);
  372. mSubMenu = nullptr;
  373. mContent->setKeyboardFocus(true);
  374. }
  375. }
  376. void GUIDropDownBox::DropDownSubMenu::elementActivated(UINT32 idx)
  377. {
  378. closeSubMenu();
  379. auto callback = mData.entries[idx].getCallback();
  380. if(callback != nullptr)
  381. callback();
  382. GUIDropDownBoxManager::instance().closeDropDownBox();
  383. }
  384. void GUIDropDownBox::DropDownSubMenu::close()
  385. {
  386. if (mParent != nullptr)
  387. mParent->closeSubMenu();
  388. else // We're the last sub-menu, close the whole thing
  389. GUIDropDownBoxManager::instance().closeDropDownBox();
  390. }
  391. void GUIDropDownBox::DropDownSubMenu::elementSelected(UINT32 idx, const Rect2I& bounds)
  392. {
  393. closeSubMenu();
  394. if (mData.entries[idx].isSubMenu())
  395. {
  396. mContent->setKeyboardFocus(false);
  397. mSubMenu = bs_new<DropDownSubMenu>(mOwner, this, GUIDropDownAreaPlacement::aroundBoundsVert(bounds),
  398. mAvailableBounds, mData.entries[idx].getSubMenuData(), mType, mDepthOffset + 1);
  399. }
  400. }
  401. }