Example #1
0
    def __init__(self, connections, id=None, model=None):
        super().__init__(id=id, model=model)
        self._matrix = Matrix()
        self._matrix_i2c = Matrix()
        self._connections = connections

        h1, h2 = Handle(), Handle()
        self._handles = [h1, h2]
        self._ports = [LinePort(h1.pos, h2.pos)]

        self._combined = None

        self.shape = IconBox(
            Box(style={"min-width": 0, "min-height": 45}, draw=self.draw_fork_node),
            Text(
                text=lambda: stereotypes_str(self.subject),
            ),
            EditableText(text=lambda: self.subject and self.subject.name or ""),
            Text(
                text=lambda: isinstance(self.subject, UML.JoinNode)
                and self.subject.joinSpec not in (None, DEFAULT_JOIN_SPEC)
                and f"{{ joinSpec = {self.subject.joinSpec} }}"
                or "",
            ),
        )

        self.watch("subject[NamedElement].name")
        self.watch("subject.appliedStereotype.classifier.name")
        self.watch("subject[JoinNode].joinSpec")

        connections.add_constraint(self, constraint(vertical=(h1.pos, h2.pos)))
        connections.add_constraint(self, constraint(above=(h1.pos, h2.pos), delta=30))
Example #2
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)
def test_projection_updates_when_matrix_is_changed(solver):
    pos = Position(0, 0)
    matrix = Matrix()
    proj = MatrixProjection(pos, matrix)
    solver.add_constraint(proj)
    solver.solve()

    matrix.translate(2, 3)
    solver.solve()

    assert proj.x == 2
    assert proj.y == 3
Example #4
0
 def _set_matrix(self, matrix):
     """
     Set the conversion matrix (parent -> item)
     """
     if not isinstance(matrix, Matrix):
         matrix = Matrix(*matrix)
     self._matrix = matrix
Example #5
0
 def set_property(self, prop, value):
     if prop.name == "hadjustment":
         if value is not None:
             self.hadjustment = value
             self._hadjustment_handler_id = self.hadjustment.connect(
                 "value-changed", self.on_adjustment_changed
             )
             self._scrolling_updated(Matrix())
     elif prop.name == "vadjustment":
         if value is not None:
             self.vadjustment = value
             self._vadjustment_handler_id = self.vadjustment.connect(
                 "value-changed", self.on_adjustment_changed
             )
             self._scrolling_updated(Matrix())
     elif prop.name == "hscroll-policy":
         self.hscroll_policy = value
     elif prop.name == "vscroll-policy":
         self.vscroll_policy = value
     else:
         raise AttributeError(f"Unknown property {prop.name}")
Example #6
0
    def __init__(self, connections, id=None, model=None):
        super().__init__(id=id, model=model)
        self._matrix = Matrix()
        self._matrix_i2c = Matrix()
        self._connections = connections

        self.bar_width = 12

        ht, hb = Handle(), Handle()
        ht.connectable = True

        self._handles = [ht, hb]

        connections.add_constraint(self, constraint(vertical=(ht.pos, hb.pos)))

        r = self.bar_width / 2
        nw = Position(-r, 0, strength=WEAK)
        ne = Position(r, 0, strength=WEAK)
        se = Position(r, 0, strength=WEAK)
        sw = Position(-r, 0, strength=WEAK)

        for c in (
                constraint(horizontal=(nw, ht.pos)),
                constraint(horizontal=(ne, ht.pos)),
                constraint(horizontal=(sw, hb.pos)),
                constraint(horizontal=(se, hb.pos)),
                constraint(vertical=(nw, ht.pos), delta=-r),
                constraint(vertical=(ne, ht.pos), delta=r),
                constraint(vertical=(sw, hb.pos), delta=-r),
                constraint(vertical=(se, hb.pos), delta=r),
        ):
            connections.add_constraint(self, c)

        self._ports = [LinePort(nw, sw), LinePort(ne, se)]

        self.shape = Box(style={"background-color": (1.0, 1.0, 1.0, 1.0)},
                         draw=draw_border)
