Browse Source

Started work on undo/redo system

Marko Pintera 12 years ago
parent
commit
88c83017de

+ 1 - 0
BansheeEngine.sln

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

+ 5 - 0
CamelotClient/CamelotClient.vcxproj

@@ -255,8 +255,10 @@
     <Text Include="ReadMe.txt" />
   </ItemGroup>
   <ItemGroup>
+    <ClInclude Include="Include\BsCmdEditPlainFieldGO.h" />
     <ClInclude Include="Include\BsDockManager.h" />
     <ClInclude Include="Include\BsEditorApplication.h" />
+    <ClInclude Include="Include\BsEditorCommand.h" />
     <ClInclude Include="Include\BsEditorGUI.h" />
     <ClInclude Include="Include\BsEditorPrerequisites.h" />
     <ClInclude Include="Include\BsEditorWidget.h" />
@@ -278,12 +280,14 @@
     <ClInclude Include="Include\CmTestTextSprite.h" />
     <ClInclude Include="Include\DbgEditorWidget1.h" />
     <ClInclude Include="Include\DbgEditorWidget2.h" />
+    <ClInclude Include="Include\BsUndoRedo.h" />
     <ClInclude Include="stdafx.h" />
     <ClInclude Include="targetver.h" />
   </ItemGroup>
   <ItemGroup>
     <ClCompile Include="CamelotClient.cpp" />
     <ClCompile Include="Source\BsDockManager.cpp" />
+    <ClCompile Include="Source\BsEditorCommand.cpp" />
     <ClCompile Include="Source\BsEditorGUI.cpp" />
     <ClCompile Include="Source\BsEditorWidget.cpp" />
     <ClCompile Include="Source\BsEditorWidgetContainer.cpp" />
@@ -300,6 +304,7 @@
     <ClCompile Include="Source\BsGUIWindowFrameWidget.cpp" />
     <ClCompile Include="Source\BsGUIWindowDropArea.cpp" />
     <ClCompile Include="Source\BsMainEditorWindow.cpp" />
+    <ClCompile Include="Source\BsUndoRedo.cpp" />
     <ClCompile Include="Source\CmDebugCamera.cpp" />
     <ClCompile Include="Source\BsEditorApplication.cpp" />
     <ClCompile Include="Source\CmTestTextSprite.cpp" />

+ 21 - 0
CamelotClient/CamelotClient.vcxproj.filters

@@ -19,6 +19,12 @@
     <Filter Include="Source Files\Editor">
       <UniqueIdentifier>{ccd6be96-5773-46cc-a751-18a7007716c3}</UniqueIdentifier>
     </Filter>
+    <Filter Include="Header Files\Editor\Commands">
+      <UniqueIdentifier>{c23d8d16-3a53-4e53-9e87-a15e33d5758c}</UniqueIdentifier>
+    </Filter>
+    <Filter Include="Source Files\Editor\Command">
+      <UniqueIdentifier>{fc5eed3b-3a94-4c0b-b462-636e84615f94}</UniqueIdentifier>
+    </Filter>
   </ItemGroup>
   <ItemGroup>
     <Text Include="ReadMe.txt" />
@@ -99,6 +105,15 @@
     <ClInclude Include="Include\BsGUITreeViewEditBox.h">
       <Filter>Header Files\Editor</Filter>
     </ClInclude>
+    <ClInclude Include="Include\BsEditorCommand.h">
+      <Filter>Header Files\Editor\Commands</Filter>
+    </ClInclude>
+    <ClInclude Include="Include\BsUndoRedo.h">
+      <Filter>Header Files\Editor</Filter>
+    </ClInclude>
+    <ClInclude Include="Include\BsCmdEditPlainFieldGO.h">
+      <Filter>Header Files\Editor\Commands</Filter>
+    </ClInclude>
   </ItemGroup>
   <ItemGroup>
     <ClCompile Include="stdafx.cpp">
@@ -173,5 +188,11 @@
     <ClCompile Include="Source\BsGUITreeViewEditBox.cpp">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="Source\BsEditorCommand.cpp">
