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.CanvasScene   F
last analyzed

Complexity

Total Complexity 149

Size/Duplication

Total Lines 849
Duplicated Lines 0 %
Metric Value
dl 0
loc 849
rs 1.2632
wmc 149

64 Methods

Rating   Name   Duplication   Size   Complexity  
C item_at() 0 21 7
A itemAt() 0 3 1
A link_for_item() 0 6 2
A on_scheme_change() 0 2 1
A mouseMoveEvent() 0 6 3
A remove_annotation_item() 0 8 1
A set_node_animation_enabled() 0 9 3
A focusItem() 0 3 1
B add_node_item() 0 35 4
A annotation_for_item() 0 4 2
A _on_position_change() 0 5 1
A set_channel_names_visible() 0 7 2
A __init__() 0 48 1
A mouseGrabberItem() 0 3 1
B set_scheme() 0 45 6
A __str__() 0 3 1
A selectedItems() 0 3 1
B add_annotation() 0 37 4
A node_items() 0 5 1
B add_node() 0 36 4
A add_annotation_item() 0 8 1
A selected_node_items() 0 5 3
A item_for_annotation() 0 2 1
A remove_node_item() 0 14 1
A commit_scheme_link() 0 13 3
A set_user_interaction_handler() 0 10 4
A node_output_links() 0 6 3
A on_link_state_change() 0 2 1
A on_widget_state_change() 0 2 1
A __update_font() 0 4 2
A link_items() 0 5 1
A remove_link_item() 0 23 1
A items() 0 3 1
C clear_scene() 0 50 8
A node_links() 0 6 1
A remove_link() 0 15 2
A remove_node() 0 16 1
A item_for_node() 0 5 1
A node_input_links() 0 6 3
A keyPressEvent() 0 5 3
A channel_names_visible() 0 5 1
A keyReleaseEvent() 0 5 3
A channel_name() 0 5 2
A collidingItems() 0 3 1
A selected_annotation_items() 0 5 3
B add_link() 0 27 3
A set_registry() 0 8 1
A annotation_items() 0 5 1
A node_for_item() 0 6 2
B mousePressEvent() 0 17 7
A item_for_link() 0 5 1
A __on_scheme_annot_geometry_change() 0 11 3
B commit_scheme_node() 0 23 4
A mouseDoubleClickEvent() 0 6 3
A mouseReleaseEvent() 0 5 3
A __on_node_pos_changed() 0 4 1
A event() 0 7 2
D new_node_item() 0 23 8
A remove_annotation() 0 18 2
A add_link_item() 0 18 2
B new_link_item() 0 29 3
A anchor_layout() 0 5 1
A set_anchor_layout() 0 10 3
A neighbor_nodes() 0 10 1

How to fix   Complexity   

Complex Class

Complex classes like Orange.canvas.canvas.CanvasScene 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
Canvas Graphics Scene
4
=====================
5
6
"""
7
8
import logging
9
import itertools
10
11
from operator import attrgetter
12
13
from xml.sax.saxutils import escape
14
15
from PyQt4.QtGui import QGraphicsScene, QPainter, QBrush, QColor, QFont, \
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...
16
                        QGraphicsItem
17
18
from PyQt4.QtCore import Qt, QPointF, QRectF, QSizeF, QLineF, QBuffer, QEvent
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
20
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...
21
from PyQt4.QtCore import PYQT_VERSION_STR
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...
22
23
24
from .. import scheme
25
26
from . import items
27
from .layout import AnchorLayout
28
from .items.utils import toGraphicsObjectIfPossible, typed_signal_mapper
29
30
log = logging.getLogger(__name__)
31
32
33
NodeItemSignalMapper = typed_signal_mapper(items.NodeItem)
34
35
36
class CanvasScene(QGraphicsScene):
37
    """
38
    A Graphics Scene for displaying an :class:`~.scheme.Scheme` instance.
