GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Orange.canvas.canvas.items.NodeItem   F
last analyzed

Complexity

Total Complexity 95

Size/Duplication

Total Lines 528
Duplicated Lines 0 %
Metric Value
dl 0
loc 528
rs 1.5789
wmc 95

44 Methods

Rating   Name   Duplication   Size   Complexity  
B setProgress() 0 12 5
A setColor() 0 8 2
F __updateMessages() 0 24 9
A outputAnchors() 0 5 1
A removeInputAnchor() 0 9 1
A boundingRect() 0 7 2
A setProcessingState() 0 14 4
A from_node() 0 11 1
A processingState() 0 5 1
A animationEnabled() 0 5 1
A font() 0 5 1
A setStatusMessage() 0 10 2
A setStateMessage() 0 19 4
A title() 0 5 1
A setFont() 0 8 2
A setInfoMessage() 0 4 2
A from_node_meta() 0 8 1
A iconItem() 0 5 1
A setWidgetCategory() 0 10 4
A anchorRotation() 0 5 1
B setWidgetDescription() 0 22 6
A shape() 0 4 1
A setAnchorRotation() 0 7 1
A focusOutEvent() 0 3 1
A inputAnchors() 0 5 1
A newInputAnchor() 0 15 3
D __updateTitleText() 0 40 8
A mousePressEvent() 0 5 2
A mouseDoubleClickEvent() 0 6 2
A setIcon() 0 10 2
A setupGraphics() 0 47 2
A focusInEvent() 0 3 1
A newOutputAnchor() 0 15 3
B __init__() 0 40 1
A itemChange() 0 8 3
A setWarningMessage() 0 4 2
A setErrorMessage() 0 4 2
A removeOutputAnchor() 0 9 1
A progress() 0 5 1
A contextMenuEvent() 0 5 2
A setAnimationEnabled() 0 7 2
A statusMessage() 0 2 1
A setTitle() 0 8 1
A setPalette() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Orange.canvas.canvas.items.NodeItem often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
"""
2
=========
3
Node Item
4
=========
5
6
"""
7
import string
0 ignored issues
show
Deprecated Code introduced by
Uses of a deprecated module 'string'
Loading history...
8
9
from xml.sax.saxutils import escape
10
11
from PyQt4.QtGui import (
0 ignored issues
show
Configuration introduced by
The import PyQt4.QtGui could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
12
    QGraphicsItem, QGraphicsObject, QGraphicsTextItem,
13
    QGraphicsDropShadowEffect, QGraphicsView,
14
    QPen, QBrush, QColor, QPalette, QIcon, QStyle, QPainter,
15
    QPainterPath, QPainterPathStroker, QApplication
16
)
17
18
from PyQt4.QtCore import Qt, QPointF, QRectF, QSize, QTimer, QPropertyAnimation
0 ignored issues
show
Configuration introduced by
The import PyQt4.QtCore could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
19
from PyQt4.QtCore import pyqtSignal as Signal
0 ignored issues
show
Configuration introduced by
The import PyQt4.QtCore could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
20
from PyQt4.QtCore import pyqtProperty as Property
0 ignored issues
show
Configuration introduced by
The import PyQt4.QtCore could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
21
22
from .graphicspathobject import GraphicsPathObject
23
from .utils import saturated, radial_gradient
24
25
from ...scheme.node import UserMessage
26
from ...registry import NAMED_COLORS
27
from ...resources import icon_loader
28
from .utils import uniform_linear_layout
29
30
31
def create_palette(light_color, color):
32
    """
33
    Return a new :class:`QPalette` from for the :class:`NodeBodyItem`.
34
    """
35
    palette = QPalette()
36
37
    palette.setColor(QPalette.Inactive, QPalette.Light,
38
                     saturated(light_color, 50))
39
    palette.setColor(QPalette.Inactive, QPalette.Midlight,
40
                     saturated(light_color, 90))
41
    palette.setColor(QPalette.Inactive, QPalette.Button,
42
                     light_color)
43
44
    palette.setColor(QPalette.Active, QPalette.Light,
45
                     saturated(color, 50))
46
    palette.setColor(QPalette.Active, QPalette.Midlight,
47
                     saturated(color, 90))
48
    palette.setColor(QPalette.Active, QPalette.Button,
49
                     color)
50
    palette.setColor(QPalette.ButtonText, QColor("#515151"))
51
    return palette
52
53
54
def default_palette():
55
    """
56
    Create and return a default palette for a node.
57
    """
58
    return create_palette(QColor(NAMED_COLORS["light-yellow"]),
59
                          QColor(NAMED_COLORS["yellow"]))
60
61
62
def animation_restart(animation):
63
    if animation.state() == QPropertyAnimation.Running:
64
        animation.pause()
65
    animation.start()
66
67
68
SHADOW_COLOR = "#9CACB4"
69
FOCUS_OUTLINE_COLOR = "#609ED7"
70
71
72
class NodeBodyItem(GraphicsPathObject):
73
    """
74
    The central part (body) of the `NodeItem`.
75
    """
76
    def __init__(self, parent=None):
77
        GraphicsPathObject.__init__(self, parent)
78
        assert(isinstance(parent, NodeItem))
0 ignored issues
show
Unused Code Coding Style introduced by
There is an unnecessary parenthesis after assert.
Loading history...
79
80
        self.__processingState = 0
81
        self.__progress = -1
82
        self.__animationEnabled = False
83
        self.__isSelected = False
84
        self.__hasFocus = False
85
        self.__hover = False
86
        self.__shapeRect = QRectF(-10, -10, 20, 20)
87
88
        self.setAcceptHoverEvents(True)
89
90
        self.setFlag(QGraphicsItem.ItemSendsScenePositionChanges, True)
91
        self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
92
93
        self.setPen(QPen(Qt.NoPen))
94
95
        self.setPalette(default_palette())
96
97
        self.shadow = QGraphicsDropShadowEffect(
98
            blurRadius=3,
99
            color=QColor(SHADOW_COLOR),
100
            offset=QPointF(0, 0),
101
            )
102
103
        self.setGraphicsEffect(self.shadow)
104
        self.shadow.setEnabled(True)