+      <Filter>Source Files\Editor\Command</Filter>
+    </ClCompile>
+    <ClCompile Include="Source\BsUndoRedo.cpp">
+      <Filter>Source Files\Editor</Filter>
+    </ClCompile>
   </ItemGroup>
 </Project>

+ 82 - 0
CamelotClient/Include/BsCmdEditPlainFieldGO.h

@@ -0,0 +1,82 @@
+#pragma once
+
+#include "BsEditorPrerequisites.h"
+#include "BsEditorCommand.h"
+
+namespace BansheeEditor
+{
+	// TODO - This is only valid for plain field types. Add something similar for pointers and/or arrays?
+	template<class T>
+	class CmdEditPlainFieldGO : public EditorCommand
+	{
+	public:
+		~CmdEditPlainFieldGO() 
+		{
+			if(mNewData != nullptr)
+				cm_free(mNewData);
+
+			if(mOldData != nullptr)
+				cm_free(mOldData);
+		}
+
+		void execute(const CM::GameObjectHandleBase& gameObject, const CM::String& fieldName, const T& fieldValue)
+		{
+			// Register command and commit it
+			CmdEditFieldGO* command = cm_new<CmdEditFieldGO>(gameObject, fieldName, fieldValue);
+			UndoRedo::instance().registerCommand(command);
+			command->commit();
+		}
+
+		void commit()
+		{
+			if(mGameObject.isDestroyed())
+				return;
+
+			T fieldValue;
+			CM::RTTIPlainType<T>::fromMemory(fieldValue, (char*)mNewData);
+
+			CM::RTTITypeBase* rtti = mGameObject->getRTTI();
+			rtti->setPlainValue(mGameObject.get(), mFieldName, fieldValue);
+		}
+
+		void revert()
+		{
+			if(mGameObject.isDestroyed())
+				return;
+
+			T fieldValue;
+			CM::RTTIPlainType<T>::fromMemory(fieldValue, (char*)mOldData);
+
+			CM::RTTITypeBase* rtti = mGameObject->getRTTI();
+			rtti->setPlainValue(mGameObject.get(), mFieldName, fieldValue);
+		}
+
+	private:
+		friend class UndoRedo;
+
+		CmdEditPlainFieldGO(const CM::GameObjectHandleBase& gameObject, const CM::String& fieldName, const T& fieldValue)
+			:mGameObject(gameObject), mFieldName(fieldName), mNewData(nullptr), mOldData(nullptr)
+		{
+			// Convert new value to bytes
+			CM::UINT32 newDataNumBytes = CM::RTTIPlainType<T>::getDynamicSize(fieldValue);
+			mNewData = CM::cm_alloc(newDataNumBytes);
+
+			CM::RTTIPlainType<T>::toMemory(fieldValue, (char*)mNewData);
+
+			// Get old value and also convert it to bytes
+			CM::String oldFieldValue;
+			gameObject->getRTTI()->getPlainValue(gameObject.get(), fieldName, oldFieldValue);
+
+			CM::UINT32 oldDataNumBytes = CM::RTTIPlainType<T>::getDynamicSize(oldFieldValue);
+			mOldData = cm_alloc(oldDataNumBytes);
+
+			CM::RTTIPlainType<T>::toMemory(oldFieldValue, (char*)mOldData);
+		}
+
+		CM::GameObjectHandleBase mGameObject;
+		CM::String mFieldName;
+
+		void* mNewData;
+		void* mOldData;
+	};
+}

+ 17 - 0
CamelotClient/Include/BsEditorCommand.h

@@ -0,0 +1,17 @@
+#pragma once
+
+#include "BsEditorPrerequisites.h"
+
+namespace BansheeEditor
+{
+	class EditorCommand
+	{
+	public:
+		virtual ~EditorCommand() { }
+
+		virtual void commit() { }
+		virtual void revert() { }
+
+		static void destroy(EditorCommand* command);
+	};
+}

+ 1 - 0
CamelotClient/Include/BsEditorPrerequisites.h

@@ -21,6 +21,7 @@ namespace BansheeEditor
 	class GUIDockSlider;
 	class GUISceneTreeView;
 	class GUITreeViewEditBox;
