source: orange/Orange/OrangeCanvas/canvas/items/nodeitem.py @ 11180:c0e3d8cdbd08

Revision 11180:c0e3d8cdbd08, 28.1 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Added link AnchorLayout class.

Line 
1"""
2NodeItem
3
4"""
5
6from xml.sax.saxutils import escape
7
8import numpy
9
10from PyQt4.QtGui import (
11    QGraphicsItem, QGraphicsPathItem, QGraphicsPixmapItem, QGraphicsObject,
12    QGraphicsTextItem, QGraphicsDropShadowEffect,
13    QPen, QBrush, QColor, QPalette, QFont, QIcon, QStyle,
14    QPainter, QPainterPath, QPainterPathStroker, QApplication
15)
16
17from PyQt4.QtCore import Qt, QPointF, QRectF, 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
477class MessageIcon(QGraphicsPixmapItem):
478    def __init__(self, *args, **kwargs):
479        QGraphicsPixmapItem.__init__(self, *args, **kwargs)
480
481
482def message_pixmap(standard_pixmap):
483    style = QApplication.instance().style()
484    icon = style.standardIcon(standard_pixmap)
485    return icon.pixmap(16, 16)
486
487
488def linspace(count):
489    return map(float, numpy.linspace(0.0, 1.0, count + 2, endpoint=True)[1:-1])
490
491
492class NodeItem(QGraphicsObject):
493    """An widget node item in the canvas.
494    """
495
496    positionChanged = Signal()
497    """Position of the node on the canvas changed"""
498
499    anchorGeometryChanged = Signal()
500    """Geometry of the channel anchors changed"""
501
502    activated = Signal()
503    """The item has been activated (by a mouse double click or a keyboard)"""
504
505    hovered = Signal()
506    """The item is under the mouse."""
507
508    ANCHOR_SPAN_ANGLE = 90
509    """Span of the anchor in degrees"""
510
511    Z_VALUE = 100
512    """Z value of the item"""
513
514    def __init__(self, widget_description=None, parent=None, **kwargs):
515        QGraphicsObject.__init__(self, parent, **kwargs)
516        self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
517        self.setFlag(QGraphicsItem.ItemHasNoContents, True)
518        self.setFlag(QGraphicsItem.ItemIsSelectable, True)
519        self.setFlag(QGraphicsItem.ItemIsMovable, True)
520        self.setFlag(QGraphicsItem.ItemIsFocusable, True)
521
522        # central body shape item
523        self.shapeItem = None
524
525        # in/output anchor items
526        self.inputAnchorItem = None
527        self.outputAnchorItem = None
528
529        # title text item
530        self.captionTextItem = None
531
532        # error, warning, info items
533        self.errorItem = None
534        self.warningItem = None
535        self.infoItem = None
536
537        self.__title = ""
538        self.__processingState = 0
539        self.__progress = -1
540
541        self.__error = None
542        self.__warning = None
543        self.__info = None
544
545        self.__anchorLayout = None
546
547        self.setZValue(self.Z_VALUE)
548        self.setupGraphics()
549
550        self.setWidgetDescription(widget_description)
551
552    @classmethod
553    def from_node(cls, node):
554        """Create an `NodeItem` instance and initialize it from an
555        `SchemeNode` instance.
556
557        """
558        self = cls()
559        self.setWidgetDescription(node.description)
560#        self.setCategoryDescription(node.category)
561        return self
562
563    @classmethod
564    def from_node_meta(cls, meta_description):
565        """Create an `NodeItem` instance from a node meta description.
566        """
567        self = cls()
568        self.setWidgetDescription(meta_description)
569        return self
570
571    def setupGraphics(self):
572        """Set up the graphics.
573        """
574        shape_rect = QRectF(-24, -24, 48, 48)
575
576        self.shapeItem = NodeBodyItem(self)
577        self.shapeItem.setShapeRect(shape_rect)
578
579        # Rect for widget's 'ears'.
580        anchor_rect = QRectF(-31, -31, 62, 62)
581        self.inputAnchorItem = SinkAnchorItem(self)
582        input_path = QPainterPath()
583        start_angle = 180 - self.ANCHOR_SPAN_ANGLE / 2
584        input_path.arcMoveTo(anchor_rect, start_angle)
585        input_path.arcTo(anchor_rect, start_angle, self.ANCHOR_SPAN_ANGLE)
586        self.inputAnchorItem.setAnchorPath(input_path)
587
588        self.outputAnchorItem = SourceAnchorItem(self)
589        output_path = QPainterPath()
590        start_angle = self.ANCHOR_SPAN_ANGLE / 2
591        output_path.arcMoveTo(anchor_rect, start_angle)
592        output_path.arcTo(anchor_rect, start_angle, - self.ANCHOR_SPAN_ANGLE)
593        self.outputAnchorItem.setAnchorPath(output_path)
594
595        self.inputAnchorItem.hide()
596        self.outputAnchorItem.hide()
597
598        # Title caption item
599        self.captionTextItem = QGraphicsTextItem(self)
600        self.captionTextItem.setPlainText("")
601        self.captionTextItem.setPos(0, 33)
602        font = QFont("Helvetica", 12)
603        self.captionTextItem.setFont(font)
604
605        self.errorItem = MessageIcon(self)
606        self.errorItem.setPixmap(message_pixmap(QStyle.SP_MessageBoxCritical))
607        self.errorItem.hide()
608
609        self.warningItem = MessageIcon(self)
610        self.warningItem.setPixmap(message_pixmap(QStyle.SP_MessageBoxWarning))
611        self.warningItem.hide()
612
613        self.infoItem = MessageIcon(self)
614        self.infoItem.setPixmap(message_pixmap(QStyle.SP_MessageBoxInformation))
615        self.infoItem.hide()
616
617    def setWidgetDescription(self, desc):
618        """Set widget description.
619        """
620        self.widget_description = desc
621        if desc is None:
622            return
623
624        icon = icon_loader.from_description(desc).get(desc.icon)
625        if icon:
626            self.setIcon(icon)
627
628        if not self.title():
629            self.setTitle(desc.name)
630
631        if desc.inputs:
632            self.inputAnchorItem.show()
633        if desc.outputs:
634            self.outputAnchorItem.show()
635
636        tooltip = NodeItem_toolTipHelper(self)
637        self.setToolTip(tooltip)
638
639    def setWidgetCategory(self, desc):
640        self.category_description = desc
641        if desc and desc.background:
642            background = NAMED_COLORS.get(desc.background, desc.background)
643            color = QColor(background)
644            if color.isValid():
645                self.setColor(color)
646
647    def setIcon(self, icon):
648        """Set the widget's icon
649        """
650        # TODO: if the icon is SVG, how can we get it?
651        if isinstance(icon, QIcon):
652            pixmap = icon.pixmap(36, 36)
653            self.pixmap_item = QGraphicsPixmapItem(pixmap, self.shapeItem)
654            self.pixmap_item.setPos(-18, -18)
655        else:
656            raise TypeError
657
658    def setColor(self, color, selectedColor=None):
659        """Set the widget color.
660        """
661        if selectedColor is None:
662            selectedColor = saturated(color, 150)
663        palette = create_palette(color, selectedColor)
664        self.shapeItem.setPalette(palette)
665
666    def setPalette(self):
667        """
668        """
669        pass
670
671    def setTitle(self, title):
672        """Set the widget title.
673        """
674        self.__title = title
675        self.__updateTitleText()
676
677    def title(self):
678        return self.__title
679
680    title_ = Property(unicode, fget=title, fset=setTitle)
681
682    def setProcessingState(self, state):
683        """Set the node processing state i.e. the node is processing
684        (is busy) or is idle.
685
686        """
687        if self.__processingState != state:
688            self.__processingState = state
689            self.shapeItem.setProcessingState(state)
690            if not state:
691                # Clear the progress meter.
692                self.setProgress(-1)
693
694    def processingState(self):
695        return self.__processingState
696
697    processingState_ = Property(int, fget=processingState,
698                                fset=setProcessingState)
699
700    def setProgress(self, progress):
701        """Set the node work progress indicator.
702        """
703        if progress is None or progress < 0:
704            progress = -1
705
706        progress = max(min(progress, 100), -1)
707        if self.__progress != progress:
708            self.__progress = progress
709            self.shapeItem.setProgress(progress)
710            self.__updateTitleText()
711
712    def progress(self):
713        return self.__progress
714
715    progress_ = Property(float, fget=progress, fset=setProgress)
716
717    def setProgressMessage(self, message):
718        """Set the node work progress message.
719        """
720        pass
721
722    def setErrorMessage(self, message):
723        if self.__error != message:
724            self.__error = message
725            self.__updateMessages()
726
727    def setWarningMessage(self, message):
728        if self.__warning != message:
729            self.__warning = message
730            self.__updateMessages()
731
732    def setInfoMessage(self, message):
733        if self.__info != message:
734            self.__info = message
735            self.__updateMessages()
736
737    def newInputAnchor(self):
738        """Create and return a new input anchor point.
739        """
740        if not (self.widget_description and self.widget_description.inputs):
741            raise ValueError("Widget has no inputs.")
742
743        anchor = AnchorPoint()
744        self.inputAnchorItem.addAnchor(anchor)
745
746        self.inputAnchorItem.setAnchorPositions(
747            linspace(self.inputAnchorItem.count())
748        )
749
750        return anchor
751
752    def removeInputAnchor(self, anchor):
753        """Remove input anchor.
754        """
755        self.inputAnchorItem.removeAnchor(anchor)
756
757        self.inputAnchorItem.setAnchorPositions(
758            linspace(self.inputAnchorItem.count())
759        )
760
761    def newOutputAnchor(self):
762        """Create a new output anchor indicator.
763        """
764        if not (self.widget_description and self.widget_description.outputs):
765            raise ValueError("Widget has no outputs.")
766
767        anchor = AnchorPoint(self)
768        self.outputAnchorItem.addAnchor(anchor)
769
770        self.outputAnchorItem.setAnchorPositions(
771            linspace(self.outputAnchorItem.count())
772        )
773
774        return anchor
775
776    def removeOutputAnchor(self, anchor):
777        """Remove output anchor.
778        """
779        self.outputAnchorItem.removeAnchor(anchor)
780
781        self.outputAnchorItem.setAnchorPositions(
782            linspace(self.outputAnchorItem.count())
783        )
784
785    def inputAnchors(self):
786        """Return a list of input anchor points.
787        """
788        return self.inputAnchorItem.anchorPoints()
789
790    def outputAnchors(self):
791        """Return a list of output anchor points.
792        """
793        return self.outputAnchorItem.anchorPoints()
794
795    def setAnchorRotation(self, angle):
796        """Set the anchor rotation.
797        """
798        self.inputAnchorItem.setRotation(angle)
799        self.outputAnchorItem.setRotation(angle)
800        self.anchorGeometryChanged.emit()
801
802    def anchorRotation(self):
803        """Return the anchor rotation.
804        """
805        return self.inputAnchorItem.rotation()
806
807    def boundingRect(self):
808        # TODO: Important because of this any time the child
809        # items change geometry the self.prepareGeometryChange()
810        # needs to be called.
811        return self.childrenBoundingRect()
812
813    def shape(self):
814        """Reimplemented: Return the shape of the 'shapeItem', This is used
815        for hit testing in QGraphicsScene.
816
817        """
818        # Should this return the union of all child items?
819        return self.shapeItem.shape()
820
821    def __updateTitleText(self):
822        """Update the title text item.
823        """
824        title_safe = escape(self.title())
825        if self.progress() > 0:
826            text = '<div align="center">%s<br/>%i%%</div>' % \
827                   (title_safe, int(self.progress()))
828        else:
829            text = '<div align="center">%s</div>' % \
830                   (title_safe)
831
832        # The NodeItems boundingRect could change.
833        self.prepareGeometryChange()
834        self.captionTextItem.setHtml(text)
835        self.captionTextItem.document().adjustSize()
836        width = self.captionTextItem.textWidth()
837        self.captionTextItem.setPos(-width / 2.0, 33)
838
839    def __updateMessages(self):
840        """Update message items (position, visibility and tool tips).
841        """
842        items = [self.errorItem, self.warningItem, self.infoItem]
843        messages = [self.__error, self.__warning, self.__info]
844        for message, item in zip(messages, items):
845            item.setVisible(bool(message))
846            item.setToolTip(message or "")
847        shown = [item for item in items if item.isVisible()]
848        count = len(shown)
849        if count:
850            spacing = 3
851            rects = [item.boundingRect() for item in shown]
852            width = sum(rect.width() for rect in rects)
853            width += spacing * max(0, count - 1)
854            height = max(rect.height() for rect in rects)
855            origin = self.shapeItem.boundingRect().top() - spacing - height
856            origin = QPointF(-width / 2, origin)
857            for item, rect in zip(shown, rects):
858                item.setPos(origin)
859                origin = origin + QPointF(rect.width() + spacing, 0)
860
861    def mousePressEvent(self, event):
862        if self.shapeItem.path().contains(event.pos()):
863            return QGraphicsObject.mousePressEvent(self, event)
864        else:
865            event.ignore()
866
867    def mouseDoubleClickEvent(self, event):
868        if self.shapeItem.path().contains(event.pos()):
869            QGraphicsObject.mouseDoubleClickEvent(self, event)
870            QTimer.singleShot(0, self.activated.emit)
871        else:
872            event.ignore()
873
874    def contextMenuEvent(self, event):
875        if self.shapeItem.path().contains(event.pos()):
876            return QGraphicsObject.contextMenuEvent(self, event)
877        else:
878            event.ignore()
879
880    def focusInEvent(self, event):
881        self.shapeItem.setHasFocus(True)
882        return QGraphicsObject.focusInEvent(self, event)
883
884    def focusOutEvent(self, event):
885        self.shapeItem.setHasFocus(False)
886        return QGraphicsObject.focusOutEvent(self, event)
887
888    def itemChange(self, change, value):
889        if change == QGraphicsItem.ItemSelectedChange:
890            self.shapeItem.setSelected(value.toBool())
891        elif change == QGraphicsItem.ItemPositionHasChanged:
892            self.positionChanged.emit()
893
894        return QGraphicsObject.itemChange(self, change, value)
895
896
897TOOLTIP_TEMPLATE = """\
898<html>
899<head>
900<style type="text/css">
901{style}
902</style>
903</head>
904<body>
905{tooltip}
906</body>
907</html>
908"""
909
910
911def NodeItem_toolTipHelper(node, links_in=[], links_out=[]):
912    """A helper function for constructing a standard tooltop for the node
913    in on the canvas.
914
915    Parameters:
916    ===========
917    node : NodeItem
918        The node item instance.
919    links_in : list of LinkItem instances
920        A list of input links for the node.
921    links_out : list of LinkItem instances
922        A list of output links for the node.
923
924    """
925    desc = node.widget_description
926    channel_fmt = "<li>{0}</li>"
927
928    title_fmt = "<b>{title}</b><hr/>"
929    title = title_fmt.format(title=escape(node.title()))
930    inputs_list_fmt = "Inputs:<ul>{inputs}</ul><hr/>"
931    outputs_list_fmt = "Outputs:<ul>{outputs}</ul>"
932    inputs = outputs = ["None"]
933    if desc.inputs:
934        inputs = [channel_fmt.format(inp.name) for inp in desc.inputs]
935
936    if desc.outputs:
937        outputs = [channel_fmt.format(out.name) for out in desc.outputs]
938
939    inputs = inputs_list_fmt.format(inputs="".join(inputs))
940    outputs = outputs_list_fmt.format(outputs="".join(outputs))
941    tooltip = title + inputs + outputs
942    style = "ul { margin-top: 1px; margin-bottom: 1px; }"
943    return TOOLTIP_TEMPLATE.format(style=style, tooltip=tooltip)
Note: See TracBrowser for help on using the repository browser.