How to correctly register Undos in Custom Inspector (within OnInspectorGUI)?

Hello,

I'm trying to correctly register undos in a custom inspector, and it doesn't look that simple.

How it should be used (C#)

In theory, it should be simple:

Undo.RegisterUndo( (MyClass) target, "Undo name" );

When you call RegisterUndo, all the properties of the given object are registered, so that they can be succesfully undone.

The problem

RegisterUndo should be called immediately BEFORE a property is going to be changed. But how can you know it? Also, if for example you have an IntField, and you drag its value, RegisterUndo should be called only before starting the drag, and not each time the value changes. How do you achieve this?

Possible (non-working) solutions I investigated

  • I tried to register undos when a user presses the left mouse button on a GUI control (using GUIUtility.hotControl), but hotControl changes even if you press other Inspectors of the same object (plus, there's no way to determine if you actually changed something after clicking on it - apart using tempVars for each GUI control, which looks terribly verbose)
  • Using tempVars for each GUI control. But, apart from it being terribly verbose, dragging an IntField (or else) value breaks it.

The C# solution (thanks to mr. Statement)

EDIT: I created a helper class and placed it on Unify. See the “correct answer” for a link.

Working on mr. Statement answer, I found the correct solution. This solution registers all undos correctly, and prevents registering multiple or useless undos (checking if, after the left mouse button was pressed, something actually changed). Also, it works even if you switch field using the TAB key.

Here is an example Class, and its relative custom Inspector.

Sample Class

using UnityEngine;
using System.Collections;

public class HOEXInspectorUndo : MonoBehaviour
{
    public      enum SampleEnum 
    {
        Enum_A,
        Enum_B,
        Enum_C,
        Enum_D
    }
    public      int     sampleInt = 0;
    public      int     sampleInt2 = 0;
    public      float       sampleFloat = 10.52f;
    public      bool        sampleBool = true;
    public      SampleEnum  sampleEnum = SampleEnum.Enum_A; 
}

Sample Class's custom Inspector

using UnityEditor;
using UnityEngine;
using System.Collections;

[CustomEditor( typeof( HOEXInspectorUndo ) )]
public class HOEXInspectorUndoEditor : Editor
{
    private     HOEXInspectorUndo       src;
    private     bool                listeningForGuiChanges;
    private     bool                guiChanged;

    private void OnEnable()
    {
        src = target as HOEXInspectorUndo;
    }

    override public void OnInspectorGUI()
    {
        CheckUndo();

        src.sampleInt = EditorGUILayout.IntField( "Sample Int", src.sampleInt );
        src.sampleInt2 = EditorGUILayout.IntSlider( "Sample Slider", src.sampleInt2, 0, 100 );
        src.sampleFloat = EditorGUILayout.FloatField( "Sample Float", src.sampleFloat );
        if ( GUILayout.Button( "Set Sample Float to 17.2" ) ) {
            guiChanged = true;
            src.sampleFloat = 17.2f;
        }
        src.sampleBool = EditorGUILayout.Toggle( "Sample Bool", src.sampleBool );
        src.sampleEnum = ( HOEXInspectorUndo.SampleEnum ) EditorGUILayout.EnumPopup( "Sample Enum", src.sampleEnum );

        if ( GUI.changed ) {
            guiChanged = true;
        }
    }

    private void CheckUndo()
    {
        Event e = Event.current;

        if ( e.type == EventType.MouseDown && e.button == 0 || e.type == EventType.KeyUp && ( e.keyCode == KeyCode.Tab ) ) {
            // When the LMB is pressed or the TAB key is released,
            // store a snapshot, but don't register it as an undo
            // ( so that if nothing changes we avoid storing a useless undo)
            Debug.Log( "PREPARE UNDO SNAPSHOT" );
            Undo.SetSnapshotTarget( src, "HOEXInspectorUndo" );
            Undo.CreateSnapshot();
            Undo.ClearSnapshotTarget();
            listeningForGuiChanges = true;
            guiChanged = false;
        }

        if ( listeningForGuiChanges && guiChanged ) {
            // Some GUI value changed after pressing the mouse.
            // Register the previous snapshot as a valid undo.
            Debug.Log( "REGISTER UNDO" );
            Undo.SetSnapshotTarget( src, "HOEXInspectorUndo" );
            Undo.RegisterSnapshot();
            Undo.ClearSnapshotTarget();
            listeningForGuiChanges = false;
        }
    }
}

RegisterUndo should be called immediately BEFORE a property is going to be changed. But how can you know it?

It sounds like you aren't responding to button clicks etc, but poll some continous value. Otherwise it would be pretty trivial:

if (GUILayout.Button("Do something")) {
    Undo.RegisterUndo(obj, "Do Something " + obj.name);
    obj.position = Vector3.zero;
}

Also, if for example you have an IntField, and you drag its value, RegisterUndo should be called only before starting the drag, and not each time the value changes. How do you achieve this?

From the reference:

Certain operations, such as dragging, consist of many small incremental changes. Typically it is not desired to create an undo step for each of these small changes. For example, if the user performs an undo after a dragging operation, it is expected that the object is reverted back to the state it had before the dragging started. The functions SetSnapshotTarget, CreateSnapshot, and RegisterSnapshot are available to handle cases like this.


I suggest you Check out this similar question with a slider control.

Example snippet from question:

using UnityEditor;
using UnityEngine;

public class Example : EditorWindow {

    [SerializeField] float sliderValue = 1f;

    [MenuItem("Example/Slider Test")]
    static void Init() {
        var myWindow = (Example)EditorWindow.GetWindow(typeof(Example));
        myWindow.autoRepaintOnSceneChange = true;
    }

    void OnGUI() {
        checkMouse();
        GUILayout.BeginVertical("box");
        GUILayout.Label("Test Slider");
        sliderValue = EditorGUILayout.Slider(sliderValue, .1f, 5f);
        GUILayout.EndVertical();
        Undo.ClearSnapshotTarget();
        this.Repaint();
    }

    void checkMouse() {
        Event e = Event.current;
        if (e.button == 0 && e.isMouse) {
            Undo.SetSnapshotTarget(this, "Changed Slider");
            Undo.CreateSnapshot();
            Undo.RegisterSnapshot();
        }
    }
}

Dear Daniele, I downloaded your InspectorUndo.zip and manage to solve your problem!

You only need to change one line of code...

  • Undo.SetSnapshotTarget(this, "HOEXInspectorUndo");

    to …

  • Undo.SetSnapshotTarget(src, "HOEXInspectorUndo");

... since you want to save the values of your target, not the values of your inspector. Below is the full editor code for completeness.

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(HOEXInspectorUndo))]
public class HOEXInspectorUndoEditor : Editor
{
    private HOEXInspectorUndo src;