+	class EditorCommand;
 
 	enum class DragAndDropType
 	{

+ 34 - 0
CamelotClient/Include/BsUndoRedo.h

@@ -0,0 +1,34 @@
+#pragma once
+
+#include "BsEditorPrerequisites.h"
+#include "CmModule.h"
+
+namespace BansheeEditor
+{
+	class UndoRedo : public CM::Module<UndoRedo>
+	{
+	public:
+		UndoRedo();
+		~UndoRedo();
+
+		void undo();
+		void redo();
+
+		void registerCommand(EditorCommand* command);
+
+	private:
+		static const CM::UINT32 MAX_STACK_ELEMENTS;
+
+		EditorCommand** mUndoStack;
+		EditorCommand** mRedoStack;
+
+		CM::UINT32 mUndoStackPtr;
+		CM::UINT32 mUndoNumElements;
+
+		CM::UINT32 mRedoStackPtr;
+		CM::UINT32 mRedoNumElements;
+
+		void clearUndoStack();
+		void clearRedoStack();
+	};
+}

+ 12 - 0
CamelotClient/Source/BsEditorCommand.cpp

@@ -0,0 +1,12 @@
+#include "BsEditorCommand.h"
+
+using namespace BansheeEngine;
+using namespace CamelotFramework;
+
+namespace BansheeEditor
+{
+	void EditorCommand::destroy(EditorCommand* command)
+	{
+		cm_delete(command);
+	}
+}

+ 93 - 0
CamelotClient/Source/BsUndoRedo.cpp

@@ -0,0 +1,93 @@
+#include "BsUndoRedo.h"
+#include "BsEditorCommand.h"
+
+using namespace CamelotFramework;
+using namespace BansheeEngine;
+
+namespace BansheeEditor
+{
+	const CM::UINT32 UndoRedo::MAX_STACK_ELEMENTS = 1000;
+
+	UndoRedo::UndoRedo()
+		:mUndoStackPtr(0), mUndoNumElements(0),
+		mRedoStackPtr(0), mRedoNumElements(0),
+		mUndoStack(nullptr), mRedoStack(nullptr)
+	{
+		mUndoStack = cm_newN<EditorCommand*>(MAX_STACK_ELEMENTS);
+		mRedoStack = cm_newN<EditorCommand*>(MAX_STACK_ELEMENTS);
+	}
+
+	UndoRedo::~UndoRedo()
+	{
+		clearUndoStack();
+		clearRedoStack();
+
+		cm_deleteN(mUndoStack, MAX_STACK_ELEMENTS);
+		cm_deleteN(mRedoStack, MAX_STACK_ELEMENTS);
+	}
+
+	void UndoRedo::undo()
+	{
+		if(mUndoNumElements == 0)
+			return;
+
+		EditorCommand* command = mUndoStack[mUndoStackPtr];
+		mUndoStackPtr = (mUndoStackPtr - 1) % MAX_STACK_ELEMENTS;
+		mUndoNumElements--;
+
+		mRedoStackPtr = (mRedoStackPtr + 1) % MAX_STACK_ELEMENTS;
+		mRedoStack[mRedoStackPtr] = command;
+		mRedoNumElements++;
+
+		command->revert();
+	}
+
+	void UndoRedo::redo()
+	{
+		if(mRedoNumElements == 0)
+			return;
+
+		EditorCommand* command = mRedoStack[mRedoStackPtr];
+		mRedoStackPtr = (mRedoStackPtr - 1) % MAX_STACK_ELEMENTS;
+		mRedoNumElements--;
+
+		mUndoStackPtr = (mUndoStackPtr + 1) % MAX_STACK_ELEMENTS;
+		mUndoStack[mUndoStackPtr] = command;
+		mUndoNumElements++;
+
+		command->commit();
+	}
+
+	void UndoRedo::registerCommand(EditorCommand* command)
+	{
+		mUndoStackPtr = (mUndoStackPtr + 1) % MAX_STACK_ELEMENTS;
+		mUndoStack[mUndoStackPtr] = command;
+		mUndoNumElements++;
+
+		clearRedoStack();
+	}
+
+	void UndoRedo::clearUndoStack()
+	{
+		while(mUndoNumElements > 0)
+		{
+			EditorCommand* command = mUndoStack[mUndoStackPtr];
+			mUndoStackPtr = (mUndoStackPtr - 1) % MAX_STACK_ELEMENTS;
+			mUndoNumElements--;
+
+			EditorCommand::destroy(command);
+		}
+	}
+
+	void UndoRedo::clearRedoStack()
+	{
+		while(mRedoNumElements > 0)
+		{
+			EditorCommand* command = mRedoStack[mRedoStackPtr];
+			mRedoStackPtr = (mRedoStackPtr - 1) % MAX_STACK_ELEMENTS;
+			mRedoNumElements--;
+
+			EditorCommand::destroy(command);
+		}
+	}
+}

+ 21 - 0
CamelotCore/Include/CmGameObjectHandle.h

@@ -43,6 +43,17 @@ namespace CamelotFramework
 		 * @brief	Internal method only. Not meant to be called directly.
 		 */
 		std::shared_ptr<GameObjectHandleData> getHandleData() const { return mData; }
+
+		GameObject* get() const 
+		{ 
+			throwIfDestroyed();
+
+			return mData->mPtr; 
+		}
+
+		GameObject* operator->() const { return get(); }
+		GameObject& operator*() const { return *get(); }
+
 	protected:
 		friend SceneObject;
 
@@ -68,6 +79,8 @@ namespace CamelotFramework
 		std::shared_ptr<GameObjectHandleData> mData;
 	};
 
