source: orange/Orange/OrangeCanvas/canvas/items/nodeitem.py @ 11369:e9c95cc39be6

Revision 11369:e9c95cc39be6, 33.3 KB checked in by Ales Erjavec <ales.erjavec@…>, 14 months ago (diff)

Added rst documentation for the canvas package.

Fixing docstrings in the process.

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