Forráskód Böngészése

Drop down & menu bar fixes:
- Make EditorWindowManager accessible from outside libraries
- Fixed drop down button styles so their "on" states have valid textures
- Properly set keyboard focus on most recently opened sub-menu
- Handle cleanup of DropDownBox in onDestroy instead of destructor to be consistant with GUIWidget behaviour
- Set up sub-menu content area before using it for calculating element size
- More reusable page calculation for sub-menu
- Made scroll up/down wrap around
- Select the first element if sub-menu receives input for the first time
- Don't expand a sub-menu if a sub-menu entry is selected via the keyboard
- Handle the case where GUI sub-menu contents don't have a widget assigned gracefully
- GUIManager properly removes destroyed element before trying to send it Redrawn event

Marko Pintera 10 éve
szülő
commit
b518d7811b

+ 1 - 1
BansheeEditor/Include/BsEditorWindowManager.h

@@ -6,7 +6,7 @@
 
 namespace BansheeEngine
 {
-	class EditorWindowManager : public Module<EditorWindowManager>
+	class BS_ED_EXPORT EditorWindowManager : public Module<EditorWindowManager>
 	{
 	public:
 		EditorWindowManager();

+ 6 - 0
BansheeEditor/Source/BsBuiltinEditorResources.cpp

@@ -650,6 +650,9 @@ namespace BansheeEngine
 		dropDownEntryBtnStyle.normal.texture = getGUITexture(DropDownBoxEntryNormalTex);
 		dropDownEntryBtnStyle.hover.texture = getGUITexture(DropDownBoxEntryHoverTex);
 		dropDownEntryBtnStyle.active.texture = dropDownEntryBtnStyle.hover.texture;
+		dropDownEntryBtnStyle.normalOn.texture = dropDownEntryBtnStyle.hover.texture;
+		dropDownEntryBtnStyle.hoverOn.texture = dropDownEntryBtnStyle.hover.texture;
+		dropDownEntryBtnStyle.activeOn.texture = dropDownEntryBtnStyle.hover.texture;
 		dropDownEntryBtnStyle.fixedHeight = true;
 		dropDownEntryBtnStyle.fixedWidth = false;
 		dropDownEntryBtnStyle.height = 14;
@@ -670,6 +673,9 @@ namespace BansheeEngine
 		dropDownEntryExpBtnStyle.normal.texture = getGUITexture(DropDownBoxEntryExpNormalTex);
 		dropDownEntryExpBtnStyle.hover.texture = getGUITexture(DropDownBoxEntryExpHoverTex);
 		dropDownEntryExpBtnStyle.active.texture = dropDownEntryExpBtnStyle.hover.texture;
+		dropDownEntryExpBtnStyle.normalOn.texture = dropDownEntryExpBtnStyle.hover.texture;
+		dropDownEntryExpBtnStyle.hoverOn.texture = dropDownEntryExpBtnStyle.hover.texture;
+		dropDownEntryExpBtnStyle.activeOn.texture = dropDownEntryExpBtnStyle.hover.texture;
 		dropDownEntryExpBtnStyle.fixedHeight = true;
 		dropDownEntryExpBtnStyle.fixedWidth = false;
 		dropDownEntryExpBtnStyle.height = 14;

+ 6 - 5
BansheeEditor/Source/BsMainEditorWindow.cpp

@@ -39,14 +39,15 @@ namespace BansheeEngine
 
 		updateAreas();
 
-		mMenuBar->addMenuItem(L"File/New project", nullptr);
-		mMenuBar->addMenuItem(L"File/Open project", nullptr);
-		mMenuBar->addSeparator(L"File");
+		mMenuBar->addMenuItem(L"File/New project", nullptr, 100);
+		mMenuBar->addMenuItem(L"File/Open project", nullptr, 100);
+		mMenuBar->addSeparator(L"File", 99);
+		mMenuBar->addMenuItem(L"File/Recent projects", nullptr, 98);
 		mMenuBar->addMenuItem(L"File/Recent projects/Project A", nullptr);
 		mMenuBar->addMenuItem(L"File/Recent projects/Project B", nullptr);
 		mMenuBar->addMenuItem(L"File/Recent projects/Project C", nullptr);
-		mMenuBar->addSeparator(L"File");
-		mMenuBar->addMenuItem(L"File/Exit", nullptr);
+		mMenuBar->addSeparator(L"File", 97);
+		mMenuBar->addMenuItem(L"File/Exit", nullptr, 96);
 		mMenuBar->addMenuItem(L"Window/Scene", nullptr);
 
 		//GameObjectHandle<TestTextSprite> textSprite = mSceneObject->addComponent<TestTextSprite>(mCamera->getViewport().get());

+ 41 - 4
BansheeEngine/Include/BsGUIDropDownBox.h

@@ -189,6 +189,17 @@ namespace BansheeEngine
 		 */
 		struct DropDownSubMenu
 		{
+			/**
+			 * @brief	Represents a single sub-menu page.
+			 */
+			struct PageInfo
+			{
+				UINT32 idx;
+				UINT32 start;
+				UINT32 end;
+				UINT32 height;
+			};
+
 		public:
 			/**
 			 * @brief	Creates a new drop down box sub-menu
@@ -222,19 +233,35 @@ namespace BansheeEngine
 			 */
 			void scrollUp();
 
+			/**
+			 * @brief	Moves the sub-menu to the first page and displays its elements.
+			 */
+			void scrollToTop();
+
+			/**
+			 * @brief	Moves the sub-menu to the last page and displays its elements.
+			 */
+			void scrollToBottom();
+
+			/**
+			 * @brief	Calculates ranges for all the pages of the sub-menu.
+			 */
+			Vector<PageInfo> getPageInfos() const;
+
 			/**
 			 * @brief	Called when the user activates an element with the specified index.
+			 *
+			 * @param	bounds	Bounds of the GUI element that is used as a visual representation
+			 *					of this drop down element.
 			 */
-			void elementActivated(UINT32 idx);
+			void elementActivated(UINT32 idx, const Rect2I& bounds);
 
 			/**
 			 * @brief	Called when the user selects an element with the specified index.
 			 * 
 			 * @param	idx		Index of the element that was selected.
-			 * @param	bounds	Bounds of the GUI element that is used as a visual representation 
-			 *					of this drop down element.
 			 */
-			void elementSelected(UINT32 idx, const Rect2I& bounds);
+			void elementSelected(UINT32 idx);
 
 			/**
 			 * @brief	Called when the user wants to close the currently open sub-menu.
@@ -251,6 +278,11 @@ namespace BansheeEngine
 			 */
 			Rect2I getVisibleBounds() const { return mVisibleBounds; }
 
+			/**
+			 * @brief	Returns the drop box object that owns this sub-menu.
+			 */
+			GUIDropDownBox* getOwner() const { return mOwner; }
+
 		public:
 			GUIDropDownBox* mOwner;
 
@@ -295,6 +327,11 @@ namespace BansheeEngine
 		 */
 		void dropDownFocusLost();
 
+		/**
+		 * @copydoc	GUIWidget::onDestroyed
+		 */
+		void onDestroyed() override;
+
 	private:
 		static const UINT32 DROP_DOWN_BOX_WIDTH;
 

+ 11 - 1
BansheeEngine/Include/BsGUIDropDownContent.h

@@ -70,7 +70,7 @@ namespace BansheeEngine
 		 * @brief	Enables or disables keyboard focus. When keyboard focus is enabled the contents
 		 *			will respond to keyboard events.
 		 */
-		void setKeyboardFocus(bool focus) { mKeyboardFocus = focus; }
+		void setKeyboardFocus(bool focus);
 
 		static const String ENTRY_STYLE_TYPE;
 		static const String ENTRY_EXP_STYLE_TYPE;
@@ -116,6 +116,16 @@ namespace BansheeEngine
 		 */
 		void setSelected(UINT32 idx);
 
+		/**
+		 * @brief	Selects the next available non-separator entry.
+		 */
+		void selectNext(UINT32 startIdx);
+
+		/**
+		 * @brief	Selects the previous available non-separator entry.
+		 */
+		void selectPrevious(UINT32 startIdx);
+
 		GUIDropDownData mDropDownData;
 		Vector<VisibleElement> mVisibleElements;
 		UINT32 mSelectedIdx;

+ 129 - 73
BansheeEngine/Source/BsGUIDropDownBox.cpp

@@ -125,10 +125,17 @@ namespace BansheeEngine
 	}
 
 	GUIDropDownBox::~GUIDropDownBox()
+	{
+
+	}
+
+	void GUIDropDownBox::onDestroyed()
 	{
 		GUIElement::destroy(mHitBox);
 		GUIElement::destroy(mCaptureHitBox);
 		bs_delete(mRootMenu);
+
+		GUIWidget::onDestroyed();
 	}
 
 	void GUIDropDownBox::dropDownFocusLost()
@@ -207,6 +214,21 @@ namespace BansheeEngine
 		mContent = GUIDropDownContent::create(this, dropDownData, mOwner->mContentStyle);
 		mContent->setKeyboardFocus(true);
 
+		// Content area
+		mContentArea = GUIArea::create(*mOwner, 0, 0, width, height);
+		mContentArea->setDepth(100 - depthOffset * 2 - 1);
+		mContentLayout = &mContentArea->getLayout().addLayoutY();
+
+		// Background frame
+		mBackgroundArea = GUIArea::create(*mOwner, 0, 0, width, height);
+		mBackgroundArea->setDepth(100 - depthOffset * 2);
+
+		mBackgroundFrame = GUITexture::create(GUIImageScaleMode::StretchToFit, mOwner->mBackgroundStyle);
+		mBackgroundArea->getLayout().addElement(mBackgroundFrame);
+
+		mContentLayout->addElement(mContent); // Note: It's important this is added to the layout before we 
+											  // use it for size calculations, in order for its skin to be assigned
+
 		// Determine x position and whether to align to left or right side of the drop down list
 		UINT32 availableRightwardWidth = (UINT32)std::max(0, (availableBounds.x + availableBounds.width) - potentialRightStart);
 		UINT32 availableLeftwardWidth = (UINT32)std::max(0, potentialLeftStart - availableBounds.x);
@@ -272,17 +294,8 @@ namespace BansheeEngine
 		if(mOpenedUpward)	
 			actualY -= (INT32)std::min(maxNeededHeight, availableUpwardHeight);
 
-		// Content area
-		mContentArea = GUIArea::create(*mOwner, x, actualY, width, height);
-		mContentArea->setDepth(100 - depthOffset * 2 - 1);
-		mContentLayout = &mContentArea->getLayout().addLayoutY();
-
-		// Background frame
-		mBackgroundArea = GUIArea::create(*mOwner, x, actualY, width, height);
-		mBackgroundArea->setDepth(100 - depthOffset * 2);
-
-		mBackgroundFrame = GUITexture::create(GUIImageScaleMode::StretchToFit, mOwner->mBackgroundStyle);
-		mBackgroundArea->getLayout().addElement(mBackgroundFrame);
+		mContentArea->setPosition(x, actualY);
+		mBackgroundArea->setPosition(x, actualY);
 
 		updateGUIElements();
 
@@ -297,10 +310,10 @@ namespace BansheeEngine
 
 		GUIElement::destroy(mContent);
 
-		if(mScrollUpBtn != nullptr)
+		if (mScrollUpBtn != nullptr)
 			GUIElement::destroy(mScrollUpBtn);
 
-		if(mScrollDownBtn != nullptr)
+		if (mScrollDownBtn != nullptr)
 			GUIElement::destroy(mScrollDownBtn);
 
 		GUIElement::destroy(mBackgroundFrame);
@@ -309,65 +322,88 @@ namespace BansheeEngine
 		GUIArea::destroy(mContentArea);
 	}
 
-	void GUIDropDownBox::DropDownSubMenu::updateGUIElements()
+	Vector<GUIDropDownBox::DropDownSubMenu::PageInfo> GUIDropDownBox::DropDownSubMenu::getPageInfos() const
 	{
-		// Remove all elements from content layout
-		while(mContentLayout->getNumChildren() > 0)
-			mContentLayout->removeChildAt(mContentLayout->getNumChildren() - 1);
-
 		const GUIElementStyle* scrollUpStyle = mOwner->getSkin().getStyle(mOwner->mScrollUpStyle);
 		const GUIElementStyle* scrollDownStyle = mOwner->getSkin().getStyle(mOwner->mScrollDownStyle);
 		const GUIElementStyle* backgroundStyle = mOwner->getSkin().getStyle(mOwner->mBackgroundStyle);
 
-		// Determine if we need scroll up and/or down buttons, number of visible elements and actual height
-		bool needsScrollUp = mPage > 0;
-		UINT32 numElements = (UINT32)mData.entries.size();
-
-		UINT32 usedHeight = backgroundStyle->margins.top + backgroundStyle->margins.bottom;
+		INT32 numElements = (INT32)mData.entries.size();
 
-		UINT32 pageStart = 0, pageEnd = 0;
-		UINT32 curPage = 0;
-		bool needsScrollDown = false;
-		for(UINT32 i = 0; i < numElements; i++)
+		PageInfo curPageInfo;
+		curPageInfo.start = 0;
+		curPageInfo.end = 0;
+		curPageInfo.idx = 0;
+		curPageInfo.height = backgroundStyle->margins.top + backgroundStyle->margins.bottom;
+		
+		Vector<PageInfo> pageInfos;
+		for (INT32 i = 0; i < numElements; i++)
 		{
-			usedHeight += mContent->getElementHeight(i);
-			pageEnd++;
+			curPageInfo.height += mContent->getElementHeight((UINT32)i);
+			curPageInfo.end++;
 
-			if(usedHeight > height)
+			if (curPageInfo.height > height)
 			{
-				usedHeight += scrollDownStyle->height;
+				curPageInfo.height += scrollDownStyle->height;
 
 				// Remove last few elements until we fit again
-				while(usedHeight > height && i >= 0)
+				while (curPageInfo.height > height && i >= 0)
 				{
-					usedHeight -= mContent->getElementHeight(i);
-					pageEnd--;
+					curPageInfo.height -= mContent->getElementHeight((UINT32)i);
+					curPageInfo.end--;
 
 					i--;
 				}
 
-				// We found our page and are done
-				if(curPage == mPage)
-				{
-					needsScrollDown = i != (numElements - 1);
-					break;
-				}
-
 				// Nothing fits, break out of infinite loop
-				if(pageStart == pageEnd)
-				{
-					needsScrollDown = i != (numElements - 1);
+				if (curPageInfo.start >= curPageInfo.end)
 					break;
-				}
 
-				pageStart = pageEnd;
-				usedHeight = backgroundStyle->margins.top + backgroundStyle->margins.bottom;
-				usedHeight += scrollUpStyle->height;
+				pageInfos.push_back(curPageInfo);
+
+				curPageInfo.start = curPageInfo.end;
+				curPageInfo.height = backgroundStyle->margins.top + backgroundStyle->margins.bottom;
+				curPageInfo.height += scrollUpStyle->height;
 
-				curPage++;
+				curPageInfo.idx++;
 			}
 		}
 
+		if (curPageInfo.start < curPageInfo.end)
+			pageInfos.push_back(curPageInfo);
+
+		return pageInfos;
+	}
+
+	void GUIDropDownBox::DropDownSubMenu::updateGUIElements()
+	{
+		// Remove all elements from content layout
+		while(mContentLayout->getNumChildren() > 0)
+			mContentLayout->removeChildAt(mContentLayout->getNumChildren() - 1);
+
+		mContentLayout->addElement(mContent); // Note: Needs to be added first so that size calculations have proper skin to work with
+
+		const GUIElementStyle* scrollUpStyle = mOwner->getSkin().getStyle(mOwner->mScrollUpStyle);
+		const GUIElementStyle* scrollDownStyle = mOwner->getSkin().getStyle(mOwner->mScrollDownStyle);
+		const GUIElementStyle* backgroundStyle = mOwner->getSkin().getStyle(mOwner->mBackgroundStyle);
+
+		// Determine if we need scroll up and/or down buttons, number of visible elements and actual height
+		bool needsScrollUp = mPage > 0;
+
+		UINT32 pageStart = 0, pageEnd = 0;
+		UINT32 pageHeight = 0;
+		Vector<PageInfo> pageInfos = getPageInfos();
+
+		if (pageInfos.size() > mPage)
+		{
+			pageStart = pageInfos[mPage].start;
+			pageEnd = pageInfos[mPage].end;
+			pageHeight = pageInfos[mPage].height;
+		}
+
+		UINT32 numElements = (UINT32)mData.entries.size();
+		bool needsScrollDown = pageEnd != numElements;
+
 		// Add scroll up button
 		if(needsScrollUp)
 		{
@@ -377,7 +413,7 @@ namespace BansheeEngine
 				mScrollUpBtn->onClick.connect(std::bind(&DropDownSubMenu::scrollUp, this));
 			}
 
-			mContentLayout->addElement(mScrollUpBtn);			
+			mContentLayout->insertElement(0, mScrollUpBtn);			
 		}
 		else
 		{
@@ -389,7 +425,6 @@ namespace BansheeEngine
 		}
 
 		mContent->setRange(pageStart, pageEnd);
-		mContentLayout->addElement(mContent);
 
 		// Add scroll down button
 		if(needsScrollDown)
@@ -415,15 +450,15 @@ namespace BansheeEngine
 		INT32 actualY = y;
 
 		if(mOpenedUpward)	
-			actualY -= (INT32)usedHeight;
+			actualY -= (INT32)pageHeight;
 
-		mBackgroundArea->setSize(width, usedHeight);
+		mBackgroundArea->setSize(width, pageHeight);
 		mBackgroundArea->setPosition(x, actualY);
 
-		mVisibleBounds = Rect2I(x, actualY, width, usedHeight);
+		mVisibleBounds = Rect2I(x, actualY, width, pageHeight);
 
 		UINT32 contentWidth = (UINT32)std::max(0, (INT32)width - (INT32)backgroundStyle->margins.left - (INT32)backgroundStyle->margins.right);
-		UINT32 contentHeight = (UINT32)std::max(0, (INT32)usedHeight - (INT32)backgroundStyle->margins.top - (INT32)backgroundStyle->margins.bottom);
+		UINT32 contentHeight = (UINT32)std::max(0, (INT32)pageHeight - (INT32)backgroundStyle->margins.top - (INT32)backgroundStyle->margins.bottom);
 		mContentArea->setSize(contentWidth, contentHeight);
 		mContentArea->setPosition(x + backgroundStyle->margins.left, actualY + backgroundStyle->margins.top);
 	}
