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

Complexity

Total Complexity 243

Size/Duplication

Total Lines 1539
Duplicated Lines 0 %
Metric Value
dl 0
loc 1539
rs 0.6316
wmc 243

88 Methods

Rating   Name   Duplication   Size   Complexity  
A removeLink() 0 6 1
B __onCustomContextMenuRequested() 0 26 6
A sceneContextMenuEvent() 0 2 1
A __onNodeActivate() 0 6 1
C sceneMouseDoubleClickEvent() 0 30 7
A __signalManagerStateChanged() 0 6 3
A toggleZoom() 0 11 2
F __duplicateSelected() 0 59 16
C __setupActions() 0 188 7
A __startControlPointEdit() 0 16 3
A setQuickMenuTriggers() 0 15 2
C setScheme() 0 41 7
A __onInteractionEnded() 0 3 1
A addLink() 0 6 1
A __onItemFocusedOut() 0 5 1
A changeEvent() 0 5 2
A addAnnotation() 0 6 1
A __onRenameAction() 0 7 2
A openSelected() 0 7 2
A __onHelpAction() 0 12 2
A copy_node() 0 5 1
A setModified() 0 12 3
F sceneMousePressEvent() 0 42 10
A __updateFont() 0 16 3
B __teardownScene() 0 33 1
B __onSelectionChanged() 0 40 6
A __setupUi() 0 21 1
F sceneKeyPressEvent() 0 45 13
B __toggleNewTextAnnotation() 0 24 4
B __toggleNewArrowAnnotation() 0 24 4
F sceneMouseReleaseEvent() 0 41 9
A __onArrowColorTriggered() 0 10 3
A nextPosition() 0 13 2
B sceneMouseMoveEvent() 0 16 5
A enumerateTitle() 0 14 2
A renameNode() 0 6 1
A focusNode() 0 16 3
A __linkRemove() 0 6 2
A font() 0 4 1
B removeSelected() 0 21 5
A removeNode() 0 6 1
A selectedAnnotations() 0 6 1
A sceneKeyReleaseEvent() 0 2 1
A setPath() 0 11 2
A __onNodePositionChanged() 0 8 2
A toolbarActions() 0 15 1
A nodeAnimationEnabled() 0 5 1
A scheme() 0 5 1
A __onAnnotationGeometryChanged() 0 9 2
A quickMenuTriggers() 0 5 1
A selectAll() 0 7 3
A __init__() 0 55 1
A __onCleanChanged() 0 4 2
A undoStack() 0 5 1
A isModified() 0 5 1
A _setUserInteractionHandler() 0 20 4
A __onAnnotationAdded() 0 22 3
B __setupScene() 0 46 1
A __onAnnotationRemoved() 0 14 3
A channelNamesVisible() 0 5 1
A __toggleLinkEnabled() 0 10 2
A __onItemFocusedIn() 0 6 2
A __showHelpFor() 0 12 2
A setDescription() 0 6 1
A editNodeTitle() 0 15 2
A removeAnnotation() 0 6 1
A view() 0 7 1
A selectedNodes() 0 6 1
A copy_link() 0 8 3
A setChannelNamesVisible() 0 10 2
A scene() 0 7 1
A isModifiedStrict() 0 18 1
A __onEditingFinished() 0 10 2
A __linkReset() 0 10 2
A __endControlPointEdit() 0 12 4
A setRegistry() 0 9 2
A newNodeHelper() 0 14 3
A menuBarActions() 0 6 1
A setNodeAnimationEnabled() 0 7 2
A setTitle() 0 6 1
A addNode() 0 6 1
B alignToGrid() 0 29 4
A quickMenu() 0 12 3
A __onFontSizeTriggered() 0 10 3
F eventFilter() 0 54 19
A path() 0 5 1
A createNewNode() 0 10 1
A color_icon() 0 13 2

How to fix   Complexity   

Complex Class

Complex classes like Orange.canvas.document.SchemeEditWidget 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
Scheme Editor Widget
4
====================
5
6
7
"""
8
9
import sys
10
import logging
11
import itertools
12
import unicodedata
13
import copy
14
15
from operator import attrgetter
16
from urllib.parse import urlencode
17
18
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...
19
    QWidget, QVBoxLayout, QInputDialog, QMenu, QAction, QActionGroup,
20
    QKeySequence, QUndoStack, QUndoCommand, QGraphicsItem, QGraphicsObject,
21
    QGraphicsTextItem, QCursor, QFont, QPainter, QPixmap, QColor,
22
    QIcon, QWhatsThisClickedEvent, QBrush
23
)
24
25
from PyQt4.QtCore import (
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...
26
    Qt, QObject, QEvent, QSignalMapper, QRectF, QCoreApplication
27
)
28
29
from PyQt4.QtCore import pyqtProperty as Property, 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...
30
31
from ..registry.qt import whats_this_helper
32
from ..gui.quickhelp import QuickHelpTipEvent
33
from ..gui.utils import message_information, disabled
34
from ..scheme import (
0 ignored issues
show
Unused Code introduced by
Unused scheme imported from scheme
Loading history...
35
    scheme, signalmanager, SchemeNode, SchemeLink, BaseSchemeAnnotation
36
)
37
from ..scheme import widgetsscheme
38
from ..canvas.scene import CanvasScene
39
from ..canvas.view import CanvasView
40
from ..canvas import items
41
from . import interactions
42
from . import commands
43
from . import quickmenu
44
45
46
log = logging.getLogger(__name__)
47
48
49
# TODO: Should this be moved to CanvasScene?
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
50
class GraphicsSceneFocusEventListener(QGraphicsObject):
51
52
    itemFocusedIn = Signal(QGraphicsItem)
53
    itemFocusedOut = Signal(QGraphicsItem)
54
55
    def __init__(self, parent=None):
56
        QGraphicsObject.__init__(self, parent)
57
        self.setFlag(QGraphicsItem.ItemHasNoContents)
58
59
    def sceneEventFilter(self, obj, event):
60
        if event.type() == QEvent.FocusIn and \
61
                obj.flags() & QGraphicsItem.ItemIsFocusable:
62
            obj.focusInEvent(event)
63
            if obj.hasFocus():
64
                self.itemFocusedIn.emit(obj)
65
            return True
66
        elif event.type() == QEvent.FocusOut:
67
            obj.focusOutEvent(event)
68
            if not obj.hasFocus():
69
                self.itemFocusedOut.emit(obj)
70
            return True
71
72
        return QGraphicsObject.sceneEventFilter(self, obj, event)
73
74
    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...
75
        return QRectF()
76
77
78
class SchemeEditWidget(QWidget):
79
    """
80
    A widget for editing a :class:`~.scheme.Scheme` instance.
