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 ( |
|
|
|
|
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 ( |
|
|
|
|
26
|
|
|
Qt, QObject, QEvent, QSignalMapper, QRectF, QCoreApplication |
27
|
|
|
) |
28
|
|
|
|
29
|
|
|
from PyQt4.QtCore import pyqtProperty as Property, pyqtSignal as Signal |
|
|
|
|
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 ( |
|
|
|
|
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? |
|
|
|
|
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): |
|
|
|
|
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() |
|
|
|
|
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): |
|
|
|
|
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() |
|
|
|
|
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). |
|
|
|
|
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 \ |
|
|
|
|
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 |
|
|
|
|
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): |
|
|
|
|
1180
|
|
|
return False |
1181
|
|
|
|
1182
|
|
|
def sceneContextMenuEvent(self, event): |
|
|
|
|
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): |
|
|
|
|
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): |
|
|
|
|
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): |
|
|
|
|
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() |
|
|
|
|
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) |
|
|
|
|
1574
|
|
|
return |
1575
|
|
|
|
1576
|
|
|
handler.editItem(item) |
1577
|
|
|
self._setUserInteractionHandler(handler) |
1578
|
|
|
|
1579
|
|
|
log.info("Control point editing started (%r)." % item) |
|
|
|
|
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): |
|
|
|
|
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
|
|
|
|
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.
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.