105
106
        self.__blurAnimation = QPropertyAnimation(self.shadow, "blurRadius",
107
                                                  self)
108
        self.__blurAnimation.setDuration(100)
109
        self.__blurAnimation.finished.connect(self.__on_finished)
110
111
        self.__pingAnimation = QPropertyAnimation(self, "scale", self)
112
        self.__pingAnimation.setDuration(250)
113
        self.__pingAnimation.setKeyValues([(0.0, 1.0), (0.5, 1.1), (1.0, 1.0)])
114
115
    # TODO: The body item should allow the setting of arbitrary painter
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
116
    # paths (for instance rounded rect, ...)
117
    def setShapeRect(self, rect):
118
        """
119
        Set the item's shape `rect`. The item should be confined within
120
        this rect.
121
122
        """
123
        path = QPainterPath()
124
        path.addEllipse(rect)
125
        self.setPath(path)
126
        self.__shapeRect = rect
127
128
    def setPalette(self, palette):
129
        """
130
        Set the body color palette (:class:`QPalette`).
131
        """
132
        self.palette = palette
133
        self.__updateBrush()
134
135
    def setAnimationEnabled(self, enabled):
136
        """
137
        Set the node animation enabled.
138
        """
139
        if self.__animationEnabled != enabled:
140
            self.__animationEnabled = enabled
141
142
    def setProcessingState(self, state):
143
        """
144
        Set the processing state of the node.
145
        """
146
        if self.__processingState != state:
147
            self.__processingState = state
148
            if not state and self.__animationEnabled:
149
                self.ping()
150
151
    def setProgress(self, progress):
152
        """
153
        Set the progress indicator state of the node. `progress` should
154
        be a number between 0 and 100.
155
156
        """
157
        self.__progress = progress
158
        self.update()
159
160
    def ping(self):
161
        """
162
        Trigger a 'ping' animation.
163
        """
164
        animation_restart(self.__pingAnimation)
165
166
    def hoverEnterEvent(self, event):
167
        self.__hover = True
168
        self.__updateShadowState()
169
        return GraphicsPathObject.hoverEnterEvent(self, event)
170
171
    def hoverLeaveEvent(self, event):
172
        self.__hover = False
173
        self.__updateShadowState()
174
        return GraphicsPathObject.hoverLeaveEvent(self, event)
175
176
    def paint(self, painter, option, widget):
0 ignored issues
show
Bug Best Practice introduced by
Signature differs from overridden 'paint' method

It is generally a good practice to use signatures that are compatible with the Liskov substitution principle.

This allows to pass instances of the child class anywhere where the instances of the super-class/interface would be acceptable.

Loading history...
177
        """
178
        Paint the shape and a progress meter.
179
        """
180
        # Let the default implementation draw the shape
181
        if option.state & QStyle.State_Selected:
182
            # Prevent the default bounding rect selection indicator.
183
            option.state = option.state ^ QStyle.State_Selected
184
        GraphicsPathObject.paint(self, painter, option, widget)
185
        if self.__progress >= 0:
186
            # Draw the progress meter over the shape.
187
            # Set the clip to shape so the meter does not overflow the shape.
188
            painter.setClipPath(self.shape(), Qt.ReplaceClip)
189
            color = self.palette.color(QPalette.ButtonText)
190
            pen = QPen(color, 5)
191
            painter.save()
192
            painter.setPen(pen)
193
            painter.setRenderHints(QPainter.Antialiasing)
194
            span = max(1, int(self.__progress * 57.60))
195
            painter.drawArc(self.__shapeRect, 90 * 16, -span)
196
            painter.restore()
197
198
    def __updateShadowState(self):
199
        if self.__hasFocus:
200
            color = QColor(FOCUS_OUTLINE_COLOR)
201
            self.setPen(QPen(color, 1.5))
202
        else:
203
            self.setPen(QPen(Qt.NoPen))
204
205
        radius = 3
206
        enabled = False
207
208
        if self.__isSelected:
209
            enabled = True
210
            radius = 7
211
212
        if self.__hover:
213
            radius = 17
214
            enabled = True
215
216
        if enabled and not self.shadow.isEnabled():
217
            self.shadow.setEnabled(enabled)
218
219
        if self.__animationEnabled:
220
            if self.__blurAnimation.state() == QPropertyAnimation.Running:
221
                self.__blurAnimation.pause()
222
223
            self.__blurAnimation.setStartValue(self.shadow.blurRadius())
224
            self.__blurAnimation.setEndValue(radius)
225
            self.__blurAnimation.start()
226
        else:
227
            self.shadow.setBlurRadius(radius)
228
229
    def __updateBrush(self):
230
        palette = self.palette
231
        if self.__isSelected:
232
            cg = QPalette.Active
233
        else:
234
            cg = QPalette.Inactive
235
236
        palette.setCurrentColorGroup(cg)
237
        c1 = palette.color(QPalette.Light)
238
        c2 = palette.color(QPalette.Button)
239
        grad = radial_gradient(c2, c1)
240
        self.setBrush(QBrush(grad))
241
242
    # TODO: The selected and focus states should be set using the
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
243
    # QStyle flags (State_Selected. State_HasFocus)
244
245
    def setSelected(self, selected):
246
        """
247
        Set the `selected` state.
248
249
        .. note:: The item does not have `QGraphicsItem.ItemIsSelectable` flag.
250
                  This property is instead controlled by the parent NodeItem.
251
252
        """
253
        self.__isSelected = selected
254
        self.__updateBrush()
255
256
    def setHasFocus(self, focus):
257
        """
258
        Set the `has focus` state.
259
260
        .. note:: The item does not have `QGraphicsItem.ItemIsFocusable` flag.
261
                  This property is instead controlled by the parent NodeItem.
262
263
        """
264
        self.__hasFocus = focus
265
        self.__updateShadowState()
266
267
    def __on_finished(self):
268
        if self.shadow.blurRadius() == 0:
269
            self.shadow.setEnabled(False)
270
271
272
class AnchorPoint(QGraphicsObject):
273
    """
274
    A anchor indicator on the :class:`NodeAnchorItem`.
275
    """
276
277
    #: Signal emitted when the item's scene position changes.