@@ -431,6 +466,9 @@ namespace BansheeEngine
 	void GUIDropDownBox::DropDownSubMenu::scrollDown()
 	{
 		mPage++;
+		if (mPage == (UINT32)getPageInfos().size())
+			mPage = 0;
+
 		updateGUIElements();
 
 		closeSubMenu();
@@ -438,11 +476,27 @@ namespace BansheeEngine
 
 	void GUIDropDownBox::DropDownSubMenu::scrollUp()
 	{
-		if(mPage > 0)
-		{
+		if (mPage > 0)
 			mPage--;
-			updateGUIElements();
-		}
+		else
+			mPage = (UINT32)getPageInfos().size() - 1;
+
+		updateGUIElements();
+		closeSubMenu();
+	}
+
+	void GUIDropDownBox::DropDownSubMenu::scrollToTop()
+	{
+		mPage = 0;
+		updateGUIElements();
+
+		closeSubMenu();
+	}
+
+	void GUIDropDownBox::DropDownSubMenu::scrollToBottom()
+	{
+		mPage = (UINT32)(getPageInfos().size() - 1);
+		updateGUIElements();
 
 		closeSubMenu();
 	}
@@ -458,15 +512,25 @@ namespace BansheeEngine
 		}
 	}
 
-	void GUIDropDownBox::DropDownSubMenu::elementActivated(UINT32 idx)
+	void GUIDropDownBox::DropDownSubMenu::elementActivated(UINT32 idx, const Rect2I& bounds)
 	{
 		closeSubMenu();
 
-		auto callback = mData.entries[idx].getCallback();
-		if(callback != nullptr)
-			callback();
+		if (!mData.entries[idx].isSubMenu())
+		{
+			auto callback = mData.entries[idx].getCallback();
+			if (callback != nullptr)
+				callback();
 
-		GUIDropDownBoxManager::instance().closeDropDownBox();
+			GUIDropDownBoxManager::instance().closeDropDownBox();
+		}
+		else
+		{
+			mContent->setKeyboardFocus(false);
+
+			mSubMenu = bs_new<DropDownSubMenu>(mOwner, this, GUIDropDownAreaPlacement::aroundBoundsVert(bounds),
+				mAvailableBounds, mData.entries[idx].getSubMenuData(), mType, mDepthOffset + 1);
+		}
 	}
 
 	void GUIDropDownBox::DropDownSubMenu::close()
