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,450 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ A module containing the ``MultiInputNode`` class: a :class:`~nodeeditor.node_node.Node`
4
+ whose input :class:`~nodeeditor.node_socket.Socket` accepts any number of
5
+ :class:`~nodeeditor.node_edge.Edge` (s) and reads them in an explicit,
6
+ persisted order.
7
+ """
8
+ from nodeeditor.node_node import Node
9
+ from nodeeditor.utils_no_qt import dumpException
10
+
11
+ import re
12
+ from typing import TYPE_CHECKING, Any, List, Optional, Tuple
13
+
14
+
15
+ if TYPE_CHECKING:
16
+ from nodeeditor.node_edge import Edge
17
+ from nodeeditor.node_scene import Scene
18
+ from nodeeditor.node_socket import Socket
19
+
20
+
21
+ class MultiInputNode(Node):
22
+ """
23
+ Base class for nodes reading an arbitrary number of inputs **in order**.
24
+
25
+ Subclass it, declare the sockets you need and read them with
26
+ :meth:`getOrderedSources` / :meth:`getOrderedValues`::
27
+
28
+ class Sum(MultiInputNode, CalcNode):
29
+ def evalImplementation(self):
30
+ return [sum(self.getOrderedValues())]
31
+
32
+ By default *every* input socket accepts multiple `Edges`
33
+ (:attr:`multi_input_all_inputs`). Set :attr:`multi_input_socket` and
34
+ :attr:`multi_input_all_inputs = False` to make only one socket do so.
35
+
36
+ The order is, by default, the order the `Edges` were connected in. It can
37
+ be changed at any time from code (:meth:`setEdgeOrder`, :meth:`moveEdgeTo`,
38
+ :meth:`swapEdges`) and is saved to / loaded from the graph file.
39
+
40
+ This class deliberately does not define ``__init__``, so it can be mixed
41
+ into an existing node hierarchy without changing its constructor chain.
42
+
43
+ :Instance Attributes:
44
+
45
+ - **multi_input_socket** - index of the input `Socket` which accepts
46
+ many `Edges` when :attr:`multi_input_all_inputs` is ``False``
47
+ - **multi_input_all_inputs** - ``True`` to let every input `Socket`
48
+ accept multiple `Edges`
49
+ """
50
+
51
+ #: index of the input socket accepting many `Edges` (see multi_input_all_inputs)
52
+ multi_input_socket: int = 0
53
+
54
+ #: when ``True`` every input socket of this node accepts multiple ``Edges``
55
+ multi_input_all_inputs: bool = True
56
+
57
+ #: when ``True`` every `Edge` on the ordered input is labeled ``#1``,
58
+ #: ``#2``, ... (1-based display of its read position) and the labels are
59
+ #: kept in sync on reorder/removal. Labels are cleared when an `Edge`
60
+ #: detaches, but only when they still match the ``#N`` pattern, so a
61
+ #: custom label you set yourself is never overwritten on disconnect.
62
+ show_input_order_labels: bool = True
63
+
64
+ def initSettings(self) -> None:
65
+ """Initialize properties and socket information"""
66
+ super().initSettings()
67
+ if self.multi_input_all_inputs:
68
+ self.input_multi_edged = True
69
+
70
+ def initSockets(self, inputs: list, outputs: list,
71
+ input_text: list = [], output_text: list = [],
72
+ reset: bool = True) -> None:
73
+ """Create sockets for inputs and outputs, keeping only
74
+ :attr:`multi_input_socket` multi-edged if :attr:`multi_input_all_inputs`
75
+ is ``False``"""
76
+ super().initSockets(inputs, outputs, input_text, output_text, reset)
77
+ if self.multi_input_all_inputs:
78
+ return
79
+ for index, socket in enumerate(self.inputs):
80
+ socket.is_multi_edges = index == self.multi_input_socket
81
+
82
+ # socket access
83
+
84
+ def getMultiSocket(self, index: Optional[int] = None) -> Optional['Socket']:
85
+ """
86
+ Get the input :class:`~nodeeditor.node_socket.Socket` which accepts many `Edges`.
87
+
88
+ :param index: index of the input `Socket` or ``None`` to use
89
+ :attr:`multi_input_socket`
90
+ :type index: ``int`` or ``None``
91
+ :return: the ordered input `Socket` or ``None`` if the node has no such socket
92
+ :rtype: :class:`~nodeeditor.node_socket.Socket` or ``None``
93
+ """
94
+ if index is None:
95
+ index = self.multi_input_socket
96
+ try:
97
+ return self.inputs[index]
98
+ except IndexError:
99
+ return None
100
+
101
+ def compactEdgeOrder(self, index: Optional[int] = None) -> bool:
102
+ """
103
+ Renumber the input `Edges` to a gap free ``0..n-1`` sequence.
104
+
105
+ Called automatically on every connection change, so it is only needed
106
+ after assigning :attr:`~nodeeditor.node_edge.Edge.input_index` directly.
107
+
108
+ :param index: index of the input `Socket` or ``None`` to use
109
+ :attr:`multi_input_socket`
110
+ :type index: ``int`` or ``None``
111
+ :return: ``True`` if anything actually changed
112
+ :rtype: ``bool``
113
+ """
114
+ socket = self.getMultiSocket(index)
115
+ if socket is None:
116
+ return False
117
+ return socket.compactEdgeOrder()
118
+
119
+ # reading
120
+
121
+ def getOrderedEdges(self, index: Optional[int] = None) -> List['Edge']:
122
+ """
123
+ Get the connected `Edges` of the ordered input `Socket` in read order.
124
+
125
+ :param index: index of the input `Socket` or ``None`` to use
126
+ :attr:`multi_input_socket`
127
+ :type index: ``int`` or ``None``
128
+ :return: `Edges` ordered by :attr:`~nodeeditor.node_edge.Edge.input_index`
129
+ :rtype: List[:class:`~nodeeditor.node_edge.Edge`]
130
+ """
131
+ socket = self.getMultiSocket(index)
132
+ if socket is None:
133
+ return []
134
+ return socket.orderedEdges()
135
+
136
+ def getOrderedSources(self, index: Optional[int] = None) -> List[Tuple['Node', 'Socket']]:
137
+ """
138
+ Get the ``(node, socket)`` pairs feeding the ordered input `Socket`.
139
+
140
+ One entry per connected `Edge`, in read order, so a source connected
141
+ twice appears twice.
142
+
143
+ :param index: index of the input `Socket` or ``None`` to use
144
+ :attr:`multi_input_socket`
145
+ :type index: ``int`` or ``None``
146
+ :return: source `Node` and its `Socket` per read position
147
+ :rtype: List[Tuple[:class:`~nodeeditor.node_node.Node`, :class:`~nodeeditor.node_socket.Socket`]]
148
+ """
149
+ socket = self.getMultiSocket(index)
150
+ if socket is None:
151
+ return []
152
+ sources = []
153
+ for edge in socket.orderedEdges():
154
+ other_socket = edge.getOtherSocket(socket)
155
+ sources.append(
156
+ (other_socket.node, other_socket) if other_socket is not None else (None, None)
157
+ )
158
+ return sources
159
+
160
+ def getOrderedValues(self, index: Optional[int] = None) -> List[Any]:
161
+ """
162
+ Evaluate every source of the ordered input `Socket` in read order.
163
+
164
+ The returned list always has one entry per connected `Edge`; a source
165
+ which fails to evaluate contributes ``None`` instead of raising, so
166
+ ``len(getOrderedValues())`` always equals the number of connections.
167
+
168
+ :param index: index of the input `Socket` or ``None`` to use
169
+ :attr:`multi_input_socket`
170
+ :type index: ``int`` or ``None``
171
+ :return: evaluated value per read position
172
+ :rtype: ``list``
173
+ """
174
+ values: List[Any] = []
175
+ for node, _socket in self.getOrderedSources(index):
176
+ if node is None:
177
+ values.append(None)
178
+ continue
179
+ try:
180
+ values.append(node.eval())
181
+ except Exception as e:
182
+ dumpException(e)
183
+ values.append(None)
184
+ return values
185
+
186
+ def getOrderedNodes(self, index: Optional[int] = None) -> List['Node']:
187
+ """
188
+ Get the source :class:`~nodeeditor.node_node.Node` (s) of the ordered
189
+ input `Socket` in read order.
190
+
191
+ :param index: index of the input `Socket` or ``None`` to use
192
+ :attr:`multi_input_socket`
193
+ :type index: ``int`` or ``None``
194
+ :return: source `Nodes`, one per read position
195
+ :rtype: List[:class:`~nodeeditor.node_node.Node`]
196
+ """
197
+ return [node for node, _socket in self.getOrderedSources(index)]
198
+
199
+ def describeInputOrder(self, index: Optional[int] = None) -> str:
200
+ """
201
+ Human readable one-line-per-position description of the input order.
202
+
203
+ Handy for tooltips and for logging what the node currently sees::
204
+
205
+ 0: Input
206
+ 1: Add
207
+
208
+ :param index: index of the input `Socket` or ``None`` to use
209
+ :attr:`multi_input_socket`
210
+ :type index: ``int`` or ``None``
211
+ :return: description of the current read order
212
+ :rtype: ``str``
213
+ """
214
+ lines = []
215
+ for position, (node, socket) in enumerate(self.getOrderedSources(index)):
216
+ title = node.title if node is not None else "?"
217
+ suffix = "" if socket is None else " [out %d]" % socket.index
218
+ lines.append("%d: %s%s" % (position, title, suffix))
219
+ return "\n".join(lines)
220
+
221
+ # edge order labels
222
+
223
+ @staticmethod
224
+ def orderLabel(position: int) -> str:
225
+ """
226
+ Display label for read position ``position`` (0-based).
227
+
228
+ :param position: 0-based read position
229
+ :type position: ``int``
230
+ :return: ``"#1"`` for position 0, ``"#2"`` for position 1, and so on
231
+ :rtype: ``str``
232
+ """
233
+ return "#%d" % (position + 1)
234
+
235
+ def updateInputOrderLabels(self, index: Optional[int] = None) -> None:
236
+ """
237
+ Write :meth:`orderLabel` onto every `Edge` of the ordered input
238
+ `Socket`. No-op when :attr:`show_input_order_labels` is ``False``.
239
+
240
+ Called automatically after every connection change and reorder, so
241
+ you only need it when toggling :attr:`show_input_order_labels` on
242
+ for a node that already has connections.
243
+
244
+ :param index: index of the input `Socket` or ``None`` to use
245
+ :attr:`multi_input_socket`
246
+ :type index: ``int`` or ``None``
247
+ """
248
+ if not self.show_input_order_labels:
249
+ return
250
+ socket = self.getMultiSocket(index)
251
+ if socket is None:
252
+ return
253
+ for position, edge in enumerate(socket.orderedEdges()):
254
+ try:
255
+ if edge.getLabel() != self.orderLabel(position):
256
+ edge.setLabel(self.orderLabel(position))
257
+ except Exception:
258
+ continue
259
+
260
+ def _clearDetachedOrderLabel(self, edge: 'Edge') -> None:
261
+ """Clear a ``#N`` label from an `Edge` that just detached from us.
262
+
263
+ Only labels still matching the managed pattern are cleared, so a
264
+ custom label set from code is never destroyed on disconnect.
265
+ """
266
+ if not self.show_input_order_labels or edge is None:
267
+ return
268
+ try:
269
+ label = edge.getLabel()
270
+ except Exception:
271
+ return
272
+ if not label:
273
+ return
274
+ if re.fullmatch(r"#\d+", label.strip()):
275
+ try:
276
+ edge.setLabel("")
277
+ except Exception:
278
+ pass
279
+
280
+ # manual ordering
281
+
282
+ def setEdgeOrder(self, edges: List['Edge'], index: Optional[int] = None) -> bool:
283
+ """
284
+ Set the read order of the connected `Edges` of the ordered input `Socket`.
285
+
286
+ ``edges`` may be a partial list; edges missing from it are appended
287
+ afterwards keeping their relative order.
288
+
289
+ :param edges: `Edges` of this node's ordered input `Socket`, first read
290
+ position first
291
+ :type edges: List[:class:`~nodeeditor.node_edge.Edge`]
292
+ :param index: index of the input `Socket` or ``None`` to use
293
+ :attr:`multi_input_socket`
294
+ :type index: ``int`` or ``None``
295
+ :return: ``True`` if the order changed
296
+ :rtype: ``bool``
297
+ """
298
+ socket = self.getMultiSocket(index)
299
+ if socket is None:
300
+ return False
301
+ if not socket.setEdgeOrder(edges):
302
+ return False
303
+ self.updateInputOrderLabels(index)
304
+ self.markDirty()
305
+ self.markDescendantsDirty()
306
+ return True
307
+
308
+ def setEdgeOrderByNodes(self, nodes: List['Node'], index: Optional[int] = None) -> bool:
309
+ """
310
+ Set the read order from a list of source :class:`~nodeeditor.node_node.Node` (s).
311
+
312
+ Each node moves to the first read position that is not already taken by
313
+ an earlier entry, so listing the same node twice moves it to position 0
314
+ and pushes the rest down.
315
+
316
+ :param nodes: source `Nodes`, first read position first
317
+ :type nodes: List[:class:`~nodeeditor.node_node.Node`]
318
+ :param index: index of the input `Socket` or ``None`` to use
319
+ :attr:`multi_input_socket`
320
+ :type index: ``int`` or ``None``
321
+ :return: ``True`` if the order changed
322
+ :rtype: ``bool``
323
+ """
324
+ socket = self.getMultiSocket(index)
325
+ if socket is None:
326
+ return False
327
+
328
+ ordered = socket.orderedEdges()
329
+ taken: List['Edge'] = []
330
+ for node in nodes or []:
331
+ for edge in ordered:
332
+ if edge in taken:
333
+ continue
334
+ other_socket = edge.getOtherSocket(socket)
335
+ if other_socket is not None and other_socket.node is node:
336
+ taken.append(edge)
337
+ break
338
+ return self.setEdgeOrder(taken, index)
339
+
340
+ def moveEdgeTo(self, edge: 'Edge', position: int, index: Optional[int] = None) -> bool:
341
+ """
342
+ Move a connected `Edge` to read position ``position``.
343
+
344
+ :param edge: :class:`~nodeeditor.node_edge.Edge` connected to this node
345
+ :type edge: :class:`~nodeeditor.node_edge.Edge`
346
+ :param position: target read position
347
+ :type position: ``int``
348
+ :param index: index of the input `Socket` or ``None`` to use
349
+ :attr:`multi_input_socket`
350
+ :type index: ``int`` or ``None``
351
+ :return: ``True`` if the order changed
352
+ :rtype: ``bool``
353
+ """
354
+ socket = self.getMultiSocket(index)
355
+ if socket is None:
356
+ return False
357
+ if not socket.moveEdgeTo(edge, position):
358
+ return False
359
+ self.updateInputOrderLabels(index)
360
+ self.markDirty()
361
+ self.markDescendantsDirty()
362
+ return True
363
+
364
+ def moveEdgeBy(self, edge: 'Edge', offset: int, index: Optional[int] = None) -> bool:
365
+ """
366
+ Shift a connected `Edge` by ``offset`` read positions.
367
+
368
+ Clamped at both ends, so ``offset=-1`` on the first input is a no-op.
369
+
370
+ :param edge: :class:`~nodeeditor.node_edge.Edge` connected to this node
371
+ :type edge: :class:`~nodeeditor.node_edge.Edge`
372
+ :param offset: how many positions to move, negative moves earlier
373
+ :type offset: ``int``
374
+ :param index: index of the input `Socket` or ``None`` to use
375
+ :attr:`multi_input_socket`
376
+ :type index: ``int`` or ``None``
377
+ :return: ``True`` if the order changed
378
+ :rtype: ``bool``
379
+ """
380
+ socket = self.getMultiSocket(index)
381
+ if socket is None:
382
+ return False
383
+ return self.moveEdgeTo(edge, socket.edgeIndex(edge) + offset, index)
384
+
385
+ def swapEdges(self, edge_a: 'Edge', edge_b: 'Edge', index: Optional[int] = None) -> bool:
386
+ """
387
+ Swap the read positions of two `Edges` connected to this node.
388
+
389
+ :param edge_a: first :class:`~nodeeditor.node_edge.Edge` to swap
390
+ :type edge_a: :class:`~nodeeditor.node_edge.Edge`
391
+ :param edge_b: second :class:`~nodeeditor.node_edge.Edge` to swap
392
+ :type edge_b: :class:`~nodeeditor.node_edge.Edge`
393
+ :param index: index of the input `Socket` or ``None`` to use
394
+ :attr:`multi_input_socket`
395
+ :type index: ``int`` or ``None``
396
+ :return: ``True`` if the order changed
397
+ :rtype: ``bool``
398
+ """
399
+ socket = self.getMultiSocket(index)
400
+ if socket is None:
401
+ return False
402
+ if not socket.swapEdges(edge_a, edge_b):
403
+ return False
404
+ self.updateInputOrderLabels(index)
405
+ self.markDirty()
406
+ self.markDescendantsDirty()
407
+ return True
408
+
409
+ def reverseEdgeOrder(self, index: Optional[int] = None) -> bool:
410
+ """
411
+ Reverse the read order of the connected `Edges`.
412
+
413
+ :param index: index of the input `Socket` or ``None`` to use
414
+ :attr:`multi_input_socket`
415
+ :type index: ``int`` or ``None``
416
+ :return: ``True`` if the order changed
417
+ :rtype: ``bool``
418
+ """
419
+ socket = self.getMultiSocket(index)
420
+ if socket is None:
421
+ return False
422
+ return self.setEdgeOrder(list(reversed(socket.orderedEdges())), index)
423
+
424
+ # events
425
+
426
+ def onEdgeConnectionChanged(self, new_edge: 'Edge') -> None:
427
+ """
428
+ Keep the read order gap free after any connection change.
429
+
430
+ This is the single hook every connection path already goes through
431
+ (``EdgeDragging``, ``EdgeRerouting``, ``Edge.remove``), so
432
+ :attr:`~nodeeditor.node_edge.Edge.input_index` stays a contiguous
433
+ ``0..n-1`` sequence and the ``#N`` labels (see
434
+ :attr:`show_input_order_labels`) stay in sync. An `Edge` that just
435
+ detached from our ordered socket gets its managed ``#N`` label
436
+ cleared. ``onInputChanged`` is intentionally not overridden
437
+ so existing subclass chains keep working.
438
+ """
439
+ self.compactEdgeOrder()
440
+ if (new_edge is not None
441
+ and new_edge.start_socket is None
442
+ and new_edge.end_socket is None):
443
+ # fully detached (Edge.remove): forget a managed #N label, but
444
+ # only when it still matches the pattern, so a custom label set
445
+ # from code is never destroyed. Edges that are still connected
446
+ # anywhere (e.g. our outputs, or rerouted elsewhere) are left
447
+ # alone; their current owner labels them through its own hook.
448
+ self._clearDetachedOrderLabel(new_edge)
449
+ self.updateInputOrderLabels()
450
+ super().onEdgeConnectionChanged(new_edge)