278
    scenePositionChanged = Signal(QPointF)
279
280
    #: Signal emitted when the item's `anchorDirection` changes.
281
    anchorDirectionChanged = Signal(QPointF)
282
283
    def __init__(self, *args):
284
        QGraphicsObject.__init__(self, *args)
285
        self.setFlag(QGraphicsItem.ItemSendsScenePositionChanges, True)
286
        self.setFlag(QGraphicsItem.ItemHasNoContents, True)
287
288
        self.__direction = QPointF()
289
290
    def anchorScenePos(self):
291
        """
292
        Return anchor position in scene coordinates.
293
        """
294
        return self.mapToScene(QPointF(0, 0))
295
296
    def setAnchorDirection(self, direction):
297
        """
298
        Set the preferred direction (QPointF) in item coordinates.
299
        """
300
        if self.__direction != direction:
301
            self.__direction = direction
302
            self.anchorDirectionChanged.emit(direction)
303
304
    def anchorDirection(self):
305
        """
306
        Return the preferred anchor direction.
307
        """
308
        return self.__direction
309
310
    def itemChange(self, change, value):
311
        if change == QGraphicsItem.ItemScenePositionHasChanged:
312
            self.scenePositionChanged.emit(value)
313
314
        return QGraphicsObject.itemChange(self, change, value)
315
316
    def boundingRect(self,):
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
317
        return QRectF()
318
319
320
class NodeAnchorItem(GraphicsPathObject):
321
    """
322
    The left/right widget input/output anchors.
323
    """
324
325
    def __init__(self, parent, *args):
326
        self.__boundingRect = None
327
        GraphicsPathObject.__init__(self, parent, *args)
328
        self.setAcceptHoverEvents(True)
329
        self.setPen(QPen(Qt.NoPen))
330
        self.normalBrush = QBrush(QColor("#CDD5D9"))
331
        self.connectedBrush = QBrush(QColor("#9CACB4"))
332
        self.setBrush(self.normalBrush)
333
334
        self.shadow = QGraphicsDropShadowEffect(
335
            blurRadius=10,
336
            color=QColor(SHADOW_COLOR),
337
            offset=QPointF(0, 0)
338
        )
339
340
        self.setGraphicsEffect(self.shadow)
341
        self.shadow.setEnabled(False)
342
343
        # Does this item have any anchored links.
344
        self.anchored = False
345
346
        if isinstance(parent, NodeItem):
347
            self.__parentNodeItem = parent
348
        else:
349
            self.__parentNodeItem = None
350
351
        self.__anchorPath = QPainterPath()
352
        self.__points = []
353
        self.__pointPositions = []
354
355
        self.__fullStroke = None
356
        self.__dottedStroke = None
357
        self.__shape = None
358
359
        self.prepareGeometryChange()
360
        self.__boundingRect = None
361
362
    def parentNodeItem(self):
363
        """
364
        Return a parent :class:`NodeItem` or ``None`` if this anchor's
365
        parent is not a :class:`NodeItem` instance.
366
367
        """
368
        return self.__parentNodeItem
369
370
    def setAnchorPath(self, path):
371
        """
372
        Set the anchor's curve path as a :class:`QPainterPath`.
373
        """
374
        self.prepareGeometryChange()
375
        self.__boundingRect = None
376
377
        self.__anchorPath = path
378
        # Create a stroke of the path.
379
        stroke_path = QPainterPathStroker()
380
        stroke_path.setCapStyle(Qt.RoundCap)
381
382
        # Shape is wider (bigger mouse hit area - should be settable)
383
        stroke_path.setWidth(12)
384
        self.__shape = stroke_path.createStroke(path)
385
386
        # The full stroke
387
        stroke_path.setWidth(3)
388
        self.__fullStroke = stroke_path.createStroke(path)
389
390
        # The dotted stroke (when not connected to anything)
391
        stroke_path.setDashPattern(Qt.DotLine)
392
        self.__dottedStroke = stroke_path.createStroke(path)
393
394
        if self.anchored:
395
            self.setPath(self.__fullStroke)
396
            self.setBrush(self.connectedBrush)
397
        else:
398
            self.setPath(self.__dottedStroke)
399
            self.setBrush(self.normalBrush)
400
401
    def anchorPath(self):
402
        """
403
        Return the anchor path (:class:`QPainterPath`). This is a curve on
404
        which the anchor points lie.
405
406
        """
407
        return self.__anchorPath
408
409
    def setAnchored(self, anchored):
410
        """
411
        Set the items anchored state. When ``False`` the item draws it self
412
        with a dotted stroke.
413
414
        """
415
        self.anchored = anchored
416
        if anchored:
417
            self.setPath(self.__fullStroke)
418
            self.setBrush(self.connectedBrush)
419
        else:
420
            self.setPath(self.__dottedStroke)
421
            self.setBrush(self.normalBrush)
422
423
    def setConnectionHint(self, hint=None):
424
        """
425
        Set the connection hint. This can be used to indicate if
426
        a connection can be made or not.
427
428
        """
429
        raise NotImplementedError
430
431
    def count(self):
432
        """
433
        Return the number of anchor points.
434
        """
435
        return len(self.__points)
436
437
    def addAnchor(self, anchor, position=0.5):
438
        """
439
        Add a new :class:`AnchorPoint` to this item and return it's index.
440
441
        The `position` specifies where along the `anchorPath` is the new
442
        point inserted.
443
444
        """
445
        return self.insertAnchor(self.count(), anchor, position)
446
447
    def insertAnchor(self, index, anchor, position=0.5):
448
        """
449
        Insert a new :class:`AnchorPoint` at `index`.
450
451
        See also
452
        --------
453
        NodeAnchorItem.addAnchor
454
455
        """
456
        if anchor in self.__points:
457
            raise ValueError("%s already added." % anchor)
458
459
        self.__points.insert(index, anchor)
460
        self.__pointPositions.insert(index, position)
461
462
        anchor.setParentItem(self)
463
        anchor.setPos(self.__anchorPath.pointAtPercent(position))
464
        anchor.destroyed.connect(self.__onAnchorDestroyed)
465
466
        self.__updatePositions()
467
468
        self.setAnchored(bool(self.__points))
