Răsfoiți Sursa

Undo/redo implemented for the animation window

BearishSun 9 ani în urmă
părinte
comite
684adaecc1

+ 6 - 0
Source/BansheeEditor/Include/BsEditorCommand.h

@@ -29,6 +29,12 @@ namespace BansheeEngine
 	private:
 		friend class UndoRedo;
 
+		/** Triggers when a command is added to an undo/redo stack. */
+		virtual void onCommandAdded() { }
+
+		/** Triggers when a command is removed from an undo/redo stack. */
+		virtual void onCommandRemoved() {}
+
 		WString mDescription;
 		UINT32 mId;
 	};

+ 2 - 2
Source/BansheeEditor/Include/BsUndoRedo.h

@@ -72,8 +72,8 @@ namespace BansheeEngine
 		/**	Removes the last undo command from the undo stack, and returns it. */
 		SPtr<EditorCommand> removeLastFromUndoStack();
 
-		/**	Adds a new command to the undo stack. */
-		void addToUndoStack(const SPtr<EditorCommand>& command);
+		/**	Adds a new command to the undo stack. Returns the command that was replaced. */
+		SPtr<EditorCommand> addToUndoStack(const SPtr<EditorCommand>& command);
 
 		/**	Removes all entries from the undo stack. */
 		void clearUndoStack();

+ 31 - 2
Source/BansheeEditor/Source/BsUndoRedo.cpp

@@ -43,6 +43,7 @@ namespace BansheeEngine
 			return;
 
 		SPtr<EditorCommand> command = mRedoStack[mRedoStackPtr];
+		mRedoStack[mRedoStackPtr] = SPtr<EditorCommand>();
 		mRedoStackPtr = (mRedoStackPtr - 1) % MAX_STACK_ELEMENTS;
 		mRedoNumElements--;
 
