source: orange/Orange/OrangeCanvas/canvas/items/linkitem.py @ 11442:279d7a51ea1d

Revision 11442:279d7a51ea1d, 14.5 KB checked in by Ales Erjavec <ales.erjavec@…>, 13 months ago (diff)

Fixes to canvas package documentation.

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