@@ -477,16 +541,8 @@ namespace BansheeEngine
 			GUIDropDownBoxManager::instance().closeDropDownBox();
 	}
 
-	void GUIDropDownBox::DropDownSubMenu::elementSelected(UINT32 idx, const Rect2I& bounds)
+	void GUIDropDownBox::DropDownSubMenu::elementSelected(UINT32 idx)
 	{
 		closeSubMenu();
-
-		if (mData.entries[idx].isSubMenu())
-		{
-			mContent->setKeyboardFocus(false);
-
-			mSubMenu = bs_new<DropDownSubMenu>(mOwner, this, GUIDropDownAreaPlacement::aroundBoundsVert(bounds),
-				mAvailableBounds, mData.entries[idx].getSubMenuData(), mType, mDepthOffset + 1);
-		}
 	}
 }

+ 126 - 105
BansheeEngine/Source/BsGUIDropDownContent.cpp

@@ -62,9 +62,9 @@ namespace BansheeEngine
 			if (element.isSeparator())
 				visElem.separator->setStyle(getSubStyleName(SEPARATOR_STYLE_TYPE));
 			else if (element.isSubMenu())
-				visElem.separator->setStyle(getSubStyleName(ENTRY_EXP_STYLE_TYPE));
+				visElem.button->setStyle(getSubStyleName(ENTRY_EXP_STYLE_TYPE));
 			else
