source: orange/Orange/OrangeCanvas/canvas/items/nodeitem.py @ 11875:5fd8d0445790

Revision 11875:5fd8d0445790, 38.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 6 weeks ago (diff)

Ensure the NodeItem shows a visual progress indicator even at 0% progress.

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