source: orange/Orange/OrangeCanvas/canvas/items/nodeitem.py @ 11331:95bea731d86c

Revision 11331:95bea731d86c, 31.5 KB checked in by Ales Erjavec <ales.erjavec@…>, 14 months ago (diff)

Extend the anchor shape/boundingRect for a wider mouse hit area.

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