1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
|
<html>
<head>
<title>Developer's Guide: The Undo Mechanism
</title>
</head>
<body bgcolor=white text=black link=blue vlink=navy alink=red>
<TABLE WIDTH="100%">
<TR>
<TH ALIGN="left" WIDTH="33%"><img SRC="Images/arrow-left.png" WIDTH="16" HEIGHT="16" ALIGN="top" ALT="Prev"></TH>
<TH ALIGN="center" WIDTH="33%"><img SRC="Images/arrow-up.png" WIDTH="16" HEIGHT="16" ALIGN="top" ALT="Up"></TH>
<TH ALIGN="right" WIDTH="33%"><img SRC="Images/arrow-right.png" WIDTH="16" HEIGHT="16" ALIGN="top" ALT="Next"></TH>
</TR>
<TR>
<TD ALIGN="left"><A HREF="devguide-9.html">The Document and Graphics Classes</A></TD>
<TD ALIGN="center"><A HREF="devguide.html">Developer's Guide</A></TD>
<TD ALIGN="right"><A HREF="devguide-20.html">Plugins
</A></TD>
</TR>
</TABLE>
<HR NOSHADE>
<H1><FONT face="Helvetica,Arial"><A NAME="N1"></A>The Undo Mechanism
</font></H1>
<P>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.</P>
<P>I'll call this information <EM>undo information</EM>, or <EM>undoinfo</EM> for
short.</P>
<H2><FONT face="Helvetica,Arial"><A NAME="N2"></A>Creation and Storage</font></H2>
<P>In Sketch, undoinfo is stored basically in a list in the document object
(actually in an instance of the <CODE>UndoRedo</CODE> class owned by the
document).</P>
<P>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.</P>
<P>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.</P>
<P>Consider for example the method <tt>SetProperties</tt> 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
<tt>SetProperties</tt> method calls the <tt>SetProperties</tt> 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".</P>
<H2><FONT face="Helvetica,Arial"><A NAME="N3"></A>Representation of Undoinfo</font></H2>
<P>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:</P>
<P>
<BLOCKQUOTE>
Undoinfo (1)<P>A tuple <CODE>t</CODE> is a tuple of undoinfo of an operation, iff the Python
expression `<CODE>apply(t[0], t[1:])</CODE>' will undo the operation.</P>
</BLOCKQUOTE>
</P>
<P>Example: The <CODE>SetRadius</CODE> method of the regular polygon plugin:
<table width="100%" cellpadding="10"><tr><td bgcolor="#FFFFD0">
<PRE>
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
</PRE>
</td></tr></table>
</P>
<P>Undoing <CODE>SetRadius</CODE> simply means to set the radius back to the old
one, that is, one has to call the method <CODE>SetRadius</CODE> of the same
object again, only this time with the old radius as parameter. Thus the
undoinfo returned is `<CODE>(self.SetRadius, self.radius)</CODE>' which is
executed before <CODE>self.radius</CODE> changes. Note, that in this example the
internal data get recomputed automatically during the undo.</P>
<P>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 <EM>redoinfo</EM>.</P>
<P>That means, that if we refine the requirements of the tuple of undoinfo
just a little bit, we get the `redo' for free:</P>
<P>
<BLOCKQUOTE>
Undoinfo (2)<P>A tuple <CODE>t</CODE> is a tuple of undoinfo
of an operation, iff <CODE>apply(t[0], t[1:])</CODE> undoes the operation
and returns redoinfo.</P>
</BLOCKQUOTE>
</P>
<P>As it turns out, in Sketch at least, this additional demand is trivial
to meet.</P>
<H2><FONT face="Helvetica,Arial"><A NAME="N4"></A>Combining Undoinfo</font></H2>
<P>The <CODE>SetFillStyle</CODE> method of the <CODE>Group</CODE> class has to return
undoinfo that describes how to undo several operations at once. To
achieve this, one can define a function <tt>UndoList</tt> as follows:</P>
<P>
<table width="100%" cellpadding="10"><tr><td bgcolor="#FFFFD0">
<PRE>
def UndoList(infos):
undoinfo = map(Undo, infos)
undoinfo.reverse()
return (UndoList, undoinfo)
</PRE>
</td></tr></table>
Here, <CODE>Undo</CODE> is a function that executes a single undoinfo (defined
in <CODE>undo.py</CODE>).</P>
<P>Given a list `<CODE>infos</CODE>' of undoinfos, <CODE>Group.SetFillStyle</CODE> can
return <CODE>(UndoList, infos)</CODE>.</P>
<P>Calling <CODE>reverse</CODE> in <CODE>UndoList</CODE> is essential, as some operations
must be undone in exactly the reverse order in which they were
performed.</P>
<P>For more complex cases where several undoinfos have to be executed in a
certain order one can easily define similar functions. See for instance
<CODE>UndoAfter</CODE> in <CODE>undo.py</CODE></P>
<H2><FONT face="Helvetica,Arial"><A NAME="N5"></A>The Undo API</font></H2>
<P>The module <CODE>undo.py</CODE> implements most functions and classes necessary
for handling undo in Sketch.</P>
<P>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.</P>
<P>The public interface to undo handling in Sketch consists of the
following functions and objects which are exported by the package
<CODE>Sketch</CODE>.</P>
<H3><FONT face="Helvetica,Arial"><A NAME="N6"></A>Constructors</font></H3>
<P>In many cases, methods that change graphics objects or are otherwise
expected to return undoinfo, simply create a tuple of the correct
format.</P>
<P>Example: The SetRadius method of the regular polygon Plugin:
<table width="100%" cellpadding="10"><tr><td bgcolor="#FFFFD0">
<PRE>
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
</PRE>
</td></tr></table>
</P>
<P>For compound objects and methods that need to combine several pieces of
undo info there are two special constructors:
<DL>
<DT><B><A NAME="N7"></A><tt>CreateListUndo(<i>infos</i>)</tt></B><DD>
<P>Return undo info that combines the undo infos in <i>infos</i>.
<i>infos</i> 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.</P>
<P><tt>CreateListUndo</tt> tries to simplify <i>infos</i>. It builds a
new list by iterating over the items of <i>infos</i> and inlining
list undo info (as created by <CODE>CreateListUndo</CODE>) and by
discarding empty undo info (represented by the <A HREF="#N9"><CODE>NullUndo</CODE></A> object, see below). If the
resulting list is empty return <CODE>NullUndo</CODE>.</P>
<DT><B><A NAME="N8"></A><tt>CreateMultiUndo(<i>info1</i>[, <i>info2</i>[, ...]])</tt></B><DD>
<P>Return undoinfo that undoes <i>info1</i>, <i>info2</i>, etc., in
reverse order. This is implemented via <tt>CreateListUndo</tt>.</P>
</DL>
</P>
<P>Sometimes it happens that nothing has to be undone:
<DL>
<DT><B><CODE>NullUndo</CODE></B><DD>
<P><A NAME="N9"></A></P>
<P><CODE>NullUndo</CODE> 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.</P>
<P>The functions that combine undo info treat it specially by
removing it from the list of undo infos.</P>
</DL>
</P>
<HR NOSHADE>
<TABLE WIDTH="100%">
<TR>
<TD ALIGN="left"><A HREF="devguide-9.html">The Document and Graphics Classes</A></TD>
<TD ALIGN="center"><A HREF="devguide.html">Developer's Guide</A></TD>
<TD ALIGN="right"><A HREF="devguide-20.html">Plugins
</A></TD>
</TR>
<TR>
<TH ALIGN="left" WIDTH="33%"><img SRC="Images/arrow-left.png" WIDTH="16" HEIGHT="16" ALIGN="top" ALT="Prev"></TH>
<TH ALIGN="center" WIDTH="33%"><img SRC="Images/arrow-up.png" WIDTH="16" HEIGHT="16" ALIGN="top" ALT="Up"></TH>
<TH ALIGN="right" WIDTH="33%"><img SRC="Images/arrow-right.png" WIDTH="16" HEIGHT="16" ALIGN="top" ALT="Next"></TH>
</TR>
</TABLE>
</body>
</html>
|