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.
Files changed (40) hide show
  1. nodeeditor/__init__.py +93 -0
  2. nodeeditor/node_colors_config.py +343 -0
  3. nodeeditor/node_content_widget.py +129 -0
  4. nodeeditor/node_decorators.py +65 -0
  5. nodeeditor/node_edge.py +518 -0
  6. nodeeditor/node_edge_dragging.py +185 -0
  7. nodeeditor/node_edge_intersect.py +219 -0
  8. nodeeditor/node_edge_rerouting.py +216 -0
  9. nodeeditor/node_edge_snapping.py +95 -0
  10. nodeeditor/node_edge_validators.py +82 -0
  11. nodeeditor/node_editor_widget.py +257 -0
  12. nodeeditor/node_editor_window.py +448 -0
  13. nodeeditor/node_graph.py +583 -0
  14. nodeeditor/node_graphics_cutline.py +57 -0
  15. nodeeditor/node_graphics_edge.py +375 -0
  16. nodeeditor/node_graphics_edge_path.py +226 -0
  17. nodeeditor/node_graphics_node.py +693 -0
  18. nodeeditor/node_graphics_scene.py +369 -0
  19. nodeeditor/node_graphics_socket.py +262 -0
  20. nodeeditor/node_graphics_view.py +907 -0
  21. nodeeditor/node_group.py +1286 -0
  22. nodeeditor/node_group_node.py +5 -0
  23. nodeeditor/node_icon_content_widget.py +176 -0
  24. nodeeditor/node_icon_graphics_node.py +524 -0
  25. nodeeditor/node_multi_input_node.py +450 -0
  26. nodeeditor/node_node.py +848 -0
  27. nodeeditor/node_scene.py +1106 -0
  28. nodeeditor/node_scene_clipboard.py +314 -0
  29. nodeeditor/node_scene_history.py +556 -0
  30. nodeeditor/node_serializable.py +43 -0
  31. nodeeditor/node_socket.py +445 -0
  32. nodeeditor/node_socket_shapes.py +261 -0
  33. nodeeditor/py.typed +0 -0
  34. nodeeditor/qss/nodestyle.qss +3 -0
  35. nodeeditor/utils.py +108 -0
  36. nodeeditor/utils_no_qt.py +21 -0
  37. nodeeditor_qtpy-0.8.1.dist-info/METADATA +165 -0
  38. nodeeditor_qtpy-0.8.1.dist-info/RECORD +40 -0
  39. nodeeditor_qtpy-0.8.1.dist-info/WHEEL +4 -0
  40. 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()