Basic document handling
This commit is contained in:
17
BEdit/.vscode/tasks.json
vendored
17
BEdit/.vscode/tasks.json
vendored
@@ -98,13 +98,28 @@
|
|||||||
"panel": "shared"
|
"panel": "shared"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "Qt: Compile Component Options UI to Python",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "pyside6-uic",
|
||||||
|
"args": [
|
||||||
|
"--from-imports",
|
||||||
|
"${workspaceFolder}/ui/component_options_dialog.ui",
|
||||||
|
"-o",
|
||||||
|
"${workspaceFolder}/src/bedit/ui_component_options_dialog.py"
|
||||||
|
],
|
||||||
|
"options": {"cwd": "${workspaceFolder}"},
|
||||||
|
"problemMatcher": [],
|
||||||
|
"presentation": {"reveal": "silent", "panel": "shared"}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "Qt: Build Designer Files",
|
"label": "Qt: Build Designer Files",
|
||||||
"dependsOrder": "sequence",
|
"dependsOrder": "sequence",
|
||||||
"dependsOn": [
|
"dependsOn": [
|
||||||
"Qt: Compile Resources to Python",
|
"Qt: Compile Resources to Python",
|
||||||
"Qt: Compile UI to Python",
|
"Qt: Compile UI to Python",
|
||||||
"Qt: Compile Settings UI to Python"
|
"Qt: Compile Settings UI to Python",
|
||||||
|
"Qt: Compile Component Options UI to Python"
|
||||||
],
|
],
|
||||||
"problemMatcher": [],
|
"problemMatcher": [],
|
||||||
"group": {
|
"group": {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ pyside6-uic ui/main_window.ui -o src/bedit/ui_main_window.py
|
|||||||
|
|
||||||
Do not hand-edit the generated Python file; change the `.ui` file and regenerate
|
Do not hand-edit the generated Python file; change the `.ui` file and regenerate
|
||||||
it. Add behavior and signal connections in `main_window.py`. Widget names from
|
it. Add behavior and signal connections in `main_window.py`. Widget names from
|
||||||
Designer are available there through `self.ui`, such as `self.ui.editor`.
|
Designer are available there through `self.ui`, such as `self.ui.graphView`.
|
||||||
|
|
||||||
In VS Code, the same commands are available through **Terminal → Run Task**:
|
In VS Code, the same commands are available through **Terminal → Run Task**:
|
||||||
|
|
||||||
@@ -87,6 +87,81 @@ In VS Code, the same commands are available through **Terminal → Run Task**:
|
|||||||
6. Add icons through a Qt resource file (`.qrc`) so packaging is reliable.
|
6. Add icons through a Qt resource file (`.qrc`) so packaging is reliable.
|
||||||
7. Test on Windows regularly; fonts, scaling, and native dialogs vary by platform.
|
7. Test on Windows regularly; fonts, scaling, and native dialogs vary by platform.
|
||||||
|
|
||||||
|
## Graph and library prototype
|
||||||
|
|
||||||
|
Documents and libraries use the same recursive format: a library is simply a
|
||||||
|
BEdit document used as a copy source. The built-in example defines A, B, and C.
|
||||||
|
|
||||||
|
- A document can own multiple independent top-level graph or text components.
|
||||||
|
Right-click **Current Document** to create one, and double-click a current
|
||||||
|
component in the tree to activate it.
|
||||||
|
- Drag a component from Libraries onto the workspace. Placement recursively
|
||||||
|
copies it with new IDs, leaving no link to the source.
|
||||||
|
- Drag components to move them; movement participates in undo and redo.
|
||||||
|
- Components, interface terminals, and connections are selectable. Use a rubber
|
||||||
|
band or Ctrl-click for multiple selection, Delete to remove items, and the
|
||||||
|
standard Cut/Copy/Paste shortcuts to duplicate selected component groups.
|
||||||
|
- Click an output port and then an input port to create a connection.
|
||||||
|
- Double-click a graph component to open its owned subgraph; use **Up** to return.
|
||||||
|
- Graph components show **Pointer**, **Input**, and **Output** tools. Select an
|
||||||
|
interface tool and click the canvas to add a visible internal terminal and a
|
||||||
|
corresponding external block port. Interface terminals can be moved afterward.
|
||||||
|
- Right-click a component on the canvas or in Current Document to edit its name,
|
||||||
|
icon shape, icon text, fill color, and border color. The same dialog can hide
|
||||||
|
that component's contained subtree from the Libraries tree.
|
||||||
|
- Double-click a text component to edit its input list, output list, and
|
||||||
|
`implementation.source` JSON.
|
||||||
|
- Right-click any graph component under Current Document to add nested graph or
|
||||||
|
text blocks. Any current-document component can also be deleted there.
|
||||||
|
- The active document hierarchy has its own Document panel; the Libraries panel
|
||||||
|
contains only configured external libraries.
|
||||||
|
- Select one or more blocks and press `Ctrl+R`, or use the Transform toolbar, to
|
||||||
|
rotate them clockwise by 90 degrees. Rotation is saved and supports undo/redo.
|
||||||
|
**Apply JSON** updates that source and participates in undo/redo.
|
||||||
|
- File → Save writes the complete recursive document to JSON.
|
||||||
|
- File → Close Document removes the active document and returns to an empty
|
||||||
|
workspace. An open graph uses a light gray, 32-unit dotted canvas.
|
||||||
|
- Edit → Settings → Libraries accepts document files or folders of JSON files.
|
||||||
|
|
||||||
|
Every component owns its ports, declarative icon, properties, and child graph:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"format": "bedit-document",
|
||||||
|
"version": 1,
|
||||||
|
"roots": [{
|
||||||
|
"id": "my-component",
|
||||||
|
"name": "My Component",
|
||||||
|
"position": {"x": 0, "y": 0},
|
||||||
|
"interface": {
|
||||||
|
"inputs": [{"id": "in", "name": "Input"}],
|
||||||
|
"outputs": [{"id": "out", "name": "Output"}]
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"shape": "rectangle",
|
||||||
|
"fill": "#dbeafe",
|
||||||
|
"border": "#245c9c",
|
||||||
|
"text": "Component"
|
||||||
|
},
|
||||||
|
"properties": {},
|
||||||
|
"implementation": {
|
||||||
|
"kind": "text",
|
||||||
|
"source": {
|
||||||
|
"equations": ["out = gain * in"],
|
||||||
|
"parameters": {"gain": 1.0}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Graph components use `"implementation": {"kind": "graph", "graph": ...}`;
|
||||||
|
text components use `"implementation": {"kind": "text", "source": ...}` and
|
||||||
|
never own a graph. Supported icon shapes are currently `rectangle` and
|
||||||
|
`ellipse`. The recursive model is under `src/bedit/document/`, library loading
|
||||||
|
and the live Current Document tree are under `src/bedit/library/`, and graphics
|
||||||
|
are isolated under `src/bedit/workspace/`.
|
||||||
|
|
||||||
## Optional tools
|
## Optional tools
|
||||||
|
|
||||||
You do not need another GUI framework. Useful additions are:
|
You do not need another GUI framework. Useful additions are:
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ bedit = "bedit.app:main"
|
|||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["src"]
|
where = ["src"]
|
||||||
|
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
bedit = ["data/libraries/*.json"]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
target-version = "py310"
|
target-version = "py310"
|
||||||
|
|
||||||
|
|||||||
BIN
BEdit/resources/icons/configure.png
Normal file
BIN
BEdit/resources/icons/configure.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
BEdit/resources/icons/media-playback-start.png
Normal file
BIN
BEdit/resources/icons/media-playback-start.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
BEdit/resources/icons/transform-rotate.png
Normal file
BIN
BEdit/resources/icons/transform-rotate.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
@@ -9,5 +9,6 @@
|
|||||||
<file>icons/document-save-as.png</file>
|
<file>icons/document-save-as.png</file>
|
||||||
<file>icons/document-open.png</file>
|
<file>icons/document-open.png</file>
|
||||||
<file>icons/document-new.png</file>
|
<file>icons/document-new.png</file>
|
||||||
|
<file>icons/transform-rotate.png</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|||||||
1089
BEdit/resources/resources_rc.py
Normal file
1089
BEdit/resources/resources_rc.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from PySide6.QtCore import QCoreApplication, Qt
|
from PySide6.QtCore import QCoreApplication, Qt
|
||||||
from PySide6.QtGui import QColor, QPalette, QIcon
|
from PySide6.QtGui import QColor, QPalette
|
||||||
from PySide6.QtWidgets import QApplication, QStyleFactory
|
from PySide6.QtWidgets import QApplication, QStyleFactory
|
||||||
|
|
||||||
from bedit.main_window import MainWindow
|
from bedit.main_window import MainWindow
|
||||||
|
|||||||
31
BEdit/src/bedit/component_options_dialog.py
Normal file
31
BEdit/src/bedit/component_options_dialog.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from PySide6.QtGui import QColor
|
||||||
|
from PySide6.QtWidgets import QDialog, QMessageBox
|
||||||
|
|
||||||
|
from bedit.document.model import Component
|
||||||
|
from bedit.ui_component_options_dialog import Ui_ComponentOptionsDialog
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentOptionsDialog(QDialog):
|
||||||
|
def __init__(self, component: Component, parent=None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.ui = Ui_ComponentOptionsDialog()
|
||||||
|
self.ui.setupUi(self)
|
||||||
|
self.ui.nameEdit.setText(component.name)
|
||||||
|
self.ui.shapeCombo.setCurrentText(component.icon.shape)
|
||||||
|
self.ui.iconTextEdit.setText(component.icon.text)
|
||||||
|
self.ui.fillEdit.setText(component.icon.fill)
|
||||||
|
self.ui.borderEdit.setText(component.icon.border)
|
||||||
|
self.ui.showSubtreeCheckBox.setChecked(component.show_subtree_in_library)
|
||||||
|
|
||||||
|
def accept(self) -> None:
|
||||||
|
if not self.ui.nameEdit.text().strip():
|
||||||
|
QMessageBox.warning(self, "Invalid name", "The component name cannot be empty.")
|
||||||
|
return
|
||||||
|
for label, value in (
|
||||||
|
("fill", self.ui.fillEdit.text()),
|
||||||
|
("border", self.ui.borderEdit.text()),
|
||||||
|
):
|
||||||
|
if not QColor(value).isValid():
|
||||||
|
QMessageBox.warning(self, "Invalid color", f"The {label} color is not valid.")
|
||||||
|
return
|
||||||
|
super().accept()
|
||||||
52
BEdit/src/bedit/data/libraries/example.json
Normal file
52
BEdit/src/bedit/data/libraries/example.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"format": "bedit-document",
|
||||||
|
"version": 1,
|
||||||
|
"metadata": {"name": "Example Library"},
|
||||||
|
"roots": [
|
||||||
|
{
|
||||||
|
"id": "example-a",
|
||||||
|
"name": "Block A",
|
||||||
|
"position": {"x": 0, "y": 0},
|
||||||
|
"interface": {
|
||||||
|
"inputs": [{"id": "in", "name": "Input", "position": {"x": 32, "y": 96}}],
|
||||||
|
"outputs": [{"id": "out", "name": "Output", "position": {"x": 320, "y": 96}}]
|
||||||
|
},
|
||||||
|
"icon": {"shape": "rectangle", "fill": "#dbeafe", "border": "#245c9c", "text": "A"},
|
||||||
|
"properties": {},
|
||||||
|
"library": {"showSubtree": true},
|
||||||
|
"implementation": {
|
||||||
|
"kind": "text",
|
||||||
|
"source": {"equations": ["out = gain * in"], "parameters": {"gain": 1.0}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "example-b",
|
||||||
|
"name": "Block B",
|
||||||
|
"position": {"x": 0, "y": 0},
|
||||||
|
"interface": {
|
||||||
|
"inputs": [{"id": "in", "name": "Input", "position": {"x": 32, "y": 96}}],
|
||||||
|
"outputs": [{"id": "out", "name": "Output", "position": {"x": 320, "y": 96}}]
|
||||||
|
},
|
||||||
|
"icon": {"shape": "ellipse", "fill": "#dcfce7", "border": "#277342", "text": "B"},
|
||||||
|
"properties": {},
|
||||||
|
"library": {"showSubtree": true},
|
||||||
|
"implementation": {
|
||||||
|
"kind": "text",
|
||||||
|
"source": {"equations": ["out = in + offset"], "parameters": {"offset": 0.0}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "example-c",
|
||||||
|
"name": "Block C",
|
||||||
|
"position": {"x": 0, "y": 0},
|
||||||
|
"interface": {
|
||||||
|
"inputs": [{"id": "in", "name": "Input", "position": {"x": 32, "y": 96}}],
|
||||||
|
"outputs": [{"id": "out", "name": "Output", "position": {"x": 320, "y": 96}}]
|
||||||
|
},
|
||||||
|
"icon": {"shape": "rectangle", "fill": "#fef3c7", "border": "#8a641c", "text": "C"},
|
||||||
|
"properties": {},
|
||||||
|
"library": {"showSubtree": true},
|
||||||
|
"implementation": {"kind": "graph", "graph": {"blocks": [], "connections": []}}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
351
BEdit/src/bedit/data/libraries/test.bedit.json
Normal file
351
BEdit/src/bedit/data/libraries/test.bedit.json
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
{
|
||||||
|
"format": "bedit-document",
|
||||||
|
"version": 1,
|
||||||
|
"metadata": {
|
||||||
|
"name": "Untitled"
|
||||||
|
},
|
||||||
|
"roots": [
|
||||||
|
{
|
||||||
|
"id": "97cd3d0b-c36a-467e-b1f5-4c794979bd99",
|
||||||
|
"name": "something",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 0.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"interface": {
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"id": "in",
|
||||||
|
"name": "Input",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 0.0
|
||||||
|
},
|
||||||
|
"properties": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": []
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"shape": "rectangle",
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"border": "#303030",
|
||||||
|
"text": "Something"
|
||||||
|
},
|
||||||
|
"properties": {},
|
||||||
|
"library": {
|
||||||
|
"showSubtree": true
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"kind": "text",
|
||||||
|
"source": {
|
||||||
|
"equations": [],
|
||||||
|
"parameters": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "614b4018-c00e-40ce-b59c-5d5795eab7d0",
|
||||||
|
"name": "Test",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 0.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"interface": {
|
||||||
|
"inputs": [],
|
||||||
|
"outputs": []
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"shape": "rectangle",
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"border": "#303030",
|
||||||
|
"text": "Test"
|
||||||
|
},
|
||||||
|
"properties": {},
|
||||||
|
"library": {
|
||||||
|
"showSubtree": false
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"kind": "graph",
|
||||||
|
"graph": {
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"id": "4ad9ab39-f332-4a9e-ae1a-02141b90a4ce",
|
||||||
|
"name": "sin",
|
||||||
|
"position": {
|
||||||
|
"x": -28.0,
|
||||||
|
"y": -179.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"interface": {
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"id": "input-0bcb214f",
|
||||||
|
"name": "a",
|
||||||
|
"position": {
|
||||||
|
"x": -223.0,
|
||||||
|
"y": -161.0
|
||||||
|
},
|
||||||
|
"properties": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"id": "output-a55cd289",
|
||||||
|
"name": "b",
|
||||||
|
"position": {
|
||||||
|
"x": 133.0,
|
||||||
|
"y": -152.0
|
||||||
|
},
|
||||||
|
"properties": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"shape": "rectangle",
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"border": "#303030",
|
||||||
|
"text": "sin()"
|
||||||
|
},
|
||||||
|
"properties": {},
|
||||||
|
"library": {
|
||||||
|
"showSubtree": false
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"kind": "graph",
|
||||||
|
"graph": {
|
||||||
|
"blocks": [],
|
||||||
|
"connections": [
|
||||||
|
{
|
||||||
|
"id": "cef30af9-fdc5-4a28-a7d6-4ab3c83e65d7",
|
||||||
|
"source": {
|
||||||
|
"interface": "input-0bcb214f"
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"interface": "output-a55cd289"
|
||||||
|
},
|
||||||
|
"name": "",
|
||||||
|
"properties": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "88f40d88-f62b-432f-a6a0-95513b48409e",
|
||||||
|
"name": "constant",
|
||||||
|
"position": {
|
||||||
|
"x": -260.0,
|
||||||
|
"y": -178.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"interface": {
|
||||||
|
"inputs": [],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"id": "output-1e15ff3f",
|
||||||
|
"name": "Output 1",
|
||||||
|
"position": {
|
||||||
|
"x": 71.0,
|
||||||
|
"y": -134.0
|
||||||
|
},
|
||||||
|
"properties": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"shape": "rectangle",
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"border": "#303030",
|
||||||
|
"text": "C"
|
||||||
|
},
|
||||||
|
"properties": {},
|
||||||
|
"library": {
|
||||||
|
"showSubtree": true
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"kind": "graph",
|
||||||
|
"graph": {
|
||||||
|
"blocks": [],
|
||||||
|
"connections": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "88941f2d-8edd-4dd9-a07b-3f206f6b76c5",
|
||||||
|
"name": "New Text Block 1",
|
||||||
|
"position": {
|
||||||
|
"x": 225.0,
|
||||||
|
"y": -167.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"interface": {
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"id": "in",
|
||||||
|
"name": "Input",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 0.0
|
||||||
|
},
|
||||||
|
"properties": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": []
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"shape": "rectangle",
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"border": "#303030",
|
||||||
|
"text": "Text"
|
||||||
|
},
|
||||||
|
"properties": {},
|
||||||
|
"library": {
|
||||||
|
"showSubtree": true
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"kind": "text",
|
||||||
|
"source": {
|
||||||
|
"equations": [],
|
||||||
|
"parameters": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": [
|
||||||
|
{
|
||||||
|
"id": "b3837790-26ba-46a6-91f9-3b6808c7ad1a",
|
||||||
|
"source": {
|
||||||
|
"block": "88f40d88-f62b-432f-a6a0-95513b48409e",
|
||||||
|
"port": "output-1e15ff3f"
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"block": "4ad9ab39-f332-4a9e-ae1a-02141b90a4ce",
|
||||||
|
"port": "input-0bcb214f"
|
||||||
|
},
|
||||||
|
"name": "",
|
||||||
|
"properties": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "50b2c7aa-d824-407c-ae0f-6b3bb4b1b79e",
|
||||||
|
"source": {
|
||||||
|
"block": "4ad9ab39-f332-4a9e-ae1a-02141b90a4ce",
|
||||||
|
"port": "output-a55cd289"
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"block": "88941f2d-8edd-4dd9-a07b-3f206f6b76c5",
|
||||||
|
"port": "in"
|
||||||
|
},
|
||||||
|
"name": "",
|
||||||
|
"properties": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "465796f5-9075-45f6-81b9-7fa17476392c",
|
||||||
|
"name": "sin",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 0.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"interface": {
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"id": "input-0bcb214f",
|
||||||
|
"name": "Input 1",
|
||||||
|
"position": {
|
||||||
|
"x": -223.0,
|
||||||
|
"y": -161.0
|
||||||
|
},
|
||||||
|
"properties": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"id": "output-a55cd289",
|
||||||
|
"name": "Output 1",
|
||||||
|
"position": {
|
||||||
|
"x": 133.0,
|
||||||
|
"y": -152.0
|
||||||
|
},
|
||||||
|
"properties": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"shape": "rectangle",
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"border": "#303030",
|
||||||
|
"text": "sin()"
|
||||||
|
},
|
||||||
|
"properties": {},
|
||||||
|
"library": {
|
||||||
|
"showSubtree": false
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"kind": "graph",
|
||||||
|
"graph": {
|
||||||
|
"blocks": [],
|
||||||
|
"connections": [
|
||||||
|
{
|
||||||
|
"id": "029ccfaa-7150-4a53-beec-286ff8e2a628",
|
||||||
|
"source": {
|
||||||
|
"interface": "input-0bcb214f"
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"interface": "output-a55cd289"
|
||||||
|
},
|
||||||
|
"name": "",
|
||||||
|
"properties": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bb0adff3-baf2-469b-9a80-f05910d0f259",
|
||||||
|
"name": "constant",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 0.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"interface": {
|
||||||
|
"inputs": [],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"id": "output-1e15ff3f",
|
||||||
|
"name": "Output 1",
|
||||||
|
"position": {
|
||||||
|
"x": 71.0,
|
||||||
|
"y": -134.0
|
||||||
|
},
|
||||||
|
"properties": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"shape": "rectangle",
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"border": "#303030",
|
||||||
|
"text": "C"
|
||||||
|
},
|
||||||
|
"properties": {},
|
||||||
|
"library": {
|
||||||
|
"showSubtree": true
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"kind": "graph",
|
||||||
|
"graph": {
|
||||||
|
"blocks": [],
|
||||||
|
"connections": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
13
BEdit/src/bedit/document/__init__.py
Normal file
13
BEdit/src/bedit/document/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from bedit.document.controller import DocumentController
|
||||||
|
from bedit.document.model import Component, Connection, Endpoint, Graph, GraphDocument, Icon, Port
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Component",
|
||||||
|
"Connection",
|
||||||
|
"DocumentController",
|
||||||
|
"Endpoint",
|
||||||
|
"Graph",
|
||||||
|
"GraphDocument",
|
||||||
|
"Icon",
|
||||||
|
"Port",
|
||||||
|
]
|
||||||
262
BEdit/src/bedit/document/commands.py
Normal file
262
BEdit/src/bedit/document/commands.py
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
from PySide6.QtCore import QPointF
|
||||||
|
from PySide6.QtGui import QUndoCommand
|
||||||
|
|
||||||
|
from bedit.document.model import Component, Connection, Port
|
||||||
|
|
||||||
|
|
||||||
|
class AddComponentCommand(QUndoCommand):
|
||||||
|
def __init__(self, controller, owner_id: str | None, component: Component) -> None:
|
||||||
|
super().__init__(f"Add {component.name}")
|
||||||
|
self.controller = controller
|
||||||
|
self.owner_id = owner_id
|
||||||
|
self.component = component
|
||||||
|
|
||||||
|
def redo(self) -> None:
|
||||||
|
self.controller._insert_component(self.owner_id, self.component)
|
||||||
|
|
||||||
|
def undo(self) -> None:
|
||||||
|
self.controller._remove_component(self.owner_id, self.component.id)
|
||||||
|
|
||||||
|
|
||||||
|
class MoveComponentCommand(QUndoCommand):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
controller,
|
||||||
|
owner_id: str,
|
||||||
|
component_id: str,
|
||||||
|
old: QPointF,
|
||||||
|
new: QPointF,
|
||||||
|
) -> None:
|
||||||
|
super().__init__("Move component")
|
||||||
|
self.controller = controller
|
||||||
|
self.owner_id = owner_id
|
||||||
|
self.component_id = component_id
|
||||||
|
self.old = old
|
||||||
|
self.new = new
|
||||||
|
|
||||||
|
def redo(self) -> None:
|
||||||
|
self.controller._move_component(self.owner_id, self.component_id, self.new)
|
||||||
|
|
||||||
|
def undo(self) -> None:
|
||||||
|
self.controller._move_component(self.owner_id, self.component_id, self.old)
|
||||||
|
|
||||||
|
|
||||||
|
class RotateComponentsCommand(QUndoCommand):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
controller,
|
||||||
|
owner_id: str,
|
||||||
|
rotations: dict[str, tuple[float, float]],
|
||||||
|
) -> None:
|
||||||
|
super().__init__("Rotate components")
|
||||||
|
self.controller = controller
|
||||||
|
self.owner_id = owner_id
|
||||||
|
self.rotations = rotations
|
||||||
|
|
||||||
|
def redo(self) -> None:
|
||||||
|
for component_id, (_old, new) in self.rotations.items():
|
||||||
|
self.controller._rotate_component(self.owner_id, component_id, new)
|
||||||
|
|
||||||
|
def undo(self) -> None:
|
||||||
|
for component_id, (old, _new) in self.rotations.items():
|
||||||
|
self.controller._rotate_component(self.owner_id, component_id, old)
|
||||||
|
|
||||||
|
|
||||||
|
class AddConnectionCommand(QUndoCommand):
|
||||||
|
def __init__(self, controller, owner_id: str, connection: Connection) -> None:
|
||||||
|
super().__init__("Connect components")
|
||||||
|
self.controller = controller
|
||||||
|
self.owner_id = owner_id
|
||||||
|
self.connection = connection
|
||||||
|
|
||||||
|
def redo(self) -> None:
|
||||||
|
self.controller._insert_connection(self.owner_id, self.connection)
|
||||||
|
|
||||||
|
def undo(self) -> None:
|
||||||
|
self.controller._remove_connection(self.owner_id, self.connection.id)
|
||||||
|
|
||||||
|
|
||||||
|
class ReplaceComponentCommand(QUndoCommand):
|
||||||
|
def __init__(self, controller, old: Component, new: Component) -> None:
|
||||||
|
super().__init__("Apply JSON changes")
|
||||||
|
self.controller = controller
|
||||||
|
self.old = old
|
||||||
|
self.new = new
|
||||||
|
|
||||||
|
def redo(self) -> None:
|
||||||
|
self.controller._replace_component(self.old.id, self.new)
|
||||||
|
|
||||||
|
def undo(self) -> None:
|
||||||
|
self.controller._replace_component(self.new.id, self.old)
|
||||||
|
|
||||||
|
|
||||||
|
class AddInterfacePortCommand(QUndoCommand):
|
||||||
|
def __init__(self, controller, owner_id: str, direction: str, port: Port) -> None:
|
||||||
|
super().__init__(f"Add {direction}")
|
||||||
|
self.controller = controller
|
||||||
|
self.owner_id = owner_id
|
||||||
|
self.direction = direction
|
||||||
|
self.port = port
|
||||||
|
|
||||||
|
def redo(self) -> None:
|
||||||
|
self.controller._insert_interface_port(self.owner_id, self.direction, self.port)
|
||||||
|
|
||||||
|
def undo(self) -> None:
|
||||||
|
self.controller._remove_interface_port(self.owner_id, self.direction, self.port.id)
|
||||||
|
|
||||||
|
|
||||||
|
class ReplaceSourceCommand(QUndoCommand):
|
||||||
|
def __init__(self, controller, component_id: str, old: dict, new: dict) -> None:
|
||||||
|
super().__init__("Apply source JSON")
|
||||||
|
self.controller = controller
|
||||||
|
self.component_id = component_id
|
||||||
|
self.old = old
|
||||||
|
self.new = new
|
||||||
|
|
||||||
|
def redo(self) -> None:
|
||||||
|
self.controller._replace_source(self.component_id, self.new)
|
||||||
|
|
||||||
|
def undo(self) -> None:
|
||||||
|
self.controller._replace_source(self.component_id, self.old)
|
||||||
|
|
||||||
|
|
||||||
|
class MoveInterfacePortCommand(QUndoCommand):
|
||||||
|
def __init__(self, controller, owner_id: str, port_id: str, old: QPointF, new: QPointF) -> None:
|
||||||
|
super().__init__("Move interface terminal")
|
||||||
|
self.controller = controller
|
||||||
|
self.owner_id = owner_id
|
||||||
|
self.port_id = port_id
|
||||||
|
self.old = old
|
||||||
|
self.new = new
|
||||||
|
|
||||||
|
def redo(self) -> None:
|
||||||
|
self.controller._move_interface_port(self.owner_id, self.port_id, self.new)
|
||||||
|
|
||||||
|
def undo(self) -> None:
|
||||||
|
self.controller._move_interface_port(self.owner_id, self.port_id, self.old)
|
||||||
|
|
||||||
|
|
||||||
|
class EditComponentAppearanceCommand(QUndoCommand):
|
||||||
|
def __init__(self, controller, component_id: str, old: dict, new: dict) -> None:
|
||||||
|
super().__init__("Edit component appearance")
|
||||||
|
self.controller = controller
|
||||||
|
self.component_id = component_id
|
||||||
|
self.old = old
|
||||||
|
self.new = new
|
||||||
|
|
||||||
|
def redo(self) -> None:
|
||||||
|
self.controller._set_component_appearance(self.component_id, self.new)
|
||||||
|
|
||||||
|
def undo(self) -> None:
|
||||||
|
self.controller._set_component_appearance(self.component_id, self.old)
|
||||||
|
|
||||||
|
|
||||||
|
class RenameInterfacePortCommand(QUndoCommand):
|
||||||
|
def __init__(self, controller, owner_id: str, port_id: str, old: str, new: str) -> None:
|
||||||
|
super().__init__("Rename interface port")
|
||||||
|
self.controller, self.owner_id, self.port_id = controller, owner_id, port_id
|
||||||
|
self.old, self.new = old, new
|
||||||
|
|
||||||
|
def redo(self) -> None:
|
||||||
|
self.controller._rename_interface_port(self.owner_id, self.port_id, self.new)
|
||||||
|
|
||||||
|
def undo(self) -> None:
|
||||||
|
self.controller._rename_interface_port(self.owner_id, self.port_id, self.old)
|
||||||
|
|
||||||
|
|
||||||
|
class RenameConnectionCommand(QUndoCommand):
|
||||||
|
def __init__(self, controller, owner_id: str, connection_id: str, old: str, new: str) -> None:
|
||||||
|
super().__init__("Rename connection")
|
||||||
|
self.controller, self.owner_id, self.connection_id = controller, owner_id, connection_id
|
||||||
|
self.old, self.new = old, new
|
||||||
|
|
||||||
|
def redo(self) -> None:
|
||||||
|
self.controller._rename_connection(self.owner_id, self.connection_id, self.new)
|
||||||
|
|
||||||
|
def undo(self) -> None:
|
||||||
|
self.controller._rename_connection(self.owner_id, self.connection_id, self.old)
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteSelectionCommand(QUndoCommand):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
controller,
|
||||||
|
owner_id: str | None,
|
||||||
|
blocks: dict[str, Component],
|
||||||
|
connections: dict[str, Connection],
|
||||||
|
inputs: list[Port],
|
||||||
|
outputs: list[Port],
|
||||||
|
) -> None:
|
||||||
|
super().__init__("Delete selection")
|
||||||
|
self.controller = controller
|
||||||
|
self.owner_id = owner_id
|
||||||
|
self.blocks = blocks
|
||||||
|
self.connections = connections
|
||||||
|
self.inputs = inputs
|
||||||
|
self.outputs = outputs
|
||||||
|
|
||||||
|
def redo(self) -> None:
|
||||||
|
self.controller._delete_items(
|
||||||
|
self.owner_id,
|
||||||
|
set(self.blocks),
|
||||||
|
set(self.connections),
|
||||||
|
{port.id for port in self.inputs},
|
||||||
|
{port.id for port in self.outputs},
|
||||||
|
)
|
||||||
|
|
||||||
|
def undo(self) -> None:
|
||||||
|
self.controller._restore_items(
|
||||||
|
self.owner_id,
|
||||||
|
self.blocks,
|
||||||
|
self.connections,
|
||||||
|
self.inputs,
|
||||||
|
self.outputs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PasteSelectionCommand(QUndoCommand):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
controller,
|
||||||
|
owner_id: str,
|
||||||
|
blocks: dict[str, Component],
|
||||||
|
connections: dict[str, Connection],
|
||||||
|
) -> None:
|
||||||
|
super().__init__("Paste selection")
|
||||||
|
self.controller = controller
|
||||||
|
self.owner_id = owner_id
|
||||||
|
self.blocks = blocks
|
||||||
|
self.connections = connections
|
||||||
|
|
||||||
|
def redo(self) -> None:
|
||||||
|
self.controller._restore_items(
|
||||||
|
self.owner_id,
|
||||||
|
self.blocks,
|
||||||
|
self.connections,
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
def undo(self) -> None:
|
||||||
|
self.controller._delete_items(
|
||||||
|
self.owner_id,
|
||||||
|
set(self.blocks),
|
||||||
|
set(self.connections),
|
||||||
|
set(),
|
||||||
|
set(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EditTextDefinitionCommand(QUndoCommand):
|
||||||
|
def __init__(self, controller, component_id: str, old: dict, new: dict) -> None:
|
||||||
|
super().__init__("Edit text component")
|
||||||
|
self.controller = controller
|
||||||
|
self.component_id = component_id
|
||||||
|
self.old = old
|
||||||
|
self.new = new
|
||||||
|
|
||||||
|
def redo(self) -> None:
|
||||||
|
self.controller._set_text_definition(self.component_id, self.new)
|
||||||
|
|
||||||
|
def undo(self) -> None:
|
||||||
|
self.controller._set_text_definition(self.component_id, self.old)
|
||||||
673
BEdit/src/bedit/document/controller.py
Normal file
673
BEdit/src/bedit/document/controller.py
Normal file
@@ -0,0 +1,673 @@
|
|||||||
|
from copy import deepcopy
|
||||||
|
from pathlib import Path
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from PySide6.QtCore import QObject, QPointF, Signal
|
||||||
|
from PySide6.QtGui import QUndoStack
|
||||||
|
|
||||||
|
from bedit.document.commands import (
|
||||||
|
AddComponentCommand,
|
||||||
|
AddConnectionCommand,
|
||||||
|
AddInterfacePortCommand,
|
||||||
|
DeleteSelectionCommand,
|
||||||
|
EditTextDefinitionCommand,
|
||||||
|
EditComponentAppearanceCommand,
|
||||||
|
MoveComponentCommand,
|
||||||
|
MoveInterfacePortCommand,
|
||||||
|
PasteSelectionCommand,
|
||||||
|
RenameConnectionCommand,
|
||||||
|
RenameInterfacePortCommand,
|
||||||
|
ReplaceSourceCommand,
|
||||||
|
RotateComponentsCommand,
|
||||||
|
)
|
||||||
|
from bedit.document.model import (
|
||||||
|
Component,
|
||||||
|
Connection,
|
||||||
|
Endpoint,
|
||||||
|
GraphDocument,
|
||||||
|
Icon,
|
||||||
|
Port,
|
||||||
|
clone_component,
|
||||||
|
)
|
||||||
|
from bedit.document.serializer import JsonDocumentSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentController(QObject):
|
||||||
|
documentReset = Signal()
|
||||||
|
documentOpenedChanged = Signal(bool)
|
||||||
|
activeGraphChanged = Signal()
|
||||||
|
componentAdded = Signal(str)
|
||||||
|
componentRemoved = Signal(str)
|
||||||
|
componentMoved = Signal(str, QPointF)
|
||||||
|
componentRotated = Signal(str, float)
|
||||||
|
connectionAdded = Signal(str)
|
||||||
|
connectionRemoved = Signal(str)
|
||||||
|
interfaceChanged = Signal()
|
||||||
|
filePathChanged = Signal(object)
|
||||||
|
modifiedChanged = Signal(bool)
|
||||||
|
|
||||||
|
def __init__(self, parent=None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.document: GraphDocument | None = None
|
||||||
|
self.active_component_id: str | None = None
|
||||||
|
self.file_path: Path | None = None
|
||||||
|
self.undo_stack = QUndoStack(self)
|
||||||
|
self.undo_stack.cleanChanged.connect(self._clean_changed)
|
||||||
|
|
||||||
|
def _clean_changed(self, clean: bool) -> None:
|
||||||
|
self.modifiedChanged.emit(not clean)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def active_component(self) -> Component | None:
|
||||||
|
if self.document is None or self.active_component_id is None:
|
||||||
|
return None
|
||||||
|
return self.document.find_component(self.active_component_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def active_graph(self):
|
||||||
|
component = self.active_component
|
||||||
|
if component is None or component.implementation_kind != "graph":
|
||||||
|
raise ValueError("There is no active graph")
|
||||||
|
return component.graph
|
||||||
|
|
||||||
|
def new_document(self) -> None:
|
||||||
|
self.document = GraphDocument.empty()
|
||||||
|
self.active_component_id = None
|
||||||
|
self.file_path = None
|
||||||
|
self.undo_stack.clear()
|
||||||
|
self.documentOpenedChanged.emit(True)
|
||||||
|
self.documentReset.emit()
|
||||||
|
self.activeGraphChanged.emit()
|
||||||
|
self.filePathChanged.emit(None)
|
||||||
|
|
||||||
|
def close_document(self) -> None:
|
||||||
|
self.document = None
|
||||||
|
self.active_component_id = None
|
||||||
|
self.file_path = None
|
||||||
|
self.undo_stack.clear()
|
||||||
|
self.documentOpenedChanged.emit(False)
|
||||||
|
self.documentReset.emit()
|
||||||
|
self.activeGraphChanged.emit()
|
||||||
|
self.filePathChanged.emit(None)
|
||||||
|
|
||||||
|
def load(self, path: Path) -> None:
|
||||||
|
self.document = JsonDocumentSerializer.load(path)
|
||||||
|
self.active_component_id = next(iter(self.document.roots), None)
|
||||||
|
self.file_path = path
|
||||||
|
self.undo_stack.clear()
|
||||||
|
self.undo_stack.setClean()
|
||||||
|
self.documentOpenedChanged.emit(True)
|
||||||
|
self.documentReset.emit()
|
||||||
|
self.activeGraphChanged.emit()
|
||||||
|
self.filePathChanged.emit(path)
|
||||||
|
|
||||||
|
def save(self, path: Path | None = None) -> Path:
|
||||||
|
if self.document is None:
|
||||||
|
raise ValueError("There is no open document")
|
||||||
|
target = path or self.file_path
|
||||||
|
if target is None:
|
||||||
|
raise ValueError("No file path has been selected")
|
||||||
|
JsonDocumentSerializer.save(self.document, target)
|
||||||
|
self.file_path = target
|
||||||
|
self.undo_stack.setClean()
|
||||||
|
self.filePathChanged.emit(target)
|
||||||
|
return target
|
||||||
|
|
||||||
|
def activate_component(self, component_id: str) -> None:
|
||||||
|
if self.document is None or self.document.find_component(component_id) is None:
|
||||||
|
return
|
||||||
|
self.active_component_id = component_id
|
||||||
|
self.activeGraphChanged.emit()
|
||||||
|
|
||||||
|
def navigate_up(self) -> None:
|
||||||
|
if self.document is None or self.active_component_id is None:
|
||||||
|
return
|
||||||
|
parent = self.document.find_parent(self.active_component_id)
|
||||||
|
if parent is not None:
|
||||||
|
self.activate_component(parent.id)
|
||||||
|
|
||||||
|
def breadcrumb(self) -> list[str]:
|
||||||
|
if self.document is None or self.active_component is None:
|
||||||
|
return []
|
||||||
|
names = [self.active_component.name]
|
||||||
|
current = self.active_component
|
||||||
|
while True:
|
||||||
|
parent = self.document.find_parent(current.id)
|
||||||
|
if parent is None:
|
||||||
|
break
|
||||||
|
names.append(parent.name)
|
||||||
|
current = parent
|
||||||
|
return list(reversed(names))
|
||||||
|
|
||||||
|
def add_root(self, kind: str) -> str:
|
||||||
|
if self.document is None:
|
||||||
|
raise ValueError("Open or create a document first")
|
||||||
|
number = len(self.document.roots) + 1
|
||||||
|
component = Component(
|
||||||
|
id=str(uuid4()),
|
||||||
|
name=f"New {'Graph' if kind == 'graph' else 'Text'} Block {number}",
|
||||||
|
implementation_kind=kind,
|
||||||
|
icon=Icon(text="Graph" if kind == "graph" else "Text"),
|
||||||
|
source={"equations": [], "parameters": {}} if kind == "text" else {},
|
||||||
|
)
|
||||||
|
self.undo_stack.push(AddComponentCommand(self, None, component))
|
||||||
|
self.activate_component(component.id)
|
||||||
|
return component.id
|
||||||
|
|
||||||
|
def add_child(self, owner_id: str, kind: str) -> str:
|
||||||
|
if self.document is None:
|
||||||
|
raise ValueError("Open or create a document first")
|
||||||
|
owner = self.document.find_component(owner_id)
|
||||||
|
if owner is None or owner.implementation_kind != "graph":
|
||||||
|
raise ValueError("Children can only be added to graph components")
|
||||||
|
number = len(owner.graph.blocks) + 1
|
||||||
|
component = Component(
|
||||||
|
id=str(uuid4()),
|
||||||
|
name=f"New {'Graph' if kind == 'graph' else 'Text'} Block {number}",
|
||||||
|
implementation_kind=kind,
|
||||||
|
icon=Icon(text="Graph" if kind == "graph" else "Text"),
|
||||||
|
source={"equations": [], "parameters": {}} if kind == "text" else {},
|
||||||
|
)
|
||||||
|
self.undo_stack.push(AddComponentCommand(self, owner_id, component))
|
||||||
|
return component.id
|
||||||
|
|
||||||
|
def delete_component(self, component_id: str) -> None:
|
||||||
|
if self.document is None:
|
||||||
|
return
|
||||||
|
component = self.document.find_component(component_id)
|
||||||
|
if component is None:
|
||||||
|
return
|
||||||
|
parent = self.document.find_parent(component_id)
|
||||||
|
owner_id = parent.id if parent else None
|
||||||
|
connections = {}
|
||||||
|
if parent is not None:
|
||||||
|
connections = {
|
||||||
|
connection.id: connection
|
||||||
|
for connection in parent.graph.connections.values()
|
||||||
|
if component_id in (connection.source.block, connection.target.block)
|
||||||
|
}
|
||||||
|
self.undo_stack.push(
|
||||||
|
DeleteSelectionCommand(
|
||||||
|
self,
|
||||||
|
owner_id,
|
||||||
|
{component_id: component},
|
||||||
|
connections,
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_component_copy(self, source: Component, position: QPointF) -> str:
|
||||||
|
if self.active_component is None or self.active_component.implementation_kind != "graph":
|
||||||
|
raise ValueError("Open a graph component before placing components")
|
||||||
|
component = clone_component(source)
|
||||||
|
component.x, component.y = position.x(), position.y()
|
||||||
|
self.undo_stack.push(AddComponentCommand(self, self.active_component_id, component))
|
||||||
|
return component.id
|
||||||
|
|
||||||
|
def move_component(self, component_id: str, old: QPointF, new: QPointF) -> None:
|
||||||
|
if old != new and self.active_component_id is not None:
|
||||||
|
self.undo_stack.push(
|
||||||
|
MoveComponentCommand(self, self.active_component_id, component_id, old, new)
|
||||||
|
)
|
||||||
|
|
||||||
|
def rotate_components(self, component_ids: set[str]) -> None:
|
||||||
|
if self.active_component is None or self.active_component_id is None:
|
||||||
|
return
|
||||||
|
rotations = {
|
||||||
|
component_id: (component.rotation, (component.rotation + 90.0) % 360.0)
|
||||||
|
for component_id in component_ids
|
||||||
|
if (component := self.active_component.graph.blocks.get(component_id)) is not None
|
||||||
|
}
|
||||||
|
if rotations:
|
||||||
|
self.undo_stack.push(
|
||||||
|
RotateComponentsCommand(self, self.active_component_id, rotations)
|
||||||
|
)
|
||||||
|
|
||||||
|
def connect(self, source: Endpoint, target: Endpoint) -> str:
|
||||||
|
if self.active_component_id is None:
|
||||||
|
raise ValueError("There is no active graph")
|
||||||
|
connection = Connection(str(uuid4()), source, target)
|
||||||
|
self.undo_stack.push(
|
||||||
|
AddConnectionCommand(self, self.active_component_id, connection)
|
||||||
|
)
|
||||||
|
return connection.id
|
||||||
|
|
||||||
|
def add_interface_port(self, direction: str, position: QPointF) -> str:
|
||||||
|
component = self.active_component
|
||||||
|
if component is None or component.implementation_kind != "graph":
|
||||||
|
raise ValueError("Open a graph component before adding an interface")
|
||||||
|
ports = component.inputs if direction == "input" else component.outputs
|
||||||
|
port = Port(
|
||||||
|
id=f"{direction}-{uuid4().hex[:8]}",
|
||||||
|
name=f"{direction.title()} {len(ports) + 1}",
|
||||||
|
x=position.x(),
|
||||||
|
y=position.y(),
|
||||||
|
)
|
||||||
|
self.undo_stack.push(
|
||||||
|
AddInterfacePortCommand(self, component.id, direction, port)
|
||||||
|
)
|
||||||
|
return port.id
|
||||||
|
|
||||||
|
def move_interface_port(self, port_id: str, old: QPointF, new: QPointF) -> None:
|
||||||
|
if old != new and self.active_component_id is not None:
|
||||||
|
self.undo_stack.push(
|
||||||
|
MoveInterfacePortCommand(self, self.active_component_id, port_id, old, new)
|
||||||
|
)
|
||||||
|
|
||||||
|
def rename_interface_port(self, port_id: str, name: str) -> None:
|
||||||
|
owner = self.active_component
|
||||||
|
if owner is None:
|
||||||
|
return
|
||||||
|
port = next((p for p in (*owner.inputs, *owner.outputs) if p.id == port_id), None)
|
||||||
|
if port is not None and port.name != name:
|
||||||
|
self.undo_stack.push(
|
||||||
|
RenameInterfacePortCommand(self, owner.id, port_id, port.name, name)
|
||||||
|
)
|
||||||
|
|
||||||
|
def rename_connection(self, connection_id: str, name: str) -> None:
|
||||||
|
owner = self.active_component
|
||||||
|
if owner is None or owner.implementation_kind != "graph":
|
||||||
|
return
|
||||||
|
connection = owner.graph.connections.get(connection_id)
|
||||||
|
if connection is not None and connection.name != name:
|
||||||
|
self.undo_stack.push(
|
||||||
|
RenameConnectionCommand(self, owner.id, connection_id, connection.name, name)
|
||||||
|
)
|
||||||
|
|
||||||
|
def replace_active_source(self, source: dict) -> None:
|
||||||
|
component = self.active_component
|
||||||
|
if component is None or component.implementation_kind != "text":
|
||||||
|
raise ValueError("Only text-defined components have source JSON")
|
||||||
|
self.undo_stack.push(
|
||||||
|
ReplaceSourceCommand(
|
||||||
|
self,
|
||||||
|
component.id,
|
||||||
|
deepcopy(component.source),
|
||||||
|
deepcopy(source),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def replace_active_text_definition(
|
||||||
|
self,
|
||||||
|
inputs: list[Port],
|
||||||
|
outputs: list[Port],
|
||||||
|
source: dict,
|
||||||
|
) -> None:
|
||||||
|
component = self.active_component
|
||||||
|
if component is None or component.implementation_kind != "text":
|
||||||
|
raise ValueError("Only text-defined components can be edited here")
|
||||||
|
input_ids = [port.id for port in inputs]
|
||||||
|
output_ids = [port.id for port in outputs]
|
||||||
|
if len(set(input_ids)) != len(input_ids) or len(set(output_ids)) != len(output_ids):
|
||||||
|
raise ValueError("Input and output IDs must be unique")
|
||||||
|
if self.document is not None:
|
||||||
|
parent = self.document.find_parent(component.id)
|
||||||
|
if parent is not None:
|
||||||
|
for connection in parent.graph.connections.values():
|
||||||
|
if connection.target.block == component.id and connection.target.port not in input_ids:
|
||||||
|
raise ValueError(
|
||||||
|
f"Input {connection.target.port!r} is still connected in the containing graph"
|
||||||
|
)
|
||||||
|
if connection.source.block == component.id and connection.source.port not in output_ids:
|
||||||
|
raise ValueError(
|
||||||
|
f"Output {connection.source.port!r} is still connected in the containing graph"
|
||||||
|
)
|
||||||
|
old = {
|
||||||
|
"inputs": [port.to_dict() for port in component.inputs],
|
||||||
|
"outputs": [port.to_dict() for port in component.outputs],
|
||||||
|
"source": deepcopy(component.source),
|
||||||
|
}
|
||||||
|
new = {
|
||||||
|
"inputs": [port.to_dict() for port in inputs],
|
||||||
|
"outputs": [port.to_dict() for port in outputs],
|
||||||
|
"source": deepcopy(source),
|
||||||
|
}
|
||||||
|
if old != new:
|
||||||
|
self.undo_stack.push(EditTextDefinitionCommand(self, component.id, old, new))
|
||||||
|
|
||||||
|
def edit_component_appearance(
|
||||||
|
self,
|
||||||
|
component_id: str,
|
||||||
|
name: str,
|
||||||
|
shape: str,
|
||||||
|
fill: str,
|
||||||
|
border: str,
|
||||||
|
text: str,
|
||||||
|
show_subtree: bool,
|
||||||
|
) -> None:
|
||||||
|
if self.document is None:
|
||||||
|
return
|
||||||
|
component = self.document.find_component(component_id)
|
||||||
|
if component is None:
|
||||||
|
return
|
||||||
|
old = {
|
||||||
|
"name": component.name,
|
||||||
|
**component.icon.to_dict(),
|
||||||
|
"show_subtree": component.show_subtree_in_library,
|
||||||
|
}
|
||||||
|
new = {
|
||||||
|
"name": name,
|
||||||
|
"shape": shape,
|
||||||
|
"fill": fill,
|
||||||
|
"border": border,
|
||||||
|
"text": text,
|
||||||
|
"show_subtree": show_subtree,
|
||||||
|
}
|
||||||
|
if old != new:
|
||||||
|
self.undo_stack.push(EditComponentAppearanceCommand(self, component_id, old, new))
|
||||||
|
|
||||||
|
def delete_selection(
|
||||||
|
self,
|
||||||
|
block_ids: set[str],
|
||||||
|
connection_ids: set[str],
|
||||||
|
input_ids: set[str],
|
||||||
|
output_ids: set[str],
|
||||||
|
) -> None:
|
||||||
|
component = self.active_component
|
||||||
|
if component is None or component.implementation_kind != "graph":
|
||||||
|
return
|
||||||
|
graph = component.graph
|
||||||
|
all_connection_ids = set(connection_ids)
|
||||||
|
for connection in graph.connections.values():
|
||||||
|
if (
|
||||||
|
connection.source.block in block_ids
|
||||||
|
or connection.target.block in block_ids
|
||||||
|
or connection.source.interface in input_ids
|
||||||
|
or connection.target.interface in output_ids
|
||||||
|
):
|
||||||
|
all_connection_ids.add(connection.id)
|
||||||
|
blocks = {block_id: graph.blocks[block_id] for block_id in block_ids if block_id in graph.blocks}
|
||||||
|
connections = {
|
||||||
|
connection_id: graph.connections[connection_id]
|
||||||
|
for connection_id in all_connection_ids
|
||||||
|
if connection_id in graph.connections
|
||||||
|
}
|
||||||
|
inputs = [port for port in component.inputs if port.id in input_ids]
|
||||||
|
outputs = [port for port in component.outputs if port.id in output_ids]
|
||||||
|
if not (blocks or connections or inputs or outputs):
|
||||||
|
return
|
||||||
|
self.undo_stack.push(
|
||||||
|
DeleteSelectionCommand(
|
||||||
|
self,
|
||||||
|
component.id,
|
||||||
|
blocks,
|
||||||
|
connections,
|
||||||
|
inputs,
|
||||||
|
outputs,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def paste_selection(
|
||||||
|
self,
|
||||||
|
source_components: list[Component],
|
||||||
|
source_connections: list[Connection],
|
||||||
|
offset: QPointF,
|
||||||
|
) -> list[str]:
|
||||||
|
owner = self.active_component
|
||||||
|
if owner is None or owner.implementation_kind != "graph":
|
||||||
|
return []
|
||||||
|
pairs = [(source, clone_component(source)) for source in source_components]
|
||||||
|
id_map = {source.id: clone.id for source, clone in pairs}
|
||||||
|
blocks = {}
|
||||||
|
for _source, clone in pairs:
|
||||||
|
clone.x += offset.x()
|
||||||
|
clone.y += offset.y()
|
||||||
|
blocks[clone.id] = clone
|
||||||
|
connections = {}
|
||||||
|
for source in source_connections:
|
||||||
|
if source.source.block not in id_map or source.target.block not in id_map:
|
||||||
|
continue
|
||||||
|
connection = Connection(
|
||||||
|
id=str(uuid4()),
|
||||||
|
source=Endpoint(block=id_map[source.source.block], port=source.source.port),
|
||||||
|
target=Endpoint(block=id_map[source.target.block], port=source.target.port),
|
||||||
|
)
|
||||||
|
connections[connection.id] = connection
|
||||||
|
if blocks:
|
||||||
|
self.undo_stack.push(
|
||||||
|
PasteSelectionCommand(self, owner.id, blocks, connections)
|
||||||
|
)
|
||||||
|
return list(blocks)
|
||||||
|
|
||||||
|
def _graph_for(self, owner_id: str):
|
||||||
|
if self.document is None:
|
||||||
|
raise ValueError("There is no open document")
|
||||||
|
owner = self.document.find_component(owner_id)
|
||||||
|
if owner is None:
|
||||||
|
raise ValueError("The containing component is no longer in the document")
|
||||||
|
return owner.graph
|
||||||
|
|
||||||
|
def _insert_component(self, owner_id: str | None, component: Component) -> None:
|
||||||
|
if self.document is None:
|
||||||
|
raise ValueError("There is no open document")
|
||||||
|
if owner_id is None:
|
||||||
|
self.document.roots[component.id] = component
|
||||||
|
else:
|
||||||
|
self._graph_for(owner_id).blocks[component.id] = component
|
||||||
|
if owner_id == self.active_component_id:
|
||||||
|
self.componentAdded.emit(component.id)
|
||||||
|
self.documentReset.emit()
|
||||||
|
|
||||||
|
def _remove_component(self, owner_id: str | None, component_id: str) -> None:
|
||||||
|
if self.document is None:
|
||||||
|
return
|
||||||
|
if owner_id is None:
|
||||||
|
self.document.roots.pop(component_id, None)
|
||||||
|
if self.active_component_id == component_id:
|
||||||
|
self.active_component_id = next(iter(self.document.roots), None)
|
||||||
|
self.activeGraphChanged.emit()
|
||||||
|
else:
|
||||||
|
self._graph_for(owner_id).blocks.pop(component_id, None)
|
||||||
|
if owner_id == self.active_component_id:
|
||||||
|
self.componentRemoved.emit(component_id)
|
||||||
|
self.documentReset.emit()
|
||||||
|
|
||||||
|
def _move_component(self, owner_id: str, component_id: str, position: QPointF) -> None:
|
||||||
|
component = self._graph_for(owner_id).blocks[component_id]
|
||||||
|
component.x, component.y = position.x(), position.y()
|
||||||
|
if owner_id == self.active_component_id:
|
||||||
|
self.componentMoved.emit(component_id, position)
|
||||||
|
self.documentReset.emit()
|
||||||
|
|
||||||
|
def _rotate_component(
|
||||||
|
self, owner_id: str, component_id: str, rotation: float
|
||||||
|
) -> None:
|
||||||
|
component = self._graph_for(owner_id).blocks.get(component_id)
|
||||||
|
if component is None:
|
||||||
|
return
|
||||||
|
component.rotation = rotation
|
||||||
|
if owner_id == self.active_component_id:
|
||||||
|
self.componentRotated.emit(component_id, rotation)
|
||||||
|
|
||||||
|
def _insert_connection(self, owner_id: str, connection: Connection) -> None:
|
||||||
|
self._graph_for(owner_id).connections[connection.id] = connection
|
||||||
|
if owner_id == self.active_component_id:
|
||||||
|
self.connectionAdded.emit(connection.id)
|
||||||
|
self.documentReset.emit()
|
||||||
|
|
||||||
|
def _remove_connection(self, owner_id: str, connection_id: str) -> None:
|
||||||
|
self._graph_for(owner_id).connections.pop(connection_id, None)
|
||||||
|
if owner_id == self.active_component_id:
|
||||||
|
self.connectionRemoved.emit(connection_id)
|
||||||
|
self.documentReset.emit()
|
||||||
|
|
||||||
|
def _replace_component(self, old_id: str, replacement: Component) -> None:
|
||||||
|
if self.document is None:
|
||||||
|
return
|
||||||
|
was_active = self.active_component_id == old_id
|
||||||
|
if old_id in self.document.roots:
|
||||||
|
self.document.roots.pop(old_id)
|
||||||
|
self.document.roots[replacement.id] = replacement
|
||||||
|
else:
|
||||||
|
parent = self.document.find_parent(old_id)
|
||||||
|
if parent is None:
|
||||||
|
raise ValueError("The component is no longer in this document")
|
||||||
|
parent.graph.blocks.pop(old_id)
|
||||||
|
parent.graph.blocks[replacement.id] = replacement
|
||||||
|
if was_active:
|
||||||
|
self.active_component_id = replacement.id
|
||||||
|
self.document.validate()
|
||||||
|
self.documentReset.emit()
|
||||||
|
if was_active:
|
||||||
|
self.activeGraphChanged.emit()
|
||||||
|
|
||||||
|
def _insert_interface_port(self, owner_id: str, direction: str, port: Port) -> None:
|
||||||
|
if self.document is None:
|
||||||
|
return
|
||||||
|
owner = self.document.find_component(owner_id)
|
||||||
|
if owner is None:
|
||||||
|
return
|
||||||
|
ports = owner.inputs if direction == "input" else owner.outputs
|
||||||
|
if all(existing.id != port.id for existing in ports):
|
||||||
|
ports.append(port)
|
||||||
|
self.interfaceChanged.emit()
|
||||||
|
self.documentReset.emit()
|
||||||
|
|
||||||
|
def _remove_interface_port(self, owner_id: str, direction: str, port_id: str) -> None:
|
||||||
|
if self.document is None:
|
||||||
|
return
|
||||||
|
owner = self.document.find_component(owner_id)
|
||||||
|
if owner is None:
|
||||||
|
return
|
||||||
|
ports = owner.inputs if direction == "input" else owner.outputs
|
||||||
|
ports[:] = [port for port in ports if port.id != port_id]
|
||||||
|
self.interfaceChanged.emit()
|
||||||
|
self.documentReset.emit()
|
||||||
|
|
||||||
|
def _move_interface_port(self, owner_id: str, port_id: str, position: QPointF) -> None:
|
||||||
|
if self.document is None:
|
||||||
|
return
|
||||||
|
owner = self.document.find_component(owner_id)
|
||||||
|
if owner is None:
|
||||||
|
return
|
||||||
|
port = next((p for p in (*owner.inputs, *owner.outputs) if p.id == port_id), None)
|
||||||
|
if port is not None:
|
||||||
|
port.x, port.y = position.x(), position.y()
|
||||||
|
self.interfaceChanged.emit()
|
||||||
|
self.documentReset.emit()
|
||||||
|
|
||||||
|
def _rename_interface_port(self, owner_id: str, port_id: str, name: str) -> None:
|
||||||
|
owner = self.document.find_component(owner_id) if self.document else None
|
||||||
|
if owner is None:
|
||||||
|
return
|
||||||
|
port = next((p for p in (*owner.inputs, *owner.outputs) if p.id == port_id), None)
|
||||||
|
if port is not None:
|
||||||
|
port.name = name
|
||||||
|
self.interfaceChanged.emit()
|
||||||
|
self.documentReset.emit()
|
||||||
|
|
||||||
|
def _rename_connection(self, owner_id: str, connection_id: str, name: str) -> None:
|
||||||
|
connection = self._graph_for(owner_id).connections.get(connection_id)
|
||||||
|
if connection is not None:
|
||||||
|
connection.name = name
|
||||||
|
self.documentReset.emit()
|
||||||
|
|
||||||
|
def _replace_source(self, component_id: str, source: dict) -> None:
|
||||||
|
if self.document is None:
|
||||||
|
return
|
||||||
|
component = self.document.find_component(component_id)
|
||||||
|
if component is None:
|
||||||
|
return
|
||||||
|
component.source = deepcopy(source)
|
||||||
|
self.documentReset.emit()
|
||||||
|
if component_id == self.active_component_id:
|
||||||
|
self.activeGraphChanged.emit()
|
||||||
|
|
||||||
|
def _set_component_appearance(self, component_id: str, values: dict) -> None:
|
||||||
|
if self.document is None:
|
||||||
|
return
|
||||||
|
component = self.document.find_component(component_id)
|
||||||
|
if component is None:
|
||||||
|
return
|
||||||
|
component.name = values["name"]
|
||||||
|
component.icon = Icon(
|
||||||
|
shape=values["shape"],
|
||||||
|
fill=values["fill"],
|
||||||
|
border=values["border"],
|
||||||
|
text=values["text"],
|
||||||
|
)
|
||||||
|
component.show_subtree_in_library = values["show_subtree"]
|
||||||
|
self.documentReset.emit()
|
||||||
|
if component_id == self.active_component_id:
|
||||||
|
self.activeGraphChanged.emit()
|
||||||
|
|
||||||
|
def _delete_items(
|
||||||
|
self,
|
||||||
|
owner_id: str | None,
|
||||||
|
block_ids: set[str],
|
||||||
|
connection_ids: set[str],
|
||||||
|
input_ids: set[str],
|
||||||
|
output_ids: set[str],
|
||||||
|
) -> None:
|
||||||
|
if self.document is None:
|
||||||
|
return
|
||||||
|
deleted_component_ids: set[str] = set()
|
||||||
|
for block_id in block_ids:
|
||||||
|
component = self.document.find_component(block_id)
|
||||||
|
if component is not None:
|
||||||
|
deleted_component_ids.update(
|
||||||
|
child.id for child in self._component_subtree(component)
|
||||||
|
)
|
||||||
|
active_was_deleted = self.active_component_id in deleted_component_ids
|
||||||
|
if owner_id is None:
|
||||||
|
for block_id in block_ids:
|
||||||
|
self.document.roots.pop(block_id, None)
|
||||||
|
else:
|
||||||
|
owner = self.document.find_component(owner_id)
|
||||||
|
if owner is None:
|
||||||
|
return
|
||||||
|
for block_id in block_ids:
|
||||||
|
owner.graph.blocks.pop(block_id, None)
|
||||||
|
for connection_id in connection_ids:
|
||||||
|
owner.graph.connections.pop(connection_id, None)
|
||||||
|
owner.inputs[:] = [port for port in owner.inputs if port.id not in input_ids]
|
||||||
|
owner.outputs[:] = [port for port in owner.outputs if port.id not in output_ids]
|
||||||
|
if active_was_deleted:
|
||||||
|
self.active_component_id = owner_id or next(iter(self.document.roots), None)
|
||||||
|
self.activeGraphChanged.emit()
|
||||||
|
self.documentReset.emit()
|
||||||
|
|
||||||
|
def _restore_items(
|
||||||
|
self,
|
||||||
|
owner_id: str | None,
|
||||||
|
blocks: dict[str, Component],
|
||||||
|
connections: dict[str, Connection],
|
||||||
|
inputs: list[Port],
|
||||||
|
outputs: list[Port],
|
||||||
|
) -> None:
|
||||||
|
if self.document is None:
|
||||||
|
return
|
||||||
|
if owner_id is None:
|
||||||
|
self.document.roots.update(blocks)
|
||||||
|
else:
|
||||||
|
owner = self.document.find_component(owner_id)
|
||||||
|
if owner is None:
|
||||||
|
return
|
||||||
|
owner.graph.blocks.update(blocks)
|
||||||
|
owner.graph.connections.update(connections)
|
||||||
|
existing_inputs = {port.id for port in owner.inputs}
|
||||||
|
existing_outputs = {port.id for port in owner.outputs}
|
||||||
|
owner.inputs.extend(port for port in inputs if port.id not in existing_inputs)
|
||||||
|
owner.outputs.extend(port for port in outputs if port.id not in existing_outputs)
|
||||||
|
self.documentReset.emit()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _component_subtree(component: Component):
|
||||||
|
yield component
|
||||||
|
for child in component.graph.blocks.values():
|
||||||
|
yield from DocumentController._component_subtree(child)
|
||||||
|
|
||||||
|
def _set_text_definition(self, component_id: str, values: dict) -> None:
|
||||||
|
if self.document is None:
|
||||||
|
return
|
||||||
|
component = self.document.find_component(component_id)
|
||||||
|
if component is None:
|
||||||
|
return
|
||||||
|
component.inputs = [Port.from_dict(item) for item in values["inputs"]]
|
||||||
|
component.outputs = [Port.from_dict(item) for item in values["outputs"]]
|
||||||
|
component.source = deepcopy(values["source"])
|
||||||
|
self.interfaceChanged.emit()
|
||||||
|
self.documentReset.emit()
|
||||||
|
if component_id == self.active_component_id:
|
||||||
|
self.activeGraphChanged.emit()
|
||||||
329
BEdit/src/bedit/document/model.py
Normal file
329
BEdit/src/bedit/document/model.py
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Port:
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
x: float = 0.0
|
||||||
|
y: float = 0.0
|
||||||
|
properties: dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"name": self.name,
|
||||||
|
"position": {"x": self.x, "y": self.y},
|
||||||
|
"properties": self.properties,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: dict[str, Any]) -> "Port":
|
||||||
|
position = data.get("position", {})
|
||||||
|
return cls(
|
||||||
|
id=str(data["id"]),
|
||||||
|
name=str(data.get("name", data["id"])),
|
||||||
|
x=float(position.get("x", 0.0)),
|
||||||
|
y=float(position.get("y", 0.0)),
|
||||||
|
properties=dict(data.get("properties", {})),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Icon:
|
||||||
|
shape: str = "rectangle"
|
||||||
|
fill: str = "#f4f4f4"
|
||||||
|
border: str = "#303030"
|
||||||
|
text: str = ""
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, str]:
|
||||||
|
return {
|
||||||
|
"shape": self.shape,
|
||||||
|
"fill": self.fill,
|
||||||
|
"border": self.border,
|
||||||
|
"text": self.text,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: dict[str, Any] | None) -> "Icon":
|
||||||
|
data = data or {}
|
||||||
|
return cls(
|
||||||
|
shape=str(data.get("shape", "rectangle")),
|
||||||
|
fill=str(data.get("fill", "#f4f4f4")),
|
||||||
|
border=str(data.get("border", "#303030")),
|
||||||
|
text=str(data.get("text", "")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Endpoint:
|
||||||
|
block: str | None = None
|
||||||
|
port: str | None = None
|
||||||
|
interface: str | None = None
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, str]:
|
||||||
|
if self.interface is not None:
|
||||||
|
return {"interface": self.interface}
|
||||||
|
if self.block is None or self.port is None:
|
||||||
|
raise ValueError("A block endpoint requires both block and port")
|
||||||
|
return {"block": self.block, "port": self.port}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: dict[str, Any]) -> "Endpoint":
|
||||||
|
if "interface" in data:
|
||||||
|
return cls(interface=str(data["interface"]))
|
||||||
|
return cls(block=str(data["block"]), port=str(data["port"]))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Connection:
|
||||||
|
id: str
|
||||||
|
source: Endpoint
|
||||||
|
target: Endpoint
|
||||||
|
name: str = ""
|
||||||
|
properties: dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"source": self.source.to_dict(),
|
||||||
|
"target": self.target.to_dict(),
|
||||||
|
"name": self.name,
|
||||||
|
"properties": self.properties,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: dict[str, Any]) -> "Connection":
|
||||||
|
return cls(
|
||||||
|
id=str(data["id"]),
|
||||||
|
source=Endpoint.from_dict(data["source"]),
|
||||||
|
target=Endpoint.from_dict(data["target"]),
|
||||||
|
name=str(data.get("name", "")),
|
||||||
|
properties=dict(data.get("properties", {})),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Graph:
|
||||||
|
blocks: dict[str, Component] = field(default_factory=dict)
|
||||||
|
connections: dict[str, Connection] = field(default_factory=dict)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"blocks": [block.to_dict() for block in self.blocks.values()],
|
||||||
|
"connections": [connection.to_dict() for connection in self.connections.values()],
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: dict[str, Any] | None) -> "Graph":
|
||||||
|
data = data or {}
|
||||||
|
blocks = [Component.from_dict(item) for item in data.get("blocks", [])]
|
||||||
|
connections = [Connection.from_dict(item) for item in data.get("connections", [])]
|
||||||
|
if len({block.id for block in blocks}) != len(blocks):
|
||||||
|
raise ValueError("A graph contains duplicate component IDs")
|
||||||
|
if len({connection.id for connection in connections}) != len(connections):
|
||||||
|
raise ValueError("A graph contains duplicate connection IDs")
|
||||||
|
return cls(
|
||||||
|
blocks={block.id: block for block in blocks},
|
||||||
|
connections={connection.id: connection for connection in connections},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Component:
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
x: float = 0.0
|
||||||
|
y: float = 0.0
|
||||||
|
rotation: float = 0.0
|
||||||
|
inputs: list[Port] = field(default_factory=list)
|
||||||
|
outputs: list[Port] = field(default_factory=list)
|
||||||
|
icon: Icon = field(default_factory=Icon)
|
||||||
|
properties: dict[str, Any] = field(default_factory=dict)
|
||||||
|
implementation_kind: str = "graph"
|
||||||
|
graph: Graph = field(default_factory=Graph)
|
||||||
|
source: dict[str, Any] = field(default_factory=dict)
|
||||||
|
show_subtree_in_library: bool = True
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
implementation = {"kind": self.implementation_kind}
|
||||||
|
if self.implementation_kind == "text":
|
||||||
|
implementation["source"] = self.source
|
||||||
|
else:
|
||||||
|
implementation["graph"] = self.graph.to_dict()
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"name": self.name,
|
||||||
|
"position": {"x": self.x, "y": self.y},
|
||||||
|
"rotation": self.rotation,
|
||||||
|
"interface": {
|
||||||
|
"inputs": [port.to_dict() for port in self.inputs],
|
||||||
|
"outputs": [port.to_dict() for port in self.outputs],
|
||||||
|
},
|
||||||
|
"icon": self.icon.to_dict(),
|
||||||
|
"properties": self.properties,
|
||||||
|
"library": {"showSubtree": self.show_subtree_in_library},
|
||||||
|
"implementation": implementation,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: dict[str, Any]) -> "Component":
|
||||||
|
interface = data.get("interface", {})
|
||||||
|
position = data.get("position", {})
|
||||||
|
implementation = data["implementation"]
|
||||||
|
kind = str(implementation.get("kind", "graph"))
|
||||||
|
if kind not in {"graph", "text"}:
|
||||||
|
raise ValueError(f"Unknown component implementation kind: {kind}")
|
||||||
|
return cls(
|
||||||
|
id=str(data["id"]),
|
||||||
|
name=str(data.get("name", "Unnamed")),
|
||||||
|
x=float(position.get("x", 0.0)),
|
||||||
|
y=float(position.get("y", 0.0)),
|
||||||
|
rotation=float(data.get("rotation", 0.0)),
|
||||||
|
inputs=[Port.from_dict(item) for item in interface.get("inputs", [])],
|
||||||
|
outputs=[Port.from_dict(item) for item in interface.get("outputs", [])],
|
||||||
|
icon=Icon.from_dict(data.get("icon")),
|
||||||
|
properties=dict(data.get("properties", {})),
|
||||||
|
show_subtree_in_library=bool(data.get("library", {}).get("showSubtree", True)),
|
||||||
|
implementation_kind=kind,
|
||||||
|
graph=Graph.from_dict(implementation.get("graph")) if kind == "graph" else Graph(),
|
||||||
|
source=dict(implementation.get("source", {})) if kind == "text" else {},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GraphDocument:
|
||||||
|
roots: dict[str, Component] = field(default_factory=dict)
|
||||||
|
metadata: dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def empty(cls) -> "GraphDocument":
|
||||||
|
return cls(metadata={"name": "Untitled"})
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"format": "bedit-document",
|
||||||
|
"version": 1,
|
||||||
|
"metadata": self.metadata,
|
||||||
|
"roots": [root.to_dict() for root in self.roots.values()],
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: dict[str, Any]) -> "GraphDocument":
|
||||||
|
if data.get("format") != "bedit-document":
|
||||||
|
raise ValueError("This is not a BEdit document")
|
||||||
|
if data.get("version") != 1:
|
||||||
|
raise ValueError(f"Unsupported BEdit document version: {data.get('version')}")
|
||||||
|
roots = [Component.from_dict(item) for item in data["roots"]]
|
||||||
|
if len({root.id for root in roots}) != len(roots):
|
||||||
|
raise ValueError("The document contains duplicate root IDs")
|
||||||
|
document = cls(
|
||||||
|
roots={root.id: root for root in roots},
|
||||||
|
metadata=dict(data.get("metadata", {})),
|
||||||
|
)
|
||||||
|
document.validate()
|
||||||
|
return document
|
||||||
|
|
||||||
|
def all_components(self):
|
||||||
|
def walk(component: Component):
|
||||||
|
yield component
|
||||||
|
if component.implementation_kind == "graph":
|
||||||
|
for child in component.graph.blocks.values():
|
||||||
|
yield from walk(child)
|
||||||
|
|
||||||
|
def all_roots():
|
||||||
|
for root in self.roots.values():
|
||||||
|
yield from walk(root)
|
||||||
|
|
||||||
|
return all_roots()
|
||||||
|
|
||||||
|
def find_component(self, component_id: str) -> Component | None:
|
||||||
|
return next(
|
||||||
|
(component for component in self.all_components() if component.id == component_id),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def find_parent(self, component_id: str) -> Component | None:
|
||||||
|
for component in self.all_components():
|
||||||
|
if component_id in component.graph.blocks:
|
||||||
|
return component
|
||||||
|
return None
|
||||||
|
|
||||||
|
def validate(self) -> None:
|
||||||
|
seen: set[str] = set()
|
||||||
|
for component in self.all_components():
|
||||||
|
if component.id in seen:
|
||||||
|
raise ValueError(f"Duplicate component ID: {component.id}")
|
||||||
|
seen.add(component.id)
|
||||||
|
if component.implementation_kind == "text" and component.graph.blocks:
|
||||||
|
raise ValueError(f"Text component {component.name} cannot contain a graph")
|
||||||
|
self._validate_graph(component)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _validate_graph(owner: Component) -> None:
|
||||||
|
input_ids = {port.id for port in owner.inputs}
|
||||||
|
output_ids = {port.id for port in owner.outputs}
|
||||||
|
for connection in owner.graph.connections.values():
|
||||||
|
if connection.source.interface is not None:
|
||||||
|
if connection.source.interface not in input_ids:
|
||||||
|
raise ValueError(f"Connection {connection.id} uses an unknown interface input")
|
||||||
|
else:
|
||||||
|
source = owner.graph.blocks.get(connection.source.block or "")
|
||||||
|
if source is None or connection.source.port not in {p.id for p in source.outputs}:
|
||||||
|
raise ValueError(f"Connection {connection.id} uses an unknown block output")
|
||||||
|
if connection.target.interface is not None:
|
||||||
|
if connection.target.interface not in output_ids:
|
||||||
|
raise ValueError(f"Connection {connection.id} uses an unknown interface output")
|
||||||
|
else:
|
||||||
|
target = owner.graph.blocks.get(connection.target.block or "")
|
||||||
|
if target is None or connection.target.port not in {p.id for p in target.inputs}:
|
||||||
|
raise ValueError(f"Connection {connection.id} uses an unknown block input")
|
||||||
|
|
||||||
|
|
||||||
|
def clone_component(source: Component) -> Component:
|
||||||
|
"""Deep-copy a component tree and remap every owned object ID."""
|
||||||
|
|
||||||
|
def clone(current: Component) -> Component:
|
||||||
|
child_pairs = [(child, clone(child)) for child in current.graph.blocks.values()]
|
||||||
|
child_ids = {old.id: new.id for old, new in child_pairs}
|
||||||
|
|
||||||
|
def remap(endpoint: Endpoint) -> Endpoint:
|
||||||
|
if endpoint.interface is not None:
|
||||||
|
return endpoint
|
||||||
|
return Endpoint(block=child_ids[endpoint.block or ""], port=endpoint.port)
|
||||||
|
|
||||||
|
graph = Graph(
|
||||||
|
blocks={new.id: new for _old, new in child_pairs},
|
||||||
|
connections={
|
||||||
|
new_id: Connection(
|
||||||
|
new_id,
|
||||||
|
remap(connection.source),
|
||||||
|
remap(connection.target),
|
||||||
|
connection.name,
|
||||||
|
deepcopy(connection.properties),
|
||||||
|
)
|
||||||
|
for connection in current.graph.connections.values()
|
||||||
|
for new_id in [str(uuid4())]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return Component(
|
||||||
|
id=str(uuid4()),
|
||||||
|
name=current.name,
|
||||||
|
x=current.x,
|
||||||
|
y=current.y,
|
||||||
|
inputs=[Port(port.id, port.name, port.x, port.y, deepcopy(port.properties)) for port in current.inputs],
|
||||||
|
outputs=[Port(port.id, port.name, port.x, port.y, deepcopy(port.properties)) for port in current.outputs],
|
||||||
|
icon=Icon(**current.icon.to_dict()),
|
||||||
|
properties=deepcopy(current.properties),
|
||||||
|
implementation_kind=current.implementation_kind,
|
||||||
|
graph=graph if current.implementation_kind == "graph" else Graph(),
|
||||||
|
source=deepcopy(current.source),
|
||||||
|
show_subtree_in_library=current.show_subtree_in_library,
|
||||||
|
)
|
||||||
|
|
||||||
|
return clone(source)
|
||||||
23
BEdit/src/bedit/document/serializer.py
Normal file
23
BEdit/src/bedit/document/serializer.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from bedit.document.model import GraphDocument
|
||||||
|
|
||||||
|
|
||||||
|
class JsonDocumentSerializer:
|
||||||
|
@staticmethod
|
||||||
|
def load(path: Path) -> GraphDocument:
|
||||||
|
with path.open(encoding="utf-8") as file:
|
||||||
|
data = json.load(file)
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise ValueError("The graph file must contain a JSON object")
|
||||||
|
return GraphDocument.from_dict(data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def save(document: GraphDocument, path: Path) -> None:
|
||||||
|
temporary_path = path.with_suffix(path.suffix + ".tmp")
|
||||||
|
with temporary_path.open("w", encoding="utf-8") as file:
|
||||||
|
json.dump(document.to_dict(), file, indent=2)
|
||||||
|
file.write("\n")
|
||||||
|
temporary_path.replace(path)
|
||||||
|
|
||||||
42
BEdit/src/bedit/item_options_dialog.py
Normal file
42
BEdit/src/bedit/item_options_dialog.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QDialog,
|
||||||
|
QDialogButtonBox,
|
||||||
|
QFormLayout,
|
||||||
|
QLineEdit,
|
||||||
|
QMessageBox,
|
||||||
|
QVBoxLayout,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ItemOptionsDialog(QDialog):
|
||||||
|
"""Small, extensible options dialog shared by ports and connections."""
|
||||||
|
|
||||||
|
def __init__(self, title: str, name: str, parent=None, *, name_required: bool = True) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.name_required = name_required
|
||||||
|
self.setWindowTitle(title)
|
||||||
|
self.resize(380, 120)
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
self.form = QFormLayout()
|
||||||
|
self.name_edit = QLineEdit(name, self)
|
||||||
|
self.form.addRow("Name:", self.name_edit)
|
||||||
|
layout.addLayout(self.form)
|
||||||
|
|
||||||
|
buttons = QDialogButtonBox(
|
||||||
|
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel,
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
buttons.accepted.connect(self.accept)
|
||||||
|
buttons.rejected.connect(self.reject)
|
||||||
|
layout.addWidget(buttons)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return self.name_edit.text().strip()
|
||||||
|
|
||||||
|
def accept(self) -> None:
|
||||||
|
if self.name_required and not self.name:
|
||||||
|
QMessageBox.warning(self, "Invalid name", "The name cannot be empty.")
|
||||||
|
return
|
||||||
|
super().accept()
|
||||||
3
BEdit/src/bedit/library/__init__.py
Normal file
3
BEdit/src/bedit/library/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from bedit.library.repository import LibraryRepository, default_library_paths
|
||||||
|
|
||||||
|
__all__ = ["LibraryRepository", "default_library_paths"]
|
||||||
56
BEdit/src/bedit/library/repository.py
Normal file
56
BEdit/src/bedit/library/repository.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import json
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PySide6.QtCore import QObject, Signal
|
||||||
|
|
||||||
|
from bedit.document.model import GraphDocument
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class LibraryDocument:
|
||||||
|
name: str
|
||||||
|
document: GraphDocument
|
||||||
|
source_path: str
|
||||||
|
|
||||||
|
|
||||||
|
def bundled_library_path() -> Path:
|
||||||
|
return Path(__file__).resolve().parents[1] / "data" / "libraries" / "example.json"
|
||||||
|
|
||||||
|
|
||||||
|
def default_library_paths() -> list[str]:
|
||||||
|
return [str(bundled_library_path())]
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryRepository(QObject):
|
||||||
|
librariesChanged = Signal()
|
||||||
|
loadWarningsChanged = Signal(list)
|
||||||
|
|
||||||
|
def __init__(self, parent=None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.libraries: list[LibraryDocument] = []
|
||||||
|
self.load_warnings: list[str] = []
|
||||||
|
|
||||||
|
def load_paths(self, paths: list[str]) -> None:
|
||||||
|
libraries: list[LibraryDocument] = []
|
||||||
|
warnings: list[str] = []
|
||||||
|
for raw_path in paths:
|
||||||
|
path = Path(raw_path).expanduser()
|
||||||
|
candidates = sorted(path.glob("*.json")) if path.is_dir() else [path]
|
||||||
|
for candidate in candidates:
|
||||||
|
try:
|
||||||
|
libraries.append(self._load_file(candidate))
|
||||||
|
except (OSError, ValueError, KeyError, TypeError, json.JSONDecodeError) as error:
|
||||||
|
warnings.append(f"{candidate}: {error}")
|
||||||
|
self.libraries = libraries
|
||||||
|
self.load_warnings = warnings
|
||||||
|
self.librariesChanged.emit()
|
||||||
|
self.loadWarningsChanged.emit(warnings)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _load_file(path: Path) -> LibraryDocument:
|
||||||
|
with path.open(encoding="utf-8") as file:
|
||||||
|
data = json.load(file)
|
||||||
|
document = GraphDocument.from_dict(data)
|
||||||
|
name = str(document.metadata.get("name") or path.stem)
|
||||||
|
return LibraryDocument(name, document, str(path))
|
||||||
91
BEdit/src/bedit/library/tree_model.py
Normal file
91
BEdit/src/bedit/library/tree_model.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from PySide6.QtCore import QByteArray, QMimeData, QModelIndex, Qt, Signal
|
||||||
|
from PySide6.QtGui import QStandardItem, QStandardItemModel
|
||||||
|
|
||||||
|
from bedit.document.controller import DocumentController
|
||||||
|
from bedit.document.model import Component
|
||||||
|
from bedit.library.repository import LibraryRepository
|
||||||
|
|
||||||
|
|
||||||
|
COMPONENT_ROLE = Qt.ItemDataRole.UserRole + 1
|
||||||
|
COMPONENT_ID_ROLE = Qt.ItemDataRole.UserRole + 2
|
||||||
|
ITEM_KIND_ROLE = Qt.ItemDataRole.UserRole + 3
|
||||||
|
COMPONENT_MIME_TYPE = "application/x-bedit-component"
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryTreeModel(QStandardItemModel):
|
||||||
|
rebuilt = Signal()
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
repository: LibraryRepository,
|
||||||
|
controller: DocumentController,
|
||||||
|
parent=None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.repository = repository
|
||||||
|
self.controller = controller
|
||||||
|
repository.librariesChanged.connect(self.rebuild)
|
||||||
|
self.rebuild()
|
||||||
|
|
||||||
|
def rebuild(self) -> None:
|
||||||
|
self.clear()
|
||||||
|
self.setHorizontalHeaderLabels(["Libraries"])
|
||||||
|
for library in self.repository.libraries:
|
||||||
|
root = QStandardItem(library.name)
|
||||||
|
root.setDragEnabled(False)
|
||||||
|
root.setToolTip(library.source_path)
|
||||||
|
for component in library.document.roots.values():
|
||||||
|
root.appendRow(self._component_item(component))
|
||||||
|
self.appendRow(root)
|
||||||
|
self.rebuilt.emit()
|
||||||
|
|
||||||
|
def _component_item(self, component: Component, current: bool = False) -> QStandardItem:
|
||||||
|
item = QStandardItem(component.name)
|
||||||
|
item.setEditable(False)
|
||||||
|
item.setData(component.to_dict(), COMPONENT_ROLE)
|
||||||
|
item.setData(component.id, COMPONENT_ID_ROLE)
|
||||||
|
item.setData("current-component" if current else "library-component", ITEM_KIND_ROLE)
|
||||||
|
if component.show_subtree_in_library:
|
||||||
|
for child in component.graph.blocks.values():
|
||||||
|
item.appendRow(self._component_item(child, current=current))
|
||||||
|
return item
|
||||||
|
|
||||||
|
def mimeTypes(self) -> list[str]: # noqa: N802
|
||||||
|
return [COMPONENT_MIME_TYPE]
|
||||||
|
|
||||||
|
def mimeData(self, indexes: list[QModelIndex]) -> QMimeData: # noqa: N802
|
||||||
|
mime_data = QMimeData()
|
||||||
|
for index in indexes:
|
||||||
|
component = index.data(COMPONENT_ROLE)
|
||||||
|
if component:
|
||||||
|
encoded = json.dumps(component).encode("utf-8")
|
||||||
|
mime_data.setData(COMPONENT_MIME_TYPE, QByteArray(encoded))
|
||||||
|
break
|
||||||
|
return mime_data
|
||||||
|
|
||||||
|
def supportedDragActions(self): # noqa: N802
|
||||||
|
return Qt.DropAction.CopyAction
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentTreeModel(LibraryTreeModel):
|
||||||
|
def __init__(self, controller: DocumentController, parent=None) -> None:
|
||||||
|
QStandardItemModel.__init__(self, parent)
|
||||||
|
self.controller = controller
|
||||||
|
controller.documentReset.connect(self.rebuild)
|
||||||
|
controller.componentMoved.connect(lambda _component_id, _position: self.rebuild())
|
||||||
|
controller.connectionAdded.connect(lambda _connection_id: self.rebuild())
|
||||||
|
controller.connectionRemoved.connect(lambda _connection_id: self.rebuild())
|
||||||
|
self.rebuild()
|
||||||
|
|
||||||
|
def rebuild(self) -> None:
|
||||||
|
self.clear()
|
||||||
|
self.setHorizontalHeaderLabels(["Document"])
|
||||||
|
if self.controller.document is not None:
|
||||||
|
current_root = QStandardItem("Current Document")
|
||||||
|
current_root.setDragEnabled(False)
|
||||||
|
current_root.setData("current-document", ITEM_KIND_ROLE)
|
||||||
|
for component in self.controller.document.roots.values():
|
||||||
|
current_root.appendRow(self._component_item(component, current=True))
|
||||||
|
self.appendRow(current_root)
|
||||||
|
self.rebuilt.emit()
|
||||||
@@ -1,13 +1,27 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from PySide6.QtCore import QSettings, Qt, Slot
|
from PySide6.QtCore import QSettings, Qt, Slot
|
||||||
from PySide6.QtGui import QCloseEvent
|
from PySide6.QtGui import QCloseEvent
|
||||||
from PySide6.QtWidgets import QMainWindow, QMessageBox
|
from PySide6.QtWidgets import QFileDialog, QMainWindow, QMenu, QMessageBox
|
||||||
|
|
||||||
|
from bedit.component_options_dialog import ComponentOptionsDialog
|
||||||
|
from bedit.document.controller import DocumentController
|
||||||
|
from bedit.document.model import Port
|
||||||
|
from bedit.item_options_dialog import ItemOptionsDialog
|
||||||
|
from bedit.library.repository import LibraryRepository
|
||||||
|
from bedit.library.tree_model import (
|
||||||
|
COMPONENT_ID_ROLE,
|
||||||
|
ITEM_KIND_ROLE,
|
||||||
|
DocumentTreeModel,
|
||||||
|
LibraryTreeModel,
|
||||||
|
)
|
||||||
from bedit.settings_dialog import SettingsDialog
|
from bedit.settings_dialog import SettingsDialog
|
||||||
from bedit.ui_main_window import Ui_MainWindow
|
from bedit.ui_main_window import Ui_MainWindow
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
"""Application shell for the window laid out in Qt Designer."""
|
"""Application shell and owner of the single active document."""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -15,44 +29,447 @@ class MainWindow(QMainWindow):
|
|||||||
self.ui.setupUi(self)
|
self.ui.setupUi(self)
|
||||||
self.settings = QSettings()
|
self.settings = QSettings()
|
||||||
|
|
||||||
|
self.libraries = LibraryRepository(self)
|
||||||
|
self.document_controller = DocumentController(self)
|
||||||
|
self.library_tree_model = LibraryTreeModel(
|
||||||
|
self.libraries,
|
||||||
|
self.document_controller,
|
||||||
|
self,
|
||||||
|
)
|
||||||
|
self.document_tree_model = DocumentTreeModel(self.document_controller, self)
|
||||||
|
self._configure_models()
|
||||||
|
self._connect_actions()
|
||||||
self._populate_view_menu()
|
self._populate_view_menu()
|
||||||
self.ui.actionExit.triggered.connect(self.close)
|
|
||||||
self.ui.actionSettings.triggered.connect(self.show_settings)
|
|
||||||
self.ui.actionAbout.triggered.connect(self.show_about)
|
|
||||||
self.ui.actionAboutQt.triggered.connect(lambda: QMessageBox.aboutQt(self, "About Qt Framework"))
|
|
||||||
self._restore_window_geometry()
|
self._restore_window_geometry()
|
||||||
|
|
||||||
self.ui.leftDockHost.setWindowFlags(Qt.WindowType.Widget)
|
self.ui.leftDockHost.setWindowFlags(Qt.WindowType.Widget)
|
||||||
self.ui.leftDockHost.show()
|
self.ui.leftDockHost.show()
|
||||||
self.ui.panel_libraries.show()
|
self.ui.panel_libraries.show()
|
||||||
|
self.ui.panel_document.show()
|
||||||
|
self.ui.leftDockHost.splitDockWidget(
|
||||||
|
self.ui.panel_document,
|
||||||
|
self.ui.panel_libraries,
|
||||||
|
Qt.Orientation.Vertical,
|
||||||
|
)
|
||||||
self.ui.workspaceSplitter.setSizes([280, 720])
|
self.ui.workspaceSplitter.setSizes([280, 720])
|
||||||
|
self.reload_libraries()
|
||||||
|
self._active_graph_changed()
|
||||||
|
self._update_title()
|
||||||
|
|
||||||
|
def _configure_models(self) -> None:
|
||||||
|
self.ui.treeView.setModel(self.library_tree_model)
|
||||||
|
self.ui.treeView.setHeaderHidden(True)
|
||||||
|
self.ui.treeView.setDragEnabled(True)
|
||||||
|
self.ui.treeView.setDragDropMode(self.ui.treeView.DragDropMode.DragOnly)
|
||||||
|
self.library_tree_model.rebuilt.connect(self.ui.treeView.expandAll)
|
||||||
|
self.ui.documentTreeView.setModel(self.document_tree_model)
|
||||||
|
self.ui.documentTreeView.setHeaderHidden(True)
|
||||||
|
self.ui.documentTreeView.setDragEnabled(True)
|
||||||
|
self.ui.documentTreeView.setDragDropMode(
|
||||||
|
self.ui.documentTreeView.DragDropMode.DragOnly
|
||||||
|
)
|
||||||
|
self.ui.documentTreeView.setContextMenuPolicy(
|
||||||
|
Qt.ContextMenuPolicy.CustomContextMenu
|
||||||
|
)
|
||||||
|
self.ui.documentTreeView.customContextMenuRequested.connect(
|
||||||
|
self.show_library_context_menu
|
||||||
|
)
|
||||||
|
self.ui.documentTreeView.doubleClicked.connect(self.activate_tree_component)
|
||||||
|
self.document_tree_model.rebuilt.connect(self.ui.documentTreeView.expandAll)
|
||||||
|
self.ui.graphView.set_model(self.document_controller)
|
||||||
|
self.ui.graphView.componentOptionsRequested.connect(self.show_component_options)
|
||||||
|
self.ui.graphView.portOptionsRequested.connect(self.show_port_options)
|
||||||
|
self.ui.graphView.connectionOptionsRequested.connect(self.show_connection_options)
|
||||||
|
self.ui.graphView.selectionAvailabilityChanged.connect(
|
||||||
|
lambda _available: self._update_edit_actions()
|
||||||
|
)
|
||||||
|
self.ui.navigateUpButton.clicked.connect(self.navigate_up)
|
||||||
|
self.ui.pointerToolButton.clicked.connect(lambda: self.set_graph_tool("pointer"))
|
||||||
|
self.ui.inputToolButton.clicked.connect(lambda: self.set_graph_tool("input"))
|
||||||
|
self.ui.outputToolButton.clicked.connect(lambda: self.set_graph_tool("output"))
|
||||||
|
self.ui.graphView.toolUsed.connect(lambda: self.set_graph_tool("pointer"))
|
||||||
|
self.ui.applyJsonButton.clicked.connect(self.apply_json)
|
||||||
|
self.document_controller.activeGraphChanged.connect(self._active_graph_changed)
|
||||||
|
|
||||||
|
def _connect_actions(self) -> None:
|
||||||
|
self.ui.actionNew.triggered.connect(self.new_document)
|
||||||
|
self.ui.actionOpen.triggered.connect(self.open_document)
|
||||||
|
self.ui.actionSave.triggered.connect(self.save_document)
|
||||||
|
self.ui.actionSaveAs.triggered.connect(self.save_document_as)
|
||||||
|
self.ui.actionClose.triggered.connect(self.close_document)
|
||||||
|
self.ui.actionExit.triggered.connect(self.close)
|
||||||
|
self.ui.actionSettings.triggered.connect(self.show_settings)
|
||||||
|
self.ui.actionAbout.triggered.connect(self.show_about)
|
||||||
|
self.ui.actionAboutQt.triggered.connect(
|
||||||
|
lambda: QMessageBox.aboutQt(self, "About Qt Framework")
|
||||||
|
)
|
||||||
|
self.ui.actionUndo.triggered.connect(self.document_controller.undo_stack.undo)
|
||||||
|
self.ui.actionRedo.triggered.connect(self.document_controller.undo_stack.redo)
|
||||||
|
self.ui.actionDelete.triggered.connect(self.ui.graphView.delete_selected)
|
||||||
|
self.ui.actionCopy.triggered.connect(self.ui.graphView.copy_selection)
|
||||||
|
self.ui.actionCut.triggered.connect(self.ui.graphView.cut_selection)
|
||||||
|
self.ui.actionPaste.triggered.connect(self.ui.graphView.paste_selection)
|
||||||
|
self.ui.actionSelectAll.triggered.connect(self.ui.graphView.select_all)
|
||||||
|
self.ui.actionRotateClockwise.triggered.connect(self.ui.graphView.rotate_selected)
|
||||||
|
self.document_controller.undo_stack.canUndoChanged.connect(self.ui.actionUndo.setEnabled)
|
||||||
|
self.document_controller.undo_stack.canRedoChanged.connect(self.ui.actionRedo.setEnabled)
|
||||||
|
self.document_controller.modifiedChanged.connect(lambda _modified: self._update_title())
|
||||||
|
self.document_controller.filePathChanged.connect(lambda _path: self._update_title())
|
||||||
|
self.document_controller.documentOpenedChanged.connect(self._document_opened_changed)
|
||||||
|
self.ui.actionUndo.setEnabled(False)
|
||||||
|
self.ui.actionRedo.setEnabled(False)
|
||||||
|
self._update_edit_actions()
|
||||||
|
self._document_opened_changed(False)
|
||||||
|
|
||||||
def _populate_view_menu(self) -> None:
|
def _populate_view_menu(self) -> None:
|
||||||
"""Add checked show/hide actions for panels and toolbars."""
|
for panel in (self.ui.panel_document, self.ui.panel_libraries):
|
||||||
panels = (self.ui.panel_libraries,)
|
|
||||||
for panel in panels:
|
|
||||||
self.ui.menuPanels.addAction(panel.toggleViewAction())
|
self.ui.menuPanels.addAction(panel.toggleViewAction())
|
||||||
|
for toolbar in (
|
||||||
toolbars = (self.ui.fileToolbar, self.ui.editToolbar)
|
self.ui.fileToolbar,
|
||||||
for toolbar in toolbars:
|
self.ui.editToolbar,
|
||||||
|
self.ui.transformToolbar,
|
||||||
|
):
|
||||||
self.ui.menuToolbars.addAction(toolbar.toggleViewAction())
|
self.ui.menuToolbars.addAction(toolbar.toggleViewAction())
|
||||||
|
|
||||||
|
def reload_libraries(self) -> None:
|
||||||
|
self.libraries.load_paths(SettingsDialog.library_paths(self.settings))
|
||||||
|
self.ui.treeView.expandAll()
|
||||||
|
if self.libraries.load_warnings:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"Some libraries could not be loaded",
|
||||||
|
"\n".join(self.libraries.load_warnings),
|
||||||
|
)
|
||||||
|
|
||||||
def _restore_window_geometry(self) -> None:
|
def _restore_window_geometry(self) -> None:
|
||||||
geometry = self.settings.value("window/geometry")
|
geometry = self.settings.value("window/geometry")
|
||||||
if geometry is not None:
|
if geometry is not None:
|
||||||
self.restoreGeometry(geometry)
|
self.restoreGeometry(geometry)
|
||||||
|
|
||||||
|
def _update_title(self) -> None:
|
||||||
|
if self.document_controller.document is None:
|
||||||
|
self.setWindowTitle("BEdit")
|
||||||
|
return
|
||||||
|
name = self.document_controller.file_path.name if self.document_controller.file_path else "Untitled"
|
||||||
|
modified = "*" if not self.document_controller.undo_stack.isClean() else ""
|
||||||
|
self.setWindowTitle(f"{modified}{name} — BEdit")
|
||||||
|
|
||||||
|
def _active_graph_changed(self) -> None:
|
||||||
|
component = self.document_controller.active_component
|
||||||
|
if component is None:
|
||||||
|
self.ui.graphBreadcrumbLabel.setText("No component selected")
|
||||||
|
self.ui.navigateUpButton.setEnabled(False)
|
||||||
|
self.ui.workspaceModeLabel.setText("")
|
||||||
|
self.ui.workspaceStack.setCurrentWidget(self.ui.emptyPage)
|
||||||
|
self.ui.applyJsonButton.setVisible(False)
|
||||||
|
for button in (
|
||||||
|
self.ui.pointerToolButton,
|
||||||
|
self.ui.inputToolButton,
|
||||||
|
self.ui.outputToolButton,
|
||||||
|
):
|
||||||
|
button.setVisible(False)
|
||||||
|
self._update_edit_actions()
|
||||||
|
return
|
||||||
|
self.ui.graphBreadcrumbLabel.setText(" › ".join(self.document_controller.breadcrumb()))
|
||||||
|
self.ui.navigateUpButton.setEnabled(
|
||||||
|
self.document_controller.document.find_parent(component.id) is not None
|
||||||
|
)
|
||||||
|
is_graph = component.implementation_kind == "graph"
|
||||||
|
self.ui.workspaceModeLabel.setText("Graph" if is_graph else "Text")
|
||||||
|
self.ui.workspaceStack.setCurrentWidget(self.ui.graphPage if is_graph else self.ui.jsonPage)
|
||||||
|
self.ui.applyJsonButton.setVisible(not is_graph)
|
||||||
|
for button in (
|
||||||
|
self.ui.pointerToolButton,
|
||||||
|
self.ui.inputToolButton,
|
||||||
|
self.ui.outputToolButton,
|
||||||
|
):
|
||||||
|
button.setVisible(is_graph)
|
||||||
|
if is_graph:
|
||||||
|
self.set_graph_tool("pointer")
|
||||||
|
else:
|
||||||
|
self._load_source_json()
|
||||||
|
self._update_edit_actions()
|
||||||
|
|
||||||
|
def _update_edit_actions(self) -> None:
|
||||||
|
component = self.document_controller.active_component
|
||||||
|
is_graph = component is not None and component.implementation_kind == "graph"
|
||||||
|
has_selection = bool(self.ui.graphView.scene() and self.ui.graphView.scene().selectedItems())
|
||||||
|
for action in (self.ui.actionCut, self.ui.actionCopy, self.ui.actionDelete):
|
||||||
|
action.setEnabled(is_graph and has_selection)
|
||||||
|
self.ui.actionRotateClockwise.setEnabled(
|
||||||
|
is_graph and self.ui.graphView.has_selected_components()
|
||||||
|
)
|
||||||
|
self.ui.actionSelectAll.setEnabled(is_graph)
|
||||||
|
self.ui.actionPaste.setEnabled(is_graph)
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def navigate_up(self) -> None:
|
||||||
|
if self._resolve_source_edits():
|
||||||
|
self.document_controller.navigate_up()
|
||||||
|
|
||||||
|
def set_graph_tool(self, mode: str) -> None:
|
||||||
|
self.ui.graphView.set_tool_mode(mode)
|
||||||
|
buttons = {
|
||||||
|
"pointer": self.ui.pointerToolButton,
|
||||||
|
"input": self.ui.inputToolButton,
|
||||||
|
"output": self.ui.outputToolButton,
|
||||||
|
}
|
||||||
|
buttons[mode].setChecked(True)
|
||||||
|
|
||||||
|
def _load_source_json(self) -> None:
|
||||||
|
component = self.document_controller.active_component
|
||||||
|
if component is None:
|
||||||
|
return
|
||||||
|
text = json.dumps(
|
||||||
|
{
|
||||||
|
"inputs": [port.to_dict() for port in component.inputs],
|
||||||
|
"outputs": [port.to_dict() for port in component.outputs],
|
||||||
|
"source": component.source,
|
||||||
|
},
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
self.ui.jsonEditor.setPlainText(text)
|
||||||
|
self.ui.jsonEditor.document().setModified(False)
|
||||||
|
|
||||||
|
def _resolve_source_edits(self) -> bool:
|
||||||
|
component = self.document_controller.active_component
|
||||||
|
if (
|
||||||
|
component is None
|
||||||
|
or component.implementation_kind != "text"
|
||||||
|
or not self.ui.jsonEditor.document().isModified()
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
answer = QMessageBox.question(
|
||||||
|
self,
|
||||||
|
"Apply text component changes?",
|
||||||
|
"The text component has unapplied input, output, or source changes.",
|
||||||
|
QMessageBox.StandardButton.Apply
|
||||||
|
| QMessageBox.StandardButton.Discard
|
||||||
|
| QMessageBox.StandardButton.Cancel,
|
||||||
|
)
|
||||||
|
if answer == QMessageBox.StandardButton.Apply:
|
||||||
|
return self.apply_json()
|
||||||
|
return answer == QMessageBox.StandardButton.Discard
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def apply_json(self) -> bool:
|
||||||
|
try:
|
||||||
|
data = json.loads(self.ui.jsonEditor.toPlainText())
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise ValueError("The text component JSON must be an object")
|
||||||
|
if not isinstance(data.get("inputs"), list):
|
||||||
|
raise ValueError("'inputs' must be a list")
|
||||||
|
if not isinstance(data.get("outputs"), list):
|
||||||
|
raise ValueError("'outputs' must be a list")
|
||||||
|
if not isinstance(data.get("source"), dict):
|
||||||
|
raise ValueError("'source' must be an object")
|
||||||
|
inputs = [Port.from_dict(item) for item in data["inputs"]]
|
||||||
|
outputs = [Port.from_dict(item) for item in data["outputs"]]
|
||||||
|
self.document_controller.replace_active_text_definition(
|
||||||
|
inputs, outputs, data["source"]
|
||||||
|
)
|
||||||
|
except (TypeError, ValueError, json.JSONDecodeError) as error:
|
||||||
|
QMessageBox.critical(self, "Invalid text component JSON", str(error))
|
||||||
|
return False
|
||||||
|
self._load_source_json()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _maybe_save(self) -> bool:
|
||||||
|
if self.document_controller.document is None:
|
||||||
|
return True
|
||||||
|
if self.document_controller.undo_stack.isClean():
|
||||||
|
return True
|
||||||
|
answer = QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"Unsaved changes",
|
||||||
|
"The current graph contains unsaved changes.",
|
||||||
|
QMessageBox.StandardButton.Save
|
||||||
|
| QMessageBox.StandardButton.Discard
|
||||||
|
| QMessageBox.StandardButton.Cancel,
|
||||||
|
)
|
||||||
|
if answer == QMessageBox.StandardButton.Save:
|
||||||
|
return self.save_document()
|
||||||
|
return answer == QMessageBox.StandardButton.Discard
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def new_document(self) -> None:
|
||||||
|
if self._resolve_source_edits() and self._maybe_save():
|
||||||
|
self.document_controller.new_document()
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def close_document(self) -> None:
|
||||||
|
if self._resolve_source_edits() and self._maybe_save():
|
||||||
|
self.document_controller.close_document()
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def open_document(self) -> None:
|
||||||
|
if not self._resolve_source_edits() or not self._maybe_save():
|
||||||
|
return
|
||||||
|
filename, _ = QFileDialog.getOpenFileName(
|
||||||
|
self, "Open graph", "", "BEdit graphs (*.bedit.json *.json);;All files (*)"
|
||||||
|
)
|
||||||
|
if not filename:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.document_controller.load(Path(filename))
|
||||||
|
except (OSError, ValueError) as error:
|
||||||
|
QMessageBox.critical(self, "Could not open graph", str(error))
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def save_document(self) -> bool:
|
||||||
|
if self.document_controller.document is None:
|
||||||
|
return False
|
||||||
|
if self.document_controller.file_path is None:
|
||||||
|
return self.save_document_as()
|
||||||
|
try:
|
||||||
|
self.document_controller.save()
|
||||||
|
except OSError as error:
|
||||||
|
QMessageBox.critical(self, "Could not save graph", str(error))
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def save_document_as(self) -> bool:
|
||||||
|
if self.document_controller.document is None:
|
||||||
|
return False
|
||||||
|
filename, _ = QFileDialog.getSaveFileName(
|
||||||
|
self,
|
||||||
|
"Save graph",
|
||||||
|
"untitled.bedit.json",
|
||||||
|
"BEdit graphs (*.bedit.json);;JSON files (*.json);;All files (*)",
|
||||||
|
)
|
||||||
|
if not filename:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
self.document_controller.save(Path(filename))
|
||||||
|
except OSError as error:
|
||||||
|
QMessageBox.critical(self, "Could not save graph", str(error))
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def show_settings(self) -> None:
|
def show_settings(self) -> None:
|
||||||
SettingsDialog(self).exec()
|
dialog = SettingsDialog(self)
|
||||||
|
dialog.settingsChanged.connect(self.reload_libraries)
|
||||||
|
dialog.exec()
|
||||||
|
|
||||||
|
def _document_opened_changed(self, opened: bool) -> None:
|
||||||
|
for action in (self.ui.actionClose, self.ui.actionSave, self.ui.actionSaveAs):
|
||||||
|
action.setEnabled(opened)
|
||||||
|
self._active_graph_changed()
|
||||||
|
|
||||||
|
@Slot(object)
|
||||||
|
def activate_tree_component(self, index) -> None:
|
||||||
|
if index.data(ITEM_KIND_ROLE) != "current-component":
|
||||||
|
return
|
||||||
|
component_id = index.data(COMPONENT_ID_ROLE)
|
||||||
|
if component_id:
|
||||||
|
self.document_controller.activate_component(component_id)
|
||||||
|
|
||||||
|
@Slot(object)
|
||||||
|
def show_library_context_menu(self, position) -> None:
|
||||||
|
tree_view = self.ui.documentTreeView
|
||||||
|
index = tree_view.indexAt(position)
|
||||||
|
kind = index.data(ITEM_KIND_ROLE)
|
||||||
|
if kind == "current-component":
|
||||||
|
component_id = index.data(COMPONENT_ID_ROLE)
|
||||||
|
document = self.document_controller.document
|
||||||
|
component = document.find_component(component_id) if document else None
|
||||||
|
if component is None:
|
||||||
|
return
|
||||||
|
menu = QMenu(self)
|
||||||
|
graph_action = None
|
||||||
|
text_action = None
|
||||||
|
if component.implementation_kind == "graph":
|
||||||
|
graph_action = menu.addAction("New Graph Block")
|
||||||
|
text_action = menu.addAction("New Text Block")
|
||||||
|
menu.addSeparator()
|
||||||
|
options_action = menu.addAction("Component Options…")
|
||||||
|
delete_action = menu.addAction("Delete")
|
||||||
|
selected = menu.exec(tree_view.viewport().mapToGlobal(position))
|
||||||
|
if selected is graph_action:
|
||||||
|
self.document_controller.add_child(component_id, "graph")
|
||||||
|
elif selected is text_action:
|
||||||
|
self.document_controller.add_child(component_id, "text")
|
||||||
|
elif selected is options_action:
|
||||||
|
self.show_component_options(component_id)
|
||||||
|
elif selected is delete_action:
|
||||||
|
answer = QMessageBox.question(
|
||||||
|
self,
|
||||||
|
"Delete component?",
|
||||||
|
f"Delete {component.name!r} and all of its contents?",
|
||||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||||
|
)
|
||||||
|
if answer == QMessageBox.StandardButton.Yes:
|
||||||
|
self.document_controller.delete_component(component_id)
|
||||||
|
return
|
||||||
|
if kind != "current-document":
|
||||||
|
return
|
||||||
|
menu = QMenu(self)
|
||||||
|
graph_action = menu.addAction("New Graph Block")
|
||||||
|
text_action = menu.addAction("New Text Block")
|
||||||
|
selected = menu.exec(tree_view.viewport().mapToGlobal(position))
|
||||||
|
if selected is graph_action:
|
||||||
|
self.document_controller.add_root("graph")
|
||||||
|
elif selected is text_action:
|
||||||
|
self.document_controller.add_root("text")
|
||||||
|
|
||||||
|
@Slot(str)
|
||||||
|
def show_component_options(self, component_id: str) -> None:
|
||||||
|
document = self.document_controller.document
|
||||||
|
component = document.find_component(component_id) if document else None
|
||||||
|
if component is None:
|
||||||
|
return
|
||||||
|
dialog = ComponentOptionsDialog(component, self)
|
||||||
|
if dialog.exec() == dialog.DialogCode.Accepted:
|
||||||
|
self.document_controller.edit_component_appearance(
|
||||||
|
component_id,
|
||||||
|
dialog.ui.nameEdit.text().strip(),
|
||||||
|
dialog.ui.shapeCombo.currentText(),
|
||||||
|
dialog.ui.fillEdit.text(),
|
||||||
|
dialog.ui.borderEdit.text(),
|
||||||
|
dialog.ui.iconTextEdit.text(),
|
||||||
|
dialog.ui.showSubtreeCheckBox.isChecked(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Slot(str, str)
|
||||||
|
def show_port_options(self, port_id: str, direction: str) -> None:
|
||||||
|
owner = self.document_controller.active_component
|
||||||
|
if owner is None:
|
||||||
|
return
|
||||||
|
ports = owner.inputs if direction == "input" else owner.outputs
|
||||||
|
port = next((item for item in ports if item.id == port_id), None)
|
||||||
|
if port is None:
|
||||||
|
return
|
||||||
|
dialog = ItemOptionsDialog(f"{direction.title()} Options", port.name, self)
|
||||||
|
if dialog.exec() == dialog.DialogCode.Accepted:
|
||||||
|
self.document_controller.rename_interface_port(port_id, dialog.name)
|
||||||
|
|
||||||
|
@Slot(str)
|
||||||
|
def show_connection_options(self, connection_id: str) -> None:
|
||||||
|
owner = self.document_controller.active_component
|
||||||
|
if owner is None or owner.implementation_kind != "graph":
|
||||||
|
return
|
||||||
|
connection = owner.graph.connections.get(connection_id)
|
||||||
|
if connection is None:
|
||||||
|
return
|
||||||
|
dialog = ItemOptionsDialog(
|
||||||
|
"Connection Options", connection.name, self, name_required=False
|
||||||
|
)
|
||||||
|
if dialog.exec() == dialog.DialogCode.Accepted:
|
||||||
|
self.document_controller.rename_connection(connection_id, dialog.name)
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def show_about(self) -> None:
|
def show_about(self) -> None:
|
||||||
QMessageBox.about(
|
QMessageBox.about(
|
||||||
self,
|
self,
|
||||||
"About BEdit",
|
"About BEdit",
|
||||||
"<h3>BEdit</h3><p>A starter desktop application built with Python and Qt.</p>",
|
"<h3>BEdit</h3><p>A graphical editor built with Python and Qt.</p>",
|
||||||
)
|
)
|
||||||
|
|
||||||
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802 (Qt API name)
|
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802 (Qt API name)
|
||||||
|
if not self._resolve_source_edits() or not self._maybe_save():
|
||||||
|
event.ignore()
|
||||||
|
return
|
||||||
self.settings.setValue("window/geometry", self.saveGeometry())
|
self.settings.setValue("window/geometry", self.saveGeometry())
|
||||||
event.accept()
|
event.accept()
|
||||||
|
|||||||
@@ -234,6 +234,114 @@ _H\xaed[3B\xe2\xfa3\xb9\x04\xc0\xec\xee\xee\
|
|||||||
\xe2\xbf\x0f\x0f\x0f\xa7)sR\xf6\xffA\x01\x8e9D\
|
\xe2\xbf\x0f\x0f\x0f\xa7)sR\xf6\xffA\x01\x8e9D\
|
||||||
R\xff\x03\x8c\xc5\xeaCX+lK\x00\x00\x00\x00I\
|
R\xff\x03\x8c\xc5\xeaCX+lK\x00\x00\x00\x00I\
|
||||||
END\xaeB`\x82\
|
END\xaeB`\x82\
|
||||||
|
\x00\x00\x06\x9c\
|
||||||
|
\x89\
|
||||||
|
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
|
||||||
|
\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
|
||||||
|
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
|
||||||
|
\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\
|
||||||
|
\x00\x00\x09pHYs\x00\x00\x06\xec\x00\x00\x06\xec\x01\
|
||||||
|
\x1eu85\x00\x00\x00\x07tIME\x07\xd9\x06\x11\
|
||||||
|
\x17\x13$\xda\x852\xab\x00\x00\x06\x1cIDATx\
|
||||||
|
\xda\xed\x95}\x8c\x15\xd5\x19\xc6\x9f\xf7\xcc\xdc;\xb3w\
|
||||||
|
?\xfd\xa0T\xd8\x22+$(\xf2\xd5j\xc2\xae\x1f$\
|
||||||
|
\xadI\xff\xd0\xb8J(1%\x9a\x08%\xd6\x15-\xb6\
|
||||||
|
\x8d\xb6\xb1]H\x0bX\xa2\xaeUB\xac\xa1\xa9\xd1H\
|
||||||
|
\x1b\x14\xa4F\xa0\xc8*\x9a\x0a\xbaT\x5cL\x88\xc9\x92\
|
||||||
|
\x88\xd4\x82Z\xe1\xde;w\xef\x9d\xef\xf3\xf6\xcc\xb9\
|
||||||
|
\xc3d7J\x1a\xdc\xb4\x7f\xf1K\xde\xfb\xce\x99\xb9\x93\
|
||||||
|
\xe79\xef{\xce\x19\x9c\xe7<g\x18\x18x\x0fce\
|
||||||
|
\xeb\xd6\xad8W\x08#\xd8\xb7\xef\xbdW=\xcfk\x0d\
|
||||||
|
\x82\x10a\x18\xc0\xf7u\xd6\x11\x04\x91\xceQ$a\x1a\
|
||||||
|
\x04\xc34`\xaa \x12*\x9bd\xdbvm\xc1\x82[\
|
||||||
|
\xbe\x87s\xc4\xc4\x08\xa2(n\x9d6m\xca\x5c!\x04\
|
||||||
|
\x88\xe8K\x91\xcb\xe5`Y\x16ER@\xf9\x80\xc9\x1e\
|
||||||
|
\x930 \xa5\x0c_ye\xc7 \x14c2\x000'\
|
||||||
|
\xb3a\xe6Q\xc2\x0a\x9d\x13c\x8e+\xd1?\xc4\xb0L\
|
||||||
|
F[!O\x9d\x93\x81\x06;\x97cf\x8c\xd9\x003\
|
||||||
|
\xb4\x88\x94\xf2+g\x9f7\x09;\x0f\x09\x5c\xf9\x8d\x1a\
|
||||||
|
&\xb6J\x1c/\x9a\xd8t\xc0\xc2\xb7'\x09\x12\xf8z\
|
||||||
|
\x08\x8c\xe6+Ko\x18\x86\xceGO\x09\xd4\xdc\x10\xdf\
|
||||||
|
l\x8a\x10DP&\x22tO\xab\xe1\xb4\xe3\xf2\xe9K\
|
||||||
|
n\xe9\xda\xf0Vx;\x14O\xbd\xe1!a\xd56\x1f\
|
||||||
|
\x7fxK\xb6#\xe5\xc9\xdd\xfe\x7f7 \x84.\xb5\x8e\
|
||||||
|
\xb4\xec\xda\x80\x95\x13\xd8\xf6\x01an\xbb\x8b0f]\
|
||||||
|
.)\x81\x18\xc0\x9cKB\xea\x9e\xees\xb3\x15?\xff\
|
||||||
|
x\x7f0\x14\x89\xdc\x0c(zo\xb6P\xaeF\xeb\x1e\
|
||||||
|
\xed\x0f\xc2\xbe\xd7\xa2G\x97\xdf`\xe1\xe1\xbfyg\xdf\
|
||||||
|
\x05{\xf6\xec{g\xd6\xac+\xba\xd2\x16dFr\xa6\
|
||||||
|
\x81\xb7\x8f\x11>=\xe5c^\x87\xaf_\xa3\xc4(\x11\
|
||||||
|
\xb2\xff%m2\x81\x7f\x96\x0d~\xf3p\x8e\xaa\xa1\xd8\
|
||||||
|
\xdcd\xc8;?*\x81\x1b-^.@\xab\x0d\x81\xa2\
|
||||||
|
m\x19\x13\xcb\xc3Q\xb0z\xbe}\xb6\x16de\xcf\x22\
|
||||||
|
\x82\x81\x1d\x07\x19]\xdfr\x11K\x80\xa1\x82\xb9\x9e\xb3\
|
||||||
|
kF\x18\x13&4KZ<7D\xd7\xa5\xe1\xc2/\
|
||||||
|
\x5c\xae\xb6\xd9\xf8\xf9o\xbb\xed\xb5\x15_\xb6\x97\xdd\xd8\
|
||||||
|
<\xe5D\xbbS\xf1/\x1b0\x05\xa3\xe4%36\xb5\
|
||||||
|
\xb0\xa9\xb2m\x99xa?\xe3\xdaI5P\x22\xc3\xf5\
|
||||||
|
\xd0\xf0\xe8\x05L\xa4\x7f\x10\xc5\xc0w\xda\x19?\xfb.\
|
||||||
|
c\xca8^\xf5\xc0\x16\xafT0E\x87\xe3\xe1\x1a\xc7\
|
||||||
|
\xe3\xeb~\xf2Bp\xf9(\x03M\xcb\x8a\xc0\xb5\x5cX\
|
||||||
|
\xf4\xf2\xd4\xae\x9f\xbe\x18s\x83\xa5\xc5\xb5\x89\x13e\x81\
|
||||||
|
\xfd\xc7\x02\x5c5\xc1\x83\xcc\xc4\xd2\x0c\xceL\x10\x01`\
|
||||||
|
\xca\xae\x19\x04\x10c\xfe,\x89\x9e\xeb\xd1R\xf6y\xfb\
|
||||||
|
\x86;\x0aC\x8e\xcb'\xca5y\xeb(\x03\xcd\xb6\xe8\
|
||||||
|
\x1cwu\xc9q|\x93\xb7\xbe\x1f\xd3\xc7\xa7\x05\xcc\xb4\
|
||||||
|
\xfc}\xfd\x01\xe6O\xaf\x22\x88\xb5\xb0\x16\xcdLh\x11\
|
||||||
|
\xa4P6\x94\xcc*$H\xb7F\xe0\xe8\xbf\xa1\xde\xa7\
|
||||||
|
\xbdPT|\x0c*\x13\x933\x03W\xfc\xba\xf4@S\
|
||||||
|
\x1e\x03-6\x19M6\xd1\xf8V\xc2\x83/\x05 !\
|
||||||
|
\xf0\xf6\x87\x11\x02\xdfCG\x9b\x0f)\xf9\x8c\x03 3\
|
||||||
|
\xc1YH)\x11\xc7*T\x06\xa7\x0b\xd4\x10\x80 l\
|
||||||
|
|7\xa6r\x85\xef\xd6\x06j\x1cW<ie\x06\x0a\
|
||||||
|
y!\x1a-\xba\xb5\xa5@\x9d\x86 \xf4\xded\xf3\xee\
|
||||||
|
\xa1\x08G>\x8f\xd5\x96\x09\xb0\xf0\xca\x12j~\x8c0\
|
||||||
|
Tf\xd2\x08\x93\x88\x22%\x18kQ\xc9\x5c\x9f\xbd\xde\
|
||||||
|
\xc2\x89p\xfd@\xcb+\x03\xcf\x0dH\x0c{\xe8\xfb\xe0\
|
||||||
|
dxb\xe6\xca\xf2\xec\x92';\x9c\x9a<\x06\xc5\xf7\
|
||||||
|
\xfb\x9c\x1cA\xd1\xb9\xc6\x81iRO\xd5\x8d\xd6\x1fX\
|
||||||
|
\xd9&\xfe\xb4\xc7\xc7\xde\xc3\x11&4\xd6\xb0hv\x05\
|
||||||
|
L\x02\x86\xde\x92\x86\xfe\x10\x09C\x8dI\x8d\xd3kS\
|
||||||
|
\xdd'\x91\xe4\xfaX\x90\xd0\xff+\xba\xc4K\x9e\x8f\xbd\
|
||||||
|
]\xf77\x16&\xff\xc2yN@\xde\xa1\x1e\x07\xea\xf9\
|
||||||
|
6\xd3\xc4\x1c\x83\xd8\x17P\xec\xfbUK\xb2\x0e\x9a/\
|
||||||
|
n\xe4\x00\x8aE\x9dy\x1c\xfc$\xc6m3\x87!\x91\
|
||||||
|
\x8a\x89D\x10Z\x88\xce\x88\x8b\xfa3\xaaW\xba~_\
|
||||||
|
\x9b%4X\x02\x0f\xef\x94Dd,\x81\xc2\xf1\xc5\xbd\
|
||||||
|
\x8e\x07\xaax\x9cW-\xf8A\xa5&O\x1e|\xa70\
|
||||||
|
C \xa5\xb9A\xb8\x0dy\xc3\x84\xe2\xc1\xcd\x9e\x9ay\
|
||||||
|
U\x09@\x8b\x90\xa1\x82(5\xa2\xb2\x00H\x0b\xd7\xc7\
|
||||||
|
\x02i\x85\xf4\xfd\xfa;{\x8f0\xef\xff8\x1ezu\
|
||||||
|
\xb9\xbd\x09\x8a\xd3O4U\xaa>\xd69.h8\xa0\
|
||||||
|
U\xc7\x1fk\x9b7q\x96\xd2@J\x8b-\x06\x0b\x0d\
|
||||||
|
9\x13`\xde\xfb\xa1\xc45\x93\x036\xcdd\x86\x80H\
|
||||||
|
\x221\x91\x1d\xcf\xa9\x11\x8c0\x92\x1d\xdb\x02\xb6\x05\xf4\
|
||||||
|
\xbe\x1c\x12Ic\xe1u\x8f8Hh\xb8\xbb\x08\x90x\
|
||||||
|
\x88\xc9\xe8v\xd6\xb7\xae\x80\xe2_}m \x8c\xe0\xfe\
|
||||||
|
M._H\x9f\x1f\xbf\x98>s~\x7f`\xca\x8c\x9b\
|
||||||
|
\xa7\xc7Xv\xbd\x0fF:S#\xc9F\xd6\x7f\xf3\x8c\
|
||||||
|
\x11#\x19\x1b\xba\xfcy\x83\xf0\xcc@\x8c'^\x8f^\
|
||||||
|
<\xb4\xa6\xe56\xd4\xc9L\xb8O_p\xf6\x8fQ\xc1\
|
||||||
|
6\xef\x1d6\xc7O:\x84\xab\xfe|x\xedE\xb4\xf1\
|
||||||
|
}\xabw\xde\xfaf\xec:\x94C\xb3\x9d\xf6W\x97\x18\
|
||||||
|
i\xefU\xd4\x17]Z% \x90\x845;B\x84\xd2\
|
||||||
|
X<\xf3\x97%\x8c \x15\x1f\x0d\x01\xa3Y\xbd=\xf8\
|
||||||
|
\xa3\x04\x96J)\xff!\x88~\xb3b\xbb\xf3\xf7\x82\xc8\
|
||||||
|
={\xd98^\xb8nA\xccs\xda\x89B\xd6\x0b\xad\
|
||||||
|
.\xac\x0f\xacz\xf9\xd5\x96\xc6\x8f7\x86\xd8vP\xf6\
|
||||||
|
\x9e\xeck^\x8dse\xedN_\xe7\xbe\xd7\xa3\xee\xc7\
|
||||||
|
\xfa\x83\xc1Gv\x05\xbc\xe2\xafANW\xe7\x9e\xf2\x0c\
|
||||||
|
,.\x0du?Y\xe4\xe3'K\xb2\x5cv\xb8\xe8\x0c\
|
||||||
|
se\xb8\xca\xd5\x9a\xc7\x9e\x17\xf0\xd0'\xbe4\x96\x16\
|
||||||
|
K\x18+\x8f\xf7\xd7\x8d<\xbdGN\xca\xda\xd3S\xd4\
|
||||||
|
\xd9\xee\xa9\xfc\x10?*\xd5\x1e\xda\xe2p\xe0)q\xd7\
|
||||||
|
\xe5\x9a\xe73s\xc8\xb3W:l\xf7\x94\xbb\xf1\x17\xc6\
|
||||||
|
\xff\x1c\xf3.\xe7wm\xf7\x95x\xf3\xbbU\x99\x88o\
|
||||||
|
\xd9\xefJ,)\x0e\xe2\x1c!\x8c\x81\x5cO\xe9\x82\xd0\
|
||||||
|
\xe3\x8dWw\x187\x1e\xfdB\xc2\x8d1\x15\xa1<\xe2\
|
||||||
|
n\xb8\x10\xff\x17\x1a\x96\x9e\xd2\xd9Z\xe6L-\xdc\xe3\
|
||||||
|
\x8c\xc7y\xbe\x06\xff\x01\xd8\x02\xdah\xfa;i$\x00\
|
||||||
|
\x00\x00\x00IEND\xaeB`\x82\
|
||||||
\x00\x00\x07\xe4\
|
\x00\x00\x07\xe4\
|
||||||
\x89\
|
\x89\
|
||||||
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
|
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
|
||||||
@@ -906,6 +1014,11 @@ qt_resource_name = b"\
|
|||||||
\x00d\
|
\x00d\
|
||||||
\x00o\x00c\x00u\x00m\x00e\x00n\x00t\x00-\x00s\x00a\x00v\x00e\x00-\x00a\x00s\x00.\
|
\x00o\x00c\x00u\x00m\x00e\x00n\x00t\x00-\x00s\x00a\x00v\x00e\x00-\x00a\x00s\x00.\
|
||||||
\x00p\x00n\x00g\
|
\x00p\x00n\x00g\
|
||||||
|
\x00\x14\
|
||||||
|
\x0c\xbb2g\
|
||||||
|
\x00t\
|
||||||
|
\x00r\x00a\x00n\x00s\x00f\x00o\x00r\x00m\x00-\x00r\x00o\x00t\x00a\x00t\x00e\x00.\
|
||||||
|
\x00p\x00n\x00g\
|
||||||
\x00\x0d\
|
\x00\x0d\
|
||||||
\x03\xd2\xbeg\
|
\x03\xd2\xbeg\
|
||||||
\x00e\
|
\x00e\
|
||||||
@@ -943,25 +1056,27 @@ qt_resource_struct = b"\
|
|||||||
\x00\x00\x00\x00\x00\x00\x00\x00\
|
\x00\x00\x00\x00\x00\x00\x00\x00\
|
||||||
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\
|
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\
|
||||||
\x00\x00\x00\x00\x00\x00\x00\x00\
|
\x00\x00\x00\x00\x00\x00\x00\x00\
|
||||||
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x09\x00\x00\x00\x03\
|
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x03\
|
||||||
\x00\x00\x00\x00\x00\x00\x00\x00\
|
\x00\x00\x00\x00\x00\x00\x00\x00\
|
||||||
\x00\x00\x00\xa2\x00\x00\x00\x00\x00\x01\x00\x00\x19Z\
|
\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x01\x00\x00\x1f\xfa\
|
||||||
\x00\x00\x01\x9f{C\xf1'\
|
\x00\x00\x01\x9f{C\xf1'\
|
||||||
\x00\x00\x00\xe2\x00\x00\x00\x00\x00\x01\x00\x00$\x8c\
|
\x00\x00\x01\x10\x00\x00\x00\x00\x00\x01\x00\x00+,\
|
||||||
\x00\x00\x01\x9f{0\xc99\
|
\x00\x00\x01\x9f{0\xc99\
|
||||||
\x00\x00\x00d\x00\x00\x00\x00\x00\x01\x00\x00\x0d\xf2\
|
\x00\x00\x00\x92\x00\x00\x00\x00\x00\x01\x00\x00\x14\x92\
|
||||||
\x00\x00\x01\x9f{C\xf1\x18\
|
\x00\x00\x01\x9f{C\xf1\x18\
|
||||||
\x00\x00\x00\x84\x00\x00\x00\x00\x00\x01\x00\x00\x15\xda\
|
\x00\x00\x00\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x1cz\
|
||||||
\x00\x00\x01\x9f{C\xf1.\
|
\x00\x00\x01\x9f{C\xf1.\
|
||||||
\x00\x00\x006\x00\x00\x00\x00\x00\x01\x00\x00\x05\x86\
|
\x00\x00\x006\x00\x00\x00\x00\x00\x01\x00\x00\x05\x86\
|
||||||
\x00\x00\x01\x9f{0\xc9B\
|
\x00\x00\x01\x9f{0\xc9B\
|
||||||
\x00\x00\x01\x0a\x00\x00\x00\x00\x00\x01\x00\x00+\x96\
|
\x00\x00\x018\x00\x00\x00\x00\x00\x01\x00\x0026\
|
||||||
\x00\x00\x01\x9f{C\xf1N\
|
\x00\x00\x01\x9f{C\xf1N\
|
||||||
|
\x00\x00\x00d\x00\x00\x00\x00\x00\x01\x00\x00\x0d\xf2\
|
||||||
|
\x00\x00\x01\x9f{\x8d\xf34\
|
||||||
\x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
|
\x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
|
||||||
\x00\x00\x01\x9f{0\xc9+\
|
\x00\x00\x01\x9f{0\xc9+\
|
||||||
\x00\x00\x00\xc2\x00\x00\x00\x00\x00\x01\x00\x00\x1c\xba\
|
\x00\x00\x00\xf0\x00\x00\x00\x00\x00\x01\x00\x00#Z\
|
||||||
\x00\x00\x01\x9f{C\xf1=\
|
\x00\x00\x01\x9f{C\xf1=\
|
||||||
\x00\x00\x01,\x00\x00\x00\x00\x00\x01\x00\x001\x18\
|
\x00\x00\x01Z\x00\x00\x00\x00\x00\x01\x00\x007\xb8\
|
||||||
\x00\x00\x01\x9f{0\xc9R\
|
\x00\x00\x01\x9f{0\xc9R\
|
||||||
"
|
"
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
from PySide6.QtCore import QSettings
|
from pathlib import Path
|
||||||
from PySide6.QtWidgets import QDialog
|
|
||||||
|
|
||||||
|
from PySide6.QtCore import QSettings, Signal
|
||||||
|
from PySide6.QtWidgets import QDialog, QFileDialog
|
||||||
|
|
||||||
|
from bedit.library.repository import default_library_paths
|
||||||
from bedit.ui_settings_dialog import Ui_SettingsDialog
|
from bedit.ui_settings_dialog import Ui_SettingsDialog
|
||||||
|
|
||||||
|
|
||||||
class SettingsDialog(QDialog):
|
class SettingsDialog(QDialog):
|
||||||
"""Edit application preferences defined in the Designer form."""
|
"""Edit application preferences defined in the Designer form."""
|
||||||
|
|
||||||
|
settingsChanged = Signal()
|
||||||
|
|
||||||
def __init__(self, parent=None) -> None:
|
def __init__(self, parent=None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.ui = Ui_SettingsDialog()
|
self.ui = Ui_SettingsDialog()
|
||||||
self.ui.setupUi(self)
|
self.ui.setupUi(self)
|
||||||
self.settings = QSettings()
|
self.settings = QSettings()
|
||||||
|
self.ui.addLibraryFileButton.clicked.connect(self._add_library_file)
|
||||||
|
self.ui.addLibraryFolderButton.clicked.connect(self._add_library_folder)
|
||||||
|
self.ui.removeLibraryPathButton.clicked.connect(self._remove_library_path)
|
||||||
|
self.ui.libraryPathsList.itemSelectionChanged.connect(self._update_remove_button)
|
||||||
self._load_settings()
|
self._load_settings()
|
||||||
|
|
||||||
def _load_settings(self) -> None:
|
def _load_settings(self) -> None:
|
||||||
@@ -21,10 +30,59 @@ class SettingsDialog(QDialog):
|
|||||||
self.ui.autosaveIntervalSpinBox.setValue(
|
self.ui.autosaveIntervalSpinBox.setValue(
|
||||||
self.settings.value("general/autosaveInterval", 5, type=int)
|
self.settings.value("general/autosaveInterval", 5, type=int)
|
||||||
)
|
)
|
||||||
|
self.ui.libraryPathsList.clear()
|
||||||
|
self.ui.libraryPathsList.addItems(self.library_paths(self.settings))
|
||||||
|
self._update_remove_button()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def library_paths(settings: QSettings | None = None) -> list[str]:
|
||||||
|
settings = settings or QSettings()
|
||||||
|
value = settings.value("libraries/paths", default_library_paths())
|
||||||
|
if isinstance(value, str):
|
||||||
|
return [value]
|
||||||
|
return [str(path) for path in value]
|
||||||
|
|
||||||
|
def _add_library_file(self) -> None:
|
||||||
|
path, _ = QFileDialog.getOpenFileName(
|
||||||
|
self,
|
||||||
|
"Add library",
|
||||||
|
"",
|
||||||
|
"BEdit libraries (*.json);;All files (*)",
|
||||||
|
)
|
||||||
|
if path:
|
||||||
|
self._append_unique_path(path)
|
||||||
|
|
||||||
|
def _add_library_folder(self) -> None:
|
||||||
|
path = QFileDialog.getExistingDirectory(self, "Add library folder")
|
||||||
|
if path:
|
||||||
|
self._append_unique_path(path)
|
||||||
|
|
||||||
|
def _append_unique_path(self, path: str) -> None:
|
||||||
|
normalized = str(Path(path).expanduser().resolve())
|
||||||
|
existing = {
|
||||||
|
self.ui.libraryPathsList.item(row).text()
|
||||||
|
for row in range(self.ui.libraryPathsList.count())
|
||||||
|
}
|
||||||
|
if normalized not in existing:
|
||||||
|
self.ui.libraryPathsList.addItem(normalized)
|
||||||
|
|
||||||
|
def _remove_library_path(self) -> None:
|
||||||
|
for item in self.ui.libraryPathsList.selectedItems():
|
||||||
|
self.ui.libraryPathsList.takeItem(self.ui.libraryPathsList.row(item))
|
||||||
|
|
||||||
|
def _update_remove_button(self) -> None:
|
||||||
|
self.ui.removeLibraryPathButton.setEnabled(bool(self.ui.libraryPathsList.selectedItems()))
|
||||||
|
|
||||||
def accept(self) -> None:
|
def accept(self) -> None:
|
||||||
self.settings.setValue("general/autosaveEnabled", self.ui.autosaveGroupBox.isChecked())
|
self.settings.setValue("general/autosaveEnabled", self.ui.autosaveGroupBox.isChecked())
|
||||||
self.settings.setValue(
|
self.settings.setValue(
|
||||||
"general/autosaveInterval", self.ui.autosaveIntervalSpinBox.value()
|
"general/autosaveInterval", self.ui.autosaveIntervalSpinBox.value()
|
||||||
)
|
)
|
||||||
|
paths = [
|
||||||
|
self.ui.libraryPathsList.item(row).text()
|
||||||
|
for row in range(self.ui.libraryPathsList.count())
|
||||||
|
]
|
||||||
|
self.settings.setValue("libraries/paths", paths)
|
||||||
|
self.settings.sync()
|
||||||
|
self.settingsChanged.emit()
|
||||||
super().accept()
|
super().accept()
|
||||||
|
|||||||
125
BEdit/src/bedit/ui_component_options_dialog.py
Normal file
125
BEdit/src/bedit/ui_component_options_dialog.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
## Form generated from reading UI file 'component_options_dialog.ui'
|
||||||
|
##
|
||||||
|
## Created by: Qt User Interface Compiler version 6.11.1
|
||||||
|
##
|
||||||
|
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
||||||
|
QMetaObject, QObject, QPoint, QRect,
|
||||||
|
QSize, QTime, QUrl, Qt)
|
||||||
|
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
||||||
|
QFont, QFontDatabase, QGradient, QIcon,
|
||||||
|
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||||
|
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||||
|
from PySide6.QtWidgets import (QAbstractButton, QApplication, QCheckBox, QComboBox,
|
||||||
|
QDialog, QDialogButtonBox, QFormLayout, QLabel,
|
||||||
|
QLineEdit, QSizePolicy, QSpacerItem, QVBoxLayout,
|
||||||
|
QWidget)
|
||||||
|
|
||||||
|
class Ui_ComponentOptionsDialog(object):
|
||||||
|
def setupUi(self, ComponentOptionsDialog):
|
||||||
|
if not ComponentOptionsDialog.objectName():
|
||||||
|
ComponentOptionsDialog.setObjectName(u"ComponentOptionsDialog")
|
||||||
|
ComponentOptionsDialog.resize(420, 260)
|
||||||
|
self.dialogLayout = QVBoxLayout(ComponentOptionsDialog)
|
||||||
|
self.dialogLayout.setObjectName(u"dialogLayout")
|
||||||
|
self.optionsForm = QFormLayout()
|
||||||
|
self.optionsForm.setObjectName(u"optionsForm")
|
||||||
|
self.nameLabel = QLabel(ComponentOptionsDialog)
|
||||||
|
self.nameLabel.setObjectName(u"nameLabel")
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(0, QFormLayout.ItemRole.LabelRole, self.nameLabel)
|
||||||
|
|
||||||
|
self.nameEdit = QLineEdit(ComponentOptionsDialog)
|
||||||
|
self.nameEdit.setObjectName(u"nameEdit")
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(0, QFormLayout.ItemRole.FieldRole, self.nameEdit)
|
||||||
|
|
||||||
|
self.shapeLabel = QLabel(ComponentOptionsDialog)
|
||||||
|
self.shapeLabel.setObjectName(u"shapeLabel")
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(1, QFormLayout.ItemRole.LabelRole, self.shapeLabel)
|
||||||
|
|
||||||
|
self.shapeCombo = QComboBox(ComponentOptionsDialog)
|
||||||
|
self.shapeCombo.addItem("")
|
||||||
|
self.shapeCombo.addItem("")
|
||||||
|
self.shapeCombo.setObjectName(u"shapeCombo")
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(1, QFormLayout.ItemRole.FieldRole, self.shapeCombo)
|
||||||
|
|
||||||
|
self.iconTextLabel = QLabel(ComponentOptionsDialog)
|
||||||
|
self.iconTextLabel.setObjectName(u"iconTextLabel")
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(2, QFormLayout.ItemRole.LabelRole, self.iconTextLabel)
|
||||||
|
|
||||||
|
self.iconTextEdit = QLineEdit(ComponentOptionsDialog)
|
||||||
|
self.iconTextEdit.setObjectName(u"iconTextEdit")
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(2, QFormLayout.ItemRole.FieldRole, self.iconTextEdit)
|
||||||
|
|
||||||
|
self.fillLabel = QLabel(ComponentOptionsDialog)
|
||||||
|
self.fillLabel.setObjectName(u"fillLabel")
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(3, QFormLayout.ItemRole.LabelRole, self.fillLabel)
|
||||||
|
|
||||||
|
self.fillEdit = QLineEdit(ComponentOptionsDialog)
|
||||||
|
self.fillEdit.setObjectName(u"fillEdit")
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(3, QFormLayout.ItemRole.FieldRole, self.fillEdit)
|
||||||
|
|
||||||
|
self.borderLabel = QLabel(ComponentOptionsDialog)
|
||||||
|
self.borderLabel.setObjectName(u"borderLabel")
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(4, QFormLayout.ItemRole.LabelRole, self.borderLabel)
|
||||||
|
|
||||||
|
self.borderEdit = QLineEdit(ComponentOptionsDialog)
|
||||||
|
self.borderEdit.setObjectName(u"borderEdit")
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(4, QFormLayout.ItemRole.FieldRole, self.borderEdit)
|
||||||
|
|
||||||
|
self.showSubtreeCheckBox = QCheckBox(ComponentOptionsDialog)
|
||||||
|
self.showSubtreeCheckBox.setObjectName(u"showSubtreeCheckBox")
|
||||||
|
self.showSubtreeCheckBox.setChecked(True)
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(5, QFormLayout.ItemRole.SpanningRole, self.showSubtreeCheckBox)
|
||||||
|
|
||||||
|
|
||||||
|
self.dialogLayout.addLayout(self.optionsForm)
|
||||||
|
|
||||||
|
self.optionsSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
|
||||||
|
|
||||||
|
self.dialogLayout.addItem(self.optionsSpacer)
|
||||||
|
|
||||||
|
self.buttonBox = QDialogButtonBox(ComponentOptionsDialog)
|
||||||
|
self.buttonBox.setObjectName(u"buttonBox")
|
||||||
|
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
|
||||||
|
|
||||||
|
self.dialogLayout.addWidget(self.buttonBox)
|
||||||
|
|
||||||
|
|
||||||
|
self.retranslateUi(ComponentOptionsDialog)
|
||||||
|
self.buttonBox.accepted.connect(ComponentOptionsDialog.accept)
|
||||||
|
self.buttonBox.rejected.connect(ComponentOptionsDialog.reject)
|
||||||
|
|
||||||
|
QMetaObject.connectSlotsByName(ComponentOptionsDialog)
|
||||||
|
# setupUi
|
||||||
|
|
||||||
|
def retranslateUi(self, ComponentOptionsDialog):
|
||||||
|
ComponentOptionsDialog.setWindowTitle(QCoreApplication.translate("ComponentOptionsDialog", u"Component Options", None))
|
||||||
|
self.nameLabel.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Name:", None))
|
||||||
|
self.shapeLabel.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Icon shape:", None))
|
||||||
|
self.shapeCombo.setItemText(0, QCoreApplication.translate("ComponentOptionsDialog", u"rectangle", None))
|
||||||
|
self.shapeCombo.setItemText(1, QCoreApplication.translate("ComponentOptionsDialog", u"ellipse", None))
|
||||||
|
|
||||||
|
self.iconTextLabel.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Icon text:", None))
|
||||||
|
self.fillLabel.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Fill color:", None))
|
||||||
|
self.fillEdit.setPlaceholderText(QCoreApplication.translate("ComponentOptionsDialog", u"#dbeafe", None))
|
||||||
|
self.borderLabel.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Border color:", None))
|
||||||
|
self.borderEdit.setPlaceholderText(QCoreApplication.translate("ComponentOptionsDialog", u"#303030", None))
|
||||||
|
self.showSubtreeCheckBox.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Show contained components in the Libraries tree", None))
|
||||||
|
# retranslateUi
|
||||||
|
|
||||||
@@ -16,10 +16,13 @@ from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
|
|||||||
QIcon, QImage, QKeySequence, QLinearGradient,
|
QIcon, QImage, QKeySequence, QLinearGradient,
|
||||||
QPainter, QPalette, QPixmap, QRadialGradient,
|
QPainter, QPalette, QPixmap, QRadialGradient,
|
||||||
QTransform)
|
QTransform)
|
||||||
from PySide6.QtWidgets import (QApplication, QDockWidget, QHBoxLayout, QHeaderView,
|
from PySide6.QtWidgets import (QApplication, QDockWidget, QFrame, QHBoxLayout,
|
||||||
QMainWindow, QMenu, QMenuBar, QSizePolicy,
|
QHeaderView, QLabel, QMainWindow, QMenu,
|
||||||
QSplitter, QToolBar, QTreeView, QVBoxLayout,
|
QMenuBar, QPlainTextEdit, QPushButton, QSizePolicy,
|
||||||
QWidget)
|
QSpacerItem, QSplitter, QStackedWidget, QToolBar,
|
||||||
|
QToolButton, QTreeView, QVBoxLayout, QWidget)
|
||||||
|
|
||||||
|
from bedit.workspace.view import GraphWorkspaceView
|
||||||
from . import resources_rc
|
from . import resources_rc
|
||||||
|
|
||||||
class Ui_MainWindow(object):
|
class Ui_MainWindow(object):
|
||||||
@@ -32,50 +35,59 @@ class Ui_MainWindow(object):
|
|||||||
icon = QIcon()
|
icon = QIcon()
|
||||||
icon.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.actionNew.setIcon(icon)
|
self.actionNew.setIcon(icon)
|
||||||
|
self.actionRotateClockwise = QAction(MainWindow)
|
||||||
|
self.actionRotateClockwise.setObjectName(u"actionRotateClockwise")
|
||||||
|
icon1 = QIcon()
|
||||||
|
icon1.addFile(u":/icons/icons/transform-rotate.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.actionRotateClockwise.setIcon(icon1)
|
||||||
self.actionOpen = QAction(MainWindow)
|
self.actionOpen = QAction(MainWindow)
|
||||||
self.actionOpen.setObjectName(u"actionOpen")
|
self.actionOpen.setObjectName(u"actionOpen")
|
||||||
icon1 = QIcon()
|
icon2 = QIcon()
|
||||||
icon1.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon2.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.actionOpen.setIcon(icon1)
|
self.actionOpen.setIcon(icon2)
|
||||||
self.actionSave = QAction(MainWindow)
|
self.actionSave = QAction(MainWindow)
|
||||||
self.actionSave.setObjectName(u"actionSave")
|
self.actionSave.setObjectName(u"actionSave")
|
||||||
icon2 = QIcon()
|
icon3 = QIcon()
|
||||||
icon2.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon3.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.actionSave.setIcon(icon2)
|
self.actionSave.setIcon(icon3)
|
||||||
self.actionSaveAs = QAction(MainWindow)
|
self.actionSaveAs = QAction(MainWindow)
|
||||||
self.actionSaveAs.setObjectName(u"actionSaveAs")
|
self.actionSaveAs.setObjectName(u"actionSaveAs")
|
||||||
icon3 = QIcon()
|
icon4 = QIcon()
|
||||||
icon3.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon4.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.actionSaveAs.setIcon(icon3)
|
self.actionSaveAs.setIcon(icon4)
|
||||||
self.actionExit = QAction(MainWindow)
|
self.actionExit = QAction(MainWindow)
|
||||||
self.actionExit.setObjectName(u"actionExit")
|
self.actionExit.setObjectName(u"actionExit")
|
||||||
|
self.actionClose = QAction(MainWindow)
|
||||||
|
self.actionClose.setObjectName(u"actionClose")
|
||||||
self.actionUndo = QAction(MainWindow)
|
self.actionUndo = QAction(MainWindow)
|
||||||
self.actionUndo.setObjectName(u"actionUndo")
|
self.actionUndo.setObjectName(u"actionUndo")
|
||||||
icon4 = QIcon()
|
icon5 = QIcon()
|
||||||
icon4.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon5.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.actionUndo.setIcon(icon4)
|
self.actionUndo.setIcon(icon5)
|
||||||
self.actionRedo = QAction(MainWindow)
|
self.actionRedo = QAction(MainWindow)
|
||||||
self.actionRedo.setObjectName(u"actionRedo")
|
self.actionRedo.setObjectName(u"actionRedo")
|
||||||
icon5 = QIcon()
|
icon6 = QIcon()
|
||||||
icon5.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon6.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.actionRedo.setIcon(icon5)
|
self.actionRedo.setIcon(icon6)
|
||||||
self.actionCut = QAction(MainWindow)
|
self.actionCut = QAction(MainWindow)
|
||||||
self.actionCut.setObjectName(u"actionCut")
|
self.actionCut.setObjectName(u"actionCut")
|
||||||
icon6 = QIcon()
|
icon7 = QIcon()
|
||||||
icon6.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon7.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.actionCut.setIcon(icon6)
|
self.actionCut.setIcon(icon7)
|
||||||
self.actionCopy = QAction(MainWindow)
|
self.actionCopy = QAction(MainWindow)
|
||||||
self.actionCopy.setObjectName(u"actionCopy")
|
self.actionCopy.setObjectName(u"actionCopy")
|
||||||
icon7 = QIcon()
|
icon8 = QIcon()
|
||||||
icon7.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon8.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.actionCopy.setIcon(icon7)
|
self.actionCopy.setIcon(icon8)
|
||||||
self.actionPaste = QAction(MainWindow)
|
self.actionPaste = QAction(MainWindow)
|
||||||
self.actionPaste.setObjectName(u"actionPaste")
|
self.actionPaste.setObjectName(u"actionPaste")
|
||||||
icon8 = QIcon()
|
icon9 = QIcon()
|
||||||
icon8.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon9.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.actionPaste.setIcon(icon8)
|
self.actionPaste.setIcon(icon9)
|
||||||
self.actionSelectAll = QAction(MainWindow)
|
self.actionSelectAll = QAction(MainWindow)
|
||||||
self.actionSelectAll.setObjectName(u"actionSelectAll")
|
self.actionSelectAll.setObjectName(u"actionSelectAll")
|
||||||
|
self.actionDelete = QAction(MainWindow)
|
||||||
|
self.actionDelete.setObjectName(u"actionDelete")
|
||||||
self.actionAbout = QAction(MainWindow)
|
self.actionAbout = QAction(MainWindow)
|
||||||
self.actionAbout.setObjectName(u"actionAbout")
|
self.actionAbout.setObjectName(u"actionAbout")
|
||||||
self.actionSettings = QAction(MainWindow)
|
self.actionSettings = QAction(MainWindow)
|
||||||
@@ -118,6 +130,24 @@ class Ui_MainWindow(object):
|
|||||||
|
|
||||||
self.panel_libraries.setWidget(self.dockWidgetContents)
|
self.panel_libraries.setWidget(self.dockWidgetContents)
|
||||||
self.leftDockHost.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.panel_libraries)
|
self.leftDockHost.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.panel_libraries)
|
||||||
|
self.panel_document = QDockWidget(self.leftDockHost)
|
||||||
|
self.panel_document.setObjectName(u"panel_document")
|
||||||
|
self.panel_document.setMinimumSize(QSize(220, 91))
|
||||||
|
self.documentDockContents = QWidget()
|
||||||
|
self.documentDockContents.setObjectName(u"documentDockContents")
|
||||||
|
self.documentPanelLayout = QVBoxLayout(self.documentDockContents)
|
||||||
|
self.documentPanelLayout.setSpacing(0)
|
||||||
|
self.documentPanelLayout.setObjectName(u"documentPanelLayout")
|
||||||
|
self.documentPanelLayout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.documentTreeView = QTreeView(self.documentDockContents)
|
||||||
|
self.documentTreeView.setObjectName(u"documentTreeView")
|
||||||
|
self.documentTreeView.setAlternatingRowColors(True)
|
||||||
|
self.documentTreeView.setUniformRowHeights(True)
|
||||||
|
|
||||||
|
self.documentPanelLayout.addWidget(self.documentTreeView)
|
||||||
|
|
||||||
|
self.panel_document.setWidget(self.documentDockContents)
|
||||||
|
self.leftDockHost.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.panel_document)
|
||||||
self.workspaceSplitter.addWidget(self.leftDockHost)
|
self.workspaceSplitter.addWidget(self.leftDockHost)
|
||||||
self.workspace = QWidget(self.workspaceSplitter)
|
self.workspace = QWidget(self.workspaceSplitter)
|
||||||
self.workspace.setObjectName(u"workspace")
|
self.workspace.setObjectName(u"workspace")
|
||||||
@@ -126,9 +156,107 @@ class Ui_MainWindow(object):
|
|||||||
sizePolicy1.setVerticalStretch(0)
|
sizePolicy1.setVerticalStretch(0)
|
||||||
sizePolicy1.setHeightForWidth(self.workspace.sizePolicy().hasHeightForWidth())
|
sizePolicy1.setHeightForWidth(self.workspace.sizePolicy().hasHeightForWidth())
|
||||||
self.workspace.setSizePolicy(sizePolicy1)
|
self.workspace.setSizePolicy(sizePolicy1)
|
||||||
self.workspace.setStyleSheet(u"QWidget#workspace {\n"
|
self.workspaceEditorLayout = QVBoxLayout(self.workspace)
|
||||||
" background-color: rgb(198, 198, 198);\n"
|
self.workspaceEditorLayout.setSpacing(0)
|
||||||
"}")
|
self.workspaceEditorLayout.setObjectName(u"workspaceEditorLayout")
|
||||||
|
self.workspaceEditorLayout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.workspaceHeader = QFrame(self.workspace)
|
||||||
|
self.workspaceHeader.setObjectName(u"workspaceHeader")
|
||||||
|
self.workspaceHeader.setMinimumSize(QSize(0, 34))
|
||||||
|
self.workspaceHeader.setMaximumSize(QSize(16777215, 34))
|
||||||
|
self.workspaceHeader.setFrameShape(QFrame.Shape.StyledPanel)
|
||||||
|
self.workspaceHeaderLayout = QHBoxLayout(self.workspaceHeader)
|
||||||
|
self.workspaceHeaderLayout.setObjectName(u"workspaceHeaderLayout")
|
||||||
|
self.workspaceHeaderLayout.setContentsMargins(6, 2, 6, 2)
|
||||||
|
self.navigateUpButton = QToolButton(self.workspaceHeader)
|
||||||
|
self.navigateUpButton.setObjectName(u"navigateUpButton")
|
||||||
|
|
||||||
|
self.workspaceHeaderLayout.addWidget(self.navigateUpButton)
|
||||||
|
|
||||||
|
self.graphBreadcrumbLabel = QLabel(self.workspaceHeader)
|
||||||
|
self.graphBreadcrumbLabel.setObjectName(u"graphBreadcrumbLabel")
|
||||||
|
|
||||||
|
self.workspaceHeaderLayout.addWidget(self.graphBreadcrumbLabel)
|
||||||
|
|
||||||
|
self.workspaceModeLabel = QLabel(self.workspaceHeader)
|
||||||
|
self.workspaceModeLabel.setObjectName(u"workspaceModeLabel")
|
||||||
|
|
||||||
|
self.workspaceHeaderLayout.addWidget(self.workspaceModeLabel)
|
||||||
|
|
||||||
|
self.workspaceHeaderSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
||||||
|
|
||||||
|
self.workspaceHeaderLayout.addItem(self.workspaceHeaderSpacer)
|
||||||
|
|
||||||
|
self.applyJsonButton = QPushButton(self.workspaceHeader)
|
||||||
|
self.applyJsonButton.setObjectName(u"applyJsonButton")
|
||||||
|
self.applyJsonButton.setVisible(False)
|
||||||
|
|
||||||
|
self.workspaceHeaderLayout.addWidget(self.applyJsonButton)
|
||||||
|
|
||||||
|
self.pointerToolButton = QToolButton(self.workspaceHeader)
|
||||||
|
self.pointerToolButton.setObjectName(u"pointerToolButton")
|
||||||
|
self.pointerToolButton.setCheckable(True)
|
||||||
|
self.pointerToolButton.setChecked(True)
|
||||||
|
self.pointerToolButton.setAutoExclusive(True)
|
||||||
|
|
||||||
|
self.workspaceHeaderLayout.addWidget(self.pointerToolButton)
|
||||||
|
|
||||||
|
self.inputToolButton = QToolButton(self.workspaceHeader)
|
||||||
|
self.inputToolButton.setObjectName(u"inputToolButton")
|
||||||
|
self.inputToolButton.setCheckable(True)
|
||||||
|
self.inputToolButton.setAutoExclusive(True)
|
||||||
|
|
||||||
|
self.workspaceHeaderLayout.addWidget(self.inputToolButton)
|
||||||
|
|
||||||
|
self.outputToolButton = QToolButton(self.workspaceHeader)
|
||||||
|
self.outputToolButton.setObjectName(u"outputToolButton")
|
||||||
|
self.outputToolButton.setCheckable(True)
|
||||||
|
self.outputToolButton.setAutoExclusive(True)
|
||||||
|
|
||||||
|
self.workspaceHeaderLayout.addWidget(self.outputToolButton)
|
||||||
|
|
||||||
|
|
||||||
|
self.workspaceEditorLayout.addWidget(self.workspaceHeader)
|
||||||
|
|
||||||
|
self.workspaceStack = QStackedWidget(self.workspace)
|
||||||
|
self.workspaceStack.setObjectName(u"workspaceStack")
|
||||||
|
self.graphPage = QWidget()
|
||||||
|
self.graphPage.setObjectName(u"graphPage")
|
||||||
|
self.graphPageLayout = QVBoxLayout(self.graphPage)
|
||||||
|
self.graphPageLayout.setObjectName(u"graphPageLayout")
|
||||||
|
self.graphPageLayout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.graphView = GraphWorkspaceView(self.graphPage)
|
||||||
|
self.graphView.setObjectName(u"graphView")
|
||||||
|
|
||||||
|
self.graphPageLayout.addWidget(self.graphView)
|
||||||
|
|
||||||
|
self.workspaceStack.addWidget(self.graphPage)
|
||||||
|
self.jsonPage = QWidget()
|
||||||
|
self.jsonPage.setObjectName(u"jsonPage")
|
||||||
|
self.jsonPageLayout = QVBoxLayout(self.jsonPage)
|
||||||
|
self.jsonPageLayout.setObjectName(u"jsonPageLayout")
|
||||||
|
self.jsonPageLayout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.jsonEditor = QPlainTextEdit(self.jsonPage)
|
||||||
|
self.jsonEditor.setObjectName(u"jsonEditor")
|
||||||
|
self.jsonEditor.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
|
||||||
|
|
||||||
|
self.jsonPageLayout.addWidget(self.jsonEditor)
|
||||||
|
|
||||||
|
self.workspaceStack.addWidget(self.jsonPage)
|
||||||
|
self.emptyPage = QWidget()
|
||||||
|
self.emptyPage.setObjectName(u"emptyPage")
|
||||||
|
self.emptyPageLayout = QVBoxLayout(self.emptyPage)
|
||||||
|
self.emptyPageLayout.setObjectName(u"emptyPageLayout")
|
||||||
|
self.emptyWorkspaceLabel = QLabel(self.emptyPage)
|
||||||
|
self.emptyWorkspaceLabel.setObjectName(u"emptyWorkspaceLabel")
|
||||||
|
self.emptyWorkspaceLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
|
||||||
|
self.emptyPageLayout.addWidget(self.emptyWorkspaceLabel)
|
||||||
|
|
||||||
|
self.workspaceStack.addWidget(self.emptyPage)
|
||||||
|
|
||||||
|
self.workspaceEditorLayout.addWidget(self.workspaceStack)
|
||||||
|
|
||||||
self.workspaceSplitter.addWidget(self.workspace)
|
self.workspaceSplitter.addWidget(self.workspace)
|
||||||
|
|
||||||
self.workspaceLayout.addWidget(self.workspaceSplitter)
|
self.workspaceLayout.addWidget(self.workspaceSplitter)
|
||||||
@@ -164,6 +292,9 @@ class Ui_MainWindow(object):
|
|||||||
self.editToolbar = QToolBar(MainWindow)
|
self.editToolbar = QToolBar(MainWindow)
|
||||||
self.editToolbar.setObjectName(u"editToolbar")
|
self.editToolbar.setObjectName(u"editToolbar")
|
||||||
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.editToolbar)
|
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.editToolbar)
|
||||||
|
self.transformToolbar = QToolBar(MainWindow)
|
||||||
|
self.transformToolbar.setObjectName(u"transformToolbar")
|
||||||
|
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.transformToolbar)
|
||||||
|
|
||||||
self.menubar.addAction(self.menuFile.menuAction())
|
self.menubar.addAction(self.menuFile.menuAction())
|
||||||
self.menubar.addAction(self.menuEdit.menuAction())
|
self.menubar.addAction(self.menuEdit.menuAction())
|
||||||
@@ -175,6 +306,7 @@ class Ui_MainWindow(object):
|
|||||||
self.menuFile.addAction(self.actionSave)
|
self.menuFile.addAction(self.actionSave)
|
||||||
self.menuFile.addAction(self.actionSaveAs)
|
self.menuFile.addAction(self.actionSaveAs)
|
||||||
self.menuFile.addSeparator()
|
self.menuFile.addSeparator()
|
||||||
|
self.menuFile.addAction(self.actionClose)
|
||||||
self.menuFile.addAction(self.actionExit)
|
self.menuFile.addAction(self.actionExit)
|
||||||
self.menuEdit.addAction(self.actionUndo)
|
self.menuEdit.addAction(self.actionUndo)
|
||||||
self.menuEdit.addAction(self.actionRedo)
|
self.menuEdit.addAction(self.actionRedo)
|
||||||
@@ -182,6 +314,8 @@ class Ui_MainWindow(object):
|
|||||||
self.menuEdit.addAction(self.actionCopy)
|
self.menuEdit.addAction(self.actionCopy)
|
||||||
self.menuEdit.addAction(self.actionCut)
|
self.menuEdit.addAction(self.actionCut)
|
||||||
self.menuEdit.addAction(self.actionPaste)
|
self.menuEdit.addAction(self.actionPaste)
|
||||||
|
self.menuEdit.addAction(self.actionDelete)
|
||||||
|
self.menuEdit.addAction(self.actionSelectAll)
|
||||||
self.menuEdit.addSeparator()
|
self.menuEdit.addSeparator()
|
||||||
self.menuEdit.addAction(self.actionSettings)
|
self.menuEdit.addAction(self.actionSettings)
|
||||||
self.menuView.addAction(self.menuPanels.menuAction())
|
self.menuView.addAction(self.menuPanels.menuAction())
|
||||||
@@ -197,9 +331,13 @@ class Ui_MainWindow(object):
|
|||||||
self.editToolbar.addAction(self.actionCopy)
|
self.editToolbar.addAction(self.actionCopy)
|
||||||
self.editToolbar.addAction(self.actionCut)
|
self.editToolbar.addAction(self.actionCut)
|
||||||
self.editToolbar.addAction(self.actionPaste)
|
self.editToolbar.addAction(self.actionPaste)
|
||||||
|
self.transformToolbar.addAction(self.actionRotateClockwise)
|
||||||
|
|
||||||
self.retranslateUi(MainWindow)
|
self.retranslateUi(MainWindow)
|
||||||
|
|
||||||
|
self.workspaceStack.setCurrentIndex(0)
|
||||||
|
|
||||||
|
|
||||||
QMetaObject.connectSlotsByName(MainWindow)
|
QMetaObject.connectSlotsByName(MainWindow)
|
||||||
# setupUi
|
# setupUi
|
||||||
|
|
||||||
@@ -211,6 +349,13 @@ class Ui_MainWindow(object):
|
|||||||
#endif // QT_CONFIG(statustip)
|
#endif // QT_CONFIG(statustip)
|
||||||
#if QT_CONFIG(shortcut)
|
#if QT_CONFIG(shortcut)
|
||||||
self.actionNew.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+N", None))
|
self.actionNew.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+N", None))
|
||||||
|
#endif // QT_CONFIG(shortcut)
|
||||||
|
self.actionRotateClockwise.setText(QCoreApplication.translate("MainWindow", u"Rotate Clockwise", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.actionRotateClockwise.setToolTip(QCoreApplication.translate("MainWindow", u"Rotate selected blocks clockwise by 90 degrees", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
#if QT_CONFIG(shortcut)
|
||||||
|
self.actionRotateClockwise.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+R", None))
|
||||||
#endif // QT_CONFIG(shortcut)
|
#endif // QT_CONFIG(shortcut)
|
||||||
self.actionOpen.setText(QCoreApplication.translate("MainWindow", u"&Open\u2026", None))
|
self.actionOpen.setText(QCoreApplication.translate("MainWindow", u"&Open\u2026", None))
|
||||||
#if QT_CONFIG(statustip)
|
#if QT_CONFIG(statustip)
|
||||||
@@ -233,6 +378,10 @@ class Ui_MainWindow(object):
|
|||||||
self.actionExit.setText(QCoreApplication.translate("MainWindow", u"E&xit", None))
|
self.actionExit.setText(QCoreApplication.translate("MainWindow", u"E&xit", None))
|
||||||
#if QT_CONFIG(shortcut)
|
#if QT_CONFIG(shortcut)
|
||||||
self.actionExit.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+Q", None))
|
self.actionExit.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+Q", None))
|
||||||
|
#endif // QT_CONFIG(shortcut)
|
||||||
|
self.actionClose.setText(QCoreApplication.translate("MainWindow", u"&Close Document", None))
|
||||||
|
#if QT_CONFIG(shortcut)
|
||||||
|
self.actionClose.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+W", None))
|
||||||
#endif // QT_CONFIG(shortcut)
|
#endif // QT_CONFIG(shortcut)
|
||||||
self.actionUndo.setText(QCoreApplication.translate("MainWindow", u"&Undo", None))
|
self.actionUndo.setText(QCoreApplication.translate("MainWindow", u"&Undo", None))
|
||||||
#if QT_CONFIG(shortcut)
|
#if QT_CONFIG(shortcut)
|
||||||
@@ -257,6 +406,10 @@ class Ui_MainWindow(object):
|
|||||||
self.actionSelectAll.setText(QCoreApplication.translate("MainWindow", u"Select &All", None))
|
self.actionSelectAll.setText(QCoreApplication.translate("MainWindow", u"Select &All", None))
|
||||||
#if QT_CONFIG(shortcut)
|
#if QT_CONFIG(shortcut)
|
||||||
self.actionSelectAll.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+A", None))
|
self.actionSelectAll.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+A", None))
|
||||||
|
#endif // QT_CONFIG(shortcut)
|
||||||
|
self.actionDelete.setText(QCoreApplication.translate("MainWindow", u"&Delete", None))
|
||||||
|
#if QT_CONFIG(shortcut)
|
||||||
|
self.actionDelete.setShortcut(QCoreApplication.translate("MainWindow", u"Del", None))
|
||||||
#endif // QT_CONFIG(shortcut)
|
#endif // QT_CONFIG(shortcut)
|
||||||
self.actionAbout.setText(QCoreApplication.translate("MainWindow", u"&About BEdit", None))
|
self.actionAbout.setText(QCoreApplication.translate("MainWindow", u"&About BEdit", None))
|
||||||
self.actionSettings.setText(QCoreApplication.translate("MainWindow", u"&Settings\u2026", None))
|
self.actionSettings.setText(QCoreApplication.translate("MainWindow", u"&Settings\u2026", None))
|
||||||
@@ -265,6 +418,25 @@ class Ui_MainWindow(object):
|
|||||||
#endif // QT_CONFIG(statustip)
|
#endif // QT_CONFIG(statustip)
|
||||||
self.actionAboutQt.setText(QCoreApplication.translate("MainWindow", u"About &Qt", None))
|
self.actionAboutQt.setText(QCoreApplication.translate("MainWindow", u"About &Qt", None))
|
||||||
self.panel_libraries.setWindowTitle(QCoreApplication.translate("MainWindow", u"Libraries", None))
|
self.panel_libraries.setWindowTitle(QCoreApplication.translate("MainWindow", u"Libraries", None))
|
||||||
|
self.panel_document.setWindowTitle(QCoreApplication.translate("MainWindow", u"Document", None))
|
||||||
|
self.navigateUpButton.setText(QCoreApplication.translate("MainWindow", u"Up", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.navigateUpButton.setToolTip(QCoreApplication.translate("MainWindow", u"Open the containing graph", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.graphBreadcrumbLabel.setText(QCoreApplication.translate("MainWindow", u"Untitled", None))
|
||||||
|
self.workspaceModeLabel.setText(QCoreApplication.translate("MainWindow", u"Graph", None))
|
||||||
|
self.applyJsonButton.setText(QCoreApplication.translate("MainWindow", u"Apply JSON", None))
|
||||||
|
self.pointerToolButton.setText(QCoreApplication.translate("MainWindow", u"Pointer", None))
|
||||||
|
self.inputToolButton.setText(QCoreApplication.translate("MainWindow", u"Input", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.inputToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Add an interface input", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.outputToolButton.setText(QCoreApplication.translate("MainWindow", u"Output", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.outputToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Add an interface output", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.jsonEditor.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Component JSON", None))
|
||||||
|
self.emptyWorkspaceLabel.setText(QCoreApplication.translate("MainWindow", u"No document open", None))
|
||||||
self.menuFile.setTitle(QCoreApplication.translate("MainWindow", u"&File", None))
|
self.menuFile.setTitle(QCoreApplication.translate("MainWindow", u"&File", None))
|
||||||
self.menuEdit.setTitle(QCoreApplication.translate("MainWindow", u"&Edit", None))
|
self.menuEdit.setTitle(QCoreApplication.translate("MainWindow", u"&Edit", None))
|
||||||
self.menuView.setTitle(QCoreApplication.translate("MainWindow", u"&View", None))
|
self.menuView.setTitle(QCoreApplication.translate("MainWindow", u"&View", None))
|
||||||
@@ -273,5 +445,6 @@ class Ui_MainWindow(object):
|
|||||||
self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"&Help", None))
|
self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"&Help", None))
|
||||||
self.fileToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"File", None))
|
self.fileToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"File", None))
|
||||||
self.editToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Edit", None))
|
self.editToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Edit", None))
|
||||||
|
self.transformToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Transform", None))
|
||||||
# retranslateUi
|
# retranslateUi
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
|||||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||||
from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox,
|
from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox,
|
||||||
QFormLayout, QGroupBox, QSizePolicy, QSpacerItem,
|
QFormLayout, QGroupBox, QHBoxLayout, QLabel,
|
||||||
QSpinBox, QTabWidget, QVBoxLayout, QWidget)
|
QListWidget, QListWidgetItem, QPushButton, QSizePolicy,
|
||||||
|
QSpacerItem, QSpinBox, QTabWidget, QVBoxLayout,
|
||||||
|
QWidget)
|
||||||
|
|
||||||
class Ui_SettingsDialog(object):
|
class Ui_SettingsDialog(object):
|
||||||
def setupUi(self, SettingsDialog):
|
def setupUi(self, SettingsDialog):
|
||||||
@@ -55,6 +57,46 @@ class Ui_SettingsDialog(object):
|
|||||||
self.generalLayout.addItem(self.generalSpacer)
|
self.generalLayout.addItem(self.generalSpacer)
|
||||||
|
|
||||||
self.settingsTabs.addTab(self.generalTab, "")
|
self.settingsTabs.addTab(self.generalTab, "")
|
||||||
|
self.librariesTab = QWidget()
|
||||||
|
self.librariesTab.setObjectName(u"librariesTab")
|
||||||
|
self.librariesTabLayout = QVBoxLayout(self.librariesTab)
|
||||||
|
self.librariesTabLayout.setObjectName(u"librariesTabLayout")
|
||||||
|
self.libraryPathsLabel = QLabel(self.librariesTab)
|
||||||
|
self.libraryPathsLabel.setObjectName(u"libraryPathsLabel")
|
||||||
|
self.libraryPathsLabel.setWordWrap(True)
|
||||||
|
|
||||||
|
self.librariesTabLayout.addWidget(self.libraryPathsLabel)
|
||||||
|
|
||||||
|
self.libraryPathsList = QListWidget(self.librariesTab)
|
||||||
|
self.libraryPathsList.setObjectName(u"libraryPathsList")
|
||||||
|
|
||||||
|
self.librariesTabLayout.addWidget(self.libraryPathsList)
|
||||||
|
|
||||||
|
self.libraryPathButtonsLayout = QHBoxLayout()
|
||||||
|
self.libraryPathButtonsLayout.setObjectName(u"libraryPathButtonsLayout")
|
||||||
|
self.addLibraryFileButton = QPushButton(self.librariesTab)
|
||||||
|
self.addLibraryFileButton.setObjectName(u"addLibraryFileButton")
|
||||||
|
|
||||||
|
self.libraryPathButtonsLayout.addWidget(self.addLibraryFileButton)
|
||||||
|
|
||||||
|
self.addLibraryFolderButton = QPushButton(self.librariesTab)
|
||||||
|
self.addLibraryFolderButton.setObjectName(u"addLibraryFolderButton")
|
||||||
|
|
||||||
|
self.libraryPathButtonsLayout.addWidget(self.addLibraryFolderButton)
|
||||||
|
|
||||||
|
self.removeLibraryPathButton = QPushButton(self.librariesTab)
|
||||||
|
self.removeLibraryPathButton.setObjectName(u"removeLibraryPathButton")
|
||||||
|
|
||||||
|
self.libraryPathButtonsLayout.addWidget(self.removeLibraryPathButton)
|
||||||
|
|
||||||
|
self.libraryButtonsSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
||||||
|
|
||||||
|
self.libraryPathButtonsLayout.addItem(self.libraryButtonsSpacer)
|
||||||
|
|
||||||
|
|
||||||
|
self.librariesTabLayout.addLayout(self.libraryPathButtonsLayout)
|
||||||
|
|
||||||
|
self.settingsTabs.addTab(self.librariesTab, "")
|
||||||
|
|
||||||
self.dialogLayout.addWidget(self.settingsTabs)
|
self.dialogLayout.addWidget(self.settingsTabs)
|
||||||
|
|
||||||
@@ -81,5 +123,10 @@ class Ui_SettingsDialog(object):
|
|||||||
self.autosaveGroupBox.setTitle(QCoreApplication.translate("SettingsDialog", u"Automatic saving", None))
|
self.autosaveGroupBox.setTitle(QCoreApplication.translate("SettingsDialog", u"Automatic saving", None))
|
||||||
self.autosaveIntervalSpinBox.setSuffix(QCoreApplication.translate("SettingsDialog", u" minutes", None))
|
self.autosaveIntervalSpinBox.setSuffix(QCoreApplication.translate("SettingsDialog", u" minutes", None))
|
||||||
self.settingsTabs.setTabText(self.settingsTabs.indexOf(self.generalTab), QCoreApplication.translate("SettingsDialog", u"General", None))
|
self.settingsTabs.setTabText(self.settingsTabs.indexOf(self.generalTab), QCoreApplication.translate("SettingsDialog", u"General", None))
|
||||||
|
self.libraryPathsLabel.setText(QCoreApplication.translate("SettingsDialog", u"Load library JSON files from these files or folders at startup:", None))
|
||||||
|
self.addLibraryFileButton.setText(QCoreApplication.translate("SettingsDialog", u"Add File\u2026", None))
|
||||||
|
self.addLibraryFolderButton.setText(QCoreApplication.translate("SettingsDialog", u"Add Folder\u2026", None))
|
||||||
|
self.removeLibraryPathButton.setText(QCoreApplication.translate("SettingsDialog", u"Remove", None))
|
||||||
|
self.settingsTabs.setTabText(self.settingsTabs.indexOf(self.librariesTab), QCoreApplication.translate("SettingsDialog", u"Libraries", None))
|
||||||
# retranslateUi
|
# retranslateUi
|
||||||
|
|
||||||
|
|||||||
4
BEdit/src/bedit/workspace/__init__.py
Normal file
4
BEdit/src/bedit/workspace/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from bedit.workspace.view import GraphWorkspaceView
|
||||||
|
|
||||||
|
__all__ = ["GraphWorkspaceView"]
|
||||||
|
|
||||||
547
BEdit/src/bedit/workspace/view.py
Normal file
547
BEdit/src/bedit/workspace/view.py
Normal file
@@ -0,0 +1,547 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from PySide6.QtCore import QMimeData, QPointF, QRectF, Qt, Signal
|
||||||
|
from PySide6.QtGui import (
|
||||||
|
QColor,
|
||||||
|
QDragEnterEvent,
|
||||||
|
QDropEvent,
|
||||||
|
QMouseEvent,
|
||||||
|
QPainter,
|
||||||
|
QPainterPath,
|
||||||
|
QPen,
|
||||||
|
QTransform,
|
||||||
|
)
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QGraphicsEllipseItem,
|
||||||
|
QGraphicsItem,
|
||||||
|
QGraphicsObject,
|
||||||
|
QGraphicsPathItem,
|
||||||
|
QGraphicsScene,
|
||||||
|
QGraphicsSceneContextMenuEvent,
|
||||||
|
QGraphicsSceneMouseEvent,
|
||||||
|
QGraphicsView,
|
||||||
|
QApplication,
|
||||||
|
QMenu,
|
||||||
|
QStyleOptionGraphicsItem,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
from bedit.document.controller import DocumentController
|
||||||
|
from bedit.document.model import Component, Connection, Endpoint, Port
|
||||||
|
from bedit.library.tree_model import COMPONENT_MIME_TYPE
|
||||||
|
|
||||||
|
|
||||||
|
SELECTION_MIME_TYPE = "application/x-bedit-selection"
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionPortItem(QGraphicsEllipseItem):
|
||||||
|
def __init__(self, endpoint: Endpoint, role: str, label: str, parent: QGraphicsItem) -> None:
|
||||||
|
super().__init__(-6, -6, 12, 12, parent)
|
||||||
|
self.endpoint = endpoint
|
||||||
|
self.role = role
|
||||||
|
self.setBrush(QColor("#ffffff"))
|
||||||
|
self.setPen(QPen(QColor("#303030"), 1.5))
|
||||||
|
self.setZValue(2)
|
||||||
|
self.setToolTip(label)
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentGraphicsItem(QGraphicsObject):
|
||||||
|
WIDTH = 120.0
|
||||||
|
HEIGHT = 72.0
|
||||||
|
|
||||||
|
def __init__(self, component: Component, controller: DocumentController) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.component_id = component.id
|
||||||
|
self.component = component
|
||||||
|
self.controller = controller
|
||||||
|
self.drag_start = QPointF()
|
||||||
|
self.setFlags(
|
||||||
|
QGraphicsItem.GraphicsItemFlag.ItemIsMovable
|
||||||
|
| QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
|
||||||
|
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
|
||||||
|
)
|
||||||
|
self.input_ports = self._create_ports(component.inputs, "target", 0.0)
|
||||||
|
self.output_ports = self._create_ports(component.outputs, "source", self.WIDTH)
|
||||||
|
self.setTransformOriginPoint(self.WIDTH / 2, self.HEIGHT / 2)
|
||||||
|
self.setRotation(component.rotation)
|
||||||
|
|
||||||
|
def _create_ports(self, ports, role: str, x: float) -> dict[str, ConnectionPortItem]:
|
||||||
|
result = {}
|
||||||
|
spacing = self.HEIGHT / (len(ports) + 1)
|
||||||
|
for index, port in enumerate(ports, start=1):
|
||||||
|
endpoint = Endpoint(block=self.component_id, port=port.id)
|
||||||
|
item = ConnectionPortItem(endpoint, role, port.name, self)
|
||||||
|
item.setPos(x, spacing * index)
|
||||||
|
result[port.id] = item
|
||||||
|
return result
|
||||||
|
|
||||||
|
def boundingRect(self) -> QRectF: # noqa: N802
|
||||||
|
return QRectF(0, 0, self.WIDTH, self.HEIGHT)
|
||||||
|
|
||||||
|
def paint(
|
||||||
|
self,
|
||||||
|
painter: QPainter,
|
||||||
|
option: QStyleOptionGraphicsItem,
|
||||||
|
widget: QWidget | None = None,
|
||||||
|
) -> None:
|
||||||
|
del option, widget
|
||||||
|
icon = self.component.icon
|
||||||
|
fill = QColor("#dbeafe") if self.isSelected() else QColor(icon.fill)
|
||||||
|
painter.setBrush(fill)
|
||||||
|
painter.setPen(QPen(QColor(icon.border), 1.5))
|
||||||
|
if icon.shape == "ellipse":
|
||||||
|
painter.drawEllipse(self.boundingRect())
|
||||||
|
else:
|
||||||
|
painter.drawRoundedRect(self.boundingRect(), 5, 5)
|
||||||
|
painter.setPen(QColor("#202020"))
|
||||||
|
painter.drawText(
|
||||||
|
self.boundingRect(),
|
||||||
|
Qt.AlignmentFlag.AlignCenter,
|
||||||
|
icon.text or self.component.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
def mouseDoubleClickEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||||
|
self.controller.activate_component(self.component_id)
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
|
||||||
|
if not self.isSelected():
|
||||||
|
scene = self.scene()
|
||||||
|
if scene is not None:
|
||||||
|
scene.clearSelection()
|
||||||
|
self.setSelected(True)
|
||||||
|
menu = QMenu()
|
||||||
|
options_action = menu.addAction("Component Options…")
|
||||||
|
if menu.exec(event.screenPos()) is options_action:
|
||||||
|
scene = self.scene()
|
||||||
|
if isinstance(scene, GraphScene):
|
||||||
|
scene.componentOptionsRequested.emit(self.component_id)
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||||
|
self.drag_start = self.pos()
|
||||||
|
super().mousePressEvent(event)
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||||
|
super().mouseReleaseEvent(event)
|
||||||
|
self.controller.move_component(self.component_id, self.drag_start, self.pos())
|
||||||
|
|
||||||
|
def itemChange(self, change, value): # noqa: N802
|
||||||
|
if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
||||||
|
scene = self.scene()
|
||||||
|
if isinstance(scene, GraphScene):
|
||||||
|
scene.update_connections_for_block(self.component_id)
|
||||||
|
return super().itemChange(change, value)
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceTerminalItem(QGraphicsObject):
|
||||||
|
WIDTH = 110.0
|
||||||
|
HEIGHT = 36.0
|
||||||
|
|
||||||
|
def __init__(self, port: Port, direction: str, controller: DocumentController) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.port = port
|
||||||
|
self.direction = direction
|
||||||
|
self.controller = controller
|
||||||
|
self.drag_start = QPointF()
|
||||||
|
role = "source" if direction == "input" else "target"
|
||||||
|
self.connection_port = ConnectionPortItem(
|
||||||
|
Endpoint(interface=port.id), role, port.name, self
|
||||||
|
)
|
||||||
|
connection_x = self.WIDTH if direction == "input" else 0.0
|
||||||
|
self.connection_port.setPos(connection_x, self.HEIGHT / 2)
|
||||||
|
self.setFlags(
|
||||||
|
QGraphicsItem.GraphicsItemFlag.ItemIsMovable
|
||||||
|
| QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
|
||||||
|
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
|
||||||
|
)
|
||||||
|
self.setToolTip(f"Component {direction}: {port.name}")
|
||||||
|
|
||||||
|
def boundingRect(self) -> QRectF: # noqa: N802
|
||||||
|
return QRectF(0, 0, self.WIDTH, self.HEIGHT)
|
||||||
|
|
||||||
|
def paint(
|
||||||
|
self,
|
||||||
|
painter: QPainter,
|
||||||
|
option: QStyleOptionGraphicsItem,
|
||||||
|
widget: QWidget | None = None,
|
||||||
|
) -> None:
|
||||||
|
del option, widget
|
||||||
|
painter.setBrush(QColor("#e5e7eb"))
|
||||||
|
painter.setPen(QPen(QColor("#4b5563"), 1.5))
|
||||||
|
painter.drawRoundedRect(self.boundingRect(), 4, 4)
|
||||||
|
painter.setPen(QColor("#202020"))
|
||||||
|
marker = "IN" if self.direction == "input" else "OUT"
|
||||||
|
painter.drawText(
|
||||||
|
self.boundingRect().adjusted(8, 0, -8, 0),
|
||||||
|
Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft,
|
||||||
|
f"{marker} {self.port.name}",
|
||||||
|
)
|
||||||
|
|
||||||
|
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||||
|
self.drag_start = self.pos()
|
||||||
|
super().mousePressEvent(event)
|
||||||
|
|
||||||
|
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
|
||||||
|
menu = QMenu()
|
||||||
|
options_action = menu.addAction(f"{self.direction.title()} Options…")
|
||||||
|
if menu.exec(event.screenPos()) is options_action:
|
||||||
|
scene = self.scene()
|
||||||
|
if isinstance(scene, GraphScene):
|
||||||
|
scene.portOptionsRequested.emit(self.port.id, self.direction)
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||||
|
super().mouseReleaseEvent(event)
|
||||||
|
self.controller.move_interface_port(self.port.id, self.drag_start, self.pos())
|
||||||
|
|
||||||
|
def itemChange(self, change, value): # noqa: N802
|
||||||
|
if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
||||||
|
scene = self.scene()
|
||||||
|
if isinstance(scene, GraphScene):
|
||||||
|
scene.update_connections_for_interface(self.port.id)
|
||||||
|
return super().itemChange(change, value)
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionGraphicsItem(QGraphicsPathItem):
|
||||||
|
def __init__(self, connection_id: str, name: str = "") -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.connection_id = connection_id
|
||||||
|
self.name = name
|
||||||
|
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
|
||||||
|
self._update_pen()
|
||||||
|
self.setZValue(-1)
|
||||||
|
self.setToolTip(name or "Connection")
|
||||||
|
|
||||||
|
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
|
||||||
|
menu = QMenu()
|
||||||
|
options_action = menu.addAction("Connection Options…")
|
||||||
|
if menu.exec(event.screenPos()) is options_action:
|
||||||
|
scene = self.scene()
|
||||||
|
if isinstance(scene, GraphScene):
|
||||||
|
scene.connectionOptionsRequested.emit(self.connection_id)
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def itemChange(self, change, value): # noqa: N802
|
||||||
|
result = super().itemChange(change, value)
|
||||||
|
if change == QGraphicsItem.GraphicsItemChange.ItemSelectedHasChanged:
|
||||||
|
self._update_pen()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _update_pen(self) -> None:
|
||||||
|
self.setPen(
|
||||||
|
QPen(
|
||||||
|
QColor("#f59e0b") if self.isSelected() else QColor("#285f9e"),
|
||||||
|
4.0 if self.isSelected() else 2.5,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GraphScene(QGraphicsScene):
|
||||||
|
componentOptionsRequested = Signal(str)
|
||||||
|
portOptionsRequested = Signal(str, str)
|
||||||
|
connectionOptionsRequested = Signal(str)
|
||||||
|
|
||||||
|
def __init__(self, controller: DocumentController, parent=None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.controller = controller
|
||||||
|
self.component_items: dict[str, ComponentGraphicsItem] = {}
|
||||||
|
self.input_items: dict[str, InterfaceTerminalItem] = {}
|
||||||
|
self.output_items: dict[str, InterfaceTerminalItem] = {}
|
||||||
|
self.connection_items: dict[str, ConnectionGraphicsItem] = {}
|
||||||
|
self.pending_source: ConnectionPortItem | None = None
|
||||||
|
self.setSceneRect(-2000, -2000, 4000, 4000)
|
||||||
|
|
||||||
|
controller.documentReset.connect(self.rebuild)
|
||||||
|
controller.activeGraphChanged.connect(self.rebuild)
|
||||||
|
controller.componentMoved.connect(self.set_component_position)
|
||||||
|
controller.componentRotated.connect(self.set_component_rotation)
|
||||||
|
self.rebuild()
|
||||||
|
|
||||||
|
def rebuild(self) -> None:
|
||||||
|
self.clear()
|
||||||
|
self.component_items.clear()
|
||||||
|
self.input_items.clear()
|
||||||
|
self.output_items.clear()
|
||||||
|
self.connection_items.clear()
|
||||||
|
self.pending_source = None
|
||||||
|
owner = self.controller.active_component
|
||||||
|
if owner is None or owner.implementation_kind != "graph":
|
||||||
|
return
|
||||||
|
for port in owner.inputs:
|
||||||
|
item = InterfaceTerminalItem(port, "input", self.controller)
|
||||||
|
self.addItem(item)
|
||||||
|
item.setPos(port.x, port.y)
|
||||||
|
self.input_items[port.id] = item
|
||||||
|
for port in owner.outputs:
|
||||||
|
item = InterfaceTerminalItem(port, "output", self.controller)
|
||||||
|
self.addItem(item)
|
||||||
|
item.setPos(port.x, port.y)
|
||||||
|
self.output_items[port.id] = item
|
||||||
|
for component in owner.graph.blocks.values():
|
||||||
|
item = ComponentGraphicsItem(component, self.controller)
|
||||||
|
self.addItem(item)
|
||||||
|
item.setPos(component.x, component.y)
|
||||||
|
self.component_items[component.id] = item
|
||||||
|
for connection in owner.graph.connections.values():
|
||||||
|
item = ConnectionGraphicsItem(connection.id, connection.name)
|
||||||
|
self.addItem(item)
|
||||||
|
self.connection_items[connection.id] = item
|
||||||
|
self.update_connection(connection.id)
|
||||||
|
|
||||||
|
def set_component_position(self, component_id: str, position: QPointF) -> None:
|
||||||
|
item = self.component_items.get(component_id)
|
||||||
|
if item is not None and item.pos() != position:
|
||||||
|
item.setPos(position)
|
||||||
|
|
||||||
|
def set_component_rotation(self, component_id: str, rotation: float) -> None:
|
||||||
|
item = self.component_items.get(component_id)
|
||||||
|
if item is not None:
|
||||||
|
item.setRotation(rotation)
|
||||||
|
self.update_connections_for_block(component_id)
|
||||||
|
|
||||||
|
def update_connections_for_block(self, component_id: str) -> None:
|
||||||
|
for connection in self.controller.active_graph.connections.values():
|
||||||
|
if component_id in (connection.source.block, connection.target.block):
|
||||||
|
self.update_connection(connection.id)
|
||||||
|
|
||||||
|
def update_connections_for_interface(self, port_id: str) -> None:
|
||||||
|
for connection in self.controller.active_graph.connections.values():
|
||||||
|
if port_id in (connection.source.interface, connection.target.interface):
|
||||||
|
self.update_connection(connection.id)
|
||||||
|
|
||||||
|
def drawBackground(self, painter: QPainter, rect: QRectF) -> None: # noqa: N802
|
||||||
|
painter.fillRect(rect, QColor("#e4e4e4"))
|
||||||
|
if self.controller.document is None or self.controller.active_component is None:
|
||||||
|
return
|
||||||
|
spacing = 32
|
||||||
|
left = int(rect.left()) - (int(rect.left()) % spacing)
|
||||||
|
top = int(rect.top()) - (int(rect.top()) % spacing)
|
||||||
|
painter.setPen(QPen(QColor("#b8b8b8"), 1))
|
||||||
|
for x in range(left, int(rect.right()) + spacing, spacing):
|
||||||
|
for y in range(top, int(rect.bottom()) + spacing, spacing):
|
||||||
|
painter.drawPoint(x, y)
|
||||||
|
|
||||||
|
def _endpoint_item(self, endpoint: Endpoint, role: str) -> ConnectionPortItem | None:
|
||||||
|
if endpoint.interface is not None:
|
||||||
|
terminals = self.input_items if role == "source" else self.output_items
|
||||||
|
terminal = terminals.get(endpoint.interface)
|
||||||
|
return terminal.connection_port if terminal else None
|
||||||
|
component = self.component_items.get(endpoint.block or "")
|
||||||
|
if component is None:
|
||||||
|
return None
|
||||||
|
ports = component.output_ports if role == "source" else component.input_ports
|
||||||
|
return ports.get(endpoint.port or "")
|
||||||
|
|
||||||
|
def update_connection(self, connection_id: str) -> None:
|
||||||
|
connection = self.controller.active_graph.connections.get(connection_id)
|
||||||
|
graphics = self.connection_items.get(connection_id)
|
||||||
|
if connection is None or graphics is None:
|
||||||
|
return
|
||||||
|
source = self._endpoint_item(connection.source, "source")
|
||||||
|
target = self._endpoint_item(connection.target, "target")
|
||||||
|
if source is None or target is None:
|
||||||
|
return
|
||||||
|
start, end = source.scenePos(), target.scenePos()
|
||||||
|
distance = max(50.0, abs(end.x() - start.x()) * 0.5)
|
||||||
|
path = QPainterPath(start)
|
||||||
|
path.cubicTo(start + QPointF(distance, 0), end - QPointF(distance, 0), end)
|
||||||
|
graphics.setPath(path)
|
||||||
|
|
||||||
|
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||||
|
item = self.itemAt(event.scenePos(), QTransform())
|
||||||
|
if isinstance(item, ConnectionPortItem):
|
||||||
|
if item.role == "source":
|
||||||
|
self._clear_pending_source()
|
||||||
|
self.pending_source = item
|
||||||
|
item.setBrush(QColor("#f5b642"))
|
||||||
|
elif self.pending_source is not None:
|
||||||
|
if self.pending_source.endpoint != item.endpoint:
|
||||||
|
self.controller.connect(self.pending_source.endpoint, item.endpoint)
|
||||||
|
self._clear_pending_source()
|
||||||
|
event.accept()
|
||||||
|
return
|
||||||
|
self._clear_pending_source()
|
||||||
|
super().mousePressEvent(event)
|
||||||
|
|
||||||
|
def _clear_pending_source(self) -> None:
|
||||||
|
if self.pending_source is not None:
|
||||||
|
self.pending_source.setBrush(QColor("#ffffff"))
|
||||||
|
self.pending_source = None
|
||||||
|
|
||||||
|
|
||||||
|
class GraphWorkspaceView(QGraphicsView):
|
||||||
|
toolUsed = Signal()
|
||||||
|
componentOptionsRequested = Signal(str)
|
||||||
|
portOptionsRequested = Signal(str, str)
|
||||||
|
connectionOptionsRequested = Signal(str)
|
||||||
|
selectionAvailabilityChanged = Signal(bool)
|
||||||
|
|
||||||
|
def __init__(self, parent=None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.controller: DocumentController | None = None
|
||||||
|
self.tool_mode = "pointer"
|
||||||
|
self.paste_count = 0
|
||||||
|
self.setAcceptDrops(True)
|
||||||
|
self.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||||
|
self.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
|
||||||
|
self.setBackgroundBrush(QColor("#9a9a9a"))
|
||||||
|
|
||||||
|
def set_model(self, controller: DocumentController) -> None:
|
||||||
|
self.controller = controller
|
||||||
|
scene = GraphScene(controller, self)
|
||||||
|
scene.componentOptionsRequested.connect(self.componentOptionsRequested)
|
||||||
|
scene.portOptionsRequested.connect(self.portOptionsRequested)
|
||||||
|
scene.connectionOptionsRequested.connect(self.connectionOptionsRequested)
|
||||||
|
scene.selectionChanged.connect(
|
||||||
|
lambda: self.selectionAvailabilityChanged.emit(bool(scene.selectedItems()))
|
||||||
|
)
|
||||||
|
self.setScene(scene)
|
||||||
|
|
||||||
|
def select_all(self) -> None:
|
||||||
|
scene = self.scene()
|
||||||
|
if scene is None:
|
||||||
|
return
|
||||||
|
for item in scene.items():
|
||||||
|
if item.flags() & QGraphicsItem.GraphicsItemFlag.ItemIsSelectable:
|
||||||
|
item.setSelected(True)
|
||||||
|
|
||||||
|
def delete_selected(self) -> None:
|
||||||
|
if self.controller is None or not isinstance(self.scene(), GraphScene):
|
||||||
|
return
|
||||||
|
blocks: set[str] = set()
|
||||||
|
connections: set[str] = set()
|
||||||
|
inputs: set[str] = set()
|
||||||
|
outputs: set[str] = set()
|
||||||
|
for item in self.scene().selectedItems():
|
||||||
|
if isinstance(item, ComponentGraphicsItem):
|
||||||
|
blocks.add(item.component_id)
|
||||||
|
elif isinstance(item, ConnectionGraphicsItem):
|
||||||
|
connections.add(item.connection_id)
|
||||||
|
elif isinstance(item, InterfaceTerminalItem):
|
||||||
|
(inputs if item.direction == "input" else outputs).add(item.port.id)
|
||||||
|
self.controller.delete_selection(blocks, connections, inputs, outputs)
|
||||||
|
|
||||||
|
def has_selected_components(self) -> bool:
|
||||||
|
scene = self.scene()
|
||||||
|
return bool(
|
||||||
|
scene
|
||||||
|
and any(isinstance(item, ComponentGraphicsItem) for item in scene.selectedItems())
|
||||||
|
)
|
||||||
|
|
||||||
|
def rotate_selected(self) -> None:
|
||||||
|
if self.controller is None or self.scene() is None:
|
||||||
|
return
|
||||||
|
component_ids = {
|
||||||
|
item.component_id
|
||||||
|
for item in self.scene().selectedItems()
|
||||||
|
if isinstance(item, ComponentGraphicsItem)
|
||||||
|
}
|
||||||
|
self.controller.rotate_components(component_ids)
|
||||||
|
|
||||||
|
def copy_selection(self) -> bool:
|
||||||
|
if self.controller is None or not isinstance(self.scene(), GraphScene):
|
||||||
|
return False
|
||||||
|
selected_ids = {
|
||||||
|
item.component_id
|
||||||
|
for item in self.scene().selectedItems()
|
||||||
|
if isinstance(item, ComponentGraphicsItem)
|
||||||
|
}
|
||||||
|
if not selected_ids:
|
||||||
|
return False
|
||||||
|
graph = self.controller.active_graph
|
||||||
|
components = [graph.blocks[component_id].to_dict() for component_id in selected_ids]
|
||||||
|
connections = [
|
||||||
|
connection.to_dict()
|
||||||
|
for connection in graph.connections.values()
|
||||||
|
if connection.source.block in selected_ids and connection.target.block in selected_ids
|
||||||
|
]
|
||||||
|
mime_data = QMimeData()
|
||||||
|
mime_data.setData(
|
||||||
|
SELECTION_MIME_TYPE,
|
||||||
|
json.dumps({"components": components, "connections": connections}).encode("utf-8"),
|
||||||
|
)
|
||||||
|
QApplication.clipboard().setMimeData(mime_data)
|
||||||
|
self.paste_count = 0
|
||||||
|
return True
|
||||||
|
|
||||||
|
def cut_selection(self) -> None:
|
||||||
|
if self.copy_selection():
|
||||||
|
self.delete_selected()
|
||||||
|
|
||||||
|
def paste_selection(self) -> None:
|
||||||
|
if self.controller is None:
|
||||||
|
return
|
||||||
|
mime_data = QApplication.clipboard().mimeData()
|
||||||
|
if not mime_data.hasFormat(SELECTION_MIME_TYPE):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
payload = json.loads(bytes(mime_data.data(SELECTION_MIME_TYPE)).decode("utf-8"))
|
||||||
|
components = [Component.from_dict(item) for item in payload.get("components", [])]
|
||||||
|
connections = [Connection.from_dict(item) for item in payload.get("connections", [])]
|
||||||
|
except (KeyError, TypeError, ValueError, json.JSONDecodeError):
|
||||||
|
return
|
||||||
|
self.paste_count += 1
|
||||||
|
new_ids = self.controller.paste_selection(
|
||||||
|
components,
|
||||||
|
connections,
|
||||||
|
QPointF(32 * self.paste_count, 32 * self.paste_count),
|
||||||
|
)
|
||||||
|
scene = self.scene()
|
||||||
|
if isinstance(scene, GraphScene):
|
||||||
|
scene.clearSelection()
|
||||||
|
for component_id in new_ids:
|
||||||
|
item = scene.component_items.get(component_id)
|
||||||
|
if item is not None:
|
||||||
|
item.setSelected(True)
|
||||||
|
|
||||||
|
def set_tool_mode(self, mode: str) -> None:
|
||||||
|
self.tool_mode = mode
|
||||||
|
self.setDragMode(
|
||||||
|
QGraphicsView.DragMode.RubberBandDrag
|
||||||
|
if mode == "pointer"
|
||||||
|
else QGraphicsView.DragMode.NoDrag
|
||||||
|
)
|
||||||
|
|
||||||
|
def mousePressEvent(self, event: QMouseEvent) -> None: # noqa: N802
|
||||||
|
if (
|
||||||
|
self.controller is not None
|
||||||
|
and self.tool_mode in {"input", "output"}
|
||||||
|
and event.button() == Qt.MouseButton.LeftButton
|
||||||
|
):
|
||||||
|
self.controller.add_interface_port(self.tool_mode, self.mapToScene(event.position().toPoint()))
|
||||||
|
self.toolUsed.emit()
|
||||||
|
event.accept()
|
||||||
|
return
|
||||||
|
super().mousePressEvent(event)
|
||||||
|
|
||||||
|
def dragEnterEvent(self, event: QDragEnterEvent) -> None: # noqa: N802
|
||||||
|
if (
|
||||||
|
event.mimeData().hasFormat(COMPONENT_MIME_TYPE)
|
||||||
|
and self.controller is not None
|
||||||
|
and self.controller.active_component is not None
|
||||||
|
and self.controller.active_component.implementation_kind == "graph"
|
||||||
|
):
|
||||||
|
event.acceptProposedAction()
|
||||||
|
return
|
||||||
|
super().dragEnterEvent(event)
|
||||||
|
|
||||||
|
def dragMoveEvent(self, event) -> None: # noqa: N802
|
||||||
|
if (
|
||||||
|
event.mimeData().hasFormat(COMPONENT_MIME_TYPE)
|
||||||
|
and self.controller is not None
|
||||||
|
and self.controller.active_component is not None
|
||||||
|
and self.controller.active_component.implementation_kind == "graph"
|
||||||
|
):
|
||||||
|
event.acceptProposedAction()
|
||||||
|
return
|
||||||
|
super().dragMoveEvent(event)
|
||||||
|
|
||||||
|
def dropEvent(self, event: QDropEvent) -> None: # noqa: N802
|
||||||
|
if self.controller is None or not event.mimeData().hasFormat(COMPONENT_MIME_TYPE):
|
||||||
|
super().dropEvent(event)
|
||||||
|
return
|
||||||
|
data = json.loads(bytes(event.mimeData().data(COMPONENT_MIME_TYPE)).decode("utf-8"))
|
||||||
|
source = Component.from_dict(data)
|
||||||
|
self.controller.add_component_copy(source, self.mapToScene(event.position().toPoint()))
|
||||||
|
event.acceptProposedAction()
|
||||||
32
BEdit/ui/component_options_dialog.ui
Normal file
32
BEdit/ui/component_options_dialog.ui
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>ComponentOptionsDialog</class>
|
||||||
|
<widget class="QDialog" name="ComponentOptionsDialog">
|
||||||
|
<property name="geometry"><rect><x>0</x><y>0</y><width>420</width><height>260</height></rect></property>
|
||||||
|
<property name="windowTitle"><string>Component Options</string></property>
|
||||||
|
<layout class="QVBoxLayout" name="dialogLayout">
|
||||||
|
<item>
|
||||||
|
<layout class="QFormLayout" name="optionsForm">
|
||||||
|
<item row="0" column="0"><widget class="QLabel" name="nameLabel"><property name="text"><string>Name:</string></property></widget></item>
|
||||||
|
<item row="0" column="1"><widget class="QLineEdit" name="nameEdit"/></item>
|
||||||
|
<item row="1" column="0"><widget class="QLabel" name="shapeLabel"><property name="text"><string>Icon shape:</string></property></widget></item>
|
||||||
|
<item row="1" column="1"><widget class="QComboBox" name="shapeCombo"><item><property name="text"><string>rectangle</string></property></item><item><property name="text"><string>ellipse</string></property></item></widget></item>
|
||||||
|
<item row="2" column="0"><widget class="QLabel" name="iconTextLabel"><property name="text"><string>Icon text:</string></property></widget></item>
|
||||||
|
<item row="2" column="1"><widget class="QLineEdit" name="iconTextEdit"/></item>
|
||||||
|
<item row="3" column="0"><widget class="QLabel" name="fillLabel"><property name="text"><string>Fill color:</string></property></widget></item>
|
||||||
|
<item row="3" column="1"><widget class="QLineEdit" name="fillEdit"><property name="placeholderText"><string>#dbeafe</string></property></widget></item>
|
||||||
|
<item row="4" column="0"><widget class="QLabel" name="borderLabel"><property name="text"><string>Border color:</string></property></widget></item>
|
||||||
|
<item row="4" column="1"><widget class="QLineEdit" name="borderEdit"><property name="placeholderText"><string>#303030</string></property></widget></item>
|
||||||
|
<item row="5" column="0" colspan="2"><widget class="QCheckBox" name="showSubtreeCheckBox"><property name="text"><string>Show contained components in the Libraries tree</string></property><property name="checked"><bool>true</bool></property></widget></item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item><spacer name="optionsSpacer"><property name="orientation"><enum>Qt::Orientation::Vertical</enum></property><property name="sizeHint" stdset="0"><size><width>20</width><height>40</height></size></property></spacer></item>
|
||||||
|
<item><widget class="QDialogButtonBox" name="buttonBox"><property name="standardButtons"><set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set></property></widget></item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections>
|
||||||
|
<connection><sender>buttonBox</sender><signal>accepted()</signal><receiver>ComponentOptionsDialog</receiver><slot>accept()</slot><hints/></connection>
|
||||||
|
<connection><sender>buttonBox</sender><signal>rejected()</signal><receiver>ComponentOptionsDialog</receiver><slot>reject()</slot><hints/></connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
||||||
@@ -94,19 +94,91 @@
|
|||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</widget>
|
</widget>
|
||||||
|
<widget class="QDockWidget" name="panel_document">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size><width>220</width><height>91</height></size>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle"><string>Document</string></property>
|
||||||
|
<attribute name="dockWidgetArea"><number>1</number></attribute>
|
||||||
|
<widget class="QWidget" name="documentDockContents">
|
||||||
|
<layout class="QVBoxLayout" name="documentPanelLayout">
|
||||||
|
<property name="spacing"><number>0</number></property>
|
||||||
|
<property name="leftMargin"><number>0</number></property>
|
||||||
|
<property name="topMargin"><number>0</number></property>
|
||||||
|
<property name="rightMargin"><number>0</number></property>
|
||||||
|
<property name="bottomMargin"><number>0</number></property>
|
||||||
|
<item>
|
||||||
|
<widget class="QTreeView" name="documentTreeView">
|
||||||
|
<property name="alternatingRowColors"><bool>true</bool></property>
|
||||||
|
<property name="uniformRowHeights"><bool>true</bool></property>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QWidget" name="workspace" native="true">
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
<widget class="QWidget" name="workspace">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||||
<horstretch>1</horstretch>
|
<horstretch>1</horstretch>
|
||||||
<verstretch>0</verstretch>
|
<verstretch>0</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
</property>
|
</property>
|
||||||
<property name="styleSheet">
|
<layout class="QVBoxLayout" name="workspaceEditorLayout">
|
||||||
<string notr="true">QWidget#workspace {
|
<property name="spacing"><number>0</number></property>
|
||||||
background-color: rgb(198, 198, 198);
|
<property name="leftMargin"><number>0</number></property>
|
||||||
}</string>
|
<property name="topMargin"><number>0</number></property>
|
||||||
</property>
|
<property name="rightMargin"><number>0</number></property>
|
||||||
|
<property name="bottomMargin"><number>0</number></property>
|
||||||
|
<item>
|
||||||
|
<widget class="QFrame" name="workspaceHeader">
|
||||||
|
<property name="minimumSize"><size><width>0</width><height>34</height></size></property>
|
||||||
|
<property name="maximumSize"><size><width>16777215</width><height>34</height></size></property>
|
||||||
|
<property name="frameShape"><enum>QFrame::Shape::StyledPanel</enum></property>
|
||||||
|
<layout class="QHBoxLayout" name="workspaceHeaderLayout">
|
||||||
|
<property name="leftMargin"><number>6</number></property>
|
||||||
|
<property name="topMargin"><number>2</number></property>
|
||||||
|
<property name="rightMargin"><number>6</number></property>
|
||||||
|
<property name="bottomMargin"><number>2</number></property>
|
||||||
|
<item><widget class="QToolButton" name="navigateUpButton"><property name="text"><string>Up</string></property><property name="toolTip"><string>Open the containing graph</string></property></widget></item>
|
||||||
|
<item><widget class="QLabel" name="graphBreadcrumbLabel"><property name="text"><string>Untitled</string></property></widget></item>
|
||||||
|
<item><widget class="QLabel" name="workspaceModeLabel"><property name="text"><string>Graph</string></property></widget></item>
|
||||||
|
<item><spacer name="workspaceHeaderSpacer"><property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item>
|
||||||
|
<item><widget class="QPushButton" name="applyJsonButton"><property name="text"><string>Apply JSON</string></property><property name="visible"><bool>false</bool></property></widget></item>
|
||||||
|
<item><widget class="QToolButton" name="pointerToolButton"><property name="text"><string>Pointer</string></property><property name="checkable"><bool>true</bool></property><property name="checked"><bool>true</bool></property><property name="autoExclusive"><bool>true</bool></property></widget></item>
|
||||||
|
<item><widget class="QToolButton" name="inputToolButton"><property name="text"><string>Input</string></property><property name="toolTip"><string>Add an interface input</string></property><property name="checkable"><bool>true</bool></property><property name="autoExclusive"><bool>true</bool></property></widget></item>
|
||||||
|
<item><widget class="QToolButton" name="outputToolButton"><property name="text"><string>Output</string></property><property name="toolTip"><string>Add an interface output</string></property><property name="checkable"><bool>true</bool></property><property name="autoExclusive"><bool>true</bool></property></widget></item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QStackedWidget" name="workspaceStack">
|
||||||
|
<property name="currentIndex"><number>0</number></property>
|
||||||
|
<widget class="QWidget" name="graphPage">
|
||||||
|
<layout class="QVBoxLayout" name="graphPageLayout">
|
||||||
|
<property name="leftMargin"><number>0</number></property><property name="topMargin"><number>0</number></property><property name="rightMargin"><number>0</number></property><property name="bottomMargin"><number>0</number></property>
|
||||||
|
<item><widget class="GraphWorkspaceView" name="graphView"/></item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<widget class="QWidget" name="jsonPage">
|
||||||
|
<layout class="QVBoxLayout" name="jsonPageLayout">
|
||||||
|
<property name="leftMargin"><number>0</number></property><property name="topMargin"><number>0</number></property><property name="rightMargin"><number>0</number></property><property name="bottomMargin"><number>0</number></property>
|
||||||
|
<item><widget class="QPlainTextEdit" name="jsonEditor"><property name="lineWrapMode"><enum>QPlainTextEdit::LineWrapMode::NoWrap</enum></property><property name="placeholderText"><string>Component JSON</string></property></widget></item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<widget class="QWidget" name="emptyPage">
|
||||||
|
<layout class="QVBoxLayout" name="emptyPageLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="emptyWorkspaceLabel">
|
||||||
|
<property name="text"><string>No document open</string></property>
|
||||||
|
<property name="alignment"><set>Qt::AlignmentFlag::AlignCenter</set></property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -131,6 +203,7 @@
|
|||||||
<addaction name="actionSave"/>
|
<addaction name="actionSave"/>
|
||||||
<addaction name="actionSaveAs"/>
|
<addaction name="actionSaveAs"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
|
<addaction name="actionClose"/>
|
||||||
<addaction name="actionExit"/>
|
<addaction name="actionExit"/>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QMenu" name="menuEdit">
|
<widget class="QMenu" name="menuEdit">
|
||||||
@@ -143,6 +216,8 @@
|
|||||||
<addaction name="actionCopy"/>
|
<addaction name="actionCopy"/>
|
||||||
<addaction name="actionCut"/>
|
<addaction name="actionCut"/>
|
||||||
<addaction name="actionPaste"/>
|
<addaction name="actionPaste"/>
|
||||||
|
<addaction name="actionDelete"/>
|
||||||
|
<addaction name="actionSelectAll"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="actionSettings"/>
|
<addaction name="actionSettings"/>
|
||||||
</widget>
|
</widget>
|
||||||
@@ -230,6 +305,12 @@
|
|||||||
<addaction name="actionCut"/>
|
<addaction name="actionCut"/>
|
||||||
<addaction name="actionPaste"/>
|
<addaction name="actionPaste"/>
|
||||||
</widget>
|
</widget>
|
||||||
|
<widget class="QToolBar" name="transformToolbar">
|
||||||
|
<property name="windowTitle"><string>Transform</string></property>
|
||||||
|
<attribute name="toolBarArea"><enum>TopToolBarArea</enum></attribute>
|
||||||
|
<attribute name="toolBarBreak"><bool>false</bool></attribute>
|
||||||
|
<addaction name="actionRotateClockwise"/>
|
||||||
|
</widget>
|
||||||
<action name="actionNew">
|
<action name="actionNew">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="../resources/resources.qrc">
|
<iconset resource="../resources/resources.qrc">
|
||||||
@@ -245,6 +326,15 @@
|
|||||||
<string>Ctrl+N</string>
|
<string>Ctrl+N</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="actionRotateClockwise">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../resources/resources.qrc">
|
||||||
|
<normaloff>:/icons/icons/transform-rotate.png</normaloff>:/icons/icons/transform-rotate.png</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="text"><string>Rotate Clockwise</string></property>
|
||||||
|
<property name="toolTip"><string>Rotate selected blocks clockwise by 90 degrees</string></property>
|
||||||
|
<property name="shortcut"><string>Ctrl+R</string></property>
|
||||||
|
</action>
|
||||||
<action name="actionOpen">
|
<action name="actionOpen">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="../resources/resources.qrc">
|
<iconset resource="../resources/resources.qrc">
|
||||||
@@ -295,6 +385,10 @@
|
|||||||
<string>Ctrl+Q</string>
|
<string>Ctrl+Q</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="actionClose">
|
||||||
|
<property name="text"><string>&Close Document</string></property>
|
||||||
|
<property name="shortcut"><string>Ctrl+W</string></property>
|
||||||
|
</action>
|
||||||
<action name="actionUndo">
|
<action name="actionUndo">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="../resources/resources.qrc">
|
<iconset resource="../resources/resources.qrc">
|
||||||
@@ -363,6 +457,10 @@
|
|||||||
<string>Ctrl+A</string>
|
<string>Ctrl+A</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="actionDelete">
|
||||||
|
<property name="text"><string>&Delete</string></property>
|
||||||
|
<property name="shortcut"><string>Del</string></property>
|
||||||
|
</action>
|
||||||
<action name="actionAbout">
|
<action name="actionAbout">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&About BEdit</string>
|
<string>&About BEdit</string>
|
||||||
@@ -382,6 +480,13 @@
|
|||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>GraphWorkspaceView</class>
|
||||||
|
<extends>QGraphicsView</extends>
|
||||||
|
<header>bedit.workspace.view</header>
|
||||||
|
</customwidget>
|
||||||
|
</customwidgets>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="../resources/resources.qrc"/>
|
<include location="../resources/resources.qrc"/>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -73,6 +73,51 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
|
<widget class="QWidget" name="librariesTab">
|
||||||
|
<attribute name="title">
|
||||||
|
<string>Libraries</string>
|
||||||
|
</attribute>
|
||||||
|
<layout class="QVBoxLayout" name="librariesTabLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="libraryPathsLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Load library JSON files from these files or folders at startup:</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QListWidget" name="libraryPathsList"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="libraryPathButtonsLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="addLibraryFileButton">
|
||||||
|
<property name="text"><string>Add File…</string></property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="addLibraryFolderButton">
|
||||||
|
<property name="text"><string>Add Folder…</string></property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="removeLibraryPathButton">
|
||||||
|
<property name="text"><string>Remove</string></property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="libraryButtonsSpacer">
|
||||||
|
<property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property>
|
||||||
|
<property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
|
|||||||
Reference in New Issue
Block a user