39
    """
40
41
    #: Signal emitted when a :class:`NodeItem` has been added to the scene.
42
    node_item_added = Signal(items.NodeItem)
43
44
    #: Signal emitted when a :class:`NodeItem` has been removed from the
45
    #: scene.
46
    node_item_removed = Signal(items.LinkItem)
47
48
    #: Signal emitted when a new :class:`LinkItem` has been added to the
49
    #: scene.
50
    link_item_added = Signal(items.LinkItem)
51
52
    #: Signal emitted when a :class:`LinkItem` has been removed.
53
    link_item_removed = Signal(items.LinkItem)
54
55
    #: Signal emitted when a :class:`Annotation` item has been added.
56
    annotation_added = Signal(items.annotationitem.Annotation)
57
58
    #: Signal emitted when a :class:`Annotation` item has been removed.
59
    annotation_removed = Signal(items.annotationitem.Annotation)
60
61
    #: Signal emitted when the position of a :class:`NodeItem` has changed.
62
    node_item_position_changed = Signal(items.NodeItem, QPointF)
63
64
    #: Signal emitted when an :class:`NodeItem` has been double clicked.
65
    node_item_double_clicked = Signal(items.NodeItem)
66
67
    #: An node item has been activated (clicked)
68
    node_item_activated = Signal(items.NodeItem)
69
70
    #: An node item has been hovered
71
    node_item_hovered = Signal(items.NodeItem)
72
73
    #: Link item has been hovered
74
    link_item_hovered = Signal(items.LinkItem)
75
76
    def __init__(self, *args, **kwargs):
77
        QGraphicsScene.__init__(self, *args, **kwargs)
78
79
        self.scheme = None
80
        self.registry = None
81
82
        # All node items
83
        self.__node_items = []
84
        # Mapping from SchemeNodes to canvas items
85
        self.__item_for_node = {}
86
        # All link items
87
        self.__link_items = []
88
        # Mapping from SchemeLinks to canvas items.
89
        self.__item_for_link = {}
90
91
        # All annotation items
92
        self.__annotation_items = []
93
        # Mapping from SchemeAnnotations to canvas items.
94
        self.__item_for_annotation = {}
95
96
        # Is the scene editable
97
        self.editable = True
98
99
        # Anchor Layout
100
        self.__anchor_layout = AnchorLayout()
101
        self.addItem(self.__anchor_layout)
102
103
        self.__channel_names_visible = True
104
        self.__node_animation_enabled = True
105
106
        self.user_interaction_handler = None
107
108
        self.activated_mapper = NodeItemSignalMapper(self)
109
        self.activated_mapper.pyMapped.connect(
110
            self.node_item_activated
111
        )
112
113
        self.hovered_mapper = NodeItemSignalMapper(self)
114
        self.hovered_mapper.pyMapped.connect(
115
            self.node_item_hovered
116
        )
117
118
        self.position_change_mapper = NodeItemSignalMapper(self)
119
        self.position_change_mapper.pyMapped.connect(
120
            self._on_position_change
121
        )
122
123
        log.info("'%s' intitialized." % self)
0 ignored issues
show
Coding Style Best Practice introduced by
Specify string format arguments as logging function parameters
Loading history...
124
125
    def clear_scene(self):
126
        """
127
        Clear (reset) the scene.
128
        """
129
        if self.scheme is not None:
130
            self.scheme.node_added.disconnect(self.add_node)
131
            self.scheme.node_removed.disconnect(self.remove_node)
132
133
            self.scheme.link_added.disconnect(self.add_link)
134
            self.scheme.link_removed.disconnect(self.remove_link)
135
136
            self.scheme.annotation_added.disconnect(self.add_annotation)
137
            self.scheme.annotation_removed.disconnect(self.remove_annotation)
138
139
            self.scheme.node_state_changed.disconnect(
140
                self.on_widget_state_change
141
            )
142
            self.scheme.channel_state_changed.disconnect(
143
                self.on_link_state_change
144
            )
145
146
            # Remove all items to make sure all signals from scheme items
147
            # to canvas items are disconnected.
148
149
            for annot in self.scheme.annotations:
150
                if annot in self.__item_for_annotation:
151
                    self.remove_annotation(annot)
152
153
            for link in self.scheme.links:
154
                if link in self.__item_for_link:
155
                    self.remove_link(link)
156
157
            for node in self.scheme.nodes:
158
                if node in self.__item_for_node:
159
                    self.remove_node(node)
160
161
        self.scheme = None
162
        self.__node_items = []
163
        self.__item_for_node = {}
164
        self.__link_items = []
165
        self.__item_for_link = {}
166
        self.__annotation_items = []
167
        self.__item_for_annotation = {}
168
169
        self.__anchor_layout.deleteLater()
170
171
        self.user_interaction_handler = None
172
173
        self.clear()
174
        log.info("'%s' cleared." % self)
0 ignored issues
show
Coding Style Best Practice introduced by
Specify string format arguments as logging function parameters
Loading history...
175
176
    def set_scheme(self, scheme):
0 ignored issues
show
Comprehensibility Bug introduced by
scheme is re-defining a name which is already available in the outer-scope (previously defined on line 24).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
177
        """
178
        Set the scheme to display. Populates the scene with nodes and links
179
        already in the scheme. Any further change to the scheme will be
180
        reflected in the scene.
181
182
        Parameters
183
        ----------
184
        scheme : :class:`~.scheme.Scheme`
