Explorar el Código

Started work on TreeView

Marko Pintera hace 12 años
padre
commit
05736aad70

+ 1 - 0
BansheeEngine.sln

@@ -51,6 +51,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
 		TODO.txt = TODO.txt
 		TODODoc.txt = TODODoc.txt
 		TODOEditor.txt = TODOEditor.txt
+		TreeView.txt = TreeView.txt
 	EndProjectSection
 EndProject
 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CamelotFreeImgImporter", "CamelotFreeImgImporter\CamelotFreeImgImporter.vcxproj", "{122B7A22-0C62-4B35-B661-EBF3F394EA79}"

+ 2 - 0
CamelotClient/CamelotClient.vcxproj

@@ -266,6 +266,7 @@
     <ClInclude Include="Include\BsEditorWindowManager.h" />
     <ClInclude Include="Include\BsGUIDockSlider.h" />
     <ClInclude Include="Include\BsGUIMenuBar.h" />
+    <ClInclude Include="Include\BsGUISceneTreeView.h" />
     <ClInclude Include="Include\BsGUITabbedTitleBar.h" />
     <ClInclude Include="Include\BsGUITabButton.h" />
     <ClInclude Include="Include\BsGUIWindowFrame.h" />
@@ -290,6 +291,7 @@
     <ClCompile Include="Source\BsEditorWindowManager.cpp" />
     <ClCompile Include="Source\BsGUIDockSlider.cpp" />
     <ClCompile Include="Source\BsGUIMenuBar.cpp" />
+    <ClCompile Include="Source\BsGUISceneTreeView.cpp" />
     <ClCompile Include="Source\BsGUITabbedTitleBar.cpp" />
     <ClCompile Include="Source\BsGUITabButton.cpp" />
     <ClCompile Include="Source\BsGUIWindowFrame.cpp" />

+ 6 - 0
CamelotClient/CamelotClient.vcxproj.filters

@@ -93,6 +93,9 @@
     <ClInclude Include="Include\BsEditorGUI.h">
       <Filter>Header Files\Editor</Filter>
     </ClInclude>
+    <ClInclude Include="Include\BsGUISceneTreeView.h">
+      <Filter>Header Files\Editor</Filter>
+    </ClInclude>
   </ItemGroup>
   <ItemGroup>
     <ClCompile Include="stdafx.cpp">
@@ -161,5 +164,8 @@
     <ClCompile Include="Source\BsEditorGUI.cpp">
       <Filter>Source Files\Editor</Filter>
     </ClCompile>
+    <ClCompile Include="Source\BsGUISceneTreeView.cpp">
+      <Filter>Source Files\Editor</Filter>
+    </ClCompile>
   </ItemGroup>
 </Project>

+ 71 - 0
CamelotClient/Include/BsGUISceneTreeView.h

