Merged ports branch with trunk
This commit is contained in:
parent
fbb31fddcf
commit
cdf08dffbe
23
demo.py
23
demo.py
@ -24,21 +24,19 @@ import math
|
||||
import gtk
|
||||
import cairo
|
||||
from gaphas import Canvas, GtkView, View
|
||||
from gaphas.examples import Box, Text, FatLine, Circle, DefaultExampleTool
|
||||
from gaphas.examples import Box, BoxX, Text, FatLine, Circle
|
||||
from gaphas.item import Line, NW, SE
|
||||
from gaphas.tool import PlacementTool, HandleTool
|
||||
from gaphas.tool import PlacementTool, HandleTool, LineSegmentTool
|
||||
from gaphas.painter import ItemPainter
|
||||
from gaphas import state
|
||||
from gaphas.util import text_extents
|
||||
|
||||
from gaphas import painter
|
||||
#painter.DEBUG_DRAW_BOUNDING_BOX = True
|
||||
|
||||
# Ensure data gets picked well:
|
||||
import gaphas.picklers
|
||||
|
||||
|
||||
#painter.DEBUG_DRAW_BOUNDING_BOX = True
|
||||
|
||||
# Global undo list
|
||||
undo_list = []
|
||||
|
||||
@ -102,7 +100,6 @@ class MyText(Text):
|
||||
|
||||
def create_window(canvas, title, zoom=1.0):
|
||||
view = GtkView()
|
||||
view.tool = DefaultExampleTool()
|
||||
|
||||
w = gtk.Window()
|
||||
w.set_title(title)
|
||||
@ -161,7 +158,8 @@ def create_window(canvas, title, zoom=1.0):
|
||||
|
||||
def on_clicked(button):
|
||||
if isinstance(view.focused_item, Line):
|
||||
view.focused_item.split_segment(0)
|
||||
tool = LineSegmentTool()
|
||||
tool.split_segment(view.focused_item, 0)
|
||||
view.queue_draw_item(view.focused_item, handles=True)
|
||||
|
||||
b.connect('clicked', on_clicked)
|
||||
@ -354,7 +352,6 @@ def create_canvas(c=None):
|
||||
fl.matrix.translate(100, 100)
|
||||
c.add(fl)
|
||||
|
||||
|
||||
circle = Circle()
|
||||
h1, h2 = circle.handles()
|
||||
circle.radius = 20
|
||||
@ -372,13 +369,21 @@ def create_canvas(c=None):
|
||||
# bb.matrix.rotate(math.pi/4.0 * i / 10.0)
|
||||
# c.add(bb, parent=b)
|
||||
|
||||
b=BoxX()
|
||||
b.min_width = 40
|
||||
b.min_height = 50
|
||||
b.width = b.height = 60
|
||||
b.matrix.translate(55, 55)
|
||||
c.add(b)
|
||||
|
||||
t=MyText('Single line')
|
||||
t.matrix.translate(70,70)
|
||||
c.add(t)
|
||||
|
||||
l=MyLine()
|
||||
l.handles()[1].pos = (30, 30)
|
||||
l.split_segment(0, 3)
|
||||
tool = LineSegmentTool()
|
||||
tool.split_segment(l, 0, 3)
|
||||
l.matrix.translate(30, 60)
|
||||
c.add(l)
|
||||
l.orthogonal = True
|
||||
|
4
doc/conf.py
Normal file
4
doc/conf.py
Normal file
@ -0,0 +1,4 @@
|
||||
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo']
|
||||
project = 'Gaphas'
|
||||
source_suffix = '.txt'
|
||||
master_doc = 'contents'
|
14
doc/contents.txt
Normal file
14
doc/contents.txt
Normal file
@ -0,0 +1,14 @@
|
||||
Gaphas Documentation
|
||||
====================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
ports
|
||||
state
|
||||
undo
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
71
doc/ports.txt
Normal file
71
doc/ports.txt
Normal file
@ -0,0 +1,71 @@
|
||||
Ports
|
||||
=====
|
||||
Port is a part of an item, which defines connectable part of an item.
|
||||
This concept has been introduced Gaphas version 0.4.0 to make Gaphas
|
||||
connection system more flexible.
|
||||
|
||||
Port Types
|
||||
----------
|
||||
There are two types of ports implemented in Gaphor (see `gaphas.connector`
|
||||
module).
|
||||
|
||||
First one is point port, as is often use in
|
||||
`EDA <http://en.wikipedia.org/wiki/Electronic_design_automation>`_
|
||||
applications, a handle connecting to such port is always kept at specific
|
||||
position, which equals to port's position.
|
||||
|
||||
Other port type is line port. A handle connecting to such port is kept on
|
||||
a line defined by start and end positions of line port.
|
||||
|
||||
Line port is used by items provided by `gaphas.item` module. `Element`
|
||||
item has one port defined at every edge of its rectangular shape (this is 4
|
||||
ports). `Line` item has one port per line segment.
|
||||
|
||||
Different types of ports can be invented like circle port or area port, they
|
||||
should implement interface defined by `gaphas.connector.Port` class to fit
|
||||
into Gaphas' connection system.
|
||||
|
||||
Ports and Constraints (Covering Handles)
|
||||
----------------------------------------
|
||||
Diagram items can have internal constraints, which can be used to position
|
||||
item's ports within an item itself.
|
||||
|
||||
For example, `Element` item could create constraints to position ports over
|
||||
its edges of rectanglular area. The result is duplication of constraints as
|
||||
`Element` already constraints position of handles to keep them in
|
||||
a rectangle.
|
||||
|
||||
*Therefore, when port covers handles, then it should reference handle
|
||||
positions*.
|
||||
|
||||
For example, an item, which is a horizontal line could be implemented
|
||||
like::
|
||||
|
||||
class HorizontalLine(gaphas.item.Item):
|
||||
def __init__(self):
|
||||
super(HorizontalLine, self).__init__()
|
||||
|
||||
self.start = Handle()
|
||||
self.end = Handle()
|
||||
|
||||
self.port = LinePort(self.start.pos, self.end.pos)
|
||||
|
||||
self.constraint(horizontal(self.start.pos, self.end.pos))
|
||||
|
||||
In case of `Element` item, each line port references positions of two
|
||||
handles, which keeps ports to lie over edges of rectangle. The same applies
|
||||
to `Line` item - every port is defined between positions of two neighbour
|
||||
handles. When `Line` item is orthogonal, then handle and ports share the
|
||||
same constraints, which guard line orthogonality.
|
||||
|
||||
This way, item's constraints are reused and their amount is limited to
|
||||
minimum.
|
||||
|
||||
Connections
|
||||
-----------
|
||||
Connection between two items is established by creating a constraint
|
||||
between handle's postion and port's positions (positions are constraint
|
||||
solver variables).
|
||||
|
||||
`ConnectHandleTool` class provides functionality to allow an user to
|
||||
perform connections between items.
|
@ -270,7 +270,7 @@ children):
|
||||
[<gaphas.item.Item object at 0x...>]
|
||||
|
||||
connector.py: Handle
|
||||
---------------
|
||||
--------------------
|
||||
Changing the Handle's position is reversible:
|
||||
|
||||
>>> from gaphas import Handle
|
||||
@ -375,8 +375,9 @@ This is our basis for further testing.
|
||||
|
||||
>>> del undo_list[:]
|
||||
|
||||
>>> l.split_segment(0)
|
||||
[<Handle object on (5, 5)>]
|
||||
>>> from gaphas import tool
|
||||
>>> tool.LineSegmentTool().split_segment(l, 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)>]
|
||||
|
@ -27,6 +27,7 @@ class Context(object):
|
||||
... except: 'got exc'
|
||||
'got exc'
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(**kwargs)
|
||||
|
||||
@ -41,7 +42,6 @@ class Canvas(object):
|
||||
|
||||
def __init__(self):
|
||||
self._tree = tree.Tree()
|
||||
self._tree_indexer = tree.TreeIndexer(self._tree, '_canvas_index')
|
||||
self._solver = solver.Solver()
|
||||
self._dirty_items = set()
|
||||
self._dirty_matrix_items = set()
|
||||
@ -255,14 +255,14 @@ class Canvas(object):
|
||||
"""
|
||||
Return a set of items that are connected to ``item``.
|
||||
The list contains tuples (item, handle). As a result an item may be
|
||||
in the list more than once (depending on the number of handles that
|
||||
are connected). If ``item`` is connected to itself it will also appear
|
||||
in the list.
|
||||
in the list more than once (depending on the number of handles that
|
||||
are connected). If ``item`` is connected to itself it will also appear
|
||||
in the list.
|
||||
|
||||
>>> c = Canvas()
|
||||
>>> from gaphas import item
|
||||
>>> i = item.Line()
|
||||
>>> c.add(i)
|
||||
>>> c = Canvas()
|
||||
>>> from gaphas import item
|
||||
>>> i = item.Line()
|
||||
>>> c.add(i)
|
||||
>>> ii = item.Line()
|
||||
>>> c.add(ii)
|
||||
>>> iii = item.Line()
|
||||
@ -304,7 +304,7 @@ class Canvas(object):
|
||||
>>> s[0] is i1 and s[1] is i2 and s[2] is i3
|
||||
True
|
||||
"""
|
||||
return self._tree_indexer.sort(items, reverse=reverse)
|
||||
return self._tree.sort(items, index_key='_canvas_index', reverse=reverse)
|
||||
|
||||
|
||||
#{ Matrices
|
||||
@ -606,7 +606,7 @@ class Canvas(object):
|
||||
Provide each item in the canvas with an index attribute. This makes
|
||||
for fast searching of items.
|
||||
"""
|
||||
self._tree_indexer.index_tree()
|
||||
self._tree.index_nodes('_canvas_index')
|
||||
|
||||
|
||||
#{ Views
|
||||
@ -685,6 +685,22 @@ class Canvas(object):
|
||||
#self.update()
|
||||
|
||||
|
||||
def project(self, item, *points):
|
||||
"""
|
||||
Project item's points into canvas coordinate system.
|
||||
|
||||
If there is only one point returned than projected point is
|
||||
returned. If there are more than one points, then tuple of
|
||||
projected points is returned.
|
||||
"""
|
||||
if len(points) == 1:
|
||||
return CanvasProjection(points[0], item)
|
||||
elif len(points) > 1:
|
||||
return tuple(CanvasProjection(p, item) for p in points)
|
||||
else:
|
||||
raise AttributeError('There should be at least one point specified')
|
||||
|
||||
|
||||
class VariableProjection(solver.Projection):
|
||||
"""
|
||||
Project a single `solver.Variable` to another space/coordinate system.
|
||||
|
@ -5,21 +5,30 @@ Basic connectors such as Ports and Handles.
|
||||
__version__ = "$Revision: 2341 $"
|
||||
# $HeadURL: https://svn.devjavu.com/gaphor/gaphas/trunk/gaphas/item.py $
|
||||
|
||||
from solver import solvable, WEAK, NORMAL, STRONG, VERY_STRONG
|
||||
from state import observed, reversible_property, disable_dispatching
|
||||
from gaphas.solver import solvable, WEAK, NORMAL, STRONG, VERY_STRONG
|
||||
from gaphas.state import observed, reversible_property, disable_dispatching
|
||||
from gaphas.geometry import distance_line_point, distance_point_point
|
||||
from gaphas.constraint import LineConstraint, PositionConstraint
|
||||
|
||||
|
||||
class Connector(object):
|
||||
class VariablePoint(object):
|
||||
"""
|
||||
Basic object for connections.
|
||||
A point constructed of two ``Variable``s.
|
||||
|
||||
>>> vp = VariablePoint((3, 5))
|
||||
>>> vp.x, vp.y
|
||||
(Variable(3, 20), Variable(5, 20))
|
||||
>>> vp.pos
|
||||
(Variable(3, 20), Variable(5, 20))
|
||||
>>> vp[0], vp[1]
|
||||
(Variable(3, 20), Variable(5, 20))
|
||||
"""
|
||||
|
||||
_x = solvable(varname='_v_x')
|
||||
_y = solvable(varname='_v_y')
|
||||
|
||||
def __init__(self, x=0, y=0, strength=NORMAL):
|
||||
self._x = x
|
||||
self._y = y
|
||||
def __init__(self, pos, strength=NORMAL):
|
||||
self._x, self._y = pos
|
||||
self._x.strength = strength
|
||||
self._y.strength = strength
|
||||
|
||||
@ -54,16 +63,16 @@ class Connector(object):
|
||||
"""
|
||||
Shorthand for returning the x(0) or y(1) component of the point.
|
||||
|
||||
>>> h = Handle(3, 5)
|
||||
>>> h[0]
|
||||
Variable(3, 20)
|
||||
>>> h[1]
|
||||
Variable(5, 20)
|
||||
>>> h = VariablePoint((3, 5))
|
||||
>>> h[0]
|
||||
Variable(3, 20)
|
||||
>>> h[1]
|
||||
Variable(5, 20)
|
||||
"""
|
||||
return (self.x, self.y)[index]
|
||||
|
||||
|
||||
class Handle(Connector):
|
||||
class Handle(VariablePoint):
|
||||
"""
|
||||
Handles are used to support modifications of Items.
|
||||
|
||||
@ -77,8 +86,8 @@ class Handle(Connector):
|
||||
not capable of pickling ``instancemethod`` or ``function`` objects.
|
||||
"""
|
||||
|
||||
def __init__(self, x=0, y=0, strength=NORMAL, connectable=False, movable=True):
|
||||
super(Handle, self).__init__(x, y, strength)
|
||||
def __init__(self, pos=(0, 0), strength=NORMAL, connectable=False, movable=True):
|
||||
super(Handle, self).__init__(pos, strength)
|
||||
|
||||
# Flags.. can't have enough of those
|
||||
self._connectable = connectable
|
||||
@ -131,39 +140,107 @@ class Handle(Connector):
|
||||
|
||||
disconnect = reversible_property(lambda s: s._disconnect or (lambda: None), _set_disconnect)
|
||||
|
||||
|
||||
class Port(object):
|
||||
"""
|
||||
Port connectable part of an item. Item's handle connects to a port.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(Port, self).__init__()
|
||||
|
||||
self._connectable = True
|
||||
|
||||
|
||||
@observed
|
||||
def _set_pos(self, pos):
|
||||
def _set_connectable(self, connectable):
|
||||
self._connectable = connectable
|
||||
|
||||
connectable = reversible_property(lambda s: s._connectable, _set_connectable)
|
||||
|
||||
|
||||
def glue(self, pos):
|
||||
"""
|
||||
Set handle position (Item coordinates).
|
||||
Get glue point on the port and distance to the port.
|
||||
"""
|
||||
self.x, self.y = pos
|
||||
raise NotImplemented('Glue method not implemented')
|
||||
|
||||
pos = property(lambda s: (s.x, s.y), _set_pos)
|
||||
|
||||
def __str__(self):
|
||||
return '<%s object on (%g, %g)>' % (self.__class__.__name__, float(self.x), float(self.y))
|
||||
__repr__ = __str__
|
||||
|
||||
def __getitem__(self, index):
|
||||
def constraint(self, canvas, item, handle, glue_item):
|
||||
"""
|
||||
Shorthand for returning the x(0) or y(1) component of the point.
|
||||
|
||||
>>> h = Handle(3, 5)
|
||||
>>> h[0]
|
||||
Variable(3, 20)
|
||||
>>> h[1]
|
||||
Variable(5, 20)
|
||||
Create connection constraint between item's handle and glue item.
|
||||
"""
|
||||
return (self.x, self.y)[index]
|
||||
raise NotImplemented('Constraint method not implemented')
|
||||
|
||||
|
||||
class Port(Connector):
|
||||
class LinePort(Port):
|
||||
"""
|
||||
A Port functions as a place on an Item where a Handle can connect.
|
||||
Port defined as a line between two handles.
|
||||
"""
|
||||
|
||||
def __init__(self, x=0, y=0, strength=NORMAL):
|
||||
super(Port, self).__init__(x, y, strength)
|
||||
def __init__(self, start, end):
|
||||
super(LinePort, self).__init__()
|
||||
|
||||
self.start = start
|
||||
self.end = end
|
||||
|
||||
|
||||
def glue(self, pos):
|
||||
"""
|
||||
Get glue point on the port and distance to the port.
|
||||
|
||||
>>> p1, p2 = (0.0, 0.0), (100.0, 100.0)
|
||||
>>> port = LinePort(p1, p2)
|
||||
>>> port.glue((50, 50))
|
||||
((50.0, 50.0), 0.0)
|
||||
>>> port.glue((0, 10))
|
||||
((5.0, 5.0), 7.0710678118654755)
|
||||
"""
|
||||
d, pl = distance_line_point(self.start, self.end, pos)
|
||||
return pl, d
|
||||
|
||||
|
||||
def constraint(self, canvas, item, handle, glue_item):
|
||||
"""
|
||||
Create connection line constraint between item's handle and the
|
||||
port.
|
||||
"""
|
||||
line = canvas.project(glue_item, self.start, self.end)
|
||||
point = canvas.project(item, handle.pos)
|
||||
return LineConstraint(line, point)
|
||||
|
||||
|
||||
class PointPort(Port):
|
||||
"""
|
||||
Port defined as a point.
|
||||
"""
|
||||
|
||||
def __init__(self, point):
|
||||
super(PointPort, self).__init__()
|
||||
self.point = point
|
||||
|
||||
|
||||
def glue(self, pos):
|
||||
"""
|
||||
Get glue point on the port and distance to the port.
|
||||
|
||||
>>> h = Handle((10, 10))
|
||||
>>> port = PointPort(h.pos)
|
||||
>>> port.glue((10, 0))
|
||||
((Variable(10, 20), Variable(10, 20)), 10.0)
|
||||
"""
|
||||
d = distance_point_point(self.point, pos)
|
||||
return self.point, d
|
||||
|
||||
|
||||
def constraint(self, canvas, item, handle, glue_item):
|
||||
"""
|
||||
Return connection position constraint between item's handle and the
|
||||
port.
|
||||
"""
|
||||
origin = canvas.project(glue_item, self.point)
|
||||
point = canvas.project(item, handle.pos)
|
||||
return PositionConstraint(origin, point)
|
||||
|
||||
|
||||
# vim: sw=4:et:ai
|
||||
|
@ -30,6 +30,7 @@ appropriate value.
|
||||
|
||||
from __future__ import division
|
||||
import operator
|
||||
import math
|
||||
from solver import Projection
|
||||
|
||||
|
||||
@ -140,17 +141,19 @@ class EqualsConstraint(Constraint):
|
||||
Variable(10.8, 20)
|
||||
"""
|
||||
|
||||
def __init__(self, a=None, b=None):
|
||||
def __init__(self, a=None, b=None, delta=0.0):
|
||||
super(EqualsConstraint, self).__init__(a, b)
|
||||
self.a = a
|
||||
self.b = b
|
||||
self._delta = delta
|
||||
|
||||
|
||||
def solve_for(self, var):
|
||||
assert var in (self.a, self.b)
|
||||
|
||||
_update(*((var is self.a) and \
|
||||
(self.a, self.b.value) or \
|
||||
(self.b, self.a.value)))
|
||||
(self.a, self.b.value + self._delta) or \
|
||||
(self.b, self.a.value + self._delta)))
|
||||
|
||||
|
||||
|
||||
@ -505,4 +508,81 @@ class LineConstraint(Constraint):
|
||||
_update(py, y)
|
||||
|
||||
|
||||
class PositionConstraint(Constraint):
|
||||
"""
|
||||
Ensure that point is always in origin position.
|
||||
|
||||
Attributes:
|
||||
- _origin: origin position
|
||||
- _point: point to be in origin position
|
||||
"""
|
||||
|
||||
def __init__(self, origin, point):
|
||||
super(PositionConstraint, self).__init__(origin[0], origin[1],
|
||||
point[0], point[1])
|
||||
|
||||
self._origin = origin
|
||||
self._point = point
|
||||
|
||||
|
||||
def solve_for(self, var=None):
|
||||
"""
|
||||
Ensure that point's coordinates are the same as coordinates of the
|
||||
origin position.
|
||||
"""
|
||||
x, y = self._origin[0].value, self._origin[1].value
|
||||
_update(self._point[0], x)
|
||||
_update(self._point[1], y)
|
||||
|
||||
|
||||
|
||||
class LineAlignConstraint(Constraint):
|
||||
"""
|
||||
Ensure a point is kept on a line in position specified by align and padding
|
||||
information.
|
||||
|
||||
Align is specified as a number between 0 and 1, for example
|
||||
0
|
||||
keep point at one end of the line
|
||||
1
|
||||
keep point at other end of the line
|
||||
0.5
|
||||
keep point in the middle of the line
|
||||
|
||||
Align can be adjusted with `delta` parameter, which specifies the padding of
|
||||
the point.
|
||||
|
||||
:Attributes:
|
||||
_line
|
||||
Line defined by tuple ((x1, y1), (x2, y2)).
|
||||
_point
|
||||
Point defined by tuple (x, y).
|
||||
_align
|
||||
Align of point.
|
||||
_delta
|
||||
Padding of the align.
|
||||
"""
|
||||
|
||||
def __init__(self, line, point, align=0.5, delta=0.0):
|
||||
super(LineAlignConstraint, self).__init__(line[0][0], line[0][1], line[1][0], line[1][1], point[0], point[1])
|
||||
|
||||
self._line = line
|
||||
self._point = point
|
||||
self._align = align
|
||||
self._delta = delta
|
||||
|
||||
|
||||
def solve_for(self, var=None):
|
||||
sx, sy = self._line[0]
|
||||
ex, ey = self._line[1]
|
||||
px, py = self._point
|
||||
a = math.atan2(ey.value - sy.value, ex.value - sx.value)
|
||||
|
||||
x = sx.value + (ex.value - sx.value) * self._align + self._delta * math.cos(a)
|
||||
y = sy.value + (ey.value - sy.value) * self._align + self._delta * math.sin(a)
|
||||
|
||||
_update(px, x)
|
||||
_update(py, y)
|
||||
|
||||
|
||||
# vim:sw=4:et:ai
|
||||
|
@ -6,13 +6,12 @@ These items are used in various tests.
|
||||
__version__ = "$Revision$"
|
||||
# $HeadURL$
|
||||
|
||||
from item import Handle, Element, Item
|
||||
from item import NW, NE,SW, SE
|
||||
from solver import solvable
|
||||
from gaphas.item import Element, Item, NW, NE,SW, SE
|
||||
from gaphas.connector import Handle, PointPort
|
||||
from gaphas.constraint import LessThanConstraint, EqualsConstraint, \
|
||||
BalanceConstraint
|
||||
from gaphas.solver import solvable, WEAK
|
||||
import tool
|
||||
from constraint import LineConstraint, LessThanConstraint, EqualsConstraint
|
||||
from canvas import CanvasProjection
|
||||
from geometry import point_on_rectangle, distance_rectangle_point
|
||||
from util import text_extents, text_align, text_multiline, path_ellipse
|
||||
from cairo import Matrix
|
||||
|
||||
@ -38,17 +37,53 @@ class Box(Element):
|
||||
c.set_source_rgb(0,0,0.8)
|
||||
c.stroke()
|
||||
|
||||
def glue(self, item, handle, x, y):
|
||||
"""
|
||||
Special glue method used by the ConnectingHandleTool to find
|
||||
a connection point.
|
||||
"""
|
||||
h = self._handles
|
||||
h_se = h[SE]
|
||||
r = (0, 0, h_se.x, h_se.y)
|
||||
por = point_on_rectangle(r, (x, y), border=True)
|
||||
p = distance_rectangle_point(r, (x, y))
|
||||
return p, por
|
||||
|
||||
class BoxX(Box):
|
||||
"""
|
||||
It is a Box but with additional port (see "x" below).
|
||||
|
||||
NW +--------+ NE
|
||||
| |
|
||||
| |
|
||||
| |x
|
||||
| |
|
||||
SW +--------+ SE
|
||||
"""
|
||||
def __init__(self, width=10, height=10):
|
||||
super(BoxX, self).__init__(width, height)
|
||||
self._hx = Handle(strength=WEAK)
|
||||
#self._hx.movable = False
|
||||
#self._hx.visible = False
|
||||
self._handles.append(self._hx)
|
||||
# define 'x' port
|
||||
self._ports.append(PointPort(self._hx.pos))
|
||||
|
||||
# keep hx handle at right edge, at 80% of height of the box
|
||||
ne = self._handles[NE]
|
||||
se = self._handles[SE]
|
||||
hxc1 = EqualsConstraint(ne.x, self._hx.x, delta=10)
|
||||
#hxc2 = BalanceConstraint(band=(ne.y, se.y), v=self._hx.y, balance=0.8)
|
||||
self._constraints.append(hxc1)
|
||||
#self._constraints.append(hxc2)
|
||||
|
||||
|
||||
def draw(self, context):
|
||||
super(BoxX, self).draw(context)
|
||||
c = context.cairo
|
||||
|
||||
# draw 'x' port
|
||||
hx = self._hx
|
||||
c.rectangle(hx.x - 20 , hx.y - 5, 20, 10)
|
||||
c.rectangle(hx.x - 1 , hx.y - 1, 2, 2)
|
||||
if context.hovered:
|
||||
c.set_source_rgba(.0, .8, 0, .8)
|
||||
else:
|
||||
c.set_source_rgba(.9, .0, .0, .8)
|
||||
c.fill_preserve()
|
||||
c.set_source_rgb(0,0,0.8)
|
||||
c.stroke()
|
||||
|
||||
|
||||
|
||||
|
||||
class Text(Item):
|
||||
@ -74,7 +109,7 @@ class Text(Item):
|
||||
else:
|
||||
text_align(cr, 0, 0, self.text, self.align_x, self.align_y)
|
||||
|
||||
def point(self, x, y):
|
||||
def point(self, pos):
|
||||
return 0
|
||||
|
||||
|
||||
@ -143,151 +178,5 @@ class Circle(Item):
|
||||
cr.stroke()
|
||||
|
||||
|
||||
class DisconnectHandle(object):
|
||||
|
||||
def __init__(self, canvas, item, handle):
|
||||
self.canvas = canvas
|
||||
self.item = item
|
||||
self.handle = handle
|
||||
|
||||
def __call__(self):
|
||||
self.handle_disconnect()
|
||||
|
||||
def handle_disconnect(self):
|
||||
canvas = self.canvas
|
||||
item = self.item
|
||||
handle = self.handle
|
||||
try:
|
||||
canvas.solver.remove_constraint(handle.connection_data)
|
||||
except KeyError:
|
||||
print 'constraint was already removed for', item, handle
|
||||
pass # constraint was alreasy removed
|
||||
else:
|
||||
print 'constraint removed for', item, handle
|
||||
handle.connection_data = None
|
||||
handle.connected_to = None
|
||||
# Remove disconnect handler:
|
||||
handle.disconnect = None
|
||||
|
||||
|
||||
class ConnectingHandleTool(tool.HandleTool):
|
||||
"""
|
||||
This is a HandleTool which supports a simple connection algorithm,
|
||||
using LineConstraint.
|
||||
"""
|
||||
|
||||
def glue(self, view, item, handle, wx, wy):
|
||||
"""
|
||||
It allows the tool to glue to a Box or (other) Line item.
|
||||
The distance from the item to the handle is determined in canvas
|
||||
coordinates, using a 10 pixel glue distance.
|
||||
"""
|
||||
if not handle.connectable:
|
||||
return
|
||||
|
||||
# Make glue distance depend on the zoom ratio (should be about 10 pixels)
|
||||
inverse = Matrix(*view.matrix)
|
||||
inverse.invert()
|
||||
#glue_distance, dummy = inverse.transform_distance(10, 0)
|
||||
glue_distance = 10
|
||||
glue_point = None
|
||||
glue_item = None
|
||||
for i in view.canvas.get_all_items():
|
||||
if not i is item:
|
||||
v2i = view.get_matrix_v2i(i).transform_point
|
||||
ix, iy = v2i(wx, wy)
|
||||
try:
|
||||
distance, point = i.glue(item, handle, ix, iy)
|
||||
if distance <= glue_distance:
|
||||
glue_distance = distance
|
||||
i2v = view.get_matrix_i2v(i).transform_point
|
||||
glue_point = i2v(*point)
|
||||
glue_item = i
|
||||
except AttributeError:
|
||||
pass
|
||||
if glue_point:
|
||||
v2i = view.get_matrix_v2i(item).transform_point
|
||||
handle.x, handle.y = v2i(*glue_point)
|
||||
return glue_item
|
||||
|
||||
def connect(self, view, item, handle, wx, wy):
|
||||
"""
|
||||
Connect a handle to another item.
|
||||
|
||||
In this "method" the following assumptios are made:
|
||||
|
||||
1. The only item that accepts handle connections are the Box instances
|
||||
2. The only items with connectable handles are Line's
|
||||
|
||||
"""
|
||||
def side(handle, glued):
|
||||
handles = glued.handles()
|
||||
hx, hy = view.get_matrix_i2v(item).transform_point(handle.x, handle.y)
|
||||
ax, ay = view.get_matrix_i2v(glued).transform_point(handles[NW].x, handles[NW].y)
|
||||
bx, by = view.get_matrix_i2v(glued).transform_point(handles[SE].x, handles[SE].y)
|
||||
|
||||
if abs(hx - ax) < 0.01:
|
||||
return handles[NW], handles[SW]
|
||||
elif abs(hy - ay) < 0.01:
|
||||
return handles[NW], handles[NE]
|
||||
elif abs(hx - bx) < 0.01:
|
||||
return handles[NE], handles[SE]
|
||||
else:
|
||||
return handles[SW], handles[SE]
|
||||
assert False
|
||||
|
||||
#print 'Handle.connect', view, item, handle, wx, wy
|
||||
glue_item = self.glue(view, item, handle, wx, wy)
|
||||
if glue_item and glue_item is handle.connected_to:
|
||||
try:
|
||||
view.canvas.solver.remove_constraint(handle.connection_data)
|
||||
except KeyError:
|
||||
pass # constraint was already removed
|
||||
|
||||
h1, h2 = side(handle, glue_item)
|
||||
handle.connection_data = LineConstraint(line=(CanvasProjection(h1.pos, glue_item),
|
||||
CanvasProjection(h2.pos, glue_item)),
|
||||
point=CanvasProjection(handle.pos, item))
|
||||
view.canvas.solver.add_constraint(handle.connection_data)
|
||||
|
||||
handle.disconnect = DisconnectHandle(view.canvas, item, handle)
|
||||
return
|
||||
|
||||
# drop old connetion
|
||||
if handle.connected_to:
|
||||
handle.disconnect()
|
||||
|
||||
if glue_item:
|
||||
if isinstance(glue_item, Element):
|
||||
h1, h2 = side(handle, glue_item)
|
||||
|
||||
# Make a constraint that keeps into account item coordinates.
|
||||
handle.connection_data = \
|
||||
LineConstraint(line=(CanvasProjection(h1.pos, glue_item),
|
||||
CanvasProjection(h2.pos, glue_item)),
|
||||
point=CanvasProjection(handle.pos, item))
|
||||
view.canvas.solver.add_constraint(handle.connection_data)
|
||||
|
||||
handle.connected_to = glue_item
|
||||
handle.disconnect = DisconnectHandle(view.canvas, item, handle)
|
||||
|
||||
def disconnect(self, view, item, handle):
|
||||
if handle.connected_to:
|
||||
#print 'Handle.disconnect', view, item, handle
|
||||
view.canvas.solver.remove_constraint(handle.connection_data)
|
||||
|
||||
|
||||
def DefaultExampleTool():
|
||||
"""
|
||||
The default tool chain build from HoverTool, ItemTool and HandleTool.
|
||||
"""
|
||||
chain = tool.ToolChain()
|
||||
chain.append(tool.HoverTool())
|
||||
chain.append(ConnectingHandleTool())
|
||||
chain.append(tool.ItemTool())
|
||||
chain.append(tool.TextEditTool())
|
||||
chain.append(tool.RubberbandTool())
|
||||
return chain
|
||||
|
||||
|
||||
# vim: sw=4:et:ai
|
||||
|
263
gaphas/item.py
263
gaphas/item.py
@ -10,9 +10,9 @@ from weakref import WeakKeyDictionary
|
||||
|
||||
from matrix import Matrix
|
||||
from geometry import distance_line_point, distance_rectangle_point
|
||||
from connector import Handle
|
||||
from gaphas.connector import Handle, LinePort
|
||||
from solver import solvable, WEAK, NORMAL, STRONG, VERY_STRONG
|
||||
from constraint import EqualsConstraint, LessThanConstraint
|
||||
from constraint import EqualsConstraint, LessThanConstraint, LineConstraint, LineAlignConstraint
|
||||
from state import observed, reversible_method, reversible_pair, reversible_property, disable_dispatching
|
||||
|
||||
class Item(object):
|
||||
@ -30,6 +30,7 @@ class Item(object):
|
||||
|
||||
- _canvas: canvas, which owns an item
|
||||
- _handles: list of handles owned by an item
|
||||
- _ports: list of ports, connectable areas of an item
|
||||
- _matrix_i2c: item to canvas coordinates matrix
|
||||
- _matrix_c2i: canvas to item coordinates matrix
|
||||
- _matrix_i2v: item to view coordinates matrices
|
||||
@ -42,6 +43,7 @@ class Item(object):
|
||||
self._matrix = Matrix()
|
||||
self._handles = []
|
||||
self._constraints = []
|
||||
self._ports = []
|
||||
|
||||
# used by gaphas.canvas.Canvas to hold conversion matrices
|
||||
self._matrix_i2c = None
|
||||
@ -153,13 +155,81 @@ class Item(object):
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def constraint(self,
|
||||
horizontal=None,
|
||||
vertical=None,
|
||||
left_of=None,
|
||||
above=None,
|
||||
line=None,
|
||||
delta=0.0,
|
||||
align=None):
|
||||
"""
|
||||
Utility method to create item's internal constraint between
|
||||
two positions or between a position and a line.
|
||||
|
||||
Position is a tuple of coordinates, i.e. ``(2, 4)``.
|
||||
|
||||
Line is a tuple of positions, i.e. ``((2, 3), (4, 2))``.
|
||||
|
||||
This method shall not be used to create constraints between
|
||||
two different items.
|
||||
|
||||
Created constraint is returned.
|
||||
|
||||
:Parameters:
|
||||
horizontal=(p1, p2)
|
||||
Keep positions ``p1`` and ``p2`` aligned horizontally.
|
||||
vertical=(p1, p2)
|
||||
Keep positions ``p1`` and ``p2`` aligned vertically.
|
||||
left_of=(p1, p2)
|
||||
Keep position ``p1`` on the left side of position ``p2``.
|
||||
above=(p1, p2)
|
||||
Keep position ``p1`` above position ``p2``.
|
||||
line=(p, l)
|
||||
Keep position ``p`` on line ``l``.
|
||||
"""
|
||||
cc = None # created constraint
|
||||
if horizontal is not None:
|
||||
p1, p2 = horizontal
|
||||
cc = EqualsConstraint(p1[1], p2[1])
|
||||
elif vertical is not None:
|
||||
p1, p2 = vertical
|
||||
cc = EqualsConstraint(p1[0], p2[0])
|
||||
elif left_of is not None:
|
||||
p1, p2 = left_of
|
||||
cc = LessThanConstraint(p1[1], p2[1], delta)
|
||||
elif above is not None:
|
||||
p1, p2 = above
|
||||
cc = LessThanConstraint(p1[0], p2[0], delta)
|
||||
elif line is not None:
|
||||
pos, l = line
|
||||
if align is None:
|
||||
cc = LineConstraint(line=l, point=pos)
|
||||
else:
|
||||
cc = LineAlignConstraint(line=l, point=pos, align=align, delta=delta)
|
||||
else:
|
||||
raise ValueError('Constraint incorrectly specified')
|
||||
assert cc is not None
|
||||
self._constraints.append(cc)
|
||||
return cc
|
||||
|
||||
|
||||
def handles(self):
|
||||
"""
|
||||
Return a list of handles owned by the item.
|
||||
"""
|
||||
return self._handles
|
||||
|
||||
def point(self, x, y):
|
||||
|
||||
def ports(self):
|
||||
"""
|
||||
Return list of ports.
|
||||
"""
|
||||
return self._ports
|
||||
|
||||
|
||||
def point(self, pos):
|
||||
"""
|
||||
Get the distance from a point (``x``, ``y``) to the item.
|
||||
``x`` and ``y`` are in item coordinates.
|
||||
@ -209,34 +279,35 @@ class Element(Item):
|
||||
super(Element, self).__init__()
|
||||
self._handles = [ h(strength=VERY_STRONG) for h in [Handle]*4 ]
|
||||
|
||||
eq = EqualsConstraint
|
||||
lt = LessThanConstraint
|
||||
handles = self._handles
|
||||
h_nw = handles[NW]
|
||||
h_ne = handles[NE]
|
||||
h_sw = handles[SW]
|
||||
h_se = handles[SE]
|
||||
|
||||
# create minimal size constraints
|
||||
self._c_min_w = LessThanConstraint(smaller=h_nw.x, bigger=h_se.x, delta=10)
|
||||
self._c_min_h = LessThanConstraint(smaller=h_nw.y, bigger=h_se.y, delta=10)
|
||||
# edge of element define default element ports
|
||||
self._ports = [
|
||||
LinePort(h_nw.pos, h_ne.pos),
|
||||
LinePort(h_ne.pos, h_se.pos),
|
||||
LinePort(h_se.pos, h_sw.pos),
|
||||
LinePort(h_sw.pos, h_nw.pos)
|
||||
]
|
||||
|
||||
# setup constraints
|
||||
self.constraints.extend([
|
||||
eq(a=h_nw.y, b=h_ne.y),
|
||||
eq(a=h_nw.x, b=h_sw.x),
|
||||
eq(a=h_se.y, b=h_sw.y),
|
||||
eq(a=h_se.x, b=h_ne.x),
|
||||
# set h_nw < h_se constraints
|
||||
# with minimal size functionality
|
||||
self._c_min_w,
|
||||
self._c_min_h,
|
||||
])
|
||||
self.constraint(horizontal=(h_nw.pos, h_ne.pos))
|
||||
self.constraint(horizontal=(h_se.pos, h_sw.pos))
|
||||
self.constraint(vertical=(h_nw.pos, h_sw.pos))
|
||||
self.constraint(vertical=(h_se.pos, h_ne.pos))
|
||||
|
||||
# create minimal size constraints
|
||||
self._c_min_w = self.constraint(left_of=(h_nw.pos, h_se.pos), delta=10)
|
||||
self._c_min_h = self.constraint(above=(h_nw.pos, h_se.pos), delta=10)
|
||||
|
||||
# set width/height when minimal size constraints exist
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
|
||||
def setup_canvas(self):
|
||||
super(Element, self).setup_canvas()
|
||||
|
||||
@ -328,13 +399,13 @@ class Element(Item):
|
||||
min_height = reversible_property(lambda s: s._c_min_h.delta, _set_min_height)
|
||||
|
||||
|
||||
def point(self, x, y):
|
||||
def point(self, pos):
|
||||
"""
|
||||
Distance from the point (x, y) to the item.
|
||||
"""
|
||||
h = self._handles
|
||||
hnw, hse = h[NW], h[SE]
|
||||
return distance_rectangle_point(map(float, (hnw.x, hnw.y, hse.x, hse.y)), (x, y))
|
||||
return distance_rectangle_point(map(float, (hnw.x, hnw.y, hse.x, hse.y)), pos)
|
||||
|
||||
|
||||
class Line(Item):
|
||||
@ -358,7 +429,9 @@ class Line(Item):
|
||||
|
||||
def __init__(self):
|
||||
super(Line, self).__init__()
|
||||
self._handles = [Handle(connectable=True), Handle(10, 10, connectable=True)]
|
||||
self._handles = [Handle(connectable=True), Handle((10, 10), connectable=True)]
|
||||
self._ports = []
|
||||
self._update_ports()
|
||||
|
||||
self._line_width = 2
|
||||
self._fuzziness = 0
|
||||
@ -395,8 +468,8 @@ class Line(Item):
|
||||
return
|
||||
|
||||
h = self._handles
|
||||
if len(h) < 3:
|
||||
self.split_segment(0)
|
||||
#if len(h) < 3:
|
||||
# self.split_segment(0)
|
||||
eq = EqualsConstraint #lambda a, b: a - b
|
||||
add = self.canvas.solver.add_constraint
|
||||
cons = []
|
||||
@ -478,98 +551,35 @@ class Line(Item):
|
||||
reversible_pair(_reversible_insert_handle, _reversible_remove_handle, \
|
||||
bind1={'index': lambda self, handle: self._handles.index(handle)})
|
||||
|
||||
@observed
|
||||
def _reversible_insert_port(self, index, port):
|
||||
self._ports.insert(index, port)
|
||||
|
||||
def split_segment(self, segment, parts=2):
|
||||
@observed
|
||||
def _reversible_remove_port(self, port):
|
||||
self._ports.remove(port)
|
||||
|
||||
reversible_pair(_reversible_insert_port, _reversible_remove_port, \
|
||||
bind1={'index': lambda self, port: self._ports.index(port)})
|
||||
|
||||
|
||||
def _create_handle(self, pos, strength=WEAK):
|
||||
return Handle(pos, strength=strength)
|
||||
|
||||
|
||||
def _create_port(self, h1, h2):
|
||||
return LinePort(h1.pos, h2.pos)
|
||||
|
||||
|
||||
def _update_ports(self):
|
||||
"""
|
||||
Split one segment in the Line in ``parts`` equal pieces.
|
||||
``segment`` 0 is the first segment (between handles 0 and 1).
|
||||
The min number of parts is 2.
|
||||
|
||||
A list of new handles is returned.
|
||||
|
||||
Note that ``split_segment`` is not able to reconnect constraints that
|
||||
are connected to the segment.
|
||||
|
||||
>>> a = Line()
|
||||
>>> a.handles()[1].pos = (20, 0)
|
||||
>>> len(a.handles())
|
||||
2
|
||||
>>> a.split_segment(0)
|
||||
[<Handle object on (10, 0)>]
|
||||
>>> a.handles()
|
||||
[<Handle object on (0, 0)>, <Handle object on (10, 0)>, <Handle object on (20, 0)>]
|
||||
|
||||
A line segment can be split into multiple (equal) parts:
|
||||
|
||||
>>> b = Line()
|
||||
>>> b.handles()[1].pos = (20, 16)
|
||||
>>> b.handles()
|
||||
[<Handle object on (0, 0)>, <Handle object on (20, 16)>]
|
||||
>>> b.split_segment(0, parts=4)
|
||||
[<Handle object on (5, 4)>, <Handle object on (10, 8)>, <Handle object on (15, 12)>]
|
||||
>>> len(b.handles())
|
||||
5
|
||||
>>> b.handles()
|
||||
[<Handle object on (0, 0)>, <Handle object on (5, 4)>, <Handle object on (10, 8)>, <Handle object on (15, 12)>, <Handle object on (20, 16)>]
|
||||
Update line ports.
|
||||
"""
|
||||
assert parts >= 2
|
||||
assert segment >= 0
|
||||
def do_split(segment, parts):
|
||||
h0 = self._handles[segment]
|
||||
h1 = self._handles[segment + 1]
|
||||
dx, dy = h1.x - h0.x, h1.y - h0.y
|
||||
new_h = Handle(h0.x + dx / parts, h0.y + dy / parts, strength=WEAK)
|
||||
self._reversible_insert_handle(segment + 1, new_h)
|
||||
if parts > 2:
|
||||
do_split(segment + 1, parts - 1)
|
||||
do_split(segment, parts)
|
||||
# Force orthogonal constraints to be recreated
|
||||
self._update_orthogonal_constraints(self.orthogonal)
|
||||
return self._handles[segment+1:segment+parts]
|
||||
|
||||
def merge_segment(self, segment, parts=2):
|
||||
"""
|
||||
Merge the ``segment`` and the next.
|
||||
The parts parameter indicates how many segments should be merged
|
||||
|
||||
The deleted handles are returned as a list.
|
||||
|
||||
>>> a = Line()
|
||||
>>> a.handles()[1].pos = (20, 0)
|
||||
>>> _ = a.split_segment(0)
|
||||
>>> a.handles()
|
||||
[<Handle object on (0, 0)>, <Handle object on (10, 0)>, <Handle object on (20, 0)>]
|
||||
>>> a.merge_segment(0)
|
||||
[<Handle object on (10, 0)>]
|
||||
>>> a.handles()
|
||||
[<Handle object on (0, 0)>, <Handle object on (20, 0)>]
|
||||
>>> try: a.merge_segment(0)
|
||||
... except AssertionError: print 'okay'
|
||||
okay
|
||||
|
||||
More than two segments can be merged at once:
|
||||
>>> _ = a.split_segment(0)
|
||||
>>> _ = a.split_segment(0)
|
||||
>>> _ = a.split_segment(0)
|
||||
>>> a.handles()
|
||||
[<Handle object on (0, 0)>, <Handle object on (2.5, 0)>, <Handle object on (5, 0)>, <Handle object on (10, 0)>, <Handle object on (20, 0)>]
|
||||
>>> a.merge_segment(0, parts=4)
|
||||
[<Handle object on (2.5, 0)>, <Handle object on (5, 0)>, <Handle object on (10, 0)>]
|
||||
>>> a.handles()
|
||||
[<Handle object on (0, 0)>, <Handle object on (20, 0)>]
|
||||
"""
|
||||
assert len(self._handles) > 2, 'Not enough segments'
|
||||
if 0 >= segment > len(self._handles) - 1:
|
||||
raise IndexError("index out of range (0 > %d > %d)" % (segment, len(self._handles) - 1))
|
||||
if segment == 0: segment = 1
|
||||
deleted_handles = self._handles[segment:segment+parts-1]
|
||||
self._reversible_remove_handle(self._handles[segment])
|
||||
if parts > 2:
|
||||
self.merge_segment(segment, parts - 1)
|
||||
else:
|
||||
# Force orthogonal constraints to be recreated
|
||||
self._update_orthogonal_constraints(self.orthogonal)
|
||||
return deleted_handles
|
||||
assert len(self._handles) >= 2, 'Not enough segments'
|
||||
self._ports = []
|
||||
handles = self._handles
|
||||
for h1, h2 in zip(handles[:-1], handles[1:]):
|
||||
self._ports.append(LinePort(h1.pos, h2.pos))
|
||||
|
||||
|
||||
def opposite(self, handle):
|
||||
@ -593,7 +603,7 @@ class Line(Item):
|
||||
h1, h0 = self._handles[-2:]
|
||||
self._tail_angle = atan2(h1.y - h0.y, h1.x - h0.x)
|
||||
|
||||
def closest_segment(self, x, y):
|
||||
def closest_segment(self, pos):
|
||||
"""
|
||||
Obtain a tuple (distance, point_on_line, segment).
|
||||
Distance is the distance from point to the closest line segment
|
||||
@ -601,32 +611,30 @@ class Line(Item):
|
||||
Segment is the line segment closest to (x, y)
|
||||
|
||||
>>> a = Line()
|
||||
>>> a.closest_segment(4, 5)
|
||||
>>> a.closest_segment((4, 5))
|
||||
(0.70710678118654757, (4.5, 4.5), 0)
|
||||
"""
|
||||
h = self._handles
|
||||
|
||||
# create a list of (distance, point_on_line) tuples:
|
||||
distances = map(distance_line_point, h[:-1], h[1:], [(x, y)] * (len(h) - 1))
|
||||
distances = map(distance_line_point, h[:-1], h[1:], [pos] * (len(h) - 1))
|
||||
distances, pols = zip(*distances)
|
||||
return reduce(min, zip(distances, pols, range(len(distances))))
|
||||
|
||||
def point(self, x, y):
|
||||
def point(self, pos):
|
||||
"""
|
||||
>>> a = Line()
|
||||
>>> a.handles()[1].pos = 30, 30
|
||||
>>> a.split_segment(0)
|
||||
[<Handle object on (15, 15)>]
|
||||
>>> a.handles()[1].pos = 25, 5
|
||||
>>> a.point(-1, 0)
|
||||
>>> a._handles.append(a._create_handle((30, 30)))
|
||||
>>> a.point((-1, 0))
|
||||
1.0
|
||||
>>> '%.3f' % a.point(5, 4)
|
||||
>>> '%.3f' % a.point((5, 4))
|
||||
'2.942'
|
||||
>>> '%.3f' % a.point(29, 29)
|
||||
>>> '%.3f' % a.point((29, 29))
|
||||
'0.784'
|
||||
"""
|
||||
h = self._handles
|
||||
distance, point, segment = self.closest_segment(x, y)
|
||||
distance, point, segment = self.closest_segment(pos)
|
||||
return max(0, distance - self.fuzziness)
|
||||
|
||||
def draw_head(self, context):
|
||||
@ -666,6 +674,15 @@ class Line(Item):
|
||||
draw_line_end(self._handles[-1], self._tail_angle, self.draw_tail)
|
||||
cr.stroke()
|
||||
|
||||
### debug code to draw line ports
|
||||
### cr.set_line_width(1)
|
||||
### cr.set_source_rgb(1.0, 0.0, 0.0)
|
||||
### for p in self.ports():
|
||||
### cr.move_to(*p.start)
|
||||
### cr.line_to(*p.end)
|
||||
### cr.stroke()
|
||||
|
||||
|
||||
|
||||
__test__ = {
|
||||
'Line._set_orthogonal': Line._set_orthogonal,
|
||||
|
@ -12,8 +12,9 @@ __version__ = "$Revision$"
|
||||
|
||||
from cairo import Matrix, ANTIALIAS_NONE, LINE_JOIN_ROUND
|
||||
|
||||
from canvas import Context
|
||||
from geometry import Rectangle
|
||||
from gaphas.canvas import Context
|
||||
from gaphas.geometry import Rectangle
|
||||
from gaphas.item import Line
|
||||
|
||||
DEBUG_DRAW_BOUNDING_BOX = False
|
||||
|
||||
@ -258,7 +259,6 @@ class BoundingBoxPainter(ItemPainter):
|
||||
for item in items:
|
||||
self._draw_item(item, view, cairo)
|
||||
|
||||
|
||||
def paint(self, context):
|
||||
cairo = context.cairo
|
||||
view = context.view
|
||||
@ -271,13 +271,15 @@ class HandlePainter(Painter):
|
||||
Draw handles of items that are marked as selected in the view.
|
||||
"""
|
||||
|
||||
def _draw_handles(self, item, view, cairo, opacity):
|
||||
def _draw_handles(self, item, view, cairo, opacity=None):
|
||||
"""
|
||||
Draw handles for an item.
|
||||
The handles are drawn in non-antialiased mode for clearity.
|
||||
"""
|
||||
cairo.save()
|
||||
i2v = view.get_matrix_i2v(item)
|
||||
if not opacity:
|
||||
opacity = (item is view.focused_item) and .7 or .4
|
||||
|
||||
cairo.set_line_width(1)
|
||||
|
||||
@ -310,17 +312,13 @@ class HandlePainter(Painter):
|
||||
view = context.view
|
||||
canvas = view.canvas
|
||||
cairo = context.cairo
|
||||
|
||||
# Selected items are already ordered:
|
||||
focused = view.focused_item
|
||||
for item in view.selected_items:
|
||||
self._draw_handles(item, view, cairo,
|
||||
opacity=(item is focused) and .7 or .4)
|
||||
|
||||
# Order matters here:
|
||||
for item in canvas.sort(view.selected_items):
|
||||
self._draw_handles(item, view, cairo)
|
||||
# Draw nice opaque handles when hovering an item:
|
||||
hovered = view.hovered_item
|
||||
if hovered:
|
||||
self._draw_handles(hovered, view, cairo, opacity=.25)
|
||||
item = view.hovered_item
|
||||
if item and item not in view.selected_items:
|
||||
self._draw_handles(item, view, cairo, opacity=.25)
|
||||
|
||||
|
||||
class ToolPainter(Painter):
|
||||
@ -339,6 +337,41 @@ class ToolPainter(Painter):
|
||||
cairo.restore()
|
||||
|
||||
|
||||
class LineSegmentPainter(Painter):
|
||||
"""
|
||||
This painter draws pseudo-hanldes on gaphas.item.Line objects. Each
|
||||
line can be split by dragging those points, which will result in
|
||||
a new handle.
|
||||
|
||||
ConnectHandleTool take care of performing the user
|
||||
interaction required for this feature.
|
||||
"""
|
||||
|
||||
def paint(self, context):
|
||||
view = context.view
|
||||
item = view.hovered_item
|
||||
if item and item is view.focused_item and isinstance(item, Line):
|
||||
cr = context.cairo
|
||||
h = item.handles()
|
||||
for h1, h2 in zip(h[:-1], h[1:]):
|
||||
cx = (h1.x + h2.x) / 2
|
||||
cy = (h1.y + h2.y) / 2
|
||||
cr.save()
|
||||
cr.identity_matrix()
|
||||
m = Matrix(*view.get_matrix_i2v(item))
|
||||
|
||||
cr.set_antialias(ANTIALIAS_NONE)
|
||||
cr.translate(*m.transform_point(cx, cy))
|
||||
cr.rectangle(-3, -3, 6, 6)
|
||||
cr.set_source_rgba(0, 0.5, 0, .4)
|
||||
cr.fill_preserve()
|
||||
cr.set_source_rgba(.25, .25, .25, .6)
|
||||
cr.set_line_width(1)
|
||||
cr.stroke()
|
||||
cr.restore()
|
||||
|
||||
|
||||
|
||||
def DefaultPainter():
|
||||
"""
|
||||
Default painter, containing item, handle and tool painters.
|
||||
@ -346,6 +379,7 @@ def DefaultPainter():
|
||||
chain = PainterChain()
|
||||
chain.append(ItemPainter())
|
||||
chain.append(HandlePainter())
|
||||
chain.append(LineSegmentPainter())
|
||||
chain.append(ToolPainter())
|
||||
return chain
|
||||
|
||||
|
69
gaphas/tests/test_constraints.py
Normal file
69
gaphas/tests/test_constraints.py
Normal file
@ -0,0 +1,69 @@
|
||||
import unittest
|
||||
|
||||
from gaphas.solver import Variable
|
||||
from gaphas.constraint import PositionConstraint, LineAlignConstraint
|
||||
|
||||
class PositionTestCase(unittest.TestCase):
|
||||
def test_pos_constraint(self):
|
||||
"""Test position constraint"""
|
||||
x1, y1 = Variable(10), Variable(11)
|
||||
x2, y2 = Variable(12), Variable(13)
|
||||
pc = PositionConstraint(origin=(x1, y1), point=(x2, y2))
|
||||
pc.solve_for()
|
||||
|
||||
# origin shall remain the same
|
||||
self.assertEquals(10, x1)
|
||||
self.assertEquals(11, y1)
|
||||
|
||||
# point shall be moved to origin
|
||||
self.assertEquals(10, x2)
|
||||
self.assertEquals(11, y2)
|
||||
|
||||
# change just x of origin
|
||||
x1.value = 15
|
||||
pc.solve_for()
|
||||
self.assertEquals(15, x2)
|
||||
|
||||
# change just y of origin
|
||||
y1.value = 14
|
||||
pc.solve_for()
|
||||
self.assertEquals(14, y2)
|
||||
|
||||
|
||||
|
||||
class LineAlignConstraintTestCase(unittest.TestCase):
|
||||
"""
|
||||
Line align constraint test case.
|
||||
"""
|
||||
def test_delta(self):
|
||||
"""Test line align delta
|
||||
"""
|
||||
line = (Variable(0), Variable(0)), (Variable(30), Variable(20))
|
||||
point = (Variable(15), Variable(10))
|
||||
lc = LineAlignConstraint(line=line, point=point, align=0.5, delta=5)
|
||||
lc.solve_for()
|
||||
self.assertAlmostEqual(19.16, point[0].value, 0.01)
|
||||
self.assertAlmostEqual(12.77, point[1].value, 0.01)
|
||||
|
||||
line[1][0].value = 40
|
||||
line[1][1].value = 30
|
||||
lc.solve_for()
|
||||
self.assertAlmostEqual(24.00, point[0].value, 0.01)
|
||||
self.assertAlmostEqual(18.00, point[1].value, 0.01)
|
||||
|
||||
|
||||
def test_delta_below_zero(self):
|
||||
"""Test line align with delta below zero
|
||||
"""
|
||||
line = (Variable(0), Variable(0)), (Variable(30), Variable(20))
|
||||
point = (Variable(15), Variable(10))
|
||||
lc = LineAlignConstraint(line=line, point=point, align=0.5, delta=-5)
|
||||
lc.solve_for()
|
||||
self.assertAlmostEqual(10.83, point[0].value, 0.01)
|
||||
self.assertAlmostEqual(7.22, point[1].value, 0.01)
|
||||
|
||||
line[1][0].value = 40
|
||||
line[1][1].value = 30
|
||||
lc.solve_for()
|
||||
self.assertAlmostEqual(16.25, point[0].value, 0.01)
|
||||
self.assertAlmostEqual(12.00, point[1].value, 0.01)
|
96
gaphas/tests/test_item.py
Normal file
96
gaphas/tests/test_item.py
Normal file
@ -0,0 +1,96 @@
|
||||
"""
|
||||
Generic gaphas item tests.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
from gaphas.item import Item
|
||||
from gaphas.constraint import LineAlignConstraint, LineConstraint, \
|
||||
EqualsConstraint, LessThanConstraint
|
||||
from gaphas.solver import Variable
|
||||
|
||||
class ItemConstraintTestCase(unittest.TestCase):
|
||||
"""
|
||||
Item constraint creation tests. The test check functionality of
|
||||
`Item.constraint` method, not constraints themselves.
|
||||
"""
|
||||
def test_line_constraint(self):
|
||||
"""
|
||||
Test line creation constraint.
|
||||
"""
|
||||
item = Item()
|
||||
pos = Variable(1), Variable(2)
|
||||
line = (Variable(3), Variable(4)), (Variable(5), Variable(6))
|
||||
item.constraint(line=(pos, line))
|
||||
self.assertEquals(1, len(item._constraints))
|
||||
|
||||
c = item._constraints[0]
|
||||
self.assertTrue(isinstance(c, LineConstraint))
|
||||
self.assertEquals((1, 2), c._point)
|
||||
self.assertEquals(((3, 4), (5, 6)), c._line)
|
||||
|
||||
|
||||
def test_horizontal_constraint(self):
|
||||
"""
|
||||
Test horizontal constraint creation.
|
||||
"""
|
||||
item = Item()
|
||||
p1 = Variable(1), Variable(2)
|
||||
p2 = Variable(3), Variable(4)
|
||||
item.constraint(horizontal=(p1, p2))
|
||||
self.assertEquals(1, len(item._constraints))
|
||||
|
||||
c = item._constraints[0]
|
||||
self.assertTrue(isinstance(c, EqualsConstraint))
|
||||
# expect constraint on y-axis
|
||||
self.assertEquals(2, c.a)
|
||||
self.assertEquals(4, c.b)
|
||||
|
||||
|
||||
def test_vertical_constraint(self):
|
||||
"""
|
||||
Test vertical constraint creation.
|
||||
"""
|
||||
item = Item()
|
||||
p1 = Variable(1), Variable(2)
|
||||
p2 = Variable(3), Variable(4)
|
||||
item.constraint(vertical=(p1, p2))
|
||||
self.assertEquals(1, len(item._constraints))
|
||||
|
||||
c = item._constraints[0]
|
||||
self.assertTrue(isinstance(c, EqualsConstraint))
|
||||
# expect constraint on x-axis
|
||||
self.assertEquals(1, c.a)
|
||||
self.assertEquals(3, c.b)
|
||||
|
||||
|
||||
def test_left_of_constraint(self):
|
||||
"""
|
||||
Test "less than" constraint (horizontal) creation.
|
||||
"""
|
||||
item = Item()
|
||||
p1 = Variable(1), Variable(2)
|
||||
p2 = Variable(3), Variable(4)
|
||||
item.constraint(left_of=(p1, p2))
|
||||
self.assertEquals(1, len(item._constraints))
|
||||
|
||||
c = item._constraints[0]
|
||||
self.assertTrue(isinstance(c, LessThanConstraint))
|
||||
self.assertEquals(2, c.smaller)
|
||||
self.assertEquals(4, c.bigger)
|
||||
|
||||
|
||||
def test_above_constraint(self):
|
||||
"""
|
||||
Test "less than" constraint (vertical) creation.
|
||||
"""
|
||||
item = Item()
|
||||
p1 = Variable(1), Variable(2)
|
||||
p2 = Variable(3), Variable(4)
|
||||
item.constraint(above=(p1, p2))
|
||||
self.assertEquals(1, len(item._constraints))
|
||||
|
||||
c = item._constraints[0]
|
||||
self.assertTrue(isinstance(c, LessThanConstraint))
|
||||
self.assertEquals(1, c.smaller)
|
||||
self.assertEquals(3, c.bigger)
|
@ -20,8 +20,12 @@ def undo():
|
||||
redo_list[:] = undo_list[:]
|
||||
del undo_list[:]
|
||||
|
||||
class LineTestCase(unittest.TestCase):
|
||||
|
||||
|
||||
class TestCaseBase(unittest.TestCase):
|
||||
"""
|
||||
Abstract test case class with undo support.
|
||||
"""
|
||||
def setUp(self):
|
||||
state.observers.add(state.revert_handler)
|
||||
state.subscribers.add(undo_handler)
|
||||
@ -30,80 +34,71 @@ class LineTestCase(unittest.TestCase):
|
||||
state.observers.remove(state.revert_handler)
|
||||
state.subscribers.remove(undo_handler)
|
||||
|
||||
|
||||
|
||||
class LineTestCase(TestCaseBase):
|
||||
"""
|
||||
Basic line item tests.
|
||||
"""
|
||||
|
||||
def test_initial_ports(self):
|
||||
"""Test initial ports amount
|
||||
"""
|
||||
line = Line()
|
||||
self.assertEquals(1, len(line.ports()))
|
||||
|
||||
|
||||
def test_orthogonal_horizontal_undo(self):
|
||||
"""
|
||||
Orthogonal line constraints bug (#107)
|
||||
"""Test orthogonal line constraints bug (#107)
|
||||
"""
|
||||
canvas = Canvas()
|
||||
line = Line()
|
||||
canvas.add(line)
|
||||
|
||||
assert len(canvas.solver._constraints) == 0
|
||||
|
||||
line.orthogonal = True
|
||||
|
||||
assert len(canvas.solver._constraints) == 2
|
||||
after_ortho = set(canvas.solver._constraints)
|
||||
|
||||
del undo_list[:]
|
||||
line.horizontal = True
|
||||
|
||||
assert len(canvas.solver._constraints) == 2
|
||||
|
||||
undo()
|
||||
|
||||
assert not line.horizontal
|
||||
assert len(canvas.solver._constraints) == 2, canvas.solver._constraints
|
||||
|
||||
line.horizontal = True
|
||||
|
||||
assert line.horizontal
|
||||
assert len(canvas.solver._constraints) == 2, canvas.solver._constraints
|
||||
|
||||
def test_orthogonal_line_split_segment(self):
|
||||
canvas = Canvas()
|
||||
line = Line()
|
||||
canvas.add(line)
|
||||
|
||||
assert len(canvas.solver._constraints) == 0
|
||||
|
||||
line.orthogonal = True
|
||||
|
||||
assert len(canvas.solver._constraints) == 2
|
||||
self.assertEquals(1, len(canvas.solver._constraints))
|
||||
after_ortho = set(canvas.solver._constraints)
|
||||
assert len(line.handles()) == 3
|
||||
|
||||
del undo_list[:]
|
||||
line.horizontal = True
|
||||
|
||||
line.split_segment(0)
|
||||
|
||||
assert len(canvas.solver._constraints) == 3
|
||||
assert len(line.handles()) == 4
|
||||
self.assertEquals(1, len(canvas.solver._constraints))
|
||||
|
||||
undo()
|
||||
|
||||
assert len(canvas.solver._constraints) == 2
|
||||
assert len(line.handles()) == 3
|
||||
assert canvas.solver._constraints == after_ortho
|
||||
self.assertFalse(line.horizontal)
|
||||
self.assertEquals(1, len(canvas.solver._constraints))
|
||||
|
||||
line.split_segment(0)
|
||||
line.horizontal = True
|
||||
|
||||
assert len(canvas.solver._constraints) == 3
|
||||
assert len(line.handles()) == 4
|
||||
after_split = set(canvas.solver._constraints)
|
||||
self.assertTrue(line.horizontal)
|
||||
self.assertEquals(1, len(canvas.solver._constraints))
|
||||
|
||||
del undo_list[:]
|
||||
|
||||
line.merge_segment(0)
|
||||
def test_orthogonal_line_undo(self):
|
||||
"""Test orthogonal line undo
|
||||
"""
|
||||
canvas = Canvas()
|
||||
line = Line()
|
||||
canvas.add(line)
|
||||
|
||||
assert len(canvas.solver._constraints) == 2
|
||||
assert len(line.handles()) == 3
|
||||
# start with no orthogonal constraints
|
||||
assert len(canvas.solver._constraints) == 0
|
||||
|
||||
line.orthogonal = True
|
||||
|
||||
# check orthogonal constraints
|
||||
assert len(canvas.solver._constraints) == 1
|
||||
assert len(line.handles()) == 2
|
||||
|
||||
undo()
|
||||
|
||||
assert len(canvas.solver._constraints) == 3
|
||||
assert len(line.handles()) == 4
|
||||
assert canvas.solver._constraints == after_split
|
||||
self.assertFalse(line.orthogonal)
|
||||
self.assertEquals(0, len(canvas.solver._constraints))
|
||||
self.assertEquals(2, len(line.handles()))
|
||||
|
||||
|
||||
# vim:sw=4:et
|
||||
|
@ -161,16 +161,16 @@ class PickleTestCase(unittest.TestCase):
|
||||
|
||||
def test_pickle_with_gtk_view_with_connection(self):
|
||||
canvas = create_canvas()
|
||||
box = canvas._tree.nodes[1]
|
||||
box = canvas._tree.nodes[0]
|
||||
assert isinstance(box, Box)
|
||||
line = canvas._tree.nodes[2]
|
||||
assert isinstance(line, Line)
|
||||
|
||||
view = GtkView(canvas=canvas)
|
||||
|
||||
from gaphas.examples import ConnectingHandleTool
|
||||
handle_tool = ConnectingHandleTool()
|
||||
handle_tool.connect(view, line, line.handles()[0], 0, 0)
|
||||
from gaphas.tool import ConnectHandleTool
|
||||
handle_tool = ConnectHandleTool()
|
||||
handle_tool.connect(view, line, line.handles()[0], (40, 0))
|
||||
assert line.handles()[0].connected_to is box, line.handles()[0].connected_to
|
||||
assert line.handles()[0].connection_data
|
||||
assert line.handles()[0].disconnect
|
||||
|
746
gaphas/tests/test_tool.py
Normal file
746
gaphas/tests/test_tool.py
Normal file
@ -0,0 +1,746 @@
|
||||
"""
|
||||
Test all the tools provided by gaphas.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
from gaphas.tool import ConnectHandleTool, LineSegmentTool
|
||||
from gaphas.canvas import Canvas
|
||||
from gaphas.examples import Box
|
||||
from gaphas.item import Item, Element, Line
|
||||
from gaphas.view import View, GtkView
|
||||
from gaphas.constraint import LineConstraint
|
||||
from gaphas.canvas import Context
|
||||
from gaphas import state
|
||||
|
||||
Event = Context
|
||||
|
||||
undo_list = []
|
||||
redo_list = []
|
||||
|
||||
|
||||
def undo_handler(event):
|
||||
undo_list.append(event)
|
||||
|
||||
|
||||
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 simple_canvas(self):
|
||||
"""
|
||||
This decorator adds view, canvas and handle connection tool to a test
|
||||
case. Two boxes and a line are added to the canvas as well.
|
||||
"""
|
||||
self.canvas = Canvas()
|
||||
|
||||
self.box1 = Box()
|
||||
self.canvas.add(self.box1)
|
||||
self.box1.matrix.translate(100, 50)
|
||||
self.box1.width = 40
|
||||
self.box1.height = 40
|
||||
self.box1.request_update()
|
||||
|
||||
self.box2 = Box()
|
||||
self.canvas.add(self.box2)
|
||||
self.box2.matrix.translate(100, 150)
|
||||
self.box2.width = 50
|
||||
self.box2.height = 50
|
||||
self.box2.request_update()
|
||||
|
||||
self.line = Line()
|
||||
self.head = self.line.handles()[0]
|
||||
self.tail = self.line.handles()[-1]
|
||||
self.tail.pos = 100, 100
|
||||
self.canvas.add(self.line)
|
||||
|
||||
self.canvas.update_now()
|
||||
self.view = GtkView()
|
||||
self.view.canvas = self.canvas
|
||||
import gtk
|
||||
win = gtk.Window()
|
||||
win.add(self.view)
|
||||
self.view.show()
|
||||
self.view.update()
|
||||
win.show()
|
||||
|
||||
self.tool = ConnectHandleTool()
|
||||
|
||||
|
||||
|
||||
class TestCaseBase(unittest.TestCase):
|
||||
"""
|
||||
Abstract test case class with undo support.
|
||||
"""
|
||||
def setUp(self):
|
||||
state.observers.add(state.revert_handler)
|
||||
state.subscribers.add(undo_handler)
|
||||
simple_canvas(self)
|
||||
|
||||
def tearDown(self):
|
||||
state.observers.remove(state.revert_handler)
|
||||
state.subscribers.remove(undo_handler)
|
||||
|
||||
|
||||
class ConnectHandleToolGlueTestCase(unittest.TestCase):
|
||||
"""
|
||||
Test handle connection tool glue method.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
simple_canvas(self)
|
||||
|
||||
|
||||
def test_item_and_port_glue(self):
|
||||
"""Test glue operation to an item and its ports"""
|
||||
|
||||
ports = self.box1.ports()
|
||||
|
||||
# glue to port nw-ne
|
||||
item, port = self.tool.glue(self.view, self.line, self.head, (120, 50))
|
||||
self.assertEquals(item, self.box1)
|
||||
self.assertEquals(ports[0], port)
|
||||
|
||||
# glue to port ne-se
|
||||
item, port = self.tool.glue(self.view, self.line, self.head, (140, 70))
|
||||
self.assertEquals(item, self.box1)
|
||||
self.assertEquals(ports[1], port)
|
||||
|
||||
# glue to port se-sw
|
||||
item, port = self.tool.glue(self.view, self.line, self.head, (120, 90))
|
||||
self.assertEquals(item, self.box1)
|
||||
self.assertEquals(ports[2], port)
|
||||
|
||||
# glue to port sw-nw
|
||||
item, port = self.tool.glue(self.view, self.line, self.head, (100, 70))
|
||||
self.assertEquals(item, self.box1)
|
||||
self.assertEquals(ports[3], port)
|
||||
|
||||
|
||||
def test_failed_glue(self):
|
||||
"""Test glue from too far distance"""
|
||||
item, port = self.tool.glue(self.view, self.line, self.head, (90, 50))
|
||||
self.assertTrue(item is None)
|
||||
self.assertTrue(port is None)
|
||||
|
||||
|
||||
def test_glue_call_can_glue_once(self):
|
||||
"""Test if glue method calls can glue once only
|
||||
|
||||
Box has 4 ports. Every port is examined once per
|
||||
ConnectHandleTool.glue method call. The purpose of this test is to
|
||||
assure that ConnectHandleTool.can_glue is called once (for the
|
||||
found port), it cannot be called four times (once for every port).
|
||||
"""
|
||||
|
||||
# count ConnectHandleTool.can_glue calls
|
||||
class Tool(ConnectHandleTool):
|
||||
def __init__(self, *args):
|
||||
super(Tool, self).__init__(*args)
|
||||
self._calls = 0
|
||||
|
||||
def can_glue(self, *args):
|
||||
self._calls += 1
|
||||
return True
|
||||
|
||||
tool = Tool()
|
||||
item, port = tool.glue(self.view, self.line, self.head, (120, 50))
|
||||
assert item and port
|
||||
self.assertEquals(1, tool._calls)
|
||||
|
||||
|
||||
def test_glue_cannot_glue(self):
|
||||
"""Test if glue method respects ConnectHandleTool.can_glue method"""
|
||||
|
||||
class Tool(ConnectHandleTool):
|
||||
def can_glue(self, *args):
|
||||
return False
|
||||
|
||||
tool = Tool()
|
||||
item, port = tool.glue(self.view, self.line, self.head, (120, 50))
|
||||
self.assertTrue(item is None)
|
||||
self.assertTrue(port is None)
|
||||
|
||||
|
||||
def test_glue_no_port_no_can_glue(self):
|
||||
"""Test if glue method does not call ConnectHandleTool.can_glue method when port is not found"""
|
||||
|
||||
class Tool(ConnectHandleTool):
|
||||
def __init__(self, *args):
|
||||
super(Tool, self).__init__(*args)
|
||||
self._calls = 0
|
||||
|
||||
def can_glue(self, *args):
|
||||
self._calls += 1
|
||||
|
||||
tool = Tool()
|
||||
# at 300, 50 there should be no item
|
||||
item, port = tool.glue(self.view, self.line, self.head, (300, 50))
|
||||
assert item is None and port is None
|
||||
self.assertEquals(0, tool._calls)
|
||||
|
||||
|
||||
|
||||
class ConnectHandleToolConnectTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
simple_canvas(self)
|
||||
|
||||
|
||||
def _get_line(self):
|
||||
line = Line()
|
||||
head = self.line.handles()[0]
|
||||
self.canvas.add(line)
|
||||
return line, head
|
||||
|
||||
|
||||
def test_connect(self):
|
||||
"""Test connection to an item"""
|
||||
line, head = self._get_line()
|
||||
self.tool.connect(self.view, line, head, (120, 50))
|
||||
self.assertEquals(self.box1, head.connected_to)
|
||||
self.assertTrue(head.connection_data is not None)
|
||||
self.assertTrue(isinstance(head.connection_data, LineConstraint))
|
||||
self.assertTrue(head.disconnect is not None)
|
||||
|
||||
line, head = self._get_line()
|
||||
self.tool.connect(self.view, line, head, (90, 50))
|
||||
self.assertTrue(head.connected_to is None)
|
||||
self.assertTrue(head.connection_data is None)
|
||||
|
||||
|
||||
def test_disconnect(self):
|
||||
"""Test disconnection from an item"""
|
||||
line, head = self._get_line()
|
||||
self.tool.connect(self.view, line, head, (120, 50))
|
||||
assert head.connected_to is not None
|
||||
|
||||
self.tool.disconnect(self.view, line, head)
|
||||
self.assertTrue(head.connected_to is None)
|
||||
self.assertTrue(head.connection_data is None)
|
||||
|
||||
|
||||
def test_reconnect_another(self):
|
||||
"""Test reconnection to another item"""
|
||||
line, head = self._get_line()
|
||||
self.tool.connect(self.view, line, head, (120, 50))
|
||||
assert head.connected_to is not None
|
||||
item = head.connected_to
|
||||
constraint = head.connection_data
|
||||
|
||||
assert item == self.box1
|
||||
assert item != self.box2
|
||||
|
||||
# connect to box2, handle's connected item and connection data
|
||||
# should differ
|
||||
self.tool.connect(self.view, line, head, (120, 150))
|
||||
assert head.connected_to is not None
|
||||
self.assertEqual(self.box2, head.connected_to)
|
||||
self.assertNotEqual(item, head.connected_to)
|
||||
self.assertNotEqual(constraint, head.connection_data)
|
||||
|
||||
|
||||
def test_reconnect_same(self):
|
||||
"""Test reconnection to same item"""
|
||||
line, head = self._get_line()
|
||||
self.tool.connect(self.view, line, head, (120, 50))
|
||||
assert head.connected_to is not None
|
||||
item = head.connected_to
|
||||
constraint = head.connection_data
|
||||
|
||||
assert item == self.box1
|
||||
assert item != self.box2
|
||||
|
||||
# connect to box1 again, handle's connected item should be the same
|
||||
# but connection constraint will differ
|
||||
connected = self.tool.connect(self.view, line, head, (120, 50))
|
||||
assert head.connected_to is not None
|
||||
self.assertEqual(self.box1, head.connected_to)
|
||||
self.assertNotEqual(constraint, head.connection_data)
|
||||
|
||||
|
||||
def test_find_port(self):
|
||||
"""Test finding a port
|
||||
"""
|
||||
line, head = self._get_line()
|
||||
p1, p2, p3, p4 = self.box1.ports()
|
||||
|
||||
head.pos = 110, 50
|
||||
port = self.tool.find_port(line, head, self.box1)
|
||||
self.assertEquals(p1, port)
|
||||
|
||||
head.pos = 140, 60
|
||||
port = self.tool.find_port(line, head, self.box1)
|
||||
self.assertEquals(p2, port)
|
||||
|
||||
head.pos = 110, 95
|
||||
port = self.tool.find_port(line, head, self.box1)
|
||||
self.assertEquals(p3, port)
|
||||
|
||||
head.pos = 100, 55
|
||||
port = self.tool.find_port(line, head, self.box1)
|
||||
self.assertEquals(p4, port)
|
||||
|
||||
|
||||
|
||||
class LineSegmentToolTestCase(unittest.TestCase):
|
||||
"""
|
||||
Line segment tool tests.
|
||||
"""
|
||||
def setUp(self):
|
||||
simple_canvas(self)
|
||||
|
||||
def test_split(self):
|
||||
"""Test if line is splitted while pressing it in the middle
|
||||
"""
|
||||
tool = LineSegmentTool()
|
||||
def dummy_grab(): pass
|
||||
|
||||
context = Context(view=self.view,
|
||||
grab=dummy_grab,
|
||||
ungrab=dummy_grab)
|
||||
|
||||
head, tail = self.line.handles()
|
||||
|
||||
self.view.hovered_item = self.line
|
||||
self.view.focused_item = self.line
|
||||
tool.on_button_press(context, Event(x=50, y=50, state=0))
|
||||
self.assertEquals(3, len(self.line.handles()))
|
||||
self.assertEquals(self.head, head)
|
||||
self.assertEquals(self.tail, tail)
|
||||
|
||||
|
||||
def test_merge(self):
|
||||
"""Test if line is merged by moving handle onto adjacent handle
|
||||
"""
|
||||
tool = LineSegmentTool()
|
||||
def dummy_grab(): pass
|
||||
|
||||
context = Context(view=self.view,
|
||||
grab=dummy_grab,
|
||||
ungrab=dummy_grab)
|
||||
|
||||
self.view.hovered_item = self.line
|
||||
self.view.focused_item = self.line
|
||||
tool.on_button_press(context, Event(x=50, y=50, state=0))
|
||||
# start with 2 segments
|
||||
assert len(self.line.handles()) == 3
|
||||
|
||||
# try to merge, now
|
||||
tool.on_button_release(context, Event(x=0, y=0, state=0))
|
||||
self.assertEquals(2, len(self.line.handles()))
|
||||
|
||||
|
||||
def test_merged_segment(self):
|
||||
"""Test if proper segment is merged
|
||||
"""
|
||||
tool = LineSegmentTool()
|
||||
def dummy_grab(): pass
|
||||
|
||||
context = Context(view=self.view,
|
||||
grab=dummy_grab,
|
||||
ungrab=dummy_grab)
|
||||
|
||||
self.view.hovered_item = self.line
|
||||
self.view.focused_item = self.line
|
||||
tool.on_button_press(context, Event(x=50, y=50, state=0))
|
||||
tool.on_button_press(context, Event(x=75, y=75, state=0))
|
||||
# start with 3 segments
|
||||
assert len(self.line.handles()) == 4
|
||||
|
||||
# ports to be removed
|
||||
port1 = self.line.ports()[0]
|
||||
port2 = self.line.ports()[1]
|
||||
|
||||
# try to merge, now
|
||||
tool.grab_handle(self.line, self.line.handles()[1])
|
||||
tool.on_button_release(context, Event(x=0, y=0, state=0))
|
||||
# check if line merging was performed
|
||||
assert len(self.line.handles()) == 3
|
||||
|
||||
# check if proper segments were merged
|
||||
self.assertFalse(port1 in self.line.ports())
|
||||
self.assertFalse(port2 in self.line.ports())
|
||||
|
||||
|
||||
|
||||
class LineSplitTestCase(TestCaseBase):
|
||||
"""
|
||||
Tests for line splitting.
|
||||
"""
|
||||
def test_split_single(self):
|
||||
"""Test single line splitting
|
||||
"""
|
||||
# we start with two handles and one port, after split 3 handles are
|
||||
# expected and 2 ports
|
||||
assert len(self.line.handles()) == 2
|
||||
assert len(self.line.ports()) == 1
|
||||
|
||||
old_port = self.line.ports()[0]
|
||||
|
||||
tool = LineSegmentTool()
|
||||
|
||||
handles, ports = tool.split_segment(self.line, 0)
|
||||
handle = handles[0]
|
||||
self.assertEquals(1, len(handles))
|
||||
self.assertEquals((50, 50), handle.pos)
|
||||
self.assertEquals(3, len(self.line.handles()))
|
||||
self.assertEquals(2, len(self.line.ports()))
|
||||
|
||||
# new handle is between old handles
|
||||
self.assertEquals(handle, self.line.handles()[1])
|
||||
# and old port is deleted
|
||||
self.assertTrue(old_port not in self.line.ports())
|
||||
|
||||
# check ports order
|
||||
p1, p2 = self.line.ports()
|
||||
h1, h2, h3 = self.line.handles()
|
||||
self.assertEquals(h1.pos, p1.start)
|
||||
self.assertEquals(h2.pos, p1.end)
|
||||
self.assertEquals(h2.pos, p2.start)
|
||||
self.assertEquals(h3.pos, p2.end)
|
||||
|
||||
|
||||
def test_split_multiple(self):
|
||||
"""Test multiple line splitting
|
||||
"""
|
||||
self.line.handles()[1].pos = (20, 16)
|
||||
handles = self.line.handles()
|
||||
old_ports = self.line.ports()[:]
|
||||
|
||||
# start with two handles, split into 4 segments - 3 new handles to
|
||||
# be expected
|
||||
assert len(handles) == 2
|
||||
assert len(old_ports) == 1
|
||||
|
||||
tool = LineSegmentTool()
|
||||
|
||||
handles, ports = tool.split_segment(self.line, 0, count=4)
|
||||
self.assertEquals(3, len(handles))
|
||||
h1, h2, h3 = handles
|
||||
self.assertEquals((5, 4), h1.pos)
|
||||
self.assertEquals((10, 8), h2.pos)
|
||||
self.assertEquals((15, 12), h3.pos)
|
||||
|
||||
# new handles between old handles
|
||||
self.assertEquals(5, len(self.line.handles()))
|
||||
self.assertEquals(h1, self.line.handles()[1])
|
||||
self.assertEquals(h2, self.line.handles()[2])
|
||||
self.assertEquals(h3, self.line.handles()[3])
|
||||
|
||||
self.assertEquals(4, len(self.line.ports()))
|
||||
|
||||
# and old port is deleted
|
||||
self.assertTrue(old_ports[0] not in self.line.ports())
|
||||
|
||||
# check ports order
|
||||
p1, p2, p3, p4 = self.line.ports()
|
||||
h1, h2, h3, h4, h5 = self.line.handles()
|
||||
self.assertEquals(h1.pos, p1.start)
|
||||
self.assertEquals(h2.pos, p1.end)
|
||||
self.assertEquals(h2.pos, p2.start)
|
||||
self.assertEquals(h3.pos, p2.end)
|
||||
self.assertEquals(h3.pos, p3.start)
|
||||
self.assertEquals(h4.pos, p3.end)
|
||||
self.assertEquals(h4.pos, p4.start)
|
||||
self.assertEquals(h5.pos, p4.end)
|
||||
|
||||
|
||||
def test_ports_after_split(self):
|
||||
"""Test ports removal after split
|
||||
"""
|
||||
self.line.handles()[1].pos = (20, 16)
|
||||
|
||||
tool = LineSegmentTool()
|
||||
|
||||
tool.split_segment(self.line, 0)
|
||||
handles = self.line.handles()
|
||||
old_ports = self.line.ports()[:]
|
||||
|
||||
# start with 3 handles and two ports
|
||||
assert len(handles) == 3
|
||||
assert len(old_ports) == 2
|
||||
|
||||
# do split of first segment again
|
||||
# first port should be deleted, but 2nd one should remain untouched
|
||||
tool.split_segment(self.line, 0)
|
||||
self.assertFalse(old_ports[0] in self.line.ports())
|
||||
self.assertEquals(old_ports[1], self.line.ports()[2])
|
||||
|
||||
|
||||
def test_constraints_after_split(self):
|
||||
"""Test if constraints are recreated after line split
|
||||
"""
|
||||
tool = LineSegmentTool()
|
||||
|
||||
# connect line2 to self.line
|
||||
line2 = Line()
|
||||
self.canvas.add(line2)
|
||||
head = line2.handles()[0]
|
||||
self.tool.connect(self.view, line2, head, (25, 25))
|
||||
self.assertEquals(self.line, head.connected_to)
|
||||
|
||||
tool.split_segment(self.line, 0)
|
||||
assert len(self.line.handles()) == 3
|
||||
h1, h2, h3 = self.line.handles()
|
||||
|
||||
# connection shall be reconstrained between 1st and 2nd handle
|
||||
c1 = head.connection_data
|
||||
self.assertEquals(h1.pos, c1._line[0]._point)
|
||||
self.assertEquals(h2.pos, c1._line[1]._point)
|
||||
|
||||
|
||||
def test_split_undo(self):
|
||||
"""Test line splitting undo
|
||||
"""
|
||||
self.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(self.line.handles()) == 2
|
||||
assert len(self.line.ports()) == 1
|
||||
|
||||
tool = LineSegmentTool()
|
||||
tool.split_segment(self.line, 0)
|
||||
assert len(self.line.handles()) == 3
|
||||
assert len(self.line.ports()) == 2
|
||||
|
||||
# after undo, 2 handles and 1 port are expected again
|
||||
undo()
|
||||
self.assertEquals(2, len(self.line.handles()))
|
||||
self.assertEquals(1, len(self.line.ports()))
|
||||
|
||||
|
||||
def test_orthogonal_line_split(self):
|
||||
"""Test orthogonal line splitting
|
||||
"""
|
||||
# start with no orthogonal constraints
|
||||
assert len(self.line._orthogonal_constraints) == 0
|
||||
|
||||
self.line.orthogonal = True
|
||||
|
||||
# check orthogonal constraints
|
||||
self.assertEquals(1, len(self.line._orthogonal_constraints))
|
||||
self.assertEquals(2, len(self.line.handles()))
|
||||
|
||||
LineSegmentTool().split_segment(self.line, 0)
|
||||
|
||||
# 3 handles and 2 ports are expected
|
||||
# 2 constraints keep the self.line orthogonal
|
||||
self.assertEquals(2, len(self.line._orthogonal_constraints))
|
||||
self.assertEquals(3, len(self.line.handles()))
|
||||
self.assertEquals(2, len(self.line.ports()))
|
||||
|
||||
|
||||
def test_params_errors(self):
|
||||
"""Test parameter error exceptions
|
||||
"""
|
||||
tool = LineSegmentTool()
|
||||
|
||||
# there is only 1 segment
|
||||
line = Line()
|
||||
self.assertRaises(ValueError, tool.split_segment, line, -1)
|
||||
|
||||
line = Line()
|
||||
self.assertRaises(ValueError, tool.split_segment, line, 1)
|
||||
|
||||
line = Line()
|
||||
# can't split into one or less segment :)
|
||||
self.assertRaises(ValueError, tool.split_segment, line, 0, 1)
|
||||
|
||||
|
||||
|
||||
class LineMergeTestCase(TestCaseBase):
|
||||
"""
|
||||
Tests for line merging.
|
||||
"""
|
||||
|
||||
def test_merge_first_single(self):
|
||||
"""Test single line merging starting from 1st segment
|
||||
"""
|
||||
tool = LineSegmentTool()
|
||||
self.line.handles()[1].pos = (20, 0)
|
||||
tool.split_segment(self.line, 0)
|
||||
|
||||
# we start with 3 handles and 2 ports, after merging 2 handles and
|
||||
# 1 port are expected
|
||||
assert len(self.line.handles()) == 3
|
||||
assert len(self.line.ports()) == 2
|
||||
old_ports = self.line.ports()[:]
|
||||
|
||||
handles, ports = tool.merge_segment(self.line, 0)
|
||||
# deleted handles and ports
|
||||
self.assertEquals(1, len(handles))
|
||||
self.assertEquals(2, len(ports))
|
||||
# handles and ports left after segment merging
|
||||
self.assertEquals(2, len(self.line.handles()))
|
||||
self.assertEquals(1, len(self.line.ports()))
|
||||
|
||||
self.assertTrue(handles[0] not in self.line.handles())
|
||||
self.assertTrue(ports[0] not in self.line.ports())
|
||||
self.assertTrue(ports[1] not in self.line.ports())
|
||||
|
||||
# old ports are completely removed as they are replaced by new one
|
||||
# port
|
||||
self.assertEquals(old_ports, ports)
|
||||
|
||||
# finally, created port shall span between first and last handle
|
||||
port = self.line.ports()[0]
|
||||
self.assertEquals((0, 0), port.start)
|
||||
self.assertEquals((20, 0), port.end)
|
||||
|
||||
|
||||
def test_constraints_after_merge(self):
|
||||
"""Test if constraints are recreated after line merge
|
||||
"""
|
||||
tool = LineSegmentTool()
|
||||
def dummy_grab(): pass
|
||||
|
||||
context = Context(view=self.view,
|
||||
grab=dummy_grab,
|
||||
ungrab=dummy_grab)
|
||||
|
||||
# connect line2 to self.line
|
||||
line2 = Line()
|
||||
self.canvas.add(line2)
|
||||
head = line2.handles()[0]
|
||||
self.tool.connect(self.view, line2, head, (25, 25))
|
||||
self.assertEquals(self.line, head.connected_to)
|
||||
|
||||
tool.split_segment(self.line, 0)
|
||||
assert len(self.line.handles()) == 3
|
||||
c1 = head.connection_data
|
||||
|
||||
tool.merge_segment(self.line, 0)
|
||||
assert len(self.line.handles()) == 2
|
||||
|
||||
h1, h2 = self.line.handles()
|
||||
# connection shall be reconstrained between 1st and 2nd handle
|
||||
c2 = head.connection_data
|
||||
self.assertEquals(c2._line[0]._point, h1.pos)
|
||||
self.assertEquals(c2._line[1]._point, h2.pos)
|
||||
self.assertFalse(c1 == c2)
|
||||
|
||||
|
||||
def test_merge_multiple(self):
|
||||
"""Test multiple line merge
|
||||
"""
|
||||
tool = LineSegmentTool()
|
||||
self.line.handles()[1].pos = (20, 16)
|
||||
tool.split_segment(self.line, 0, count=3)
|
||||
|
||||
# start with 4 handles and 3 ports, merge 3 segments
|
||||
assert len(self.line.handles()) == 4
|
||||
assert len(self.line.ports()) == 3
|
||||
|
||||
print self.line.handles()
|
||||
handles, ports = tool.merge_segment(self.line, 0, count=3)
|
||||
self.assertEquals(2, len(handles))
|
||||
self.assertEquals(3, len(ports))
|
||||
self.assertEquals(2, len(self.line.handles()))
|
||||
self.assertEquals(1, len(self.line.ports()))
|
||||
|
||||
self.assertTrue(not set(handles).intersection(set(self.line.handles())))
|
||||
self.assertTrue(not set(ports).intersection(set(self.line.ports())))
|
||||
|
||||
# finally, created port shall span between first and last handle
|
||||
port = self.line.ports()[0]
|
||||
self.assertEquals((0, 0), port.start)
|
||||
self.assertEquals((20, 16), port.end)
|
||||
|
||||
|
||||
def test_merge_undo(self):
|
||||
"""Test line merging undo
|
||||
"""
|
||||
tool = LineSegmentTool()
|
||||
|
||||
self.line.handles()[1].pos = (20, 0)
|
||||
|
||||
# split for merging
|
||||
tool.split_segment(self.line, 0)
|
||||
assert len(self.line.handles()) == 3
|
||||
assert len(self.line.ports()) == 2
|
||||
|
||||
# clear undo stack before merging
|
||||
del undo_list[:]
|
||||
|
||||
# merge with empty undo stack
|
||||
tool.merge_segment(self.line, 0)
|
||||
assert len(self.line.handles()) == 2
|
||||
assert len(self.line.ports()) == 1
|
||||
|
||||
# after merge undo, 3 handles and 2 ports are expected again
|
||||
undo()
|
||||
self.assertEquals(3, len(self.line.handles()))
|
||||
self.assertEquals(2, len(self.line.ports()))
|
||||
|
||||
|
||||
def test_orthogonal_line_merge(self):
|
||||
"""Test orthogonal line merging
|
||||
"""
|
||||
self.assertEquals(12, len(self.canvas.solver._constraints))
|
||||
|
||||
tool = LineSegmentTool()
|
||||
self.line.handles()[-1].pos = 100, 100
|
||||
|
||||
# prepare the self.line for merging
|
||||
tool.split_segment(self.line, 0)
|
||||
tool.split_segment(self.line, 0)
|
||||
self.line.orthogonal = True
|
||||
|
||||
self.assertEquals(12 + 3, len(self.canvas.solver._constraints))
|
||||
self.assertEquals(4, len(self.line.handles()))
|
||||
self.assertEquals(3, len(self.line.ports()))
|
||||
|
||||
# test the merging
|
||||
tool.merge_segment(self.line, 0)
|
||||
|
||||
self.assertEquals(12 + 2, len(self.canvas.solver._constraints))
|
||||
self.assertEquals(3, len(self.line.handles()))
|
||||
self.assertEquals(2, len(self.line.ports()))
|
||||
|
||||
|
||||
def test_params_errors(self):
|
||||
"""Test parameter error exceptions
|
||||
"""
|
||||
tool = LineSegmentTool()
|
||||
|
||||
line = Line()
|
||||
tool.split_segment(line, 0)
|
||||
# no segment -1
|
||||
self.assertRaises(ValueError, tool.merge_segment, line, -1)
|
||||
|
||||
line = Line()
|
||||
tool.split_segment(line, 0)
|
||||
# no segment no 2
|
||||
self.assertRaises(ValueError, tool.merge_segment, line, 2)
|
||||
|
||||
line = Line()
|
||||
tool.split_segment(line, 0)
|
||||
# can't merge one or less segments :)
|
||||
self.assertRaises(ValueError, tool.merge_segment, line, 0, 1)
|
||||
|
||||
line = Line()
|
||||
# can't merge line with one segment
|
||||
self.assertRaises(ValueError, tool.merge_segment, line, 0)
|
||||
|
||||
line = Line()
|
||||
tool.split_segment(line, 0)
|
||||
# 2 segments: no 0 and 1. cannot merge as there are no segments
|
||||
# after segment no 1
|
||||
self.assertRaises(ValueError, tool.merge_segment, line, 1)
|
||||
|
||||
line = Line()
|
||||
tool.split_segment(line, 0)
|
||||
# 2 segments: no 0 and 1. cannot merge 3 segments as there are no 3
|
||||
# segments
|
||||
self.assertRaises(ValueError, tool.merge_segment, line, 0, 3)
|
||||
|
||||
|
||||
# vim: sw=4:et:ai
|
@ -13,30 +13,6 @@ from gaphas.tool import ToolContext, HoverTool
|
||||
|
||||
class ViewTestCase(unittest.TestCase):
|
||||
|
||||
def test_selecting_items(self):
|
||||
"""
|
||||
Test selection of items
|
||||
"""
|
||||
canvas = Canvas()
|
||||
view = GtkView(canvas)
|
||||
window = gtk.Window(gtk.WINDOW_TOPLEVEL)
|
||||
window.add(view)
|
||||
window.show_all()
|
||||
|
||||
box = Box()
|
||||
canvas.add(box)
|
||||
view.focused_item = box
|
||||
assert view.focused_item is box
|
||||
assert len(view.selected_items) == 1
|
||||
|
||||
view.select_item(box)
|
||||
assert len(view.selected_items) == 1
|
||||
|
||||
view.unselect_all()
|
||||
assert len(view.selected_items) == 0
|
||||
assert view.focused_item is None
|
||||
|
||||
|
||||
def test_bounding_box_calculations(self):
|
||||
"""
|
||||
A view created before and after the canvas is populated should contain
|
||||
@ -57,7 +33,7 @@ class ViewTestCase(unittest.TestCase):
|
||||
line = Line()
|
||||
line.fyzzyness = 1
|
||||
line.handles()[1].pos = (30, 30)
|
||||
line.split_segment(0, 3)
|
||||
#line.split_segment(0, 3)
|
||||
line.matrix.translate(30, 60)
|
||||
canvas.add(line)
|
||||
|
||||
@ -103,8 +79,8 @@ class ViewTestCase(unittest.TestCase):
|
||||
assert len(view._qtree._ids) == 1
|
||||
assert not view._qtree._bucket.bounds == (0, 0, 0, 0), view._qtree._bucket.bounds
|
||||
|
||||
assert view.get_item_at_point(10, 10) is box
|
||||
assert view.get_item_at_point(60, 10) is None
|
||||
assert view.get_item_at_point((10, 10)) is box
|
||||
assert view.get_item_at_point((60, 10)) is None
|
||||
|
||||
window.destroy()
|
||||
|
||||
@ -210,35 +186,7 @@ class ViewTestCase(unittest.TestCase):
|
||||
assert not box._matrix_v2i.has_key(view)
|
||||
|
||||
|
||||
def test_setting_canvas(self):
|
||||
"""
|
||||
Test if everything is reset properly after a new canvas is set on
|
||||
view.
|
||||
"""
|
||||
canvas = Canvas()
|
||||
view = View(canvas)
|
||||
|
||||
box = Box()
|
||||
canvas.add(box)
|
||||
|
||||
view.focused_item = box
|
||||
view.hovered_item = box
|
||||
view.dropzone_item = box
|
||||
|
||||
assert len(view.selected_items) > 0
|
||||
|
||||
# check registered_views
|
||||
# check _qtree data
|
||||
|
||||
view.canvas = Canvas()
|
||||
|
||||
assert view.selected_items == []
|
||||
assert view.focused_item is None
|
||||
assert view.hovered_item is None
|
||||
assert view.dropzone_item is None
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
|
621
gaphas/tool.py
621
gaphas/tool.py
@ -13,6 +13,7 @@ Current tools:
|
||||
ItemTool - handle selection and movement of items
|
||||
HandleTool - handle selection and movement of handles
|
||||
RubberbandTool - for Rubber band selection
|
||||
PanTool - for easily moving the canvas around
|
||||
PlacementTool - for placing items on the canvas
|
||||
|
||||
Maybe even:
|
||||
@ -24,11 +25,14 @@ Maybe even:
|
||||
__version__ = "$Revision$"
|
||||
# $HeadURL$
|
||||
|
||||
import sys
|
||||
|
||||
import cairo
|
||||
import gtk
|
||||
from canvas import Context
|
||||
from item import Element
|
||||
from geometry import Rectangle
|
||||
from gaphas.canvas import Context
|
||||
from gaphas.geometry import Rectangle
|
||||
from gaphas.geometry import distance_point_point_fast, distance_line_point
|
||||
from gaphas.item import Line
|
||||
|
||||
DEBUG_TOOL = False
|
||||
DEBUG_TOOL_CHAIN = False
|
||||
@ -131,6 +135,13 @@ class Tool(object):
|
||||
if DEBUG_TOOL: print 'on_key_release', context, event
|
||||
pass
|
||||
|
||||
def on_scroll(self, context, event):
|
||||
"""
|
||||
Scroll wheel was turned.
|
||||
"""
|
||||
if DEBUG_TOOL: print 'on_scroll', context, event, event.direction
|
||||
pass
|
||||
|
||||
def draw(self, context):
|
||||
"""
|
||||
Some tools (such as Rubberband selection) may need to draw something
|
||||
@ -275,6 +286,9 @@ class ToolChain(Tool):
|
||||
def on_key_release(self, context, event):
|
||||
self._handle('on_key_release', context, event)
|
||||
|
||||
def on_scroll(self, context, event):
|
||||
self._handle('on_scroll', context, event)
|
||||
|
||||
def draw(self, context):
|
||||
if self._grabbed_tool:
|
||||
self._grabbed_tool.draw(context)
|
||||
@ -291,7 +305,7 @@ class HoverTool(Tool):
|
||||
def on_motion_notify(self, context, event):
|
||||
view = context.view
|
||||
old_hovered = view.hovered_item
|
||||
view.hovered_item = view.get_item_at_point(event.x, event.y)
|
||||
view.hovered_item = view.get_item_at_point((event.x, event.y))
|
||||
return None
|
||||
|
||||
|
||||
@ -377,8 +391,7 @@ class ItemTool(Tool):
|
||||
class HandleTool(Tool):
|
||||
"""
|
||||
Tool for moving handles around. By default this tool does not provide
|
||||
connecting handles to another item (see examples.ConnectingHandleTool for
|
||||
an example).
|
||||
connecting handles to another item (see `ConnectHandleTool`).
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
@ -445,30 +458,29 @@ class HandleTool(Tool):
|
||||
return None, None
|
||||
|
||||
|
||||
def move(self, view, item, handle, x, y):
|
||||
def move(self, view, item, handle, pos):
|
||||
"""
|
||||
Move the handle to position ``(x,y)``. ``x`` and ``y`` are in
|
||||
item coordnates. ``item`` is the item whose ``handle`` is moved.
|
||||
"""
|
||||
handle.x = x
|
||||
handle.y = y
|
||||
handle.x, handle.y = pos
|
||||
|
||||
|
||||
def glue(self, view, item, handle, vx, vy):
|
||||
def glue(self, view, item, handle, vpos):
|
||||
"""
|
||||
Find an item that ``handle`` can connect to. ``item`` is the ``Item``
|
||||
owing the handle.
|
||||
``vx`` and ``vy`` are the pointer (view) coordinates.
|
||||
``vpos`` is the point in the pointer (view) coordinates.
|
||||
|
||||
The ``glue()`` code should take care of moving ``handle`` to the
|
||||
correct position, creating a glue effect.
|
||||
"""
|
||||
|
||||
def connect(self, view, item, handle, vx, vy):
|
||||
def connect(self, view, item, handle, vpos):
|
||||
"""
|
||||
Find an item that ``handle`` can connect to and create a connection.
|
||||
``item`` is the ``Item`` owning the handle.
|
||||
``vx`` and ``vy`` are the pointer (view) coordinates.
|
||||
``vpos`` is the point in the pointer (view) coordinates.
|
||||
|
||||
A typical connect action may involve the following:
|
||||
|
||||
@ -529,7 +541,7 @@ class HandleTool(Tool):
|
||||
try:
|
||||
view = context.view
|
||||
if grabbed_handle and grabbed_handle.connectable:
|
||||
self.connect(view, grabbed_item, grabbed_handle, event.x, event.y)
|
||||
self.connect(view, grabbed_item, grabbed_handle, (event.x, event.y))
|
||||
finally:
|
||||
context.ungrab()
|
||||
self.ungrab_handle()
|
||||
@ -554,14 +566,14 @@ class HandleTool(Tool):
|
||||
x, y = v2i.transform_point(event.x, event.y)
|
||||
|
||||
# Do the actual move:
|
||||
self.move(view, item, handle, x, y)
|
||||
self.move(view, item, handle, (x, y))
|
||||
|
||||
# do not request matrix update as matrix recalculation will be
|
||||
# performed due to item normalization if required
|
||||
item.request_update(matrix=False)
|
||||
try:
|
||||
if self._grabbed_handle.connectable:
|
||||
self.glue(view, item, handle, event.x, event.y)
|
||||
self.glue(view, item, handle, (event.x, event.y))
|
||||
# TODO: elif isinstance(item, Element):
|
||||
# schedule (item, handle) to be handled by some "guides" tool
|
||||
# that tries to align the handle with some other Element's
|
||||
@ -617,6 +629,67 @@ class RubberbandTool(Tool):
|
||||
cr.fill()
|
||||
|
||||
|
||||
class PanTool(Tool):
|
||||
"""
|
||||
Captures drag events with the middle mouse button and uses them to
|
||||
translate the canvas within the view. Trumps the ZoomTool, so should be
|
||||
placed later in the ToolChain.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.x0, self.y0 = 0, 0
|
||||
self.speed = 10
|
||||
|
||||
def on_button_press(self,context,event):
|
||||
if event.button == 2:
|
||||
context.grab()
|
||||
self.x0, self.y0 = event.x, event.y
|
||||
return True
|
||||
|
||||
def on_button_release(self, context, event):
|
||||
context.ungrab()
|
||||
self.x0, self.y0 = event.x, event.y
|
||||
return True
|
||||
|
||||
def on_motion_notify(self, context, event):
|
||||
if event.state & gtk.gdk.BUTTON2_MASK:
|
||||
view = context.view
|
||||
dx = self.x0 - event.x
|
||||
dy = self.y0 - event.y
|
||||
if dx:
|
||||
adj = context.view.hadjustment
|
||||
adj.value = adj.value + dx
|
||||
adj.value_changed()
|
||||
self.x0 = event.x
|
||||
|
||||
if dy:
|
||||
adj = context.view.vadjustment
|
||||
adj.value = adj.value + dy
|
||||
adj.value_changed()
|
||||
self.y0 = event.y
|
||||
return True
|
||||
|
||||
def on_scroll(self, context, event):
|
||||
direction = event.direction
|
||||
gdk = gtk.gdk
|
||||
adj = None
|
||||
if direction == gdk.SCROLL_LEFT:
|
||||
adj = context.view.hadjustment
|
||||
adj.value = adj.value - self.speed
|
||||
elif direction == gdk.SCROLL_RIGHT:
|
||||
adj = context.view.hadjustment
|
||||
adj.value = adj.value + self.speed
|
||||
elif direction == gdk.SCROLL_UP:
|
||||
adj = context.view.vadjustment
|
||||
adj.value = adj.value - self.speed
|
||||
elif direction == gdk.SCROLL_DOWN:
|
||||
adj = context.view.vadjustment
|
||||
adj.value = adj.value + self.speed
|
||||
if adj:
|
||||
adj.value_changed()
|
||||
return True
|
||||
|
||||
|
||||
class PlacementTool(Tool):
|
||||
|
||||
def __init__(self, factory, handle_tool, handle_index):
|
||||
@ -635,7 +708,7 @@ class PlacementTool(Tool):
|
||||
def on_button_press(self, context, event):
|
||||
view = context.view
|
||||
canvas = view.canvas
|
||||
new_item = self._create_item(context, event.x, event.y)
|
||||
new_item = self._create_item(context, (event.x, event.y))
|
||||
# Enforce matrix update, as a good matrix is required for the handle
|
||||
# positioning:
|
||||
canvas.get_matrix_i2c(new_item, calculate=True)
|
||||
@ -651,11 +724,11 @@ class PlacementTool(Tool):
|
||||
return True
|
||||
|
||||
|
||||
def _create_item(self, context, x, y):
|
||||
def _create_item(self, context, pos):
|
||||
view = context.view
|
||||
canvas = view.canvas
|
||||
item = self._factory()
|
||||
x, y = view.get_matrix_v2i(item).transform_point(x, y)
|
||||
x, y = view.get_matrix_v2i(item).transform_point(*pos)
|
||||
item.matrix.translate(x, y)
|
||||
return item
|
||||
|
||||
@ -725,15 +798,515 @@ class TextEditTool(Tool):
|
||||
widget.destroy()
|
||||
|
||||
|
||||
|
||||
class ConnectHandleTool(HandleTool):
|
||||
"""
|
||||
This is a handle tool which allows to connect item's handle to another
|
||||
item's port.
|
||||
"""
|
||||
GLUE_DISTANCE = 10
|
||||
|
||||
def glue(self, view, item, handle, vpos):
|
||||
"""
|
||||
Glue to an item.
|
||||
|
||||
Look for items in glue rectangle (which is defined by ``vpos`` (vx, vy)
|
||||
and glue distance) and find the closest port.
|
||||
|
||||
Glue position is found for closest port as well. Handle of
|
||||
connecting item is moved to glue point.
|
||||
|
||||
Return found item and its connection port or `(None, None)` if
|
||||
not found.
|
||||
|
||||
:Parameters:
|
||||
view
|
||||
View used by user.
|
||||
item
|
||||
Connecting item.
|
||||
handle
|
||||
Handle of connecting item.
|
||||
"""
|
||||
if not handle.connectable:
|
||||
return None
|
||||
|
||||
dist = self.GLUE_DISTANCE
|
||||
max_dist = dist
|
||||
port = None
|
||||
glue_pos = None
|
||||
glue_item = None
|
||||
v2i = view.get_matrix_v2i
|
||||
vx, vy = vpos
|
||||
|
||||
rect = (vx - dist, vy - dist, dist * 2, dist * 2)
|
||||
items = view.get_items_in_rectangle(rect, reverse=True)
|
||||
for i in items:
|
||||
if i is item:
|
||||
continue
|
||||
for p in i.ports():
|
||||
if not p.connectable:
|
||||
continue
|
||||
|
||||
ix, iy = v2i(i).transform_point(vx, vy)
|
||||
pg, d = p.glue((ix, iy))
|
||||
|
||||
if d >= max_dist:
|
||||
continue
|
||||
|
||||
glue_item = i
|
||||
port = p
|
||||
|
||||
# transform coordinates from connectable item space to view
|
||||
# space
|
||||
i2v = view.get_matrix_i2v(i).transform_point
|
||||
glue_pos = i2v(*pg)
|
||||
|
||||
# check if item and glue item can be connected on closest port
|
||||
if port is not None \
|
||||
and not self.can_glue(view, item, handle, glue_item, port):
|
||||
glue_item, port = None, None
|
||||
|
||||
if port is not None:
|
||||
# transport coordinates from view space to connecting item
|
||||
# space and update position connecting item's handle
|
||||
v2i = view.get_matrix_v2i(item).transform_point
|
||||
handle.pos = v2i(*glue_pos)
|
||||
|
||||
return glue_item, port
|
||||
|
||||
|
||||
def can_glue(self, view, item, handle, glue_item, port):
|
||||
"""
|
||||
Determine if item's handle can connect to glue item's port.
|
||||
|
||||
`True` is returned by default. Override this method to disallow
|
||||
glueing on higher level (i.e. because classes of item and glue item
|
||||
does not much, etc.).
|
||||
|
||||
:Parameters:
|
||||
view
|
||||
View used by user.
|
||||
item
|
||||
Item connecting to glue item.
|
||||
handle
|
||||
Connecting handle of the item.
|
||||
glue_item
|
||||
Connectable item.
|
||||
port
|
||||
Port of connectable item.
|
||||
"""
|
||||
return True
|
||||
|
||||
|
||||
def pre_connect(self, view, item, handle, glue_item, port):
|
||||
"""
|
||||
The method is invoked just before connection is performed by
|
||||
`ConnectHandleTool.connect` method. It can be overriden by deriving
|
||||
tools to perform higher level connection.
|
||||
|
||||
`True` is returned to indicate that higher level connection is
|
||||
performed.
|
||||
"""
|
||||
return True
|
||||
|
||||
|
||||
def connect(self, view, item, handle, vpos):
|
||||
"""
|
||||
Connect a handle of connecting item to connectable item.
|
||||
Connectable item is found by `glue` method.
|
||||
|
||||
Return `True` if connection is performed.
|
||||
|
||||
:Parameters:
|
||||
view
|
||||
View used by user.
|
||||
item
|
||||
Connectable item.
|
||||
handle
|
||||
Handle of connecting item.
|
||||
"""
|
||||
glue_item, port = self.glue(view, item, handle, vpos)
|
||||
|
||||
# disconnect when
|
||||
# - no glued item
|
||||
# - currently connected item is not glue item
|
||||
if not glue_item \
|
||||
or glue_item and handle.connected_to is not glue_item:
|
||||
handle.disconnect()
|
||||
|
||||
# no glue item, no connection
|
||||
if not glue_item:
|
||||
return
|
||||
|
||||
# connection on higher level
|
||||
self.pre_connect(view, item, handle, glue_item, port)
|
||||
# low-level connection
|
||||
self.post_connect(view.canvas, item, handle, glue_item, port)
|
||||
|
||||
|
||||
def post_connect(self, canvas, item, handle, glue_item, port):
|
||||
"""
|
||||
Create constraint between item's handle and port of glue item.
|
||||
"""
|
||||
ConnectHandleTool.create_constraint(item, handle, glue_item, port)
|
||||
|
||||
handle.connected_to = glue_item
|
||||
handle.disconnect = DisconnectHandle(canvas, item, handle)
|
||||
|
||||
|
||||
def disconnect(self, view, item, handle):
|
||||
if handle.disconnect:
|
||||
handle.disconnect()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def find_port(line, handle, item):
|
||||
"""
|
||||
Find port of an item at position of line's handle.
|
||||
|
||||
:Parameters:
|
||||
line
|
||||
Line supposed to connect to an item.
|
||||
handle
|
||||
Handle of a line connecting to an item.
|
||||
item
|
||||
Item to be connected to a line.
|
||||
"""
|
||||
port = None
|
||||
max_dist = sys.maxint
|
||||
canvas = item.canvas
|
||||
|
||||
# line's handle position to canvas coordinates
|
||||
i2c = canvas.get_matrix_i2c(line)
|
||||
hx, hy = i2c.transform_point(*handle.pos)
|
||||
|
||||
# from canvas to item coordinates
|
||||
c2i = canvas.get_matrix_c2i(item)
|
||||
ix, iy = c2i.transform_point(hx, hy)
|
||||
|
||||
# find the port using item's coordinates
|
||||
for p in item.ports():
|
||||
pg, d = p.glue((ix, iy))
|
||||
if d >= max_dist:
|
||||
continue
|
||||
port = p
|
||||
max_dist = d
|
||||
|
||||
return port
|
||||
|
||||
|
||||
@staticmethod
|
||||
def create_constraint(line, handle, item, port):
|
||||
"""
|
||||
Create connection constraint between line's handle and item's port.
|
||||
|
||||
If constraint already exists, then it is removed and new constraint
|
||||
is created instead.
|
||||
|
||||
:Parameters:
|
||||
line
|
||||
Line connecting to an item.
|
||||
handle
|
||||
Handle of a line connecting to an item.
|
||||
item
|
||||
Item to be connected to a line.
|
||||
port
|
||||
Item's port used for connection with a line.
|
||||
"""
|
||||
canvas = line.canvas
|
||||
solver = canvas.solver
|
||||
|
||||
if handle.connection_data:
|
||||
solver.remove_constraint(handle.connection_data)
|
||||
|
||||
constraint = port.constraint(canvas, line, handle, item)
|
||||
handle.connection_data = constraint
|
||||
solver.add_constraint(constraint)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def remove_constraint(line, handle):
|
||||
"""
|
||||
Remove connection constraint created between line's handle and
|
||||
connected item's port.
|
||||
|
||||
:Parameters:
|
||||
line
|
||||
Line connecting to an item.
|
||||
handle
|
||||
Handle of a line connecting to an item.
|
||||
"""
|
||||
if handle.connection_data:
|
||||
line.canvas.solver.remove_constraint(handle.connection_data)
|
||||
handle.connection_data = None
|
||||
|
||||
|
||||
|
||||
class DisconnectHandle(object):
|
||||
|
||||
def __init__(self, canvas, item, handle):
|
||||
self.canvas = canvas
|
||||
self.item = item
|
||||
self.handle = handle
|
||||
|
||||
def __call__(self):
|
||||
self.handle_disconnect()
|
||||
|
||||
def handle_disconnect(self):
|
||||
canvas = self.canvas
|
||||
item = self.item
|
||||
handle = self.handle
|
||||
try:
|
||||
canvas.solver.remove_constraint(handle.connection_data)
|
||||
except KeyError:
|
||||
print 'constraint was already removed for', item, handle
|
||||
pass # constraint was alreasy removed
|
||||
else:
|
||||
print 'constraint removed for', item, handle
|
||||
handle.connection_data = None
|
||||
handle.connected_to = None
|
||||
# Remove disconnect handler:
|
||||
handle.disconnect = None
|
||||
|
||||
|
||||
class LineSegmentTool(ConnectHandleTool):
|
||||
"""
|
||||
Line segment tool provides functionality for splitting and merging line
|
||||
segments.
|
||||
|
||||
Line segment is defined by two adjacent handles.
|
||||
|
||||
Line splitting is performed by clicking a line in the middle of
|
||||
a segment.
|
||||
|
||||
Line merging is performed by moving a handle onto adjacent handle.
|
||||
|
||||
Please note, that this tool provides functionality without involving
|
||||
context menu. Any further research into line spliting/merging
|
||||
functionality should take into account this assumption and new
|
||||
improvements (or even this tool replacement) shall be behavior based.
|
||||
|
||||
It is possible to use this tool from a menu by using
|
||||
`LineSegmentTool.split_segment` and `LineSegmentTool.merge_segment`
|
||||
methods.
|
||||
"""
|
||||
def split_segment(self, line, segment, count=2):
|
||||
"""
|
||||
Split one line segment into ``count`` equal pieces.
|
||||
|
||||
Two lists are returned
|
||||
|
||||
- list of created handles
|
||||
- list of created ports
|
||||
|
||||
:Parameters:
|
||||
line
|
||||
Line item, which segment shall be split.
|
||||
segment
|
||||
Segment number to split (starting from zero).
|
||||
count
|
||||
Amount of new segments to be created (minimum 2).
|
||||
"""
|
||||
if segment < 0 or segment >= len(line.ports()):
|
||||
raise ValueError('Incorrect segment')
|
||||
if count < 2:
|
||||
raise ValueError('Incorrect count of segments')
|
||||
|
||||
def do_split(segment, count):
|
||||
handles = line.handles()
|
||||
h0 = handles[segment]
|
||||
h1 = handles[segment + 1]
|
||||
dx, dy = h1.x - h0.x, h1.y - h0.y
|
||||
new_h = line._create_handle((h0.x + dx / count, h0.y + dy / count))
|
||||
line._reversible_insert_handle(segment + 1, new_h)
|
||||
|
||||
p0 = line._create_port(h0, new_h)
|
||||
p1 = line._create_port(new_h, h1)
|
||||
line._reversible_remove_port(line.ports()[segment])
|
||||
line._reversible_insert_port(segment, p0)
|
||||
line._reversible_insert_port(segment + 1, p1)
|
||||
|
||||
if count > 2:
|
||||
do_split(segment + 1, count - 1)
|
||||
|
||||
# get rid of connection constraints (to be recreated later)
|
||||
citems, chandles = self._remove_constraints(line)
|
||||
|
||||
do_split(segment, count)
|
||||
|
||||
# force orthogonal constraints to be recreated
|
||||
line._update_orthogonal_constraints(line.orthogonal)
|
||||
|
||||
# recreate connection constraints
|
||||
self._recreate_constraints(citems, chandles, line)
|
||||
|
||||
handles = line.handles()[segment + 1:segment + count]
|
||||
ports = line.ports()[segment:segment + count - 1]
|
||||
return handles, ports
|
||||
|
||||
|
||||
def merge_segment(self, line, segment, count=2):
|
||||
"""
|
||||
Merge two (or more) line segments.
|
||||
|
||||
Tuple of two lists is returned, list of deleted handles and list of
|
||||
deleted ports.
|
||||
|
||||
:Parameters:
|
||||
line
|
||||
Line item, which segments shall be merged.
|
||||
segment
|
||||
Segment number to start merging from (starting from zero).
|
||||
count
|
||||
Amount of segments to be merged (minimum 2).
|
||||
"""
|
||||
if len(line.ports()) < 2:
|
||||
raise ValueError('Cannot merge line with one segment')
|
||||
if segment < 0 or segment >= len(line.ports()):
|
||||
raise ValueError('Incorrect segment')
|
||||
if count < 2 or segment + count > len(line.ports()):
|
||||
raise ValueError('Incorrect count of segments')
|
||||
|
||||
# get rid of connection constraints (to be recreated later)
|
||||
citems, chandles = self._remove_constraints(line)
|
||||
|
||||
# remove handle and ports which share position with handle
|
||||
deleted_handles = line.handles()[segment + 1:segment + count]
|
||||
deleted_ports = line.ports()[segment:segment + count]
|
||||
for h in deleted_handles:
|
||||
line._reversible_remove_handle(h)
|
||||
for p in deleted_ports:
|
||||
line._reversible_remove_port(p)
|
||||
|
||||
# create new port, which replaces old ports destroyed due to
|
||||
# deleted handle
|
||||
h1 = line.handles()[segment]
|
||||
h2 = line.handles()[segment + 1]
|
||||
port = line._create_port(h1, h2)
|
||||
line._reversible_insert_port(segment, port)
|
||||
|
||||
# force orthogonal constraints to be recreated
|
||||
line._update_orthogonal_constraints(line.orthogonal)
|
||||
|
||||
# recreate connection constraints
|
||||
self._recreate_constraints(citems, chandles, line)
|
||||
|
||||
return deleted_handles, deleted_ports
|
||||
|
||||
|
||||
def _recreate_constraints(self, lines, handles, item):
|
||||
"""
|
||||
Create connection constraints between connecting lines and an item.
|
||||
|
||||
:Parameters:
|
||||
lines
|
||||
Lines connecting to an item.
|
||||
handles
|
||||
Handles connecting to an item.
|
||||
item
|
||||
Item connected to lines.
|
||||
"""
|
||||
for line, h in zip(lines, handles):
|
||||
port = ConnectHandleTool.find_port(line, h, item)
|
||||
ConnectHandleTool.create_constraint(line, h, item, port)
|
||||
|
||||
|
||||
def _remove_constraints(self, item):
|
||||
"""
|
||||
Remove connection constraints established between an item and all
|
||||
connecting items.
|
||||
|
||||
List of connecting items and list of connecting handles are
|
||||
returned.
|
||||
|
||||
:Parameters:
|
||||
item
|
||||
Item, to which connections shall be removed.
|
||||
"""
|
||||
lines = []
|
||||
handles = []
|
||||
if item.canvas: # no canvas, no connections
|
||||
data = item.canvas.get_connected_items(item)
|
||||
for line, h in data:
|
||||
ConnectHandleTool.remove_constraint(line, h)
|
||||
if data:
|
||||
lines, handles = zip(*data)
|
||||
|
||||
return lines, handles
|
||||
|
||||
|
||||
def on_button_press(self, context, event):
|
||||
"""
|
||||
In addition to the normal behaviour, the button press event creates
|
||||
new handles if it is activated on the middle of a line segment.
|
||||
"""
|
||||
if super(LineSegmentTool, self).on_button_press(context, event):
|
||||
return True
|
||||
|
||||
view = context.view
|
||||
item = view.hovered_item
|
||||
if item and item is view.focused_item and isinstance(item, Line):
|
||||
handles = item.handles()
|
||||
x, y = view.get_matrix_v2i(item).transform_point(event.x, event.y)
|
||||
for h1, h2 in zip(handles[:-1], handles[1:]):
|
||||
xp = (h1.x + h2.x) / 2
|
||||
yp = (h1.y + h2.y) / 2
|
||||
if distance_point_point_fast((x,y), (xp, yp)) <= 4:
|
||||
segment = handles.index(h1)
|
||||
self.split_segment(item, segment)
|
||||
|
||||
self.grab_handle(item, item.handles()[segment + 1])
|
||||
context.grab()
|
||||
return True
|
||||
|
||||
|
||||
def on_button_release(self, context, event):
|
||||
"""
|
||||
In addition to the normal behavior, the button release event
|
||||
removes line segment if grabbed handle is close enough to an
|
||||
adjacent handle.
|
||||
"""
|
||||
grabbed_handle = self._grabbed_handle
|
||||
grabbed_item = self._grabbed_item
|
||||
if super(LineSegmentTool, self).on_button_release(context, event):
|
||||
if grabbed_handle and grabbed_item:
|
||||
handles = grabbed_item.handles()
|
||||
|
||||
# don't merge using first or last handle
|
||||
if handles[0] is grabbed_handle or handles[-1] is grabbed_handle:
|
||||
return True
|
||||
|
||||
handle_index = handles.index(grabbed_handle)
|
||||
segment = handle_index - 1
|
||||
|
||||
# cannot merge starting from last segment
|
||||
if segment == len(grabbed_item.ports()) - 1:
|
||||
segment =- 1
|
||||
assert segment >= 0 and segment < len(grabbed_item.ports()) - 1
|
||||
|
||||
before = handles[handle_index - 1]
|
||||
after = handles[handle_index + 1]
|
||||
d, p = distance_line_point(before.pos, after.pos, grabbed_handle.pos)
|
||||
|
||||
if d < 2:
|
||||
assert len(context.view.canvas.solver._marked_cons) == 0
|
||||
self.merge_segment(grabbed_item, segment)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
def DefaultTool():
|
||||
"""
|
||||
The default tool chain build from HoverTool, ItemTool and HandleTool.
|
||||
"""
|
||||
chain = ToolChain().\
|
||||
append(HoverTool()).\
|
||||
append(HandleTool()).\
|
||||
append(ItemTool()).\
|
||||
append(TextEditTool()).\
|
||||
#append(ConnectHandleTool()). \
|
||||
chain = ToolChain(). \
|
||||
append(HoverTool()). \
|
||||
append(LineSegmentTool()). \
|
||||
append(PanTool()). \
|
||||
append(ItemTool()). \
|
||||
append(TextEditTool()). \
|
||||
append(RubberbandTool())
|
||||
return chain
|
||||
|
||||
|
131
gaphas/tree.py
131
gaphas/tree.py
@ -158,6 +158,59 @@ class Tree(object):
|
||||
yield parent
|
||||
parent = self.get_parent(parent)
|
||||
|
||||
def index_nodes(self, index_key):
|
||||
"""
|
||||
Provide each item in the tree with an index attribute. This makes
|
||||
for fast sorting of items.
|
||||
|
||||
>>> class A(object):
|
||||
... def __init__(self, n):
|
||||
... self.n = n
|
||||
... def __repr__(self):
|
||||
... return self.n
|
||||
>>> t = Tree()
|
||||
>>> a = A('a')
|
||||
>>> t.add(a)
|
||||
>>> t.add(A('b'))
|
||||
>>> t.add(A('c'), parent=a)
|
||||
>>> t.nodes
|
||||
[a, c, b]
|
||||
>>> t.index_nodes('my_key')
|
||||
>>> t.nodes[0].my_key, t.nodes[1].my_key, t.nodes[2].my_key
|
||||
(0, 1, 2)
|
||||
|
||||
For sorting, see ``sort()``.
|
||||
"""
|
||||
nodes = self.nodes
|
||||
lnodes = len(nodes)
|
||||
map(setattr, nodes, [index_key] * lnodes, xrange(lnodes))
|
||||
|
||||
def sort(self, nodes, index_key, reverse=False):
|
||||
"""
|
||||
Sort a set (or list) of nodes.
|
||||
|
||||
>>> class A(object):
|
||||
... def __init__(self, n):
|
||||
... self.n = n
|
||||
... def __repr__(self):
|
||||
... return self.n
|
||||
>>> t = Tree()
|
||||
>>> a = A('a')
|
||||
>>> t.add(a)
|
||||
>>> t.add(A('b'))
|
||||
>>> t.add(A('c'), parent=a)
|
||||
>>> t.nodes # the series from Tree.index_nodes
|
||||
[a, c, b]
|
||||
>>> t.index_nodes('my_key')
|
||||
>>> selection = (t.nodes[2], t.nodes[1])
|
||||
>>> t.sort(selection, index_key='my_key')
|
||||
[c, b]
|
||||
"""
|
||||
if index_key:
|
||||
return sorted(nodes, key=attrgetter(index_key), reverse=reverse)
|
||||
else:
|
||||
raise NotImplemented('index_key should be provided.')
|
||||
|
||||
def _add_to_nodes(self, node, parent):
|
||||
"""
|
||||
Called only from add()
|
||||
@ -281,84 +334,6 @@ class Tree(object):
|
||||
disable_dispatching(_remove)
|
||||
|
||||
|
||||
class TreeIndexer(object):
|
||||
"""
|
||||
The ``TreeIndexer`` is a straight-forward indexer that adds an
|
||||
index-attribute to each object in the tree. This attribute can be used
|
||||
to do fast sorting.
|
||||
"""
|
||||
|
||||
def __init__(self, tree, index_key):
|
||||
self._tree = tree
|
||||
self._index_key = index_key
|
||||
|
||||
|
||||
def index_tree(self):
|
||||
"""
|
||||
Provide each item in the tree with an index attribute. This makes
|
||||
for fast sorting of items.
|
||||
|
||||
>>> class A(object):
|
||||
... def __init__(self, n):
|
||||
... self.n = n
|
||||
... def __repr__(self):
|
||||
... return self.n
|
||||
>>> t = Tree()
|
||||
>>> a = A('a')
|
||||
>>> t.add(a)
|
||||
>>> t.add(A('b'))
|
||||
>>> t.add(A('c'), parent=a)
|
||||
>>> t.nodes
|
||||
[a, c, b]
|
||||
>>> idx = TreeIndexer(t, 'my_key')
|
||||
>>> idx.index_tree()
|
||||
>>> t.nodes[0].my_key, t.nodes[1].my_key, t.nodes[2].my_key
|
||||
(0, 1, 2)
|
||||
|
||||
For sorting, see ``sort()``.
|
||||
"""
|
||||
nodes = self._tree.nodes
|
||||
lnodes = len(nodes)
|
||||
map(setattr, nodes, [self._index_key] * lnodes, xrange(lnodes))
|
||||
|
||||
|
||||
def sort(self, nodes, reverse=False):
|
||||
"""
|
||||
Sort a set (or list) of nodes to the order defined by the ``Tree``.
|
||||
|
||||
>>> class A(object):
|
||||
... def __init__(self, n):
|
||||
... self.n = n
|
||||
... def __repr__(self):
|
||||
... return self.n
|
||||
>>> t = Tree()
|
||||
>>> a = A('a')
|
||||
>>> t.add(a)
|
||||
>>> t.add(A('b'))
|
||||
>>> t.add(A('c'), parent=a)
|
||||
>>> t.nodes # the series from Tree.index_nodes
|
||||
[a, c, b]
|
||||
>>> idx = TreeIndexer(t, 'my_key')
|
||||
>>> idx.index_tree()
|
||||
>>> selection = (t.nodes[2], t.nodes[1])
|
||||
>>> idx.sort(selection)
|
||||
[c, b]
|
||||
|
||||
If the tree is not yet indexed, it is indexed on the fly.
|
||||
|
||||
>>> idx = TreeIndexer(t, 'my_key')
|
||||
>>> selection = (t.nodes[2], t.nodes[1])
|
||||
>>> idx.sort(selection)
|
||||
[c, b]
|
||||
"""
|
||||
try:
|
||||
return sorted(nodes, key=attrgetter(self._index_key), reverse=reverse)
|
||||
except AttributeError:
|
||||
# Looks like not all elements are indexed, so let's do it instantly
|
||||
self.index_tree()
|
||||
return sorted(nodes, key=attrgetter(self._index_key), reverse=reverse)
|
||||
|
||||
|
||||
__test__ = {
|
||||
'Tree.add': Tree.add,
|
||||
'Tree.remove': Tree.remove,
|
||||
|
@ -34,7 +34,7 @@ class View(object):
|
||||
self._painter = DefaultPainter()
|
||||
|
||||
# Handling selections.
|
||||
self._selected_items = list()
|
||||
self._selected_items = set()
|
||||
self._focused_item = None
|
||||
self._hovered_item = None
|
||||
self._dropzone_item = None
|
||||
@ -58,17 +58,18 @@ class View(object):
|
||||
if self._canvas:
|
||||
self._qtree = Quadtree()
|
||||
|
||||
# Handling selections.
|
||||
del self._selected_items[:]
|
||||
self._focused_item = None
|
||||
self._hovered_item = None
|
||||
self._dropzone_item = None
|
||||
|
||||
self._canvas = canvas
|
||||
|
||||
canvas = property(lambda s: s._canvas, _set_canvas)
|
||||
|
||||
|
||||
def emit(self, args, **kwargs):
|
||||
"""
|
||||
Placeholder method for signal emission functionality.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def select_item(self, item):
|
||||
"""
|
||||
Select an item. This adds @item to the set of selected items. Do::
|
||||
@ -79,8 +80,7 @@ class View(object):
|
||||
"""
|
||||
self.queue_draw_item(item)
|
||||
if item not in self._selected_items:
|
||||
self._selected_items.append(item)
|
||||
self._selected_items = self._canvas.sort(self._selected_items)
|
||||
self._selected_items.add(item)
|
||||
self.emit('selection-changed', self._selected_items)
|
||||
|
||||
|
||||
@ -90,7 +90,7 @@ class View(object):
|
||||
"""
|
||||
self.queue_draw_item(item)
|
||||
if item in self._selected_items:
|
||||
self._selected_items.remove(item)
|
||||
self._selected_items.discard(item)
|
||||
self.emit('selection-changed', self._selected_items)
|
||||
|
||||
|
||||
@ -104,7 +104,7 @@ class View(object):
|
||||
Clearing the selected_item also clears the focused_item.
|
||||
"""
|
||||
self.queue_draw_item(*self._selected_items)
|
||||
del self._selected_items[:]
|
||||
self._selected_items.clear()
|
||||
self.focused_item = None
|
||||
self.emit('selection-changed', self._selected_items)
|
||||
|
||||
@ -197,22 +197,21 @@ class View(object):
|
||||
painter = property(lambda s: s._painter, _set_painter)
|
||||
|
||||
|
||||
def get_item_at_point(self, x, y, selected=True):
|
||||
def get_item_at_point(self, pos, selected=True):
|
||||
"""
|
||||
Return the topmost item located at (x, y).
|
||||
Return the topmost item located at ``pos`` (x, y).
|
||||
|
||||
Parameters:
|
||||
- selected: if False returns first non-selected item
|
||||
"""
|
||||
point = (x, y)
|
||||
items = self._qtree.find_intersect((x, y, 1, 1))
|
||||
items = self._qtree.find_intersect((pos[0], pos[1], 1, 1))
|
||||
for item in self._canvas.sort(items, reverse=True):
|
||||
if not selected and item in self.selected_items:
|
||||
continue # skip selected items
|
||||
|
||||
v2i = self.get_matrix_v2i(item)
|
||||
ix, iy = v2i.transform_point(x, y)
|
||||
if item.point(ix, iy) < 0.5:
|
||||
ix, iy = v2i.transform_point(*pos)
|
||||
if item.point((ix, iy)) < 0.5:
|
||||
return item
|
||||
return None
|
||||
|
||||
@ -338,21 +337,6 @@ class View(object):
|
||||
pass
|
||||
|
||||
|
||||
def emit(self, *args, **kwargs):
|
||||
"""
|
||||
Placeholder method for signal emission functionality.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def queue_draw_item(self, *items):
|
||||
"""
|
||||
Placeholder method. Items that should be redrawn should be queued
|
||||
by this method.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# Map GDK events to tool methods
|
||||
EVENT_HANDLERS = {
|
||||
gtk.gdk.BUTTON_PRESS: 'on_button_press',
|
||||
@ -361,7 +345,8 @@ EVENT_HANDLERS = {
|
||||
gtk.gdk._3BUTTON_PRESS: 'on_triple_click',
|
||||
gtk.gdk.MOTION_NOTIFY: 'on_motion_notify',
|
||||
gtk.gdk.KEY_PRESS: 'on_key_press',
|
||||
gtk.gdk.KEY_RELEASE: 'on_key_release'
|
||||
gtk.gdk.KEY_RELEASE: 'on_key_release',
|
||||
gtk.gdk.SCROLL: 'on_scroll'
|
||||
}
|
||||
|
||||
|
||||
@ -412,7 +397,8 @@ class GtkView(gtk.DrawingArea, View):
|
||||
| gtk.gdk.BUTTON_RELEASE_MASK
|
||||
| gtk.gdk.POINTER_MOTION_MASK
|
||||
| gtk.gdk.KEY_PRESS_MASK
|
||||
| gtk.gdk.KEY_RELEASE_MASK)
|
||||
| gtk.gdk.KEY_RELEASE_MASK
|
||||
| gtk.gdk.SCROLL_MASK)
|
||||
|
||||
self._hadjustment = hadjustment or gtk.Adjustment()
|
||||
self._vadjustment = vadjustment or gtk.Adjustment()
|
||||
@ -500,10 +486,13 @@ class GtkView(gtk.DrawingArea, View):
|
||||
adjustment.page_increment = viewport_size
|
||||
adjustment.step_increment = viewport_size/10
|
||||
adjustment.upper = adjustment.value + canvas_offset + canvas_size
|
||||
adjustment.lower = adjustment.value + canvas_offset
|
||||
adjustment.lower = 0 #adjustment.value + canvas_offset
|
||||
|
||||
if adjustment.value > adjustment.upper - viewport_size:
|
||||
adjustment.value = adjustment.upper - viewport_size
|
||||
elif adjustment.value < 0:
|
||||
adjustment.value = 0
|
||||
|
||||
|
||||
@async(single=True)
|
||||
def update_adjustments(self, allocation=None):
|
||||
@ -580,7 +569,7 @@ class GtkView(gtk.DrawingArea, View):
|
||||
|
||||
for item in removed_items:
|
||||
self._qtree.remove(item)
|
||||
self.selected_items.remove(item)
|
||||
self.selected_items.discard(item)
|
||||
|
||||
if self.focused_item in removed_items:
|
||||
self.focused_item = None
|
||||
|
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user