185
186
        """
187
        if self.scheme is not None:
188
            # Clear the old scheme
189
            self.clear_scene()
190
191
        log.info("Setting scheme '%s' on '%s'" % (scheme, self))
0 ignored issues
show
Coding Style Best Practice introduced by
Specify string format arguments as logging function parameters
Loading history...
192
193
        self.scheme = scheme
194
        if self.scheme is not None:
195
            self.scheme.node_added.connect(self.add_node)
196
            self.scheme.node_removed.connect(self.remove_node)
197
198
            self.scheme.link_added.connect(self.add_link)
199
            self.scheme.link_removed.connect(self.remove_link)
200
201
            self.scheme.annotation_added.connect(self.add_annotation)
202
            self.scheme.annotation_removed.connect(self.remove_annotation)
203
204
            self.scheme.node_state_changed.connect(
205
                self.on_widget_state_change
206
            )
207
            self.scheme.channel_state_changed.connect(
208
                self.on_link_state_change
209
            )
210
211
            self.scheme.topology_changed.connect(self.on_scheme_change)
212
213
        for node in scheme.nodes:
214
            self.add_node(node)
215
216
        for link in scheme.links:
217
            self.add_link(link)
218
219
        for annot in scheme.annotations:
220
            self.add_annotation(annot)
221
222
    def set_registry(self, registry):
223
        """
224
        Set the widget registry.
225
        """
226
        # TODO: Remove/Deprecate. Is used only to get the category/background
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
227
        # color. That should be part of the SchemeNode/WidgetDescription.
228
        log.info("Setting registry '%s on '%s'." % (registry, self))
0 ignored issues
show
Coding Style Best Practice introduced by
Specify string format arguments as logging function parameters
Loading history...
229
        self.registry = registry
230
231
    def set_anchor_layout(self, layout):
232
        """
233
        Set an :class:`~.layout.AnchorLayout`
234
        """
235
        if self.__anchor_layout != layout:
236
            if self.__anchor_layout:
237
                self.__anchor_layout.deleteLater()
238
                self.__anchor_layout = None
239
240
            self.__anchor_layout = layout
241
242
    def anchor_layout(self):
243
        """
244
        Return the anchor layout instance.
245
        """
246
        return self.__anchor_layout
247
248
    def set_channel_names_visible(self, visible):
249
        """
250
        Set the channel names visibility.
251
        """
252
        self.__channel_names_visible = visible
253
        for link in self.__link_items:
254
            link.setChannelNamesVisible(visible)
255
256
    def channel_names_visible(self):
257
        """
258
        Return the channel names visibility state.
259
        """
260
        return self.__channel_names_visible
261
262
    def set_node_animation_enabled(self, enabled):
263
        """
264
        Set node animation enabled state.
265
        """
266
        if self.__node_animation_enabled != enabled:
267
            self.__node_animation_enabled = enabled
268
269
            for node in self.__node_items:
270
                node.setAnimationEnabled(enabled)
271
272
    def add_node_item(self, item):
273
        """
274
        Add a :class:`.NodeItem` instance to the scene.
275
        """
276
        if item in self.__node_items:
277
            raise ValueError("%r is already in the scene." % item)
278
279
        if item.pos().isNull():
280
            if self.__node_items:
281
                pos = self.__node_items[-1].pos() + QPointF(150, 0)
282
            else:
283
                pos = QPointF(150, 150)
284
285
            item.setPos(pos)
286
287
        item.setFont(self.font())
288
289
        # Set signal mappings
290
        self.activated_mapper.setPyMapping(item, item)
291
        item.activated.connect(self.activated_mapper.pyMap)
292
293
        self.hovered_mapper.setPyMapping(item, item)
294
        item.hovered.connect(self.hovered_mapper.pyMap)
295
296
        self.position_change_mapper.setPyMapping(item, item)
297
        item.positionChanged.connect(self.position_change_mapper.pyMap)
298
299
        self.addItem(item)
300
301
        self.__node_items.append(item)
302
303
        self.node_item_added.emit(item)
304
305
        log.info("Added item '%s' to '%s'" % (item, self))
0 ignored issues
show
Coding Style Best Practice introduced by
Specify string format arguments as logging function parameters
Loading history...
306
        return item
307
308
    def add_node(self, node):
309
        """
310
        Add and return a default constructed :class:`.NodeItem` for a
311
        :class:`SchemeNode` instance `node`. If the `node` is already in
312
        the scene do nothing and just return its item.