81
82
    """
83
    #: Undo command has become available/unavailable.
84
    undoAvailable = Signal(bool)
85
86
    #: Redo command has become available/unavailable.
87
    redoAvailable = Signal(bool)
88
89
    #: Document modified state has changed.
90
    modificationChanged = Signal(bool)
91
92
    #: Undo command was added to the undo stack.
93
    undoCommandAdded = Signal()
94
95
    #: Item selection has changed.
96
    selectionChanged = Signal()
97
98
    #: Document title has changed.
99
    titleChanged = Signal(str)
100
101
    #: Document path has changed.
102
    pathChanged = Signal(str)
103
104
    # Quick Menu triggers
105
    (NoTriggers,
106
     RightClicked,
107
     DoubleClicked,
108
     SpaceKey,
109
     AnyKey) = [0, 1, 2, 4, 8]
110
111
    def __init__(self, parent=None, ):
112
        QWidget.__init__(self, parent)
113
114
        self.__modified = False
115
        self.__registry = None
116
        self.__scheme = None
117
        self.__path = ""
118
        self.__quickMenuTriggers = SchemeEditWidget.SpaceKey | \
119
                                   SchemeEditWidget.DoubleClicked
120
        self.__emptyClickButtons = 0
121
        self.__channelNamesVisible = True
122
        self.__nodeAnimationEnabled = True
123
        self.__possibleSelectionHandler = None
124
        self.__possibleMouseItemsMove = False
125
        self.__itemsMoving = {}
126
        self.__contextMenuTarget = None
127
        self.__quickMenu = None
128
        self.__quickTip = ""
129
130
        self.__undoStack = QUndoStack(self)
131
        self.__undoStack.cleanChanged[bool].connect(self.__onCleanChanged)
132
133
        # scheme node properties when set to a clean state
134
        self.__cleanProperties = []
135
136
        self.__editFinishedMapper = QSignalMapper(self)
137
        self.__editFinishedMapper.mapped[QObject].connect(
138
            self.__onEditingFinished
139
        )
140
141
        self.__annotationGeomChanged = QSignalMapper(self)
142
143
        self.__setupActions()
144
        self.__setupUi()
145
146
        self.__editMenu = QMenu(self.tr("&Edit"), self)
147
        self.__editMenu.addAction(self.__undoAction)
148
        self.__editMenu.addAction(self.__redoAction)
149
        self.__editMenu.addSeparator()
150
        self.__editMenu.addAction(self.__duplicateSelectedAction)
151
        self.__editMenu.addAction(self.__selectAllAction)
152
153
        self.__widgetMenu = QMenu(self.tr("&Widget"), self)
154
        self.__widgetMenu.addAction(self.__openSelectedAction)
155
        self.__widgetMenu.addSeparator()
156
        self.__widgetMenu.addAction(self.__renameAction)
157
        self.__widgetMenu.addAction(self.__removeSelectedAction)
158
        self.__widgetMenu.addSeparator()
159
        self.__widgetMenu.addAction(self.__helpAction)
160
161
        self.__linkMenu = QMenu(self.tr("Link"), self)
162
        self.__linkMenu.addAction(self.__linkEnableAction)
163
        self.__linkMenu.addSeparator()
164
        self.__linkMenu.addAction(self.__linkRemoveAction)
165
        self.__linkMenu.addAction(self.__linkResetAction)
166
167
    def __setupActions(self):
168
169
        self.__zoomAction = \
170
            QAction(self.tr("Zoom"), self,
171
                    objectName="zoom-action",
172
                    checkable=True,
173
                    shortcut=QKeySequence.ZoomIn,
174
                    toolTip=self.tr("Zoom in the workflow."),
175
                    toggled=self.toggleZoom,
176
                    )
177
178
        self.__cleanUpAction = \
179
            QAction(self.tr("Clean Up"), self,
180
                    objectName="cleanup-action",
181
                    toolTip=self.tr("Align widget to a grid."),
182
                    triggered=self.alignToGrid,
183
                    )
184
185
        self.__newTextAnnotationAction = \
186
            QAction(self.tr("Text"), self,
187
                    objectName="new-text-action",
188
                    toolTip=self.tr("Add a text annotation to the workflow."),
189
                    checkable=True,
190
                    toggled=self.__toggleNewTextAnnotation,
191
                    )
192
193
        # Create a font size menu for the new annotation action.
194
        self.__fontMenu = QMenu("Font Size", self)
195
        self.__fontActionGroup = group = \
196
            QActionGroup(self, exclusive=True,
197
                         triggered=self.__onFontSizeTriggered)
198
199
        def font(size):
200
            f = QFont(self.font())
201
            f.setPixelSize(size)
202
            return f
203
204
        for size in [12, 14, 16, 18, 20, 22, 24]:
205
            action = QAction("%ipx" % size, group,
206
                             checkable=True,
207
                             font=font(size))
208
209
            self.__fontMenu.addAction(action)
210
211
        group.actions()[2].setChecked(True)
212
213
        self.__newTextAnnotationAction.setMenu(self.__fontMenu)
214
215
        self.__newArrowAnnotationAction = \
216
            QAction(self.tr("Arrow"), self,
217
                    objectName="new-arrow-action",
218
                    toolTip=self.tr("Add a arrow annotation to the workflow."),
219
                    checkable=True,
220
                    toggled=self.__toggleNewArrowAnnotation,
221
                    )
222
223
        # Create a color menu for the arrow annotation action
224
        self.__arrowColorMenu = QMenu("Arrow Color",)
225
        self.__arrowColorActionGroup = group = \
226
            QActionGroup(self, exclusive=True,
227
                         triggered=self.__onArrowColorTriggered)
228
229
        def color_icon(color):
230
            icon = QIcon()
231
            for size in [16, 24, 32]:
232
                pixmap = QPixmap(size, size)
233
                pixmap.fill(QColor(0, 0, 0, 0))
234
                p = QPainter(pixmap)
235
                p.setRenderHint(QPainter.Antialiasing)
236
                p.setBrush(color)
237
                p.setPen(Qt.NoPen)
238
                p.drawEllipse(1, 1, size - 2, size - 2)
239
                p.end()
240
                icon.addPixmap(pixmap)
241
            return icon
242
243
        for color in ["#000", "#C1272D", "#662D91", "#1F9CDF", "#39B54A"]:
244
            icon = color_icon(QColor(color))
245
            action = QAction(group, icon=icon, checkable=True,
246
                             iconVisibleInMenu=True)
247
            action.setData(color)
248
            self.__arrowColorMenu.addAction(action)
249
250
        group.actions()[1].setChecked(True)
251
252
        self.__newArrowAnnotationAction.setMenu(self.__arrowColorMenu)
253
254
        self.__undoAction = self.__undoStack.createUndoAction(self)
255
        self.__undoAction.setShortcut(QKeySequence.Undo)
256
        self.__undoAction.setObjectName("undo-action")
257
258
        self.__redoAction = self.__undoStack.createRedoAction(self)
259
        self.__redoAction.setShortcut(QKeySequence.Redo)
260
        self.__redoAction.setObjectName("redo-action")
261
262
        self.__selectAllAction = \
263
            QAction(self.tr("Select all"), self,
264
                    objectName="select-all-action",
265
                    toolTip=self.tr("Select all items."),
266
                    triggered=self.selectAll,
267
                    shortcut=QKeySequence.SelectAll
268
                    )
269
270
        self.__openSelectedAction = \
271
            QAction(self.tr("Open"), self,
272
                    objectName="open-action",
273
                    toolTip=self.tr("Open selected widget"),
274
                    triggered=self.openSelected,
275
                    enabled=False)
276
277
        self.__removeSelectedAction = \
278
            QAction(self.tr("Remove"), self,
279
                    objectName="remove-selected",
280
                    toolTip=self.tr("Remove selected items"),
281
                    triggered=self.removeSelected,
282
                    enabled=False
283
                    )
284
285
        shortcuts = [Qt.Key_Delete,
286
                     Qt.ControlModifier + Qt.Key_Backspace]
287
288
        if sys.platform == "darwin":
289
            # Command Backspace should be the first
290
            # (visible shortcut in the menu)
291
            shortcuts.reverse()
292
293
        self.__removeSelectedAction.setShortcuts(shortcuts)
294
295
        self.__renameAction = \
296
            QAction(self.tr("Rename"), self,
297
                    objectName="rename-action",
298
                    toolTip=self.tr("Rename selected widget"),
299
                    triggered=self.__onRenameAction,
300
                    shortcut=QKeySequence(Qt.Key_F2),
301
                    enabled=False)
302
303
        self.__helpAction = \
304
            QAction(self.tr("Help"), self,
305
                    objectName="help-action",
306
                    toolTip=self.tr("Show widget help"),
307
                    triggered=self.__onHelpAction,
308
                    shortcut=QKeySequence("F1"),
309
                    enabled=False,
310
                    )
311
312
        self.__linkEnableAction = \
313
            QAction(self.tr("Enabled"), self,
314
                    objectName="link-enable-action",
315
                    triggered=self.__toggleLinkEnabled,
316
                    checkable=True,
317
                    )
318
319
        self.__linkRemoveAction = \
320
            QAction(self.tr("Remove"), self,
321
                    objectName="link-remove-action",
322
                    triggered=self.__linkRemove,
323
                    toolTip=self.tr("Remove link."),
324
                    )
325
326
        self.__linkResetAction = \
327
            QAction(self.tr("Reset Signals"), self,
328
                    objectName="link-reset-action",
329
                    triggered=self.__linkReset,
330
                    )
331
332
        self.__duplicateSelectedAction = \
333
            QAction(self.tr("Duplicate Selected"), self,
334
                    objectName="duplicate-action",
335
                    enabled=False,
336
                    shortcut=QKeySequence(Qt.ControlModifier + Qt.Key_D),
337
                    triggered=self.__duplicateSelected,
338
                    )
339
340
        self.addActions([self.__newTextAnnotationAction,
341
                         self.__newArrowAnnotationAction,
342
                         self.__linkEnableAction,
343
                         self.__linkRemoveAction,
344
                         self.__linkResetAction,
345
                         self.__duplicateSelectedAction])
346
347
        # Actions which should be disabled while a multistep
348
        # interaction is in progress.
349
        self.__disruptiveActions = \
350
                [self.__undoAction,
351
                 self.__redoAction,
352
                 self.__removeSelectedAction,
353
                 self.__selectAllAction,
354
                 self.__duplicateSelectedAction]
355
356
    def __setupUi(self):
357
        layout = QVBoxLayout()
358
        layout.setContentsMargins(0, 0, 0, 0)
359
        layout.setSpacing(0)
360
361
        scene = CanvasScene()
362
        self.__setupScene(scene)
363
364
        view = CanvasView(scene)
365
        view.setFrameStyle(CanvasView.NoFrame)
366
        view.setRenderHint(QPainter.Antialiasing)
367
        view.setContextMenuPolicy(Qt.CustomContextMenu)
368
        view.customContextMenuRequested.connect(
369
            self.__onCustomContextMenuRequested
370
        )
371
372
        self.__view = view
373
        self.__scene = scene
374
375
        layout.addWidget(view)
376
        self.setLayout(layout)
377
378
    def __setupScene(self, scene):
379
        """
