def test_lookups(self): qtree = Quadtree((0, 0, 100, 100)) for i in range(100, 10): for j in range(100, 10): qtree.add("%dx%d" % (i, j), (i, j, 10, 10)) for i in range(100, 10): for j in range(100, 10): assert qtree.find_intersect((i+1, j+1, 1, 1)) == ['%dx%d' % (i, j)], \ qtree.find_intersect((i+1, j+1, 1, 1))
def test_with_rectangles(self): from gaphas.geometry import Rectangle qtree = Quadtree((0, 0, 100, 100)) for i in range(0, 100, 10): for j in range(0, 100, 10): qtree.add("%dx%d" % (i, j), Rectangle(i, j, 10, 10)) assert len(qtree._ids) == 100, len(qtree._ids) for i in range(100, 10): for j in range(100, 10): assert qtree.find_intersect((i+1, j+1, 1, 1)) == ['%dx%d' % (i, j)], \ qtree.find_intersect((i+1, j+1, 1, 1))
class View: """ View class for gaphas.Canvas objects. """ def __init__(self, canvas=None): self._matrix = cairo.Matrix() self._painter = DefaultPainter(self) self._bounding_box_painter = BoundingBoxPainter(self) # Handling selections. # TODO: Move this to a context? self._selected_items = set() self._focused_item = None self._hovered_item = None self._dropzone_item = None self._qtree = Quadtree() self._bounds = Rectangle(0, 0, 0, 0) self._canvas = None if canvas: self._set_canvas(canvas) matrix = property(lambda s: s._matrix, doc="Canvas to view transformation matrix") def _set_canvas(self, canvas): """ Use view.canvas = my_canvas to set the canvas to be rendered in the view. """ if self._canvas: self._qtree.clear() self._selected_items.clear() 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 queue_draw_item(self, *items): """ Placeholder for item redraw queueing. """ pass def select_item(self, item): """ Select an item. This adds @item to the set of selected items. """ self.queue_draw_item(item) if item not in self._selected_items: self._selected_items.add(item) self.emit("selection-changed", self._selected_items) def unselect_item(self, item): """ Unselect an item. """ self.queue_draw_item(item) if item in self._selected_items: self._selected_items.discard(item) self.emit("selection-changed", self._selected_items) def select_all(self): for item in self.canvas.get_all_items(): self.select_item(item) def unselect_all(self): """ Clearing the selected_item also clears the focused_item. """ self.queue_draw_item(*self._selected_items) self._selected_items.clear() self.focused_item = None self.emit("selection-changed", self._selected_items) selected_items = property( lambda s: s._selected_items, select_item, unselect_all, "Items selected by the view", ) def _set_focused_item(self, item): """ Set the focused item, this item is also added to the selected_items set. """ if not item is self._focused_item: self.queue_draw_item(self._focused_item, item) if item: self.select_item(item) if item is not self._focused_item: self._focused_item = item self.emit("focus-changed", item) def _del_focused_item(self): """ Items that loose focus remain selected. """ self._set_focused_item(None) focused_item = property( lambda s: s._focused_item, _set_focused_item, _del_focused_item, "The item with focus (receives key events a.o.)", ) def _set_hovered_item(self, item): """ Set the hovered item. """ if item is not self._hovered_item: self.queue_draw_item(self._hovered_item, item) self._hovered_item = item self.emit("hover-changed", item) def _del_hovered_item(self): """ Unset the hovered item. """ self._set_hovered_item(None) hovered_item = property( lambda s: s._hovered_item, _set_hovered_item, _del_hovered_item, "The item directly under the mouse pointer", ) def _set_dropzone_item(self, item): """ Set dropzone item. """ if item is not self._dropzone_item: self.queue_draw_item(self._dropzone_item, item) self._dropzone_item = item self.emit("dropzone-changed", item) def _del_dropzone_item(self): """ Unset dropzone item. """ self._set_dropzone_item(None) dropzone_item = property( lambda s: s._dropzone_item, _set_dropzone_item, _del_dropzone_item, "The item which can group other items", ) def _set_painter(self, painter): """ Set the painter to use. Painters should implement painter.Painter. """ self._painter = painter painter.set_view(self) self.emit("painter-changed") painter = property(lambda s: s._painter, _set_painter) def _set_bounding_box_painter(self, painter): """ Set the painter to use for bounding box calculations. """ self._bounding_box_painter = painter painter.set_view(self) self.emit("painter-changed") bounding_box_painter = property( lambda s: s._bounding_box_painter, _set_bounding_box_painter ) def get_item_at_point(self, pos, selected=True): """ Return the topmost item located at ``pos`` (x, y). Parameters: - selected: if False returns first non-selected item """ 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(*pos) item_distance = item.point((ix, iy)) if item_distance is None: print(f"Item distance is None for {item}") continue if item_distance < 0.5: return item return None def get_handle_at_point(self, pos, distance=6): """ Look for a handle at ``pos`` and return the tuple (item, handle). """ def find(item): """ Find item's handle at pos """ v2i = self.get_matrix_v2i(item) d = distance_point_point_fast(v2i.transform_distance(0, distance)) x, y = v2i.transform_point(*pos) for h in item.handles(): if not h.movable: continue hx, hy = h.pos if -d < (hx - x) < d and -d < (hy - y) < d: return h # The focused item is the preferred item for handle grabbing if self.focused_item: h = find(self.focused_item) if h: return self.focused_item, h # then try hovered item if self.hovered_item: h = find(self.hovered_item) if h: return self.hovered_item, h # Last try all items, checking the bounding box first x, y = pos items = self.get_items_in_rectangle( (x - distance, y - distance, distance * 2, distance * 2), reverse=True ) found_item, found_h = None, None for item in items: h = find(item) if h: return item, h return None, None def get_port_at_point(self, vpos, distance=10, exclude=None): """ Find item with port closest to specified position. List of items to be ignored can be specified with `exclude` parameter. Tuple is returned - found item - closest, connectable port - closest point on found port (in view coordinates) :Parameters: vpos Position specified in view coordinates. distance Max distance from point to a port (default 10) exclude Set of items to ignore. """ v2i = self.get_matrix_v2i vx, vy = vpos max_dist = distance port = None glue_pos = None item = None rect = (vx - distance, vy - distance, distance * 2, distance * 2) items = self.get_items_in_rectangle(rect, reverse=True) for i in items: if i in exclude: 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 max_dist = d item = i port = p # transform coordinates from connectable item space to view # space i2v = self.get_matrix_i2v(i).transform_point glue_pos = i2v(*pg) return item, port, glue_pos def get_items_in_rectangle(self, rect, intersect=True, reverse=False): """ Return the items in the rectangle 'rect'. Items are automatically sorted in canvas' processing order. """ if intersect: items = self._qtree.find_intersect(rect) else: items = self._qtree.find_inside(rect) return self._canvas.sort(items, reverse=reverse) def select_in_rectangle(self, rect): """ Select all items who have their bounding box within the rectangle @rect. """ items = self._qtree.find_inside(rect) list(map(self.select_item, items)) def zoom(self, factor): """ Zoom in/out by factor @factor. """ # TODO: should the scale factor be clipped? self._matrix.scale(factor, factor) # Make sure everything's updated # map(self.update_matrix, self._canvas.get_all_items()) self.request_update((), self._canvas.get_all_items()) def set_item_bounding_box(self, item, bounds): """ Update the bounding box of the item. ``bounds`` is in view coordinates. Coordinates are calculated back to item coordinates, so matrix-only updates can occur. """ v2i = self.get_matrix_v2i(item).transform_point ix0, iy0 = v2i(bounds.x, bounds.y) ix1, iy1 = v2i(bounds.x1, bounds.y1) self._qtree.add(item=item, bounds=bounds, data=(ix0, iy0, ix1, iy1)) def get_item_bounding_box(self, item): """ Get the bounding box for the item, in view coordinates. """ return self._qtree.get_bounds(item) bounding_box = property(lambda s: s._bounds) def update_bounding_box(self, cr, items=None): """ Update the bounding boxes of the canvas items for this view, in canvas coordinates. """ painter = self._bounding_box_painter if items is None: items = self.canvas.get_all_items() # The painter calls set_item_bounding_box() for each rendered item. painter.paint(Context(cairo=cr, items=items, area=None)) # Update the view's bounding box with the rest of the items self._bounds = Rectangle(*self._qtree.soft_bounds) def get_matrix_i2v(self, item): """ Get Item to View matrix for ``item``. """ if self not in item._matrix_i2v: self.update_matrix(item) return item._matrix_i2v[self] def get_matrix_v2i(self, item): """ Get View to Item matrix for ``item``. """ if self not in item._matrix_v2i: self.update_matrix(item) return item._matrix_v2i[self] def update_matrix(self, item): """ Update item matrices related to view. """ matrix_i2c = self.canvas.get_matrix_i2c(item) try: i2v = matrix_i2c.multiply(self._matrix) except AttributeError: # Fall back to old behaviour i2v = matrix_i2c * self._matrix item._matrix_i2v[self] = i2v v2i = cairo.Matrix(*i2v) v2i.invert() item._matrix_v2i[self] = v2i def _clear_matrices(self): """ Clear registered data in Item's _matrix{i2c|v2i} attributes. """ for item in self.canvas.get_all_items(): try: del item._matrix_i2v[self] del item._matrix_v2i[self] except KeyError: pass