@@ -73,6 +74,9 @@ namespace BansheeEngine
 
 		for(UINT32 i = 0; i < topGroup.numEntries; i++)
 		{
+			if (mUndoStack[mUndoStackPtr] != nullptr)
+				mUndoStack[mUndoStackPtr]->onCommandRemoved();
+
 			mUndoStack[mUndoStackPtr] = SPtr<EditorCommand>();
 			mUndoStackPtr = (mUndoStackPtr - 1) % MAX_STACK_ELEMENTS;
 			mUndoNumElements--;
@@ -85,7 +89,11 @@ namespace BansheeEngine
 	void UndoRedo::registerCommand(const SPtr<EditorCommand>& command)
 	{
 		command->mId = mNextCommandId++;
-		addToUndoStack(command);
+		command->onCommandAdded();
+
+		SPtr<EditorCommand> existingCommand = addToUndoStack(command);
+		if (existingCommand != nullptr)
+			existingCommand->onCommandRemoved();
 
 		clearRedoStack();
 	}
@@ -105,6 +113,11 @@ namespace BansheeEngine
 		{
 			if (mUndoStack[undoPtr]->mId == id)
 			{
+				if (mUndoStack[undoPtr] != nullptr)
+					mUndoStack[undoPtr]->onCommandRemoved();
+
+				mUndoStack[undoPtr] = SPtr<EditorCommand>();
+
 				for (UINT32 j = mUndoNumElements - i; j < (mUndoNumElements - 1); j++)
 				{
 					UINT32 nextUndoPtr = (undoPtr + 1) % MAX_STACK_ELEMENTS;
@@ -126,6 +139,11 @@ namespace BansheeEngine
 		{
 			if (mRedoStack[redoPtr]->mId == id)
 			{
+				if (mRedoStack[redoPtr] != nullptr)
+					mRedoStack[redoPtr]->onCommandRemoved();
+
+				mRedoStack[redoPtr] = SPtr<EditorCommand>();
+
 				for (UINT32 j = mRedoNumElements - i; j < (mRedoNumElements - 1); j++)
 				{
 					UINT32 nextRedoPtr = (redoPtr + 1) % MAX_STACK_ELEMENTS;
@@ -173,9 +191,12 @@ namespace BansheeEngine
 		return command;
 	}
 
-	void UndoRedo::addToUndoStack(const SPtr<EditorCommand>& command)
+	SPtr<EditorCommand> UndoRedo::addToUndoStack(const SPtr<EditorCommand>& command)
 	{
 		mUndoStackPtr = (mUndoStackPtr + 1) % MAX_STACK_ELEMENTS;
+
+		SPtr<EditorCommand> existingCommand = mUndoStack[mUndoStackPtr];
+
 		mUndoStack[mUndoStackPtr] = command;
 		mUndoNumElements = std::min(mUndoNumElements + 1, MAX_STACK_ELEMENTS);
 
@@ -184,12 +205,17 @@ namespace BansheeEngine
 			GroupData& topGroup = mGroups.top();
 			topGroup.numEntries = std::min(topGroup.numEntries + 1, MAX_STACK_ELEMENTS);
 		}
+
+		return existingCommand;
 	}
 
 	void UndoRedo::clearUndoStack()
 	{
 		while(mUndoNumElements > 0)
 		{
+			if (mUndoStack[mUndoStackPtr] != nullptr)
+				mUndoStack[mUndoStackPtr]->onCommandRemoved();
+
 			mUndoStack[mUndoStackPtr] = SPtr<EditorCommand>();
 			mUndoStackPtr = (mUndoStackPtr - 1) % MAX_STACK_ELEMENTS;
 			mUndoNumElements--;
@@ -203,6 +229,9 @@ namespace BansheeEngine
 	{
 		while(mRedoNumElements > 0)
 		{
+			if (mRedoStack[mRedoStackPtr] != nullptr)
+				mRedoStack[mRedoStackPtr]->onCommandRemoved();
+
 			mRedoStack[mRedoStackPtr] = SPtr<EditorCommand>();
 			mRedoStackPtr = (mRedoStackPtr - 1) % MAX_STACK_ELEMENTS;
 			mRedoNumElements--;

+ 8 - 0
Source/MBansheeEditor/Utility/EdAnimationCurve.cs

@@ -96,6 +96,14 @@ namespace BansheeEditor
             get { return keyFrames; }
         }
 
+        /// <summary>
+        /// Returns the non-editor version of the curve.
+        /// </summary>
+        public AnimationCurve Normal
+        {
+            get { return native; }
+        }
+
         /// <summary>
         /// Creates a new animation curve with zero keyframes.
         /// </summary>

+ 24 - 4
Source/MBansheeEditor/Utility/UndoRedo.cs

@@ -20,6 +20,8 @@ namespace BansheeEditor
     /// </summary>
     public class UndoRedo : ScriptObject
     {
+        private static UndoRedo global;
+
         /// <summary>
         /// Constructor for internal runtime use.
         /// </summary>
@@ -40,7 +42,7 @@ namespace BansheeEditor
         /// </summary>
         public static UndoRedo Global
         {
-            get { return Internal_GetGlobal(); }
+            get { return global; }
         }
 
         /// <summary>
@@ -107,6 +109,14 @@ namespace BansheeEditor
             Internal_PopCommand(mCachedPtr, id);
         }
 
+        /// <summary>
+        /// Clears all undo/redo commands from the stack.
+        /// </summary>
+        public void Clear()
+        {
+            Internal_Clear(mCachedPtr);
+        }
+
         /// <summary>
         /// Records a state of the entire scene object at a specific point and allows you to restore it to its original 
         /// values as needed. Undo operation recorded in global undo/redo stack.
@@ -252,11 +262,18 @@ namespace BansheeEditor
                 Internal_BreakPrefab(so.GetCachedPtr(), description);
         }
 
-        [MethodImpl(MethodImplOptions.InternalCall)]
-        internal static extern void Internal_CreateInstance(UndoRedo instance);
+        /// <summary>
+        /// Used by the runtime to set the global undo/redo stack.
+        /// </summary>
+        /// <param name="global">Instance of the global undo/redo stack.</param>
+        private static void Internal_SetGlobal(UndoRedo global)
+        {
+            // We can't set this directly through the field because there is an issue with Mono and static fields
+            UndoRedo.global = global;
+        }
 
         [MethodImpl(MethodImplOptions.InternalCall)]
-        internal static extern UndoRedo Internal_GetGlobal();
+        internal static extern void Internal_CreateInstance(UndoRedo instance);
 
         [MethodImpl(MethodImplOptions.InternalCall)]
         internal static extern void Internal_Undo(IntPtr thisPtr);
@@ -273,6 +290,9 @@ namespace BansheeEditor
         [MethodImpl(MethodImplOptions.InternalCall)]
         internal static extern void Internal_PopGroup(IntPtr thisPtr, string name);
 
+        [MethodImpl(MethodImplOptions.InternalCall)]
+        internal static extern void Internal_Clear(IntPtr thisPtr);
+
         [MethodImpl(MethodImplOptions.InternalCall)]
         internal static extern void Internal_PopCommand(IntPtr thisPtr, int id);
 

+ 2 - 2
Source/MBansheeEditor/Window/MenuItems.cs

@@ -718,7 +718,7 @@ namespace BansheeEditor
         /// <summary>
         /// Executes the last command on the undo stack, undoing its operations.
         /// </summary>
-        [MenuItem("Edit/Undo", 9500, true)]
+        [MenuItem("Edit/Undo", ButtonModifier.Ctrl, ButtonCode.Z, 9500, true)]
         [ToolbarItem("Undo", ToolbarIcon.Undo, "Undo (Ctrl + Z)", 1900, true)]
         public static void Undo()
         {
@@ -742,7 +742,7 @@ namespace BansheeEditor
         /// <summary>
         /// Executes the last command on the redo stack (last command we called undo on), re-applying its operation.
         /// </summary>
-        [MenuItem("Edit/Redo", 9499)]
+        [MenuItem("Edit/Redo", ButtonModifier.Ctrl, ButtonCode.Y, 9499)]
         [ToolbarItem("Redo", ToolbarIcon.Redo, "Redo (Ctrl + Y)", 1899)]
         public static void Redo()
         {

+ 58 - 20
Source/MBansheeEditor/Windows/Animation/GUICurveEditor.cs

@@ -531,8 +531,6 @@ namespace BansheeEditor
                                 }
                             }
 
-                            // TODO - UNDOREDO record keyframe or tangent
-
                             isDragInProgress = true;
                         }
                     }
@@ -583,6 +581,8 @@ namespace BansheeEditor
                             }
 
                             isModifiedDuringDrag = true;
+                            window.RecordClipState();
+
                             guiCurveDrawing.Rebuild();
                             UpdateEventsGUI();
                         }
@@ -624,6 +624,8 @@ namespace BansheeEditor
                                 curve.Apply();
 
                                 isModifiedDuringDrag = true;
+                                window.RecordClipState();
+
                                 guiCurveDrawing.Rebuild();
                             }
                         }
@@ -756,7 +758,7 @@ namespace BansheeEditor
             else
                 ShowReadOnlyMessage();
 
-            // TODO - UNDOREDO
+            window.RecordClipState();
 
             OnCurveModified?.Invoke();
             guiCurveDrawing.Rebuild();
@@ -774,7 +776,7 @@ namespace BansheeEditor
             EventInfo eventInfo = new EventInfo();
             eventInfo.animEvent = new AnimationEvent("", eventTime);
             
-            events.Add(eventInfo); // TODO - UNDOREDO
+            events.Add(eventInfo);
             OnEventAdded?.Invoke();
 
             UpdateEventsGUI();
@@ -863,7 +865,7 @@ namespace BansheeEditor
                 curve.Apply();
             }
 
