source: orange/Orange/OrangeCanvas/canvas/items/nodeitem.py @ 11442:279d7a51ea1d

Revision 11442:279d7a51ea1d, 35.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 13 months ago (diff)

Fixes to canvas package documentation.

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