Shotwell Architecture Overview: Commands and the Command Manager (Undo / Redo)

Shotwell 0.4 introduced an undo/redo stack. Internally, this is implemented with a CommandManager object, which manages Command objects. Each undoable action in Shotwell is implemented as a Command. Whenever a new feature is added to Shotwell, it should be considered whether or not it is undoable, and if so, implement it as a Command rather than direct code.

The design goals of the CommandManager is to provide a generic way of executing various operations in such a way that they can be undone (unwound) and redone (performed again). Because the state of any operation may rely on the operations immediately before and after it, Commands are guaranteed to be performed in strict stack (LIFO) ordering. Thus, there is no way to remove a Command embedded in the stack (although it’s possible for a new Command to be merged with the topmost Command; see below). If a stored Command recognizes at any time that it cannot be undone, it must reset the CommandManager; it’s considered too tricky to demand all affected Commands unwind themselves from the chain.

CommandManager

It’s possible for multiple CommandManagers to exist in the application. (For example, it’s possible for each Page to have its own undo/redo stack.) However, at this time there is only one global CommandManager stored in AppWindow. Each Page has an accessor which fetches it from AppWindow; Page implementations should use that, in case it is changed in the future.

The CommandManager manages two stacks, one for undo and one for redo. Whenever either stack is modified it fires an "altered" signal with parameters indicating whether or not an undo and/or redo is possible.

Each stack has a maximum size. The default is 20. When the stack overflows, Commands are simply dropped. If a Command needs to clean up if its dropped, it should implement a destructor.

To add a Command to the CommandManager, submit it via execute (). This sets in motion the following events:

  • The redo stack is cleared.
  • A merge is attempted between the new command and the current topmost undoable command. (See below for more on Command compression.) If the merge succeeds, execute () exits.
  • The new Command is pushed on the undo stack.
  • Command.prepare () is called.
  • Command.execute () is called.
  • The CommandManager’s "altered" signal is fired.

Undo and redo are similar operations:

  • The topmost command on the appropriate stack is popped.
  • It’s pushed on the other stack.
  • Command.prepare () is called.
  • Command.undo () (or redo ()) is called.
  • The CommandManager’s "altered" signal is fired.

The order of the operations is important. The Command methods are explained below.

Commands

Command is an abstract base class. A bare implementation must only supply for the constructor a user-readable name and explanation (both are displayed in the UI and should be translatable). The only two methods that must be implemented are execute () and undo (). Other methods may be overridden, if needed.

The CommandManager calls the Command methods in a strict order:

  • prepare
  • execute (once and only once)
  • prepare
  • undo
  • prepare
  • redo
  • prepare
  • undo
  • etc.

A Command should do its initialization in its constructor rather than prepare (). prepare () exists to allow subclasses to perform operations common to undo () and redo () easily.

The default implementation of redo () is to call execute (). There are certain commands that can perform a redo () more efficiently than their initial execute (), hence the reason for overriding.

Command Compression

Certain commands can be merged or compressed. For example, if the user makes four saturation adjustments to a photo and wants to undo them, she expects to go back to the saturation she started with.

When given a Command to execute, the CommandManager first attempts to compress it with the topmost undoable Command via compress (). If this method returns false the Command is dealt with as already explained.

If it returns true, the CommandManager assumes whatever operation the new Command represents has been executed and the topmost command has absorbed its result into its state (if necessary) so it can be undone.

Utility Commands

Various utility Commands have been implemented to ease the implementation of specific Commands. These work with the major abstract data structures of Shotwell.

PageCommand is simply a Command that’s tied to a particular Page. When a PageCommand is undone or redone, its prepare () method switches the user to that Page so the results are visible.

MultipleDataSourceCommand provide methods for commands that deal with a single or multiple DataSources — TransformablePhotos, Events, etc. In particular, both monitor their DataSources for "destroyed" signals and reset the CommandManager if that happens. (The Command is necessarily unable to undo at that point, and as explained above, resetting the CommandManager is therefore required.) MultipleDataSourceCommand also provides utility methods to perform the operation on individual DataSources while it displays a progress bar window. If the user cancels an execute () or redo (), MultipleDataSourceCommand also ensures that only the objects already affected are undone.

GenericPhotoTransformationCommand are specific to Commands dealing with the various photographic transformations available via TransformablePhoto. TransformablePhoto offers new methods (load_transformation_state and save_transformation_state) which package all its transformations into an opaque data structure that can be stored. Thus, the photo transformation Commands merely execute () their operation and these base classes deal with undo () and redo () by loading an appropriate transformation state.

SimpleProxyableCommand works with DataSources that may be dehydrated and rehydrated via a SourceProxy. This is useful when the Command (or even another Command) destroys the DataSource but it must be reinstantiated to perform the Command. SimpleProxyableCommand handles the work of dealing with the SourceProxy and resetting the CommandManager when the SourceProxy is broken (that is, it can no longer rehydrate the DataSource).

Apps/Shotwell/Architecture/CommandManager (last edited 2013-11-22 22:51:19 by JimNelson)