-            // TODO - UNDOREDO
+            window.RecordClipState();
 
             OnCurveModified?.Invoke();
             guiCurveDrawing.Rebuild();
@@ -893,7 +895,7 @@ namespace BansheeEditor
                 else
                     ShowReadOnlyMessage();
 
-                // TODO - UNDOREDO
+                window.RecordClipState();
 
                 OnCurveModified?.Invoke();
                 guiCurveDrawing.Rebuild();
@@ -915,9 +917,9 @@ namespace BansheeEditor
                 EventInfo eventInfo = new EventInfo();
                 eventInfo.animEvent = new AnimationEvent("", time);
 
-                events.Add(eventInfo); // TODO - UNDOREDO
-                OnEventAdded?.Invoke();
+                events.Add(eventInfo);
 
+                OnEventAdded?.Invoke();
                 UpdateEventsGUI();
                 guiCurveDrawing.Rebuild();
 
@@ -951,8 +953,7 @@ namespace BansheeEditor
             else
                 ShowReadOnlyMessage();
 
-            // TODO - UNDOREDO
-
+            window.RecordClipState();
             ClearSelection();
 
             OnCurveModified?.Invoke();
@@ -972,9 +973,10 @@ namespace BansheeEditor
                     newEvents.Add(entry);
             }
 
-            events = newEvents; // TODO - UNDOREDO
-            OnEventDeleted?.Invoke();
+            events = newEvents;
+            window.RecordClipState();
 
+            OnEventDeleted?.Invoke();
             ClearSelection();
 
             guiCurveDrawing.Rebuild();
@@ -1077,10 +1079,14 @@ namespace BansheeEditor
             {
                 curve.UpdateKeyframe(keyIndex, x.time, x.value);
                 curve.Apply();
-                // TODO UNDOREDO
 
                 guiCurveDrawing.Rebuild();
                 OnCurveModified?.Invoke();
+            },
+            x =>
+            {
+                if (x)
+                    window.RecordClipState();
             });
         }
 
