Remove gaphas.state module
This commit is contained in:
parent
1e0fa71449
commit
dc5e026b51
@ -39,7 +39,6 @@ Table of Contents
|
||||
tools
|
||||
connectors
|
||||
solver
|
||||
state
|
||||
|
||||
.. toctree::
|
||||
:caption: Advanced
|
||||
|
195
docs/state.rst
195
docs/state.rst
@ -1,195 +0,0 @@
|
||||
State management
|
||||
================
|
||||
|
||||
.. important:: This functionality will be removed.
|
||||
|
||||
A special word should be mentioned about state management. Managing state is
|
||||
the first step in creating an undo system.
|
||||
|
||||
The state system consists of two parts:
|
||||
|
||||
1. A basic observer (the ``@observed`` decorator)
|
||||
2. A reverser
|
||||
|
||||
|
||||
Observer
|
||||
--------
|
||||
|
||||
The observer simply dispatches the function called (as ``<function ..>``, not as
|
||||
``<unbound method..>``!) to each handler registered in an observers list.
|
||||
|
||||
>>> from gaphas import state
|
||||
>>> state.observers.clear()
|
||||
>>> state.subscribers.clear()
|
||||
|
||||
Let's start with creating a Canvas instance and some items:
|
||||
|
||||
>>> from gaphas.canvas import Canvas
|
||||
>>> from examples.exampleitems import Circle
|
||||
>>> canvas = Canvas()
|
||||
>>> item1, item2 = Circle(), Circle()
|
||||
|
||||
For this demonstration let's use the Canvas class (which contains an add/remove
|
||||
method pair).
|
||||
|
||||
It works (see how the add method automatically schedules the item for update):
|
||||
|
||||
>>> def handler(event):
|
||||
... print('event handled', event)
|
||||
>>> state.observers.add(handler)
|
||||
>>> canvas.add(item1) # doctest: +ELLIPSIS
|
||||
event handled (<function Canvas.add at ...>, (<gaphas.canvas.Canvas object at ...>, <examples.exampleitems.Circle object at ...>), {})
|
||||
>>> canvas.add(item2, parent=item1) # doctest: +ELLIPSIS
|
||||
event handled (<function Canvas.add at ...>, (<gaphas.canvas.Canvas object at ...>, <examples.exampleitems.Circle object at ...>), {'parent': <examples.exampleitems.Circle object at ...>})
|
||||
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
|
||||
[<examples.exampleitems.Circle object at 0x...>, <examples.exampleitems.Circle object at 0x...>]
|
||||
|
||||
Note that the handler is invoked before the actual call is made. This is
|
||||
important if you want to store the (old) state for an undo mechanism.
|
||||
|
||||
Remember that this observer is just a simple method call notifier and knows
|
||||
nothing about the internals of the ``Canvas`` class (in this case the
|
||||
``remove()`` method recursively calls ``remove()`` for each of it's children).
|
||||
Therefore some careful crafting of methods may be necessary in order to get the
|
||||
right effect (items should be removed in the right order, child first):
|
||||
|
||||
>>> canvas.remove(item1) # doctest: +ELLIPSIS
|
||||
event handled (<function Canvas._remove at ...>, (<gaphas.canvas.Canvas object at 0x...>, <examples.exampleitems.Circle object at 0x...>), {})
|
||||
event handled (<function Canvas._remove at ...>, (<gaphas.canvas.Canvas object at 0x...>, <examples.exampleitems.Circle object at 0x...>), {})
|
||||
>>> list(canvas.get_all_items())
|
||||
[]
|
||||
|
||||
The ``@observed`` decorator can also be applied to properties, as is done in
|
||||
gaphas/connector.py's Handle class:
|
||||
|
||||
>>> from gaphas.solver import Variable
|
||||
>>> var = Variable()
|
||||
>>> var.value = 10 # doctest: +ELLIPSIS
|
||||
event handled (<function Variable.set_value at 0x...>, (Variable(0, 20), 10), {})
|
||||
|
||||
(this is simply done by observing the setter method).
|
||||
|
||||
Off course handlers can be removed as well (only the default revert handler
|
||||
is present now):
|
||||
|
||||
>>> state.observers.remove(handler)
|
||||
>>> state.observers # doctest: +ELLIPSIS
|
||||
set()
|
||||
|
||||
What should you know:
|
||||
|
||||
1. The observer always generates events based on 'function' calls. Even for
|
||||
class method invocations. This is because, when calling a method (say
|
||||
Tree.add) it's the ``im_func`` field is executed, which is a function type
|
||||
object.
|
||||
|
||||
2. It's important to know if an event came from invoking a method or a simple
|
||||
function. With methods, the first argument always is an instance. This can
|
||||
be handy when writing an undo management systems in case multiple calls
|
||||
from the same instance do not have to be registered (e.g. if a method
|
||||
``set_point()`` is called with exact coordinates (in stead of deltas), only
|
||||
the first call to ``set_point()`` needs to be remembered).
|
||||
|
||||
|
||||
Reverser
|
||||
--------
|
||||
|
||||
The reverser requires some registration.
|
||||
|
||||
1. Property setters should be declared with ``reversible_property()``
|
||||
2. Method (or function) pairs that implement each others reverse operation
|
||||
(e.g. add and remove) should be registered as ``reversible_pair()``'s in the
|
||||
reverser engine.
|
||||
The reverser will construct a tuple (callable, arguments) which are send
|
||||
to every handler registered in the subscribers list. Arguments is a
|
||||
``dict()``.
|
||||
|
||||
First thing to do is to actually enable the ``revert_handler``:
|
||||
|
||||
>>> state.observers.add(state.revert_handler)
|
||||
|
||||
This handler is not enabled by default because:
|
||||
|
||||
1. it generates quite a bit of overhead if it isn't used anyway
|
||||
2. you might want to add some additional filtering.
|
||||
|
||||
Point 2 may require some explanation. First of all observers have been added
|
||||
to almost every method that involves a state change. As a result loads of
|
||||
events are generated. In most cases you're only interested in the first event,
|
||||
since that one contains the state before it started changing.
|
||||
|
||||
Handlers for the reverse events should be registered on the subscribers list:
|
||||
|
||||
>>> events = []
|
||||
>>> def handler(event):
|
||||
... events.append(event)
|
||||
... print('event handler', event)
|
||||
>>> state.subscribers.add(handler)
|
||||
|
||||
After that, signals can be received of undoable (reverse-)events:
|
||||
|
||||
>>> canvas.add(Circle()) # doctest: +ELLIPSIS
|
||||
event handler (<function Handle._set_movable at ...>, {'self': <Handle object on (Variable(0, 20), Variable(0, 20))>, 'movable': True})
|
||||
event handler (<function Canvas._remove at ...>, {'self': <gaphas.canvas.Canvas object at 0x...>, 'item': <examples.exampleitems.Circle object at 0x...>})
|
||||
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
|
||||
[<examples.exampleitems.Circle object at 0x...>]
|
||||
|
||||
As you can see this event is constructed of only two parameters: the function
|
||||
that does the inverse operation of ``add()`` and the arguments that should be
|
||||
applied to that function.
|
||||
|
||||
The inverse operation is easiest performed by the function ``saveapply()``. Of
|
||||
course an inverse operation is emitting a change event too:
|
||||
|
||||
>>> state.saveapply(*events.pop()) # doctest: +ELLIPSIS
|
||||
event handler (<function Canvas.add at 0x...>, {'self': <gaphas.canvas.Canvas object at 0x...>, 'item': <examples.exampleitems.Circle object at 0x...>, 'parent': None, 'index': 0})
|
||||
>>> list(canvas.get_all_items())
|
||||
[]
|
||||
|
||||
Just handling method pairs is one thing. Handling properties (descriptors) in
|
||||
a simple fashion is another matter. First of all the original value should
|
||||
be retrieved before the new value is applied (this is different from applying
|
||||
the same arguments to another method in order to reverse an operation).
|
||||
|
||||
For this a ``reversible_property`` has been introduced. It works just like a
|
||||
property (in fact it creates a plain old property descriptor), but also
|
||||
registers the property as being reversible.
|
||||
|
||||
>>> var = Variable()
|
||||
>>> var.value = 10 # doctest: +ELLIPSIS
|
||||
event handler (<function Variable.set_value at 0x...>, {'self': Variable(0, 20), 'value': 0.0})
|
||||
|
||||
Handlers can be simply removed:
|
||||
|
||||
>>> state.subscribers.remove(handler)
|
||||
>>> state.observers.remove(state.revert_handler)
|
||||
|
||||
What is Observed
|
||||
----------------
|
||||
|
||||
As far as Gaphas is concerned, only properties and methods related to the
|
||||
model (e.g. ``Canvas``, ``Item``) emit state changes. Some extra effort has
|
||||
been taken to monitor the ``Matrix`` class (which is from Cairo).
|
||||
|
||||
canvas.py:
|
||||
``Canvas``: ``add()``, ``remove()``, ``reparent()``, ``request_update()``
|
||||
|
||||
connector.py:
|
||||
``Handle``: ``connectable``, ``movable``, ``visible``, ``connected_to`` and ``disconnect`` properties
|
||||
|
||||
connections.py:
|
||||
``Connections``: ``connect_item()``, ``disconnect_item()``, ``reconnect_item()``
|
||||
|
||||
item.py:
|
||||
``Element``: ``matrix``, ``min_height`` and ``min_width`` properties
|
||||
|
||||
``Line``: matrix``, ``line_width``, ``fuzziness``, ``orthogonal`` and ``horizontal`` properties
|
||||
|
||||
variable.py:
|
||||
``Variable``: ``strength`` and ``value`` properties
|
||||
|
||||
solver.py:
|
||||
``Solver``: ``add_constraint()`` and ``remove_constraint()``
|
||||
|
||||
matrix.py:
|
||||
``Matrix``: ``invert()``, ``translate()``, ``rotate()``, ``scale()`` and ``set()``
|
@ -1,414 +0,0 @@
|
||||
Undo example
|
||||
============
|
||||
|
||||
This document describes a basic undo system and tests Gaphas' classes with this
|
||||
system.
|
||||
|
||||
This document contains a set of test cases that is used to prove that it really
|
||||
works.
|
||||
|
||||
For this to work, some boilerplate has to be configured:
|
||||
|
||||
>>> from gaphas import state
|
||||
>>> state.observers.clear()
|
||||
>>> state.subscribers.clear()
|
||||
|
||||
>>> undo_list = []
|
||||
>>> redo_list = []
|
||||
>>> def undo_handler(event):
|
||||
... undo_list.append(event)
|
||||
>>> state.observers.add(state.revert_handler)
|
||||
>>> state.subscribers.add(undo_handler)
|
||||
|
||||
This simple undo function will revert all states collected in the undo_list:
|
||||
|
||||
>>> def undo():
|
||||
... apply_me = list(undo_list)
|
||||
... del undo_list[:]
|
||||
... apply_me.reverse()
|
||||
... for e in apply_me:
|
||||
... state.saveapply(*e)
|
||||
... redo_list[:] = undo_list[:]
|
||||
... del undo_list[:]
|
||||
|
||||
Undo functionality tests
|
||||
========================
|
||||
|
||||
The following sections contain most of the basis unit tests for undo
|
||||
management.
|
||||
|
||||
tree.py: Tree
|
||||
-------------
|
||||
Tree has no observed methods.
|
||||
|
||||
matrix.py: Matrix
|
||||
-----------------
|
||||
Matrix is used by Item classes.
|
||||
|
||||
>>> from gaphas.matrix import Matrix
|
||||
>>> m = Matrix()
|
||||
>>> m
|
||||
Matrix(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
|
||||
|
||||
translate(tx, ty):
|
||||
|
||||
>>> m.translate(12, 16)
|
||||
>>> m
|
||||
Matrix(1.0, 0.0, 0.0, 1.0, 12.0, 16.0)
|
||||
>>> undo()
|
||||
>>> m
|
||||
Matrix(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
|
||||
|
||||
scale(sx, sy):
|
||||
|
||||
>>> m.scale(1.5, 1.5)
|
||||
>>> m
|
||||
Matrix(1.5, 0.0, 0.0, 1.5, 0.0, 0.0)
|
||||
>>> undo()
|
||||
>>> m
|
||||
Matrix(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
|
||||
|
||||
rotate(radians):
|
||||
|
||||
>>> def matrix_approx(m):
|
||||
... a = []
|
||||
... for i in tuple(m):
|
||||
... if -1e-10 < i < 1e-10: i=0
|
||||
... a.append(i)
|
||||
... return tuple(a)
|
||||
|
||||
>>> m.rotate(0.5)
|
||||
>>> m
|
||||
Matrix(0.8775825618903728, 0.479425538604203, -0.479425538604203, 0.8775825618903728, 0.0, 0.0)
|
||||
>>> undo()
|
||||
>>> matrix_approx(m)
|
||||
(1.0, 0, 0, 1.0, 0, 0)
|
||||
|
||||
Okay, nearly, close enough IMHO...
|
||||
|
||||
>>> m = Matrix()
|
||||
>>> m.translate(12, 10)
|
||||
>>> m.scale(1.5, 1.5)
|
||||
>>> m.rotate(0.5)
|
||||
>>> m
|
||||
Matrix(1.3163738428355591, 0.7191383079063045, -0.7191383079063045, 1.3163738428355591, 12.0, 10.0)
|
||||
>>> m.invert()
|
||||
>>> m
|
||||
Matrix(0.5850550412602484, -0.3196170257361353, 0.3196170257361353, 0.5850550412602484, -10.216830752484334, -2.0151461037688607)
|
||||
>>> undo()
|
||||
>>> matrix_approx(m)
|
||||
(1.0, 0, 0, 1.0, 0, 0)
|
||||
|
||||
Again, rotate does not result in an exact match, but it's close enough.
|
||||
|
||||
>>> undo_list
|
||||
[]
|
||||
|
||||
canvas.py: Canvas
|
||||
-----------------
|
||||
|
||||
>>> from gaphas import Canvas
|
||||
>>> from examples.exampleitems import Circle
|
||||
>>> canvas = Canvas()
|
||||
>>> list(canvas.get_all_items())
|
||||
[]
|
||||
>>> item = Circle()
|
||||
>>> canvas.add(item)
|
||||
|
||||
The ``request_update()`` method is observed:
|
||||
|
||||
>>> len(undo_list)
|
||||
2
|
||||
>>> canvas.request_update(item)
|
||||
>>> len(undo_list)
|
||||
3
|
||||
|
||||
On the canvas only ``add()`` and ``remove()`` are monitored:
|
||||
|
||||
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
|
||||
[<examples.exampleitems.Circle object at 0x...>]
|
||||
>>> undo()
|
||||
>>> list(canvas.get_all_items())
|
||||
[]
|
||||
>>> canvas.add(item)
|
||||
>>> del undo_list[:]
|
||||
>>> canvas.remove(item)
|
||||
>>> list(canvas.get_all_items())
|
||||
[]
|
||||
>>> undo()
|
||||
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
|
||||
[<examples.exampleitems.Circle object at 0x...>]
|
||||
>>> undo_list
|
||||
[]
|
||||
|
||||
Parent-child relationships are restored as well:
|
||||
|
||||
TODO!
|
||||
|
||||
|
||||
>>> child = Circle()
|
||||
>>> canvas.add(child, parent=item)
|
||||
>>> canvas.get_parent(child) is item
|
||||
True
|
||||
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
|
||||
[<examples.exampleitems.Circle object at 0x...>, <examples.exampleitems.Circle object at 0x...>]
|
||||
>>> undo()
|
||||
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
|
||||
[<examples.exampleitems.Circle object at 0x...>]
|
||||
>>> child in canvas.get_all_items()
|
||||
False
|
||||
|
||||
Now redo the previous undo action:
|
||||
|
||||
>>> undo_list[:] = redo_list[:]
|
||||
>>> undo()
|
||||
>>> canvas.get_parent(child) is item
|
||||
True
|
||||
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
|
||||
[<examples.exampleitems.Circle object at 0x...>, <examples.exampleitems.Circle object at 0x...>]
|
||||
|
||||
Remove also works when items are removed recursively (an item and it's
|
||||
children):
|
||||
|
||||
>>> child = Circle()
|
||||
>>> canvas.add(child, parent=item)
|
||||
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
|
||||
[<examples.exampleitems.Circle object at 0x...>, <examples.exampleitems.Circle object at 0x...>]
|
||||
>>> del undo_list[:]
|
||||
>>> canvas.remove(item)
|
||||
>>> list(canvas.get_all_items())
|
||||
[]
|
||||
>>> undo()
|
||||
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
|
||||
[<examples.exampleitems.Circle object at 0x...>, <examples.exampleitems.Circle object at 0x...>]
|
||||
>>> canvas.get_children(item) # doctest: +ELLIPSIS
|
||||
[<examples.exampleitems.Circle object at 0x...>]
|
||||
|
||||
As well as the reparent() method:
|
||||
|
||||
>>> canvas = Canvas()
|
||||
>>> class NameItem:
|
||||
... def __init__(self, name):
|
||||
... super(NameItem, self).__init__()
|
||||
... self.name = name
|
||||
... def handles(self): return []
|
||||
... def ports(self): return []
|
||||
... def point(self, x, y): return 0
|
||||
... def __repr__(self):
|
||||
... return '<%s>' % self.name
|
||||
>>> ni1 = NameItem('a')
|
||||
>>> canvas.add(ni1)
|
||||
>>> ni2 = NameItem('b')
|
||||
>>> canvas.add(ni2)
|
||||
>>> ni3 = NameItem('c')
|
||||
>>> canvas.add(ni3, parent=ni1)
|
||||
>>> ni4 = NameItem('d')
|
||||
>>> canvas.add(ni4, parent=ni3)
|
||||
>>> list(canvas.get_all_items())
|
||||
[<a>, <c>, <d>, <b>]
|
||||
>>> del undo_list[:]
|
||||
>>> canvas.reparent(ni3, parent=ni2)
|
||||
>>> list(canvas.get_all_items())
|
||||
[<a>, <b>, <c>, <d>]
|
||||
>>> len(undo_list)
|
||||
1
|
||||
>>> undo()
|
||||
>>> list(canvas.get_all_items())
|
||||
[<a>, <c>, <d>, <b>]
|
||||
|
||||
Redo should work too:
|
||||
|
||||
>>> undo_list[:] = redo_list[:]
|
||||
>>> undo()
|
||||
>>> list(canvas.get_all_items())
|
||||
[<a>, <b>, <c>, <d>]
|
||||
|
||||
|
||||
Undo/redo a connection: see gaphas/tests/test_undo.py
|
||||
|
||||
|
||||
connector.py: Handle
|
||||
--------------------
|
||||
Changing the Handle's position is reversible:
|
||||
|
||||
>>> from gaphas import Handle
|
||||
>>> handle = Handle()
|
||||
>>> handle.pos = 10, 12
|
||||
>>> handle.pos
|
||||
<Position object on (Variable(10, 20), Variable(12, 20))>
|
||||
>>> undo()
|
||||
>>> handle.pos
|
||||
<Position object on (Variable(0, 20), Variable(0, 20))>
|
||||
|
||||
As are all other properties:
|
||||
|
||||
>>> handle.connectable, handle.movable, handle.visible
|
||||
(False, True, True)
|
||||
>>> handle.connectable = True
|
||||
>>> handle.movable = False
|
||||
>>> handle.visible = False
|
||||
>>> handle.connectable, handle.movable, handle.visible
|
||||
(True, False, False)
|
||||
|
||||
And now undo the whole lot at once:
|
||||
|
||||
>>> undo()
|
||||
>>> handle.connectable, handle.movable, handle.visible
|
||||
(False, True, True)
|
||||
|
||||
item.py: Item
|
||||
-------------
|
||||
|
||||
The basic Item properties are canvas and matrix. Canvas has been tested before,
|
||||
while testing the Canvas class.
|
||||
|
||||
The Matrix has been tested in section matrix.py: Matrix.
|
||||
|
||||
item.py: Element
|
||||
----------------
|
||||
|
||||
An element has ``min_height`` and ``min_width`` properties.
|
||||
|
||||
>>> from gaphas import Element
|
||||
>>> from gaphas.connections import Connections
|
||||
>>> e = Element(Connections())
|
||||
>>> e.min_height, e.min_width
|
||||
(Variable(10, 100), Variable(10, 100))
|
||||
>>> e.min_height, e.min_width = 30, 40
|
||||
>>> e.min_height, e.min_width
|
||||
(Variable(30, 100), Variable(40, 100))
|
||||
|
||||
>>> undo()
|
||||
>>> e.min_height, e.min_width
|
||||
(Variable(0, 100), Variable(0, 100))
|
||||
|
||||
>>> canvas = Canvas()
|
||||
>>> canvas.add(e)
|
||||
>>> undo()
|
||||
|
||||
item.py: Line
|
||||
-------------
|
||||
|
||||
A line has the following properties: ``line_width``, ``fuzziness``,
|
||||
``orthogonal`` and ``horizontal``. Each one of then is observed for changes:
|
||||
|
||||
>>> from gaphas import Line
|
||||
>>> from gaphas.segment import Segment
|
||||
>>> l = Line(Connections())
|
||||
|
||||
Let's first add a segment to the line, to test orthogonal lines as well.
|
||||
|
||||
>>> segment = Segment(l, canvas)
|
||||
>>> _ = segment.split_segment(0)
|
||||
|
||||
>>> l.line_width, l.fuzziness, l.orthogonal, l.horizontal
|
||||
(2.0, 0.0, False, False)
|
||||
|
||||
Now change the properties:
|
||||
|
||||
>>> l.line_width = 4
|
||||
>>> l.fuzziness = 2
|
||||
>>> l.orthogonal = True
|
||||
>>> l.horizontal = True
|
||||
>>> l.line_width, l.fuzziness, l.orthogonal, l.horizontal
|
||||
(4, 2, True, True)
|
||||
|
||||
And undo the changes:
|
||||
|
||||
>>> undo()
|
||||
>>> l.line_width, l.fuzziness, l.orthogonal, l.horizontal
|
||||
(2.0, 0.0, False, False)
|
||||
|
||||
In addition to those properties, line segments can be split and merged.
|
||||
|
||||
>>> l.handles()[1].pos = 10, 10
|
||||
>>> l.handles()
|
||||
[<Handle object on (Variable(0, 20), Variable(0, 20))>, <Handle object on (Variable(10, 20), Variable(10, 20))>]
|
||||
|
||||
This is our basis for further testing.
|
||||
|
||||
>>> del undo_list[:]
|
||||
|
||||
>>> Segment(l, canvas).split_segment(0) # doctest: +ELLIPSIS
|
||||
([<Handle object on (Variable(5, 10), Variable(5, 10))>], [<gaphas.connector.LinePort object at 0x...>])
|
||||
>>> l.handles()
|
||||
[<Handle object on (Variable(0, 20), Variable(0, 20))>, <Handle object on (Variable(5, 10), Variable(5, 10))>, <Handle object on (Variable(10, 20), Variable(10, 20))>]
|
||||
|
||||
The opposite operation is performed with the merge_segment() method:
|
||||
|
||||
>>> undo()
|
||||
>>> l.handles()
|
||||
[<Handle object on (Variable(0, 20), Variable(0, 20))>, <Handle object on (Variable(10, 20), Variable(10, 20))>]
|
||||
|
||||
Also creation and removal of connected lines is recorded and can be undone:
|
||||
|
||||
>>> canvas = Canvas()
|
||||
>>> def real_connect(hitem, handle, item):
|
||||
... def real_disconnect():
|
||||
... pass
|
||||
... canvas.connections.connect_item(hitem, handle, item, port=None, constraint=None, callback=real_disconnect)
|
||||
>>> b0 = Circle()
|
||||
>>> canvas.add(b0)
|
||||
>>> b1 = Circle()
|
||||
>>> canvas.add(b1)
|
||||
>>> l = Line(Connections())
|
||||
>>> canvas.add(l)
|
||||
>>> real_connect(l, l.handles()[0], b0)
|
||||
>>> real_connect(l, l.handles()[1], b1)
|
||||
>>> canvas.connections.get_connection(l.handles()[0]) # doctest: +ELLIPSIS
|
||||
Connection(item=<gaphas.item.Line object at 0x...>)
|
||||
>>> canvas.connections.get_connection(l.handles()[1]) # doctest: +ELLIPSIS
|
||||
Connection(item=<gaphas.item.Line object at 0x...>)
|
||||
|
||||
Clear already collected undo data:
|
||||
|
||||
>>> del undo_list[:]
|
||||
|
||||
Now remove the line from the canvas:
|
||||
|
||||
>>> canvas.remove(l)
|
||||
|
||||
The handles are disconnected:
|
||||
|
||||
>>> canvas.connections.get_connection(l.handles()[0])
|
||||
>>> canvas.connections.get_connection(l.handles()[1])
|
||||
|
||||
Undoing the remove() action should put everything back in place again:
|
||||
|
||||
>>> undo()
|
||||
|
||||
>>> canvas.connections.get_connection(l.handles()[0]) # doctest: +ELLIPSIS
|
||||
Connection(item=<gaphas.item.Line object at 0x...>)
|
||||
>>> canvas.connections.get_connection(l.handles()[1]) # doctest: +ELLIPSIS
|
||||
Connection(item=<gaphas.item.Line object at 0x...>)
|
||||
|
||||
|
||||
solver.py: Variable
|
||||
-------------------
|
||||
|
||||
Variable's strength and value properties are observed:
|
||||
|
||||
>>> from gaphas.solver import Variable
|
||||
>>> v = Variable()
|
||||
>>> v.value = 10
|
||||
>>> v
|
||||
Variable(10, 20)
|
||||
>>> undo()
|
||||
>>> v
|
||||
Variable(0, 20)
|
||||
|
||||
solver.py: Solver
|
||||
-----------------
|
||||
|
||||
Solvers ``add_constraint()`` and ``remove_constraint()`` are observed.
|
||||
|
||||
>>> from gaphas.solver import Solver
|
||||
>>> from gaphas.constraint import EquationConstraint
|
||||
>>> s = Solver()
|
||||
>>> a, b = Variable(1.0), Variable(2.0)
|
||||
>>> s.add_constraint(EquationConstraint(lambda a,b: a+b, a=a, b=b))
|
||||
EquationConstraint(<lambda>, a=Variable(1, 20), b=Variable(2, 20))
|
||||
>>> undo()
|
||||
|
||||
>>> undo_list[:] = redo_list[:]
|
||||
>>> undo()
|
@ -7,8 +7,6 @@ It sports a small canvas and some trivial operations:
|
||||
- Zoom in/out
|
||||
- Split a line segment
|
||||
- Delete focused item
|
||||
- Record state changes
|
||||
- Play back state changes (= undo !) With visual updates
|
||||
- Exports to SVG and PNG
|
||||
"""
|
||||
import math
|
||||
@ -17,7 +15,7 @@ import cairo
|
||||
import gi
|
||||
|
||||
from examples.exampleitems import Box, Circle, Text
|
||||
from gaphas import Canvas, GtkView, state
|
||||
from gaphas import Canvas, GtkView
|
||||
from gaphas.guide import GuidePainter
|
||||
from gaphas.item import Line
|
||||
from gaphas.painter import (
|
||||
@ -236,42 +234,6 @@ def create_window(canvas, title, zoom=1.0): # noqa too complex
|
||||
b.connect("clicked", on_delete_focused_clicked)
|
||||
v.add(b)
|
||||
|
||||
v.add(Gtk.Label.new("State:"))
|
||||
b = Gtk.ToggleButton.new_with_label("Record")
|
||||
|
||||
def on_toggled(button):
|
||||
global undo_list
|
||||
if button.get_active():
|
||||
print("start recording")
|
||||
del undo_list[:]
|
||||
state.subscribers.add(undo_handler)
|
||||
else:
|
||||
print("stop recording")
|
||||
state.subscribers.remove(undo_handler)
|
||||
|
||||
b.connect("toggled", on_toggled)
|
||||
v.add(b)
|
||||
|
||||
b = Gtk.Button.new_with_label("Play back")
|
||||
|
||||
def on_play_back_clicked(self):
|
||||
global undo_list
|
||||
apply_me = list(undo_list)
|
||||
del undo_list[:]
|
||||
print("Actions on the undo stack:", len(apply_me))
|
||||
apply_me.reverse()
|
||||
saveapply = state.saveapply
|
||||
for event in apply_me:
|
||||
print("Undo: invoking", event)
|
||||
saveapply(*event)
|
||||
print("New undo stack size:", len(undo_list))
|
||||
# Visualize each event:
|
||||
# while Gtk.events_pending():
|
||||
# Gtk.main_iteration()
|
||||
|
||||
b.connect("clicked", on_play_back_clicked)
|
||||
v.add(b)
|
||||
|
||||
v.add(Gtk.Label.new("Export:"))
|
||||
|
||||
b = Gtk.Button.new_with_label("Write demo.png")
|
||||
@ -415,9 +377,6 @@ def create_canvas(c=None):
|
||||
def main():
|
||||
# State handling (a.k.a. undo handlers)
|
||||
|
||||
# First, activate the revert handler:
|
||||
state.observers.add(state.revert_handler)
|
||||
|
||||
def print_handler(event):
|
||||
print("event:", event)
|
||||
|
||||
@ -427,8 +386,6 @@ def main():
|
||||
|
||||
create_canvas(c)
|
||||
|
||||
# state.subscribers.add(print_handler)
|
||||
|
||||
# Start the main application
|
||||
|
||||
create_window(c, "View created after")
|
||||
|
@ -36,7 +36,6 @@ from typing_extensions import Protocol
|
||||
from gaphas import matrix, tree
|
||||
from gaphas.connections import Connection, Connections
|
||||
from gaphas.decorators import nonrecursive
|
||||
from gaphas.state import observed, reversible_method, reversible_pair
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gaphas.item import Item
|
||||
@ -67,7 +66,6 @@ class Canvas:
|
||||
def connections(self) -> Connections:
|
||||
return self._connections
|
||||
|
||||
@observed
|
||||
def add(self, item, parent=None, index=None):
|
||||
"""Add an item to the canvas.
|
||||
|
||||
@ -85,7 +83,6 @@ class Canvas:
|
||||
self._tree.add(item, parent, index)
|
||||
self.request_update(item)
|
||||
|
||||
@observed
|
||||
def _remove(self, item):
|
||||
"""Remove is done in a separate, @observed, method so the undo system
|
||||
can restore removed items in the right order."""
|
||||
@ -110,29 +107,10 @@ class Canvas:
|
||||
self._connections.remove_connections_to_item(item)
|
||||
self._remove(item)
|
||||
|
||||
reversible_pair(
|
||||
add,
|
||||
_remove,
|
||||
bind1={
|
||||
"parent": lambda self, item: self.get_parent(item),
|
||||
"index": lambda self, item: self._tree.get_siblings(item).index(item),
|
||||
},
|
||||
)
|
||||
|
||||
@observed
|
||||
def reparent(self, item, parent, index=None):
|
||||
"""Set new parent for an item."""
|
||||
self._tree.move(item, parent, index)
|
||||
|
||||
reversible_method(
|
||||
reparent,
|
||||
reverse=reparent,
|
||||
bind={
|
||||
"parent": lambda self, item: self.get_parent(item),
|
||||
"index": lambda self, item: self._tree.get_siblings(item).index(item),
|
||||
},
|
||||
)
|
||||
|
||||
def get_all_items(self) -> Iterable[Item]:
|
||||
"""Get a list of all items.
|
||||
|
||||
@ -236,7 +214,6 @@ class Canvas:
|
||||
m = m.multiply(self.get_matrix_i2c(parent))
|
||||
return m
|
||||
|
||||
@observed
|
||||
def request_update(
|
||||
self, item: Item, update: bool = True, matrix: bool = True
|
||||
) -> None:
|
||||
@ -261,12 +238,6 @@ class Canvas:
|
||||
elif matrix:
|
||||
self._update_views(dirty_matrix_items=(item,))
|
||||
|
||||
reversible_method(
|
||||
request_update,
|
||||
reverse=request_update,
|
||||
bind={"update": lambda: True, "matrix": lambda: True},
|
||||
)
|
||||
|
||||
def request_matrix_update(self, item):
|
||||
"""Schedule only the matrix to be updated."""
|
||||
self.request_update(item, update=False, matrix=True)
|
||||
|
@ -9,7 +9,6 @@ from gaphas.connector import Handle, Port
|
||||
from gaphas.constraint import Constraint
|
||||
from gaphas.item import Item
|
||||
from gaphas.solver import Solver
|
||||
from gaphas.state import observed, reversible_method, reversible_pair
|
||||
|
||||
|
||||
class Connection(NamedTuple):
|
||||
@ -84,7 +83,6 @@ class Connections:
|
||||
self._solver.remove_constraint(constraint)
|
||||
self._connections.delete(item, None, None, None, constraint, None)
|
||||
|
||||
@observed
|
||||
def connect_item(
|
||||
self,
|
||||
item: Item,
|
||||
@ -131,7 +129,6 @@ class Connections:
|
||||
for cinfo in list(self._connections.query(item=item, handle=handle)):
|
||||
self._disconnect_item(*cinfo)
|
||||
|
||||
@observed
|
||||
def _disconnect_item(
|
||||
self,
|
||||
item: Item,
|
||||
@ -151,8 +148,6 @@ class Connections:
|
||||
|
||||
self._connections.delete(item, handle, connected, port, constraint, callback)
|
||||
|
||||
reversible_pair(connect_item, _disconnect_item)
|
||||
|
||||
def remove_connections_to_item(self, item: Item) -> None:
|
||||
"""Remove all connections (handles connected to and constraints) for a
|
||||
specific item (to and from the item).
|
||||
@ -168,7 +163,6 @@ class Connections:
|
||||
for cinfo in list(self._connections.query(connected=item)):
|
||||
disconnect_item(*cinfo)
|
||||
|
||||
@observed
|
||||
def reconnect_item(
|
||||
self,
|
||||
item: Item,
|
||||
@ -203,17 +197,6 @@ class Connections:
|
||||
if constraint:
|
||||
self._solver.add_constraint(constraint)
|
||||
|
||||
reversible_method(
|
||||
reconnect_item,
|
||||
reverse=reconnect_item,
|
||||
bind={
|
||||
"port": lambda self, item, handle: self.get_connection(handle).port,
|
||||
"constraint": lambda self, item, handle: self.get_connection(
|
||||
handle
|
||||
).constraint,
|
||||
},
|
||||
)
|
||||
|
||||
def get_connection(self, handle: Handle) -> Optional[Connection]:
|
||||
"""Get connection information for specified handle.
|
||||
|
||||
|
@ -7,7 +7,6 @@ from gaphas.constraint import Constraint, LineConstraint, PositionConstraint
|
||||
from gaphas.geometry import distance_line_point, distance_point_point
|
||||
from gaphas.position import MatrixProjection, Position
|
||||
from gaphas.solver import NORMAL, MultiConstraint
|
||||
from gaphas.state import observed, reversible_property
|
||||
from gaphas.types import Pos, SupportsFloatPos, TypedProperty
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -59,31 +58,28 @@ class Handle:
|
||||
pos: TypedProperty[Position, Union[Position, SupportsFloatPos]]
|
||||
pos = property(lambda s: s._pos, _set_pos, doc="The Handle's position")
|
||||
|
||||
@observed
|
||||
def _set_connectable(self, connectable: bool) -> None:
|
||||
self._connectable = connectable
|
||||
|
||||
connectable = reversible_property(
|
||||
connectable = property(
|
||||
lambda s: s._connectable,
|
||||
_set_connectable,
|
||||
doc="Can this handle actually connectect to a port?",
|
||||
)
|
||||
|
||||
@observed
|
||||
def _set_movable(self, movable: bool) -> None:
|
||||
self._movable = movable
|
||||
|
||||
movable = reversible_property(
|
||||
movable = property(
|
||||
lambda s: s._movable,
|
||||
_set_movable,
|
||||
doc="Can this handle be moved by a mouse pointer?",
|
||||
)
|
||||
|
||||
@observed
|
||||
def _set_visible(self, visible: bool) -> None:
|
||||
self._visible = visible
|
||||
|
||||
visible = reversible_property(
|
||||
visible = property(
|
||||
lambda s: s._visible, _set_visible, doc="Is this handle visible to the user?"
|
||||
)
|
||||
|
||||
@ -104,11 +100,10 @@ class Port:
|
||||
|
||||
self._connectable = True
|
||||
|
||||
@observed
|
||||
def _set_connectable(self, connectable: bool) -> None:
|
||||
self._connectable = connectable
|
||||
|
||||
connectable = reversible_property(lambda s: s._connectable, _set_connectable)
|
||||
connectable = property(lambda s: s._connectable, _set_connectable)
|
||||
|
||||
def glue(self, pos: SupportsFloatPos) -> Tuple[Pos, float]:
|
||||
"""Get glue point on the port and distance to the port."""
|
||||
|
@ -12,12 +12,6 @@ from gaphas.constraint import Constraint, EqualsConstraint, constraint
|
||||
from gaphas.geometry import distance_line_point, distance_rectangle_point
|
||||
from gaphas.matrix import Matrix
|
||||
from gaphas.solver import REQUIRED, VERY_STRONG, variable
|
||||
from gaphas.state import (
|
||||
observed,
|
||||
reversible_method,
|
||||
reversible_pair,
|
||||
reversible_property,
|
||||
)
|
||||
from gaphas.types import CairoContext
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -271,17 +265,15 @@ class Line(Matrices):
|
||||
def tail(self) -> Handle:
|
||||
return self._handles[-1]
|
||||
|
||||
@observed
|
||||
def _set_line_width(self, line_width: float) -> None:
|
||||
self._line_width = line_width
|
||||
|
||||
line_width = reversible_property(lambda s: s._line_width, _set_line_width)
|
||||
line_width = property(lambda s: s._line_width, _set_line_width)
|
||||
|
||||
@observed
|
||||
def _set_fuzziness(self, fuzziness: float) -> None:
|
||||
self._fuzziness = fuzziness
|
||||
|
||||
fuzziness = reversible_property(lambda s: s._fuzziness, _set_fuzziness)
|
||||
fuzziness = property(lambda s: s._fuzziness, _set_fuzziness)
|
||||
|
||||
def update_orthogonal_constraints(self, orthogonal: bool) -> None:
|
||||
"""Update the constraints required to maintain the orthogonal line.
|
||||
@ -304,7 +296,6 @@ class Line(Matrices):
|
||||
]
|
||||
self._set_orthogonal_constraints(cons)
|
||||
|
||||
@observed
|
||||
def _set_orthogonal_constraints(
|
||||
self, orthogonal_constraints: List[Constraint]
|
||||
) -> None:
|
||||
@ -314,11 +305,6 @@ class Line(Matrices):
|
||||
"""
|
||||
self._orthogonal_constraints = orthogonal_constraints
|
||||
|
||||
reversible_property(
|
||||
lambda s: s._orthogonal_constraints, _set_orthogonal_constraints
|
||||
)
|
||||
|
||||
@observed
|
||||
def _set_orthogonal(self, orthogonal: bool) -> None:
|
||||
"""
|
||||
>>> a = Line()
|
||||
@ -329,20 +315,11 @@ class Line(Matrices):
|
||||
raise ValueError("Can't set orthogonal line with less than 3 handles")
|
||||
self.update_orthogonal_constraints(orthogonal)
|
||||
|
||||
orthogonal = reversible_property(
|
||||
lambda s: bool(s._orthogonal_constraints), _set_orthogonal
|
||||
)
|
||||
orthogonal = property(lambda s: bool(s._orthogonal_constraints), _set_orthogonal)
|
||||
|
||||
@observed
|
||||
def _inner_set_horizontal(self, horizontal: bool) -> None:
|
||||
self._horizontal = horizontal
|
||||
|
||||
reversible_method(
|
||||
_inner_set_horizontal,
|
||||
_inner_set_horizontal,
|
||||
{"horizontal": lambda horizontal: not horizontal},
|
||||
)
|
||||
|
||||
def _set_horizontal(self, horizontal: bool) -> None:
|
||||
"""
|
||||
>>> line = Line()
|
||||
@ -355,36 +332,20 @@ class Line(Matrices):
|
||||
self._inner_set_horizontal(horizontal)
|
||||
self.update_orthogonal_constraints(self.orthogonal)
|
||||
|
||||
horizontal = reversible_property(lambda s: s._horizontal, _set_horizontal)
|
||||
horizontal = property(lambda s: s._horizontal, _set_horizontal)
|
||||
|
||||
@observed
|
||||
def insert_handle(self, index: int, handle: Handle) -> None:
|
||||
self._handles.insert(index, handle)
|
||||
|
||||
@observed
|
||||
def remove_handle(self, handle: Handle) -> None:
|
||||
self._handles.remove(handle)
|
||||
|
||||
reversible_pair(
|
||||
insert_handle,
|
||||
remove_handle,
|
||||
bind1={"index": lambda self, handle: self._handles.index(handle)},
|
||||
)
|
||||
|
||||
@observed
|
||||
def insert_port(self, index: int, port: Port) -> None:
|
||||
self._ports.insert(index, port)
|
||||
|
||||
@observed
|
||||
def remove_port(self, port: Port) -> None:
|
||||
self._ports.remove(port)
|
||||
|
||||
reversible_pair(
|
||||
insert_port,
|
||||
remove_port,
|
||||
bind1={"index": lambda self, port: self._ports.index(port)},
|
||||
)
|
||||
|
||||
def _update_ports(self) -> None:
|
||||
"""Update line ports.
|
||||
|
||||
|
@ -4,7 +4,7 @@ correct properties on gaphas' modules.
|
||||
Matrix
|
||||
------
|
||||
Small utility class wrapping cairo.Matrix. The `Matrix` class adds
|
||||
state preservation capabilities.
|
||||
state notification capabilities.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@ -13,13 +13,11 @@ from typing import Callable, Optional, Set, Tuple
|
||||
|
||||
import cairo
|
||||
|
||||
from gaphas.state import observed, reversible_method
|
||||
|
||||
MatrixTuple = Tuple[float, float, float, float, float, float]
|
||||
|
||||
|
||||
class Matrix:
|
||||
"""Matrix wrapper. This version sends @observed messages on state changes.
|
||||
"""Matrix wrapper.
|
||||
|
||||
>>> Matrix()
|
||||
Matrix(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
|
||||
@ -54,31 +52,26 @@ class Matrix:
|
||||
for handler in self._handlers:
|
||||
handler(self, old)
|
||||
|
||||
@observed
|
||||
def invert(self) -> None:
|
||||
old: MatrixTuple = self.tuple()
|
||||
self._matrix.invert()
|
||||
self.notify(old)
|
||||
|
||||
@observed
|
||||
def rotate(self, radians: float) -> None:
|
||||
old: MatrixTuple = self.tuple()
|
||||
self._matrix.rotate(radians)
|
||||
self.notify(old)
|
||||
|
||||
@observed
|
||||
def scale(self, sx: float, sy: float) -> None:
|
||||
old = self.tuple()
|
||||
self._matrix.scale(sx, sy)
|
||||
self.notify(old)
|
||||
|
||||
@observed
|
||||
def translate(self, tx: float, ty: float) -> None:
|
||||
old: MatrixTuple = self.tuple()
|
||||
self._matrix.translate(tx, ty)
|
||||
self.notify(old)
|
||||
|
||||
@observed
|
||||
def set(
|
||||
self,
|
||||
xx: Optional[float] = None,
|
||||
@ -105,13 +98,6 @@ class Matrix:
|
||||
if updated:
|
||||
self.notify(old)
|
||||
|
||||
reversible_method(invert, invert)
|
||||
reversible_method(rotate, rotate, {"radians": lambda radians: -radians})
|
||||
reversible_method(scale, scale, {"sx": lambda sx: 1 / sx, "sy": lambda sy: 1 / sy})
|
||||
reversible_method(
|
||||
translate, translate, {"tx": lambda tx: -tx, "ty": lambda ty: -ty}
|
||||
)
|
||||
|
||||
def multiply(self, m: Matrix) -> Matrix:
|
||||
return Matrix(matrix=self._matrix.multiply(m._matrix))
|
||||
|
||||
|
@ -36,7 +36,6 @@ import functools
|
||||
from typing import Callable, Collection, List, Optional, Set
|
||||
|
||||
from gaphas.solver.constraint import Constraint, ContainsConstraints
|
||||
from gaphas.state import observed, reversible_pair
|
||||
|
||||
|
||||
class Solver:
|
||||
@ -73,7 +72,6 @@ class Solver:
|
||||
def constraints(self) -> Collection[Constraint]:
|
||||
return self._constraints
|
||||
|
||||
@observed
|
||||
def add_constraint(self, constraint: Constraint) -> Constraint:
|
||||
"""Add a constraint. The actual constraint is returned, so the
|
||||
constraint can be removed later on.
|
||||
@ -100,7 +98,6 @@ class Solver:
|
||||
constraint.add_handler(self.request_resolve_constraint)
|
||||
return constraint
|
||||
|
||||
@observed
|
||||
def remove_constraint(self, constraint: Constraint) -> None:
|
||||
"""Remove a constraint from the solver.
|
||||
|
||||
@ -126,8 +123,6 @@ class Solver:
|
||||
while constraint in self._marked_cons:
|
||||
self._marked_cons.remove(constraint)
|
||||
|
||||
reversible_pair(add_constraint, remove_constraint)
|
||||
|
||||
def request_resolve_constraint(self, c: Constraint) -> None:
|
||||
"""Request resolving a constraint."""
|
||||
if not self._solving:
|
||||
|
@ -2,7 +2,6 @@ from __future__ import annotations
|
||||
|
||||
from typing import Callable, Set, SupportsFloat
|
||||
|
||||
from gaphas.state import observed, reversible_property
|
||||
from gaphas.types import TypedProperty
|
||||
|
||||
# epsilon for float comparison
|
||||
@ -104,7 +103,6 @@ class Variable:
|
||||
"""
|
||||
self.notify(self._value)
|
||||
|
||||
@observed
|
||||
def set_value(self, value: SupportsFloat) -> None:
|
||||
oldval = self._value
|
||||
v = float(value)
|
||||
@ -113,7 +111,7 @@ class Variable:
|
||||
self.notify(oldval)
|
||||
|
||||
value: TypedProperty[float, SupportsFloat]
|
||||
value = reversible_property(lambda s: s._value, set_value)
|
||||
value = property(lambda s: s._value, set_value)
|
||||
|
||||
def __str__(self):
|
||||
return f"Variable({self._value:g}, {self._strength:d})"
|
||||
|
254
gaphas/state.py
254
gaphas/state.py
@ -1,254 +0,0 @@
|
||||
"""This module is the central point where Gaphas' classes report their state
|
||||
changes.
|
||||
|
||||
Invocations of method and state changing properties are emitted to all
|
||||
functions (or bound methods) registered in the 'observers' set. Use
|
||||
`observers.add()` and `observers.remove()` to add/remove handlers.
|
||||
|
||||
This module also contains a second layer: a state inverser. Instead of
|
||||
emitting the invoked method, it emits a signal (callable, \\*\\*kwargs)
|
||||
that can be applied to revert the state of the object to the point
|
||||
before the method invocation.
|
||||
|
||||
For this to work the revert_handler has to be added to the observers
|
||||
set::
|
||||
|
||||
gaphas.state.observers.add(gaphas.state.revert_handler)
|
||||
"""
|
||||
from functools import update_wrapper
|
||||
from inspect import getfullargspec as _getargspec
|
||||
from threading import Lock
|
||||
from typing import Callable, Set, Tuple
|
||||
|
||||
# This string is added to each docstring in order to denote is's observed
|
||||
# OBSERVED_DOCSTRING = \
|
||||
# '\n\n This method is @observed. See gaphas.state for extra info.\n'
|
||||
|
||||
# Tell @observed to dispatch invocation messages by default
|
||||
# May be changed (but be sure to do that right at the start of your
|
||||
# application,otherwise you have no idea what's enabled and what's not!)
|
||||
DISPATCH_BY_DEFAULT = True
|
||||
|
||||
# Add/remove methods from this subscribers list.
|
||||
# Subscribers should have signature method(event) where event is a
|
||||
# Event has the form: (func, keywords)
|
||||
# Since most events originate from methods, it's safe to call
|
||||
# saveapply(func, keywords) for those functions
|
||||
subscribers: Set[Callable[[Tuple[Callable, str]], None]] = set()
|
||||
|
||||
# Subscribe to low-level change events:
|
||||
observers: Set[Callable[[Tuple[Callable, str]], None]] = set()
|
||||
|
||||
|
||||
# Perform locking (should be per thread?).
|
||||
mutex = Lock()
|
||||
|
||||
|
||||
def observed(func):
|
||||
"""Simple observer, dispatches events to functions registered in the
|
||||
observers list.
|
||||
|
||||
Also note that the events are dispatched *before* the function is
|
||||
invoked. This is an important feature, esp. for the reverter code.
|
||||
"""
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
acquired = mutex.acquire(False)
|
||||
try:
|
||||
if acquired:
|
||||
dispatch((observer, args, kwargs), queue=observers)
|
||||
# args[0].notify()
|
||||
return func(*args, **kwargs)
|
||||
finally:
|
||||
if acquired:
|
||||
mutex.release()
|
||||
|
||||
observer = update_wrapper(wrapper, func)
|
||||
return observer
|
||||
|
||||
|
||||
def dispatch(event, queue):
|
||||
"""Dispatch an event to a queue of event handlers. Event handlers should
|
||||
have signature: handler(event).
|
||||
|
||||
>>> def handler(event):
|
||||
... print("event handled", event)
|
||||
>>> observers.add(handler)
|
||||
>>> @observed
|
||||
... def callme():
|
||||
... pass
|
||||
>>> callme() # doctest: +ELLIPSIS
|
||||
event handled (<function callme at 0x...>, (), {})
|
||||
>>> class Callme(object):
|
||||
... @observed
|
||||
... def callme(self):
|
||||
... pass
|
||||
>>> Callme().callme() # doctest: +ELLIPSIS
|
||||
event handled (<function Callme.callme at 0x...), {})
|
||||
>>> observers.remove(handler)
|
||||
>>> callme()
|
||||
"""
|
||||
for s in queue:
|
||||
s(event)
|
||||
|
||||
|
||||
_reverse = {}
|
||||
|
||||
|
||||
def reversible_function(func, reverse, bind={}):
|
||||
"""Straight forward reversible method, if func is invoked, reverse is
|
||||
dispatched with bind as arguments."""
|
||||
global _reverse
|
||||
func = getfunction(func)
|
||||
_reverse[func] = (reverse, getargnames(reverse), bind)
|
||||
|
||||
|
||||
reversible_method = reversible_function
|
||||
|
||||
|
||||
def reversible_pair(func1, func2, bind1={}, bind2={}):
|
||||
"""Treat a pair of functions (func1 and func2) as each others inverse
|
||||
operation. bind1 provides arguments that can overrule the default values
|
||||
(or add additional values). bind2 does the same for func2.
|
||||
|
||||
See `revert_handler()` for doctesting.
|
||||
"""
|
||||
global _reverse
|
||||
# We need the function, since that's what's in the events
|
||||
func1 = getfunction(func1)
|
||||
func2 = getfunction(func2)
|
||||
_reverse[func1] = (func2, getargnames(func2), bind2)
|
||||
_reverse[func2] = (func1, getargnames(func1), bind1)
|
||||
|
||||
|
||||
def reversible_property(fget=None, fset=None, fdel=None, doc=None, bind={}):
|
||||
"""Replacement for the property descriptor. In addition to creating a
|
||||
property instance, the property is registered as reversible and reverse
|
||||
events can be send out when changes occur.
|
||||
|
||||
Caveat: we can't handle both fset and fdel in the proper
|
||||
way. Therefore fdel should somehow invoke fset. (personally, I
|
||||
hardly use fdel)
|
||||
|
||||
See revert_handler() for doctesting.
|
||||
"""
|
||||
# given fset, read the value argument name (second arg) and create a
|
||||
# bind {value: lambda self: fget(self)}
|
||||
|
||||
# TODO! handle fdel
|
||||
if fset:
|
||||
spec = getargnames(fset)
|
||||
argnames = spec[0]
|
||||
assert len(argnames) == 2, f"Set argument {fset} has argnames {argnames}"
|
||||
|
||||
argself, argvalue = argnames
|
||||
func = getfunction(fset)
|
||||
b = {argvalue: lambda self: fget(self)}
|
||||
b.update(bind)
|
||||
_reverse[func] = (func, spec, b)
|
||||
|
||||
return property(fget=fget, fset=fset, fdel=fdel, doc=doc)
|
||||
|
||||
|
||||
def revert_handler(event):
|
||||
"""Event handler, generates undoable statements and puts them on the
|
||||
subscribers queue.
|
||||
|
||||
First thing to do is to actually enable the revert_handler:
|
||||
|
||||
>>> observers.add(revert_handler)
|
||||
|
||||
First let's define our simple list:
|
||||
|
||||
>>> class SList(object):
|
||||
... def __init__(self):
|
||||
... self.l = list()
|
||||
... def add(self, node, before=None):
|
||||
... if before: self.l.insert(self.l.index(before), node)
|
||||
... else: self.l.append(node)
|
||||
... add = observed(add)
|
||||
... @observed
|
||||
... def remove(self, node):
|
||||
... self.l.remove(self.l.index(node))
|
||||
>>> sl = SList()
|
||||
>>> sl.add(10)
|
||||
>>> sl.l
|
||||
[10]
|
||||
>>> sl.add(11)
|
||||
>>> sl.l
|
||||
[10, 11]
|
||||
>>> sl.add(12, before=11)
|
||||
>>> sl.l
|
||||
[10, 12, 11]
|
||||
|
||||
It works, so let's add some reversible stuff:
|
||||
|
||||
>>> reversible_pair(SList.add, SList.remove, \
|
||||
bind1={'before': lambda self, node: self.l[self.l.index(node)+1] })
|
||||
>>> def handler(event):
|
||||
... print('handle', event)
|
||||
>>> subscribers.add(handler)
|
||||
>>> sl.add(20) # doctest: +ELLIPSIS
|
||||
handle (<function SList.remove at 0x...)
|
||||
|
||||
Same goes for properties (more or less):
|
||||
|
||||
>>> class PropTest(object):
|
||||
... def __init__(self): self._a = 0
|
||||
... @observed
|
||||
... def _set_a(self, value): self._a = value
|
||||
... a = reversible_property(lambda s: s._a, _set_a)
|
||||
>>> pt = PropTest()
|
||||
>>> pt.a
|
||||
0
|
||||
>>> pt.a = 10 # doctest: +ELLIPSIS
|
||||
handle (<function PropTest._set_a at 0x...>, {'self': <gaphas.state.PropTest object at 0x...>, 'value': 0})
|
||||
|
||||
>>> subscribers.remove(handler)
|
||||
"""
|
||||
global _reverse
|
||||
func, args, kwargs = event
|
||||
spec = getargnames(func)
|
||||
reverse, revspec, bind = _reverse.get(func, (None, None, {}))
|
||||
if not reverse:
|
||||
return
|
||||
|
||||
kw = dict(kwargs)
|
||||
kw.update(dict(list(zip(spec[0], args))))
|
||||
for arg, binding in list(bind.items()):
|
||||
kw[arg] = saveapply(binding, kw)
|
||||
argnames = list(revspec[0]) # normal args
|
||||
if spec[1]: # *args
|
||||
argnames.append(revspec[1])
|
||||
if spec[2]: # **kwargs
|
||||
argnames.append(revspec[2])
|
||||
kwargs = {arg: kw.get(arg) for arg in argnames}
|
||||
dispatch((reverse, kwargs), queue=subscribers)
|
||||
|
||||
|
||||
def saveapply(func, kw):
|
||||
"""Do apply a set of keywords to a method or function.
|
||||
|
||||
The function names should be known at meta-level, since arguments
|
||||
are applied as func(\\*\\*kwargs).
|
||||
"""
|
||||
spec = getargnames(func)
|
||||
argnames = list(spec[0])
|
||||
if spec[1]:
|
||||
argnames.append(spec[1])
|
||||
if spec[2]:
|
||||
argnames.append(spec[2])
|
||||
kwargs = {arg: kw.get(arg) for arg in argnames}
|
||||
return func(**kwargs)
|
||||
|
||||
|
||||
def getargnames(func):
|
||||
while hasattr(func, "__wrapped__"):
|
||||
func = func.__wrapped__
|
||||
spec = _getargspec(func)
|
||||
return spec[:3]
|
||||
|
||||
|
||||
def getfunction(func):
|
||||
"""Return the function associated with a class method."""
|
||||
return func
|
@ -1,7 +1,6 @@
|
||||
import gi
|
||||
import pytest
|
||||
|
||||
from gaphas import state
|
||||
from gaphas.canvas import Canvas
|
||||
from gaphas.item import Element as Box
|
||||
from gaphas.item import Line
|
||||
@ -56,37 +55,6 @@ def line(canvas, connections):
|
||||
return line
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def undo_fixture():
|
||||
undo_list = [] # type: ignore[var-annotated]
|
||||
redo_list = [] # type: ignore[var-annotated]
|
||||
|
||||
def undo():
|
||||
apply_me = list(undo_list)
|
||||
del undo_list[:]
|
||||
apply_me.reverse()
|
||||
for e in apply_me:
|
||||
state.saveapply(*e)
|
||||
redo_list[:] = undo_list[:]
|
||||
del undo_list[:]
|
||||
|
||||
def undo_handler(event):
|
||||
undo_list.append(event)
|
||||
|
||||
return undo, undo_handler, undo_list
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def revert_undo(undo_fixture):
|
||||
state.observers.clear()
|
||||
state.subscribers.clear()
|
||||
state.observers.add(state.revert_handler)
|
||||
state.subscribers.add(undo_fixture[1])
|
||||
yield
|
||||
state.observers.remove(state.revert_handler)
|
||||
state.subscribers.remove(undo_fixture[1])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def handler():
|
||||
events = []
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
from gaphas.canvas import Canvas
|
||||
from gaphas.item import Line
|
||||
from gaphas.segment import Segment
|
||||
|
||||
|
||||
def test_initial_ports():
|
||||
@ -10,59 +9,3 @@ def test_initial_ports():
|
||||
canvas = Canvas()
|
||||
line = Line(canvas.connections)
|
||||
assert 1 == len(line.ports())
|
||||
|
||||
|
||||
def test_orthogonal_horizontal_undo(revert_undo, undo_fixture):
|
||||
"""Test orthogonal line constraints bug (#107)."""
|
||||
canvas = Canvas()
|
||||
line = Line(canvas.connections)
|
||||
canvas.add(line)
|
||||
assert not line.horizontal
|
||||
assert len(canvas.solver._constraints) == 0
|
||||
|
||||
segment = Segment(line, canvas)
|
||||
segment.split_segment(0)
|
||||
|
||||
line.orthogonal = True
|
||||
|
||||
assert 2 == len(canvas.solver._constraints)
|
||||
|
||||
del undo_fixture[2][:] # Clear undo_list
|
||||
line.horizontal = True
|
||||
|
||||
assert 2 == len(canvas.solver._constraints)
|
||||
|
||||
undo_fixture[0]() # Call undo
|
||||
|
||||
assert not line.horizontal
|
||||
assert 2 == len(canvas.solver._constraints)
|
||||
|
||||
line.horizontal = True
|
||||
|
||||
assert line.horizontal
|
||||
assert 2 == len(canvas.solver._constraints)
|
||||
|
||||
|
||||
def test_orthogonal_line_undo(revert_undo, undo_fixture):
|
||||
"""Test orthogonal line undo."""
|
||||
canvas = Canvas()
|
||||
line = Line(canvas.connections)
|
||||
canvas.add(line)
|
||||
|
||||
segment = Segment(line, canvas)
|
||||
segment.split_segment(0)
|
||||
|
||||
# Start with no orthogonal constraints
|
||||
assert len(canvas.solver._constraints) == 0
|
||||
|
||||
line.orthogonal = True
|
||||
|
||||
# Check orthogonal constraints
|
||||
assert 2 == len(canvas.solver._constraints)
|
||||
assert 3 == len(line.handles())
|
||||
|
||||
undo_fixture[0]() # Call undo
|
||||
|
||||
assert not line.orthogonal
|
||||
assert 0 == len(canvas.solver._constraints)
|
||||
assert 2 == len(line.handles())
|
||||
|
@ -140,26 +140,6 @@ def test_constraints_after_split(canvas, connections, line, view):
|
||||
assert cinfo.constraint != orig_constraint
|
||||
|
||||
|
||||
def test_split_undo(canvas, line, revert_undo, undo_fixture):
|
||||
"""Test line splitting undo."""
|
||||
line.handles()[1].pos = (20, 0)
|
||||
|
||||
# We start with two handles and one port, after split 3 handles and
|
||||
# 2 ports are expected
|
||||
assert len(line.handles()) == 2
|
||||
assert len(line.ports()) == 1
|
||||
|
||||
segment = Segment(line, canvas)
|
||||
segment.split_segment(0)
|
||||
assert len(line.handles()) == 3
|
||||
assert len(line.ports()) == 2
|
||||
|
||||
# After undo, 2 handles and 1 port are expected again
|
||||
undo_fixture[0]() # Call Undo
|
||||
assert 2 == len(line.handles())
|
||||
assert 1 == len(line.ports())
|
||||
|
||||
|
||||
def test_orthogonal_line_split(canvas, line):
|
||||
"""Test orthogonal line splitting."""
|
||||
# Start with no orthogonal constraints
|
||||
@ -294,31 +274,6 @@ def test_merge_multiple(canvas, line):
|
||||
assert (20, 16) == port.end.pos
|
||||
|
||||
|
||||
def test_merge_undo(canvas, line, revert_undo, undo_fixture):
|
||||
"""Test line merging undo."""
|
||||
line.handles()[1].pos = (20, 0)
|
||||
|
||||
segment = Segment(line, canvas)
|
||||
|
||||
# Split for merging
|
||||
segment.split_segment(0)
|
||||
assert len(line.handles()) == 3
|
||||
assert len(line.ports()) == 2
|
||||
|
||||
# Clear undo stack before merging
|
||||
del undo_fixture[2][:]
|
||||
|
||||
# Merge with empty undo stack
|
||||
segment.merge_segment(0)
|
||||
assert len(line.handles()) == 2
|
||||
assert len(line.ports()) == 1
|
||||
|
||||
# After merge undo, 3 handles and 2 ports are expected again
|
||||
undo_fixture[0]() # Undo
|
||||
assert 3 == len(line.handles())
|
||||
assert 2 == len(line.ports())
|
||||
|
||||
|
||||
def test_orthogonal_line_merge(canvas, connections, line):
|
||||
"""Test orthogonal line merging."""
|
||||
assert 0 == len(connections.solver._constraints)
|
||||
|
@ -1,36 +0,0 @@
|
||||
import sys
|
||||
|
||||
from gaphas.state import _reverse, observed, reversible_pair
|
||||
|
||||
|
||||
class SList:
|
||||
def __init__(self):
|
||||
self.list = []
|
||||
|
||||
def add(self, node, before=None):
|
||||
if before:
|
||||
self.list.insert(self.list.index(before), node)
|
||||
else:
|
||||
self.list.append(node)
|
||||
|
||||
add = observed(add)
|
||||
|
||||
@observed
|
||||
def remove(self, node):
|
||||
self.list.remove(self.list.index(node))
|
||||
|
||||
|
||||
def test_adding_pair():
|
||||
"""Test adding reversible pair."""
|
||||
reversible_pair(
|
||||
SList.add,
|
||||
SList.remove,
|
||||
bind1={"before": lambda self, node: self.list[self.list.index(node) + 1]},
|
||||
)
|
||||
|
||||
if sys.version_info.major >= 3: # Modern Python
|
||||
assert SList.add in _reverse
|
||||
assert SList.remove in _reverse
|
||||
else: # Legacy Python
|
||||
assert SList.add.__func__ in _reverse # type: ignore[attr-defined]
|
||||
assert SList.remove.__func__ in _reverse
|
@ -1,58 +0,0 @@
|
||||
from gaphas.aspect import ConnectionSink, Connector
|
||||
from gaphas.canvas import Canvas
|
||||
from gaphas.item import Element as Box
|
||||
from gaphas.item import Line
|
||||
|
||||
|
||||
def test_undo_on_delete_element(revert_undo, undo_fixture):
|
||||
canvas = Canvas()
|
||||
b1 = Box(canvas.connections)
|
||||
b2 = Box(canvas.connections)
|
||||
line = Line(canvas.connections)
|
||||
|
||||
canvas.add(b1)
|
||||
assert 12 == len(canvas.solver.constraints)
|
||||
|
||||
canvas.add(b2)
|
||||
assert 12 == len(canvas.solver.constraints)
|
||||
|
||||
canvas.add(line)
|
||||
|
||||
sink = ConnectionSink(b1, b1.ports()[0])
|
||||
connector = Connector(line, line.handles()[0], canvas.connections)
|
||||
connector.connect(sink)
|
||||
|
||||
sink = ConnectionSink(b2, b2.ports()[0])
|
||||
connector = Connector(line, line.handles()[-1], canvas.connections)
|
||||
connector.connect(sink)
|
||||
|
||||
assert 14 == len(canvas.solver.constraints)
|
||||
assert 2 == len(list(canvas.connections.get_connections(item=line)))
|
||||
|
||||
del undo_fixture[2][:] # Clear undo_list
|
||||
|
||||
# Here disconnect is not invoked!
|
||||
canvas.remove(b2)
|
||||
|
||||
assert 7 == len(canvas.solver.constraints)
|
||||
assert 1 == len(list(canvas.connections.get_connections(item=line)))
|
||||
|
||||
cinfo = canvas.connections.get_connection(line.handles()[0])
|
||||
assert cinfo
|
||||
assert b1 == cinfo.connected
|
||||
|
||||
cinfo = canvas.connections.get_connection(line.handles()[-1])
|
||||
assert cinfo is None
|
||||
|
||||
undo_fixture[0]() # Call undo
|
||||
|
||||
assert 14 == len(canvas.solver.constraints)
|
||||
assert 2 == len(list(canvas.connections.get_connections(item=line)))
|
||||
|
||||
cinfo = canvas.connections.get_connection(line.handles()[0])
|
||||
assert cinfo
|
||||
assert b1 == cinfo.connected
|
||||
|
||||
cinfo = canvas.connections.get_connection(line.handles()[-1])
|
||||
assert cinfo
|
||||
assert b2 == cinfo.connected
|
Loading…
x
Reference in New Issue
Block a user