root/gaphas/tags/gaphas-0.3.2/state.txt

Revision 1803, 7.0 kB (checked in by arj..@yirdis.nl, 1 year ago)

updated (rst'ed) txt files.

Line 
1 State management
2 ================
3
4 A special word should be mentioned about state management. Managing state is
5 the first step in creating an undo system.
6
7 The state system consists of two parts:
8
9 1. A basic observer (the ``@observed`` decorator)
10 2. A reverter
11
12 .. contents::
13
14 Observer
15 --------
16
17 The observer simply dispatches the function called (as ``<function ..>``, not as
18 ``<unbound method..>``!) to each handler registered in an observers list.
19
20     >>> from gaphas import state
21     >>> state.observers.clear()
22     >>> state.subscribers.clear()
23
24     >>> from gaphas.tree import Tree
25     >>> tree = Tree()
26
27 For this demonstration let's use the Tree class (which contains an add/remove
28 method pair). It's methods are not dispatched by default though (because they're
29 only used in the Canvas class. So first dispatching should be enabled:
30
31     >>> state.enable_dispatching(Tree.add)
32     >>> state.enable_dispatching(Tree._remove)
33
34 It works:
35
36     >>> def handler(event):
37     ...     print 'event handled', event
38     >>> state.observers.add(handler)
39     >>> tree.add(1)                                     # doctest: +ELLIPSIS
40     event handled (<function add at ...>, (<gaphas.tree.Tree object at 0x...>, 1, None), {})
41     >>> tree.add(2, parent=1)                           # doctest: +ELLIPSIS
42     event handled (<function add at ...>, (<gaphas.tree.Tree object at 0x...>, 2, 1), {})
43     >>> tree.nodes
44     [1, 2]
45
46 Remember that this observer is just a simple method call notifier and knows
47 nothing about the internals of the tree class (in this case the remove() method
48 recursively calls remove() for each of it's children). Therefore some careful
49 crafting of methods may be nessecary in order to get the right effect:
50
51     >>> tree.remove(1)                                  # doctest: +ELLIPSIS
52     event handled (<function _remove at ...>, (<gaphas.tree.Tree object at 0x...>, 2), {})
53     event handled (<function _remove at ...>, (<gaphas.tree.Tree object at 0x...>, 1), {})
54     >>> tree.nodes
55     []
56
57 The ``@observed`` decorator can also be applied to properties, as is done in
58 gaphas/item.py's Handle class:
59
60     >>> from gaphas.solver import Variable
61     >>> var = Variable()
62     >>> var.value = 10                                  # doctest: +ELLIPSIS
63     event handled (<function set_value at 0x...>, (Variable(0, 20), 10), {})
64
65 (this is simply done by observing the setter method).
66
67 Off course handlers can be removed as well (only the default revert handler
68 is present now):
69
70     >>> state.observers.remove(handler)
71     >>> state.observers                                 # doctest: +ELLIPSIS
72     set([])
73
74 What should you know:
75
76 1. The observer always generates events based on 'function' calls. Even for
77    class method invokations. This is because, when calling a method (say
78    Tree.add) it's the im_func field is executed, which is a function type
79    object.
80
81 2. It's important to know if an event came from invoking a method or a simple
82    function. With methods, the first argument always is an instance. This can
83    be handy when writing an undo management systems in case multiple calls
84    from the same instance do not have to be registered (e.g. if a method
85    set_point() is called with exact coordinates (in stead of deltas), only the
86    first call to set_point needs to be remembered.
87
88
89 Reverser
90 --------
91
92 The reverser requires some registration.
93
94 1. Property setters should be declared with reversible_property()
95 2. Method (or function) pairs that implement each others reverse operation
96    (e.g. add and remove) should be registered as reversible_pair()'s in the
97    reverser engine.
98    The reverser will construct a tuple (callable, arguments) which are send
99    to every handler registered in the subscribers list. Arguments is a dict().
100
101 First thing to do is to actually enable the ``revert_handler``:
102
103     >>> state.observers.add(state.revert_handler)
104
105 This handler is not enabled by default because:
106
107 1. it generates quite a bit of overhead if it isn't used anyway
108 2. you might want to add some additional filtering.
109
110 Point 2 may require some explanation. First of all observers have been added
111 to almost every method that involves a state change. As a result multiple
112 (conflicting) revert actions may be generated (e.g. Canvas.add calls Tree.add,
113 both of which are observed).
114
115 Handlers for the reverse events should be registered on the subscribers list:
116
117     >>> events = []
118     >>> def handler(event):
119     ...     events.append(event)
120     ...     print 'event handler', event
121     >>> state.subscribers.add(handler)
122
123 After that, signals can be received of undoable (reverse-)events:
124
125     >>> tree.add(3)                                     # doctest: +ELLIPSIS
126     event handler (<function _remove at ...>, {'node': 3, 'self': <gaphas.tree.Tree object at 0x...>})
127     >>> tree.nodes
128     [3]
129
130 As you can see this event is constructed of only two parameters: the function
131 that does the inverse operation of add() and the arguments that should be
132 applied to that function.
133
134 The inverse operation is easiest performed by the function saveapply(). Of
135 course an inverse operation is emitting a change event too:
136
137     >>> state.saveapply(*events.pop())                  # doctest: +ELLIPSIS
138     event handler (<function add at 0x...>, {'node': 3, 'self': <gaphas.tree.Tree object at 0x...>, 'parent': None})
139     >>> tree.nodes
140     []
141
142 Just handling method pairs is one thing. handling properties (descriptors) in
143 a simple fashion is another matter. First of all the original value should
144 be retrieved before the new value is applied (this is different from applying
145 the same arguments to another method in order to reverse an operation).
146
147 For this a reversible_property has been introduced. It works just like a
148 property (in fact it creates a plain old property descriptor), but also
149 registers the property as being reversible.
150
151     >>> var = Variable()
152     >>> var.value = 10                                  # doctest: +ELLIPSIS
153     event handler (<function set_value at 0x...>, {'self': Variable(0, 20), 'value': 0.0})
154    
155 Handlers can be simply removed:
156
157     >>> state.subscribers.remove(handler)
158     >>> state.observers.remove(state.revert_handler)
159
160 What is Observed
161 ----------------
162
163 As far as Gaphas is concerned, only properties and methods related to the
164 model (e.g. Canvas, Items) emit state changes. Some extra effort has been taken
165 to monitor the Matrix class (which is from Cairo).
166
167 canvas.py:
168   Canvas:
169     add() and remove()
170
171 item.py:
172   Handle:
173     x, y, connectable, movable, visible, connected_to and disconnect properties
174   Item:
175     canvas and matrix properties
176   Element:
177     min_height and min_width properties
178   Line:
179     line_width, fuzziness, orthogonal and horizontal properties;
180     split_segment() and merge_segment()
181
182 solver.py:
183   Variable:
184     strength and value properties
185   Solver:
186     add_constraint() and remove_constraint()
187
188 tree.py:
189   Tree:
190     add() and remove()
191
192 matrix.py:
193   Matrix:
194    invert, translate, rotate and scale
195
196 Testcases are described in undo.txt.
197
198 (disable dispatching again, not frustrating other tests)
199
200     >>> state.disable_dispatching(Tree.add)
201     >>> state.disable_dispatching(Tree._remove)
202
Note: See TracBrowser for help on using the browser.