-				visElem.separator->setStyle(getSubStyleName(ENTRY_STYLE_TYPE));
+				visElem.button->setStyle(getSubStyleName(ENTRY_STYLE_TYPE));
 		}
 	}
 
@@ -73,11 +73,16 @@ namespace BansheeEngine
 		std::function<void(UINT32, UINT32)> onHover = 
 			[&](UINT32 idx, UINT32 visIdx) 
 		{ 
-			mSelectedIdx = visIdx; 
-			mParent->elementSelected(idx, mVisibleElements[visIdx].button->_getCachedBounds()); 
+			setSelected(visIdx);
+			mParent->elementSelected(idx); 
 		};
 
-		std::function<void(UINT32)> onClick = [&](UINT32 idx) { mParent->elementActivated(idx); };
+		std::function<void(UINT32, UINT32)> onClick =
+			[&](UINT32 idx, UINT32 visIdx)
+		{ 
+			setSelected(visIdx);
+			mParent->elementActivated(idx, mVisibleElements[visIdx].button->_getCachedBounds());
+		};
 
 		// Remove all elements
 		while (_getNumChildren() > 0)
@@ -108,14 +113,14 @@ namespace BansheeEngine
 			else if (element.isSubMenu())
 			{
 				visElem.button = GUIButton::create(getElementLocalizedName(i), getSubStyleName(ENTRY_EXP_STYLE_TYPE));
-				visElem.button->onHover.connect(std::bind(onHover, i, curVisIdx));
+				visElem.button->onHover.connect(std::bind(onClick, i, curVisIdx));
 				_registerChildElement(visElem.button);
 			}
 			else
 			{
 				visElem.button = GUIButton::create(getElementLocalizedName(i), getSubStyleName(ENTRY_STYLE_TYPE));
 				visElem.button->onHover.connect(std::bind(onHover, i, curVisIdx));
-				visElem.button->onClick.connect(std::bind(onClick, i));
+				visElem.button->onClick.connect(std::bind(onClick, i, curVisIdx));
 				_registerChildElement(visElem.button);
 
 				const WString& shortcutTag = element.getShortcutTag();
@@ -129,12 +134,14 @@ namespace BansheeEngine
 			curVisIdx++;
 		}
 
