source: orange/Orange/OrangeCanvas/canvas/items/nodeitem.py @ 11153:a53287e0dc74

Revision 11153:a53287e0dc74, 28.2 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Allow links to be dropped on anchor handles.

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