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
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
A module containing ``NodeEditorWidget`` class
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
from qtpy.QtCore import Qt
|
|
7
|
+
from qtpy.QtGui import QBrush, QPen, QFont, QColor
|
|
8
|
+
from qtpy.QtWidgets import QWidget, QVBoxLayout, QApplication, QMessageBox, QLabel, QGraphicsItem, QTextEdit, QPushButton
|
|
9
|
+
|
|
10
|
+
from nodeeditor.node_scene import Scene, InvalidFile
|
|
11
|
+
from nodeeditor.node_node import Node
|
|
12
|
+
from nodeeditor.node_edge import Edge, EDGE_TYPE_BEZIER
|
|
13
|
+
from nodeeditor.node_graphics_view import QDMGraphicsView
|
|
14
|
+
from nodeeditor.utils import dumpException
|
|
15
|
+
|
|
16
|
+
from typing import TYPE_CHECKING, List, Optional, Tuple, Any
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from nodeeditor.node_graphics_view import QDMGraphicsView
|
|
21
|
+
from nodeeditor.node_edge import Edge
|
|
22
|
+
from nodeeditor.node_socket import Socket
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NodeEditorWidget(QWidget):
|
|
26
|
+
"""Widget hosting a graph :class:`~nodeeditor.node_scene.Scene` and its graphics view.
|
|
27
|
+
|
|
28
|
+
The ``Scene`` and the ``GraphicsView`` classes used by this widget can be replaced
|
|
29
|
+
by subclasses through the ``Scene_class`` and ``GraphicsView_class`` attributes.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
Scene_class = Scene
|
|
33
|
+
GraphicsView_class = QDMGraphicsView
|
|
34
|
+
|
|
35
|
+
def __init__(self, parent: QWidget = None):
|
|
36
|
+
"""
|
|
37
|
+
:param parent: parent widget
|
|
38
|
+
:type parent: ``QWidget``
|
|
39
|
+
|
|
40
|
+
:Instance Attributes:
|
|
41
|
+
|
|
42
|
+
- **filename** - currently graph's filename or ``None``
|
|
43
|
+
"""
|
|
44
|
+
super().__init__(parent)
|
|
45
|
+
|
|
46
|
+
self.filename: Optional[str] = None
|
|
47
|
+
|
|
48
|
+
self.initUI()
|
|
49
|
+
|
|
50
|
+
def initUI(self):
|
|
51
|
+
"""Set up this ``NodeEditorWidget`` with its layout, :class:`~nodeeditor.node_scene.Scene` and
|
|
52
|
+
:class:`~nodeeditor.node_graphics_view.QDMGraphicsView`"""
|
|
53
|
+
self.layout = QVBoxLayout()
|
|
54
|
+
self.layout.setContentsMargins(0, 0, 0, 0)
|
|
55
|
+
self.setLayout(self.layout)
|
|
56
|
+
|
|
57
|
+
# crate graphics scene
|
|
58
|
+
self.scene = self.__class__.Scene_class()
|
|
59
|
+
|
|
60
|
+
# create graphics view
|
|
61
|
+
self.view = self.__class__.GraphicsView_class(self.scene.grScene, self)
|
|
62
|
+
self.layout.addWidget(self.view)
|
|
63
|
+
|
|
64
|
+
def isModified(self) -> bool:
|
|
65
|
+
"""Has the `Scene` been modified?
|
|
66
|
+
|
|
67
|
+
:return: ``True`` if the `Scene` has been modified
|
|
68
|
+
:rtype: ``bool``
|
|
69
|
+
"""
|
|
70
|
+
return self.scene.isModified()
|
|
71
|
+
|
|
72
|
+
def isFilenameSet(self) -> bool:
|
|
73
|
+
"""Do we have a graph loaded from file or are we creating a new one?
|
|
74
|
+
|
|
75
|
+
:return: ``True`` if filename is set. ``False`` if it is a new graph not yet saved to a file
|
|
76
|
+
:rtype: ''bool''
|
|
77
|
+
"""
|
|
78
|
+
return self.filename is not None
|
|
79
|
+
|
|
80
|
+
def getSelectedItems(self) -> list:
|
|
81
|
+
"""Shortcut returning `Scene`'s currently selected items
|
|
82
|
+
|
|
83
|
+
:return: list of ``QGraphicsItems``
|
|
84
|
+
:rtype: list[QGraphicsItem]
|
|
85
|
+
"""
|
|
86
|
+
return self.scene.getSelectedItems()
|
|
87
|
+
|
|
88
|
+
def hasSelectedItems(self) -> bool:
|
|
89
|
+
"""Is there something selected in the :class:`nodeeditor.node_scene.Scene`?
|
|
90
|
+
|
|
91
|
+
:return: ``True`` if there is something selected in the `Scene`
|
|
92
|
+
:rtype: ``bool``
|
|
93
|
+
"""
|
|
94
|
+
return self.getSelectedItems() != []
|
|
95
|
+
|
|
96
|
+
def canUndo(self) -> bool:
|
|
97
|
+
"""Can Undo be performed right now?
|
|
98
|
+
|
|
99
|
+
:return: ``True`` if we can undo
|
|
100
|
+
:rtype: ``bool``
|
|
101
|
+
"""
|
|
102
|
+
return self.scene.history.canUndo()
|
|
103
|
+
|
|
104
|
+
def canRedo(self) -> bool:
|
|
105
|
+
"""Can Redo be performed right now?
|
|
106
|
+
|
|
107
|
+
:return: ``True`` if we can redo
|
|
108
|
+
:rtype: ``bool``
|
|
109
|
+
"""
|
|
110
|
+
return self.scene.history.canRedo()
|
|
111
|
+
|
|
112
|
+
def getUserFriendlyFilename(self) -> str:
|
|
113
|
+
"""Get user friendly filename. Used in the window title
|
|
114
|
+
|
|
115
|
+
:return: just a base name of the file or `'New Graph'`
|
|
116
|
+
:rtype: ``str``
|
|
117
|
+
"""
|
|
118
|
+
if self.filename is None:
|
|
119
|
+
return "New Graph"
|
|
120
|
+
|
|
121
|
+
print("filename:", self.filename)
|
|
122
|
+
|
|
123
|
+
name = os.path.basename(
|
|
124
|
+
self.filename) if self.isFilenameSet() else "New Graph"
|
|
125
|
+
|
|
126
|
+
print("name:", name)
|
|
127
|
+
|
|
128
|
+
return name + ("*" if self.isModified() else "")
|
|
129
|
+
|
|
130
|
+
def fileNew(self):
|
|
131
|
+
"""Empty the scene (create new file)"""
|
|
132
|
+
self.scene.clear()
|
|
133
|
+
self.filename = None
|
|
134
|
+
self.scene.history.clear()
|
|
135
|
+
self.scene.history.storeInitialHistoryStamp()
|
|
136
|
+
|
|
137
|
+
def fileLoad(self, filename: str):
|
|
138
|
+
"""Load serialized graph from JSON file
|
|
139
|
+
|
|
140
|
+
:param filename: file to load
|
|
141
|
+
:type filename: ``str``
|
|
142
|
+
"""
|
|
143
|
+
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
|
|
144
|
+
try:
|
|
145
|
+
self.scene.loadFromFile(filename)
|
|
146
|
+
self.filename = filename
|
|
147
|
+
self.scene.history.clear()
|
|
148
|
+
self.scene.history.storeInitialHistoryStamp()
|
|
149
|
+
return True
|
|
150
|
+
except FileNotFoundError as e:
|
|
151
|
+
dumpException(e)
|
|
152
|
+
QMessageBox.warning(self, "Error loading %s" % os.path.basename(
|
|
153
|
+
filename), str(e).replace('[Errno 2]', ''))
|
|
154
|
+
return False
|
|
155
|
+
except InvalidFile as e:
|
|
156
|
+
dumpException(e)
|
|
157
|
+
# QApplication.restoreOverrideCursor()
|
|
158
|
+
QMessageBox.warning(self, "Error loading %s" %
|
|
159
|
+
os.path.basename(filename), str(e))
|
|
160
|
+
return False
|
|
161
|
+
finally:
|
|
162
|
+
QApplication.restoreOverrideCursor()
|
|
163
|
+
|
|
164
|
+
def fileSave(self, filename: str = None) -> bool:
|
|
165
|
+
"""Save serialized graph to JSON file. When called with an empty parameter, we won't store/remember the filename.
|
|
166
|
+
|
|
167
|
+
:param filename: file to store the graph
|
|
168
|
+
:type filename: ``str``
|
|
169
|
+
:return: ``True`` if the file was saved successfully, ``False`` otherwise
|
|
170
|
+
"""
|
|
171
|
+
# Check if we have a valid filename
|
|
172
|
+
if not self.filename and not filename:
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
# Update filename if provided
|
|
176
|
+
if filename:
|
|
177
|
+
self.filename = filename
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
|
|
181
|
+
self.scene.saveToFile(self.filename)
|
|
182
|
+
return True
|
|
183
|
+
except Exception as e:
|
|
184
|
+
dumpException(e)
|
|
185
|
+
return False
|
|
186
|
+
finally:
|
|
187
|
+
QApplication.restoreOverrideCursor()
|
|
188
|
+
|
|
189
|
+
def addNodes(self):
|
|
190
|
+
"""Testing method to create 3 `Nodes` with 3 `Edges` connecting them"""
|
|
191
|
+
node1 = Node(self.scene, "My Awesome Node 1",
|
|
192
|
+
inputs=[0, 0, 0], outputs=[1, 5])
|
|
193
|
+
node2 = Node(self.scene, "My Awesome Node 2",
|
|
194
|
+
inputs=[3, 3, 3], outputs=[1])
|
|
195
|
+
node3 = Node(self.scene, "My Awesome Node 3",
|
|
196
|
+
inputs=[2, 2, 2], outputs=[1])
|
|
197
|
+
node1.setPos(-350, -250)
|
|
198
|
+
node2.setPos(-75, 0)
|
|
199
|
+
node3.setPos(200, -200)
|
|
200
|
+
|
|
201
|
+
edge1 = Edge(
|
|
202
|
+
self.scene, node1.outputs[0], node2.inputs[0], edge_type=EDGE_TYPE_BEZIER)
|
|
203
|
+
edge2 = Edge(
|
|
204
|
+
self.scene, node2.outputs[0], node3.inputs[0], edge_type=EDGE_TYPE_BEZIER)
|
|
205
|
+
edge3 = Edge(
|
|
206
|
+
self.scene, node1.outputs[0], node3.inputs[2], edge_type=EDGE_TYPE_BEZIER)
|
|
207
|
+
|
|
208
|
+
self.scene.history.storeInitialHistoryStamp()
|
|
209
|
+
|
|
210
|
+
def addCustomNode(self):
|
|
211
|
+
"""Testing method to create a custom Node with custom content"""
|
|
212
|
+
from nodeeditor.node_content_widget import QDMNodeContentWidget
|
|
213
|
+
from nodeeditor.node_serializable import Serializable
|
|
214
|
+
|
|
215
|
+
class NNodeContent(QLabel): # , Serializable):
|
|
216
|
+
def __init__(self, node, parent=None):
|
|
217
|
+
super().__init__("FooBar")
|
|
218
|
+
self.node = node
|
|
219
|
+
self.setParent(parent)
|
|
220
|
+
|
|
221
|
+
class NNode(Node):
|
|
222
|
+
NodeContent_class = NNodeContent
|
|
223
|
+
|
|
224
|
+
self.scene.setNodeClassSelector(lambda data: NNode)
|
|
225
|
+
node = NNode(self.scene, "A Custom Node 1", inputs=[0, 1, 2])
|
|
226
|
+
|
|
227
|
+
print("node content:", node.content)
|
|
228
|
+
|
|
229
|
+
def addDebugContent(self):
|
|
230
|
+
"""Testing method to put random QGraphicsItems and elements into QGraphicsScene"""
|
|
231
|
+
greenBrush = QBrush(Qt.green)
|
|
232
|
+
outlinePen = QPen(Qt.black)
|
|
233
|
+
outlinePen.setWidth(2)
|
|
234
|
+
|
|
235
|
+
rect = self.grScene.addRect(-100, -100, 80,
|
|
236
|
+
100, outlinePen, greenBrush)
|
|
237
|
+
rect.setFlag(QGraphicsItem.ItemIsMovable)
|
|
238
|
+
|
|
239
|
+
text = self.grScene.addText(
|
|
240
|
+
"This is my Awesome text!", QFont("Ubuntu"))
|
|
241
|
+
text.setFlag(QGraphicsItem.ItemIsSelectable)
|
|
242
|
+
text.setFlag(QGraphicsItem.ItemIsMovable)
|
|
243
|
+
text.setDefaultTextColor(QColor.fromRgbF(1.0, 1.0, 1.0))
|
|
244
|
+
|
|
245
|
+
widget1 = QPushButton("Hello World")
|
|
246
|
+
proxy1 = self.grScene.addWidget(widget1)
|
|
247
|
+
proxy1.setFlag(QGraphicsItem.ItemIsMovable)
|
|
248
|
+
proxy1.setPos(0, 30)
|
|
249
|
+
|
|
250
|
+
widget2 = QTextEdit()
|
|
251
|
+
proxy2 = self.grScene.addWidget(widget2)
|
|
252
|
+
proxy2.setFlag(QGraphicsItem.ItemIsSelectable)
|
|
253
|
+
proxy2.setPos(0, 60)
|
|
254
|
+
|
|
255
|
+
line = self.grScene.addLine(-200, -200, 400, -100, outlinePen)
|
|
256
|
+
line.setFlag(QGraphicsItem.ItemIsMovable)
|
|
257
|
+
line.setFlag(QGraphicsItem.ItemIsSelectable)
|