root/gaphas/tags/release-0.1.5/state.txt

Revision 1162, 7.2 kB (checked in by arjanmol, 2 years ago)

Integrated nose tests, use setup.py nosetests to test all gaphas tests at once

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