@@ -0,0 +1,71 @@
+#pragma once
+
+#include "BsEditorPrerequisites.h"
+#include "BsGUIElementContainer.h"
+#include <boost/signal.hpp>
+
+namespace BansheeEditor
+{
+	class GUISceneTreeView : public BS::GUIElementContainer
+	{
+		struct TreeElement
+		{
+			TreeElement();
+			~TreeElement();
+
+			TreeElement* mParent;
+			CM::Vector<TreeElement*>::type mChildren;
+
+			BS::GUIButton* mFoldoutBtn;
+			BS::GUILabel* mElement;
+
+			CM::HSceneObject mSceneObject;
+			CM::String mName;
+			CM::UINT32 mId;
+
+			CM::UINT32 mSortedIdx;
+			bool mIsExpanded;
+			bool mIsDirty;
+			bool mIsVisible;
+		};
+
+	public:
+		static const CM::String& getGUITypeName();
+
+		static GUISceneTreeView* create(BS::GUIWidget& parent,
+			BS::GUIElementStyle* backgroundStyle = nullptr, BS::GUIElementStyle* elementBtnStyle = nullptr, 
+			BS::GUIElementStyle* foldoutBtnStyle = nullptr);
+
+		static GUISceneTreeView* create(BS::GUIWidget& parent, const BS::GUILayoutOptions& layoutOptions);
+		static GUISceneTreeView* create(BS::GUIWidget& parent, const BS::GUILayoutOptions& layoutOptions, 
+			BS::GUIElementStyle* backgroundStyle = nullptr, BS::GUIElementStyle* elementBtnStyle = nullptr, 
+			BS::GUIElementStyle* foldoutBtnStyle = nullptr);
+
+		void update();
+
+	protected:
+		virtual ~GUISceneTreeView();
+
+		void updateClippedBounds();
+
+		void _updateLayoutInternal(CM::INT32 x, CM::INT32 y, CM::UINT32 width, CM::UINT32 height,
+			CM::RectI clipRect, CM::UINT8 widgetDepth, CM::UINT16 areaDepth);
+	protected:
+		static const CM::UINT32 ELEMENT_EXTRA_SPACING;
+		static const CM::UINT32 INDENT_SIZE;
+
+		const BS::GUIElementStyle* mBackgroundStyle;
+		const BS::GUIElementStyle* mElementBtnStyle;
+		const BS::GUIElementStyle* mFoldoutBtnStyle;
+
+		BS::GUITexture* mBackgroundImage;
+		TreeElement mRootElement;
+
+		CM::Vector<bool>::type mTempToDelete;
+
+		GUISceneTreeView(BS::GUIWidget& parent, BS::GUIElementStyle* backgroundStyle, BS::GUIElementStyle* elementBtnStyle, 
+			BS::GUIElementStyle* foldoutBtnStyle, const BS::GUILayoutOptions& layoutOptions);
+
+		virtual bool mouseEvent(const BS::GUIMouseEvent& ev);
+	};
+}

+ 324 - 0
CamelotClient/Source/BsGUISceneTreeView.cpp

