[PEAK] Commands, Actions, Undo/Redo, and transactions

Phillip J. Eby pje at telecommunity.com
Thu Jul 29 11:49:18 EDT 2004

At 11:30 AM 7/29/04 +0300, Niki Spahiev wrote:
>Some comments from GUI front.

Thanks for all the feedback!

>Phillip J. Eby wrote:
>>I've pretty much decided that "command" sucks as a term and as a metaphor 
>>for how workspaces will track undo/redo and transactions.  For one thing, 
>>most of what I've been calling commands aren't truly commands in the 
>>"Command Pattern" sense, since they don't know how to "execute themselves".
>>So, I'm going to define a different terminology:
>>   class IAction(Interface):
>>       """A behavior unit that can be undone or redone"""
>>       undoable = Attribute("""True if this action can potentially be 
>> undone""")
>>       key = Attribute(
>>           """A unique key identifying the subject of this action, or None"""
>>       )
>>       def undo():
>>           """Undo the behavior"""
>>       def redo():
>>           """Reapply the behavior"""
>Will there be do() method? Or is redo() first time considered do()?

Neither; the IAction is to record an action that has already been taken, or 
is about to be taken.  However, there is nothing stopping you from taking 
the action by calling redo(), if the action is built that way.  However for 
Memento-style actions, redo() doesn't mean anything because mementos just 
snapshot a state.

>Also action has some different meaning in GUI world see QAction from QT.
>IMHO better stick with pattern names.

I suppose we could go with IMemento instead of IAction, and IHistory 
instead of IActionGroup.  Or maybe IHistoryItem for action.  Yes, I like 
that better.  That way, after you do something (or sometimes before), you 
"add a history item" to the current history.

>Other useful concept is SavePoint - position in undo stack that 
>corresponds to externally saved state. It needs markSavePoint() and 

What would you use them for?  In order to determine whether a file needs to 
be saved?  (i.e. undo/redo that positions you on the save point means you 
don't need to save, otherwise you do).  Are there any other uses?

My thought here is that the basic undo interface doesn't need this, but 
there is nothing stopping an application from subclassing the default 
UndoManager and adding those methods, or any other methods that make sense 
for that specific application.  I don't want to add so many ways to do 
things that the base framework is harder to understand or use.  But, as 
long as the base framework can be extended to do lots of different things 
by different applications, I think we're good.

Also, note that you can implement this a slightly different way, if your 
use case is the one I described above.  Let's say you have a "dirty" flag 
that indicates the file is unsaved.  Initially, it's False, and whenever 
you save, you set it to False.  Then all you need to do is have the routine 
that sets the flag to True, also add a history item to the current history, 
that will reset the flag to False when undone, and back to True when 
redone.  This will accomplish the effect of keeping the file's "savedness" 
in sync with the undo history.  Indeed, you can do this for almost any kind 
of state you need to track, and workspaces will use this to track changes 
to their internal caches/indexes as well.

>>Because a state-based action simply puts things back the way they were 
>>when undone, there's no need to have more than one state-based action per 
>>target state per action group.  That's what the 'key' is for.  You don't 
>>create a new action if the group already has an action for that key, and 
>>when the group is added (committed) to a larger group, it can omit any 
>>actions that already have a matching action in the larger 
>>group.  (Because action groups are undone or redone as a unit, there's no 
>>need to keep an action that represents a snapshot somewhere in the middle 
>>of the action.)
>I have some unclear thoughts about hierarchies. If keys used are like URIs 
>and correspond to model nesting then having state for key=a/b/c implies 
>that state for key=a/b/c/d is not needed as its part of a/b/c

Well, you can always do:

     for key in parentKeys:
         if key in history:
         # add most-specific key

so I don't see a need to make histories (action groups) have any built-in 
interpretation of keys.  This frees you to use whatever meanings of keys 
you want.

>>Change-based actions are simpler than state-based actions, but they can 
>>consume more time and space if objects change frequently within a 
>>top-level action group.  Change-based actions only know how to do 
>>something forwards or backwards, and they are always added to the current 
>>action group regardless of whether similar actions already appear in the 
>>group.  They have a key of 'None', so they also don't get aggregated when 
>>an action group is merged with a parent action group.
>For change-based actions very usable is merging. Sequence of actions with 
>same key is merged as this:
>a -> b
>b -> c
>becomes single action a -> c

I guess we could add a 'merge(otherItem)' method to IHistoryItem, which 
took a second item (b->c in your example) and merged it into the existing 
item with that key.  The 'add()' operation on IHistory would look to see if 
it had an item with the same key and then would call 'merge(new)' on the 
old one.  State-based mementos would just define 'merge()' as 'pass', so I 
guess it wouldn't complicate things too much.

Of course, that would pretty much mean that if a history item has a key, it 
must either be completely responsible for that key (i.e. merge = no-op), or 
else be able to merge into itself any other item with the same key.

More information about the PEAK mailing list