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,448 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ A module containing the Main Window class
4
+ """
5
+ import os
6
+ import orjson as json
7
+ from orjson import OPT_INDENT_2, JSONDecodeError
8
+ from qtpy.QtCore import QSize, QSettings, QPoint
9
+ from qtpy.QtWidgets import QMainWindow, QLabel, QAction, QMessageBox, QFileDialog, QApplication, QMenu, QMenuBar
10
+ from nodeeditor.node_editor_widget import NodeEditorWidget
11
+
12
+ from typing import TYPE_CHECKING, List, Optional, Tuple, Any, cast
13
+
14
+
15
+ if TYPE_CHECKING:
16
+ from nodeeditor.node_graphics_view import QDMGraphicsView
17
+ from nodeeditor.node_edge import Edge
18
+ from nodeeditor.node_socket import Socket
19
+
20
+
21
+ class NodeEditorWindow(QMainWindow):
22
+ """The Main Window of NodeEditor.
23
+
24
+ Holds the file and edit menus, the actions behind them and the
25
+ :class:`~nodeeditor.node_editor_widget.NodeEditorWidget` that draws the graph.
26
+
27
+ :Instance Attributes:
28
+
29
+ - **name_company** - name of the company, used for permanent profile settings
30
+ - **name_product** - name of this App, used for permanent profile settings
31
+ """
32
+
33
+ NodeEditorWidget_class = NodeEditorWidget
34
+
35
+ """Class representing NodeEditor's Main Window"""
36
+
37
+ def __init__(self) -> None:
38
+ """
39
+ :Instance Attributes:
40
+
41
+ - **name_company** - name of the company, used for permanent profile settings
42
+ - **name_product** - name of this App, used for permanent profile settings
43
+ """
44
+ super().__init__()
45
+
46
+ self.name_company = 'Trigger'
47
+ self.name_product = 'NodeEditor'
48
+
49
+ self.initUI()
50
+
51
+ def initUI(self) -> None:
52
+ """Set up this ``QMainWindow``. Create :class:`~nodeeditor.node_editor_widget.NodeEditorWidget`, Actions and Menus"""
53
+ self.createActions()
54
+ self.createMenus()
55
+
56
+ # create node editor widget
57
+ self.nodeeditor = self.__class__.NodeEditorWidget_class(self)
58
+ self.nodeeditor.scene.addHasBeenModifiedListener(self.setTitle)
59
+ self.setCentralWidget(self.nodeeditor)
60
+
61
+ self.createStatusBar()
62
+
63
+ # set window properties
64
+ # self.setGeometry(200, 200, 800, 600)
65
+ self.setTitle()
66
+ self.show()
67
+
68
+ def sizeHint(self):
69
+ """The preferred size of the main window.
70
+
71
+ :return: the default window size
72
+ :rtype: ``QSize``
73
+ """
74
+ return QSize(800, 600)
75
+
76
+ def createStatusBar(self) -> None:
77
+ """Create Status bar and connect to `Graphics View` scenePosChanged event"""
78
+ self.statusBar().showMessage("")
79
+ self.status_mouse_pos = QLabel("")
80
+ self.statusBar().addPermanentWidget(self.status_mouse_pos)
81
+ self.nodeeditor.view.scenePosChanged.connect(self.onScenePosChanged)
82
+
83
+ # type: ignore[override]
84
+ def createActions(self) -> None:
85
+ """Create basic `File` and `Edit` actions"""
86
+ self.actNew = QAction('&New', self, shortcut='Ctrl+N',
87
+ statusTip="Create new graph", triggered=self.onFileNew)
88
+ self.actOpen = QAction('&Open', self, shortcut='Ctrl+O',
89
+ statusTip="Open file", triggered=self.onFileOpen)
90
+ self.actSave = QAction('&Save', self, shortcut='Ctrl+S',
91
+ statusTip="Save file", triggered=self.onFileSave)
92
+ self.actSaveAs = QAction('Save &As...', self, shortcut='Ctrl+Shift+S',
93
+ statusTip="Save file as...", triggered=self.onFileSaveAs)
94
+ self.actExit = QAction('E&xit', self, shortcut='Ctrl+Q',
95
+ statusTip="Exit application", triggered=self.close)
96
+
97
+ self.actUndo = QAction('&Undo', self, shortcut='Ctrl+Z',
98
+ statusTip="Undo last operation", triggered=self.onEditUndo)
99
+ self.actRedo = QAction('&Redo', self, shortcut='Ctrl+Shift+Z',
100
+ statusTip="Redo last operation", triggered=self.onEditRedo)
101
+ self.actCut = QAction('Cu&t', self, shortcut='Ctrl+X',
102
+ statusTip="Cut to clipboard", triggered=self.onEditCut)
103
+ self.actCopy = QAction('&Copy', self, shortcut='Ctrl+C',
104
+ statusTip="Copy to clipboard", triggered=self.onEditCopy)
105
+ self.actPaste = QAction('&Paste', self, shortcut='Ctrl+V',
106
+ statusTip="Paste from clipboard", triggered=self.onEditPaste)
107
+ self.actDelete = QAction('&Delete', self, shortcut='Del',
108
+ statusTip="Delete selected items", triggered=self.onEditDelete)
109
+
110
+ self.actGroup = QAction('&Group selected', self, shortcut='G',
111
+ statusTip="Group selected nodes", triggered=self.onGroupSelected)
112
+ self.actUngroup = QAction('&Ungroup', self, shortcut='U',
113
+ statusTip="Ungroup selected groups (keep nodes)", triggered=self.onUngroupSelected)
114
+ try:
115
+ self.actUngroup.setShortcuts(['U', 'Shift+G'])
116
+ except Exception:
117
+ pass
118
+ self.actCollapseGroup = QAction('&Collapse/Expand group', self, shortcut='C',
119
+ statusTip="Collapse/expand selected groups", triggered=self.onToggleCollapseSelected)
120
+
121
+ def createMenus(self) -> None:
122
+ """Create Menus for `File` and `Edit`"""
123
+ self.createFileMenu()
124
+ self.createEditMenu()
125
+
126
+ def createFileMenu(self) -> None:
127
+ """Create the `File` menu and fill it with the file actions"""
128
+ menubar: QMenuBar = self.menuBar()
129
+ self.fileMenu: QMenu = menubar.addMenu('&File')
130
+ self.fileMenu.addAction(self.actNew)
131
+ self.fileMenu.addSeparator()
132
+ self.fileMenu.addAction(self.actOpen)
133
+ self.fileMenu.addAction(self.actSave)
134
+ self.fileMenu.addAction(self.actSaveAs)
135
+ self.fileMenu.addSeparator()
136
+ self.fileMenu.addAction(self.actExit)
137
+
138
+ def createEditMenu(self) -> None:
139
+ """Create the `Edit` menu and fill it with the edit and grouping actions"""
140
+ menubar: QMenuBar = self.menuBar()
141
+ self.editMenu: QMenu = menubar.addMenu('&Edit')
142
+ self.editMenu.addAction(self.actUndo)
143
+ self.editMenu.addAction(self.actRedo)
144
+ self.editMenu.addSeparator()
145
+ self.editMenu.addAction(self.actCut)
146
+ self.editMenu.addAction(self.actCopy)
147
+ self.editMenu.addAction(self.actPaste)
148
+ self.editMenu.addSeparator()
149
+ self.editMenu.addAction(self.actDelete)
150
+ self.editMenu.addSeparator()
151
+ self.editMenu.addAction(self.actGroup)
152
+ self.editMenu.addAction(self.actUngroup)
153
+ self.editMenu.addAction(self.actCollapseGroup)
154
+
155
+ def setTitle(self) -> None:
156
+ """Function responsible for setting window title"""
157
+ title = "Node Editor - "
158
+ title += self.getCurrentNodeEditorWidget().getUserFriendlyFilename()
159
+
160
+ self.setWindowTitle(title)
161
+
162
+ def closeEvent(self, event) -> None:
163
+ """Handle close event. Ask before we loose work"""
164
+ if self.maybeSave():
165
+ event.accept()
166
+ else:
167
+ event.ignore()
168
+
169
+ def isModified(self) -> bool:
170
+ """Has current :class:`~nodeeditor.node_scene.Scene` been modified?
171
+
172
+ :return: ``True`` if current :class:`~nodeeditor.node_scene.Scene` has been modified
173
+ :rtype: ``bool``
174
+ """
175
+ nodeeditor = self.getCurrentNodeEditorWidget()
176
+ return nodeeditor.scene.isModified() if nodeeditor else False
177
+
178
+ def getCurrentNodeEditorWidget(self) -> NodeEditorWidget:
179
+ """get current :class:`~nodeeditor.node_editor_widget`
180
+
181
+ :return: get current :class:`~nodeeditor.node_editor_widget`
182
+ :rtype: :class:`~nodeeditor.node_editor_widget`
183
+ """
184
+ return cast(NodeEditorWidget, self.centralWidget())
185
+
186
+ def maybeSave(self) -> bool:
187
+ """If current `Scene` is modified, ask a dialog to save the changes. Used before
188
+ closing window / mdi child document
189
+
190
+ :return: ``True`` if we can continue in the `Close Event` and shutdown. ``False`` if we should cancel
191
+ :rtype: ``bool``
192
+ """
193
+ if not self.isModified():
194
+ return True
195
+
196
+ res = QMessageBox.warning(self, "About to loose your work?",
197
+ "The document has been modified.\n Do you want to save your changes?",
198
+ QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel
199
+ )
200
+
201
+ if res == QMessageBox.StandardButton.Save:
202
+ return self.onFileSave()
203
+ elif res == QMessageBox.StandardButton.Cancel:
204
+ return False
205
+
206
+ return True
207
+
208
+ def onScenePosChanged(self, x: int, y: int) -> None:
209
+ """Handle event when cursor position changed on the `Scene`
210
+
211
+ :param x: new cursor x position
212
+ :type x:
213
+ :param y: new cursor y position
214
+ :type y:
215
+ """
216
+ self.status_mouse_pos.setText("Scene Pos: [%d, %d]" % (x, y))
217
+
218
+ def getFileDialogDirectory(self) -> str:
219
+ """Returns starting directory for ``QFileDialog`` file open/save"""
220
+ return ''
221
+
222
+ def getFileDialogFilter(self) -> str:
223
+ """Returns ``str`` standard file open/save filter for ``QFileDialog``"""
224
+ return 'Design (*.tds);;All Files (*)'
225
+
226
+ def onFileNew(self) -> None:
227
+ """Hande File New operation"""
228
+ if self.maybeSave():
229
+ self.getCurrentNodeEditorWidget().fileNew()
230
+ self.setTitle()
231
+
232
+ def onFileOpen(self) -> None:
233
+ """Handle File Open operation"""
234
+ if self.maybeSave():
235
+ fname, filter = QFileDialog.getOpenFileName(
236
+ self, 'Open graph from file', self.getFileDialogDirectory(), self.getFileDialogFilter())
237
+ if fname != '' and os.path.isfile(fname):
238
+ self.getCurrentNodeEditorWidget().fileLoad(fname)
239
+ self.setTitle()
240
+
241
+ def onFileSave(self):
242
+ """Handle File Save operation"""
243
+ current_nodeeditor = self.getCurrentNodeEditorWidget()
244
+ if current_nodeeditor is not None:
245
+ if not current_nodeeditor.isFilenameSet():
246
+ return self.onFileSaveAs()
247
+
248
+ current_nodeeditor.fileSave()
249
+ self.statusBar().showMessage("Successfully saved %s" %
250
+ current_nodeeditor.filename, 5000)
251
+
252
+ # support for MDI app
253
+ if hasattr(current_nodeeditor, "setTitle"):
254
+ current_nodeeditor.setTitle()
255
+ else:
256
+ self.setTitle()
257
+ return True
258
+
259
+ def onFileSaveAs(self) -> bool:
260
+ """Handle File Save As operation"""
261
+ current_nodeeditor = self.getCurrentNodeEditorWidget()
262
+ if current_nodeeditor is not None:
263
+ fname, filter = QFileDialog.getSaveFileName(
264
+ self, 'Save design to file', self.getFileDialogDirectory(), self.getFileDialogFilter())
265
+ if fname == '':
266
+ return False
267
+
268
+ self.onBeforeSaveAs(current_nodeeditor, fname)
269
+ current_nodeeditor.fileSave(fname)
270
+ self.statusBar().showMessage("Successfully saved as %s" %
271
+ current_nodeeditor.filename, 5000)
272
+
273
+ # support for MDI app
274
+ if hasattr(current_nodeeditor, "setTitle"):
275
+ current_nodeeditor.setTitle()
276
+ else:
277
+ self.setTitle()
278
+ return True
279
+
280
+ def onBeforeSaveAs(self, current_nodeeditor: 'NodeEditorWidget', filename: str) -> None:
281
+ """
282
+ Event triggered after choosing filename and before actual fileSave(). We are passing current_nodeeditor because
283
+ we will loose focus after asking with QFileDialog and therefore getCurrentNodeEditorWidget will return None
284
+ """
285
+ pass
286
+
287
+ def onEditUndo(self) -> None:
288
+ """Handle Edit Undo operation"""
289
+ if self.getCurrentNodeEditorWidget():
290
+ self.getCurrentNodeEditorWidget().scene.undo()
291
+
292
+ def onEditRedo(self) -> None:
293
+ """Handle Edit Redo operation"""
294
+ if self.getCurrentNodeEditorWidget():
295
+ self.getCurrentNodeEditorWidget().scene.redo()
296
+
297
+ def onEditDelete(self) -> None:
298
+ """Handle Delete Selected operation"""
299
+ if self.getCurrentNodeEditorWidget():
300
+ self.getCurrentNodeEditorWidget().scene.getView().deleteSelected()
301
+
302
+ def _groupingFocusInTextEdit(self) -> bool:
303
+ """Guard plain-letter group shortcuts so typing in node widgets still works."""
304
+ try:
305
+ from qtpy.QtWidgets import QLineEdit, QTextEdit, QPlainTextEdit
306
+ w = QApplication.focusWidget()
307
+ return isinstance(w, (QLineEdit, QTextEdit, QPlainTextEdit))
308
+ except Exception:
309
+ return False
310
+
311
+ def _selectedLogicNodes(self, scene):
312
+ nodes = []
313
+ try:
314
+ for item in list(scene.getSelectedItems()):
315
+ try:
316
+ n = getattr(item, 'node', None)
317
+ if n is not None and n not in nodes:
318
+ nodes.append(n)
319
+ except Exception:
320
+ continue
321
+ except Exception:
322
+ pass
323
+ return nodes
324
+
325
+ def _selectedGroups(self, scene):
326
+ try:
327
+ from nodeeditor.node_group import Group
328
+ except Exception:
329
+ return []
330
+ try:
331
+ return [it for it in list(scene.getSelectedItems()) if isinstance(it, Group)]
332
+ except Exception:
333
+ return []
334
+
335
+ def onGroupSelected(self) -> None:
336
+ """Group currently selected nodes (needs 2+). Shortcut G."""
337
+ if self._groupingFocusInTextEdit():
338
+ return
339
+ editor = self.getCurrentNodeEditorWidget()
340
+ if not editor:
341
+ return
342
+ scene = editor.scene
343
+ nodes = self._selectedLogicNodes(scene)
344
+ if len(nodes) < 2:
345
+ try:
346
+ self.statusBar().showMessage("Select 2+ nodes to group", 3000)
347
+ except Exception:
348
+ pass
349
+ return
350
+ try:
351
+ from nodeeditor.node_group import Group
352
+ group = Group(scene, title=f"Group ({len(nodes)} nodes)")
353
+ for node in nodes:
354
+ group.addNode(node)
355
+ group.updateBounds()
356
+ scene.history.storeHistory(f"Created group with {len(nodes)} nodes", setModified=True)
357
+ except Exception:
358
+ pass
359
+
360
+ def onUngroupSelected(self) -> None:
361
+ """Ungroup selected groups, keep nodes. Shortcut U / Shift+G."""
362
+ if self._groupingFocusInTextEdit():
363
+ return
364
+ editor = self.getCurrentNodeEditorWidget()
365
+ if not editor:
366
+ return
367
+ scene = editor.scene
368
+ groups = self._selectedGroups(scene)
369
+ if not groups:
370
+ return
371
+ for grp in list(groups):
372
+ try:
373
+ grp.ungroup()
374
+ except Exception:
375
+ continue
376
+ try:
377
+ scene.history.storeHistory("Ungrouped", setModified=True)
378
+ except Exception:
379
+ pass
380
+
381
+ def onToggleCollapseSelected(self) -> None:
382
+ """Collapse/expand selected groups. Shortcut C."""
383
+ if self._groupingFocusInTextEdit():
384
+ return
385
+ editor = self.getCurrentNodeEditorWidget()
386
+ if not editor:
387
+ return
388
+ scene = editor.scene
389
+ groups = self._selectedGroups(scene)
390
+ if not groups:
391
+ return
392
+ for grp in list(groups):
393
+ try:
394
+ grp.toggleCollapse()
395
+ except Exception:
396
+ continue
397
+ try:
398
+ scene.history.storeHistory("Toggled group collapse", setModified=True)
399
+ except Exception:
400
+ pass
401
+
402
+ def onEditCut(self) -> None:
403
+ """Handle Edit Cut to clipboard operation"""
404
+ if self.getCurrentNodeEditorWidget():
405
+ data = self.getCurrentNodeEditorWidget(
406
+ ).scene.clipboard.serializeSelected(delete=True)
407
+ str_data = json.dumps(data, option=OPT_INDENT_2).decode("utf-8")
408
+ QApplication.instance().clipboard().setText(str_data)
409
+
410
+ def onEditCopy(self) -> None:
411
+ """Handle Edit Copy to clipboard operation"""
412
+ if self.getCurrentNodeEditorWidget():
413
+ data = self.getCurrentNodeEditorWidget(
414
+ ).scene.clipboard.serializeSelected(delete=False)
415
+ str_data = json.dumps(data, option=OPT_INDENT_2).decode("utf-8")
416
+ QApplication.instance().clipboard().setText(str_data)
417
+
418
+ def onEditPaste(self):
419
+ """Handle Edit Paste from clipboard operation"""
420
+ if self.getCurrentNodeEditorWidget():
421
+ raw_data = QApplication.instance().clipboard().text()
422
+
423
+ try:
424
+ data = json.loads(raw_data)
425
+ except JSONDecodeError as e:
426
+ print("Pasting of not valid json data!", e)
427
+ return
428
+
429
+ # check if the json data are correct
430
+ if 'nodes' not in data:
431
+ print("JSON does not contain any nodes!")
432
+ return
433
+
434
+ return self.getCurrentNodeEditorWidget().scene.clipboard.deserializeFromClipboard(data)
435
+
436
+ def readSettings(self) -> None:
437
+ """Read the permanent profile settings for this app"""
438
+ settings = QSettings(self.name_company, self.name_product)
439
+ pos = settings.value('pos', QPoint(200, 200))
440
+ size = settings.value('size', QSize(400, 400))
441
+ self.move(pos)
442
+ self.resize(size)
443
+
444
+ def writeSettings(self) -> None:
445
+ """Write the permanent profile settings for this app"""
446
+ settings = QSettings(self.name_company, self.name_product)
447
+ settings.setValue('pos', self.pos())
448
+ settings.setValue('size', self.size())