Access to the history stack

Hi,

I’m working with on a Blender add-on for Mozilla Hubs, and we are doing some things in the add-on that require access to the history stack to know both the previous and current undo step types (whether it’s an undo or a redo) and their names. We know that there are undo_post and redo_post handlers but they don’t expose any information about the history stack so that’s not useful in our case. To work around this, we’ve ended up redirecting the C level stdout to bring the output of window_manager.print_undo_steps() back into Python in order to read the stack, but that has it’s own set of issues. Hooking into native DNA structs doesn’t seem to be the best thing to do either as that can change at anytime and that would also break things. So we are wondering if adding some more information to the undo_post and redo_post handlers would be something that you would be open to considering as an API enhancement. We are happy to contribute that work but we want to first make sure that this is something you are open to having, or see if there is another, better approach that you can think of or have discussed in the past that we could help with.

Thanks!

9 Likes

Can you give more context for the problem you are solving?

It’s not obvious if access to the history stack is the right solution, or if it’s working around some other limitation that may be easier to solve another way.

In the Hubs add-on objects can have Hubs components attached. Hubs components are just a group of properties attached to the object that we add to the output GLTF file when exported and we read in the Hubs client web page. When a new Blend file is opened or linked/appended, we perform migration on the Hubs components to make sure their component data is consistent with the currently installed add-on/component version and in sync with the client. For new opened Blend file we achieve this adding a load_post handler and that works fine but that doesn’t trigger for linked/appended files so we use depsgraph_update_post for that scenario and also to handle the history jumps. As an example: if you link in the current scene an object with a Hubs component attached using an older add-on version, we migrate the component data, if you undo the linking, we do nothing as the object is gone but if you redo, we need to migrate again.

This is where we do that: hubs-blender-exporter/handlers.py at ba092087d07abefa5dc15e7a4d648b336845ee9d · MozillaReality/hubs-blender-exporter · GitHub

We make the decision about migrating (and some other things like some gizmo refreshing cases) based on the step type (undo/redo) and the action that happened in that step (Link, Delete, Unlink, Make Local,…). Tthe only way we have found of getting that information is by reading the output from bpy.context.window_manager.print_undo_steps(). This way of reading the history stack is hard to maintain and meant to break as soon as the print_undo_steps output changes so we would rather having an official API for accessing that information in a safe way.

To add some more context after our conversations in blender.chat:

Solutions that we have considered for accessing the history stack:

  • Hook into the native DNA undo stack structs.
  • Read the history stack redirecting the C level stdout from window_manager.print_undo_steps() in order to read it. This is what we are currently doing.

Both are prone to breaking pretty easily so they don’t seem like a maintainable solution in the long run. So we wondered if adding some more information to the undo_post and redo_post handlers would be something that you would be open to considering as an API enhancement. Something like:

undo/redo_post(history_stack, prevIdx, curIdx) (stack being an array of step names)

That would be enough for our use case, maybe exposing more info would be useful for a more general usage.

If we expose information about the history stack, I think we would just make the history stack accessible from from window_manager as a list of steps + the index of the active one. Maybe the handlers could have the previous index.

However, using the undo stack for this is very unreliable. There are various operators that can directly or indirectly change things, even custom Python operators.

A more reliable solution could be a handler that runs when link/append happens, before the undo push so there will never be a wrong state in the undo history.

IIUC you first proposed (and quickly discarded) option would be to add access to the stack in window_manager exposing the steps and the index of the active one but this wouldn’t be reliable because some operators would modify the stack without triggering undo_post, redo_post or depsgraph_update_post?

If that’s the case adding handlers for link/append also sounds like a good solution but note that the previous step would still need to be exposed to know what the interim steps are when the user jumps around the history stack using Edit → Undo History.

Also you first comment also makes me think that we are probable missing migrations in the case you mentioned when an operator changes the stack without that triggering any handlers. Is that the expected behavior or a bug?

Regardless of what’s done with the undo stack, I think it would be nice to have handlers for append and link, although I’m unsure the link one would do us much good unless it’s triggered each time data is reread from the linked file and not just the initial link. But we are also using the undo stack to trigger gizmo refreshes based on certain user actions, which would not be accommodated by append/link handlers. We use gizmos as omnipresent visual indicators for certain components, so we try to update them as little as possible as it can be fairly expensive to redraw them all if there are lots.

Operators that modify Blender data should trigger those handlers, unless they are implemented incorrectly.

My point was that if you can do your data changes in a handler that triggers at the right moment, you shouldn’t have to do any undo stack tricks.

This issue is not that the handlers are not triggered, it’s that trying to detect if link/append happened based on matching a few names like “Link”, “Append”, “Delete” is unreliable as there are other operators that do these operations directly or indirectly, and that there can even be custom operators in add-ons whose name you can never predict.

