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.

Line 
1"""
2=========
3Node Item
4=========
5
6"""
7
8from xml.sax.saxutils import escape
9
10from PyQt4.QtGui import (
11    QGraphicsItem, QGraphicsObject, QGraphicsTextItem,
12    QGraphicsDropShadowEffect, QGraphicsView,
13    QPen, QBrush, QColor, QPalette, QIcon, QStyle, QPainter,
14    QPainterPath, QPainterPathStroker, QApplication
15)
16
17from PyQt4.QtCore import Qt, QPointF, QRectF, QSize, QTimer, QPropertyAnimation
18from PyQt4.QtCore import pyqtSignal as Signal
19from PyQt4.QtCore import pyqtProperty as Property
20
21from .graphicspathobject import GraphicsPathObject
22from .utils import saturated, radial_gradient
23
24from ...registry import NAMED_COLORS
25from ...resources import icon_loader
26from .utils import uniform_linear_layout
27
28
29def create_palette(light_color, color):
30    """
31    Return a new :class:`QPalette` from for the :class:`NodeBodyItem`.
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():
53    """
54    Create and return a default palette for a node.
55    """
56    return create_palette(QColor(NAMED_COLORS["light-yellow"]),
57                          QColor(NAMED_COLORS["yellow"]))
58
59
60def animation_restart(animation):
61    if animation.state() == QPropertyAnimation.Running:
62        animation.pause()
63    animation.start()
64
65
66SHADOW_COLOR = "#9CACB4"
67FOCUS_OUTLINE_COLOR = "#609ED7"
68
69
70class NodeBodyItem(GraphicsPathObject):
71    """
72    The central part (body) of the `NodeItem`.
73    """
74    def __init__(self, parent=None):
75        GraphicsPathObject.__init__(self, parent)
76        assert(isinstance(parent, NodeItem))
77
78        self.__processingState = 0
79        self.__progress = -1
80        self.__animationEnabled = False
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(
96            blurRadius=3,
97            color=QColor(SHADOW_COLOR),
98            offset=QPointF(0, 0),
99            )
100
101        self.setGraphicsEffect(self.shadow)
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)])
112
113    # TODO: The body item should allow the setting of arbitrary painter
114    # paths (for instance rounded rect, ...)
115    def setShapeRect(self, rect):
116        """
117        Set the item's shape `rect`. The item should be confined within
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):
127        """
128        Set the body color palette (:class:`QPalette`).
129        """
130        self.palette = palette
131        self.__updateBrush()
132
133    def setAnimationEnabled(self, enabled):
134        """
135        Set the node animation enabled.
136        """
137        if self.__animationEnabled != enabled:
138            self.__animationEnabled = enabled
139
140    def setProcessingState(self, state):
141        """
142        Set the processing state of the node.
143        """
144        if self.__processingState != state:
145            self.__processingState = state
146            if not state and self.__animationEnabled:
147                self.ping()
148
149    def setProgress(self, progress):
150        """
151        Set the progress indicator state of the node. `progress` should
152        be a number between 0 and 100.
153
154        """
155        self.__progress = progress
156        self.update()
157
158    def ping(self):
159        """
160        Trigger a 'ping' animation.
161        """
162        animation_restart(self.__pingAnimation)
163
164    def hoverEnterEvent(self, event):
165        self.__hover = True
166        self.__updateShadowState()
167        return GraphicsPathObject.hoverEnterEvent(self, event)
168
169    def hoverLeaveEvent(self, event):
170        self.__hover = False
171        self.__updateShadowState()
172        return GraphicsPathObject.hoverLeaveEvent(self, event)
173
174    def paint(self, painter, option, widget):
175        """
176        Paint the shape and a progress meter.
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
182        GraphicsPathObject.paint(self, painter, option, widget)
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
204        radius = 3
205        enabled = False
206
207        if self.__isSelected:
208            enabled = True
209            radius = 7
210
211        if self.__hover:
212            radius = 17
213            enabled = True
214
215        if enabled and not self.shadow.isEnabled():
216            self.shadow.setEnabled(enabled)
217
218        if self.__animationEnabled:
219            if self.__blurAnimation.state() == QPropertyAnimation.Running:
220                self.__blurAnimation.pause()
221
222            self.__blurAnimation.setStartValue(self.shadow.blurRadius())
223            self.__blurAnimation.setEndValue(radius)
224            self.__blurAnimation.start()
225        else:
226            self.shadow.setBlurRadius(radius)
227
228    def __updateBrush(self):
229        palette = self.palette
230        if self.__isSelected:
231            cg = QPalette.Active
232        else:
233            cg = QPalette.Inactive
234
235        palette.setCurrentColorGroup(cg)
236        c1 = palette.color(QPalette.Light)
237        c2 = palette.color(QPalette.Button)
238        grad = radial_gradient(c2, c1)
239        self.setBrush(QBrush(grad))
240
241    # TODO: The selected and focus states should be set using the
242    # QStyle flags (State_Selected. State_HasFocus)
243
244    def setSelected(self, selected):
245        """
246        Set the `selected` state.
247
248        .. note:: The item does not have `QGraphicsItem.ItemIsSelectable` flag.
249                  This property is instead controlled by the parent NodeItem.
250
251        """
252        self.__isSelected = selected
253        self.__updateBrush()
254
255    def setHasFocus(self, focus):
256        """
257        Set the `has focus` state.
258
259        .. note:: The item does not have `QGraphicsItem.ItemIsFocusable` flag.
260                  This property is instead controlled by the parent NodeItem.
261
262        """
263        self.__hasFocus = focus
264        self.__updateShadowState()
265
266    def __on_finished(self):
267        if self.shadow.blurRadius() == 0:
268            self.shadow.setEnabled(False)
269
270
271class AnchorPoint(QGraphicsObject):
272    """
273    A anchor indicator on the :class:`NodeAnchorItem`.
274    """
275
276    #: Signal emitted when the item's scene position changes.
277    scenePositionChanged = Signal(QPointF)
278
279    #: Signal emitted when the item's `anchorDirection` changes.
280    anchorDirectionChanged = Signal(QPointF)
281
282    def __init__(self, *args):
283        QGraphicsObject.__init__(self, *args)
284        self.setFlag(QGraphicsItem.ItemSendsScenePositionChanges, True)
285        self.setFlag(QGraphicsItem.ItemHasNoContents, True)
286
287        self.__direction = QPointF()
288
289    def anchorScenePos(self):
290        """
291        Return anchor position in scene coordinates.
292        """
293        return self.mapToScene(QPointF(0, 0))
294
295    def setAnchorDirection(self, direction):
296        """
297        Set the preferred direction (QPointF) in item coordinates.
298        """
299        if self.__direction != direction:
300            self.__direction = direction
301            self.anchorDirectionChanged.emit(direction)
302
303    def anchorDirection(self):
304        """
305        Return the preferred anchor direction.
306        """
307        return self.__direction
308
309    def itemChange(self, change, value):
310        if change == QGraphicsItem.ItemScenePositionHasChanged:
311            self.scenePositionChanged.emit(value.toPointF())
312
313        return QGraphicsObject.itemChange(self, change, value)
314
315    def boundingRect(self,):
316        return QRectF()
317
318
319class NodeAnchorItem(GraphicsPathObject):
320    """
321    The left/right widget input/output anchors.
322    """
323
324    def __init__(self, parent, *args):
325        GraphicsPathObject.__init__(self, parent, *args)
326        self.setAcceptHoverEvents(True)
327        self.setPen(QPen(Qt.NoPen))
328        self.normalBrush = QBrush(QColor("#CDD5D9"))
329        self.connectedBrush = QBrush(QColor("#9CACB4"))
330        self.setBrush(self.normalBrush)
331
332        self.shadow = QGraphicsDropShadowEffect(
333            blurRadius=10,
334            color=QColor(SHADOW_COLOR),
335            offset=QPointF(0, 0)
336        )
337
338        self.setGraphicsEffect(self.shadow)
339        self.shadow.setEnabled(False)
340
341        # Does this item have any anchored links.
342        self.anchored = False
343
344        if isinstance(parent, NodeItem):
345            self.__parentNodeItem = parent
346        else:
347            self.__parentNodeItem = None
348
349        self.__anchorPath = QPainterPath()
350        self.__points = []
351        self.__pointPositions = []
352
353        self.__fullStroke = None
354        self.__dottedStroke = None
355        self.__shape = None
356
357    def parentNodeItem(self):
358        """
359        Return a parent :class:`NodeItem` or ``None`` if this anchor's
360        parent is not a :class:`NodeItem` instance.
361
362        """
363        return self.__parentNodeItem
364
365    def setAnchorPath(self, path):
366        """
367        Set the anchor's curve path as a :class:`QPainterPath`.
368        """
369        self.__anchorPath = path
370        # Create a stroke of the path.
371        stroke_path = QPainterPathStroker()
372        stroke_path.setCapStyle(Qt.RoundCap)
373
374        # Shape is wider (bigger mouse hit area - should be settable)
375        stroke_path.setWidth(9)
376        self.__shape = stroke_path.createStroke(path)
377
378        # The full stroke
379        stroke_path.setWidth(3)
380        self.__fullStroke = stroke_path.createStroke(path)
381
382        # The dotted stroke (when not connected to anything)
383        stroke_path.setDashPattern(Qt.DotLine)
384        self.__dottedStroke = stroke_path.createStroke(path)
385
386        if self.anchored:
387            self.setPath(self.__fullStroke)
388            self.setBrush(self.connectedBrush)
389        else:
390            self.setPath(self.__dottedStroke)
391            self.setBrush(self.normalBrush)
392
393    def anchorPath(self):
394        """
395        Return the anchor path (:class:`QPainterPath`). This is a curve on
396        which the anchor points lie.
397
398        """
399        return self.__anchorPath
400
401    def setAnchored(self, anchored):
402        """
403        Set the items anchored state. When ``False`` the item draws it self
404        with a dotted stroke.
405
406        """
407        self.anchored = anchored
408        if anchored:
409            self.setPath(self.__fullStroke)
410            self.setBrush(self.connectedBrush)
411        else:
412            self.setPath(self.__dottedStroke)
413            self.setBrush(self.normalBrush)
414
415    def setConnectionHint(self, hint=None):
416        """
417        Set the connection hint. This can be used to indicate if
418        a connection can be made or not.
419
420        """
421        raise NotImplementedError
422
423    def count(self):
424        """
425        Return the number of anchor points.
426        """
427        return len(self.__points)
428
429    def addAnchor(self, anchor, position=0.5):
430        """
431        Add a new :class:`AnchorPoint` to this item and return it's index.
432
433        The `position` specifies where along the `anchorPath` is the new
434        point inserted.
435
436        """
437        return self.insertAnchor(self.count(), anchor, position)
438
439    def insertAnchor(self, index, anchor, position=0.5):
440        """
441        Insert a new :class:`AnchorPoint` at `index`.
442
443        See also
444        --------
445        NodeAnchorItem.addAnchor
446
447        """
448        if anchor in self.__points:
449            raise ValueError("%s already added." % anchor)
450
451        self.__points.insert(index, anchor)
452        self.__pointPositions.insert(index, position)
453
454        anchor.setParentItem(self)
455        anchor.setPos(self.__anchorPath.pointAtPercent(position))
456        anchor.destroyed.connect(self.__onAnchorDestroyed)
457
458        self.__updatePositions()
459
460        self.setAnchored(bool(self.__points))
461
462        return index
463
464    def removeAnchor(self, anchor):
465        """
466        Remove and delete the anchor point.
467        """
468        anchor = self.takeAnchor(anchor)
469
470        anchor.hide()
471        anchor.setParentItem(None)
472        anchor.deleteLater()
473
474    def takeAnchor(self, anchor):
475        """
476        Remove the anchor but don't delete it.
477        """
478        index = self.__points.index(anchor)
479
480        del self.__points[index]
481        del self.__pointPositions[index]
482
483        anchor.destroyed.disconnect(self.__onAnchorDestroyed)
484
485        self.__updatePositions()
486
487        self.setAnchored(bool(self.__points))
488
489        return anchor
490
491    def __onAnchorDestroyed(self, anchor):
492        try:
493            index = self.__points.index(anchor)
494        except ValueError:
495            return
496
497        del self.__points[index]
498        del self.__pointPositions[index]
499
500    def anchorPoints(self):
501        """
502        Return a list of anchor points.
503        """
504        return list(self.__points)
505
506    def anchorPoint(self, index):
507        """
508        Return the anchor point at `index`.
509        """
510        return self.__points[index]
511
512    def setAnchorPositions(self, positions):
513        """
514        Set the anchor positions in percentages (0..1) along the path curve.
515        """
516        if self.__pointPositions != positions:
517            self.__pointPositions = list(positions)
518
519            self.__updatePositions()
520
521    def anchorPositions(self):
522        """
523        Return the positions of anchor points as a list of floats where
524        each float is between 0 and 1 and specifies where along the anchor
525        path does the point lie (0 is at start 1 is at the end).
526
527        """
528        return list(self.__pointPositions)
529
530    def shape(self):
531        if self.__shape is not None:
532            return self.__shape
533        else:
534            return GraphicsPathObject.shape(self)
535
536    def boundingRect(self):
537        if self.__shape is not None:
538            return self.__shape.controlPointRect()
539        else:
540            return GraphicsPathObject.boundingRect(self)
541
542    def hoverEnterEvent(self, event):
543        self.shadow.setEnabled(True)
544        return GraphicsPathObject.hoverEnterEvent(self, event)
545
546    def hoverLeaveEvent(self, event):
547        self.shadow.setEnabled(False)
548        return GraphicsPathObject.hoverLeaveEvent(self, event)
549
550    def __updatePositions(self):
551        """Update anchor points positions.
552        """
553        for point, t in zip(self.__points, self.__pointPositions):
554            pos = self.__anchorPath.pointAtPercent(t)
555            point.setPos(pos)
556
557
558class SourceAnchorItem(NodeAnchorItem):
559    """
560    A source anchor item
561    """
562    pass
563
564
565class SinkAnchorItem(NodeAnchorItem):
566    """
567    A sink anchor item.
568    """
569    pass
570
571
572def standard_icon(standard_pixmap):
573    """
574    Return return the application style's standard icon for a
575    `QStyle.StandardPixmap`.
576
577    """
578    style = QApplication.instance().style()
579    return style.standardIcon(standard_pixmap)
580
581
582class GraphicsIconItem(QGraphicsItem):
583    """
584    A graphics item displaying an :class:`QIcon`.
585    """
586    def __init__(self, parent=None, icon=None, iconSize=None, **kwargs):
587        QGraphicsItem.__init__(self, parent, **kwargs)
588        self.setFlag(QGraphicsItem.ItemUsesExtendedStyleOption, True)
589
590        if icon is None:
591            icon = QIcon()
592
593        if iconSize is None:
594            style = QApplication.instance().style()
595            size = style.pixelMetric(style.PM_LargeIconSize)
596            iconSize = QSize(size, size)
597
598        self.__transformationMode = Qt.SmoothTransformation
599
600        self.__iconSize = QSize(iconSize)
601        self.__icon = QIcon(icon)
602
603    def setIcon(self, icon):
604        """
605        Set the icon (:class:`QIcon`).
606        """
607        if self.__icon != icon:
608            self.__icon = QIcon(icon)
609            self.update()
610
611    def icon(self):
612        """
613        Return the icon (:class:`QIcon`).
614        """
615        return QIcon(self.__icon)
616
617    def setIconSize(self, size):
618        """
619        Set the icon (and this item's) size (:class:`QSize`).
620        """
621        if self.__iconSize != size:
622            self.prepareGeometryChange()
623            self.__iconSize = QSize(size)
624            self.update()
625
626    def iconSize(self):
627        """
628        Return the icon size (:class:`QSize`).
629        """
630        return QSize(self.__iconSize)
631
632    def setTransformationMode(self, mode):
633        """
634        Set pixmap transformation mode. (`Qt.SmoothTransformation` or
635        `Qt.FastTransformation`).
636
637        """
638        if self.__transformationMode != mode:
639            self.__transformationMode = mode
640            self.update()
641
642    def transformationMode(self):
643        """
644        Return the pixmap transformation mode.
645        """
646        return self.__transformationMode
647
648    def boundingRect(self):
649        return QRectF(0, 0, self.__iconSize.width(), self.__iconSize.height())
650
651    def paint(self, painter, option, widget=None):
652        if not self.__icon.isNull():
653            if option.state & QStyle.State_Selected:
654                mode = QIcon.Selected
655            elif option.state & QStyle.State_Enabled:
656                mode = QIcon.Normal
657            elif option.state & QStyle.State_Active:
658                mode = QIcon.Active
659            else:
660                mode = QIcon.Disabled
661
662            transform = self.sceneTransform()
663
664            if widget is not None:
665                # 'widget' is the QGraphicsView.viewport()
666                view = widget.parent()
667                if isinstance(view, QGraphicsView):
668                    # Combine the scene transform with the view transform.
669                    view_transform = view.transform()
670                    transform = view_transform * view_transform
671
672            lod = option.levelOfDetailFromTransform(transform)
673
674            w, h = self.__iconSize.width(), self.__iconSize.height()
675            target = QRectF(0, 0, w, h)
676            source = QRectF(0, 0, w * lod, w * lod).toRect()
677
678            # The actual size of the requested pixmap can be smaller.
679            size = self.__icon.actualSize(source.size(), mode=mode)
680            source.setSize(size)
681
682            pixmap = self.__icon.pixmap(source.size(), mode=mode)
683
684            painter.setRenderHint(
685                QPainter.SmoothPixmapTransform,
686                self.__transformationMode == Qt.SmoothTransformation
687            )
688
689            painter.drawPixmap(target, pixmap, QRectF(source))
690
691
692class NodeItem(QGraphicsObject):
693    """
694    An widget node item in the canvas.
695    """
696
697    #: Signal emitted when the scene position of the node has changed.
698    positionChanged = Signal()
699
700    #: Signal emitted when the geometry of the channel anchors changes.
701    anchorGeometryChanged = Signal()
702
703    #: Signal emitted when the item has been activated (by a mouse double
704    #: click or a keyboard)
705    activated = Signal()
706
707    #: The item is under the mouse.
708    hovered = Signal()
709
710    #: Span of the anchor in degrees
711    ANCHOR_SPAN_ANGLE = 90
712
713    #: Z value of the item
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
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
739        self.__title = ""
740        self.__processingState = 0
741        self.__progress = -1
742
743        self.__error = None
744        self.__warning = None
745        self.__info = None
746
747        self.__anchorLayout = None
748        self.__animationEnabled = False
749
750        self.setZValue(self.Z_VALUE)
751        self.setupGraphics()
752
753        self.setWidgetDescription(widget_description)
754
755    @classmethod
756    def from_node(cls, node):
757        """
758        Create an :class:`NodeItem` instance and initialize it from a
759        :class:`SchemeNode` instance.
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):
769        """
770        Create an `NodeItem` instance from a node meta description.
771        """
772        self = cls()
773        self.setWidgetDescription(meta_description)
774        return self
775
776    def setupGraphics(self):
777        """
778        Set up the graphics.
779        """
780        shape_rect = QRectF(-24, -24, 48, 48)
781
782        self.shapeItem = NodeBodyItem(self)
783        self.shapeItem.setShapeRect(shape_rect)
784        self.shapeItem.setAnimationEnabled(self.__animationEnabled)
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
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
815
816        self.errorItem = iconItem(QStyle.SP_MessageBoxCritical)
817        self.warningItem = iconItem(QStyle.SP_MessageBoxWarning)
818        self.infoItem = iconItem(QStyle.SP_MessageBoxInformation)
819
820    # TODO: Remove the set[Widget|Category]Description. The user should
821    # handle setting of icons, title, ...
822    def setWidgetDescription(self, desc):
823        """
824        Set widget description.
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):
846        """
847        Set the widget category.
848        """
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):
857        """
858        Set the node item's icon (:class:`QIcon`).
859        """
860        if isinstance(icon, QIcon):
861            self.icon_item = GraphicsIconItem(self.shapeItem, icon=icon,
862                                              iconSize=QSize(36, 36))
863            self.icon_item.setPos(-18, -18)
864        else:
865            raise TypeError
866
867    def setColor(self, color, selectedColor=None):
868        """
869        Set the widget color.
870        """
871        if selectedColor is None:
872            selectedColor = saturated(color, 150)
873        palette = create_palette(color, selectedColor)
874        self.shapeItem.setPalette(palette)
875
876    def setPalette(self, palette):
877        # TODO: The palette should override the `setColor`
878        raise NotImplementedError
879
880    def setTitle(self, title):
881        """
882        Set the node title. The title text is displayed at the bottom of the
883        node.
884
885        """
886        self.__title = title
887        self.__updateTitleText()
888
889    def title(self):
890        """
891        Return the node title.
892        """
893        return self.__title
894
895    title_ = Property(unicode, fget=title, fset=setTitle,
896                      doc="Node title text.")
897
898    def setFont(self, font):
899        """
900        Set the title text font (:class:`QFont`).
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
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
927    def setProcessingState(self, state):
928        """
929        Set the node processing state i.e. the node is processing
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)
939                if self.__animationEnabled:
940                    self.shapeItem.ping()
941
942    def processingState(self):
943        """
944        The node processing state.
945        """
946        return self.__processingState
947
948    processingState_ = Property(int, fget=processingState,
949                                fset=setProcessingState)
950
951    def setProgress(self, progress):
952        """
953        Set the node work progress state (number between 0 and 100).
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):
965        """
966        Return the node work progress state.
967        """
968        return self.__progress
969
970    progress_ = Property(float, fget=progress, fset=setProgress,
971                         doc="Node progress state.")
972
973    def setProgressMessage(self, message):
974        """
975        Set the node work progress message.
976
977        .. note:: Not yet implemented
978
979        """
980        pass
981
982    def setErrorMessage(self, message):
983        if self.__error != message:
984            self.__error = message
985            self.__updateMessages()
986
987    def setWarningMessage(self, message):
988        if self.__warning != message:
989            self.__warning = message
990            self.__updateMessages()
991
992    def setInfoMessage(self, message):
993        if self.__info != message:
994            self.__info = message
995            self.__updateMessages()
996
997    def newInputAnchor(self):
998        """
999        Create and return a new input :class:`AnchorPoint`.
1000        """
1001        if not (self.widget_description and self.widget_description.inputs):
1002            raise ValueError("Widget has no inputs.")
1003
1004        anchor = AnchorPoint()
1005        self.inputAnchorItem.addAnchor(anchor, position=1.0)
1006
1007        positions = self.inputAnchorItem.anchorPositions()
1008        positions = uniform_linear_layout(positions)
1009        self.inputAnchorItem.setAnchorPositions(positions)
1010
1011        return anchor
1012
1013    def removeInputAnchor(self, anchor):
1014        """
1015        Remove input anchor.
1016        """
1017        self.inputAnchorItem.removeAnchor(anchor)
1018
1019        positions = self.inputAnchorItem.anchorPositions()
1020        positions = uniform_linear_layout(positions)
1021        self.inputAnchorItem.setAnchorPositions(positions)
1022
1023    def newOutputAnchor(self):
1024        """
1025        Create and return a new output :class:`AnchorPoint`.
1026        """
1027        if not (self.widget_description and self.widget_description.outputs):
1028            raise ValueError("Widget has no outputs.")
1029
1030        anchor = AnchorPoint(self)
1031        self.outputAnchorItem.addAnchor(anchor, position=1.0)
1032
1033        positions = self.outputAnchorItem.anchorPositions()
1034        positions = uniform_linear_layout(positions)
1035        self.outputAnchorItem.setAnchorPositions(positions)
1036
1037        return anchor
1038
1039    def removeOutputAnchor(self, anchor):
1040        """
1041        Remove output anchor.
1042        """
1043        self.outputAnchorItem.removeAnchor(anchor)
1044
1045        positions = self.outputAnchorItem.anchorPositions()
1046        positions = uniform_linear_layout(positions)
1047        self.outputAnchorItem.setAnchorPositions(positions)
1048
1049    def inputAnchors(self):
1050        """
1051        Return a list of all input anchor points.
1052        """
1053        return self.inputAnchorItem.anchorPoints()
1054
1055    def outputAnchors(self):
1056        """
1057        Return a list of all output anchor points.
1058        """
1059        return self.outputAnchorItem.anchorPoints()
1060
1061    def setAnchorRotation(self, angle):
1062        """
1063        Set the anchor rotation.
1064        """
1065        self.inputAnchorItem.setRotation(angle)
1066        self.outputAnchorItem.setRotation(angle)
1067        self.anchorGeometryChanged.emit()
1068
1069    def anchorRotation(self):
1070        """
1071        Return the anchor rotation.
1072        """
1073        return self.inputAnchorItem.rotation()
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):
1082        # Shape for mouse hit detection.
1083        # TODO: Should this return the union of all child items?
1084        return self.shapeItem.shape()
1085
1086    def __updateTitleText(self):
1087        """
1088        Update the title text item.
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
1105    def __updateMessages(self):
1106        """
1107        Update message items (position, visibility and tool tips).
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
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=[]):
1179    """
1180    A helper function for constructing a standard tooltop for the node
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.