nodeeditor-qtpy 0.8.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nodeeditor/__init__.py +93 -0
- nodeeditor/node_colors_config.py +343 -0
- nodeeditor/node_content_widget.py +129 -0
- nodeeditor/node_decorators.py +65 -0
- nodeeditor/node_edge.py +518 -0
- nodeeditor/node_edge_dragging.py +185 -0
- nodeeditor/node_edge_intersect.py +219 -0
- nodeeditor/node_edge_rerouting.py +216 -0
- nodeeditor/node_edge_snapping.py +95 -0
- nodeeditor/node_edge_validators.py +82 -0
- nodeeditor/node_editor_widget.py +257 -0
- nodeeditor/node_editor_window.py +448 -0
- nodeeditor/node_graph.py +583 -0
- nodeeditor/node_graphics_cutline.py +57 -0
- nodeeditor/node_graphics_edge.py +375 -0
- nodeeditor/node_graphics_edge_path.py +226 -0
- nodeeditor/node_graphics_node.py +693 -0
- nodeeditor/node_graphics_scene.py +369 -0
- nodeeditor/node_graphics_socket.py +262 -0
- nodeeditor/node_graphics_view.py +907 -0
- nodeeditor/node_group.py +1286 -0
- nodeeditor/node_group_node.py +5 -0
- nodeeditor/node_icon_content_widget.py +176 -0
- nodeeditor/node_icon_graphics_node.py +524 -0
- nodeeditor/node_multi_input_node.py +450 -0
- nodeeditor/node_node.py +848 -0
- nodeeditor/node_scene.py +1106 -0
- nodeeditor/node_scene_clipboard.py +314 -0
- nodeeditor/node_scene_history.py +556 -0
- nodeeditor/node_serializable.py +43 -0
- nodeeditor/node_socket.py +445 -0
- nodeeditor/node_socket_shapes.py +261 -0
- nodeeditor/py.typed +0 -0
- nodeeditor/qss/nodestyle.qss +3 -0
- nodeeditor/utils.py +108 -0
- nodeeditor/utils_no_qt.py +21 -0
- nodeeditor_qtpy-0.8.1.dist-info/METADATA +165 -0
- nodeeditor_qtpy-0.8.1.dist-info/RECORD +40 -0
- nodeeditor_qtpy-0.8.1.dist-info/WHEEL +4 -0
- nodeeditor_qtpy-0.8.1.dist-info/licenses/LICENCE +23 -0
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
A module containing all code for working with History (Undo/Redo)
|
|
4
|
+
"""
|
|
5
|
+
from nodeeditor.utils import dumpException
|
|
6
|
+
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from typing import (
|
|
9
|
+
TYPE_CHECKING,
|
|
10
|
+
List,
|
|
11
|
+
Optional,
|
|
12
|
+
Tuple,
|
|
13
|
+
Any,
|
|
14
|
+
Callable,
|
|
15
|
+
Iterator,
|
|
16
|
+
TypedDict,
|
|
17
|
+
Union,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from nodeeditor.node_graphics_view import QDMGraphicsView
|
|
22
|
+
from nodeeditor.node_socket import Socket
|
|
23
|
+
from nodeeditor.node_scene import Scene
|
|
24
|
+
|
|
25
|
+
DEBUG = False
|
|
26
|
+
DEBUG_SELECTION = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SelectionDict(TypedDict, total=False):
|
|
30
|
+
"""Define the TypedDict for selection objects"""
|
|
31
|
+
nodes: List[str] # list of node IDs
|
|
32
|
+
edges: List[str] # list of edge IDs
|
|
33
|
+
groups: List[str] # list of group IDs
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SceneHistory():
|
|
37
|
+
"""Class contains all the code for undo/redo operations
|
|
38
|
+
|
|
39
|
+
Two guards control re-entrancy:
|
|
40
|
+
|
|
41
|
+
- **restore depth** - incremented by :meth:`restoring`. Any history store
|
|
42
|
+
attempted while the depth is non-zero is ignored, so restoring a stamp
|
|
43
|
+
can never push a new entry onto the stack.
|
|
44
|
+
- **merge key** - an optional grouping token supplied to
|
|
45
|
+
:meth:`storeHistory`. Consecutive stores sharing a key are coalesced
|
|
46
|
+
into a single entry, which is how bursts of keystrokes collapse into
|
|
47
|
+
one undo step.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
scene: 'Scene',
|
|
53
|
+
history_limit: Optional[int] = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""
|
|
56
|
+
:param scene: Reference to the :class:`~nodeeditor.node_scene.Scene`
|
|
57
|
+
:type scene: :class:`~nodeeditor.node_scene.Scene`
|
|
58
|
+
:param history_limit: Maximum number of stamps to keep. ``None``
|
|
59
|
+
(the default) means unlimited.
|
|
60
|
+
:type history_limit: ``int`` or ``None``
|
|
61
|
+
|
|
62
|
+
:Instance Attributes:
|
|
63
|
+
|
|
64
|
+
- **scene** - reference to the :class:`~nodeeditor.node_scene.Scene`
|
|
65
|
+
- **history_limit** - number of history steps that can be stored
|
|
66
|
+
"""
|
|
67
|
+
self.scene = scene
|
|
68
|
+
|
|
69
|
+
self.clear()
|
|
70
|
+
self.history_limit = history_limit
|
|
71
|
+
|
|
72
|
+
self.undo_selection_has_changed = False
|
|
73
|
+
self.if_undo = False
|
|
74
|
+
self._restore_depth = 0
|
|
75
|
+
|
|
76
|
+
# listeners
|
|
77
|
+
self._history_modified_listeners: List[Callable[[], None]] = []
|
|
78
|
+
self._history_stored_listeners: List[Callable[[], None]] = []
|
|
79
|
+
self._history_restored_listeners: List[Callable[[], None]] = []
|
|
80
|
+
|
|
81
|
+
def clear(self) -> None:
|
|
82
|
+
"""Reset the history stack"""
|
|
83
|
+
self.history_stack = []
|
|
84
|
+
self.history_current_step = -1
|
|
85
|
+
|
|
86
|
+
def storeInitialHistoryStamp(self) -> None:
|
|
87
|
+
"""Helper function usually used when new or open file requested"""
|
|
88
|
+
self.storeHistory("Initial History Stamp")
|
|
89
|
+
|
|
90
|
+
# ------------------------------------------------------------------
|
|
91
|
+
# Re-entrancy guard
|
|
92
|
+
# ------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def is_restoring_history(self) -> bool:
|
|
96
|
+
"""``True`` while a history stamp is being restored.
|
|
97
|
+
|
|
98
|
+
Retained as a property for backward compatibility: legacy callers
|
|
99
|
+
assign to it directly instead of using :meth:`restoring`.
|
|
100
|
+
"""
|
|
101
|
+
return self._restore_depth > 0
|
|
102
|
+
|
|
103
|
+
@is_restoring_history.setter
|
|
104
|
+
def is_restoring_history(self, value: bool) -> None:
|
|
105
|
+
if value:
|
|
106
|
+
self._restore_depth = max(self._restore_depth, 1)
|
|
107
|
+
else:
|
|
108
|
+
self._restore_depth = 0
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def is_restoring(self) -> bool:
|
|
112
|
+
"""Alias of :attr:`is_restoring_history` for new call sites."""
|
|
113
|
+
return self._restore_depth > 0
|
|
114
|
+
|
|
115
|
+
@contextmanager
|
|
116
|
+
def restoring(self, is_undo: bool = False) -> Iterator['SceneHistory']:
|
|
117
|
+
"""Context manager guarding a history restore.
|
|
118
|
+
|
|
119
|
+
Nestable: the depth counter only reaches zero when the outermost
|
|
120
|
+
block exits, so an inner callback cannot re-enable history storage
|
|
121
|
+
for the remainder of an outer restore.
|
|
122
|
+
|
|
123
|
+
:param is_undo: ``True`` when the restore is an undo, which is the
|
|
124
|
+
flag legacy ``history_stamp_callback`` implementations read to
|
|
125
|
+
decide which side of a diff to apply.
|
|
126
|
+
"""
|
|
127
|
+
self._restore_depth += 1
|
|
128
|
+
previous_if_undo = self.if_undo
|
|
129
|
+
self.if_undo = is_undo
|
|
130
|
+
try:
|
|
131
|
+
yield self
|
|
132
|
+
finally:
|
|
133
|
+
self.if_undo = previous_if_undo
|
|
134
|
+
self._restore_depth = max(0, self._restore_depth - 1)
|
|
135
|
+
|
|
136
|
+
# ------------------------------------------------------------------
|
|
137
|
+
# Listeners
|
|
138
|
+
# ------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
def addHistoryModifiedListener(self, callback: Callable[[], None]) -> None:
|
|
141
|
+
"""
|
|
142
|
+
Register callback for `HistoryModified` event
|
|
143
|
+
|
|
144
|
+
:param callback: callback function
|
|
145
|
+
"""
|
|
146
|
+
self._history_modified_listeners.append(callback)
|
|
147
|
+
|
|
148
|
+
def addHistoryStoredListener(self, callback: Callable[[], None]) -> None:
|
|
149
|
+
"""
|
|
150
|
+
Register callback for `HistoryStored` event
|
|
151
|
+
|
|
152
|
+
:param callback: callback function
|
|
153
|
+
"""
|
|
154
|
+
self._history_stored_listeners.append(callback)
|
|
155
|
+
|
|
156
|
+
def addHistoryRestoredListener(self, callback: Callable[[], None]) -> None:
|
|
157
|
+
"""
|
|
158
|
+
Register callback for `HistoryRestored` event
|
|
159
|
+
|
|
160
|
+
:param callback: callback function
|
|
161
|
+
"""
|
|
162
|
+
self._history_restored_listeners.append(callback)
|
|
163
|
+
|
|
164
|
+
def removeHistoryModifiedListener(self, callback: Callable[[], None]) -> None:
|
|
165
|
+
"""
|
|
166
|
+
Remove registered callback for `HistoryModified` event
|
|
167
|
+
|
|
168
|
+
:param callback: callback function
|
|
169
|
+
"""
|
|
170
|
+
if callback in self._history_modified_listeners:
|
|
171
|
+
self._history_modified_listeners.remove(callback)
|
|
172
|
+
|
|
173
|
+
def removeHistoryStoredListener(self, callback: Callable[[], None]) -> None:
|
|
174
|
+
"""
|
|
175
|
+
Remove registered callback for `HistoryStored` event
|
|
176
|
+
|
|
177
|
+
:param callback: callback function
|
|
178
|
+
"""
|
|
179
|
+
if callback in self._history_stored_listeners:
|
|
180
|
+
self._history_stored_listeners.remove(callback)
|
|
181
|
+
|
|
182
|
+
def removeHistoryRestoredListener(self, callback: Callable[[], None]) -> None:
|
|
183
|
+
"""
|
|
184
|
+
Remove registered callback for `HistoryRestored` event
|
|
185
|
+
|
|
186
|
+
:param callback: callback function
|
|
187
|
+
"""
|
|
188
|
+
if callback in self._history_restored_listeners:
|
|
189
|
+
self._history_restored_listeners.remove(callback)
|
|
190
|
+
|
|
191
|
+
# ------------------------------------------------------------------
|
|
192
|
+
# Stack state
|
|
193
|
+
# ------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
def canUndo(self) -> bool:
|
|
196
|
+
"""Return ``True`` if Undo is available for current `History Stack`
|
|
197
|
+
|
|
198
|
+
:rtype: ``bool``
|
|
199
|
+
"""
|
|
200
|
+
return self.history_current_step > 0
|
|
201
|
+
|
|
202
|
+
def canRedo(self) -> bool:
|
|
203
|
+
"""
|
|
204
|
+
Return ``True`` if Redo is available for current `History Stack`
|
|
205
|
+
|
|
206
|
+
:rtype: ``bool``
|
|
207
|
+
"""
|
|
208
|
+
return self.history_current_step + 1 < len(self.history_stack)
|
|
209
|
+
|
|
210
|
+
def currentText(self) -> str:
|
|
211
|
+
"""Description of the stamp the next undo would revert."""
|
|
212
|
+
if not self.canUndo():
|
|
213
|
+
return ""
|
|
214
|
+
return self.history_stack[self.history_current_step].get('desc', '')
|
|
215
|
+
|
|
216
|
+
def nextText(self) -> str:
|
|
217
|
+
"""Description of the stamp the next redo would reapply."""
|
|
218
|
+
if not self.canRedo():
|
|
219
|
+
return ""
|
|
220
|
+
return self.history_stack[self.history_current_step + 1].get('desc', '')
|
|
221
|
+
|
|
222
|
+
def undo(self) -> None:
|
|
223
|
+
"""Undo operation (restore previous snapshot)."""
|
|
224
|
+
if DEBUG:
|
|
225
|
+
print("UNDO")
|
|
226
|
+
|
|
227
|
+
if self.canUndo():
|
|
228
|
+
self.history_current_step -= 1
|
|
229
|
+
with self.restoring(is_undo=True):
|
|
230
|
+
self.restoreHistory()
|
|
231
|
+
self.scene.has_been_modified = True
|
|
232
|
+
|
|
233
|
+
def redo(self) -> None:
|
|
234
|
+
"""Redo operation"""
|
|
235
|
+
if DEBUG:
|
|
236
|
+
print("REDO")
|
|
237
|
+
if self.canRedo():
|
|
238
|
+
self.history_current_step += 1
|
|
239
|
+
with self.restoring(is_undo=False):
|
|
240
|
+
self.restoreHistory()
|
|
241
|
+
self.scene.has_been_modified = True
|
|
242
|
+
|
|
243
|
+
def restoreHistory(self) -> None:
|
|
244
|
+
"""
|
|
245
|
+
Restore `History Stamp` from `History stack`.
|
|
246
|
+
|
|
247
|
+
Triggers:
|
|
248
|
+
|
|
249
|
+
- `History Modified` event
|
|
250
|
+
- `History Restored` event
|
|
251
|
+
"""
|
|
252
|
+
if DEBUG:
|
|
253
|
+
print("Restoring history",
|
|
254
|
+
".... current_step: @%d" % self.history_current_step,
|
|
255
|
+
"(%d)" % len(self.history_stack))
|
|
256
|
+
|
|
257
|
+
history_stamp = self.history_stack[self.history_current_step]
|
|
258
|
+
self.restoreHistoryStamp(history_stamp)
|
|
259
|
+
self._dispatch_stamp_data(history_stamp)
|
|
260
|
+
|
|
261
|
+
for callback in self._history_modified_listeners:
|
|
262
|
+
callback()
|
|
263
|
+
for callback in self._history_restored_listeners:
|
|
264
|
+
callback()
|
|
265
|
+
|
|
266
|
+
def _dispatch_stamp_data(self, history_stamp: dict) -> None:
|
|
267
|
+
"""Hand the stamp's side-channel payload back to its content widget.
|
|
268
|
+
|
|
269
|
+
The owning content is resolved defensively: a stamp may carry a
|
|
270
|
+
payload for a node that has since been deleted, or for a content
|
|
271
|
+
class that never implemented ``history_stamp_callback``. Neither is
|
|
272
|
+
an error, and neither may abort the restore.
|
|
273
|
+
"""
|
|
274
|
+
data = history_stamp.get('data', None)
|
|
275
|
+
if not data:
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
node = data.get('node') if isinstance(data, dict) else None
|
|
279
|
+
content = getattr(node, 'content', None)
|
|
280
|
+
callback = getattr(content, 'history_stamp_callback', None)
|
|
281
|
+
if not callable(callback):
|
|
282
|
+
if DEBUG:
|
|
283
|
+
print("No history_stamp_callback for stamp:", history_stamp.get('desc'))
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
callback(data, self.if_undo)
|
|
288
|
+
except Exception as e:
|
|
289
|
+
dumpException(e)
|
|
290
|
+
|
|
291
|
+
def storeHistory(
|
|
292
|
+
self,
|
|
293
|
+
desc: str,
|
|
294
|
+
setModified: bool = False,
|
|
295
|
+
data: dict = None,
|
|
296
|
+
callback: 'function' = None,
|
|
297
|
+
merge_key: Optional[str] = None,
|
|
298
|
+
merge: Optional[Callable[[Optional[dict], Optional[dict]], Optional[dict]]] = None,
|
|
299
|
+
) -> bool:
|
|
300
|
+
"""
|
|
301
|
+
Store History Stamp into History Stack
|
|
302
|
+
|
|
303
|
+
:param desc: Description of current History Stamp
|
|
304
|
+
:type desc: ``str``
|
|
305
|
+
:param setModified: if ``True`` marks :class:`~nodeeditor.node_scene.Scene` with `has_been_modified`
|
|
306
|
+
:type setModified: ``bool``
|
|
307
|
+
:param data: Additional data to store with History Stamp
|
|
308
|
+
:type data: ``dict``
|
|
309
|
+
:param callback: Callback function to call after storing the History Stamp
|
|
310
|
+
:type callback: ``function``
|
|
311
|
+
:param merge_key: Grouping token. When it matches the key of the
|
|
312
|
+
current top-of-stack stamp, that stamp is updated in place
|
|
313
|
+
instead of a new one being appended, so a burst of related
|
|
314
|
+
changes collapses into a single undo step.
|
|
315
|
+
:type merge_key: ``str`` or ``None``
|
|
316
|
+
:param merge: Combines the existing payload with the incoming one
|
|
317
|
+
when a merge happens. Receives ``(existing_data, new_data)`` and
|
|
318
|
+
returns the payload to store. Defaults to replacing the payload
|
|
319
|
+
outright, which is correct when the payload is a self-contained
|
|
320
|
+
snapshot of the post-change state. Use it to preserve the
|
|
321
|
+
*original* side of a diff so undo still returns to where the
|
|
322
|
+
burst began.
|
|
323
|
+
:type merge: ``callable`` or ``None``
|
|
324
|
+
:return: ``True`` if a new stamp was pushed, ``False`` if the store
|
|
325
|
+
was skipped (restore in progress) or coalesced into the top stamp.
|
|
326
|
+
:rtype: ``bool``
|
|
327
|
+
|
|
328
|
+
Triggers:
|
|
329
|
+
|
|
330
|
+
- `HistoryModified`
|
|
331
|
+
- `History Stored`
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
if self.is_restoring_history:
|
|
335
|
+
return False
|
|
336
|
+
|
|
337
|
+
if setModified:
|
|
338
|
+
self.scene.has_been_modified = True
|
|
339
|
+
|
|
340
|
+
if DEBUG:
|
|
341
|
+
print("Storing history", '"%s"' % desc,
|
|
342
|
+
".... current_step: @%d" % self.history_current_step,
|
|
343
|
+
"(%d)" % len(self.history_stack))
|
|
344
|
+
|
|
345
|
+
if self._coalesce(desc, data, merge_key, merge):
|
|
346
|
+
return False
|
|
347
|
+
|
|
348
|
+
# if the pointer (history_current_step) is not at the end of history_stack
|
|
349
|
+
if self.history_current_step+1 < len(self.history_stack):
|
|
350
|
+
self.history_stack = self.history_stack[0:self.history_current_step+1]
|
|
351
|
+
|
|
352
|
+
# history is outside of the limits
|
|
353
|
+
if (
|
|
354
|
+
self.history_limit is not None
|
|
355
|
+
and self.history_current_step+1 >= self.history_limit
|
|
356
|
+
):
|
|
357
|
+
self.history_stack = self.history_stack[1:]
|
|
358
|
+
self.history_current_step -= 1
|
|
359
|
+
|
|
360
|
+
hs = self.createHistoryStamp(desc, data=data, merge_key=merge_key)
|
|
361
|
+
|
|
362
|
+
self.history_stack.append(hs)
|
|
363
|
+
self.history_current_step += 1
|
|
364
|
+
if DEBUG:
|
|
365
|
+
print(" -- setting step to:", self.history_current_step)
|
|
366
|
+
|
|
367
|
+
# always trigger history modified (for i.e. updateEditMenu)
|
|
368
|
+
for callback in self._history_modified_listeners:
|
|
369
|
+
callback()
|
|
370
|
+
for callback in self._history_stored_listeners:
|
|
371
|
+
callback()
|
|
372
|
+
|
|
373
|
+
return True
|
|
374
|
+
|
|
375
|
+
def _coalesce(
|
|
376
|
+
self,
|
|
377
|
+
desc: str,
|
|
378
|
+
data: Optional[dict],
|
|
379
|
+
merge_key: Optional[str],
|
|
380
|
+
merge: Optional[Callable[[Optional[dict], Optional[dict]], Optional[dict]]],
|
|
381
|
+
) -> bool:
|
|
382
|
+
"""Fold this store into the current top stamp when keys match.
|
|
383
|
+
|
|
384
|
+
Only merges when the stack is already at its tip - after an undo
|
|
385
|
+
there is a redo tail, and folding into a stamp the user has stepped
|
|
386
|
+
back from would rewrite history they are about to redo.
|
|
387
|
+
"""
|
|
388
|
+
if merge_key is None or not self.history_stack:
|
|
389
|
+
return False
|
|
390
|
+
if self.history_current_step != len(self.history_stack) - 1:
|
|
391
|
+
return False
|
|
392
|
+
|
|
393
|
+
top = self.history_stack[-1]
|
|
394
|
+
if top.get('merge_key') != merge_key:
|
|
395
|
+
return False
|
|
396
|
+
|
|
397
|
+
existing_data = top.get('data', None)
|
|
398
|
+
if merge is not None:
|
|
399
|
+
try:
|
|
400
|
+
data = merge(existing_data, data)
|
|
401
|
+
except Exception as e:
|
|
402
|
+
dumpException(e)
|
|
403
|
+
return False
|
|
404
|
+
|
|
405
|
+
hs = self.createHistoryStamp(desc, data=data, merge_key=merge_key)
|
|
406
|
+
self.history_stack[-1] = hs
|
|
407
|
+
# current step is unchanged: the entry was folded, not added
|
|
408
|
+
|
|
409
|
+
for callback in self._history_modified_listeners:
|
|
410
|
+
callback()
|
|
411
|
+
for callback in self._history_stored_listeners:
|
|
412
|
+
callback()
|
|
413
|
+
return True
|
|
414
|
+
|
|
415
|
+
def captureCurrentSelection(self) -> SelectionDict:
|
|
416
|
+
"""
|
|
417
|
+
Create dictionary with a list of selected nodes and a list of selected edges
|
|
418
|
+
:return: ``dict`` 'nodes' - list of selected nodes, 'edges' - list of selected edges
|
|
419
|
+
:rtype: ``dict``
|
|
420
|
+
"""
|
|
421
|
+
sel_obj: SelectionDict = {
|
|
422
|
+
'nodes': [],
|
|
423
|
+
'edges': [],
|
|
424
|
+
'groups': [],
|
|
425
|
+
}
|
|
426
|
+
for item in self.scene.grScene.selectedItems():
|
|
427
|
+
try:
|
|
428
|
+
# Groups are QGraphicsRectItem without .node/.edge; check first
|
|
429
|
+
groups = getattr(self.scene, 'groups', [])
|
|
430
|
+
if groups and item in groups:
|
|
431
|
+
sel_obj['groups'].append(item.id)
|
|
432
|
+
continue
|
|
433
|
+
except Exception:
|
|
434
|
+
pass
|
|
435
|
+
if hasattr(item, 'node'):
|
|
436
|
+
sel_obj['nodes'].append(item.node.id)
|
|
437
|
+
elif hasattr(item, 'edge'):
|
|
438
|
+
sel_obj['edges'].append(item.edge.id)
|
|
439
|
+
return sel_obj
|
|
440
|
+
|
|
441
|
+
def createHistoryStamp(
|
|
442
|
+
self,
|
|
443
|
+
desc: str,
|
|
444
|
+
data: Optional[dict] = None,
|
|
445
|
+
merge_key: Optional[str] = None,
|
|
446
|
+
) -> dict:
|
|
447
|
+
"""
|
|
448
|
+
Create History Stamp. Internally serialize whole scene and the current selection
|
|
449
|
+
|
|
450
|
+
:param desc: Descriptive label for the History Stamp
|
|
451
|
+
:param data: Optional side-channel payload replayed to the owning content
|
|
452
|
+
:param merge_key: Optional grouping token recorded alongside the stamp
|
|
453
|
+
:return: History stamp serializing state of `Scene` and current selection
|
|
454
|
+
:rtype: ``dict``
|
|
455
|
+
"""
|
|
456
|
+
history_stamp = {
|
|
457
|
+
'desc': desc,
|
|
458
|
+
'snapshot': self.scene.serialize(),
|
|
459
|
+
'selection': self.captureCurrentSelection(),
|
|
460
|
+
'merge_key': merge_key,
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if data is not None:
|
|
464
|
+
history_stamp['data'] = data
|
|
465
|
+
|
|
466
|
+
return history_stamp
|
|
467
|
+
|
|
468
|
+
def restoreHistoryStamp(self, history_stamp: dict) -> None:
|
|
469
|
+
"""
|
|
470
|
+
Restore History Stamp to current `Scene` with selection of items included
|
|
471
|
+
|
|
472
|
+
:param history_stamp: History Stamp to restore
|
|
473
|
+
:type history_stamp: ``dict``
|
|
474
|
+
"""
|
|
475
|
+
if DEBUG:
|
|
476
|
+
print("RHS: ", history_stamp['desc'])
|
|
477
|
+
|
|
478
|
+
try:
|
|
479
|
+
self.undo_selection_has_changed = False
|
|
480
|
+
previous_selection = self.captureCurrentSelection()
|
|
481
|
+
if DEBUG_SELECTION:
|
|
482
|
+
print("selected nodes before restore:",
|
|
483
|
+
previous_selection['nodes'])
|
|
484
|
+
|
|
485
|
+
self.scene.deserialize(history_stamp['snapshot'])
|
|
486
|
+
|
|
487
|
+
# restore selection
|
|
488
|
+
|
|
489
|
+
# first clear all selection on edges
|
|
490
|
+
for edge in self.scene.edges:
|
|
491
|
+
try:
|
|
492
|
+
if getattr(edge, 'grEdge', None) is not None:
|
|
493
|
+
edge.grEdge.setSelected(False)
|
|
494
|
+
except Exception:
|
|
495
|
+
pass
|
|
496
|
+
# now restore selected edges from history_stamp
|
|
497
|
+
for edge_id in history_stamp['selection'].get('edges', []):
|
|
498
|
+
for edge in self.scene.edges:
|
|
499
|
+
if edge.id == edge_id:
|
|
500
|
+
try:
|
|
501
|
+
edge.grEdge.setSelected(True)
|
|
502
|
+
except Exception:
|
|
503
|
+
pass
|
|
504
|
+
break
|
|
505
|
+
|
|
506
|
+
# first clear all selection on nodes
|
|
507
|
+
for node in self.scene.nodes:
|
|
508
|
+
try:
|
|
509
|
+
if getattr(node, 'grNode', None) is not None:
|
|
510
|
+
node.grNode.setSelected(False)
|
|
511
|
+
except Exception:
|
|
512
|
+
pass
|
|
513
|
+
# now restore selected nodes from history_stamp
|
|
514
|
+
for node_id in history_stamp['selection'].get('nodes', []):
|
|
515
|
+
for node in self.scene.nodes:
|
|
516
|
+
if node.id == node_id:
|
|
517
|
+
try:
|
|
518
|
+
node.grNode.setSelected(True)
|
|
519
|
+
except Exception:
|
|
520
|
+
pass
|
|
521
|
+
break
|
|
522
|
+
|
|
523
|
+
# groups (new; old stamps may lack the key)
|
|
524
|
+
try:
|
|
525
|
+
for grp in list(getattr(self.scene, 'groups', [])):
|
|
526
|
+
try:
|
|
527
|
+
grp.setSelected(False)
|
|
528
|
+
except Exception:
|
|
529
|
+
pass
|
|
530
|
+
for gid in history_stamp['selection'].get('groups', []):
|
|
531
|
+
for grp in list(getattr(self.scene, 'groups', [])):
|
|
532
|
+
if getattr(grp, 'id', None) == gid:
|
|
533
|
+
try:
|
|
534
|
+
grp.setSelected(True)
|
|
535
|
+
except Exception:
|
|
536
|
+
pass
|
|
537
|
+
break
|
|
538
|
+
except Exception:
|
|
539
|
+
pass
|
|
540
|
+
|
|
541
|
+
current_selection = self.captureCurrentSelection()
|
|
542
|
+
if DEBUG_SELECTION:
|
|
543
|
+
print("selected nodes after restore:",
|
|
544
|
+
current_selection['nodes'])
|
|
545
|
+
|
|
546
|
+
# reset the last_selected_items - since we're comparing change to the last_selected state
|
|
547
|
+
self.scene._last_selected_items = self.scene.getSelectedItems()
|
|
548
|
+
|
|
549
|
+
# if the selection of nodes differ before and after restoration, set flag
|
|
550
|
+
if current_selection.get('nodes') != previous_selection.get('nodes') or current_selection.get('edges') != previous_selection.get('edges') or current_selection.get('groups') != previous_selection.get('groups'):
|
|
551
|
+
if DEBUG_SELECTION:
|
|
552
|
+
print("\nSCENE: Selection has changed")
|
|
553
|
+
self.undo_selection_has_changed = True
|
|
554
|
+
|
|
555
|
+
except Exception as e:
|
|
556
|
+
dumpException(e)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
A module containing Serializable "Interface". We pretend its an abstract class
|
|
4
|
+
"""
|
|
5
|
+
from collections import OrderedDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Serializable():
|
|
9
|
+
"""Class representing the serializable "interface" of NodeEditor."""
|
|
10
|
+
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
"""
|
|
13
|
+
Default constructor automatically creates data which are common to any serializable object.
|
|
14
|
+
In our case we create ``self.id`` which we use in every object in NodeEditor.
|
|
15
|
+
"""
|
|
16
|
+
self.id = id(self)
|
|
17
|
+
|
|
18
|
+
def serialize(self) -> OrderedDict:
|
|
19
|
+
"""
|
|
20
|
+
Serialization method to serialize this class data into ``OrderedDict`` which can be easily stored
|
|
21
|
+
in memory or file.
|
|
22
|
+
|
|
23
|
+
:return: data serialized in ``OrderedDict``
|
|
24
|
+
:rtype: ``OrderedDict``
|
|
25
|
+
"""
|
|
26
|
+
raise NotImplementedError()
|
|
27
|
+
|
|
28
|
+
def deserialize(self, data: dict, hashmap: dict = {}, restore_id: bool = True) -> bool:
|
|
29
|
+
"""
|
|
30
|
+
Deserialization method which take data in python ``dict`` format with helping `hashmap` containing
|
|
31
|
+
references to existing entities.
|
|
32
|
+
|
|
33
|
+
:param data: Dictionary containing serialized data
|
|
34
|
+
:type data: ``dict``
|
|
35
|
+
:param hashmap: Helper dictionary containing references (by id == key) to existing objects
|
|
36
|
+
:type hashmap: ``dict``
|
|
37
|
+
:param restore_id: True if we are creating new Sockets. False is useful when loading existing
|
|
38
|
+
Sockets of which we want to keep the existing object's `id`.
|
|
39
|
+
:type restore_id: bool
|
|
40
|
+
:return: ``True`` if deserialization was successful, otherwise ``False``
|
|
41
|
+
:rtype: ``bool``
|
|
42
|
+
"""
|
|
43
|
+
raise NotImplementedError()
|