+	// NOTE: It is important this class contains no data since we often value 
+	// cast it to its base 
 	template <typename T>
 	class GameObjectHandle : public GameObjectHandleBase
 	{
@@ -93,6 +106,14 @@ namespace CamelotFramework
 			return *this;
 		}
 
+		GameObjectHandleBase toBase()
+		{
+			GameObjectHandleBase base;
+			base.mData = this->mData;
+
+			return base;
+		}
+
 		T* get() const 
 		{ 
 			throwIfDestroyed();

+ 3 - 1
CamelotCore/Include/CmSceneObjectRTTI.h

@@ -9,11 +9,13 @@ namespace CamelotFramework
 	class CM_EXPORT SceneObjectRTTI : public RTTIType<SceneObject, GameObject, SceneObjectRTTI>
 	{
 	private:
+		String& getName(SceneObject* obj) { return obj->mName; }
+		void setName(SceneObject* obj, String& name) { obj->mName = name; }
 
 	public:
 		SceneObjectRTTI()
 		{
-
+			addPlainField("mName", 0, &SceneObjectRTTI::getName, &SceneObjectRTTI::setName);
 		}
 
 		virtual const String& getRTTIName()

+ 23 - 0
UndoRedo.txt

@@ -0,0 +1,23 @@
+Undo/Redo can be problematic when moving objects via transform gizmos
+ - Although I can just trigger it on mouse up
+   - CmdMoveSO, CmdRotateSO, CmdScaleSO
+
+It will also be problematic when editing text fields
+ - Technically I can register each character as its own undo operation
+Then when I leave the text field I register the entire change?
+
+How will I detect changes in the inspector?
+ - Just use overriden GUIElements that detect focus lost and changes
+
+Multiple stacks
+ - Used for pushing finer or more grained commands
+ - e.g. when starting an edit on an InputBox I do UndoRedo.newStack which returns an unique index I provide when adding commands to UndoRedo. 
+   Then I register each invidual keystroke as a separate command. Once the InputBox loses focus though I do UndoRedo.removeStack.
+
+How do I register undo/redo on generic properties?
+ - Just use RTTI names for updating/reading those properties
+ - How will I deal with arrays?
+   - Just save/restore the entire array, it should be no different than single fields
+ - What about C# stuff?
+  - For each Serializable class, on re-compile I go and create instances of "ScriptRTTIType"
+   - In constructor that class iterates over all serializable fields in the C# class and fills the mFields array same as custom RTTI classes do