Example #7
0
    def __init__(self):
        self._canvas = None
        self._matrix = Matrix()
        self._handles = []
        self._constraints = []
        self._ports = []

        # used by gaphas.canvas.Canvas to hold conversion matrices
        self._matrix_i2c = None
        self._matrix_c2i = None

        # used by gaphas.view.GtkView to hold item 2 view matrices (view=key)
        self._matrix_i2v = WeakKeyDictionary()
        self._matrix_v2i = WeakKeyDictionary()
        self._canvas_projections = WeakSet()
Example #8
0
    def __init__(self, connections, id=None, model=None):
        super().__init__(id=id, model=model)
        self._matrix = Matrix()
        self._matrix_i2c = Matrix()
        self._connections = connections

        h1 = Handle(connectable=True)
        self._handles = [h1]

        d = self.dimensions()
        top_left = Position(d.x, d.y)
        top_right = Position(d.x1, d.y)
        bottom_right = Position(d.x1, d.y1)
        bottom_left = Position(d.x, d.y1)
        self._ports = [
            LinePort(top_left, top_right),
            LinePort(top_right, bottom_right),
            LinePort(bottom_right, bottom_left),
            LinePort(bottom_left, top_left),
        ]

        self._last_connected_side = None
        self.watch("subject[NamedElement].name")
        self.update_shapes()
Example #9
0
    def on_adjustment_changed(self, adj):
        """Change the transformation matrix of the view to reflect the value of
        the x/y adjustment (scrollbar)."""
        value = adj.get_value()
        if value == 0.0:
            return

        m = Matrix()
        if adj is self.hadjustment:
            m.translate(self._last_hvalue - value, 0)
            self._last_hvalue = value
        elif adj is self.vadjustment:
            m.translate(0, self._last_vvalue - value)
            self._last_vvalue = value

        self._scrolling_updated(m)
def test_matrix_projection_sets_handlers_just_in_time():
    pos = Position(0, 0)
    matrix = Matrix()
    proj = MatrixProjection(pos, matrix)

    def handler(c):
        pass

    assert not matrix._handlers
    assert not pos.x._handlers
    assert not pos.y._handlers

    proj.add_handler(handler)

    assert matrix._handlers
    assert pos.x._handlers
    assert pos.y._handlers

    proj.remove_handler(handler)

    assert not matrix._handlers
    assert not pos.x._handlers
    assert not pos.y._handlers
Example #11
0
 def position(self, pos):
     self.matrix = Matrix(x0=pos[0], y0=pos[1])
def test_matrix_projection_exposes_variables():
    proj = MatrixProjection(Position(0, 0), Matrix())

    assert isinstance(proj.x, Variable)
    assert isinstance(proj.y, Variable)
    assert len(handler.events) == 1
    assert handler.events[0][0] is pos
    assert handler.events[0][1] == (3.0, 3.0)


def test_matrix_projection_exposes_variables():
    proj = MatrixProjection(Position(0, 0), Matrix())

    assert isinstance(proj.x, Variable)
    assert isinstance(proj.y, Variable)


@pytest.mark.parametrize(
    "position,matrix,result",
    [
        [(0, 0), Matrix(1, 0, 0, 1, 0, 0), (0, 0)],
        [(2, 4), Matrix(2, 0, 0, 1, 2, 3), (6, 7)],
        [(2, 4), Matrix(2, 0, 0, 1, 2, 3), (6, 7)],
    ],
)
def test_projection_updates_when_original_is_changed(solver, position, matrix,
                                                     result):
    pos = Position(0, 0)
    proj = MatrixProjection(pos, matrix)
    solver.add_constraint(proj)
    solver.solve()

    pos.x, pos.y = position
    solver.solve()

    assert proj.x == result[0]
Example #14
0
def test_multiply_equals_should_result_in_same_matrix():
    m1 = Matrix()
    m2 = m1
    m2 *= Matrix(20, 20)

    assert m1 is m2
