How can I use Undo for an EditorWindow?

So I have made a few custom inspectors before and used the Undo system but how do you use it for an EditorWindow when there are no objects to pass to Undo.RecordObject(), I just want to be able to undo changes to local variables.
Here is what I have

namespace RapidIconUIC
{
    public class RapidIconWindow : EditorWindow
    {
        static RapidIconWindow window;

        public AssetList assetList;
        public DraggableSeparator leftSeparator, rightSeparator;
        public AssetGrid assetGrid;
        public IconEditor iconEditor;

        [MenuItem("Tools/RapidIcon")]
        static void Init()
        {
            window = (RapidIconWindow)GetWindow(typeof(RapidIconWindow), false, "RapidIcon");
            window.minSize = new Vector2(870, 600);
            window.Show();
        }

        private void OnEnable()
        {
            //A load of init stuff, including this:
            iconEditor = new IconEditor(assetGrid, window);
            iconEditor.LoadData();
        }

        private void OnDisable()
        {
            //Saves some variables to EditorPrefs
            iconEditor.SaveData();
        }

        void OnGUI()
        {
            if (!window)
                Init();

            //A load of stuff including:
            iconEditor.Draw(window.position.width - rightSeparator.value, window);
        }
    }
}

Then in the IconEditor class (doesn’t derive from anything), I have a bunch of variables that are changed with GUI controls e.g.

void DrawTransformControls()
        {
            EditorGUI.BeginChangeCheck();
            Vector3 camPos = EditorGUILayout.Vector3Field("Camera Position", currentIcon.cameraPosition);
            //And some others
            if (EditorGUI.EndChangeCheck())
            {
                Undo.RecordObject(this, "IconEditor Undo"); //Can't do this as IconEditor is not an Object
                currentIcon.cameraPosition = camPos; //bad example as I'm changing another object here, but sometimes I do just change a local variable to the IconEditor class
                //currentIcon also cannot be converted to an Object either anyway
                UpdateIcon(currentIcon);
            }
        }

So how can I add undo functionality in this script, and others like it

Thanks :slight_smile:

1 Like

You can mark IconEditor as Serializable, and then pass the RapidIconWindow to the IconEditor to record that.

When doing Undo/Serialization in an Editor Window, it’s the editor window itself you’re serializing, as it’s a ScriptableObject that Unity does some magic to. Exactly the rules for what serializes and why is undocumented and a bit esoteric, but for the most part it works as usual.

Thanks, I will give that a go :slight_smile:

That worked, thanks a lot :smile:

Hello, Sir, it’s been a year but I hope you can explain a little bit more about “passing the RapidIconWindow to the IconEditor”
I have a trouble in trying to implement this Undo system. My code looks like this inside the Editor:

[System.Serializable]
[CustomEditor(typeof(ChatAsset))]
public class Editor_ChatAsset : Editor
{
    public override void OnInspectorGUI()
    {
        if (GUILayout.Button("Open Window"))
        {
            Window_ChatAsset.Open((ChatAsset)target);
        }
        EditorGUILayout.Space();
        base.OnInspectorGUI();
    }
}

If you’re wondering what Window_ChatAsset.Open() does:

    public static void Open(ChatAsset chatAsset)
    {
        Window_ChatAsset window = GetWindow<Window_ChatAsset>("Chat Asset Editor");
        window.serializedObject = new SerializedObject(chatAsset);
    }

I don’t really understand how Undo works, and I have read many articles and threads about. Nothing works, or to put it more precise, I don’t know how to make it work. Please help…

Is the problem not resolved yet? Try this.

[Serializable]
    public class TestWindow : EditorWindow
    {
        [SerializeField] private float test = 0;
        private static TestWindow testWindow;
        private static SerializedObject serializedTestWindow;



        [MenuItem("Test/OpenTestWindow")]
        private static void OpenTestWindow()
        {
            testWindow = GetWindow<TestWindow>(false);
            serializedTestWindow = new SerializedObject(testWindow);
        }
        private void OnGUI()
        {
            Undo.RecordObject(serializedTestWindow.targetObject, "test");
            test = EditorGUILayout.FloatField(nameof(test), test);
        }
    }

Open window and change test property. And try undo.

1 Like

The one thing you want to do is to call _serializedWindow.Update(); (serializedTestWindow in previous answer) at the beginning of your OnGUI method, to make variables… “update”.

Are you surprised that this isn’t mentioned in the Undo documentation page? Be honest, literally anyone would have gone straight to the SerializedObject doc when trying to understand why it does nothing when its variables are changed by an undo… wait… “its” variables?.. Anyway.

You don’t even need the RecordObject call, you only need it if you subscribe to the undo-redo event to check the undoName of a undo (or a redo, it’s not like “designation” or “source” were available), to know if it’s about this window or something else.

	public class TestWindow: EditorWindow
	{
		// --- Serialized variables
		[SerializeField]
		private float _foo = default;


		// --- Working variables
		private SerializedObject _serializedWindow;
		private SerializedProperty _fooProperty;
		

		// --- Lifecycle methods
		[MenuItem("Foo/Bar")]
		private static void ShowWindow()
		{
			var window = GetWindow<TestWindow>();
			window.titleContent = new GUIContent("Bar");

			// Not strictly required, just to show it can be set from here too.
			// But this method will only be called once when opening the window, unlike `OnEnable`.
			window._serializedWindow = new SerializedObject(this);

			window.Show();
		}

		private void OnEnable()
		{
			_serializedWindow = new SerializedObject(this);
			_fooProperty = _serializedWindow.FindProperty(nameof(_foo));

			Undo.undoRedoEvent -= OnUndoRedoEvent;
			Undo.undoRedoEvent += OnUndoRedoEvent;
		}

		private void OnDisable()
		{
			Undo.undoRedoEvent -= OnUndoRedoEvent;
		}

		private void OnGUI()
		{
			_serializedWindow.Update();
			Undo.RecordObject(_serializedWindow.targetObject, nameof(TestWindow));
			// OR
			// if (_serializedWindow.UpdateIfRequiredOrScript())
			// 	Undo.RecordObject(_serializedWindow.targetObject, nameof(TestWindow));

			EditorGUILayout.PropertyField(_fooProperty);
		}


		// --- Event methods
		private void OnUndoRedoEvent(in UndoRedoInfo info)
		{
			if (info.undoName != nameof(TestWindow))
				return;

			// Re-start operation here if the window automatically does something when variables change.
			// Otherwise, all this event handling and `RecordObject` are simply not needed.

			// Repaint();
		}
	}

Sorry for the rant, after 15 years of this all over the place, patience ran out long ago.