-		if (mSelectedIdx == UINT_MAX)
-			setSelected(0);
+		markContentAsDirty();
 	}
 
 	UINT32 GUIDropDownContent::getElementHeight(UINT32 idx) const
 	{
+		if (_getParentWidget() == nullptr)
+			return 14; // Arbitrary
+
 		if (mDropDownData.entries[idx].isSeparator())
 			return _getParentWidget()->getSkin().getStyle(getSubStyleName(SEPARATOR_STYLE_TYPE))->height;
 		else if (mDropDownData.entries[idx].isSubMenu())
@@ -154,104 +161,32 @@ namespace BansheeEngine
 			return HString(label);
 	}
 
+	void GUIDropDownContent::setKeyboardFocus(bool focus) 
+	{ 
+		mKeyboardFocus = focus; 
+		setFocus(focus);
+	}
+
 	bool GUIDropDownContent::_commandEvent(const GUICommandEvent& ev)
 	{
 		if (!mKeyboardFocus)
 			return false;
 
-		UINT32 maxElemIdx = (UINT32)mDropDownData.entries.size() - 1;
+		UINT32 maxElemIdx = (UINT32)mDropDownData.entries.size();
 
 		switch (ev.getType())
 		{
 		case GUICommandEventType::MoveDown:
-		{
-			{
-				UINT32 curIdx = mVisibleElements[mSelectedIdx].idx;
-				UINT32 nextIdx = curIdx;
-				bool gotNextIndex = false;
-
-				for (nextIdx = curIdx + 1; nextIdx <= maxElemIdx; nextIdx++)
-				{
-					GUIDropDownDataEntry& entry = mDropDownData.entries[nextIdx];
-					if (!entry.isSeparator())
-					{
-						gotNextIndex = true;
-						break;
-					}
-				}
-
-				if (gotNextIndex)
-				{
-					while (nextIdx > mRangeEnd)
-					{
-						if (maxElemIdx == mRangeEnd)
-							break;
-
-						mParent->scrollDown();
-					}
-
-					if (nextIdx <= mRangeEnd)
-					{
-						UINT32 visIdx = 0;
-						for (auto& visElem : mVisibleElements)
-						{
-							if (visElem.idx == nextIdx)
-							{
-								setSelected(visIdx);
-								break;
-							}
-
-							visIdx++;
-						}
-					}
-				}
-			}
-		}
+			if (mSelectedIdx == UINT_MAX)
+				selectNext(0);
+			else
+				selectNext(mSelectedIdx + 1);
 			return true;
 		case GUICommandEventType::MoveUp:
-		{
-			{
-				UINT32 curIdx = mVisibleElements[mSelectedIdx].idx;
-				INT32 prevIdx = curIdx;
-				bool gotNextIndex = false;
-
-				for (prevIdx = (INT32)curIdx - 1; prevIdx >= 0; prevIdx--)
-				{
-					GUIDropDownDataEntry& entry = mDropDownData.entries[prevIdx];
-					if (!entry.isSeparator())
-					{
-						gotNextIndex = true;
-						break;
-					}
-				}
-
-				if (gotNextIndex)
-				{
-					while (prevIdx < (INT32)mRangeStart)
-					{
-						if (mRangeStart == 0)
-							break;
-
-						mParent->scrollUp();
-					}
-
-					if (prevIdx >= (INT32)mRangeStart)
-					{
-						UINT32 visIdx = 0;
-						for (auto& visElem : mVisibleElements)
-						{
-							if (visElem.idx == prevIdx)
-							{
-								setSelected(visIdx);
-								break;
-							}
-
-							visIdx++;
-						}
-					}
-				}
-			}
-		}
+			if (mSelectedIdx == UINT_MAX)
+				selectNext(0);
+			else
+				selectPrevious(mSelectedIdx - 1);
 			return true;
 		case GUICommandEventType::Escape:
 		case GUICommandEventType::MoveLeft:
@@ -259,19 +194,24 @@ namespace BansheeEngine
 			return true;
 		case GUICommandEventType::MoveRight:
 		{
-			GUIDropDownDataEntry& entry = mDropDownData.entries[mVisibleElements[mSelectedIdx].idx];
-			if (entry.isSubMenu())
-				mParent->elementActivated(mVisibleElements[mSelectedIdx].idx);
+			if (mSelectedIdx == UINT_MAX)
+				selectNext(0);
+			else
+			{
+				GUIDropDownDataEntry& entry = mDropDownData.entries[mVisibleElements[mSelectedIdx].idx];
+				if (entry.isSubMenu())
+					mParent->elementActivated(mVisibleElements[mSelectedIdx].idx, mVisibleElements[mSelectedIdx].button->_getCachedBounds());
+			}
 		}
 			return true;
 		case GUICommandEventType::Return:
-			mParent->elementActivated(mVisibleElements[mSelectedIdx].idx);
+			if (mSelectedIdx == UINT_MAX)
+				selectNext(0);
+			else
+				mParent->elementActivated(mVisibleElements[mSelectedIdx].idx, mVisibleElements[mSelectedIdx].button->_getCachedBounds());
 			return true;
 		}
 
-		// TODO - When closing a sub-menu I might need to also call GUIDropDownBoxManager::instance().closeDropDownBox();
-		// TODO - I should be able to set focus to this element, so only the most recent element receives keyboard input
-
 		return false;
 	}
 