@@ -0,0 +1,324 @@
+#include "BsGUISceneTreeView.h"
+#include "BsGUIArea.h"
+#include "BsGUILayout.h"
+#include "BsGUITexture.h"
+#include "BsGUIButton.h"
+#include "BsGUILabel.h"
+#include "BsGUISpace.h"
+#include "BsGUIWidget.h"
+#include "BsGUIMouseEvent.h"
+#include "BsGUISkin.h"
+#include "CmSceneObject.h"
+#include "CmSceneManager.h"
+
+using namespace CamelotFramework;
+using namespace BansheeEngine;
+
+namespace BansheeEditor
+{
+	const UINT32 GUISceneTreeView::ELEMENT_EXTRA_SPACING = 3;
+	const UINT32 GUISceneTreeView::INDENT_SIZE = 10;
+
+	GUISceneTreeView::TreeElement::TreeElement()
+		:mParent(nullptr), mFoldoutBtn(nullptr), mElement(nullptr),
+		mId(0), mIsExpanded(false), mSortedIdx(0), mIsDirty(false), mIsVisible(true)
+	{ }
+
+	GUISceneTreeView::TreeElement::~TreeElement()
+	{
+		for(auto& child : mChildren)
+			cm_delete(child);
+
+		if(mFoldoutBtn != nullptr)
+			GUIElement::destroy(mFoldoutBtn);
+
+		if(mElement != nullptr)
+			GUIElement::destroy(mElement);
+
+		mChildren.clear();
+	}
+
+	GUISceneTreeView::GUISceneTreeView(GUIWidget& parent, GUIElementStyle* backgroundStyle, GUIElementStyle* elementBtnStyle, 
+		GUIElementStyle* foldoutBtnStyle, const BS::GUILayoutOptions& layoutOptions)
+		:GUIElementContainer(parent, layoutOptions), mBackgroundStyle(backgroundStyle),
+		mElementBtnStyle(elementBtnStyle), mFoldoutBtnStyle(foldoutBtnStyle)
+	{
+		if(mBackgroundStyle == nullptr)
+			mBackgroundStyle = parent.getSkin().getStyle("TreeViewBackground");
+
+		if(mElementBtnStyle == nullptr)
+			mElementBtnStyle = parent.getSkin().getStyle("TreeViewElementBtn");
+
+		if(mFoldoutBtnStyle == nullptr)
+			mFoldoutBtnStyle = parent.getSkin().getStyle("TreeViewFoldoutBtn");
+
+		mBackgroundImage = GUITexture::create(parent, mBackgroundStyle);
+		_registerChildElement(mBackgroundImage);
+	}
+
+	GUISceneTreeView::~GUISceneTreeView()
+	{
+
+	}
+
+	GUISceneTreeView* GUISceneTreeView::create(GUIWidget& parent, GUIElementStyle* backgroundStyle, GUIElementStyle* elementBtnStyle, 
+		GUIElementStyle* foldoutBtnStyle)
+	{
+		return new (cm_alloc<GUISceneTreeView, PoolAlloc>()) GUISceneTreeView(parent, backgroundStyle, elementBtnStyle, foldoutBtnStyle, 
+			GUILayoutOptions::create(&GUISkin::DefaultStyle));
+	}
+
+	GUISceneTreeView* GUISceneTreeView::create(GUIWidget& parent, const GUILayoutOptions& layoutOptions)
+	{
+		return new (cm_alloc<GUISceneTreeView, PoolAlloc>()) GUISceneTreeView(parent, nullptr, nullptr, nullptr, layoutOptions);
+	}
+
+	GUISceneTreeView* GUISceneTreeView::create(GUIWidget& parent, const GUILayoutOptions& layoutOptions, GUIElementStyle* backgroundStyle,
+		GUIElementStyle* elementBtnStyle, GUIElementStyle* foldoutBtnStyle)
+	{
+		return new (cm_alloc<GUISceneTreeView, PoolAlloc>()) GUISceneTreeView(parent, backgroundStyle, elementBtnStyle, 
+			foldoutBtnStyle, layoutOptions);
+	}
+
+	void GUISceneTreeView::update()
+	{
+		// NOTE - Instead of iterating through every visible element and comparing it with internal values,
+		// I might just want to add callbacks to SceneManager that notify me of any changes and then only perform
+		// update if anything is actually dirty
+
+		struct UpdateTreeElement
+		{
+			UpdateTreeElement(TreeElement* element, UINT32 seqIdx, bool visible)
+				:element(element), seqIdx(seqIdx), visible(visible)
+			{ }
+
+			TreeElement* element;
+			UINT32 seqIdx;
+			bool visible;
+		};
+
+		HSceneObject root = CM::gSceneManager().getRootNode();
+		mRootElement.mSceneObject = root;
+		mRootElement.mId = root->getId();
+		mRootElement.mSortedIdx = 0;
+
+		Stack<UpdateTreeElement>::type todo;
+		todo.push(UpdateTreeElement(&mRootElement, 0, true));
+
+		while(!todo.empty())
+		{
+			UpdateTreeElement updateElement = todo.top();
+			TreeElement* current = updateElement.element;
+			HSceneObject currentSO = current->mSceneObject;
+			todo.pop();
+
+			// Check if SceneObject has changed in any way and update the tree element
+			if(updateElement.visible)
+			{
+				bool completeMatch = (UINT32)current->mChildren.size() == currentSO->getNumChildren();
+
+				// Early exit case - Most commonly there will be no changes between active and cached data so 
+				// we first do a quick check in order to avoid expensive comparison later
+				if(completeMatch)
+				{
+					for(UINT32 i = 0; i < currentSO->getNumChildren(); i++)
+					{
+						UINT32 curId = currentSO->getChild(i)->getId();
+						if(curId != current->mChildren[i]->mId)
+						{
+							completeMatch = false;
+							break;
+						}
+					}
+				}
+
+				// Not a complete match, compare everything and insert/delete elements as needed
+				if(!completeMatch)
+				{
+					Vector<TreeElement*>::type newChildren;
+
+					mTempToDelete.resize(current->mChildren.size(), true);
+					for(UINT32 i = 0; i < currentSO->getNumChildren(); i++)
+					{
+						UINT32 curId = currentSO->getChild(i)->getId();
+						bool found = false;
+						for(UINT32 j = 0; j < current->mChildren.size(); j++)
+						{
+							TreeElement* currentChild = current->mChildren[i];
+
+							if(curId == currentChild->mId)
+							{
+								mTempToDelete[j] = false;
+								currentChild->mIsDirty = true;
+								currentChild->mSortedIdx = (UINT32)newChildren.size();
+								newChildren.push_back(currentChild);
+
+								found = true;
+								break;
+							}
+						}
+
+						if(!found)
+						{
+							TreeElement* newChild = cm_new<TreeElement>();
+							newChild->mParent = current;
+							newChild->mSceneObject = currentSO;
+							newChild->mId = currentSO->getId();
+							newChild->mName = currentSO->getName();
+							newChild->mSortedIdx = (UINT32)newChildren.size();
+							newChild->mIsDirty = true;
+
+							newChildren.push_back(newChild);
+						}
+					}
+
+					for(UINT32 i = 0; i < current->mChildren.size(); i++)
+					{
+						if(!mTempToDelete[i])
+							continue;
+
+						cm_delete(current->mChildren[i]);
+					}
+
+					current->mChildren = newChildren;
+					current->mIsDirty = true;
+				}
+
+				// Check if name needs updating
+				const String& name = current->mSceneObject->getName();
+				if(current->mName != name)
+				{
+					current->mName = name;
+					current->mIsDirty = true;		
+				}
+
+				// Calculate the sorted index of the element based on its name
+				TreeElement* parent = current->mParent;
+				if(current->mIsDirty && parent != nullptr)
+				{
+					for(UINT32 i = 0; i < (UINT32)parent->mChildren.size(); i++)
+					{
+						if(current->mSortedIdx <= parent->mChildren[i]->mSortedIdx)
+							continue;
+
+						UINT32 stringCompare = current->mName.compare(parent->mChildren[i]->mName);
+						if(stringCompare > 0)
+						{
+							std::swap(current->mSortedIdx, parent->mChildren[i]->mSortedIdx);
+						}
+					}
+				}
+			}
+
+			bool visibilityChanged = false;
+			if(current->mIsVisible != updateElement.visible)
+			{
+				visibilityChanged = true;
+				current->mIsVisible = updateElement.visible;
+				current->mIsDirty = true;
+			}
+			
+			if(current->mIsDirty)
+			{
+				if(updateElement.visible)
+				{
+					// TODO - If no label exists create it
+					// TODO - If has children and no expand button exists create it
+					// TODO - If it has no children but an expand button exists remove it
+				}
+				else
+				{
+					// TODO - If either label or expand button exist remove them
+				}
+
+				markContentAsDirty();
+				current->mIsDirty = false;
+			}
+
+			// Queue children for next iteration
+			if(visibilityChanged || current->mIsVisible)
+			{
+				for(UINT32 i = 0; i < (UINT32)current->mChildren.size(); i++)
+				{
+					todo.push(UpdateTreeElement(current->mChildren[i], i, current->mIsVisible && current->mIsExpanded));
+				}
+			}
+		}
+	}
+
+	bool GUISceneTreeView::mouseEvent(const GUIMouseEvent& event)
+	{
+		
+
+		return false;
+	}
+
+	void GUISceneTreeView::updateClippedBounds()
+	{
+		Vector2I offset = _getOffset();
+		mClippedBounds = RectI(offset.x, offset.y, _getWidth(), _getHeight());
+	}
+
+	void GUISceneTreeView::_updateLayoutInternal(INT32 x, INT32 y, UINT32 width, UINT32 height,
+		RectI clipRect, UINT8 widgetDepth, UINT16 areaDepth)
+	{
+		struct UpdateTreeElement
+		{
+			UpdateTreeElement(TreeElement* element, UINT32 indent)
+				:element(element), indent(indent)
+			{ }
+
+			TreeElement* element;
+			UINT32 indent;
+		};
+
+		Stack<UpdateTreeElement>::type todo;
+		todo.push(UpdateTreeElement(&mRootElement, 0));
+
+		// NOTE - Instead of iterating through all elements, try to find those within the clip rect
+		// and only iterate through those. Others should somehow be marked in-active (similar to GUIElement::isDisabled()?)
+
+		Vector2I offset(x, y);
+		while(!todo.empty())
+		{
+			UpdateTreeElement currentUpdateElement = todo.top();
+			TreeElement* current = currentUpdateElement.element;
+			UINT32 indent = currentUpdateElement.indent;
+			todo.pop();
+
+			for(auto& child : current->mChildren)
+			{
+				if(!child->mIsVisible)
+					continue;
+
+				if(child->mElement != nullptr)
+				{
+					Vector2I elementSize = child->mElement->_getOptimalSize();
+
+					offset.x = indent * INDENT_SIZE;
+
+					child->mElement->_setOffset(offset);
+					child->mElement->_setWidth(elementSize.x);
+					child->mElement->_setHeight(elementSize.y);
+					child->mElement->_setAreaDepth(areaDepth);
+					child->mElement->_setWidgetDepth(widgetDepth);
+
+					RectI elemClipRect(clipRect.x - offset.x, clipRect.y - offset.y, clipRect.width, clipRect.height);
+					child->mElement->_setClipRect(elemClipRect);
+
+					offset.y += elementSize.y + ELEMENT_EXTRA_SPACING;
+				}
+
+				// TODO - Position expand buttons
+				
+				todo.push(UpdateTreeElement(child, indent + 1));
+			}
+		}
+	}
+
+	const String& GUISceneTreeView::getGUITypeName()
+	{
+		static String typeName = "SceneTreeView";
+		return typeName;
+	}
+}

