source: orange/Orange/OrangeCanvas/canvas/items/nodeitem.py @ 11440:cb2508213612

Revision 11440:cb2508213612, 35.5 KB checked in by Ales Erjavec <ales.erjavec@…>, 13 months ago (diff)

Fixed default colors for nodes in the canvas.

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