380
        Set up a :class:`CanvasScene` instance for use by the editor.
381
382
        .. note:: If an existing scene is in use it must be teared down using
383
            __teardownScene
384
385
        """
386
        scene.set_channel_names_visible(self.__channelNamesVisible)
387
        scene.set_node_animation_enabled(
388
            self.__nodeAnimationEnabled
389
        )
390
391
        scene.setFont(self.font())
392
393
        scene.installEventFilter(self)
394
395
        scene.set_registry(self.__registry)
396
397
        # Focus listener
398
        self.__focusListener = GraphicsSceneFocusEventListener()
0 ignored issues
show
Coding Style introduced by
The attribute __focusListener 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...
399
        self.__focusListener.itemFocusedIn.connect(
400
            self.__onItemFocusedIn
401
        )
402
        self.__focusListener.itemFocusedOut.connect(
403
            self.__onItemFocusedOut
404
        )
405
        scene.addItem(self.__focusListener)
406
407
        scene.selectionChanged.connect(
408
            self.__onSelectionChanged
409
        )
410
411
        scene.node_item_activated.connect(
412
            self.__onNodeActivate
413
        )
414
415
        scene.annotation_added.connect(
416
            self.__onAnnotationAdded
417
        )
418
419
        scene.annotation_removed.connect(
420
            self.__onAnnotationRemoved
421
        )
422
423
        self.__annotationGeomChanged = QSignalMapper(self)
424
425
    def __teardownScene(self, scene):
426
        """
427
        Tear down an instance of :class:`CanvasScene` that was used by the
428
        editor.
429
430
        """
431
        # Clear the current item selection in the scene so edit action
432
        # states are updated accordingly.
433
        scene.clearSelection()
434
435
        # Clear focus from any item.
436
        scene.setFocusItem(None)
437
438
        # Clear the annotation mapper
439
        self.__annotationGeomChanged.deleteLater()
440
        self.__annotationGeomChanged = None
441
442
        self.__focusListener.itemFocusedIn.disconnect(
443
            self.__onItemFocusedIn
444
        )
445
        self.__focusListener.itemFocusedOut.disconnect(
446
            self.__onItemFocusedOut
447
        )
448
449
        scene.selectionChanged.disconnect(
450
            self.__onSelectionChanged
451
        )
452
453
        scene.removeEventFilter(self)
454
455
        # Clear all items from the scene
456
        scene.blockSignals(True)
457
        scene.clear_scene()
458
459
    def toolbarActions(self):
460
        """
461
        Return a list of actions that can be inserted into a toolbar.
462
        At the moment these are:
463
464
            - 'Zoom' action
465
            - 'Clean up' action (align to grid)
466
            - 'New text annotation' action (with a size menu)
467
            - 'New arrow annotation' action (with a color menu)
468
469
        """
470
        return [self.__zoomAction,
471
                self.__cleanUpAction,
472
                self.__newTextAnnotationAction,
473
                self.__newArrowAnnotationAction]
474
475
    def menuBarActions(self):
476
        """
477
        Return a list of actions that can be inserted into a `QMenuBar`.
478
479
        """
480
        return [self.__editMenu.menuAction(), self.__widgetMenu.menuAction()]
481
482
    def isModified(self):
483
        """
484
        Is the document is a modified state.
485
        """
486
        return self.__modified or not self.__undoStack.isClean()
487
488
    def setModified(self, modified):
489
        """
490
        Set the document modified state.
491
        """
492
        if self.__modified != modified:
493
            self.__modified = modified
494
495
        if not modified:
496
            self.__cleanProperties = node_properties(self.__scheme)
497
            self.__undoStack.setClean()
498
        else:
499
            self.__cleanProperties = []
500
501
    modified = Property(bool, fget=isModified, fset=setModified)
502
503
    def isModifiedStrict(self):
504
        """
505
        Is the document modified.
506
507
        Run a strict check against all node properties as they were
508
        at the time when the last call to `setModified(True)` was made.
509
510
        """
511
        propertiesChanged = self.__cleanProperties != \
512
                            node_properties(self.__scheme)
513
514
        log.debug("Modified strict check (modified flag: %s, "
515
                  "undo stack clean: %s, properties: %s)",
516
                  self.__modified,
517
                  self.__undoStack.isClean(),
518
                  propertiesChanged)
519
520
        return self.isModified() or propertiesChanged
521
522
    def setQuickMenuTriggers(self, triggers):
523
        """
524
        Set quick menu trigger flags.
525
526
        Flags can be a bitwise `or` of:
527
528
            - `SchemeEditWidget.NoTrigeres`
529
            - `SchemeEditWidget.RightClicked`
530
            - `SchemeEditWidget.DoubleClicked`
531
            - `SchemeEditWidget.SpaceKey`
532
            - `SchemeEditWidget.AnyKey`
533
534
        """
535
        if self.__quickMenuTriggers != triggers:
536
            self.__quickMenuTriggers = triggers
537
538
    def quickMenuTriggers(self):
539
        """
540
        Return quick menu trigger flags.
541
        """
542
        return self.__quickMenuTriggers
543
544
    def setChannelNamesVisible(self, visible):
545
        """
546
        Set channel names visibility state. When enabled the links
547
        in the view will have a source/sink channel names displayed over
548
        them.
549
550
        """
551
        if self.__channelNamesVisible != visible:
552
            self.__channelNamesVisible = visible
553
            self.__scene.set_channel_names_visible(visible)
554
555
    def channelNamesVisible(self):
556
        """
557
        Return the channel name visibility state.
558
        """
559
        return self.__channelNamesVisible
560
561
    def setNodeAnimationEnabled(self, enabled):
562
        """
563
        Set the node item animation enabled state.
564
        """
565
        if self.__nodeAnimationEnabled != enabled:
566
            self.__nodeAnimationEnabled = enabled
567
            self.__scene.set_node_animation_enabled(enabled)
568
569
    def nodeAnimationEnabled(self):
570
        """
571
        Return the node item animation enabled state.
572
        """
573
        return self.__nodeAnimationEnabled
574
575
    def undoStack(self):
576
        """
577
        Return the undo stack.
578
        """
579
        return self.__undoStack
580
581
    def setPath(self, path):
582
        """
583
        Set the path associated with the current scheme.
584
585
        .. note:: Calling `setScheme` will invalidate the path (i.e. set it
586
                  to an empty string)
