Beispiel #1
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()
Beispiel #2
0
class Canvas:
    """
    Container class for items.
    """
    def __init__(self):
        self._tree = tree.Tree()
        self._solver = solver.Solver()
        self._connections = table.Table(Connection, list(range(4)))
        self._dirty_items = set()
        self._dirty_matrix_items = set()
        self._dirty_index = False

        self._registered_views = set()

    solver = property(lambda s: s._solver)

    @observed
    def add(self, item, parent=None, index=None):
        """
        Add an item to the canvas.

        >>> c = Canvas()
        >>> from gaphas import item
        >>> i = item.Item()
        >>> c.add(i)
        >>> len(c._tree.nodes)
        1
        >>> i._canvas is c
        True
        """
        assert item not in self._tree.nodes, f"Adding already added node {item}"
        self._tree.add(item, parent, index)
        self._dirty_index = True

        self.update_matrix(item, parent)

        item._set_canvas(self)

        self.request_update(item)

    @observed
    def _remove(self, item):
        """
        Remove is done in a separate, @observed, method so the undo
        system can restore removed items in the right order.
        """
        item._set_canvas(None)
        self._tree.remove(item)
        self._update_views(removed_items=(item, ))
        self._dirty_items.discard(item)
        self._dirty_matrix_items.discard(item)

    def remove(self, item):
        """
        Remove item from the canvas.

        >>> c = Canvas()
        >>> from gaphas import item
        >>> i = item.Item()
        >>> c.add(i)
        >>> c.remove(i)
        >>> c._tree.nodes
        []
        >>> i._canvas
        """
        for child in reversed(self.get_children(item)):
            self.remove(child)
        self.remove_connections_to_item(item)
        self._remove(item)

    reversible_pair(
        add,
        _remove,
        bind1={
            "parent": lambda self, item: self.get_parent(item),
            "index":
            lambda self, item: self._tree.get_siblings(item).index(item),
        },
    )

    @observed
    def reparent(self, item, parent, index=None):
        """
        Set new parent for an item.
        """
        self._tree.reparent(item, parent, index)

        self._dirty_index = True

    reversible_method(
        reparent,
        reverse=reparent,
        bind={
            "parent": lambda self, item: self.get_parent(item),
            "index":
            lambda self, item: self._tree.get_siblings(item).index(item),
        },
    )

    def get_all_items(self):
        """
        Get a list of all items.

        >>> c = Canvas()
        >>> c.get_all_items()
        []
        >>> from gaphas import item
        >>> i = item.Item()
        >>> c.add(i)
        >>> c.get_all_items() # doctest: +ELLIPSIS
        [<gaphas.item.Item ...>]
        """
        return self._tree.nodes

    def get_root_items(self):
        """
        Return the root items of the canvas.

        >>> c = Canvas()
        >>> c.get_all_items()
        []
        >>> from gaphas import item
        >>> i = item.Item()
        >>> c.add(i)
        >>> ii = item.Item()
        >>> c.add(ii, i)
        >>> c.get_root_items() # doctest: +ELLIPSIS
        [<gaphas.item.Item ...>]
        """
        return self._tree.get_children(None)

    def get_parent(self, item):
        """
        See `tree.Tree.get_parent()`.

        >>> c = Canvas()
        >>> from gaphas import item
        >>> i = item.Item()
        >>> c.add(i)
        >>> ii = item.Item()
        >>> c.add(ii, i)
        >>> c.get_parent(i)
        >>> c.get_parent(ii) # doctest: +ELLIPSIS
        <gaphas.item.Item ...>
        """
        return self._tree.get_parent(item)

    def get_ancestors(self, item):
        """
        See `tree.Tree.get_ancestors()`.

        >>> c = Canvas()
        >>> from gaphas import item
        >>> i = item.Item()
        >>> c.add(i)
        >>> ii = item.Item()
        >>> c.add(ii, i)
        >>> iii = item.Item()
        >>> c.add(iii, ii)
        >>> list(c.get_ancestors(i))
        []
        >>> list(c.get_ancestors(ii)) # doctest: +ELLIPSIS
        [<gaphas.item.Item ...>]
        >>> list(c.get_ancestors(iii)) # doctest: +ELLIPSIS
        [<gaphas.item.Item ...>, <gaphas.item.Item ...>]
        """
        return self._tree.get_ancestors(item)

    def get_children(self, item):
        """
        See `tree.Tree.get_children()`.

        >>> c = Canvas()
        >>> from gaphas import item
        >>> i = item.Item()
        >>> c.add(i)
        >>> ii = item.Item()
        >>> c.add(ii, i)
        >>> iii = item.Item()
        >>> c.add(iii, ii)
        >>> list(c.get_children(iii))
        []
        >>> list(c.get_children(ii)) # doctest: +ELLIPSIS
        [<gaphas.item.Item ...>]
        >>> list(c.get_children(i)) # doctest: +ELLIPSIS
        [<gaphas.item.Item ...>]
        """
        return self._tree.get_children(item)

    def get_all_children(self, item):
        """
        See `tree.Tree.get_all_children()`.

        >>> c = Canvas()
        >>> from gaphas import item
        >>> i = item.Item()
        >>> c.add(i)
        >>> ii = item.Item()
        >>> c.add(ii, i)
        >>> iii = item.Item()
        >>> c.add(iii, ii)
        >>> list(c.get_all_children(iii))
        []
        >>> list(c.get_all_children(ii)) # doctest: +ELLIPSIS
        [<gaphas.item.Item ...>]
        >>> list(c.get_all_children(i)) # doctest: +ELLIPSIS
        [<gaphas.item.Item ...>, <gaphas.item.Item ...>]
        """
        return self._tree.get_all_children(item)

    @observed
    def connect_item(self,
                     item,
                     handle,
                     connected,
                     port,
                     constraint=None,
                     callback=None):
        """
        Create a connection between two items. The connection is
        registered and the constraint is added to the constraint
        solver.

        The pair (item, handle) should be unique and not yet connected.

        The callback is invoked when the connection is broken.

        :Parameters:
         item
            Connecting item (i.e. a line).
         handle
            Handle of connecting item.
         connected
            Connected item (i.e. a box).
         port
            Port of connected item.
         constraint
            Constraint to keep the connection in place.
         callback
            Function to be called on disconnection.

        ConnectionError is raised in case handle is already registered
        on a connection.
        """
        if self.get_connection(handle):
            raise ConnectionError(
                f"Handle {handle} of item {item} is already connected")

        self._connections.insert(item, handle, connected, port, constraint,
                                 callback)

        if constraint:
            self._solver.add_constraint(constraint)

    def disconnect_item(self, item, handle=None):
        """
        Disconnect the connections of an item. If handle is not None,
        only the connection for that handle is disconnected.
        """
        # disconnect on canvas level
        for cinfo in list(self._connections.query(item=item, handle=handle)):
            self._disconnect_item(*cinfo)

    @observed
    def _disconnect_item(self, item, handle, connected, port, constraint,
                         callback):
        """
        Perform the real disconnect.
        """
        # Same arguments as connect_item, makes reverser easy
        if constraint:
            self._solver.remove_constraint(constraint)

        if callback:
            callback()

        self._connections.delete(item, handle, connected, port, constraint,
                                 callback)

    reversible_pair(connect_item, _disconnect_item)

    def remove_connections_to_item(self, item):
        """
        Remove all connections (handles connected to and constraints)
        for a specific item (to and from the item).  This is some
        brute force cleanup (e.g. if constraints are referenced by
        items, those references are not cleaned up).
        """
        disconnect_item = self._disconnect_item
        # remove connections from this item
        for cinfo in list(self._connections.query(item=item)):
            disconnect_item(*cinfo)
        # remove constraints to this item
        for cinfo in list(self._connections.query(connected=item)):
            disconnect_item(*cinfo)

    @observed
    def reconnect_item(self, item, handle, port=None, constraint=None):
        """
        Update an existing connection. This is used to provide a new
        constraint to the connection. ``item`` and ``handle`` are
        the keys to the to-be-updated connection.

        >>> c = Canvas()
        >>> from gaphas import item
        >>> i = item.Line()
        >>> c.add(i)
        >>> ii = item.Line()
        >>> c.add(ii, i)
        >>> iii = item.Line()
        >>> c.add(iii, ii)

        We need a few constraints, because that's what we're updating:

        >>> from gaphas.constraint import EqualsConstraint
        >>> cons1 = EqualsConstraint(i.handles()[0].pos.x, i.handles()[0].pos.x)
        >>> cons2 = EqualsConstraint(i.handles()[0].pos.y, i.handles()[0].pos.y)
        >>> c.connect_item(i, i.handles()[0], ii, ii.ports()[0], cons1)
        >>> c.get_connection(i.handles()[0]) # doctest: +ELLIPSIS
        Connection(item=<gaphas.item.Line object at 0x...)
        >>> c.get_connection(i.handles()[0]).constraint is cons1
        True
        >>> cons1 in c.solver.constraints
        True
        >>> c.reconnect_item(i, i.handles()[0], cons2)
        >>> c.get_connection(i.handles()[0]) # doctest: +ELLIPSIS
        Connection(item=<gaphas.item.Line object at 0x...)
        >>> c.get_connection(i.handles()[0]).constraint is cons2
        True
        >>> cons1 in c.solver.constraints
        False
        >>> cons2 in c.solver.constraints
        True

        An exception is raised if no connection exists:
        >>> c.reconnect_item(ii, ii.handles()[0], cons2) # doctest: +ELLIPSIS
        Traceback (most recent call last):
        ...
        ValueError: No data available for item ...

        """
        # checks:
        cinfo = self.get_connection(handle)
        if not cinfo:
            raise ValueError(
                f'No data available for item "{item}" and handle "{handle}"')

        if cinfo.constraint:
            self._solver.remove_constraint(cinfo.constraint)
        self._connections.delete(item=cinfo.item, handle=cinfo.handle)

        self._connections.insert(
            item,
            handle,
            cinfo.connected,
            port or cinfo.port,
            constraint,
            cinfo.callback,
        )
        if constraint:
            self._solver.add_constraint(constraint)

    reversible_method(
        reconnect_item,
        reverse=reconnect_item,
        bind={
            "port":
            lambda self, item, handle: self.get_connection(handle).port,
            "constraint":
            lambda self, item, handle: self.get_connection(handle).constraint,
        },
    )

    def get_connection(self, handle):
        """
        Get connection information for specified handle.

        >>> c = Canvas()
        >>> from gaphas.item import Line
        >>> line = Line()
        >>> from gaphas import item
        >>> i = item.Line()
        >>> c.add(i)
        >>> ii = item.Line()
        >>> c.add(ii)
        >>> c.connect_item(i, i.handles()[0], ii, ii.ports()[0])
        >>> c.get_connection(i.handles()[0])     # doctest: +ELLIPSIS
        Connection(item=<gaphas.item.Line object at 0x...)
        >>> c.get_connection(i.handles()[1])     # doctest: +ELLIPSIS
        >>> c.get_connection(ii.handles()[0])    # doctest: +ELLIPSIS
        """
        try:
            return next(self._connections.query(handle=handle))
        except StopIteration as ex:
            return None

    def get_connections(self,
                        item=None,
                        handle=None,
                        connected=None,
                        port=None):
        """
        Return an iterator of connection information.

        The list contains (item, handle). As a result an item may be
        in the list more than once (depending on the number of handles
        that are connected). If ``item`` is connected to itself it
        will also appear in the list.

        >>> c = Canvas()
        >>> from gaphas import item
        >>> i = item.Line()
        >>> c.add(i)
        >>> ii = item.Line()
        >>> c.add(ii)
        >>> iii = item.Line()
        >>> c.add (iii)
        >>> c.connect_item(i, i.handles()[0], ii, ii.ports()[0], None)

        >>> list(c.get_connections(item=i)) # doctest: +ELLIPSIS
        [Connection(item=<gaphas.item.Line object at 0x...]
        >>> list(c.get_connections(connected=i))
        []
        >>> list(c.get_connections(connected=ii)) # doctest: +ELLIPSIS
        [Connection(item=<gaphas.item.Line object at 0x...]

        >>> c.connect_item(ii, ii.handles()[0], iii, iii.ports()[0], None)
        >>> list(c.get_connections(item=ii)) # doctest: +ELLIPSIS
        [Connection(item=<gaphas.item.Line object at 0x...]
        >>> list(c.get_connections(connected=iii)) # doctest: +ELLIPSIS
        [Connection(item=<gaphas.item.Line object at 0x...]
        """
        return self._connections.query(item=item,
                                       handle=handle,
                                       connected=connected,
                                       port=port)

    def sort(self, items, reverse=False):
        """
        Sort a list of items in the order in which they are traversed
        in the canvas (Depth first).

        >>> c = Canvas()
        >>> from gaphas import item
        >>> i1 = item.Line()
        >>> c.add(i1)
        >>> i2 = item.Line()
        >>> c.add(i2)
        >>> i3 = item.Line()
        >>> c.add (i3)
        >>> c.update() # ensure items are indexed
        >>> i1._canvas_index
        0
        >>> s = c.sort([i2, i3, i1])
        >>> s[0] is i1 and s[1] is i2 and s[2] is i3
        True
        """
        return sorted(items, key=attrgetter("_canvas_index"), reverse=reverse)

    def get_matrix_i2c(self, item, calculate=False):
        """
        Get the Item to Canvas matrix for ``item``.

        item:
            The item who's item-to-canvas transformation matrix should
            be found
        calculate:
            True will allow this function to actually calculate it,
            instead of raising an `AttributeError` when no matrix is
            present yet. Note that out-of-date matrices are not
            recalculated.
        """
        if item._matrix_i2c is None or calculate:
            self.update_matrix(item)
        return item._matrix_i2c

    def get_matrix_c2i(self, item, calculate=False):
        """
        Get the Canvas to Item matrix for ``item``.
        See `get_matrix_i2c()`.
        """
        if item._matrix_c2i is None or calculate:
            self.update_matrix(item)
        return item._matrix_c2i

    def get_matrix_i2i(self, from_item, to_item, calculate=False):
        i2c = self.get_matrix_i2c(from_item, calculate)
        c2i = self.get_matrix_c2i(to_item, calculate)
        try:
            return i2c.multiply(c2i)
        except AttributeError:
            # Fall back to old behaviour
            return i2c * c2i

    @observed
    def request_update(self, item, update=True, matrix=True):
        """
        Set an update request for the item.

        >>> c = Canvas()
        >>> from gaphas import item
        >>> i = item.Item()
        >>> ii = item.Item()
        >>> c.add(i)
        >>> c.add(ii, i)
        >>> len(c._dirty_items)
        0
        >>> c.update_now()
        >>> len(c._dirty_items)
        0
        """
        if update:
            self._dirty_items.add(item)
        if matrix:
            self._dirty_matrix_items.add(item)

        self.update()

    reversible_method(request_update, reverse=request_update)

    def request_matrix_update(self, item):
        """
        Schedule only the matrix to be updated.
        """
        self.request_update(item, update=False, matrix=True)

    def require_update(self):
        """
        Returns ``True`` or ``False`` depending on if an update is
        needed.

        >>> c=Canvas()
        >>> c.require_update()
        False
        >>> from gaphas import item
        >>> i = item.Item()
        >>> c.add(i)
        >>> c.require_update()
        False

        Since we're not in a GTK+ mainloop, the update is not scheduled
        asynchronous. Therefore ``require_update()`` returns ``False``.
        """
        return bool(self._dirty_items)

    @AsyncIO(single=True)
    def update(self):
        """
        Update the canvas, if called from within a gtk-mainloop, the
        update job is scheduled as idle job.
        """
        self.update_now()

    def _pre_update_items(self, items, context):
        for item in items:
            item.pre_update(context)

    def _post_update_items(self, items, context):
        for item in items:
            item.post_update(context)

    @nonrecursive
    def update_now(self):
        """
        Perform an update of the items that requested an update.
        """
        sort = self.sort

        if self._dirty_index:
            self.update_index()
            self._dirty_index = False

        def dirty_items_with_ancestors():
            for item in self._dirty_items:
                yield item
                yield from self._tree.get_ancestors(item)

        dirty_items = sort(dirty_items_with_ancestors(), reverse=True)

        try:
            context = Context(cairo=instant_cairo_context())

            # allow programmers to perform tricks and hacks before item
            # full update (only called for items that requested a full update)
            self._pre_update_items(dirty_items, context)

            # recalculate matrices
            dirty_matrix_items = self.update_matrices(self._dirty_matrix_items)
            self._dirty_matrix_items.clear()

            self.update_constraints(dirty_matrix_items)

            # no matrix can change during constraint solving
            assert (
                not self._dirty_matrix_items
            ), f"No matrices may have been marked dirty ({self._dirty_matrix_items})"

            # item's can be marked dirty due to external constraints solving
            if len(dirty_items) != len(self._dirty_items):
                dirty_items = sort(self._dirty_items, reverse=True)

            # normalize items, which changed after constraint solving;
            # recalculate matrices of normalized items
            dirty_matrix_items.update(self._normalize(dirty_items))

            # ensure constraints are still true after normalization
            self._solver.solve()

            # item's can be marked dirty due to normalization and solving
            if len(dirty_items) != len(self._dirty_items):
                dirty_items = sort(self._dirty_items, reverse=True)

            self._dirty_items.clear()

            self._post_update_items(dirty_items, context)

        except Exception as e:
            logging.error("Error while updating canvas", exc_info=e)

        assert (
            len(self._dirty_items) == 0 and len(self._dirty_matrix_items) == 0
        ), f"dirty: {self._dirty_items}; matrix: {self._dirty_matrix_items}"

        self._update_views(dirty_items, dirty_matrix_items)

    def update_matrices(self, items):
        """
        Recalculate matrices of the items. Items' children matrices
        are recalculated, too.

        Return items, which matrices were recalculated.
        """
        changed = set()
        for item in items:
            parent = self._tree.get_parent(item)
            if parent is not None and parent in items:
                # item's matrix will be updated thanks to parent's matrix
                # update
                continue

            self.update_matrix(item, parent)
            changed.add(item)

            changed_children = self.update_matrices(
                set(self.get_children(item)))
            changed.update(changed_children)

        return changed

    def update_matrix(self, item, parent=None):
        """
        Update matrices of an item.
        """
        try:
            orig_matrix_i2c = cairo.Matrix(*item._matrix_i2c)
        except:
            orig_matrix_i2c = None

        item._matrix_i2c = cairo.Matrix(*item.matrix)

        if parent is not None:
            try:
                item._matrix_i2c = item._matrix_i2c.multiply(
                    parent._matrix_i2c)
            except AttributeError:
                # Fall back to old behaviour
                item._matrix_i2c *= parent._matrix_i2c

        if orig_matrix_i2c is None or orig_matrix_i2c != item._matrix_i2c:
            # calculate c2i matrix and view matrices
            item._matrix_c2i = cairo.Matrix(*item._matrix_i2c)
            item._matrix_c2i.invert()

    def update_constraints(self, items):
        """
        Update constraints. Also variables may be marked as dirty
        before the constraint solver kicks in.
        """
        # request solving of external constraints associated with dirty items
        request_resolve = self._solver.request_resolve
        for item in items:
            for p in item._canvas_projections:
                request_resolve(p[0], projections_only=True)
                request_resolve(p[1], projections_only=True)

        # solve all constraints
        self._solver.solve()

    def _normalize(self, items):
        """
        Update handle positions of items, so the first handle is
        always located at (0, 0).

        Return those items, which matrices changed due to first handle
        movement.

        For example having an item

        >>> from gaphas.item import Element
        >>> c = Canvas()
        >>> e = Element()
        >>> c.add(e)
        >>> e.min_width = e.min_height = 0
        >>> c.update_now()
        >>> e.handles()
        [<Handle object on (0, 0)>, <Handle object on (10, 0)>, <Handle object on (10, 10)>, <Handle object on (0, 10)>]

        and moving its first handle a bit

        >>> e.handles()[0].pos.x += 1
        >>> list(map(float, e.handles()[0].pos))
        [1.0, 0.0]

        After normalization

        >>> c._normalize([e])          # doctest: +ELLIPSIS
        {<gaphas.item.Element object at 0x...>}
        >>> e.handles()
        [<Handle object on (0, 0)>, <Handle object on (9, 0)>, <Handle object on (9, 10)>, <Handle object on (0, 10)>]
        """
        dirty_matrix_items = set()
        for item in items:
            if item.normalize():
                dirty_matrix_items.add(item)

        return self.update_matrices(dirty_matrix_items)

    def update_index(self):
        """
        Provide each item in the canvas with an index attribute. This
        makes for fast searching of items.
        """
        self._tree.index_nodes("_canvas_index")

    def register_view(self, view):
        """
        Register a view on this canvas. This method is called when
        setting a canvas on a view and should not be called directly
        from user code.
        """
        self._registered_views.add(view)

    def unregister_view(self, view):
        """
        Unregister a view on this canvas. This method is called when
        setting a canvas on a view and should not be called directly
        from user code.
        """
        self._registered_views.discard(view)

    def _update_views(self,
                      dirty_items=(),
                      dirty_matrix_items=(),
                      removed_items=()):
        """
        Send an update notification to all registered views.
        """
        for v in self._registered_views:
            v.request_update(dirty_items, dirty_matrix_items, removed_items)

    def __getstate__(self):
        """
        Persist canvas. Dirty item sets and views are not saved.
        """
        d = dict(self.__dict__)
        for n in (
                "_dirty_items",
                "_dirty_matrix_items",
                "_dirty_index",
                "_registered_views",
        ):
            try:
                del d[n]
            except KeyError:
                pass
        return d

    def __setstate__(self, state):
        """
        Load persisted state.

        Before loading the state, the constructor is called.
        """
        self.__dict__.update(state)
        self._dirty_items = set(self._tree.nodes)
        self._dirty_matrix_items = set(self._tree.nodes)
        self._dirty_index = True
        self._registered_views = set()
        # self.update()

    def project(self, item, *points):
        """
        Project item's points into canvas coordinate system.

        If there is only one point returned than projected point is
        returned. If there are more than one points, then tuple of
        projected points is returned.
        """
        def reg(cp):
            item._canvas_projections.add(cp)
            return cp

        if len(points) == 1:
            return reg(CanvasProjection(points[0], item))
        elif len(points) > 1:
            return tuple(reg(CanvasProjection(p, item)) for p in points)
        else:
            raise AttributeError(
                "There should be at least one point specified")