This again sounds like a workaround for something that could be solved in a better way.

This issue is not that the handlers are not triggered, it’s that trying to detect if link/append happened based on matching a few names like “Link”, “Append”, “Delete” is unreliable as there are other operators that do these operations directly or indirectly, and that there can even be custom operators in add-ons whose name you can never predict.

My point was that if you can do your data changes in a handler that triggers at the right moment, you shouldn’t have to do any undo stack tricks.

Gotcha, In case that’s implemented as link/append handlers, how would a link/append undo/redo be handled? Those actions don’t currently trigger the undo_post/undo_post handlers.

For correctly handle migrations on linked objects we also need to know when the object it’s made local or it’s data has been localized. What would be the best way of handling those? Would also be handlers? undo_post/undo_post handlers are also not triggered for those.

Undoing link or append operators should trigger an undo handler like any other operator. And they seem to in a quick test here. If that’s not the case it’s a bug.

But what I’m proposing is a new handler that lets you modify linked or appended datablocks before they get recorded into undo stack history. Undo/redo handlers should not be involved at all to solve your use case properly, as I understand it.

I’m not sure that you need to know when an object is made local. Blender update of a linked datablock saved in an earlier Blender version happens immediately when the object gets linked. Because it needs to be in a valid state even if it is read-only. Maybe you have other reasons to want to do something on make local, but again perhaps there is a better solution to that.

1 Like

Oops sorry, it does work correctly. I think adding link/append handlers would cover all our migration use cases as long as they are triggered every time an link/append happens (even if it’s through undo/redo).

It isn’t making the object local that we need to know, but when that action is undone because it causes a reread of the data from the linked blend file. So as long as the link handler is triggered every time the linked data is reread, and not just on the initial link, it should fit our use case. Does this sound like a workable design to you?

Well, this was initially done to solve corner cases and to try to increase performance, but looking at it again I think the corner cases can/have been solved a different way, and it didn’t do as much for performance as hoped because of other limitations, so I guess they can just be handled with the regular undo/redo handlers.

If the link handler is added then indeed this should work too.

But note that undo will no re-link the data or call any link handler. It will restore the data to the previous state in the undo stack, and as long as we can ensure that state in the undo stack is correct it should be fine.

That hasn’t entirely been the case in my experience. For the most part undoing things on a linked object acts as you said, but undoing certain actions seems to cause it to be reread from the blend file on the disk (we’re currently handling Make Local, Localized Data, Delete, and Unlink Object and remigrating whenever one of these undo steps is undone). To see for yourself what I’m talking about, link in some object from another blend file, alter it’s location via python in the python console, add in some new object, move that, then undo (note that the linked object still has the modified location), then redo the move, then delete the linked object, and then undo the deletion of the linked object (note that the linked object has returned to it’s original position). So I think we need the link handler to trigger whenever the linked data is reread, but let me know if I’m missing anything.

Side note: If you were to move the file where the linked object comes from after deleting the object and then undo the deletion you will be left with a broken link, so this is what makes me think Blender is rereading the data from the file on disk and not storing a copy of the file (or part of the file) in memory.

Hey Brecht, What would be the next steps to move forward with your proposal? Should be file a bug or send a PR ourselves? Is it something that it’s already been added to your roadmap?

There’s no plan from my side to work on this, sending a PR would be the fastest way to get this done.

I would also like to access the undo stack, so that I can call bpy.ops.ed.undo_history() to undo to an undo state manually created using:

bpy.ops.ed.undo_push(message='my undo')

My issue is, without access to the undo stack, how do I undo to that state? I can see bpy.ops.ed.undo_history() is what I need to use, but it’s asking for an integer rather than the ‘my undo’ string created with undo_push.

Did anyone here manage to find a way to get the index of the most recently added undo state by name? in my case ‘my undo’. Most recently, because there may be several undo states of the same name.

I was expecting to find something along the lines of :

for item in reversed(bpy.data.scenes['scene'].undo_history) :
    if item.name == 'my undo':
        bpy.ops.ed.undo_history(item.id)
        break
1 Like

Apologies for the long overdue reply. Unfortunately I don’t think Blender has an interface like you describe, but you can sort of create your own version of it, we do something similar for the Hubs add-on if you want to check that out, but it’s hacky. Essentially we call bpy.context.window_manager.print_undo_steps() and redirect it’s C stdout output back to python and then parse the returned undo steps. After it’s been parsed into a list you should be able to find the undo step matching your ‘my undo’ string and get its index.

3 Likes

Bump

I need this in my addon as well!

1 Like

I think I’m a +1 for this also.

I created another thread about it since I wasn’t sure if there’s enough in common, but if an upgrade to support this would let us access something like the output of bpy.context.window_manager.print_undo_steps() that’d do it for me.