587
588
        """
589
        if self.__path != path:
590
            self.__path = str(path)
591
            self.pathChanged.emit(self.__path)
592
593
    def path(self):
594
        """
595
        Return the path associated with the scheme
596
        """
597
        return self.__path
598
599
    def setScheme(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 34).

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...
600
        """
601
        Set the :class:`~.scheme.Scheme` instance to display/edit.
602
        """
603
        if self.__scheme is not scheme:
604
            if self.__scheme:
605
                self.__scheme.title_changed.disconnect(self.titleChanged)
606
                self.__scheme.removeEventFilter(self)
607
                sm = self.__scheme.findChild(signalmanager.SignalManager)
608
                if sm:
609
                    sm.stateChanged.disconnect(
610
                        self.__signalManagerStateChanged)
611
612
            self.__scheme = scheme
613
614
            self.setPath("")
615
616
            if self.__scheme:
617
                self.__scheme.title_changed.connect(self.titleChanged)
618
                self.titleChanged.emit(scheme.title)
619
                self.__cleanProperties = node_properties(scheme)
620
                sm = scheme.findChild(signalmanager.SignalManager)
621
                if sm:
622
                    sm.stateChanged.connect(self.__signalManagerStateChanged)
623
            else:
624
                self.__cleanProperties = []
625
626
            self.__teardownScene(self.__scene)
627
            self.__scene.deleteLater()
628
629
            self.__undoStack.clear()
630
631
            self.__scene = CanvasScene()
0 ignored issues
show
Coding Style introduced by
The attribute __scene 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...
632
            self.__setupScene(self.__scene)
633
634
            self.__view.setScene(self.__scene)
635
636
            self.__scene.set_scheme(scheme)
637
638
            if self.__scheme:
639
                self.__scheme.installEventFilter(self)
640
641
    def scheme(self):
642
        """
643
        Return the :class:`~.scheme.Scheme` edited by the widget.
644
        """
645
        return self.__scheme
646
647
    def scene(self):
648
        """
649
        Return the :class:`QGraphicsScene` instance used to display the
650
        current scheme.
651
652
        """
653
        return self.__scene
654
655
    def view(self):
656
        """
657
        Return the :class:`QGraphicsView` instance used to display the
658
        current scene.
659
660
        """
661
        return self.__view
662
663
    def setRegistry(self, registry):
664
        # Is this method necessary?
665
        # It should be removed when the scene (items) is fixed
666
        # so all information regarding the visual appearance is
667
        # included in the node/widget description.
668
        self.__registry = registry
669
        if self.__scene:
670
            self.__scene.set_registry(registry)
671
            self.__quickMenu = None
672
673
    def quickMenu(self):
674
        """
675
        Return a :class:`~.quickmenu.QuickMenu` popup menu instance for
676
        new node creation.
677
678
        """
679
        if self.__quickMenu is None:
680
            menu = quickmenu.QuickMenu(self)
681
            if self.__registry is not None:
682
                menu.setModel(self.__registry.model())
683
            self.__quickMenu = menu
684
        return self.__quickMenu
685
686
    def setTitle(self, title):
687
        """
688
        Set the scheme title.
689
        """
690
        self.__undoStack.push(
691
            commands.SetAttrCommand(self.__scheme, "title", title)
692
        )
693
694
    def setDescription(self, description):
695
        """
696
        Set the scheme description string.
697
        """
698
        self.__undoStack.push(
699
            commands.SetAttrCommand(self.__scheme, "description", description)
700
        )
701
702
    def addNode(self, node):
703
        """
704
        Add a new node (:class:`.SchemeNode`) to the document.
705
        """
706
        command = commands.AddNodeCommand(self.__scheme, node)
707
        self.__undoStack.push(command)
708
709
    def createNewNode(self, description, title=None, position=None):
710
        """
711
        Create a new :class:`.SchemeNode` and add it to the document.
712
        The new node is constructed using :func:`newNodeHelper` method.
713
714
        """
715
        node = self.newNodeHelper(description, title, position)
716
        self.addNode(node)
717
718
        return node
719
720
    def newNodeHelper(self, description, title=None, position=None):
721
        """
722
        Return a new initialized :class:`.SchemeNode`. If `title`
723
        and `position` are not supplied they are initialized to sensible
724
        defaults.
725
726
        """
727
        if title is None:
728
            title = self.enumerateTitle(description.name)
729
730
        if position is None:
731
            position = self.nextPosition()
732
733
        return SchemeNode(description, title=title, position=position)
734
735
    def enumerateTitle(self, title):
736
        """
737
        Enumerate a `title` string (i.e. add a number in parentheses) so
738
        it is not equal to any node title in the current scheme.
739
740
        """
741
        curr_titles = set([node.title for node in self.scheme().nodes])
742
        template = title + " ({0})"
743
744
        enumerated = map(template.format, itertools.count(1))
745
        candidates = itertools.chain([title], enumerated)
746
747
        seq = itertools.dropwhile(curr_titles.__contains__, candidates)
748
        return next(seq)
749
750
    def nextPosition(self):
751
        """
752
        Return the next default node position as a (x, y) tuple. This is
753
        a position left of the last added node.
754
755
        """
756
        nodes = self.scheme().nodes
757
        if nodes:
758
            x, y = nodes[-1].position
759
            position = (x + 150, y)
760
        else:
761
            position = (150, 150)
762
        return position
763
764
    def removeNode(self, node):
765
        """
766
        Remove a `node` (:class:`.SchemeNode`) from the scheme
767
        """
768
        command = commands.RemoveNodeCommand(self.__scheme, node)
769
        self.__undoStack.push(command)
770
771
    def renameNode(self, node, title):
772
        """
773
        Rename a `node` (:class:`.SchemeNode`) to `title`.
774
        """
775
        command = commands.RenameNodeCommand(self.__scheme, node, title)
776
        self.__undoStack.push(command)
777
778
    def addLink(self, link):
779
        """
780
        Add a `link` (:class:`.SchemeLink`) to the scheme.
781
        """
782
        command = commands.AddLinkCommand(self.__scheme, link)
783
        self.__undoStack.push(command)
784
785
    def removeLink(self, link):
786
        """
787
        Remove a link (:class:`.SchemeLink`) from the scheme.
788
        """
789
        command = commands.RemoveLinkCommand(self.__scheme, link)
790
        self.__undoStack.push(command)
791
792
    def addAnnotation(self, annotation):
793
        """
794
        Add `annotation` (:class:`.BaseSchemeAnnotation`) to the scheme
795
        """
796
        command = commands.AddAnnotationCommand(self.__scheme, annotation)
797
        self.__undoStack.push(command)
798
799
    def removeAnnotation(self, annotation):
800
        """
801
        Remove `annotation` (:class:`.BaseSchemeAnnotation`) from the scheme.
802
        """
803
        command = commands.RemoveAnnotationCommand(self.__scheme, annotation)
804
        self.__undoStack.push(command)
805
806
    def removeSelected(self):
807
        """
808
        Remove all selected items in the scheme.
809
        """
810
        selected = self.scene().selectedItems()
811
        if not selected:
812
            return
813
814
        self.__undoStack.beginMacro(self.tr("Remove"))
815
        for item in selected:
816
            if isinstance(item, items.NodeItem):
817
                node = self.scene().node_for_item(item)
818
                self.__undoStack.push(
819
                    commands.RemoveNodeCommand(self.__scheme, node)
820
                )
821
            elif isinstance(item, items.annotationitem.Annotation):
822
                annot = self.scene().annotation_for_item(item)
823
                self.__undoStack.push(
824
                    commands.RemoveAnnotationCommand(self.__scheme, annot)
825
                )
826
        self.__undoStack.endMacro()
827
828
    def selectAll(self):
829
        """
830
        Select all selectable items in the scheme.
831
        """
832
        for item in self.__scene.items():
833
            if item.flags() & QGraphicsItem.ItemIsSelectable:
834
                item.setSelected(True)
835
836
    def toggleZoom(self, zoom):
837
        """
838
        Toggle view zoom. If `zoom` is True the scheme is displayed
