Undo/Redo and tool scripts
- Edited
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.
- Edited
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.
- Edited
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()
- Edited
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 ?
- Edited
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.
- Edited
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
- Edited
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.
- Edited
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 ?
- Edited
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.
- Edited
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.
- Edited
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()
- Edited
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
- Edited
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
- Edited
xyz In the case of the "EditorInspectorPlugin" its diferent
@tool
extends EditorInspectorPlugin
const inspect0 = preload("res://inspector/inspector.tscn");
func _can_handle(object):
if object is tileDrawNode:
return true
return false
func _parse_begin(object):
inspect1 = inspect0.instantiate();
everytime you press the editor screen it triggres the ready function in "inspect1"... i only found out about it now,
maybe the undoRedo history of that tscn gets reset
- Edited
For some reason its reseting it... I dont have anything in the code that does that
If i use this:
tileDrawNode.gd
@tool
extends Node2D
#class_name tileDrawX1;
func _process(_delta):
if ( Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) && mouseJustPress == false ):
mouseJustPress = true;
if ( drawText == null ):
return;
mouseGroup.Fnum.clear();
#-----------------------------------------------------------
#-----------------undo-rendo-draw-------------------------------
undoredo.create_action("tileDraw - drawing Tiles");
undoredo.add_undo_method(self, "remove_element", layers.Lnum[ layerPOS ].duplicate(true) );
undoredo.add_do_method(self, "add_element", layers.Lnum[ layerPOS ].duplicate(true) );
undoredo.commit_action();
I get 2 prints on the other side in the tscn the EditorInspectorPlugin spawns
tileDrawInspector.gd
@tool
extends EditorInspectorPlugin
const inspect0 = preload("res://inspector/inspector.tscn");
func _parse_begin(object):
inspect1 = inspect0.instantiate();
this prints out 2 times everytime i mouse click on the editor, "inspector Ready"
inspector.gd
@tool
extends Control
func _ready():
print("inspector Ready")
Its the undoRedo block of code for some reason is deleting or restarting the inspector, i dont have anything else if i remove that block of code the inspctor.tscn ready function no longer prints ?
- Edited
jonSS For some reason its reseting it
Who resets what? Remember that this is not a thread about your project. Have you managed to implement a simple undo system on your own in a fresh project following the model shown in my example? Do that and it'll make things a bit clearer. Don't just copypaste. Make your own variations. Add stuff to it. Do "what if" experiments with it. Tinker until you gain full understanding of how it works.
Again, the inspector has nothing to do with undo. If you're storing undoable data in an inspector then your architecture is not properly done. All undoable data needs to be on edited node's side. An inspector should only read that data and display it. Akin to classical model-view-controller design pattern.
Typically you'd implement something like update()
or refresh()
method in your inspector and call it every time you want to refresh what the inspector displays. In that method you'd read all the relevant data from the edited node and set the inspector's state accordingly. Everything that lives in the inspector should be totally disposable.
Also, not directly related, but all viewport input concerning the plugin (e.g. editing plugin supported nodes...) is best handled in EditorPlugin::_forward_*_gui_input()
. See my example.