| 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 |
|
|---|