source: orange/Orange/OrangeCanvas/canvas/items/nodeitem.py @ 11207:8145a5984767

Revision 11207:8145a5984767, 31.2 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Fixed anchor point layout order reset while dragging (creating) a new link.

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