@@ -283,7 +223,88 @@ namespace BansheeEngine
 		mSelectedIdx = idx;
 		mVisibleElements[mSelectedIdx].button->_setOn(true);
 
-		mParent->elementSelected(mVisibleElements[mSelectedIdx].idx, mVisibleElements[mSelectedIdx].button->_getCachedBounds());
+		mParent->elementSelected(mVisibleElements[mSelectedIdx].idx);
+	}
+
+	void GUIDropDownContent::selectNext(UINT32 startIdx)
+	{
+		UINT32 numElements = (UINT32)mDropDownData.entries.size();
+
+		bool gotNextIndex = false;
+		UINT32 nextIdx = startIdx;
+		for (UINT32 i = 0; i < numElements; i++)
+		{
+			if (nextIdx >= numElements)
+				nextIdx = 0; // Wrap around
+
+			GUIDropDownDataEntry& entry = mDropDownData.entries[nextIdx];
+			if (!entry.isSeparator())
+			{
+				gotNextIndex = true;
+				break;
+			}
+
+			nextIdx++;
+		}
+
+		if (gotNextIndex)
+		{
+			while (nextIdx < mRangeStart || nextIdx >= mRangeEnd)
+				mParent->scrollDown();
+
+			UINT32 visIdx = 0;
+			for (auto& visElem : mVisibleElements)
+			{
+				if (visElem.idx == nextIdx)
+				{
+					setSelected(visIdx);
+					break;
+				}
+
+				visIdx++;
+			}
+		}
+	}
+
+	void GUIDropDownContent::selectPrevious(UINT32 startIdx)
+	{
+		UINT32 numElements = (UINT32)mDropDownData.entries.size();
+
+		bool gotNextIndex = false;
+		INT32 prevIdx = (INT32)startIdx;
+
+		for (UINT32 i = 0; i < numElements; i++)
+		{
+			if (prevIdx < 0)
+				prevIdx = numElements - 1; // Wrap around
+
+			GUIDropDownDataEntry& entry = mDropDownData.entries[prevIdx];
+			if (!entry.isSeparator())
+			{
+				gotNextIndex = true;
+				break;
+			}
+
+			prevIdx--;
+		}
+
+		if (gotNextIndex)
+		{
+			while (prevIdx < (INT32)mRangeStart || prevIdx >= (INT32)mRangeEnd)
+				mParent->scrollUp();
+
+			UINT32 visIdx = 0;
+			for (auto& visElem : mVisibleElements)
+			{
+				if (visElem.idx == prevIdx)
+				{
+					setSelected(visIdx);
+					break;
+				}
+
+				visIdx++;
+			}
+		}
 	}
 
 	Vector2I GUIDropDownContent::_getOptimalSize() const
