source: orange/Orange/OrangeCanvas/canvas/items/nodeitem.py @ 11343:84f579a58442

Revision 11343:84f579a58442, 31.8 KB checked in by Ales Erjavec <ales.erjavec@…>, 14 months ago (diff)

Fixed font size in the canvas scene.

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