|
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__.pyfiles in your module folders. Make sure that you place one file in each sub-folder.