313
314
        """
315
        if node in self.__item_for_node:
316
            # Already added
317
            return self.__item_for_node[node]
318
319
        item = self.new_node_item(node.description)
320
321
        if node.position:
322
            pos = QPointF(*node.position)
323
            item.setPos(pos)
324
325
        item.setTitle(node.title)
326
        item.setProcessingState(node.processing_state)
327
        item.setProgress(node.progress)
328
329
        for message in node.state_messages():
330
            item.setStateMessage(message)
331
332
        item.setStatusMessage(node.status_message())
333
334
        self.__item_for_node[node] = item
335
336
        node.position_changed.connect(self.__on_node_pos_changed)
337
        node.title_changed.connect(item.setTitle)
338
        node.progress_changed.connect(item.setProgress)
339
        node.processing_state_changed.connect(item.setProcessingState)
340
        node.state_message_changed.connect(item.setStateMessage)
341
        node.status_message_changed.connect(item.setStatusMessage)
342
343
        return self.add_node_item(item)
344
345
    def new_node_item(self, widget_desc, category_desc=None):
346
        """
347
        Construct an new :class:`.NodeItem` from a `WidgetDescription`.
348
        Optionally also set `CategoryDescription`.
349
350
        """
351
        item = items.NodeItem()
352
        item.setWidgetDescription(widget_desc)
353
354
        if category_desc is None and self.registry and widget_desc.category:
355
            category_desc = self.registry.category(widget_desc.category)
356
357
        if category_desc is None and self.registry is not None:
358
            try:
359
                category_desc = self.registry.category(widget_desc.category)
360
            except KeyError:
0 ignored issues
show
Unused Code introduced by
This except handler seems to be unused and could be removed.

Except handlers which only contain pass and do not have an else clause can usually simply be removed:

try:
    raises_exception()
except:  # Could be removed
    pass
Loading history...
361
                pass
362
363
        if category_desc is not None:
364
            item.setWidgetCategory(category_desc)
365
366
        item.setAnimationEnabled(self.__node_animation_enabled)
367
        return item
368
369
    def remove_node_item(self, item):
370
        """
371
        Remove `item` (:class:`.NodeItem`) from the scene.
372
        """
373
        self.activated_mapper.removePyMappings(item)
374
        self.hovered_mapper.removePyMappings(item)
375
376
        item.hide()
377
        self.removeItem(item)
378
        self.__node_items.remove(item)
379
380
        self.node_item_removed.emit(item)
381
382
        log.info("Removed item '%s' from '%s'" % (item, self))
0 ignored issues
show
Coding Style Best Practice introduced by
Specify string format arguments as logging function parameters
Loading history...
383
384
    def remove_node(self, node):
385
        """
386
        Remove the :class:`.NodeItem` instance that was previously
387
        constructed for a :class:`SchemeNode` `node` using the `add_node`
388
        method.
389
390
        """
391
        item = self.__item_for_node.pop(node)
392
393
        node.position_changed.disconnect(self.__on_node_pos_changed)
394
        node.title_changed.disconnect(item.setTitle)
395
        node.progress_changed.disconnect(item.setProgress)
396
        node.processing_state_changed.disconnect(item.setProcessingState)
397
        node.state_message_changed.disconnect(item.setStateMessage)
398
399
        self.remove_node_item(item)
400
401
    def node_items(self):
402
        """
403
        Return all :class:`.NodeItem` instances in the scene.
404
        """
405
        return list(self.__node_items)
406
407
    def add_link_item(self, item):
408
        """
409
        Add a link (:class:`.LinkItem`) to the scene.
410
        """
411
        if item.scene() is not self:
412
            self.addItem(item)
413
414
        item.setFont(self.font())
415
        self.__link_items.append(item)
416
417
        self.link_item_added.emit(item)
418
419
        log.info("Added link %r -> %r to '%s'" % \
0 ignored issues
show
Coding Style Best Practice introduced by
Specify string format arguments as logging function parameters
Loading history...
420
                 (item.sourceItem.title(), item.sinkItem.title(), self))
421
422
        self.__anchor_layout.invalidateLink(item)
423
424
        return item
425
426
    def add_link(self, scheme_link):
427
        """
428
        Create and add a :class:`.LinkItem` instance for a
429
        :class:`SchemeLink` instance. If the link is already in the scene
430
        do nothing and just return its :class:`.LinkItem`.
431
432
        """
433
        if scheme_link in self.__item_for_link:
434
            return self.__item_for_link[scheme_link]
435
436
        source = self.__item_for_node[scheme_link.source_node]
437
        sink = self.__item_for_node[scheme_link.sink_node]
438
439
        item = self.new_link_item(source, scheme_link.source_channel,
440
                                  sink, scheme_link.sink_channel)
441
442
        item.setEnabled(scheme_link.enabled)
443
        scheme_link.enabled_changed.connect(item.setEnabled)
444
445
        if scheme_link.is_dynamic():
446
            item.setDynamic(True)
447
            item.setDynamicEnabled(scheme_link.dynamic_enabled)
448
            scheme_link.dynamic_enabled_changed.connect(item.setDynamicEnabled)
449
450
        self.add_link_item(item)
451
        self.__item_for_link[scheme_link] = item
452
        return item
453
454
    def new_link_item(self, source_item, source_channel,
455
                      sink_item, sink_channel):
456
        """