    private void OnEnable()
    {
        src = target as HOEXInspectorUndo;
        EditorApplication.modifierKeysChanged += this.Repaint;
    }

    private void OnDisable()
    {
        EditorApplication.modifierKeysChanged -= this.Repaint;
    }

    override public void OnInspectorGUI()
    {
        Undo.SetSnapshotTarget(src, "HOEXInspectorUndo");
        src.sampleInt = EditorGUILayout.IntField("Sample Int", src.sampleInt);
        if (GUI.changed)
        {
            Undo.CreateSnapshot();
            Undo.RegisterSnapshot();
        }
        Undo.ClearSnapshotTarget();
    }
}

This is what SerializedObject and SerializedProperty are supposed to make easier. I have not tested the following, but it should work :slight_smile:

using UnityEngine;

[CustomEditor(typeof(HOEXInspectorUndo))]
public class HOEXInspectorUndoEditor : Editor {

    private HOEXInspectorUndo src;

    private SerializedProperty _prop_sampleInt;
    private SerializedProperty _prop_sampleInt2;
    private SerializedProperty _prop_sampleFloat;
    private SerializedProperty _prop_sampleBool;
    private SerializedProperty _prop_sampleEnum;
    
    private void OnEnable() {
        src = target as HOEXInspectorUndo;
        
        _prop_sampleInt = serializedObject.FindProperty("sampleInt");
        _prop_sampleInt2 = serializedObject.FindProperty("sampleInt2");
        _prop_sampleFloat = serializedObject.FindProperty("sampleFloat");
        _prop_sampleBool = serializedObject.FindProperty("sampleBool");
        _prop_sampleEnum = serializedObject.FindProperty("sampleEnum");
    }

    override public void OnInspectorGUI() {
        serializedObject.Update();

        _prop_sampleInt.intValue = EditorGUILayout.IntField("Sample Int", src.sampleInt);
        _prop_sampleInt2.intValue = EditorGUILayout.IntSlider("Sample Slider", src.sampleInt2, 0, 100);
        
        _prop_sampleFloat.floatValue = EditorGUILayout.FloatField("Sample Float", src.sampleFloat);
        if (GUILayout.Button("Set Sample Float to 17.2")) {
            _prop_sampleFloat.floatValue = 17.2f;
            EditorGUIUtility.ExitGUI();
        }
        _prop_sampleBool.boolValue = EditorGUILayout.Toggle( "Sample Bool", src.sampleBool );
        _prop_sampleEnum.enumValueIndex = (int)(HOEXInspectorUndo.SampleEnum)EditorGUILayout.EnumPopup("Sample Enum", src.sampleEnum);

        if (GUI.changed)
            serializedObject.ApplyModifiedProperties();
    }

}

For further information see:

Complete class to correctly manage undo

I posted it on Unify. You can find it here.