839
        scaled to 150%.
840
841
        """
842
        view = self.view()
843
        if zoom:
844
            view.scale(1.5, 1.5)
845
        else:
846
            view.resetTransform()
847
848
    def alignToGrid(self):
849
        """
850
        Align nodes to a grid.
851
        """
852
        # TODO: The the current layout implementation is BAD (fix is urgent).
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
853
        tile_size = 150
854
        tiles = {}
855
856
        nodes = sorted(self.scheme().nodes, key=attrgetter("position"))
857
858
        if nodes:
859
            self.__undoStack.beginMacro(self.tr("Align To Grid"))
860
861
            for node in nodes:
862
                x, y = node.position
863
                x = int(round(float(x) / tile_size) * tile_size)
864
                y = int(round(float(y) / tile_size) * tile_size)
865
                while (x, y) in tiles:
866
                    x += tile_size
867
868
                self.__undoStack.push(
869
                    commands.MoveNodeCommand(self.scheme(), node,
870
                                             node.position, (x, y))
871
                )
872
873
                tiles[x, y] = node
874
                self.__scene.item_for_node(node).setPos(x, y)
875
876
            self.__undoStack.endMacro()
877
878
    def focusNode(self):
879
        """
880
        Return the current focused :class:`.SchemeNode` or ``None`` if no
881
        node has focus.
882
883
        """
884
        focus = self.__scene.focusItem()
885
        node = None
886
        if isinstance(focus, items.NodeItem):
887
            try:
888
                node = self.__scene.node_for_item(focus)
889
            except KeyError:
890
                # in case the node has been removed but the scene was not
891
                # yet fully updated.
892
                node = None
893
        return node
894
895
    def selectedNodes(self):
896
        """
897
        Return all selected :class:`.SchemeNode` items.
898
        """
899
        return list(map(self.scene().node_for_item,
900
                        self.scene().selected_node_items()))
901
902
    def selectedAnnotations(self):
903
        """
904
        Return all selected :class:`.BaseSchemeAnnotation` items.
905
        """
906
        return list(map(self.scene().annotation_for_item,
907
                        self.scene().selected_annotation_items()))
908
909
    def openSelected(self):
910
        """
911
        Open (show and raise) all widgets for the current selected nodes.
912
        """
913
        selected = self.scene().selected_node_items()
914
        for item in selected:
915
            self.__onNodeActivate(item)
916
917
    def editNodeTitle(self, node):
918
        """
919
        Edit (rename) the `node`'s title. Opens an input dialog.
920
        """
921
        name, ok = QInputDialog.getText(
922
                    self, self.tr("Rename"),
923
                    str(self.tr("Enter a new name for the '%s' widget")) \
924
                    % node.title,
925
                    text=node.title
926
                    )
927
928
        if ok:
929
            self.__undoStack.push(
930
                commands.RenameNodeCommand(self.__scheme, node, node.title,
931
                                           str(name))
932
            )
933
934
    def __onCleanChanged(self, clean):
935
        if self.isWindowModified() != (not clean):
936
            self.setWindowModified(not clean)
937
            self.modificationChanged.emit(not clean)
938
939
    def changeEvent(self, event):
940
        if event.type() == QEvent.FontChange:
941
            self.__updateFont()
942
943
        QWidget.changeEvent(self, event)
944
945
    def eventFilter(self, obj, event):
946
        # Filter the scene's drag/drop events.
947
        if obj is self.scene():
948
            etype = event.type()
949
            if  etype == QEvent.GraphicsSceneDragEnter or \
950
                    etype == QEvent.GraphicsSceneDragMove:
951
                mime_data = event.mimeData()
952
                if mime_data.hasFormat(
953
                        "application/vnv.orange-canvas.registry.qualified-name"
954
                        ):
955
                    event.acceptProposedAction()
956
                else:
957
                    event.ignore()
958
                return True
959
            elif etype == QEvent.GraphicsSceneDrop:
960
                data = event.mimeData()
961
                qname = data.data(
962
                    "application/vnv.orange-canvas.registry.qualified-name"
963
                )
964
                try:
965
                    desc = self.__registry.widget(bytes(qname).decode())
966
                except KeyError:
967
                    log.error("Unknown qualified name '%s'", qname)
968
                else:
969
                    pos = event.scenePos()
970
                    self.createNewNode(desc, position=(pos.x(), pos.y()))
971
                return True
972
973
            elif etype == QEvent.GraphicsSceneMousePress:
974
                return self.sceneMousePressEvent(event)
975
            elif etype == QEvent.GraphicsSceneMouseMove:
976
                return self.sceneMouseMoveEvent(event)
977
            elif etype == QEvent.GraphicsSceneMouseRelease:
978
                return self.sceneMouseReleaseEvent(event)
979
            elif etype == QEvent.GraphicsSceneMouseDoubleClick:
980
                return self.sceneMouseDoubleClickEvent(event)
981
            elif etype == QEvent.KeyPress:
982
                return self.sceneKeyPressEvent(event)
983
            elif etype == QEvent.KeyRelease:
984
                return self.sceneKeyReleaseEvent(event)
985
            elif etype == QEvent.GraphicsSceneContextMenu:
986
                return self.sceneContextMenuEvent(event)
987
988
        elif obj is self.__scheme:
989
            if event.type() == QEvent.WhatsThisClicked:
990
                # Re post the event
991
                self.__showHelpFor(event.href())
992
993
            elif event.type() == \
994
                    widgetsscheme.ActivateParentEvent.ActivateParent:
995
                self.window().activateWindow()
996
                self.window().raise_()
997
998
        return QWidget.eventFilter(self, obj, event)
999
1000
    def sceneMousePressEvent(self, event):
1001
        scene = self.__scene
1002
        if scene.user_interaction_handler:
1003
            return False
1004
1005
        pos = event.scenePos()
1006
1007
        anchor_item = scene.item_at(pos, items.NodeAnchorItem,
1008
                                    buttons=Qt.LeftButton)
1009
        if anchor_item and event.button() == Qt.LeftButton:
1010
            # Start a new link starting at item
1011
            scene.clearSelection()
1012
            handler = interactions.NewLinkAction(self)
1013
            self._setUserInteractionHandler(handler)
1014
            return handler.mousePressEvent(event)
1015
1016
        any_item = scene.item_at(pos)
1017
        if not any_item:
1018
            self.__emptyClickButtons |= event.button()
1019
1020
        if not any_item and event.button() == Qt.LeftButton:
1021
            # Create a RectangleSelectionAction but do not set in on the scene
1022
            # just yet (instead wait for the mouse move event).
1023
            handler = interactions.RectangleSelectionAction(self)
1024
            rval = handler.mousePressEvent(event)
1025
            if rval == True:
1026
                self.__possibleSelectionHandler = handler
1027
            return rval
1028
1029
        if any_item and event.button() == Qt.LeftButton:
1030
            self.__possibleMouseItemsMove = True
1031
            self.__itemsMoving.clear()
1032
            self.__scene.node_item_position_changed.connect(
1033
                self.__onNodePositionChanged
1034
            )
1035
            self.__annotationGeomChanged.mapped[QObject].connect(
1036
                self.__onAnnotationGeometryChanged
1037
            )
1038
1039
            set_enabled_all(self.__disruptiveActions, False)
1040
1041
        return False
1042
1043
    def sceneMouseMoveEvent(self, event):
1044
        scene = self.__scene
1045
        if scene.user_interaction_handler:
1046
            return False
1047
1048
        if self.__emptyClickButtons & Qt.LeftButton and \
1049
                event.buttons() & Qt.LeftButton and \
1050
                self.__possibleSelectionHandler:
1051
            # Set the RectangleSelection (initialized in mousePressEvent)
1052
            # on the scene
1053
            handler = self.__possibleSelectionHandler
1054
            self._setUserInteractionHandler(handler)
1055
            self.__possibleSelectionHandler = None
1056
            return handler.mouseMoveEvent(event)
1057
1058
        return False
1059
1060
    def sceneMouseReleaseEvent(self, event):
1061
        scene = self.__scene
1062
        if scene.user_interaction_handler:
1063
            return False
1064
1065
        if event.button() == Qt.LeftButton and self.__possibleMouseItemsMove:
1066
            self.__possibleMouseItemsMove = False
1067
            self.__scene.node_item_position_changed.disconnect(
1068
                self.__onNodePositionChanged
1069
            )
1070
            self.__annotationGeomChanged.mapped[QObject].disconnect(
1071
                self.__onAnnotationGeometryChanged
1072
            )
1073
1074
            set_enabled_all(self.__disruptiveActions, True)
1075
1076
            if self.__itemsMoving:
1077
                self.__scene.mouseReleaseEvent(event)
1078
                stack = self.undoStack()
1079
                stack.beginMacro(self.tr("Move"))
1080
                for scheme_item, (old, new) in self.__itemsMoving.items():
1081
                    if isinstance(scheme_item, SchemeNode):
1082
                        command = commands.MoveNodeCommand(
1083
                            self.scheme(), scheme_item, old, new
1084
                        )
1085
                    elif isinstance(scheme_item, BaseSchemeAnnotation):
1086
                        command = commands.AnnotationGeometryChange(
1087
                            self.scheme(), scheme_item, old, new
1088
                        )
1089
                    else:
1090
                        continue
1091
1092
                    stack.push(command)
1093
                stack.endMacro()
1094
1095
                self.__itemsMoving.clear()
1096
                return True
1097
        elif event.button() == Qt.LeftButton:
1098
            self.__possibleSelectionHandler = None
1099
1100
        return False
1101
1102
    def sceneMouseDoubleClickEvent(self, event):
1103
        scene = self.__scene
1104
        if scene.user_interaction_handler:
1105
            return False
1106
1107
        item = scene.item_at(event.scenePos())
1108
        if not item and self.__quickMenuTriggers & \
1109
                SchemeEditWidget.DoubleClicked:
1110
            # Double click on an empty spot
1111
            # Create a new node using QuickMenu
1112
            action = interactions.NewNodeAction(self)
1113
1114
            with disabled(self.__undoAction), disabled(self.__redoAction):
1115
                action.create_new(event.screenPos())
1116
1117
            event.accept()
1118
            return True
1119
1120
        item = scene.item_at(event.scenePos(), items.LinkItem,
1121
                             buttons=Qt.LeftButton)
1122
1123
        if item is not None and event.button() == Qt.LeftButton:
1124
            link = self.scene().link_for_item(item)
1125
            action = interactions.EditNodeLinksAction(self, link.source_node,
1126
                                                      link.sink_node)
1127
            action.edit_links()
1128
            event.accept()
1129
            return True
1130
1131
        return False
1132
1133
    def sceneKeyPressEvent(self, event):
1134
        scene = self.__scene
1135
        if scene.user_interaction_handler:
1136
            return False
1137
1138
        # If a QGraphicsItem is in text editing mode, don't interrupt it
1139
        focusItem = scene.focusItem()
1140
        if focusItem and isinstance(focusItem, QGraphicsTextItem) and \
1141
                focusItem.textInteractionFlags() & Qt.TextEditable:
1142
            return False
1143
1144
        # If the mouse is not over out view
1145
        if not self.view().underMouse():
1146
            return False
1147
1148
        handler = None
1149
        searchText = ""
1150
        if (event.key() == Qt.Key_Space and \
0 ignored issues
show
Unused Code Coding Style introduced by
There is an unnecessary parenthesis after if.
Loading history...
1151
                self.__quickMenuTriggers & SchemeEditWidget.SpaceKey):
1152
            handler = interactions.NewNodeAction(self)
1153
1154
        elif len(event.text()) and \
1155
                self.__quickMenuTriggers & SchemeEditWidget.AnyKey and \
1156
                is_printable(str(event.text())[0]):
1157
            handler = interactions.NewNodeAction(self)
1158
            searchText = str(event.text())
1159
1160
            # TODO: set the search text to event.text() and set focus on the
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
1161
            # search line
1162
1163
        if handler is not None:
1164
            # Control + Backspace (remove widget action on Mac OSX) conflicts
1165
            # with the 'Clear text' action in the search widget (there might
1166
            # be selected items in the canvas), so we disable the
1167
            # remove widget action so the text editing follows standard
1168
            # 'look and feel'
1169
            with disabled(self.__removeSelectedAction), \
1170
                 disabled(self.__undoAction), \
1171
                 disabled(self.__redoAction):
1172
                handler.create_new(QCursor.pos(), searchText)
1173
1174
            event.accept()
1175
            return True
1176
1177
        return False
1178
1179
    def sceneKeyReleaseEvent(self, event):
0 ignored issues
show
Unused Code introduced by
The argument event seems to be unused.
Loading history...
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...
1180
        return False
1181
1182
    def sceneContextMenuEvent(self, event):
0 ignored issues
show
Unused Code introduced by
The argument event seems to be unused.
Loading history...
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...
1183
        return False
1184
1185
    def _setUserInteractionHandler(self, handler):
1186
        """