@@ -316,7 +337,7 @@ namespace BansheeEngine
 	void GUIDropDownContent::_updateLayoutInternal(INT32 x, INT32 y, UINT32 width, UINT32 height,
 		Rect2I clipRect, UINT8 widgetDepth, UINT16 areaDepth)
 	{
-		INT32 yOffset = 0;
+		INT32 yOffset = y;
 		for (auto& visElem : mVisibleElements)
 		{
 			const GUIDropDownDataEntry& element = mDropDownData.entries[visElem.idx];

+ 19 - 19
BansheeEngine/Source/BsGUIManager.cpp

@@ -198,25 +198,6 @@ namespace BansheeEngine
 		}
 		gProfilerCPU().endSample("UpdateLayout");
 
-		// Blink caret
-		float curTime = gTime().getTime();
-
-		if((curTime - mCaretLastBlinkTime) >= mCaretBlinkInterval)
-		{
-			mCaretLastBlinkTime = curTime;
-			mIsCaretOn = !mIsCaretOn;
-
-			mCommandEvent = GUICommandEvent();
-			mCommandEvent.setType(GUICommandEventType::Redraw);
-
-			for(auto& elementInfo : mElementsInFocus)
-			{
-				sendCommandEvent(elementInfo.widget, elementInfo.element, mCommandEvent);
-			}
-		}
-
-		PROFILE_CALL(updateMeshes(), "UpdateMeshes");
-
 		// Destroy all queued elements (and loop in case any new ones get queued during destruction)
 		do
 		{
@@ -290,6 +271,25 @@ namespace BansheeEngine
 			mForcedFocusElements.clear();
 
 		} while (processDestroyQueue());
+
+		// Blink caret
+		float curTime = gTime().getTime();
+
+		if ((curTime - mCaretLastBlinkTime) >= mCaretBlinkInterval)
+		{
+			mCaretLastBlinkTime = curTime;
+			mIsCaretOn = !mIsCaretOn;
+
+			mCommandEvent = GUICommandEvent();
+			mCommandEvent.setType(GUICommandEventType::Redraw);
+
+			for (auto& elementInfo : mElementsInFocus)
+			{
+				sendCommandEvent(elementInfo.widget, elementInfo.element, mCommandEvent);
+			}
+		}
+
+		PROFILE_CALL(updateMeshes(), "UpdateMeshes");
 	}
 
 	void GUIManager::render(ViewportPtr& target, DrawList& drawList) const

