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,848 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ A module containing NodeEditor's class for representing `Node`.
4
+ """
5
+ from collections import OrderedDict
6
+ from qtpy.QtCore import QObject, Signal
7
+ from nodeeditor.node_graphics_node import QDMGraphicsNode
8
+ from nodeeditor.node_content_widget import QDMNodeContentWidget
9
+ from nodeeditor.node_serializable import Serializable
10
+ from nodeeditor.node_socket import Socket, LEFT_BOTTOM, LEFT_CENTER, LEFT_TOP, RIGHT_BOTTOM, RIGHT_CENTER, RIGHT_TOP
11
+ from nodeeditor.node_socket_shapes import SOCKET_SHAPES
12
+ import nodeeditor.node_graph as node_graph
13
+ from nodeeditor.utils_no_qt import dumpException, pp
14
+
15
+ from typing import TYPE_CHECKING, List, Optional, Tuple, Any, Union
16
+
17
+
18
+ if TYPE_CHECKING:
19
+ from nodeeditor.node_graphics_view import QDMGraphicsView
20
+ from nodeeditor.node_edge import Edge
21
+ from nodeeditor.node_socket import Socket
22
+ from nodeeditor.node_scene import Scene
23
+ from nodeeditor.node_group import Group
24
+
25
+ DEBUG = False
26
+
27
+
28
+ class Node(QObject, Serializable):
29
+ """
30
+ Class representing `Node` in the `Scene`.
31
+ """
32
+ # Signal emitted when node position changes (after user finishes dragging)
33
+ positionChanged = Signal(object) # Emits the Node object
34
+ # Signal emitted when the floating label text is changed by user or code
35
+ labelChanged = Signal(str)
36
+
37
+ GraphicsNode_class = QDMGraphicsNode
38
+ NodeContent_class = QDMNodeContentWidget
39
+ Socket_class = Socket
40
+
41
+ def __init__(self, scene: 'Scene', title: str = "Undefined Node", inputs: list = [], outputs: list = [], input_text: list = [], output_text: list = []) -> None:
42
+ """
43
+
44
+ :param scene: reference to the :class:`~nodeeditor.node_scene.Scene`
45
+ :type scene: :class:`~nodeeditor.node_scene.Scene`
46
+ :param title: Node Title shown in Scene
47
+ :type title: str
48
+ :param inputs: list of :class:`~nodeeditor.node_socket.Socket` types from which the `Sockets` will be auto created
49
+ :param outputs: list of :class:`~nodeeditor.node_socket.Socket` types from which the `Sockets` will be auto created
50
+
51
+ :Instance Attributes:
52
+
53
+ - **scene** - reference to the :class:`~nodeeditor.node_scene.Scene`
54
+ - **grNode** - Instance of :class:`~nodeeditor.node_graphics_node.QDMGraphicsNode` handling graphical representation in the ``QGraphicsScene``. Automatically created in the constructor
55
+ - **content** - Instance of :class:`~nodeeditor.node_graphics_content.QDMGraphicsContent` which is child of ``QWidget`` representing container for all inner widgets inside of the Node. Automatically created in the constructor
56
+ - **inputs** - list containin Input :class:`~nodeeditor.node_socket.Socket` instances
57
+ - **outputs** - list containin Output :class:`~nodeeditor.node_socket.Socket` instances
58
+
59
+ """
60
+ super().__init__() # Initialize QObject
61
+ super(Serializable).__init__() # Initialize Serializable
62
+ self._title = title
63
+ self.scene = scene
64
+
65
+ # just to be sure, init these variables
66
+ self.content: QDMNodeContentWidget
67
+ self.grNode: QDMGraphicsNode
68
+
69
+ self.initInnerClasses()
70
+ self.initSettings()
71
+
72
+ self.title = title
73
+
74
+ self.scene.addNode(self)
75
+ self.scene.grScene.addItem(self.grNode)
76
+
77
+ # create socket for inputs and outputs
78
+ self.inputs: List['Socket'] = []
79
+ self.outputs: List['Socket'] = []
80
+ self.initSockets(inputs, outputs, input_text, output_text)
81
+
82
+ # dirty and evaluation
83
+ self._is_dirty = False
84
+ self._is_invalid = False
85
+
86
+ # grouping support (single parent Group or None; see nodeeditor.node_group)
87
+ self.parent_group: Optional["Group"] = None
88
+
89
+ # The scene may already be at low zoom (a restore into a zoomed-out
90
+ # view): sockets and the content proxy were built after Qt reported
91
+ # this node joining the scene, so push the current state onto them.
92
+ sync_detail = getattr(self.grNode, "syncDetailVisibility", None)
93
+ if sync_detail is not None:
94
+ sync_detail()
95
+
96
+ def __str__(self) -> str:
97
+ """Return a short identifying string of this `Node`."""
98
+ return "<%s:%s %s..%s>" % (self.title, self.__class__.__name__, hex(id(self))[2:5], hex(id(self))[-3:])
99
+
100
+ @property
101
+ def title(self):
102
+ """
103
+ Title shown in the scene
104
+
105
+ :getter: return current Node title
106
+ :setter: sets Node title and passes it to Graphics Node class
107
+ :type: ``str``
108
+ """
109
+ return self._title
110
+
111
+ @title.setter
112
+ def title(self, value) -> None:
113
+ self._title = value
114
+ self.grNode.title = self._title
115
+
116
+ def setNodeLabel(self, text: str, store_history: bool = True) -> None:
117
+ """Set the floating single-line label above the node.
118
+
119
+ :param text: label text; empty string hides the label
120
+ :param store_history: store undo history entry (skip during load/deserialize)
121
+ """
122
+ if self.grNode is None or not hasattr(self.grNode, 'setLabelText'):
123
+ return
124
+ self.grNode.setLabelText(text)
125
+ try:
126
+ self.labelChanged.emit(self.grNode.labelText())
127
+ except Exception:
128
+ pass
129
+ if store_history:
130
+ try:
131
+ self.scene.history.storeHistory("Node label changed", setModified=True)
132
+ except Exception:
133
+ pass
134
+
135
+ def getNodeLabel(self) -> str:
136
+ """Return floating label text (``""`` when none)."""
137
+ if self.grNode is None or not hasattr(self.grNode, 'labelText'):
138
+ return ""
139
+ try:
140
+ return self.grNode.labelText()
141
+ except Exception:
142
+ return ""
143
+
144
+ def clearNodeLabel(self, store_history: bool = True) -> None:
145
+ """Remove the floating label."""
146
+ self.setNodeLabel("", store_history=store_history)
147
+
148
+ def setNodeLabelVisible(self, visible: bool) -> None:
149
+ """Show or hide the floating label of this `Node`.
150
+
151
+ The label stays hidden while it carries no text.
152
+
153
+ :param visible: ``True`` shows the label, ``False`` hides it
154
+ :type visible: ``bool``
155
+ """
156
+ if self.grNode is None or not hasattr(self.grNode, 'setLabelVisible'):
157
+ return
158
+ self.grNode.setLabelVisible(visible)
159
+
160
+ def isNodeLabelVisible(self) -> bool:
161
+ """Is the floating label of this `Node` currently visible?
162
+
163
+ :return: ``True`` if the label is visible
164
+ :rtype: ``bool``
165
+ """
166
+ if self.grNode is None or not hasattr(self.grNode, 'isLabelVisible'):
167
+ return False
168
+ try:
169
+ return bool(self.grNode.isLabelVisible())
170
+ except Exception:
171
+ return False
172
+
173
+ def setNodeLabelOffset(self, offset: float) -> None:
174
+ """Set the gap between the top of this `Node` and the bottom of its floating label.
175
+
176
+ :param offset: gap in pixels
177
+ :type offset: ``float``
178
+ """
179
+ if self.grNode is None or not hasattr(self.grNode, 'setLabelOffset'):
180
+ return
181
+ self.grNode.setLabelOffset(offset)
182
+
183
+ @property
184
+ def pos(self):
185
+ """
186
+ Retrieve Node's position in the Scene
187
+
188
+ :return: Node position
189
+ :rtype: ``QPointF``
190
+ """
191
+ return self.grNode.pos() # QPointF
192
+
193
+ def setPos(self, x: float, y: float) -> None:
194
+ """
195
+ Sets position of the Graphics Node
196
+
197
+ :param x: X `Scene` position
198
+ :param y: Y `Scene` position
199
+ """
200
+ # Moving the graphics item fires ``itemChange(ItemPositionHasChanged)``,
201
+ # which refreshes the connected edges. The loop that used to live here
202
+ # did the same work again, and called ``calcPath()`` a second time
203
+ # before immediately invalidating that path, so it was pure waste.
204
+ self.grNode.setPos(x, y)
205
+
206
+ def initInnerClasses(self) -> None:
207
+ """Sets up graphics Node (PyQt) and Content Widget"""
208
+ node_content_class = self.getNodeContentClass()
209
+ graphics_node_class = self.getGraphicsNodeClass()
210
+ if node_content_class is not None:
211
+ self.content = node_content_class(self)
212
+ if graphics_node_class is not None:
213
+ self.grNode = graphics_node_class(self)
214
+
215
+ def getNodeContentClass(self):
216
+ """Returns class representing nodeeditor content"""
217
+ return self.__class__.NodeContent_class
218
+
219
+ def getGraphicsNodeClass(self):
220
+ """Returns class representing the graphics node"""
221
+ return self.__class__.GraphicsNode_class
222
+
223
+ def initSettings(self) -> None:
224
+ """Initialize properties and socket information"""
225
+ self.socket_spacing = 22
226
+ # Todo: set input_multi_edged option.
227
+ self.input_socket_position = LEFT_BOTTOM
228
+ self.output_socket_position = RIGHT_TOP
229
+ self.input_multi_edged = False
230
+ self.output_multi_edged = True
231
+ #: shape used by this node's input sockets, keyed by socket type.
232
+ #: ``'*'`` is the wildcard for every type the map does not name.
233
+ self.input_socket_shapes: dict = {}
234
+ #: shape used by this node's output sockets, keyed by socket type
235
+ self.output_socket_shapes: dict = {}
236
+ self.socket_offsets = {
237
+ LEFT_BOTTOM: -1,
238
+ LEFT_CENTER: -1,
239
+ LEFT_TOP: -1,
240
+ RIGHT_BOTTOM: 1,
241
+ RIGHT_CENTER: 1,
242
+ RIGHT_TOP: 1,
243
+ }
244
+
245
+ def getSocketShapeName(self, socket_type, is_input: bool) -> str:
246
+ """Name of the shape used by a socket of ``socket_type`` on this node.
247
+
248
+ The side's own map is consulted first, then its ``'*'`` wildcard
249
+ entry, and the circle is the last resort. Types that are not mapped
250
+ keep the classic circle.
251
+
252
+ :param socket_type: the socket's type, as passed to ``initSockets``
253
+ :param is_input: ``True`` for an input socket
254
+ :return: the registered shape name
255
+ :rtype: ``str``
256
+
257
+ Example::
258
+
259
+ class MyNode(Node):
260
+ def initSettings(self):
261
+ super().initSettings()
262
+ self.input_socket_shapes = {1: 'chevron'}
263
+ self.output_socket_shapes = {'*': 'triangle'}
264
+ """
265
+ if is_input:
266
+ mapping = getattr(self, "input_socket_shapes", None)
267
+ else:
268
+ mapping = getattr(self, "output_socket_shapes", None)
269
+
270
+ name = None
271
+ if isinstance(mapping, dict):
272
+ name = mapping.get(socket_type)
273
+ if name is None:
274
+ name = mapping.get("*")
275
+ # an unregistered name draws as the circle rather than breaking
276
+ if isinstance(name, str) and name in SOCKET_SHAPES:
277
+ return name
278
+ return "circle"
279
+
280
+ def initSockets(self, inputs: list, outputs: list,
281
+ input_text: list = [], output_text: list = [],
282
+ reset: bool = True) -> None:
283
+ """
284
+ Create sockets for inputs and outputs
285
+
286
+ :param inputs: list of Socket Types (int)
287
+ :type inputs: ``list``
288
+ :param outputs: list of Socket Types (int)
289
+ :type outputs: ``list``
290
+ :param input_text: list of Socket Types (int)
291
+ :type input_text: ``list``
292
+ :param output_text: list of Socket Types (int)
293
+ :type output_text: ``list``
294
+ :param reset: if ``True`` destroys and removes old `Sockets`
295
+ :type reset: ``bool``
296
+ """
297
+
298
+ # Initialize empty lists if None provided
299
+ input_text = input_text or []
300
+ output_text = output_text or []
301
+
302
+ if reset:
303
+ # clear old sockets
304
+ if hasattr(self, 'inputs') and hasattr(self, 'outputs'):
305
+ # remove grSockets from scene
306
+ for socket in (self.inputs + self.outputs):
307
+ self.scene.grScene.removeItem(socket.grSocket)
308
+ self.inputs = []
309
+ self.outputs = []
310
+
311
+ # create new sockets
312
+ for i, item in enumerate(inputs):
313
+ # Get text if available, otherwise empty string
314
+ text = input_text[i] if i < len(input_text) else ""
315
+ socket = self.__class__.Socket_class(
316
+ node=self,
317
+ index=i,
318
+ position=self.input_socket_position,
319
+ socket_type=item,
320
+ multi_edges=self.input_multi_edged,
321
+ count_on_this_node_side=len(inputs),
322
+ is_input=True
323
+ )
324
+ socket.grSocket.setText(text)
325
+ self.inputs.append(socket)
326
+
327
+ for i, item in enumerate(outputs):
328
+ # Get text if available, otherwise empty string
329
+ text = output_text[i] if i < len(output_text) else ""
330
+ socket = self.__class__.Socket_class(
331
+ node=self,
332
+ index=i,
333
+ position=self.output_socket_position,
334
+ socket_type=item,
335
+ multi_edges=self.output_multi_edged,
336
+ count_on_this_node_side=len(outputs),
337
+ is_input=False
338
+ )
339
+ socket.grSocket.setText(text)
340
+ self.outputs.append(socket)
341
+
342
+ def onEdgeConnectionChanged(self, new_edge: 'Edge') -> None:
343
+ """
344
+ Event handling that any connection (`Edge`) has changed. Currently not used...
345
+
346
+ :param new_edge: reference to the changed :class:`~nodeeditor.node_edge.Edge`
347
+ :type new_edge: :class:`~nodeeditor.node_edge.Edge`
348
+ """
349
+ pass
350
+
351
+ def onInputChanged(self, socket: 'Socket') -> None:
352
+ """Event handling when Node's input Edge has changed. We auto-mark this `Node` to be `Dirty` with all it's
353
+ descendants
354
+
355
+ :param socket: reference to the changed :class:`~nodeeditor.node_socket.Socket`
356
+ :type socket: :class:`~nodeeditor.node_socket.Socket`
357
+ """
358
+ self.markDirty()
359
+ self.markDescendantsDirty()
360
+
361
+ def onDeserialized(self, data: dict) -> None:
362
+ """Event manually called when this node was deserialized. Currently called when node is deserialized from scene
363
+ Passing `data` containing the data which have been deserialized """
364
+ pass
365
+
366
+ def onDoubleClicked(self, event) -> None:
367
+ """Event handling double click on Graphics Node in `Scene`"""
368
+ pass
369
+
370
+ def doSelect(self, new_state: bool = True) -> None:
371
+ """Shortcut method for selecting/deselecting the `Node`
372
+
373
+ :param new_state: ``True`` if you want to select the `Node`. ``False`` if you want to deselect the `Node`
374
+ :type new_state: ``bool``
375
+ """
376
+ self.grNode.doSelect(new_state)
377
+
378
+ def isSelected(self):
379
+ """Returns ``True`` if current `Node` is selected"""
380
+ return self.grNode.isSelected()
381
+
382
+ def hasConnectedEdge(self, edge: 'Edge') -> bool:
383
+ """Returns ``True`` if edge is connected to any :class:`~nodeeditor.node_socket.Socket` of this `Node`"""
384
+ for socket in (self.inputs + self.outputs):
385
+ if socket.isConnected(edge):
386
+ return True
387
+ return False
388
+
389
+ def getSocketPosition(self, index: int, position: int, num_out_of: int = 1) -> List[float]:
390
+ """
391
+ Get the relative `x, y` position of a :class:`~nodeeditor.node_socket.Socket`. This is used for placing
392
+ the `Graphics Sockets` on `Graphics Node`.
393
+
394
+ :param index: Order number of the Socket. (0, 1, 2, ...)
395
+ :type index: ``int``
396
+ :param position: `Socket Position Constant` describing where the Socket is located. See :ref:`socket-position-constants`
397
+ :type position: ``int``
398
+ :param num_out_of: Total number of Sockets on this `Socket Position`
399
+ :type num_out_of: ``int``
400
+ :return: Position of described Socket on the `Node`
401
+ :rtype: ``x, y``
402
+ """
403
+ x = self.socket_offsets[position] if (position in (
404
+ LEFT_TOP, LEFT_CENTER, LEFT_BOTTOM)) else self.grNode.width + self.socket_offsets[position]
405
+
406
+ if position in (LEFT_BOTTOM, RIGHT_BOTTOM):
407
+ # start from bottom
408
+ y = self.grNode.height - self.grNode.edge_roundness - \
409
+ self.grNode.title_vertical_padding - index * self.socket_spacing
410
+ elif position in (LEFT_CENTER, RIGHT_CENTER):
411
+ num_sockets = num_out_of
412
+ node_height = self.grNode.height
413
+ top_offset = self.grNode.title_height + 2 * \
414
+ self.grNode.title_vertical_padding + self.grNode.edge_padding
415
+ available_height = node_height - top_offset
416
+
417
+ total_height_of_all_sockets = num_sockets * self.socket_spacing
418
+ new_top = available_height - total_height_of_all_sockets
419
+
420
+ # y = top_offset + index * self.socket_spacing + new_top / 2
421
+ y = top_offset + available_height / \
422
+ 2.0 + (index-0.5)*self.socket_spacing
423
+ if num_sockets > 1:
424
+ y -= self.socket_spacing * (num_sockets-1)/2
425
+
426
+ elif position in (LEFT_TOP, RIGHT_TOP):
427
+ # start from top
428
+ y = self.grNode.title_height + self.grNode.title_vertical_padding + \
429
+ self.grNode.edge_roundness + index * self.socket_spacing
430
+ else:
431
+ # this should never happen
432
+ y = 0
433
+
434
+ return [x, y]
435
+
436
+ def getSocketScenePosition(self, socket: 'Socket') -> Tuple[float, float]:
437
+ """
438
+ Get absolute Socket position in the Scene
439
+
440
+ :param socket: `Socket` which position we want to know
441
+ :return: (x, y) Socket's scene position
442
+ """
443
+ nodepos = self.grNode.pos()
444
+ socketpos = self.getSocketPosition(
445
+ socket.index, socket.position, socket.count_on_this_node_side)
446
+ return (nodepos.x() + socketpos[0], nodepos.y() + socketpos[1])
447
+
448
+ def updateConnectedEdges(self) -> None:
449
+ """Recalculate (Refresh) positions of all connected `Edges`. Used for updating Graphics Edges"""
450
+ for socket in self.inputs + self.outputs:
451
+ # if socket.hasEdge():
452
+ for edge in socket.edges:
453
+ edge.updatePositions()
454
+
455
+ def remove(self) -> None:
456
+ """
457
+ Safely remove this Node (detaches from parent Group first)
458
+ """
459
+ # detach from group first so bounds/stubs update without this node
460
+ try:
461
+ grp = getattr(self, 'parent_group', None)
462
+ if grp is not None:
463
+ try:
464
+ grp.removeNode(self, update_bounds=False)
465
+ except Exception:
466
+ try:
467
+ self.parent_group = None
468
+ except Exception:
469
+ pass
470
+ except Exception:
471
+ pass
472
+ if DEBUG:
473
+ print("> Removing Node", self)
474
+ if DEBUG:
475
+ print(" - remove all edges from sockets")
476
+ for socket in (self.inputs+self.outputs):
477
+ # if socket.hasEdge():
478
+ for edge in socket.edges.copy():
479
+ if DEBUG:
480
+ print(" - removing from socket:", socket, "edge:", edge)
481
+ edge.remove()
482
+ if DEBUG:
483
+ print(" - remove grNode")
484
+ try:
485
+ if self.grNode is not None:
486
+ self.scene.grScene.removeItem(self.grNode)
487
+ except Exception:
488
+ pass
489
+ self.grNode = None
490
+ if DEBUG:
491
+ print(" - remove node from the scene")
492
+ self.scene.removeNode(self)
493
+ if DEBUG:
494
+ print(" - everything was done.")
495
+
496
+ # node evaluation stuff
497
+
498
+ def isDirty(self) -> bool:
499
+ """Is this node marked as `Dirty`
500
+
501
+ :return: ``True`` if `Node` is marked as `Dirty`
502
+ :rtype: ``bool``
503
+ """
504
+ return self._is_dirty
505
+
506
+ def markDirty(self, new_value: bool = True) -> None:
507
+ """Mark this `Node` as `Dirty`. See :ref:`evaluation` for more
508
+
509
+ :param new_value: ``True`` if this `Node` should be `Dirty`. ``False`` if you want to un-dirty this `Node`
510
+ :type new_value: ``bool``
511
+ """
512
+ self._is_dirty = new_value
513
+ if self._is_dirty:
514
+ self.onMarkedDirty()
515
+
516
+ def onMarkedDirty(self) -> None:
517
+ """Called when this `Node` has been marked as `Dirty`. This method is supposed to be overridden"""
518
+ pass
519
+
520
+ def markChildrenDirty(self, new_value: bool = True) -> None:
521
+ """Mark all first level children of this `Node` to be `Dirty`. Not this `Node` it self. Not other descendants
522
+
523
+ :param new_value: ``True`` if children should be `Dirty`. ``False`` if you want to un-dirty children
524
+ :type new_value: ``bool``
525
+ """
526
+ for other_node in self.getChildrenNodes():
527
+ other_node.markDirty(new_value)
528
+
529
+ def markDescendantsDirty(self, new_value: bool = True) -> None:
530
+ """Mark all children and descendants of this `Node` to be `Dirty`. Not this `Node` it self
531
+
532
+ The traversal is iterative and keeps a visited set, so a graph with a
533
+ cycle terminates instead of exhausting the interpreter's recursion
534
+ limit, and a node reachable through several paths is visited once
535
+ rather than once per path.
536
+
537
+ :param new_value: ``True`` if children and descendants should be `Dirty`. ``False`` if you want to un-dirty children and descendants
538
+ :type new_value: ``bool``
539
+ """
540
+ for other_node in node_graph.descendants(self.scene, self):
541
+ other_node.markDirty(new_value)
542
+
543
+ def isInvalid(self) -> bool:
544
+ """Is this node marked as `Invalid`?
545
+
546
+ :return: ``True`` if `Node` is marked as `Invalid`
547
+ :rtype: ``bool``
548
+ """
549
+ return self._is_invalid
550
+
551
+ def markInvalid(self, new_value: bool = True) -> None:
552
+ """Mark this `Node` as `Invalid`. See :ref:`evaluation` for more
553
+
554
+ :param new_value: ``True`` if this `Node` should be `Invalid`. ``False`` if you want to make this `Node` valid
555
+ :type new_value: ``bool``
556
+ """
557
+ self._is_invalid = new_value
558
+ if self._is_invalid:
559
+ self.onMarkedInvalid()
560
+
561
+ def onMarkedInvalid(self) -> None:
562
+ """Called when this `Node` has been marked as `Invalid`. This method is supposed to be overridden"""
563
+ pass
564
+
565
+ def markChildrenInvalid(self, new_value: bool = True) -> None:
566
+ """Mark all first level children of this `Node` to be `Invalid`. Not this `Node` it self. Not other descendants
567
+
568
+ :param new_value: ``True`` if children should be `Invalid`. ``False`` if you want to make children valid
569
+ :type new_value: ``bool``
570
+ """
571
+ for other_node in self.getChildrenNodes():
572
+ other_node.markInvalid(new_value)
573
+
574
+ def markDescendantsInvalid(self, new_value: bool = True) -> None:
575
+ """Mark all children and descendants of this `Node` to be `Invalid`. Not this `Node` it self
576
+
577
+ The traversal is iterative and keeps a visited set, so a graph with a
578
+ cycle terminates instead of exhausting the interpreter's recursion
579
+ limit, and a node reachable through several paths is visited once
580
+ rather than once per path.
581
+
582
+ :param new_value: ``True`` if children and descendants should be `Invalid`. ``False`` if you want to make children and descendants valid
583
+ :type new_value: ``bool``
584
+ """
585
+ for other_node in node_graph.descendants(self.scene, self):
586
+ other_node.markInvalid(new_value)
587
+
588
+ def eval(self, index: int = 0) -> int:
589
+ """Evaluate this `Node`. This is supposed to be overridden. See :ref:`evaluation` for more"""
590
+ self.markDirty(False)
591
+ self.markInvalid(False)
592
+ return 0
593
+
594
+ def evalChildren(self) -> None:
595
+ """Evaluate all children of this `Node`"""
596
+ for node in self.getChildrenNodes():
597
+ node.eval()
598
+
599
+ # traversing nodes functions
600
+
601
+ def getChildrenNodes(self) -> 'List[Node]':
602
+ """
603
+ Retreive all first-level children connected to this `Node` `Outputs`
604
+
605
+ :return: list of `Nodes` connected to this `Node` from all `Outputs`
606
+ :rtype: List[:class:`~nodeeditor.node_node.Node`]
607
+ """
608
+ if self.outputs == []:
609
+ return []
610
+ other_nodes = []
611
+ for ix in range(len(self.outputs)):
612
+ for edge in self.outputs[ix].edges:
613
+ other_node = edge.getOtherSocket(self.outputs[ix]).node
614
+ other_nodes.append(other_node)
615
+ return other_nodes
616
+
617
+ def getInput(self, index: int = 0) -> Optional['Node']:
618
+ """
619
+ Get the **first** `Node` connected to the Input specified by `index`
620
+
621
+ :param index: Order number of the `Input Socket`
622
+ :type index: ``int``
623
+ :return: :class:`~nodeeditor.node_node.Node` which is connected to the specified `Input` or ``None`` if
624
+ there is no connection or the index is out of range
625
+ :rtype: :class:`~nodeeditor.node_node.Node` or ``None``
626
+ """
627
+ try:
628
+ input_socket = self.inputs[index]
629
+ if len(input_socket.edges) == 0:
630
+ return None
631
+ connecting_edge = input_socket.edges[0]
632
+ other_socket = connecting_edge.getOtherSocket(self.inputs[index])
633
+ return other_socket.node
634
+ except Exception as e:
635
+ dumpException(e)
636
+ return None
637
+
638
+ def getInputWithSocket(self, index: int = 0) -> Union[Tuple['Node', 'Socket'], Tuple[None, None]]:
639
+ """
640
+ Get the **first** `Node` connected to the Input specified by `index` and the connection `Socket`
641
+
642
+ :param index: Order number of the `Input Socket`
643
+ :type index: ``int``
644
+ :return: Tuple containing :class:`~nodeeditor.node_node.Node` and :class:`~nodeeditor.node_socket.Socket` which
645
+ is connected to the specified `Input` or ``None`` if there is no connection or the index is out of range
646
+ :rtype: (:class:`~nodeeditor.node_node.Node`, :class:`~nodeeditor.node_socket.Socket`)
647
+ """
648
+ try:
649
+ input_socket = self.inputs[index]
650
+ if len(input_socket.edges) == 0:
651
+ return None, None
652
+ connecting_edge = input_socket.edges[0]
653
+ other_socket = connecting_edge.getOtherSocket(self.inputs[index])
654
+ return other_socket.node, other_socket
655
+ except Exception as e:
656
+ dumpException(e)
657
+ return None, None
658
+
659
+ def getInputWithSocketIndex(self, index: int = 0) -> Union[Tuple['Node', int], Tuple[None, None]]:
660
+ """
661
+ Get the **first** `Node` connected to the Input specified by `index` and the connection `Socket`
662
+
663
+ :param index: Order number of the `Input Socket`
664
+ :type index: ``int``
665
+ :return: Tuple containing :class:`~nodeeditor.node_node.Node` and :class:`~nodeeditor.node_socket.Socket` which
666
+ is connected to the specified `Input` or ``None`` if there is no connection or the index is out of range
667
+ :rtype: (:class:`~nodeeditor.node_node.Node`, int)
668
+ """
669
+ try:
670
+ edge = self.inputs[index].edges[0]
671
+ socket = edge.getOtherSocket(self.inputs[index])
672
+ return socket.node, socket.index
673
+ except IndexError:
674
+ # print("EXC: Trying to get input with socket index %d, but none is attached to" % index, self)
675
+ return None, None
676
+ except Exception as e:
677
+ dumpException(e)
678
+ return None, None
679
+
680
+ def getInputs(self, index: int = 0) -> 'List[Node]':
681
+ """
682
+ Get **all** `Nodes` connected to the Input specified by `index`
683
+
684
+ :param index: Order number of the `Input Socket`
685
+ :type index: ``int``
686
+ :return: all :class:`~nodeeditor.node_node.Node` instances which are connected to the
687
+ specified `Input` or ``[]`` if there is no connection or the index is out of range
688
+ :rtype: List[:class:`~nodeeditor.node_node.Node`]
689
+ """
690
+ ins = []
691
+ for edge in self.inputs[index].edges:
692
+ other_socket = edge.getOtherSocket(self.inputs[index])
693
+ ins.append(other_socket.node)
694
+ return ins
695
+
696
+ def getOutputs(self, index: int = 0) -> 'List[Node]':
697
+ """
698
+ Get **all** `Nodes` connected to the Output specified by `index`
699
+
700
+ :param index: Order number of the `Output Socket`
701
+ :type index: ``int``
702
+ :return: all :class:`~nodeeditor.node_node.Node` instances which are connected to the
703
+ specified `Output` or ``[]`` if there is no connection or the index is out of range
704
+ :rtype: List[:class:`~nodeeditor.node_node.Node`]
705
+ """
706
+ outs = []
707
+ for edge in self.outputs[index].edges:
708
+ other_socket = edge.getOtherSocket(self.outputs[index])
709
+ outs.append(other_socket.node)
710
+ return outs
711
+
712
+ # serialization functions
713
+
714
+ def serialize(self) -> OrderedDict:
715
+ """
716
+ Serialize this `Node` into an ``OrderedDict`` for storing in memory or on disk.
717
+
718
+ :return: data serialized in ``OrderedDict``
719
+ :rtype: ``OrderedDict``
720
+ """
721
+ inputs, outputs = [], []
722
+ for socket in self.inputs:
723
+ inputs.append(socket.serialize())
724
+ for socket in self.outputs:
725
+ outputs.append(socket.serialize())
726
+ ser_content = self.content.serialize() if isinstance(
727
+ self.content, Serializable) else {}
728
+ try:
729
+ label_text = self.getNodeLabel()
730
+ except Exception:
731
+ label_text = ""
732
+ try:
733
+ label_visible = self.isNodeLabelVisible() if label_text else False
734
+ except Exception:
735
+ label_visible = False
736
+ try:
737
+ label_offset = float(self.grNode.labelOffset()) if self.grNode is not None and hasattr(self.grNode, 'labelOffset') else 10.0
738
+ except Exception:
739
+ label_offset = 10.0
740
+ return OrderedDict([
741
+ ('id', self.id),
742
+ ('title', self.title),
743
+ ('pos_x', self.grNode.scenePos().x()),
744
+ ('pos_y', self.grNode.scenePos().y()),
745
+ ('inputs', inputs),
746
+ ('outputs', outputs),
747
+ ('content', ser_content),
748
+ ('label_text', label_text),
749
+ ('label_visible', label_visible),
750
+ ('label_offset', label_offset),
751
+ ])
752
+
753
+ def deserialize(self, data: dict, hashmap: dict = {}, restore_id: bool = True, *args, **kwargs) -> bool:
754
+ """
755
+ Deserialize this `Node` from a ``dict`` of serialized data.
756
+
757
+ Sockets missing in the data are created, sockets already present are reused.
758
+
759
+ :param data: Dictionary containing serialized data
760
+ :type data: ``dict``
761
+ :param hashmap: Helper dictionary containing references (by id == key) to existing objects
762
+ :type hashmap: ``dict``
763
+ :param restore_id: True if we are creating new Sockets. False is useful when loading existing
764
+ Sockets of which we want to keep the existing object's `id`.
765
+ :type restore_id: bool
766
+ :return: ``True`` if deserialization was successful, otherwise ``False``
767
+ :rtype: ``bool``
768
+ """
769
+ try:
770
+ if restore_id:
771
+ self.id = data['id']
772
+ hashmap[data['id']] = self
773
+
774
+ self.setPos(data['pos_x'], data['pos_y'])
775
+ self.title = data['title']
776
+
777
+ # floating label (backward compatible: old files have no keys)
778
+ try:
779
+ if self.grNode is not None and hasattr(self.grNode, 'setLabelText'):
780
+ self.grNode.setLabelOffset(float(data.get('label_offset', 10.0)))
781
+ self.grNode.setLabelText(data.get('label_text', ""))
782
+ self.grNode.setLabelVisible(bool(data.get('label_visible', bool(data.get('label_text', "")))))
783
+ except Exception:
784
+ pass
785
+
786
+ data['inputs'].sort(
787
+ key=lambda socket: socket['index'] + socket['position'] * 10000)
788
+ data['outputs'].sort(
789
+ key=lambda socket: socket['index'] + socket['position'] * 10000)
790
+ num_inputs = len(data['inputs'])
791
+ num_outputs = len(data['outputs'])
792
+
793
+ # print("> deserialize node, num inputs:", num_inputs, "num outputs:", num_outputs)
794
+ # pp(data)
795
+
796
+ # possible way to do it is reuse existing sockets...
797
+ # dont create new ones if not necessary
798
+
799
+ for socket_data in data['inputs']:
800
+ found = None
801
+ for socket in self.inputs:
802
+ # print("\t", socket, socket.index, "=?", socket_data['index'])
803
+ if socket.index == socket_data['index']:
804
+ found = socket
805
+ break
806
+ if found is None:
807
+ # print("deserialization of socket data has not found input socket with index:", socket_data['index'])
808
+ # print("actual socket data:", socket_data)
809
+ # we can create new socket for this
810
+ found = self.__class__.Socket_class(
811
+ node=self, index=socket_data['index'], position=socket_data['position'],
812
+ socket_type=socket_data['socket_type'], count_on_this_node_side=num_inputs,
813
+ is_input=True
814
+ )
815
+ # append newly created input to the list
816
+ self.inputs.append(found)
817
+ found.deserialize(socket_data, hashmap, restore_id)
818
+
819
+ for socket_data in data['outputs']:
820
+ found = None
821
+ for socket in self.outputs:
822
+ # print("\t", socket, socket.index, "=?", socket_data['index'])
823
+ if socket.index == socket_data['index']:
824
+ found = socket
825
+ break
826
+ if found is None:
827
+ # print("deserialization of socket data has not found output socket with index:", socket_data['index'])
828
+ # print("actual socket data:", socket_data)
829
+ # we can create new socket for this
830
+ found = self.__class__.Socket_class(
831
+ node=self, index=socket_data['index'], position=socket_data['position'],
832
+ socket_type=socket_data['socket_type'], count_on_this_node_side=num_outputs,
833
+ is_input=False
834
+ )
835
+ # append newly created output to the list
836
+ self.outputs.append(found)
837
+ found.deserialize(socket_data, hashmap, restore_id)
838
+
839
+ except Exception as e:
840
+ dumpException(e)
841
+
842
+ # also deserialize the content of the node
843
+ # so far the rest was ok, now as last step the content...
844
+ if isinstance(self.content, Serializable):
845
+ res = self.content.deserialize(data['content'], hashmap)
846
+ return res
847
+
848
+ return True