source: orange/Orange/OrangeCanvas/canvas/items/nodeitem.py @ 11768:572797f576ad

Revision 11768:572797f576ad, 38.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 5 months ago (diff)

Added node title background highlights for selected nodes.

Line 
1"""
2=========
3Node Item
4=========
5
6"""
7
8from xml.sax.saxutils import escape
9
10from PyQt4.QtGui import (
11    QGraphicsItem, QGraphicsObject, QGraphicsTextItem,
12    QGraphicsDropShadowEffect, QGraphicsView,
13    QPen, QBrush, QColor, QPalette, QIcon, QStyle, QPainter,
14    QPainterPath, QPainterPathStroker, QApplication
15)
16
17from PyQt4.QtCore import Qt, QPointF, QRectF, QSize, QTimer, QPropertyAnimation
18from PyQt4.QtCore import pyqtSignal as Signal
19from PyQt4.QtCore import pyqtProperty as Property
20
21from .graphicspathobject import GraphicsPathObject
22from .utils import saturated, radial_gradient
23
24from ...scheme.node import UserMessage
25from ...registry import NAMED_COLORS
26from ...resources import icon_loader
27from .utils import uniform_linear_layout
28
29
30def create_palette(light_color, color):
31    """
32    Return a new :class:`QPalette` from for the :class:`NodeBodyItem`.
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():
54    """
55    Create and return a default palette for a node.
56    """
57    return create_palette(QColor(NAMED_COLORS["light-yellow"]),
58                          QColor(NAMED_COLORS["yellow"]))
59
60
61def animation_restart(animation):
62    if animation.state() == QPropertyAnimation.Running:
63        animation.pause()
64    animation.start()
65
66
67SHADOW_COLOR = "#9CACB4"
68FOCUS_OUTLINE_COLOR = "#609ED7"
69
70
71class NodeBodyItem(GraphicsPathObject):
72    """
73    The central part (body) of the `NodeItem`.
74    """
75    def __init__(self, parent=None):
76        GraphicsPathObject.__init__(self, parent)
77        assert(isinstance(parent, NodeItem))
78
79        self.__processingState = 0
80        self.__progress = -1
81        self.__animationEnabled = False
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(
97            blurRadius=3,
98            color=QColor(SHADOW_COLOR),
99            offset=QPointF(0, 0),
100            )
101
102        self.setGraphicsEffect(self.shadow)
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)])
113
114    # TODO: The body item should allow the setting of arbitrary painter
115    # paths (for instance rounded rect, ...)
116    def setShapeRect(self, rect):
117        """
118        Set the item's shape `rect`. The item should be confined within
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):
128        """
129        Set the body color palette (:class:`QPalette`).
130        """
131        self.palette = palette
132        self.__updateBrush()
133
134    def setAnimationEnabled(self, enabled):
135        """
136        Set the node animation enabled.
137        """
138        if self.__animationEnabled != enabled:
139            self.__animationEnabled = enabled
140
141    def setProcessingState(self, state):
142        """
143        Set the processing state of the node.
144        """
145        if self.__processingState != state:
146            self.__processingState = state
147            if not state and self.__animationEnabled:
148                self.ping()
149
150    def setProgress(self, progress):
151        """
152        Set the progress indicator state of the node. `progress` should
153        be a number between 0 and 100.
154
155        """
156        self.__progress = progress
157        self.update()
158
159    def ping(self):
160        """
161        Trigger a 'ping' animation.
162        """
163        animation_restart(self.__pingAnimation)
164
165    def hoverEnterEvent(self, event):
166        self.__hover = True
167        self.__updateShadowState()
168        return GraphicsPathObject.hoverEnterEvent(self, event)
169
170    def hoverLeaveEvent(self, event):
171        self.__hover = False
172        self.__updateShadowState()
173        return GraphicsPathObject.hoverLeaveEvent(self, event)
174
175    def paint(self, painter, option, widget):
176        """
177        Paint the shape and a progress meter.
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
183        GraphicsPathObject.paint(self, painter, option, widget)
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
205        radius = 3
206        enabled = False
207
208        if self.__isSelected:
209            enabled = True
210            radius = 7
211
212        if self.__hover:
213            radius = 17
214            enabled = True
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)
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):
246        """
247        Set the `selected` state.
248
249        .. note:: The item does not have `QGraphicsItem.ItemIsSelectable` flag.
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):
257        """
258        Set the `has focus` state.
259
260        .. note:: The item does not have `QGraphicsItem.ItemIsFocusable` flag.
261                  This property is instead controlled by the parent NodeItem.
262
263        """
264        self.__hasFocus = focus
265        self.__updateShadowState()
266
267    def __on_finished(self):
268        if self.shadow.blurRadius() == 0:
269            self.shadow.setEnabled(False)
270
271
272class AnchorPoint(QGraphicsObject):
273    """
274    A anchor indicator on the :class:`NodeAnchorItem`.
275    """
276
277    #: Signal emitted when the item's scene position changes.
278    scenePositionChanged = Signal(QPointF)
279
280    #: Signal emitted when the item's `anchorDirection` changes.
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):
291        """
292        Return anchor position in scene coordinates.
293        """
294        return self.mapToScene(QPointF(0, 0))
295
296    def setAnchorDirection(self, direction):
297        """
298        Set the preferred direction (QPointF) in item coordinates.
299        """
300        if self.__direction != direction:
301            self.__direction = direction
302            self.anchorDirectionChanged.emit(direction)
303
304    def anchorDirection(self):
305        """
306        Return the preferred anchor direction.
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
320class NodeAnchorItem(GraphicsPathObject):
321    """
322    The left/right widget input/output anchors.
323    """
324
325    def __init__(self, parent, *args):
326        GraphicsPathObject.__init__(self, parent, *args)
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
344
345        if isinstance(parent, NodeItem):
346            self.__parentNodeItem = parent
347        else:
348            self.__parentNodeItem = None
349
350        self.__anchorPath = QPainterPath()
351        self.__points = []
352        self.__pointPositions = []
353
354        self.__fullStroke = None
355        self.__dottedStroke = None
356        self.__shape = None
357
358    def parentNodeItem(self):
359        """
360        Return a parent :class:`NodeItem` or ``None`` if this anchor's
361        parent is not a :class:`NodeItem` instance.
362
363        """
364        return self.__parentNodeItem
365
366    def setAnchorPath(self, path):
367        """
368        Set the anchor's curve path as a :class:`QPainterPath`.
369        """
370        self.__anchorPath = path
371        # Create a stroke of the path.
372        stroke_path = QPainterPathStroker()
373        stroke_path.setCapStyle(Qt.RoundCap)
374
375        # Shape is wider (bigger mouse hit area - should be settable)
376        stroke_path.setWidth(12)
377        self.__shape = stroke_path.createStroke(path)
378
379        # The full stroke
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
394    def anchorPath(self):
395        """
396        Return the anchor path (:class:`QPainterPath`). This is a curve on
397        which the anchor points lie.
398
399        """
400        return self.__anchorPath
401
402    def setAnchored(self, anchored):
403        """
404        Set the items anchored state. When ``False`` the item draws it self
405        with a dotted stroke.
406
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):
417        """
418        Set the connection hint. This can be used to indicate if
419        a connection can be made or not.
420
421        """
422        raise NotImplementedError
423
424    def count(self):
425        """
426        Return the number of anchor points.
427        """
428        return len(self.__points)
429
430    def addAnchor(self, anchor, position=0.5):
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
437        """
438        return self.insertAnchor(self.count(), anchor, position)
439
440    def insertAnchor(self, index, anchor, position=0.5):
441        """
442        Insert a new :class:`AnchorPoint` at `index`.
443
444        See also
445        --------
446        NodeAnchorItem.addAnchor
447
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):
466        """
467        Remove and delete the anchor point.
468        """
469        anchor = self.takeAnchor(anchor)
470
471        anchor.hide()
472        anchor.setParentItem(None)
473        anchor.deleteLater()
474
475    def takeAnchor(self, anchor):
476        """
477        Remove the anchor but don't delete it.
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):
502        """
503        Return a list of anchor points.
504        """
505        return list(self.__points)
506
507    def anchorPoint(self, index):
508        """
509        Return the anchor point at `index`.
510        """
511        return self.__points[index]
512
513    def setAnchorPositions(self, positions):
514        """
515        Set the anchor positions in percentages (0..1) along the path curve.
516        """
517        if self.__pointPositions != positions:
518            self.__pointPositions = list(positions)
519
520            self.__updatePositions()
521
522    def anchorPositions(self):
523        """
524        Return the positions of anchor points as a list of floats where
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        """
529        return list(self.__pointPositions)
530
531    def shape(self):
532        if self.__shape is not None:
533            return self.__shape
534        else:
535            return GraphicsPathObject.shape(self)
536
537    def boundingRect(self):
538        if self.__shape is not None:
539            return self.__shape.controlPointRect()
540        else:
541            return GraphicsPathObject.boundingRect(self)
542
543    def hoverEnterEvent(self, event):
544        self.shadow.setEnabled(True)
545        return GraphicsPathObject.hoverEnterEvent(self, event)
546
547    def hoverLeaveEvent(self, event):
548        self.shadow.setEnabled(False)
549        return GraphicsPathObject.hoverLeaveEvent(self, event)
550
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
558
559class SourceAnchorItem(NodeAnchorItem):
560    """
561    A source anchor item
562    """
563    pass
564
565
566class SinkAnchorItem(NodeAnchorItem):
567    """
568    A sink anchor item.
569    """
570    pass
571
572
573def standard_icon(standard_pixmap):
574    """
575    Return return the application style's standard icon for a
576    `QStyle.StandardPixmap`.
577
578    """
579    style = QApplication.instance().style()
580    return style.standardIcon(standard_pixmap)
581
582
583class GraphicsIconItem(QGraphicsItem):
584    """
585    A graphics item displaying an :class:`QIcon`.
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):
605        """
606        Set the icon (:class:`QIcon`).
607        """
608        if self.__icon != icon:
609            self.__icon = QIcon(icon)
610            self.update()
611
612    def icon(self):
613        """
614        Return the icon (:class:`QIcon`).
615        """
616        return QIcon(self.__icon)
617
618    def setIconSize(self, size):
619        """
620        Set the icon (and this item's) size (:class:`QSize`).
621        """
622        if self.__iconSize != size:
623            self.prepareGeometryChange()
624            self.__iconSize = QSize(size)
625            self.update()
626
627    def iconSize(self):
628        """
629        Return the icon size (:class:`QSize`).
630        """
631        return QSize(self.__iconSize)
632
633    def setTransformationMode(self, mode):
634        """
635        Set pixmap transformation mode. (`Qt.SmoothTransformation` or
636        `Qt.FastTransformation`).
637
638        """
639        if self.__transformationMode != mode:
640            self.__transformationMode = mode
641            self.update()
642
643    def transformationMode(self):
644        """
645        Return the pixmap transformation mode.
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))
691
692
693class NameTextItem(QGraphicsTextItem):
694    def __init__(self, *args, **kwargs):
695        super(NameTextItem, self).__init__(*args, **kwargs)
696        self.__selected = False
697        self.__palette = None
698
699    def paint(self, painter, option, widget=None):
700        if self.__selected:
701            painter.save()
702            painter.setPen(QPen(Qt.NoPen))
703            painter.setBrush(self.palette().color(QPalette.Highlight))
704            doc = self.document()
705            margin = doc.documentMargin()
706            painter.translate(margin, margin)
707            offset = min(margin, 2)
708            for line in self._lines(doc):
709                rect = line.naturalTextRect()
710                painter.drawRoundedRect(
711                    rect.adjusted(-offset, -offset, offset, offset),
712                    3, 3
713                )
714
715            painter.restore()
716
717        super(NameTextItem, self).paint(painter, option, widget)
718
719    def _blocks(self, doc):
720        block = doc.begin()
721        while block != doc.end():
722            yield block
723            block = block.next()
724
725    def _lines(self, doc):
726        for block in self._blocks(doc):
727            blocklayout = block.layout()
728            for i in range(blocklayout.lineCount()):
729                yield blocklayout.lineAt(i)
730
731    def setSelectionState(self, state):
732        if self.__selected != state:
733            self.__selected = state
734            self.__updateDefaultTextColor()
735            self.update()
736
737    def setPalatte(self, palette):
738        if self.__palette != palette:
739            self.__palette = palette
740            self.__updateDefaultTextColor()
741            self.update()
742
743    def palette(self):
744        if self.__palette is None:
745            scene = self.scene()
746            if scene is not None:
747                return scene.palette()
748            else:
749                return QPalette()
750        else:
751            return self.__palette
752
753    def __updateDefaultTextColor(self):
754        if self.__selected:
755            role = QPalette.HighlightedText
756        else:
757            role = QPalette.WindowText
758        self.setDefaultTextColor(self.palette().color(role))
759
760
761class NodeItem(QGraphicsObject):
762    """
763    An widget node item in the canvas.
764    """
765
766    #: Signal emitted when the scene position of the node has changed.
767    positionChanged = Signal()
768
769    #: Signal emitted when the geometry of the channel anchors changes.
770    anchorGeometryChanged = Signal()
771
772    #: Signal emitted when the item has been activated (by a mouse double
773    #: click or a keyboard)
774    activated = Signal()
775
776    #: The item is under the mouse.
777    hovered = Signal()
778
779    #: Span of the anchor in degrees
780    ANCHOR_SPAN_ANGLE = 90
781
782    #: Z value of the item
783    Z_VALUE = 100
784
785    def __init__(self, widget_description=None, parent=None, **kwargs):
786        QGraphicsObject.__init__(self, parent, **kwargs)
787        self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
788        self.setFlag(QGraphicsItem.ItemHasNoContents, True)
789        self.setFlag(QGraphicsItem.ItemIsSelectable, True)
790        self.setFlag(QGraphicsItem.ItemIsMovable, True)
791        self.setFlag(QGraphicsItem.ItemIsFocusable, True)
792
793        # central body shape item
794        self.shapeItem = None
795
796        # in/output anchor items
797        self.inputAnchorItem = None
798        self.outputAnchorItem = None
799
800        # title text item
801        self.captionTextItem = None
802
803        # error, warning, info items
804        self.errorItem = None
805        self.warningItem = None
806        self.infoItem = None
807
808        self.__title = ""
809        self.__processingState = 0
810        self.__progress = -1
811
812        self.__error = None
813        self.__warning = None
814        self.__info = None
815
816        self.__anchorLayout = None
817        self.__animationEnabled = False
818
819        self.setZValue(self.Z_VALUE)
820        self.setupGraphics()
821
822        self.setWidgetDescription(widget_description)
823
824    @classmethod
825    def from_node(cls, node):
826        """
827        Create an :class:`NodeItem` instance and initialize it from a
828        :class:`SchemeNode` instance.
829
830        """
831        self = cls()
832        self.setWidgetDescription(node.description)
833#        self.setCategoryDescription(node.category)
834        return self
835
836    @classmethod
837    def from_node_meta(cls, meta_description):
838        """
839        Create an `NodeItem` instance from a node meta description.
840        """
841        self = cls()
842        self.setWidgetDescription(meta_description)
843        return self
844
845    def setupGraphics(self):
846        """
847        Set up the graphics.
848        """
849        shape_rect = QRectF(-24, -24, 48, 48)
850
851        self.shapeItem = NodeBodyItem(self)
852        self.shapeItem.setShapeRect(shape_rect)
853        self.shapeItem.setAnimationEnabled(self.__animationEnabled)
854
855        # Rect for widget's 'ears'.
856        anchor_rect = QRectF(-31, -31, 62, 62)
857        self.inputAnchorItem = SinkAnchorItem(self)
858        input_path = QPainterPath()
859        start_angle = 180 - self.ANCHOR_SPAN_ANGLE / 2
860        input_path.arcMoveTo(anchor_rect, start_angle)
861        input_path.arcTo(anchor_rect, start_angle, self.ANCHOR_SPAN_ANGLE)
862        self.inputAnchorItem.setAnchorPath(input_path)
863
864        self.outputAnchorItem = SourceAnchorItem(self)
865        output_path = QPainterPath()
866        start_angle = self.ANCHOR_SPAN_ANGLE / 2
867        output_path.arcMoveTo(anchor_rect, start_angle)
868        output_path.arcTo(anchor_rect, start_angle, - self.ANCHOR_SPAN_ANGLE)
869        self.outputAnchorItem.setAnchorPath(output_path)
870
871        self.inputAnchorItem.hide()
872        self.outputAnchorItem.hide()
873
874        # Title caption item
875        self.captionTextItem = NameTextItem(self)
876
877        self.captionTextItem.setPlainText("")
878        self.captionTextItem.setPos(0, 33)
879
880        def iconItem(standard_pixmap):
881            item = GraphicsIconItem(self, icon=standard_icon(standard_pixmap),
882                                    iconSize=QSize(16, 16))
883            item.hide()
884            return item
885
886        self.errorItem = iconItem(QStyle.SP_MessageBoxCritical)
887        self.warningItem = iconItem(QStyle.SP_MessageBoxWarning)
888        self.infoItem = iconItem(QStyle.SP_MessageBoxInformation)
889
890    # TODO: Remove the set[Widget|Category]Description. The user should
891    # handle setting of icons, title, ...
892    def setWidgetDescription(self, desc):
893        """
894        Set widget description.
895        """
896        self.widget_description = desc
897        if desc is None:
898            return
899
900        icon = icon_loader.from_description(desc).get(desc.icon)
901        if icon:
902            self.setIcon(icon)
903
904        if not self.title():
905            self.setTitle(desc.name)
906
907        if desc.inputs:
908            self.inputAnchorItem.show()
909        if desc.outputs:
910            self.outputAnchorItem.show()
911
912        tooltip = NodeItem_toolTipHelper(self)
913        self.setToolTip(tooltip)
914
915    def setWidgetCategory(self, desc):
916        """
917        Set the widget category.
918        """
919        self.category_description = desc
920        if desc and desc.background:
921            background = NAMED_COLORS.get(desc.background, desc.background)
922            color = QColor(background)
923            if color.isValid():
924                self.setColor(color)
925
926    def setIcon(self, icon):
927        """
928        Set the node item's icon (:class:`QIcon`).
929        """
930        if isinstance(icon, QIcon):
931            self.icon_item = GraphicsIconItem(self.shapeItem, icon=icon,
932                                              iconSize=QSize(36, 36))
933            self.icon_item.setPos(-18, -18)
934        else:
935            raise TypeError
936
937    def setColor(self, color, selectedColor=None):
938        """
939        Set the widget color.
940        """
941        if selectedColor is None:
942            selectedColor = saturated(color, 150)
943        palette = create_palette(color, selectedColor)
944        self.shapeItem.setPalette(palette)
945
946    def setPalette(self, palette):
947        # TODO: The palette should override the `setColor`
948        raise NotImplementedError
949
950    def setTitle(self, title):
951        """
952        Set the node title. The title text is displayed at the bottom of the
953        node.
954
955        """
956        self.__title = title
957        self.__updateTitleText()
958
959    def title(self):
960        """
961        Return the node title.
962        """
963        return self.__title
964
965    title_ = Property(unicode, fget=title, fset=setTitle,
966                      doc="Node title text.")
967
968    def setFont(self, font):
969        """
970        Set the title text font (:class:`QFont`).
971        """
972        if font != self.font():
973            self.prepareGeometryChange()
974            self.captionTextItem.setFont(font)
975            self.__updateTitleText()
976
977    def font(self):
978        """
979        Return the title text font.
980        """
981        return self.captionTextItem.font()
982
983    def setAnimationEnabled(self, enabled):
984        """
985        Set the node animation enabled state.
986        """
987        if self.__animationEnabled != enabled:
988            self.__animationEnabled = enabled
989            self.shapeItem.setAnimationEnabled(enabled)
990
991    def animationEnabled(self):
992        """
993        Are node animations enabled.
994        """
995        return self.__animationEnabled
996
997    def setProcessingState(self, state):
998        """
999        Set the node processing state i.e. the node is processing
1000        (is busy) or is idle.
1001
1002        """
1003        if self.__processingState != state:
1004            self.__processingState = state
1005            self.shapeItem.setProcessingState(state)
1006            if not state:
1007                # Clear the progress meter.
1008                self.setProgress(-1)
1009                if self.__animationEnabled:
1010                    self.shapeItem.ping()
1011
1012    def processingState(self):
1013        """
1014        The node processing state.
1015        """
1016        return self.__processingState
1017
1018    processingState_ = Property(int, fget=processingState,
1019                                fset=setProcessingState)
1020
1021    def setProgress(self, progress):
1022        """
1023        Set the node work progress state (number between 0 and 100).
1024        """
1025        if progress is None or progress < 0:
1026            progress = -1
1027
1028        progress = max(min(progress, 100), -1)
1029        if self.__progress != progress:
1030            self.__progress = progress
1031            self.shapeItem.setProgress(progress)
1032            self.__updateTitleText()
1033
1034    def progress(self):
1035        """
1036        Return the node work progress state.
1037        """
1038        return self.__progress
1039
1040    progress_ = Property(float, fget=progress, fset=setProgress,
1041                         doc="Node progress state.")
1042
1043    def setProgressMessage(self, message):
1044        """
1045        Set the node work progress message.
1046
1047        .. note:: Not yet implemented
1048
1049        """
1050        pass
1051
1052    def setStateMessage(self, message):
1053        """
1054        Set a state message to display over the item.
1055
1056        Parameters
1057        ----------
1058        message : UserMessage
1059            Message to display. `message.severity` is used to determine
1060            the icon and `message.contents` is used as a tool tip.
1061
1062        """
1063        # TODO: Group messages by message_id not by severity
1064        # and deprecate set[Error|Warning|Error]Message
1065        if message.severity == UserMessage.Info:
1066            self.setInfoMessage(message.contents)
1067        elif message.severity == UserMessage.Warning:
1068            self.setWarningMessage(message.contents)
1069        elif message.severity == UserMessage.Error:
1070            self.setErrorMessage(message.contents)
1071
1072    def setErrorMessage(self, message):
1073        if self.__error != message:
1074            self.__error = message
1075            self.__updateMessages()
1076
1077    def setWarningMessage(self, message):
1078        if self.__warning != message:
1079            self.__warning = message
1080            self.__updateMessages()
1081
1082    def setInfoMessage(self, message):
1083        if self.__info != message:
1084            self.__info = message
1085            self.__updateMessages()
1086
1087    def newInputAnchor(self):
1088        """
1089        Create and return a new input :class:`AnchorPoint`.
1090        """
1091        if not (self.widget_description and self.widget_description.inputs):
1092            raise ValueError("Widget has no inputs.")
1093
1094        anchor = AnchorPoint()
1095        self.inputAnchorItem.addAnchor(anchor, position=1.0)
1096
1097        positions = self.inputAnchorItem.anchorPositions()
1098        positions = uniform_linear_layout(positions)
1099        self.inputAnchorItem.setAnchorPositions(positions)
1100
1101        return anchor
1102
1103    def removeInputAnchor(self, anchor):
1104        """
1105        Remove input anchor.
1106        """
1107        self.inputAnchorItem.removeAnchor(anchor)
1108
1109        positions = self.inputAnchorItem.anchorPositions()
1110        positions = uniform_linear_layout(positions)
1111        self.inputAnchorItem.setAnchorPositions(positions)
1112
1113    def newOutputAnchor(self):
1114        """
1115        Create and return a new output :class:`AnchorPoint`.
1116        """
1117        if not (self.widget_description and self.widget_description.outputs):
1118            raise ValueError("Widget has no outputs.")
1119
1120        anchor = AnchorPoint(self)
1121        self.outputAnchorItem.addAnchor(anchor, position=1.0)
1122
1123        positions = self.outputAnchorItem.anchorPositions()
1124        positions = uniform_linear_layout(positions)
1125        self.outputAnchorItem.setAnchorPositions(positions)
1126
1127        return anchor
1128
1129    def removeOutputAnchor(self, anchor):
1130        """
1131        Remove output anchor.
1132        """
1133        self.outputAnchorItem.removeAnchor(anchor)
1134
1135        positions = self.outputAnchorItem.anchorPositions()
1136        positions = uniform_linear_layout(positions)
1137        self.outputAnchorItem.setAnchorPositions(positions)
1138
1139    def inputAnchors(self):
1140        """
1141        Return a list of all input anchor points.
1142        """
1143        return self.inputAnchorItem.anchorPoints()
1144
1145    def outputAnchors(self):
1146        """
1147        Return a list of all output anchor points.
1148        """
1149        return self.outputAnchorItem.anchorPoints()
1150
1151    def setAnchorRotation(self, angle):
1152        """
1153        Set the anchor rotation.
1154        """
1155        self.inputAnchorItem.setRotation(angle)
1156        self.outputAnchorItem.setRotation(angle)
1157        self.anchorGeometryChanged.emit()
1158
1159    def anchorRotation(self):
1160        """
1161        Return the anchor rotation.
1162        """
1163        return self.inputAnchorItem.rotation()
1164
1165    def boundingRect(self):
1166        # TODO: Important because of this any time the child
1167        # items change geometry the self.prepareGeometryChange()
1168        # needs to be called.
1169        return self.childrenBoundingRect()
1170
1171    def shape(self):
1172        # Shape for mouse hit detection.
1173        # TODO: Should this return the union of all child items?
1174        return self.shapeItem.shape()
1175
1176    def __updateTitleText(self):
1177        """
1178        Update the title text item.
1179        """
1180        title_safe = escape(self.title())
1181        if self.progress() > 0:
1182            text = '<div align="center">%s<br/>%i%%</div>' % \
1183                   (title_safe, int(self.progress()))
1184        else:
1185            text = '<div align="center">%s</div>' % \
1186                   (title_safe)
1187
1188        # The NodeItems boundingRect could change.
1189        self.prepareGeometryChange()
1190        self.captionTextItem.setHtml(text)
1191        self.captionTextItem.document().adjustSize()
1192        width = self.captionTextItem.textWidth()
1193        self.captionTextItem.setPos(-width / 2.0, 33)
1194
1195    def __updateMessages(self):
1196        """
1197        Update message items (position, visibility and tool tips).
1198        """
1199        items = [self.errorItem, self.warningItem, self.infoItem]
1200        messages = [self.__error, self.__warning, self.__info]
1201        for message, item in zip(messages, items):
1202            item.setVisible(bool(message))
1203            item.setToolTip(message or "")
1204        shown = [item for item in items if item.isVisible()]
1205        count = len(shown)
1206        if count:
1207            spacing = 3
1208            rects = [item.boundingRect() for item in shown]
1209            width = sum(rect.width() for rect in rects)
1210            width += spacing * max(0, count - 1)
1211            height = max(rect.height() for rect in rects)
1212            origin = self.shapeItem.boundingRect().top() - spacing - height
1213            origin = QPointF(-width / 2, origin)
1214            for item, rect in zip(shown, rects):
1215                item.setPos(origin)
1216                origin = origin + QPointF(rect.width() + spacing, 0)
1217
1218    def mousePressEvent(self, event):
1219        if self.shapeItem.path().contains(event.pos()):
1220            return QGraphicsObject.mousePressEvent(self, event)
1221        else:
1222            event.ignore()
1223
1224    def mouseDoubleClickEvent(self, event):
1225        if self.shapeItem.path().contains(event.pos()):
1226            QGraphicsObject.mouseDoubleClickEvent(self, event)
1227            QTimer.singleShot(0, self.activated.emit)
1228        else:
1229            event.ignore()
1230
1231    def contextMenuEvent(self, event):
1232        if self.shapeItem.path().contains(event.pos()):
1233            return QGraphicsObject.contextMenuEvent(self, event)
1234        else:
1235            event.ignore()
1236
1237    def focusInEvent(self, event):
1238        self.shapeItem.setHasFocus(True)
1239        return QGraphicsObject.focusInEvent(self, event)
1240
1241    def focusOutEvent(self, event):
1242        self.shapeItem.setHasFocus(False)
1243        return QGraphicsObject.focusOutEvent(self, event)
1244
1245    def itemChange(self, change, value):
1246        if change == QGraphicsItem.ItemSelectedChange:
1247            self.shapeItem.setSelected(value.toBool())
1248            self.captionTextItem.setSelectionState(value.toBool())
1249        elif change == QGraphicsItem.ItemPositionHasChanged:
1250            self.positionChanged.emit()
1251
1252        return QGraphicsObject.itemChange(self, change, value)
1253
1254
1255TOOLTIP_TEMPLATE = """\
1256<html>
1257<head>
1258<style type="text/css">
1259{style}
1260</style>
1261</head>
1262<body>
1263{tooltip}
1264</body>
1265</html>
1266"""
1267
1268
1269def NodeItem_toolTipHelper(node, links_in=[], links_out=[]):
1270    """
1271    A helper function for constructing a standard tooltip for the node
1272    in on the canvas.
1273
1274    Parameters:
1275    ===========
1276    node : NodeItem
1277        The node item instance.
1278    links_in : list of LinkItem instances
1279        A list of input links for the node.
1280    links_out : list of LinkItem instances
1281        A list of output links for the node.
1282
1283    """
1284    desc = node.widget_description
1285    channel_fmt = "<li>{0}</li>"
1286
1287    title_fmt = "<b>{title}</b><hr/>"
1288    title = title_fmt.format(title=escape(node.title()))
1289    inputs_list_fmt = "Inputs:<ul>{inputs}</ul><hr/>"
1290    outputs_list_fmt = "Outputs:<ul>{outputs}</ul>"
1291    if desc.inputs:
1292        inputs = [channel_fmt.format(inp.name) for inp in desc.inputs]
1293        inputs = inputs_list_fmt.format(inputs="".join(inputs))
1294    else:
1295        inputs = "No inputs<hr/>" 
1296
1297    if desc.outputs:
1298        outputs = [channel_fmt.format(out.name) for out in desc.outputs]
1299        outputs = outputs_list_fmt.format(outputs="".join(outputs))
1300    else:
1301        outputs = "No outputs" 
1302
1303    tooltip = title + inputs + outputs
1304    style = "ul { margin-top: 1px; margin-bottom: 1px; }"
1305    return TOOLTIP_TEMPLATE.format(style=style, tooltip=tooltip)
Note: See TracBrowser for help on using the repository browser.