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.

RevLine 
[11102]1"""
2NodeItem
3
4"""
5
6from xml.sax.saxutils import escape
7
8from PyQt4.QtGui import (
[11411]9    QGraphicsItem, QGraphicsObject, QGraphicsTextItem,
10    QGraphicsDropShadowEffect, QGraphicsView,
[11369]11    QPen, QBrush, QColor, QPalette, QIcon, QStyle, QPainter,
12    QPainterPath, QPainterPathStroker, QApplication
[11102]13)
14
[11411]15from PyQt4.QtCore import Qt, QPointF, QRectF, QSize, QTimer, QPropertyAnimation
[11102]16from PyQt4.QtCore import pyqtSignal as Signal
17from PyQt4.QtCore import pyqtProperty as Property
18
[11293]19from .graphicspathobject import GraphicsPathObject
[11138]20from .utils import saturated, radial_gradient
[11102]21
22from ...registry import NAMED_COLORS
23from ...resources import icon_loader
[11207]24from .utils import uniform_linear_layout
[11102]25
26
27def create_palette(light_color, color):
[11369]28    """
29    Return a new :class:`QPalette` from for the :class:`NodeBodyItem`.
[11102]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():
[11369]51    """
52    Create and return a default palette for a node.
[11102]53    """
[11440]54    return create_palette(QColor(NAMED_COLORS["light-yellow"]),
55                          QColor(NAMED_COLORS["yellow"]))
[11102]56
57
[11411]58def animation_restart(animation):
59    if animation.state() == QPropertyAnimation.Running:
60        animation.pause()
61    animation.start()
62
63
[11102]64SHADOW_COLOR = "#9CACB4"
65FOCUS_OUTLINE_COLOR = "#609ED7"
66
67
[11411]68class NodeBodyItem(GraphicsPathObject):
[11369]69    """
70    The central part (body) of the `NodeItem`.
[11102]71    """
72    def __init__(self, parent=None):
[11411]73        GraphicsPathObject.__init__(self, parent)
[11102]74        assert(isinstance(parent, NodeItem))
75
76        self.__processingState = 0
77        self.__progress = -1
[11411]78        self.__animationEnabled = False
[11102]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(
[11411]94            blurRadius=3,
[11102]95            color=QColor(SHADOW_COLOR),
96            offset=QPointF(0, 0),
97            )
98
99        self.setGraphicsEffect(self.shadow)
[11411]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)])
[11102]110
111    # TODO: The body item should allow the setting of arbitrary painter
112    # paths (for instance rounded rect, ...)
113    def setShapeRect(self, rect):
[11369]114        """
115        Set the item's shape `rect`. The item should be confined within
[11102]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):
[11369]125        """
126        Set the body color palette (:class:`QPalette`).
[11102]127        """
128        self.palette = palette
129        self.__updateBrush()
130
[11411]131    def setAnimationEnabled(self, enabled):
132        """
133        Set the node animation enabled.
134        """
135        if self.__animationEnabled != enabled:
136            self.__animationEnabled = enabled
137
[11102]138    def setProcessingState(self, state):
[11369]139        """
140        Set the processing state of the node.
[11102]141        """
[11411]142        if self.__processingState != state:
143            self.__processingState = state
144            if not state and self.__animationEnabled:
145                self.ping()
[11102]146
147    def setProgress(self, progress):
[11369]148        """
149        Set the progress indicator state of the node. `progress` should
150        be a number between 0 and 100.
151
152        """
[11102]153        self.__progress = progress
154        self.update()
155
[11411]156    def ping(self):
157        """
158        Trigger a 'ping' animation.
159        """
160        animation_restart(self.__pingAnimation)
161
[11102]162    def hoverEnterEvent(self, event):
163        self.__hover = True
164        self.__updateShadowState()
[11411]165        return GraphicsPathObject.hoverEnterEvent(self, event)
[11102]166
167    def hoverLeaveEvent(self, event):
168        self.__hover = False
169        self.__updateShadowState()
[11411]170        return GraphicsPathObject.hoverLeaveEvent(self, event)
[11102]171
172    def paint(self, painter, option, widget):
[11369]173        """
174        Paint the shape and a progress meter.
[11102]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
[11411]180        GraphicsPathObject.paint(self, painter, option, widget)
[11102]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
[11411]202        radius = 3
[11102]203        enabled = False
[11411]204
[11102]205        if self.__isSelected:
206            enabled = True
[11411]207            radius = 7
208
209        if self.__hover:
210            radius = 17
[11102]211            enabled = True
[11411]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)
[11102]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):
[11369]243        """
244        Set the `selected` state.
[11102]245
[11369]246        .. note:: The item does not have `QGraphicsItem.ItemIsSelectable` flag.
[11102]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):
[11369]254        """
255        Set the `has focus` state.
[11102]256
[11369]257        .. note:: The item does not have `QGraphicsItem.ItemIsFocusable` flag.
[11102]258                  This property is instead controlled by the parent NodeItem.
[11369]259
[11102]260        """
261        self.__hasFocus = focus
262        self.__updateShadowState()
263
[11411]264    def __on_finished(self):
265        if self.shadow.blurRadius() == 0:
266            self.shadow.setEnabled(False)
267
[11102]268
[11138]269class AnchorPoint(QGraphicsObject):
[11369]270    """
271    A anchor indicator on the :class:`NodeAnchorItem`.
[11138]272    """
273
[11369]274    # Signal emitted when the item's scene position changes.
[11138]275    scenePositionChanged = Signal(QPointF)
[11369]276
[11138]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):
[11369]287        """
288        Return anchor position in scene coordinates.
[11138]289        """
290        return self.mapToScene(QPointF(0, 0))
291
292    def setAnchorDirection(self, direction):
[11369]293        """
294        Set the preferred direction (QPointF) in item coordinates.
[11138]295        """
296        if self.__direction != direction:
297            self.__direction = direction
298            self.anchorDirectionChanged.emit(direction)
299
300    def anchorDirection(self):
[11369]301        """
302        Return the preferred anchor direction.
[11138]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
[11293]316class NodeAnchorItem(GraphicsPathObject):
[11369]317    """
318    The left/right widget input/output anchors.
[11102]319    """
320
[11153]321    def __init__(self, parent, *args):
[11293]322        GraphicsPathObject.__init__(self, parent, *args)
[11102]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
[11138]340
[11153]341        if isinstance(parent, NodeItem):
342            self.__parentNodeItem = parent
343        else:
344            self.__parentNodeItem = None
345
[11138]346        self.__anchorPath = QPainterPath()
347        self.__points = []
348        self.__pointPositions = []
349
[11102]350        self.__fullStroke = None
351        self.__dottedStroke = None
[11331]352        self.__shape = None
[11102]353
[11153]354    def parentNodeItem(self):
[11369]355        """
356        Return a parent :class:`NodeItem` or ``None`` if this anchor's
357        parent is not a :class:`NodeItem` instance.
[11153]358
359        """
360        return self.__parentNodeItem
361
[11102]362    def setAnchorPath(self, path):
[11369]363        """
364        Set the anchor's curve path as a :class:`QPainterPath`.
[11102]365        """
[11138]366        self.__anchorPath = path
[11102]367        # Create a stroke of the path.
368        stroke_path = QPainterPathStroker()
369        stroke_path.setCapStyle(Qt.RoundCap)
[11331]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
[11102]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
[11138]390    def anchorPath(self):
[11369]391        """
392        Return the anchor path (:class:`QPainterPath`). This is a curve on
393        which the anchor points lie.
[11138]394
395        """
396        return self.__anchorPath
397
[11102]398    def setAnchored(self, anchored):
[11369]399        """
400        Set the items anchored state. When ``False`` the item draws it self
[11138]401        with a dotted stroke.
402
[11102]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):
[11369]413        """
414        Set the connection hint. This can be used to indicate if
[11102]415        a connection can be made or not.
416
417        """
418        raise NotImplementedError
419
[11138]420    def count(self):
[11369]421        """
422        Return the number of anchor points.
[11138]423        """
424        return len(self.__points)
425
426    def addAnchor(self, anchor, position=0.5):
[11369]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
[11138]433        """
434        return self.insertAnchor(self.count(), anchor, position)
435
436    def insertAnchor(self, index, anchor, position=0.5):
[11369]437        """
438        Insert a new :class:`AnchorPoint` at `index`.
439
440        See also
441        --------
442        NodeAnchorItem.addAnchor
443
[11138]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):
[11369]462        """
463        Remove and delete the anchor point.
[11138]464        """
465        anchor = self.takeAnchor(anchor)
466
467        anchor.hide()
468        anchor.setParentItem(None)
469        anchor.deleteLater()
470
471    def takeAnchor(self, anchor):
[11369]472        """
473        Remove the anchor but don't delete it.
[11138]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):
[11369]498        """
499        Return a list of anchor points.
[11138]500        """
501        return list(self.__points)
502
503    def anchorPoint(self, index):
[11369]504        """
505        Return the anchor point at `index`.
[11138]506        """
507        return self.__points[index]
508
509    def setAnchorPositions(self, positions):
[11369]510        """
511        Set the anchor positions in percentages (0..1) along the path curve.
[11138]512        """
513        if self.__pointPositions != positions:
[11207]514            self.__pointPositions = list(positions)
[11138]515
516            self.__updatePositions()
517
518    def anchorPositions(self):
[11369]519        """
520        Return the positions of anchor points as a list of floats where
[11138]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        """
[11207]525        return list(self.__pointPositions)
[11138]526
[11102]527    def shape(self):
[11331]528        if self.__shape is not None:
529            return self.__shape
[11102]530        else:
[11293]531            return GraphicsPathObject.shape(self)
[11102]532
[11331]533    def boundingRect(self):
534        if self.__shape is not None:
535            return self.__shape.controlPointRect()
536        else:
537            return GraphicsPathObject.boundingRect(self)
538
[11102]539    def hoverEnterEvent(self, event):
540        self.shadow.setEnabled(True)
[11293]541        return GraphicsPathObject.hoverEnterEvent(self, event)
[11102]542
543    def hoverLeaveEvent(self, event):
544        self.shadow.setEnabled(False)
[11293]545        return GraphicsPathObject.hoverLeaveEvent(self, event)
[11102]546
[11138]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
[11102]554
555class SourceAnchorItem(NodeAnchorItem):
[11369]556    """
557    A source anchor item
[11102]558    """
559    pass
560
561
562class SinkAnchorItem(NodeAnchorItem):
[11369]563    """
564    A sink anchor item.
[11102]565    """
566    pass
567
568
[11186]569def standard_icon(standard_pixmap):
[11369]570    """
571    Return return the application style's standard icon for a
[11186]572    `QStyle.StandardPixmap`.
[11134]573
[11186]574    """
575    style = QApplication.instance().style()
576    return style.standardIcon(standard_pixmap)
[11134]577
[11186]578
579class GraphicsIconItem(QGraphicsItem):
[11369]580    """
581    A graphics item displaying an :class:`QIcon`.
[11186]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):
[11369]601        """
602        Set the icon (:class:`QIcon`).
[11186]603        """
604        if self.__icon != icon:
605            self.__icon = QIcon(icon)
606            self.update()
607
608    def icon(self):
[11369]609        """
610        Return the icon (:class:`QIcon`).
[11186]611        """
612        return QIcon(self.__icon)
613
614    def setIconSize(self, size):
[11369]615        """
616        Set the icon (and this item's) size (:class:`QSize`).
[11186]617        """
618        if self.__iconSize != size:
619            self.prepareGeometryChange()
620            self.__iconSize = QSize(size)
621            self.update()
622
623    def iconSize(self):
[11369]624        """
625        Return the icon size (:class:`QSize`).
[11186]626        """
627        return QSize(self.__iconSize)
628
629    def setTransformationMode(self, mode):
[11369]630        """
631        Set pixmap transformation mode. (`Qt.SmoothTransformation` or
[11186]632        `Qt.FastTransformation`).
633
634        """
635        if self.__transformationMode != mode:
636            self.__transformationMode = mode
637            self.update()
638
639    def transformationMode(self):
[11369]640        """
641        Return the pixmap transformation mode.
[11186]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))
[11134]687
688
[11102]689class NodeItem(QGraphicsObject):
[11369]690    """
691    An widget node item in the canvas.
[11102]692    """
693
[11369]694    # Scene position of the node has changed.
[11102]695    positionChanged = Signal()
696
[11369]697    # Geometry of the channel anchors changed
[11102]698    anchorGeometryChanged = Signal()
699
[11369]700    # The item has been activated (by a mouse double click or a keyboard).
[11102]701    activated = Signal()
702
[11369]703    # The item is under the mouse.
[11102]704    hovered = Signal()
705
[11369]706    #: Span of the anchor in degrees
[11102]707    ANCHOR_SPAN_ANGLE = 90
708
[11369]709    #: Z value of the item
[11102]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
[11134]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
[11102]735        self.__title = ""
736        self.__processingState = 0
737        self.__progress = -1
738
[11134]739        self.__error = None
740        self.__warning = None
741        self.__info = None
742
[11138]743        self.__anchorLayout = None
[11411]744        self.__animationEnabled = False
[11138]745
[11102]746        self.setZValue(self.Z_VALUE)
747        self.setupGraphics()
748
749        self.setWidgetDescription(widget_description)
750
751    @classmethod
752    def from_node(cls, node):
[11369]753        """
754        Create an :class:`NodeItem` instance and initialize it from a
755        :class:`SchemeNode` instance.
[11102]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):
[11369]765        """
766        Create an `NodeItem` instance from a node meta description.
[11102]767        """
768        self = cls()
769        self.setWidgetDescription(meta_description)
770        return self
771
772    def setupGraphics(self):
[11369]773        """
774        Set up the graphics.
[11102]775        """
776        shape_rect = QRectF(-24, -24, 48, 48)
777
778        self.shapeItem = NodeBodyItem(self)
779        self.shapeItem.setShapeRect(shape_rect)
[11411]780        self.shapeItem.setAnimationEnabled(self.__animationEnabled)
[11102]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
[11186]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
[11134]811
[11186]812        self.errorItem = iconItem(QStyle.SP_MessageBoxCritical)
813        self.warningItem = iconItem(QStyle.SP_MessageBoxWarning)
814        self.infoItem = iconItem(QStyle.SP_MessageBoxInformation)
[11134]815
[11369]816    # TODO: Remove the set[Widget|Category]Description. The user should
817    # handle setting of icons, title, ...
[11102]818    def setWidgetDescription(self, desc):
[11369]819        """
820        Set widget description.
[11102]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):
[11369]842        """
843        Set the widget category.
844        """
[11102]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):
[11369]853        """
854        Set the node item's icon.
[11102]855        """
856        if isinstance(icon, QIcon):
[11186]857            self.icon_item = GraphicsIconItem(self.shapeItem, icon=icon,
858                                              iconSize=QSize(36, 36))
859            self.icon_item.setPos(-18, -18)
[11102]860        else:
861            raise TypeError
862
863    def setColor(self, color, selectedColor=None):
[11369]864        """
865        Set the widget color.
[11102]866        """
867        if selectedColor is None:
868            selectedColor = saturated(color, 150)
869        palette = create_palette(color, selectedColor)
870        self.shapeItem.setPalette(palette)
871
[11369]872    def setPalette(self, palette):
873        # TODO: The palette should override the `setColor`
874        raise NotImplementedError
[11102]875
876    def setTitle(self, title):
[11369]877        """
878        Set the node title. The title text is displayed at the bottom of the
879        node.
880
[11102]881        """
882        self.__title = title
883        self.__updateTitleText()
884
885    def title(self):
[11369]886        """
887        Return the node title.
888        """
[11102]889        return self.__title
890
[11369]891    title_ = Property(unicode, fget=title, fset=setTitle,
892                      doc="Node title text.")
[11102]893
[11343]894    def setFont(self, font):
895        """
[11369]896        Set the title text font (:class:`QFont`).
[11343]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
[11411]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
[11102]923    def setProcessingState(self, state):
[11369]924        """
925        Set the node processing state i.e. the node is processing
[11102]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)
[11411]935                if self.__animationEnabled:
936                    self.shapeItem.ping()
[11102]937
938    def processingState(self):
[11369]939        """
940        The node processing state.
941        """
[11102]942        return self.__processingState
943
944    processingState_ = Property(int, fget=processingState,
945                                fset=setProcessingState)
946
947    def setProgress(self, progress):
[11369]948        """
949        Set the node work progress state (number between 0 and 100).
[11102]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):
[11369]961        """
962        Return the node work progress state.
963        """
[11102]964        return self.__progress
965
[11369]966    progress_ = Property(float, fget=progress, fset=setProgress,
967                         doc="Node progress state.")
[11102]968
969    def setProgressMessage(self, message):
[11369]970        """
971        Set the node work progress message.
972
973        .. note:: Not yet implemented
974
[11102]975        """
976        pass
977
978    def setErrorMessage(self, message):
[11134]979        if self.__error != message:
980            self.__error = message
981            self.__updateMessages()
[11102]982
983    def setWarningMessage(self, message):
[11134]984        if self.__warning != message:
985            self.__warning = message
986            self.__updateMessages()
[11102]987
[11134]988    def setInfoMessage(self, message):
989        if self.__info != message:
990            self.__info = message
991            self.__updateMessages()
[11102]992
993    def newInputAnchor(self):
[11369]994        """
995        Create and return a new input anchor point.
[11102]996        """
997        if not (self.widget_description and self.widget_description.inputs):
998            raise ValueError("Widget has no inputs.")
999
[11138]1000        anchor = AnchorPoint()
[11207]1001        self.inputAnchorItem.addAnchor(anchor, position=1.0)
[11102]1002
[11207]1003        positions = self.inputAnchorItem.anchorPositions()
1004        positions = uniform_linear_layout(positions)
1005        self.inputAnchorItem.setAnchorPositions(positions)
[11102]1006
1007        return anchor
1008
1009    def removeInputAnchor(self, anchor):
[11369]1010        """
1011        Remove input anchor.
[11102]1012        """
[11138]1013        self.inputAnchorItem.removeAnchor(anchor)
[11102]1014
[11207]1015        positions = self.inputAnchorItem.anchorPositions()
1016        positions = uniform_linear_layout(positions)
1017        self.inputAnchorItem.setAnchorPositions(positions)
[11102]1018
1019    def newOutputAnchor(self):
[11369]1020        """
1021        Create a new output anchor indicator.
[11102]1022        """
1023        if not (self.widget_description and self.widget_description.outputs):
1024            raise ValueError("Widget has no outputs.")
1025
1026        anchor = AnchorPoint(self)
[11207]1027        self.outputAnchorItem.addAnchor(anchor, position=1.0)
[11102]1028
[11207]1029        positions = self.outputAnchorItem.anchorPositions()
1030        positions = uniform_linear_layout(positions)
1031        self.outputAnchorItem.setAnchorPositions(positions)
[11102]1032
1033        return anchor
1034
1035    def removeOutputAnchor(self, anchor):
[11369]1036        """
1037        Remove output anchor.
[11102]1038        """
[11138]1039        self.outputAnchorItem.removeAnchor(anchor)
[11102]1040
[11207]1041        positions = self.outputAnchorItem.anchorPositions()
1042        positions = uniform_linear_layout(positions)
1043        self.outputAnchorItem.setAnchorPositions(positions)
[11102]1044
[11138]1045    def inputAnchors(self):
[11369]1046        """
1047        Return a list of input anchor points.
[11138]1048        """
1049        return self.inputAnchorItem.anchorPoints()
[11102]1050
[11138]1051    def outputAnchors(self):
[11369]1052        """
1053        Return a list of output anchor points.
[11138]1054        """
1055        return self.outputAnchorItem.anchorPoints()
[11102]1056
[11138]1057    def setAnchorRotation(self, angle):
[11369]1058        """
1059        Set the anchor rotation.
[11138]1060        """
1061        self.inputAnchorItem.setRotation(angle)
1062        self.outputAnchorItem.setRotation(angle)
1063        self.anchorGeometryChanged.emit()
[11102]1064
[11138]1065    def anchorRotation(self):
[11369]1066        """
1067        Return the anchor rotation.
[11102]1068        """
[11138]1069        return self.inputAnchorItem.rotation()
[11102]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):
[11369]1078        # Shape for mouse hit detection.
1079        # TODO: Should this return the union of all child items?
[11102]1080        return self.shapeItem.shape()
1081
1082    def __updateTitleText(self):
[11369]1083        """
1084        Update the title text item.
[11102]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
[11134]1101    def __updateMessages(self):
[11369]1102        """
1103        Update message items (position, visibility and tool tips).
[11134]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
[11102]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=[]):
[11369]1175    """
1176    A helper function for constructing a standard tooltop for the node
[11102]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.