469
470
        return index
471
472
    def removeAnchor(self, anchor):
473
        """
474
        Remove and delete the anchor point.
475
        """
476
        anchor = self.takeAnchor(anchor)
477
478
        anchor.hide()
479
        anchor.setParentItem(None)
480
        anchor.deleteLater()
481
482
    def takeAnchor(self, anchor):
483
        """
484
        Remove the anchor but don't delete it.
485
        """
486
        index = self.__points.index(anchor)
487
488
        del self.__points[index]
489
        del self.__pointPositions[index]
490
491
        anchor.destroyed.disconnect(self.__onAnchorDestroyed)
492
493
        self.__updatePositions()
494
495
        self.setAnchored(bool(self.__points))
496
497
        return anchor
498
499
    def __onAnchorDestroyed(self, anchor):
500
        try:
501
            index = self.__points.index(anchor)
502
        except ValueError:
503
            return
504
505
        del self.__points[index]
506
        del self.__pointPositions[index]
507
508
    def anchorPoints(self):
509
        """
510
        Return a list of anchor points.
511
        """
512
        return list(self.__points)
513
514
    def anchorPoint(self, index):
515
        """
516
        Return the anchor point at `index`.
517
        """
518
        return self.__points[index]
519
520
    def setAnchorPositions(self, positions):
521
        """
522
        Set the anchor positions in percentages (0..1) along the path curve.
523
        """
524
        if self.__pointPositions != positions:
525
            self.__pointPositions = list(positions)
526
527
            self.__updatePositions()
528
529
    def anchorPositions(self):
530
        """
531
        Return the positions of anchor points as a list of floats where
532
        each float is between 0 and 1 and specifies where along the anchor
533
        path does the point lie (0 is at start 1 is at the end).
534
535
        """
536
        return list(self.__pointPositions)
537
538
    def shape(self):
539
        if self.__shape is not None:
540
            return self.__shape
541
        else:
542
            return GraphicsPathObject.shape(self)
543
544
    def boundingRect(self):
545
        if self.__boundingRect is None:
546
            self.__boundingRect = super().boundingRect().adjusted(-5, -5, 5, 5)
547
        return self.__boundingRect
548
549
    def hoverEnterEvent(self, event):
550
        self.prepareGeometryChange()
551
        self.__boundingRect = None
552
        self.shadow.setEnabled(True)
553
        return GraphicsPathObject.hoverEnterEvent(self, event)
554
555
    def hoverLeaveEvent(self, event):
556
        self.prepareGeometryChange()
557
        self.__boundingRect = None
558
        self.shadow.setEnabled(False)
559
        return GraphicsPathObject.hoverLeaveEvent(self, event)
560
561
    def __updatePositions(self):
562
        """Update anchor points positions.
563
        """
564
        self.prepareGeometryChange()
565
        self.__boundingRect = None
566
        for point, t in zip(self.__points, self.__pointPositions):
567
            pos = self.__anchorPath.pointAtPercent(t)
568
            point.setPos(pos)
569
570
571
class SourceAnchorItem(NodeAnchorItem):
572
    """
573
    A source anchor item
574
    """
575
    pass
576
577
578
class SinkAnchorItem(NodeAnchorItem):
579
    """
580
    A sink anchor item.
581
    """
582
    pass
583
584
585
def standard_icon(standard_pixmap):
586
    """
587
    Return return the application style's standard icon for a
588
    `QStyle.StandardPixmap`.
589
590
    """
591
    style = QApplication.instance().style()
592
    return style.standardIcon(standard_pixmap)
593
594
595
class GraphicsIconItem(QGraphicsItem):
596
    """
597
    A graphics item displaying an :class:`QIcon`.
598
    """
599
    def __init__(self, parent=None, icon=None, iconSize=None, **kwargs):
600
        QGraphicsItem.__init__(self, parent, **kwargs)
601
        self.setFlag(QGraphicsItem.ItemUsesExtendedStyleOption, True)
602
603
        if icon is None:
604
            icon = QIcon()
605
606
        if iconSize is None:
607
            style = QApplication.instance().style()
608
            size = style.pixelMetric(style.PM_LargeIconSize)
609
            iconSize = QSize(size, size)
610
611
        self.__transformationMode = Qt.SmoothTransformation
612
613
        self.__iconSize = QSize(iconSize)
614
        self.__icon = QIcon(icon)
615
616
    def setIcon(self, icon):
617
        """
618
        Set the icon (:class:`QIcon`).
619
        """
620
        if self.__icon != icon:
621
            self.__icon = QIcon(icon)
622
            self.update()
623
624
    def icon(self):
625
        """
626
        Return the icon (:class:`QIcon`).
627
        """
628
        return QIcon(self.__icon)
629
630
    def setIconSize(self, size):
631
        """
632
        Set the icon (and this item's) size (:class:`QSize`).
633
        """
634
        if self.__iconSize != size:
635
            self.prepareGeometryChange()
636
            self.__iconSize = QSize(size)
637
            self.update()
638
639
    def iconSize(self):
640
        """
641
        Return the icon size (:class:`QSize`).
642
        """
643
        return QSize(self.__iconSize)
644
645
    def setTransformationMode(self, mode):
646
        """
647
        Set pixmap transformation mode. (`Qt.SmoothTransformation` or
648
        `Qt.FastTransformation`).
649
650
        """
651
        if self.__transformationMode != mode:
652
            self.__transformationMode = mode
653
            self.update()
654
655
    def transformationMode(self):
656
        """
657
        Return the pixmap transformation mode.
658
        """
659
        return self.__transformationMode
660
661
    def boundingRect(self):
662
        return QRectF(0, 0, self.__iconSize.width(), self.__iconSize.height())
663
664
    def paint(self, painter, option, widget=None):
665
        if not self.__icon.isNull():
666
            if option.state & QStyle.State_Selected:
667
                mode = QIcon.Selected
668
            elif option.state & QStyle.State_Enabled:
669
                mode = QIcon.Normal
670
            elif option.state & QStyle.State_Active:
671
                mode = QIcon.Active
672
            else:
673
                mode = QIcon.Disabled
674
675
            transform = self.sceneTransform()
676
677
            if widget is not None:
