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
    (10, 10)
    >>> e.min_height, e.min_width = 30, 40
    >>> e.min_height, e.min_width
    (30, 40)

    >>> undo()
    >>> e.min_height, e.min_width
    (10, 10)
    
    >>> 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))]
    
