- Edited
I'm seeing people getting severely confused with engine's undo functionality in conjunction with tool scripts, so here's a mini tutorial that'll hopefully clarify how things work. [I'm looking at you @jonSS]
Engine automatically adds every property change done via inspector to scene's undo queue. However, with custom tool scripts the user can change a property value via code. This change won't automatically be added to the undo queue. This is understandable because doing otherwise would severely impede tool script performance. Many changes of property values may happen in a typical tool script, but only a fraction of those would be candidates for undo. That's why engine leaves to the user to decide which property changes should be committed to the undo queue. This is done via EditorUndoRedoManager
interface.
Let's look how an undo can be implemented for a simple int property when changed via script triggered by some user input:
@tool
extends Node
@export var foo: int = 1
func _process(delta):
if Input.is_action_just_pressed("ui_up"):
# get engine's EditorUndoRedoManager via dummy EditorPlugin node
var dummy_ep = EditorPlugin.new()
var undoredo = dummy_ep.get_undo_redo()
dummy_ep.free() # delete the dummy node
# create undo/redo action
undoredo.create_action("foo increment by 1")
undoredo.add_undo_property(self, "foo", foo) # property value before action
undoredo.add_do_property(self, "foo", foo+1) # property value after action
undoredo.commit_action() # execute action, the action is now in scene's undo/redo queue
When user input is received, we get the EditorUndoRedoManager
object and then create an action that will represent a step in the undo/redo queue. To keep the example self-sufficient the EditorUndoRedoManager
is acquired from a dummy EditorPlugin
node. This doesn't matter since manager is a singleton.
The action represents a step in the undo queue. We need to provide property values before and after the action so the undo/redo system knows which values to set the property to. That's pretty much the whole wisdom for properties that are passed by values.
For properties that are passed by reference such as objects, arrays or dictionaries, we cannot supply before/after values as that would require creating the object's duplicate. However, there's an additional mechanism provided by EditorUndoRedoManager
. Instead of passing before/after values, we supply methods that will be called when action is undoed/redoed. In those methods we can adjust the state of the object accordingly:
@tool
extends Node
@export var foo: Dictionary = Dictionary()
func _process(delta):
if Input.is_action_just_pressed("ui_up"):
var dummy_ep = EditorPlugin.new()
var undoredo = dummy_ep.get_undo_redo()
dummy_ep.free()
undoredo.create_action("add element to foo")
var i = randi()
undoredo.add_undo_method(self, "remove_element", i)
undoredo.add_do_method(self, "add_element", i)
undoredo.commit_action()
func add_element(i):
foo.merge({i:0})
notify_property_list_changed()
func remove_element(i):
foo.erase(i)
notify_property_list_changed()
Note that commit_action()
will automatically execute our 'do' method to update the value.