678
                # 'widget' is the QGraphicsView.viewport()
679
                view = widget.parent()
680
                if isinstance(view, QGraphicsView):
681
                    # Combine the scene transform with the view transform.
682
                    view_transform = view.transform()
683
                    transform = view_transform * view_transform
684
685
            lod = option.levelOfDetailFromTransform(transform)
686
687
            w, h = self.__iconSize.width(), self.__iconSize.height()
688
            target = QRectF(0, 0, w, h)
689
            source = QRectF(0, 0, w * lod, w * lod).toRect()
690
691
            # The actual size of the requested pixmap can be smaller.
692
            size = self.__icon.actualSize(source.size(), mode=mode)
693
            source.setSize(size)
694
695
            pixmap = self.__icon.pixmap(source.size(), mode=mode)
696
697
            painter.setRenderHint(
698
                QPainter.SmoothPixmapTransform,
699
                self.__transformationMode == Qt.SmoothTransformation
700
            )
701
702
            painter.drawPixmap(target, pixmap, QRectF(source))
703
704
705
class NameTextItem(QGraphicsTextItem):
706
    def __init__(self, *args, **kwargs):
707
        super(NameTextItem, self).__init__(*args, **kwargs)
708
        self.__selected = False
709
        self.__palette = None
710
711
    def paint(self, painter, option, widget=None):
712
        if self.__selected:
713
            painter.save()
714
            painter.setPen(QPen(Qt.NoPen))
715
            painter.setBrush(self.palette().color(QPalette.Highlight))
716
            doc = self.document()
717
            margin = doc.documentMargin()
718
            painter.translate(margin, margin)
719
            offset = min(margin, 2)
720
            for line in self._lines(doc):
721
                rect = line.naturalTextRect()
722
                painter.drawRoundedRect(
723
                    rect.adjusted(-offset, -offset, offset, offset),
724
                    3, 3
725
                )
726
727
            painter.restore()
728
729
        super(NameTextItem, self).paint(painter, option, widget)
730
731
    def _blocks(self, doc):
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
732
        block = doc.begin()
733
        while block != doc.end():
734
            yield block
735
            block = block.next()
736
737
    def _lines(self, doc):
738
        for block in self._blocks(doc):
739
            blocklayout = block.layout()
740
            for i in range(blocklayout.lineCount()):
741
                yield blocklayout.lineAt(i)
742
743
    def setSelectionState(self, state):
744
        if self.__selected != state:
745
            self.__selected = state
746
            self.__updateDefaultTextColor()
747
            self.update()
748
749
    def setPalatte(self, palette):
750
        if self.__palette != palette:
751
            self.__palette = palette
752
            self.__updateDefaultTextColor()
753
            self.update()
754
755
    def palette(self):
756
        if self.__palette is None:
757
            scene = self.scene()
758
            if scene is not None:
759
                return scene.palette()
760
            else:
761
                return QPalette()
762
        else:
763
            return self.__palette
764
765
    def __updateDefaultTextColor(self):
766
        if self.__selected:
767
            role = QPalette.HighlightedText
768
        else:
769
            role = QPalette.WindowText
770
        self.setDefaultTextColor(self.palette().color(role))
771
772
773
class NodeItem(QGraphicsObject):
0 ignored issues
show
Unused Code introduced by
This abstract class does not seem to be used anywhere.
Loading history...
774
    """
775
    An widget node item in the canvas.
776
    """
777
778
    #: Signal emitted when the scene position of the node has changed.
779
    positionChanged = Signal()
780
781
    #: Signal emitted when the geometry of the channel anchors changes.
782
    anchorGeometryChanged = Signal()
783
784
    #: Signal emitted when the item has been activated (by a mouse double
785
    #: click or a keyboard)
786
    activated = Signal()
787
788
    #: The item is under the mouse.
789
    hovered = Signal()
790
791
    #: Span of the anchor in degrees
792
    ANCHOR_SPAN_ANGLE = 90
793
794
    #: Z value of the item
795
    Z_VALUE = 100
796
797
    def __init__(self, widget_description=None, parent=None, **kwargs):
798
        self.__boundingRect = None
799
        QGraphicsObject.__init__(self, parent, **kwargs)
800
        self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
801
        self.setFlag(QGraphicsItem.ItemHasNoContents, True)
802
        self.setFlag(QGraphicsItem.ItemIsSelectable, True)
803
        self.setFlag(QGraphicsItem.ItemIsMovable, True)
804
        self.setFlag(QGraphicsItem.ItemIsFocusable, True)
805
806
        # central body shape item
807
        self.shapeItem = None
808
809
        # in/output anchor items
810
        self.inputAnchorItem = None
811
        self.outputAnchorItem = None
812
813
        # title text item
814
        self.captionTextItem = None
815
816
        # error, warning, info items
817
        self.errorItem = None
818
        self.warningItem = None
819
        self.infoItem = None
820
821
        self.__title = ""
822
        self.__processingState = 0
823
        self.__progress = -1
824
        self.__statusMessage = ""
825
826
        self.__error = None
827
        self.__warning = None
828
        self.__info = None
829
830
        self.__anchorLayout = None
831
        self.__animationEnabled = False
832
833
        self.setZValue(self.Z_VALUE)
834
        self.setupGraphics()
835
836
        self.setWidgetDescription(widget_description)
837
838
    @classmethod
839
    def from_node(cls, node):
840
        """
841
        Create an :class:`NodeItem` instance and initialize it from a
842
        :class:`SchemeNode` instance.
843
844
        """
845
        self = cls()
846
        self.setWidgetDescription(node.description)
847
#        self.setCategoryDescription(node.category)
848
        return self
849
850
    @classmethod
851
    def from_node_meta(cls, meta_description):
852
        """
853
        Create an `NodeItem` instance from a node meta description.
854
        """
855
        self = cls()
856
        self.setWidgetDescription(meta_description)
857
        return self
858
859
    def setupGraphics(self):
860
        """
861
        Set up the graphics.
862
        """
863
        shape_rect = QRectF(-24, -24, 48, 48)
864
865
        self.shapeItem = NodeBodyItem(self)
866
        self.shapeItem.setShapeRect(shape_rect)
