source: orange/Orange/OrangeCanvas/canvas/items/linkitem.py @ 11184:5532ef651afd

Revision 11184:5532ef651afd, 12.3 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Changed how hover events are handled in 'LinkItem'.

Line 
1"""
2Link Item
3
4"""
5
6from PyQt4.QtGui import (
7    QGraphicsItem, QGraphicsEllipseItem, QGraphicsPathItem, QGraphicsObject,
8    QGraphicsTextItem, QGraphicsDropShadowEffect, QPen, QBrush, QColor,
9    QPainterPath, QFont, QTransform
10)
11
12from PyQt4.QtCore import Qt, QPointF, QEvent
13
14from .nodeitem import SHADOW_COLOR
15
16
17class LinkCurveItem(QGraphicsPathItem):
18    """Link curve item. The main component of `LinkItem`.
19    """
20    def __init__(self, parent):
21        QGraphicsPathItem.__init__(self, parent)
22        assert(isinstance(parent, LinkItem))
23
24        self.__canvasLink = parent
25        self.setAcceptHoverEvents(True)
26        self.setAcceptedMouseButtons(Qt.RightButton)
27        self.setAcceptedMouseButtons(Qt.LeftButton)
28
29        self.shadow = QGraphicsDropShadowEffect(
30            blurRadius=5, color=QColor(SHADOW_COLOR),
31            offset=QPointF(0, 0)
32        )
33
34        self.normalPen = QPen(QBrush(QColor("#9CACB4")), 2.0)
35        self.hoverPen = QPen(QBrush(QColor("#7D7D7D")), 2.1)
36        self.setPen(self.normalPen)
37        self.setGraphicsEffect(self.shadow)
38        self.shadow.setEnabled(False)
39
40        self.__hover = False
41
42    def linkItem(self):
43        """Return the :class:`LinkItem` instance this curve belongs to.
44
45        """
46        return self.__canvasLink
47
48    def setHoverState(self, state):
49        self.__hover = state
50        self.__update()
51
52    def setCurvePenSet(self, pen, hoverPen):
53        if pen is not None:
54            self.normalPen = pen
55        if hoverPen is not None:
56            self.hoverPen = hoverPen
57        self.__update()
58
59    def itemChange(self, change, value):
60        if change == QGraphicsItem.ItemEnabledHasChanged:
61            # Update the pen style
62            self.__update()
63
64        return QGraphicsPathItem.itemChange(self, change, value)
65
66    def __update(self):
67        shadow_enabled = self.__hover
68        if self.shadow.isEnabled() != shadow_enabled:
69            self.shadow.setEnabled(shadow_enabled)
70
71        link_enabled = self.isEnabled()
72        if link_enabled:
73            pen_style = Qt.SolidLine
74        else:
75            pen_style = Qt.DashLine
76
77        if self.__hover:
78            pen = self.hoverPen
79        else:
80            pen = self.normalPen
81
82        pen.setStyle(pen_style)
83        self.setPen(pen)
84
85
86class LinkAnchorIndicator(QGraphicsEllipseItem):
87    """A visual indicator of the link anchor point at both ends
88    of the `LinkItem`.
89
90    """
91    def __init__(self, *args):
92        QGraphicsEllipseItem.__init__(self, *args)
93        self.setRect(-3, -3, 6, 6)
94        self.setPen(QPen(Qt.NoPen))
95        self.normalBrush = QBrush(QColor("#9CACB4"))
96        self.hoverBrush = QBrush(QColor("#7D7D7D"))
97        self.setBrush(self.normalBrush)
98        self.__hover = False
99
100    def setHoverState(self, state):
101        """The hover state is set by the LinkItem.
102        """
103        self.__hover = state
104        if state:
105            self.setBrush(self.hoverBrush)
106        else:
107            self.setBrush(self.normalBrush)
108
109
110class LinkItem(QGraphicsObject):
111    """A Link in the canvas.
112    """
113
114    Z_VALUE = 0
115    """Z value of the item"""
116
117    def __init__(self, *args):
118        QGraphicsObject.__init__(self, *args)
119        self.setFlag(QGraphicsItem.ItemHasNoContents, True)
120        self.setAcceptedMouseButtons(Qt.RightButton)
121        self.setAcceptHoverEvents(True)
122
123        self.setZValue(self.Z_VALUE)
124
125        self.sourceItem = None
126        self.sourceAnchor = None
127        self.sinkItem = None
128        self.sinkAnchor = None
129
130        self.curveItem = LinkCurveItem(self)
131
132        self.sourceIndicator = LinkAnchorIndicator(self)
133        self.sinkIndicator = LinkAnchorIndicator(self)
134        self.sourceIndicator.hide()
135        self.sinkIndicator.hide()
136
137        self.linkTextItem = QGraphicsTextItem(self)
138        self.linkTextItem.setFont(QFont("Helvetica", 11))
139
140        self.__sourceName = ""
141        self.__sinkName = ""
142
143        self.__dynamic = False
144        self.__dynamicEnabled = False
145
146        self.hover = False
147
148    def setSourceItem(self, item, anchor=None):
149        """Set the source `item` (:class:`CanvasNodeItem`). Use `anchor`
150        (:class:`AnchorPoint) as the curve start point (if `None` a new
151        output anchor will be created).
152
153        Setting item to `None` and a valid anchor is a valid operation
154        (for instance while mouse dragging one and of the link).
155
156        """
157        if item is not None and anchor is not None:
158            if anchor not in item.outputAnchors():
159                raise ValueError("Anchor must be belong to the item")
160
161        if self.sourceItem != item:
162            if self.sourceAnchor:
163                # Remove a previous source item and the corresponding anchor
164                self.sourceAnchor.scenePositionChanged.disconnect(
165                    self._sourcePosChanged
166                )
167
168                if self.sourceItem is not None:
169                    self.sourceItem.removeOutputAnchor(self.sourceAnchor)
170
171                self.sourceItem = self.sourceAnchor = None
172
173            self.sourceItem = item
174
175            if item is not None and anchor is None:
176                # Create a new output anchor for the item if none is provided.
177                anchor = item.newOutputAnchor()
178
179            # Update the visibility of the start point indicator.
180            self.sourceIndicator.setVisible(bool(item))
181
182        if anchor != self.sourceAnchor:
183            if self.sourceAnchor is not None:
184                self.sourceAnchor.scenePositionChanged.disconnect(
185                    self._sourcePosChanged
186                )
187
188            self.sourceAnchor = anchor
189
190            if self.sourceAnchor is not None:
191                self.sourceAnchor.scenePositionChanged.connect(
192                    self._sourcePosChanged
193                )
194
195        self.__updateCurve()
196
197    def setSinkItem(self, item, anchor=None):
198        """Set the sink `item` (:class:`CanvasNodeItem`). Use `anchor`
199        (:class:`AnchorPoint) as the curve end point (if `None` a new
200        input anchor will be created).
201
202        Setting item to `None` and a valid anchor is a valid operation
203        (for instance while mouse dragging one and of the link).
204        """
205        if item is not None and anchor is not None:
206            if anchor not in item.inputAnchors():
207                raise ValueError("Anchor must be belong to the item")
208
209        if self.sinkItem != item:
210            if self.sinkAnchor:
211                # Remove a previous source item and the corresponding anchor
212                self.sinkAnchor.scenePositionChanged.disconnect(
213                    self._sinkPosChanged
214                )
215
216                if self.sinkItem is not None:
217                    self.sinkItem.removeInputAnchor(self.sinkAnchor)
218
219                self.sinkItem = self.sinkAnchor = None
220
221            self.sinkItem = item
222
223            if item is not None and anchor is None:
224                # Create a new input anchor for the item if none is provided.
225                anchor = item.newInputAnchor()
226
227            # Update the visibility of the end point indicator.
228            self.sinkIndicator.setVisible(bool(item))
229
230        if self.sinkAnchor != anchor:
231            if self.sinkAnchor is not None:
232                self.sinkAnchor.scenePositionChanged.disconnect(
233                    self._sinkPosChanged
234                )
235
236            self.sinkAnchor = anchor
237
238            if self.sinkAnchor is not None:
239                self.sinkAnchor.scenePositionChanged.connect(
240                    self._sinkPosChanged
241                )
242
243        self.__updateCurve()
244
245    def setChannelNamesVisible(self, visible):
246        self.linkTextItem.setVisible(visible)
247
248    def setSourceName(self, name):
249        if self.__sourceName != name:
250            self.__sourceName = name
251            self.__updateText()
252
253    def sourceName(self):
254        return self.__sourceName
255
256    def setSinkName(self, name):
257        if self.__sinkName != name:
258            self.__sinkName = name
259            self.__updateText()
260
261    def sinkName(self):
262        return self.__sinkName
263
264    def _sinkPosChanged(self, *arg):
265        self.__updateCurve()
266
267    def _sourcePosChanged(self, *arg):
268        self.__updateCurve()
269
270    def __updateCurve(self):
271        self.prepareGeometryChange()
272        if self.sourceAnchor and self.sinkAnchor:
273            source_pos = self.sourceAnchor.anchorScenePos()
274            sink_pos = self.sinkAnchor.anchorScenePos()
275            source_pos = self.curveItem.mapFromScene(source_pos)
276            sink_pos = self.curveItem.mapFromScene(sink_pos)
277            # TODO: get the orthogonal angle to the anchors path.
278            path = QPainterPath()
279            path.moveTo(source_pos)
280            path.cubicTo(source_pos + QPointF(60, 0),
281                         sink_pos - QPointF(60, 0),
282                         sink_pos)
283
284            self.curveItem.setPath(path)
285            self.sourceIndicator.setPos(source_pos)
286            self.sinkIndicator.setPos(sink_pos)
287            self.__updateText()
288        else:
289            self.setHoverState(False)
290            self.curveItem.setPath(QPainterPath())
291
292    def __updateText(self):
293        self.prepareGeometryChange()
294
295        if self.__sourceName or self.__sinkName:
296            text = "{0} --> {1}".format(self.__sourceName, self.__sinkName)
297        else:
298            text = ""
299        self.linkTextItem.setPlainText(text)
300
301        path = self.curveItem.path()
302        if not path.isEmpty():
303            center = path.pointAtPercent(0.5)
304            angle = path.angleAtPercent(0.5)
305
306            brect = self.linkTextItem.boundingRect()
307
308            transform = QTransform()
309            transform.translate(center.x(), center.y())
310            transform.rotate(-angle)
311
312            # Center and move above the curve path.
313            transform.translate(-brect.width() / 2, -brect.height())
314
315            self.linkTextItem.setTransform(transform)
316
317    def removeLink(self):
318        self.setSinkItem(None)
319        self.setSourceItem(None)
320        self.__updateCurve()
321
322    def setHoverState(self, state):
323        if self.hover != state:
324            self.prepareGeometryChange()
325            self.hover = state
326            self.sinkIndicator.setHoverState(state)
327            self.sourceIndicator.setHoverState(state)
328            self.curveItem.setHoverState(state)
329
330    def hoverEnterEvent(self, event):
331        # Hover enter event happens when the mouse enters any child object
332        # but we only want to show the 'hovered' shadow when the mouse
333        # is over the 'curveItem', so we install self as an event filter
334        # on the item and listen to the hover events.
335        self.curveItem.installSceneEventFilter(self)
336        return QGraphicsObject.hoverEnterEvent(self, event)
337
338    def hoverLeaveEvent(self, event):
339        # Remove the event filter to prevent unnecessary work in
340        # scene event filter when not needed
341        self.curveItem.removeSceneEventFilter(self)
342        return QGraphicsObject.hoverLeaveEvent(self, event)
343
344    def sceneEventFilter(self, obj, event):
345        if obj is self.curveItem:
346            if event.type() == QEvent.GraphicsSceneHoverEnter:
347                self.setHoverState(True)
348            elif event.type() == QEvent.GraphicsSceneHoverLeave:
349                self.setHoverState(False)
350
351        return QGraphicsObject.sceneEventFilter(self, obj, event)
352
353    def boundingRect(self):
354        return self.childrenBoundingRect()
355
356    def shape(self):
357        return self.curveItem.shape()
358
359    def setEnabled(self, enabled):
360        QGraphicsObject.setEnabled(self, enabled)
361
362    def setDynamicEnabled(self, enabled):
363        if self.__dynamicEnabled != enabled:
364            self.__dynamicEnabled = enabled
365            if self.__dynamic:
366                self.__updatePen()
367
368    def isDynamicEnabled(self):
369        return self.__dynamicEnabled
370
371    def setDynamic(self, dynamic):
372        if self.__dynamic != dynamic:
373            self.__dynamic = dynamic
374            self.__updatePen()
375
376    def isDynamic(self):
377        return self.__dynamic
378
379    def __updatePen(self):
380        self.prepareGeometryChange()
381        if self.__dynamic:
382            if self.__dynamicEnabled:
383                color = QColor(0, 150, 0, 150)
384            else:
385                color = QColor(150, 0, 0, 150)
386
387            normal = QPen(QBrush(color), 2.0)
388            hover = QPen(QBrush(color.darker(120)), 2.1)
389        else:
390            normal = QPen(QBrush(QColor("#9CACB4")), 2.0)
391            hover = QPen(QBrush(QColor("#7D7D7D")), 2.1)
392
393        self.curveItem.setCurvePenSet(normal, hover)
Note: See TracBrowser for help on using the repository browser.