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/__init__.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError
|
|
4
|
+
from importlib.metadata import version as _dist_version
|
|
5
|
+
|
|
6
|
+
__name__ = 'Node Editor'
|
|
7
|
+
__author__ = 'Azad Kshitij'
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
__version__ = _dist_version("nodeeditor-qtpy")
|
|
11
|
+
except PackageNotFoundError: # running from a source checkout
|
|
12
|
+
__version__ = "0.8.1"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_QT_API_NAME, _QT_API_VERSION = None, None
|
|
16
|
+
|
|
17
|
+
if _QT_API_NAME is None:
|
|
18
|
+
try:
|
|
19
|
+
from PyQt5.QtCore import PYQT_VERSION_STR # type: ignore
|
|
20
|
+
_QT_API_NAME, _QT_API_VERSION = "pyqt5", PYQT_VERSION_STR
|
|
21
|
+
except ImportError:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
if _QT_API_NAME is None:
|
|
25
|
+
try:
|
|
26
|
+
from PySide2 import __version__ # type: ignore
|
|
27
|
+
_QT_API_NAME, _QT_API_VERSION = "pyside2", __version__
|
|
28
|
+
except ImportError:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
if _QT_API_NAME is None:
|
|
32
|
+
try:
|
|
33
|
+
from PyQt6.QtCore import PYQT_VERSION_STR
|
|
34
|
+
_QT_API_NAME, _QT_API_VERSION = "pyqt6", PYQT_VERSION_STR
|
|
35
|
+
except ImportError:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
if _QT_API_NAME is None:
|
|
39
|
+
try:
|
|
40
|
+
from PySide6 import __version__ # type: ignore
|
|
41
|
+
_QT_API_NAME, _QT_API_VERSION = "pyside6", __version__
|
|
42
|
+
except ImportError:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
if _QT_API_NAME is None:
|
|
46
|
+
raise ImportError(
|
|
47
|
+
"No Qt binding found. Install nodeeditor-qtpy with one, e.g. "
|
|
48
|
+
"'pip install nodeeditor-qtpy[pyqt6]' or "
|
|
49
|
+
"'pip install nodeeditor-qtpy[pyside6]', "
|
|
50
|
+
"or install PyQt5/PySide2/PyQt6/PySide6 directly."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Export color configuration classes for easier access
|
|
54
|
+
from nodeeditor.node_colors_config import (
|
|
55
|
+
NodeEditorColorScheme,
|
|
56
|
+
EdgeColors,
|
|
57
|
+
SocketColors,
|
|
58
|
+
get_color_scheme,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Export the socket shape system for easier access
|
|
62
|
+
from nodeeditor.node_socket_shapes import (
|
|
63
|
+
SocketShape,
|
|
64
|
+
SocketShapeContext,
|
|
65
|
+
CircleShape,
|
|
66
|
+
SquareShape,
|
|
67
|
+
TriangleShape,
|
|
68
|
+
ChevronShape,
|
|
69
|
+
register_socket_shape,
|
|
70
|
+
get_socket_shape,
|
|
71
|
+
SOCKET_SHAPES,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Export the graph algorithm exception
|
|
75
|
+
from nodeeditor.node_graph import GraphCycleError
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
__all__ = [
|
|
79
|
+
"NodeEditorColorScheme",
|
|
80
|
+
"EdgeColors",
|
|
81
|
+
"SocketColors",
|
|
82
|
+
"get_color_scheme",
|
|
83
|
+
"SocketShape",
|
|
84
|
+
"SocketShapeContext",
|
|
85
|
+
"CircleShape",
|
|
86
|
+
"SquareShape",
|
|
87
|
+
"TriangleShape",
|
|
88
|
+
"ChevronShape",
|
|
89
|
+
"register_socket_shape",
|
|
90
|
+
"get_socket_shape",
|
|
91
|
+
"SOCKET_SHAPES",
|
|
92
|
+
"GraphCycleError",
|
|
93
|
+
]
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
A module for centralized color configuration for the Node Editor.
|
|
4
|
+
|
|
5
|
+
This module provides a single point to manage and update all colors used in:
|
|
6
|
+
- Edge graphics (default, selected, hovered, dragging)
|
|
7
|
+
- Socket graphics (background colors per type, highlighting, outlines)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from qtpy.QtGui import QColor
|
|
11
|
+
from typing import Dict, List
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseColors:
|
|
15
|
+
"""Base class for color configurations
|
|
16
|
+
_Parameters:
|
|
17
|
+
- **_default**: Default color
|
|
18
|
+
- **_selected**: Selected color
|
|
19
|
+
- **_hovered**: Hovered color
|
|
20
|
+
- **_dragging**: Dragging color
|
|
21
|
+
"""
|
|
22
|
+
def __init__(self):
|
|
23
|
+
"""Initialize default colors
|
|
24
|
+
"""
|
|
25
|
+
self._default: QColor = QColor("#B08848")
|
|
26
|
+
self._selected: QColor = QColor("#00ff00")
|
|
27
|
+
self._hovered: QColor = QColor("#FF37A6FF")
|
|
28
|
+
self._dragging: QColor = QColor("#333334")
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def default(self) -> QColor:
|
|
32
|
+
"""Get default edge color"""
|
|
33
|
+
return QColor(self._default)
|
|
34
|
+
|
|
35
|
+
@default.setter
|
|
36
|
+
def default(self, color):
|
|
37
|
+
"""Set default edge color"""
|
|
38
|
+
self._default = self._parse_color(color)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def selected(self) -> QColor:
|
|
42
|
+
"""Get selected edge color"""
|
|
43
|
+
return QColor(self._selected)
|
|
44
|
+
|
|
45
|
+
@selected.setter
|
|
46
|
+
def selected(self, color):
|
|
47
|
+
"""Set selected edge color"""
|
|
48
|
+
self._selected = self._parse_color(color)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def hovered(self) -> QColor:
|
|
52
|
+
"""Get hovered edge color"""
|
|
53
|
+
return QColor(self._hovered)
|
|
54
|
+
|
|
55
|
+
@hovered.setter
|
|
56
|
+
def hovered(self, color):
|
|
57
|
+
"""Set hovered edge color"""
|
|
58
|
+
self._hovered = self._parse_color(color)
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def _parse_color(color) -> QColor:
|
|
62
|
+
"""Parse color from various formats to QColor"""
|
|
63
|
+
if isinstance(color, QColor):
|
|
64
|
+
return QColor(color)
|
|
65
|
+
elif isinstance(color, str):
|
|
66
|
+
return QColor(color)
|
|
67
|
+
elif isinstance(color, tuple) and len(color) >= 3:
|
|
68
|
+
# RGB or RGBA tuple
|
|
69
|
+
return QColor(*color)
|
|
70
|
+
else:
|
|
71
|
+
raise ValueError(f"Invalid color format: {color}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class EdgeColors(BaseColors):
|
|
75
|
+
"""Configuration for Edge colors"""
|
|
76
|
+
|
|
77
|
+
def __init__(self):
|
|
78
|
+
"""Initialize default edge colors"""
|
|
79
|
+
self._default = QColor("#B08848")
|
|
80
|
+
self._selected = QColor("#ff00d0")
|
|
81
|
+
self._hovered = QColor("#FF37A6FF")
|
|
82
|
+
self._dragging = QColor("#333334")
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def default(self) -> QColor:
|
|
86
|
+
"""Get default edge color"""
|
|
87
|
+
return QColor(self._default)
|
|
88
|
+
|
|
89
|
+
@default.setter
|
|
90
|
+
def default(self, color):
|
|
91
|
+
"""Set default edge color"""
|
|
92
|
+
self._default = self._parse_color(color)
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def selected(self) -> QColor:
|
|
96
|
+
"""Get selected edge color"""
|
|
97
|
+
return QColor(self._selected)
|
|
98
|
+
|
|
99
|
+
@selected.setter
|
|
100
|
+
def selected(self, color):
|
|
101
|
+
"""Set selected edge color"""
|
|
102
|
+
self._selected = self._parse_color(color)
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def hovered(self) -> QColor:
|
|
106
|
+
"""Get hovered edge color"""
|
|
107
|
+
return QColor(self._hovered)
|
|
108
|
+
|
|
109
|
+
@hovered.setter
|
|
110
|
+
def hovered(self, color):
|
|
111
|
+
"""Set hovered edge color"""
|
|
112
|
+
self._hovered = self._parse_color(color)
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def dragging(self) -> QColor:
|
|
116
|
+
"""Get dragging edge color"""
|
|
117
|
+
return QColor(self._dragging)
|
|
118
|
+
|
|
119
|
+
@dragging.setter
|
|
120
|
+
def dragging(self, color):
|
|
121
|
+
"""Set dragging edge color"""
|
|
122
|
+
self._dragging = self._parse_color(color)
|
|
123
|
+
|
|
124
|
+
def set_all(self, default=None, selected=None, hovered=None, dragging=None):
|
|
125
|
+
"""Set all edge colors at once
|
|
126
|
+
|
|
127
|
+
:param default: Default edge color
|
|
128
|
+
:param selected: Selected edge color
|
|
129
|
+
:param hovered: Hovered edge color
|
|
130
|
+
:param dragging: Dragging edge color
|
|
131
|
+
"""
|
|
132
|
+
if default is not None:
|
|
133
|
+
self.default = default
|
|
134
|
+
if selected is not None:
|
|
135
|
+
self.selected = selected
|
|
136
|
+
if hovered is not None:
|
|
137
|
+
self.hovered = hovered
|
|
138
|
+
if dragging is not None:
|
|
139
|
+
self.dragging = dragging
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class SocketColors(BaseColors):
|
|
143
|
+
"""Configuration for Socket colors"""
|
|
144
|
+
|
|
145
|
+
def __init__(self):
|
|
146
|
+
"""Initialize default socket colors"""
|
|
147
|
+
self._socket_type_colors = [
|
|
148
|
+
QColor("#FFFF7700"), # Orange
|
|
149
|
+
QColor("#FF52e220"), # Green
|
|
150
|
+
QColor("#FF0056a6"), # Blue
|
|
151
|
+
QColor("#FFa86db1"), # Purple
|
|
152
|
+
QColor("#FFb54747"), # Red
|
|
153
|
+
QColor("#FFdbe220"), # Yellow
|
|
154
|
+
QColor("#FF888888"), # Gray
|
|
155
|
+
QColor("#FFFF7700"), # Orange (duplicate)
|
|
156
|
+
QColor("#FF52e220"), # Green (duplicate)
|
|
157
|
+
QColor("#FF0056a6"), # Blue (duplicate)
|
|
158
|
+
QColor("#FFa86db1"), # Purple (duplicate)
|
|
159
|
+
QColor("#FFb54747"), # Red (duplicate)
|
|
160
|
+
QColor("#FFdbe220"), # Yellow (duplicate)
|
|
161
|
+
QColor("#FF888888"), # Gray (duplicate)
|
|
162
|
+
]
|
|
163
|
+
self._outline = QColor("#FF000000")
|
|
164
|
+
self._highlight = QColor("#FF37A6FF")
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def outline(self) -> QColor:
|
|
168
|
+
"""Get socket outline color"""
|
|
169
|
+
return QColor(self._outline)
|
|
170
|
+
|
|
171
|
+
@outline.setter
|
|
172
|
+
def outline(self, color):
|
|
173
|
+
"""Set socket outline color"""
|
|
174
|
+
self._outline = self._parse_color(color)
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def highlight(self) -> QColor:
|
|
178
|
+
"""Get socket highlight color"""
|
|
179
|
+
return QColor(self._highlight)
|
|
180
|
+
|
|
181
|
+
@highlight.setter
|
|
182
|
+
def highlight(self, color):
|
|
183
|
+
"""Set socket highlight color"""
|
|
184
|
+
self._highlight = self._parse_color(color)
|
|
185
|
+
|
|
186
|
+
def get_type_color(self, socket_type: int) -> QColor:
|
|
187
|
+
"""Get color for a specific socket type
|
|
188
|
+
|
|
189
|
+
:param socket_type: Socket type index
|
|
190
|
+
:return: QColor for the socket type
|
|
191
|
+
"""
|
|
192
|
+
if isinstance(socket_type, int):
|
|
193
|
+
return QColor(self._socket_type_colors[socket_type % len(self._socket_type_colors)])
|
|
194
|
+
elif isinstance(socket_type, str):
|
|
195
|
+
return QColor(socket_type)
|
|
196
|
+
return QColor(0, 0, 0, 0)
|
|
197
|
+
|
|
198
|
+
def set_type_color(self, socket_type: int, color):
|
|
199
|
+
"""Set color for a specific socket type
|
|
200
|
+
|
|
201
|
+
:param socket_type: Socket type index
|
|
202
|
+
:param color: Color to set
|
|
203
|
+
"""
|
|
204
|
+
self._socket_type_colors[socket_type] = self._parse_color(color)
|
|
205
|
+
|
|
206
|
+
def set_type_colors(self, colors: List) -> None:
|
|
207
|
+
"""Set all socket type colors at once
|
|
208
|
+
|
|
209
|
+
:param colors: List of colors
|
|
210
|
+
"""
|
|
211
|
+
self._socket_type_colors = [self._parse_color(c) for c in colors]
|
|
212
|
+
|
|
213
|
+
def get_all_type_colors(self) -> List[QColor]:
|
|
214
|
+
"""Get all socket type colors
|
|
215
|
+
|
|
216
|
+
:return: List of QColor objects
|
|
217
|
+
"""
|
|
218
|
+
return [QColor(c) for c in self._socket_type_colors]
|
|
219
|
+
|
|
220
|
+
def set_all(self, outline=None, highlight=None, type_colors=None):
|
|
221
|
+
"""Set all socket colors at once
|
|
222
|
+
|
|
223
|
+
:param outline: Socket outline color
|
|
224
|
+
:param highlight: Socket highlight color
|
|
225
|
+
:param type_colors: List of socket type colors
|
|
226
|
+
"""
|
|
227
|
+
if outline is not None:
|
|
228
|
+
self.outline = outline
|
|
229
|
+
if highlight is not None:
|
|
230
|
+
self.highlight = highlight
|
|
231
|
+
if type_colors is not None:
|
|
232
|
+
self.set_type_colors(type_colors)
|
|
233
|
+
|
|
234
|
+
class NodesColors(BaseColors):
|
|
235
|
+
"""Configuration for Node colors
|
|
236
|
+
|
|
237
|
+
This class reuses the EdgeColors structure to define colors for nodes.
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
def __init__(self):
|
|
241
|
+
"""Initialize default node colors"""
|
|
242
|
+
super().__init__()
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class NodeEditorColorScheme:
|
|
246
|
+
"""Global color scheme configuration for Node Editor
|
|
247
|
+
|
|
248
|
+
This class provides a centralized way to manage and update all colors
|
|
249
|
+
used in the node editor, including edge colors and socket colors.
|
|
250
|
+
|
|
251
|
+
Example:
|
|
252
|
+
>>> scheme = NodeEditorColorScheme()
|
|
253
|
+
>>> scheme.edges.default = "#FF0000" # Red edges
|
|
254
|
+
>>> scheme.sockets.highlight = "#00FF00" # Green highlights
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
_instance = None
|
|
258
|
+
|
|
259
|
+
def __new__(cls):
|
|
260
|
+
"""Singleton pattern - ensure only one instance exists"""
|
|
261
|
+
if cls._instance is None:
|
|
262
|
+
cls._instance = super().__new__(cls)
|
|
263
|
+
cls._instance._initialized = False
|
|
264
|
+
return cls._instance
|
|
265
|
+
|
|
266
|
+
def __init__(self):
|
|
267
|
+
"""Initialize color scheme (only once)"""
|
|
268
|
+
if self._initialized:
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
self.edges = EdgeColors()
|
|
272
|
+
self.sockets = SocketColors()
|
|
273
|
+
self.nodes = NodesColors() # Reusing EdgeColors for node colors
|
|
274
|
+
self._initialized = True
|
|
275
|
+
|
|
276
|
+
@classmethod
|
|
277
|
+
def get_instance(cls) -> 'NodeEditorColorScheme':
|
|
278
|
+
"""Get the singleton instance of the color scheme
|
|
279
|
+
|
|
280
|
+
:return: NodeEditorColorScheme instance
|
|
281
|
+
"""
|
|
282
|
+
return cls()
|
|
283
|
+
|
|
284
|
+
def reset_to_defaults(self):
|
|
285
|
+
"""Reset all colors to their default values"""
|
|
286
|
+
self.edges = EdgeColors()
|
|
287
|
+
self.sockets = SocketColors()
|
|
288
|
+
self.nodes = NodesColors()
|
|
289
|
+
|
|
290
|
+
def to_dict(self) -> Dict:
|
|
291
|
+
"""Export current color scheme to dictionary
|
|
292
|
+
|
|
293
|
+
:return: Dictionary representation of the color scheme
|
|
294
|
+
"""
|
|
295
|
+
return {
|
|
296
|
+
'edges': {
|
|
297
|
+
'default': self.edges.default.name(),
|
|
298
|
+
'selected': self.edges.selected.name(),
|
|
299
|
+
'hovered': self.edges.hovered.name(),
|
|
300
|
+
'dragging': self.edges.dragging.name(),
|
|
301
|
+
},
|
|
302
|
+
'sockets': {
|
|
303
|
+
'outline': self.sockets.outline.name(),
|
|
304
|
+
'highlight': self.sockets.highlight.name(),
|
|
305
|
+
'type_colors': [c.name() for c in self.sockets.get_all_type_colors()],
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
def from_dict(self, config: Dict):
|
|
310
|
+
"""Import color scheme from dictionary
|
|
311
|
+
|
|
312
|
+
:param config: Dictionary with color configuration
|
|
313
|
+
"""
|
|
314
|
+
if 'edges' in config:
|
|
315
|
+
edge_config = config['edges']
|
|
316
|
+
self.edges.set_all(
|
|
317
|
+
default=edge_config.get('default'),
|
|
318
|
+
selected=edge_config.get('selected'),
|
|
319
|
+
hovered=edge_config.get('hovered'),
|
|
320
|
+
dragging=edge_config.get('dragging')
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
if 'sockets' in config:
|
|
324
|
+
socket_config = config['sockets']
|
|
325
|
+
self.sockets.set_all(
|
|
326
|
+
outline=socket_config.get('outline'),
|
|
327
|
+
highlight=socket_config.get('highlight'),
|
|
328
|
+
type_colors=socket_config.get('type_colors')
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Global singleton instance
|
|
332
|
+
_global_scheme = None
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def get_color_scheme() -> NodeEditorColorScheme:
|
|
336
|
+
"""Get the global color scheme instance
|
|
337
|
+
|
|
338
|
+
:return: NodeEditorColorScheme instance
|
|
339
|
+
"""
|
|
340
|
+
global _global_scheme
|
|
341
|
+
if _global_scheme is None:
|
|
342
|
+
_global_scheme = NodeEditorColorScheme()
|
|
343
|
+
return _global_scheme
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""A module containing the base class for the Node's content graphical representation. It also contains an example of
|
|
3
|
+
an overridden Text Widget, which can pass a notification to it's parent about being modified."""
|
|
4
|
+
from collections import OrderedDict
|
|
5
|
+
from nodeeditor.node_serializable import Serializable
|
|
6
|
+
from qtpy.QtWidgets import QWidget, QLabel, QVBoxLayout, QTextEdit
|
|
7
|
+
from qtpy.QtGui import QFocusEvent
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from nodeeditor.node_node import Node
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class QDMNodeContentWidget(QWidget, Serializable):
|
|
16
|
+
"""Base class for representation of the Node's graphics content. This class also provides layout
|
|
17
|
+
for other widgets inside of a :py:class:`~nodeeditor.node_node.Node`"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, node: 'Node', parent: QWidget = None) -> None:
|
|
20
|
+
"""
|
|
21
|
+
:param node: reference to the :py:class:`~nodeeditor.node_node.Node`
|
|
22
|
+
:type node: :py:class:`~nodeeditor.node_node.Node`
|
|
23
|
+
:param parent: parent widget
|
|
24
|
+
:type parent: QWidget
|
|
25
|
+
|
|
26
|
+
:Instance Attributes:
|
|
27
|
+
- **node** - reference to the :class:`~nodeeditor.node_node.Node`
|
|
28
|
+
- **layout** - ``QLayout`` container
|
|
29
|
+
"""
|
|
30
|
+
self.node = node
|
|
31
|
+
super().__init__(parent)
|
|
32
|
+
|
|
33
|
+
self.initUI()
|
|
34
|
+
|
|
35
|
+
def initUI(self) -> None:
|
|
36
|
+
"""Sets up layouts and widgets to be rendered in :py:class:`~nodeeditor.node_graphics_node.QDMGraphicsNode` class.
|
|
37
|
+
"""
|
|
38
|
+
self.layout = QVBoxLayout()
|
|
39
|
+
self.layout.setContentsMargins(0, 0, 0, 0)
|
|
40
|
+
self.setLayout(self.layout)
|
|
41
|
+
|
|
42
|
+
self.wdg_label = QLabel("Some Title")
|
|
43
|
+
self.layout.addWidget(self.wdg_label)
|
|
44
|
+
self.layout.addWidget(QDMTextEdit("foo"))
|
|
45
|
+
|
|
46
|
+
def setEditingFlag(self, value: bool) -> None:
|
|
47
|
+
"""
|
|
48
|
+
.. note::
|
|
49
|
+
|
|
50
|
+
If you are handling keyPress events by default Qt Window's shortcuts and ``QActions``, you will not
|
|
51
|
+
need to use this method.
|
|
52
|
+
|
|
53
|
+
Helper function which sets editingFlag inside :py:class:`~nodeeditor.node_graphics_view.QDMGraphicsView` class.
|
|
54
|
+
|
|
55
|
+
This is a helper function to handle keys inside nodes with ``QLineEdits`` or ``QTextEdits`` (you can
|
|
56
|
+
use overridden :py:class:`QDMTextEdit` class) and with QGraphicsView class method ``keyPressEvent``.
|
|
57
|
+
|
|
58
|
+
:param value: new value for editing flag
|
|
59
|
+
"""
|
|
60
|
+
self.node.scene.getView().editingFlag = value
|
|
61
|
+
|
|
62
|
+
def serialize(self) -> OrderedDict:
|
|
63
|
+
"""Serialization method to serialize this class data into ``OrderedDict``.
|
|
64
|
+
|
|
65
|
+
The content widget holds no state of its own, so there is nothing to store.
|
|
66
|
+
|
|
67
|
+
:return: data serialized in ``OrderedDict``
|
|
68
|
+
:rtype: ``OrderedDict``
|
|
69
|
+
"""
|
|
70
|
+
return OrderedDict([
|
|
71
|
+
])
|
|
72
|
+
|
|
73
|
+
def deserialize(self, data: dict, hashmap: dict = {}, restore_id: bool = True) -> bool:
|
|
74
|
+
"""Deserialization method which takes data in python ``dict`` format.
|
|
75
|
+
|
|
76
|
+
The content widget holds no state of its own, so there is nothing to
|
|
77
|
+
restore and the call always succeeds.
|
|
78
|
+
|
|
79
|
+
:param data: Dictionary containing serialized data
|
|
80
|
+
:type data: ``dict``
|
|
81
|
+
:param hashmap: Helper dictionary containing references (by id == key) to existing objects
|
|
82
|
+
:type hashmap: ``dict``
|
|
83
|
+
:param restore_id: True if we are creating new objects. False is useful when loading existing
|
|
84
|
+
objects of which we want to keep the existing object's `id`.
|
|
85
|
+
:type restore_id: bool
|
|
86
|
+
:return: ``True`` if deserialization was successful, otherwise ``False``
|
|
87
|
+
:rtype: ``bool``
|
|
88
|
+
"""
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
# NOTE: deliberately no `history_stamp_callback` here. SceneHistory's
|
|
92
|
+
# dispatch resolves the hook with getattr and tolerates its absence, and
|
|
93
|
+
# defining a no-op base would shadow the real implementation in any
|
|
94
|
+
# content class whose MRO puts this Qt base ahead of its own mixin.
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class QDMTextEdit(QTextEdit):
|
|
98
|
+
"""
|
|
99
|
+
.. note::
|
|
100
|
+
|
|
101
|
+
This class is an example of a ``QTextEdit`` modification that handles the `Delete` key event with an overridden
|
|
102
|
+
Qt's ``keyPressEvent`` (when not using ``QActions`` in menu or toolbar)
|
|
103
|
+
|
|
104
|
+
Overridden ``QTextEdit`` which sends a notification about being edited to its parent's container :py:class:`QDMNodeContentWidget`
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def focusInEvent(self, event: QFocusEvent | None) -> None:
|
|
108
|
+
"""Example of an overridden focusInEvent to mark the start of editing
|
|
109
|
+
|
|
110
|
+
:param event: Qt's focus event
|
|
111
|
+
:type event: QFocusEvent
|
|
112
|
+
"""
|
|
113
|
+
parent = self.parentWidget()
|
|
114
|
+
if isinstance(parent, QDMNodeContentWidget):
|
|
115
|
+
parent.setEditingFlag(True)
|
|
116
|
+
|
|
117
|
+
super().focusInEvent(event)
|
|
118
|
+
|
|
119
|
+
def focusOutEvent(self, event: QFocusEvent | None) -> None:
|
|
120
|
+
"""Example of an overridden focusOutEvent to mark the end of editing
|
|
121
|
+
|
|
122
|
+
:param event: Qt's focus event
|
|
123
|
+
:type event: QFocusEvent
|
|
124
|
+
"""
|
|
125
|
+
parent = self.parentWidget()
|
|
126
|
+
if isinstance(parent, QDMNodeContentWidget):
|
|
127
|
+
parent.setEditingFlag(False)
|
|
128
|
+
|
|
129
|
+
super().focusOutEvent(event)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A module containing the decorator which marks classes as automatically serializable
|
|
3
|
+
"""
|
|
4
|
+
from typing import Any, Callable, Optional, Type, Dict, TypeVar, List, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from nodeeditor.node_scene import Scene
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
T = TypeVar('T')
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MakeSerializable:
|
|
14
|
+
"""Registry for classes that should be automatically serialized"""
|
|
15
|
+
_registry: Dict[str, Type] = {}
|
|
16
|
+
_instances: Dict[str, List['Scene']] = {} # Track instances per scene
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def register(cls, key: str = "") -> Callable[[Type[T]], Type[T]]:
|
|
20
|
+
"""Class decorator to mark a class as automatically serializable"""
|
|
21
|
+
def decorator(wrapped_class: Type[T]) -> Type[T]:
|
|
22
|
+
# Use class name as key if none provided
|
|
23
|
+
serialize_key = key or wrapped_class.__name__.lower()
|
|
24
|
+
|
|
25
|
+
# Ensure the class has required methods
|
|
26
|
+
if not hasattr(wrapped_class, 'serialize') or not hasattr(wrapped_class, 'deserialize'):
|
|
27
|
+
raise TypeError(
|
|
28
|
+
f"Class {wrapped_class.__name__} must implement serialize and deserialize methods")
|
|
29
|
+
|
|
30
|
+
# Store the original __init__
|
|
31
|
+
original_init = wrapped_class.__init__
|
|
32
|
+
|
|
33
|
+
# Create new __init__ that registers with scene
|
|
34
|
+
def new_init(self, scene: 'Scene', *args, **kwargs):
|
|
35
|
+
original_init(self, scene, *args, **kwargs)
|
|
36
|
+
# Auto-register with scene
|
|
37
|
+
if serialize_key not in cls._instances:
|
|
38
|
+
cls._instances[serialize_key] = []
|
|
39
|
+
cls._instances[serialize_key].append(scene)
|
|
40
|
+
# Force scene to register serializers
|
|
41
|
+
scene._register_auto_serializers()
|
|
42
|
+
|
|
43
|
+
wrapped_class.__init__ = new_init
|
|
44
|
+
cls._registry[serialize_key] = wrapped_class
|
|
45
|
+
return wrapped_class
|
|
46
|
+
return decorator
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def get_registered(cls) -> Dict[str, Type]:
|
|
50
|
+
"""Get all classes registered as automatically serializable
|
|
51
|
+
|
|
52
|
+
:return: mapping of serialize keys to the registered classes
|
|
53
|
+
:rtype: ``dict``
|
|
54
|
+
"""
|
|
55
|
+
return cls._registry
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def get_instance(cls, key: str, scene: 'Scene') -> Optional[Any]:
|
|
59
|
+
"""Get instance for given key and scene"""
|
|
60
|
+
if key in cls._instances:
|
|
61
|
+
scenes = cls._instances[key]
|
|
62
|
+
if scene in scenes:
|
|
63
|
+
return next((obj for obj in scene.__dict__.values()
|
|
64
|
+
if isinstance(obj, cls._registry[key])), None)
|
|
65
|
+
return None
|