source: orange/Orange/OrangeCanvas/canvas/items/linkitem.py @ 11369:e9c95cc39be6

Revision 11369:e9c95cc39be6, 13.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 14 months ago (diff)

Added rst documentation for the canvas package.

Fixing docstrings in the process.

Line 
1"""
2Link Item
3
4"""
5
6from PyQt4.QtGui import (
7    QGraphicsItem, QGraphicsEllipseItem, QGraphicsPathItem, QGraphicsObject,
8    QGraphicsTextItem, QGraphicsDropShadowEffect, QPen, QBrush, QColor,
9    QPainterPath, 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    """
110    A Link in the canvas.
111    """
112
113    Z_VALUE = 0
114    """Z value of the item"""
115
116    def __init__(self, *args):
117        QGraphicsObject.__init__(self, *args)
118        self.setFlag(QGraphicsItem.ItemHasNoContents, True)
119        self.setAcceptedMouseButtons(Qt.RightButton | Qt.LeftButton)
120        self.setAcceptHoverEvents(True)
121
122        self.setZValue(self.Z_VALUE)
123
124        self.sourceItem = None
125        self.sourceAnchor = None
126        self.sinkItem = None
127        self.sinkAnchor = None
128
129        self.curveItem = LinkCurveItem(self)
130
131        self.sourceIndicator = LinkAnchorIndicator(self)
132        self.sinkIndicator = LinkAnchorIndicator(self)
133        self.sourceIndicator.hide()
134        self.sinkIndicator.hide()
135
136        self.linkTextItem = QGraphicsTextItem(self)
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        """
148        Set the source `item` (:class:`NodeItem`). Use `anchor`
149        (:class:`AnchorPoint`) as the curve start point (if ``None`` a new
150        output anchor will be created).
151
152        Setting item to ``None`` and a valid anchor is a valid operation
153        (for instance while mouse dragging one end of the link).
154
155        """
156        if item is not None and anchor is not None:
157            if anchor not in item.outputAnchors():
158                raise ValueError("Anchor must be belong to the item")
159
160        if self.sourceItem != item:
161            if self.sourceAnchor:
162                # Remove a previous source item and the corresponding anchor
163                self.sourceAnchor.scenePositionChanged.disconnect(
164                    self._sourcePosChanged
165                )
166
167                if self.sourceItem is not None:
168                    self.sourceItem.removeOutputAnchor(self.sourceAnchor)
169
170                self.sourceItem = self.sourceAnchor = None
171
172            self.sourceItem = item
173
174            if item is not None and anchor is None:
175                # Create a new output anchor for the item if none is provided.
176                anchor = item.newOutputAnchor()
177
178            # Update the visibility of the start point indicator.
179            self.sourceIndicator.setVisible(bool(item))
180
181        if anchor != self.sourceAnchor:
182            if self.sourceAnchor is not None:
183                self.sourceAnchor.scenePositionChanged.disconnect(
184                    self._sourcePosChanged
185                )
186
187            self.sourceAnchor = anchor
188
189            if self.sourceAnchor is not None:
190                self.sourceAnchor.scenePositionChanged.connect(
191                    self._sourcePosChanged
192                )
193
194        self.__updateCurve()
195
196    def setSinkItem(self, item, anchor=None):
197        """
198        Set the sink `item` (:class:`NodeItem`). 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        """
206        if item is not None and anchor is not None:
207            if anchor not in item.inputAnchors():
208                raise ValueError("Anchor must be belong to the item")
209
210        if self.sinkItem != item:
211            if self.sinkAnchor:
212                # Remove a previous source item and the corresponding anchor
213                self.sinkAnchor.scenePositionChanged.disconnect(
214                    self._sinkPosChanged
215                )
216
217                if self.sinkItem is not None:
218                    self.sinkItem.removeInputAnchor(self.sinkAnchor)
219
220                self.sinkItem = self.sinkAnchor = None
221
222            self.sinkItem = item
223
224            if item is not None and anchor is None:
225                # Create a new input anchor for the item if none is provided.
226                anchor = item.newInputAnchor()
227
228            # Update the visibility of the end point indicator.
229            self.sinkIndicator.setVisible(bool(item))
230
231        if self.sinkAnchor != anchor:
232            if self.sinkAnchor is not None:
233                self.sinkAnchor.scenePositionChanged.disconnect(
234                    self._sinkPosChanged
235                )
236
237            self.sinkAnchor = anchor
238
239            if self.sinkAnchor is not None:
240                self.sinkAnchor.scenePositionChanged.connect(
241                    self._sinkPosChanged
242                )
243
244        self.__updateCurve()
245
246    def setFont(self, font):
247        """
248        Set the font for the channel names text.
249        """
250        if font != self.font():
251            self.linkTextItem.setFont(font)
252            self.__updateText()
253
254    def font(self):
255        """
256        Return the font for the channel names text.
257        """
258        return self.linkTextItem.font()
259
260    def setChannelNamesVisible(self, visible):
261        """
262        Set the visibility of the channel name text.
263        """
264        self.linkTextItem.setVisible(visible)
265
266    def setSourceName(self, name):
267        """
268        Set the name of the source (used in channel name text).
269        """
270        if self.__sourceName != name:
271            self.__sourceName = name
272            self.__updateText()
273
274    def sourceName(self):
275        """
276        Return the source name.
277        """
278        return self.__sourceName
279
280    def setSinkName(self, name):
281        """
282        Set the name of the sink (used in channel name text).
283        """
284        if self.__sinkName != name:
285            self.__sinkName = name
286            self.__updateText()
287
288    def sinkName(self):
289        """
290        Return the sink name.
291        """
292        return self.__sinkName
293
294    def _sinkPosChanged(self, *arg):
295        self.__updateCurve()
296
297    def _sourcePosChanged(self, *arg):
298        self.__updateCurve()
299
300    def __updateCurve(self):
301        self.prepareGeometryChange()
302        if self.sourceAnchor and self.sinkAnchor:
303            source_pos = self.sourceAnchor.anchorScenePos()
304            sink_pos = self.sinkAnchor.anchorScenePos()
305            source_pos = self.curveItem.mapFromScene(source_pos)
306            sink_pos = self.curveItem.mapFromScene(sink_pos)
307            # TODO: get the orthogonal angle to the anchors path.
308            path = QPainterPath()
309            path.moveTo(source_pos)
310            path.cubicTo(source_pos + QPointF(60, 0),
311                         sink_pos - QPointF(60, 0),
312                         sink_pos)
313
314            self.curveItem.setPath(path)
315            self.sourceIndicator.setPos(source_pos)
316            self.sinkIndicator.setPos(sink_pos)
317            self.__updateText()
318        else:
319            self.setHoverState(False)
320            self.curveItem.setPath(QPainterPath())
321
322    def __updateText(self):
323        self.prepareGeometryChange()
324
325        if self.__sourceName or self.__sinkName:
326            if self.__sourceName != self.__sinkName:
327                text = u"{0} \u2192 {1}".format(self.__sourceName,
328                                                self.__sinkName)
329            else:
330                # If the names are the same show only one.
331                # Is this right? If the sink has two input channels of the
332                # same type having the name on the link help elucidate
333                # the scheme.
334                text = self.__sourceName
335        else:
336            text = ""
337
338        self.linkTextItem.setPlainText(text)
339
340        path = self.curveItem.path()
341        if not path.isEmpty():
342            center = path.pointAtPercent(0.5)
343            angle = path.angleAtPercent(0.5)
344
345            brect = self.linkTextItem.boundingRect()
346
347            transform = QTransform()
348            transform.translate(center.x(), center.y())
349            transform.rotate(-angle)
350
351            # Center and move above the curve path.
352            transform.translate(-brect.width() / 2, -brect.height())
353
354            self.linkTextItem.setTransform(transform)
355
356    def removeLink(self):
357        self.setSinkItem(None)
358        self.setSourceItem(None)
359        self.__updateCurve()
360
361    def setHoverState(self, state):
362        if self.hover != state:
363            self.prepareGeometryChange()
364            self.hover = state
365            self.sinkIndicator.setHoverState(state)
366            self.sourceIndicator.setHoverState(state)
367            self.curveItem.setHoverState(state)
368
369    def hoverEnterEvent(self, event):
370        # Hover enter event happens when the mouse enters any child object
371        # but we only want to show the 'hovered' shadow when the mouse
372        # is over the 'curveItem', so we install self as an event filter
373        # on the LinkCurveItem and listen to its hover events.
374        self.curveItem.installSceneEventFilter(self)
375        return QGraphicsObject.hoverEnterEvent(self, event)
376
377    def hoverLeaveEvent(self, event):
378        # Remove the event filter to prevent unnecessary work in
379        # scene event filter when not needed
380        self.curveItem.removeSceneEventFilter(self)
381        return QGraphicsObject.hoverLeaveEvent(self, event)
382
383    def sceneEventFilter(self, obj, event):
384        if obj is self.curveItem:
385            if event.type() == QEvent.GraphicsSceneHoverEnter:
386                self.setHoverState(True)
387            elif event.type() == QEvent.GraphicsSceneHoverLeave:
388                self.setHoverState(False)
389
390        return QGraphicsObject.sceneEventFilter(self, obj, event)
391
392    def boundingRect(self):
393        return self.childrenBoundingRect()
394
395    def shape(self):
396        return self.curveItem.shape()
397
398    def setEnabled(self, enabled):
399        """
400        Set link enabled state. When disabled the link is rendered with a
401        dashed line.
402
403        """
404        QGraphicsObject.setEnabled(self, enabled)
405
406    def setDynamicEnabled(self, enabled):
407        """
408        Set the link's dynamic enabled state.
409
410        If the link is `dynamic` it will be rendered in red/green color
411        respectively depending on the state of the dynamic enabled state.
412
413        """
414        if self.__dynamicEnabled != enabled:
415            self.__dynamicEnabled = enabled
416            if self.__dynamic:
417                self.__updatePen()
418
419    def isDynamicEnabled(self):
420        """
421        Is the link dynamic enabled.
422        """
423        return self.__dynamicEnabled
424
425    def setDynamic(self, dynamic):
426        """
427        Mark the link as dynamic (e.i. it responds to the
428        ``setDynamicEnabled``).
429
430        """
431        if self.__dynamic != dynamic:
432            self.__dynamic = dynamic
433            self.__updatePen()
434
435    def isDynamic(self):
436        """
437        Is the link dynamic.
438        """
439        return self.__dynamic
440
441    def __updatePen(self):
442        self.prepareGeometryChange()
443        if self.__dynamic:
444            if self.__dynamicEnabled:
445                color = QColor(0, 150, 0, 150)
446            else:
447                color = QColor(150, 0, 0, 150)
448
449            normal = QPen(QBrush(color), 2.0)
450            hover = QPen(QBrush(color.darker(120)), 2.1)
451        else:
452            normal = QPen(QBrush(QColor("#9CACB4")), 2.0)
453            hover = QPen(QBrush(QColor("#7D7D7D")), 2.1)
454
455        self.curveItem.setCurvePenSet(normal, hover)
Note: See TracBrowser for help on using the repository browser.