source: orange/Orange/OrangeCanvas/canvas/items/nodeitem.py @ 11733:705bccbfe63f

Revision 11733:705bccbfe63f, 36.5 KB checked in by Ales Erjavec <ales.erjavec@…>, 6 months ago (diff)

A larger node anchor hit area.

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 NodeItem(QGraphicsObject):
694    """
695    An widget node item in the canvas.
696    """
697
698    #: Signal emitted when the scene position of the node has changed.
699    positionChanged = Signal()
700
701    #: Signal emitted when the geometry of the channel anchors changes.
702    anchorGeometryChanged = Signal()
703
704    #: Signal emitted when the item has been activated (by a mouse double
705    #: click or a keyboard)
706    activated = Signal()
707
708    #: The item is under the mouse.
709    hovered = Signal()
710
711    #: Span of the anchor in degrees
712    ANCHOR_SPAN_ANGLE = 90
713
714    #: Z value of the item
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
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
740        self.__title = ""
741        self.__processingState = 0
742        self.__progress = -1
743
744        self.__error = None
745        self.__warning = None
746        self.__info = None
747
748        self.__anchorLayout = None
749        self.__animationEnabled = False
750
751        self.setZValue(self.Z_VALUE)
752        self.setupGraphics()
753
754        self.setWidgetDescription(widget_description)
755
756    @classmethod
757    def from_node(cls, node):
758        """
759        Create an :class:`NodeItem` instance and initialize it from a
760        :class:`SchemeNode` instance.
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):
770        """
771        Create an `NodeItem` instance from a node meta description.
772        """
773        self = cls()
774        self.setWidgetDescription(meta_description)
775        return self
776
777    def setupGraphics(self):
778        """
779        Set up the graphics.
780        """
781        shape_rect = QRectF(-24, -24, 48, 48)
782
783        self.shapeItem = NodeBodyItem(self)
784        self.shapeItem.setShapeRect(shape_rect)
785        self.shapeItem.setAnimationEnabled(self.__animationEnabled)
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
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
816
817        self.errorItem = iconItem(QStyle.SP_MessageBoxCritical)
818        self.warningItem = iconItem(QStyle.SP_MessageBoxWarning)
819        self.infoItem = iconItem(QStyle.SP_MessageBoxInformation)
820
821    # TODO: Remove the set[Widget|Category]Description. The user should
822    # handle setting of icons, title, ...
823    def setWidgetDescription(self, desc):
824        """
825        Set widget description.
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):
847        """
848        Set the widget category.
849        """
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):
858        """
859        Set the node item's icon (:class:`QIcon`).
860        """
861        if isinstance(icon, QIcon):
862            self.icon_item = GraphicsIconItem(self.shapeItem, icon=icon,
863                                              iconSize=QSize(36, 36))
864            self.icon_item.setPos(-18, -18)
865        else:
866            raise TypeError
867
868    def setColor(self, color, selectedColor=None):
869        """
870        Set the widget color.
871        """
872        if selectedColor is None:
873            selectedColor = saturated(color, 150)
874        palette = create_palette(color, selectedColor)
875        self.shapeItem.setPalette(palette)
876
877    def setPalette(self, palette):
878        # TODO: The palette should override the `setColor`
879        raise NotImplementedError
880
881    def setTitle(self, title):
882        """
883        Set the node title. The title text is displayed at the bottom of the
884        node.
885
886        """
887        self.__title = title
888        self.__updateTitleText()
889
890    def title(self):
891        """
892        Return the node title.
893        """
894        return self.__title
895
896    title_ = Property(unicode, fget=title, fset=setTitle,
897                      doc="Node title text.")
898
899    def setFont(self, font):
900        """
901        Set the title text font (:class:`QFont`).
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
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
928    def setProcessingState(self, state):
929        """
930        Set the node processing state i.e. the node is processing
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)
940                if self.__animationEnabled:
941                    self.shapeItem.ping()
942
943    def processingState(self):
944        """
945        The node processing state.
946        """
947        return self.__processingState
948
949    processingState_ = Property(int, fget=processingState,
950                                fset=setProcessingState)
951
952    def setProgress(self, progress):
953        """
954        Set the node work progress state (number between 0 and 100).
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):
966        """
967        Return the node work progress state.
968        """
969        return self.__progress
970
971    progress_ = Property(float, fget=progress, fset=setProgress,
972                         doc="Node progress state.")
973
974    def setProgressMessage(self, message):
975        """
976        Set the node work progress message.
977
978        .. note:: Not yet implemented
979
980        """
981        pass
982
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
1003    def setErrorMessage(self, message):
1004        if self.__error != message:
1005            self.__error = message
1006            self.__updateMessages()
1007
1008    def setWarningMessage(self, message):
1009        if self.__warning != message:
1010            self.__warning = message
1011            self.__updateMessages()
1012
1013    def setInfoMessage(self, message):
1014        if self.__info != message:
1015            self.__info = message
1016            self.__updateMessages()
1017
1018    def newInputAnchor(self):
1019        """
1020        Create and return a new input :class:`AnchorPoint`.
1021        """
1022        if not (self.widget_description and self.widget_description.inputs):
1023            raise ValueError("Widget has no inputs.")
1024
1025        anchor = AnchorPoint()
1026        self.inputAnchorItem.addAnchor(anchor, position=1.0)
1027
1028        positions = self.inputAnchorItem.anchorPositions()
1029        positions = uniform_linear_layout(positions)
1030        self.inputAnchorItem.setAnchorPositions(positions)
1031
1032        return anchor
1033
1034    def removeInputAnchor(self, anchor):
1035        """
1036        Remove input anchor.
1037        """
1038        self.inputAnchorItem.removeAnchor(anchor)
1039
1040        positions = self.inputAnchorItem.anchorPositions()
1041        positions = uniform_linear_layout(positions)
1042        self.inputAnchorItem.setAnchorPositions(positions)
1043
1044    def newOutputAnchor(self):
1045        """
1046        Create and return a new output :class:`AnchorPoint`.
1047        """
1048        if not (self.widget_description and self.widget_description.outputs):
1049            raise ValueError("Widget has no outputs.")
1050
1051        anchor = AnchorPoint(self)
1052        self.outputAnchorItem.addAnchor(anchor, position=1.0)
1053
1054        positions = self.outputAnchorItem.anchorPositions()
1055        positions = uniform_linear_layout(positions)
1056        self.outputAnchorItem.setAnchorPositions(positions)
1057
1058        return anchor
1059
1060    def removeOutputAnchor(self, anchor):
1061        """
1062        Remove output anchor.
1063        """
1064        self.outputAnchorItem.removeAnchor(anchor)
1065
1066        positions = self.outputAnchorItem.anchorPositions()
1067        positions = uniform_linear_layout(positions)
1068        self.outputAnchorItem.setAnchorPositions(positions)
1069
1070    def inputAnchors(self):
1071        """
1072        Return a list of all input anchor points.
1073        """
1074        return self.inputAnchorItem.anchorPoints()
1075
1076    def outputAnchors(self):
1077        """
1078        Return a list of all output anchor points.
1079        """
1080        return self.outputAnchorItem.anchorPoints()
1081
1082    def setAnchorRotation(self, angle):
1083        """
1084        Set the anchor rotation.
1085        """
1086        self.inputAnchorItem.setRotation(angle)
1087        self.outputAnchorItem.setRotation(angle)
1088        self.anchorGeometryChanged.emit()
1089
1090    def anchorRotation(self):
1091        """
1092        Return the anchor rotation.
1093        """
1094        return self.inputAnchorItem.rotation()
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):
1103        # Shape for mouse hit detection.
1104        # TODO: Should this return the union of all child items?
1105        return self.shapeItem.shape()
1106
1107    def __updateTitleText(self):
1108        """
1109        Update the title text item.
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
1126    def __updateMessages(self):
1127        """
1128        Update message items (position, visibility and tool tips).
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
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=[]):
1200    """
1201    A helper function for constructing a standard tooltip for the node
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]
1223        inputs = inputs_list_fmt.format(inputs="".join(inputs))
1224    else:
1225        inputs = "No inputs<hr/>" 
1226
1227    if desc.outputs:
1228        outputs = [channel_fmt.format(out.name) for out in desc.outputs]
1229        outputs = outputs_list_fmt.format(outputs="".join(outputs))
1230    else:
1231        outputs = "No outputs" 
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.