Ejemplo n.º 1
0
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)
Ejemplo n.º 2
0
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")
Ejemplo n.º 3
0
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)
Ejemplo n.º 4
0
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)
Ejemplo n.º 5
0
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
Ejemplo n.º 6
0
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)
Ejemplo n.º 7
0
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
Ejemplo n.º 8
0
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 ()
Ejemplo n.º 9
0
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
Ejemplo n.º 10
0
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
Ejemplo n.º 11
0
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()
Ejemplo n.º 12
0
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
Ejemplo n.º 13
0
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()
Ejemplo n.º 14
0
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"])
Ejemplo n.º 15
0
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()
Ejemplo n.º 16
0
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)
Ejemplo n.º 17
0
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__
Ejemplo n.º 18
0
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