Beispiel #3
0
class Matrix(object):
    """
    Matrix wrapper. This version sends @observed messages on state changes

    >>> cairo.Matrix()
    cairo.Matrix(1, 0, 0, 1, 0, 0)
    >>> Matrix()
    Matrix(1, 0, 0, 1, 0, 0)
    """
    def __init__(self, xx=1.0, yx=0.0, xy=0.0, yy=1.0, x0=0.0, y0=0.0):
        self._matrix = cairo.Matrix(xx, yx, xy, yy, x0, y0)

    @staticmethod
    def init_rotate(radians):
        return cairo.Matrix.init_rotate(radians)

    @observed
    def invert(self):
        return self._matrix.invert()

    @observed
    def rotate(self, radians):
        return self._matrix.rotate(radians)

    @observed
    def scale(self, sx, sy):
        return self._matrix.scale(sx, sy)

    @observed
    def translate(self, tx, ty):
        self._matrix.translate(tx, ty)

    @observed
    def multiply(self, m):
        return self._matrix.multiply(m)

    reversible_method(invert, invert)
    reversible_method(rotate, rotate, {"radians": lambda radians: -radians})
    reversible_method(scale, scale, {
        "sx": lambda sx: 1 / sx,
        "sy": lambda sy: 1 / sy
    })
    reversible_method(translate, translate, {
        "tx": lambda tx: -tx,
        "ty": lambda ty: -ty
    })

    def transform_distance(self, dx, dy):
        self._matrix.transform_distance(dx, dy)

    def transform_point(self, x, y):
        self._matrix.transform_point(x, y)

    def __eq__(self, other):
        return self._matrix.__eq__(other)

    def __ne__(self, other):
        return self._matrix.__ne__(other)

    def __le__(self, other):
        return self._matrix.__le__(other)

    def __lt__(self, other):
        return self._matrix.__lt__(other)

    def __ge__(self, other):
        return self._matrix.__ge__(other)

    def __gt__(self, other):
        return self._matrix.__gt__(other)

    def __getitem__(self, val):
        return self._matrix.__getitem__(val)

    @observed
    def __mul__(self, other):
        return self._matrix.__mul__(other)

    @observed
    def __rmul__(self, other):
        return self._matrix.__rmul__(other)

    def __repr__(self):
        return "Matrix(%g, %g, %g, %g, %g, %g)" % tuple(self._matrix)