457
        Construct and return a new :class:`.LinkItem`
458
        """
459
        item = items.LinkItem()
460
        item.setSourceItem(source_item)
461
        item.setSinkItem(sink_item)
462
463
        def channel_name(channel):
464
            if isinstance(channel, str):
465
                return channel
466
            else:
467
                return channel.name
468
469
        source_name = channel_name(source_channel)
470
        sink_name = channel_name(sink_channel)
471
472
        fmt = "<b>{0}</b>&nbsp; \u2192 &nbsp;<b>{1}</b>"
473
        item.setToolTip(
474
            fmt.format(escape(source_name),
475
                       escape(sink_name))
476
        )
477
478
        item.setSourceName(source_name)
479
        item.setSinkName(sink_name)
480
        item.setChannelNamesVisible(self.__channel_names_visible)
481
482
        return item
483
484
    def remove_link_item(self, item):
485
        """
486
        Remove a link (:class:`.LinkItem`) from the scene.
487
        """
488
        # Invalidate the anchor layout.
489
        self.__anchor_layout.invalidateAnchorItem(
490
            item.sourceItem.outputAnchorItem
491
        )
492
        self.__anchor_layout.invalidateAnchorItem(
493
            item.sinkItem.inputAnchorItem
494
        )
495
496
        self.__link_items.remove(item)
497
498
        # Remove the anchor points.
499
        item.removeLink()
500
        self.removeItem(item)
501
502
        self.link_item_removed.emit(item)
503
504
        log.info("Removed link '%s' from '%s'" % (item, self))
0 ignored issues
show
Coding Style Best Practice introduced by
Specify string format arguments as logging function parameters
Loading history...
505
506
        return item
507
508
    def remove_link(self, scheme_link):
509
        """
510
        Remove a :class:`.LinkItem` instance that was previously constructed
511
        for a :class:`SchemeLink` instance `link` using the `add_link` method.
512
513
        """
514
        item = self.__item_for_link.pop(scheme_link)
515
        scheme_link.enabled_changed.disconnect(item.setEnabled)
516
517
        if scheme_link.is_dynamic():
518
            scheme_link.dynamic_enabled_changed.disconnect(
519
                item.setDynamicEnabled
520
            )
521
522
        self.remove_link_item(item)
523
524
    def link_items(self):
525
        """
526
        Return all :class:`.LinkItem`\s in the scene.
527
        """
528
        return list(self.__link_items)
529
530
    def add_annotation_item(self, annotation):
531
        """
532
        Add an :class:`.Annotation` item to the scene.
533
        """
534
        self.__annotation_items.append(annotation)
535
        self.addItem(annotation)
536
        self.annotation_added.emit(annotation)
537
        return annotation
538
539
    def add_annotation(self, scheme_annot):
540
        """
541
        Create a new item for :class:`SchemeAnnotation` and add it
542
        to the scene. If the `scheme_annot` is already in the scene do
543
        nothing and just return its item.
544
545
        """
546
        if scheme_annot in self.__item_for_annotation:
547
            # Already added
548
            return self.__item_for_annotation[scheme_annot]
549
550
        if isinstance(scheme_annot, scheme.SchemeTextAnnotation):
551
            item = items.TextAnnotation()
552
            item.setPlainText(scheme_annot.text)
553
            x, y, w, h = scheme_annot.rect
554
            item.setPos(x, y)
555
            item.resize(w, h)
556
            item.setTextInteractionFlags(Qt.TextEditorInteraction)
557
558
            font = font_from_dict(scheme_annot.font, item.font())
559
            item.setFont(font)
560
            scheme_annot.text_changed.connect(item.setPlainText)
561
562
        elif isinstance(scheme_annot, scheme.SchemeArrowAnnotation):
563
            item = items.ArrowAnnotation()
564
            start, end = scheme_annot.start_pos, scheme_annot.end_pos
565
            item.setLine(QLineF(QPointF(*start), QPointF(*end)))
566
            item.setColor(QColor(scheme_annot.color))
567
568
        scheme_annot.geometry_changed.connect(
569
            self.__on_scheme_annot_geometry_change
570
        )
571
572
        self.add_annotation_item(item)
573
        self.__item_for_annotation[scheme_annot] = item
574
575
        return item
576
577
    def remove_annotation_item(self, annotation):
578
        """