1187
        Helper method for setting the user interaction handlers.
1188
        """
1189
        if self.__scene.user_interaction_handler:
1190
            if isinstance(self.__scene.user_interaction_handler,
1191
                          (interactions.ResizeArrowAnnotation,
1192
                           interactions.ResizeTextAnnotation)):
1193
                self.__scene.user_interaction_handler.commit()
1194
1195
            self.__scene.user_interaction_handler.ended.disconnect(
1196
                self.__onInteractionEnded
1197
            )
1198
1199
        if handler:
1200
            handler.ended.connect(self.__onInteractionEnded)
1201
            # Disable actions which could change the model
1202
            set_enabled_all(self.__disruptiveActions, False)
1203
1204
        self.__scene.set_user_interaction_handler(handler)
1205
1206
    def __onInteractionEnded(self):
1207
        self.sender().ended.disconnect(self.__onInteractionEnded)
1208
        set_enabled_all(self.__disruptiveActions, True)
1209
1210
    def __onSelectionChanged(self):
1211
        nodes = self.selectedNodes()
1212
        annotations = self.selectedAnnotations()
1213
1214
        self.__openSelectedAction.setEnabled(bool(nodes))
1215
        self.__removeSelectedAction.setEnabled(
1216
            bool(nodes) or bool(annotations)
1217
        )
1218
1219
        self.__helpAction.setEnabled(len(nodes) == 1)
1220
        self.__renameAction.setEnabled(len(nodes) == 1)
1221
        self.__duplicateSelectedAction.setEnabled(bool(nodes))
1222
1223
        if len(nodes) > 1:
1224
            self.__openSelectedAction.setText(self.tr("Open All"))
1225
        else:
1226
            self.__openSelectedAction.setText(self.tr("Open"))
1227
1228
        if len(nodes) + len(annotations) > 1:
1229
            self.__removeSelectedAction.setText(self.tr("Remove All"))
1230
        else:
1231
            self.__removeSelectedAction.setText(self.tr("Remove"))
1232
1233
        if len(nodes) == 0:
1234
            self.__openSelectedAction.setText(self.tr("Open"))
1235
            self.__removeSelectedAction.setText(self.tr("Remove"))
1236
1237
        focus = self.focusNode()
1238
        if focus is not None:
1239
            desc = focus.description
1240
            tip = whats_this_helper(desc, include_more_link=True)
1241
        else:
1242
            tip = ""
1243
1244
        if tip != self.__quickTip:
1245
            self.__quickTip = tip
1246
            ev = QuickHelpTipEvent("", self.__quickTip,
1247
                                   priority=QuickHelpTipEvent.Permanent)
1248
1249
            QCoreApplication.sendEvent(self, ev)
1250
1251
    def __onNodeActivate(self, item):
1252
        node = self.__scene.node_for_item(item)
1253
        widget = self.scheme().widget_for_node(node)
1254
        widget.show()
1255
        widget.raise_()
1256
        widget.activateWindow()
1257
1258
    def __onNodePositionChanged(self, item, pos):
1259
        node = self.__scene.node_for_item(item)
1260
        new = (pos.x(), pos.y())
1261
        if node not in self.__itemsMoving:
1262
            self.__itemsMoving[node] = (node.position, new)
1263
        else:
1264
            old, _ = self.__itemsMoving[node]
1265
            self.__itemsMoving[node] = (old, new)
1266
1267
    def __onAnnotationGeometryChanged(self, item):
1268
        annot = self.scene().annotation_for_item(item)
1269
        if annot not in self.__itemsMoving:
1270
            self.__itemsMoving[annot] = (annot.geometry,
1271
                                         geometry_from_annotation_item(item))
1272
        else:
1273
            old, _ = self.__itemsMoving[annot]
1274
            self.__itemsMoving[annot] = (old,
1275
                                         geometry_from_annotation_item(item))
1276
1277
    def __onAnnotationAdded(self, item):
1278
        log.debug("Annotation added (%r)", item)
1279
        item.setFlag(QGraphicsItem.ItemIsSelectable)
1280
        item.setFlag(QGraphicsItem.ItemIsMovable)
1281
        item.setFlag(QGraphicsItem.ItemIsFocusable)
1282
1283
        item.installSceneEventFilter(self.__focusListener)
1284
1285
        if isinstance(item, items.ArrowAnnotation):
1286
            pass
1287
        elif isinstance(item, items.TextAnnotation):
1288
            # Make the annotation editable.
1289
            item.setTextInteractionFlags(Qt.TextEditorInteraction)
1290
1291
            self.__editFinishedMapper.setMapping(item, item)
1292
            item.editingFinished.connect(
1293
                self.__editFinishedMapper.map
1294
            )
1295
1296
        self.__annotationGeomChanged.setMapping(item, item)
1297
        item.geometryChanged.connect(
1298
            self.__annotationGeomChanged.map
1299
        )
1300
1301
    def __onAnnotationRemoved(self, item):
1302
        log.debug("Annotation removed (%r)", item)
1303
        if isinstance(item, items.ArrowAnnotation):
1304
            pass
1305
        elif isinstance(item, items.TextAnnotation):
1306
            item.editingFinished.disconnect(
1307
                self.__editFinishedMapper.map
1308
            )
1309
1310
        item.removeSceneEventFilter(self.__focusListener)
1311
1312
        self.__annotationGeomChanged.removeMappings(item)
1313
        item.geometryChanged.disconnect(
1314
            self.__annotationGeomChanged.map
1315
        )
1316
1317
    def __onItemFocusedIn(self, item):
1318
        """