+ 7 - 1
CamelotCore/Include/CmSceneObject.h

@@ -19,10 +19,13 @@ namespace CamelotFramework
 		static HSceneObject create(const String& name);
 		void destroy();
 
+		UINT32 getId() const { return mId; }
+		const String& getName() const { return mName; }
+
 	private:
 		HSceneObject mThisHandle;
 
-		SceneObject(const String& name);
+		SceneObject(const String& name, UINT32 id);
 		~SceneObject();
 
 		static HSceneObject createInternal(const String& name);
@@ -118,7 +121,10 @@ namespace CamelotFramework
 		void pitch(const Radian& angle);
 
 	private:
+		static UINT32 NextFreeId;
+
 		String mName;
+		UINT32 mId;
 
 		Vector3 mPosition;
 		Quaternion mRotation;

+ 5 - 3
CamelotCore/Source/CmSceneObject.cpp

@@ -7,8 +7,10 @@
 
 namespace CamelotFramework
 {
-	SceneObject::SceneObject(const String& name)
-		:mName(name), mPosition(Vector3::ZERO), mRotation(Quaternion::IDENTITY), mScale(Vector3::ONE),
+	UINT32 SceneObject::NextFreeId = 0;
+
+	SceneObject::SceneObject(const String& name, UINT32 id)
+		:mName(name), mId(id), mPosition(Vector3::ZERO), mRotation(Quaternion::IDENTITY), mScale(Vector3::ONE),
 		mWorldPosition(Vector3::ZERO), mWorldRotation(Quaternion::IDENTITY), mWorldScale(Vector3::ONE),
 		mCachedLocalTfrm(Matrix4::IDENTITY), mIsCachedLocalTfrmUpToDate(false),
 		mCachedWorldTfrm(Matrix4::IDENTITY), mIsCachedWorldTfrmUpToDate(false),
@@ -36,7 +38,7 @@ namespace CamelotFramework
 	HSceneObject SceneObject::createInternal(const String& name)
 	{
 		HSceneObject sceneObject = GameObjectHandle<SceneObject>(
-			new (cm_alloc<SceneObject, PoolAlloc>()) SceneObject(name),
+			new (cm_alloc<SceneObject, PoolAlloc>()) SceneObject(name, NextFreeId++),
 			&cm_delete<PoolAlloc, GameObject>);
 		sceneObject->mThisHandle = sceneObject;
 

+ 3 - 13
TODO.txt

@@ -1,11 +1,8 @@
 ----------------------- CAMELOT 2D / GUI -----------------------------------------------------------
 
-LONGTERM TODO:
-1. Finish docking manager
-2. OPTIMIZE GUI
- - This should be a fairly long-term task, about 2 months. I need to study GUI system in detail and identify bottlenecks.
-   Then develop a clear and tight plan how to reorganize everything so it runs at maximum efficiency. GUI system is pretty much the 
-   only large fully fledged system in the engine, so investing this much time into optimizing it is worth it.
+Editor localization should be different from in-game one.
+ - Because what happens when user decides to set "File" localized string to something else? It will also modify the editor string.
+ - HString probably needs to accept an optional parameter of which StringTable to use
 
 Optimization notes:
  - submitToCoreThread calls are EXTREMELY slow. In 10-50ms range.
@@ -41,13 +38,6 @@ TextBox needed elements:
   - Remove updateText calls from updateRenderElementsInternal and instead call it whenever offsets change
   - I might consider not rendering caret from within input sprite to avoid redrawing it while, and draw it directly from GUIManager
 
-GUIDragManager
- - GUI system sends startdrag/enddrag/drag events to all elements
- - startDrag(void* userPtr) changes cursor
- - releasing the cursor sends enddrag event and then the control can retrieve the user ptr
- - SINCE currently non-active elements ignore drag events, add GUIElement::acceptsMouseDrop property
-   - Such elements will receive mouse drag and mouse up events if other element is active, and they can filter if they want to accept it
-
 -----------
 
  - My test model is rendering back faces. I need to flip them.

+ 46 - 0
TreeView.txt

@@ -0,0 +1,46 @@
+TreeView
+ - internally I store TreeElements in a hierarchy. Each has a:
+    - isExpanded
+      - modifying this value will update GUIElements of all the child values (add or remove them)
+	  - adding/removing a child to/from an element should add or remove the foldout button
+      - call markContentAsDirty
+  - updateLayoutInternal
+    - goes through all TreeElements and updates their positions
+  - Name editing
+     - Detect mouse input in TreeView and if I double click over a GUILabel start rename
+     - Or if I select it and hit F2 also start rename
+     - Has callbacks with which it notifies TreeView of actual changes
+  - Selecting
+     - Clicking on a row of TreeView sets a flag that item is selected, and a GUIElement is created. 
+     - Element is positioned in updateLayoutInternal and it shows a different colored background
+  - Deleting
+      - Simply track currently selected element and Delete event
+
+Implementation steps:
+ - Get just labels with indenting rendering
+   - Test if SceneObject add/remove/rename works and updates data properly
+ - Add expand buttons
+   - Test if expanding/closing works properly
+ - Selection
+   - Clicking on element selects it, delete removes it, F2 renames is. Slow double click renames it.
+   - Rename implementation to follow, just set up an empty method for now
+   - Callback on selection
+ - Rename implementation
+   - When rename is initiated hide the objects label and replace it with an input-box with the same text as a label
+   - Rename ends when user hits enter or click anywhere else but the rename box
+ - Drag and drop
+   - If a mouse drag is detected DragAndDropManager is activated
+   - Element is not removed from its original position until drag is complete
+   - Dragging over an element will make a highlight box appear around it
+   - Dragging between elements will make a thick line appear between them
+   - Releasing the drag changes the element parents
+ - Auto scroll
+    - Sliding over top or bottom of the tree view while dragging an element will search GUIElement parents to find a ScrollArea. If it finds one it will attempt to scroll up or down.
+
+Detecting external clicks:
+ - LostFocus event?
+   - Triggered when I click on another control within my own windows, or when controls parent window loses the focus himself
+   - Each GUIElement can implement acceptsMouseFocus event. If it does, then mouse click will cause a ReceivedFocus event.
+     - Mouse click on any other element, or window losing focus will trigger LostFocus events
+ - I can replace clumsy Selective Input with this approach
+ - DropDownBox can also use the same approach although I will likely need to convert DropDownBox to a GUIElement - DO THIS AFTER TreeView implementation is working