source: orange/Orange/OrangeCanvas/canvas/items/linkitem.py @ 11241:72cdee438307

Revision 11241:72cdee438307, 12.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 16 months ago (diff)

Fixed scene hit testing for mouse press events.

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