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.

RevLine 
[11102]1"""
2NodeItem
3
4"""
5
6from xml.sax.saxutils import escape
7
8from PyQt4.QtGui import (
[11186]9    QGraphicsItem, QGraphicsPathItem, QGraphicsObject,
10    QGraphicsTextItem, QGraphicsDropShadowEffect, QGraphicsView,
[11369]11    QPen, QBrush, QColor, QPalette, QIcon, QStyle, QPainter,
12    QPainterPath, QPainterPathStroker, QApplication
[11102]13)
14
[11186]15from PyQt4.QtCore import Qt, QPointF, QRectF, QSize, QTimer
[11102]16from PyQt4.QtCore import pyqtSignal as Signal
17from PyQt4.QtCore import pyqtProperty as Property
18
[11293]19from .graphicspathobject import GraphicsPathObject
[11138]20from .utils import saturated, radial_gradient
[11102]21
22from ...registry import NAMED_COLORS
23from ...resources import icon_loader
[11207]24from .utils import uniform_linear_layout
[11102]25
26
27def create_palette(light_color, color):
[11369]28    """
29    Return a new :class:`QPalette` from for the :class:`NodeBodyItem`.
[11102]30    """
31    palette = QPalette()
32
33    palette.setColor(QPalette.Inactive, QPalette.Light,
34                     saturated(light_color, 50))
35    palette.setColor(QPalette.Inactive, QPalette.Midlight,
36                     saturated(light_color, 90))
37    palette.setColor(QPalette.Inactive, QPalette.Button,
38                     light_color)
39
40    palette.setColor(QPalette.Active, QPalette.Light,
41                     saturated(color, 50))
42    palette.setColor(QPalette.Active, QPalette.Midlight,
43                     saturated(color, 90))
44    palette.setColor(QPalette.Active, QPalette.Button,
45                     color)
46    palette.setColor(QPalette.ButtonText, QColor("#515151"))
47    return palette
48
49
50def default_palette():
[11369]51    """
52    Create and return a default palette for a node.
[11102]53    """
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):
[11369]63    """
64    The central part (body) of the `NodeItem`.
[11102]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):
[11369]98        """
99        Set the item's shape `rect`. The item should be confined within
[11102]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):
[11369]109        """
110        Set the body color palette (:class:`QPalette`).
[11102]111        """
112        self.palette = palette
113        self.__updateBrush()
114
115    def setProcessingState(self, state):
[11369]116        """
117        Set the processing state of the node.
[11102]118        """
119        self.__processingState = state
120        self.update()
121
122    def setProgress(self, progress):
[11369]123        """
124        Set the progress indicator state of the node. `progress` should
125        be a number between 0 and 100.
126
127        """
[11102]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):
[11369]142        """
143        Paint the shape and a progress meter.
[11102]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):
[11369]197        """
198        Set the `selected` state.
[11102]199
[11369]200        .. note:: The item does not have `QGraphicsItem.ItemIsSelectable` flag.
[11102]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):
[11369]208        """
209        Set the `has focus` state.
[11102]210
[11369]211        .. note:: The item does not have `QGraphicsItem.ItemIsFocusable` flag.
[11102]212                  This property is instead controlled by the parent NodeItem.
[11369]213
[11102]214        """
215        self.__hasFocus = focus
216        self.__updateShadowState()
217
218
[11138]219class AnchorPoint(QGraphicsObject):
[11369]220    """
221    A anchor indicator on the :class:`NodeAnchorItem`.
[11138]222    """
223
[11369]224    # Signal emitted when the item's scene position changes.
[11138]225    scenePositionChanged = Signal(QPointF)
[11369]226
[11138]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):
[11369]237        """
238        Return anchor position in scene coordinates.
[11138]239        """
240        return self.mapToScene(QPointF(0, 0))
241
242    def setAnchorDirection(self, direction):
[11369]243        """
244        Set the preferred direction (QPointF) in item coordinates.
[11138]245        """
246        if self.__direction != direction:
247            self.__direction = direction
248            self.anchorDirectionChanged.emit(direction)
249
250    def anchorDirection(self):
[11369]251        """
252        Return the preferred anchor direction.
[11138]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
[11293]266class NodeAnchorItem(GraphicsPathObject):
[11369]267    """
268    The left/right widget input/output anchors.
[11102]269    """
270
[11153]271    def __init__(self, parent, *args):
[11293]272        GraphicsPathObject.__init__(self, parent, *args)
[11102]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
[11138]290
[11153]291        if isinstance(parent, NodeItem):
292            self.__parentNodeItem = parent
293        else:
294            self.__parentNodeItem = None
295
[11138]296        self.__anchorPath = QPainterPath()
297        self.__points = []
298        self.__pointPositions = []
299
[11102]300        self.__fullStroke = None
301        self.__dottedStroke = None
[11331]302        self.__shape = None
[11102]303
[11153]304    def parentNodeItem(self):
[11369]305        """
306        Return a parent :class:`NodeItem` or ``None`` if this anchor's
307        parent is not a :class:`NodeItem` instance.
[11153]308
309        """
310        return self.__parentNodeItem
311
[11102]312    def setAnchorPath(self, path):
[11369]313        """
314        Set the anchor's curve path as a :class:`QPainterPath`.
[11102]315        """
[11138]316        self.__anchorPath = path
[11102]317        # Create a stroke of the path.
318        stroke_path = QPainterPathStroker()
319        stroke_path.setCapStyle(Qt.RoundCap)
[11331]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
[11102]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
[11138]340    def anchorPath(self):
[11369]341        """
342        Return the anchor path (:class:`QPainterPath`). This is a curve on
343        which the anchor points lie.
[11138]344
345        """
346        return self.__anchorPath
347
[11102]348    def setAnchored(self, anchored):
[11369]349        """
350        Set the items anchored state. When ``False`` the item draws it self
[11138]351        with a dotted stroke.
352
[11102]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):
[11369]363        """
364        Set the connection hint. This can be used to indicate if
[11102]365        a connection can be made or not.
366
367        """
368        raise NotImplementedError
369
[11138]370    def count(self):
[11369]371        """
372        Return the number of anchor points.
[11138]373        """
374        return len(self.__points)
375
376    def addAnchor(self, anchor, position=0.5):
[11369]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
[11138]383        """
384        return self.insertAnchor(self.count(), anchor, position)
385
386    def insertAnchor(self, index, anchor, position=0.5):
[11369]387        """
388        Insert a new :class:`AnchorPoint` at `index`.
389
390        See also
391        --------
392        NodeAnchorItem.addAnchor
393
[11138]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):
[11369]412        """
413        Remove and delete the anchor point.
[11138]414        """
415        anchor = self.takeAnchor(anchor)
416
417        anchor.hide()
418        anchor.setParentItem(None)
419        anchor.deleteLater()
420
421    def takeAnchor(self, anchor):
[11369]422        """
423        Remove the anchor but don't delete it.
[11138]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):
[11369]448        """
449        Return a list of anchor points.
[11138]450        """
451        return list(self.__points)
452
453    def anchorPoint(self, index):
[11369]454        """
455        Return the anchor point at `index`.
[11138]456        """
457        return self.__points[index]
458
459    def setAnchorPositions(self, positions):
[11369]460        """
461        Set the anchor positions in percentages (0..1) along the path curve.
[11138]462        """
463        if self.__pointPositions != positions:
[11207]464            self.__pointPositions = list(positions)
[11138]465
466            self.__updatePositions()
467
468    def anchorPositions(self):
[11369]469        """
470        Return the positions of anchor points as a list of floats where
[11138]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        """
[11207]475        return list(self.__pointPositions)
[11138]476
[11102]477    def shape(self):
[11331]478        if self.__shape is not None:
479            return self.__shape
[11102]480        else:
[11293]481            return GraphicsPathObject.shape(self)
[11102]482
[11331]483    def boundingRect(self):
484        if self.__shape is not None:
485            return self.__shape.controlPointRect()
486        else:
487            return GraphicsPathObject.boundingRect(self)
488
[11102]489    def hoverEnterEvent(self, event):
490        self.shadow.setEnabled(True)
[11293]491        return GraphicsPathObject.hoverEnterEvent(self, event)
[11102]492
493    def hoverLeaveEvent(self, event):
494        self.shadow.setEnabled(False)
[11293]495        return GraphicsPathObject.hoverLeaveEvent(self, event)
[11102]496
[11138]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
[11102]504
505class SourceAnchorItem(NodeAnchorItem):
[11369]506    """
507    A source anchor item
[11102]508    """
509    pass
510
511
512class SinkAnchorItem(NodeAnchorItem):
[11369]513    """
514    A sink anchor item.
[11102]515    """
516    pass
517
518
[11186]519def standard_icon(standard_pixmap):
[11369]520    """
521    Return return the application style's standard icon for a
[11186]522    `QStyle.StandardPixmap`.
[11134]523
[11186]524    """
525    style = QApplication.instance().style()
526    return style.standardIcon(standard_pixmap)
[11134]527
[11186]528
529class GraphicsIconItem(QGraphicsItem):
[11369]530    """
531    A graphics item displaying an :class:`QIcon`.
[11186]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):
[11369]551        """
552        Set the icon (:class:`QIcon`).
[11186]553        """
554        if self.__icon != icon:
555            self.__icon = QIcon(icon)
556            self.update()
557
558    def icon(self):
[11369]559        """
560        Return the icon (:class:`QIcon`).
[11186]561        """
562        return QIcon(self.__icon)
563
564    def setIconSize(self, size):
[11369]565        """
566        Set the icon (and this item's) size (:class:`QSize`).
[11186]567        """
568        if self.__iconSize != size:
569            self.prepareGeometryChange()
570            self.__iconSize = QSize(size)
571            self.update()
572
573    def iconSize(self):
[11369]574        """
575        Return the icon size (:class:`QSize`).
[11186]576        """
577        return QSize(self.__iconSize)
578
579    def setTransformationMode(self, mode):
[11369]580        """
581        Set pixmap transformation mode. (`Qt.SmoothTransformation` or
[11186]582        `Qt.FastTransformation`).
583
584        """
585        if self.__transformationMode != mode:
586            self.__transformationMode = mode
587            self.update()
588
589    def transformationMode(self):
[11369]590        """
591        Return the pixmap transformation mode.
[11186]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))
[11134]637
638
[11102]639class NodeItem(QGraphicsObject):
[11369]640    """
641    An widget node item in the canvas.
[11102]642    """
643
[11369]644    # Scene position of the node has changed.
[11102]645    positionChanged = Signal()
646
[11369]647    # Geometry of the channel anchors changed
[11102]648    anchorGeometryChanged = Signal()
649
[11369]650    # The item has been activated (by a mouse double click or a keyboard).
[11102]651    activated = Signal()
652
[11369]653    # The item is under the mouse.
[11102]654    hovered = Signal()
655
[11369]656    #: Span of the anchor in degrees
[11102]657    ANCHOR_SPAN_ANGLE = 90
658
[11369]659    #: Z value of the item
[11102]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
[11134]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
[11102]685        self.__title = ""
686        self.__processingState = 0
687        self.__progress = -1
688
[11134]689        self.__error = None
690        self.__warning = None
691        self.__info = None
692
[11138]693        self.__anchorLayout = None
694
[11102]695        self.setZValue(self.Z_VALUE)
696        self.setupGraphics()
697
698        self.setWidgetDescription(widget_description)
699
700    @classmethod
701    def from_node(cls, node):
[11369]702        """
703        Create an :class:`NodeItem` instance and initialize it from a
704        :class:`SchemeNode` instance.
[11102]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):
[11369]714        """
715        Create an `NodeItem` instance from a node meta description.
[11102]716        """
717        self = cls()
718        self.setWidgetDescription(meta_description)
719        return self
720
721    def setupGraphics(self):
[11369]722        """
723        Set up the graphics.
[11102]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
[11186]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
[11134]759
[11186]760        self.errorItem = iconItem(QStyle.SP_MessageBoxCritical)
761        self.warningItem = iconItem(QStyle.SP_MessageBoxWarning)
762        self.infoItem = iconItem(QStyle.SP_MessageBoxInformation)
[11134]763
[11369]764    # TODO: Remove the set[Widget|Category]Description. The user should
765    # handle setting of icons, title, ...
[11102]766    def setWidgetDescription(self, desc):
[11369]767        """
768        Set widget description.
[11102]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):
[11369]790        """
791        Set the widget category.
792        """
[11102]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):
[11369]801        """
802        Set the node item's icon.
[11102]803        """
804        if isinstance(icon, QIcon):
[11186]805            self.icon_item = GraphicsIconItem(self.shapeItem, icon=icon,
806                                              iconSize=QSize(36, 36))
807            self.icon_item.setPos(-18, -18)
[11102]808        else:
809            raise TypeError
810
811    def setColor(self, color, selectedColor=None):
[11369]812        """
813        Set the widget color.
[11102]814        """
815        if selectedColor is None:
816            selectedColor = saturated(color, 150)
817        palette = create_palette(color, selectedColor)
818        self.shapeItem.setPalette(palette)
819
[11369]820    def setPalette(self, palette):
821        # TODO: The palette should override the `setColor`
822        raise NotImplementedError
[11102]823
824    def setTitle(self, title):
[11369]825        """
826        Set the node title. The title text is displayed at the bottom of the
827        node.
828
[11102]829        """
830        self.__title = title
831        self.__updateTitleText()
832
833    def title(self):
[11369]834        """
835        Return the node title.
836        """
[11102]837        return self.__title
838
[11369]839    title_ = Property(unicode, fget=title, fset=setTitle,
840                      doc="Node title text.")
[11102]841
[11343]842    def setFont(self, font):
843        """
[11369]844        Set the title text font (:class:`QFont`).
[11343]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
[11102]857    def setProcessingState(self, state):
[11369]858        """
859        Set the node processing state i.e. the node is processing
[11102]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):
[11369]871        """
872        The node processing state.
873        """
[11102]874        return self.__processingState
875
876    processingState_ = Property(int, fget=processingState,
877                                fset=setProcessingState)
878
879    def setProgress(self, progress):
[11369]880        """
881        Set the node work progress state (number between 0 and 100).
[11102]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):
[11369]893        """
894        Return the node work progress state.
895        """
[11102]896        return self.__progress
897
[11369]898    progress_ = Property(float, fget=progress, fset=setProgress,
899                         doc="Node progress state.")
[11102]900
901    def setProgressMessage(self, message):
[11369]902        """
903        Set the node work progress message.
904
905        .. note:: Not yet implemented
906
[11102]907        """
908        pass
909
910    def setErrorMessage(self, message):
[11134]911        if self.__error != message:
912            self.__error = message
913            self.__updateMessages()
[11102]914
915    def setWarningMessage(self, message):
[11134]916        if self.__warning != message:
917            self.__warning = message
918            self.__updateMessages()
[11102]919
[11134]920    def setInfoMessage(self, message):
921        if self.__info != message:
922            self.__info = message
923            self.__updateMessages()
[11102]924
925    def newInputAnchor(self):
[11369]926        """
927        Create and return a new input anchor point.
[11102]928        """
929        if not (self.widget_description and self.widget_description.inputs):
930            raise ValueError("Widget has no inputs.")
931
[11138]932        anchor = AnchorPoint()
[11207]933        self.inputAnchorItem.addAnchor(anchor, position=1.0)
[11102]934
[11207]935        positions = self.inputAnchorItem.anchorPositions()
936        positions = uniform_linear_layout(positions)
937        self.inputAnchorItem.setAnchorPositions(positions)
[11102]938
939        return anchor
940
941    def removeInputAnchor(self, anchor):
[11369]942        """
943        Remove input anchor.
[11102]944        """
[11138]945        self.inputAnchorItem.removeAnchor(anchor)
[11102]946
[11207]947        positions = self.inputAnchorItem.anchorPositions()
948        positions = uniform_linear_layout(positions)
949        self.inputAnchorItem.setAnchorPositions(positions)
[11102]950
951    def newOutputAnchor(self):
[11369]952        """
953        Create a new output anchor indicator.
[11102]954        """
955        if not (self.widget_description and self.widget_description.outputs):
956            raise ValueError("Widget has no outputs.")
957
958        anchor = AnchorPoint(self)
[11207]959        self.outputAnchorItem.addAnchor(anchor, position=1.0)
[11102]960
[11207]961        positions = self.outputAnchorItem.anchorPositions()
962        positions = uniform_linear_layout(positions)
963        self.outputAnchorItem.setAnchorPositions(positions)
[11102]964
965        return anchor
966
967    def removeOutputAnchor(self, anchor):
[11369]968        """
969        Remove output anchor.
[11102]970        """
[11138]971        self.outputAnchorItem.removeAnchor(anchor)
[11102]972
[11207]973        positions = self.outputAnchorItem.anchorPositions()
974        positions = uniform_linear_layout(positions)
975        self.outputAnchorItem.setAnchorPositions(positions)
[11102]976
[11138]977    def inputAnchors(self):
[11369]978        """
979        Return a list of input anchor points.
[11138]980        """
981        return self.inputAnchorItem.anchorPoints()
[11102]982
[11138]983    def outputAnchors(self):
[11369]984        """
985        Return a list of output anchor points.
[11138]986        """
987        return self.outputAnchorItem.anchorPoints()
[11102]988
[11138]989    def setAnchorRotation(self, angle):
[11369]990        """
991        Set the anchor rotation.
[11138]992        """
993        self.inputAnchorItem.setRotation(angle)
994        self.outputAnchorItem.setRotation(angle)
995        self.anchorGeometryChanged.emit()
[11102]996
[11138]997    def anchorRotation(self):
[11369]998        """
999        Return the anchor rotation.
[11102]1000        """
[11138]1001        return self.inputAnchorItem.rotation()
[11102]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):
[11369]1010        # Shape for mouse hit detection.
1011        # TODO: Should this return the union of all child items?
[11102]1012        return self.shapeItem.shape()
1013
1014    def __updateTitleText(self):
[11369]1015        """
1016        Update the title text item.
[11102]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
[11134]1033    def __updateMessages(self):
[11369]1034        """
1035        Update message items (position, visibility and tool tips).
[11134]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
[11102]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=[]):
[11369]1107    """
1108    A helper function for constructing a standard tooltop for the node
[11102]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.