Пример #1
0
    def test_get_data(self):
        """
        Extra data may be added to a node:
        """
        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), (i, j, 10, 10), i+j)

        for i in range(0, 100, 10):
            for j in range(0, 100, 10):
                assert i+j == qtree.get_data("%dx%d" % (i, j))
Пример #2
0
    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)
Пример #3
0
    def __init__(self,
                 model: Optional[Model] = None,
                 selection: Selection = None):
        """Create a new view.

        Args:
            model (Model): optional model to be set on construction time.
            selection (Selection): optional selection object, in case the default
                selection object (hover/select/focus) is not enough.
        """
        Gtk.DrawingArea.__init__(self)

        self._dirty_items: Set[Item] = set()

        self._back_buffer: Optional[cairo.Surface] = None
        self._back_buffer_needs_resizing = True

        self._controllers: Set[Gtk.EventController] = set()

        self.set_can_focus(True)
        if Gtk.get_major_version() == 3:
            self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK
                            | Gdk.EventMask.BUTTON_RELEASE_MASK
                            | Gdk.EventMask.POINTER_MOTION_MASK
                            | Gdk.EventMask.KEY_PRESS_MASK
                            | Gdk.EventMask.KEY_RELEASE_MASK
                            | Gdk.EventMask.SCROLL_MASK
                            | Gdk.EventMask.STRUCTURE_MASK)
            self.set_app_paintable(True)
        else:
            self.set_draw_func(GtkView.do_draw)
            self.connect_after("resize", GtkView.on_resize)

        def alignment_updated(matrix: Matrix) -> None:
            if self._model:
                self._matrix *= matrix  # type: ignore[misc]

        self._scrolling = Scrolling(alignment_updated)

        self._selection = selection or Selection()

        self._matrix = Matrix()
        self._painter: Painter = DefaultPainter(self)
        self._bounding_box_painter: ItemPainterType = ItemPainter(
            self._selection)
        self._matrix_changed = False

        self._qtree: Quadtree[Item, Tuple[float, float, float,
                                          float]] = Quadtree()

        self._model: Optional[Model] = None
        if model:
            self.model = model

        self._selection.add_handler(self.on_selection_update)
        self._matrix.add_handler(self.on_matrix_update)
Пример #4
0
    def test_moving_items(self):
        qtree = Quadtree((0, 0, 100, 100), capacity=10)
        for i in range(0, 100, 10):
            for j in range(0, 100, 10):
                qtree.add("%dx%d" % (i, j), (i, j, 10, 10))
        assert len(qtree._ids) == 100, len(qtree._ids)
        assert qtree._bucket._buckets, qtree._bucket._buckets
        for i in range(4):
            assert qtree._bucket._buckets[i]._buckets
            for j in range(4):
                assert not qtree._bucket._buckets[i]._buckets[j]._buckets

        # Check contents:
        # First sub-level contains 9 items. second level contains 4 items
        # ==> 4 * (9 + (4 * 4)) = 100
        assert len(qtree._bucket.items) == 0, qtree._bucket.items
        for i in range(4):
            assert len(qtree._bucket._buckets[i].items) == 9
            for item, bounds in qtree._bucket._buckets[i].items.iteritems():
                assert qtree._bucket.find_bucket(bounds) is qtree._bucket._buckets[i]
            for j in range(4):
                assert len(qtree._bucket._buckets[i]._buckets[j].items) == 4

        assert qtree.get_bounds('0x0')
        # Now move item '0x0' to the center of the first quadrant (20, 20)
        qtree.add('0x0', (20, 20, 10, 10))
        assert len(qtree._bucket.items) == 0
        assert len(qtree._bucket._buckets[0]._buckets[0].items) == 3, \
                qtree._bucket._buckets[0]._buckets[0].items
        assert len(qtree._bucket._buckets[0].items) == 10, \
                qtree._bucket._buckets[0].items

        # Now move item '0x0' to the second quadrant (70, 20)
        qtree.add('0x0', (70, 20, 10, 10))
        assert len(qtree._bucket.items) == 0
        assert len(qtree._bucket._buckets[0]._buckets[0].items) == 3, \
                qtree._bucket._buckets[0]._buckets[0].items
        assert len(qtree._bucket._buckets[0].items) == 9, \
                qtree._bucket._buckets[0].items
        assert len(qtree._bucket._buckets[1].items) == 10, \
                qtree._bucket._buckets[1].items
Пример #5
0
    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))
Пример #6
0
    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))
Пример #7
0
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
Пример #8
0
def qtree():
    qtree = Quadtree((0, 0, 100, 100))
    for i in range(0, 100, 10):
        for j in range(0, 100, 10):
            qtree.add(item=f"{i:d}x{j:d}", bounds=Rectangle(i, j, 10, 10))
    return qtree
Пример #9
0
 def test_clipped_bounds(self):
     qtree = Quadtree((0, 0, 100, 100), capacity=10)
     qtree.add(1, (-100, -100, 120, 120))
     self.assertEquals((0, 0, 20, 20), qtree.get_clipped_bounds(1))
Пример #10
0
def qtree():
    qtree = Quadtree((0, 0, 100, 100))
    for i in range(0, 100, 10):
        for j in range(0, 100, 10):
            qtree.add(item="%dx%d" % (i, j), bounds=Rectangle(i, j, 10, 10))
    return qtree
Пример #11
0
def test_many_items_on_same_position():
    capacity = 10
    qtree: Quadtree[int, None] = Quadtree((0, 0, 0, 0), capacity=10)

    for i in range(0, capacity + 2):
        qtree.add(item=i, bounds=(0, 0, 10, 10), data=None)
Пример #12
0
def qtree():
    qtree: Quadtree[str, None] = Quadtree((0, 0, 100, 100))
    for i in range(0, 100, 10):
        for j in range(0, 100, 10):
            qtree.add(item=f"{i:d}x{j:d}", bounds=(i, j, 10, 10), data=None)
    return qtree
Пример #13
0
def qtree():
    qtree = Quadtree((0, 0, 100, 100))
    for i in range(0, 100, 10):
        for j in range(0, 100, 10):
            qtree.add(item="%dx%d" % (i, j), bounds=Rectangle(i, j, 10, 10))
    return qtree