Addon Operators and Undo support

It seems like most aspects of Undo support for addon Operators remain troublesome, full of land mines, and generally ‘broken’ enough to stay away from entirely.

I’m attempting to write an Operator, typically invoked after the user is already in Edit mode, that performs several steps and Undo is completely broken for it.

The guidance seems to be to stick to native API calls as much as possible but that is quite the ordeal when there’s about 10 or so core bpy.ops which would have to be reimplemented and maintained etc. Is this still the case?

Additionally, even native API calls directly are problematic. Consider what the Undo stack looks like after attempting 1 Operator call:

  • My Operator
  • [several steps of inconsequential actions…]
  • Toggle Editmode
  • original

In order for the user to Undo what is 1 logical operation (my operator), they have to hit Ctrl-Z as many times as it takes so they end up before the “magical” edit-model toggle which Blender seems to use as the restore point (erasing all their work in the meantime).

So what are my options?

  • I plan to explore keeping track of state in my Addon directly and providing a MyUndo operator there which would reverse as much state as possible
  • Is there a way to override that Toggle Editmode save-point somehow?

There’s a operator called bpy.ops.ed.undo_push() it will create a undo step each time you call it, you can use it to manually controll the undo from your operator. I use it all the time and it generally solves some problems regarding undo/redo.

3 Likes

Hmm, yeah this was suggested to me over on BA as well but the docs say this:
Add an undo state (internal use only)

I’ll play around with it a bit to see if it can solve some of the issues I have. Hopefully, it remains “external” enough for addon devs through the 2.8 cycle.

Eh, yeah, doesn’t seem to work :-/

Modifiers added to an object still remain, a newly added collection still remains, and a separated mesh part still remains separated – nothing was undone. I have to ctrl-z to before I entered edit-mode for it to work out.

Things pretty much work if I’m in Object mode first (and the user has done some steps which the addon would automate for them). However, my tooling is kinda geared to be used while you’re in Edit mode already for convenience and workflow speed.

Oh, by the way do you have an undo tag in the operator register options?
bl_options = {"REGISTER", "UNDO"}

Yes, the tag is present :slight_smile:

maybe if you give an example code, we would help you more in finding a workaround.

See 1 part below. It’s just the collection aspect of things, there’s a variety of other operations performed. The usage of the collection API directly was suggested by brecht here: Where to find collection_index for moving an object?

Repro steps:

  • start with default scene
  • select the Cube and go into Edit mode (stay in edit mode for rest of steps)
  • make several edits to the Cube (just move some edges around)
  • Invoke the operator below (notice it does the right thing)
  • Ctrl-Z (it does not do the right thing; you’ll have to Ctrl-Z past the point where you went to edit mode for things to Undo)
class RB_OT_undo_issue(bpy.types.Operator):
    bl_idname = "view3d.rb_undo_issue"
    bl_label = "Test test"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        # Add new collection to the scene
        col_test = bpy.data.collections.new("Test")
        bpy.context.scene.collection.children.link(col_test)

        # Remove default cube from Original and place it into Test...
        col_orig = bpy.data.collections["Collection"]

        cube = bpy.context.active_object
        col_test.objects.link(cube)
        col_orig.objects.unlink(cube)

        return {"FINISHED"}

weird, here I am having no trouble, its working fine.

Hmm, that’s really weird. I also looked at my prefs and I can repro the issue with Global Undo either On or Off… I thought that might have affected things - darn.

So if you remain in edit mode the entire time, invoking the operator and then undoing just once, it removes the new collection and puts cube back in the original?

Oh, really this doesnt work in editmode, works in object mode tho.

Yup… back to the original question then :cry:

Thanks for taking the time to repro though. Appreciate that.

If you register the class without {‘REGISTER’, ‘UNDO’} and manually set the undo_push() right before linking as I suggested in the thread on BA, the undo will work.

import bpy

class RB_OT_undo_issue(bpy.types.Operator):
    bl_idname = "view3d.rb_undo_issue"
    bl_label = "Test test"
#    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):

        # Remove any orphaned instances from previous run
        test = bpy.data.collections.get("Test")
        if test:
            bpy.data.collections.remove(test)
            
        # Add new collection to the scene
        col_test = bpy.data.collections.new("Test")
        
        # Manually add undo here
        bpy.ops.ed.undo_push()
        bpy.context.scene.collection.children.link(col_test)
        
        # Remove default cube from Original and place it into Test...
        col_orig = bpy.data.collections["Collection"]

        cube = bpy.context.active_object
        col_test.objects.link(cube)
        col_orig.objects.unlink(cube)

        return {"FINISHED"}
    
def register():
    bpy.utils.register_class(RB_OT_undo_issue)
    
def unregister():
    bpy.utils.unregister_class(RB_OT_undo_issue)
    
if __name__ == '__main__':
    register()
2 Likes

Oh, interesting, removing the REGISTER, UNDO tags + adding a strategic undo_push does allow things to work a bit… better, but not completely.

I’m seeing a few immediate issues after those changes in my real operator. There’s the mesh separation problem from the BA thread that would still need addressing, Object mode undo now crashes in certain cases, and (the kicker) for whatever reason, I do not have to special case my operator’s bpy.data.collections.new call like you do above – no idea why. If I ignore all these things, it kinda sorta works :-/

At this point I’m still utterly confused on the proper way to do this. I’m not sure if I’m hitting bugs, unimplemented 2.8 features, or plain 2.7x legacy – or a combination of all of those.

@brecht, @ideasman42 : I hoped to address this without bugging devs, but I think we need your inputs to this. I can provide the complete Operator on request here if needed.

I think there’s probably a bug here, maybe related to the new unified undo stack. So it’s best to report a bug in the tracker.

Normally operators that edit the scene should use bl_options = {'REGISTER', 'UNDO'}, and then all their operations should be bundled as a single undo step.

Is there a way to bundle all operations that occur during the life of a modal operator as a single undo step? (e.g. bundling actions taken while in a custom mode)