source: orange/Orange/OrangeCanvas/canvas/layout.py @ 11463:c91eabaff9fe

Revision 11463:c91eabaff9fe, 4.8 KB checked in by Ales Erjavec <ales.erjavec@…>, 12 months ago (diff)

Using 'LayoutRequest' event to schedule an anchor layout.

This prevents the layout routine from being invoked after the AnchorLayout
object is deleted (Qt automatically clears the event queue for deleted
objects).

Line 
1"""
2Node/Link layout.
3
4"""
5from operator import attrgetter, add
6
7import numpy
8
9import sip
10
11from PyQt4.QtGui import QGraphicsObject, QApplication
12from PyQt4.QtCore import QRectF, QLineF, QEvent
13
14from .items import NodeItem, LinkItem, SourceAnchorItem, SinkAnchorItem
15from .items.utils import typed_signal_mapper, invert_permutation_indices, \
16                         linspace
17
18LinkItemSignalMapper = typed_signal_mapper(LinkItem)
19
20
21def composition(f, g):
22    """Return a composition of two functions
23    """
24    def fg(arg):
25        return g(f(arg))
26    return fg
27
28
29class AnchorLayout(QGraphicsObject):
30    def __init__(self, parent=None, **kwargs):
31        QGraphicsObject.__init__(self, parent, **kwargs)
32        self.setFlag(QGraphicsObject.ItemHasNoContents)
33
34        self.__layoutPending = False
35        self.__isActive = False
36        self.__invalidatedAnchors = []
37        self.__enabled = True
38
39    def boundingRect(self):
40        return QRectF()
41
42    def activate(self):
43        if self.isEnabled() and not self.__isActive:
44            self.__isActive = True
45            try:
46                self._doLayout()
47            finally:
48                self.__isActive = False
49                self.__layoutPending = False
50
51    def isActivated(self):
52        return self.__isActive
53
54    def _doLayout(self):
55        if not self.isEnabled():
56            return
57
58        scene = self.scene()
59        items = scene.items()
60        links = [item for item in items if isinstance(item, LinkItem)]
61        point_pairs = [(link.sourceAnchor, link.sinkAnchor) for link in links]
62        point_pairs.extend(map(reversed, point_pairs))
63        to_other = dict(point_pairs)
64
65        anchors = set(self.__invalidatedAnchors)
66
67        for anchor_item in anchors:
68            if sip.isdeleted(anchor_item):
69                continue
70
71            points = anchor_item.anchorPoints()
72            anchor_pos = anchor_item.mapToScene(anchor_item.pos())
73            others = [to_other[point] for point in points]
74
75            if isinstance(anchor_item, SourceAnchorItem):
76                others_angle = [-angle(anchor_pos, other.anchorScenePos())
77                                for other in others]
78            else:
79                others_angle = [angle(other.anchorScenePos(), anchor_pos)
80                                for other in others]
81
82            indices = list(numpy.argsort(others_angle))
83            # Invert the indices.
84            indices = invert_permutation_indices(indices)
85
86            positions = numpy.array(linspace(len(points)))
87            positions = list(positions[indices])
88
89            anchor_item.setAnchorPositions(positions)
90
91        self.__invalidatedAnchors = []
92
93    def invalidate(self):
94        items = self.scene().items()
95        nodes = [item for item in items is isinstance(item, NodeItem)]
96        anchors = reduce(add,
97                         [[node.outputAnchorItem, node.inputAnchorItem]
98                          for node in nodes],
99                         [])
100        self.__invalidatedAnchors.extend(anchors)
101        self.scheduleDelayedActivate()
102
103    def invalidateLink(self, link):
104        self.invalidateAnchorItem(link.sourceItem.outputAnchorItem)
105        self.invalidateAnchorItem(link.sinkItem.inputAnchorItem)
106
107        self.scheduleDelayedActivate()
108
109    def invalidateNode(self, node):
110        self.invalidateAnchorItem(node.inputAnchorItem)
111        self.invalidateAnchorItem(node.outputAnchorItem)
112
113        self.scheduleDelayedActivate()
114
115    def invalidateAnchorItem(self, anchor):
116        self.__invalidatedAnchors.append(anchor)
117
118        scene = self.scene()
119        if isinstance(anchor, SourceAnchorItem):
120            links = scene.node_output_links(anchor.parentNodeItem())
121            getter = composition(attrgetter("sinkItem"),
122                                 attrgetter("inputAnchorItem"))
123        elif isinstance(anchor, SinkAnchorItem):
124            links = scene.node_input_links(anchor.parentNodeItem())
125            getter = composition(attrgetter("sourceItem"),
126                                 attrgetter("outputAnchorItem"))
127        else:
128            raise TypeError(type(anchor))
129
130        self.__invalidatedAnchors.extend(map(getter, links))
131
132        self.scheduleDelayedActivate()
133
134    def scheduleDelayedActivate(self):
135        if self.isEnabled() and not self.__layoutPending:
136            self.__layoutPending = True
137            QApplication.postEvent(self, QEvent(QEvent.LayoutRequest))
138
139    def __delayedActivate(self):
140        if self.__layoutPending:
141            self.activate()
142
143    def event(self, event):
144        if event.type() == QEvent.LayoutRequest:
145            self.activate()
146            return True
147
148        return QGraphicsObject.event(self, event)
149
150
151def angle(point1, point2):
152    """Return the angle between the two points in range from -180 to 180.
153    """
154    angle = QLineF(point1, point2).angle()
155    if angle > 180:
156        return angle - 360
157    else:
158        return angle
Note: See TracBrowser for help on using the repository browser.