Compare commits
2 Commits
da69acf919
...
76bd313f79
| Author | SHA1 | Date | |
|---|---|---|---|
| 76bd313f79 | |||
| c5ab3329ae |
10
BEdit/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.egg-info/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
.pytest_cache/
|
||||||
|
.idea/
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/tasks.json
|
||||||
136
BEdit/.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "Qt: Open Main Window in Designer",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "pyside6-designer",
|
||||||
|
"args": [
|
||||||
|
"${workspaceFolder}/ui/main_window.ui"
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"env": {
|
||||||
|
"QT_QPA_PLATFORMTHEME" :"qt6ct",
|
||||||
|
"QT_QPA_PLATFORM": "xcb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"problemMatcher": [],
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "dedicated"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Qt: Open Settings Dialog in Designer",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "pyside6-designer",
|
||||||
|
"args": [
|
||||||
|
"${workspaceFolder}/ui/settings_dialog.ui"
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"env": {
|
||||||
|
"QT_QPA_PLATFORMTHEME": "qt6ct",
|
||||||
|
"QT_QPA_PLATFORM": "xcb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"problemMatcher": [],
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "dedicated"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Qt: Compile Resources to Python",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "pyside6-rcc",
|
||||||
|
"args": [
|
||||||
|
"${workspaceFolder}/resources/resources.qrc",
|
||||||
|
"-o",
|
||||||
|
"${workspaceFolder}/src/bedit/resources_rc.py"
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
"problemMatcher": [],
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "silent",
|
||||||
|
"panel": "shared",
|
||||||
|
"clear": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Qt: Compile UI to Python",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "pyside6-uic",
|
||||||
|
"args": [
|
||||||
|
"--from-imports",
|
||||||
|
"${workspaceFolder}/ui/main_window.ui",
|
||||||
|
"-o",
|
||||||
|
"${workspaceFolder}/src/bedit/ui_main_window.py"
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
"problemMatcher": [],
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "silent",
|
||||||
|
"panel": "shared"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Qt: Compile Settings UI to Python",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "pyside6-uic",
|
||||||
|
"args": [
|
||||||
|
"--from-imports",
|
||||||
|
"${workspaceFolder}/ui/settings_dialog.ui",
|
||||||
|
"-o",
|
||||||
|
"${workspaceFolder}/src/bedit/ui_settings_dialog.py"
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
"problemMatcher": [],
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "silent",
|
||||||
|
"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",
|
||||||
|
"dependsOrder": "sequence",
|
||||||
|
"dependsOn": [
|
||||||
|
"Qt: Compile Resources to Python",
|
||||||
|
"Qt: Compile UI to Python",
|
||||||
|
"Qt: Compile Settings UI to Python",
|
||||||
|
"Qt: Compile Component Options UI to Python"
|
||||||
|
],
|
||||||
|
"problemMatcher": [],
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "silent",
|
||||||
|
"panel": "shared",
|
||||||
|
"clear": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
177
BEdit/README.md
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
# BEdit Qt starter
|
||||||
|
|
||||||
|
A small desktop application scaffold using Python and PySide6 (the official Qt
|
||||||
|
bindings). Its interface is maintained in Qt Designer and it includes a main
|
||||||
|
window, menu bar, blank central workspace, common keyboard shortcuts, and
|
||||||
|
persisted window geometry. The application forces Qt's light Fusion palette, so
|
||||||
|
it remains light even when the operating-system theme is dark.
|
||||||
|
|
||||||
|
## Run it
|
||||||
|
|
||||||
|
Python 3.10 or newer is required. From this directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
```
|
||||||
|
|
||||||
|
Activate the environment:
|
||||||
|
|
||||||
|
- Windows PowerShell: `.venv\Scripts\Activate.ps1`
|
||||||
|
- Windows Command Prompt: `.venv\Scripts\activate.bat`
|
||||||
|
- Linux/macOS: `source .venv/bin/activate`
|
||||||
|
|
||||||
|
Then install and launch:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install -e ".[dev]"
|
||||||
|
python -m bedit
|
||||||
|
```
|
||||||
|
|
||||||
|
After installation, the `bedit` command also launches the application.
|
||||||
|
|
||||||
|
## Project layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
.
|
||||||
|
├── pyproject.toml dependencies, package metadata, and `bedit` command
|
||||||
|
├── README.md
|
||||||
|
├── ui/main_window.ui editable Qt Designer source
|
||||||
|
└── src/bedit
|
||||||
|
├── __main__.py supports `python -m bedit`
|
||||||
|
├── app.py starts Qt and applies the forced light palette
|
||||||
|
├── main_window.py behavior and signal connections
|
||||||
|
└── ui_main_window.py generated from the Designer file; do not hand-edit
|
||||||
|
```
|
||||||
|
|
||||||
|
## Designing the UI further
|
||||||
|
|
||||||
|
The project now uses Qt Designer. It is included with PySide6 on most
|
||||||
|
installations. Open the existing form with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pyside6-designer ui/main_window.ui
|
||||||
|
```
|
||||||
|
|
||||||
|
In Designer, use the Widget Box to add controls, the Object Inspector to select
|
||||||
|
them, and the Property Editor to name and configure them. Always put widgets in
|
||||||
|
a layout (horizontal, vertical, grid, or form) so the window resizes correctly.
|
||||||
|
|
||||||
|
Save the form, close the running application if necessary, then regenerate its
|
||||||
|
Python wrapper:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
it. Add behavior and signal connections in `main_window.py`. Widget names from
|
||||||
|
Designer are available there through `self.ui`, such as `self.ui.graphView`.
|
||||||
|
|
||||||
|
In VS Code, the same commands are available through **Terminal → Run Task**:
|
||||||
|
|
||||||
|
- **Qt: Open Main Window in Designer** opens the form for visual editing.
|
||||||
|
- **Qt: Build Designer Files** compiles resources and then regenerates the UI
|
||||||
|
wrapper. It is the default build task, available with `Ctrl+Shift+B`.
|
||||||
|
- The separate resource and UI compilation tasks remain available when only one
|
||||||
|
generated file needs rebuilding.
|
||||||
|
|
||||||
|
## A sensible next design pass
|
||||||
|
|
||||||
|
1. Sketch the main tasks and screens before choosing widgets.
|
||||||
|
2. Turn each major area into its own widget class in `src/bedit/widgets/`.
|
||||||
|
3. Use a `QStackedWidget` for page-like navigation, or `QDockWidget` for movable
|
||||||
|
tool panels in an editor-style application.
|
||||||
|
4. Use reusable `QAction` objects for menu commands and any future toolbars.
|
||||||
|
5. Keep file/data operations outside widget classes as the application grows.
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
You do not need another GUI framework. Useful additions are:
|
||||||
|
|
||||||
|
- **Qt Designer** for drag-and-drop form layout.
|
||||||
|
- **Ruff** for formatting/linting (`ruff check .`).
|
||||||
|
- **pytest-qt** later for GUI interaction tests.
|
||||||
|
- **PyInstaller** or **Nuitka** later to produce a Windows `.exe`.
|
||||||
|
|
||||||
|
The light colors are defined in `apply_light_theme()` in `src/bedit/app.py`.
|
||||||
|
Adjust that palette if you want different light colors. Avoid a large stylesheet
|
||||||
|
unless the application needs highly customized controls; palettes preserve more
|
||||||
|
of Qt's standard behavior.
|
||||||
32
BEdit/pyproject.toml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=69"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "bedit"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A starter Qt desktop application"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = [
|
||||||
|
"PySide6>=6.7,<7",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8",
|
||||||
|
"ruff>=0.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.gui-scripts]
|
||||||
|
bedit = "bedit.app:main"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
|
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
bedit = ["data/libraries/*.json"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 100
|
||||||
|
target-version = "py310"
|
||||||
24
BEdit/resources/icons/AUTHORS
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
Oxygen Icon Theme has been developed by The Oxygen Team.
|
||||||
|
|
||||||
|
Art Directors:
|
||||||
|
Nuno F. Pinheiro <nuno@nuno-icons.com>
|
||||||
|
David Vignoni <david@oxygen-icons.org>
|
||||||
|
|
||||||
|
Naming Coordinator
|
||||||
|
Jakob Petsovits <jpetso@gmx.at>
|
||||||
|
|
||||||
|
Designers:
|
||||||
|
David J. Miller <miller@oxygen-icons.org>
|
||||||
|
David Vignoni <david@oxygen-icons.org>
|
||||||
|
Johann Ollivier Lapeyre <johann@oxygen-icons.org>
|
||||||
|
Kenneth Wimer <ken@oxygen-icons.org>
|
||||||
|
Nuno F. Pinheiro <nuno@nuno-icons.com>
|
||||||
|
Riccardo Iaconelli <riccardo@oxygen-icons.org>
|
||||||
|
David J. Miller <miller@oxygen-icons.org>
|
||||||
|
|
||||||
|
Thanks to:
|
||||||
|
Lee Olson: Contributed drawing used in application-x-bittorent icon.
|
||||||
|
Marco Aurélio "Coré": Improved audio-input-microphone icon.
|
||||||
|
Matthias Kretz: Contributed "audio-input-line" device icon.
|
||||||
|
Mauricio Piacentini <piacentini@kde.org> : game icons mashup
|
||||||
|
Erlend Hamberg: "text-x-haskell" mimetype icon.
|
||||||
216
BEdit/resources/icons/COPYING
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
The Oxygen Icon Theme
|
||||||
|
Copyright (C) 2007 Nuno Pinheiro <nuno@oxygen-icons.org>
|
||||||
|
Copyright (C) 2007 David Vignoni <david@icon-king.com>
|
||||||
|
Copyright (C) 2007 David Miller <miller@oxygen-icons.org>
|
||||||
|
Copyright (C) 2007 Johann Ollivier Lapeyre <johann@oxygen-icons.org>
|
||||||
|
Copyright (C) 2007 Kenneth Wimer <kwwii@bootsplash.org>
|
||||||
|
Copyright (C) 2007 Riccardo Iaconelli <riccardo@oxygen-icons.org>
|
||||||
|
|
||||||
|
|
||||||
|
and others
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 3 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Clarification:
|
||||||
|
|
||||||
|
The GNU Lesser General Public License or LGPL is written for
|
||||||
|
software libraries in the first place. We expressly want the LGPL to
|
||||||
|
be valid for this artwork library too.
|
||||||
|
|
||||||
|
KDE Oxygen theme icons is a special kind of software library, it is an
|
||||||
|
artwork library, it's elements can be used in a Graphical User Interface, or
|
||||||
|
GUI.
|
||||||
|
|
||||||
|
Source code, for this library means:
|
||||||
|
- where they exist, SVG;
|
||||||
|
- otherwise, if applicable, the multi-layered formats xcf or psd, or
|
||||||
|
otherwise png.
|
||||||
|
|
||||||
|
The LGPL in some sections obliges you to make the files carry
|
||||||
|
notices. With images this is in some cases impossible or hardly useful.
|
||||||
|
|
||||||
|
With this library a notice is placed at a prominent place in the directory
|
||||||
|
containing the elements. You may follow this practice.
|
||||||
|
|
||||||
|
The exception in section 5 of the GNU Lesser General Public License covers
|
||||||
|
the use of elements of this art library in a GUI.
|
||||||
|
|
||||||
|
kde-artists [at] kde.org
|
||||||
|
|
||||||
|
-----
|
||||||
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
|
||||||
|
This version of the GNU Lesser General Public License incorporates
|
||||||
|
the terms and conditions of version 3 of the GNU General Public
|
||||||
|
License, supplemented by the additional permissions listed below.
|
||||||
|
|
||||||
|
0. Additional Definitions.
|
||||||
|
|
||||||
|
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||||
|
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||||
|
General Public License.
|
||||||
|
|
||||||
|
"The Library" refers to a covered work governed by this License,
|
||||||
|
other than an Application or a Combined Work as defined below.
|
||||||
|
|
||||||
|
An "Application" is any work that makes use of an interface provided
|
||||||
|
by the Library, but which is not otherwise based on the Library.
|
||||||
|
Defining a subclass of a class defined by the Library is deemed a mode
|
||||||
|
of using an interface provided by the Library.
|
||||||
|
|
||||||
|
A "Combined Work" is a work produced by combining or linking an
|
||||||
|
Application with the Library. The particular version of the Library
|
||||||
|
with which the Combined Work was made is also called the "Linked
|
||||||
|
Version".
|
||||||
|
|
||||||
|
The "Minimal Corresponding Source" for a Combined Work means the
|
||||||
|
Corresponding Source for the Combined Work, excluding any source code
|
||||||
|
for portions of the Combined Work that, considered in isolation, are
|
||||||
|
based on the Application, and not on the Linked Version.
|
||||||
|
|
||||||
|
The "Corresponding Application Code" for a Combined Work means the
|
||||||
|
object code and/or source code for the Application, including any data
|
||||||
|
and utility programs needed for reproducing the Combined Work from the
|
||||||
|
Application, but excluding the System Libraries of the Combined Work.
|
||||||
|
|
||||||
|
1. Exception to Section 3 of the GNU GPL.
|
||||||
|
|
||||||
|
You may convey a covered work under sections 3 and 4 of this License
|
||||||
|
without being bound by section 3 of the GNU GPL.
|
||||||
|
|
||||||
|
2. Conveying Modified Versions.
|
||||||
|
|
||||||
|
If you modify a copy of the Library, and, in your modifications, a
|
||||||
|
facility refers to a function or data to be supplied by an Application
|
||||||
|
that uses the facility (other than as an argument passed when the
|
||||||
|
facility is invoked), then you may convey a copy of the modified
|
||||||
|
version:
|
||||||
|
|
||||||
|
a) under this License, provided that you make a good faith effort to
|
||||||
|
ensure that, in the event an Application does not supply the
|
||||||
|
function or data, the facility still operates, and performs
|
||||||
|
whatever part of its purpose remains meaningful, or
|
||||||
|
|
||||||
|
b) under the GNU GPL, with none of the additional permissions of
|
||||||
|
this License applicable to that copy.
|
||||||
|
|
||||||
|
3. Object Code Incorporating Material from Library Header Files.
|
||||||
|
|
||||||
|
The object code form of an Application may incorporate material from
|
||||||
|
a header file that is part of the Library. You may convey such object
|
||||||
|
code under terms of your choice, provided that, if the incorporated
|
||||||
|
material is not limited to numerical parameters, data structure
|
||||||
|
layouts and accessors, or small macros, inline functions and templates
|
||||||
|
(ten or fewer lines in length), you do both of the following:
|
||||||
|
|
||||||
|
a) Give prominent notice with each copy of the object code that the
|
||||||
|
Library is used in it and that the Library and its use are
|
||||||
|
covered by this License.
|
||||||
|
|
||||||
|
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||||
|
document.
|
||||||
|
|
||||||
|
4. Combined Works.
|
||||||
|
|
||||||
|
You may convey a Combined Work under terms of your choice that,
|
||||||
|
taken together, effectively do not restrict modification of the
|
||||||
|
portions of the Library contained in the Combined Work and reverse
|
||||||
|
engineering for debugging such modifications, if you also do each of
|
||||||
|
the following:
|
||||||
|
|
||||||
|
a) Give prominent notice with each copy of the Combined Work that
|
||||||
|
the Library is used in it and that the Library and its use are
|
||||||
|
covered by this License.
|
||||||
|
|
||||||
|
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||||
|
document.
|
||||||
|
|
||||||
|
c) For a Combined Work that displays copyright notices during
|
||||||
|
execution, include the copyright notice for the Library among
|
||||||
|
these notices, as well as a reference directing the user to the
|
||||||
|
copies of the GNU GPL and this license document.
|
||||||
|
|
||||||
|
d) Do one of the following:
|
||||||
|
|
||||||
|
0) Convey the Minimal Corresponding Source under the terms of this
|
||||||
|
License, and the Corresponding Application Code in a form
|
||||||
|
suitable for, and under terms that permit, the user to
|
||||||
|
recombine or relink the Application with a modified version of
|
||||||
|
the Linked Version to produce a modified Combined Work, in the
|
||||||
|
manner specified by section 6 of the GNU GPL for conveying
|
||||||
|
Corresponding Source.
|
||||||
|
|
||||||
|
1) Use a suitable shared library mechanism for linking with the
|
||||||
|
Library. A suitable mechanism is one that (a) uses at run time
|
||||||
|
a copy of the Library already present on the user's computer
|
||||||
|
system, and (b) will operate properly with a modified version
|
||||||
|
of the Library that is interface-compatible with the Linked
|
||||||
|
Version.
|
||||||
|
|
||||||
|
e) Provide Installation Information, but only if you would otherwise
|
||||||
|
be required to provide such information under section 6 of the
|
||||||
|
GNU GPL, and only to the extent that such information is
|
||||||
|
necessary to install and execute a modified version of the
|
||||||
|
Combined Work produced by recombining or relinking the
|
||||||
|
Application with a modified version of the Linked Version. (If
|
||||||
|
you use option 4d0, the Installation Information must accompany
|
||||||
|
the Minimal Corresponding Source and Corresponding Application
|
||||||
|
Code. If you use option 4d1, you must provide the Installation
|
||||||
|
Information in the manner specified by section 6 of the GNU GPL
|
||||||
|
for conveying Corresponding Source.)
|
||||||
|
|
||||||
|
5. Combined Libraries.
|
||||||
|
|
||||||
|
You may place library facilities that are a work based on the
|
||||||
|
Library side by side in a single library together with other library
|
||||||
|
facilities that are not Applications and are not covered by this
|
||||||
|
License, and convey such a combined library under terms of your
|
||||||
|
choice, if you do both of the following:
|
||||||
|
|
||||||
|
a) Accompany the combined library with a copy of the same work based
|
||||||
|
on the Library, uncombined with any other library facilities,
|
||||||
|
conveyed under the terms of this License.
|
||||||
|
|
||||||
|
b) Give prominent notice with the combined library that part of it
|
||||||
|
is a work based on the Library, and explaining where to find the
|
||||||
|
accompanying uncombined form of the same work.
|
||||||
|
|
||||||
|
6. Revised Versions of the GNU Lesser General Public License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions
|
||||||
|
of the GNU Lesser General Public License from time to time. Such new
|
||||||
|
versions will be similar in spirit to the present version, but may
|
||||||
|
differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Library as you received it specifies that a certain numbered version
|
||||||
|
of the GNU Lesser General Public License "or any later version"
|
||||||
|
applies to it, you have the option of following the terms and
|
||||||
|
conditions either of that published version or of any later version
|
||||||
|
published by the Free Software Foundation. If the Library as you
|
||||||
|
received it does not specify a version number of the GNU Lesser
|
||||||
|
General Public License, you may choose any version of the GNU Lesser
|
||||||
|
General Public License ever published by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Library as you received it specifies that a proxy can decide
|
||||||
|
whether future versions of the GNU Lesser General Public License shall
|
||||||
|
apply, that proxy's public statement of acceptance of any version is
|
||||||
|
permanent authorization for you to choose that version for the
|
||||||
|
Library.
|
||||||
1
BEdit/resources/icons/README
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Oxygen Icons is a freedesktop.org compatible icon theme originally developed for the KDE Plasma desktop environment in combination with the Oxygen Style. It features smooth gradients, soft shadows, and a slightly glossy look.
|
||||||
BIN
BEdit/resources/icons/configure.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
BEdit/resources/icons/document-new.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
BEdit/resources/icons/document-open.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
BEdit/resources/icons/document-save-as.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
BEdit/resources/icons/document-save.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
BEdit/resources/icons/edit-copy.png
Normal file
|
After Width: | Height: | Size: 860 B |
BIN
BEdit/resources/icons/edit-cut.png
Normal file
|
After Width: | Height: | Size: 892 B |
BIN
BEdit/resources/icons/edit-paste.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
BEdit/resources/icons/edit-redo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
BEdit/resources/icons/edit-undo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
BEdit/resources/icons/media-playback-start.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
BEdit/resources/icons/transform-rotate.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
14
BEdit/resources/resources.qrc
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<RCC>
|
||||||
|
<qresource prefix="icons">
|
||||||
|
<file>icons/edit-paste.png</file>
|
||||||
|
<file>icons/edit-redo.png</file>
|
||||||
|
<file>icons/edit-cut.png</file>
|
||||||
|
<file>icons/edit-copy.png</file>
|
||||||
|
<file>icons/edit-undo.png</file>
|
||||||
|
<file>icons/document-save.png</file>
|
||||||
|
<file>icons/document-save-as.png</file>
|
||||||
|
<file>icons/document-open.png</file>
|
||||||
|
<file>icons/document-new.png</file>
|
||||||
|
<file>icons/transform-rotate.png</file>
|
||||||
|
</qresource>
|
||||||
|
</RCC>
|
||||||
1089
BEdit/resources/resources_rc.py
Normal file
4
BEdit/src/bedit/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
"""BEdit desktop application."""
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
|
|
||||||
6
BEdit/src/bedit/__main__.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from bedit.app import main
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
||||||
51
BEdit/src/bedit/app.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
from PySide6.QtCore import QCoreApplication, Qt
|
||||||
|
from PySide6.QtGui import QColor, QPalette
|
||||||
|
from PySide6.QtWidgets import QApplication, QStyleFactory
|
||||||
|
|
||||||
|
from bedit.main_window import MainWindow
|
||||||
|
|
||||||
|
|
||||||
|
def apply_light_theme(app: QApplication) -> None:
|
||||||
|
"""Use a predictable light Qt theme, independent of the desktop theme."""
|
||||||
|
app.setStyle(QStyleFactory.create("Fusion"))
|
||||||
|
|
||||||
|
palette = QPalette()
|
||||||
|
palette.setColor(QPalette.ColorRole.Window, QColor(240, 240, 240))
|
||||||
|
palette.setColor(QPalette.ColorRole.WindowText, Qt.GlobalColor.black)
|
||||||
|
palette.setColor(QPalette.ColorRole.Base, Qt.GlobalColor.white)
|
||||||
|
palette.setColor(QPalette.ColorRole.AlternateBase, QColor(233, 233, 233))
|
||||||
|
palette.setColor(QPalette.ColorRole.ToolTipBase, Qt.GlobalColor.white)
|
||||||
|
palette.setColor(QPalette.ColorRole.ToolTipText, Qt.GlobalColor.black)
|
||||||
|
palette.setColor(QPalette.ColorRole.Text, Qt.GlobalColor.black)
|
||||||
|
palette.setColor(QPalette.ColorRole.Button, QColor(240, 240, 240))
|
||||||
|
palette.setColor(QPalette.ColorRole.ButtonText, Qt.GlobalColor.black)
|
||||||
|
palette.setColor(QPalette.ColorRole.BrightText, Qt.GlobalColor.red)
|
||||||
|
palette.setColor(QPalette.ColorRole.Link, QColor(0, 102, 204))
|
||||||
|
palette.setColor(QPalette.ColorRole.Highlight, QColor(0, 120, 215))
|
||||||
|
palette.setColor(QPalette.ColorRole.HighlightedText, Qt.GlobalColor.white)
|
||||||
|
palette.setColor(
|
||||||
|
QPalette.ColorGroup.Disabled,
|
||||||
|
QPalette.ColorRole.Text,
|
||||||
|
QColor(109, 109, 109),
|
||||||
|
)
|
||||||
|
palette.setColor(
|
||||||
|
QPalette.ColorGroup.Disabled,
|
||||||
|
QPalette.ColorRole.ButtonText,
|
||||||
|
QColor(109, 109, 109),
|
||||||
|
)
|
||||||
|
app.setPalette(palette)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
app.setApplicationName("BEdit")
|
||||||
|
app.setApplicationDisplayName("BEdit")
|
||||||
|
app.setOrganizationName("BEdit")
|
||||||
|
QCoreApplication.setApplicationVersion("0.1.0")
|
||||||
|
|
||||||
|
apply_light_theme(app)
|
||||||
|
window = MainWindow()
|
||||||
|
window.show()
|
||||||
|
return app.exec()
|
||||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||||
475
BEdit/src/bedit/main_window.py
Normal file
@@ -0,0 +1,475 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PySide6.QtCore import QSettings, Qt, Slot
|
||||||
|
from PySide6.QtGui import QCloseEvent
|
||||||
|
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.ui_main_window import Ui_MainWindow
|
||||||
|
|
||||||
|
|
||||||
|
class MainWindow(QMainWindow):
|
||||||
|
"""Application shell and owner of the single active document."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.ui = Ui_MainWindow()
|
||||||
|
self.ui.setupUi(self)
|
||||||
|
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._restore_window_geometry()
|
||||||
|
|
||||||
|
self.ui.leftDockHost.setWindowFlags(Qt.WindowType.Widget)
|
||||||
|
self.ui.leftDockHost.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.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:
|
||||||
|
for panel in (self.ui.panel_document, self.ui.panel_libraries):
|
||||||
|
self.ui.menuPanels.addAction(panel.toggleViewAction())
|
||||||
|
for toolbar in (
|
||||||
|
self.ui.fileToolbar,
|
||||||
|
self.ui.editToolbar,
|
||||||
|
self.ui.transformToolbar,
|
||||||
|
):
|
||||||
|
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:
|
||||||
|
geometry = self.settings.value("window/geometry")
|
||||||
|
if geometry is not None:
|
||||||
|
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()
|
||||||
|
def show_settings(self) -> None:
|
||||||
|
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()
|
||||||
|
def show_about(self) -> None:
|
||||||
|
QMessageBox.about(
|
||||||
|
self,
|
||||||
|
"About BEdit",
|
||||||
|
"<h3>BEdit</h3><p>A graphical editor built with Python and Qt.</p>",
|
||||||
|
)
|
||||||
|
|
||||||
|
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())
|
||||||
|
event.accept()
|
||||||
1089
BEdit/src/bedit/resources_rc.py
Normal file
88
BEdit/src/bedit/settings_dialog.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsDialog(QDialog):
|
||||||
|
"""Edit application preferences defined in the Designer form."""
|
||||||
|
|
||||||
|
settingsChanged = Signal()
|
||||||
|
|
||||||
|
def __init__(self, parent=None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.ui = Ui_SettingsDialog()
|
||||||
|
self.ui.setupUi(self)
|
||||||
|
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()
|
||||||
|
|
||||||
|
def _load_settings(self) -> None:
|
||||||
|
self.ui.autosaveGroupBox.setChecked(
|
||||||
|
self.settings.value("general/autosaveEnabled", False, type=bool)
|
||||||
|
)
|
||||||
|
self.ui.autosaveIntervalSpinBox.setValue(
|
||||||
|
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:
|
||||||
|
self.settings.setValue("general/autosaveEnabled", self.ui.autosaveGroupBox.isChecked())
|
||||||
|
self.settings.setValue(
|
||||||
|
"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()
|
||||||
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
|
||||||
|
|
||||||
450
BEdit/src/bedit/ui_main_window.py
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
## Form generated from reading UI file 'main_window.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 (QAction, QBrush, QColor, QConicalGradient,
|
||||||
|
QCursor, QFont, QFontDatabase, QGradient,
|
||||||
|
QIcon, QImage, QKeySequence, QLinearGradient,
|
||||||
|
QPainter, QPalette, QPixmap, QRadialGradient,
|
||||||
|
QTransform)
|
||||||
|
from PySide6.QtWidgets import (QApplication, QDockWidget, QFrame, QHBoxLayout,
|
||||||
|
QHeaderView, QLabel, QMainWindow, QMenu,
|
||||||
|
QMenuBar, QPlainTextEdit, QPushButton, QSizePolicy,
|
||||||
|
QSpacerItem, QSplitter, QStackedWidget, QToolBar,
|
||||||
|
QToolButton, QTreeView, QVBoxLayout, QWidget)
|
||||||
|
|
||||||
|
from bedit.workspace.view import GraphWorkspaceView
|
||||||
|
from . import resources_rc
|
||||||
|
|
||||||
|
class Ui_MainWindow(object):
|
||||||
|
def setupUi(self, MainWindow):
|
||||||
|
if not MainWindow.objectName():
|
||||||
|
MainWindow.setObjectName(u"MainWindow")
|
||||||
|
MainWindow.resize(1000, 700)
|
||||||
|
self.actionNew = QAction(MainWindow)
|
||||||
|
self.actionNew.setObjectName(u"actionNew")
|
||||||
|
icon = QIcon()
|
||||||
|
icon.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
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.setObjectName(u"actionOpen")
|
||||||
|
icon2 = QIcon()
|
||||||
|
icon2.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.actionOpen.setIcon(icon2)
|
||||||
|
self.actionSave = QAction(MainWindow)
|
||||||
|
self.actionSave.setObjectName(u"actionSave")
|
||||||
|
icon3 = QIcon()
|
||||||
|
icon3.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.actionSave.setIcon(icon3)
|
||||||
|
self.actionSaveAs = QAction(MainWindow)
|
||||||
|
self.actionSaveAs.setObjectName(u"actionSaveAs")
|
||||||
|
icon4 = QIcon()
|
||||||
|
icon4.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.actionSaveAs.setIcon(icon4)
|
||||||
|
self.actionExit = QAction(MainWindow)
|
||||||
|
self.actionExit.setObjectName(u"actionExit")
|
||||||
|
self.actionClose = QAction(MainWindow)
|
||||||
|
self.actionClose.setObjectName(u"actionClose")
|
||||||
|
self.actionUndo = QAction(MainWindow)
|
||||||
|
self.actionUndo.setObjectName(u"actionUndo")
|
||||||
|
icon5 = QIcon()
|
||||||
|
icon5.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.actionUndo.setIcon(icon5)
|
||||||
|
self.actionRedo = QAction(MainWindow)
|
||||||
|
self.actionRedo.setObjectName(u"actionRedo")
|
||||||
|
icon6 = QIcon()
|
||||||
|
icon6.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.actionRedo.setIcon(icon6)
|
||||||
|
self.actionCut = QAction(MainWindow)
|
||||||
|
self.actionCut.setObjectName(u"actionCut")
|
||||||
|
icon7 = QIcon()
|
||||||
|
icon7.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.actionCut.setIcon(icon7)
|
||||||
|
self.actionCopy = QAction(MainWindow)
|
||||||
|
self.actionCopy.setObjectName(u"actionCopy")
|
||||||
|
icon8 = QIcon()
|
||||||
|
icon8.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.actionCopy.setIcon(icon8)
|
||||||
|
self.actionPaste = QAction(MainWindow)
|
||||||
|
self.actionPaste.setObjectName(u"actionPaste")
|
||||||
|
icon9 = QIcon()
|
||||||
|
icon9.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.actionPaste.setIcon(icon9)
|
||||||
|
self.actionSelectAll = QAction(MainWindow)
|
||||||
|
self.actionSelectAll.setObjectName(u"actionSelectAll")
|
||||||
|
self.actionDelete = QAction(MainWindow)
|
||||||
|
self.actionDelete.setObjectName(u"actionDelete")
|
||||||
|
self.actionAbout = QAction(MainWindow)
|
||||||
|
self.actionAbout.setObjectName(u"actionAbout")
|
||||||
|
self.actionSettings = QAction(MainWindow)
|
||||||
|
self.actionSettings.setObjectName(u"actionSettings")
|
||||||
|
self.actionAboutQt = QAction(MainWindow)
|
||||||
|
self.actionAboutQt.setObjectName(u"actionAboutQt")
|
||||||
|
self.centralwidget = QWidget(MainWindow)
|
||||||
|
self.centralwidget.setObjectName(u"centralwidget")
|
||||||
|
self.workspaceLayout = QHBoxLayout(self.centralwidget)
|
||||||
|
self.workspaceLayout.setSpacing(0)
|
||||||
|
self.workspaceLayout.setObjectName(u"workspaceLayout")
|
||||||
|
self.workspaceLayout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.workspaceSplitter = QSplitter(self.centralwidget)
|
||||||
|
self.workspaceSplitter.setObjectName(u"workspaceSplitter")
|
||||||
|
sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
|
||||||
|
sizePolicy.setHorizontalStretch(0)
|
||||||
|
sizePolicy.setVerticalStretch(0)
|
||||||
|
sizePolicy.setHeightForWidth(self.workspaceSplitter.sizePolicy().hasHeightForWidth())
|
||||||
|
self.workspaceSplitter.setSizePolicy(sizePolicy)
|
||||||
|
self.workspaceSplitter.setOrientation(Qt.Orientation.Horizontal)
|
||||||
|
self.workspaceSplitter.setChildrenCollapsible(False)
|
||||||
|
self.leftDockHost = QMainWindow(self.workspaceSplitter)
|
||||||
|
self.leftDockHost.setObjectName(u"leftDockHost")
|
||||||
|
self.leftDockHost.setMinimumSize(QSize(220, 0))
|
||||||
|
self.panel_libraries = QDockWidget(self.leftDockHost)
|
||||||
|
self.panel_libraries.setObjectName(u"panel_libraries")
|
||||||
|
self.panel_libraries.setMinimumSize(QSize(220, 91))
|
||||||
|
self.dockWidgetContents = QWidget()
|
||||||
|
self.dockWidgetContents.setObjectName(u"dockWidgetContents")
|
||||||
|
self.librariesLayout = QVBoxLayout(self.dockWidgetContents)
|
||||||
|
self.librariesLayout.setSpacing(0)
|
||||||
|
self.librariesLayout.setObjectName(u"librariesLayout")
|
||||||
|
self.librariesLayout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.treeView = QTreeView(self.dockWidgetContents)
|
||||||
|
self.treeView.setObjectName(u"treeView")
|
||||||
|
self.treeView.setAlternatingRowColors(True)
|
||||||
|
self.treeView.setUniformRowHeights(True)
|
||||||
|
|
||||||
|
self.librariesLayout.addWidget(self.treeView)
|
||||||
|
|
||||||
|
self.panel_libraries.setWidget(self.dockWidgetContents)
|
||||||
|
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.workspace = QWidget(self.workspaceSplitter)
|
||||||
|
self.workspace.setObjectName(u"workspace")
|
||||||
|
sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
|
||||||
|
sizePolicy1.setHorizontalStretch(1)
|
||||||
|
sizePolicy1.setVerticalStretch(0)
|
||||||
|
sizePolicy1.setHeightForWidth(self.workspace.sizePolicy().hasHeightForWidth())
|
||||||
|
self.workspace.setSizePolicy(sizePolicy1)
|
||||||
|
self.workspaceEditorLayout = QVBoxLayout(self.workspace)
|
||||||
|
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.workspaceLayout.addWidget(self.workspaceSplitter)
|
||||||
|
|
||||||
|
MainWindow.setCentralWidget(self.centralwidget)
|
||||||
|
self.menubar = QMenuBar(MainWindow)
|
||||||
|
self.menubar.setObjectName(u"menubar")
|
||||||
|
self.menubar.setGeometry(QRect(0, 0, 1000, 24))
|
||||||
|
self.menuFile = QMenu(self.menubar)
|
||||||
|
self.menuFile.setObjectName(u"menuFile")
|
||||||
|
self.menuEdit = QMenu(self.menubar)
|
||||||
|
self.menuEdit.setObjectName(u"menuEdit")
|
||||||
|
self.menuView = QMenu(self.menubar)
|
||||||
|
self.menuView.setObjectName(u"menuView")
|
||||||
|
self.menuPanels = QMenu(self.menuView)
|
||||||
|
self.menuPanels.setObjectName(u"menuPanels")
|
||||||
|
self.menuToolbars = QMenu(self.menuView)
|
||||||
|
self.menuToolbars.setObjectName(u"menuToolbars")
|
||||||
|
self.menuHelp = QMenu(self.menubar)
|
||||||
|
self.menuHelp.setObjectName(u"menuHelp")
|
||||||
|
MainWindow.setMenuBar(self.menubar)
|
||||||
|
self.fileToolbar = QToolBar(MainWindow)
|
||||||
|
self.fileToolbar.setObjectName(u"fileToolbar")
|
||||||
|
sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||||
|
sizePolicy2.setHorizontalStretch(0)
|
||||||
|
sizePolicy2.setVerticalStretch(0)
|
||||||
|
sizePolicy2.setHeightForWidth(self.fileToolbar.sizePolicy().hasHeightForWidth())
|
||||||
|
self.fileToolbar.setSizePolicy(sizePolicy2)
|
||||||
|
self.fileToolbar.setMinimumSize(QSize(0, 40))
|
||||||
|
self.fileToolbar.setMaximumSize(QSize(16777215, 40))
|
||||||
|
self.fileToolbar.setIconSize(QSize(24, 24))
|
||||||
|
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.fileToolbar)
|
||||||
|
self.editToolbar = QToolBar(MainWindow)
|
||||||
|
self.editToolbar.setObjectName(u"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.menuEdit.menuAction())
|
||||||
|
self.menubar.addAction(self.menuView.menuAction())
|
||||||
|
self.menubar.addAction(self.menuHelp.menuAction())
|
||||||
|
self.menuFile.addAction(self.actionNew)
|
||||||
|
self.menuFile.addAction(self.actionOpen)
|
||||||
|
self.menuFile.addSeparator()
|
||||||
|
self.menuFile.addAction(self.actionSave)
|
||||||
|
self.menuFile.addAction(self.actionSaveAs)
|
||||||
|
self.menuFile.addSeparator()
|
||||||
|
self.menuFile.addAction(self.actionClose)
|
||||||
|
self.menuFile.addAction(self.actionExit)
|
||||||
|
self.menuEdit.addAction(self.actionUndo)
|
||||||
|
self.menuEdit.addAction(self.actionRedo)
|
||||||
|
self.menuEdit.addSeparator()
|
||||||
|
self.menuEdit.addAction(self.actionCopy)
|
||||||
|
self.menuEdit.addAction(self.actionCut)
|
||||||
|
self.menuEdit.addAction(self.actionPaste)
|
||||||
|
self.menuEdit.addAction(self.actionDelete)
|
||||||
|
self.menuEdit.addAction(self.actionSelectAll)
|
||||||
|
self.menuEdit.addSeparator()
|
||||||
|
self.menuEdit.addAction(self.actionSettings)
|
||||||
|
self.menuView.addAction(self.menuPanels.menuAction())
|
||||||
|
self.menuView.addAction(self.menuToolbars.menuAction())
|
||||||
|
self.menuHelp.addAction(self.actionAbout)
|
||||||
|
self.menuHelp.addAction(self.actionAboutQt)
|
||||||
|
self.fileToolbar.addAction(self.actionNew)
|
||||||
|
self.fileToolbar.addAction(self.actionOpen)
|
||||||
|
self.fileToolbar.addAction(self.actionSave)
|
||||||
|
self.fileToolbar.addAction(self.actionSaveAs)
|
||||||
|
self.editToolbar.addAction(self.actionUndo)
|
||||||
|
self.editToolbar.addAction(self.actionRedo)
|
||||||
|
self.editToolbar.addAction(self.actionCopy)
|
||||||
|
self.editToolbar.addAction(self.actionCut)
|
||||||
|
self.editToolbar.addAction(self.actionPaste)
|
||||||
|
self.transformToolbar.addAction(self.actionRotateClockwise)
|
||||||
|
|
||||||
|
self.retranslateUi(MainWindow)
|
||||||
|
|
||||||
|
self.workspaceStack.setCurrentIndex(0)
|
||||||
|
|
||||||
|
|
||||||
|
QMetaObject.connectSlotsByName(MainWindow)
|
||||||
|
# setupUi
|
||||||
|
|
||||||
|
def retranslateUi(self, MainWindow):
|
||||||
|
MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"BEdit", None))
|
||||||
|
self.actionNew.setText(QCoreApplication.translate("MainWindow", u"&New", None))
|
||||||
|
#if QT_CONFIG(statustip)
|
||||||
|
self.actionNew.setStatusTip(QCoreApplication.translate("MainWindow", u"Create a new document", None))
|
||||||
|
#endif // QT_CONFIG(statustip)
|
||||||
|
#if QT_CONFIG(shortcut)
|
||||||
|
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)
|
||||||
|
self.actionOpen.setText(QCoreApplication.translate("MainWindow", u"&Open\u2026", None))
|
||||||
|
#if QT_CONFIG(statustip)
|
||||||
|
self.actionOpen.setStatusTip(QCoreApplication.translate("MainWindow", u"Open a document", None))
|
||||||
|
#endif // QT_CONFIG(statustip)
|
||||||
|
#if QT_CONFIG(shortcut)
|
||||||
|
self.actionOpen.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+O", None))
|
||||||
|
#endif // QT_CONFIG(shortcut)
|
||||||
|
self.actionSave.setText(QCoreApplication.translate("MainWindow", u"&Save", None))
|
||||||
|
#if QT_CONFIG(statustip)
|
||||||
|
self.actionSave.setStatusTip(QCoreApplication.translate("MainWindow", u"Save the current document", None))
|
||||||
|
#endif // QT_CONFIG(statustip)
|
||||||
|
#if QT_CONFIG(shortcut)
|
||||||
|
self.actionSave.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+S", None))
|
||||||
|
#endif // QT_CONFIG(shortcut)
|
||||||
|
self.actionSaveAs.setText(QCoreApplication.translate("MainWindow", u"Save &As\u2026", None))
|
||||||
|
#if QT_CONFIG(shortcut)
|
||||||
|
self.actionSaveAs.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+Shift+S", None))
|
||||||
|
#endif // QT_CONFIG(shortcut)
|
||||||
|
self.actionExit.setText(QCoreApplication.translate("MainWindow", u"E&xit", None))
|
||||||
|
#if QT_CONFIG(shortcut)
|
||||||
|
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)
|
||||||
|
self.actionUndo.setText(QCoreApplication.translate("MainWindow", u"&Undo", None))
|
||||||
|
#if QT_CONFIG(shortcut)
|
||||||
|
self.actionUndo.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+Z", None))
|
||||||
|
#endif // QT_CONFIG(shortcut)
|
||||||
|
self.actionRedo.setText(QCoreApplication.translate("MainWindow", u"&Redo", None))
|
||||||
|
#if QT_CONFIG(shortcut)
|
||||||
|
self.actionRedo.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+Y", None))
|
||||||
|
#endif // QT_CONFIG(shortcut)
|
||||||
|
self.actionCut.setText(QCoreApplication.translate("MainWindow", u"Cu&t", None))
|
||||||
|
#if QT_CONFIG(shortcut)
|
||||||
|
self.actionCut.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+X", None))
|
||||||
|
#endif // QT_CONFIG(shortcut)
|
||||||
|
self.actionCopy.setText(QCoreApplication.translate("MainWindow", u"&Copy", None))
|
||||||
|
#if QT_CONFIG(shortcut)
|
||||||
|
self.actionCopy.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+C", None))
|
||||||
|
#endif // QT_CONFIG(shortcut)
|
||||||
|
self.actionPaste.setText(QCoreApplication.translate("MainWindow", u"&Paste", None))
|
||||||
|
#if QT_CONFIG(shortcut)
|
||||||
|
self.actionPaste.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+V", None))
|
||||||
|
#endif // QT_CONFIG(shortcut)
|
||||||
|
self.actionSelectAll.setText(QCoreApplication.translate("MainWindow", u"Select &All", None))
|
||||||
|
#if QT_CONFIG(shortcut)
|
||||||
|
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)
|
||||||
|
self.actionAbout.setText(QCoreApplication.translate("MainWindow", u"&About BEdit", None))
|
||||||
|
self.actionSettings.setText(QCoreApplication.translate("MainWindow", u"&Settings\u2026", None))
|
||||||
|
#if QT_CONFIG(statustip)
|
||||||
|
self.actionSettings.setStatusTip(QCoreApplication.translate("MainWindow", u"Configure BEdit", None))
|
||||||
|
#endif // QT_CONFIG(statustip)
|
||||||
|
self.actionAboutQt.setText(QCoreApplication.translate("MainWindow", u"About &Qt", 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.menuEdit.setTitle(QCoreApplication.translate("MainWindow", u"&Edit", None))
|
||||||
|
self.menuView.setTitle(QCoreApplication.translate("MainWindow", u"&View", None))
|
||||||
|
self.menuPanels.setTitle(QCoreApplication.translate("MainWindow", u"&Panels", None))
|
||||||
|
self.menuToolbars.setTitle(QCoreApplication.translate("MainWindow", u"&Toolbars", None))
|
||||||
|
self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"&Help", None))
|
||||||
|
self.fileToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"File", None))
|
||||||
|
self.editToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Edit", None))
|
||||||
|
self.transformToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Transform", None))
|
||||||
|
# retranslateUi
|
||||||
|
|
||||||
132
BEdit/src/bedit/ui_settings_dialog.py
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
## Form generated from reading UI file 'settings_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, QDialog, QDialogButtonBox,
|
||||||
|
QFormLayout, QGroupBox, QHBoxLayout, QLabel,
|
||||||
|
QListWidget, QListWidgetItem, QPushButton, QSizePolicy,
|
||||||
|
QSpacerItem, QSpinBox, QTabWidget, QVBoxLayout,
|
||||||
|
QWidget)
|
||||||
|
|
||||||
|
class Ui_SettingsDialog(object):
|
||||||
|
def setupUi(self, SettingsDialog):
|
||||||
|
if not SettingsDialog.objectName():
|
||||||
|
SettingsDialog.setObjectName(u"SettingsDialog")
|
||||||
|
SettingsDialog.resize(480, 300)
|
||||||
|
SettingsDialog.setModal(True)
|
||||||
|
self.dialogLayout = QVBoxLayout(SettingsDialog)
|
||||||
|
self.dialogLayout.setObjectName(u"dialogLayout")
|
||||||
|
self.settingsTabs = QTabWidget(SettingsDialog)
|
||||||
|
self.settingsTabs.setObjectName(u"settingsTabs")
|
||||||
|
self.generalTab = QWidget()
|
||||||
|
self.generalTab.setObjectName(u"generalTab")
|
||||||
|
self.generalLayout = QVBoxLayout(self.generalTab)
|
||||||
|
self.generalLayout.setObjectName(u"generalLayout")
|
||||||
|
self.autosaveGroupBox = QGroupBox(self.generalTab)
|
||||||
|
self.autosaveGroupBox.setObjectName(u"autosaveGroupBox")
|
||||||
|
self.autosaveGroupBox.setCheckable(True)
|
||||||
|
self.autosaveGroupBox.setChecked(False)
|
||||||
|
self.autosaveLayout = QFormLayout(self.autosaveGroupBox)
|
||||||
|
self.autosaveLayout.setObjectName(u"autosaveLayout")
|
||||||
|
self.autosaveIntervalSpinBox = QSpinBox(self.autosaveGroupBox)
|
||||||
|
self.autosaveIntervalSpinBox.setObjectName(u"autosaveIntervalSpinBox")
|
||||||
|
self.autosaveIntervalSpinBox.setMinimum(1)
|
||||||
|
self.autosaveIntervalSpinBox.setMaximum(120)
|
||||||
|
self.autosaveIntervalSpinBox.setValue(5)
|
||||||
|
|
||||||
|
self.autosaveLayout.setWidget(0, QFormLayout.ItemRole.FieldRole, self.autosaveIntervalSpinBox)
|
||||||
|
|
||||||
|
|
||||||
|
self.generalLayout.addWidget(self.autosaveGroupBox)
|
||||||
|
|
||||||
|
self.generalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
|
||||||
|
|
||||||
|
self.generalLayout.addItem(self.generalSpacer)
|
||||||
|
|
||||||
|
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.buttonBox = QDialogButtonBox(SettingsDialog)
|
||||||
|
self.buttonBox.setObjectName(u"buttonBox")
|
||||||
|
self.buttonBox.setOrientation(Qt.Orientation.Horizontal)
|
||||||
|
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
|
||||||
|
|
||||||
|
self.dialogLayout.addWidget(self.buttonBox)
|
||||||
|
|
||||||
|
|
||||||
|
self.retranslateUi(SettingsDialog)
|
||||||
|
self.buttonBox.accepted.connect(SettingsDialog.accept)
|
||||||
|
self.buttonBox.rejected.connect(SettingsDialog.reject)
|
||||||
|
|
||||||
|
self.settingsTabs.setCurrentIndex(0)
|
||||||
|
|
||||||
|
|
||||||
|
QMetaObject.connectSlotsByName(SettingsDialog)
|
||||||
|
# setupUi
|
||||||
|
|
||||||
|
def retranslateUi(self, SettingsDialog):
|
||||||
|
SettingsDialog.setWindowTitle(QCoreApplication.translate("SettingsDialog", u"Settings", None))
|
||||||
|
self.autosaveGroupBox.setTitle(QCoreApplication.translate("SettingsDialog", u"Automatic saving", 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.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
|
||||||
|
|
||||||
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
@@ -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
@@ -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>
|
||||||
494
BEdit/ui/main_window.ui
Normal file
@@ -0,0 +1,494 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>MainWindow</class>
|
||||||
|
<widget class="QMainWindow" name="MainWindow">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>1000</width>
|
||||||
|
<height>700</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>BEdit</string>
|
||||||
|
</property>
|
||||||
|
<widget class="QWidget" name="centralwidget">
|
||||||
|
<layout class="QHBoxLayout" name="workspaceLayout">
|
||||||
|
<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="QSplitter" name="workspaceSplitter">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Orientation::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="childrenCollapsible">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<widget class="QMainWindow" name="leftDockHost">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>220</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<widget class="QDockWidget" name="panel_libraries">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>220</width>
|
||||||
|
<height>91</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Libraries</string>
|
||||||
|
</property>
|
||||||
|
<attribute name="dockWidgetArea">
|
||||||
|
<number>1</number>
|
||||||
|
</attribute>
|
||||||
|
<widget class="QWidget" name="dockWidgetContents">
|
||||||
|
<layout class="QVBoxLayout" name="librariesLayout">
|
||||||
|
<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="treeView">
|
||||||
|
<property name="alternatingRowColors">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="uniformRowHeights">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</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>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
<widget class="QWidget" name="workspace">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||||
|
<horstretch>1</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="workspaceEditorLayout">
|
||||||
|
<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="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>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<widget class="QMenuBar" name="menubar">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>1000</width>
|
||||||
|
<height>24</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<widget class="QMenu" name="menuFile">
|
||||||
|
<property name="title">
|
||||||
|
<string>&File</string>
|
||||||
|
</property>
|
||||||
|
<addaction name="actionNew"/>
|
||||||
|
<addaction name="actionOpen"/>
|
||||||
|
<addaction name="separator"/>
|
||||||
|
<addaction name="actionSave"/>
|
||||||
|
<addaction name="actionSaveAs"/>
|
||||||
|
<addaction name="separator"/>
|
||||||
|
<addaction name="actionClose"/>
|
||||||
|
<addaction name="actionExit"/>
|
||||||
|
</widget>
|
||||||
|
<widget class="QMenu" name="menuEdit">
|
||||||
|
<property name="title">
|
||||||
|
<string>&Edit</string>
|
||||||
|
</property>
|
||||||
|
<addaction name="actionUndo"/>
|
||||||
|
<addaction name="actionRedo"/>
|
||||||
|
<addaction name="separator"/>
|
||||||
|
<addaction name="actionCopy"/>
|
||||||
|
<addaction name="actionCut"/>
|
||||||
|
<addaction name="actionPaste"/>
|
||||||
|
<addaction name="actionDelete"/>
|
||||||
|
<addaction name="actionSelectAll"/>
|
||||||
|
<addaction name="separator"/>
|
||||||
|
<addaction name="actionSettings"/>
|
||||||
|
</widget>
|
||||||
|
<widget class="QMenu" name="menuView">
|
||||||
|
<property name="title">
|
||||||
|
<string>&View</string>
|
||||||
|
</property>
|
||||||
|
<widget class="QMenu" name="menuPanels">
|
||||||
|
<property name="title">
|
||||||
|
<string>&Panels</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QMenu" name="menuToolbars">
|
||||||
|
<property name="title">
|
||||||
|
<string>&Toolbars</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<addaction name="menuPanels"/>
|
||||||
|
<addaction name="menuToolbars"/>
|
||||||
|
</widget>
|
||||||
|
<widget class="QMenu" name="menuHelp">
|
||||||
|
<property name="title">
|
||||||
|
<string>&Help</string>
|
||||||
|
</property>
|
||||||
|
<addaction name="actionAbout"/>
|
||||||
|
<addaction name="actionAboutQt"/>
|
||||||
|
</widget>
|
||||||
|
<addaction name="menuFile"/>
|
||||||
|
<addaction name="menuEdit"/>
|
||||||
|
<addaction name="menuView"/>
|
||||||
|
<addaction name="menuHelp"/>
|
||||||
|
</widget>
|
||||||
|
<widget class="QToolBar" name="fileToolbar">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>0</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>16777215</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>File</string>
|
||||||
|
</property>
|
||||||
|
<property name="iconSize">
|
||||||
|
<size>
|
||||||
|
<width>24</width>
|
||||||
|
<height>24</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<attribute name="toolBarArea">
|
||||||
|
<enum>TopToolBarArea</enum>
|
||||||
|
</attribute>
|
||||||
|
<attribute name="toolBarBreak">
|
||||||
|
<bool>false</bool>
|
||||||
|
</attribute>
|
||||||
|
<addaction name="actionNew"/>
|
||||||
|
<addaction name="actionOpen"/>
|
||||||
|
<addaction name="actionSave"/>
|
||||||
|
<addaction name="actionSaveAs"/>
|
||||||
|
</widget>
|
||||||
|
<widget class="QToolBar" name="editToolbar">
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Edit</string>
|
||||||
|
</property>
|
||||||
|
<attribute name="toolBarArea">
|
||||||
|
<enum>TopToolBarArea</enum>
|
||||||
|
</attribute>
|
||||||
|
<attribute name="toolBarBreak">
|
||||||
|
<bool>false</bool>
|
||||||
|
</attribute>
|
||||||
|
<addaction name="actionUndo"/>
|
||||||
|
<addaction name="actionRedo"/>
|
||||||
|
<addaction name="actionCopy"/>
|
||||||
|
<addaction name="actionCut"/>
|
||||||
|
<addaction name="actionPaste"/>
|
||||||
|
</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">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../resources/resources.qrc">
|
||||||
|
<normaloff>:/icons/icons/document-new.png</normaloff>:/icons/icons/document-new.png</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>&New</string>
|
||||||
|
</property>
|
||||||
|
<property name="statusTip">
|
||||||
|
<string>Create a new document</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>Ctrl+N</string>
|
||||||
|
</property>
|
||||||
|
</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">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../resources/resources.qrc">
|
||||||
|
<normaloff>:/icons/icons/document-open.png</normaloff>:/icons/icons/document-open.png</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>&Open…</string>
|
||||||
|
</property>
|
||||||
|
<property name="statusTip">
|
||||||
|
<string>Open a document</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>Ctrl+O</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionSave">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../resources/resources.qrc">
|
||||||
|
<normaloff>:/icons/icons/document-save.png</normaloff>:/icons/icons/document-save.png</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>&Save</string>
|
||||||
|
</property>
|
||||||
|
<property name="statusTip">
|
||||||
|
<string>Save the current document</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>Ctrl+S</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionSaveAs">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../resources/resources.qrc">
|
||||||
|
<normaloff>:/icons/icons/document-save-as.png</normaloff>:/icons/icons/document-save-as.png</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Save &As…</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>Ctrl+Shift+S</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionExit">
|
||||||
|
<property name="text">
|
||||||
|
<string>E&xit</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>Ctrl+Q</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionClose">
|
||||||
|
<property name="text"><string>&Close Document</string></property>
|
||||||
|
<property name="shortcut"><string>Ctrl+W</string></property>
|
||||||
|
</action>
|
||||||
|
<action name="actionUndo">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../resources/resources.qrc">
|
||||||
|
<normaloff>:/icons/icons/edit-undo.png</normaloff>:/icons/icons/edit-undo.png</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>&Undo</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>Ctrl+Z</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionRedo">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../resources/resources.qrc">
|
||||||
|
<normaloff>:/icons/icons/edit-redo.png</normaloff>:/icons/icons/edit-redo.png</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>&Redo</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>Ctrl+Y</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionCut">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../resources/resources.qrc">
|
||||||
|
<normaloff>:/icons/icons/edit-cut.png</normaloff>:/icons/icons/edit-cut.png</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Cu&t</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>Ctrl+X</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionCopy">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../resources/resources.qrc">
|
||||||
|
<normaloff>:/icons/icons/edit-copy.png</normaloff>:/icons/icons/edit-copy.png</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>&Copy</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>Ctrl+C</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionPaste">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../resources/resources.qrc">
|
||||||
|
<normaloff>:/icons/icons/edit-paste.png</normaloff>:/icons/icons/edit-paste.png</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>&Paste</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>Ctrl+V</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionSelectAll">
|
||||||
|
<property name="text">
|
||||||
|
<string>Select &All</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>Ctrl+A</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionDelete">
|
||||||
|
<property name="text"><string>&Delete</string></property>
|
||||||
|
<property name="shortcut"><string>Del</string></property>
|
||||||
|
</action>
|
||||||
|
<action name="actionAbout">
|
||||||
|
<property name="text">
|
||||||
|
<string>&About BEdit</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionSettings">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Settings…</string>
|
||||||
|
</property>
|
||||||
|
<property name="statusTip">
|
||||||
|
<string>Configure BEdit</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionAboutQt">
|
||||||
|
<property name="text">
|
||||||
|
<string>About &Qt</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
</widget>
|
||||||
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>GraphWorkspaceView</class>
|
||||||
|
<extends>QGraphicsView</extends>
|
||||||
|
<header>bedit.workspace.view</header>
|
||||||
|
</customwidget>
|
||||||
|
</customwidgets>
|
||||||
|
<resources>
|
||||||
|
<include location="../resources/resources.qrc"/>
|
||||||
|
</resources>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
||||||
170
BEdit/ui/settings_dialog.ui
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>SettingsDialog</class>
|
||||||
|
<widget class="QDialog" name="SettingsDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>480</width>
|
||||||
|
<height>300</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Settings</string>
|
||||||
|
</property>
|
||||||
|
<property name="modal">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="dialogLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QTabWidget" name="settingsTabs">
|
||||||
|
<property name="currentIndex">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<widget class="QWidget" name="generalTab">
|
||||||
|
<attribute name="title">
|
||||||
|
<string>General</string>
|
||||||
|
</attribute>
|
||||||
|
<layout class="QVBoxLayout" name="generalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="autosaveGroupBox">
|
||||||
|
<property name="title">
|
||||||
|
<string>Automatic saving</string>
|
||||||
|
</property>
|
||||||
|
<property name="checkable">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="checked">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<layout class="QFormLayout" name="autosaveLayout">
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QSpinBox" name="autosaveIntervalSpinBox">
|
||||||
|
<property name="suffix">
|
||||||
|
<string> minutes</string>
|
||||||
|
</property>
|
||||||
|
<property name="minimum">
|
||||||
|
<number>1</number>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<number>120</number>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>5</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="generalSpacer">
|
||||||
|
<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>
|
||||||
|
</layout>
|
||||||
|
</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>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Orientation::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<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>SettingsDialog</receiver>
|
||||||
|
<slot>accept()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>390</x>
|
||||||
|
<y>275</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>240</x>
|
||||||
|
<y>150</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>rejected()</signal>
|
||||||
|
<receiver>SettingsDialog</receiver>
|
||||||
|
<slot>reject()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>390</x>
|
||||||
|
<y>275</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>240</x>
|
||||||
|
<y>150</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
||||||