Page MenuHome

Add "undo_history" property to bpy.context
AbandonedPublic

Authored by Aleksandr Zinovev (raa) on Jan 7 2017, 12:52 PM.

Details

Summary

The patch adds a new collection property "undo_history" to bpy.context.
It can be used to get current undo history index:

bpy.context.undo_history.index

Or to get all pushed undo states:

bpy.context.undo_history.keys()

Please read @dima glib (dairin0d)'s email to understand why we need this feature.

Diff Detail

Repository
rB Blender

Event Timeline

Aleksandr Zinovev (raa) retitled this revision from to Add "undo_history" property to bpy.context.
Aleksandr Zinovev (raa) updated this object.
Aleksandr Zinovev (raa) set the repository for this revision to rB Blender.
Julian Eisel (Severin) resigned from this revision.EditedFeb 10 2017, 4:35 PM

This is not really my area, so resigning myself from reviewers.

Sergey Sharybin (sergey) requested changes to this revision.Feb 20 2017, 10:51 AM

I am not convinced at all we should expose such things. Those are fully internal guts and exposing them must not be done as-is. I'm also not convinced with the reasoning you gave. There are following issues:

  • Global undo might be disabled, which will make your addon depending on the history to work unreliably depending on user settings and current mode.
  • If it happens so certain steps are cleared from undo history your addon will stop behaving correctly.
  • If you're mixing edits in multiple objects behavior of your addon will become unpredictable.
  • The claim about being faster to access undo history than doing scene update hook and checking is_updated flag is quite wrong. With is_updated flag you know what exactly changed, while otherwise you'll need to iterate over objects to see changes.
  • Undo should be fully hidden implementation which just works for users. If we ever change something here it'll break all the scripts which relies on particular behavior of undo.

There must be solution for your original problem which does not involve dependency of internal stuff and which will behave predictable and reliably in all cases.

This revision now requires changes to proceed.Feb 20 2017, 10:51 AM

Those are fully internal guts

Er... Can something that is clearly accessible to the user (Ctrl+Alt+Z) be considered "fully internal"? The basic logic behind proposals like these is that if users are able to see and/or manipulate some part of the program's state, it's reasonable to expect that it should be accessible to the scripts too.

A disabled Global Undo indeed limits the usefulness of undo history. Though for certain kinds of applications (e.g. macros recorders) this wouldn't be an issue, since without Global Undo there is no need to track undo/redo operations.

There must be solution for your original problem which does not involve dependency of internal stuff and which will behave predictable and reliably in all cases.

At least in the cases I encountered, what I actually needed were the following:

  1. For calculating multi-object statistics: a way to be notified when anything in the scene has changed (e.g. selection, objects' properties, objects' data, etc.). This basically has a 1:1 correspondence to a new entry being added to the undo history.
  2. For macros recording: a way to be notified whenever undo and redo operations are performed (and preferably a way to know if current state is at the top of the undo stack).

Regarding (1):

  • The existing scene update hooks (scene_update_pre and scene_update_post) are invoked every 5 milliseconds (even if nothing happens), so they cannot be used to detect if something has changed.
  • Even with is_updated flag I'll have to iterate over all objects anyway, to see if any one had changed. Also, an object's is_updated flag is set to True in only a limited amount of circumstances (e.g. changing object's layers or display properties won't trigger is_updated).
  • In addition to checking if objects were updated, I also have to detect whether any objects/elements were (de)selected. This adds another layer of complexity.
  • To be sufficiently responsive, I'll have to run these checks at least several times per second. Depending on how many objects there are in the scene (or elements in the currently edited mesh), just looping over all elements might lead to noticeable stutter/drop in performance.

The best solution here would be a hook/callback which is actually invoked only when any sort of change has occurred (essentially, whenever an "undoable" operation has been performed, even if Global Undo is disabled).

For (2), the solution which doesn't involve undo history would be 2 separate hooks/callbacks for undo and redo operations (or a single callback with some sort of undo_redo argument).

I am not convinced at all we should expose such things. Those are fully internal guts and exposing them must not be done as-is. I'm also not convinced with the reasoning you gave. There are following issues:

  • Global undo might be disabled, which will make your addon depending on the history to work unreliably depending on user settings and current mode.
  • If it happens so certain steps are cleared from undo history your addon will stop behaving correctly.
  • If you're mixing edits in multiple objects behavior of your addon will become unpredictable.
  • The claim about being faster to access undo history than doing scene update hook and checking is_updated flag is quite wrong. With is_updated flag you know what exactly changed, while otherwise you'll need to iterate over objects to see changes.
  • Undo should be fully hidden implementation which just works for users. If we ever change something here it'll break all the scripts which relies on particular behavior of undo.

There must be solution for your original problem which does not involve dependency of internal stuff and which will behave predictable and reliably in all cases.

Forget about my previous example from the email. Here is a better one:

F key toggles between Fill and Grid Fill tools:

To decide which tool to use (Fill or Grid Fill) we need some information about last used command.
C.active_operator or C.window_manager.operators can't be useful here, because these features ignore some basic tools (for example RMB select tool).
Only C.undo_history can be handy in this case.

I know we can implement the F key tool as a modal operator, but the modal operators add one extra step to confirm/cancel tool which slows down the workflow.
Also there are a few ways to implement the tool without C.undo_history feature. But all of them are slow or/and not 100% accurate.

C.undo_history is based on existing Undo History operator (ctrl+alt+Z), which works in mesh edit mode (even if Global Undo is disabled).
I understand that it won't work in all modes and all cases. But even if C.undo_history is empty it's still useful info for addons.

Note that C.undo_history is dynamic and read-only so it shouldn't break any existing features.

Talked with @Aleksandr Zinovev (raa) and think it might be enough to add a is_last_action() method to Operators. this way you can do:

if context.window_manager.operators[-1].is_last_action():
    ...

Or perhaps better to expose:

context.window_manager.operators.active

Which would be the active operator or None

This is a way to know if an operator is showing up in the toolbox for redo, (which may be useful in general for other tools). And avoids exposing undo stack.

Aleksandr Zinovev (raa) edited edge metadata.

Implemented 'Operator.is_last_action' property which returns True if the operator is the last action in undo stack.

Talked with @Aleksandr Zinovev (raa) and think it might be enough to add a is_last_action() method to Operators. this way you can do:

if context.window_manager.operators[-1].is_last_action():
    ...

Or perhaps better to expose:

context.window_manager.operators.active

Which would be the active operator or None

This is a way to know if an operator is showing up in the toolbox for redo, (which may be useful in general for other tools). And avoids exposing undo stack.

Imo your first solution is more intuitive.
The active operator can be available for redo in toolbox even if it's not the last action in undo stack. That's why the second solution can confuse users.

Would rather follow logic that toolbar uses to check if an operator will show in the toolbar, this patch basically duplicates that logic.


Correction, toolbar shows even if there have been selections since.

Talking with @Bastien Montagne (mont29), P453 - we think it might be best to add undo operators into window_manager.operators list, it allows most flexibility.

Without this its very hard to reason usefully about user-actions (from C or Python).

Add operators with UNDO flag into C.window_manager.operators.