source: orange/Orange/OrangeCanvas/canvas/items/nodeitem.py @ 11614:a4aa7f9b09fd

Revision 11614:a4aa7f9b09fd, 36.5 KB checked in by Ales Erjavec <ales.erjavec@…>, 10 months ago (diff)

Refactored widget state messages handling.

Moved the resposibility for handling messages (OWBaseWidget.widgetStateChanged)
to WidgetsScheme.

Added support for messages in base SchemeNode class.

RevLine 
[11102]1"""
[11442]2=========
3Node Item
4=========
[11102]5
6"""
7
8from xml.sax.saxutils import escape
9
10from PyQt4.QtGui import (
[11411]11    QGraphicsItem, QGraphicsObject, QGraphicsTextItem,
12    QGraphicsDropShadowEffect, QGraphicsView,
[11369]13    QPen, QBrush, QColor, QPalette, QIcon, QStyle, QPainter,
14    QPainterPath, QPainterPathStroker, QApplication
[11102]15)
16
[11411]17from PyQt4.QtCore import Qt, QPointF, QRectF, QSize, QTimer, QPropertyAnimation
[11102]18from PyQt4.QtCore import pyqtSignal as Signal
19from PyQt4.QtCore import pyqtProperty as Property
20
[11293]21from .graphicspathobject import GraphicsPathObject
[11138]22from .utils import saturated, radial_gradient
[11102]23
[11614]24from ...scheme.node import UserMessage
[11102]25from ...registry import NAMED_COLORS
26from ...resources import icon_loader
[11207]27from .utils import uniform_linear_layout
[11102]28
29
30def create_palette(light_color, color):
[11369]31    """
32    Return a new :class:`QPalette` from for the :class:`NodeBodyItem`.
[11102]33    """
34    palette = QPalette()
35
36    palette.setColor(QPalette.Inactive, QPalette.Light,
37                     saturated(light_color, 50))
38    palette.setColor(QPalette.Inactive, QPalette.Midlight,
39                     saturated(light_color, 90))
40    palette.setColor(QPalette.Inactive, QPalette.Button,
41                     light_color)
42
43    palette.setColor(QPalette.Active, QPalette.Light,
44                     saturated(color, 50))
45    palette.setColor(QPalette.Active, QPalette.Midlight,
46                     saturated(color, 90))
47    palette.setColor(QPalette.Active, QPalette.Button,
48                     color)
49    palette.setColor(QPalette.ButtonText, QColor("#515151"))
50    return palette
51
52
53def default_palette():
[11369]54    """
55    Create and return a default palette for a node.
[11102]56    """
[11440]57    return create_palette(QColor(NAMED_COLORS["light-yellow"]),
58                          QColor(NAMED_COLORS["yellow"]))
[11102]59
60
[11411]61def animation_restart(animation):
62    if animation.state() == QPropertyAnimation.Running:
63        animation.pause()
64    animation.start()
65
66
[11102]67SHADOW_COLOR = "#9CACB4"
68FOCUS_OUTLINE_COLOR = "#609ED7"
69
70
[11411]71class NodeBodyItem(GraphicsPathObject):
[11369]72    """
73    The central part (body) of the `NodeItem`.
[11102]74    """
75    def __init__(self, parent=None):
[11411]76        GraphicsPathObject.__init__(self, parent)
[11102]77        assert(isinstance(parent, NodeItem))
78
79        self.__processingState = 0
80        self.__progress = -1
[11411]81        self.__animationEnabled = False
[11102]82        self.__isSelected = False
83        self.__hasFocus = False
84        self.__hover = False
85        self.__shapeRect = QRectF(-10, -10, 20, 20)
86
87        self.setAcceptHoverEvents(True)
88
89        self.setFlag(QGraphicsItem.ItemSendsScenePositionChanges, True)
90        self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
91
92        self.setPen(QPen(Qt.NoPen))
93
94        self.setPalette(default_palette())
95
96        self.shadow = QGraphicsDropShadowEffect(
[11411]97            blurRadius=3,
[11102]98            color=QColor(SHADOW_COLOR),
99            offset=QPointF(0, 0),
100            )
101
102        self.setGraphicsEffect(self.shadow)
[11411]103        self.shadow.setEnabled(True)
104
105        self.__blurAnimation = QPropertyAnimation(self.shadow, "blurRadius",
106                                                  self)
107        self.__blurAnimation.setDuration(100)
108        self.__blurAnimation.finished.connect(self.__on_finished)
109
110        self.__pingAnimation = QPropertyAnimation(self, "scale", self)
111        self.__pingAnimation.setDuration(250)
112        self.__pingAnimation.setKeyValues([(0.0, 1.0), (0.5, 1.1), (1.0, 1.0)])
[11102]113
114    # TODO: The body item should allow the setting of arbitrary painter
115    # paths (for instance rounded rect, ...)
116    def setShapeRect(self, rect):
[11369]117        """
118        Set the item's shape `rect`. The item should be confined within
[11102]119        this rect.
120
121        """
122        path = QPainterPath()
123        path.addEllipse(rect)
124        self.setPath(path)
125        self.__shapeRect = rect
126
127    def setPalette(self, palette):
[11369]128        """
129        Set the body color palette (:class:`QPalette`).
[11102]130        """
131        self.palette = palette
132        self.__updateBrush()
133
[11411]134    def setAnimationEnabled(self, enabled):
135        """
136        Set the node animation enabled.
137        """
138        if self.__animationEnabled != enabled:
139            self.__animationEnabled = enabled
140
[11102]141    def setProcessingState(self, state):
[11369]142        """
143        Set the processing state of the node.
[11102]144        """
[11411]145        if self.__processingState != state:
146            self.__processingState = state
147            if not state and self.__animationEnabled:
148                self.ping()
[11102]149
150    def setProgress(self, progress):
[11369]151        """
152        Set the progress indicator state of the node. `progress` should
153        be a number between 0 and 100.
154
155        """
[11102]156        self.__progress = progress
157        self.update()
158
[11411]159    def ping(self):
160        """
161        Trigger a 'ping' animation.
162        """
163        animation_restart(self.__pingAnimation)
164
[11102]165    def hoverEnterEvent(self, event):
166        self.__hover = True
167        self.__updateShadowState()
[11411]168        return GraphicsPathObject.hoverEnterEvent(self, event)
[11102]169
170    def hoverLeaveEvent(self, event):
171        self.__hover = False
172        self.__updateShadowState()
[11411]173        return GraphicsPathObject.hoverLeaveEvent(self, event)
[11102]174
175    def paint(self, painter, option, widget):
[11369]176        """
177        Paint the shape and a progress meter.
[11102]178        """
179        # Let the default implementation draw the shape
180        if option.state & QStyle.State_Selected:
181            # Prevent the default bounding rect selection indicator.
182            option.state = option.state ^ QStyle.State_Selected
[11411]183        GraphicsPathObject.paint(self, painter, option, widget)
[11102]184
185        if self.__progress >= 0:
186            # Draw the progress meter over the shape.
187            # Set the clip to shape so the meter does not overflow the shape.
188            painter.setClipPath(self.shape(), Qt.ReplaceClip)
189            color = self.palette.color(QPalette.ButtonText)
190            pen = QPen(color, 5)
191            painter.save()
192            painter.setPen(pen)
193            painter.setRenderHints(QPainter.Antialiasing)
194            span = int(self.__progress * 57.60)
195            painter.drawArc(self.__shapeRect, 90 * 16, -span)
196            painter.restore()
197
198    def __updateShadowState(self):
199        if self.__hasFocus:
200            color = QColor(FOCUS_OUTLINE_COLOR)
201            self.setPen(QPen(color, 1.5))
202        else:
203            self.setPen(QPen(Qt.NoPen))
204
[11411]205        radius = 3
[11102]206        enabled = False
[11411]207
[11102]208        if self.__isSelected:
209            enabled = True
[11411]210            radius = 7
211
212        if self.__hover:
213            radius = 17
[11102]214            enabled = True
[11411]215
216        if enabled and not self.shadow.isEnabled():
217            self.shadow.setEnabled(enabled)
218
219        if self.__animationEnabled:
220            if self.__blurAnimation.state() == QPropertyAnimation.Running:
221                self.__blurAnimation.pause()
222
223            self.__blurAnimation.setStartValue(self.shadow.blurRadius())
224            self.__blurAnimation.setEndValue(radius)
225            self.__blurAnimation.start()
226        else:
227            self.shadow.setBlurRadius(radius)
[11102]228
229    def __updateBrush(self):
230        palette = self.palette
231        if self.__isSelected:
232            cg = QPalette.Active
233        else:
234            cg = QPalette.Inactive
235
236        palette.setCurrentColorGroup(cg)
237        c1 = palette.color(QPalette.Light)
238        c2 = palette.color(QPalette.Button)
239        grad = radial_gradient(c2, c1)
240        self.setBrush(QBrush(grad))
241
242    # TODO: The selected and focus states should be set using the
243    # QStyle flags (State_Selected. State_HasFocus)
244
245    def setSelected(self, selected):
[11369]246        """
247        Set the `selected` state.
[11102]248
[11369]249        .. note:: The item does not have `QGraphicsItem.ItemIsSelectable` flag.
[11102]250                  This property is instead controlled by the parent NodeItem.
251
252        """
253        self.__isSelected = selected
254        self.__updateBrush()
255
256    def setHasFocus(self, focus):
[11369]257        """
258        Set the `has focus` state.
[11102]259
[11369]260        .. note:: The item does not have `QGraphicsItem.ItemIsFocusable` flag.
[11102]261                  This property is instead controlled by the parent NodeItem.
[11369]262
[11102]263        """
264        self.__hasFocus = focus
265        self.__updateShadowState()
266
[11411]267    def __on_finished(self):
268        if self.shadow.blurRadius() == 0:
269            self.shadow.setEnabled(False)
270
[11102]271
[11138]272class AnchorPoint(QGraphicsObject):
[11369]273    """
274    A anchor indicator on the :class:`NodeAnchorItem`.
[11138]275    """
276
[11442]277    #: Signal emitted when the item's scene position changes.
[11138]278    scenePositionChanged = Signal(QPointF)
[11369]279
[11442]280    #: Signal emitted when the item's `anchorDirection` changes.
[11138]281    anchorDirectionChanged = Signal(QPointF)
282
283    def __init__(self, *args):
284        QGraphicsObject.__init__(self, *args)
285        self.setFlag(QGraphicsItem.ItemSendsScenePositionChanges, True)
286        self.setFlag(QGraphicsItem.ItemHasNoContents, True)
287
288        self.__direction = QPointF()
289
290    def anchorScenePos(self):
[11369]291        """
292        Return anchor position in scene coordinates.
[11138]293        """
294        return self.mapToScene(QPointF(0, 0))
295
296    def setAnchorDirection(self, direction):
[11369]297        """
298        Set the preferred direction (QPointF) in item coordinates.
[11138]299        """
300        if self.__direction != direction:
301            self.__direction = direction
302            self.anchorDirectionChanged.emit(direction)
303
304    def anchorDirection(self):
[11369]305        """
306        Return the preferred anchor direction.
[11138]307        """
308        return self.__direction
309
310    def itemChange(self, change, value):
311        if change == QGraphicsItem.ItemScenePositionHasChanged:
312            self.scenePositionChanged.emit(value.toPointF())
313
314        return QGraphicsObject.itemChange(self, change, value)
315
316    def boundingRect(self,):
317        return QRectF()
318
319
[11293]320class NodeAnchorItem(GraphicsPathObject):
[11369]321    """
322    The left/right widget input/output anchors.
[11102]323    """
324
[11153]325    def __init__(self, parent, *args):
[11293]326        GraphicsPathObject.__init__(self, parent, *args)
[11102]327        self.setAcceptHoverEvents(True)
328        self.setPen(QPen(Qt.NoPen))
329        self.normalBrush = QBrush(QColor("#CDD5D9"))
330        self.connectedBrush = QBrush(QColor("#9CACB4"))
331        self.setBrush(self.normalBrush)
332
333        self.shadow = QGraphicsDropShadowEffect(
334            blurRadius=10,
335            color=QColor(SHADOW_COLOR),
336            offset=QPointF(0, 0)
337        )
338
339        self.setGraphicsEffect(self.shadow)
340        self.shadow.setEnabled(False)
341
342        # Does this item have any anchored links.
343        self.anchored = False
[11138]344
[11153]345        if isinstance(parent, NodeItem):
346            self.__parentNodeItem = parent
347        else:
348            self.__parentNodeItem = None
349
[11138]350        self.__anchorPath = QPainterPath()
351        self.__points = []
352        self.__pointPositions = []
353
[11102]354        self.__fullStroke = None
355        self.__dottedStroke = None
[11331]356        self.__shape = None
[11102]357
[11153]358    def parentNodeItem(self):
[11369]359        """
360        Return a parent :class:`NodeItem` or ``None`` if this anchor's
361        parent is not a :class:`NodeItem` instance.
[11153]362
363        """
364        return self.__parentNodeItem
365
[11102]366    def setAnchorPath(self, path):
[11369]367        """
368        Set the anchor's curve path as a :class:`QPainterPath`.
[11102]369        """
[11138]370        self.__anchorPath = path
[11102]371        # Create a stroke of the path.
372        stroke_path = QPainterPathStroker()
373        stroke_path.setCapStyle(Qt.RoundCap)
[11331]374
375        # Shape is wider (bigger mouse hit area - should be settable)
376        stroke_path.setWidth(9)
377        self.__shape = stroke_path.createStroke(path)
378
379        # The full stroke
[11102]380        stroke_path.setWidth(3)
381        self.__fullStroke = stroke_path.createStroke(path)
382
383        # The dotted stroke (when not connected to anything)
384        stroke_path.setDashPattern(Qt.DotLine)
385        self.__dottedStroke = stroke_path.createStroke(path)
386
387        if self.anchored:
388            self.setPath(self.__fullStroke)
389            self.setBrush(self.connectedBrush)
390        else:
391            self.setPath(self.__dottedStroke)
392            self.setBrush(self.normalBrush)
393
[11138]394    def anchorPath(self):
[11369]395        """
396        Return the anchor path (:class:`QPainterPath`). This is a curve on
397        which the anchor points lie.
[11138]398
399        """
400        return self.__anchorPath
401
[11102]402    def setAnchored(self, anchored):
[11369]403        """
404        Set the items anchored state. When ``False`` the item draws it self
[11138]405        with a dotted stroke.
406
[11102]407        """
408        self.anchored = anchored
409        if anchored:
410            self.setPath(self.__fullStroke)
411            self.setBrush(self.connectedBrush)
412        else:
413            self.setPath(self.__dottedStroke)
414            self.setBrush(self.normalBrush)
415
416    def setConnectionHint(self, hint=None):
[11369]417        """
418        Set the connection hint. This can be used to indicate if
[11102]419        a connection can be made or not.
420
421        """
422        raise NotImplementedError
423
[11138]424    def count(self):
[11369]425        """
426        Return the number of anchor points.
[11138]427        """
428        return len(self.__points)
429
430    def addAnchor(self, anchor, position=0.5):
[11369]431        """
432        Add a new :class:`AnchorPoint` to this item and return it's index.
433
434        The `position` specifies where along the `anchorPath` is the new
435        point inserted.
436
[11138]437        """
438        return self.insertAnchor(self.count(), anchor, position)
439
440    def insertAnchor(self, index, anchor, position=0.5):
[11369]441        """
442        Insert a new :class:`AnchorPoint` at `index`.
443
444        See also
445        --------
446        NodeAnchorItem.addAnchor
447
[11138]448        """
449        if anchor in self.__points:
450            raise ValueError("%s already added." % anchor)
451
452        self.__points.insert(index, anchor)
453        self.__pointPositions.insert(index, position)
454
455        anchor.setParentItem(self)
456        anchor.setPos(self.__anchorPath.pointAtPercent(position))
457        anchor.destroyed.connect(self.__onAnchorDestroyed)
458
459        self.__updatePositions()
460
461        self.setAnchored(bool(self.__points))
462
463        return index
464
465    def removeAnchor(self, anchor):
[11369]466        """
467        Remove and delete the anchor point.
[11138]468        """
469        anchor = self.takeAnchor(anchor)
470
471        anchor.hide()
472        anchor.setParentItem(None)
473        anchor.deleteLater()
474
475    def takeAnchor(self, anchor):
[11369]476        """
477        Remove the anchor but don't delete it.
[11138]478        """
479        index = self.__points.index(anchor)
480
481        del self.__points[index]
482        del self.__pointPositions[index]
483
484        anchor.destroyed.disconnect(self.__onAnchorDestroyed)
485
486        self.__updatePositions()
487
488        self.setAnchored(bool(self.__points))
489
490        return anchor
491
492    def __onAnchorDestroyed(self, anchor):
493        try:
494            index = self.__points.index(anchor)
495        except ValueError:
496            return
497
498        del self.__points[index]
499        del self.__pointPositions[index]
500
501    def anchorPoints(self):
[11369]502        """
503        Return a list of anchor points.
[11138]504        """
505        return list(self.__points)
506
507    def anchorPoint(self, index):
[11369]508        """
509        Return the anchor point at `index`.
[11138]510        """
511        return self.__points[index]
512
513    def setAnchorPositions(self, positions):
[11369]514        """
515        Set the anchor positions in percentages (0..1) along the path curve.
[11138]516        """
517        if self.__pointPositions != positions:
[11207]518            self.__pointPositions = list(positions)
[11138]519
520            self.__updatePositions()
521
522    def anchorPositions(self):
[11369]523        """
524        Return the positions of anchor points as a list of floats where
[11138]525        each float is between 0 and 1 and specifies where along the anchor
526        path does the point lie (0 is at start 1 is at the end).
527
528        """
[11207]529        return list(self.__pointPositions)
[11138]530
[11102]531    def shape(self):
[11331]532        if self.__shape is not None:
533            return self.__shape
[11102]534        else:
[11293]535            return GraphicsPathObject.shape(self)
[11102]536
[11331]537    def boundingRect(self):
538        if self.__shape is not None:
539            return self.__shape.controlPointRect()
540        else:
541            return GraphicsPathObject.boundingRect(self)
542
[11102]543    def hoverEnterEvent(self, event):
544        self.shadow.setEnabled(True)
[11293]545        return GraphicsPathObject.hoverEnterEvent(self, event)
[11102]546
547    def hoverLeaveEvent(self, event):
548        self.shadow.setEnabled(False)
[11293]549        return GraphicsPathObject.hoverLeaveEvent(self, event)
[11102]550
[11138]551    def __updatePositions(self):
552        """Update anchor points positions.
553        """
554        for point, t in zip(self.__points, self.__pointPositions):
555            pos = self.__anchorPath.pointAtPercent(t)
556            point.setPos(pos)
557
[11102]558
559class SourceAnchorItem(NodeAnchorItem):
[11369]560    """
561    A source anchor item
[11102]562    """
563    pass
564
565
566class SinkAnchorItem(NodeAnchorItem):
[11369]567    """
568    A sink anchor item.
[11102]569    """
570    pass
571
572
[11186]573def standard_icon(standard_pixmap):
[11369]574    """
575    Return return the application style's standard icon for a
[11186]576    `QStyle.StandardPixmap`.
[11134]577
[11186]578    """
579    style = QApplication.instance().style()
580    return style.standardIcon(standard_pixmap)
[11134]581
[11186]582
583class GraphicsIconItem(QGraphicsItem):
[11369]584    """
585    A graphics item displaying an :class:`QIcon`.
[11186]586    """
587    def __init__(self, parent=None, icon=None, iconSize=None, **kwargs):
588        QGraphicsItem.__init__(self, parent, **kwargs)
589        self.setFlag(QGraphicsItem.ItemUsesExtendedStyleOption, True)
590
591        if icon is None:
592            icon = QIcon()
593
594        if iconSize is None:
595            style = QApplication.instance().style()
596            size = style.pixelMetric(style.PM_LargeIconSize)
597            iconSize = QSize(size, size)
598
599        self.__transformationMode = Qt.SmoothTransformation
600
601        self.__iconSize = QSize(iconSize)
602        self.__icon = QIcon(icon)
603
604    def setIcon(self, icon):
[11369]605        """
606        Set the icon (:class:`QIcon`).
[11186]607        """
608        if self.__icon != icon:
609            self.__icon = QIcon(icon)
610            self.update()
611
612    def icon(self):
[11369]613        """
614        Return the icon (:class:`QIcon`).
[11186]615        """
616        return QIcon(self.__icon)
617
618    def setIconSize(self, size):
[11369]619        """
620        Set the icon (and this item's) size (:class:`QSize`).
[11186]621        """
622        if self.__iconSize != size:
623            self.prepareGeometryChange()
624            self.__iconSize = QSize(size)
625            self.update()
626
627    def iconSize(self):
[11369]628        """
629        Return the icon size (:class:`QSize`).
[11186]630        """
631        return QSize(self.__iconSize)
632
633    def setTransformationMode(self, mode):
[11369]634        """
635        Set pixmap transformation mode. (`Qt.SmoothTransformation` or
[11186]636        `Qt.FastTransformation`).
637
638        """
639        if self.__transformationMode != mode:
640            self.__transformationMode = mode
641            self.update()
642
643    def transformationMode(self):
[11369]644        """
645        Return the pixmap transformation mode.
[11186]646        """
647        return self.__transformationMode
648
649    def boundingRect(self):
650        return QRectF(0, 0, self.__iconSize.width(), self.__iconSize.height())
651
652    def paint(self, painter, option, widget=None):
653        if not self.__icon.isNull():
654            if option.state & QStyle.State_Selected:
655                mode = QIcon.Selected
656            elif option.state & QStyle.State_Enabled:
657                mode = QIcon.Normal
658            elif option.state & QStyle.State_Active:
659                mode = QIcon.Active
660            else:
661                mode = QIcon.Disabled
662
663            transform = self.sceneTransform()
664
665            if widget is not None:
666                # 'widget' is the QGraphicsView.viewport()
667                view = widget.parent()
668                if isinstance(view, QGraphicsView):
669                    # Combine the scene transform with the view transform.
670                    view_transform = view.transform()
671                    transform = view_transform * view_transform
672
673            lod = option.levelOfDetailFromTransform(transform)
674
675            w, h = self.__iconSize.width(), self.__iconSize.height()
676            target = QRectF(0, 0, w, h)
677            source = QRectF(0, 0, w * lod, w * lod).toRect()
678
679            # The actual size of the requested pixmap can be smaller.
680            size = self.__icon.actualSize(source.size(), mode=mode)
681            source.setSize(size)
682
683            pixmap = self.__icon.pixmap(source.size(), mode=mode)
684
685            painter.setRenderHint(
686                QPainter.SmoothPixmapTransform,
687                self.__transformationMode == Qt.SmoothTransformation
688            )
689
690            painter.drawPixmap(target, pixmap, QRectF(source))
[11134]691
692
[11102]693class NodeItem(QGraphicsObject):
[11369]694    """
695    An widget node item in the canvas.
[11102]696    """
697
[11442]698    #: Signal emitted when the scene position of the node has changed.
[11102]699    positionChanged = Signal()
700
[11442]701    #: Signal emitted when the geometry of the channel anchors changes.
[11102]702    anchorGeometryChanged = Signal()
703
[11442]704    #: Signal emitted when the item has been activated (by a mouse double
705    #: click or a keyboard)
[11102]706    activated = Signal()
707
[11442]708    #: The item is under the mouse.
[11102]709    hovered = Signal()
710
[11369]711    #: Span of the anchor in degrees
[11102]712    ANCHOR_SPAN_ANGLE = 90
713
[11369]714    #: Z value of the item
[11102]715    Z_VALUE = 100
716
717    def __init__(self, widget_description=None, parent=None, **kwargs):
718        QGraphicsObject.__init__(self, parent, **kwargs)
719        self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
720        self.setFlag(QGraphicsItem.ItemHasNoContents, True)
721        self.setFlag(QGraphicsItem.ItemIsSelectable, True)
722        self.setFlag(QGraphicsItem.ItemIsMovable, True)
723        self.setFlag(QGraphicsItem.ItemIsFocusable, True)
724
[11134]725        # central body shape item
726        self.shapeItem = None
727
728        # in/output anchor items
729        self.inputAnchorItem = None
730        self.outputAnchorItem = None
731
732        # title text item
733        self.captionTextItem = None
734
735        # error, warning, info items
736        self.errorItem = None
737        self.warningItem = None
738        self.infoItem = None
739
[11102]740        self.__title = ""
741        self.__processingState = 0
742        self.__progress = -1
743
[11134]744        self.__error = None
745        self.__warning = None
746        self.__info = None
747
[11138]748        self.__anchorLayout = None
[11411]749        self.__animationEnabled = False
[11138]750
[11102]751        self.setZValue(self.Z_VALUE)
752        self.setupGraphics()
753
754        self.setWidgetDescription(widget_description)
755
756    @classmethod
757    def from_node(cls, node):
[11369]758        """
759        Create an :class:`NodeItem` instance and initialize it from a
760        :class:`SchemeNode` instance.
[11102]761
762        """
763        self = cls()
764        self.setWidgetDescription(node.description)
765#        self.setCategoryDescription(node.category)
766        return self
767
768    @classmethod
769    def from_node_meta(cls, meta_description):
[11369]770        """
771        Create an `NodeItem` instance from a node meta description.
[11102]772        """
773        self = cls()
774        self.setWidgetDescription(meta_description)
775        return self
776
777    def setupGraphics(self):
[11369]778        """
779        Set up the graphics.
[11102]780        """
781        shape_rect = QRectF(-24, -24, 48, 48)
782
783        self.shapeItem = NodeBodyItem(self)
784        self.shapeItem.setShapeRect(shape_rect)
[11411]785        self.shapeItem.setAnimationEnabled(self.__animationEnabled)
[11102]786
787        # Rect for widget's 'ears'.
788        anchor_rect = QRectF(-31, -31, 62, 62)
789        self.inputAnchorItem = SinkAnchorItem(self)
790        input_path = QPainterPath()
791        start_angle = 180 - self.ANCHOR_SPAN_ANGLE / 2
792        input_path.arcMoveTo(anchor_rect, start_angle)
793        input_path.arcTo(anchor_rect, start_angle, self.ANCHOR_SPAN_ANGLE)
794        self.inputAnchorItem.setAnchorPath(input_path)
795
796        self.outputAnchorItem = SourceAnchorItem(self)
797        output_path = QPainterPath()
798        start_angle = self.ANCHOR_SPAN_ANGLE / 2
799        output_path.arcMoveTo(anchor_rect, start_angle)
800        output_path.arcTo(anchor_rect, start_angle, - self.ANCHOR_SPAN_ANGLE)
801        self.outputAnchorItem.setAnchorPath(output_path)
802
803        self.inputAnchorItem.hide()
804        self.outputAnchorItem.hide()
805
806        # Title caption item
807        self.captionTextItem = QGraphicsTextItem(self)
808        self.captionTextItem.setPlainText("")
809        self.captionTextItem.setPos(0, 33)
810
[11186]811        def iconItem(standard_pixmap):
812            item = GraphicsIconItem(self, icon=standard_icon(standard_pixmap),
813                                    iconSize=QSize(16, 16))
814            item.hide()
815            return item
[11134]816
[11186]817        self.errorItem = iconItem(QStyle.SP_MessageBoxCritical)
818        self.warningItem = iconItem(QStyle.SP_MessageBoxWarning)
819        self.infoItem = iconItem(QStyle.SP_MessageBoxInformation)
[11134]820
[11369]821    # TODO: Remove the set[Widget|Category]Description. The user should
822    # handle setting of icons, title, ...
[11102]823    def setWidgetDescription(self, desc):
[11369]824        """
825        Set widget description.
[11102]826        """
827        self.widget_description = desc
828        if desc is None:
829            return
830
831        icon = icon_loader.from_description(desc).get(desc.icon)
832        if icon:
833            self.setIcon(icon)
834
835        if not self.title():
836            self.setTitle(desc.name)
837
838        if desc.inputs:
839            self.inputAnchorItem.show()
840        if desc.outputs:
841            self.outputAnchorItem.show()
842
843        tooltip = NodeItem_toolTipHelper(self)
844        self.setToolTip(tooltip)
845
846    def setWidgetCategory(self, desc):
[11369]847        """
848        Set the widget category.
849        """
[11102]850        self.category_description = desc
851        if desc and desc.background:
852            background = NAMED_COLORS.get(desc.background, desc.background)
853            color = QColor(background)
854            if color.isValid():
855                self.setColor(color)
856
857    def setIcon(self, icon):
[11369]858        """
[11442]859        Set the node item's icon (:class:`QIcon`).
[11102]860        """
861        if isinstance(icon, QIcon):
[11186]862            self.icon_item = GraphicsIconItem(self.shapeItem, icon=icon,
863                                              iconSize=QSize(36, 36))
864            self.icon_item.setPos(-18, -18)
[11102]865        else:
866            raise TypeError
867
868    def setColor(self, color, selectedColor=None):
[11369]869        """
870        Set the widget color.
[11102]871        """
872        if selectedColor is None:
873            selectedColor = saturated(color, 150)
874        palette = create_palette(color, selectedColor)
875        self.shapeItem.setPalette(palette)
876
[11369]877    def setPalette(self, palette):
878        # TODO: The palette should override the `setColor`
879        raise NotImplementedError
[11102]880
881    def setTitle(self, title):
[11369]882        """
883        Set the node title. The title text is displayed at the bottom of the
884        node.
885
[11102]886        """
887        self.__title = title
888        self.__updateTitleText()
889
890    def title(self):
[11369]891        """
892        Return the node title.
893        """
[11102]894        return self.__title
895
[11369]896    title_ = Property(unicode, fget=title, fset=setTitle,
897                      doc="Node title text.")
[11102]898
[11343]899    def setFont(self, font):
900        """
[11369]901        Set the title text font (:class:`QFont`).
[11343]902        """
903        if font != self.font():
904            self.prepareGeometryChange()
905            self.captionTextItem.setFont(font)
906            self.__updateTitleText()
907
908    def font(self):
909        """
910        Return the title text font.
911        """
912        return self.captionTextItem.font()
913
[11411]914    def setAnimationEnabled(self, enabled):
915        """
916        Set the node animation enabled state.
917        """
918        if self.__animationEnabled != enabled:
919            self.__animationEnabled = enabled
920            self.shapeItem.setAnimationEnabled(enabled)
921
922    def animationEnabled(self):
923        """
924        Are node animations enabled.
925        """
926        return self.__animationEnabled
927
[11102]928    def setProcessingState(self, state):
[11369]929        """
930        Set the node processing state i.e. the node is processing
[11102]931        (is busy) or is idle.
932
933        """
934        if self.__processingState != state:
935            self.__processingState = state
936            self.shapeItem.setProcessingState(state)
937            if not state:
938                # Clear the progress meter.
939                self.setProgress(-1)
[11411]940                if self.__animationEnabled:
941                    self.shapeItem.ping()
[11102]942
943    def processingState(self):
[11369]944        """
945        The node processing state.
946        """
[11102]947        return self.__processingState
948
949    processingState_ = Property(int, fget=processingState,
950                                fset=setProcessingState)
951
952    def setProgress(self, progress):
[11369]953        """
954        Set the node work progress state (number between 0 and 100).
[11102]955        """
956        if progress is None or progress < 0:
957            progress = -1
958
959        progress = max(min(progress, 100), -1)
960        if self.__progress != progress:
961            self.__progress = progress
962            self.shapeItem.setProgress(progress)
963            self.__updateTitleText()
964
965    def progress(self):
[11369]966        """
967        Return the node work progress state.
968        """
[11102]969        return self.__progress
970
[11369]971    progress_ = Property(float, fget=progress, fset=setProgress,
972                         doc="Node progress state.")
[11102]973
974    def setProgressMessage(self, message):
[11369]975        """
976        Set the node work progress message.
977
978        .. note:: Not yet implemented
979
[11102]980        """
981        pass
982
[11614]983    def setStateMessage(self, message):
984        """
985        Set a state message to display over the item.
986
987        Parameters
988        ----------
989        message : UserMessage
990            Message to display. `message.severity` is used to determine
991            the icon and `message.contents` is used as a tool tip.
992
993        """
994        # TODO: Group messages by message_id not by severity
995        # and deprecate set[Error|Warning|Error]Message
996        if message.severity == UserMessage.Info:
997            self.setInfoMessage(message.contents)
998        elif message.severity == UserMessage.Warning:
999            self.setWarningMessage(message.contents)
1000        elif message.severity == UserMessage.Error:
1001            self.setErrorMessage(message.contents)
1002
[11102]1003    def setErrorMessage(self, message):
[11134]1004        if self.__error != message:
1005            self.__error = message
1006            self.__updateMessages()
[11102]1007
1008    def setWarningMessage(self, message):
[11134]1009        if self.__warning != message:
1010            self.__warning = message
1011            self.__updateMessages()
[11102]1012
[11134]1013    def setInfoMessage(self, message):
1014        if self.__info != message:
1015            self.__info = message
1016            self.__updateMessages()
[11102]1017
1018    def newInputAnchor(self):
[11369]1019        """
[11442]1020        Create and return a new input :class:`AnchorPoint`.
[11102]1021        """
1022        if not (self.widget_description and self.widget_description.inputs):
1023            raise ValueError("Widget has no inputs.")
1024
[11138]1025        anchor = AnchorPoint()
[11207]1026        self.inputAnchorItem.addAnchor(anchor, position=1.0)
[11102]1027
[11207]1028        positions = self.inputAnchorItem.anchorPositions()
1029        positions = uniform_linear_layout(positions)
1030        self.inputAnchorItem.setAnchorPositions(positions)
[11102]1031
1032        return anchor
1033
1034    def removeInputAnchor(self, anchor):
[11369]1035        """
1036        Remove input anchor.
[11102]1037        """
[11138]1038        self.inputAnchorItem.removeAnchor(anchor)
[11102]1039
[11207]1040        positions = self.inputAnchorItem.anchorPositions()
1041        positions = uniform_linear_layout(positions)
1042        self.inputAnchorItem.setAnchorPositions(positions)
[11102]1043
1044    def newOutputAnchor(self):
[11369]1045        """
[11442]1046        Create and return a new output :class:`AnchorPoint`.
[11102]1047        """
1048        if not (self.widget_description and self.widget_description.outputs):
1049            raise ValueError("Widget has no outputs.")
1050
1051        anchor = AnchorPoint(self)
[11207]1052        self.outputAnchorItem.addAnchor(anchor, position=1.0)
[11102]1053
[11207]1054        positions = self.outputAnchorItem.anchorPositions()
1055        positions = uniform_linear_layout(positions)
1056        self.outputAnchorItem.setAnchorPositions(positions)
[11102]1057
1058        return anchor
1059
1060    def removeOutputAnchor(self, anchor):
[11369]1061        """
1062        Remove output anchor.
[11102]1063        """
[11138]1064        self.outputAnchorItem.removeAnchor(anchor)
[11102]1065
[11207]1066        positions = self.outputAnchorItem.anchorPositions()
1067        positions = uniform_linear_layout(positions)
1068        self.outputAnchorItem.setAnchorPositions(positions)
[11102]1069
[11138]1070    def inputAnchors(self):
[11369]1071        """
[11442]1072        Return a list of all input anchor points.
[11138]1073        """
1074        return self.inputAnchorItem.anchorPoints()
[11102]1075
[11138]1076    def outputAnchors(self):
[11369]1077        """
[11442]1078        Return a list of all output anchor points.
[11138]1079        """
1080        return self.outputAnchorItem.anchorPoints()
[11102]1081
[11138]1082    def setAnchorRotation(self, angle):
[11369]1083        """
1084        Set the anchor rotation.
[11138]1085        """
1086        self.inputAnchorItem.setRotation(angle)
1087        self.outputAnchorItem.setRotation(angle)
1088        self.anchorGeometryChanged.emit()
[11102]1089
[11138]1090    def anchorRotation(self):
[11369]1091        """
1092        Return the anchor rotation.
[11102]1093        """
[11138]1094        return self.inputAnchorItem.rotation()
[11102]1095
1096    def boundingRect(self):
1097        # TODO: Important because of this any time the child
1098        # items change geometry the self.prepareGeometryChange()
1099        # needs to be called.
1100        return self.childrenBoundingRect()
1101
1102    def shape(self):
[11369]1103        # Shape for mouse hit detection.
1104        # TODO: Should this return the union of all child items?
[11102]1105        return self.shapeItem.shape()
1106
1107    def __updateTitleText(self):
[11369]1108        """
1109        Update the title text item.
[11102]1110        """
1111        title_safe = escape(self.title())
1112        if self.progress() > 0:
1113            text = '<div align="center">%s<br/>%i%%</div>' % \
1114                   (title_safe, int(self.progress()))
1115        else:
1116            text = '<div align="center">%s</div>' % \
1117                   (title_safe)
1118
1119        # The NodeItems boundingRect could change.
1120        self.prepareGeometryChange()
1121        self.captionTextItem.setHtml(text)
1122        self.captionTextItem.document().adjustSize()
1123        width = self.captionTextItem.textWidth()
1124        self.captionTextItem.setPos(-width / 2.0, 33)
1125
[11134]1126    def __updateMessages(self):
[11369]1127        """
1128        Update message items (position, visibility and tool tips).
[11134]1129        """
1130        items = [self.errorItem, self.warningItem, self.infoItem]
1131        messages = [self.__error, self.__warning, self.__info]
1132        for message, item in zip(messages, items):
1133            item.setVisible(bool(message))
1134            item.setToolTip(message or "")
1135        shown = [item for item in items if item.isVisible()]
1136        count = len(shown)
1137        if count:
1138            spacing = 3
1139            rects = [item.boundingRect() for item in shown]
1140            width = sum(rect.width() for rect in rects)
1141            width += spacing * max(0, count - 1)
1142            height = max(rect.height() for rect in rects)
1143            origin = self.shapeItem.boundingRect().top() - spacing - height
1144            origin = QPointF(-width / 2, origin)
1145            for item, rect in zip(shown, rects):
1146                item.setPos(origin)
1147                origin = origin + QPointF(rect.width() + spacing, 0)
1148
[11102]1149    def mousePressEvent(self, event):
1150        if self.shapeItem.path().contains(event.pos()):
1151            return QGraphicsObject.mousePressEvent(self, event)
1152        else:
1153            event.ignore()
1154
1155    def mouseDoubleClickEvent(self, event):
1156        if self.shapeItem.path().contains(event.pos()):
1157            QGraphicsObject.mouseDoubleClickEvent(self, event)
1158            QTimer.singleShot(0, self.activated.emit)
1159        else:
1160            event.ignore()
1161
1162    def contextMenuEvent(self, event):
1163        if self.shapeItem.path().contains(event.pos()):
1164            return QGraphicsObject.contextMenuEvent(self, event)
1165        else:
1166            event.ignore()
1167
1168    def focusInEvent(self, event):
1169        self.shapeItem.setHasFocus(True)
1170        return QGraphicsObject.focusInEvent(self, event)
1171
1172    def focusOutEvent(self, event):
1173        self.shapeItem.setHasFocus(False)
1174        return QGraphicsObject.focusOutEvent(self, event)
1175
1176    def itemChange(self, change, value):
1177        if change == QGraphicsItem.ItemSelectedChange:
1178            self.shapeItem.setSelected(value.toBool())
1179        elif change == QGraphicsItem.ItemPositionHasChanged:
1180            self.positionChanged.emit()
1181
1182        return QGraphicsObject.itemChange(self, change, value)
1183
1184
1185TOOLTIP_TEMPLATE = """\
1186<html>
1187<head>
1188<style type="text/css">
1189{style}
1190</style>
1191</head>
1192<body>
1193{tooltip}
1194</body>
1195</html>
1196"""
1197
1198
1199def NodeItem_toolTipHelper(node, links_in=[], links_out=[]):
[11369]1200    """
[11525]1201    A helper function for constructing a standard tooltip for the node
[11102]1202    in on the canvas.
1203
1204    Parameters:
1205    ===========
1206    node : NodeItem
1207        The node item instance.
1208    links_in : list of LinkItem instances
1209        A list of input links for the node.
1210    links_out : list of LinkItem instances
1211        A list of output links for the node.
1212
1213    """
1214    desc = node.widget_description
1215    channel_fmt = "<li>{0}</li>"
1216
1217    title_fmt = "<b>{title}</b><hr/>"
1218    title = title_fmt.format(title=escape(node.title()))
1219    inputs_list_fmt = "Inputs:<ul>{inputs}</ul><hr/>"
1220    outputs_list_fmt = "Outputs:<ul>{outputs}</ul>"
1221    if desc.inputs:
1222        inputs = [channel_fmt.format(inp.name) for inp in desc.inputs]
[11525]1223        inputs = inputs_list_fmt.format(inputs="".join(inputs))
1224    else:
1225        inputs = "No inputs<hr/>" 
[11102]1226
1227    if desc.outputs:
1228        outputs = [channel_fmt.format(out.name) for out in desc.outputs]
[11525]1229        outputs = outputs_list_fmt.format(outputs="".join(outputs))
1230    else:
1231        outputs = "No outputs" 
[11102]1232
1233    tooltip = title + inputs + outputs
1234    style = "ul { margin-top: 1px; margin-bottom: 1px; }"
1235    return TOOLTIP_TEMPLATE.format(style=style, tooltip=tooltip)
Note: See TracBrowser for help on using the repository browser.