source: orange/Orange/OrangeCanvas/canvas/items/nodeitem.py @ 11293:467a276e7fa6

Revision 11293:467a276e7fa6, 31.3 KB checked in by Ales Erjavec <ales.erjavec@…>, 15 months ago (diff)

Changed the base class of NodeAnchorItem from QGraphicsPathItem to GraphicsPathObject.

It seems that on some PyQt4 builds 'AnchorPoint.setParentItem' does not
accept a non QGraphicsObject derived instance as a parent.

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