Example #15
0
class GtkView(Gtk.DrawingArea, Gtk.Scrollable):
    """GTK+ widget for rendering a gaphas.view.model.Model to a screen.  The
    view uses Tools to handle events and Painters to draw. Both are
    configurable.

    The widget already contains adjustment objects (`hadjustment`,
    `vadjustment`) to be used for scrollbars.

    This view registers itself on the model, so it will receive
    update events.
    """

    # Just defined a name to make GTK register this class.
    __gtype_name__ = "GaphasView"

    # properties required by Gtk.Scrollable
    __gproperties__ = {
        "hscroll-policy": (
            Gtk.ScrollablePolicy,
            "hscroll-policy",
            "hscroll-policy",
            Gtk.ScrollablePolicy.MINIMUM,
            GObject.ParamFlags.READWRITE,
        ),
        "hadjustment": (
            Gtk.Adjustment,
            "hadjustment",
            "hadjustment",
            GObject.ParamFlags.READWRITE,
        ),
        "vscroll-policy": (
            Gtk.ScrollablePolicy,
            "vscroll-policy",
            "vscroll-policy",
            Gtk.ScrollablePolicy.MINIMUM,
            GObject.ParamFlags.READWRITE,
        ),
        "vadjustment": (
            Gtk.Adjustment,
            "vadjustment",
            "vadjustment",
            GObject.ParamFlags.READWRITE,
        ),
    }

    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)

    def do_get_property(self, prop: str) -> object:
        return self._scrolling.get_property(prop)

    def do_set_property(self, prop: str, value: object) -> None:
        self._scrolling.set_property(prop, value)

    @property
    def matrix(self) -> Matrix:
        """Model root to view transformation matrix."""
        return self._matrix

    def get_matrix_i2v(self, item: Item) -> Matrix:
        """Get Item to View matrix for ``item``."""
        return item.matrix_i2c.multiply(self._matrix)

    def get_matrix_v2i(self, item: Item) -> Matrix:
        """Get View to Item matrix for ``item``."""
        m = self.get_matrix_i2v(item)
        m.invert()
        return m

    @property
    def model(self) -> Optional[Model]:
        """The model."""
        return self._model

    @model.setter
    def model(self, model: Optional[Model]) -> None:
        if self._model:
            self._model.unregister_view(self)
            self._selection.clear()
            self._qtree.clear()

        self._model = model

        if self._model:
            self._model.register_view(self)
            self.request_update(self._model.get_all_items())

    @property
    def painter(self) -> Painter:
        """Painter for drawing the view."""
        return self._painter

    @painter.setter
    def painter(self, painter: Painter) -> None:
        self._painter = painter

    @property
    def bounding_box_painter(self) -> ItemPainterType:
        """Special painter for calculating item bounding boxes."""
        return self._bounding_box_painter

    @bounding_box_painter.setter
    def bounding_box_painter(self, painter: ItemPainterType) -> None:
        self._bounding_box_painter = painter

    @property
    def selection(self) -> Selection:
        """Selected, focused and hovered items."""
        return self._selection

    @selection.setter
    def selection(self, selection: Selection) -> None:
        """Selected, focused and hovered items."""
        self._selection = selection
        if self._model:
            self.request_update(self._model.get_all_items())

    @property
    def bounding_box(self) -> Rectangle:
        """The bounding box of the complete view, relative to the view port."""
        return Rectangle(*self._qtree.soft_bounds)

    @property
    def hadjustment(self) -> Gtk.Adjustment:
        """Gtk adjustment object for use with a scrollbar."""
        return self._scrolling.hadjustment

    @property
    def vadjustment(self) -> Gtk.Adjustment:
        """Gtk adjustment object for use with a scrollbar."""
        return self._scrolling.vadjustment

    def clamp_item(self, item):
        """Update adjustments so the item is located inside the view port."""
        x, y, w, h = self._qtree.get_bounds(item)
        self.hadjustment.clamp_page(x, x + w)
        self.vadjustment.clamp_page(y, y + h)

    def add_controller(self, *controllers: Gtk.EventController) -> None:
        """Add a controller.

        A convenience method, so you have a place to store the event
        controllers. Events controllers are linked to a widget (in GTK3)
        on creation time, so calling this method is not necessary.
        """
        if Gtk.get_major_version() != 3:
            for controller in controllers:
                super().add_controller(controller)
        self._controllers.update(controllers)

    def remove_controller(self, controller: Gtk.EventController) -> bool:
        """Remove a controller.

        The event controller's propagation phase is set to
        `Gtk.PropagationPhase.NONE` to ensure it's not invoked
        anymore.

        NB. The controller is only really removed from the widget when it's destroyed!
            This is a Gtk3 limitation.
        """
        if Gtk.get_major_version() != 3:
            super().remove_controller(controller)
        if controller in self._controllers:
            controller.set_propagation_phase(Gtk.PropagationPhase.NONE)
            self._controllers.discard(controller)
            return True
        return False

    def remove_all_controllers(self) -> None:
        """Remove all registered controllers."""
        for controller in set(self._controllers):
            self.remove_controller(controller)

    def zoom(self, factor: float) -> None:
        """Zoom in/out by factor ``factor``."""
        assert self._model
        self.matrix.scale(factor, factor)
        self.request_update(self._model.get_all_items())

    def get_items_in_rectangle(self,
                               rect: Rect,
                               contain: bool = False) -> Iterable[Item]:
        """Return the items in the rectangle 'rect'.

        Items are automatically sorted in model's processing order.
        """
        assert self._model
        items = (self._qtree.find_inside(rect)
                 if contain else self._qtree.find_intersect(rect))
        return self._model.sort(items)

    def get_item_bounding_box(self, item: Item) -> Rectangle:
        """Get the bounding box for the item, in view coordinates."""
        return Rectangle(*self._qtree.get_bounds(item))

    def request_update(
            self,
            items: Iterable[Item],
            removed_items: Iterable[Item] = (),
    ) -> None:
        """Request update for items."""
        if items:
            self._dirty_items.update(items)

        if removed_items:
            selection = self._selection
            self._dirty_items.difference_update(removed_items)

            for item in removed_items:
                self._qtree.remove(item)
                selection.unselect_item(item)

        if items or removed_items:
            self.update()

    @g_async(single=True)
    def update(self) -> None:
        """Update view status according to the items updated in the model."""
        model = self._model
        if not model:
            return

        dirty_items = self.all_dirty_items()
        model.update_now(dirty_items)

        dirty_items |= self.all_dirty_items()
        self.update_bounding_box(dirty_items)

        allocation = self.get_allocation()
        self._scrolling.update_adjustments(allocation.width, allocation.height,
                                           self._qtree.soft_bounds)
        self.update_back_buffer()

    def all_dirty_items(self) -> Set[Item]:
        """Return all dirty items, clearing the marked items."""
        model = self._model
        if not model:
            return set()

        def iterate_items(items: Iterable[Item]) -> Iterable[Item]:
            assert model
            for item in items:
                parent = model.get_parent(item)
                if parent is not None and parent in items:
                    # item's matrix will be updated thanks to parent's matrix update
                    continue

                yield item

                yield from iterate_items(set(model.get_children(item)))

        dirty_items = set(iterate_items(self._dirty_items))
        self._dirty_items.clear()
        return dirty_items

    def update_bounding_box(self, items: Collection[Item]) -> None:
        """Update the bounding boxes of the model items for this view, in model
        coordinates."""
        painter = self._bounding_box_painter
        qtree = self._qtree
        c2v = self._matrix
        for item in items:
            surface = cairo.RecordingSurface(cairo.Content.COLOR_ALPHA,
                                             None)  # type: ignore[arg-type]
            cr = cairo.Context(surface)
            painter.paint_item(item, cr)
            x, y, w, h = surface.ink_extents()

            vx, vy = c2v.transform_point(x, y)
            vw, vh = c2v.transform_distance(w, h)

            qtree.add(item=item, bounds=(vx, vy, vw, vh), data=(x, y, w, h))

        if self._matrix_changed and self._model:
            for item in self._model.get_all_items():
                if item not in items:
                    bounds = self._qtree.get_data(item)
                    x, y = c2v.transform_point(bounds[0], bounds[1])
                    w, h = c2v.transform_distance(bounds[2], bounds[3])
                    qtree.add(item=item, bounds=(x, y, w, h), data=bounds)
            self._matrix_changed = False

    @g_async(single=True, priority=GLib.PRIORITY_HIGH_IDLE)
    def update_back_buffer(self) -> None:
        if Gtk.get_major_version() == 3:
            surface = self.get_window()
        else:
            surface = self.get_native() and self.get_native().get_surface()

        if self.model and surface:
            allocation = self.get_allocation()
            width = allocation.width
            height = allocation.height

            if not self._back_buffer or self._back_buffer_needs_resizing:
                self._back_buffer = surface.create_similar_surface(
                    cairo.Content.COLOR_ALPHA, width, height)
                self._back_buffer_needs_resizing = False

            assert self._back_buffer
            cr = cairo.Context(self._back_buffer)

            cr.save()
            cr.set_operator(cairo.OPERATOR_CLEAR)
            cr.paint()
            cr.restore()

            Gtk.render_background(self.get_style_context(), cr, 0, 0, width,
                                  height)

            items = self.get_items_in_rectangle((0, 0, width, height))

            cr.set_matrix(self.matrix.to_cairo())
            cr.save()
            self.painter.paint(list(items), cr)
            cr.restore()

            if DEBUG_DRAW_BOUNDING_BOX:
                for item in self.get_items_in_rectangle((0, 0, width, height)):
                    try:
                        b = self.get_item_bounding_box(item)
                    except KeyError:
                        pass  # No bounding box right now..
                    else:
                        cr.save()
                        cr.identity_matrix()
                        cr.set_source_rgb(0.8, 0, 0)
                        cr.set_line_width(1.0)
                        cr.rectangle(*b)
                        cr.stroke()
                        cr.restore()

            if DEBUG_DRAW_QUADTREE:

                def draw_qtree_bucket(bucket: QuadtreeBucket) -> None:
                    cr.rectangle(*bucket.bounds)
                    cr.stroke()
                    for b in bucket._buckets:
                        draw_qtree_bucket(b)

                cr.set_source_rgb(0, 0, 0.8)
                cr.set_line_width(1.0)
                draw_qtree_bucket(self._qtree._bucket)

            if Gtk.get_major_version() == 3:
                self.get_window().invalidate_rect(allocation, True)
            else:
                self.queue_draw()

    def do_realize(self) -> None:
        Gtk.DrawingArea.do_realize(self)

        if self._model:
            # Ensure updates are propagated
            self._model.register_view(self)
            self.request_update(self._model.get_all_items())

    def do_unrealize(self) -> None:
        if self._model:
            self._model.unregister_view(self)

        self._qtree.clear()

        self._dirty_items.clear()

        Gtk.DrawingArea.do_unrealize(self)

    def do_configure_event(self, event: Gdk.EventConfigure) -> bool:
        # GTK+ 3 only
        allocation = self.get_allocation()
        self.on_resize(allocation.width, allocation.height)

        return False

    def on_selection_update(self, item: Optional[Item]) -> None:
        if self._model:
            if item is None:
                self.request_update(self._model.get_all_items())
            elif item in self._model.get_all_items():
                self.request_update((item, ))

    def on_matrix_update(self, matrix, old_matrix_values):
        if not self._matrix_changed:
            self._matrix_changed = True
            self.update()

    def on_resize(self, width: int, height: int) -> None:
        self._qtree.resize((0, 0, width, height))
        self._scrolling.update_adjustments(width, height,
                                           self._qtree.soft_bounds)
        if self.get_realized():
            self._back_buffer_needs_resizing = True
            self.update_back_buffer()
        else:
            self._back_buffer = None

    def do_draw(self,
                cr: cairo.Context,
                width: int = 0,
                height: int = 0) -> bool:
        if not self._model:
            return False

        if not self._back_buffer:
            return False

        cr.set_source_surface(self._back_buffer, 0, 0)
        cr.paint()

        return False
Example #16
0
 def __init__(self, **kwargs: object) -> None:
     super().__init__(**kwargs)  # type: ignore[call-arg]
     self._matrix = Matrix()
     self._matrix_i2c = Matrix()