@@ -1125,6 +1131,11 @@ namespace BansheeEditor
             {
                 UpdateEventsGUI();
                 OnEventModified?.Invoke();
+            },
+            x =>
+            {
+                if(x)
+                    window.RecordClipState();
             });
         }
 
@@ -1148,21 +1159,25 @@ namespace BansheeEditor
     [DefaultSize(120, 80)]
     internal class KeyframeEditWindow : DropDownWindow
     {
+        private Action<bool> closeCallback;
+        private bool changesMade;
+
         /// <summary>
         /// Initializes the drop down window by creating the necessary GUI. Must be called after construction and before
         /// use.
         /// </summary>
         /// <param name="keyFrame">Keyframe whose properties to edit.</param>
         /// <param name="updateCallback">Callback triggered when event values change.</param>
-        internal void Initialize(KeyFrame keyFrame, Action<KeyFrame> updateCallback)
+        /// <param name="closeCallback">Callback triggered just before the window closes.</param>
+        internal void Initialize(KeyFrame keyFrame, Action<KeyFrame> updateCallback, Action<bool> closeCallback)
         {
             GUIFloatField timeField = new GUIFloatField(new LocEdString("Time"), 40, "");
             timeField.Value = keyFrame.time;
-            timeField.OnChanged += x => { keyFrame.time = x; updateCallback(keyFrame); };
+            timeField.OnChanged += x => { keyFrame.time = x; changesMade = true; updateCallback(keyFrame); };
 
             GUIFloatField valueField = new GUIFloatField(new LocEdString("Value"), 40, "");
             valueField.Value = keyFrame.value;
-            valueField.OnChanged += x => { keyFrame.value = x; updateCallback(keyFrame); };
+            valueField.OnChanged += x => { keyFrame.value = x; changesMade = true; updateCallback(keyFrame); };
 
             GUILayoutY vertLayout = GUI.AddLayoutY();
 
@@ -1180,6 +1195,13 @@ namespace BansheeEditor
             componentLayout.AddFlexibleSpace();
             horzLayout.AddFlexibleSpace();
             vertLayout.AddFlexibleSpace();
+
+            this.closeCallback = closeCallback;
+        }
+
+        private void OnDestroy()
+        {
+            closeCallback?.Invoke(changesMade);
         }
     }
 
@@ -1189,6 +1211,9 @@ namespace BansheeEditor
     [DefaultSize(200, 80)]
     internal class EventEditWindow : DropDownWindow
     {
+        private Action<bool> closeCallback;
+        private bool changesMade;
+
         /// <summary>
         /// Initializes the drop down window by creating the necessary GUI. Must be called after construction and before
         /// use.
@@ -1196,7 +1221,9 @@ namespace BansheeEditor
         /// <param name="animEvent">Event whose properties to edit.</param>
         /// <param name="componentNames">List of component names that the user can select from.</param>
         /// <param name="updateCallback">Callback triggered when event values change.</param>
-        internal void Initialize(AnimationEvent animEvent, string[] componentNames, Action updateCallback)
+        /// <param name="closeCallback">Callback triggered just before the window closes.</param>
+        internal void Initialize(AnimationEvent animEvent, string[] componentNames, Action updateCallback, 
+            Action<bool> closeCallback)
         {
             int selectedIndex = -1;
             string methodName = "";
@@ -1221,7 +1248,7 @@ namespace BansheeEditor
 
             GUIFloatField timeField = new GUIFloatField(new LocEdString("Time"), 40, "");
             timeField.Value = animEvent.Time;
-            timeField.OnChanged += x => { animEvent.Time = x; updateCallback(); }; // TODO UNDOREDO  
+            timeField.OnChanged += x => { animEvent.Time = x; changesMade = true; updateCallback(); };
 
             GUIListBoxField componentField = new GUIListBoxField(componentNames, new LocEdString("Component"), 40);
             if (selectedIndex != -1)
@@ -1234,8 +1261,10 @@ namespace BansheeEditor
                     compName = componentNames[x] + "/";
 
                 animEvent.Name = compName + x;
+
+                changesMade = true;
                 updateCallback();
-            };// TODO UNDOREDO 
+            };
 
             GUITextField methodField = new GUITextField(new LocEdString("Method"), 40, false, "", GUIOption.FixedWidth(190));
             methodField.Value = methodName;
@@ -1246,8 +1275,10 @@ namespace BansheeEditor
                     compName = componentNames[componentField.Index] + "/";
 
                 animEvent.Name = compName + x;
