source: orange/Orange/OrangeCanvas/canvas/items/nodeitem.py @ 11186:33cead167272

Revision 11186:33cead167272, 31.3 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Replaced uses of 'QGraphicsPixmapItem' with a custom 'GraphicsIconItem'.

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