Prepare documentation for Gaphas 3

This commit is contained in:
Arjan Molenaar 2020-12-05 21:54:01 +01:00
parent ece885056e
commit 1d5e07caec
No known key found for this signature in database
GPG Key ID: BF977B918996CB13
15 changed files with 1416 additions and 3780 deletions

View File

@ -1,28 +0,0 @@
Gaphas vs jHotDraw
------------------
Gaphas || jHotDraw
Item | Figure
Canvas | Drawing
Tool | Tool
Painter | Painter
View | DrawingView
jHotDraw let's you enable one tool at a time. In Gaphas everything should be
handled from within one tool chain. Tool chains can be switched, e.g. for
specific actions such as item placement. Everything else (item, handle
manupilation, zooming, selection) is handled without the need to select
specific tools.
Items (Figures in jHotDraw) keep track of their own child items. In Gaphas, the
Canvas object maintains the hierarchical order of items. Advantage is that
addition of items is never unnoticed. Also iterating the nodes in the tree
structure is a bit faster.
In jHotDraw, connections of items (figures) are maintained within the special
connection figure. Gaphas maintains connections between items as constraints
and uses a constraint solver (one per canvas) to ensure the constraints remain
true.

View File

@ -30,9 +30,5 @@ Tools
Several tools_ are used to make the overall user experience complete.
.. image:: tools.png
:width: 532
.. _quadtree: quadtree.html
.. _tools: tools.html

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 94 KiB

File diff suppressed because it is too large Load Diff

4
docs/guide.rst Normal file
View File

@ -0,0 +1,4 @@
Guides
######
Guides are a tool to align elements with one another.

View File

@ -1,30 +1,30 @@
Gaphas Documentation
Gaphas 3 Documentation
====================
.. important:: This documentation is in the process of being updated for Gaphas 3.0.
Gaphas is the diagramming widget library for Python.
Gaphas has been built with some extensibility in mind. It can be used for many
drawing purposes, including vector drawing applications, diagram drawing tools
and we even have a geographical map demo in our repository.
Gaphas has been built with extensibility in mind. It can be used for many
drawing purposes, including vector drawing applications, and diagram drawing tools.
The basic idea is:
- Items (canvas items) can be added to a Canvas.
- A Canvas can be visualized by one or more Views.
- The canvas maintains the tree structure (parent-child relationships between
items).
- Gaphas has a Model-View-Controller_ design.
- A model is presented as a protocol in Gaphas. This means that it's very easy to define a class that acts as a model.
- A model can be visualized by one or more Views.
- A constraint solver is used to maintain item constraints and inter-item
constraints.
- The item (and user) should not be bothered with things like bounding-box
calculations.
- Very modular: e.g. handle support could be swapped in and swapped out.
- Very modular: The view contains the basic features. Painters and tools can be swapped out as needed.
- Rendering using Cairo_. This implies the diagrams can be exported in a number
of formats, including PNG and SVG.
Gaphas is released under the terms of the Apache Software License, version 2.0.
Gaphas has been build using `setuptools` and can be installed as Python Egg.
* Git repository: https://github.com/gaphor/gaphas
* Python Package index (PyPI): https://pypi.org/project/gaphas
@ -32,20 +32,39 @@ Table of Contents
=================
.. toctree::
:caption: The basics
:maxdepth: 2
diagram
tools
ports
state
undo
quadtree
connectors
solver
state
.. toctree::
:caption: Advanced
:maxdepth: 2
guide
segment
.. toctree::
:caption: API
:maxdepth: 2
api
.. toctree::
:caption: Internals
:maxdepth: 1
quadtree
table
tree
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
.. _Cairo: https://cairographics.org
.. _Model-View-Controller: https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller

View File

@ -1,5 +1,5 @@
Quadtree implementation
=======================
Quadtree
########
In order to find items and handles fast on a 2D surface, a geometric structure is required.
@ -63,17 +63,11 @@ The screen is divided into four equal quadrants. The first quadrant has many ite
References
~~~~~~~~~~
(Roids)
http://roids.slowchop.com/roids/browser/trunk/src/quadtree.py
(???)
http://mu.arete.cc/pcr/syntax/quadtree/1/quadtree.py
(!PyGame)
http://www.pygame.org/wiki/QuadTree?parent=CookBook
(PythonCAD)
http://www.koders.com/python/fidB93B4D02B1C4E9F90F351736873DF6BE4D8D5783.aspx
https://sourceforge.net/p/pythoncad/code/ci/master/tree/PythonCAD/Generic/quadtree.py
.. _Quadtrees: http://en.wikipedia.org/wiki/Quadtree
.. _R-trees: http://en.wikipedia.org/wiki/R-tree

6
docs/segment.rst Normal file
View File

@ -0,0 +1,6 @@
Line Segments
#############
The line segment functionality is an add-on, that will allow the user to add line segments to a line, and merge them.
To use this behavior, import the ``gaphas.segment`` module and add ``LineSegmentPainter`` to the list of painters for the view.

