Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorgaphor@gmail.com <gaphor@gmail.com@a8418922-720d-0410-834f-a69b97ada669>2009-01-27 04:50:42 (GMT)
committer gaphor@gmail.com <gaphor@gmail.com@a8418922-720d-0410-834f-a69b97ada669>2009-01-27 04:50:42 (GMT)
commitd56d0671e0cf7744e3c418aea5e6b402daa7588a (patch)
tree7260983bceb2c6880aae4984142231c3dc21b11d
parentc7a3c5e791d3a6382ed46ffee53695502f608a92 (diff)
changed Tree.add() and Tree.reparent() to take an optional index argument. reparent seems to break
git-svn-id: http://svn.devjavu.com/gaphor/gaphas/trunk@2669 a8418922-720d-0410-834f-a69b97ada669
-rw-r--r--NEWS2
-rw-r--r--doc/state.txt4
-rw-r--r--doc/undo.txt36
-rw-r--r--gaphas/canvas.py15
-rw-r--r--gaphas/tests/test_tree.py43
-rw-r--r--gaphas/tool.py164
-rw-r--r--gaphas/tree.py39
-rw-r--r--gaphas/view.py87
8 files changed, 282 insertions, 108 deletions
diff --git a/NEWS b/NEWS
index cd35605..55d201d 100644
--- a/NEWS
+++ b/NEWS
@@ -5,5 +5,7 @@
- line segment tool implemented (code taken from gaphor)
- implemented Item.constraint method to simplify item's constraint
creation
+- The canvas (-view) is no longer tied to the (0, 0) position. Scrolling can
+ be done quite fluidly with the new PanTool implementation.
- API changes
- use positions instead of "x, y" pairs in all method calls
diff --git a/doc/state.txt b/doc/state.txt
index 9b501b6..c84a256 100644
--- a/doc/state.txt
+++ b/doc/state.txt
@@ -39,7 +39,7 @@ It works (see how the add method automatically schedules the item for update):
... print 'event handled', event
>>> state.observers.add(handler)
>>> canvas.add(item1) # doctest: +ELLIPSIS
- event handled (<function add at ...>, (<gaphas.canvas.Canvas object at 0x...>, <gaphas.item.Item object at 0x...>, None), {})
+ event handled (<function add at ...>, (<gaphas.canvas.Canvas object at 0x...>, <gaphas.item.Item object at 0x...>, None, None), {})
>>> canvas.add(item2, parent=item1) # doctest: +ELLIPSIS
event handled (<function add at ...>, (<gaphas.canvas.Canvas object at 0x...>, <gaphas.item.Item object at 0x...>, <gaphas.item.Item object at 0x...), {})
>>> canvas.get_all_items() # doctest: +ELLIPSIS
@@ -139,7 +139,7 @@ The inverse operation is easiest performed by the function ``saveapply()``. Of
course an inverse operation is emitting a change event too:
>>> state.saveapply(*events.pop()) # doctest: +ELLIPSIS
- event handler (<function add at 0x...>, {'item': <gaphas.item.Item object at 0x...>, 'self': <gaphas.canvas.Canvas object at 0x...>, 'parent': None})
+ event handler (<function add at 0x...>, {'item': <gaphas.item.Item object at 0x...>, 'self': <gaphas.canvas.Canvas object at 0x...>, 'parent': None, 'index': 0})
>>> canvas.get_all_items()
[]
diff --git a/doc/undo.txt b/doc/undo.txt
index f055918..a1c8b38 100644
--- a/doc/undo.txt
+++ b/doc/undo.txt
@@ -191,6 +191,42 @@ children):
>>> canvas.get_children(item) # doctest: +ELLIPSIS
[<gaphas.item.Item object at 0x...>]
+As well as the reparent() method:
+
+ >>> canvas = Canvas()
+ >>> class NameItem(Item):
+ ... def __init__(self, name):
+ ... super(NameItem, self).__init__()
+ ... self.name = name
+ ... def __repr__(self):
+ ... return '<%s>' % self.name
+ >>> ni1 = NameItem('a')
+ >>> canvas.add(ni1)
+ >>> ni2 = NameItem('b')
+ >>> canvas.add(ni2)
+ >>> ni3 = NameItem('c')
+ >>> canvas.add(ni3, parent=ni1)
+ >>> ni4 = NameItem('d')
+ >>> canvas.add(ni4, parent=ni3)
+ >>> canvas.get_all_items()
+ [<a>, <c>, <d>, <b>]
+ >>> del undo_list[:]
+ >>> canvas.reparent(ni3, parent=ni2)
+ >>> canvas.get_all_items()
+ [<a>, <b>, <c>, <d>]
+ >>> len(undo_list)
+ 1
+ >>> undo()
+ >>> canvas.get_all_items()
+ [<a>, <c>, <d>, <b>]
+
+Redo should work too:
+
+ >>> undo_list[:] = redo_list[:]
+ >>> undo()
+ >>> canvas.get_all_items()
+ [<a>, <b>, <c>, <d>]
+
connector.py: Handle
--------------------
Changing the Handle's position is reversible:
diff --git a/gaphas/canvas.py b/gaphas/canvas.py
index 856a1ab..f90264f 100644
--- a/gaphas/canvas.py
+++ b/gaphas/canvas.py
@@ -53,7 +53,7 @@ class Canvas(object):
@observed
- def add(self, item, parent=None):
+ def add(self, item, parent=None, index=None):
"""
Add an item to the canvas.
@@ -67,7 +67,7 @@ class Canvas(object):
True
"""
assert item not in self._tree.nodes, 'Adding already added node %s' % item
- self._tree.add(item, parent)
+ self._tree.add(item, parent, index)
item._set_canvas(self)
self._dirty_index = True
@@ -108,7 +108,8 @@ class Canvas(object):
self._remove(item)
reversible_pair(add, _remove,
- bind1={'parent': lambda self, item: self.get_parent(item)})
+ bind1={'parent': lambda self, item: self.get_parent(item),
+ 'index': lambda self, item: self._tree.get_siblings(item).index(item) })
def remove_connections_to_item(self, item):
@@ -125,14 +126,18 @@ class Canvas(object):
h.disconnect = lambda: 0
@observed
- def reparent(self, item, parent):
+ def reparent(self, item, parent, index=None):
"""
Set new parent for an item.
"""
- self._tree.reparent(item, parent)
+ self._tree.reparent(item, parent, index)
self._dirty_index = True
+ reversible_method(reparent, reverse=reparent,
+ bind={'parent': lambda self, item: self.get_parent(item),
+ 'index': lambda self, item: self._tree.get_siblings(item).index(item) })
+
def get_all_items(self):
"""
diff --git a/gaphas/tests/test_tree.py b/gaphas/tests/test_tree.py
index 242b059..6c3fedc 100644
--- a/gaphas/tests/test_tree.py
+++ b/gaphas/tests/test_tree.py
@@ -54,6 +54,27 @@ class TreeTestCase(unittest.TestCase):
assert tree.get_parent(n2) is None
assert tree.get_parent(n1) is None
+ def test_add_on_index(self):
+ tree = Tree()
+ n1 = 'n1'
+ n2 = 'n2'
+ n3 = 'n3'
+ n4 = 'n4'
+ n5 = 'n5'
+
+ tree.add(n1)
+ tree.add(n2)
+ tree.add(n3, index=1)
+ assert tree.get_children(None) == [n1, n3, n2], tree.get_children(None)
+ assert tree.nodes == [n1, n3, n2], tree.nodes
+
+ tree.add(n4, parent=n3)
+ tree.add(n5, parent=n3, index=0)
+ assert tree.get_children(None) == [n1, n3, n2], tree.get_children(None)
+ assert tree.nodes == [n1, n3, n5, n4, n2], tree.nodes
+ assert tree.get_children(n3) == [n5, n4], tree.get_children(n3)
+
+
def test_remove(self):
"""
Test removal of nodes.
@@ -113,4 +134,24 @@ class TreeTestCase(unittest.TestCase):
else:
raise AssertionError, 'Index should be out of range, not %s' % tree.get_previous_sibling(n1)
-# vi:sw=4:et
+ def test_reparent(self):
+ tree = Tree()
+ n1 = 'n1'
+ n2 = 'n2'
+ n3 = 'n3'
+ n4 = 'n4'
+ n5 = 'n5'
+
+ tree.add(n1)
+ tree.add(n2)
+ tree.add(n3)
+ tree.add(n4, parent=n2)
+ tree.add(n5, parent=n4)
+
+ assert tree.nodes == [n1, n2, n4, n5, n3], tree.nodes
+
+ tree.reparent(n4, parent=None, index=0)
+ assert tree.nodes == [n4, n5, n1, n2, n3], tree.nodes
+
+
+# vi:sw=4:et:ai
diff --git a/gaphas/tool.py b/gaphas/tool.py
index 3f7dcfd..1525937 100644
--- a/gaphas/tool.py
+++ b/gaphas/tool.py
@@ -237,7 +237,7 @@ class ToolChain(Tool):
<gaphas.tool.ToolChain object at 0x...>
Now the HoverTool has been substituted for the ItemTool:
-
+
>>> chain._tools # doctest: +ELLIPSIS
[<gaphas.tool.ItemTool object at 0x...>, <gaphas.tool.RubberbandTool object at 0x...>]
"""
@@ -276,7 +276,7 @@ class ToolChain(Tool):
rt = getattr(tool, func)(context, event)
if rt:
return rt
-
+
def on_button_press(self, context, event):
self._handle('on_button_press', context, event)
@@ -310,7 +310,7 @@ class HoverTool(Tool):
"""
Make the item under the mouse cursor the "hovered item".
"""
-
+
def __init__(self):
pass
@@ -328,7 +328,7 @@ class ItemTool(Tool):
already selected items remain selected. The last selected item gets the
focus (e.g. receives key press events).
"""
-
+
def __init__(self, buttons=(1,)):
self.last_x = 0
self.last_y = 0
@@ -461,7 +461,7 @@ class HandleTool(Tool):
# Last try all items, checking the bounding box first
x, y = event.x, event.y
items = view.get_items_in_rectangle((x - 6, y - 6, 12, 12), reverse=True)
-
+
found_item, found_h = None, None
for item in items:
h = self._find_handle(view, event, item)
@@ -493,16 +493,16 @@ class HandleTool(Tool):
Find an item that ``handle`` can connect to and create a connection.
``item`` is the ``Item`` owning the handle.
``vpos`` is the point in the pointer (view) coordinates.
-
+
A typical connect action may involve the following:
-
+
- Find an item near ``handle`` that can be connected to.
- Move ``handle`` to the right position.
- Set ``handle.connected_to`` to point to the new item.
- Add constraints to the constraint solver (``view.canvas.solver``).
- Set ``handle.disconnect`` to point to a method that can be called when
the handle is disconnected (no arguments).
-
+
NOTE: ``connect()`` can not expect ``glue()`` has been called,
therefore it should ensure the handle is moved to the correct location
before.
@@ -514,7 +514,7 @@ class HandleTool(Tool):
constraints. ``item`` is the Item owning the handle.
A typical disconnect operation may look like this:
-
+
- Call ``handle.disconnect()`` (assigned in ``connect()``).
- Disconnect existing constraints.
- Set ``handle.connected_to`` to ``None``.
@@ -579,7 +579,7 @@ class HandleTool(Tool):
# Do the actual move:
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)
@@ -640,6 +640,8 @@ class RubberbandTool(Tool):
cr.rectangle(min(x0, x1), min(y0, y1), abs(x1 - x0), abs(y1 - y0))
cr.fill()
+PAN_MASK = gtk.gdk.SHIFT_MASK | gtk.gdk.MOD1_MASK | gtk.gdk.CONTROL_MASK
+PAN_VALUE = 0
class PanTool(Tool):
"""
@@ -653,7 +655,9 @@ class PanTool(Tool):
self.speed = 10
def on_button_press(self,context,event):
- if event.button == 2 and not event.state:
+ if not event.state & PAN_MASK == PAN_VALUE:
+ return False
+ if event.button == 2:
context.grab()
self.x0, self.y0 = event.x, event.y
return True
@@ -664,47 +668,120 @@ class PanTool(Tool):
return True
def on_motion_notify(self, context, event):
- if event.state & gtk.gdk.BUTTON2_MASK and not event.state:
+ 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
+ self.x1, self.y1 = event.x, event.y
+ dx = self.x1 - self.x0
+ dy = self.y1 - self.y0
+ view._matrix.translate(dx/view._matrix[0], dy/view._matrix[3])
+ # Make sure everything's updated
+ view.request_update((), view._canvas.get_all_items())
+ view.update_adjustments()
+ self.x0 = self.x1
+ self.y0 = self.y1
return True
def on_scroll(self, context, event):
- # No modifiers:
- if event.state:
- return
+ # Ensure no modifiers
+ if not event.state & PAN_MASK == PAN_VALUE:
+ return False
+ view = context.view
direction = event.direction
gdk = gtk.gdk
- adj = None
if direction == gdk.SCROLL_LEFT:
- adj = context.view.hadjustment
- adj.value = adj.value - self.speed
+ view._matrix.translate(self.speed/view._matrix[0], 0)
elif direction == gdk.SCROLL_RIGHT:
- adj = context.view.hadjustment
- adj.value = adj.value + self.speed
+ view._matrix.translate(-self.speed/view._matrix[0], 0)
elif direction == gdk.SCROLL_UP:
- adj = context.view.vadjustment
- adj.value = adj.value - self.speed
+ view._matrix.translate(0, self.speed/view._matrix[3])
elif direction == gdk.SCROLL_DOWN:
- adj = context.view.vadjustment
- adj.value = adj.value + self.speed
- if adj:
- adj.value_changed()
+ view._matrix.translate(0, -self.speed/view._matrix[3])
+ view.request_update((), view._canvas.get_all_items())
+ view.update_adjustments()
return True
+ZOOM_MASK = gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK | gtk.gdk.MOD1_MASK
+ZOOM_VALUE = gtk.gdk.CONTROL_MASK
+
+class ZoomTool(Tool):
+ """
+ Tool for zooming using either of two techniquies:
+ * ctrl + middle-mouse dragging in the up-down direction.
+ * ctrl + mouse-wheeel
+ """
+
+ def __init__(self):
+ self.x0, self.y0 = 0, 0
+ self.lastdiff = 0;
+
+ def on_button_press(self, context, event):
+ if event.button == 2 \
+ and event.state & ZOOM_MASK == ZOOM_VALUE:
+ print "GRABBING"
+ context.grab()
+ self.x0 = event.x
+ self.y0 = event.y
+ self.lastdiff = 0
+ return True
+
+ def on_button_release(self, context, event):
+ context.ungrab()
+ self.lastdiff = 0
+ return True
+
+ def on_motion_notify(self, context, event):
+ if event.state & ZOOM_MASK == ZOOM_VALUE \
+ and event.state & gtk.gdk.BUTTON2_MASK:
+ view = context.view
+ dy = event.y - self.y0
+
+ sx = view._matrix[0]
+ sy = view._matrix[3]
+ ox = (view._matrix[4] - self.x0) / sx
+ oy = (view._matrix[5] - self.y0) / sy
+
+ if abs(dy - self.lastdiff) > 20:
+ if dy - self.lastdiff < 0:
+ factor = 1./0.9
+ else:
+ factor = 0.9
+
+ view._matrix.translate(-ox, -oy)
+ view._matrix.scale(factor, factor)
+ view._matrix.translate(+ox, +oy)
+
+ # Make sure everything's updated
+ map(view.update_matrix, view._canvas.get_all_items())
+ view.request_update(view._canvas.get_all_items())
+ view.update_scrollbars_from_matrix(view.hadjustment,view.vadjustment)
+
+ self.lastdiff = dy;
+ return True
+
+ def on_scroll(self, context, event):
+ if event.state & gtk.gdk.CONTROL_MASK:
+ view = context.view
+ context.grab()
+ sx = view._matrix[0]
+ sy = view._matrix[3]
+ ox = (view._matrix[4] - event.x) / sx
+ oy = (view._matrix[5] - event.y) / sy
+ factor = 0.9
+ if event.direction == gtk.gdk.SCROLL_UP:
+ factor = 1. / factor
+ view._matrix.translate(-ox, -oy)
+ view._matrix.scale(factor, factor)
+ view._matrix.translate(+ox, +oy)
+ # Make sure everything's updated
+ map(view.update_matrix, view._canvas.get_all_items())
+ view.request_update(view._canvas.get_all_items())
+ view.update_scrollbars_from_matrix(view.hadjustment,view.vadjustment)
+ context.ungrab()
+ return True
+
+
+
class PlacementTool(Tool):
def __init__(self, factory, handle_tool, handle_index):
@@ -817,7 +894,7 @@ class TextEditTool(Tool):
class ConnectHandleTool(HandleTool):
"""
Tool for connecting two items.
-
+
There are two items involved. Handle of connecting item (usually
a line) is being dragged by an user towards another item (item in
short). Port of an item is found by the tool and connection is
@@ -865,11 +942,11 @@ class ConnectHandleTool(HandleTool):
# update position of line's handle
v2i = view.get_matrix_v2i(line).transform_point
handle.pos = v2i(*glue_pos)
-
+
# else item and port will be set to None
return item, port
-
+
def get_item_at_point(self, view, vpos, exclude=None):
"""
Find item with port closest to specified position.
@@ -1176,7 +1253,7 @@ class LineSegmentTool(ConnectHandleTool):
Split one line segment into ``count`` equal pieces.
Two lists are returned
-
+
- list of created handles
- list of created ports
@@ -1386,6 +1463,7 @@ def DefaultTool():
append(HoverTool()). \
append(LineSegmentTool()). \
append(PanTool()). \
+ append(ZoomTool()). \
append(ItemTool()). \
append(TextEditTool()). \
append(RubberbandTool())
diff --git a/gaphas/tree.py b/gaphas/tree.py
index 5c24891..63565b7 100644
--- a/gaphas/tree.py
+++ b/gaphas/tree.py
@@ -227,17 +227,28 @@ class Tree(object):
# append to root node:
nodes.append(node)
- def add(self, node, parent=None):
+ def add(self, node, parent=None, index=None):
"""
Add node to the tree. parent is the parent node, which may
be None if the item should be added to the root item.
For usage, see the unit tests.
"""
- assert not self._children.get(node)
+
+ assert node not in self._nodes
+
siblings = self._children[parent]
- self._add_to_nodes(node, parent)
- siblings.append(node)
+ try:
+ atnode = siblings[index]
+ except (TypeError, IndexError):
+ index = len(siblings)
+ self._add_to_nodes(node, parent)
+ else:
+ self._nodes.insert(self._nodes.index(atnode), node)
+
+ # Fix parent-child and child-parent relationship
+ siblings.insert(index, node)
+
# Create new entry for it's own children:
self._children[node] = []
if parent:
@@ -277,7 +288,7 @@ class Tree(object):
for c in self._children[node]:
self._reparent_nodes(c, node)
- def reparent(self, node, parent):
+ def reparent(self, node, parent, index=None):
"""
Set new parent for a ``node``. ``Parent`` can be ``None``, indicating
it's added to the top.
@@ -311,11 +322,21 @@ class Tree(object):
"""
# Add to new parent's children:
self.get_siblings(node).remove(node)
- self._children[parent].append(node)
+
self._parents[node] = parent
+
+ # Change this to get index working:
+ siblings = self._children[parent]
+ try:
+ atnode = siblings[index]
+ except (TypeError, IndexError):
+ siblings.append(node)
+ # reorganize nodes
+ self._reparent_nodes(node, parent)
+ else:
+ self._nodes.insert(self._nodes.index(atnode), node)
+
- # reorganize nodes
- self._reparent_nodes(node, parent)
-# vi:sw=4:et
+# vi: sw=4:et:ai
diff --git a/gaphas/view.py b/gaphas/view.py
index b72e289..c949922 100644
--- a/gaphas/view.py
+++ b/gaphas/view.py
@@ -269,7 +269,7 @@ class View(object):
return self._qtree.get_bounds(item)
- bounding_box = property(lambda s: s._bounds) #Rectangle(*s._qtree.soft_bounds))
+ bounding_box = property(lambda s: s._bounds)
def update_bounding_box(self, cr, items=None):
@@ -471,55 +471,47 @@ class GtkView(gtk.DrawingArea, View):
self.queue_draw_refresh()
- def _update_adjustment(self, adjustment, canvas_size, viewport_size):
- """
- Update one gtk.Adjustment instance. Values are in view coordinates.
-
- >>> v = GtkView()
- >>> a = gtk.Adjustment()
- >>> v._hadjustment = a
- >>> v._update_adjustment(a, 100, 20)
- >>> a.page_size, a.page_increment, a.value
- (20.0, 20.0, 0.0)
- """
- canvas_size = max(canvas_size, viewport_size)
- view_size = canvas_size + viewport_size
- if viewport_size != adjustment.page_size or view_size != adjustment.upper:
- adjustment.lower = 0
- adjustment.upper = view_size
- adjustment.page_size = viewport_size
- adjustment.page_increment = viewport_size
- adjustment.step_increment = viewport_size / 10
-
- if adjustment.value > canvas_size:
- adjustment.value = canvas_size
- elif adjustment.value < 0:
- adjustment.value = 0
-
@async(single=True)
def update_adjustments(self, allocation=None):
- """
- Update the allocation objects (for scrollbars).
- """
if not allocation:
allocation = self.allocation
- # Define a minimal view size:
- aw, ah = allocation.width, allocation.height
- x, y, w, h = self.bounding_box
- ox, oy = self.matrix.transform_point(0, 0)
-
- self._update_adjustment(self._hadjustment,
- canvas_size=w + x - ox,
- viewport_size=aw)
- self._update_adjustment(self._vadjustment,
- canvas_size=h + y - oy,
- viewport_size=aw)
-
- x, y, w, h = self._qtree.bounds
- if w != aw or h != ah:
- self._qtree.resize((0, 0, aw, ah))
-
+ hadjustment = self._hadjustment
+ vadjustment = self._vadjustment
+
+ # canvas limits (in view coordinates)
+ c = Rectangle(*self._qtree.soft_bounds)
+
+ # view limits
+ v = Rectangle(0, 0, self.allocation.width, self.allocation.height)
+
+ # union of these limits gives scrollbar limits
+ if v in c:
+ u = c
+ else:
+ u = c + v
+
+ # set lower limits
+ hadjustment.lower, vadjustment.lower = u.x, u.y
+
+ # set upper limits
+ hadjustment.upper, vadjustment.upper = u.x1, u.y1
+
+ # set position
+ if v.x != hadjustment.value or v.y != vadjustment.value:
+ hadjustment.value, vadjustment.value = v.x, v.y
+
+ # set page size
+ aw, ah = self.allocation.width, self.allocation.height
+ hadjustment.page_size = aw
+ vadjustment.page_size = ah
+
+ # set increments
+ hadjustment.page_increment = aw
+ hadjustment.step_increment = aw / 10
+ vadjustment.page_increment = ah
+ vadjustment.step_increment = ah / 10
+
def queue_draw_item(self, *items):
"""
@@ -734,12 +726,11 @@ class GtkView(gtk.DrawingArea, View):
value of the x/y adjustment (scrollbar).
"""
if adj is self._hadjustment:
- self._matrix.translate(- self._matrix[4] - adj.value , 0)
+ self._matrix.translate( - adj.value , 0)
elif adj is self._vadjustment:
- self._matrix.translate(0, - self._matrix[5] - adj.value)
+ self._matrix.translate(0, - adj.value)
# Force recalculation of the bounding boxes:
- map(self.update_matrix, self._canvas.get_all_items())
self.request_update((), self._canvas.get_all_items())
self.queue_draw_refresh()