+ 1 - 1
BansheeEngine/Source/BsGUIMenu.cpp

@@ -6,7 +6,7 @@ namespace BansheeEngine
 {
 	bool GUIMenuItemComparer::operator() (const GUIMenuItem* const& a, const GUIMenuItem* const& b)
 	{
-		return a->mPriority > b->mPriority;
+		return a->mPriority > b->mPriority || (a->mPriority == b->mPriority && a->mName.compare(b->mName) < 0);
 	}
 
 	GUIMenuItem::GUIMenuItem(GUIMenuItem* parent, const WString& name, std::function<void()> callback, INT32 priority, const ShortcutKey& key)

+ 1 - 1
MBansheeEditor/EditorApplication.cs

@@ -118,7 +118,7 @@ namespace BansheeEditor
             gizmoDbgObject.AddComponent<DbgGizmoComponent>();
 
             //ProgressBar.Show("Test", 0.5f);
-            ColorPicker.Show();
+            //ColorPicker.Show();
 
             // DEBUG ONLY END
         }

+ 4 - 0
TODO.txt

@@ -27,6 +27,10 @@ When closing a sub-menu I think I need to call "GUIDropDownBoxManager::instance(
  - Also when a submenu is closed or opened I should set some kind of focus on it, so it knows to accept/reject keyboard input
  - "Esc" should move back one sub-menu instead of closing all of it
 
+LATER: Add keyboard controls to GUIMenuBar (left/right arrows should move between entries if user is not browsing a sub-menu)
+ - esc should cancel out of the menu bar
+ - alt should focus the menu bar
+
 ----------------------------------------------------------------------
 VisualStudio integration