Accessing Undo History via Python API

I’ve been trying to build a simple Addon that just displays the current Undo History on the 3D ViewPort (SpaceView3D?) so it’s always visible. So basically I need read-only access to the undo stack/history.

I’ve almost been successful using existing API, but what I’m doing isn’t reliable enough. I’m always able to find a way to break it so the Edit->Undo History gets out of sync with what I’m showing. Basically it would look something like this:

What does seem reliable is the output from bpy.context.window_manager.print_undo_steps(), but redirecting that from the console back to Python for display in the ViewPort is pretty hacky. Also I need a solution that works on Linux and the example posted in the other thread I linked looks Win specific.

Undo 12 Steps (*: active, #=applied, M=memfile-active, S=skip)
[    ]   0 {0x7fe9a8a69908} type='Global Undo', name='Original'
[    ]   1 {0x7fe9a6c40f08} type='Global Undo', name='Console Execute'
[    ]   2 {0x7fe9a72bf808} type='Global Undo', name='Select'
[    ]   3 {0x7fe9a53f7808} type='Global Undo', name='Select'
[    ]   4 {0x7fe9a5700ee8} type='Edit Mesh', name='Toggle Edit Mode'
[    ]   5 {0x7fe9a8a60c08} type='Global Undo', name='Toggle Edit Mode'
[    ]   6 {0x7fe9a8a60f88} type='Global Undo', name='Select'
[    ]   7 {0x7fe9a635e188} type='Edit Mesh', name='Toggle Edit Mode'
[    ]   8 {0x7fe9a56792e8} type='Edit Mesh', name='Select'
[    ]   9 {0x7fe9a8a5b708} type='Global Undo', name='Toggle Edit Mode'
[    ]  10 {0x7fe9d8114388} type='Global Undo', name='Select'
[* M ]  11 {0x7fe9a53f7d08} type='Global Undo', name='Select'

Of course it would be preferable to get this in a proper data structure, rather than text that needs to be parsed.

This thread sounds similar and maybe an API upgrade could solve some of those problems too. Access to the history stack

I was considering trying a PR to enhance the API, but it’s been a while since I’ve coded in C and I’m uncertain how difficult an API upgrade for this would be. Am I headed down a feasible path to make this possible?


Background: I ended up down this rabbit hole because I’m new to Blender, and I’m finding myself constantly accessing the Edit->Undo History menu option to have better visibility into what operations were recently done (or not done). When I wasn’t checking this, I found that I was making mistakes more often that I didn’t notice right away.

Hmm, I guess either I’m in the wrong place or everyone who might have useful feedback for this prefers live chat or something. :sweat_smile:

TLDR: Would a pull request that adds a call to the Python API which exposes the data that this print_undo_steps() function displays even be accepted, or would I be wasting my time trying to do this?

Well… I’ve got a basic change which seems to be working to provide Python with a short list of recent undo step names, building and running locally now. Works for my case but I doubt it’d pass reviews considering the limited utility.

My C++ days are too far behind to get it to a more acceptable (IMO) state in a reasonable amount of time, so unless someone’s willing to collaborate or assist putting together a sensible PR (with greater utility for other use cases), that is probably about as far as it’ll go.

Hello,
I would be happy to help you develop your idea.
Can you put a link to your PR so I can see what you already tried?

If I understand correctly, you would like a Python function that would return information on the undo history.
Example:

undo_history_information = get_undo_history_information()

The output would be an ordered array (undo steps) of a certain data structure (dictionary?) holding textual information like type and name for each undo step.
Am I right?

Something like that would increase the utility. There might be a few non textual flags for the undo stack also that should probably be exposed. But I’m no expert on Blender’s Undo System.

I haven’t done a PR because I’m sure there wouldn’t be interest yet. It only solves the immediate problem I had creating a simple Addon, and probably wouldn’t be much use other than that. But the changes are simple enough I can throw it up here.

I made the additions (additive only, no edits) locally on the blender-v4.2-release branch (just for stability, not to push) within this file: source/blender/makesrna/intern/rna_wm.cc

Inserted at line 1212:

static int rna_WindowManager_undo_stack_length(PointerRNA *ptr)
{
    wmWindowManager *wm = (wmWindowManager *)ptr->data;
    UndoStack *undoStack = wm->undo_stack;
    UndoStep *step = (UndoStep*)undoStack->steps.last;
    int total_length = 0;
    int count = 0;

    while (step && count < 10) {
        total_length += strlen(step->name);
        if (count < 9) {
            total_length += 1;
        }
        count++;
        step = step->prev;
    }

    return total_length;
}

static void rna_WindowManager_undo_stack_get(PointerRNA *ptr, char *value)
{
    wmWindowManager *wm = (wmWindowManager *)ptr->data;
    UndoStack *undoStack = wm->undo_stack;
    UndoStep *step = (UndoStep*)undoStack->steps.last;
    int count = 0;

    value[0] = '\0';

    while (step && count < 10) {
        if (count > 0) {
            strcat(value, "\n");
        }
        strcat(value, step->name);

        count++;
        step = step->prev;
    }
}

Inserted at line 2754:

  prop = RNA_def_property(srna, "undo_history", PROP_STRING, PROP_NONE);
  RNA_def_property_string_funcs(prop, "rna_WindowManager_undo_stack_get", "rna_WindowManager_undo_stack_length", nullptr);
  RNA_def_property_clear_flag(prop, PROP_EDITABLE);

Also added a #include for BKE_undo_system.hh. I don’t even know if those two locations inside window_manager are the best place for such additions!

So what Python gets isn’t a method. I access the history like this in a Python Addon:

    undo_history = bpy.context.window_manager.undo_history
    debug_log(f"undo_history: {undo_history}")

    undo_history = undo_history.split('\n')

    for i, op_name in enumerate(undo_history):
        blf.position(font_id, x, y + i * 25, 0)
        blf.draw(font_id, f"{len(undo_history) - i}) {op_name}")

    blf.disable(font_id, blf.SHADOW)

For this undo history display in the bottom right of the 3D viewport:

image

I still have some tweaks to do, but that’s all I was looking to accomplish. For this API to help someone else I believe it would need to return much more info about the UndoStack in a proper data structure. But I wasn’t sure how to return a more complex data structure like a dictionary (or maybe an array of dictionaries), so I ended up with this kludgey string delimited by \n newlines that Python splits to make use of it. Of course we’d want to return the entire history, not just the last 10 things as I did in the CPP code.

So I don’t know… if this is a useful starting point, I’d be happy to try expanding on it to do a PR. It is a bit of a struggle though after 20++ years not touching C++. I was pretty good at it back then but that knowledge has mostly evaporated by now. :sweat_smile: It’s quite possible that this code is only useful for my own idea.

Alright, thank you for these explanations.
Indeed, I think we should investigate first how the undo system works in Blender. I guess we should consider the (sort of) proposal made in Access to the history stack. I will probably need to read the thread several times because I do not fully understand it for the moment. Moreover, I need to look at the source code to better see how it is done technically.

Do you already have links or resources about the undo system in Blender?

Yeah unfortunately that info seems pretty hard to come by. The only docs I remember locating were here:

https://developer.blender.org/docs/features/core/undo/

Sections on the DNA/RNA were also useful. Mostly I was going off of what the source code does, and the Python API resources.

Actually even my simple Addon could use more information too. When I undo a step, I’ve noticed that the history doesn’t actually change. Instead the “Active” UndoStep just goes back one, so the list I’m displaying is stale. Also some steps are internal implementation which shouldn’t be displayed on the viewport at all (those may be indicated with a “skip” boolean).

Ideally I think what I’d like is to essentially mimic what the Edit->Undo History menu shows, so I’d need to know which undo step is the “active” one to do that.

Probably the UndoStep *step_active pointer is of concern for that. So I figure we’d want to expose these two data structures which are basically a bi-directional linked list with some additional metadata. (Maybe the C brain cells are starting to wake up a little bit… ha.)

struct UndoStack {
  ListBase steps;
  UndoStep *step_active;
  /**
   * The last memfile state read, used so we can be sure the names from the
   * library state matches the state an undo step was written in.
   */
  UndoStep *step_active_memfile;

  /**
   * Some undo systems require begin/end, see: #UndoType.step_encode_init
   *
   * \note This is not included in the 'steps' list.
   * That is done once end is called.
   */
  UndoStep *step_init;

  /**
   * Keep track of nested group begin/end calls,
   * within which all but the last undo-step is marked for skipping.
   */
  int group_level;
};
struct UndoStep {
  UndoStep *next, *prev;
  char name[64];
  const UndoType *type;
  /** Size in bytes of all data in step (not including the step). */
  size_t data_size;
  /** Users should never see this step (only use for internal consistency). */
  bool skip;
  /** Some situations require the global state to be stored, edge cases when exiting modes. */
  bool use_memfile_step;
  /** When this is true, undo/memfile read code is allowed to re-use old data-blocks for unchanged
   * IDs, and existing depsgraphs. This has to be forbidden in some cases (like renamed IDs). */
  bool use_old_bmain_data;
  /** For use by undo systems that accumulate changes (mesh-sculpt & image-painting). */
  bool is_applied;
  /* Over alloc 'type->struct_size'. */
};

Thank you a lot for sharing.

Ideally I think what I’d like is to essentially mimic what the Edit->Undo History menu shows, so I’d need to know which undo step is the “active” one to do that.

Ok, I guess we could look at how they made this menu. If I remember correctly, a lot of UI elements are written in Python.

Ok, I better understand.
The code for this menu is actually written in C++, found it in space_topbar.cc:212.
If we were finally able to expose C data through the Python API, why not use this new API to write the undo history menu then? It could even help us make the future MR more “attractive” haha.

That was exactly my first thought, but I guess I had no luck and looked for other solutions. I’ll take a look at that file.

Maybe we should also get the attention of someone who can actually commit to the repo and/or whoever might voice an objection to a change like this. I have no idea what this team is like but I know sometimes OSS teams can be quite… picky.

I was hoping we’d see someone comment here, but I guess Blender devs don’t use this forum (or this thread lol).

Yeah I think I saw that, thought “MEH… why the heck are these functions static?” and followed where wmWindowManager->undo_stack lead to. I did end up accessing the same undo_stack object inside wmWindowManager, getting to it another way.

I’ll take a closer look at the function though to see what they do to clean up some of the non-user-relevant “undo steps”.

Oh I’m not sure I pasted the latest here, I ended up with this below. This at least got the text descriptions pretty close to what I was looking for.

line 1212

static int rna_WindowManager_undo_stack_length(PointerRNA *ptr)
{
    wmWindowManager *wm = (wmWindowManager *)ptr->data;
    UndoStack *undoStack = wm->undo_stack;
    UndoStep *step = (UndoStep*)undoStack->steps.last;
    int total_length = 0;
    int count = 0;

    while (step && count < 10) {
        total_length += strlen(step->name);
        if (count < 9) {
            total_length += 1;
        }
        count++;
        step = step->prev;
    }

    return total_length;
}
static void rna_WindowManager_undo_stack_get(PointerRNA *ptr, char *value)
{
    wmWindowManager *wm = (wmWindowManager *)ptr->data;
    UndoStack *undoStack = wm->undo_stack;
    UndoStep *step = (UndoStep*)undoStack->steps.last;
    int count = 0;

    value[0] = '\0';

    while (step && count < 10) {
        if (count > 0) {
            strcat(value, "\n");
        }
        strcat(value, step->name);

        count++;
        step = step->prev;
    }
}

line 2754

  prop = RNA_def_property(srna, "undo_history", PROP_STRING, PROP_NONE);
  RNA_def_property_string_funcs(prop, "rna_WindowManager_undo_stack_get", "rna_WindowManager_undo_stack_length", nullptr);
  RNA_def_property_clear_flag(prop, PROP_EDITABLE);

I switched to working off of the blender-v4.2-release branch, since master or main or whichever it is moves quite fast.

@Failxxx So are you more of a Python dev, C++, or both? Edit - or maybe I should say “C” since it definitely looks like Blender is more C-ish than C++ -ish.

Maybe we should also get the attention of someone who can actually commit to the repo and/or whoever might voice an objection to a change like this. I have no idea what this team is like but I know sometimes OSS teams can be quite… picky.

That is good point. However I am pretty sure we could fork the repository, create a new branch then propose a MR. Of course, we would have to explain why we would like to merge these changes.

@Failxxx So are you more of a Python dev, C++, or both? Edit - or maybe I should say “C” since it definitely looks like Blender is more C-ish than C++ -ish.

I am a C++ developer. I am pretty familiar with Python.

Ok, so for the next days, I would like to take a closer look at the documentation and the code. This is to better understand what we need here and maybe try something.

I worked on a project GitHub - Failxxx/blender: Official mirror of Blender + custom "Physarum Editor". two years ago. I remember we exposed C++ data and settings through the Python API. I will re-read our code to remember myself how to do it haha.

1 Like

Cool, sounds good. The next thing I was probably going to look into is how to send a more complex data structure from C++ to a Python Addon (maybe just an array of “dictionary” objects is fine). But currently working an unrelated project so not just yet.

Alright, indeed, that is important.
Ok ! I will come back here once I have new information. Probably during this week or next weekend.

Hello,
I am still investigating on how to expose C data. I think I have an idea how to do it. Here is the branch where I put my work : Felix-Olart/blender - blender - Blender Projects

It does not compile for the moment but I am working on it haha.

1 Like

Wait we can create our own repositories on Blender’s Git site? I had no idea… lol

Cool though I’ll check it out some time. I don’t know if I just haven’t found the right documentation or something but the RNA/DNA system seems like it’s seriously difficult to understand and not so well documented.

Yes of course, it is indicated in the documentation on how to proceed to create a pull request. It is requested to fork the project: https://developer.blender.org/docs/handbook/contributing/pull_requests/

Yes, the RNA/DNA system is not trivial. I do not really understand it for the moment but here is the documentation I found: https://developer.blender.org/docs/features/core/rna/

1 Like

You can access the undo stack using Blender’s Python API, but syncing it reliably in real-time can be tricky due to limitations in the current API. Consider tracking undo events through bpy.app.handlers to maintain consistency, with Blender’s internal undo history.

I tried that (Python handlers) but as you say, it’s not very usable for this. We need a simple way to just get the current undo state.

For my case I’d just like to show the last 5 to 20 undo steps, including which step is the “active” one. (The number of steps shown would be configurable in the Python plugin.)

I find myself needing to go into the Edit->Undo History menu just to see this information, because when you undo a step there’s often little or no visual feedback. It’s a pain to do that, slowing down your workflow. I’ve often found that I need to undo a few more times to get back to the state I wanted. Without checking the history it’s very easy to make a mess of things.

1 Like