1319
        Annotation item has gained focus.
1320
        """
1321
        if not self.__scene.user_interaction_handler:
1322
            self.__startControlPointEdit(item)
1323
1324
    def __onItemFocusedOut(self, item):
0 ignored issues
show
Unused Code introduced by
The argument item seems to be unused.
Loading history...
1325
        """
1326
        Annotation item lost focus.
1327
        """
1328
        self.__endControlPointEdit()
1329
1330
    def __onEditingFinished(self, item):
1331
        """
1332
        Text annotation editing has finished.
1333
        """
1334
        annot = self.__scene.annotation_for_item(item)
1335
        text = str(item.toPlainText())
1336
        if annot.text != text:
1337
            self.__undoStack.push(
1338
                commands.TextChangeCommand(self.scheme(), annot,
1339
                                           annot.text, text)
1340
            )
1341
1342
    def __toggleNewArrowAnnotation(self, checked):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1343
        if self.__newTextAnnotationAction.isChecked():
1344
            # Uncheck the text annotation action if needed.
1345
            self.__newTextAnnotationAction.setChecked(not checked)
1346
1347
        action = self.__newArrowAnnotationAction
1348
1349
        if not checked:
1350
            # The action was unchecked (canceled by the user)
1351
            handler = self.__scene.user_interaction_handler
1352
            if isinstance(handler, interactions.NewArrowAnnotation):
1353
                # Cancel the interaction and restore the state
1354
                handler.ended.disconnect(action.toggle)
1355
                handler.cancel(interactions.UserInteraction.UserCancelReason)
1356
                log.info("Canceled new arrow annotation")
1357
1358
        else:
1359
            handler = interactions.NewArrowAnnotation(self)
1360
            checked = self.__arrowColorActionGroup.checkedAction()
1361
            handler.setColor(checked.data())
1362
1363
            handler.ended.connect(action.toggle)
1364
1365
            self._setUserInteractionHandler(handler)
1366
1367
    def __onFontSizeTriggered(self, action):
1368
        if not self.__newTextAnnotationAction.isChecked():
1369
            # When selecting from the (font size) menu the 'Text'
1370
            # action does not get triggered automatically.
1371
            self.__newTextAnnotationAction.trigger()
1372
        else:
1373
            # Update the preferred font on the interaction handler.
1374
            handler = self.__scene.user_interaction_handler
1375
            if isinstance(handler, interactions.NewTextAnnotation):
1376
                handler.setFont(action.font())
1377
1378
    def __toggleNewTextAnnotation(self, checked):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1379
        if self.__newArrowAnnotationAction.isChecked():
1380
            # Uncheck the arrow annotation if needed.
1381
            self.__newArrowAnnotationAction.setChecked(not checked)
1382
1383
        action = self.__newTextAnnotationAction
1384
1385
        if not checked:
1386
            # The action was unchecked (canceled by the user)
1387
            handler = self.__scene.user_interaction_handler
1388
            if isinstance(handler, interactions.NewTextAnnotation):
1389
                # cancel the interaction and restore the state
1390
                handler.ended.disconnect(action.toggle)
1391
                handler.cancel(interactions.UserInteraction.UserCancelReason)
1392
                log.info("Canceled new text annotation")
1393
1394
        else:
1395
            handler = interactions.NewTextAnnotation(self)
1396
            checked = self.__fontActionGroup.checkedAction()
1397
            handler.setFont(checked.font())
1398
1399
            handler.ended.connect(action.toggle)
1400
1401
            self._setUserInteractionHandler(handler)
1402
1403
    def __onArrowColorTriggered(self, action):
1404
        if not self.__newArrowAnnotationAction.isChecked():
1405
            # When selecting from the (color) menu the 'Arrow'
1406
            # action does not get triggered automatically.
1407
            self.__newArrowAnnotationAction.trigger()
1408
        else:
1409
            # Update the preferred color on the interaction handler
1410
            handler = self.__scene.user_interaction_handler
1411
            if isinstance(handler, interactions.NewArrowAnnotation):
1412
                handler.setColor(action.data())
1413
1414
    def __onCustomContextMenuRequested(self, pos):
1415
        scenePos = self.view().mapToScene(pos)
1416
        globalPos = self.view().mapToGlobal(pos)
1417
1418
        item = self.scene().item_at(scenePos, items.NodeItem)
1419
        if item is not None:
1420
            self.__widgetMenu.popup(globalPos)
1421
            return
1422
1423
        item = self.scene().item_at(scenePos, items.LinkItem,
1424
                                    buttons=Qt.RightButton)
1425
        if item is not None:
1426
            link = self.scene().link_for_item(item)
1427
            self.__linkEnableAction.setChecked(link.enabled)
1428
            self.__contextMenuTarget = link
1429
            self.__linkMenu.popup(globalPos)
1430
            return
1431
1432
        item = self.scene().item_at(scenePos)
1433
        if not item and \
1434
                self.__quickMenuTriggers & SchemeEditWidget.RightClicked:
1435
            action = interactions.NewNodeAction(self)
1436
1437
            with disabled(self.__undoAction), disabled(self.__redoAction):
1438
                action.create_new(globalPos)
1439
            return
1440
1441
    def __onRenameAction(self):
1442
        """
1443
        Rename was requested for the selected widget.
1444
        """
1445
        selected = self.selectedNodes()
1446
        if len(selected) == 1:
1447
            self.editNodeTitle(selected[0])
1448
1449
    def __onHelpAction(self):
1450
        """
1451
        Help was requested for the selected widget.
1452
        """
1453
        nodes = self.selectedNodes()
1454
        help_url = None
1455
        if len(nodes) == 1:
1456
            node = nodes[0]
1457
            desc = node.description
1458
1459
            help_url = "help://search?" + urlencode({"id": desc.id})
1460
            self.__showHelpFor(help_url)
1461
1462
    def __showHelpFor(self, help_url):
1463
        """
