Developer's Guide: The Undo Mechanism

The Document and Graphics Classes | Developer's Guide | Plugins

# The Undo Mechanism

Sketch allows the user to undo every operation that is performed on a document. To achieve this, it needs to keep some information on how to undo the operation. The fact that in Python objects are dynamically typed and functions and (bound) methods are objects allows us to store this information in a very simple and yet very flexible way.

I'll call this information undo information, or undoinfo for short.

## Creation and Storage

In Sketch, undoinfo is stored basically in a list in the document object (actually in an instance of the `UndoRedo` class owned by the document).

A document method that changes the document either creates the undoinfo itself or requires that the functions and methods it calls return the undoinfo. The latter case is the most common, as most operations do not change the state of the document object itself but the state of a layer or a primitive.

In fact, most functions that are expected to return undoinfo do not perform the operation themselves, but call other methods and simply pass the undoinfo they get from those methods back to their caller.

Consider for example the method `SetProperties` of a group object. A group object has no graphics properties of its own, but setting, e. g., the fill pattern of a group will set the fill pattern of all of the group's children (the objects contained in the group). So its `SetProperties` method calls the `SetProperties` method of each of its children and stores the undo info it gets in a list. The undoinfo it returns to its caller will basically say: "to undo the operation you must undo everything in this list".

## Representation of Undoinfo

In Sketch, undoinfo is stored as a tuple whose first element is a callable object. The rest of the tuple are the arguments that the callable object must be called with to actually do the undo:

> Undoinfo (1)

> A tuple `t` is a tuple of undoinfo of an operation, iff the Python expression ``apply(t[0], t[1:])`' will undo the operation.

Example: The `SetRadius` method of the regular polygon plugin:

    def SetRadius(self, radius):
        undo = self.SetRadius, self.radius	# create undoinfo
        self.radius = radius	# set the instance variable
        # ... update internal data here ...
        return undo			# finally, return undo info

Undoing `SetRadius` simply means to set the radius back to the old one, that is, one has to call the method `SetRadius` of the same object again, only this time with the old radius as parameter. Thus the undoinfo returned is ``(self.SetRadius, self.radius)`' which is executed before `self.radius` changes. Note, that in this example the internal data get recomputed automatically during the undo.

Closer examination reveals that performing this undo returns again some undoinfo. This new undo info tells us how to undo the undo! This is the information we need to perform the `redo' operation, the redoinfo.

That means, that if we refine the requirements of the tuple of undoinfo just a little bit, we get the `redo' for free:

> Undoinfo (2)

> A tuple `t` is a tuple of undoinfo of an operation, iff `apply(t[0], t[1:])` undoes the operation and returns redoinfo.

As it turns out, in Sketch at least, this additional demand is trivial to meet.

## Combining Undoinfo

The `SetFillStyle` method of the `Group` class has to return undoinfo that describes how to undo several operations at once. To achieve this, one can define a function `UndoList` as follows:

    def UndoList(infos):
        undoinfo = map(Undo, infos)
        undoinfo.reverse()
        return (UndoList, undoinfo)

Here, `Undo` is a function that executes a single undoinfo (defined in `undo.py`).

Given a list ``infos`' of undoinfos, `Group.SetFillStyle` can return `(UndoList, infos)`.

Calling `reverse` in `UndoList` is essential, as some operations must be undone in exactly the reverse order in which they were performed.

For more complex cases where several undoinfos have to be executed in a certain order one can easily define similar functions. See for instance `UndoAfter` in `undo.py`

## The Undo API

The module `undo.py` implements most functions and classes necessary for handling undo in Sketch.

The format for a single unit of undoinfo is as described above with a single extension: The first item in the tuple may be a string, in which case the second item is callable and the items [2:] are the arguments. The string is meant to provide a short description of the operation, such as `Create Rectangle', that may be displayed as a menu item like `Undo Create Rectangle'. Therefore, this string is only necessary at the top level undo info, which is currently always created by the document object.

The public interface to undo handling in Sketch consists of the following functions and objects which are exported by the package `Sketch`.

### Constructors

In many cases, methods that change graphics objects or are otherwise expected to return undoinfo, simply create a tuple of the correct format.

Example: The SetRadius method of the regular polygon Plugin:

    def SetRadius(self, radius):
        undo = self.SetRadius, self.radius  # create undoinfo
        self.radius = radius    # set the instance variable
        self.compute_poly()     # update the polygon
        self._changed()	    # notify interested objects that self changed,
    			    # and force update of bounding rects, etc.
        return undo             # finally, return undo info

For compound objects and methods that need to combine several pieces of undo info there are two special constructors:

`CreateListUndo(infos)`

  Return undo info that combines the undo infos in infos. infos is expected to be a sequence of undo infos listed in the order in which the actions were performed. When the operation is undone they are executed in reverse order.

  `CreateListUndo` tries to simplify infos. It builds a new list by iterating over the items of infos and inlining list undo info (as created by `CreateListUndo`) and by discarding empty undo info (represented by the `NullUndo` object, see below). If the resulting list is empty return `NullUndo`.

`CreateMultiUndo(info1[, info2[, ...]])`

  Return undoinfo that undoes info1, info2, etc., in reverse order. This is implemented via `CreateListUndo`.

Sometimes it happens that nothing has to be undone:

`NullUndo`

  `NullUndo` is an undo info tuple that does nothing. If a method needs to return undo info, but for some reason nothing needs to be undone, this object should be returned.

  The functions that combine undo info treat it specially by removing it from the list of undo infos.

The Document and Graphics Classes | Developer's Guide | Plugins