- Home /
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;
}
}
}
I haven't had time to look at this yet; this looks like what I tried and had no success with, and it looks like you've already been through this. I will try again later. http://answers.unity3d.com/questions/44552
Yep, I had seen your post but I was looking for better solutions :) If you find anything please let me know, and thanks :)
Hey, I'm having the same problem too. It baffles me how something basic such as this is not even documented. How is one supposed to even make good custom tools for unity editor if this isn't even possible? $$anonymous$$y artists are going to kill me for this disability. lol
Yup, same for me: the artist working with me was not very happy when I told him "no undo for this and that"! :P
The first ClearSnapshotTarget() allows for multiple changes to the same control to be undone. What is the point of the later one?
Answer by Demigiant · Nov 27, 2011 at 06:09 PM
Complete class to correctly manage undo
posted it on Unify. You can find it here.
Link only answers are bad. Now there is a 404 at the link.
I've fixed the wiki link. However the wiki seems to be currently down. I've found this class which seems to be based on the script posted on the wiki (according to the comment at the top)
Ah, that is indeed my old script, though it's very old now and newer Unity versions work differently and with easier undo logic :P
Answer by Statement · Apr 02, 2011 at 04:20 PM
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?
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();
}
}
}
First of all thanks for the detailed answer, Statement :) I had totally misunderstood the "setSnapshotTarget" concept. I'll check with your way and see how it goes. About the "Undo.RegisterUndo" thing: I have almost no buttons in my inspectors. Ins$$anonymous$$d, I have many toggles, fields, and such.
Your method doesn't seem to work with an Editor ins$$anonymous$$d than an EditorWindow (thus placed within OnInspectorGUI). I built a simple example, and you can find it here: http://www.holoville.com/appo/InspectorUndo.zip - any chance you might take a look at it and see if I'm doing something stupid? I also tried to use the "Check$$anonymous$$ouse" method from the above snippet, but that doesn't work either.
Sorry, miswritten link. The correct example is here: http://www.holoville.com/appo/_keep/InspectorUndo.zip
See my new answer dedicated for your current code base.
Answer by Statement · Apr 03, 2011 at 10:53 AM
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();
}
}
Ooops, now I feel really dumb L Thanks Statement, that was a stupid mistake on my side. Now it works almost perfectly :) Only issue is: setting Undo.CreateSnapshot on GUI.changed stores the snapshot the first time the gui changes. Thus, if you drag the IntField slider to the right, undoing goes back to a slightly higher value than the initial one. Dragging left ins$$anonymous$$d undoes to a lower value. Anyway, now that I understood the concept I'll work on it some more, and try to find the correct way to register the snapshot - and then grant you the deserved 100 rep :)
If anyone needs the full solution, look at my answer. I edited it adding a sample Class and its Inspector. Statement's solution doesn't work correctly, but he was very detailed and greatly helped me to understand the SetSnapshotTarget concept and how undo works. Thus I'm marking his answer as the correct one, so he can grab the bounty :) Thanks again Statement :)
You could always write an answer and accept it after 24h.
You're a true gentleman ;) Anyway, it's too late: you got bounty :D
Hey Statement, I made a full class and set it as the correct answer. I hope that after all this time the points stayed with you :)
Answer by numberkruncher · Jul 04, 2012 at 02:50 PM
This is what SerializedObject
and SerializedProperty
are supposed to make easier. I have not tested the following, but it should work :-)
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: http://docs.unity3d.com/Documentation/ScriptReference/Editor.html
Your answer
Follow this Question
Related Questions
[Editor Scripting] Call Function in other GameObjects Editor Script? 0 Answers
Custom Editor for Monobehavior with custom property not retaining Gameobject references 0 Answers
Responsive Editor UI Button with custom style | How to remove GUIStyle.hover delay 0 Answers
OnInspectorGUI changes reset when played in editor or building 2 Answers