source: orange/Orange/OrangeCanvas/canvas/layout.py @ 11207:8145a5984767

Revision 11207:8145a5984767, 4.6 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Fixed anchor point layout order reset while dragging (creating) a new link.

Line 
1"""
2Node/Link layout.
3
4"""
5from operator import attrgetter, add
6
7import numpy
8
9import sip
10
11from PyQt4.QtGui import QGraphicsObject
12from PyQt4.QtCore import QRectF, QLineF, QTimer
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            QTimer.singleShot(0, self.__delayedActivate)
138
139    def __delayedActivate(self):
140        if self.__layoutPending:
141            self.activate()
142
143
144def angle(point1, point2):
145    """Return the angle between the two points in range from -180 to 180.
146    """
147    angle = QLineF(point1, point2).angle()
148    if angle > 180:
149        return angle - 360
150    else:
151        return angle
Note: See TracBrowser for help on using the repository browser.