View File

@ -1,6 +1,8 @@
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.
@ -193,3 +195,419 @@ matrix.py:
``Matrix``: ``invert()``, ``translate()``, ``rotate()`` and ``scale()``
Test cases are described in undo.txt.
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, 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 (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.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))
>>> undo()
>>> undo_list[:] = redo_list[:]
>>> undo()

8
docs/table.rst Normal file
View File

@ -0,0 +1,8 @@
Table
#####
Table is an internal structure. It can be best compared to a table in a database. On the table, indexes can be defined.
Tables are used when data should be made available in different forms.
Source code: https://github.com/gaphor/gaphas/blob/master/gaphas/table.py.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

View File

@ -8,6 +8,7 @@ Tools are registered on the ``View``. They have some internal state (e.g. when a
where a mouse button was pressed). Therefore tools can not be reused by
different views [#]_.
Tools are simply `Gtk.EventController` instances.
For a certain action to happen multiple user events are used. For example a
click is a combination of a button press and button release event (only talking
mouse clicks now). In most cases also some movement is done. A sequence of a
@ -37,36 +38,26 @@ To organize the event sequences and keep some order in what the user is doing To
Gaphas contains a set of default tools. Each tool is meant to deal with a special part of the view. A list of responsibilities is also defined here:
:HoverTool:
:hover tool:
First thing a user wants to know is if the mouse cursor is over an item. The ``HoverTool`` makes that explicit.
- Find a handle or item, if found, mark it as the ``hovered_item``
:HandleTool and ConnectHandleTool:
Handles are an important means to interact with items. They are used to
resize element, move lines and (in case of ``ConnectHandleTool``) establish
connections between items. Handles are rendered on top of items so it makes
sense to deal with them before you deal with items.
- On click: find a handle, if found become the grabbed tool and set focus on the selected item. Deselected current selection based on modifier keys.
- On motion: move the handle
- On release: release grab and release the handle
:ItemTool:
:item tool:
Items are the elements that are actually providing any (visual) meaning to the diagram. ItemTool deals with moving them around. The tool makes sure the right subset of selected elements are moved (e.g. you don't want to move a nested item if its parent item is already moved, this gives funny visual effects)
- On click: find an item, if found become the grabbed tool and set the item as focused. Some extra behaviour regarding multiple select is also done here.
- On click: find an item, if found become the grabbed tool and set the item as focused. If a used clicked on a handle position that is taken into account
- On motion: move the selected items (only the ones that have no selected parent items)
- On release: release grab and release item
:RubberBandTool:
If no handle or item is selected a rubberband selection is started.
:PanTool and ZoomTool:
Handy tools for moving the canvas around and zooming in and out. Convenience functionality, basically.
The item tool invokes the `Move` aspect, or the `HandleMove` aspect in case a handle is being grabbed.
All tools mentioned above are linked in a ``ToolChain``. Only one tool can deal with a use event.
:rubberband tool:
If no handle or item is selected a rubberband selection is started.
:scroll and zoom tool:
Handy tools for moving the canvas around and zooming in and out. Convenience functionality, basically.
There is one more tool, that has not been mentioned yet:
:PlacementTool:
:placement tool:
A special tool to use for placing new items on the screen.
As said, tools define *what* has to happen, they don't say how. Take for example finding a handle: on a normal element (a box or something) that would mean find the handle in one of the corners. On a line, however, that may also mean a not-yet existing handle in the middle of a line segment (there is a functionality that splits the line segment).
@ -88,9 +79,6 @@ The advantage is that more complex behaviour can be composed. Since the
decision on what should happen is done in the tool, the aspect which is then
used to work on the item ensures a certain behaviour is performed.
.. image:: tools.png
:width: 700
The diagram above shows the relation between tools and their aspects. Note that
tools that delegate their behaviour to aspects have more than one aspects. The
reason is that there are different concerns involved in defining what the tools
@ -98,15 +86,5 @@ should do. Typically ``ItemTool`` will be selecting the actual item and takes
care of moving it around as well. ``HandleTool`` does similar things for
handles.
Big changes from Gaphas 0.4 tool include:
* Tools can contain state and should be used for one view only.
* Grabbing is done automatically for press-move-release event sequence.
* The _What_ is separated from the _How_, leaving less tools and less
overhead (like finding the item under the mouse pointer).
.. [#] as opposed to versions < 0.5, where tools could be shared among multiple views.
.. [#] not the AOP term. The term aspect is coming from a paper by Dick Riehe: The Tools and Materials metaphore <url...>.

8
docs/tree.rst Normal file
View File

@ -0,0 +1,8 @@
Tree
####
Tree is an internal structure used by the default view model implementation (``gaphas.Canvas``). A tree consists of nodes.
The tree is optimized for depth-first search.
Source code: https://github.com/gaphor/gaphas/blob/master/gaphas/tree.py.

View File

@ -1,419 +0,0 @@
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.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, 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 (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.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))
>>> undo()
>>> undo_list[:] = redo_list[:]
>>> undo()