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))
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
def _set_matrix(self, matrix): """ Set the conversion matrix (parent -> item) """ if not isinstance(matrix, Matrix): matrix = Matrix(*matrix) self._matrix = matrix
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}")
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)
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()
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()
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
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]
def test_multiply_equals_should_result_in_same_matrix(): m1 = Matrix() m2 = m1 m2 *= Matrix(20, 20) assert m1 is m2
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
def __init__(self, **kwargs: object) -> None: super().__init__(**kwargs) # type: ignore[call-arg] self._matrix = Matrix() self._matrix_i2c = Matrix()