+
+                changesMade = true;
                 updateCallback();
-            }; // TODO UNDOREDO 
+            };
 
             GUILayoutY vertLayout = GUI.AddLayoutY();
 
@@ -1269,6 +1300,13 @@ namespace BansheeEditor
             methodLayout.AddFlexibleSpace();
             horzLayout.AddFlexibleSpace();
             vertLayout.AddFlexibleSpace();
+
+            this.closeCallback = closeCallback;
+        }
+
+        private void OnDestroy()
+        {
+            closeCallback?.Invoke(changesMade);
         }
     }
 

+ 172 - 0
Source/MBansheeEditor/Windows/AnimationWindow.cs

@@ -767,6 +767,7 @@ namespace BansheeEditor
                 zoomAmount = 0.0f;
                 selectedFields.Clear();
                 clipInfo = null;
+                UndoRedo.Clear();
 
                 RebuildGUI();
 
@@ -787,6 +788,7 @@ namespace BansheeEditor
 
                 SwitchState(State.Normal);
 
+                currentClipState = CreateClipState();
                 if (selectedSO != null)
                 {
                     // Select first curve by default
@@ -833,6 +835,129 @@ namespace BansheeEditor
 
         #endregion
 
+        #region Undo/Redo
+
+        private AnimationClipState currentClipState;
+
+        /// <summary>
+        /// Records current clip state for undo/redo purposes.
+        /// </summary>
+        internal void RecordClipState()
+        {
+            AnimationClipState clipState = CreateClipState();
+
+            AnimationUndo undoCommand = new AnimationUndo(currentClipState, clipState);
+            UndoRedo.RegisterCommand(undoCommand);
+
+            currentClipState = clipState;
+        }
+
+        /// <summary>
+        /// Records current clip state for undo/redo purposes.
+        /// </summary>
+        internal AnimationClipState CreateClipState()
+        {
+            AnimationClipState clipState = new AnimationClipState();
+
+            clipState.events = new AnimationEvent[clipInfo.events.Length];
+            for (int i = 0; i < clipState.events.Length; i++)
+                clipState.events[i] = new AnimationEvent(clipInfo.events[i].Name, clipInfo.events[i].Time);
+
+            foreach (var curveField in clipInfo.curves)
+            {
+                AnimationCurveState[] curveData = new AnimationCurveState[curveField.Value.curveInfos.Length];
+                for (int i = 0; i < curveData.Length; i++)
+                {
+                    curveData[i] = new AnimationCurveState();
+
+                    TangentMode[] tangentModes = curveField.Value.curveInfos[i].curve.TangentModes;
+                    int numTangentModes = tangentModes.Length;
+                    curveData[i].tangentModes = new TangentMode[numTangentModes];
+                    Array.Copy(tangentModes, curveData[i].tangentModes, numTangentModes);
+
+                    KeyFrame[] keyFrames = curveField.Value.curveInfos[i].curve.KeyFrames;
+                    int numKeyframes = keyFrames.Length;
+                    curveData[i].keyFrames = new KeyFrame[numKeyframes];
+                    Array.Copy(keyFrames, curveData[i].keyFrames, numKeyframes);
+                }
+
+                clipState.curves[curveField.Key] = curveData;
+            }
+
+            return clipState;
+        }
+
+        /// <summary>
+        /// Updates the current animation fields from the keyframes and events in the provided state.
+        /// </summary>
+        /// <param name="animationClipState">Saved state of an animation clip.</param>
+        internal void ApplyClipState(AnimationClipState animationClipState)
+        {
+            if (state == State.Empty)
+                return;
+
+            SwitchState(State.Normal);
+
+            AnimationEvent[] events = animationClipState.events;
+            clipInfo.events = new AnimationEvent[events.Length];
+            for(int i = 0; i < events.Length; i++)
+                clipInfo.events[i] = new AnimationEvent(events[i].Name, events[i].Time);
+
+            foreach (var KVP in animationClipState.curves)
+            {
+                FieldAnimCurves fieldCurves;
+                if (!clipInfo.curves.TryGetValue(KVP.Key, out fieldCurves))
+                    continue;
+
+                for (int i = 0; i < fieldCurves.curveInfos.Length; i++)
+                {
+                    AnimationCurve curve = fieldCurves.curveInfos[i].curve.Normal;
+                    curve.KeyFrames = KVP.Value[i].keyFrames;
+
+                    fieldCurves.curveInfos[i].curve = new EdAnimationCurve(curve, KVP.Value[i].tangentModes);
+                }
+
+                clipInfo.curves[KVP.Key] = fieldCurves;
+            }
+
+            // Clear all keyframes from curves not in the stored state
+            foreach (var currentKVP in clipInfo.curves)
+            {
+                bool found = false;
+                foreach (var stateKVP in animationClipState.curves)
+                {
+                    if (currentKVP.Key == stateKVP.Key)
+                    {
+                        found = true;
+                        break;
+                    }
+                }
+
+                if (found)
+                    continue;
+
+                FieldAnimCurves fieldCurves = currentKVP.Value;
+                for (int i = 0; i < fieldCurves.curveInfos.Length; i++)
+                {
+                    AnimationCurve curve = currentKVP.Value.curveInfos[i].curve.Normal;
+                    curve.KeyFrames = new KeyFrame[0];
+
+                    fieldCurves.curveInfos[i].curve = new EdAnimationCurve(curve, new TangentMode[0]);
+                }
+            }
+
+            currentClipState = animationClipState;
+
+            UpdateDisplayedCurves();
+
+            ApplyClipChanges();
+            PreviewFrame(currentFrameIdx);
+
+            EditorApplication.SetProjectDirty();
+        }
+
+        #endregion
+
         #region Record/Playback
 
         /// <summary>
@@ -1835,5 +1960,52 @@ namespace BansheeEditor
         }
     }
 