867
        self.shapeItem.setAnimationEnabled(self.__animationEnabled)
868
869
        # Rect for widget's 'ears'.
870
        anchor_rect = QRectF(-31, -31, 62, 62)
871
        self.inputAnchorItem = SinkAnchorItem(self)
872
        input_path = QPainterPath()
873
        start_angle = 180 - self.ANCHOR_SPAN_ANGLE / 2
874
        input_path.arcMoveTo(anchor_rect, start_angle)
875
        input_path.arcTo(anchor_rect, start_angle, self.ANCHOR_SPAN_ANGLE)
876
        self.inputAnchorItem.setAnchorPath(input_path)
877
878
        self.outputAnchorItem = SourceAnchorItem(self)
879
        output_path = QPainterPath()
880
        start_angle = self.ANCHOR_SPAN_ANGLE / 2
881
        output_path.arcMoveTo(anchor_rect, start_angle)
882
        output_path.arcTo(anchor_rect, start_angle, - self.ANCHOR_SPAN_ANGLE)
883
        self.outputAnchorItem.setAnchorPath(output_path)
884
885
        self.inputAnchorItem.hide()
886
        self.outputAnchorItem.hide()
887
888
        # Title caption item
889
        self.captionTextItem = NameTextItem(self)
890
891
        self.captionTextItem.setPlainText("")
892
        self.captionTextItem.setPos(0, 33)
893
894
        def iconItem(standard_pixmap):
895
            item = GraphicsIconItem(self, icon=standard_icon(standard_pixmap),
896
                                    iconSize=QSize(16, 16))
897
            item.hide()
898
            return item
899
900
        self.errorItem = iconItem(QStyle.SP_MessageBoxCritical)
901
        self.warningItem = iconItem(QStyle.SP_MessageBoxWarning)
902
        self.infoItem = iconItem(QStyle.SP_MessageBoxInformation)
903
904
        self.prepareGeometryChange()
905
        self.__boundingRect = None
906
907
    # TODO: Remove the set[Widget|Category]Description. The user should
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
908
    # handle setting of icons, title, ...
909
    def setWidgetDescription(self, desc):
910
        """
911
        Set widget description.
912
        """
913
        self.widget_description = desc
914
        if desc is None:
915
            return
916
917
        icon = icon_loader.from_description(desc).get(desc.icon)
918
        if icon:
919
            self.setIcon(icon)
920
921
        if not self.title():
922
            self.setTitle(desc.name)
923
924
        if desc.inputs:
925
            self.inputAnchorItem.show()
926
        if desc.outputs:
927
            self.outputAnchorItem.show()
928
929
        tooltip = NodeItem_toolTipHelper(self)
930
        self.setToolTip(tooltip)
931
932
    def setWidgetCategory(self, desc):
933
        """
934
        Set the widget category.
935
        """
936
        self.category_description = desc
0 ignored issues
show
Coding Style introduced by
The attribute category_description was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
937
        if desc and desc.background:
938
            background = NAMED_COLORS.get(desc.background, desc.background)
939
            color = QColor(background)
940
            if color.isValid():
941
                self.setColor(color)
942
943
    def setIcon(self, icon):
944
        """
945
        Set the node item's icon (:class:`QIcon`).
946
        """
947
        if isinstance(icon, QIcon):
948
            self.icon_item = GraphicsIconItem(self.shapeItem, icon=icon,
0 ignored issues
show
Coding Style introduced by
The attribute icon_item was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
949
                                              iconSize=QSize(36, 36))
950
            self.icon_item.setPos(-18, -18)
951
        else:
952
            raise TypeError
953
954
    def setColor(self, color, selectedColor=None):
955
        """
956
        Set the widget color.
957
        """
958
        if selectedColor is None:
959
            selectedColor = saturated(color, 150)
960
        palette = create_palette(color, selectedColor)
961
        self.shapeItem.setPalette(palette)
962
963
    def setPalette(self, palette):
964
        # TODO: The palette should override the `setColor`
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
965
        raise NotImplementedError
966
967
    def setTitle(self, title):
968
        """
969
        Set the node title. The title text is displayed at the bottom of the
970
        node.
971
972
        """
973
        self.__title = title
974
        self.__updateTitleText()
975
976
    def title(self):
977
        """
978
        Return the node title.
979
        """
980
        return self.__title
981
982
    title_ = Property(str, fget=title, fset=setTitle,
983
                      doc="Node title text.")
984
985
    def setFont(self, font):
986
        """
987
        Set the title text font (:class:`QFont`).
988
        """
989
        if font != self.font():
990
            self.prepareGeometryChange()
991
            self.captionTextItem.setFont(font)
992
            self.__updateTitleText()
993
994
    def font(self):
995
        """
996
        Return the title text font.
997
        """
998
        return self.captionTextItem.font()
999
1000
    def setAnimationEnabled(self, enabled):
1001
        """
1002
        Set the node animation enabled state.
1003
        """
1004
        if self.__animationEnabled != enabled:
1005
            self.__animationEnabled = enabled
1006
            self.shapeItem.setAnimationEnabled(enabled)
1007
1008
    def animationEnabled(self):
1009
        """
1010
        Are node animations enabled.
1011
        """
1012
        return self.__animationEnabled
1013
1014
    def setProcessingState(self, state):
1015
        """
1016
        Set the node processing state i.e. the node is processing
1017
        (is busy) or is idle.
1018
1019
        """
1020
        if self.__processingState != state:
1021
            self.__processingState = state
1022
            self.shapeItem.setProcessingState(state)
1023
            if not state:
1024
                # Clear the progress meter.
1025
                self.setProgress(-1)
1026
                if self.__animationEnabled:
1027
                    self.shapeItem.ping()
1028
1029
    def processingState(self):
1030
        """
1031
        The node processing state.
1032
        """
1033
        return self.__processingState
1034
1035
    processingState_ = Property(int, fget=processingState,
1036
                                fset=setProcessingState)
1037
1038
    def setProgress(self, progress):
1039
        """
1040
        Set the node work progress state (number between 0 and 100).
1041
        """
1042
        if progress is None or progress < 0 or not self.__processingState:
1043
            progress = -1