1464
        Show help for an "help" url.
1465
        """
1466
        # Notify the parent chain and let them respond
1467
        ev = QWhatsThisClickedEvent(help_url)
1468
        handled = QCoreApplication.sendEvent(self, ev)
1469
1470
        if not handled:
1471
            message_information(
1472
                self.tr("There is no documentation for this widget yet."),
1473
                parent=self)
1474
1475
    def __toggleLinkEnabled(self, enabled):
1476
        """
1477
        Link 'enabled' state was toggled in the context menu.
1478
        """
1479
        if self.__contextMenuTarget:
1480
            link = self.__contextMenuTarget
1481
            command = commands.SetAttrCommand(
1482
                link, "enabled", enabled, name=self.tr("Set enabled"),
1483
            )
1484
            self.__undoStack.push(command)
1485
1486
    def __linkRemove(self):
1487
        """
1488
        Remove link was requested from the context menu.
1489
        """
1490
        if self.__contextMenuTarget:
1491
            self.removeLink(self.__contextMenuTarget)
1492
1493
    def __linkReset(self):
1494
        """
1495
        Link reset from the context menu was requested.
1496
        """
1497
        if self.__contextMenuTarget:
1498
            link = self.__contextMenuTarget
1499
            action = interactions.EditNodeLinksAction(
1500
                self, link.source_node, link.sink_node
1501
            )
1502
            action.edit_links()
1503
1504
    def __duplicateSelected(self):
1505
        """
1506
        Duplicate currently selected nodes.
1507
        """
1508
        def copy_node(node):
1509
            x, y = node.position
1510
            return SchemeNode(
1511
                node.description, node.title, position=(x + 20, y + 20),
1512
                properties=copy.deepcopy(node.properties))
1513
1514
        def copy_link(link, source=None, sink=None):
1515
            source = link.source_node if source is None else source
1516
            sink = link.sink_node if sink is None else sink
1517
            return SchemeLink(
1518
                source, link.source_channel,
1519
                sink, link.sink_channel,
1520
                enabled=link.enabled,
1521
                properties=copy.deepcopy(link.properties))
1522
1523
        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 34).

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...
1524
        # ensure up to date node properties (settings)
1525
        scheme.sync_node_properties()
1526
1527
        selection = self.selectedNodes()
1528
1529
        links = [link for link in scheme.links
1530
                 if link.source_node in selection and
1531
                    link.sink_node in selection]
1532
        nodedups = [copy_node(node) for node in selection]
1533
        allnames = {node.title for node in scheme.nodes + nodedups}
1534
        for nodedup in nodedups:
1535
            nodedup.title = uniquify(
1536
                nodedup.title, allnames, pattern="{item} ({_})", start=1)
1537
1538
        node_to_dup = dict(zip(selection, nodedups))
1539
1540
        linkdups = [copy_link(link, source=node_to_dup[link.source_node],
1541
                              sink=node_to_dup[link.sink_node])
1542
                    for link in links]
1543
1544
        command = QUndoCommand(self.tr("Duplicate"))
1545
        macrocommands = []
1546
        for nodedup in nodedups:
1547
            macrocommands.append(
1548
                commands.AddNodeCommand(scheme, nodedup, parent=command))
1549
        for linkdup in linkdups:
1550
            macrocommands.append(
1551
                commands.AddLinkCommand(scheme, linkdup, parent=command))
1552
1553
        self.__undoStack.push(command)
1554
        scene = self.__scene
1555
1556
        for node in selection:
1557
            item = scene.item_for_node(node)
1558
            item.setSelected(False)
1559
1560
        for node in nodedups:
1561
            item = scene.item_for_node(node)
1562
            item.setSelected(True)
1563
1564
    def __startControlPointEdit(self, item):
1565
        """
1566
        Start a control point edit interaction for `item`.
1567
        """
1568
        if isinstance(item, items.ArrowAnnotation):
1569
            handler = interactions.ResizeArrowAnnotation(self)
1570
        elif isinstance(item, items.TextAnnotation):
1571
            handler = interactions.ResizeTextAnnotation(self)
1572
        else:
1573
            log.warning("Unknown annotation item type %r" % item)
0 ignored issues
show
Coding Style Best Practice introduced by
Specify string format arguments as logging function parameters
Loading history...
1574
            return
1575
1576
        handler.editItem(item)
1577
        self._setUserInteractionHandler(handler)
1578
1579
        log.info("Control point editing started (%r)." % item)
0 ignored issues
show
Coding Style Best Practice introduced by
Specify string format arguments as logging function parameters
Loading history...
1580
1581
    def __endControlPointEdit(self):
1582
        """
1583
        End the current control point edit interaction.
1584
        """
1585
        handler = self.__scene.user_interaction_handler
1586
        if isinstance(handler, (interactions.ResizeArrowAnnotation,
1587
                                interactions.ResizeTextAnnotation)) and \
1588
                not handler.isFinished() and not handler.isCanceled():
1589
            handler.commit()
1590
            handler.end()
1591
1592
            log.info("Control point editing finished.")
1593
1594
    def __updateFont(self):
1595
        """
1596
        Update the font for the "Text size' menu and the default font
1597
        used in the `CanvasScene`.
1598
1599
        """
1600
        actions = self.__fontActionGroup.actions()
1601
        font = self.font()
1602
        for action in actions:
1603
            size = action.font().pixelSize()
1604
            action_font = QFont(font)
1605
            action_font.setPixelSize(size)
1606
            action.setFont(action_font)
1607
1608
        if self.__scene:
1609
            self.__scene.setFont(font)
1610
1611
    def __signalManagerStateChanged(self, state):
1612
        if state == signalmanager.SignalManager.Running:
1613
            self.__view.setBackgroundBrush(QBrush(Qt.NoBrush))
1614
#            self.__view.setBackgroundIcon(QIcon())
1615
        elif state == signalmanager.SignalManager.Paused:
1616
            self.__view.setBackgroundBrush(QBrush(QColor(235, 235, 235)))
1617
#            self.__view.setBackgroundIcon(QIcon("canvas_icons:Pause.svg"))
1618
1619
1620
def geometry_from_annotation_item(item):
1621
    if isinstance(item, items.ArrowAnnotation):
1622
        line = item.line()
1623
        p1 = item.mapToScene(line.p1())
1624
        p2 = item.mapToScene(line.p2())
1625
        return ((p1.x(), p1.y()), (p2.x(), p2.y()))
1626
    elif isinstance(item, items.TextAnnotation):
1627
        geom = item.geometry()
1628
        return (geom.x(), geom.y(), geom.width(), geom.height())
1629
1630
1631
def mouse_drag_distance(event, button=Qt.LeftButton):
1632
    """
1633
    Return the (manhattan) distance between the mouse position
1634
    when the `button` was pressed and the current mouse position.
1635
1636
    """
1637
    diff = (event.buttonDownScreenPos(button) - event.screenPos())
1638
    return diff.manhattanLength()
1639
1640
1641
def set_enabled_all(objects, enable):
1642
    """
1643
    Set `enabled` properties on all objects (objects with `setEnabled` method).
1644
    """
1645
    for obj in objects:
1646
        obj.setEnabled(enable)
1647
1648
1649
# All control character categories.
1650
_control = set(["Cc", "Cf", "Cs", "Co", "Cn"])
1651
1652
1653
def is_printable(unichar):
1654
    """
1655
    Return True if the unicode character `unichar` is a printable character.
1656
    """
1657
    return unicodedata.category(unichar) not in _control
1658
1659
1660
def node_properties(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 34).

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...
1661
    scheme.sync_node_properties()
1662
    return [dict(node.properties) for node in scheme.nodes]
1663
1664
1665
def uniquify(item, names, pattern="{item}-{_}", start=0):
1666
    candidates = (pattern.format(item=item, _=i)
1667
                  for i in itertools.count(start))
1668
    candidates = itertools.dropwhile(lambda item: item in names, candidates)
1669
    return next(candidates)
1670