class DecisionNodeItem(ElementPresentation, ActivityNodeItem): """Representation of decision or merge node.""" def __init__(self, id=None, model=None): super().__init__(id, model) no_movable_handles(self) self._combined = None self.shape = IconBox( Box(style={"min-width": 20, "min-height": 30}, draw=draw_decision_node), # Text should be left-top Text(text=lambda: stereotypes_str(self.subject),), EditableText(text=lambda: self.subject and self.subject.name or ""), ) self.watch("subject[NamedElement].name") self.watch("subject.appliedStereotype.classifier.name") def save(self, save_func): if self._combined: save_func("combined", self._combined) super().save(save_func) def load(self, name, value): if name == "combined": self._combined = value else: super().load(name, value) @observed def _set_combined(self, value): self._combined = value combined = reversible_property(lambda s: s._combined, _set_combined)
class Port(object): """Port connectable part of an item. The Item's handle connects to a port. """ def __init__(self): super(Port, self).__init__() self._connectable = True @observed def _set_connectable(self, connectable): self._connectable = connectable connectable = reversible_property(lambda s: s._connectable, _set_connectable) def glue(self, pos): """ Get glue point on the port and distance to the port. """ raise NotImplemented("Glue method not implemented") def constraint(self, canvas, item, handle, glue_item): """ Create connection constraint between item's handle and glue item. """ raise NotImplemented("Constraint method not implemented")
class ObjectNodeItem(ElementPresentation, Named): """ Representation of object node. Object node is ordered and has upper bound specification. Ordering information can be hidden by user. """ def __init__(self, id=None, model=None): super().__init__(id, model) self._show_ordering = False self.shape = IconBox( Box( Text(text=lambda: stereotypes_str(self.subject), ), EditableText(text=lambda: self.subject.name or ""), style={ "min-width": 50, "min-height": 30, "padding": (5, 10, 5, 10) }, draw=draw_border, ), Text(text=lambda: self.subject.upperBound not in (None, DEFAULT_UPPER_BOUND ) and f"{{ upperBound = {self.subject.upperBound} }}", ), Text(text=lambda: self._show_ordering and self.subject.ordering and f"{{ ordering = {self.subject.ordering} }}", ), ) self.watch("subject[NamedElement].name") self.watch("subject.appliedStereotype.classifier.name") self.watch("subject[ObjectNode].upperBound") self.watch("subject[ObjectNode].ordering") @observed def _set_show_ordering(self, value): self._show_ordering = value self.request_update() show_ordering = reversible_property(lambda s: s._show_ordering, _set_show_ordering) def save(self, save_func): save_func("show-ordering", self._show_ordering) super().save(save_func) def load(self, name, value): if name == "show-ordering": self._show_ordering = ast.literal_eval(value) else: super().load(name, value)
class DecisionNodeItem(ActivityNodeItem): """ Representation of decision or merge node. """ __uml__ = uml2.DecisionNode __style__ = { 'min-size': (20, 30), 'name-align': (ALIGN_LEFT, ALIGN_TOP), } RADIUS = 15 def __init__(self, id=None): ActivityNodeItem.__init__(self, id) self._combined = None #self.set_prop_persistent('combined') def save(self, save_func): if self._combined: save_func('combined', self._combined, reference=True) super(DecisionNodeItem, self).save(save_func) def load(self, name, value): if name == 'combined': self._combined = value else: super(DecisionNodeItem, self).load(name, value) @observed def _set_combined(self, value): #self.preserve_property('combined') self._combined = value combined = reversible_property(lambda s: s._combined, _set_combined) def draw(self, context): """ Draw diamond shape, which represents decision and merge nodes. """ cr = context.cairo r = self.RADIUS r2 = r * 2 / 3 cr.move_to(r2, 0) cr.line_to(r2 * 2, r) cr.line_to(r2, r * 2) cr.line_to(0, r) cr.close_path() cr.stroke() super(DecisionNodeItem, self).draw(context)
class ForkNodeItem(UML.Presentation, Item): """ Representation of fork and join node. """ def __init__(self, id=None, model=None): super().__init__(id, model) h1, h2 = Handle(), Handle() self._handles.append(h1) self._handles.append(h2) self._ports.append(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), style={"min-width": 0, "min-height": 0}, ), 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 "", style={"min-width": 0, "min-height": 0}, ), ) self.watch("subject[NamedElement].name") self.watch("subject.appliedStereotype.classifier.name") self.watch("subject[JoinNode].joinSpec") def save(self, save_func): save_func("matrix", tuple(self.matrix)) save_func("height", float(self._handles[1].pos.y)) if self._combined: save_func("combined", self._combined, reference=True) super().save(save_func) def load(self, name, value): if name == "matrix": self.matrix = ast.literal_eval(value) elif name == "height": self._handles[1].pos.y = ast.literal_eval(value) elif name == "combined": self._combined = value else: # DiagramItem.load(self, name, value) super().load(name, value) @observed def _set_combined(self, value): # self.preserve_property('combined') self._combined = value combined = reversible_property(lambda s: s._combined, _set_combined) def setup_canvas(self): assert self.canvas super().setup_canvas() h1, h2 = self._handles cadd = self.canvas.solver.add_constraint c1 = EqualsConstraint(a=h1.pos.x, b=h2.pos.x) c2 = LessThanConstraint(smaller=h1.pos.y, bigger=h2.pos.y, delta=30) self.__constraints = (cadd(c1), cadd(c2)) list(map(self.canvas.solver.add_constraint, self.__constraints)) def teardown_canvas(self): assert self.canvas super().teardown_canvas() list(map(self.canvas.solver.remove_constraint, self.__constraints)) def pre_update(self, context): cr = context.cairo _, h2 = self.handles() _, height = self.shape.size(cr) h2.pos.y = max(h2.pos.y, height) def draw(self, context): h1, h2 = self.handles() height = h2.pos.y - h1.pos.y self.shape.draw(context, Rectangle(0, 0, 1, height)) def draw_fork_node(self, _box, context, _bounding_box): """ Draw vertical line - symbol of fork and join nodes. Join specification is also drawn above the item. """ cr = context.cairo cr.set_line_width(6) h1, h2 = self._handles cr.move_to(h1.pos.x, h1.pos.y) cr.line_to(h2.pos.x, h2.pos.y) cr.stroke() def point(self, pos): h1, h2 = self._handles d, p = distance_line_point(h1.pos, h2.pos, pos) # Substract line_width / 2 return d - 3
class InterfaceItem(ClassItem): """ Interface item supporting class box, folded notations and assembly connector icon mode. When in folded mode, provided (ball) notation is used by default. """ __uml__ = UML.Interface __stereotype__ = { 'interface': lambda self: self.drawing_style != self.DRAW_ICON } __style__ = { 'icon-size': (20, 20), 'icon-size-provided': (20, 20), 'icon-size-required': (28, 28), 'name-outside': False, } UNFOLDED_STYLE = { 'text-align': (ALIGN_CENTER, ALIGN_TOP), 'text-outside': False, } FOLDED_STYLE = { 'text-align': (ALIGN_CENTER, ALIGN_BOTTOM), 'text-outside': True, } RADIUS_PROVIDED = 10 RADIUS_REQUIRED = 14 # Non-folded mode. FOLDED_NONE = 0 # Folded mode, provided (ball) notation. FOLDED_PROVIDED = 1 # Folded mode, required (socket) notation. FOLDED_REQUIRED = 2 # Folded mode, notation of assembly connector icon mode (ball&socket). FOLDED_ASSEMBLY = 3 def __init__(self, id=None): ClassItem.__init__(self, id) self._folded = self.FOLDED_NONE self._angle = 0 old_f = self._name.is_visible self._name.is_visible = lambda: old_f( ) and self._folded != self.FOLDED_ASSEMBLY handles = self._handles h_nw = handles[NW] h_ne = handles[NE] h_sw = handles[SW] h_se = handles[SE] # edge of element define default element ports self._ports = [ InterfacePort(h_nw.pos, h_ne.pos, self, 0), InterfacePort(h_ne.pos, h_se.pos, self, old_div(pi, 2)), InterfacePort(h_se.pos, h_sw.pos, self, pi), InterfacePort(h_sw.pos, h_nw.pos, self, pi * 1.5) ] self.watch('subject<Interface>.ownedAttribute', self.on_class_owned_attribute) \ .watch('subject<Interface>.ownedOperation', self.on_class_owned_operation) \ .watch('subject<Interface>.supplierDependency') @observed def set_drawing_style(self, style): """ In addition to setting the drawing style, the handles are make non-movable if the icon (folded) style is used. """ super(InterfaceItem, self).set_drawing_style(style) if self._drawing_style == self.DRAW_ICON: self.folded = self.FOLDED_PROVIDED # set default folded mode else: self.folded = self.FOLDED_NONE # unset default folded mode drawing_style = reversible_property(lambda self: self._drawing_style, set_drawing_style) def _is_folded(self): """ Check if interface item is folded interface item. """ return self._folded def _set_folded(self, folded): """ Set folded notation. :param folded: Folded state, see FOLDED_* constants. """ self._folded = folded if folded == self.FOLDED_NONE: movable = True draw_mode = self.DRAW_COMPARTMENT name_style = self.UNFOLDED_STYLE else: if self._folded == self.FOLDED_PROVIDED: icon_size = self.style.icon_size_provided else: # required interface or assembly icon mode icon_size = self.style.icon_size_required self.style.icon_size = icon_size self.min_width, self.min_height = icon_size self.width, self.height = icon_size # update only h_se handle - rest of handles should be updated by # constraints h_nw = self._handles[NW] h_se = self._handles[SE] h_se.pos.x = h_nw.pos.x + self.min_width h_se.pos.y = h_nw.pos.y + self.min_height movable = False draw_mode = self.DRAW_ICON name_style = self.FOLDED_STYLE # call super method to avoid recursion (set_drawing_style calls # _set_folded method) super(InterfaceItem, self).set_drawing_style(draw_mode) self._name.style.update(name_style) for h in self._handles: h.movable = movable self.request_update() folded = property( _is_folded, _set_folded, doc="Check or set folded notation, see FOLDED_* constants.") def draw_icon(self, context): cr = context.cairo h_nw = self._handles[NW] cx, cy = h_nw.pos.x + old_div(self.width, 2), h_nw.pos.y + old_div( self.height, 2) required = self._folded == self.FOLDED_REQUIRED or self._folded == self.FOLDED_ASSEMBLY provided = self._folded == self.FOLDED_PROVIDED or self._folded == self.FOLDED_ASSEMBLY if required: cr.save() cr.arc_negative(cx, cy, self.RADIUS_REQUIRED, self._angle, pi + self._angle) cr.restore() if provided: cr.move_to(cx + self.RADIUS_PROVIDED, cy) cr.arc(cx, cy, self.RADIUS_PROVIDED, 0, pi * 2) cr.stroke() super(InterfaceItem, self).draw(context)
class CompartmentItem(NamedItem): """ Abstract class for visualization of named items and classifiers, which have compartments, i.e. classes, interfaces, components, states. Compartment item has ability to display stereotypes attributes. They are displayed in separate compartments (one per stereotype). Compartment item has three drawing styles (changed with `ClassifierItem.drawing_style` property) - the comparttment view - often used by classes - a compartment view, but with a little stereotype icon in the right corner - an icon - used by actor and interface items Methods pre_update/post_update/draw are defined to support drawing styles. Appropriate methods are called depending on drawing style. """ # Do not use preset drawing style DRAW_NONE = 0 # Draw the famous box style DRAW_COMPARTMENT = 1 # Draw compartment with little icon in upper right corner DRAW_COMPARTMENT_ICON = 2 # Draw as icon DRAW_ICON = 3 __style__ = { 'min-size': (100, 50), 'icon-size': (20, 20), 'feature-font': 'sans 10', 'from-padding': (7, 2, 7, 2), 'compartment-padding': (5, 5, 5, 5), # (top, right, bottom, left) 'compartment-vspacing': 0, 'name-padding': (10, 10, 10, 10), 'stereotype-padding': (10, 10, 2, 10), # extra space can be used by header or a compartment; # we don't want to consume the extra space by compartments, which # contain stereotype information 'extra-space': 'header', # 'header' or 'compartment' } # Default size for small icons ICON_WIDTH = 15 ICON_HEIGHT = 25 ICON_MARGIN_X = 10 ICON_MARGIN_Y = 10 def __init__(self, id=None): NamedItem.__init__(self, id) self._compartments = [] self._drawing_style = CompartmentItem.DRAW_NONE self.watch('subject.appliedStereotype', self.on_stereotype_change) \ .watch('subject.appliedStereotype.slot', self.on_stereotype_attr_change) \ .watch('subject.appliedStereotype.slot.definingFeature.name') \ .watch('subject.appliedStereotype.slot.value') self._extra_space = 0 def on_stereotype_change(self, event): if self._show_stereotypes_attrs: if isinstance(event, event.AssociationAddEvent): self._create_stereotype_compartment(event.new_value) elif isinstance(event, event.AssociationDeleteEvent): self._remove_stereotype_compartment(event.old_value) def _find_stereotype_compartment(self, obj): for comp in self._compartments: if comp.id is obj: return comp def on_stereotype_attr_change(self, event): if event and self.subject \ and event.element in self.subject.appliedStereotype \ and self._show_stereotypes_attrs: comp = self._find_stereotype_compartment(event.element) if comp is None: log.debug('No compartment found for %s' % event.element) return if isinstance( event, (event.AssociationAddEvent, event.AssociationDeleteEvent)): self._update_stereotype_compartment(comp, event.element) self.request_update() def _create_stereotype_compartment(self, obj): st = obj.classifier[0].name c = Compartment(st, self, obj) c.title = modelfactory.STEREOTYPE_FMT % st self._update_stereotype_compartment(c, obj) self._compartments.append(c) self.request_update() def _remove_stereotype_compartment(self, obj): comp = self._find_stereotype_compartment(obj) if comp is not None: self._compartments.remove(comp) self.request_update() def _update_stereotype_compartment(self, comp, obj): del comp[:] for slot in obj.slot: item = FeatureItem() item.subject = slot comp.append(item) comp.visible = len(obj.slot) > 0 def update_stereotypes_attrs(self): """ Display or hide stereotypes attributes. New compartment is created for every stereotype having attributes redefined. """ # remove all stereotype compartments first for comp in self._compartments: if isinstance(comp.id, uml2.InstanceSpecification): self._compartments.remove(comp) if self._show_stereotypes_attrs: for obj in self.subject.appliedStereotype: self._create_stereotype_compartment(obj) log.debug('Showing stereotypes attributes enabled') else: log.debug('Showing stereotypes attributes disabled') def save(self, save_func): # Store the show- properties *before* the width/height properties, # otherwise the classes will unintentionally grow due to "visible" # attributes or operations. self.save_property(save_func, 'drawing-style') NamedItem.save(self, save_func) @observed def set_drawing_style(self, style): """ Set the drawing style for this classifier: DRAW_COMPARTMENT, DRAW_COMPARTMENT_ICON or DRAW_ICON. """ if style != self._drawing_style: self._drawing_style = style self.request_update() # if self.canvas: # request_resolve = self.canvas.solver.request_resolve # for h in self._handles: # request_resolve(h.x) # request_resolve(h.y) if self._drawing_style == self.DRAW_COMPARTMENT: self.draw = self.draw_compartment self.pre_update = self.pre_update_compartment self.post_update = self.post_update_compartment elif self._drawing_style == self.DRAW_COMPARTMENT_ICON: self.draw = self.draw_compartment_icon self.pre_update = self.pre_update_compartment_icon self.post_update = self.post_update_compartment_icon elif self._drawing_style == self.DRAW_ICON: self.draw = self.draw_icon self.pre_update = self.pre_update_icon self.post_update = self.post_update_icon drawing_style = reversible_property(lambda self: self._drawing_style, set_drawing_style) def create_compartment(self, name): """ Create a new compartment. Compartments contain data such as attributes and operations. It is common to create compartments during the construction of the diagram item. Their visibility can be toggled by Compartment.visible. """ c = Compartment(name, self) self._compartments.append(c) return c compartments = property(lambda s: s._compartments) def sync_uml_elements(self, elements, compartment, creator=None): """ This method synchronized a list of elements with the items in a compartment. A creator-function should be passed which is used for creating new compartment items. @elements: the list of attributes or operations in the model @compartment: our local representation @creator: factory method for creating new attr. or oper.'s """ # extract the UML elements from the compartment local_elements = [f.subject for f in compartment] # map local element with compartment element mapping = dict(list(zip(local_elements, compartment))) to_add = [el for el in elements if el not in local_elements] # sync local elements with elements del compartment[:] for el in elements: if el in to_add: creator(el) else: compartment.append(mapping[el]) #log.debug('elements order in model: %s' % [f.name for f in elements]) #log.debug('elements order in diagram: %s' % [f.subject.name for f in compartment]) assert tuple([f.subject for f in compartment]) == tuple(elements) self.request_update() def pre_update_compartment_icon(self, context): self.pre_update_compartment(context) # icon width plus right margin self.min_width = max(self.min_width, self._header_size[0] + self.ICON_WIDTH + 10) def pre_update_icon(self, context): super(CompartmentItem, self).pre_update(context) def pre_update_compartment(self, context): """ Update state for box-style presentation. Calculate minimal size, which is based on header and comparments sizes. """ super(CompartmentItem, self).pre_update(context) for comp in self._compartments: comp.pre_update(context) sizes = [ comp.get_size() for comp in self._compartments if comp.visible ] sizes.append((self.min_width, self._header_size[1])) self.min_width = max(size[0] for size in sizes) h = sum(size[1] for size in sizes) self.min_height = max(self.style.min_size[1], h) def post_update_compartment_icon(self, context): """ Update state for box-style w/ small icon. """ super(CompartmentItem, self).post_update(context) def post_update_icon(self, context): """ Update state for icon-only presentation. """ super(CompartmentItem, self).post_update(context) def post_update_compartment(self, context): super(CompartmentItem, self).post_update(context) assert abs(self.width - self.min_width) >= 0, 'failed %s >= %s' % ( self.width, self.min_width) assert abs(self.height - self.min_height) >= 0, 'failed %s >= %s' % ( self.height, self.min_height) def get_icon_pos(self): """ Get icon position. """ return self.width - self.ICON_MARGIN_X - self.ICON_WIDTH, \ self.ICON_MARGIN_Y def draw_compartment_border(self, context): """ Standard classifier border is a rectangle. """ cr = context.cairo cr.rectangle(0, 0, self.width, self.height) self.fill_background(context) cr.stroke() def draw_compartment(self, context): self.draw_compartment_border(context) super(CompartmentItem, self).draw(context) cr = context.cairo # make room for name, stereotype, etc. y = self._header_size[1] cr.translate(0, y) if self._drawing_style == self.DRAW_COMPARTMENT_ICON: width = self.width - self.ICON_WIDTH else: width = self.width extra_space = self.height - self.min_height # extra space is used by header if self.style.extra_space == 'header': cr.translate(0, extra_space) # draw compartments and stereotype compartments extra_used = False for comp in self._compartments: if not comp.visible: continue cr.save() cr.move_to(0, 0) cr.line_to(self.width, 0) cr.stroke() try: comp.draw(context) finally: cr.restore() d = comp.height if not extra_used and comp.use_extra_space \ and self.style.extra_space == 'compartment': d += extra_space extra_used = True cr.translate(0, d) # if extra space is used by last compartment, then do nothing def item_at(self, x, y): """ Find the composite item (attribute or operation) for the classifier. """ if self.drawing_style not in (self.DRAW_COMPARTMENT, self.DRAW_COMPARTMENT_ICON): return self header_height = self._header_size[1] compartments = [comp for comp in self.compartments if comp.visible] # Edit is in name compartment -> edit name if y < header_height or not len(compartments): return self padding = self.style.compartment_padding vspacing = self.style.compartment_vspacing # place offset at top of first comparement y -= header_height y += vspacing / 2.0 for comp in compartments: item = comp.item_at(x, y) if item: return item y -= comp.height return None
class StereotypeSupport(object): """ Support for stereotypes for every diagram item. """ STEREOTYPE_ALIGN = { 'text-align': (ALIGN_CENTER, ALIGN_TOP), 'text-padding': (5, 10, 2, 10), 'text-outside': False, 'text-align-group': 'stereotype', 'line-width': 2, } def __init__(self): self._stereotype = self.add_text('stereotype', style=self.STEREOTYPE_ALIGN, visible=lambda: self._stereotype.text) self._show_stereotypes_attrs = False @observed def _set_show_stereotypes_attrs(self, value): self._show_stereotypes_attrs = value self.update_stereotypes_attrs() show_stereotypes_attrs = reversible_property( fget=lambda s: s._show_stereotypes_attrs, fset=_set_show_stereotypes_attrs, doc=""" Diagram item should show stereotypes attributes when property is set to True. When changed, method `update_stereotypes_attrs` is called. """) def update_stereotypes_attrs(self): """ Update display of stereotypes attributes. The method does nothing at the moment. In the future it should probably display stereotypes attributes under stereotypes header. Abstract class for classifiers overrides this method to display stereotypes attributes in compartments. """ pass def set_stereotype(self, text=None): """ Set the stereotype text for the diagram item. Note, that text is not Stereotype object. @arg text: stereotype text """ self._stereotype.text = text self.request_update() stereotype = property(lambda s: s._stereotype, set_stereotype) def update_stereotype(self): """ Update the stereotype definitions (text) of this item. Note, that this method is also called from ExtensionItem.confirm_connect_handle method. """ # by default no stereotype, however check for __stereotype__ # attribute to assign some static stereotype see interfaces, # use case relationships, package or class for examples stereotype = getattr(self, '__stereotype__', ()) if stereotype: stereotype = self.parse_stereotype(stereotype) # Phew! :] :P stereotype = UML.model.stereotypes_str(self.subject, stereotype) self.set_stereotype(stereotype) def parse_stereotype(self, data): if isinstance(data, str): # return data as stereotype if it is a string return (data, ) subject = self.subject for stereotype, condition in list(data.items()): if isinstance(condition, tuple): cls, predicate = condition elif isinstance(condition, type): cls = condition predicate = None elif callable(condition): cls = None predicate = condition else: assert False, 'wrong conditional %s' % condition ok = True if cls: ok = isinstance(subject, cls) #isinstance(subject, cls) if predicate: ok = predicate(self) if ok: return (stereotype, ) return ()
class AssociationItem(LinePresentation, Named): """ AssociationItem represents associations. An AssociationItem has two AssociationEnd items. Each AssociationEnd item represents a Property (with Property.association == my association). """ def __init__(self, id=None, model=None): super().__init__(id, model) # AssociationEnds are really inseperable from the AssociationItem. # We give them the same id as the association item. self._head_end = AssociationEnd(owner=self, end="head") self._tail_end = AssociationEnd(owner=self, end="tail") # Direction depends on the ends that hold the ownedEnd attributes. self._show_direction = False self._dir_angle = 0 self._dir_pos = 0, 0 self.shape_middle = Box( Text( text=lambda: stereotypes_str(self.subject), style={ "min-width": 0, "min-height": 0 }, ), EditableText(text=lambda: self.subject.name or ""), ) # For the association ends: base = "subject[Association].memberEnd[Property]" self.watch("subject[NamedElement].name").watch( "subject.appliedStereotype.classifier.name" ).watch(f"{base}.name", self.on_association_end_value).watch( f"{base}.aggregation", self.on_association_end_value ).watch(f"{base}.classifier", self.on_association_end_value).watch( f"{base}.visibility", self.on_association_end_value).watch( f"{base}.lowerValue", self.on_association_end_value).watch( f"{base}.upperValue", self.on_association_end_value).watch( f"{base}.owningAssociation", self.on_association_end_value).watch( f"{base}.type[Class].ownedAttribute", self.on_association_end_value).watch( f"{base}.type[Interface].ownedAttribute", self.on_association_end_value).watch( f"{base}.appliedStereotype.classifier", self.on_association_end_value ).watch("subject[Association].ownedEnd").watch( "subject[Association].navigableOwnedEnd") def set_show_direction(self, dir): self._show_direction = dir self.request_update() show_direction = reversible_property(lambda s: s._show_direction, set_show_direction) def save(self, save_func): super().save(save_func) save_func("show-direction", self._show_direction) if self._head_end.subject: save_func("head-subject", self._head_end.subject) if self._tail_end.subject: save_func("tail-subject", self._tail_end.subject) def load(self, name, value): # end_head and end_tail were used in an older Gaphor version if name in ("head_end", "head_subject", "head-subject"): self._head_end.subject = value elif name in ("tail_end", "tail_subject", "tail-subject"): self._tail_end.subject = value elif name == "show-direction": self._show_direction = ast.literal_eval(value) else: super().load(name, value) def postload(self): super().postload() self._head_end.set_text() self._tail_end.set_text() head_end = property(lambda self: self._head_end) tail_end = property(lambda self: self._tail_end) def unlink(self): self._head_end.unlink() self._tail_end.unlink() super().unlink() def invert_direction(self): """ Invert the direction of the association, this is done by swapping the head and tail-ends subjects. """ if not self.subject: return self.subject.memberEnd.swap(self.subject.memberEnd[0], self.subject.memberEnd[1]) self.request_update() def on_named_element_name(self, event): """ Update names of the association as well as its ends. """ if event is None: super().on_named_element_name(event) self.on_association_end_value(event) elif event.element is self.subject: super().on_named_element_name(event) else: self.on_association_end_value(event) def on_association_end_value(self, event): """ Handle events and update text on association end. """ for end in (self._head_end, self._tail_end): end.set_text() self.request_update() def post_update(self, context): """ Update the shapes and sub-items of the association. """ handles = self.handles() # Update line endings: head_subject = self._head_end.subject tail_subject = self._tail_end.subject # Update line ends using the aggregation and isNavigable values: if head_subject and tail_subject: if tail_subject.aggregation == "composite": self.draw_head = draw_head_composite elif tail_subject.aggregation == "shared": self.draw_head = draw_head_shared elif self._head_end.subject.navigability is True: self.draw_head = draw_head_navigable elif self._head_end.subject.navigability is False: self.draw_head = draw_head_none else: self.draw_head = draw_default_head if head_subject.aggregation == "composite": self.draw_tail = draw_tail_composite elif head_subject.aggregation == "shared": self.draw_tail = draw_tail_shared elif self._tail_end.subject.navigability is True: self.draw_tail = draw_tail_navigable elif self._tail_end.subject.navigability is False: self.draw_tail = draw_tail_none else: self.draw_tail = draw_default_tail if self._show_direction: inverted = self.tail_end.subject is self.subject.memberEnd[0] pos, angle = get_center_pos(self.handles(), inverted) self._dir_pos = pos self._dir_angle = angle else: self.draw_head = draw_default_head self.draw_tail = draw_default_tail # update relationship after self.set calls to avoid circural updates super().post_update(context) # Calculate alignment of the head name and multiplicity self._head_end.post_update(context, handles[0].pos, handles[1].pos) # Calculate alignment of the tail name and multiplicity self._tail_end.post_update(context, handles[-1].pos, handles[-2].pos) def point(self, pos): """ Returns the distance from the Association to the (mouse) cursor. """ return min(super().point(pos), self._head_end.point(pos), self._tail_end.point(pos)) def draw(self, context): super().draw(context) cr = context.cairo self._head_end.draw(context) self._tail_end.draw(context) if self._show_direction: cr.save() try: cr.translate(*self._dir_pos) cr.rotate(self._dir_angle) cr.move_to(0, 0) cr.line_to(6, 5) cr.line_to(0, 10) cr.fill() finally: cr.restore() def item_at(self, x, y): if distance_point_point_fast(self._handles[0].pos, (x, y)) < 10: return self._head_end elif distance_point_point_fast(self._handles[-1].pos, (x, y)) < 10: return self._tail_end return self
class ObjectNodeItem(NamedItem): """ Representation of object node. Object node is ordered and has upper bound specification. Ordering information can be hidden by user. """ element_factory = inject('element_factory') __uml__ = UML.ObjectNode STYLE_BOTTOM = { 'text-align': (ALIGN_CENTER, ALIGN_BOTTOM), 'text-outside': True, 'text-align-group': 'bottom', } def __init__(self, id = None): NamedItem.__init__(self, id) self._show_ordering = False self._upper_bound = self.add_text('upperBound', pattern='{ upperBound = %s }', style=self.STYLE_BOTTOM, visible=self.is_upper_bound_visible) self._ordering = self.add_text('ordering', pattern = '{ ordering = %s }', style = self.STYLE_BOTTOM, visible=self._get_show_ordering) self.watch('subject<ObjectNode>.upperBound', self.on_object_node_upper_bound)\ .watch('subject<ObjectNode>.ordering', self.on_object_node_ordering) def on_object_node_ordering(self, event): if self.subject: self._ordering.text = self.subject.ordering self.request_update() def on_object_node_upper_bound(self, event): subject = self.subject if subject and subject.upperBound: self._upper_bound.text = subject.upperBound self.request_update() def is_upper_bound_visible(self): """ Do not show upper bound, when it's set to default value. """ subject = self.subject return subject and subject.upperBound != DEFAULT_UPPER_BOUND @observed def _set_show_ordering(self, value): self._show_ordering = value self.request_update() def _get_show_ordering(self): return self._show_ordering show_ordering = reversible_property(_get_show_ordering, _set_show_ordering) def save(self, save_func): save_func('show-ordering', self._show_ordering) super(ObjectNodeItem, self).save(save_func) def load(self, name, value): if name == 'show-ordering': self._show_ordering = eval(value) else: super(ObjectNodeItem, self).load(name, value) def postload(self): if self.subject and self.subject.upperBound: self._upper_bound.text = self.subject.upperBound if self.subject and self._show_ordering: self.set_ordering(self.subject.ordering) super(ObjectNodeItem, self).postload() def draw(self, context): cr = context.cairo cr.rectangle(0, 0, self.width, self.height) cr.stroke() super(ObjectNodeItem, self).draw(context) def set_upper_bound(self, value): """ Set upper bound value of object node. """ subject = self.subject if subject: if not value: value = DEFAULT_UPPER_BOUND subject.upperBound = value #self._upper_bound.text = value def set_ordering(self, value): """ Set object node ordering value. """ subject = self.subject subject.ordering = value self._ordering.text = value
class ForkNodeItem(Item, DiagramItem): """ Representation of fork and join node. """ element_factory = inject('element_factory') __uml__ = uml2.ForkNode __style__ = { 'min-size': (6, 45), 'name-align': (ALIGN_CENTER, ALIGN_BOTTOM), 'name-padding': (2, 2, 2, 2), 'name-outside': True, 'name-align-str': None, } STYLE_TOP = { 'text-align': (ALIGN_CENTER, ALIGN_TOP), 'text-outside': True, } def __init__(self, id=None): Item.__init__(self) DiagramItem.__init__(self, id) h1, h2 = Handle(), Handle() self._handles.append(h1) self._handles.append(h2) self._ports.append(LinePort(h1.pos, h2.pos)) self._combined = None self._join_spec = self.add_text('joinSpec', pattern='{ joinSpec = %s }', style=self.STYLE_TOP, visible=self.is_join_spec_visible) self._name = self.add_text('name', style={ 'text-align': self.style.name_align, 'text-padding': self.style.name_padding, 'text-outside': self.style.name_outside, 'text-align-str': self.style.name_align_str, 'text-align-group': 'stereotype', }, editable=True) self.watch('subject<NamedElement>.name', self.on_named_element_name)\ .watch('subject<JoinNode>.joinSpec', self.on_join_node_join_spec) def save(self, save_func): save_func('matrix', tuple(self.matrix)) save_func('height', float(self._handles[1].pos.y)) if self._combined: save_func('combined', self._combined, reference=True) DiagramItem.save(self, save_func) def load(self, name, value): if name == 'matrix': self.matrix = eval(value) elif name == 'height': self._handles[1].pos.y = eval(value) elif name == 'combined': self._combined = value else: #DiagramItem.load(self, name, value) super(ForkNodeItem, self).load(name, value) def postload(self): subject = self.subject if subject and isinstance(subject, uml2.JoinNode) and subject.joinSpec: self._join_spec.text = self.subject.joinSpec self.on_named_element_name(None) super(ForkNodeItem, self).postload() @observed def _set_combined(self, value): #self.preserve_property('combined') self._combined = value combined = reversible_property(lambda s: s._combined, _set_combined) def setup_canvas(self): super(ForkNodeItem, self).setup_canvas() self.register_handlers() h1, h2 = self._handles cadd = self.canvas.solver.add_constraint c1 = EqualsConstraint(a=h1.pos.x, b=h2.pos.x) c2 = LessThanConstraint(smaller=h1.pos.y, bigger=h2.pos.y, delta=30) self.__constraints = (cadd(c1), cadd(c2)) list(map(self.canvas.solver.add_constraint, self.__constraints)) def teardown_canvas(self): super(ForkNodeItem, self).teardown_canvas() list(map(self.canvas.solver.remove_constraint, self.__constraints)) self.unregister_handlers() def is_join_spec_visible(self): """ Check if join specification should be displayed. """ return isinstance(self.subject, uml2.JoinNode) \ and self.subject.joinSpec is not None \ and self.subject.joinSpec != DEFAULT_JOIN_SPEC def text_align(self, extents, align, padding, outside): h1, h2 = self._handles w, _ = self.style.min_size h = h2.pos.y - h1.pos.y x, y = get_text_point(extents, w, h, align, padding, outside) return x, y def pre_update(self, context): self.update_stereotype() Item.pre_update(self, context) DiagramItem.pre_update(self, context) def post_update(self, context): Item.post_update(self, context) DiagramItem.post_update(self, context) def draw(self, context): """ Draw vertical line - symbol of fork and join nodes. Join specification is also drawn above the item. """ Item.draw(self, context) DiagramItem.draw(self, context) cr = context.cairo cr.set_line_width(6) h1, h2 = self._handles cr.move_to(h1.pos.x, h1.pos.y) cr.line_to(h2.pos.x, h2.pos.y) cr.stroke() def point(self, pos): h1, h2 = self._handles d, p = distance_line_point(h1.pos, h2.pos, pos) # Substract line_width / 2 return d - 3 def on_named_element_name(self, event): print('on_named_element_name', self.subject) subject = self.subject if subject: self._name.text = subject.name self.request_update() def on_join_node_join_spec(self, event): subject = self.subject if subject: self._join_spec.text = subject.joinSpec or DEFAULT_JOIN_SPEC self.request_update()
class AssociationItem(NamedLine): """ AssociationItem represents associations. An AssociationItem has two AssociationEnd items. Each AssociationEnd item represents a Property (with Property.association == my association). """ __uml__ = UML.Association def __init__(self, id=None): NamedLine.__init__(self, id) # AssociationEnds are really inseperable from the AssociationItem. # We give them the same id as the association item. self._head_end = AssociationEnd(owner=self, end="head") self._tail_end = AssociationEnd(owner=self, end="tail") # Direction depends on the ends that hold the ownedEnd attributes. self._show_direction = False self._dir_angle = 0 self._dir_pos = 0, 0 #self.watch('subject<Association>.ownedEnd')\ #.watch('subject<Association>.memberEnd') # For the association ends: base = 'subject<Association>.memberEnd<Property>.' self.watch(base + 'name', self.on_association_end_value)\ .watch(base + 'aggregation', self.on_association_end_value)\ .watch(base + 'classifier', self.on_association_end_value)\ .watch(base + 'visibility', self.on_association_end_value)\ .watch(base + 'lowerValue', self.on_association_end_value)\ .watch(base + 'upperValue', self.on_association_end_value)\ .watch(base + 'owningAssociation', self.on_association_end_value) \ .watch(base + 'type<Class>.ownedAttribute', self.on_association_end_value) \ .watch(base + 'type<Interface>.ownedAttribute', self.on_association_end_value) \ .watch('subject<Association>.ownedEnd') \ .watch('subject<Association>.navigableOwnedEnd') def set_show_direction(self, dir): self._show_direction = dir self.request_update() show_direction = reversible_property(lambda s: s._show_direction, set_show_direction) def setup_canvas(self): super(AssociationItem, self).setup_canvas() def teardown_canvas(self): super(AssociationItem, self).teardown_canvas() def save(self, save_func): NamedLine.save(self, save_func) save_func('show-direction', self._show_direction) if self._head_end.subject: save_func('head-subject', self._head_end.subject) if self._tail_end.subject: save_func('tail-subject', self._tail_end.subject) def load(self, name, value): # end_head and end_tail were used in an older Gaphor version if name in ('head_end', 'head_subject', 'head-subject'): #type(self._head_end).subject.load(self._head_end, value) #self._head_end.load('subject', value) self._head_end.subject = value elif name in ('tail_end', 'tail_subject', 'tail-subject'): #type(self._tail_end).subject.load(self._tail_end, value) #self._tail_end.load('subject', value) self._tail_end.subject = value else: NamedLine.load(self, name, value) def postload(self): NamedLine.postload(self) self._head_end.set_text() self._tail_end.set_text() head_end = property(lambda self: self._head_end) tail_end = property(lambda self: self._tail_end) def unlink(self): self._head_end.unlink() self._tail_end.unlink() super(AssociationItem, self).unlink() def invert_direction(self): """ Invert the direction of the association, this is done by swapping the head and tail-ends subjects. """ if not self.subject: return self.subject.memberEnd.swap(self.subject.memberEnd[0], self.subject.memberEnd[1]) self.request_update() def on_named_element_name(self, event): """ Update names of the association as well as its ends. Override NamedLine.on_named_element_name. """ if event is None: super(AssociationItem, self).on_named_element_name(event) self.on_association_end_value(event) elif event.element is self.subject: super(AssociationItem, self).on_named_element_name(event) else: self.on_association_end_value(event) def on_association_end_value(self, event): """ Handle events and update text on association end. """ #if event: # element = event.element # for end in (self._head_end, self._tail_end): # subject = end.subject # if subject and element in (subject, subject.lowerValue, \ # subject.upperValue, subject.taggedValue): # end.set_text() # self.request_update() ## break; #else: for end in (self._head_end, self._tail_end): end.set_text() self.request_update() def post_update(self, context): """ Update the shapes and sub-items of the association. """ handles = self.handles() # Update line endings: head_subject = self._head_end.subject tail_subject = self._tail_end.subject # Update line ends using the aggregation and isNavigable values: if head_subject and tail_subject: if tail_subject.aggregation == intern('composite'): self.draw_head = self.draw_head_composite elif tail_subject.aggregation == intern('shared'): self.draw_head = self.draw_head_shared elif self._head_end.subject.navigability is True: self.draw_head = self.draw_head_navigable elif self._head_end.subject.navigability is False: self.draw_head = self.draw_head_none else: self.draw_head = self.draw_head_undefined if head_subject.aggregation == intern('composite'): self.draw_tail = self.draw_tail_composite elif head_subject.aggregation == intern('shared'): self.draw_tail = self.draw_tail_shared elif self._tail_end.subject.navigability is True: self.draw_tail = self.draw_tail_navigable elif self._tail_end.subject.navigability is False: self.draw_tail = self.draw_tail_none else: self.draw_tail = self.draw_tail_undefined if self._show_direction: inverted = self.tail_end.subject is self.subject.memberEnd[0] pos, angle = self._get_center_pos(inverted) self._dir_pos = pos self._dir_angle = angle else: self.draw_head = self.draw_head_undefined self.draw_tail = self.draw_tail_undefined # update relationship after self.set calls to avoid circural updates super(AssociationItem, self).post_update(context) # Calculate alignment of the head name and multiplicity self._head_end.post_update(context, handles[0].pos, handles[1].pos) # Calculate alignment of the tail name and multiplicity self._tail_end.post_update(context, handles[-1].pos, handles[-2].pos) def point(self, pos): """ Returns the distance from the Association to the (mouse) cursor. """ return min( super(AssociationItem, self).point(pos), self._head_end.point(pos), self._tail_end.point(pos)) def draw_head_none(self, context): """ Draw an 'x' on the line end to indicate no navigability at association head. """ cr = context.cairo cr.move_to(6, -4) cr.rel_line_to(8, 8) cr.rel_move_to(0, -8) cr.rel_line_to(-8, 8) cr.stroke() cr.move_to(0, 0) def draw_tail_none(self, context): """ Draw an 'x' on the line end to indicate no navigability at association tail. """ cr = context.cairo cr.line_to(0, 0) cr.move_to(6, -4) cr.rel_line_to(8, 8) cr.rel_move_to(0, -8) cr.rel_line_to(-8, 8) cr.stroke() def _draw_diamond(self, cr): """ Helper function to draw diamond shape for shared and composite aggregations. """ cr.move_to(20, 0) cr.line_to(10, -6) cr.line_to(0, 0) cr.line_to(10, 6) #cr.line_to(20, 0) cr.close_path() def draw_head_composite(self, context): """ Draw a closed diamond on the line end to indicate composite aggregation at association head. """ cr = context.cairo self._draw_diamond(cr) context.cairo.fill_preserve() cr.stroke() cr.move_to(20, 0) def draw_tail_composite(self, context): """ Draw a closed diamond on the line end to indicate composite aggregation at association tail. """ cr = context.cairo cr.line_to(20, 0) cr.stroke() self._draw_diamond(cr) cr.fill_preserve() cr.stroke() def draw_head_shared(self, context): """ Draw an open diamond on the line end to indicate shared aggregation at association head. """ cr = context.cairo self._draw_diamond(cr) cr.move_to(20, 0) def draw_tail_shared(self, context): """ Draw an open diamond on the line end to indicate shared aggregation at association tail. """ cr = context.cairo cr.line_to(20, 0) cr.stroke() self._draw_diamond(cr) cr.stroke() def draw_head_navigable(self, context): """ Draw a normal arrow to indicate association end navigability at association head. """ cr = context.cairo cr.move_to(15, -6) cr.line_to(0, 0) cr.line_to(15, 6) cr.stroke() cr.move_to(0, 0) def draw_tail_navigable(self, context): """ Draw a normal arrow to indicate association end navigability at association tail. """ cr = context.cairo cr.line_to(0, 0) cr.stroke() cr.move_to(15, -6) cr.line_to(0, 0) cr.line_to(15, 6) def draw_head_undefined(self, context): """ Draw nothing to indicate undefined association end at association head. """ context.cairo.move_to(0, 0) def draw_tail_undefined(self, context): """ Draw nothing to indicate undefined association end at association tail. """ context.cairo.line_to(0, 0) def draw(self, context): super(AssociationItem, self).draw(context) cr = context.cairo self._head_end.draw(context) self._tail_end.draw(context) if self._show_direction: cr.save() try: cr.translate(*self._dir_pos) cr.rotate(self._dir_angle) cr.move_to(0, 0) cr.line_to(6, 5) cr.line_to(0, 10) cr.fill() finally: cr.restore() def item_at(self, x, y): if distance_point_point_fast(self._handles[0].pos, (x, y)) < 10: return self._head_end elif distance_point_point_fast(self._handles[-1].pos, (x, y)) < 10: return self._tail_end return self
class Line(Item): """ A Line item. Properties: - fuzziness (0.0..n): an extra margin that should be taken into account when calculating the distance from the line (using point()). - orthogonal (bool): whether or not the line should be orthogonal (only straight angles) - horizontal: first line segment is horizontal - line_width: width of the line to be drawn This line also supports arrow heads on both the begin and end of the line. These are drawn with the methods draw_head(context) and draw_tail(context). The coordinate system is altered so the methods do not have to know about the angle of the line segment (e.g. drawing a line from (10, 10) via (0, 0) to (10, -10) will draw an arrow point). """ def __init__(self): super(Line, self).__init__() self._handles = [ Handle(connectable=True), Handle((10, 10), connectable=True) ] self._ports = [] self._update_ports() self._line_width = 2 self._fuzziness = 0 self._orthogonal_constraints = [] self._horizontal = False self._head_angle = self._tail_angle = 0 @observed def _set_line_width(self, line_width): self._line_width = line_width line_width = reversible_property(lambda s: s._line_width, _set_line_width) @observed def _set_fuzziness(self, fuzziness): self._fuzziness = fuzziness fuzziness = reversible_property(lambda s: s._fuzziness, _set_fuzziness) def _update_orthogonal_constraints(self, orthogonal): """ Update the constraints required to maintain the orthogonal line. The actual constraints attribute (``_orthogonal_constraints``) is observed, so the undo system will update the contents properly """ if not self.canvas: self._orthogonal_constraints = orthogonal and [None] or [] return for c in self._orthogonal_constraints: self.canvas.solver.remove_constraint(c) del self._orthogonal_constraints[:] if not orthogonal: return h = self._handles # if len(h) < 3: # self.split_segment(0) eq = EqualsConstraint # lambda a, b: a - b add = self.canvas.solver.add_constraint cons = [] rest = self._horizontal and 1 or 0 for pos, (h0, h1) in enumerate(zip(h, h[1:])): p0 = h0.pos p1 = h1.pos if pos % 2 == rest: # odd cons.append(add(eq(a=p0.x, b=p1.x))) else: cons.append(add(eq(a=p0.y, b=p1.y))) self.canvas.solver.request_resolve(p1.x) self.canvas.solver.request_resolve(p1.y) self._set_orthogonal_constraints(cons) self.request_update() @observed def _set_orthogonal_constraints(self, orthogonal_constraints): """ Setter for the constraints maintained. Required for the undo system. """ self._orthogonal_constraints = orthogonal_constraints reversible_property(lambda s: s._orthogonal_constraints, _set_orthogonal_constraints) @observed def _set_orthogonal(self, orthogonal): """ >>> a = Line() >>> a.orthogonal False """ if orthogonal and len(self.handles()) < 3: raise ValueError( "Can't set orthogonal line with less than 3 handles") self._update_orthogonal_constraints(orthogonal) orthogonal = reversible_property(lambda s: bool(s._orthogonal_constraints), _set_orthogonal) @observed def _inner_set_horizontal(self, horizontal): self._horizontal = horizontal reversible_method( _inner_set_horizontal, _inner_set_horizontal, {"horizontal": lambda horizontal: not horizontal}, ) def _set_horizontal(self, horizontal): """ >>> line = Line() >>> line.horizontal False >>> line.horizontal = False >>> line.horizontal False """ self._inner_set_horizontal(horizontal) self._update_orthogonal_constraints(self.orthogonal) horizontal = reversible_property(lambda s: s._horizontal, _set_horizontal) def setup_canvas(self): """ Setup constraints. In this case orthogonal. """ super(Line, self).setup_canvas() self._update_orthogonal_constraints(self.orthogonal) def teardown_canvas(self): """ Remove constraints created in setup_canvas(). """ super(Line, self).teardown_canvas() for c in self._orthogonal_constraints: self.canvas.solver.remove_constraint(c) @observed def _reversible_insert_handle(self, index, handle): self._handles.insert(index, handle) @observed def _reversible_remove_handle(self, handle): self._handles.remove(handle) reversible_pair( _reversible_insert_handle, _reversible_remove_handle, bind1={ "index": lambda self, handle: self._handles.index(handle) }, ) @observed def _reversible_insert_port(self, index, port): self._ports.insert(index, port) @observed def _reversible_remove_port(self, port): self._ports.remove(port) reversible_pair( _reversible_insert_port, _reversible_remove_port, bind1={ "index": lambda self, port: self._ports.index(port) }, ) def _create_handle(self, pos, strength=WEAK): return Handle(pos, strength=strength) def _create_port(self, p1, p2): return LinePort(p1, p2) def _update_ports(self): """ Update line ports. This destroys all previously created ports and should only be used when initializing the line. """ assert len(self._handles) >= 2, "Not enough segments" self._ports = [] handles = self._handles for h1, h2 in zip(handles[:-1], handles[1:]): self._ports.append(self._create_port(h1.pos, h2.pos)) def opposite(self, handle): """ Given the handle of one end of the line, return the other end. """ handles = self._handles if handle is handles[0]: return handles[-1] elif handle is handles[-1]: return handles[0] else: raise KeyError("Handle is not an end handle") def post_update(self, context): """ """ super(Line, self).post_update(context) h0, h1 = self._handles[:2] p0, p1 = h0.pos, h1.pos self._head_angle = atan2(p1.y - p0.y, p1.x - p0.x) h1, h0 = self._handles[-2:] p1, p0 = h1.pos, h0.pos self._tail_angle = atan2(p1.y - p0.y, p1.x - p0.x) def point(self, pos): """ >>> a = Line() >>> a.handles()[1].pos = 25, 5 >>> a._handles.append(a._create_handle((30, 30))) >>> a.point((-1, 0)) 1.0 >>> '%.3f' % a.point((5, 4)) '2.942' >>> '%.3f' % a.point((29, 29)) '0.784' """ hpos = [h.pos for h in self._handles] distance, _point = min( map(distance_line_point, hpos[:-1], hpos[1:], [pos] * (len(hpos) - 1))) return max(0, distance - self.fuzziness) def draw_head(self, context): """ Default head drawer: move cursor to the first handle. """ context.cairo.move_to(0, 0) def draw_tail(self, context): """ Default tail drawer: draw line to the last handle. """ context.cairo.line_to(0, 0) def draw(self, context): """ Draw the line itself. See Item.draw(context). """ def draw_line_end(pos, angle, draw): cr = context.cairo cr.save() try: cr.translate(*pos) cr.rotate(angle) draw(context) finally: cr.restore() cr = context.cairo cr.set_line_width(self.line_width) draw_line_end(self._handles[0].pos, self._head_angle, self.draw_head) for h in self._handles[1:-1]: cr.line_to(*h.pos) draw_line_end(self._handles[-1].pos, self._tail_angle, self.draw_tail) cr.stroke()
class Item(object): """ Base class (or interface) for items on a canvas.Canvas. Attributes: - matrix: item's transformation matrix - canvas: canvas, which owns an item - constraints: list of item constraints, automatically registered when the item is added to a canvas; may be extended in subclasses Private: - _canvas: canvas, which owns an item - _handles: list of handles owned by an item - _ports: list of ports, connectable areas of an item - _matrix_i2c: item to canvas coordinates matrix - _matrix_c2i: canvas to item coordinates matrix - _matrix_i2v: item to view coordinates matrices - _matrix_v2i: view to item coordinates matrices - _sort_key: used to sort items - _canvas_projections: used to sort items """ 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() @observed def _set_canvas(self, canvas): """ Set the canvas. Should only be called from Canvas.add and Canvas.remove(). """ assert not canvas or not self._canvas or self._canvas is canvas if self._canvas: self.teardown_canvas() self._canvas = canvas if canvas: self.setup_canvas() canvas = reversible_property(lambda s: s._canvas, _set_canvas, doc="Canvas owning this item") constraints = property(lambda s: s._constraints, doc="Item constraints") def setup_canvas(self): """ Called when the canvas is set for the item. This method can be used to create constraints. """ add = self.canvas.solver.add_constraint for c in self._constraints: add(c) def teardown_canvas(self): """ Called when the canvas is unset for the item. This method can be used to dispose constraints. """ self.canvas.disconnect_item(self) remove = self.canvas.solver.remove_constraint for c in self._constraints: remove(c) @observed def _set_matrix(self, matrix): """ Set the conversion matrix (parent -> item) """ if not isinstance(matrix, Matrix): matrix = Matrix(*matrix) self._matrix = matrix matrix = reversible_property(lambda s: s._matrix, _set_matrix) def request_update(self, update=True, matrix=True): if self._canvas: self._canvas.request_update(self, update=update, matrix=matrix) def pre_update(self, context): """ Perform any changes before item update here, for example: - change matrix - move handles Gaphas does not guarantee that any canvas invariant is valid at this point (i.e. constraints are not solved, first handle is not in position (0, 0), etc). """ pass def post_update(self, context): """ Method called after item update. If some variables should be used during drawing or in another update, then they should be calculated in post method. Changing matrix or moving handles programmatically is really not advised to be performed here. All canvas invariants are true. """ pass def normalize(self): """ Update handle positions of the item, so the first handle is always located at (0, 0). Note that, since this method basically does some housekeeping during the update phase, there's no need to keep track of the changes. Alternative implementation can also be created, e.g. set (0, 0) in the center of a circle or change it depending on the location of a rotation point. Returns ``True`` if some updates have been done, ``False`` otherwise. See ``canvas._normalize()`` for tests. """ updated = False handles = self._handles if handles: x, y = list(map(float, handles[0].pos)) if x: self.matrix.translate(x, 0) updated = True for h in handles: h.pos.x -= x if y: self.matrix.translate(0, y) updated = True for h in handles: h.pos.y -= y return updated def draw(self, context): """ Render the item to a canvas view. Context contains the following attributes: - cairo: the Cairo Context use this one to draw - view: the view that is to be rendered to - selected, focused, hovered, dropzone: view state of items (True/False) - draw_all: a request to draw everything, for bounding box calculations """ pass def handles(self): """ Return a list of handles owned by the item. """ return self._handles def ports(self): """ Return list of ports. """ return self._ports def point(self, pos): """ Get the distance from a point (``x``, ``y``) to the item. ``x`` and ``y`` are in item coordinates. """ pass def constraint( self, horizontal=None, vertical=None, left_of=None, above=None, line=None, delta=0.0, align=None, ): """ Utility (factory) method to create item's internal constraint between two positions or between a position and a line. Position is a tuple of coordinates, i.e. ``(2, 4)``. Line is a tuple of positions, i.e. ``((2, 3), (4, 2))``. This method shall not be used to create constraints between two different items. Created constraint is returned. :Parameters: horizontal=(p1, p2) Keep positions ``p1`` and ``p2`` aligned horizontally. vertical=(p1, p2) Keep positions ``p1`` and ``p2`` aligned vertically. left_of=(p1, p2) Keep position ``p1`` on the left side of position ``p2``. above=(p1, p2) Keep position ``p1`` above position ``p2``. line=(p, l) Keep position ``p`` on line ``l``. """ cc = None # created constraint if horizontal: p1, p2 = horizontal cc = EqualsConstraint(p1[1], p2[1], delta) elif vertical: p1, p2 = vertical cc = EqualsConstraint(p1[0], p2[0], delta) elif left_of: p1, p2 = left_of cc = LessThanConstraint(p1[0], p2[0], delta) elif above: p1, p2 = above cc = LessThanConstraint(p1[1], p2[1], delta) elif line: pos, l = line if align is None: cc = LineConstraint(line=l, point=pos) else: cc = LineAlignConstraint(line=l, point=pos, align=align, delta=delta) else: raise ValueError("Constraint incorrectly specified") assert cc is not None self._constraints.append(cc) return cc def __getstate__(self): """ Persist all, but calculated values (``_matrix_?2?``). """ d = dict(self.__dict__) for n in ("_matrix_i2c", "_matrix_c2i", "_matrix_i2v", "_matrix_v2i"): try: del d[n] except KeyError: pass d["_canvas_projections"] = tuple(self._canvas_projections) return d def __setstate__(self, state): """ Set state. No ``__init__()`` is called. """ for n in ("_matrix_i2c", "_matrix_c2i"): setattr(self, n, None) for n in ("_matrix_i2v", "_matrix_v2i"): setattr(self, n, WeakKeyDictionary()) self.__dict__.update(state) self._canvas_projections = WeakSet(state["_canvas_projections"])
class ClassItem(ClassifierItem): """This item visualizes a Class instance. A ClassItem contains two compartments (Compartment): one for attributes and one for operations. To add and remove such features the ClassItem implements the CanvasGroupable interface. Items can be added by callling class.add() and class.remove(). This is used to handle CanvasItems, not UML objects!""" __uml__ = UML.Class, UML.Stereotype __stereotype__ = { "stereotype": UML.Stereotype, "metaclass": lambda s: UML.model.is_metaclass(s.subject), } __style__ = { "extra-space": "compartment", "abstract-feature-font": "sans italic 10", } def __init__(self, id=None, model=None): """Constructor. Initialize the ClassItem. This will also call the ClassifierItem constructor. The drawing style is set here as well. The class item will create two compartments - one for attributes and another for operations.""" ClassifierItem.__init__(self, id, model) self.drawing_style = self.DRAW_COMPARTMENT self._attributes = self.create_compartment("attributes") self._attributes.font = self.style.feature_font self._operations = self.create_compartment("operations") self._operations.font = self.style.feature_font self._operations.use_extra_space = True self.watch( "subject<Class>.ownedOperation", self.on_class_owned_operation ).watch( "subject<Class>.ownedAttribute.association", self.on_class_owned_attribute ).watch("subject<Class>.ownedAttribute.name").watch( "subject<Class>.ownedAttribute.isStatic" ).watch("subject<Class>.ownedAttribute.isDerived").watch( "subject<Class>.ownedAttribute.visibility" ).watch("subject<Class>.ownedAttribute.lowerValue").watch( "subject<Class>.ownedAttribute.upperValue" ).watch("subject<Class>.ownedAttribute.defaultValue").watch( "subject<Class>.ownedAttribute.typeValue" ).watch("subject<Class>.ownedOperation.name").watch( "subject<Class>.ownedOperation.isAbstract", self.on_operation_is_abstract ).watch("subject<Class>.ownedOperation.isStatic").watch( "subject<Class>.ownedOperation.visibility" ).watch("subject<Class>.ownedOperation.returnResult.lowerValue").watch( "subject<Class>.ownedOperation.returnResult.upperValue" ).watch("subject<Class>.ownedOperation.returnResult.typeValue").watch( "subject<Class>.ownedOperation.formalParameter.lowerValue").watch( "subject<Class>.ownedOperation.formalParameter.upperValue" ).watch( "subject<Class>.ownedOperation.formalParameter.typeValue" ).watch( "subject<Class>.ownedOperation.formalParameter.defaultValue") def save(self, save_func): """Store the show- properties *before* the width/height properties, otherwise the classes will unintentionally grow due to "visible" attributes or operations.""" self.save_property(save_func, "show-attributes") self.save_property(save_func, "show-operations") ClassifierItem.save(self, save_func) def postload(self): """Called once the ClassItem has been loaded. First the ClassifierItem is "post-loaded", then the attributes and operations are synchronized.""" super(ClassItem, self).postload() self.sync_attributes() self.sync_operations() @observed def _set_show_operations(self, value): """Sets the show operations property. This will either show or hide the operations compartment of the ClassItem. This is part of the show_operations property.""" self._operations.visible = value self._operations.use_extra_space = value self._attributes.use_extra_space = not self._operations.visible show_operations = reversible_property(fget=lambda s: s._operations.visible, fset=_set_show_operations) @observed def _set_show_attributes(self, value): """Sets the show attributes property. This will either show or hide the attributes compartment of the ClassItem. This is part of the show_attributes property.""" self._attributes.visible = value show_attributes = reversible_property(fget=lambda s: s._attributes.visible, fset=_set_show_attributes) def _create_attribute(self, attribute): """Create a new attribute item. This will create a new FeatureItem and assigns the specified attribute as the subject.""" new = FeatureItem() new.subject = attribute new.font = self.style.feature_font self._attributes.append(new) def _create_operation(self, operation): """Create a new operation item. This will create a new OperationItem and assigns the specified operation as the subject.""" new = OperationItem() new.subject = operation new.font = self.style.feature_font self._operations.append(new) def sync_attributes(self): """Sync the contents of the attributes compartment with the data in self.subject.""" owned_attributes = [ a for a in self.subject.ownedAttribute if not a.association ] self.sync_uml_elements(owned_attributes, self._attributes, self._create_attribute) def sync_operations(self): """Sync the contents of the operations compartment with the data in self.subject.""" self.sync_uml_elements(self.subject.ownedOperation, self._operations, self._create_operation) def on_class_owned_attribute(self, event): """Event handler for owned attributes. This will synchronize the attributes of this ClassItem.""" if self.subject: self.sync_attributes() def on_class_owned_operation(self, event): """Event handler for owned operations. This will synchronize the operations of this ClassItem.""" if self.subject: self.sync_operations() def on_operation_is_abstract(self, event): """Event handler for abstract operations. This will change the font of the operation.""" o = [o for o in self._operations if o.subject is event.element] if o: o = o[0] o.font = ((o.subject and o.subject.isAbstract) and self.style.abstract_feature_font or self.style.feature_font) self.request_update()
class Variable: """Representation of a variable in the constraint solver. Each Variable has a @value and a @strength. In a constraint the weakest variables are changed. You can even do some calculating with it. The Variable always represents a float variable. """ def __init__(self, value=0.0, strength=NORMAL): self._value = float(value) self._strength = strength # These variables are set by the Solver: self._solver = None self._constraints = set() def __hash__(self): return object.__hash__(self) @observed def _set_strength(self, strength): self._strength = strength for c in self._constraints: c.create_weakest_list() strength = reversible_property(lambda s: s._strength, _set_strength) def dirty(self): """ Mark the variable dirty in both the constraint solver and attached constraints. Variables are marked dirty also during constraints solving to solve all dependent constraints, i.e. two equals constraints between 3 variables. """ solver = self._solver if not solver: return solver.request_resolve(self) @observed def set_value(self, value): oldval = self._value if abs(oldval - value) > EPSILON: self._value = float(value) self.dirty() value = reversible_property(lambda s: s._value, set_value) def __str__(self): return f"Variable({self._value:g}, {self._strength:d})" __repr__ = __str__ def __float__(self): return float(self._value) def __eq__(self, other): """ >>> Variable(5) == 5 True >>> Variable(5) == 4 False >>> Variable(5) != 5 False """ return abs(self._value - other) < EPSILON def __ne__(self, other): """ >>> Variable(5) != 4 True >>> Variable(5) != 5 False """ return abs(self._value - other) > EPSILON def __gt__(self, other): """ >>> Variable(5) > 4 True >>> Variable(5) > 5 False """ return self._value.__gt__(float(other)) def __lt__(self, other): """ >>> Variable(5) < 4 False >>> Variable(5) < 6 True """ return self._value.__lt__(float(other)) def __ge__(self, other): """ >>> Variable(5) >= 5 True """ return self._value.__ge__(float(other)) def __le__(self, other): """ >>> Variable(5) <= 5 True """ return self._value.__le__(float(other)) def __add__(self, other): """ >>> Variable(5) + 4 9.0 """ return self._value.__add__(float(other)) def __sub__(self, other): """ >>> Variable(5) - 4 1.0 >>> Variable(5) - Variable(4) 1.0 """ return self._value.__sub__(float(other)) def __mul__(self, other): """ >>> Variable(5) * 4 20.0 >>> Variable(5) * Variable(4) 20.0 """ return self._value.__mul__(float(other)) def __floordiv__(self, other): """ >>> Variable(21) // 4 5.0 >>> Variable(21) // Variable(4) 5.0 """ return self._value.__floordiv__(float(other)) def __mod__(self, other): """ >>> Variable(5) % 4 1.0 >>> Variable(5) % Variable(4) 1.0 """ return self._value.__mod__(float(other)) def __divmod__(self, other): """ >>> divmod(Variable(21), 4) (5.0, 1.0) >>> divmod(Variable(21), Variable(4)) (5.0, 1.0) """ return self._value.__divmod__(float(other)) def __pow__(self, other): """ >>> pow(Variable(5), 4) 625.0 >>> pow(Variable(5), Variable(4)) 625.0 """ return self._value.__pow__(float(other)) def __div__(self, other): """ >>> Variable(5) / 4. 1.25 >>> Variable(5) / Variable(4) 1.25 """ return self._value.__div__(float(other)) def __truediv__(self, other): """ >>> Variable(5.) / 4 1.25 >>> 10 / Variable(5.) 2.0 """ return self._value.__truediv__(float(other)) # .. And the other way around: def __radd__(self, other): """ >>> 4 + Variable(5) 9.0 >>> Variable(4) + Variable(5) 9.0 """ return self._value.__radd__(float(other)) def __rsub__(self, other): """ >>> 6 - Variable(5) 1.0 """ return self._value.__rsub__(other) def __rmul__(self, other): """ >>> 4 * Variable(5) 20.0 """ return self._value.__rmul__(other) def __rfloordiv__(self, other): """ >>> 21 // Variable(4) 5.0 """ return self._value.__rfloordiv__(other) def __rmod__(self, other): """ >>> 5 % Variable(4) 1.0 """ return self._value.__rmod__(other) def __rdivmod__(self, other): """ >>> divmod(21, Variable(4)) (5.0, 1.0) """ return self._value.__rdivmod__(other) def __rpow__(self, other): """ >>> pow(4, Variable(5)) 1024.0 """ return self._value.__rpow__(other) def __rdiv__(self, other): """ >>> 5 / Variable(4.) 1.25 """ return self._value.__rdiv__(other) def __rtruediv__(self, other): """ >>> 5. / Variable(4) 1.25 """ return self._value.__rtruediv__(other)
class Handle(object): """ Handles are used to support modifications of Items. If the handle is connected to an item, the ``connected_to`` property should refer to the item. A ``disconnect`` handler should be provided that handles all disconnect behaviour (e.g. clean up constraints and ``connected_to``). Note for those of you that use the Pickle module to persist a canvas: The property ``disconnect`` should contain a callable object (with __call__() method), so the pickle handler can also pickle that. Pickle is not capable of pickling ``instancemethod`` or ``function`` objects. """ def __init__(self, pos=(0, 0), strength=NORMAL, connectable=False, movable=True): self._pos = Position(pos, strength) self._connectable = connectable self._movable = movable self._visible = True def _set_pos(self, pos): """ Shortcut for ``handle.pos.pos = pos`` >>> h = Handle((10, 10)) >>> h.pos <Position object on (10, 10)> >>> h.pos = (20, 15) >>> h.pos <Position object on (20, 15)> """ self._pos.pos = pos pos = property(lambda s: s._pos, _set_pos) def _set_x(self, x): """ Shortcut for ``handle.pos.x = x`` """ self._pos.x = x def _get_x(self): return self._pos.x x = property(deprecated(_get_x), deprecated(_set_x)) def _set_y(self, y): """ Shortcut for ``handle.pos.y = y`` """ self._pos.y = y def _get_y(self): return self._pos.y y = property(deprecated(_get_y), deprecated(_set_y)) @observed def _set_connectable(self, connectable): self._connectable = connectable connectable = reversible_property(lambda s: s._connectable, _set_connectable) @observed def _set_movable(self, movable): self._movable = movable movable = reversible_property(lambda s: s._movable, _set_movable) @observed def _set_visible(self, visible): self._visible = visible visible = reversible_property(lambda s: s._visible, _set_visible) def __str__(self): return "<%s object on (%g, %g)>" % ( self.__class__.__name__, float(self._pos.x), float(self._pos.y), ) __repr__ = __str__
class ForkNodeItem(Presentation[UML.ForkNode], Item, Named): """ Representation of fork and join node. """ def __init__(self, id=None, model=None): super().__init__(id, model) h1, h2 = Handle(), Handle() self._handles.append(h1) self._handles.append(h2) self._ports.append(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") self.constraint(vertical=(h1.pos, h2.pos)) self.constraint(above=(h1.pos, h2.pos), delta=30) def save(self, save_func): save_func("matrix", tuple(self.matrix)) save_func("height", float(self._handles[1].pos.y)) if self._combined: save_func("combined", self._combined) super().save(save_func) def load(self, name, value): if name == "matrix": self.matrix = ast.literal_eval(value) elif name == "height": self._handles[1].pos.y = ast.literal_eval(value) elif name == "combined": self._combined = value else: # DiagramItem.load(self, name, value) super().load(name, value) @observed def _set_combined(self, value): # self.preserve_property('combined') self._combined = value combined = reversible_property(lambda s: s._combined, _set_combined) def draw(self, context): h1, h2 = self.handles() height = h2.pos.y - h1.pos.y self.shape.draw(context, Rectangle(0, 0, 1, height)) def draw_fork_node(self, _box, context, _bounding_box): """ Draw vertical line - symbol of fork and join nodes. Join specification is also drawn above the item. """ cr = context.cairo cr.set_line_width(6) h1, h2 = self._handles cr.move_to(h1.pos.x, h1.pos.y) cr.line_to(h2.pos.x, h2.pos.y) stroke(context) def point(self, pos): h1, h2 = self._handles d, p = distance_line_point(h1.pos, h2.pos, pos) # Subtract line_width / 2 return d - 3