Last week I discussed uiundo – Matlab’s undocumented undo/redo manager. Today, I will show how this object can be customized for some specific needs. However, we first need to understand a little more about how uiundo works beneath the hood.
Matlab stores all of a figure’s undo/redo data in a hidden figure object, referenced by getappdata(hFig,’uitools_FigureToolManager’). This means that by default uiundo works at the figure level, rather than the application level or the GUI component level. If we wish to modify this default behavior, we need to programmatically inspect and filter the undo/redo actions stack based on the action source. Read below to see how this can be done.
The hidden uitools_FigureToolManager object, defined in %MATLABROOT%\toolbox\matlab\uitools\@uiundo\, uses a stack to store instances of the undo/redo cmd data structure introduced in last week’s post:
% Retrieve redo/undo object undoObj = getappdata(hFig,'uitools_FigureToolManager'); if isempty(undoObj) try % R2014a and earlier undoObj = uitools.FigureToolManager(hFig); catch % R2014b and newer undoObj = matlab.uitools.internal.FigureToolManager(hFig); end setappdata(hFig,'uitools_FigureToolManager',undoObj); end >> get(undoObj) CommandManager: [1x1 uiundo.CommandManager] Figure: [1x1 figure] UndoUIMenu: [1x1 uimenu] RedoUIMenu: [1x1 uimenu] |
There are several interesting things we can do with this undoObj. First, let’s modify the main-menu items (I will discuss menu customization in more detail in another post):
% Modify the main menu item (similarly for redo/undo) if ~isempty(undoObj.RedoUIMenu) undoObj.RedoUIMenu.Position =1; %default=2 (undo above redo) undoObj.RedoUIMenu.Enable = 'off'; % default='on' undoObj.RedoUIMenu.Checked = 'on'; % default='off' undoObj.RedoUIMenu.ForegroundColor = [1,0,0]; % =red end if ~isempty(undoObj.UndoUIMenu) undoObj.UndoUIMenu.Label = '<html><b><i>Undo action'; % Note: &Undo underlines 'U' and adds a keyboard accelerator % but unfortunately only if the label is non-HTML ... undoObj.UndoUIMenu.Separator = 'on'; % default='off' undoObj.UndoUIMenu.Checked = 'on'; % default='off' undoObj.UndoUIMenu.ForegroundColor = 'blue'; % default=black end </i></b></html> |
Now, let’s take a look at undoObj’s CommandManager child (the Figure child object is simply handle(hFig), and so is not very interesting):
>> undoObj.CommandManager.get UndoStack: [13x1 uiundo.FunctionCommand] RedoStack: [1x1 uiundo.FunctionCommand] MaxUndoStackLength: [] Verbose: [] >> undoObj.CommandManager.UndoStack(end).get Parent: [] MCodeComment: [] Name: 'slider update (0.48 to 0.38)' Function: @internal_update Varargin: {[53.0037841796875] [0.38] [1x1 double]} InverseFunction: @internal_update InverseVarargin: {[53.0037841796875] [0.48] [1x1 double]} |
This looks familiar: In fact, it is exactly the cmd data structure being passed to the uiundo function, with the additional (apparently unused) properties Parent and MCodeComment. CommandManager‘s UndoStack and RedoStack child objects contain all stored undo/redo actions such that the latest action is at the end of these arrays. In the snippet above, there are 13 undo-able actions, with the latest action in UndoStack(end). UndoStack and RedoStack have the same structure:
- Name contains the action description (presented in the figure’s menu)
- Function is the function handle that will be invoked if the action is redone
- Varargin are the arguments passed to Function during redo
- InverseFunction is the function handle that will be invoked if the action is undone
- InverseVarargin are the arguments passed to InverseFunction during undo
- Parent and MCodeComment – I could not determine what these are used for
We can inspect the latest undo/redo actions, without activating them, by using CommandManager‘s peekundo() and peekredo() methods (which return empty [] if no undo/redo action is available):
>> undoObj.CommandManager.peekredo.get % first check if isempty Parent: [] MCodeComment: [] Name: 'slider update (0.38 to 0.28)' Function: @internal_update Varargin: {[53.0037841796875] [0.28] [1x1 double]} InverseFunction: @internal_update InverseVarargin: {[53.0037841796875] [0.38] [1x1 double]} >> undoObj.CommandManager.peekundo.get Parent: [] MCodeComment: [] Name: 'slider update (0.48 to 0.38)' Function: @internal_update Varargin: {[53.0037841796875] [0.38] [1x1 double]} InverseFunction: @internal_update InverseVarargin: {[53.0037841796875] [0.48] [1x1 double]} >> undoObj.CommandManager.peekundo.Name ans = slider update (0.48 to 0.38) |
We can undo/redo the latest action (last element of the UndoStack/RedoStack) by invoking CommandManager‘s undo()/redo() methods. This is actually what uiundo is doing behind the scenes when it is called with the ‘execUndo’ and ‘execRedo’ arguments:
undoObj.CommandManager.undo; undoObj.CommandManager.redo; |
We can clear the entire actions stack by using CommandManager‘s empty() method. This can be useful, for example, after a ‘Save’ or ‘Apply’ operation in our GUI:
undoObj.CommandManager.empty; |
If we set CommandManager‘s Verbose property to any non-empty value, debug information is spilled onto the Command Window when new uiundo actions are added:
>> undoObj.CommandManager.Verbose = 1; % now move the slider and see the debug info below: internal_update(h_uicontrol, [0.48,], h_uicontrol); % Called by slider update (0.28 to 0.48) internal_update(h_uicontrol, [0.58,], h_uicontrol); % Called by slider update (0.48 to 0.58) |
Finally, CommandManager uses its MaxUndoStackLength property to limit the size of the undo/redo stacks. This property is defined as read-only in %matlabroot%\toolbox\matlab\uitools\@uiundo\@CommandManager\schema.m line #12, so if you wish to programmatically modify this property from its default value of empty (=unlimited), you will need to comment out that line.
Hi Yair,
Can you please answer my query at
http://groups.google.com/group/comp.soft-sys.matlab/browse_thread/thread/8bba37279a78b926#
sorry for the irrevalent post here
thanks
Hey Yair,
I’m trying to implement this in a GUI, and I have the basic functionality working, but I’m having trouble jumping to an undo point earlier than the most recent, i.e. if there are 5 possible undo actions, I would like to jump to the 3rd or 4th instead of the last.
Also, Do you know how to create the list box used in the function button or the cell button in the editor? its a context menu with a scroll bar on the side. I’d like to use that functionality for an undo dropdown.
Thanks for any help!
Jason
small update: I’ve gotten this to work partially by using:
but the undoObj structure wont update to reflect the changes.
Thanks Again
Sorry for more updates, but here is my current solution
This does what I need, but it requires cycling through all the previous changes. I’d rather just skip to the desired step, because for my gui, this process is pretty slow.
@Jason – when you undo steps, you first need to undo all the actions that followed your desired undo-action, because they might affect the way your undone action behaves. Effectively, you are unrolling the actions at the exact reverse order in which they happened. If you look at any program with an undo list, they all behave the same way (try any editor, for example). I actually think it’s very intuitive.
Regarding the scrollable undo/redo lists – see the previous post for an example. The scrollbar appears automatically when more than 8 list items are available. This is standard JComboBox behavior (the number 8 is a modifiable property of the JComboBox).
Hello Yair,
thank you for your great guide! I implemented a redo/undo functionality in a small tool a couple of years ago, and it worked great … until now. Unfortunately, newer Matlab versions than 2014a don’t have the uitools_FigureToolManager object anymore, and I couldn’t find any alternatives. Do you (or anybody else) know if just the location has changed, or has the object completely been removed? Are there already any alternatives found to access Matlab’s redo/undo manager?
Best regards
@John – I think you mean R2014b, not R2014a. In 14b the entire graphics engine changed (HG2) so this is not surprising. I updated the main post, but here is the relevant fix:
Hello Yair,
Can you please help me. I am using matlab 2017b.
I am using uiundo function for undo and redo feature. when the debugger is inside the uiundo function i can see UndoStack has the value.
but once i come out of uiundo function and execute below code:
undostack is empty. Can you please help me to resolve the issue.
Veena – After you execute the undo function, of course the undo stack will be empty because the undo command was already executed.