+    /// <summary>
+    /// Raw data representing a single animation curve.
+    /// </summary>
+    class AnimationCurveState
+    {
+        public KeyFrame[] keyFrames;
+        public TangentMode[] tangentModes;
+    }
+
+    /// <summary>
+    /// Raw data representing a single animation clip state.
+    /// </summary>
+    class AnimationClipState
+    {
+        public Dictionary<string, AnimationCurveState[]> curves = new Dictionary<string, AnimationCurveState[]>();
+        public AnimationEvent[] events;
+    }
+
+    /// <summary>
+    /// Undo command used in the AnimationWindow.
+    /// </summary>
+    internal class AnimationUndo : UndoableCommand
+    {
+        private AnimationClipState prevClipState;
+        private AnimationClipState clipState;
+
+        public AnimationUndo(AnimationClipState prevClipState, AnimationClipState clipState)
+        {
+            this.prevClipState = prevClipState;
+            this.clipState = clipState;
+        }
+
+        /// <inheritdoc/>
+        protected override void Commit()
+        {
+            AnimationWindow window = EditorWindow.GetWindow<AnimationWindow>();
+            window?.ApplyClipState(clipState);
+        }
+
+        /// <inheritdoc/>
+        protected override void Revert()
+        {
+            AnimationWindow window = EditorWindow.GetWindow<AnimationWindow>();
+            window?.ApplyClipState(prevClipState);
+        }
+    }
+
     /** @} */
 }

+ 9 - 0
Source/SBansheeEditor/Include/BsManagedEditorCommand.h

@@ -77,6 +77,12 @@ namespace BansheeEngine
 
 		CmdManaged(ScriptCmdManaged* scriptObj);
 
+		/** @copydoc EditorCommand::commit */
+		void onCommandAdded() override;
+
+		/** @copydoc EditorCommand::commit */
+		void onCommandRemoved() override;
+
 		/** 
 		 * Notifies the command the managed script object instance it is referencing has been destroyed. Normally when this
 		 * happens the command should already be outside of the undo/redo stack, but we clear the instance just in case.
@@ -84,6 +90,9 @@ namespace BansheeEngine
 		void notifyScriptInstanceDestroyed();
 
 		ScriptCmdManaged* mScriptObj;
+		uint32_t mGCHandle;
+		UINT32 mRefCount;
+
 	};
 
 	/** @} */

+ 10 - 2
Source/SBansheeEditor/Include/BsScriptUndoRedo.h

@@ -23,22 +23,30 @@ namespace BansheeEngine
 		/** Creates a new managed UndoRedo stack. */
 		static MonoObject* create();
 
+		/**	Creates the globally accessible undo/redo stack. */
+		static void startUp();
+
+		/** Cleans up any data related to the global undo/redo stack. */
+		static void shutDown();
+
 	private:
 		ScriptUndoRedo(MonoObject* instance, const SPtr<UndoRedo>& undoRedo);
 
 		SPtr<UndoRedo> mUndoRedo;
-		static ScriptUndoRedo* sGlobalUndoRedo;
 
+		static ScriptUndoRedo* sGlobalUndoRedo;
+		static HEvent sDomainLoadConn;
+		
 		/************************************************************************/
 		/* 								CLR HOOKS						   		*/
 		/************************************************************************/
 		static void internal_CreateInstance(MonoObject* instance);