579
        Remove an :class:`.Annotation` instance from the scene.
580
581
        """
582
        self.__annotation_items.remove(annotation)
583
        self.removeItem(annotation)
584
        self.annotation_removed.emit(annotation)
585
586
    def remove_annotation(self, scheme_annotation):
587
        """
588
        Remove an :class:`.Annotation` instance that was previously added
589
        using :func:`add_anotation`.
590
591
        """
592
        item = self.__item_for_annotation.pop(scheme_annotation)
593
594
        scheme_annotation.geometry_changed.disconnect(
595
            self.__on_scheme_annot_geometry_change
596
        )
597
598
        if isinstance(scheme_annotation, scheme.SchemeTextAnnotation):
599
            scheme_annotation.text_changed.disconnect(
600
                item.setPlainText
601
            )
602
603
        self.remove_annotation_item(item)
604
605
    def annotation_items(self):
606
        """
607
        Return all :class:`.Annotation` items in the scene.
608
        """
609
        return self.__annotation_items
610
611
    def item_for_annotation(self, scheme_annotation):
612
        return self.__item_for_annotation[scheme_annotation]
613
614
    def annotation_for_item(self, item):
615
        rev = dict(reversed(item) \
616
                   for item in self.__item_for_annotation.items())
617
        return rev[item]
618
619
    def commit_scheme_node(self, node):
620
        """
621
        Commit the `node` into the scheme.
622
        """
623
        if not self.editable:
624
            raise Exception("Scheme not editable.")
625
626
        if node not in self.__item_for_node:
627
            raise ValueError("No 'NodeItem' for node.")
628
629
        item = self.__item_for_node[node]
630
631
        try:
632
            self.scheme.add_node(node)
633
        except Exception:
634
            log.error("An error occurred while committing node '%s'",
635
                      node, exc_info=True)
636
            # Cleanup (remove the node item)
637
            self.remove_node_item(item)
638
            raise
639
640
        log.info("Commited node '%s' from '%s' to '%s'" % \
0 ignored issues
show
Coding Style Best Practice introduced by
Specify string format arguments as logging function parameters
Loading history...
641
                 (node, self, self.scheme))
642
643
    def commit_scheme_link(self, link):
644
        """
645
        Commit a scheme link.
646
        """
647
        if not self.editable:
648
            raise Exception("Scheme not editable")
649
650
        if link not in self.__item_for_link:
651
            raise ValueError("No 'LinkItem' for link.")
652
653
        self.scheme.add_link(link)
654
        log.info("Commited link '%s' from '%s' to '%s'" % \
0 ignored issues
show
Coding Style Best Practice introduced by
Specify string format arguments as logging function parameters
Loading history...
655
                 (link, self, self.scheme))
656
657
    def node_for_item(self, item):
658
        """
659
        Return the `SchemeNode` for the `item`.
660
        """
661
        rev = dict([(v, k) for k, v in self.__item_for_node.items()])
662
        return rev[item]
663
664
    def item_for_node(self, node):
665
        """
666
        Return the :class:`NodeItem` instance for a :class:`SchemeNode`.
667
        """
668
        return self.__item_for_node[node]
669
670
    def link_for_item(self, item):
671
        """
