source: orange/Orange/OrangeCanvas/canvas/layout.py @ 11180:c0e3d8cdbd08

Revision 11180:c0e3d8cdbd08, 4.5 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Added link AnchorLayout class.

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