- Rename all .txt doc files to .rst to make it more clear that they are restructured text. - Add requirements.txt for Sphinx - Rerun sphinx-quickstart to make compatible with the latest Sphinx version. - Fix module names from gaphor to gaphas - Fix warnings about incorrect indent following :mod: lines - Fix TypeError for class Segmant: function object has no attribute '__mro__' Signed-off-by: Dan Yeaw <dan@yeaw.me>
437 lines
11 KiB
ReStructuredText
437 lines
11 KiB
ReStructuredText
Undo - implementing basic undo behaviour with Gaphas
|
|
####################################################
|
|
|
|
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.
|
|
|
|
See state.txt about how state is recorded.
|
|
|
|
.. contents::
|
|
|
|
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, 1, 0, 0)
|
|
|
|
translate(tx, ty):
|
|
|
|
>>> m.translate(12, 16)
|
|
>>> m
|
|
Matrix(1, 0, 0, 1, 12, 16)
|
|
>>> undo()
|
|
>>> m
|
|
Matrix(1, 0, 0, 1, 0, 0)
|
|
|
|
scale(sx, sy):
|
|
|
|
>>> m.scale(1.5, 1.5)
|
|
>>> m
|
|
Matrix(1.5, 0, 0, 1.5, 0, 0)
|
|
>>> undo()
|
|
>>> m
|
|
Matrix(1, 0, 0, 1, 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.877583, 0.479426, -0.479426, 0.877583, 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.31637, 0.719138, -0.719138, 1.31637, 12, 10)
|
|
>>> m.invert()
|
|
>>> m
|
|
Matrix(0.585055, -0.319617, 0.319617, 0.585055, -10.2168, -2.01515)
|
|
>>> 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, Item
|
|
>>> canvas = Canvas()
|
|
>>> canvas.get_all_items()
|
|
[]
|
|
>>> item = Item()
|
|
>>> canvas.add(item)
|
|
|
|
The ``request_update()`` method is observed:
|
|
|
|
>>> len(undo_list)
|
|
1
|
|
>>> canvas.request_update(item)
|
|
>>> len(undo_list)
|
|
2
|
|
|
|
On the canvas only ``add()`` and ``remove()`` are monitored:
|
|
|
|
>>> canvas.get_all_items() # doctest: +ELLIPSIS
|
|
[<gaphas.item.Item object at 0x...>]
|
|
>>> item.canvas is canvas
|
|
True
|
|
>>> undo()
|
|
>>> canvas.get_all_items()
|
|
[]
|
|
>>> item.canvas is None
|
|
True
|
|
>>> canvas.add(item)
|
|
>>> del undo_list[:]
|
|
>>> canvas.remove(item)
|
|
>>> canvas.get_all_items()
|
|
[]
|
|
>>> undo()
|
|
>>> canvas.get_all_items() # doctest: +ELLIPSIS
|
|
[<gaphas.item.Item object at 0x...>]
|
|
>>> undo_list
|
|
[]
|
|
|
|
Parent-child relationships are restored as well:
|
|
|
|
TODO!
|
|
|
|
|
|
>>> child = Item()
|
|
>>> canvas.add(child, parent=item)
|
|
>>> child.canvas is canvas
|
|
True
|
|
>>> canvas.get_parent(child) is item
|
|
True
|
|
>>> canvas.get_all_items() # doctest: +ELLIPSIS
|
|
[<gaphas.item.Item object at 0x...>, <gaphas.item.Item object at 0x...>]
|
|
>>> undo()
|
|
>>> child.canvas is None
|
|
True
|
|
>>> canvas.get_all_items() # doctest: +ELLIPSIS
|
|
[<gaphas.item.Item object at 0x...>]
|
|
>>> child in canvas.get_all_items()
|
|
False
|
|
|
|
Now redo the previous undo action:
|
|
|
|
>>> undo_list[:] = redo_list[:]
|
|
>>> undo()
|
|
>>> child.canvas is canvas
|
|
True
|
|
>>> canvas.get_parent(child) is item
|
|
True
|
|
>>> canvas.get_all_items() # doctest: +ELLIPSIS
|
|
[<gaphas.item.Item object at 0x...>, <gaphas.item.Item object at 0x...>]
|
|
|
|
Remove also works when items are removed recursively (an item and it's
|
|
children):
|
|
|
|
>>> child = Item()
|
|
>>> canvas.add(child, parent=item)
|
|
>>> canvas.get_all_items() # doctest: +ELLIPSIS
|
|
[<gaphas.item.Item object at 0x...>, <gaphas.item.Item object at 0x...>]
|
|
>>> del undo_list[:]
|
|
>>> canvas.remove(item)
|
|
>>> canvas.get_all_items()
|
|
[]
|
|
>>> undo()
|
|
>>> canvas.get_all_items() # doctest: +ELLIPSIS
|
|
[<gaphas.item.Item object at 0x...>, <gaphas.item.Item object at 0x...>]
|
|
>>> canvas.get_children(item) # doctest: +ELLIPSIS
|
|
[<gaphas.item.Item object at 0x...>]
|
|
|
|
As well as the reparent() method:
|
|
|
|
>>> canvas = Canvas()
|
|
>>> class NameItem(Item):
|
|
... def __init__(self, name):
|
|
... super(NameItem, self).__init__()
|
|
... self.name = name
|
|
... 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)
|
|
>>> canvas.get_all_items()
|
|
[<a>, <c>, <d>, <b>]
|
|
>>> del undo_list[:]
|
|
>>> canvas.reparent(ni3, parent=ni2)
|
|
>>> canvas.get_all_items()
|
|
[<a>, <b>, <c>, <d>]
|
|
>>> len(undo_list)
|
|
1
|
|
>>> undo()
|
|
>>> canvas.get_all_items()
|
|
[<a>, <c>, <d>, <b>]
|
|
|
|
Redo should work too:
|
|
|
|
>>> undo_list[:] = redo_list[:]
|
|
>>> undo()
|
|
>>> 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 (10, 12)>
|
|
>>> undo()
|
|
>>> handle.pos
|
|
<Position object on (0, 0)>
|
|
|
|
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
|
|
>>> e = Element()
|
|
>>> 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()
|
|
>>> e.canvas
|
|
|
|
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()
|
|
|
|
Let's first add a segment to the line, to test orthogonal lines as well.
|
|
|
|
>>> segment = Segment(l, None)
|
|
>>> _ = segment.split_segment(0)
|
|
|
|
>>> l.line_width, l.fuzziness, l.orthogonal, l.horizontal
|
|
(2, 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, 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 (0, 0)>, <Handle object on (10, 10)>]
|
|
|
|
This is our basis for further testing.
|
|
|
|
>>> del undo_list[:]
|
|
|
|
>>> Segment(l, None).split_segment(0) # doctest: +ELLIPSIS
|
|
([<Handle object on (5, 5)>], [<gaphas.connector.LinePort object at 0x...>])
|
|
>>> l.handles()
|
|
[<Handle object on (0, 0)>, <Handle object on (5, 5)>, <Handle object on (10, 10)>]
|
|
|
|
The opposite operation is performed with the merge_segment() method:
|
|
|
|
>>> undo()
|
|
>>> l.handles()
|
|
[<Handle object on (0, 0)>, <Handle object on (10, 10)>]
|
|
|
|
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.connect_item(hitem, handle, item, port=None, constraint=None, callback=real_disconnect)
|
|
>>> b0 = Item()
|
|
>>> canvas.add(b0)
|
|
>>> b1 = Item()
|
|
>>> canvas.add(b1)
|
|
>>> l = Line()
|
|
>>> canvas.add(l)
|
|
>>> real_connect(l, l.handles()[0], b0)
|
|
>>> real_connect(l, l.handles()[1], b1)
|
|
>>> canvas.get_connection(l.handles()[0]) # doctest: +ELLIPSIS
|
|
Connection(item=<gaphas.item.Line object at 0x...>)
|
|
>>> canvas.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:
|
|
|
|
>>> l.canvas
|
|
>>> canvas.get_connection(l.handles()[0])
|
|
>>> canvas.get_connection(l.handles()[1])
|
|
|
|
Undoing the remove() action should put everything back in place again:
|
|
|
|
>>> undo()
|
|
|
|
>>> l.canvas # doctest: +ELLIPSIS
|
|
<gaphas.canvas.Canvas object at 0x...>
|
|
>>> canvas.get_connection(l.handles()[0]) # doctest: +ELLIPSIS
|
|
Connection(item=<gaphas.item.Line object at 0x...>)
|
|
>>> canvas.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.strength = 100
|
|
>>> v
|
|
Variable(10, 100)
|
|
>>> 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))
|
|
>>> list(s.constraints_with_variable(a))
|
|
[EquationConstraint(<lambda>, a=Variable(1, 20), b=Variable(2, 20))]
|
|
|
|
>>> undo()
|
|
>>> list(s.constraints_with_variable(a))
|
|
[]
|
|
|
|
>>> undo_list[:] = redo_list[:]
|
|
>>> undo()
|
|
>>> list(s.constraints_with_variable(a))
|
|
[EquationConstraint(<lambda>, a=Variable(1, 20), b=Variable(2, 20))]
|
|
|