1044
1045
        progress = max(min(progress, 100), -1)
1046
        if self.__progress != progress:
1047
            self.__progress = progress
1048
            self.shapeItem.setProgress(progress)
1049
            self.__updateTitleText()
1050
1051
    def progress(self):
1052
        """
1053
        Return the node work progress state.
1054
        """
1055
        return self.__progress
1056
1057
    progress_ = Property(float, fget=progress, fset=setProgress,
1058
                         doc="Node progress state.")
1059
1060
    def setStatusMessage(self, message):
1061
        """
1062
        Set the node status message text.
1063
1064
        This text is displayed below the node's title.
1065
1066
        """
1067
        if self.__statusMessage != message:
1068
            self.__statusMessage = message
1069
            self.__updateTitleText()
1070
1071
    def statusMessage(self):
1072
        return self.__statusMessage
1073
1074
    def setStateMessage(self, message):
1075
        """
1076
        Set a state message to display over the item.
1077
1078
        Parameters
1079
        ----------
1080
        message : UserMessage
1081
            Message to display. `message.severity` is used to determine
1082
            the icon and `message.contents` is used as a tool tip.
1083
1084
        """
1085
        # TODO: Group messages by message_id not by severity
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
1086
        # and deprecate set[Error|Warning|Error]Message
1087
        if message.severity == UserMessage.Info:
1088
            self.setInfoMessage(message.contents)
1089
        elif message.severity == UserMessage.Warning:
1090
            self.setWarningMessage(message.contents)
1091
        elif message.severity == UserMessage.Error:
1092
            self.setErrorMessage(message.contents)
1093
1094
    def setErrorMessage(self, message):
1095
        if self.__error != message:
1096
            self.__error = message
1097
            self.__updateMessages()
1098
1099
    def setWarningMessage(self, message):
1100
        if self.__warning != message:
1101
            self.__warning = message
1102
            self.__updateMessages()
1103
1104
    def setInfoMessage(self, message):
1105
        if self.__info != message:
1106
            self.__info = message
1107
            self.__updateMessages()
1108
1109
    def newInputAnchor(self):
1110
        """
1111
        Create and return a new input :class:`AnchorPoint`.
1112
        """
1113
        if not (self.widget_description and self.widget_description.inputs):
1114
            raise ValueError("Widget has no inputs.")
1115
1116
        anchor = AnchorPoint()
1117
        self.inputAnchorItem.addAnchor(anchor, position=1.0)
1118
1119
        positions = self.inputAnchorItem.anchorPositions()
1120
        positions = uniform_linear_layout(positions)
1121
        self.inputAnchorItem.setAnchorPositions(positions)
1122
1123
        return anchor
1124
1125
    def removeInputAnchor(self, anchor):
1126
        """
1127
        Remove input anchor.
1128
        """
1129
        self.inputAnchorItem.removeAnchor(anchor)
1130
1131
        positions = self.inputAnchorItem.anchorPositions()
1132
        positions = uniform_linear_layout(positions)
1133
        self.inputAnchorItem.setAnchorPositions(positions)
1134
1135
    def newOutputAnchor(self):
1136
        """
1137
        Create and return a new output :class:`AnchorPoint`.
1138
        """
1139
        if not (self.widget_description and self.widget_description.outputs):
1140
            raise ValueError("Widget has no outputs.")
1141
1142
        anchor = AnchorPoint(self)
1143
        self.outputAnchorItem.addAnchor(anchor, position=1.0)
1144
1145
        positions = self.outputAnchorItem.anchorPositions()
1146
        positions = uniform_linear_layout(positions)
1147
        self.outputAnchorItem.setAnchorPositions(positions)
1148
1149
        return anchor
1150
1151
    def removeOutputAnchor(self, anchor):
1152
        """
1153
        Remove output anchor.
1154
        """
1155
        self.outputAnchorItem.removeAnchor(anchor)
1156
1157
        positions = self.outputAnchorItem.anchorPositions()
1158
        positions = uniform_linear_layout(positions)
1159
        self.outputAnchorItem.setAnchorPositions(positions)
1160
1161
    def inputAnchors(self):
1162
        """
1163
        Return a list of all input anchor points.
1164
        """
1165
        return self.inputAnchorItem.anchorPoints()
1166
1167
    def outputAnchors(self):
1168
        """
1169
        Return a list of all output anchor points.
1170
        """
1171
        return self.outputAnchorItem.anchorPoints()
1172
1173
    def setAnchorRotation(self, angle):
1174
        """
1175
        Set the anchor rotation.
1176
        """
1177
        self.inputAnchorItem.setRotation(angle)
1178
        self.outputAnchorItem.setRotation(angle)
1179
        self.anchorGeometryChanged.emit()
1180
1181
    def anchorRotation(self):
1182
        """
1183
        Return the anchor rotation.
1184
        """
1185
        return self.inputAnchorItem.rotation()
1186
1187
    def boundingRect(self):
1188
        # TODO: Important because of this any time the child
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
1189
        # items change geometry the self.prepareGeometryChange()
1190
        # needs to be called.
1191
        if self.__boundingRect is None:
1192
            self.__boundingRect = self.childrenBoundingRect()
1193
        return self.__boundingRect
1194
1195
    def shape(self):
1196
        # Shape for mouse hit detection.
1197
        # TODO: Should this return the union of all child items?
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
1198
        return self.shapeItem.shape()
1199
1200
    def __updateTitleText(self):
1201
        """
1202
        Update the title text item.
1203
        """
1204
        text = ['<div align="center">%s' % escape(self.title())]
1205
1206
        status_text = []
1207
1208
        progress_included = False
1209
        if self.__statusMessage:
1210
            msg = escape(self.__statusMessage)
1211
            format_fields = dict(parse_format_fields(msg))
1212
            if "progress" in format_fields and len(format_fields) == 1:
1213
                # Insert progress into the status text format string.
1214
                spec, _ = format_fields["progress"]
1215
                if spec != None:
1216
                    progress_included = True
1217
                    progress_str = "{0:.0f}%".format(self.progress())
1218
                    status_text.append(msg.format(progress=progress_str))
1219
            else:
1220
                status_text.append(msg)
