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
nodeeditor/node_edge.py
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
A module containing NodeEditor's class for representing Edge and Edge Type Constants.
|
|
4
|
+
"""
|
|
5
|
+
from collections import OrderedDict
|
|
6
|
+
from nodeeditor.node_graphics_edge import QDMGraphicsEdge
|
|
7
|
+
from nodeeditor.node_serializable import Serializable
|
|
8
|
+
from nodeeditor.utils_no_qt import dumpException
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING, List, Optional, Tuple, Any, Callable
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from nodeeditor.node_graphics_view import QDMGraphicsView
|
|
15
|
+
from nodeeditor.node_socket import Socket
|
|
16
|
+
from nodeeditor.node_scene import Scene
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
EDGE_TYPE_DIRECT = 1 #:
|
|
20
|
+
EDGE_TYPE_BEZIER = 2 #:
|
|
21
|
+
EDGE_TYPE_SQUARE = 3 #:
|
|
22
|
+
EDGE_TYPE_IMPROVED_SHARP = 4 #:
|
|
23
|
+
EDGE_TYPE_IMPROVED_BEZIER = 5 #:
|
|
24
|
+
EDGE_TYPE_DEFAULT = EDGE_TYPE_IMPROVED_BEZIER
|
|
25
|
+
|
|
26
|
+
DEBUG = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Edge(Serializable):
|
|
30
|
+
"""
|
|
31
|
+
Class for representing Edge in NodeEditor.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
#: class variable containing list of registered edge validators
|
|
35
|
+
edge_validators: List['function'] = []
|
|
36
|
+
|
|
37
|
+
def __init__(self, scene: 'Scene', start_socket: 'Socket' = None, end_socket: 'Socket' = None,
|
|
38
|
+
edge_type=EDGE_TYPE_DIRECT, label: str = "", input_index: int = -1) -> None:
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
:param scene: Reference to the :py:class:`~nodeeditor.node_scene.Scene`
|
|
42
|
+
:type scene: :py:class:`~nodeeditor.node_scene.Scene`
|
|
43
|
+
:param start_socket: Reference to the starting socket
|
|
44
|
+
:type start_socket: :py:class:`~nodeeditor.node_socket.Socket`
|
|
45
|
+
:param end_socket: Reference to the End socket or ``None``
|
|
46
|
+
:type end_socket: :py:class:`~nodeeditor.node_socket.Socket` or ``None``
|
|
47
|
+
:param edge_type: Constant determining type of edge. See :ref:`edge-type-constants`
|
|
48
|
+
:param label: Code-set text drawn on top of the edge. Not editable by
|
|
49
|
+
the user in the view; change it from code via ``edge.label = ...``
|
|
50
|
+
or ``edge.setLabel(...)`` at creation time or runtime.
|
|
51
|
+
:param input_index: Read position of this `Edge` on its **end** (input)
|
|
52
|
+
:class:`~nodeeditor.node_socket.Socket`, ``0`` being read first.
|
|
53
|
+
Only meaningful when ``end_socket`` is an input socket accepting
|
|
54
|
+
multiple `Edges`; pass ``-1`` (the default) to append the `Edge`,
|
|
55
|
+
i.e. read it last. See :ref:`multi-input-nodes`.
|
|
56
|
+
|
|
57
|
+
:Instance Attributes:
|
|
58
|
+
|
|
59
|
+
- **scene** - reference to the :class:`~nodeeditor.node_scene.Scene`
|
|
60
|
+
- **grEdge** - Instance of :class:`~nodeeditor.node_graphics_edge.QDMGraphicsEdge` subclass handling graphical representation in the ``QGraphicsScene``.
|
|
61
|
+
"""
|
|
62
|
+
super().__init__()
|
|
63
|
+
self.scene = scene
|
|
64
|
+
|
|
65
|
+
# default init
|
|
66
|
+
self._start_socket = None
|
|
67
|
+
self._end_socket = None
|
|
68
|
+
|
|
69
|
+
# Must be set *before* attaching the sockets: an ordered input
|
|
70
|
+
# `Socket` reads it in `addEdge()` to place this Edge at the right
|
|
71
|
+
# slot instead of appending it.
|
|
72
|
+
self.input_index = input_index
|
|
73
|
+
|
|
74
|
+
self.start_socket = start_socket
|
|
75
|
+
self.end_socket = end_socket
|
|
76
|
+
self._edge_type = edge_type
|
|
77
|
+
|
|
78
|
+
# create Graphics Edge instance
|
|
79
|
+
self.grEdge: QDMGraphicsEdge = self.createEdgeClassInstance()
|
|
80
|
+
|
|
81
|
+
if label:
|
|
82
|
+
self.setLabel(label)
|
|
83
|
+
|
|
84
|
+
self.scene.addEdge(self)
|
|
85
|
+
|
|
86
|
+
def __str__(self) -> str:
|
|
87
|
+
"""A short description of this `Edge` and the two sockets it connects"""
|
|
88
|
+
return "<Edge %s..%s -- S:%s E:%s>" % (
|
|
89
|
+
hex(id(self))[2:5], hex(id(self))[-3:],
|
|
90
|
+
self.start_socket, self.end_socket
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def input_index(self) -> int:
|
|
95
|
+
"""
|
|
96
|
+
Read position of this `Edge` on its end (input) :class:`~nodeeditor.node_socket.Socket`.
|
|
97
|
+
|
|
98
|
+
``0`` is read first, ``1`` next, and so on. Only meaningful for `Edges`
|
|
99
|
+
whose ``end_socket`` is an input socket; the owning
|
|
100
|
+
:class:`~nodeeditor.node_socket.Socket` owns and compacts the value, so
|
|
101
|
+
assign it from code only together with
|
|
102
|
+
:meth:`~nodeeditor.node_socket.Socket.compactEdgeOrder`. Use
|
|
103
|
+
:class:`~nodeeditor.node_multi_input_node.MultiInputNode` for a safe
|
|
104
|
+
reordering API.
|
|
105
|
+
|
|
106
|
+
:getter: current read position, or ``-1`` when not attached to an input socket
|
|
107
|
+
:setter: coerces to ``int``; anything invalid becomes ``-1``
|
|
108
|
+
:type: ``int``
|
|
109
|
+
"""
|
|
110
|
+
return self._input_index
|
|
111
|
+
|
|
112
|
+
@input_index.setter
|
|
113
|
+
def input_index(self, value) -> None:
|
|
114
|
+
try:
|
|
115
|
+
self._input_index = int(value)
|
|
116
|
+
except (TypeError, ValueError):
|
|
117
|
+
self._input_index = -1
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def start_socket(self) -> Optional["Socket"]:
|
|
121
|
+
"""
|
|
122
|
+
Start socket
|
|
123
|
+
|
|
124
|
+
:getter: Returns start :class:`~nodeeditor.node_socket.Socket`
|
|
125
|
+
:setter: Sets start :class:`~nodeeditor.node_socket.Socket` safely
|
|
126
|
+
:type: :class:`~nodeeditor.node_socket.Socket`
|
|
127
|
+
"""
|
|
128
|
+
return self._start_socket if self._start_socket else None
|
|
129
|
+
|
|
130
|
+
@start_socket.setter
|
|
131
|
+
def start_socket(self, value: "Socket") -> None:
|
|
132
|
+
# if we were assigned to some socket before, delete us from the socket
|
|
133
|
+
if self._start_socket is not None:
|
|
134
|
+
self._start_socket.removeEdge(self)
|
|
135
|
+
|
|
136
|
+
# assign new start socket
|
|
137
|
+
self._start_socket = value
|
|
138
|
+
# addEdge to the Socket class
|
|
139
|
+
if self.start_socket is not None:
|
|
140
|
+
self.start_socket.addEdge(self)
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def end_socket(self):
|
|
144
|
+
"""
|
|
145
|
+
End socket
|
|
146
|
+
|
|
147
|
+
:getter: Returns end :class:`~nodeeditor.node_socket.Socket` or ``None`` if not set
|
|
148
|
+
:setter: Sets end :class:`~nodeeditor.node_socket.Socket` safely
|
|
149
|
+
:type: :class:`~nodeeditor.node_socket.Socket` or ``None``
|
|
150
|
+
"""
|
|
151
|
+
return self._end_socket
|
|
152
|
+
|
|
153
|
+
@end_socket.setter
|
|
154
|
+
def end_socket(self, value) -> None:
|
|
155
|
+
# if we were assigned to some socket before, delete us from the socket
|
|
156
|
+
if self._end_socket is not None:
|
|
157
|
+
self._end_socket.removeEdge(self)
|
|
158
|
+
|
|
159
|
+
# assign new end socket
|
|
160
|
+
self._end_socket = value
|
|
161
|
+
# addEdge to the Socket class
|
|
162
|
+
if self.end_socket is not None:
|
|
163
|
+
self.end_socket.addEdge(self)
|
|
164
|
+
if not self.end_socket.is_input:
|
|
165
|
+
# a read position is only meaningful on an input socket
|
|
166
|
+
self.input_index = -1
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def edge_type(self):
|
|
170
|
+
"""
|
|
171
|
+
Edge type
|
|
172
|
+
|
|
173
|
+
:getter: get edge type constant for current ``Edge``. See :ref:`edge-type-constants`
|
|
174
|
+
:setter: sets new edge type. On background, creates new :class:`~nodeeditor.node_graphics_edge.QDMGraphicsEdge`
|
|
175
|
+
child class if necessary, adds this ``QGraphicsPathItem`` to the ``QGraphicsScene`` and updates edge sockets
|
|
176
|
+
positions.
|
|
177
|
+
"""
|
|
178
|
+
return self._edge_type
|
|
179
|
+
|
|
180
|
+
@edge_type.setter
|
|
181
|
+
def edge_type(self, value) -> None:
|
|
182
|
+
# assign new value
|
|
183
|
+
self._edge_type = value
|
|
184
|
+
|
|
185
|
+
# update the grEdge pathCalculator
|
|
186
|
+
self.grEdge.createEdgePathCalculator()
|
|
187
|
+
|
|
188
|
+
if self.start_socket is not None:
|
|
189
|
+
self.updatePositions()
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def label(self) -> str:
|
|
193
|
+
"""
|
|
194
|
+
Code-set text drawn on top of the edge.
|
|
195
|
+
|
|
196
|
+
:getter: current label text (``""`` when unset)
|
|
197
|
+
:setter: sets new label text; ``""``/``None`` hides the label.
|
|
198
|
+
Not user-editable in the view -- change only from code, both at
|
|
199
|
+
creation time (``Edge(..., label="...")``) and at runtime
|
|
200
|
+
(``edge.label = "..."``).
|
|
201
|
+
"""
|
|
202
|
+
return self.grEdge.label()
|
|
203
|
+
|
|
204
|
+
@label.setter
|
|
205
|
+
def label(self, value: str) -> None:
|
|
206
|
+
self.setLabel(value)
|
|
207
|
+
|
|
208
|
+
def getLabel(self) -> str:
|
|
209
|
+
"""Return the current code-set edge label."""
|
|
210
|
+
return self.grEdge.label()
|
|
211
|
+
|
|
212
|
+
def setLabel(self, text: str) -> None:
|
|
213
|
+
"""Set the text drawn on top of the edge (``""`` hides it)."""
|
|
214
|
+
self.grEdge.setLabel(text)
|
|
215
|
+
|
|
216
|
+
@classmethod
|
|
217
|
+
def getEdgeValidators(cls):
|
|
218
|
+
"""Return the list of Edge Validator Callbacks"""
|
|
219
|
+
return cls.edge_validators
|
|
220
|
+
|
|
221
|
+
@classmethod
|
|
222
|
+
def registerEdgeValidator(cls, validator_callback: 'function') -> None:
|
|
223
|
+
"""Register Edge Validator Callback
|
|
224
|
+
|
|
225
|
+
:param validator_callback: A function handle to validate Edge
|
|
226
|
+
:type validator_callback: `function`
|
|
227
|
+
"""
|
|
228
|
+
cls.edge_validators.append(validator_callback)
|
|
229
|
+
|
|
230
|
+
@classmethod
|
|
231
|
+
def validateEdge(cls, start_socket: 'Socket', end_socket: 'Socket') -> bool:
|
|
232
|
+
"""Validate Edge agains all registered `Edge Validator Callbacks`
|
|
233
|
+
|
|
234
|
+
:param start_socket: Starting :class:`~nodeeditor.node_socket.Socket` of Edge to check
|
|
235
|
+
:type start_socket: :class:`~nodeeditor.node_socket.Socket`
|
|
236
|
+
:param end_socket: Target/End :class:`~nodeeditor.node_socket.Socket` of Edge to check
|
|
237
|
+
:type end_socket: :class:`~nodeeditor.node_socket.Socket`
|
|
238
|
+
:return: ``True`` if the Edge is valid or ``False`` if not
|
|
239
|
+
:rtype: ``bool``
|
|
240
|
+
"""
|
|
241
|
+
for validator in cls.getEdgeValidators():
|
|
242
|
+
if not validator(start_socket, end_socket):
|
|
243
|
+
return False
|
|
244
|
+
return True
|
|
245
|
+
|
|
246
|
+
def reconnect(self, from_socket: 'Socket', to_socket: 'Socket') -> None:
|
|
247
|
+
"""Helper function which reconnects edge `from_socket` to `to_socket`"""
|
|
248
|
+
if self.start_socket == from_socket:
|
|
249
|
+
self.start_socket = to_socket
|
|
250
|
+
elif self.end_socket == from_socket:
|
|
251
|
+
self.end_socket = to_socket
|
|
252
|
+
|
|
253
|
+
def getGraphicsEdgeClass(self):
|
|
254
|
+
"""Returns the class representing Graphics Edge"""
|
|
255
|
+
return QDMGraphicsEdge
|
|
256
|
+
|
|
257
|
+
def createEdgeClassInstance(self):
|
|
258
|
+
"""
|
|
259
|
+
Create instance of grEdge class
|
|
260
|
+
:return: Instance of `grEdge` class representing the Graphics Edge in the grScene
|
|
261
|
+
"""
|
|
262
|
+
self.grEdge = self.getGraphicsEdgeClass()(self)
|
|
263
|
+
self.scene.grScene.addItem(self.grEdge)
|
|
264
|
+
if self.start_socket is not None:
|
|
265
|
+
self.updatePositions()
|
|
266
|
+
return self.grEdge
|
|
267
|
+
|
|
268
|
+
def getOtherSocket(self, known_socket: 'Socket'):
|
|
269
|
+
"""
|
|
270
|
+
Returns the opposite socket on this ``Edge``
|
|
271
|
+
|
|
272
|
+
:param known_socket: Provide known :class:`~nodeeditor.node_socket.Socket` to be able to determine the opposite one.
|
|
273
|
+
:type known_socket: :class:`~nodeeditor.node_socket.Socket`
|
|
274
|
+
:return: The oposite socket on this ``Edge`` or ``None``
|
|
275
|
+
:rtype: :class:`~nodeeditor.node_socket.Socket` or ``None``
|
|
276
|
+
"""
|
|
277
|
+
return self.start_socket if known_socket == self.end_socket else self.end_socket
|
|
278
|
+
|
|
279
|
+
def doSelect(self, new_state: bool = True) -> None:
|
|
280
|
+
"""
|
|
281
|
+
Provide the safe selecting/deselecting operation. In the background it takes care about the flags, notifications
|
|
282
|
+
and storing history for undo/redo.
|
|
283
|
+
|
|
284
|
+
:param new_state: ``True`` if you want to select the ``Edge``, ``False`` if you want to deselect the ``Edge``
|
|
285
|
+
:type new_state: ``bool``
|
|
286
|
+
"""
|
|
287
|
+
self.grEdge.doSelect(new_state)
|
|
288
|
+
|
|
289
|
+
def updatePositions(self) -> None:
|
|
290
|
+
"""
|
|
291
|
+
Updates the internal `Graphics Edge` positions.
|
|
292
|
+
|
|
293
|
+
Collapsed-group aware: if an endpoint lives in a collapsed Group,
|
|
294
|
+
the endpoint is overridden to that group's per-edge stub row.
|
|
295
|
+
Logical sockets are never mutated. Internal edges of a collapsed
|
|
296
|
+
group stay hidden.
|
|
297
|
+
"""
|
|
298
|
+
try:
|
|
299
|
+
if self.start_socket is None:
|
|
300
|
+
return
|
|
301
|
+
if getattr(self, 'grEdge', None) is None:
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
start_node = getattr(self.start_socket, 'node', None)
|
|
305
|
+
end_node = getattr(self.end_socket, 'node', None) if self.end_socket is not None else None
|
|
306
|
+
|
|
307
|
+
start_group = None
|
|
308
|
+
end_group = None
|
|
309
|
+
try:
|
|
310
|
+
if start_node is not None:
|
|
311
|
+
start_group = self.scene.findCollapsedGroupForNode(start_node)
|
|
312
|
+
if end_node is not None:
|
|
313
|
+
end_group = self.scene.findCollapsedGroupForNode(end_node)
|
|
314
|
+
except Exception:
|
|
315
|
+
start_group = end_group = None
|
|
316
|
+
|
|
317
|
+
# internal edge of a collapsed group -> keep hidden
|
|
318
|
+
try:
|
|
319
|
+
if start_group is not None and start_group is end_group and end_group is not None:
|
|
320
|
+
try:
|
|
321
|
+
self.grEdge.hide()
|
|
322
|
+
except Exception:
|
|
323
|
+
pass
|
|
324
|
+
return
|
|
325
|
+
except Exception:
|
|
326
|
+
pass
|
|
327
|
+
|
|
328
|
+
# source
|
|
329
|
+
source_pos = None
|
|
330
|
+
try:
|
|
331
|
+
if start_group is not None:
|
|
332
|
+
p = start_group.stubPosFor(self)
|
|
333
|
+
if p is not None:
|
|
334
|
+
source_pos = [p.x(), p.y()]
|
|
335
|
+
if source_pos is None:
|
|
336
|
+
gs = getattr(self.start_socket, 'grSocket', None)
|
|
337
|
+
if gs is None:
|
|
338
|
+
return
|
|
339
|
+
sp = gs.scenePos()
|
|
340
|
+
source_pos = [sp.x(), sp.y()]
|
|
341
|
+
except Exception:
|
|
342
|
+
return
|
|
343
|
+
self.grEdge.setSource(*source_pos)
|
|
344
|
+
|
|
345
|
+
if self.end_socket is not None:
|
|
346
|
+
end_pos = None
|
|
347
|
+
try:
|
|
348
|
+
if end_group is not None:
|
|
349
|
+
p = end_group.stubPosFor(self)
|
|
350
|
+
if p is not None:
|
|
351
|
+
end_pos = [p.x(), p.y()]
|
|
352
|
+
if end_pos is None:
|
|
353
|
+
gs = getattr(self.end_socket, 'grSocket', None)
|
|
354
|
+
if gs is None:
|
|
355
|
+
self.grEdge.setDestination(*source_pos)
|
|
356
|
+
self.grEdge.update()
|
|
357
|
+
return
|
|
358
|
+
ep = gs.scenePos()
|
|
359
|
+
end_pos = [ep.x(), ep.y()]
|
|
360
|
+
except Exception:
|
|
361
|
+
end_pos = list(source_pos)
|
|
362
|
+
self.grEdge.setDestination(*end_pos)
|
|
363
|
+
else:
|
|
364
|
+
self.grEdge.setDestination(*source_pos)
|
|
365
|
+
self.grEdge.update()
|
|
366
|
+
except Exception:
|
|
367
|
+
try:
|
|
368
|
+
dumpException()
|
|
369
|
+
except Exception:
|
|
370
|
+
pass
|
|
371
|
+
|
|
372
|
+
def remove_from_sockets(self) -> None:
|
|
373
|
+
"""
|
|
374
|
+
Helper function which sets start and end :class:`~nodeeditor.node_socket.Socket` to ``None``
|
|
375
|
+
"""
|
|
376
|
+
self.end_socket = None
|
|
377
|
+
self.start_socket = None
|
|
378
|
+
# forget the read position; re-attaching appends at the end by default
|
|
379
|
+
self.input_index = -1
|
|
380
|
+
|
|
381
|
+
def remove(self, silent_for_socket: 'Socket' = None, silent: bool=False) -> None:
|
|
382
|
+
"""
|
|
383
|
+
Safely remove this Edge.
|
|
384
|
+
|
|
385
|
+
Removes `Graphics Edge` from the ``QGraphicsScene`` and it's reference to all GC to clean it up.
|
|
386
|
+
Notifies nodes previously connected :class:`~nodeeditor.node_node.Node` (s) about this event.
|
|
387
|
+
|
|
388
|
+
Triggers Nodes':
|
|
389
|
+
|
|
390
|
+
- :py:meth:`~nodeeditor.node_node.Node.onEdgeConnectionChanged`
|
|
391
|
+
- :py:meth:`~nodeeditor.node_node.Node.onInputChanged`
|
|
392
|
+
|
|
393
|
+
:param silent_for_socket: :class:`~nodeeditor.node_socket.Socket` of a :class:`~nodeeditor.node_node.Node` which
|
|
394
|
+
won't be notified, when this ``Edge`` is going to be removed
|
|
395
|
+
:type silent_for_socket: :class:`~nodeeditor.node_socket.Socket`
|
|
396
|
+
:param silent: ``True`` if no events should be triggered during removing
|
|
397
|
+
:type silent: ``bool``
|
|
398
|
+
"""
|
|
399
|
+
old_sockets = [self.start_socket, self.end_socket]
|
|
400
|
+
|
|
401
|
+
# ugly hack, since I noticed that even when you remove grEdge from scene,
|
|
402
|
+
# sometimes it stays there! How dare you Qt!
|
|
403
|
+
if DEBUG:
|
|
404
|
+
print(" - hide grEdge")
|
|
405
|
+
self.grEdge.hide()
|
|
406
|
+
|
|
407
|
+
if DEBUG:
|
|
408
|
+
print(" - remove grEdge", self.grEdge)
|
|
409
|
+
self.scene.grScene.removeItem(self.grEdge)
|
|
410
|
+
if DEBUG:
|
|
411
|
+
print(" grEdge:", self.grEdge)
|
|
412
|
+
|
|
413
|
+
self.scene.grScene.update()
|
|
414
|
+
|
|
415
|
+
if DEBUG:
|
|
416
|
+
print("# Removing Edge", self)
|
|
417
|
+
if DEBUG:
|
|
418
|
+
print(" - remove edge from all sockets")
|
|
419
|
+
self.remove_from_sockets()
|
|
420
|
+
if DEBUG:
|
|
421
|
+
print(" - remove edge from scene")
|
|
422
|
+
try:
|
|
423
|
+
self.scene.removeEdge(self)
|
|
424
|
+
except ValueError:
|
|
425
|
+
pass
|
|
426
|
+
if DEBUG:
|
|
427
|
+
print(" - everything is done.")
|
|
428
|
+
|
|
429
|
+
try:
|
|
430
|
+
# notify nodes from old sockets
|
|
431
|
+
for socket in old_sockets:
|
|
432
|
+
if socket and socket.node:
|
|
433
|
+
if silent:
|
|
434
|
+
continue
|
|
435
|
+
if silent_for_socket is not None and socket == silent_for_socket:
|
|
436
|
+
# if we requested silence for Socket and it's this one, skip notifications
|
|
437
|
+
continue
|
|
438
|
+
|
|
439
|
+
# notify Socket's Node
|
|
440
|
+
socket.node.onEdgeConnectionChanged(self)
|
|
441
|
+
if socket.is_input:
|
|
442
|
+
socket.node.onInputChanged(socket)
|
|
443
|
+
|
|
444
|
+
except Exception as e:
|
|
445
|
+
dumpException(e)
|
|
446
|
+
|
|
447
|
+
def serialize(self) -> OrderedDict:
|
|
448
|
+
"""
|
|
449
|
+
Serialize this `Edge` into an ``OrderedDict`` for storing in memory or on disk.
|
|
450
|
+
|
|
451
|
+
:return: data serialized in ``OrderedDict``
|
|
452
|
+
:rtype: ``OrderedDict``
|
|
453
|
+
"""
|
|
454
|
+
return OrderedDict([
|
|
455
|
+
('id', self.id),
|
|
456
|
+
('edge_type', self.edge_type),
|
|
457
|
+
('start', self.start_socket.id if self.start_socket is not None else None),
|
|
458
|
+
('end', self.end_socket.id if self.end_socket is not None else None),
|
|
459
|
+
('label', self.getLabel()),
|
|
460
|
+
('input_index', self.input_index),
|
|
461
|
+
])
|
|
462
|
+
|
|
463
|
+
def deserialize(self, data: dict, hashmap: dict = {}, restore_id: bool = True, *args, **kwargs) -> bool:
|
|
464
|
+
"""
|
|
465
|
+
Deserialize this `Edge` from a ``dict`` of serialized data.
|
|
466
|
+
|
|
467
|
+
Both sockets are looked up in the ``hashmap``, so they have to be
|
|
468
|
+
deserialized before the edge that connects them.
|
|
469
|
+
|
|
470
|
+
:param data: Dictionary containing serialized data
|
|
471
|
+
:type data: ``dict``
|
|
472
|
+
:param hashmap: Helper dictionary containing references (by id == key) to existing objects
|
|
473
|
+
:type hashmap: ``dict``
|
|
474
|
+
:param restore_id: True if we are creating new Edges. False is useful when loading existing
|
|
475
|
+
Edges of which we want to keep the existing object's `id`.
|
|
476
|
+
:type restore_id: bool
|
|
477
|
+
:return: ``True`` if deserialization was successful, otherwise ``False``
|
|
478
|
+
:rtype: ``bool``
|
|
479
|
+
"""
|
|
480
|
+
try:
|
|
481
|
+
if restore_id:
|
|
482
|
+
self.id = data['id']
|
|
483
|
+
# Restore the read position *before* attaching the sockets: an
|
|
484
|
+
# ordered input `Socket` inserts the `Edge` at this slot. Old files
|
|
485
|
+
# without the key load as -1, i.e. order of connection in the file.
|
|
486
|
+
self.input_index = data.get('input_index', -1)
|
|
487
|
+
try:
|
|
488
|
+
start_id = data.get('start', None)
|
|
489
|
+
end_id = data.get('end', None)
|
|
490
|
+
except Exception:
|
|
491
|
+
return False
|
|
492
|
+
if start_id not in hashmap or end_id not in hashmap:
|
|
493
|
+
return False
|
|
494
|
+
self.start_socket = hashmap[start_id]
|
|
495
|
+
self.end_socket = hashmap[end_id]
|
|
496
|
+
if self.start_socket is None or self.end_socket is None:
|
|
497
|
+
return False
|
|
498
|
+
self.edge_type = data['edge_type']
|
|
499
|
+
# 'label' is optional so old files without it still load
|
|
500
|
+
self.setLabel(data.get('label', "") or "")
|
|
501
|
+
return True
|
|
502
|
+
except Exception:
|
|
503
|
+
try:
|
|
504
|
+
dumpException()
|
|
505
|
+
except Exception:
|
|
506
|
+
pass
|
|
507
|
+
return False
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
# Example: using validators for Edge
|
|
511
|
+
# You can register edge validators wherever you want, even here...
|
|
512
|
+
# However if you do use overridden Edge, you should call registerEdgeValidator on that overridden class
|
|
513
|
+
#
|
|
514
|
+
# from nodeeditor.node_edge_validators import *
|
|
515
|
+
# Edge.registerEdgeValidator(edge_validator_debug)
|
|
516
|
+
# Edge.registerEdgeValidator(edge_cannot_connect_two_outputs_or_two_inputs)
|
|
517
|
+
# Edge.registerEdgeValidator(edge_cannot_connect_input_and_output_of_same_node)
|
|
518
|
+
# Edge.registerEdgeValidator(edge_cannot_connect_input_and_output_of_different_color)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
A module containing the Edge Dragging functionality
|
|
4
|
+
"""
|
|
5
|
+
from nodeeditor.node_graphics_socket import QDMGraphicsSocket
|
|
6
|
+
from nodeeditor.node_edge import EDGE_TYPE_DEFAULT
|
|
7
|
+
from nodeeditor.utils import dumpException
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from qtpy.QtWidgets import QGraphicsItem
|
|
13
|
+
from nodeeditor.node_socket import Socket
|
|
14
|
+
from nodeeditor.node_edge import Edge
|
|
15
|
+
from nodeeditor.node_graphics_view import QDMGraphicsView
|
|
16
|
+
|
|
17
|
+
DEBUG = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class EdgeDragging:
|
|
21
|
+
"""Class handling the dragging of a new :class:`~nodeeditor.node_edge.Edge` between sockets"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, grView: 'QDMGraphicsView') -> None:
|
|
24
|
+
"""
|
|
25
|
+
:param grView: reference to the :class:`~nodeeditor.node_graphics_view.QDMGraphicsView`
|
|
26
|
+
:type grView: :class:`~nodeeditor.node_graphics_view.QDMGraphicsView`
|
|
27
|
+
|
|
28
|
+
:Instance Attributes:
|
|
29
|
+
|
|
30
|
+
- **grView** - reference to the :class:`~nodeeditor.node_graphics_view.QDMGraphicsView`
|
|
31
|
+
- **drag_edge** - the :class:`~nodeeditor.node_edge.Edge` currently being dragged
|
|
32
|
+
- **drag_start_socket** - the :class:`~nodeeditor.node_socket.Socket` where the drag started
|
|
33
|
+
"""
|
|
34
|
+
self.grView = grView
|
|
35
|
+
# initializing these variable to know we're using them in this class...
|
|
36
|
+
self.drag_edge: Edge | None = None
|
|
37
|
+
self.drag_start_socket: Socket | None = None
|
|
38
|
+
|
|
39
|
+
def getEdgeClass(self):
|
|
40
|
+
"""Helper function to get the Edge class. Using what Scene class provides"""
|
|
41
|
+
return self.grView.grScene.scene.getEdgeClass()
|
|
42
|
+
|
|
43
|
+
def updateDestination(self, x: float, y: float) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Update the end point of our dragging edge
|
|
46
|
+
|
|
47
|
+
:param x: new X scene position
|
|
48
|
+
:param y: new Y scene position
|
|
49
|
+
"""
|
|
50
|
+
# according to sentry: 'NoneType' object has no attribute 'grEdge'
|
|
51
|
+
if self.drag_edge is None:
|
|
52
|
+
print(">>> Warning: drag_edge is None!")
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
if self.drag_edge.grEdge is None:
|
|
56
|
+
print(">>> Warning: drag_edge.grEdge is None!")
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
self.drag_edge.grEdge.setDestination(x, y)
|
|
60
|
+
self.drag_edge.grEdge.update()
|
|
61
|
+
|
|
62
|
+
def edgeDragStart(self, item: QDMGraphicsSocket) -> None:
|
|
63
|
+
"""Code handling the start of a dragging an `Edge` operation"""
|
|
64
|
+
try:
|
|
65
|
+
if DEBUG:
|
|
66
|
+
print('View::edgeDragStart ~ Start dragging edge')
|
|
67
|
+
if DEBUG:
|
|
68
|
+
print('View::edgeDragStart ~ assign Start Socket to:', item.socket)
|
|
69
|
+
self.drag_start_socket = item.socket
|
|
70
|
+
self.drag_edge = self.getEdgeClass()(
|
|
71
|
+
item.socket.node.scene, item.socket, None, EDGE_TYPE_DEFAULT)
|
|
72
|
+
|
|
73
|
+
if self.drag_edge is None:
|
|
74
|
+
print(">>> Warning: Failed to create drag_edge")
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
if self.drag_edge.grEdge is None:
|
|
78
|
+
print(">>> Warning: drag_edge.grEdge is None")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
self.drag_edge.grEdge.makeUnselectable()
|
|
82
|
+
|
|
83
|
+
if DEBUG:
|
|
84
|
+
print('View::edgeDragStart ~ dragEdge:', self.drag_edge)
|
|
85
|
+
|
|
86
|
+
except Exception as e:
|
|
87
|
+
dumpException(e)
|
|
88
|
+
|
|
89
|
+
def edgeDragEnd(self, item: 'QGraphicsItem'):
|
|
90
|
+
"""Code handling the end of the dragging an `Edge` operation. If this code returns True then skip the
|
|
91
|
+
rest of the mouse event processing. Can be called with ``None`` to cancel the edge dragging mode
|
|
92
|
+
|
|
93
|
+
:param item: Item in the `Graphics Scene` where we ended dragging an `Edge`
|
|
94
|
+
:type item: ``QGraphicsItem``
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
# Cancel edge if clicking on empty space or receiving None (ESC pressed)
|
|
98
|
+
if item is None or not isinstance(item, QDMGraphicsSocket):
|
|
99
|
+
self.cancelDragEdge()
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
# early out - clicked on something else than Socket
|
|
103
|
+
if not isinstance(item, QDMGraphicsSocket):
|
|
104
|
+
self.grView.resetMode()
|
|
105
|
+
if DEBUG:
|
|
106
|
+
print('View::edgeDragEnd ~ End dragging edge early')
|
|
107
|
+
|
|
108
|
+
# don't notify sockets about removing drag_edge
|
|
109
|
+
if self.drag_edge is not None:
|
|
110
|
+
self.drag_edge.remove(silent=True)
|
|
111
|
+
self.drag_edge = None
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
# clicked on socket
|
|
115
|
+
if isinstance(item, QDMGraphicsSocket):
|
|
116
|
+
if self.drag_edge is None or self.drag_start_socket is None:
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
# check if edge would be valid
|
|
120
|
+
if not self.drag_edge.validateEdge(self.drag_start_socket, item.socket):
|
|
121
|
+
print("NOT VALID EDGE")
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
# regular processing of drag edge
|
|
125
|
+
self.grView.resetMode()
|
|
126
|
+
|
|
127
|
+
if DEBUG:
|
|
128
|
+
print('View::edgeDragEnd ~ End dragging edge')
|
|
129
|
+
# don't notify sockets about removing drag_edge
|
|
130
|
+
self.drag_edge.remove(silent=True)
|
|
131
|
+
self.drag_edge = None
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
if item.socket != self.drag_start_socket:
|
|
135
|
+
# First verify both sockets exist
|
|
136
|
+
if self.drag_start_socket is None:
|
|
137
|
+
print(">>> Warning: drag_start_socket is None!")
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
# First remove old edges / send notifications
|
|
141
|
+
for socket in (item.socket, self.drag_start_socket):
|
|
142
|
+
if socket is None:
|
|
143
|
+
print(">>> Warning: socket is None!")
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
if not socket.is_multi_edges:
|
|
147
|
+
if socket.is_input:
|
|
148
|
+
socket.removeAllEdges(silent=True)
|
|
149
|
+
else:
|
|
150
|
+
socket.removeAllEdges(silent=False)
|
|
151
|
+
|
|
152
|
+
# Create new Edge
|
|
153
|
+
new_edge = self.getEdgeClass()(item.socket.node.scene, self.drag_start_socket,
|
|
154
|
+
item.socket, edge_type=EDGE_TYPE_DEFAULT)
|
|
155
|
+
if DEBUG:
|
|
156
|
+
print("View::edgeDragEnd ~ created new edge:", new_edge,
|
|
157
|
+
"connecting", new_edge.start_socket, "<-->", new_edge.end_socket)
|
|
158
|
+
|
|
159
|
+
# Send notifications for the new edge
|
|
160
|
+
for socket in [self.drag_start_socket, item.socket]:
|
|
161
|
+
# @TODO: Add possibility (ie when an input edge was replaced) to be silent and don't trigger change
|
|
162
|
+
socket.node.onEdgeConnectionChanged(new_edge)
|
|
163
|
+
if socket.is_input:
|
|
164
|
+
socket.node.onInputChanged(socket)
|
|
165
|
+
|
|
166
|
+
self.grView.grScene.scene.history.storeHistory(
|
|
167
|
+
"Created new edge by dragging", setModified=True)
|
|
168
|
+
return True
|
|
169
|
+
except Exception as e:
|
|
170
|
+
dumpException(e)
|
|
171
|
+
|
|
172
|
+
if DEBUG:
|
|
173
|
+
print('View::edgeDragEnd ~ everything done.')
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
def cancelDragEdge(self) -> None:
|
|
177
|
+
"""Cancel the edge dragging operation and clean up"""
|
|
178
|
+
self.grView.resetMode()
|
|
179
|
+
if DEBUG:
|
|
180
|
+
print('View::cancelDragEdge ~ Cancel dragging edge')
|
|
181
|
+
|
|
182
|
+
if self.drag_edge is not None:
|
|
183
|
+
self.drag_edge.remove(silent=True)
|
|
184
|
+
self.drag_edge = None
|
|
185
|
+
self.drag_start_socket = None
|