672
        Return the `SchemeLink for `item` (:class:`LinkItem`).
673
        """
674
        rev = dict([(v, k) for k, v in self.__item_for_link.items()])
675
        return rev[item]
676
677
    def item_for_link(self, link):
678
        """
679
        Return the :class:`LinkItem` for a :class:`SchemeLink`
680
        """
681
        return self.__item_for_link[link]
682
683
    def selected_node_items(self):
684
        """
685
        Return the selected :class:`NodeItem`'s.
686
        """
687
        return [item for item in self.__node_items if item.isSelected()]
688
689
    def selected_annotation_items(self):
690
        """
691
        Return the selected :class:`Annotation`'s
692
        """
693
        return [item for item in self.__annotation_items if item.isSelected()]
694
695
    def node_links(self, node_item):
696
        """
697
        Return all links from the `node_item` (:class:`NodeItem`).
698
        """
699
        return self.node_output_links(node_item) + \
700
               self.node_input_links(node_item)
701
702
    def node_output_links(self, node_item):
703
        """
704
        Return a list of all output links from `node_item`.
705
        """
706
        return [link for link in self.__link_items
707
                if link.sourceItem == node_item]
708
709
    def node_input_links(self, node_item):
710
        """
711
        Return a list of all input links for `node_item`.
712
        """
713
        return [link for link in self.__link_items
714
                if link.sinkItem == node_item]
715
716
    def neighbor_nodes(self, node_item):
717
        """
718
        Return a list of `node_item`'s (class:`NodeItem`) neighbor nodes.
719
        """
720
        neighbors = list(map(attrgetter("sourceItem"),
721
                             self.node_input_links(node_item)))
722
723
        neighbors.extend(map(attrgetter("sinkItem"),
724
                             self.node_output_links(node_item)))
725
        return neighbors
726
727
    def on_widget_state_change(self, widget, state):
728
        pass
729
730
    def on_link_state_change(self, link, state):
731
        pass
732
733
    def on_scheme_change(self, ):
734
        pass
735
736
    def _on_position_change(self, item):
737
        # Invalidate the anchor point layout and schedule a layout.
738
        self.__anchor_layout.invalidateNode(item)
739
740
        self.node_item_position_changed.emit(item, item.pos())
741
742
    def __on_node_pos_changed(self, pos):
743
        node = self.sender()
744
        item = self.__item_for_node[node]
745
        item.setPos(*pos)
746
747
    def __on_scheme_annot_geometry_change(self):
748
        annot = self.sender()
749
        item = self.__item_for_annotation[annot]
750
        if isinstance(annot, scheme.SchemeTextAnnotation):
751
            item.setGeometry(QRectF(*annot.rect))
752
        elif isinstance(annot, scheme.SchemeArrowAnnotation):
753
            p1 = item.mapFromScene(QPointF(*annot.start_pos))
754
            p2 = item.mapFromScene(QPointF(*annot.end_pos))
755
            item.setLine(QLineF(p1, p2))
756
        else:
757
            pass
758
759
    def item_at(self, pos, type_or_tuple=None, buttons=0):
760
        """Return the item at `pos` that is an instance of the specified
761
        type (`type_or_tuple`). If `buttons` (`Qt.MouseButtons`) is given
762
        only return the item if it is the top level item that would
763
        accept any of the buttons (`QGraphicsItem.acceptedMouseButtons`).
764
765
        """
766
        rect = QRectF(pos, QSizeF(1, 1))
767
        items = self.items(rect)
0 ignored issues
show
Comprehensibility Bug introduced by
items is re-defining a name which is already available in the outer-scope (previously defined on line 26).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
768
769
        if buttons:
770
            items = itertools.dropwhile(
771
                lambda item: not item.acceptedMouseButtons() & buttons,
772
                items
773
            )
774
            items = list(items)[:1]
775
776
        if type_or_tuple:
777
            items = [i for i in items if isinstance(i, type_or_tuple)]
778
779
        return items[0] if items else None
780
781
    if list(map(int, PYQT_VERSION_STR.split('.'))) < [4, 9]:
782
        # For QGraphicsObject subclasses items, itemAt ... return a
783
        # QGraphicsItem wrapper instance and not the actual class instance.
784
        def itemAt(self, *args, **kwargs):
785
            item = QGraphicsScene.itemAt(self, *args, **kwargs)
786
            return toGraphicsObjectIfPossible(item)
787
788
        def items(self, *args, **kwargs):
789
            items = QGraphicsScene.items(self, *args, **kwargs)
0 ignored issues
show
Comprehensibility Bug introduced by
items is re-defining a name which is already available in the outer-scope (previously defined on line 26).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
790
            return list(map(toGraphicsObjectIfPossible, items))
791
792
        def selectedItems(self, *args, **kwargs):
793
            return list(map(toGraphicsObjectIfPossible,
794
                       QGraphicsScene.selectedItems(self, *args, **kwargs)))
795
796
        def collidingItems(self, *args, **kwargs):
797
            return list(map(toGraphicsObjectIfPossible,
798
                       QGraphicsScene.collidingItems(self, *args, **kwargs)))
799
800
        def focusItem(self, *args, **kwargs):
801
            item = QGraphicsScene.focusItem(self, *args, **kwargs)
802
            return toGraphicsObjectIfPossible(item)
803
804
        def mouseGrabberItem(self, *args, **kwargs):
805
            item = QGraphicsScene.mouseGrabberItem(self, *args, **kwargs)
806
            return toGraphicsObjectIfPossible(item)
807
808
    def mousePressEvent(self, event):
809
        if self.user_interaction_handler and \
810
                self.user_interaction_handler.mousePressEvent(event):
811
            return
812
813
        # Right (context) click on the node item. If the widget is not
814
        # in the current selection then select the widget (only the widget).
815
        # Else simply return and let customContextMenuReqested signal
816
        # handle it
817
        shape_item = self.item_at(event.scenePos(), items.NodeItem)
818
        if shape_item and event.button() == Qt.RightButton and \
819
                shape_item.flags() & QGraphicsItem.ItemIsSelectable:
820
            if not shape_item.isSelected():
821
                self.clearSelection()
822
                shape_item.setSelected(True)
823
824
        return QGraphicsScene.mousePressEvent(self, event)
825
826
    def mouseMoveEvent(self, event):
827
        if self.user_interaction_handler and \
828
                self.user_interaction_handler.mouseMoveEvent(event):
829
            return
830
831
        return QGraphicsScene.mouseMoveEvent(self, event)
832
833
    def mouseReleaseEvent(self, event):
834
        if self.user_interaction_handler and \
835
                self.user_interaction_handler.mouseReleaseEvent(event):
836
            return
837
        return QGraphicsScene.mouseReleaseEvent(self, event)
838
839
    def mouseDoubleClickEvent(self, event):
840
        if self.user_interaction_handler and \
841
                self.user_interaction_handler.mouseDoubleClickEvent(event):
842
            return
843
844
        return QGraphicsScene.mouseDoubleClickEvent(self, event)
845
846
    def keyPressEvent(self, event):
847
        if self.user_interaction_handler and \
848
                self.user_interaction_handler.keyPressEvent(event):
849
            return
850
        return QGraphicsScene.keyPressEvent(self, event)
851
852
    def keyReleaseEvent(self, event):
853
        if self.user_interaction_handler and \
854
                self.user_interaction_handler.keyReleaseEvent(event):
855
            return
856
        return QGraphicsScene.keyReleaseEvent(self, event)
857
858
    def set_user_interaction_handler(self, handler):
859
        if self.user_interaction_handler and \
860
                not self.user_interaction_handler.isFinished():
861
            self.user_interaction_handler.cancel()
862
863
        log.info("Setting interaction '%s' to '%s'" % (handler, self))
0 ignored issues
show
Coding Style Best Practice introduced by
Specify string format arguments as logging function parameters
Loading history...
864
865
        self.user_interaction_handler = handler
866
        if handler:
867
            handler.start()
868
869
    def event(self, event):
870
        # TODO: change the base class of Node/LinkItem to QGraphicsWidget.
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
871
        # It already handles font changes.
872
        if event.type() == QEvent.FontChange:
873
            self.__update_font()
874
875
        return QGraphicsScene.event(self, event)
876
877
    def __update_font(self):
878
        font = self.font()
879
        for item in self.__node_items + self.__link_items:
880
            item.setFont(font)
881
882
    def __str__(self):
883
        return "%s(objectName=%r, ...)" % \
884
                (type(self).__name__, str(self.objectName()))
885
886
887
def font_from_dict(font_dict, font=None):
888
    if font is None:
889
        font = QFont()
890
    else:
891
        font = QFont(font)
892
893
    if "family" in font_dict:
894
        font.setFamily(font_dict["family"])
895
896
    if "size" in font_dict:
897
        font.setPixelSize(font_dict["size"])
898
899
    return font
900
901
902
def grab_svg(scene):
903
    """
904
    Return a SVG rendering of the scene contents.
905
906
    Parameters
907
    ----------
908
    scene : :class:`CanvasScene`
909
910
    """
911
    from PyQt4.QtSvg import QSvgGenerator
0 ignored issues
show
Configuration introduced by
The import PyQt4.QtSvg 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...
912
    svg_buffer = QBuffer()
913
    gen = QSvgGenerator()
914
    gen.setOutputDevice(svg_buffer)
915
916
    items_rect = scene.itemsBoundingRect().adjusted(-10, -10, 10, 10)
917
918
    if items_rect.isNull():
919
        items_rect = QRectF(0, 0, 10, 10)
920
921
    width, height = items_rect.width(), items_rect.height()
922
    rect_ratio = float(width) / height
923
924
    # Keep a fixed aspect ratio.
925
    aspect_ratio = 1.618
926
    if rect_ratio > aspect_ratio:
927
        height = int(height * rect_ratio / aspect_ratio)
928
    else:
929
        width = int(width * aspect_ratio / rect_ratio)
930
931
    target_rect = QRectF(0, 0, width, height)
932
    source_rect = QRectF(0, 0, width, height)
933
    source_rect.moveCenter(items_rect.center())
934
935
    gen.setSize(target_rect.size().toSize())
936
    gen.setViewBox(target_rect)
937
938
    painter = QPainter(gen)
939
940
    # Draw background.
941
    painter.setBrush(QBrush(Qt.white))
942
    painter.drawRect(target_rect)
943
944
    # Render the scene
945
    scene.render(painter, target_rect, source_rect)
946
    painter.end()
947
948
    buffer_str = bytes(svg_buffer.buffer())
949
    return buffer_str.decode("utf-8")
950