-		static MonoObject* internal_GetGlobal();
 		static void internal_Undo(ScriptUndoRedo* thisPtr);
 		static void internal_Redo(ScriptUndoRedo* thisPtr);
 		static void internal_RegisterCommand(ScriptUndoRedo* thisPtr, ScriptCmdManaged* command);
 		static void internal_PushGroup(ScriptUndoRedo* thisPtr, MonoString* name);
 		static void internal_PopGroup(ScriptUndoRedo* thisPtr, MonoString* name);
+		static void internal_Clear(ScriptUndoRedo* thisPtr);
 		static UINT32 internal_GetTopCommandId(ScriptUndoRedo* thisPtr);
 		static void internal_PopCommand(ScriptUndoRedo* thisPtr, UINT32 id);
 		static void internal_RecordSO(ScriptSceneObject* soPtr, bool recordHierarchy, MonoString* description);

+ 4 - 1
Source/SBansheeEditor/Source/BsEditorScriptManager.cpp

@@ -25,6 +25,7 @@
 #include "BsScriptInspectorUtility.h"
 #include "BsScriptEditorInput.h"
 #include "BsScriptEditorVirtualInput.h"
+#include "BsScriptUndoRedo.h"
 
 namespace BansheeEngine
 {
@@ -39,6 +40,7 @@ namespace BansheeEngine
 		loadMonoTypes();
 		ScriptAssemblyManager::instance().loadAssemblyInfo(EDITOR_ASSEMBLY);
 
+		ScriptUndoRedo::startUp();
 		ScriptEditorInput::startUp();
 		ScriptEditorVirtualInput::startUp();
 		ScriptEditorApplication::startUp();
@@ -84,6 +86,7 @@ namespace BansheeEngine
 		ScriptEditorApplication::shutDown();
 		ScriptEditorVirtualInput::shutDown();
 		ScriptEditorInput::shutDown();
+		ScriptUndoRedo::shutDown();
 	}
 
 	void EditorScriptManager::update()
@@ -134,4 +137,4 @@ namespace BansheeEngine
 		ScriptEditorWindow::clearRegisteredEditorWindow();
 		ScriptEditorWindow::registerManagedEditorWindows();
 	}
-}
+}

+ 21 - 2
Source/SBansheeEditor/Source/BsManagedEditorCommand.cpp

@@ -6,6 +6,7 @@
 #include "BsMonoClass.h"
 #include "BsMonoMethod.h"
 #include "BsMonoManager.h"
+#include "BsMonoUtil.h"
 
 namespace BansheeEngine
 {
@@ -62,7 +63,7 @@ namespace BansheeEngine
 	}
 
 	CmdManaged::CmdManaged(ScriptCmdManaged* scriptObj)
-		: EditorCommand(L""), mScriptObj(scriptObj)
+		: EditorCommand(L""), mScriptObj(scriptObj), mGCHandle(0), mRefCount(0)
 	{
 
 	}
@@ -103,8 +104,26 @@ namespace BansheeEngine
 		mScriptObj->triggerRevert();
 	}
 
+	void CmdManaged::onCommandAdded()
+	{
+		if (mGCHandle == 0 && mScriptObj != nullptr)
+			mGCHandle = MonoUtil::newGCHandle(mScriptObj->getManagedInstance());
+
+		mRefCount++;
+	}
+
+	void CmdManaged::onCommandRemoved()
+	{
+		assert(mRefCount > 0);
+
+		mRefCount--;
+
+		if (mRefCount == 0 && mGCHandle != 0)
+			MonoUtil::freeGCHandle(mGCHandle);
+	}
+
 	void CmdManaged::notifyScriptInstanceDestroyed()
 	{
 		mScriptObj = nullptr;
 	}
-}
+}

+ 36 - 15
Source/SBansheeEditor/Source/BsScriptUndoRedo.cpp

@@ -18,10 +18,12 @@
 #include "BsScriptPrefab.h"
 #include "BsManagedEditorCommand.h"
 #include "BsPrefab.h"
