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.

    xyz thanks 🙂 ( you knew how to do it after all )

    at frist i couldnt understand it... i mean shouldnt he get "undo" value before then the "rendo" after ?

    1. undo - ( get the value ) when mouse button is just pressed
    2. rendo - ( get the value ) when mouse button have been just released

    for some reason its working... both "undo" and "rendo" only work on 1. - mouse button is just pressed

      jonSS ( turn off adblocker, or the download button wont work )

      never turn off adblocker!

        kuligs2 never turn off adblocker!

        I never do.

        jonSS thanks 🙂 ( you knew how to do it after all )

        Why would you think otherwise? Your question (most of them in fact) was really unclear because you tend to jump to conclusions and operate under strange (often wrong) presuppositions. So I had to decipher through your noise what's really bothering you. Please work on clarity of your communication. Also try to take one problem at a time. You cram all this half-broken stuff into your project and then get confused when multiple things at the same time won't work as you think they should. It's a mess that requires extra effort to untangle. Be aware of this when asking questions. Always try to fully isolate an issue, best into a fresh unencumbered project. It facilitates clarity and understanding. Help us help you 😃

        add_undo_method() and add_do_method() as well as add_undo_property() and add_do_property() don't actually do anything noticeable when called. They just register callbacks or values respectively. So the order in which you call them makes no difference. The only important thing is that they get called between create_action() and commit_action() which form a begin/end block for the rest of the calls that define action's behavior.

        Only when you finally call commit_action(), the action gets inserted into scene's undo queue. At this point the method(s) registered via add_do_method() will be called and the value(s) specified by add_do_property() will be assigned. From that moment on, do/undo values and do/undo methods you supplied to the action will be set and/or called whenever it's your action's turn to be undone or redone.

        xyz It always throws an error on "var dummy_ep = EditorPlugin.new();", when the scene is run ?

        the "Engine.is_editor_hint()" doesnt seem to be working in this case:

        • xyz replied to this.

          This has nothing to do with undo. The acquirement of the EditorUndoRedoManager in my example is a bit hacky (as I pointed out above) just to keep the example self contained, or should I say it; minimal 🙂 The code is NOT a generalized boilerplate to copypaste into your projects.

          In a real project you'd obtain the manager from the actual plugin node, so no abstract classes would need to be instantiated.

          jonSS As a quick hack you can pluck the manager from one of the built in plugins currently present in the editor tree. This too shouldn't be used in a real project, only when playing with the stuff to learn how it works. This now shouldn't confuse the runtime compiler:

          if Engine.is_editor_hint():
          	undoredo = get_tree().get_root().find_children("*", "EditorPlugin", true, false)[0].get_undo_redo()

            xyz thanks,
            what about the "EditorPlugin" script ?
            Ive made it using "add_custom_type". But the only way i know of on how to get the @tool node is in the handle(Object)function

            Couldnt there be way to to get the node "add_custom_type" in the enter tree ? And pass the undoRendo there ?
            I mean... Iam not doing anything in the "EditorPlugin", all the code is being done in the @tool node ?

            • xyz replied to this.

              jonSS Yes, the most convenient way is to inject the manager reference into the edited object in EditorPlugin::_handles(). Or declare a static reference in the edited object class and assign it once from EditorPlugin::_enter_tree()

              Alternatively you can name the plugin node and then each edited node can get the manager from it using something similar to the "hack" I posted above.

                xyz great thanks
                ill leave it has it is then in the _handles() function

                there is another problem...
                it seems the undo rendo is lost or doesnt change places when using diferent nodes

                If i use undo rendo on an "EditorInspectorPlugin" scene,
                it goes to the undo rendo on the other side ?

                inspector.gd

                #----//--ADD-itemList-layer-//----
                func _on_btn_add_layer_pressed():
                	var listSize = itemList.get_item_count();
                	var itemName = "layer " + str(listSize)
                	#-----------------undo-rendo-add-layer------------------------------
                	undoredo.create_action("tileDraw - add layer");
                	undoredo.add_do_method(self, "_do_add_layer", itemName, listSize);
                	undoredo.add_undo_method(self, "_undo_remove_layer", listSize);
                	undoredo.commit_action();
                	#-----------------------------------------------------------
                
                #---------//--UNDO-RENDO-ADD-LAYER-//--------------
                func _do_add_layer(layer_name: String, index: int):
                	itemList.add_item(layer_name)
                	PlugTileDrawNode2.layerName.append(layer_name);
                	PlugTileDrawNode2.layers.Lnum.append({});
                	PlugTileDrawNode2.layers.Lnum.resize(index + 1);
                	notify_property_list_changed()
                	
                
                func _undo_remove_layer(index: int):
                	if index >= 0 and index < itemList.get_item_count():
                		itemList.select(index-1, true);
                		_on_item_list_multi_selected(index-1, true);
                		itemList.remove_item(index)
                		PlugTileDrawNode2.layerName.remove_at(index)
                		PlugTileDrawNode2.layers.Lnum.remove_at(index)
                	
                	notify_property_list_changed()
                	pass;

                This happens when draw on layer / add layer / draw on layer / add layer...etc... the undo_rendo always goes into the tileDraw script

                • xyz replied to this.

                  jonSS Editor undo queue is scene specific. Each edited scene has its own queue. That's why undo/redo commands are in the Scene menu 🙂. Read the docs for EditorUndoRedoManager class.

                  If you want a node specific or otherwise independent system, use UndoRedo object. However this will not be tied with the editor undo queue and you'll have to add your own gui for issuing undo commands. This is likely not something you actually need here.

                  Btw it's re-do, not re-ndo.

                    xyz i dont know...
                    this is the amount of code i have to do, just to merge ( the selected layers ) together into 1...
                    it uses previous functions on item select, and previous vars... and runs remove layers at the end.

                    I dont think the undo redo is supossed to fit in here ?
                    wouldnt it just be simpler to just save everything inside a var, tileDraw.gd... inspector.gd, and use the undo redo to restore everything ?

                    I just dont think the way of how it is, the undo rendo is supossed to deal with this ?

                    • xyz replied to this.

                      jonSS Again, it has nothing to do with undo system per se. This thread is meant as a tutorial on how it actually works.
                      In your case your whole data architecture is likely not properly done. That's why you have problems conforming the whole thing to engine's undo system. So if you want/need to use that system, reorganize your plugin to play nice with it.

                      From what I've seen so far in your code, you're not fully understanding how arrays/dictionaries work. Best to first entrench your knowledge about that in a simpler project. This is fundamental, so cursory understanding is not enough if you wish to make something more ambitious like a plugin.

                      Btw just posting random excerpts of code form your project is again not a very good communication of the problem. For umpteenth time, isolate the problem into a fresh project, make a new thread about it.

                      jonSS The thing with undo/redo for a complex object/action is that you need to have a function that does the operation and a function that undoes the operation, both changing the state of the object. How exactly to do this is closely knit with your data architecture. You typically need to store only necessary parameters to (un)perform the action. That's precisely what Godot's UndoRedo class helps you with. This is how undo is implemented in almost every app, it's not a Godot specific thing.

                      To conclude, here's a bit more elaborate example that implements undo in a plugin that draws grided circles on a canvas item node. It's somewhat similar to your situation @jonSS only simpler, for clarity. the action is implemented in a proper way so the undo works between any number of drawable nodes in the scene. I won't go into any explanations as I think the code is self explanatory enough. If not, just study it harder 😃

                      @tool
                      class_name Drawable extends CanvasItem
                      
                      @export var color = Color.YELLOW
                      var draw_list = {}
                      
                      func _draw():
                      	for pos in draw_list.keys():
                      		draw_circle(pos, 32, color, true)
                      		
                      func _get_property_list():
                      	return [{"name": "draw_list", "type" : TYPE_DICTIONARY, "usage": PROPERTY_USAGE_STORAGE}] 
                      @tool
                      extends EditorPlugin
                      
                      var drawable = null # current selected node we draw on
                      var stroke_draw_list = {} # current stroke draw list, used for undo
                      
                      func _handles(object):
                      	drawable = object if object is Drawable else null
                      	return object is Drawable
                      
                      func _forward_canvas_gui_input(e):
                      	if not drawable or e is InputEventKey: 
                      		return false
                      	if e is InputEventMouseMotion and e.button_mask == MOUSE_BUTTON_MASK_LEFT:
                      		add_to_current_stroke()
                      	if e is InputEventMouseButton and e.button_index == MOUSE_BUTTON_LEFT and not e.is_pressed():
                      		end_stroke()
                      	return true
                      		
                      func add_to_current_stroke():
                      	var position = snapped(Vector2i(drawable.get_local_mouse_position()), Vector2i(64, 64))
                      	if not drawable.draw_list.has(position):
                      		drawable.draw_list[position] = 1 # some dummy value, when drawing textures can be a texture reference
                      		stroke_draw_list[position] = 1
                      		drawable.queue_redraw()
                      
                      func end_stroke():
                      	remove_from_draw_list(drawable, stroke_draw_list)
                      	var undoredo = get_undo_redo()
                      	undoredo.create_action("draw")
                      	undoredo.add_do_method(self, "add_to_draw_list", drawable, stroke_draw_list.duplicate())
                      	undoredo.add_undo_method(self, "remove_from_draw_list", drawable, stroke_draw_list.duplicate())
                      	undoredo.commit_action()
                      	stroke_draw_list.clear()
                      	
                      func add_to_draw_list(object, stroke):
                      	object.draw_list.merge(stroke)
                      	object.queue_redraw()
                      	
                      func remove_from_draw_list(object, stroke):
                      	for pos in stroke.keys():
                      		object.draw_list.erase(pos)
                      	object.queue_redraw()

                      Did you add all the code ?
                      For me the undo redo doesnt work, changing the color replaces the color of all circles on screen
                      I also had to use "class_name Drawable extends Node2D" instead of "CanvasItem" or i cant get node to show in the list

                      • xyz replied to this.

                        jonSS That's all the code. I never said the class will appear on the list of nodes. It won't because it inherits an abstract class. You can just attach the Drawable script to any existing canvas item node and it will work. But that all is beside the point. You can inherit any other canvas item class. Makes no difference for what the example is about.

                        The color is for the whole node. It's just to differentiate between the nodes so we can see how undo properly functions with multiple nodes. Not meant to work as some kind of brush color and it's not included in the undoable action. Although that'd be easy to implement. I'm leaving it to you as a homework.

                        I'm here in the business of demonstrating the basic key concepts in the most compact form, not finetuning the user experience 🙂