Undo - implementing basic undo behavior 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[:]

tree.py: Tree
-------------
Tree's ``add()`` and ``remove()`` methods are disabled by default.

    >>> from gaphas.tree import Tree
    >>> state.enable_dispatching(Tree.add)
    >>> state.enable_dispatching(Tree._remove)

One can create a nice tree:

    >>> tree = Tree()
    >>> tree.add(1)
    >>> tree.nodes
    [1]
    >>> undo()
    >>> tree.nodes
    []
    >>> undo_list[:] = redo_list[:]
    >>> undo()
    >>> tree.nodes
    [1]
    
Adding and removing complete sub-trees also works:

    >>> tree.add(2, parent=1)
    >>> tree.add(3, parent=2)
    >>> tree.add(4, parent=2)
    >>> tree.add(5, parent=4)
    >>> tree.nodes
    [1, 2, 3, 4, 5]
    >>> del undo_list[:]
    >>> tree.remove(1)
    >>> tree.nodes
    []
    >>> undo()
    >>> tree.nodes
    [1, 2, 3, 4, 5]
    >>> tree.get_children(2)
    [3, 4]
    >>> tree.get_children(4)
    [5]

Undo the last undo action:

    >>> undo_list[:] = redo_list[:]
    >>> undo()
    >>> tree.nodes
    []
    >>> undo_list[:] = redo_list[:]
    >>> undo()
    >>> tree.nodes
    [1, 2, 3, 4, 5]
    >>> tree.get_children(2)
    [3, 4]
    >>> tree.get_children(4)
    [5]

Moving a node from one node to another is easy:

    >>> tree.reparent(3, parent=5)
    >>> tree.get_children(2)
    [4]
    >>> tree.get_children(5)
    [3]
    >>> tree.nodes
    [1, 2, 4, 5, 3]

This change can be undone:

    >>> undo()
    >>> tree.nodes
    [1, 2, 4, 5, 3]
    >>> tree.get_children(2)
    [4, 3]
    >>> tree.get_children(5)
    []

As you can see, there's currently one flaw: the item is added at the end of the list.

(disable dispatching again, not frustrating other tests)

    >>> state.disable_dispatching(Tree.add)
    >>> state.disable_dispatching(Tree._remove)

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)
    3
    >>> canvas.request_update(item)
    >>> len(undo_list)
    4

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

item.py: Handle
---------------
Changing the Handle's position is reversible:

    >>> from gaphas import Handle
    >>> handle = Handle()
    >>> handle.x, handle.y = 10, 12
    >>> handle.pos
    (Variable(10, 20), Variable(12, 20))
    >>> undo()
    >>> handle.pos
    (Variable(0, 20), Variable(0, 20))

As are all other properties:

    >>> handle.connectable, handle.movable, handle.visible, handle.connected_to
    (False, True, True, None)
    >>> handle.disconnect                               # doctest: +ELLIPSIS
    <function <lambda> at 0x...>
    >>> handle.connectable = True
    >>> handle.movable = False
    >>> handle.visible = False
    >>> handle.connected_to = item
    >>> def my_fancy_disconnect(): pass
    >>> handle.disconnect = my_fancy_disconnect
    >>> handle.connectable, handle.movable, handle.visible
    (True, False, False)
    >>> handle.connected_to                             # doctest: +ELLIPSIS
    <gaphas.item.Item object at 0x...>
    >>> handle.disconnect                               # doctest: +ELLIPSIS
    <function my_fancy_disconnect at 0x...>

And now undo the whole lot at once:

    >>> undo()
    >>> handle.connectable, handle.movable, handle.visible, handle.connected_to
    (False, True, True, None)
    >>> handle.disconnect                               # doctest: +ELLIPSIS
    <function <lambda> at 0x...>

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()
    >>> e.canvas = canvas
    >>> 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
    >>> l = Line()
    >>> 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[:]

    >>> l.split_segment(0)
    [<Handle object on (5, 5)>]
    >>> 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)>]

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