1221
1222
        if self.progress() >= 0 and not progress_included:
1223
            status_text.append("%i%%" % int(self.progress()))
1224
1225
        if status_text:
1226
            text += ["<br/>",
1227
                     '<span style="font-style: italic">',
1228
                     "<br/>".join(status_text),
1229
                     "</span>"]
1230
        text += ["</div>"]
1231
        text = "".join(text)
1232
1233
        # The NodeItems boundingRect could change.
1234
        self.prepareGeometryChange()
1235
        self.__boundingRect = None
1236
        self.captionTextItem.setHtml(text)
1237
        self.captionTextItem.document().adjustSize()
1238
        width = self.captionTextItem.textWidth()
1239
        self.captionTextItem.setPos(-width / 2.0, 33)
1240
1241
    def __updateMessages(self):
1242
        """
1243
        Update message items (position, visibility and tool tips).
1244
        """
1245
        items = [self.errorItem, self.warningItem, self.infoItem]
1246
1247
        messages = [self.__error, self.__warning, self.__info]
1248
        for message, item in zip(messages, items):
1249
            item.setVisible(bool(message))
1250
            item.setToolTip(message or "")
1251
1252
        shown = [item for item in items if item.isVisible()]
1253
        count = len(shown)
1254
        if count:
1255
            spacing = 3
1256
            rects = [item.boundingRect() for item in shown]
1257
            width = sum(rect.width() for rect in rects)
1258
            width += spacing * max(0, count - 1)
1259
            height = max(rect.height() for rect in rects)
1260
            origin = self.shapeItem.boundingRect().top() - spacing - height
1261
            origin = QPointF(-width / 2, origin)
1262
            for item, rect in zip(shown, rects):
1263
                item.setPos(origin)
1264
                origin = origin + QPointF(rect.width() + spacing, 0)
1265
1266
    def mousePressEvent(self, event):
1267
        if self.shapeItem.path().contains(event.pos()):
1268
            return QGraphicsObject.mousePressEvent(self, event)
1269
        else:
1270
            event.ignore()
1271
1272
    def mouseDoubleClickEvent(self, event):
1273
        if self.shapeItem.path().contains(event.pos()):
1274
            QGraphicsObject.mouseDoubleClickEvent(self, event)
1275
            QTimer.singleShot(0, self.activated.emit)
1276
        else:
1277
            event.ignore()
1278
1279
    def contextMenuEvent(self, event):
1280
        if self.shapeItem.path().contains(event.pos()):
1281
            return QGraphicsObject.contextMenuEvent(self, event)
1282
        else:
1283
            event.ignore()
1284
1285
    def focusInEvent(self, event):
1286
        self.shapeItem.setHasFocus(True)
1287
        return QGraphicsObject.focusInEvent(self, event)
1288
1289
    def focusOutEvent(self, event):
1290
        self.shapeItem.setHasFocus(False)
1291
        return QGraphicsObject.focusOutEvent(self, event)
1292
1293
    def itemChange(self, change, value):
1294
        if change == QGraphicsItem.ItemSelectedChange:
1295
            self.shapeItem.setSelected(value)
1296
            self.captionTextItem.setSelectionState(value)
1297
        elif change == QGraphicsItem.ItemPositionHasChanged:
1298
            self.positionChanged.emit()
1299
1300
        return QGraphicsObject.itemChange(self, change, value)
1301
1302
1303
TOOLTIP_TEMPLATE = """\
1304
<html>
1305
<head>
1306
<style type="text/css">
1307
{style}
1308
</style>
1309
</head>
1310
<body>
1311
{tooltip}
1312
</body>
1313
</html>
1314
"""
1315
1316
1317
def NodeItem_toolTipHelper(node, links_in=[], links_out=[]):
0 ignored issues
show
Bug Best Practice introduced by
The default value [] might cause unintended side-effects.

Objects as default values are only created once in Python and not on each invocation of the function. If the default object is modified, this modification is carried over to the next invocation of the method.

# Bad:
# If array_param is modified inside the function, the next invocation will
# receive the modified object.
def some_function(array_param=[]):
    # ...

# Better: Create an array on each invocation
def some_function(array_param=None):
    array_param = array_param or []
    # ...
Loading history...
Unused Code introduced by
The argument links_in seems to be unused.
Loading history...
Unused Code introduced by
The argument links_out seems to be unused.
Loading history...
1318
    """
1319
    A helper function for constructing a standard tooltip for the node
1320
    in on the canvas.
1321
1322
    Parameters:
1323
    ===========
1324
    node : NodeItem
1325
        The node item instance.
1326
    links_in : list of LinkItem instances
1327
        A list of input links for the node.
1328
    links_out : list of LinkItem instances
1329
        A list of output links for the node.
1330
1331
    """
1332
    desc = node.widget_description
1333
    channel_fmt = "<li>{0}</li>"
1334
1335
    title_fmt = "<b>{title}</b><hr/>"
1336
    title = title_fmt.format(title=escape(node.title()))
1337
    inputs_list_fmt = "Inputs:<ul>{inputs}</ul><hr/>"
1338
    outputs_list_fmt = "Outputs:<ul>{outputs}</ul>"
1339
    if desc.inputs:
1340
        inputs = [channel_fmt.format(inp.name) for inp in desc.inputs]
1341
        inputs = inputs_list_fmt.format(inputs="".join(inputs))
1342
    else:
1343
        inputs = "No inputs<hr/>" 
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
1344
1345
    if desc.outputs:
1346
        outputs = [channel_fmt.format(out.name) for out in desc.outputs]
1347
        outputs = outputs_list_fmt.format(outputs="".join(outputs))
1348
    else:
1349
        outputs = "No outputs" 
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
1350
1351
    tooltip = title + inputs + outputs
1352
    style = "ul { margin-top: 1px; margin-bottom: 1px; }"
1353
    return TOOLTIP_TEMPLATE.format(style=style, tooltip=tooltip)
1354
1355
1356
def parse_format_fields(format_str):
1357
    formatter = string.Formatter()
1358
    format_fields = [(field, (spec, conv))
1359
                     for _, field, spec, conv in formatter.parse(format_str)
1360
                     if field is not None]
1361
    return format_fields
1362