+#include "BsScriptObjectManager.h"
 
 namespace BansheeEngine
 {
 	ScriptUndoRedo* ScriptUndoRedo::sGlobalUndoRedo = nullptr;
+	HEvent ScriptUndoRedo::sDomainLoadConn;
 
 	ScriptUndoRedo::ScriptUndoRedo(MonoObject* instance, const SPtr<UndoRedo>& undoRedo)
 		:ScriptObject(instance), mUndoRedo(undoRedo)
@@ -30,12 +32,12 @@ namespace BansheeEngine
 	void ScriptUndoRedo::initRuntimeData()
 	{
 		metaData.scriptClass->addInternalCall("Internal_CreateInstance", &ScriptUndoRedo::internal_CreateInstance);
-		metaData.scriptClass->addInternalCall("Internal_GetGlobal", &ScriptUndoRedo::internal_GetGlobal);
 		metaData.scriptClass->addInternalCall("Internal_Undo", &ScriptUndoRedo::internal_Undo);
 		metaData.scriptClass->addInternalCall("Internal_Redo", &ScriptUndoRedo::internal_Redo);
 		metaData.scriptClass->addInternalCall("Internal_RegisterCommand", &ScriptUndoRedo::internal_RegisterCommand);
 		metaData.scriptClass->addInternalCall("Internal_PushGroup", &ScriptUndoRedo::internal_PushGroup);
 		metaData.scriptClass->addInternalCall("Internal_PopGroup", &ScriptUndoRedo::internal_PopGroup);
+		metaData.scriptClass->addInternalCall("Internal_Clear", &ScriptUndoRedo::internal_Clear);
 		metaData.scriptClass->addInternalCall("Internal_GetTopCommandId", &ScriptUndoRedo::internal_GetTopCommandId);
 		metaData.scriptClass->addInternalCall("Internal_PopCommand", &ScriptUndoRedo::internal_PopCommand);
 		metaData.scriptClass->addInternalCall("Internal_RecordSO", &ScriptUndoRedo::internal_RecordSO);
@@ -49,6 +51,33 @@ namespace BansheeEngine
 		metaData.scriptClass->addInternalCall("Internal_BreakPrefab", &ScriptUndoRedo::internal_BreakPrefab);
 	}
 
+	void ScriptUndoRedo::startUp()
+	{
+		auto createPanel = []()
+		{
+			assert(sGlobalUndoRedo == nullptr);
+
+			bool dummy = false;
+			void* ctorParams[1] = { &dummy };
+
+			MonoObject* instance = metaData.scriptClass->createInstance("bool", ctorParams);
+			new (bs_alloc<ScriptUndoRedo>()) ScriptUndoRedo(instance, nullptr);
+
+			MonoMethod* setGlobal = metaData.scriptClass->getMethod("Internal_SetGlobal", 1);
+			void* setGlobalParams[1] = { instance };
+			setGlobal->invoke(nullptr, setGlobalParams);
+		};
+
+		sDomainLoadConn = ScriptObjectManager::instance().onRefreshDomainLoaded.connect(createPanel);
+
+		createPanel();
+	}
+
+	void ScriptUndoRedo::shutDown()
+	{
+		sDomainLoadConn.disconnect();
+	}
+
 	MonoObject* ScriptUndoRedo::create()
 	{
 		bool dummy = false;
@@ -68,20 +97,6 @@ namespace BansheeEngine
 		new (bs_alloc<ScriptUndoRedo>()) ScriptUndoRedo(instance, undoRedo);
 	}
 
-	MonoObject* ScriptUndoRedo::internal_GetGlobal()
-	{
-		if(sGlobalUndoRedo == nullptr)
-		{
-			bool dummy = false;
-			void* params[1] = { &dummy };
-
-			MonoObject* instance = metaData.scriptClass->createInstance("bool", params);
-			sGlobalUndoRedo = new (bs_alloc<ScriptUndoRedo>()) ScriptUndoRedo(instance, nullptr);
-		}
-
-		return sGlobalUndoRedo->getManagedInstance();
-	}
-
 	void ScriptUndoRedo::internal_Undo(ScriptUndoRedo* thisPtr)
 	{
 		UndoRedo* undoRedo = thisPtr->mUndoRedo != nullptr ? thisPtr->mUndoRedo.get() : UndoRedo::instancePtr();
@@ -116,6 +131,12 @@ namespace BansheeEngine
 		undoRedo->popGroup(nativeName);
 	}
 
+	void ScriptUndoRedo::internal_Clear(ScriptUndoRedo* thisPtr)
+	{
+		UndoRedo* undoRedo = thisPtr->mUndoRedo != nullptr ? thisPtr->mUndoRedo.get() : UndoRedo::instancePtr();
+		return undoRedo->clear();
+	}
+
 	UINT32 ScriptUndoRedo::internal_GetTopCommandId(ScriptUndoRedo* thisPtr)
 	{
 		UndoRedo* undoRedo = thisPtr->mUndoRedo != nullptr ? thisPtr->mUndoRedo.get() : UndoRedo::instancePtr();