Compare commits
5 Commits
8df5c2a8f9
...
4ab74f64e2
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ab74f64e2 | |||
| ee859c4311 | |||
| 8fa450d734 | |||
| 48a2b4c8d0 | |||
| 1a47952358 |
2
BEdit/.gitignore
vendored
@@ -8,3 +8,5 @@ dist/
|
|||||||
.idea/
|
.idea/
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/tasks.json
|
!.vscode/tasks.json
|
||||||
|
.ruff_cache
|
||||||
|
ui/*_ui.py
|
||||||
|
|||||||
61
BEdit/.vscode/tasks.json
vendored
@@ -2,11 +2,11 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"label": "Qt: Open Main Window in Designer",
|
"label": "Qt: Open Designer",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "pyside6-designer",
|
"command": "pyside6-designer",
|
||||||
"args": [
|
"args": [
|
||||||
"${workspaceFolder}/ui/main_window.ui"
|
"${workspaceFolder}/ui/*.ui"
|
||||||
],
|
],
|
||||||
"options": {
|
"options": {
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
@@ -21,26 +21,6 @@
|
|||||||
"panel": "dedicated"
|
"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",
|
"label": "Qt: Compile Resources to Python",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
@@ -48,7 +28,7 @@
|
|||||||
"args": [
|
"args": [
|
||||||
"${workspaceFolder}/resources/resources.qrc",
|
"${workspaceFolder}/resources/resources.qrc",
|
||||||
"-o",
|
"-o",
|
||||||
"${workspaceFolder}/src/bedit/resources_rc.py"
|
"${workspaceFolder}/src/bedit/gui/generated/resources_rc.py"
|
||||||
],
|
],
|
||||||
"options": {
|
"options": {
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
@@ -68,7 +48,7 @@
|
|||||||
"--from-imports",
|
"--from-imports",
|
||||||
"${workspaceFolder}/ui/main_window.ui",
|
"${workspaceFolder}/ui/main_window.ui",
|
||||||
"-o",
|
"-o",
|
||||||
"${workspaceFolder}/src/bedit/ui_main_window.py"
|
"${workspaceFolder}/src/bedit/gui/generated/ui_main_window.py"
|
||||||
],
|
],
|
||||||
"options": {
|
"options": {
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
@@ -87,7 +67,7 @@
|
|||||||
"--from-imports",
|
"--from-imports",
|
||||||
"${workspaceFolder}/ui/settings_dialog.ui",
|
"${workspaceFolder}/ui/settings_dialog.ui",
|
||||||
"-o",
|
"-o",
|
||||||
"${workspaceFolder}/src/bedit/ui_settings_dialog.py"
|
"${workspaceFolder}/src/bedit/gui/generated/ui_settings_dialog.py"
|
||||||
],
|
],
|
||||||
"options": {
|
"options": {
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
@@ -106,12 +86,36 @@
|
|||||||
"--from-imports",
|
"--from-imports",
|
||||||
"${workspaceFolder}/ui/component_options_dialog.ui",
|
"${workspaceFolder}/ui/component_options_dialog.ui",
|
||||||
"-o",
|
"-o",
|
||||||
"${workspaceFolder}/src/bedit/ui_component_options_dialog.py"
|
"${workspaceFolder}/src/bedit/gui/generated/ui_component_options_dialog.py"
|
||||||
],
|
],
|
||||||
"options": {"cwd": "${workspaceFolder}"},
|
"options": {"cwd": "${workspaceFolder}"},
|
||||||
"problemMatcher": [],
|
"problemMatcher": [],
|
||||||
"presentation": {"reveal": "silent", "panel": "shared"}
|
"presentation": {"reveal": "silent", "panel": "shared"}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "Qt: Compile Port Options UI to Python",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "pyside6-uic",
|
||||||
|
"args": ["--from-imports", "${workspaceFolder}/ui/port_options_dialog.ui", "-o", "${workspaceFolder}/src/bedit/gui/generated/ui_port_options_dialog.py"],
|
||||||
|
"options": {"cwd": "${workspaceFolder}"},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Qt: Compile Shape Options UI to Python",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "pyside6-uic",
|
||||||
|
"args": ["--from-imports", "${workspaceFolder}/ui/shape_options_dialog.ui", "-o", "${workspaceFolder}/src/bedit/gui/generated/ui_shape_options_dialog.py"],
|
||||||
|
"options": {"cwd": "${workspaceFolder}"},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Qt: Compile Icon Editor UI to Python",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "pyside6-uic",
|
||||||
|
"args": ["--from-imports", "${workspaceFolder}/ui/icon_editor_dialog.ui", "-o", "${workspaceFolder}/src/bedit/gui/generated/ui_icon_editor_dialog.py"],
|
||||||
|
"options": {"cwd": "${workspaceFolder}"},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "Qt: Build Designer Files",
|
"label": "Qt: Build Designer Files",
|
||||||
"dependsOrder": "sequence",
|
"dependsOrder": "sequence",
|
||||||
@@ -119,7 +123,10 @@
|
|||||||
"Qt: Compile Resources to Python",
|
"Qt: Compile Resources to Python",
|
||||||
"Qt: Compile UI to Python",
|
"Qt: Compile UI to Python",
|
||||||
"Qt: Compile Settings UI to Python",
|
"Qt: Compile Settings UI to Python",
|
||||||
"Qt: Compile Component Options UI to Python"
|
"Qt: Compile Component Options UI to Python",
|
||||||
|
"Qt: Compile Port Options UI to Python",
|
||||||
|
"Qt: Compile Shape Options UI to Python",
|
||||||
|
"Qt: Compile Icon Editor UI to Python"
|
||||||
],
|
],
|
||||||
"problemMatcher": [],
|
"problemMatcher": [],
|
||||||
"group": {
|
"group": {
|
||||||
|
|||||||
215
BEdit/AGENTS.md
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
This file is the working guide for AI coding agents contributing to BEdit.
|
||||||
|
Its instructions apply to the entire repository.
|
||||||
|
|
||||||
|
## Project purpose
|
||||||
|
|
||||||
|
BEdit is a Python/PySide6 graphical editor for hierarchical block and bond-graph
|
||||||
|
models. A document contains reusable components, typed and oriented ports,
|
||||||
|
connections, nested graphs, and editable vector icons.
|
||||||
|
|
||||||
|
The application is evolving quickly. Prefer small, coherent changes that improve
|
||||||
|
the architecture without introducing compatibility layers unless compatibility is
|
||||||
|
explicitly requested.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The main boundary is strict:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/bedit/
|
||||||
|
├── __main__.py
|
||||||
|
├── core/ # Pure Python; must not import PySide6
|
||||||
|
│ ├── model.py # Document, component, graph, port, icon data
|
||||||
|
│ ├── port_types.py # Port type definitions and compatibility
|
||||||
|
│ ├── serializer.py # JSON persistence
|
||||||
|
│ └── libraries.py # Library file discovery and parsing
|
||||||
|
└── gui/ # All Qt-dependent code
|
||||||
|
├── app.py # QApplication startup and palette
|
||||||
|
├── main_window.py # Top-level UI orchestration
|
||||||
|
├── preferences.py # Explicit disk-backed QSettings factory
|
||||||
|
├── controllers/ # Document controller and undo commands
|
||||||
|
├── dialogs/ # Dialog behavior
|
||||||
|
├── graphics/ # Workspace, icon editor, vector rendering
|
||||||
|
├── models/ # Qt tree models and repository adapters
|
||||||
|
└── generated/ # Generated UI/resource Python; never hand-edit
|
||||||
|
```
|
||||||
|
|
||||||
|
Dependency direction:
|
||||||
|
|
||||||
|
- `bedit.core` may depend only on the Python standard library.
|
||||||
|
- `bedit.gui` may depend on `bedit.core` and PySide6.
|
||||||
|
- `bedit.core` must never import `bedit.gui` or PySide6.
|
||||||
|
- Data parsing and validation belong in `core`.
|
||||||
|
- Signals, widgets, painting, Qt models, undo integration, and settings UI belong
|
||||||
|
in `gui`.
|
||||||
|
|
||||||
|
Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
|
||||||
|
`gui` subpackage rather than the package root.
|
||||||
|
|
||||||
|
## Important behavior and design decisions
|
||||||
|
|
||||||
|
- Components may contain nested graph or text implementations.
|
||||||
|
- Ports retain stable IDs. Display names, types, orientation, positions, and icon
|
||||||
|
anchors may change without changing IDs.
|
||||||
|
- Port orientation is presented as one unified list in the UI, while the model
|
||||||
|
indexes inputs and outputs separately for connection semantics.
|
||||||
|
- Port types are registered in `core/port_types.py`. Only compatible types may be
|
||||||
|
connected. `signal` is currently the only type.
|
||||||
|
- Port removal or reorientation must be rejected when it would invalidate an
|
||||||
|
existing connection.
|
||||||
|
- Connections reference port IDs, never port names.
|
||||||
|
- Graph interaction has separate Pointer and Connect modes. Connections store a
|
||||||
|
`properties.routing` value (`direct`, `angled`, or `spline`); angled routes
|
||||||
|
store absolute scene points in `properties.waypoints`.
|
||||||
|
- Connection appearance is configured per port type in
|
||||||
|
`gui/graphics/connection_styles.py`, including color, width, pen style, and
|
||||||
|
source/target arrowheads. Do not scatter those constants through painters.
|
||||||
|
- Graph annotations are `box`, `line`, or `text` objects in `Graph.annotations`.
|
||||||
|
They use integer layers below or above graph layer 0. Annotation lines reuse
|
||||||
|
connection routing and absolute `properties.waypoints` semantics. Graph
|
||||||
|
annotations and icon elements share `ShapeOptionsDialog` and `shape_pen` so
|
||||||
|
their style fields and rendering must remain aligned.
|
||||||
|
- Icon editing uses a fixed 128×128 coordinate space.
|
||||||
|
- The visible/selectable component hitbox is calculated from vector elements,
|
||||||
|
not from the complete 128×128 icon canvas.
|
||||||
|
- New component icons default to a centered 64×64 shape.
|
||||||
|
- Vector elements include rectangles, circles, ellipses, lines, triangles, and
|
||||||
|
text. Shape styling and geometry are stored in document JSON.
|
||||||
|
- Icon port anchors are stored in `Port.properties["iconPosition"]`.
|
||||||
|
- Workspace grid size, workspace snapping size, and icon grid size are separate
|
||||||
|
persisted settings.
|
||||||
|
- Settings use `gui.preferences.application_settings()` and are stored under the
|
||||||
|
explicit `BEdit/BEdit` identity. Do not create anonymous `QSettings()` objects.
|
||||||
|
- Avoid the QSettings group name `general`; Qt treats `General` specially in INI
|
||||||
|
files. Autosave keys live under `autosave/`.
|
||||||
|
- User-visible document edits should participate in undo/redo.
|
||||||
|
|
||||||
|
## Qt Designer and generated files
|
||||||
|
|
||||||
|
Editable Designer sources are in `ui/`. Resources are defined in
|
||||||
|
`resources/resources.qrc`.
|
||||||
|
|
||||||
|
Never hand-edit files in `src/bedit/gui/generated/`. Modify the corresponding
|
||||||
|
`.ui` or `.qrc` source and regenerate instead.
|
||||||
|
|
||||||
|
Use the configured VS Code task **Qt: Build Designer Files**, or run the relevant
|
||||||
|
commands directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pyside6-rcc resources/resources.qrc \
|
||||||
|
-o src/bedit/gui/generated/resources_rc.py
|
||||||
|
|
||||||
|
pyside6-uic --from-imports ui/main_window.ui \
|
||||||
|
-o src/bedit/gui/generated/ui_main_window.py
|
||||||
|
|
||||||
|
pyside6-uic --from-imports ui/settings_dialog.ui \
|
||||||
|
-o src/bedit/gui/generated/ui_settings_dialog.py
|
||||||
|
|
||||||
|
pyside6-uic --from-imports ui/component_options_dialog.ui \
|
||||||
|
-o src/bedit/gui/generated/ui_component_options_dialog.py
|
||||||
|
|
||||||
|
pyside6-uic --from-imports ui/port_options_dialog.ui \
|
||||||
|
-o src/bedit/gui/generated/ui_port_options_dialog.py
|
||||||
|
|
||||||
|
pyside6-uic --from-imports ui/shape_options_dialog.ui \
|
||||||
|
-o src/bedit/gui/generated/ui_shape_options_dialog.py
|
||||||
|
|
||||||
|
pyside6-uic --from-imports ui/icon_editor_dialog.ui \
|
||||||
|
-o src/bedit/gui/generated/ui_icon_editor_dialog.py
|
||||||
|
```
|
||||||
|
|
||||||
|
When adding a promoted/custom widget in Designer, its header must use the real
|
||||||
|
Python module path, for example `bedit.gui.graphics.workspace`.
|
||||||
|
|
||||||
|
Substantial windows and dialogs must have a Designer `.ui` source. Python classes
|
||||||
|
bind behavior and data but must not reconstruct or replace those layouts at
|
||||||
|
runtime. A tiny generic prompt with one field and OK/Cancel may remain code-only.
|
||||||
|
|
||||||
|
## Editing conventions
|
||||||
|
|
||||||
|
- Preserve stable document IDs and existing connections.
|
||||||
|
- Validate candidate document changes before pushing an undo command.
|
||||||
|
- Keep model serialization symmetrical: additions to `to_dict()` require matching
|
||||||
|
handling in `from_dict()` and cloning where relevant.
|
||||||
|
- Use descriptive domain names; avoid generic `utils.py` modules.
|
||||||
|
- Prefer focused classes and helpers over growing `main_window.py` further.
|
||||||
|
- Shared vector calculations that do not require Qt belong in `core`; Qt painter
|
||||||
|
code belongs in `gui/graphics`.
|
||||||
|
- Do not silently swallow malformed document data. Raise a useful `ValueError` in
|
||||||
|
`core`, then present it through the GUI layer.
|
||||||
|
- Preserve unrelated user changes. The worktree may already be dirty.
|
||||||
|
- Do not delete or overwrite library/document JSON unless the requested workflow
|
||||||
|
explicitly calls for it.
|
||||||
|
|
||||||
|
## Document and library files
|
||||||
|
|
||||||
|
- Documents use the `bedit-document` JSON format.
|
||||||
|
- `test.bedit.json` is a useful manually created example during development.
|
||||||
|
- Library documents use the same recursive document model.
|
||||||
|
- Library parsing belongs in `core/libraries.py`; Qt change notifications belong
|
||||||
|
in `gui/models/library_repository.py`.
|
||||||
|
- Package library data, if present, belongs under `src/bedit/data/libraries/`.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
There is not yet a complete automated test suite. For every change, run at least:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m compileall -q src
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Run imports with the source tree explicitly available:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=src python3 -c "import bedit.core; import bedit.gui.app"
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify the backend remains Qt-free after core changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=src python3 - <<'PY'
|
||||||
|
import sys
|
||||||
|
import bedit.core
|
||||||
|
assert not any(name.startswith("PySide6") for name in sys.modules)
|
||||||
|
PY
|
||||||
|
```
|
||||||
|
|
||||||
|
For model changes, add a focused in-memory round-trip check using
|
||||||
|
`GraphDocument.to_dict()` and `GraphDocument.from_dict()`. For controller changes,
|
||||||
|
exercise undo and redo when applicable.
|
||||||
|
|
||||||
|
GUI smoke tests may fail in headless environments because the system Qt platform
|
||||||
|
theme tries to access a display even with an offscreen platform. Do not claim an
|
||||||
|
interactive GUI test passed unless a display-capable environment was actually
|
||||||
|
used. Compilation, imports, and pure model/controller checks remain useful.
|
||||||
|
|
||||||
|
If Ruff is installed, also run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ruff check src
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the application
|
||||||
|
|
||||||
|
Install the package in editable mode or run it from the source tree:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=src python3 -m bedit
|
||||||
|
```
|
||||||
|
|
||||||
|
The installed GUI entry point is `bedit.gui.app:main`.
|
||||||
|
|
||||||
|
## Completion checklist
|
||||||
|
|
||||||
|
Before handing off a change:
|
||||||
|
|
||||||
|
1. Confirm the `core`/`gui` dependency boundary is intact.
|
||||||
|
2. Confirm generated files were not hand-edited.
|
||||||
|
3. Check serialization and cloning for model changes.
|
||||||
|
4. Check undo/redo for document mutations.
|
||||||
|
5. Run compilation and `git diff --check`.
|
||||||
|
6. Report any GUI behavior that could not be tested interactively.
|
||||||
|
7. Update README or this file if the architecture or workflow changed.
|
||||||
@@ -36,12 +36,18 @@ After installation, the `bedit` command also launches the application.
|
|||||||
.
|
.
|
||||||
├── pyproject.toml dependencies, package metadata, and `bedit` command
|
├── pyproject.toml dependencies, package metadata, and `bedit` command
|
||||||
├── README.md
|
├── README.md
|
||||||
├── ui/main_window.ui editable Qt Designer source
|
├── ui/ editable Qt Designer sources
|
||||||
└── src/bedit
|
└── src/bedit
|
||||||
├── __main__.py supports `python -m bedit`
|
├── __main__.py minimal `python -m bedit` entry point
|
||||||
├── app.py starts Qt and applies the forced light palette
|
├── core/ Qt-free domain model, port types, files, libraries
|
||||||
├── main_window.py behavior and signal connections
|
└── gui/ every PySide-dependent module
|
||||||
└── ui_main_window.py generated from the Designer file; do not hand-edit
|
├── app.py Qt startup and application palette
|
||||||
|
├── main_window.py top-level UI orchestration
|
||||||
|
├── controllers/ document controller and undo commands
|
||||||
|
├── dialogs/ dialog behavior
|
||||||
|
├── graphics/ graph workspace and vector icon editor
|
||||||
|
├── models/ Qt tree models and repository adapters
|
||||||
|
└── generated/ generated UI/resources; do not hand-edit
|
||||||
```
|
```
|
||||||
|
|
||||||
## Designing the UI further
|
## Designing the UI further
|
||||||
@@ -61,13 +67,19 @@ Save the form, close the running application if necessary, then regenerate its
|
|||||||
Python wrapper:
|
Python wrapper:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pyside6-uic ui/main_window.ui -o src/bedit/ui_main_window.py
|
pyside6-uic --from-imports ui/main_window.ui \
|
||||||
|
-o src/bedit/gui/generated/ui_main_window.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Do not hand-edit the generated Python file; change the `.ui` file and regenerate
|
Do not hand-edit files in `gui/generated`; change the `.ui` source and regenerate
|
||||||
it. Add behavior and signal connections in `main_window.py`. Widget names from
|
it. Add behavior and signal connections in `gui/main_window.py`. Widget names from
|
||||||
Designer are available there through `self.ui`, such as `self.ui.graphView`.
|
Designer are available there through `self.ui`, such as `self.ui.graphView`.
|
||||||
|
|
||||||
|
Substantial views are all represented in Designer: the main window, settings,
|
||||||
|
component options, port options, shape options, and icon editor. Python binds
|
||||||
|
data and behavior to those forms; it does not rebuild their layouts at runtime.
|
||||||
|
Only tiny generic prompts may be code-only.
|
||||||
|
|
||||||
In VS Code, the same commands are available through **Terminal → Run Task**:
|
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: Open Main Window in Designer** opens the form for visual editing.
|
||||||
@@ -76,16 +88,13 @@ In VS Code, the same commands are available through **Terminal → Run Task**:
|
|||||||
- The separate resource and UI compilation tasks remain available when only one
|
- The separate resource and UI compilation tasks remain available when only one
|
||||||
generated file needs rebuilding.
|
generated file needs rebuilding.
|
||||||
|
|
||||||
## A sensible next design pass
|
## Architecture rules
|
||||||
|
|
||||||
1. Sketch the main tasks and screens before choosing widgets.
|
- `bedit.core` must remain importable without PySide6.
|
||||||
2. Turn each major area into its own widget class in `src/bedit/widgets/`.
|
- `bedit.gui` may depend on `core`; `core` must never import `gui`.
|
||||||
3. Use a `QStackedWidget` for page-like navigation, or `QDockWidget` for movable
|
- Designer output and resource output belong only in `gui/generated`.
|
||||||
tool panels in an editor-style application.
|
- Domain serialization and library parsing stay in `core`; Qt signals, models,
|
||||||
4. Use reusable `QAction` objects for menu commands and any future toolbars.
|
undo integration, painting, and widgets stay in `gui`.
|
||||||
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
|
## Graph and library prototype
|
||||||
|
|
||||||
@@ -101,26 +110,38 @@ BEdit document used as a copy source. The built-in example defines A, B, and C.
|
|||||||
- Components, interface terminals, and connections are selectable. Use a rubber
|
- Components, interface terminals, and connections are selectable. Use a rubber
|
||||||
band or Ctrl-click for multiple selection, Delete to remove items, and the
|
band or Ctrl-click for multiple selection, Delete to remove items, and the
|
||||||
standard Cut/Copy/Paste shortcuts to duplicate selected component groups.
|
standard Cut/Copy/Paste shortcuts to duplicate selected component groups.
|
||||||
- Click an output port and then an input port to create a connection.
|
- Switch the graph header to **Connect**, choose Direct, Angled, or Spline, then
|
||||||
- Double-click a graph component to open its owned subgraph; use **Up** to return.
|
click an output and input. Angled connections accept intermediate corner
|
||||||
|
clicks, use right-angle segments, and snap to the graph snapping grid.
|
||||||
|
Right-click cancels an unfinished connection.
|
||||||
|
- Select a routed connection to reveal its draggable nodes. Right-click a line
|
||||||
|
to add a node, or right-click a node to delete it.
|
||||||
|
- Use **Box**, **Line**, and **Text** to add persistent graph annotations. Lines
|
||||||
|
share the Direct, Angled, and Spline routing controls. Annotation context menus
|
||||||
|
provide the same shape styling as the icon editor and can move annotations
|
||||||
|
through integer layers below or above graph layer 0.
|
||||||
|
- Double-click a graph component or select it and use **Down** to open its owned
|
||||||
|
subgraph; use **Up** to return.
|
||||||
- Graph components show **Pointer**, **Input**, and **Output** tools. Select an
|
- Graph components show **Pointer**, **Input**, and **Output** tools. Select an
|
||||||
interface tool and click the canvas to add a visible internal terminal and a
|
interface tool and click the canvas to add a visible internal terminal and a
|
||||||
corresponding external block port. Interface terminals can be moved afterward.
|
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,
|
- 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
|
icon shape, icon text, fill color, and border color. The same dialog can hide
|
||||||
that component's contained subtree from the Libraries tree.
|
that component's contained subtree from the Libraries tree.
|
||||||
|
- The icon editor uses Pointer and click-drag drawing tools. Its toolbar and
|
||||||
|
mouse wheel provide zoom in, zoom out, and fit-to-canvas controls.
|
||||||
- Double-click a text component to edit its input list, output list, and
|
- Double-click a text component to edit its input list, output list, and
|
||||||
`implementation.source` JSON.
|
`implementation.source` JSON.
|
||||||
- Right-click any graph component under Current Document to add nested graph or
|
- Right-click any graph component under Current Document to add nested graph or
|
||||||
text blocks. Any current-document component can also be deleted there.
|
text blocks. Any current-document component can also be deleted there.
|
||||||
- The active document hierarchy has its own Document panel; the Libraries panel
|
- The left navigator presents the current Document and external Libraries as tabs.
|
||||||
contains only configured external libraries.
|
- Select one or more blocks and press `Ctrl+R`, or use **Rotate** in the graph header, to
|
||||||
- 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.
|
rotate them clockwise by 90 degrees. Rotation is saved and supports undo/redo.
|
||||||
**Apply JSON** updates that source and participates in undo/redo.
|
**Apply JSON** updates that source and participates in undo/redo.
|
||||||
- File → Save writes the complete recursive document to JSON.
|
- File → Save writes the complete recursive document to JSON.
|
||||||
- File → Close Document removes the active document and returns to an empty
|
- File → Close Document removes the active document and returns to an empty
|
||||||
workspace. An open graph uses a light gray, 32-unit dotted canvas.
|
workspace. An open graph uses a light gray grid whose visible and snapping
|
||||||
|
spacing are configured separately.
|
||||||
- Edit → Settings → Libraries accepts document files or folders of JSON files.
|
- Edit → Settings → Libraries accepts document files or folders of JSON files.
|
||||||
|
|
||||||
Every component owns its ports, declarative icon, properties, and child graph:
|
Every component owns its ports, declarative icon, properties, and child graph:
|
||||||
@@ -169,10 +190,10 @@ never own a graph. Vector icons can contain rectangles, circles, ellipses,
|
|||||||
lines, triangles, and text. Each element owns its geometry, fill, stroke, and
|
lines, triangles, and text. Each element owns its geometry, fill, stroke, and
|
||||||
line style; ports keep their icon anchor in `properties.iconPosition`. The
|
line style; ports keep their icon anchor in `properties.iconPosition`. The
|
||||||
port type registry controls which types may connect (currently `signal` only).
|
port type registry controls which types may connect (currently `signal` only).
|
||||||
Graph and icon grid sizes are configured independently in Settings. The
|
The workspace grid size, its finer snapping size, and the icon grid size are
|
||||||
recursive model is under `src/bedit/document/`, library loading
|
configured independently in Settings. The recursive model and file loading are
|
||||||
and the live Current Document tree are under `src/bedit/library/`, and graphics
|
under `src/bedit/core/`; the live Qt trees and graphics are isolated under
|
||||||
are isolated under `src/bedit/workspace/`.
|
`src/bedit/gui/models/` and `src/bedit/gui/graphics/`.
|
||||||
|
|
||||||
## Optional tools
|
## Optional tools
|
||||||
|
|
||||||
|
|||||||
@@ -1,162 +0,0 @@
|
|||||||
{
|
|
||||||
"format": "bedit-document",
|
|
||||||
"version": 1,
|
|
||||||
"metadata": {
|
|
||||||
"name": "Untitled"
|
|
||||||
},
|
|
||||||
"roots": [
|
|
||||||
{
|
|
||||||
"id": "5ee742db-b25c-4d86-b4ca-cc0e61c4002a",
|
|
||||||
"name": "New Graph Block 1",
|
|
||||||
"position": {
|
|
||||||
"x": 0.0,
|
|
||||||
"y": 0.0
|
|
||||||
},
|
|
||||||
"rotation": 0.0,
|
|
||||||
"interface": {
|
|
||||||
"inputs": [],
|
|
||||||
"outputs": []
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"shape": "rectangle",
|
|
||||||
"fill": "#f4f4f4",
|
|
||||||
"border": "#303030",
|
|
||||||
"text": "Graph",
|
|
||||||
"size": {
|
|
||||||
"width": 128.0,
|
|
||||||
"height": 128.0
|
|
||||||
},
|
|
||||||
"elements": [
|
|
||||||
{
|
|
||||||
"type": "rectangle",
|
|
||||||
"x": 1.0,
|
|
||||||
"y": 1.0,
|
|
||||||
"width": 126.0,
|
|
||||||
"height": 126.0,
|
|
||||||
"fill": "#f4f4f4",
|
|
||||||
"stroke": "#303030",
|
|
||||||
"lineWidth": 1.5,
|
|
||||||
"lineStyle": "solid",
|
|
||||||
"cornerRadius": 5.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"x": 8.0,
|
|
||||||
"y": 8.0,
|
|
||||||
"width": 112.0,
|
|
||||||
"height": 112.0,
|
|
||||||
"text": "Graph",
|
|
||||||
"color": "#202020",
|
|
||||||
"fontSize": 12.0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"properties": {},
|
|
||||||
"library": {
|
|
||||||
"showSubtree": true
|
|
||||||
},
|
|
||||||
"implementation": {
|
|
||||||
"kind": "graph",
|
|
||||||
"graph": {
|
|
||||||
"blocks": [
|
|
||||||
{
|
|
||||||
"id": "89944479-e73e-446a-8d58-ebf35ac4144b",
|
|
||||||
"name": "New Graph Block 1",
|
|
||||||
"position": {
|
|
||||||
"x": 0.0,
|
|
||||||
"y": -96.0
|
|
||||||
},
|
|
||||||
"rotation": 0.0,
|
|
||||||
"interface": {
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"id": "port-e5cb81a5",
|
|
||||||
"name": "Port 1",
|
|
||||||
"position": {
|
|
||||||
"x": 0.0,
|
|
||||||
"y": 0.0
|
|
||||||
},
|
|
||||||
"properties": {
|
|
||||||
"iconPosition": {
|
|
||||||
"x": 32.0,
|
|
||||||
"y": 64.0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "signal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"id": "port-eb0034af",
|
|
||||||
"name": "Port 2",
|
|
||||||
"position": {
|
|
||||||
"x": 0.0,
|
|
||||||
"y": 0.0
|
|
||||||
},
|
|
||||||
"properties": {
|
|
||||||
"iconPosition": {
|
|
||||||
"x": 96.0,
|
|
||||||
"y": 64.0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "signal"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"shape": "rectangle",
|
|
||||||
"fill": "#f4f4f4",
|
|
||||||
"border": "#303030",
|
|
||||||
"text": "Graph",
|
|
||||||
"size": {
|
|
||||||
"width": 128.0,
|
|
||||||
"height": 128.0
|
|
||||||
},
|
|
||||||
"elements": [
|
|
||||||
{
|
|
||||||
"type": "rectangle",
|
|
||||||
"x": 32.0,
|
|
||||||
"y": 32.0,
|
|
||||||
"width": 64.0,
|
|
||||||
"height": 64.0,
|
|
||||||
"fill": "#f4f4f4",
|
|
||||||
"stroke": "#303030",
|
|
||||||
"lineWidth": 1.5,
|
|
||||||
"lineStyle": "solid",
|
|
||||||
"cornerRadius": 5.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"x": 40.0,
|
|
||||||
"y": 40.0,
|
|
||||||
"width": 48.0,
|
|
||||||
"height": 48.0,
|
|
||||||
"text": "Graph",
|
|
||||||
"color": "#303030",
|
|
||||||
"fontSize": 12.0,
|
|
||||||
"lineStyle": "solid",
|
|
||||||
"lineWidth": 1.5,
|
|
||||||
"stroke": "#303030",
|
|
||||||
"fill": "#ffffff"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"properties": {},
|
|
||||||
"library": {
|
|
||||||
"showSubtree": true
|
|
||||||
},
|
|
||||||
"implementation": {
|
|
||||||
"kind": "graph",
|
|
||||||
"graph": {
|
|
||||||
"blocks": [],
|
|
||||||
"connections": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"connections": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -19,7 +19,7 @@ dev = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[project.gui-scripts]
|
[project.gui-scripts]
|
||||||
bedit = "bedit.app:main"
|
bedit = "bedit.gui.app:main"
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["src"]
|
where = ["src"]
|
||||||
|
|||||||
BIN
BEdit/resources/icons/arrow-down.png
Normal file
|
After Width: | Height: | Size: 1006 B |
BIN
BEdit/resources/icons/arrow-up.png
Normal file
|
After Width: | Height: | Size: 927 B |
BIN
BEdit/resources/icons/draw-bezier-curves.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
BEdit/resources/icons/draw-rectangle.png
Normal file
|
After Width: | Height: | Size: 658 B |
BIN
BEdit/resources/icons/draw-text.png
Normal file
|
After Width: | Height: | Size: 598 B |
BIN
BEdit/resources/icons/edit-select.png
Normal file
|
After Width: | Height: | Size: 991 B |
BIN
BEdit/resources/icons/zoom-in.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
BEdit/resources/icons/zoom-original.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
BEdit/resources/icons/zoom-out.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
@@ -1,14 +1,25 @@
|
|||||||
<RCC>
|
<RCC>
|
||||||
<qresource prefix="icons">
|
<qresource prefix="icons">
|
||||||
|
<file>icons/arrow-down.png</file>
|
||||||
|
<file>icons/configure.png</file>
|
||||||
|
<file>icons/arrow-up.png</file>
|
||||||
|
<file>icons/document-new.png</file>
|
||||||
|
<file>icons/document-open.png</file>
|
||||||
|
<file>icons/document-save-as.png</file>
|
||||||
|
<file>icons/document-save.png</file>
|
||||||
|
<file>icons/draw-bezier-curves.png</file>
|
||||||
|
<file>icons/draw-rectangle.png</file>
|
||||||
|
<file>icons/draw-text.png</file>
|
||||||
|
<file>icons/edit-copy.png</file>
|
||||||
|
<file>icons/edit-cut.png</file>
|
||||||
<file>icons/edit-paste.png</file>
|
<file>icons/edit-paste.png</file>
|
||||||
<file>icons/edit-redo.png</file>
|
<file>icons/edit-redo.png</file>
|
||||||
<file>icons/edit-cut.png</file>
|
<file>icons/edit-select.png</file>
|
||||||
<file>icons/edit-copy.png</file>
|
|
||||||
<file>icons/edit-undo.png</file>
|
<file>icons/edit-undo.png</file>
|
||||||
<file>icons/document-save.png</file>
|
<file>icons/media-playback-start.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>
|
<file>icons/transform-rotate.png</file>
|
||||||
|
<file>icons/zoom-in.png</file>
|
||||||
|
<file>icons/zoom-original.png</file>
|
||||||
|
<file>icons/zoom-out.png</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from bedit.app import main
|
from bedit.gui.app import main
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
raise SystemExit(main())
|
||||||
|
|
||||||
|
|||||||
5
BEdit/src/bedit/core/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""Pure BEdit domain model and persistence; this package has no Qt dependency."""
|
||||||
|
|
||||||
|
from bedit.core.model import Component, Connection, Endpoint, Graph, GraphDocument, Icon, Port
|
||||||
|
|
||||||
|
__all__ = ["Component", "Connection", "Endpoint", "Graph", "GraphDocument", "Icon", "Port"]
|
||||||
32
BEdit/src/bedit/core/libraries.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import json
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from bedit.core.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())]
|
||||||
|
|
||||||
|
|
||||||
|
def load_library_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))
|
||||||
|
|
||||||
|
|
||||||
|
def library_candidates(path: Path) -> list[Path]:
|
||||||
|
return sorted(path.glob("*.json")) if path.is_dir() else [path]
|
||||||
@@ -5,7 +5,7 @@ from dataclasses import dataclass, field
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from bedit.document.port_types import PortTypeRegistry
|
from bedit.core.port_types import PortTypeRegistry
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -51,18 +51,33 @@ class Icon:
|
|||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if not self.elements:
|
if not self.elements:
|
||||||
self.elements = [{
|
self.elements = [
|
||||||
|
{
|
||||||
"type": "ellipse" if self.shape == "ellipse" else "rectangle",
|
"type": "ellipse" if self.shape == "ellipse" else "rectangle",
|
||||||
"x": 32.0, "y": 32.0, "width": 64.0, "height": 64.0,
|
"x": 32.0,
|
||||||
"fill": self.fill, "stroke": self.border, "lineWidth": 1.5,
|
"y": 32.0,
|
||||||
"lineStyle": "solid", "cornerRadius": 5.0,
|
"width": 64.0,
|
||||||
}]
|
"height": 64.0,
|
||||||
|
"fill": self.fill,
|
||||||
|
"stroke": self.border,
|
||||||
|
"lineWidth": 1.5,
|
||||||
|
"lineStyle": "solid",
|
||||||
|
"cornerRadius": 5.0,
|
||||||
|
}
|
||||||
|
]
|
||||||
if self.text:
|
if self.text:
|
||||||
self.elements.append({
|
self.elements.append(
|
||||||
"type": "text", "x": 40.0, "y": 40.0,
|
{
|
||||||
"width": 48.0, "height": 48.0,
|
"type": "text",
|
||||||
"text": self.text, "color": "#202020", "fontSize": 12.0,
|
"x": 40.0,
|
||||||
})
|
"y": 40.0,
|
||||||
|
"width": 48.0,
|
||||||
|
"height": 48.0,
|
||||||
|
"text": self.text,
|
||||||
|
"color": "#202020",
|
||||||
|
"fontSize": 12.0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, Any]:
|
def to_dict(self) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
@@ -137,15 +152,57 @@ class Connection:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Annotation:
|
||||||
|
id: str
|
||||||
|
kind: str
|
||||||
|
x: float = 0.0
|
||||||
|
y: float = 0.0
|
||||||
|
width: float = 0.0
|
||||||
|
height: float = 0.0
|
||||||
|
text: str = ""
|
||||||
|
layer: int = -1
|
||||||
|
properties: dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"kind": self.kind,
|
||||||
|
"position": {"x": self.x, "y": self.y},
|
||||||
|
"size": {"width": self.width, "height": self.height},
|
||||||
|
"text": self.text,
|
||||||
|
"layer": self.layer,
|
||||||
|
"properties": deepcopy(self.properties),
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: dict[str, Any]) -> "Annotation":
|
||||||
|
position = data.get("position", {})
|
||||||
|
size = data.get("size", {})
|
||||||
|
return cls(
|
||||||
|
id=str(data["id"]),
|
||||||
|
kind=str(data["kind"]),
|
||||||
|
x=float(position.get("x", 0)),
|
||||||
|
y=float(position.get("y", 0)),
|
||||||
|
width=float(size.get("width", 0)),
|
||||||
|
height=float(size.get("height", 0)),
|
||||||
|
text=str(data.get("text", "")),
|
||||||
|
layer=int(data.get("layer", -1)),
|
||||||
|
properties=deepcopy(data.get("properties", {})),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Graph:
|
class Graph:
|
||||||
blocks: dict[str, Component] = field(default_factory=dict)
|
blocks: dict[str, Component] = field(default_factory=dict)
|
||||||
connections: dict[str, Connection] = field(default_factory=dict)
|
connections: dict[str, Connection] = field(default_factory=dict)
|
||||||
|
annotations: dict[str, Annotation] = field(default_factory=dict)
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, Any]:
|
def to_dict(self) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"blocks": [block.to_dict() for block in self.blocks.values()],
|
"blocks": [block.to_dict() for block in self.blocks.values()],
|
||||||
"connections": [connection.to_dict() for connection in self.connections.values()],
|
"connections": [connection.to_dict() for connection in self.connections.values()],
|
||||||
|
"annotations": [item.to_dict() for item in self.annotations.values()],
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -153,13 +210,19 @@ class Graph:
|
|||||||
data = data or {}
|
data = data or {}
|
||||||
blocks = [Component.from_dict(item) for item in data.get("blocks", [])]
|
blocks = [Component.from_dict(item) for item in data.get("blocks", [])]
|
||||||
connections = [Connection.from_dict(item) for item in data.get("connections", [])]
|
connections = [Connection.from_dict(item) for item in data.get("connections", [])]
|
||||||
|
annotations = [Annotation.from_dict(item) for item in data.get("annotations", [])]
|
||||||
if len({block.id for block in blocks}) != len(blocks):
|
if len({block.id for block in blocks}) != len(blocks):
|
||||||
raise ValueError("A graph contains duplicate component IDs")
|
raise ValueError("A graph contains duplicate component IDs")
|
||||||
if len({connection.id for connection in connections}) != len(connections):
|
if len({connection.id for connection in connections}) != len(connections):
|
||||||
raise ValueError("A graph contains duplicate connection IDs")
|
raise ValueError("A graph contains duplicate connection IDs")
|
||||||
|
if len({item.id for item in annotations}) != len(annotations):
|
||||||
|
raise ValueError("A graph contains duplicate annotation IDs")
|
||||||
|
if any(item.kind not in {"box", "line", "text"} for item in annotations):
|
||||||
|
raise ValueError("A graph contains an unknown annotation kind")
|
||||||
return cls(
|
return cls(
|
||||||
blocks={block.id: block for block in blocks},
|
blocks={block.id: block for block in blocks},
|
||||||
connections={connection.id: connection for connection in connections},
|
connections={connection.id: connection for connection in connections},
|
||||||
|
annotations={item.id: item for item in annotations},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -349,14 +412,35 @@ def clone_component(source: Component) -> Component:
|
|||||||
for connection in current.graph.connections.values()
|
for connection in current.graph.connections.values()
|
||||||
for new_id in [str(uuid4())]
|
for new_id in [str(uuid4())]
|
||||||
},
|
},
|
||||||
|
annotations={
|
||||||
|
new_id: Annotation(
|
||||||
|
new_id,
|
||||||
|
annotation.kind,
|
||||||
|
annotation.x,
|
||||||
|
annotation.y,
|
||||||
|
annotation.width,
|
||||||
|
annotation.height,
|
||||||
|
annotation.text,
|
||||||
|
annotation.layer,
|
||||||
|
deepcopy(annotation.properties),
|
||||||
|
)
|
||||||
|
for annotation in current.graph.annotations.values()
|
||||||
|
for new_id in [str(uuid4())]
|
||||||
|
},
|
||||||
)
|
)
|
||||||
return Component(
|
return Component(
|
||||||
id=str(uuid4()),
|
id=str(uuid4()),
|
||||||
name=current.name,
|
name=current.name,
|
||||||
x=current.x,
|
x=current.x,
|
||||||
y=current.y,
|
y=current.y,
|
||||||
inputs=[Port(port.id, port.name, port.x, port.y, deepcopy(port.properties), port.type) for port in current.inputs],
|
inputs=[
|
||||||
outputs=[Port(port.id, port.name, port.x, port.y, deepcopy(port.properties), port.type) for port in current.outputs],
|
Port(port.id, port.name, port.x, port.y, deepcopy(port.properties), port.type)
|
||||||
|
for port in current.inputs
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
Port(port.id, port.name, port.x, port.y, deepcopy(port.properties), port.type)
|
||||||
|
for port in current.outputs
|
||||||
|
],
|
||||||
icon=Icon.from_dict(current.icon.to_dict()),
|
icon=Icon.from_dict(current.icon.to_dict()),
|
||||||
properties=deepcopy(current.properties),
|
properties=deepcopy(current.properties),
|
||||||
implementation_kind=current.implementation_kind,
|
implementation_kind=current.implementation_kind,
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from bedit.document.model import GraphDocument
|
from bedit.core.model import GraphDocument
|
||||||
|
|
||||||
|
|
||||||
class JsonDocumentSerializer:
|
class JsonDocumentSerializer:
|
||||||
@@ -20,4 +20,3 @@ class JsonDocumentSerializer:
|
|||||||
json.dump(document.to_dict(), file, indent=2)
|
json.dump(document.to_dict(), file, indent=2)
|
||||||
file.write("\n")
|
file.write("\n")
|
||||||
temporary_path.replace(path)
|
temporary_path.replace(path)
|
||||||
|
|
||||||
212
BEdit/src/bedit/data/libraries/example.json
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
{
|
||||||
|
"format": "bedit-document",
|
||||||
|
"version": 1,
|
||||||
|
"metadata": {
|
||||||
|
"name": "Example"
|
||||||
|
},
|
||||||
|
"roots": [
|
||||||
|
{
|
||||||
|
"id": "bf714517-e7b2-4f4b-8b53-55642e3beb28",
|
||||||
|
"name": "A",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 0.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"interface": {
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"id": "port-5c5d4695",
|
||||||
|
"name": "Port 1",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 0.0
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"iconPosition": {
|
||||||
|
"x": 32.0,
|
||||||
|
"y": 64.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "signal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"id": "port-de109124",
|
||||||
|
"name": "Port 2",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 0.0
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"iconPosition": {
|
||||||
|
"x": 96.0,
|
||||||
|
"y": 64.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "signal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"shape": "rectangle",
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"border": "#303030",
|
||||||
|
"text": "Text",
|
||||||
|
"size": {
|
||||||
|
"width": 128.0,
|
||||||
|
"height": 128.0
|
||||||
|
},
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "rectangle",
|
||||||
|
"x": 32.0,
|
||||||
|
"y": 32.0,
|
||||||
|
"width": 64.0,
|
||||||
|
"height": 64.0,
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"stroke": "#303030",
|
||||||
|
"lineWidth": 1.5,
|
||||||
|
"lineStyle": "solid",
|
||||||
|
"cornerRadius": 5.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"x": 40.0,
|
||||||
|
"y": 40.0,
|
||||||
|
"width": 48.0,
|
||||||
|
"height": 48.0,
|
||||||
|
"text": "A",
|
||||||
|
"color": "#303030",
|
||||||
|
"fontSize": 12.0,
|
||||||
|
"lineStyle": "solid",
|
||||||
|
"lineWidth": 1.5,
|
||||||
|
"stroke": "#303030",
|
||||||
|
"fill": "#ffffff"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"properties": {},
|
||||||
|
"library": {
|
||||||
|
"showSubtree": true
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"kind": "text",
|
||||||
|
"source": {
|
||||||
|
"equations": [],
|
||||||
|
"parameters": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "73c9d0c0-293d-4a55-8b89-a1f095dfa75f",
|
||||||
|
"name": "B",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 0.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"interface": {
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"id": "port-4f732b3e",
|
||||||
|
"name": "Port 1",
|
||||||
|
"position": {
|
||||||
|
"x": -176.0,
|
||||||
|
"y": -144.0
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"iconPosition": {
|
||||||
|
"x": 32.0,
|
||||||
|
"y": 48.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "signal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "port-ef7c4218",
|
||||||
|
"name": "Port 2",
|
||||||
|
"position": {
|
||||||
|
"x": -176.0,
|
||||||
|
"y": -16.0
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"iconPosition": {
|
||||||
|
"x": 32.0,
|
||||||
|
"y": 80.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "signal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"id": "port-b68679b2",
|
||||||
|
"name": "Port 3",
|
||||||
|
"position": {
|
||||||
|
"x": 128.0,
|
||||||
|
"y": -80.0
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"iconPosition": {
|
||||||
|
"x": 96.0,
|
||||||
|
"y": 64.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "signal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"shape": "rectangle",
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"border": "#303030",
|
||||||
|
"text": "Graph",
|
||||||
|
"size": {
|
||||||
|
"width": 128.0,
|
||||||
|
"height": 128.0
|
||||||
|
},
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "rectangle",
|
||||||
|
"x": 32.0,
|
||||||
|
"y": 32.0,
|
||||||
|
"width": 64.0,
|
||||||
|
"height": 64.0,
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"stroke": "#303030",
|
||||||
|
"lineWidth": 1.5,
|
||||||
|
"lineStyle": "solid",
|
||||||
|
"cornerRadius": 5.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"x": 40.0,
|
||||||
|
"y": 40.0,
|
||||||
|
"width": 48.0,
|
||||||
|
"height": 48.0,
|
||||||
|
"text": "B",
|
||||||
|
"color": "#303030",
|
||||||
|
"fontSize": 12.0,
|
||||||
|
"lineStyle": "solid",
|
||||||
|
"lineWidth": 1.5,
|
||||||
|
"stroke": "#303030",
|
||||||
|
"fill": "#ffffff"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"properties": {},
|
||||||
|
"library": {
|
||||||
|
"showSubtree": true
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"kind": "graph",
|
||||||
|
"graph": {
|
||||||
|
"blocks": [],
|
||||||
|
"connections": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
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",
|
|
||||||
]
|
|
||||||
1
BEdit/src/bedit/gui/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Qt user interface and application adapters."""
|
||||||
@@ -4,7 +4,7 @@ from PySide6.QtCore import QCoreApplication, Qt
|
|||||||
from PySide6.QtGui import QColor, QPalette
|
from PySide6.QtGui import QColor, QPalette
|
||||||
from PySide6.QtWidgets import QApplication, QStyleFactory
|
from PySide6.QtWidgets import QApplication, QStyleFactory
|
||||||
|
|
||||||
from bedit.main_window import MainWindow
|
from bedit.gui.main_window import MainWindow
|
||||||
|
|
||||||
|
|
||||||
def apply_light_theme(app: QApplication) -> None:
|
def apply_light_theme(app: QApplication) -> None:
|
||||||
@@ -39,11 +39,11 @@ def apply_light_theme(app: QApplication) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
app = QApplication(sys.argv)
|
QCoreApplication.setApplicationName("BEdit")
|
||||||
app.setApplicationName("BEdit")
|
QCoreApplication.setOrganizationName("BEdit")
|
||||||
app.setApplicationDisplayName("BEdit")
|
|
||||||
app.setOrganizationName("BEdit")
|
|
||||||
QCoreApplication.setApplicationVersion("0.1.0")
|
QCoreApplication.setApplicationVersion("0.1.0")
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
app.setApplicationDisplayName("BEdit")
|
||||||
|
|
||||||
apply_light_theme(app)
|
apply_light_theme(app)
|
||||||
window = MainWindow()
|
window = MainWindow()
|
||||||
5
BEdit/src/bedit/gui/controllers/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""Qt-aware application controllers and undo commands."""
|
||||||
|
|
||||||
|
from bedit.gui.controllers.document import DocumentController
|
||||||
|
|
||||||
|
__all__ = ["DocumentController"]
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from PySide6.QtCore import QPointF
|
from PySide6.QtCore import QPointF
|
||||||
from PySide6.QtGui import QUndoCommand
|
from PySide6.QtGui import QUndoCommand
|
||||||
|
|
||||||
from bedit.document.model import Component, Connection, Port
|
from bedit.core.model import Annotation, Component, Connection, Port
|
||||||
|
|
||||||
|
|
||||||
class AddComponentCommand(QUndoCommand):
|
class AddComponentCommand(QUndoCommand):
|
||||||
@@ -76,6 +76,55 @@ class AddConnectionCommand(QUndoCommand):
|
|||||||
self.controller._remove_connection(self.owner_id, self.connection.id)
|
self.controller._remove_connection(self.owner_id, self.connection.id)
|
||||||
|
|
||||||
|
|
||||||
|
class AddAnnotationCommand(QUndoCommand):
|
||||||
|
def __init__(self, controller, owner_id: str, annotation: Annotation) -> None:
|
||||||
|
super().__init__(f"Draw {annotation.kind}")
|
||||||
|
self.controller, self.owner_id, self.annotation = controller, owner_id, annotation
|
||||||
|
|
||||||
|
def redo(self) -> None:
|
||||||
|
self.controller._insert_annotation(self.owner_id, self.annotation)
|
||||||
|
|
||||||
|
def undo(self) -> None:
|
||||||
|
self.controller._remove_annotation(self.owner_id, self.annotation.id)
|
||||||
|
|
||||||
|
|
||||||
|
class EditGraphItemCommand(QUndoCommand):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
controller,
|
||||||
|
owner_id: str,
|
||||||
|
item_kind: str,
|
||||||
|
item_id: str,
|
||||||
|
old: dict,
|
||||||
|
new: dict,
|
||||||
|
text: str,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(text)
|
||||||
|
self.controller, self.owner_id = controller, owner_id
|
||||||
|
self.item_kind, self.item_id = item_kind, item_id
|
||||||
|
self.old, self.new = old, new
|
||||||
|
|
||||||
|
def redo(self) -> None:
|
||||||
|
self.controller._set_graph_item_data(self.owner_id, self.item_kind, self.item_id, self.new)
|
||||||
|
|
||||||
|
def undo(self) -> None:
|
||||||
|
self.controller._set_graph_item_data(self.owner_id, self.item_kind, self.item_id, self.old)
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteAnnotationsCommand(QUndoCommand):
|
||||||
|
def __init__(self, controller, owner_id: str, annotations: dict[str, Annotation]) -> None:
|
||||||
|
super().__init__("Delete annotations")
|
||||||
|
self.controller, self.owner_id, self.annotations = controller, owner_id, annotations
|
||||||
|
|
||||||
|
def redo(self) -> None:
|
||||||
|
for annotation_id in self.annotations:
|
||||||
|
self.controller._remove_annotation(self.owner_id, annotation_id)
|
||||||
|
|
||||||
|
def undo(self) -> None:
|
||||||
|
for annotation in self.annotations.values():
|
||||||
|
self.controller._insert_annotation(self.owner_id, annotation)
|
||||||
|
|
||||||
|
|
||||||
class ReplaceComponentCommand(QUndoCommand):
|
class ReplaceComponentCommand(QUndoCommand):
|
||||||
def __init__(self, controller, old: Component, new: Component) -> None:
|
def __init__(self, controller, old: Component, new: Component) -> None:
|
||||||
super().__init__("Apply JSON changes")
|
super().__init__("Apply JSON changes")
|
||||||
@@ -5,11 +5,14 @@ from uuid import uuid4
|
|||||||
from PySide6.QtCore import QObject, QPointF, Signal
|
from PySide6.QtCore import QObject, QPointF, Signal
|
||||||
from PySide6.QtGui import QUndoStack
|
from PySide6.QtGui import QUndoStack
|
||||||
|
|
||||||
from bedit.document.commands import (
|
from bedit.gui.controllers.commands import (
|
||||||
AddComponentCommand,
|
AddComponentCommand,
|
||||||
AddConnectionCommand,
|
AddConnectionCommand,
|
||||||
|
AddAnnotationCommand,
|
||||||
AddInterfacePortCommand,
|
AddInterfacePortCommand,
|
||||||
DeleteSelectionCommand,
|
DeleteSelectionCommand,
|
||||||
|
DeleteAnnotationsCommand,
|
||||||
|
EditGraphItemCommand,
|
||||||
EditTextDefinitionCommand,
|
EditTextDefinitionCommand,
|
||||||
EditComponentAppearanceCommand,
|
EditComponentAppearanceCommand,
|
||||||
MoveComponentCommand,
|
MoveComponentCommand,
|
||||||
@@ -20,7 +23,8 @@ from bedit.document.commands import (
|
|||||||
ReplaceSourceCommand,
|
ReplaceSourceCommand,
|
||||||
RotateComponentsCommand,
|
RotateComponentsCommand,
|
||||||
)
|
)
|
||||||
from bedit.document.model import (
|
from bedit.core.model import (
|
||||||
|
Annotation,
|
||||||
Component,
|
Component,
|
||||||
Connection,
|
Connection,
|
||||||
Endpoint,
|
Endpoint,
|
||||||
@@ -29,8 +33,8 @@ from bedit.document.model import (
|
|||||||
Port,
|
Port,
|
||||||
clone_component,
|
clone_component,
|
||||||
)
|
)
|
||||||
from bedit.document.port_types import PortTypeRegistry
|
from bedit.core.port_types import PortTypeRegistry
|
||||||
from bedit.document.serializer import JsonDocumentSerializer
|
from bedit.core.serializer import JsonDocumentSerializer
|
||||||
|
|
||||||
|
|
||||||
class DocumentController(QObject):
|
class DocumentController(QObject):
|
||||||
@@ -43,6 +47,9 @@ class DocumentController(QObject):
|
|||||||
componentRotated = Signal(str, float)
|
componentRotated = Signal(str, float)
|
||||||
connectionAdded = Signal(str)
|
connectionAdded = Signal(str)
|
||||||
connectionRemoved = Signal(str)
|
connectionRemoved = Signal(str)
|
||||||
|
graphItemChanged = Signal(str, str)
|
||||||
|
annotationAdded = Signal(str)
|
||||||
|
annotationRemoved = Signal(str)
|
||||||
interfaceChanged = Signal()
|
interfaceChanged = Signal()
|
||||||
filePathChanged = Signal(object)
|
filePathChanged = Signal(object)
|
||||||
modifiedChanged = Signal(bool)
|
modifiedChanged = Signal(bool)
|
||||||
@@ -221,11 +228,16 @@ class DocumentController(QObject):
|
|||||||
if (component := self.active_component.graph.blocks.get(component_id)) is not None
|
if (component := self.active_component.graph.blocks.get(component_id)) is not None
|
||||||
}
|
}
|
||||||
if rotations:
|
if rotations:
|
||||||
self.undo_stack.push(
|
self.undo_stack.push(RotateComponentsCommand(self, self.active_component_id, rotations))
|
||||||
RotateComponentsCommand(self, self.active_component_id, rotations)
|
|
||||||
)
|
|
||||||
|
|
||||||
def connect(self, source: Endpoint, target: Endpoint) -> str:
|
def connect(
|
||||||
|
self,
|
||||||
|
source: Endpoint,
|
||||||
|
target: Endpoint,
|
||||||
|
*,
|
||||||
|
routing: str = "angled",
|
||||||
|
waypoints: list[QPointF] | None = None,
|
||||||
|
) -> str:
|
||||||
if self.active_component_id is None:
|
if self.active_component_id is None:
|
||||||
raise ValueError("There is no active graph")
|
raise ValueError("There is no active graph")
|
||||||
source_port = self._port_for_endpoint(source, "source")
|
source_port = self._port_for_endpoint(source, "source")
|
||||||
@@ -233,15 +245,163 @@ class DocumentController(QObject):
|
|||||||
if source_port is None or target_port is None:
|
if source_port is None or target_port is None:
|
||||||
raise ValueError("A connection endpoint no longer exists")
|
raise ValueError("A connection endpoint no longer exists")
|
||||||
if not PortTypeRegistry.compatible(source_port.type, target_port.type):
|
if not PortTypeRegistry.compatible(source_port.type, target_port.type):
|
||||||
raise ValueError(
|
raise ValueError(f"Cannot connect {source_port.type!r} to {target_port.type!r}")
|
||||||
f"Cannot connect {source_port.type!r} to {target_port.type!r}"
|
if routing not in {"direct", "angled", "spline"}:
|
||||||
)
|
raise ValueError(f"Unknown connection routing: {routing}")
|
||||||
connection = Connection(str(uuid4()), source, target)
|
connection = Connection(
|
||||||
self.undo_stack.push(
|
str(uuid4()),
|
||||||
AddConnectionCommand(self, self.active_component_id, connection)
|
source,
|
||||||
|
target,
|
||||||
|
properties={
|
||||||
|
"routing": routing,
|
||||||
|
"waypoints": [{"x": point.x(), "y": point.y()} for point in (waypoints or [])],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
self.undo_stack.push(AddConnectionCommand(self, self.active_component_id, connection))
|
||||||
return connection.id
|
return connection.id
|
||||||
|
|
||||||
|
def add_annotation(
|
||||||
|
self,
|
||||||
|
kind: str,
|
||||||
|
start: QPointF,
|
||||||
|
end: QPointF,
|
||||||
|
*,
|
||||||
|
text: str = "",
|
||||||
|
routing: str = "angled",
|
||||||
|
waypoints: list[QPointF] | None = None,
|
||||||
|
) -> str:
|
||||||
|
if self.active_component_id is None:
|
||||||
|
raise ValueError("There is no active graph")
|
||||||
|
if kind != "line":
|
||||||
|
left, right = sorted((start.x(), end.x()))
|
||||||
|
top, bottom = sorted((start.y(), end.y()))
|
||||||
|
start, end = QPointF(left, top), QPointF(right, bottom)
|
||||||
|
style = {
|
||||||
|
"stroke": "#303030",
|
||||||
|
"lineWidth": 1.5,
|
||||||
|
"lineStyle": "solid",
|
||||||
|
"fill": "none" if kind in {"line", "text"} else "#dbeafe",
|
||||||
|
}
|
||||||
|
if kind == "box":
|
||||||
|
style["cornerRadius"] = 0.0
|
||||||
|
if kind == "text":
|
||||||
|
style.update({"fontSize": 12.0, "color": "#202020"})
|
||||||
|
annotation = Annotation(
|
||||||
|
id=str(uuid4()),
|
||||||
|
kind=kind,
|
||||||
|
x=start.x(),
|
||||||
|
y=start.y(),
|
||||||
|
width=end.x() - start.x(),
|
||||||
|
height=end.y() - start.y(),
|
||||||
|
text=text,
|
||||||
|
layer=-1,
|
||||||
|
properties={
|
||||||
|
**style,
|
||||||
|
"routing": routing,
|
||||||
|
"waypoints": [{"x": point.x(), "y": point.y()} for point in (waypoints or [])],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.undo_stack.push(AddAnnotationCommand(self, self.active_component_id, annotation))
|
||||||
|
return annotation.id
|
||||||
|
|
||||||
|
def set_route_waypoints(
|
||||||
|
self, item_kind: str, item_id: str, waypoints: list[QPointF], routing: str | None = None
|
||||||
|
) -> None:
|
||||||
|
item = (
|
||||||
|
self.active_graph.connections
|
||||||
|
if item_kind == "connection"
|
||||||
|
else self.active_graph.annotations
|
||||||
|
).get(item_id)
|
||||||
|
if item is None or self.active_component_id is None:
|
||||||
|
return
|
||||||
|
old = deepcopy(item.properties)
|
||||||
|
new = deepcopy(old)
|
||||||
|
if routing is not None:
|
||||||
|
new["routing"] = routing
|
||||||
|
new["waypoints"] = [{"x": point.x(), "y": point.y()} for point in waypoints]
|
||||||
|
if old != new:
|
||||||
|
self.undo_stack.push(
|
||||||
|
EditGraphItemCommand(
|
||||||
|
self, self.active_component_id, item_kind, item_id, old, new, "Edit line nodes"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_annotation_geometry(self, annotation_id: str, old: dict, new: dict) -> None:
|
||||||
|
if old != new and self.active_component_id is not None:
|
||||||
|
self.undo_stack.push(
|
||||||
|
EditGraphItemCommand(
|
||||||
|
self,
|
||||||
|
self.active_component_id,
|
||||||
|
"annotation_geometry",
|
||||||
|
annotation_id,
|
||||||
|
old,
|
||||||
|
new,
|
||||||
|
"Move annotation",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def edit_annotation(self, annotation_id: str, values: dict) -> None:
|
||||||
|
annotation = self.active_graph.annotations.get(annotation_id)
|
||||||
|
if annotation is None or self.active_component_id is None:
|
||||||
|
return
|
||||||
|
old = annotation.to_dict()
|
||||||
|
if old != values:
|
||||||
|
self.undo_stack.push(
|
||||||
|
EditGraphItemCommand(
|
||||||
|
self,
|
||||||
|
self.active_component_id,
|
||||||
|
"annotation_data",
|
||||||
|
annotation_id,
|
||||||
|
old,
|
||||||
|
deepcopy(values),
|
||||||
|
"Edit shape",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def reorder_annotations(self, annotation_ids: set[str], operation: str) -> None:
|
||||||
|
if not annotation_ids or self.active_component_id is None:
|
||||||
|
return
|
||||||
|
graph = self.active_graph
|
||||||
|
layers = [item.layer for item in graph.annotations.values()]
|
||||||
|
minimum, maximum = min(layers, default=-1), max(layers, default=1)
|
||||||
|
for annotation_id in annotation_ids:
|
||||||
|
item = graph.annotations.get(annotation_id)
|
||||||
|
if item is None:
|
||||||
|
continue
|
||||||
|
old = {"layer": item.layer}
|
||||||
|
forward = item.layer + 1
|
||||||
|
backward = item.layer - 1
|
||||||
|
if forward == 0:
|
||||||
|
forward = 1
|
||||||
|
if backward == 0:
|
||||||
|
backward = -1
|
||||||
|
layer = {
|
||||||
|
"forward": forward,
|
||||||
|
"backward": backward,
|
||||||
|
"front": max(1, maximum + 1),
|
||||||
|
"back": min(-1, minimum - 1),
|
||||||
|
}[operation]
|
||||||
|
self.undo_stack.push(
|
||||||
|
EditGraphItemCommand(
|
||||||
|
self,
|
||||||
|
self.active_component_id,
|
||||||
|
"annotation_layer",
|
||||||
|
annotation_id,
|
||||||
|
old,
|
||||||
|
{"layer": layer},
|
||||||
|
"Reorder annotation",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete_annotations(self, annotation_ids: set[str]) -> None:
|
||||||
|
items = {
|
||||||
|
key: self.active_graph.annotations[key]
|
||||||
|
for key in annotation_ids
|
||||||
|
if key in self.active_graph.annotations
|
||||||
|
}
|
||||||
|
if items and self.active_component_id is not None:
|
||||||
|
self.undo_stack.push(DeleteAnnotationsCommand(self, self.active_component_id, items))
|
||||||
|
|
||||||
def _port_for_endpoint(self, endpoint: Endpoint, role: str) -> Port | None:
|
def _port_for_endpoint(self, endpoint: Endpoint, role: str) -> Port | None:
|
||||||
owner = self.active_component
|
owner = self.active_component
|
||||||
if owner is None:
|
if owner is None:
|
||||||
@@ -253,7 +413,13 @@ class DocumentController(QObject):
|
|||||||
if component is None:
|
if component is None:
|
||||||
return None
|
return None
|
||||||
ports = component.outputs if role == "source" else component.inputs
|
ports = component.outputs if role == "source" else component.inputs
|
||||||
return next((port for port in ports if port.id == (endpoint.interface or endpoint.port)), None)
|
return next(
|
||||||
|
(port for port in ports if port.id == (endpoint.interface or endpoint.port)), None
|
||||||
|
)
|
||||||
|
|
||||||
|
def connection_port_type(self, connection: Connection) -> str:
|
||||||
|
port = self._port_for_endpoint(connection.source, "source")
|
||||||
|
return port.type if port is not None else "signal"
|
||||||
|
|
||||||
def add_interface_port(self, direction: str, position: QPointF) -> str:
|
def add_interface_port(self, direction: str, position: QPointF) -> str:
|
||||||
component = self.active_component
|
component = self.active_component
|
||||||
@@ -266,9 +432,7 @@ class DocumentController(QObject):
|
|||||||
x=position.x(),
|
x=position.x(),
|
||||||
y=position.y(),
|
y=position.y(),
|
||||||
)
|
)
|
||||||
self.undo_stack.push(
|
self.undo_stack.push(AddInterfacePortCommand(self, component.id, direction, port))
|
||||||
AddInterfacePortCommand(self, component.id, direction, port)
|
|
||||||
)
|
|
||||||
return port.id
|
return port.id
|
||||||
|
|
||||||
def move_interface_port(self, port_id: str, old: QPointF, new: QPointF) -> None:
|
def move_interface_port(self, port_id: str, old: QPointF, new: QPointF) -> None:
|
||||||
@@ -327,11 +491,17 @@ class DocumentController(QObject):
|
|||||||
parent = self.document.find_parent(component.id)
|
parent = self.document.find_parent(component.id)
|
||||||
if parent is not None:
|
if parent is not None:
|
||||||
for connection in parent.graph.connections.values():
|
for connection in parent.graph.connections.values():
|
||||||
if connection.target.block == component.id and connection.target.port not in input_ids:
|
if (
|
||||||
|
connection.target.block == component.id
|
||||||
|
and connection.target.port not in input_ids
|
||||||
|
):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Input {connection.target.port!r} is still connected in the containing graph"
|
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:
|
if (
|
||||||
|
connection.source.block == component.id
|
||||||
|
and connection.source.port not in output_ids
|
||||||
|
):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Output {connection.source.port!r} is still connected in the containing graph"
|
f"Output {connection.source.port!r} is still connected in the containing graph"
|
||||||
)
|
)
|
||||||
@@ -399,9 +569,15 @@ class DocumentController(QObject):
|
|||||||
parent = self.document.find_parent(component_id)
|
parent = self.document.find_parent(component_id)
|
||||||
if parent is not None:
|
if parent is not None:
|
||||||
for connection in parent.graph.connections.values():
|
for connection in parent.graph.connections.values():
|
||||||
if connection.target.block == component_id and connection.target.port not in input_ids:
|
if (
|
||||||
|
connection.target.block == component_id
|
||||||
|
and connection.target.port not in input_ids
|
||||||
|
):
|
||||||
raise ValueError("An input cannot be removed or reoriented while connected")
|
raise ValueError("An input cannot be removed or reoriented while connected")
|
||||||
if connection.source.block == component_id and connection.source.port not in output_ids:
|
if (
|
||||||
|
connection.source.block == component_id
|
||||||
|
and connection.source.port not in output_ids
|
||||||
|
):
|
||||||
raise ValueError("An output cannot be removed or reoriented while connected")
|
raise ValueError("An output cannot be removed or reoriented while connected")
|
||||||
for connection in component.graph.connections.values():
|
for connection in component.graph.connections.values():
|
||||||
if connection.source.interface and connection.source.interface not in input_ids:
|
if connection.source.interface and connection.source.interface not in input_ids:
|
||||||
@@ -448,7 +624,9 @@ class DocumentController(QObject):
|
|||||||
or connection.target.interface in output_ids
|
or connection.target.interface in output_ids
|
||||||
):
|
):
|
||||||
all_connection_ids.add(connection.id)
|
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}
|
blocks = {
|
||||||
|
block_id: graph.blocks[block_id] for block_id in block_ids if block_id in graph.blocks
|
||||||
|
}
|
||||||
connections = {
|
connections = {
|
||||||
connection_id: graph.connections[connection_id]
|
connection_id: graph.connections[connection_id]
|
||||||
for connection_id in all_connection_ids
|
for connection_id in all_connection_ids
|
||||||
@@ -489,16 +667,21 @@ class DocumentController(QObject):
|
|||||||
for source in source_connections:
|
for source in source_connections:
|
||||||
if source.source.block not in id_map or source.target.block not in id_map:
|
if source.source.block not in id_map or source.target.block not in id_map:
|
||||||
continue
|
continue
|
||||||
|
properties = deepcopy(source.properties)
|
||||||
|
for point in properties.get("waypoints", []):
|
||||||
|
if isinstance(point, dict):
|
||||||
|
point["x"] = float(point.get("x", 0)) + offset.x()
|
||||||
|
point["y"] = float(point.get("y", 0)) + offset.y()
|
||||||
connection = Connection(
|
connection = Connection(
|
||||||
id=str(uuid4()),
|
id=str(uuid4()),
|
||||||
source=Endpoint(block=id_map[source.source.block], port=source.source.port),
|
source=Endpoint(block=id_map[source.source.block], port=source.source.port),
|
||||||
target=Endpoint(block=id_map[source.target.block], port=source.target.port),
|
target=Endpoint(block=id_map[source.target.block], port=source.target.port),
|
||||||
|
name=source.name,
|
||||||
|
properties=properties,
|
||||||
)
|
)
|
||||||
connections[connection.id] = connection
|
connections[connection.id] = connection
|
||||||
if blocks:
|
if blocks:
|
||||||
self.undo_stack.push(
|
self.undo_stack.push(PasteSelectionCommand(self, owner.id, blocks, connections))
|
||||||
PasteSelectionCommand(self, owner.id, blocks, connections)
|
|
||||||
)
|
|
||||||
return list(blocks)
|
return list(blocks)
|
||||||
|
|
||||||
def _graph_for(self, owner_id: str):
|
def _graph_for(self, owner_id: str):
|
||||||
@@ -541,9 +724,7 @@ class DocumentController(QObject):
|
|||||||
self.componentMoved.emit(component_id, position)
|
self.componentMoved.emit(component_id, position)
|
||||||
self.documentReset.emit()
|
self.documentReset.emit()
|
||||||
|
|
||||||
def _rotate_component(
|
def _rotate_component(self, owner_id: str, component_id: str, rotation: float) -> None:
|
||||||
self, owner_id: str, component_id: str, rotation: float
|
|
||||||
) -> None:
|
|
||||||
component = self._graph_for(owner_id).blocks.get(component_id)
|
component = self._graph_for(owner_id).blocks.get(component_id)
|
||||||
if component is None:
|
if component is None:
|
||||||
return
|
return
|
||||||
@@ -563,6 +744,46 @@ class DocumentController(QObject):
|
|||||||
self.connectionRemoved.emit(connection_id)
|
self.connectionRemoved.emit(connection_id)
|
||||||
self.documentReset.emit()
|
self.documentReset.emit()
|
||||||
|
|
||||||
|
def _insert_annotation(self, owner_id: str, annotation: Annotation) -> None:
|
||||||
|
self._graph_for(owner_id).annotations[annotation.id] = annotation
|
||||||
|
if owner_id == self.active_component_id:
|
||||||
|
self.annotationAdded.emit(annotation.id)
|
||||||
|
self.documentReset.emit()
|
||||||
|
|
||||||
|
def _remove_annotation(self, owner_id: str, annotation_id: str) -> None:
|
||||||
|
self._graph_for(owner_id).annotations.pop(annotation_id, None)
|
||||||
|
if owner_id == self.active_component_id:
|
||||||
|
self.annotationRemoved.emit(annotation_id)
|
||||||
|
self.documentReset.emit()
|
||||||
|
|
||||||
|
def _set_graph_item_data(
|
||||||
|
self, owner_id: str, item_kind: str, item_id: str, values: dict
|
||||||
|
) -> None:
|
||||||
|
graph = self._graph_for(owner_id)
|
||||||
|
if item_kind == "connection":
|
||||||
|
item = graph.connections.get(item_id)
|
||||||
|
if item is not None:
|
||||||
|
item.properties = deepcopy(values)
|
||||||
|
else:
|
||||||
|
item = graph.annotations.get(item_id)
|
||||||
|
if item is not None:
|
||||||
|
if item_kind == "annotation_geometry":
|
||||||
|
item.x, item.y = float(values["x"]), float(values["y"])
|
||||||
|
item.width, item.height = float(values["width"]), float(values["height"])
|
||||||
|
elif item_kind == "annotation_layer":
|
||||||
|
item.layer = int(values["layer"])
|
||||||
|
elif item_kind == "annotation_data":
|
||||||
|
replacement = Annotation.from_dict(values)
|
||||||
|
item.kind = replacement.kind
|
||||||
|
item.x, item.y = replacement.x, replacement.y
|
||||||
|
item.width, item.height = replacement.width, replacement.height
|
||||||
|
item.text, item.layer = replacement.text, replacement.layer
|
||||||
|
item.properties = replacement.properties
|
||||||
|
else:
|
||||||
|
item.properties = deepcopy(values)
|
||||||
|
if owner_id == self.active_component_id:
|
||||||
|
self.graphItemChanged.emit(item_kind, item_id)
|
||||||
|
|
||||||
def _replace_component(self, old_id: str, replacement: Component) -> None:
|
def _replace_component(self, old_id: str, replacement: Component) -> None:
|
||||||
if self.document is None:
|
if self.document is None:
|
||||||
return
|
return
|
||||||
1
BEdit/src/bedit/gui/dialogs/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Application dialogs."""
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
from PySide6.QtWidgets import QDialog, QMessageBox, QPushButton
|
from PySide6.QtWidgets import QDialog, QMessageBox
|
||||||
|
|
||||||
from bedit.document.model import Component
|
from bedit.core.model import Component
|
||||||
from bedit.icon_editor import IconEditorDialog
|
from bedit.gui.graphics.icon_editor import IconEditorDialog
|
||||||
from bedit.ui_component_options_dialog import Ui_ComponentOptionsDialog
|
from bedit.gui.generated.ui_component_options_dialog import Ui_ComponentOptionsDialog
|
||||||
|
|
||||||
|
|
||||||
class ComponentOptionsDialog(QDialog):
|
class ComponentOptionsDialog(QDialog):
|
||||||
@@ -15,16 +15,7 @@ class ComponentOptionsDialog(QDialog):
|
|||||||
self.edited_inputs = component.inputs
|
self.edited_inputs = component.inputs
|
||||||
self.edited_outputs = component.outputs
|
self.edited_outputs = component.outputs
|
||||||
self.ui.nameEdit.setText(component.name)
|
self.ui.nameEdit.setText(component.name)
|
||||||
for widget in (
|
self.ui.editIconButton.clicked.connect(self.edit_icon)
|
||||||
self.ui.shapeLabel, self.ui.shapeCombo, self.ui.iconTextLabel,
|
|
||||||
self.ui.iconTextEdit, self.ui.fillLabel, self.ui.fillEdit,
|
|
||||||
self.ui.borderLabel, self.ui.borderEdit,
|
|
||||||
):
|
|
||||||
widget.hide()
|
|
||||||
self.icon_editor_button = QPushButton("Edit Icon…", self)
|
|
||||||
self.icon_editor_button.setToolTip("Open the vector icon and port-position editor")
|
|
||||||
self.icon_editor_button.clicked.connect(self.edit_icon)
|
|
||||||
self.ui.optionsForm.insertRow(1, "Icon:", self.icon_editor_button)
|
|
||||||
self.ui.showSubtreeCheckBox.setChecked(component.show_subtree_in_library)
|
self.ui.showSubtreeCheckBox.setChecked(component.show_subtree_in_library)
|
||||||
|
|
||||||
def edit_icon(self) -> None:
|
def edit_icon(self) -> None:
|
||||||
122
BEdit/src/bedit/gui/dialogs/port_options.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
from copy import deepcopy
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from PySide6.QtCore import Qt
|
||||||
|
from PySide6.QtWidgets import QDialog, QDialogButtonBox, QListWidgetItem, QMessageBox
|
||||||
|
|
||||||
|
from bedit.core.model import Component, Port
|
||||||
|
from bedit.core.port_types import PortTypeRegistry
|
||||||
|
from bedit.gui.generated.ui_port_options_dialog import Ui_PortOptionsDialog
|
||||||
|
|
||||||
|
|
||||||
|
PORT_ROLE = Qt.ItemDataRole.UserRole
|
||||||
|
|
||||||
|
|
||||||
|
class PortOptionsDialog(QDialog):
|
||||||
|
"""Unified editor for a component's typed, oriented ports."""
|
||||||
|
|
||||||
|
def __init__(self, component: Component, parent=None, *, read_only: bool = False) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.ui = Ui_PortOptionsDialog()
|
||||||
|
self.ui.setupUi(self)
|
||||||
|
self.setWindowTitle(f"Port Options — {component.name}")
|
||||||
|
self.ports: list[tuple[Port, str]] = [
|
||||||
|
*((deepcopy(port), "input") for port in component.inputs),
|
||||||
|
*((deepcopy(port), "output") for port in component.outputs),
|
||||||
|
]
|
||||||
|
self._loading = False
|
||||||
|
self.ui.typeCombo.clear()
|
||||||
|
for port_type in PortTypeRegistry.all():
|
||||||
|
self.ui.typeCombo.addItem(port_type.display_name, port_type.id)
|
||||||
|
self.ui.orientationCombo.setItemData(0, "input")
|
||||||
|
self.ui.orientationCombo.setItemData(1, "output")
|
||||||
|
self.ui.portList.currentRowChanged.connect(self._load_current)
|
||||||
|
self.ui.addPortButton.clicked.connect(self.add_port)
|
||||||
|
self.ui.removePortButton.clicked.connect(self.remove_port)
|
||||||
|
self.ui.nameEdit.textEdited.connect(self._store_current)
|
||||||
|
self.ui.typeCombo.currentIndexChanged.connect(self._store_current)
|
||||||
|
self.ui.orientationCombo.currentIndexChanged.connect(self._store_current)
|
||||||
|
self.ui.portSplitter.setSizes([250, 370])
|
||||||
|
if read_only:
|
||||||
|
self.ui.addPortButton.setEnabled(False)
|
||||||
|
self.ui.removePortButton.setEnabled(False)
|
||||||
|
self.ui.nameEdit.setReadOnly(True)
|
||||||
|
self.ui.typeCombo.setEnabled(False)
|
||||||
|
self.ui.orientationCombo.setEnabled(False)
|
||||||
|
self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText("Close")
|
||||||
|
self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).hide()
|
||||||
|
self._rebuild_list(0 if self.ports else -1)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inputs(self) -> list[Port]:
|
||||||
|
return [port for port, orientation in self.ports if orientation == "input"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def outputs(self) -> list[Port]:
|
||||||
|
return [port for port, orientation in self.ports if orientation == "output"]
|
||||||
|
|
||||||
|
def _rebuild_list(self, row: int = -1) -> None:
|
||||||
|
self.ui.portList.clear()
|
||||||
|
for port, orientation in self.ports:
|
||||||
|
item = QListWidgetItem(f"{port.name} [{orientation}, {port.type}]")
|
||||||
|
item.setData(PORT_ROLE, port.id)
|
||||||
|
self.ui.portList.addItem(item)
|
||||||
|
self.ui.portList.setCurrentRow(min(row, len(self.ports) - 1))
|
||||||
|
self._update_enabled()
|
||||||
|
|
||||||
|
def _load_current(self, row: int) -> None:
|
||||||
|
self._loading = True
|
||||||
|
enabled = 0 <= row < len(self.ports)
|
||||||
|
if enabled:
|
||||||
|
port, orientation = self.ports[row]
|
||||||
|
self.ui.nameEdit.setText(port.name)
|
||||||
|
self.ui.typeCombo.setCurrentIndex(self.ui.typeCombo.findData(port.type))
|
||||||
|
self.ui.orientationCombo.setCurrentIndex(self.ui.orientationCombo.findData(orientation))
|
||||||
|
else:
|
||||||
|
self.ui.nameEdit.clear()
|
||||||
|
self._loading = False
|
||||||
|
self._update_enabled()
|
||||||
|
|
||||||
|
def _update_enabled(self) -> None:
|
||||||
|
enabled = self.ui.portList.currentRow() >= 0
|
||||||
|
self.ui.removePortButton.setEnabled(enabled)
|
||||||
|
self.ui.nameEdit.setEnabled(enabled)
|
||||||
|
self.ui.typeCombo.setEnabled(enabled)
|
||||||
|
self.ui.orientationCombo.setEnabled(enabled)
|
||||||
|
|
||||||
|
def _store_current(self) -> None:
|
||||||
|
row = self.ui.portList.currentRow()
|
||||||
|
if self._loading or not (0 <= row < len(self.ports)):
|
||||||
|
return
|
||||||
|
port, _orientation = self.ports[row]
|
||||||
|
port.name = self.ui.nameEdit.text()
|
||||||
|
port.type = self.ui.typeCombo.currentData()
|
||||||
|
self.ports[row] = (port, self.ui.orientationCombo.currentData())
|
||||||
|
self.ui.portList.item(row).setText(
|
||||||
|
f"{port.name} [{self.ports[row][1]}, {port.type}]"
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_port(self) -> None:
|
||||||
|
port = Port(
|
||||||
|
id=f"port-{uuid4().hex[:8]}",
|
||||||
|
name=f"Port {len(self.ports) + 1}",
|
||||||
|
properties={"iconPosition": {"x": 0.0, "y": 0.0}},
|
||||||
|
type="signal",
|
||||||
|
)
|
||||||
|
self.ports.append((port, "input"))
|
||||||
|
self._rebuild_list(len(self.ports) - 1)
|
||||||
|
self.ui.nameEdit.selectAll()
|
||||||
|
self.ui.nameEdit.setFocus()
|
||||||
|
|
||||||
|
def remove_port(self) -> None:
|
||||||
|
row = self.ui.portList.currentRow()
|
||||||
|
if row >= 0:
|
||||||
|
self.ports.pop(row)
|
||||||
|
self._rebuild_list(min(row, len(self.ports) - 1))
|
||||||
|
|
||||||
|
def accept(self) -> None:
|
||||||
|
self._store_current()
|
||||||
|
if any(not port.name.strip() for port, _orientation in self.ports):
|
||||||
|
QMessageBox.warning(self, "Invalid port", "Every port must have a name.")
|
||||||
|
return
|
||||||
|
super().accept()
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from PySide6.QtCore import QSettings, Signal
|
from PySide6.QtCore import QSettings, Signal
|
||||||
from PySide6.QtWidgets import QDialog, QFileDialog, QFormLayout, QGroupBox, QSpinBox
|
from PySide6.QtWidgets import QDialog, QFileDialog
|
||||||
|
|
||||||
from bedit.library.repository import default_library_paths
|
from bedit.core.libraries import default_library_paths
|
||||||
from bedit.ui_settings_dialog import Ui_SettingsDialog
|
from bedit.gui.preferences import application_settings
|
||||||
|
from bedit.gui.generated.ui_settings_dialog import Ui_SettingsDialog
|
||||||
|
|
||||||
|
|
||||||
class SettingsDialog(QDialog):
|
class SettingsDialog(QDialog):
|
||||||
@@ -16,18 +17,10 @@ class SettingsDialog(QDialog):
|
|||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.ui = Ui_SettingsDialog()
|
self.ui = Ui_SettingsDialog()
|
||||||
self.ui.setupUi(self)
|
self.ui.setupUi(self)
|
||||||
self.grid_group = QGroupBox("Editor grids", self.ui.generalTab)
|
self.ui.graphGridSpinBox.valueChanged.connect(
|
||||||
grid_form = QFormLayout(self.grid_group)
|
self.ui.graphSnapSpinBox.setMaximum
|
||||||
self.graph_grid_spin = QSpinBox()
|
)
|
||||||
self.graph_grid_spin.setRange(2, 256)
|
self.settings = application_settings()
|
||||||
self.graph_grid_spin.setSuffix(" units")
|
|
||||||
self.icon_grid_spin = QSpinBox()
|
|
||||||
self.icon_grid_spin.setRange(1, 64)
|
|
||||||
self.icon_grid_spin.setSuffix(" units")
|
|
||||||
grid_form.addRow("Graph grid size:", self.graph_grid_spin)
|
|
||||||
grid_form.addRow("Icon grid size:", self.icon_grid_spin)
|
|
||||||
self.ui.generalLayout.insertWidget(1, self.grid_group)
|
|
||||||
self.settings = QSettings()
|
|
||||||
self.ui.addLibraryFileButton.clicked.connect(self._add_library_file)
|
self.ui.addLibraryFileButton.clicked.connect(self._add_library_file)
|
||||||
self.ui.addLibraryFolderButton.clicked.connect(self._add_library_folder)
|
self.ui.addLibraryFolderButton.clicked.connect(self._add_library_folder)
|
||||||
self.ui.removeLibraryPathButton.clicked.connect(self._remove_library_path)
|
self.ui.removeLibraryPathButton.clicked.connect(self._remove_library_path)
|
||||||
@@ -35,21 +28,32 @@ class SettingsDialog(QDialog):
|
|||||||
self._load_settings()
|
self._load_settings()
|
||||||
|
|
||||||
def _load_settings(self) -> None:
|
def _load_settings(self) -> None:
|
||||||
|
enabled = self.settings.value("autosave/enabled", None)
|
||||||
|
if enabled is None:
|
||||||
|
enabled = self.settings.value("General/autosaveEnabled", False)
|
||||||
|
interval = self.settings.value("autosave/intervalMinutes", None)
|
||||||
|
if interval is None:
|
||||||
|
interval = self.settings.value("General/autosaveInterval", 5)
|
||||||
self.ui.autosaveGroupBox.setChecked(
|
self.ui.autosaveGroupBox.setChecked(
|
||||||
self.settings.value("general/autosaveEnabled", False, type=bool)
|
self._as_bool(enabled)
|
||||||
)
|
|
||||||
self.ui.autosaveIntervalSpinBox.setValue(
|
|
||||||
self.settings.value("general/autosaveInterval", 5, type=int)
|
|
||||||
)
|
)
|
||||||
|
self.ui.autosaveIntervalSpinBox.setValue(int(interval))
|
||||||
self.ui.libraryPathsList.clear()
|
self.ui.libraryPathsList.clear()
|
||||||
self.ui.libraryPathsList.addItems(self.library_paths(self.settings))
|
self.ui.libraryPathsList.addItems(self.library_paths(self.settings))
|
||||||
self.graph_grid_spin.setValue(self.graph_grid_size(self.settings))
|
self.ui.graphGridSpinBox.setValue(self.graph_grid_size(self.settings))
|
||||||
self.icon_grid_spin.setValue(self.icon_grid_size(self.settings))
|
self.ui.graphSnapSpinBox.setValue(self.graph_snap_size(self.settings))
|
||||||
|
self.ui.iconGridSpinBox.setValue(self.icon_grid_size(self.settings))
|
||||||
self._update_remove_button()
|
self._update_remove_button()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _as_bool(value) -> bool:
|
||||||
|
if isinstance(value, str):
|
||||||
|
return value.strip().lower() in {"1", "true", "yes", "on"}
|
||||||
|
return bool(value)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def library_paths(settings: QSettings | None = None) -> list[str]:
|
def library_paths(settings: QSettings | None = None) -> list[str]:
|
||||||
settings = settings or QSettings()
|
settings = settings if settings is not None else application_settings()
|
||||||
value = settings.value("libraries/paths", default_library_paths())
|
value = settings.value("libraries/paths", default_library_paths())
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
return [value]
|
return [value]
|
||||||
@@ -57,11 +61,18 @@ class SettingsDialog(QDialog):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def graph_grid_size(settings: QSettings | None = None) -> int:
|
def graph_grid_size(settings: QSettings | None = None) -> int:
|
||||||
return (settings or QSettings()).value("grid/graphSize", 32, type=int)
|
settings = settings if settings is not None else application_settings()
|
||||||
|
return settings.value("grid/graphSize", 64, type=int)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def graph_snap_size(settings: QSettings | None = None) -> int:
|
||||||
|
settings = settings if settings is not None else application_settings()
|
||||||
|
return settings.value("grid/graphSnapSize", 8, type=int)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def icon_grid_size(settings: QSettings | None = None) -> int:
|
def icon_grid_size(settings: QSettings | None = None) -> int:
|
||||||
return (settings or QSettings()).value("grid/iconSize", 8, type=int)
|
settings = settings if settings is not None else application_settings()
|
||||||
|
return settings.value("grid/iconSize", 8, type=int)
|
||||||
|
|
||||||
def _add_library_file(self) -> None:
|
def _add_library_file(self) -> None:
|
||||||
path, _ = QFileDialog.getOpenFileName(
|
path, _ = QFileDialog.getOpenFileName(
|
||||||
@@ -95,17 +106,20 @@ class SettingsDialog(QDialog):
|
|||||||
self.ui.removeLibraryPathButton.setEnabled(bool(self.ui.libraryPathsList.selectedItems()))
|
self.ui.removeLibraryPathButton.setEnabled(bool(self.ui.libraryPathsList.selectedItems()))
|
||||||
|
|
||||||
def accept(self) -> None:
|
def accept(self) -> None:
|
||||||
self.settings.setValue("general/autosaveEnabled", self.ui.autosaveGroupBox.isChecked())
|
self.settings.setValue("autosave/enabled", self.ui.autosaveGroupBox.isChecked())
|
||||||
self.settings.setValue(
|
self.settings.setValue(
|
||||||
"general/autosaveInterval", self.ui.autosaveIntervalSpinBox.value()
|
"autosave/intervalMinutes", self.ui.autosaveIntervalSpinBox.value()
|
||||||
)
|
)
|
||||||
|
self.settings.remove("General/autosaveEnabled")
|
||||||
|
self.settings.remove("General/autosaveInterval")
|
||||||
paths = [
|
paths = [
|
||||||
self.ui.libraryPathsList.item(row).text()
|
self.ui.libraryPathsList.item(row).text()
|
||||||
for row in range(self.ui.libraryPathsList.count())
|
for row in range(self.ui.libraryPathsList.count())
|
||||||
]
|
]
|
||||||
self.settings.setValue("libraries/paths", paths)
|
self.settings.setValue("libraries/paths", paths)
|
||||||
self.settings.setValue("grid/graphSize", self.graph_grid_spin.value())
|
self.settings.setValue("grid/graphSize", self.ui.graphGridSpinBox.value())
|
||||||
self.settings.setValue("grid/iconSize", self.icon_grid_spin.value())
|
self.settings.setValue("grid/graphSnapSize", self.ui.graphSnapSpinBox.value())
|
||||||
|
self.settings.setValue("grid/iconSize", self.ui.iconGridSpinBox.value())
|
||||||
self.settings.sync()
|
self.settings.sync()
|
||||||
self.settingsChanged.emit()
|
self.settingsChanged.emit()
|
||||||
super().accept()
|
super().accept()
|
||||||
1
BEdit/src/bedit/gui/generated/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Generated Qt code. Do not edit these modules by hand."""
|
||||||
@@ -15,9 +15,9 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
|||||||
QFont, QFontDatabase, QGradient, QIcon,
|
QFont, QFontDatabase, QGradient, QIcon,
|
||||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||||
from PySide6.QtWidgets import (QAbstractButton, QApplication, QCheckBox, QComboBox,
|
from PySide6.QtWidgets import (QAbstractButton, QApplication, QCheckBox, QDialog,
|
||||||
QDialog, QDialogButtonBox, QFormLayout, QLabel,
|
QDialogButtonBox, QFormLayout, QLabel, QLineEdit,
|
||||||
QLineEdit, QSizePolicy, QSpacerItem, QVBoxLayout,
|
QPushButton, QSizePolicy, QSpacerItem, QVBoxLayout,
|
||||||
QWidget)
|
QWidget)
|
||||||
|
|
||||||
class Ui_ComponentOptionsDialog(object):
|
class Ui_ComponentOptionsDialog(object):
|
||||||
@@ -39,53 +39,21 @@ class Ui_ComponentOptionsDialog(object):
|
|||||||
|
|
||||||
self.optionsForm.setWidget(0, QFormLayout.ItemRole.FieldRole, self.nameEdit)
|
self.optionsForm.setWidget(0, QFormLayout.ItemRole.FieldRole, self.nameEdit)
|
||||||
|
|
||||||
self.shapeLabel = QLabel(ComponentOptionsDialog)
|
self.iconLabel = QLabel(ComponentOptionsDialog)
|
||||||
self.shapeLabel.setObjectName(u"shapeLabel")
|
self.iconLabel.setObjectName(u"iconLabel")
|
||||||
|
|
||||||
self.optionsForm.setWidget(1, QFormLayout.ItemRole.LabelRole, self.shapeLabel)
|
self.optionsForm.setWidget(1, QFormLayout.ItemRole.LabelRole, self.iconLabel)
|
||||||
|
|
||||||
self.shapeCombo = QComboBox(ComponentOptionsDialog)
|
self.editIconButton = QPushButton(ComponentOptionsDialog)
|
||||||
self.shapeCombo.addItem("")
|
self.editIconButton.setObjectName(u"editIconButton")
|
||||||
self.shapeCombo.addItem("")
|
|
||||||
self.shapeCombo.setObjectName(u"shapeCombo")
|
|
||||||
|
|
||||||
self.optionsForm.setWidget(1, QFormLayout.ItemRole.FieldRole, self.shapeCombo)
|
self.optionsForm.setWidget(1, QFormLayout.ItemRole.FieldRole, self.editIconButton)
|
||||||
|
|
||||||
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 = QCheckBox(ComponentOptionsDialog)
|
||||||
self.showSubtreeCheckBox.setObjectName(u"showSubtreeCheckBox")
|
self.showSubtreeCheckBox.setObjectName(u"showSubtreeCheckBox")
|
||||||
self.showSubtreeCheckBox.setChecked(True)
|
self.showSubtreeCheckBox.setChecked(True)
|
||||||
|
|
||||||
self.optionsForm.setWidget(5, QFormLayout.ItemRole.SpanningRole, self.showSubtreeCheckBox)
|
self.optionsForm.setWidget(2, QFormLayout.ItemRole.SpanningRole, self.showSubtreeCheckBox)
|
||||||
|
|
||||||
|
|
||||||
self.dialogLayout.addLayout(self.optionsForm)
|
self.dialogLayout.addLayout(self.optionsForm)
|
||||||
@@ -111,15 +79,11 @@ class Ui_ComponentOptionsDialog(object):
|
|||||||
def retranslateUi(self, ComponentOptionsDialog):
|
def retranslateUi(self, ComponentOptionsDialog):
|
||||||
ComponentOptionsDialog.setWindowTitle(QCoreApplication.translate("ComponentOptionsDialog", u"Component Options", None))
|
ComponentOptionsDialog.setWindowTitle(QCoreApplication.translate("ComponentOptionsDialog", u"Component Options", None))
|
||||||
self.nameLabel.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Name:", None))
|
self.nameLabel.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Name:", None))
|
||||||
self.shapeLabel.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Icon shape:", None))
|
self.iconLabel.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Icon:", None))
|
||||||
self.shapeCombo.setItemText(0, QCoreApplication.translate("ComponentOptionsDialog", u"rectangle", None))
|
self.editIconButton.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Edit Icon\u2026", None))
|
||||||
self.shapeCombo.setItemText(1, QCoreApplication.translate("ComponentOptionsDialog", u"ellipse", None))
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.editIconButton.setToolTip(QCoreApplication.translate("ComponentOptionsDialog", u"Open the vector icon and port-position editor", None))
|
||||||
self.iconTextLabel.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Icon text:", None))
|
#endif // QT_CONFIG(tooltip)
|
||||||
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))
|
self.showSubtreeCheckBox.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Show contained components in the Libraries tree", None))
|
||||||
# retranslateUi
|
# retranslateUi
|
||||||
|
|
||||||
160
BEdit/src/bedit/gui/generated/ui_icon_editor_dialog.py
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
## Form generated from reading UI file 'icon_editor_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,
|
||||||
|
QGraphicsView, QHBoxLayout, QLabel, QPushButton,
|
||||||
|
QSizePolicy, QSpacerItem, QToolButton, QVBoxLayout,
|
||||||
|
QWidget)
|
||||||
|
|
||||||
|
from bedit.gui.graphics.icon_canvas import IconCanvasView
|
||||||
|
|
||||||
|
class Ui_IconEditorDialog(object):
|
||||||
|
def setupUi(self, IconEditorDialog):
|
||||||
|
if not IconEditorDialog.objectName():
|
||||||
|
IconEditorDialog.setObjectName(u"IconEditorDialog")
|
||||||
|
IconEditorDialog.resize(850, 600)
|
||||||
|
self.dialogLayout = QVBoxLayout(IconEditorDialog)
|
||||||
|
self.dialogLayout.setObjectName(u"dialogLayout")
|
||||||
|
self.shapeToolbarLayout = QHBoxLayout()
|
||||||
|
self.shapeToolbarLayout.setObjectName(u"shapeToolbarLayout")
|
||||||
|
self.pointerButton = QToolButton(IconEditorDialog)
|
||||||
|
self.pointerButton.setObjectName(u"pointerButton")
|
||||||
|
self.pointerButton.setCheckable(True)
|
||||||
|
self.pointerButton.setChecked(True)
|
||||||
|
|
||||||
|
self.shapeToolbarLayout.addWidget(self.pointerButton)
|
||||||
|
|
||||||
|
self.addShapeLabel = QLabel(IconEditorDialog)
|
||||||
|
self.addShapeLabel.setObjectName(u"addShapeLabel")
|
||||||
|
|
||||||
|
self.shapeToolbarLayout.addWidget(self.addShapeLabel)
|
||||||
|
|
||||||
|
self.addRectangleButton = QToolButton(IconEditorDialog)
|
||||||
|
self.addRectangleButton.setObjectName(u"addRectangleButton")
|
||||||
|
self.addRectangleButton.setCheckable(True)
|
||||||
|
|
||||||
|
self.shapeToolbarLayout.addWidget(self.addRectangleButton)
|
||||||
|
|
||||||
|
self.addCircleButton = QToolButton(IconEditorDialog)
|
||||||
|
self.addCircleButton.setObjectName(u"addCircleButton")
|
||||||
|
self.addCircleButton.setCheckable(True)
|
||||||
|
|
||||||
|
self.shapeToolbarLayout.addWidget(self.addCircleButton)
|
||||||
|
|
||||||
|
self.addEllipseButton = QToolButton(IconEditorDialog)
|
||||||
|
self.addEllipseButton.setObjectName(u"addEllipseButton")
|
||||||
|
self.addEllipseButton.setCheckable(True)
|
||||||
|
|
||||||
|
self.shapeToolbarLayout.addWidget(self.addEllipseButton)
|
||||||
|
|
||||||
|
self.addLineButton = QToolButton(IconEditorDialog)
|
||||||
|
self.addLineButton.setObjectName(u"addLineButton")
|
||||||
|
self.addLineButton.setCheckable(True)
|
||||||
|
|
||||||
|
self.shapeToolbarLayout.addWidget(self.addLineButton)
|
||||||
|
|
||||||
|
self.addTriangleButton = QToolButton(IconEditorDialog)
|
||||||
|
self.addTriangleButton.setObjectName(u"addTriangleButton")
|
||||||
|
self.addTriangleButton.setCheckable(True)
|
||||||
|
|
||||||
|
self.shapeToolbarLayout.addWidget(self.addTriangleButton)
|
||||||
|
|
||||||
|
self.addTextButton = QToolButton(IconEditorDialog)
|
||||||
|
self.addTextButton.setObjectName(u"addTextButton")
|
||||||
|
self.addTextButton.setCheckable(True)
|
||||||
|
|
||||||
|
self.shapeToolbarLayout.addWidget(self.addTextButton)
|
||||||
|
|
||||||
|
self.toolbarSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
||||||
|
|
||||||
|
self.shapeToolbarLayout.addItem(self.toolbarSpacer)
|
||||||
|
|
||||||
|
self.zoomInButton = QToolButton(IconEditorDialog)
|
||||||
|
self.zoomInButton.setObjectName(u"zoomInButton")
|
||||||
|
|
||||||
|
self.shapeToolbarLayout.addWidget(self.zoomInButton)
|
||||||
|
|
||||||
|
self.zoomOutButton = QToolButton(IconEditorDialog)
|
||||||
|
self.zoomOutButton.setObjectName(u"zoomOutButton")
|
||||||
|
|
||||||
|
self.shapeToolbarLayout.addWidget(self.zoomOutButton)
|
||||||
|
|
||||||
|
self.centerButton = QToolButton(IconEditorDialog)
|
||||||
|
self.centerButton.setObjectName(u"centerButton")
|
||||||
|
|
||||||
|
self.shapeToolbarLayout.addWidget(self.centerButton)
|
||||||
|
|
||||||
|
self.deleteSelectedButton = QPushButton(IconEditorDialog)
|
||||||
|
self.deleteSelectedButton.setObjectName(u"deleteSelectedButton")
|
||||||
|
|
||||||
|
self.shapeToolbarLayout.addWidget(self.deleteSelectedButton)
|
||||||
|
|
||||||
|
|
||||||
|
self.dialogLayout.addLayout(self.shapeToolbarLayout)
|
||||||
|
|
||||||
|
self.iconView = IconCanvasView(IconEditorDialog)
|
||||||
|
self.iconView.setObjectName(u"iconView")
|
||||||
|
self.iconView.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
|
||||||
|
|
||||||
|
self.dialogLayout.addWidget(self.iconView)
|
||||||
|
|
||||||
|
self.portHintLabel = QLabel(IconEditorDialog)
|
||||||
|
self.portHintLabel.setObjectName(u"portHintLabel")
|
||||||
|
self.portHintLabel.setWordWrap(True)
|
||||||
|
|
||||||
|
self.dialogLayout.addWidget(self.portHintLabel)
|
||||||
|
|
||||||
|
self.buttonBox = QDialogButtonBox(IconEditorDialog)
|
||||||
|
self.buttonBox.setObjectName(u"buttonBox")
|
||||||
|
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
|
||||||
|
|
||||||
|
self.dialogLayout.addWidget(self.buttonBox)
|
||||||
|
|
||||||
|
|
||||||
|
self.retranslateUi(IconEditorDialog)
|
||||||
|
self.buttonBox.accepted.connect(IconEditorDialog.accept)
|
||||||
|
self.buttonBox.rejected.connect(IconEditorDialog.reject)
|
||||||
|
|
||||||
|
QMetaObject.connectSlotsByName(IconEditorDialog)
|
||||||
|
# setupUi
|
||||||
|
|
||||||
|
def retranslateUi(self, IconEditorDialog):
|
||||||
|
IconEditorDialog.setWindowTitle(QCoreApplication.translate("IconEditorDialog", u"Icon Editor", None))
|
||||||
|
self.pointerButton.setText(QCoreApplication.translate("IconEditorDialog", u"Pointer", None))
|
||||||
|
self.addShapeLabel.setText(QCoreApplication.translate("IconEditorDialog", u"Add:", None))
|
||||||
|
self.addRectangleButton.setText(QCoreApplication.translate("IconEditorDialog", u"Rectangle", None))
|
||||||
|
self.addCircleButton.setText(QCoreApplication.translate("IconEditorDialog", u"Circle", None))
|
||||||
|
self.addEllipseButton.setText(QCoreApplication.translate("IconEditorDialog", u"Ellipse", None))
|
||||||
|
self.addLineButton.setText(QCoreApplication.translate("IconEditorDialog", u"Line", None))
|
||||||
|
self.addTriangleButton.setText(QCoreApplication.translate("IconEditorDialog", u"Triangle", None))
|
||||||
|
self.addTextButton.setText(QCoreApplication.translate("IconEditorDialog", u"Text", None))
|
||||||
|
self.zoomInButton.setText(QCoreApplication.translate("IconEditorDialog", u"+", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.zoomInButton.setToolTip(QCoreApplication.translate("IconEditorDialog", u"Zoom in", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.zoomOutButton.setText(QCoreApplication.translate("IconEditorDialog", u"\u2212", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.zoomOutButton.setToolTip(QCoreApplication.translate("IconEditorDialog", u"Zoom out", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.centerButton.setText(QCoreApplication.translate("IconEditorDialog", u"Fit", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.centerButton.setToolTip(QCoreApplication.translate("IconEditorDialog", u"Fit and center the icon canvas", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.deleteSelectedButton.setText(QCoreApplication.translate("IconEditorDialog", u"Delete selected", None))
|
||||||
|
self.portHintLabel.setText(QCoreApplication.translate("IconEditorDialog", u"Green points are inputs; red points are outputs. Drag them to place connection anchors.", None))
|
||||||
|
# retranslateUi
|
||||||
|
|
||||||
@@ -22,14 +22,14 @@ from PySide6.QtWidgets import (QApplication, QDockWidget, QFrame, QHBoxLayout,
|
|||||||
QSpacerItem, QSplitter, QStackedWidget, QToolBar,
|
QSpacerItem, QSplitter, QStackedWidget, QToolBar,
|
||||||
QToolButton, QTreeView, QVBoxLayout, QWidget)
|
QToolButton, QTreeView, QVBoxLayout, QWidget)
|
||||||
|
|
||||||
from bedit.workspace.view import GraphWorkspaceView
|
from bedit.gui.graphics.workspace import GraphWorkspaceView
|
||||||
from . import resources_rc
|
from . import resources_rc
|
||||||
|
|
||||||
class Ui_MainWindow(object):
|
class Ui_MainWindow(object):
|
||||||
def setupUi(self, MainWindow):
|
def setupUi(self, MainWindow):
|
||||||
if not MainWindow.objectName():
|
if not MainWindow.objectName():
|
||||||
MainWindow.setObjectName(u"MainWindow")
|
MainWindow.setObjectName(u"MainWindow")
|
||||||
MainWindow.resize(1000, 700)
|
MainWindow.resize(1209, 777)
|
||||||
self.actionNew = QAction(MainWindow)
|
self.actionNew = QAction(MainWindow)
|
||||||
self.actionNew.setObjectName(u"actionNew")
|
self.actionNew.setObjectName(u"actionNew")
|
||||||
icon = QIcon()
|
icon = QIcon()
|
||||||
@@ -40,50 +40,65 @@ class Ui_MainWindow(object):
|
|||||||
icon1 = QIcon()
|
icon1 = QIcon()
|
||||||
icon1.addFile(u":/icons/icons/transform-rotate.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon1.addFile(u":/icons/icons/transform-rotate.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.actionRotateClockwise.setIcon(icon1)
|
self.actionRotateClockwise.setIcon(icon1)
|
||||||
|
self.actionZoomIn = QAction(MainWindow)
|
||||||
|
self.actionZoomIn.setObjectName(u"actionZoomIn")
|
||||||
|
icon2 = QIcon()
|
||||||
|
icon2.addFile(u":/icons/icons/zoom-in.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.actionZoomIn.setIcon(icon2)
|
||||||
|
self.actionZoomOut = QAction(MainWindow)
|
||||||
|
self.actionZoomOut.setObjectName(u"actionZoomOut")
|
||||||
|
icon3 = QIcon()
|
||||||
|
icon3.addFile(u":/icons/icons/zoom-out.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.actionZoomOut.setIcon(icon3)
|
||||||
|
self.actionCenterView = QAction(MainWindow)
|
||||||
|
self.actionCenterView.setObjectName(u"actionCenterView")
|
||||||
|
icon4 = QIcon()
|
||||||
|
icon4.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.actionCenterView.setIcon(icon4)
|
||||||
self.actionOpen = QAction(MainWindow)
|
self.actionOpen = QAction(MainWindow)
|
||||||
self.actionOpen.setObjectName(u"actionOpen")
|
self.actionOpen.setObjectName(u"actionOpen")
|
||||||
icon2 = QIcon()
|
icon5 = QIcon()
|
||||||
icon2.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon5.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.actionOpen.setIcon(icon2)
|
self.actionOpen.setIcon(icon5)
|
||||||
self.actionSave = QAction(MainWindow)
|
self.actionSave = QAction(MainWindow)
|
||||||
self.actionSave.setObjectName(u"actionSave")
|
self.actionSave.setObjectName(u"actionSave")
|
||||||
icon3 = QIcon()
|
icon6 = QIcon()
|
||||||
icon3.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon6.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.actionSave.setIcon(icon3)
|
self.actionSave.setIcon(icon6)
|
||||||
self.actionSaveAs = QAction(MainWindow)
|
self.actionSaveAs = QAction(MainWindow)
|
||||||
self.actionSaveAs.setObjectName(u"actionSaveAs")
|
self.actionSaveAs.setObjectName(u"actionSaveAs")
|
||||||
icon4 = QIcon()
|
icon7 = QIcon()
|
||||||
icon4.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon7.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.actionSaveAs.setIcon(icon4)
|
self.actionSaveAs.setIcon(icon7)
|
||||||
self.actionExit = QAction(MainWindow)
|
self.actionExit = QAction(MainWindow)
|
||||||
self.actionExit.setObjectName(u"actionExit")
|
self.actionExit.setObjectName(u"actionExit")
|
||||||
self.actionClose = QAction(MainWindow)
|
self.actionClose = QAction(MainWindow)
|
||||||
self.actionClose.setObjectName(u"actionClose")
|
self.actionClose.setObjectName(u"actionClose")
|
||||||
self.actionUndo = QAction(MainWindow)
|
self.actionUndo = QAction(MainWindow)
|
||||||
self.actionUndo.setObjectName(u"actionUndo")
|
self.actionUndo.setObjectName(u"actionUndo")
|
||||||
icon5 = QIcon()
|
icon8 = QIcon()
|
||||||
icon5.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon8.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.actionUndo.setIcon(icon5)
|
self.actionUndo.setIcon(icon8)
|
||||||
self.actionRedo = QAction(MainWindow)
|
self.actionRedo = QAction(MainWindow)
|
||||||
self.actionRedo.setObjectName(u"actionRedo")
|
self.actionRedo.setObjectName(u"actionRedo")
|
||||||
icon6 = QIcon()
|
icon9 = QIcon()
|
||||||
icon6.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon9.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.actionRedo.setIcon(icon6)
|
self.actionRedo.setIcon(icon9)
|
||||||
self.actionCut = QAction(MainWindow)
|
self.actionCut = QAction(MainWindow)
|
||||||
self.actionCut.setObjectName(u"actionCut")
|
self.actionCut.setObjectName(u"actionCut")
|
||||||
icon7 = QIcon()
|
icon10 = QIcon()
|
||||||
icon7.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon10.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.actionCut.setIcon(icon7)
|
self.actionCut.setIcon(icon10)
|
||||||
self.actionCopy = QAction(MainWindow)
|
self.actionCopy = QAction(MainWindow)
|
||||||
self.actionCopy.setObjectName(u"actionCopy")
|
self.actionCopy.setObjectName(u"actionCopy")
|
||||||
icon8 = QIcon()
|
icon11 = QIcon()
|
||||||
icon8.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon11.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.actionCopy.setIcon(icon8)
|
self.actionCopy.setIcon(icon11)
|
||||||
self.actionPaste = QAction(MainWindow)
|
self.actionPaste = QAction(MainWindow)
|
||||||
self.actionPaste.setObjectName(u"actionPaste")
|
self.actionPaste.setObjectName(u"actionPaste")
|
||||||
icon9 = QIcon()
|
icon12 = QIcon()
|
||||||
icon9.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon12.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.actionPaste.setIcon(icon9)
|
self.actionPaste.setIcon(icon12)
|
||||||
self.actionSelectAll = QAction(MainWindow)
|
self.actionSelectAll = QAction(MainWindow)
|
||||||
self.actionSelectAll.setObjectName(u"actionSelectAll")
|
self.actionSelectAll.setObjectName(u"actionSelectAll")
|
||||||
self.actionDelete = QAction(MainWindow)
|
self.actionDelete = QAction(MainWindow)
|
||||||
@@ -123,7 +138,9 @@ class Ui_MainWindow(object):
|
|||||||
self.librariesLayout.setContentsMargins(0, 0, 0, 0)
|
self.librariesLayout.setContentsMargins(0, 0, 0, 0)
|
||||||
self.treeView = QTreeView(self.dockWidgetContents)
|
self.treeView = QTreeView(self.dockWidgetContents)
|
||||||
self.treeView.setObjectName(u"treeView")
|
self.treeView.setObjectName(u"treeView")
|
||||||
|
self.treeView.setStyleSheet(u"QTreeView::item { height: 32px; }")
|
||||||
self.treeView.setAlternatingRowColors(True)
|
self.treeView.setAlternatingRowColors(True)
|
||||||
|
self.treeView.setIconSize(QSize(28, 28))
|
||||||
self.treeView.setUniformRowHeights(True)
|
self.treeView.setUniformRowHeights(True)
|
||||||
|
|
||||||
self.librariesLayout.addWidget(self.treeView)
|
self.librariesLayout.addWidget(self.treeView)
|
||||||
@@ -142,6 +159,7 @@ class Ui_MainWindow(object):
|
|||||||
self.documentTreeView = QTreeView(self.documentDockContents)
|
self.documentTreeView = QTreeView(self.documentDockContents)
|
||||||
self.documentTreeView.setObjectName(u"documentTreeView")
|
self.documentTreeView.setObjectName(u"documentTreeView")
|
||||||
self.documentTreeView.setAlternatingRowColors(True)
|
self.documentTreeView.setAlternatingRowColors(True)
|
||||||
|
self.documentTreeView.setIconSize(QSize(16, 16))
|
||||||
self.documentTreeView.setUniformRowHeights(True)
|
self.documentTreeView.setUniformRowHeights(True)
|
||||||
|
|
||||||
self.documentPanelLayout.addWidget(self.documentTreeView)
|
self.documentPanelLayout.addWidget(self.documentTreeView)
|
||||||
@@ -170,9 +188,21 @@ class Ui_MainWindow(object):
|
|||||||
self.workspaceHeaderLayout.setContentsMargins(6, 2, 6, 2)
|
self.workspaceHeaderLayout.setContentsMargins(6, 2, 6, 2)
|
||||||
self.navigateUpButton = QToolButton(self.workspaceHeader)
|
self.navigateUpButton = QToolButton(self.workspaceHeader)
|
||||||
self.navigateUpButton.setObjectName(u"navigateUpButton")
|
self.navigateUpButton.setObjectName(u"navigateUpButton")
|
||||||
|
icon13 = QIcon()
|
||||||
|
icon13.addFile(u":/icons/icons/arrow-up.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.navigateUpButton.setIcon(icon13)
|
||||||
|
|
||||||
self.workspaceHeaderLayout.addWidget(self.navigateUpButton)
|
self.workspaceHeaderLayout.addWidget(self.navigateUpButton)
|
||||||
|
|
||||||
|
self.navigateDownButton = QToolButton(self.workspaceHeader)
|
||||||
|
self.navigateDownButton.setObjectName(u"navigateDownButton")
|
||||||
|
self.navigateDownButton.setEnabled(False)
|
||||||
|
icon14 = QIcon()
|
||||||
|
icon14.addFile(u":/icons/icons/arrow-down.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.navigateDownButton.setIcon(icon14)
|
||||||
|
|
||||||
|
self.workspaceHeaderLayout.addWidget(self.navigateDownButton)
|
||||||
|
|
||||||
self.graphBreadcrumbLabel = QLabel(self.workspaceHeader)
|
self.graphBreadcrumbLabel = QLabel(self.workspaceHeader)
|
||||||
self.graphBreadcrumbLabel.setObjectName(u"graphBreadcrumbLabel")
|
self.graphBreadcrumbLabel.setObjectName(u"graphBreadcrumbLabel")
|
||||||
|
|
||||||
@@ -195,25 +225,76 @@ class Ui_MainWindow(object):
|
|||||||
|
|
||||||
self.pointerToolButton = QToolButton(self.workspaceHeader)
|
self.pointerToolButton = QToolButton(self.workspaceHeader)
|
||||||
self.pointerToolButton.setObjectName(u"pointerToolButton")
|
self.pointerToolButton.setObjectName(u"pointerToolButton")
|
||||||
|
icon15 = QIcon()
|
||||||
|
icon15.addFile(u":/icons/icons/edit-select.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.pointerToolButton.setIcon(icon15)
|
||||||
self.pointerToolButton.setCheckable(True)
|
self.pointerToolButton.setCheckable(True)
|
||||||
self.pointerToolButton.setChecked(True)
|
self.pointerToolButton.setChecked(True)
|
||||||
self.pointerToolButton.setAutoExclusive(True)
|
|
||||||
|
|
||||||
self.workspaceHeaderLayout.addWidget(self.pointerToolButton)
|
self.workspaceHeaderLayout.addWidget(self.pointerToolButton)
|
||||||
|
|
||||||
self.inputToolButton = QToolButton(self.workspaceHeader)
|
self.connectToolButton = QToolButton(self.workspaceHeader)
|
||||||
self.inputToolButton.setObjectName(u"inputToolButton")
|
self.connectToolButton.setObjectName(u"connectToolButton")
|
||||||
self.inputToolButton.setCheckable(True)
|
self.connectToolButton.setCheckable(True)
|
||||||
self.inputToolButton.setAutoExclusive(True)
|
|
||||||
|
|
||||||
self.workspaceHeaderLayout.addWidget(self.inputToolButton)
|
self.workspaceHeaderLayout.addWidget(self.connectToolButton)
|
||||||
|
|
||||||
self.outputToolButton = QToolButton(self.workspaceHeader)
|
self.boxToolButton = QToolButton(self.workspaceHeader)
|
||||||
self.outputToolButton.setObjectName(u"outputToolButton")
|
self.boxToolButton.setObjectName(u"boxToolButton")
|
||||||
self.outputToolButton.setCheckable(True)
|
icon16 = QIcon()
|
||||||
self.outputToolButton.setAutoExclusive(True)
|
icon16.addFile(u":/icons/icons/draw-rectangle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.boxToolButton.setIcon(icon16)
|
||||||
|
self.boxToolButton.setCheckable(True)
|
||||||
|
|
||||||
self.workspaceHeaderLayout.addWidget(self.outputToolButton)
|
self.workspaceHeaderLayout.addWidget(self.boxToolButton)
|
||||||
|
|
||||||
|
self.lineToolButton = QToolButton(self.workspaceHeader)
|
||||||
|
self.lineToolButton.setObjectName(u"lineToolButton")
|
||||||
|
icon17 = QIcon()
|
||||||
|
icon17.addFile(u":/icons/icons/draw-bezier-curves.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.lineToolButton.setIcon(icon17)
|
||||||
|
self.lineToolButton.setCheckable(True)
|
||||||
|
|
||||||
|
self.workspaceHeaderLayout.addWidget(self.lineToolButton)
|
||||||
|
|
||||||
|
self.textToolButton = QToolButton(self.workspaceHeader)
|
||||||
|
self.textToolButton.setObjectName(u"textToolButton")
|
||||||
|
icon18 = QIcon()
|
||||||
|
icon18.addFile(u":/icons/icons/draw-text.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.textToolButton.setIcon(icon18)
|
||||||
|
self.textToolButton.setCheckable(True)
|
||||||
|
|
||||||
|
self.workspaceHeaderLayout.addWidget(self.textToolButton)
|
||||||
|
|
||||||
|
self.rotateToolButton = QToolButton(self.workspaceHeader)
|
||||||
|
self.rotateToolButton.setObjectName(u"rotateToolButton")
|
||||||
|
self.rotateToolButton.setIcon(icon1)
|
||||||
|
|
||||||
|
self.workspaceHeaderLayout.addWidget(self.rotateToolButton)
|
||||||
|
|
||||||
|
self.routingLabel = QLabel(self.workspaceHeader)
|
||||||
|
self.routingLabel.setObjectName(u"routingLabel")
|
||||||
|
|
||||||
|
self.workspaceHeaderLayout.addWidget(self.routingLabel)
|
||||||
|
|
||||||
|
self.directRoutingButton = QToolButton(self.workspaceHeader)
|
||||||
|
self.directRoutingButton.setObjectName(u"directRoutingButton")
|
||||||
|
self.directRoutingButton.setCheckable(True)
|
||||||
|
|
||||||
|
self.workspaceHeaderLayout.addWidget(self.directRoutingButton)
|
||||||
|
|
||||||
|
self.angledRoutingButton = QToolButton(self.workspaceHeader)
|
||||||
|
self.angledRoutingButton.setObjectName(u"angledRoutingButton")
|
||||||
|
self.angledRoutingButton.setCheckable(True)
|
||||||
|
self.angledRoutingButton.setChecked(True)
|
||||||
|
|
||||||
|
self.workspaceHeaderLayout.addWidget(self.angledRoutingButton)
|
||||||
|
|
||||||
|
self.splineRoutingButton = QToolButton(self.workspaceHeader)
|
||||||
|
self.splineRoutingButton.setObjectName(u"splineRoutingButton")
|
||||||
|
self.splineRoutingButton.setCheckable(True)
|
||||||
|
|
||||||
|
self.workspaceHeaderLayout.addWidget(self.splineRoutingButton)
|
||||||
|
|
||||||
|
|
||||||
self.workspaceEditorLayout.addWidget(self.workspaceHeader)
|
self.workspaceEditorLayout.addWidget(self.workspaceHeader)
|
||||||
@@ -245,10 +326,12 @@ class Ui_MainWindow(object):
|
|||||||
self.workspaceStack.addWidget(self.jsonPage)
|
self.workspaceStack.addWidget(self.jsonPage)
|
||||||
self.emptyPage = QWidget()
|
self.emptyPage = QWidget()
|
||||||
self.emptyPage.setObjectName(u"emptyPage")
|
self.emptyPage.setObjectName(u"emptyPage")
|
||||||
|
self.emptyPage.setStyleSheet(u"background-color: #9a9a9a;")
|
||||||
self.emptyPageLayout = QVBoxLayout(self.emptyPage)
|
self.emptyPageLayout = QVBoxLayout(self.emptyPage)
|
||||||
self.emptyPageLayout.setObjectName(u"emptyPageLayout")
|
self.emptyPageLayout.setObjectName(u"emptyPageLayout")
|
||||||
self.emptyWorkspaceLabel = QLabel(self.emptyPage)
|
self.emptyWorkspaceLabel = QLabel(self.emptyPage)
|
||||||
self.emptyWorkspaceLabel.setObjectName(u"emptyWorkspaceLabel")
|
self.emptyWorkspaceLabel.setObjectName(u"emptyWorkspaceLabel")
|
||||||
|
self.emptyWorkspaceLabel.setStyleSheet(u"background: transparent; color: #202020;")
|
||||||
self.emptyWorkspaceLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
self.emptyWorkspaceLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
|
||||||
self.emptyPageLayout.addWidget(self.emptyWorkspaceLabel)
|
self.emptyPageLayout.addWidget(self.emptyWorkspaceLabel)
|
||||||
@@ -264,7 +347,7 @@ class Ui_MainWindow(object):
|
|||||||
MainWindow.setCentralWidget(self.centralwidget)
|
MainWindow.setCentralWidget(self.centralwidget)
|
||||||
self.menubar = QMenuBar(MainWindow)
|
self.menubar = QMenuBar(MainWindow)
|
||||||
self.menubar.setObjectName(u"menubar")
|
self.menubar.setObjectName(u"menubar")
|
||||||
self.menubar.setGeometry(QRect(0, 0, 1000, 24))
|
self.menubar.setGeometry(QRect(0, 0, 1209, 24))
|
||||||
self.menuFile = QMenu(self.menubar)
|
self.menuFile = QMenu(self.menubar)
|
||||||
self.menuFile.setObjectName(u"menuFile")
|
self.menuFile.setObjectName(u"menuFile")
|
||||||
self.menuEdit = QMenu(self.menubar)
|
self.menuEdit = QMenu(self.menubar)
|
||||||
@@ -292,9 +375,9 @@ class Ui_MainWindow(object):
|
|||||||
self.editToolbar = QToolBar(MainWindow)
|
self.editToolbar = QToolBar(MainWindow)
|
||||||
self.editToolbar.setObjectName(u"editToolbar")
|
self.editToolbar.setObjectName(u"editToolbar")
|
||||||
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.editToolbar)
|
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.editToolbar)
|
||||||
self.transformToolbar = QToolBar(MainWindow)
|
self.cameraToolbar = QToolBar(MainWindow)
|
||||||
self.transformToolbar.setObjectName(u"transformToolbar")
|
self.cameraToolbar.setObjectName(u"cameraToolbar")
|
||||||
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.transformToolbar)
|
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.cameraToolbar)
|
||||||
|
|
||||||
self.menubar.addAction(self.menuFile.menuAction())
|
self.menubar.addAction(self.menuFile.menuAction())
|
||||||
self.menubar.addAction(self.menuEdit.menuAction())
|
self.menubar.addAction(self.menuEdit.menuAction())
|
||||||
@@ -331,7 +414,9 @@ class Ui_MainWindow(object):
|
|||||||
self.editToolbar.addAction(self.actionCopy)
|
self.editToolbar.addAction(self.actionCopy)
|
||||||
self.editToolbar.addAction(self.actionCut)
|
self.editToolbar.addAction(self.actionCut)
|
||||||
self.editToolbar.addAction(self.actionPaste)
|
self.editToolbar.addAction(self.actionPaste)
|
||||||
self.transformToolbar.addAction(self.actionRotateClockwise)
|
self.cameraToolbar.addAction(self.actionZoomIn)
|
||||||
|
self.cameraToolbar.addAction(self.actionZoomOut)
|
||||||
|
self.cameraToolbar.addAction(self.actionCenterView)
|
||||||
|
|
||||||
self.retranslateUi(MainWindow)
|
self.retranslateUi(MainWindow)
|
||||||
|
|
||||||
@@ -356,6 +441,18 @@ class Ui_MainWindow(object):
|
|||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
#if QT_CONFIG(shortcut)
|
#if QT_CONFIG(shortcut)
|
||||||
self.actionRotateClockwise.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+R", None))
|
self.actionRotateClockwise.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+R", None))
|
||||||
|
#endif // QT_CONFIG(shortcut)
|
||||||
|
self.actionZoomIn.setText(QCoreApplication.translate("MainWindow", u"Zoom In", None))
|
||||||
|
#if QT_CONFIG(shortcut)
|
||||||
|
self.actionZoomIn.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl++", None))
|
||||||
|
#endif // QT_CONFIG(shortcut)
|
||||||
|
self.actionZoomOut.setText(QCoreApplication.translate("MainWindow", u"Zoom Out", None))
|
||||||
|
#if QT_CONFIG(shortcut)
|
||||||
|
self.actionZoomOut.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+-", None))
|
||||||
|
#endif // QT_CONFIG(shortcut)
|
||||||
|
self.actionCenterView.setText(QCoreApplication.translate("MainWindow", u"Center", None))
|
||||||
|
#if QT_CONFIG(shortcut)
|
||||||
|
self.actionCenterView.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+0", None))
|
||||||
#endif // QT_CONFIG(shortcut)
|
#endif // QT_CONFIG(shortcut)
|
||||||
self.actionOpen.setText(QCoreApplication.translate("MainWindow", u"&Open\u2026", None))
|
self.actionOpen.setText(QCoreApplication.translate("MainWindow", u"&Open\u2026", None))
|
||||||
#if QT_CONFIG(statustip)
|
#if QT_CONFIG(statustip)
|
||||||
@@ -419,22 +516,39 @@ class Ui_MainWindow(object):
|
|||||||
self.actionAboutQt.setText(QCoreApplication.translate("MainWindow", u"About &Qt", None))
|
self.actionAboutQt.setText(QCoreApplication.translate("MainWindow", u"About &Qt", None))
|
||||||
self.panel_libraries.setWindowTitle(QCoreApplication.translate("MainWindow", u"Libraries", None))
|
self.panel_libraries.setWindowTitle(QCoreApplication.translate("MainWindow", u"Libraries", None))
|
||||||
self.panel_document.setWindowTitle(QCoreApplication.translate("MainWindow", u"Document", None))
|
self.panel_document.setWindowTitle(QCoreApplication.translate("MainWindow", u"Document", None))
|
||||||
self.navigateUpButton.setText(QCoreApplication.translate("MainWindow", u"Up", None))
|
|
||||||
#if QT_CONFIG(tooltip)
|
#if QT_CONFIG(tooltip)
|
||||||
self.navigateUpButton.setToolTip(QCoreApplication.translate("MainWindow", u"Open the containing graph", None))
|
self.navigateUpButton.setToolTip(QCoreApplication.translate("MainWindow", u"Open the containing graph", None))
|
||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.navigateUpButton.setText(QCoreApplication.translate("MainWindow", u"Up", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.navigateDownButton.setToolTip(QCoreApplication.translate("MainWindow", u"Open the selected block", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.navigateDownButton.setText(QCoreApplication.translate("MainWindow", u"Down", None))
|
||||||
self.graphBreadcrumbLabel.setText(QCoreApplication.translate("MainWindow", u"Untitled", None))
|
self.graphBreadcrumbLabel.setText(QCoreApplication.translate("MainWindow", u"Untitled", None))
|
||||||
self.workspaceModeLabel.setText(QCoreApplication.translate("MainWindow", u"Graph", None))
|
self.workspaceModeLabel.setText(QCoreApplication.translate("MainWindow", u"Graph", None))
|
||||||
self.applyJsonButton.setText(QCoreApplication.translate("MainWindow", u"Apply JSON", None))
|
self.applyJsonButton.setText(QCoreApplication.translate("MainWindow", u"Apply JSON", None))
|
||||||
self.pointerToolButton.setText(QCoreApplication.translate("MainWindow", u"Pointer", None))
|
self.pointerToolButton.setText(QCoreApplication.translate("MainWindow", u"Pointer", None))
|
||||||
self.inputToolButton.setText(QCoreApplication.translate("MainWindow", u"Input", None))
|
self.connectToolButton.setText(QCoreApplication.translate("MainWindow", u"Connect", None))
|
||||||
#if QT_CONFIG(tooltip)
|
#if QT_CONFIG(tooltip)
|
||||||
self.inputToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Add an interface input", None))
|
self.boxToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Draw a box annotation", None))
|
||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
self.outputToolButton.setText(QCoreApplication.translate("MainWindow", u"Output", None))
|
self.boxToolButton.setText(QCoreApplication.translate("MainWindow", u"Box", None))
|
||||||
#if QT_CONFIG(tooltip)
|
#if QT_CONFIG(tooltip)
|
||||||
self.outputToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Add an interface output", None))
|
self.lineToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Draw a line annotation", None))
|
||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.lineToolButton.setText(QCoreApplication.translate("MainWindow", u"Line", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.textToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Add a text annotation", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.textToolButton.setText(QCoreApplication.translate("MainWindow", u"Text", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.rotateToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Rotate selected blocks clockwise", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.rotateToolButton.setText(QCoreApplication.translate("MainWindow", u"Rotate", None))
|
||||||
|
self.routingLabel.setText(QCoreApplication.translate("MainWindow", u"Line:", None))
|
||||||
|
self.directRoutingButton.setText(QCoreApplication.translate("MainWindow", u"Direct", None))
|
||||||
|
self.angledRoutingButton.setText(QCoreApplication.translate("MainWindow", u"Angled", None))
|
||||||
|
self.splineRoutingButton.setText(QCoreApplication.translate("MainWindow", u"Spline", None))
|
||||||
self.jsonEditor.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Component JSON", None))
|
self.jsonEditor.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Component JSON", None))
|
||||||
self.emptyWorkspaceLabel.setText(QCoreApplication.translate("MainWindow", u"No document open", None))
|
self.emptyWorkspaceLabel.setText(QCoreApplication.translate("MainWindow", u"No document open", None))
|
||||||
self.menuFile.setTitle(QCoreApplication.translate("MainWindow", u"&File", None))
|
self.menuFile.setTitle(QCoreApplication.translate("MainWindow", u"&File", None))
|
||||||
@@ -445,6 +559,6 @@ class Ui_MainWindow(object):
|
|||||||
self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"&Help", None))
|
self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"&Help", None))
|
||||||
self.fileToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"File", None))
|
self.fileToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"File", None))
|
||||||
self.editToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Edit", None))
|
self.editToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Edit", None))
|
||||||
self.transformToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Transform", None))
|
self.cameraToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Camera", None))
|
||||||
# retranslateUi
|
# retranslateUi
|
||||||
|
|
||||||
135
BEdit/src/bedit/gui/generated/ui_port_options_dialog.py
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
## Form generated from reading UI file 'port_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, QComboBox, QDialog,
|
||||||
|
QDialogButtonBox, QFormLayout, QHBoxLayout, QLabel,
|
||||||
|
QLineEdit, QListWidget, QListWidgetItem, QPushButton,
|
||||||
|
QSizePolicy, QSplitter, QVBoxLayout, QWidget)
|
||||||
|
|
||||||
|
class Ui_PortOptionsDialog(object):
|
||||||
|
def setupUi(self, PortOptionsDialog):
|
||||||
|
if not PortOptionsDialog.objectName():
|
||||||
|
PortOptionsDialog.setObjectName(u"PortOptionsDialog")
|
||||||
|
PortOptionsDialog.resize(620, 380)
|
||||||
|
self.dialogLayout = QVBoxLayout(PortOptionsDialog)
|
||||||
|
self.dialogLayout.setObjectName(u"dialogLayout")
|
||||||
|
self.portSplitter = QSplitter(PortOptionsDialog)
|
||||||
|
self.portSplitter.setObjectName(u"portSplitter")
|
||||||
|
self.portSplitter.setOrientation(Qt.Orientation.Horizontal)
|
||||||
|
self.portListPanel = QWidget(self.portSplitter)
|
||||||
|
self.portListPanel.setObjectName(u"portListPanel")
|
||||||
|
self.portListLayout = QVBoxLayout(self.portListPanel)
|
||||||
|
self.portListLayout.setObjectName(u"portListLayout")
|
||||||
|
self.portListLayout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.portList = QListWidget(self.portListPanel)
|
||||||
|
self.portList.setObjectName(u"portList")
|
||||||
|
|
||||||
|
self.portListLayout.addWidget(self.portList)
|
||||||
|
|
||||||
|
self.portButtonsLayout = QHBoxLayout()
|
||||||
|
self.portButtonsLayout.setObjectName(u"portButtonsLayout")
|
||||||
|
self.addPortButton = QPushButton(self.portListPanel)
|
||||||
|
self.addPortButton.setObjectName(u"addPortButton")
|
||||||
|
|
||||||
|
self.portButtonsLayout.addWidget(self.addPortButton)
|
||||||
|
|
||||||
|
self.removePortButton = QPushButton(self.portListPanel)
|
||||||
|
self.removePortButton.setObjectName(u"removePortButton")
|
||||||
|
|
||||||
|
self.portButtonsLayout.addWidget(self.removePortButton)
|
||||||
|
|
||||||
|
|
||||||
|
self.portListLayout.addLayout(self.portButtonsLayout)
|
||||||
|
|
||||||
|
self.portSplitter.addWidget(self.portListPanel)
|
||||||
|
self.portDetailsPanel = QWidget(self.portSplitter)
|
||||||
|
self.portDetailsPanel.setObjectName(u"portDetailsPanel")
|
||||||
|
self.portDetailsForm = QFormLayout(self.portDetailsPanel)
|
||||||
|
self.portDetailsForm.setObjectName(u"portDetailsForm")
|
||||||
|
self.portDetailsForm.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.nameLabel = QLabel(self.portDetailsPanel)
|
||||||
|
self.nameLabel.setObjectName(u"nameLabel")
|
||||||
|
|
||||||
|
self.portDetailsForm.setWidget(0, QFormLayout.ItemRole.LabelRole, self.nameLabel)
|
||||||
|
|
||||||
|
self.nameEdit = QLineEdit(self.portDetailsPanel)
|
||||||
|
self.nameEdit.setObjectName(u"nameEdit")
|
||||||
|
|
||||||
|
self.portDetailsForm.setWidget(0, QFormLayout.ItemRole.FieldRole, self.nameEdit)
|
||||||
|
|
||||||
|
self.typeLabel = QLabel(self.portDetailsPanel)
|
||||||
|
self.typeLabel.setObjectName(u"typeLabel")
|
||||||
|
|
||||||
|
self.portDetailsForm.setWidget(1, QFormLayout.ItemRole.LabelRole, self.typeLabel)
|
||||||
|
|
||||||
|
self.typeCombo = QComboBox(self.portDetailsPanel)
|
||||||
|
self.typeCombo.addItem("")
|
||||||
|
self.typeCombo.setObjectName(u"typeCombo")
|
||||||
|
|
||||||
|
self.portDetailsForm.setWidget(1, QFormLayout.ItemRole.FieldRole, self.typeCombo)
|
||||||
|
|
||||||
|
self.orientationLabel = QLabel(self.portDetailsPanel)
|
||||||
|
self.orientationLabel.setObjectName(u"orientationLabel")
|
||||||
|
|
||||||
|
self.portDetailsForm.setWidget(2, QFormLayout.ItemRole.LabelRole, self.orientationLabel)
|
||||||
|
|
||||||
|
self.orientationCombo = QComboBox(self.portDetailsPanel)
|
||||||
|
self.orientationCombo.addItem("")
|
||||||
|
self.orientationCombo.addItem("")
|
||||||
|
self.orientationCombo.setObjectName(u"orientationCombo")
|
||||||
|
|
||||||
|
self.portDetailsForm.setWidget(2, QFormLayout.ItemRole.FieldRole, self.orientationCombo)
|
||||||
|
|
||||||
|
self.positionHintLabel = QLabel(self.portDetailsPanel)
|
||||||
|
self.positionHintLabel.setObjectName(u"positionHintLabel")
|
||||||
|
self.positionHintLabel.setWordWrap(True)
|
||||||
|
|
||||||
|
self.portDetailsForm.setWidget(3, QFormLayout.ItemRole.SpanningRole, self.positionHintLabel)
|
||||||
|
|
||||||
|
self.portSplitter.addWidget(self.portDetailsPanel)
|
||||||
|
|
||||||
|
self.dialogLayout.addWidget(self.portSplitter)
|
||||||
|
|
||||||
|
self.buttonBox = QDialogButtonBox(PortOptionsDialog)
|
||||||
|
self.buttonBox.setObjectName(u"buttonBox")
|
||||||
|
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
|
||||||
|
|
||||||
|
self.dialogLayout.addWidget(self.buttonBox)
|
||||||
|
|
||||||
|
|
||||||
|
self.retranslateUi(PortOptionsDialog)
|
||||||
|
self.buttonBox.accepted.connect(PortOptionsDialog.accept)
|
||||||
|
self.buttonBox.rejected.connect(PortOptionsDialog.reject)
|
||||||
|
|
||||||
|
QMetaObject.connectSlotsByName(PortOptionsDialog)
|
||||||
|
# setupUi
|
||||||
|
|
||||||
|
def retranslateUi(self, PortOptionsDialog):
|
||||||
|
PortOptionsDialog.setWindowTitle(QCoreApplication.translate("PortOptionsDialog", u"Port Options", None))
|
||||||
|
self.addPortButton.setText(QCoreApplication.translate("PortOptionsDialog", u"Add Port", None))
|
||||||
|
self.removePortButton.setText(QCoreApplication.translate("PortOptionsDialog", u"Remove Port", None))
|
||||||
|
self.nameLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Name:", None))
|
||||||
|
self.typeLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Type:", None))
|
||||||
|
self.typeCombo.setItemText(0, QCoreApplication.translate("PortOptionsDialog", u"Signal", None))
|
||||||
|
|
||||||
|
self.orientationLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Orientation:", None))
|
||||||
|
self.orientationCombo.setItemText(0, QCoreApplication.translate("PortOptionsDialog", u"Input", None))
|
||||||
|
self.orientationCombo.setItemText(1, QCoreApplication.translate("PortOptionsDialog", u"Output", None))
|
||||||
|
|
||||||
|
self.positionHintLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"New ports start at (0, 0) in the icon editor.", None))
|
||||||
|
# retranslateUi
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ class Ui_SettingsDialog(object):
|
|||||||
def setupUi(self, SettingsDialog):
|
def setupUi(self, SettingsDialog):
|
||||||
if not SettingsDialog.objectName():
|
if not SettingsDialog.objectName():
|
||||||
SettingsDialog.setObjectName(u"SettingsDialog")
|
SettingsDialog.setObjectName(u"SettingsDialog")
|
||||||
SettingsDialog.resize(480, 300)
|
SettingsDialog.resize(480, 420)
|
||||||
SettingsDialog.setModal(True)
|
SettingsDialog.setModal(True)
|
||||||
self.dialogLayout = QVBoxLayout(SettingsDialog)
|
self.dialogLayout = QVBoxLayout(SettingsDialog)
|
||||||
self.dialogLayout.setObjectName(u"dialogLayout")
|
self.dialogLayout.setObjectName(u"dialogLayout")
|
||||||
@@ -52,6 +52,52 @@ class Ui_SettingsDialog(object):
|
|||||||
|
|
||||||
self.generalLayout.addWidget(self.autosaveGroupBox)
|
self.generalLayout.addWidget(self.autosaveGroupBox)
|
||||||
|
|
||||||
|
self.editorGridsGroupBox = QGroupBox(self.generalTab)
|
||||||
|
self.editorGridsGroupBox.setObjectName(u"editorGridsGroupBox")
|
||||||
|
self.editorGridsLayout = QFormLayout(self.editorGridsGroupBox)
|
||||||
|
self.editorGridsLayout.setObjectName(u"editorGridsLayout")
|
||||||
|
self.graphGridLabel = QLabel(self.editorGridsGroupBox)
|
||||||
|
self.graphGridLabel.setObjectName(u"graphGridLabel")
|
||||||
|
|
||||||
|
self.editorGridsLayout.setWidget(0, QFormLayout.ItemRole.LabelRole, self.graphGridLabel)
|
||||||
|
|
||||||
|
self.graphGridSpinBox = QSpinBox(self.editorGridsGroupBox)
|
||||||
|
self.graphGridSpinBox.setObjectName(u"graphGridSpinBox")
|
||||||
|
self.graphGridSpinBox.setMinimum(8)
|
||||||
|
self.graphGridSpinBox.setMaximum(512)
|
||||||
|
self.graphGridSpinBox.setValue(64)
|
||||||
|
|
||||||
|
self.editorGridsLayout.setWidget(0, QFormLayout.ItemRole.FieldRole, self.graphGridSpinBox)
|
||||||
|
|
||||||
|
self.graphSnapLabel = QLabel(self.editorGridsGroupBox)
|
||||||
|
self.graphSnapLabel.setObjectName(u"graphSnapLabel")
|
||||||
|
|
||||||
|
self.editorGridsLayout.setWidget(1, QFormLayout.ItemRole.LabelRole, self.graphSnapLabel)
|
||||||
|
|
||||||
|
self.graphSnapSpinBox = QSpinBox(self.editorGridsGroupBox)
|
||||||
|
self.graphSnapSpinBox.setObjectName(u"graphSnapSpinBox")
|
||||||
|
self.graphSnapSpinBox.setMinimum(1)
|
||||||
|
self.graphSnapSpinBox.setMaximum(128)
|
||||||
|
self.graphSnapSpinBox.setValue(8)
|
||||||
|
|
||||||
|
self.editorGridsLayout.setWidget(1, QFormLayout.ItemRole.FieldRole, self.graphSnapSpinBox)
|
||||||
|
|
||||||
|
self.iconGridLabel = QLabel(self.editorGridsGroupBox)
|
||||||
|
self.iconGridLabel.setObjectName(u"iconGridLabel")
|
||||||
|
|
||||||
|
self.editorGridsLayout.setWidget(2, QFormLayout.ItemRole.LabelRole, self.iconGridLabel)
|
||||||
|
|
||||||
|
self.iconGridSpinBox = QSpinBox(self.editorGridsGroupBox)
|
||||||
|
self.iconGridSpinBox.setObjectName(u"iconGridSpinBox")
|
||||||
|
self.iconGridSpinBox.setMinimum(1)
|
||||||
|
self.iconGridSpinBox.setMaximum(64)
|
||||||
|
self.iconGridSpinBox.setValue(8)
|
||||||
|
|
||||||
|
self.editorGridsLayout.setWidget(2, QFormLayout.ItemRole.FieldRole, self.iconGridSpinBox)
|
||||||
|
|
||||||
|
|
||||||
|
self.generalLayout.addWidget(self.editorGridsGroupBox)
|
||||||
|
|
||||||
self.generalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
|
self.generalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
|
||||||
|
|
||||||
self.generalLayout.addItem(self.generalSpacer)
|
self.generalLayout.addItem(self.generalSpacer)
|
||||||
@@ -122,6 +168,13 @@ class Ui_SettingsDialog(object):
|
|||||||
SettingsDialog.setWindowTitle(QCoreApplication.translate("SettingsDialog", u"Settings", None))
|
SettingsDialog.setWindowTitle(QCoreApplication.translate("SettingsDialog", u"Settings", None))
|
||||||
self.autosaveGroupBox.setTitle(QCoreApplication.translate("SettingsDialog", u"Automatic saving", None))
|
self.autosaveGroupBox.setTitle(QCoreApplication.translate("SettingsDialog", u"Automatic saving", None))
|
||||||
self.autosaveIntervalSpinBox.setSuffix(QCoreApplication.translate("SettingsDialog", u" minutes", None))
|
self.autosaveIntervalSpinBox.setSuffix(QCoreApplication.translate("SettingsDialog", u" minutes", None))
|
||||||
|
self.editorGridsGroupBox.setTitle(QCoreApplication.translate("SettingsDialog", u"Editor grids", None))
|
||||||
|
self.graphGridLabel.setText(QCoreApplication.translate("SettingsDialog", u"Workspace grid size:", None))
|
||||||
|
self.graphGridSpinBox.setSuffix(QCoreApplication.translate("SettingsDialog", u" units", None))
|
||||||
|
self.graphSnapLabel.setText(QCoreApplication.translate("SettingsDialog", u"Workspace snapping size:", None))
|
||||||
|
self.graphSnapSpinBox.setSuffix(QCoreApplication.translate("SettingsDialog", u" units", None))
|
||||||
|
self.iconGridLabel.setText(QCoreApplication.translate("SettingsDialog", u"Icon grid size:", None))
|
||||||
|
self.iconGridSpinBox.setSuffix(QCoreApplication.translate("SettingsDialog", u" units", None))
|
||||||
self.settingsTabs.setTabText(self.settingsTabs.indexOf(self.generalTab), QCoreApplication.translate("SettingsDialog", u"General", None))
|
self.settingsTabs.setTabText(self.settingsTabs.indexOf(self.generalTab), QCoreApplication.translate("SettingsDialog", u"General", None))
|
||||||
self.libraryPathsLabel.setText(QCoreApplication.translate("SettingsDialog", u"Load library JSON files from these files or folders at startup:", None))
|
self.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.addLibraryFileButton.setText(QCoreApplication.translate("SettingsDialog", u"Add File\u2026", None))
|
||||||
194
BEdit/src/bedit/gui/generated/ui_shape_options_dialog.py
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
## Form generated from reading UI file 'shape_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, QComboBox, QDialog,
|
||||||
|
QDialogButtonBox, QDoubleSpinBox, QFormLayout, QLabel,
|
||||||
|
QLineEdit, QPushButton, QSizePolicy, QSpacerItem,
|
||||||
|
QVBoxLayout, QWidget)
|
||||||
|
|
||||||
|
class Ui_ShapeOptionsDialog(object):
|
||||||
|
def setupUi(self, ShapeOptionsDialog):
|
||||||
|
if not ShapeOptionsDialog.objectName():
|
||||||
|
ShapeOptionsDialog.setObjectName(u"ShapeOptionsDialog")
|
||||||
|
ShapeOptionsDialog.resize(420, 440)
|
||||||
|
self.dialogLayout = QVBoxLayout(ShapeOptionsDialog)
|
||||||
|
self.dialogLayout.setObjectName(u"dialogLayout")
|
||||||
|
self.optionsForm = QFormLayout()
|
||||||
|
self.optionsForm.setObjectName(u"optionsForm")
|
||||||
|
self.widthLabel = QLabel(ShapeOptionsDialog)
|
||||||
|
self.widthLabel.setObjectName(u"widthLabel")
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(0, QFormLayout.ItemRole.LabelRole, self.widthLabel)
|
||||||
|
|
||||||
|
self.widthSpin = QDoubleSpinBox(ShapeOptionsDialog)
|
||||||
|
self.widthSpin.setObjectName(u"widthSpin")
|
||||||
|
self.widthSpin.setMinimum(1.000000000000000)
|
||||||
|
self.widthSpin.setMaximum(500.000000000000000)
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(0, QFormLayout.ItemRole.FieldRole, self.widthSpin)
|
||||||
|
|
||||||
|
self.heightLabel = QLabel(ShapeOptionsDialog)
|
||||||
|
self.heightLabel.setObjectName(u"heightLabel")
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(1, QFormLayout.ItemRole.LabelRole, self.heightLabel)
|
||||||
|
|
||||||
|
self.heightSpin = QDoubleSpinBox(ShapeOptionsDialog)
|
||||||
|
self.heightSpin.setObjectName(u"heightSpin")
|
||||||
|
self.heightSpin.setMinimum(1.000000000000000)
|
||||||
|
self.heightSpin.setMaximum(500.000000000000000)
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(1, QFormLayout.ItemRole.FieldRole, self.heightSpin)
|
||||||
|
|
||||||
|
self.lineStyleLabel = QLabel(ShapeOptionsDialog)
|
||||||
|
self.lineStyleLabel.setObjectName(u"lineStyleLabel")
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(2, QFormLayout.ItemRole.LabelRole, self.lineStyleLabel)
|
||||||
|
|
||||||
|
self.lineStyleCombo = QComboBox(ShapeOptionsDialog)
|
||||||
|
self.lineStyleCombo.addItem("")
|
||||||
|
self.lineStyleCombo.addItem("")
|
||||||
|
self.lineStyleCombo.addItem("")
|
||||||
|
self.lineStyleCombo.addItem("")
|
||||||
|
self.lineStyleCombo.addItem("")
|
||||||
|
self.lineStyleCombo.setObjectName(u"lineStyleCombo")
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(2, QFormLayout.ItemRole.FieldRole, self.lineStyleCombo)
|
||||||
|
|
||||||
|
self.lineWidthLabel = QLabel(ShapeOptionsDialog)
|
||||||
|
self.lineWidthLabel.setObjectName(u"lineWidthLabel")
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(3, QFormLayout.ItemRole.LabelRole, self.lineWidthLabel)
|
||||||
|
|
||||||
|
self.lineWidthSpin = QDoubleSpinBox(ShapeOptionsDialog)
|
||||||
|
self.lineWidthSpin.setObjectName(u"lineWidthSpin")
|
||||||
|
self.lineWidthSpin.setMinimum(0.100000000000000)
|
||||||
|
self.lineWidthSpin.setMaximum(20.000000000000000)
|
||||||
|
self.lineWidthSpin.setSingleStep(0.500000000000000)
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(3, QFormLayout.ItemRole.FieldRole, self.lineWidthSpin)
|
||||||
|
|
||||||
|
self.strokeColorLabel = QLabel(ShapeOptionsDialog)
|
||||||
|
self.strokeColorLabel.setObjectName(u"strokeColorLabel")
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(4, QFormLayout.ItemRole.LabelRole, self.strokeColorLabel)
|
||||||
|
|
||||||
|
self.strokeColorButton = QPushButton(ShapeOptionsDialog)
|
||||||
|
self.strokeColorButton.setObjectName(u"strokeColorButton")
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(4, QFormLayout.ItemRole.FieldRole, self.strokeColorButton)
|
||||||
|
|
||||||
|
self.fillTypeLabel = QLabel(ShapeOptionsDialog)
|
||||||
|
self.fillTypeLabel.setObjectName(u"fillTypeLabel")
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(5, QFormLayout.ItemRole.LabelRole, self.fillTypeLabel)
|
||||||
|
|
||||||
|
self.fillTypeCombo = QComboBox(ShapeOptionsDialog)
|
||||||
|
self.fillTypeCombo.addItem("")
|
||||||
|
self.fillTypeCombo.addItem("")
|
||||||
|
self.fillTypeCombo.setObjectName(u"fillTypeCombo")
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(5, QFormLayout.ItemRole.FieldRole, self.fillTypeCombo)
|
||||||
|
|
||||||
|
self.fillColorLabel = QLabel(ShapeOptionsDialog)
|
||||||
|
self.fillColorLabel.setObjectName(u"fillColorLabel")
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(6, QFormLayout.ItemRole.LabelRole, self.fillColorLabel)
|
||||||
|
|
||||||
|
self.fillColorButton = QPushButton(ShapeOptionsDialog)
|
||||||
|
self.fillColorButton.setObjectName(u"fillColorButton")
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(6, QFormLayout.ItemRole.FieldRole, self.fillColorButton)
|
||||||
|
|
||||||
|
self.cornerRadiusLabel = QLabel(ShapeOptionsDialog)
|
||||||
|
self.cornerRadiusLabel.setObjectName(u"cornerRadiusLabel")
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(7, QFormLayout.ItemRole.LabelRole, self.cornerRadiusLabel)
|
||||||
|
|
||||||
|
self.cornerRadiusSpin = QDoubleSpinBox(ShapeOptionsDialog)
|
||||||
|
self.cornerRadiusSpin.setObjectName(u"cornerRadiusSpin")
|
||||||
|
self.cornerRadiusSpin.setMaximum(50.000000000000000)
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(7, QFormLayout.ItemRole.FieldRole, self.cornerRadiusSpin)
|
||||||
|
|
||||||
|
self.textLabel = QLabel(ShapeOptionsDialog)
|
||||||
|
self.textLabel.setObjectName(u"textLabel")
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(8, QFormLayout.ItemRole.LabelRole, self.textLabel)
|
||||||
|
|
||||||
|
self.textEdit = QLineEdit(ShapeOptionsDialog)
|
||||||
|
self.textEdit.setObjectName(u"textEdit")
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(8, QFormLayout.ItemRole.FieldRole, self.textEdit)
|
||||||
|
|
||||||
|
self.fontSizeLabel = QLabel(ShapeOptionsDialog)
|
||||||
|
self.fontSizeLabel.setObjectName(u"fontSizeLabel")
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(9, QFormLayout.ItemRole.LabelRole, self.fontSizeLabel)
|
||||||
|
|
||||||
|
self.fontSizeSpin = QDoubleSpinBox(ShapeOptionsDialog)
|
||||||
|
self.fontSizeSpin.setObjectName(u"fontSizeSpin")
|
||||||
|
self.fontSizeSpin.setMinimum(4.000000000000000)
|
||||||
|
self.fontSizeSpin.setMaximum(96.000000000000000)
|
||||||
|
|
||||||
|
self.optionsForm.setWidget(9, QFormLayout.ItemRole.FieldRole, self.fontSizeSpin)
|
||||||
|
|
||||||
|
|
||||||
|
self.dialogLayout.addLayout(self.optionsForm)
|
||||||
|
|
||||||
|
self.verticalSpacer = QSpacerItem(20, 20, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
|
||||||
|
|
||||||
|
self.dialogLayout.addItem(self.verticalSpacer)
|
||||||
|
|
||||||
|
self.buttonBox = QDialogButtonBox(ShapeOptionsDialog)
|
||||||
|
self.buttonBox.setObjectName(u"buttonBox")
|
||||||
|
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
|
||||||
|
|
||||||
|
self.dialogLayout.addWidget(self.buttonBox)
|
||||||
|
|
||||||
|
|
||||||
|
self.retranslateUi(ShapeOptionsDialog)
|
||||||
|
self.buttonBox.accepted.connect(ShapeOptionsDialog.accept)
|
||||||
|
self.buttonBox.rejected.connect(ShapeOptionsDialog.reject)
|
||||||
|
|
||||||
|
QMetaObject.connectSlotsByName(ShapeOptionsDialog)
|
||||||
|
# setupUi
|
||||||
|
|
||||||
|
def retranslateUi(self, ShapeOptionsDialog):
|
||||||
|
ShapeOptionsDialog.setWindowTitle(QCoreApplication.translate("ShapeOptionsDialog", u"Shape Options", None))
|
||||||
|
self.widthLabel.setText(QCoreApplication.translate("ShapeOptionsDialog", u"Width:", None))
|
||||||
|
self.heightLabel.setText(QCoreApplication.translate("ShapeOptionsDialog", u"Height:", None))
|
||||||
|
self.lineStyleLabel.setText(QCoreApplication.translate("ShapeOptionsDialog", u"Line style:", None))
|
||||||
|
self.lineStyleCombo.setItemText(0, QCoreApplication.translate("ShapeOptionsDialog", u"solid", None))
|
||||||
|
self.lineStyleCombo.setItemText(1, QCoreApplication.translate("ShapeOptionsDialog", u"dash", None))
|
||||||
|
self.lineStyleCombo.setItemText(2, QCoreApplication.translate("ShapeOptionsDialog", u"dot", None))
|
||||||
|
self.lineStyleCombo.setItemText(3, QCoreApplication.translate("ShapeOptionsDialog", u"dash-dot", None))
|
||||||
|
self.lineStyleCombo.setItemText(4, QCoreApplication.translate("ShapeOptionsDialog", u"none", None))
|
||||||
|
|
||||||
|
self.lineWidthLabel.setText(QCoreApplication.translate("ShapeOptionsDialog", u"Line width:", None))
|
||||||
|
self.strokeColorLabel.setText(QCoreApplication.translate("ShapeOptionsDialog", u"Line colour:", None))
|
||||||
|
self.strokeColorButton.setText(QCoreApplication.translate("ShapeOptionsDialog", u"Choose\u2026", None))
|
||||||
|
self.fillTypeLabel.setText(QCoreApplication.translate("ShapeOptionsDialog", u"Fill type:", None))
|
||||||
|
self.fillTypeCombo.setItemText(0, QCoreApplication.translate("ShapeOptionsDialog", u"solid", None))
|
||||||
|
self.fillTypeCombo.setItemText(1, QCoreApplication.translate("ShapeOptionsDialog", u"none", None))
|
||||||
|
|
||||||
|
self.fillColorLabel.setText(QCoreApplication.translate("ShapeOptionsDialog", u"Fill colour:", None))
|
||||||
|
self.fillColorButton.setText(QCoreApplication.translate("ShapeOptionsDialog", u"Choose\u2026", None))
|
||||||
|
self.cornerRadiusLabel.setText(QCoreApplication.translate("ShapeOptionsDialog", u"Corner radius:", None))
|
||||||
|
self.textLabel.setText(QCoreApplication.translate("ShapeOptionsDialog", u"Text:", None))
|
||||||
|
self.fontSizeLabel.setText(QCoreApplication.translate("ShapeOptionsDialog", u"Font size:", None))
|
||||||
|
# retranslateUi
|
||||||
|
|
||||||
5
BEdit/src/bedit/gui/graphics/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""Graphics scenes, views, editors, and renderers."""
|
||||||
|
|
||||||
|
from bedit.gui.graphics.workspace import GraphWorkspaceView
|
||||||
|
|
||||||
|
__all__ = ["GraphWorkspaceView"]
|
||||||
25
BEdit/src/bedit/gui/graphics/connection_styles.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from PySide6.QtCore import Qt
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ConnectionStyle:
|
||||||
|
color: str = "#285f9e"
|
||||||
|
selected_color: str = "#f59e0b"
|
||||||
|
width: float = 2.5
|
||||||
|
selected_width: float = 4.0
|
||||||
|
line_style: Qt.PenStyle = Qt.PenStyle.SolidLine
|
||||||
|
arrow_at_source: bool = False
|
||||||
|
arrow_at_target: bool = True
|
||||||
|
arrow_size: float = 10.0
|
||||||
|
|
||||||
|
|
||||||
|
# This is the intentional code-level styling point for every port/connection type.
|
||||||
|
CONNECTION_STYLES: dict[str, ConnectionStyle] = {
|
||||||
|
"signal": ConnectionStyle(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def connection_style(port_type: str) -> ConnectionStyle:
|
||||||
|
return CONNECTION_STYLES.get(port_type, ConnectionStyle())
|
||||||
47
BEdit/src/bedit/gui/graphics/icon_canvas.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from PySide6.QtCore import QPointF, QRectF
|
||||||
|
from PySide6.QtCore import Qt
|
||||||
|
from PySide6.QtGui import QColor, QPainter, QPen, QWheelEvent
|
||||||
|
from PySide6.QtWidgets import QGraphicsView
|
||||||
|
|
||||||
|
from bedit.gui.preferences import application_settings
|
||||||
|
|
||||||
|
|
||||||
|
def icon_grid_size() -> int:
|
||||||
|
return application_settings().value("grid/iconSize", 8, type=int)
|
||||||
|
|
||||||
|
|
||||||
|
class IconCanvasView(QGraphicsView):
|
||||||
|
"""Designer-promotable view that paints the icon editor grid."""
|
||||||
|
|
||||||
|
def drawBackground(self, painter: QPainter, rect: QRectF) -> None: # noqa: N802
|
||||||
|
painter.fillRect(rect, QColor("#ffffff"))
|
||||||
|
grid = icon_grid_size()
|
||||||
|
painter.setPen(QPen(QColor("#dbeafe"), 0))
|
||||||
|
left = int(rect.left()) - int(rect.left()) % grid
|
||||||
|
top = int(rect.top()) - int(rect.top()) % grid
|
||||||
|
for x in range(left, int(rect.right()) + grid, grid):
|
||||||
|
painter.drawLine(QPointF(x, rect.top()), QPointF(x, rect.bottom()))
|
||||||
|
for y in range(top, int(rect.bottom()) + grid, grid):
|
||||||
|
painter.drawLine(QPointF(rect.left(), y), QPointF(rect.right(), y))
|
||||||
|
|
||||||
|
def _zoom(self, factor: float) -> None:
|
||||||
|
target = self.transform().m11() * factor
|
||||||
|
if 0.25 <= target <= 16.0:
|
||||||
|
self.scale(factor, factor)
|
||||||
|
|
||||||
|
def zoom_in(self) -> None:
|
||||||
|
self._zoom(1.2)
|
||||||
|
|
||||||
|
def zoom_out(self) -> None:
|
||||||
|
self._zoom(1 / 1.2)
|
||||||
|
|
||||||
|
def center_icon(self) -> None:
|
||||||
|
if self.scene() is not None:
|
||||||
|
self.fitInView(
|
||||||
|
self.scene().sceneRect().adjusted(-10, -10, 10, 10),
|
||||||
|
Qt.AspectRatioMode.KeepAspectRatio,
|
||||||
|
)
|
||||||
|
|
||||||
|
def wheelEvent(self, event: QWheelEvent) -> None: # noqa: N802
|
||||||
|
self._zoom(1.2 if event.angleDelta().y() > 0 else 1 / 1.2)
|
||||||
|
event.accept()
|
||||||
494
BEdit/src/bedit/gui/graphics/icon_editor.py
Normal file
@@ -0,0 +1,494 @@
|
|||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
from PySide6.QtCore import QPointF, QRectF, QSizeF, Qt
|
||||||
|
from PySide6.QtGui import QColor, QPainter, QPainterPath, QPen, QPolygonF
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QColorDialog,
|
||||||
|
QButtonGroup,
|
||||||
|
QDialog,
|
||||||
|
QGraphicsEllipseItem,
|
||||||
|
QGraphicsItem,
|
||||||
|
QGraphicsObject,
|
||||||
|
QGraphicsPathItem,
|
||||||
|
QGraphicsScene,
|
||||||
|
QGraphicsSceneContextMenuEvent,
|
||||||
|
QGraphicsSceneMouseEvent,
|
||||||
|
QInputDialog,
|
||||||
|
QMenu,
|
||||||
|
)
|
||||||
|
|
||||||
|
from bedit.core.model import Component, Icon, Port
|
||||||
|
from bedit.gui.generated.ui_icon_editor_dialog import Ui_IconEditorDialog
|
||||||
|
from bedit.gui.generated.ui_shape_options_dialog import Ui_ShapeOptionsDialog
|
||||||
|
from bedit.gui.graphics.icon_canvas import icon_grid_size
|
||||||
|
from bedit.gui.graphics.icon_renderer import shape_pen
|
||||||
|
|
||||||
|
|
||||||
|
def _icon_grid_size() -> int:
|
||||||
|
return icon_grid_size()
|
||||||
|
|
||||||
|
|
||||||
|
def _snap(value: float) -> float:
|
||||||
|
grid = _icon_grid_size()
|
||||||
|
return round(value / grid) * grid
|
||||||
|
|
||||||
|
|
||||||
|
class ResizeHandle(QGraphicsEllipseItem):
|
||||||
|
def __init__(self, owner: "ShapeItem") -> None:
|
||||||
|
super().__init__(-4, -4, 8, 8, owner)
|
||||||
|
self.owner = owner
|
||||||
|
self.setBrush(QColor("#ffffff"))
|
||||||
|
self.setPen(QPen(QColor("#2563eb"), 1.5))
|
||||||
|
self.setCursor(Qt.CursorShape.SizeFDiagCursor)
|
||||||
|
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable)
|
||||||
|
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
|
||||||
|
self.setZValue(20)
|
||||||
|
|
||||||
|
def itemChange(self, change, value): # noqa: N802
|
||||||
|
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
|
||||||
|
maximum = (
|
||||||
|
self.owner.scene().sceneRect().bottomRight() - self.owner.pos()
|
||||||
|
if self.owner.scene()
|
||||||
|
else QPointF(128, 128)
|
||||||
|
)
|
||||||
|
if self.owner.element.get("type") == "line":
|
||||||
|
minimum = (
|
||||||
|
self.owner.scene().sceneRect().topLeft() - self.owner.pos()
|
||||||
|
if self.owner.scene()
|
||||||
|
else QPointF(-128, -128)
|
||||||
|
)
|
||||||
|
value = QPointF(
|
||||||
|
min(maximum.x(), max(minimum.x(), _snap(value.x()))),
|
||||||
|
min(maximum.y(), max(minimum.y(), _snap(value.y()))),
|
||||||
|
)
|
||||||
|
self.owner.prepareGeometryChange()
|
||||||
|
self.owner.element["width"] = value.x()
|
||||||
|
self.owner.element["height"] = value.y()
|
||||||
|
self.owner.update()
|
||||||
|
return super().itemChange(change, value)
|
||||||
|
value = QPointF(
|
||||||
|
min(maximum.x(), max(_icon_grid_size(), _snap(value.x()))),
|
||||||
|
min(maximum.y(), max(_icon_grid_size(), _snap(value.y()))),
|
||||||
|
)
|
||||||
|
if self.owner.element.get("type") == "circle":
|
||||||
|
side = min(maximum.x(), maximum.y(), max(value.x(), value.y()))
|
||||||
|
value = QPointF(side, side)
|
||||||
|
self.owner.prepareGeometryChange()
|
||||||
|
self.owner.element["width"] = value.x()
|
||||||
|
self.owner.element["height"] = value.y()
|
||||||
|
self.owner.update()
|
||||||
|
return super().itemChange(change, value)
|
||||||
|
|
||||||
|
|
||||||
|
class ShapeOptionsDialog(QDialog):
|
||||||
|
def __init__(self, element: dict, parent=None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.ui = Ui_ShapeOptionsDialog()
|
||||||
|
self.ui.setupUi(self)
|
||||||
|
self.element = deepcopy(element)
|
||||||
|
self.stroke_color = self.element.get("stroke", "#303030")
|
||||||
|
fill = self.element.get("fill", "#ffffff")
|
||||||
|
self.fill_color = "#ffffff" if fill in {"none", "transparent", ""} else fill
|
||||||
|
self.ui.widthSpin.setValue(float(self.element.get("width", 20)))
|
||||||
|
self.ui.heightSpin.setValue(float(self.element.get("height", 20)))
|
||||||
|
self.ui.lineStyleCombo.setCurrentText(self.element.get("lineStyle", "solid"))
|
||||||
|
self.ui.lineWidthSpin.setValue(float(self.element.get("lineWidth", 1.5)))
|
||||||
|
self.ui.fillTypeCombo.setCurrentText(
|
||||||
|
"none" if fill in {"none", "transparent", ""} else "solid"
|
||||||
|
)
|
||||||
|
self.ui.cornerRadiusSpin.setValue(float(self.element.get("cornerRadius", 0)))
|
||||||
|
self.ui.textEdit.setText(str(self.element.get("text", "Text")))
|
||||||
|
self.ui.fontSizeSpin.setValue(float(self.element.get("fontSize", 12)))
|
||||||
|
self._show_row(
|
||||||
|
self.ui.fillTypeLabel, self.ui.fillTypeCombo, self.element.get("type") != "line"
|
||||||
|
)
|
||||||
|
self._show_row(
|
||||||
|
self.ui.fillColorLabel, self.ui.fillColorButton, self.element.get("type") != "line"
|
||||||
|
)
|
||||||
|
self._show_row(
|
||||||
|
self.ui.cornerRadiusLabel,
|
||||||
|
self.ui.cornerRadiusSpin,
|
||||||
|
self.element.get("type") == "rectangle",
|
||||||
|
)
|
||||||
|
is_text = self.element.get("type") == "text"
|
||||||
|
self._show_row(self.ui.textLabel, self.ui.textEdit, is_text)
|
||||||
|
self._show_row(self.ui.fontSizeLabel, self.ui.fontSizeSpin, is_text)
|
||||||
|
self.ui.strokeColorButton.clicked.connect(self._choose_stroke)
|
||||||
|
self.ui.fillColorButton.clicked.connect(self._choose_fill)
|
||||||
|
self._refresh_color_buttons()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _show_row(label, field, visible: bool) -> None:
|
||||||
|
label.setVisible(visible)
|
||||||
|
field.setVisible(visible)
|
||||||
|
|
||||||
|
def _choose_color(self, current: str) -> str:
|
||||||
|
color = QColorDialog.getColor(
|
||||||
|
QColor(current),
|
||||||
|
self,
|
||||||
|
"Choose colour",
|
||||||
|
QColorDialog.ColorDialogOption.ShowAlphaChannel,
|
||||||
|
)
|
||||||
|
if not color.isValid():
|
||||||
|
return current
|
||||||
|
return color.name(QColor.NameFormat.HexArgb) if color.alpha() < 255 else color.name()
|
||||||
|
|
||||||
|
def _choose_stroke(self) -> None:
|
||||||
|
self.stroke_color = self._choose_color(self.stroke_color)
|
||||||
|
self._refresh_color_buttons()
|
||||||
|
|
||||||
|
def _choose_fill(self) -> None:
|
||||||
|
self.fill_color = self._choose_color(self.fill_color)
|
||||||
|
self._refresh_color_buttons()
|
||||||
|
|
||||||
|
def _refresh_color_buttons(self) -> None:
|
||||||
|
for button, color in (
|
||||||
|
(self.ui.strokeColorButton, self.stroke_color),
|
||||||
|
(self.ui.fillColorButton, self.fill_color),
|
||||||
|
):
|
||||||
|
button.setText(color)
|
||||||
|
button.setStyleSheet(f"QPushButton {{ background: {color}; }}")
|
||||||
|
|
||||||
|
def accept(self) -> None:
|
||||||
|
self.element["lineStyle"] = self.ui.lineStyleCombo.currentText()
|
||||||
|
self.element["lineWidth"] = self.ui.lineWidthSpin.value()
|
||||||
|
self.element["stroke"] = self.stroke_color
|
||||||
|
self.element["width"] = self.ui.widthSpin.value()
|
||||||
|
self.element["height"] = self.ui.heightSpin.value()
|
||||||
|
if self.element.get("type") != "line":
|
||||||
|
self.element["fill"] = (
|
||||||
|
self.fill_color if self.ui.fillTypeCombo.currentText() == "solid" else "none"
|
||||||
|
)
|
||||||
|
if self.element.get("type") == "rectangle":
|
||||||
|
self.element["cornerRadius"] = self.ui.cornerRadiusSpin.value()
|
||||||
|
if self.element.get("type") == "text":
|
||||||
|
self.element["text"] = self.ui.textEdit.text()
|
||||||
|
self.element["fontSize"] = self.ui.fontSizeSpin.value()
|
||||||
|
self.element["color"] = self.stroke_color
|
||||||
|
super().accept()
|
||||||
|
|
||||||
|
|
||||||
|
class ShapeItem(QGraphicsObject):
|
||||||
|
def __init__(self, element: dict) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.element = element
|
||||||
|
self.setPos(float(element.get("x", 0)), float(element.get("y", 0)))
|
||||||
|
self.setFlags(
|
||||||
|
QGraphicsItem.GraphicsItemFlag.ItemIsMovable
|
||||||
|
| QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
|
||||||
|
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
|
||||||
|
)
|
||||||
|
self.resize_handle = ResizeHandle(self)
|
||||||
|
self.resize_handle.setPos(float(element.get("width", 20)), float(element.get("height", 20)))
|
||||||
|
self.resize_handle.hide()
|
||||||
|
|
||||||
|
def boundingRect(self) -> QRectF: # noqa: N802
|
||||||
|
margin = max(3.0, float(self.element.get("lineWidth", 1.5)))
|
||||||
|
return (
|
||||||
|
QRectF(
|
||||||
|
0, 0, float(self.element.get("width", 20)), float(self.element.get("height", 20))
|
||||||
|
)
|
||||||
|
.normalized()
|
||||||
|
.adjusted(-margin, -margin, margin, margin)
|
||||||
|
)
|
||||||
|
|
||||||
|
def paint(self, painter: QPainter, option, widget=None) -> None:
|
||||||
|
del option, widget
|
||||||
|
rect = QRectF(
|
||||||
|
0, 0, float(self.element.get("width", 20)), float(self.element.get("height", 20))
|
||||||
|
)
|
||||||
|
painter.setPen(shape_pen(self.element))
|
||||||
|
fill = self.element.get("fill", "none")
|
||||||
|
painter.setBrush(
|
||||||
|
Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill)
|
||||||
|
)
|
||||||
|
kind = self.element.get("type")
|
||||||
|
if kind == "rectangle":
|
||||||
|
radius = float(self.element.get("cornerRadius", 0))
|
||||||
|
painter.drawRoundedRect(rect, radius, radius)
|
||||||
|
elif kind in {"circle", "ellipse"}:
|
||||||
|
painter.drawEllipse(rect)
|
||||||
|
elif kind == "line":
|
||||||
|
painter.drawLine(rect.topLeft(), rect.bottomRight())
|
||||||
|
elif kind == "triangle":
|
||||||
|
painter.drawPolygon(
|
||||||
|
QPolygonF([QPointF(rect.center().x(), 0), rect.bottomRight(), rect.bottomLeft()])
|
||||||
|
)
|
||||||
|
elif kind == "text":
|
||||||
|
painter.setPen(QColor(self.element.get("color", "#202020")))
|
||||||
|
font = painter.font()
|
||||||
|
font.setPointSizeF(float(self.element.get("fontSize", 12)))
|
||||||
|
painter.setFont(font)
|
||||||
|
painter.drawText(
|
||||||
|
rect, Qt.AlignmentFlag.AlignCenter, str(self.element.get("text", "Text"))
|
||||||
|
)
|
||||||
|
if self.isSelected():
|
||||||
|
painter.setBrush(Qt.BrushStyle.NoBrush)
|
||||||
|
painter.setPen(QPen(QColor("#2563eb"), 1, Qt.PenStyle.DashLine))
|
||||||
|
painter.drawRect(rect)
|
||||||
|
|
||||||
|
def itemChange(self, change, value): # noqa: N802
|
||||||
|
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
|
||||||
|
value = self._bounded_position(value)
|
||||||
|
elif change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
||||||
|
self.element["x"], self.element["y"] = value.x(), value.y()
|
||||||
|
elif change == QGraphicsItem.GraphicsItemChange.ItemSelectedHasChanged:
|
||||||
|
self.resize_handle.setVisible(bool(value))
|
||||||
|
return super().itemChange(change, value)
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||||
|
super().mouseReleaseEvent(event)
|
||||||
|
self.setPos(self._bounded_position(self.pos()))
|
||||||
|
|
||||||
|
def _bounded_position(self, position: QPointF) -> QPointF:
|
||||||
|
bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128)
|
||||||
|
width = float(self.element.get("width", 20))
|
||||||
|
height = float(self.element.get("height", 20))
|
||||||
|
minimum_x, maximum_x = min(0.0, width), max(0.0, width)
|
||||||
|
minimum_y, maximum_y = min(0.0, height), max(0.0, height)
|
||||||
|
return QPointF(
|
||||||
|
max(bounds.left() - minimum_x, min(bounds.right() - maximum_x, _snap(position.x()))),
|
||||||
|
max(bounds.top() - minimum_y, min(bounds.bottom() - maximum_y, _snap(position.y()))),
|
||||||
|
)
|
||||||
|
|
||||||
|
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
|
||||||
|
menu = QMenu()
|
||||||
|
options = menu.addAction("Shape Options…")
|
||||||
|
delete = menu.addAction("Delete Shape")
|
||||||
|
chosen = menu.exec(event.screenPos())
|
||||||
|
if chosen is options:
|
||||||
|
options_element = deepcopy(self.element)
|
||||||
|
width_sign = -1 if float(options_element.get("width", 20)) < 0 else 1
|
||||||
|
height_sign = -1 if float(options_element.get("height", 20)) < 0 else 1
|
||||||
|
options_element["width"] = abs(float(options_element.get("width", 20)))
|
||||||
|
options_element["height"] = abs(float(options_element.get("height", 20)))
|
||||||
|
dialog = ShapeOptionsDialog(options_element)
|
||||||
|
if dialog.exec() == dialog.DialogCode.Accepted:
|
||||||
|
self.prepareGeometryChange()
|
||||||
|
self.element.clear()
|
||||||
|
self.element.update(dialog.element)
|
||||||
|
if self.element.get("type") == "line":
|
||||||
|
self.element["width"] *= width_sign
|
||||||
|
self.element["height"] *= height_sign
|
||||||
|
self.resize_handle.setPos(
|
||||||
|
float(self.element.get("width", 20)),
|
||||||
|
float(self.element.get("height", 20)),
|
||||||
|
)
|
||||||
|
self.update()
|
||||||
|
elif chosen is delete and self.scene() is not None:
|
||||||
|
self.scene().removeItem(self)
|
||||||
|
self.element["_deleted"] = True
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
|
||||||
|
class PortHandle(QGraphicsEllipseItem):
|
||||||
|
def __init__(self, port: Port, direction: str, position: QPointF) -> None:
|
||||||
|
super().__init__(-5, -5, 10, 10)
|
||||||
|
self.port, self.direction = port, direction
|
||||||
|
self.setPos(position)
|
||||||
|
self.setBrush(QColor("#16a34a" if direction == "input" else "#dc2626"))
|
||||||
|
self.setPen(QPen(QColor("#ffffff"), 1.5))
|
||||||
|
self.setToolTip(f"{direction.title()}: {port.name} (drag to position)")
|
||||||
|
self.setZValue(10)
|
||||||
|
self.setFlags(
|
||||||
|
QGraphicsItem.GraphicsItemFlag.ItemIsMovable
|
||||||
|
| QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
|
||||||
|
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
|
||||||
|
)
|
||||||
|
|
||||||
|
def itemChange(self, change, value): # noqa: N802
|
||||||
|
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
|
||||||
|
bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128)
|
||||||
|
value = QPointF(
|
||||||
|
min(bounds.right(), max(bounds.left(), _snap(value.x()))),
|
||||||
|
min(bounds.bottom(), max(bounds.top(), _snap(value.y()))),
|
||||||
|
)
|
||||||
|
elif change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
||||||
|
self.port.properties["iconPosition"] = {"x": value.x(), "y": value.y()}
|
||||||
|
return super().itemChange(change, value)
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||||
|
super().mouseReleaseEvent(event)
|
||||||
|
bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128)
|
||||||
|
self.setPos(
|
||||||
|
min(bounds.right(), max(bounds.left(), _snap(self.pos().x()))),
|
||||||
|
min(bounds.bottom(), max(bounds.top(), _snap(self.pos().y()))),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IconDrawingScene(QGraphicsScene):
|
||||||
|
def __init__(self, editor: "IconEditorDialog", rect: QRectF) -> None:
|
||||||
|
super().__init__(rect, editor)
|
||||||
|
self.editor = editor
|
||||||
|
self.mode = "pointer"
|
||||||
|
self.start: QPointF | None = None
|
||||||
|
self.preview: QGraphicsPathItem | None = None
|
||||||
|
|
||||||
|
def set_mode(self, mode: str) -> None:
|
||||||
|
self.mode = mode
|
||||||
|
self._clear_preview()
|
||||||
|
|
||||||
|
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||||
|
if self.mode == "pointer":
|
||||||
|
super().mousePressEvent(event)
|
||||||
|
return
|
||||||
|
point = self._bounded(event.scenePos())
|
||||||
|
if self.mode == "text":
|
||||||
|
text, accepted = QInputDialog.getText(self.editor, "Add Text", "Text:", text="Text")
|
||||||
|
if accepted:
|
||||||
|
self.editor.create_shape("text", point, point + QPointF(55, 35), text)
|
||||||
|
event.accept()
|
||||||
|
return
|
||||||
|
self.start = point
|
||||||
|
self.preview = QGraphicsPathItem()
|
||||||
|
self.preview.setPen(QPen(QColor("#64748b"), 1.5, Qt.PenStyle.DashLine))
|
||||||
|
self.preview.setZValue(100)
|
||||||
|
self.addItem(self.preview)
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||||
|
if self.start is None or self.preview is None:
|
||||||
|
super().mouseMoveEvent(event)
|
||||||
|
return
|
||||||
|
end = self._bounded(event.scenePos())
|
||||||
|
path = QPainterPath()
|
||||||
|
rect = QRectF(self.start, end).normalized()
|
||||||
|
if self.mode == "line":
|
||||||
|
path.moveTo(self.start)
|
||||||
|
path.lineTo(end)
|
||||||
|
elif self.mode in {"circle", "ellipse"}:
|
||||||
|
if self.mode == "circle":
|
||||||
|
side = min(rect.width(), rect.height())
|
||||||
|
rect.setSize(QSizeF(side, side))
|
||||||
|
path.addEllipse(rect)
|
||||||
|
else:
|
||||||
|
path.addRect(rect)
|
||||||
|
self.preview.setPath(path)
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||||
|
if self.start is None:
|
||||||
|
super().mouseReleaseEvent(event)
|
||||||
|
return
|
||||||
|
end = self._bounded(event.scenePos())
|
||||||
|
start = self.start
|
||||||
|
self._clear_preview()
|
||||||
|
if start != end:
|
||||||
|
self.editor.create_shape(self.mode, start, end)
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def _bounded(self, point: QPointF) -> QPointF:
|
||||||
|
rect = self.sceneRect()
|
||||||
|
return QPointF(
|
||||||
|
min(rect.right(), max(rect.left(), _snap(point.x()))),
|
||||||
|
min(rect.bottom(), max(rect.top(), _snap(point.y()))),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _clear_preview(self) -> None:
|
||||||
|
if self.preview is not None:
|
||||||
|
self.removeItem(self.preview)
|
||||||
|
self.preview = None
|
||||||
|
self.start = None
|
||||||
|
|
||||||
|
|
||||||
|
class IconEditorDialog(QDialog):
|
||||||
|
def __init__(self, component: Component, parent=None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.ui = Ui_IconEditorDialog()
|
||||||
|
self.ui.setupUi(self)
|
||||||
|
self.setWindowTitle(f"Icon Editor — {component.name}")
|
||||||
|
self.icon = Icon.from_dict(component.icon.to_dict())
|
||||||
|
self.inputs = deepcopy(component.inputs)
|
||||||
|
self.outputs = deepcopy(component.outputs)
|
||||||
|
self.tool_group = QButtonGroup(self)
|
||||||
|
self.tool_group.setExclusive(True)
|
||||||
|
self.tool_group.addButton(self.ui.pointerButton)
|
||||||
|
self.ui.pointerButton.clicked.connect(lambda: self.set_draw_mode("pointer"))
|
||||||
|
for button, kind in (
|
||||||
|
(self.ui.addRectangleButton, "rectangle"),
|
||||||
|
(self.ui.addCircleButton, "circle"),
|
||||||
|
(self.ui.addEllipseButton, "ellipse"),
|
||||||
|
(self.ui.addLineButton, "line"),
|
||||||
|
(self.ui.addTriangleButton, "triangle"),
|
||||||
|
(self.ui.addTextButton, "text"),
|
||||||
|
):
|
||||||
|
self.tool_group.addButton(button)
|
||||||
|
button.clicked.connect(lambda _checked=False, value=kind: self.set_draw_mode(value))
|
||||||
|
self.ui.deleteSelectedButton.clicked.connect(self.delete_selected)
|
||||||
|
self.scene = IconDrawingScene(self, QRectF(0, 0, self.icon.width, self.icon.height))
|
||||||
|
self.ui.iconView.setScene(self.scene)
|
||||||
|
self.view = self.ui.iconView
|
||||||
|
self.view.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||||
|
self.ui.zoomInButton.clicked.connect(self.view.zoom_in)
|
||||||
|
self.ui.zoomOutButton.clicked.connect(self.view.zoom_out)
|
||||||
|
self.ui.centerButton.clicked.connect(self.view.center_icon)
|
||||||
|
self.scene.addRect(self.scene.sceneRect(), QPen(QColor("#64748b"), 0)).setZValue(-100)
|
||||||
|
for element in self.icon.elements:
|
||||||
|
self.scene.addItem(ShapeItem(element))
|
||||||
|
self._add_ports(self.inputs, "input", 0.0)
|
||||||
|
self._add_ports(self.outputs, "output", self.icon.width)
|
||||||
|
self.view.center_icon()
|
||||||
|
|
||||||
|
def _add_ports(self, ports: list[Port], direction: str, default_x: float) -> None:
|
||||||
|
spacing = self.icon.height / (len(ports) + 1)
|
||||||
|
for index, port in enumerate(ports, 1):
|
||||||
|
saved = port.properties.get("iconPosition", {})
|
||||||
|
position = QPointF(
|
||||||
|
float(saved.get("x", default_x)), float(saved.get("y", spacing * index))
|
||||||
|
)
|
||||||
|
self.scene.addItem(PortHandle(port, direction, position))
|
||||||
|
|
||||||
|
def set_draw_mode(self, mode: str) -> None:
|
||||||
|
self.scene.set_mode(mode)
|
||||||
|
self.view.setDragMode(
|
||||||
|
self.view.DragMode.RubberBandDrag if mode == "pointer" else self.view.DragMode.NoDrag
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_shape(self, kind: str, start: QPointF, end: QPointF, text: str = "Text") -> None:
|
||||||
|
left, right = sorted((start.x(), end.x()))
|
||||||
|
top, bottom = sorted((start.y(), end.y()))
|
||||||
|
element = {
|
||||||
|
"type": kind,
|
||||||
|
"x": left,
|
||||||
|
"y": top,
|
||||||
|
"width": max(_icon_grid_size(), right - left),
|
||||||
|
"height": max(_icon_grid_size(), bottom - top),
|
||||||
|
"fill": "#dbeafe",
|
||||||
|
"stroke": "#303030",
|
||||||
|
"lineWidth": 1.5,
|
||||||
|
"lineStyle": "solid",
|
||||||
|
}
|
||||||
|
if kind == "circle":
|
||||||
|
element["width"] = element["height"] = min(element["width"], element["height"])
|
||||||
|
if kind == "line":
|
||||||
|
element["x"], element["y"] = start.x(), start.y()
|
||||||
|
element["width"], element["height"] = end.x() - start.x(), end.y() - start.y()
|
||||||
|
element["fill"] = "none"
|
||||||
|
if kind == "rectangle":
|
||||||
|
element["cornerRadius"] = 0
|
||||||
|
if kind == "text":
|
||||||
|
element.update(
|
||||||
|
{
|
||||||
|
"text": text,
|
||||||
|
"fontSize": 12,
|
||||||
|
"color": "#202020",
|
||||||
|
"fill": "none",
|
||||||
|
"lineStyle": "none",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.icon.elements.append(element)
|
||||||
|
item = ShapeItem(element)
|
||||||
|
self.scene.addItem(item)
|
||||||
|
item.setSelected(True)
|
||||||
|
|
||||||
|
def delete_selected(self) -> None:
|
||||||
|
for item in self.scene.selectedItems():
|
||||||
|
if isinstance(item, ShapeItem):
|
||||||
|
self.scene.removeItem(item)
|
||||||
|
item.element["_deleted"] = True
|
||||||
|
|
||||||
|
def accept(self) -> None:
|
||||||
|
self.icon.elements = [
|
||||||
|
element for element in self.icon.elements if not element.pop("_deleted", False)
|
||||||
|
]
|
||||||
|
super().accept()
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from PySide6.QtCore import QPointF, QRectF, Qt
|
from PySide6.QtCore import QPointF, QRectF, Qt
|
||||||
from PySide6.QtGui import QColor, QFont, QIcon, QPainter, QPen, QPixmap, QPolygonF
|
from PySide6.QtGui import QColor, QFont, QIcon, QPainter, QPen, QPixmap, QPolygonF
|
||||||
|
|
||||||
from bedit.document.model import Icon
|
from bedit.core.model import Icon
|
||||||
|
|
||||||
|
|
||||||
def icon_bounds(icon: Icon) -> QRectF:
|
def icon_bounds(icon: Icon) -> QRectF:
|
||||||
@@ -13,14 +13,14 @@ def icon_bounds(icon: Icon) -> QRectF:
|
|||||||
rect = QRectF(
|
rect = QRectF(
|
||||||
float(element.get("x", 0)),
|
float(element.get("x", 0)),
|
||||||
float(element.get("y", 0)),
|
float(element.get("y", 0)),
|
||||||
max(0.0, float(element.get("width", 0))),
|
float(element.get("width", 0)),
|
||||||
max(0.0, float(element.get("height", 0))),
|
float(element.get("height", 0)),
|
||||||
)
|
).normalized()
|
||||||
bounds = rect if bounds.isNull() else bounds.united(rect)
|
bounds = rect if bounds.isNull() else bounds.united(rect)
|
||||||
return bounds if not bounds.isNull() else QRectF(32, 32, 64, 64)
|
return bounds if not bounds.isNull() else QRectF(32, 32, 64, 64)
|
||||||
|
|
||||||
|
|
||||||
def _pen(element: dict) -> QPen:
|
def shape_pen(element: dict) -> QPen:
|
||||||
styles = {
|
styles = {
|
||||||
"solid": Qt.PenStyle.SolidLine,
|
"solid": Qt.PenStyle.SolidLine,
|
||||||
"dash": Qt.PenStyle.DashLine,
|
"dash": Qt.PenStyle.DashLine,
|
||||||
@@ -51,12 +51,16 @@ def paint_icon(
|
|||||||
for element in icon.elements:
|
for element in icon.elements:
|
||||||
kind = element.get("type", "rectangle")
|
kind = element.get("type", "rectangle")
|
||||||
rect = QRectF(
|
rect = QRectF(
|
||||||
float(element.get("x", 0)), float(element.get("y", 0)),
|
float(element.get("x", 0)),
|
||||||
float(element.get("width", 20)), float(element.get("height", 20)),
|
float(element.get("y", 0)),
|
||||||
|
float(element.get("width", 20)),
|
||||||
|
float(element.get("height", 20)),
|
||||||
)
|
)
|
||||||
painter.setPen(_pen(element))
|
painter.setPen(shape_pen(element))
|
||||||
fill = element.get("fill", "#ffffff")
|
fill = element.get("fill", "#ffffff")
|
||||||
painter.setBrush(Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill))
|
painter.setBrush(
|
||||||
|
Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill)
|
||||||
|
)
|
||||||
if kind == "rectangle":
|
if kind == "rectangle":
|
||||||
radius = float(element.get("cornerRadius", 0))
|
radius = float(element.get("cornerRadius", 0))
|
||||||
painter.drawRoundedRect(rect, radius, radius)
|
painter.drawRoundedRect(rect, radius, radius)
|
||||||
@@ -65,7 +69,11 @@ def paint_icon(
|
|||||||
elif kind == "line":
|
elif kind == "line":
|
||||||
painter.drawLine(rect.topLeft(), rect.bottomRight())
|
painter.drawLine(rect.topLeft(), rect.bottomRight())
|
||||||
elif kind == "triangle":
|
elif kind == "triangle":
|
||||||
painter.drawPolygon(QPolygonF([QPointF(rect.center().x(), rect.top()), rect.bottomRight(), rect.bottomLeft()]))
|
painter.drawPolygon(
|
||||||
|
QPolygonF(
|
||||||
|
[QPointF(rect.center().x(), rect.top()), rect.bottomRight(), rect.bottomLeft()]
|
||||||
|
)
|
||||||
|
)
|
||||||
elif kind == "text":
|
elif kind == "text":
|
||||||
painter.setPen(QColor(element.get("color", element.get("stroke", "#202020"))))
|
painter.setPen(QColor(element.get("color", element.get("stroke", "#202020"))))
|
||||||
font = QFont()
|
font = QFont()
|
||||||
1344
BEdit/src/bedit/gui/graphics/workspace.py
Normal file
@@ -2,26 +2,34 @@ import json
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from PySide6.QtCore import QSettings, QSize, Qt, Slot
|
from PySide6.QtCore import Qt, Slot
|
||||||
from PySide6.QtGui import QAction, QCloseEvent
|
from PySide6.QtGui import QCloseEvent
|
||||||
from PySide6.QtWidgets import QFileDialog, QMainWindow, QMenu, QMessageBox, QToolBar
|
from PySide6.QtWidgets import (
|
||||||
|
QButtonGroup,
|
||||||
|
QFileDialog,
|
||||||
|
QMainWindow,
|
||||||
|
QMenu,
|
||||||
|
QMessageBox,
|
||||||
|
QTabWidget,
|
||||||
|
)
|
||||||
|
|
||||||
from bedit.component_options_dialog import ComponentOptionsDialog
|
from bedit.core.model import Component, Port
|
||||||
from bedit.document.controller import DocumentController
|
from bedit.core.serializer import JsonDocumentSerializer
|
||||||
from bedit.document.model import Component, Port
|
from bedit.gui.controllers.document import DocumentController
|
||||||
from bedit.document.serializer import JsonDocumentSerializer
|
from bedit.gui.dialogs.component_options import ComponentOptionsDialog
|
||||||
from bedit.item_options_dialog import ItemOptionsDialog
|
from bedit.gui.dialogs.item_options import ItemOptionsDialog
|
||||||
from bedit.library.repository import LibraryRepository
|
from bedit.gui.models.library_repository import LibraryRepository
|
||||||
from bedit.library.tree_model import (
|
from bedit.gui.models.library_tree import (
|
||||||
COMPONENT_ID_ROLE,
|
COMPONENT_ID_ROLE,
|
||||||
COMPONENT_INSTANCE_ROLE,
|
COMPONENT_INSTANCE_ROLE,
|
||||||
ITEM_KIND_ROLE,
|
ITEM_KIND_ROLE,
|
||||||
DocumentTreeModel,
|
DocumentTreeModel,
|
||||||
LibraryTreeModel,
|
LibraryTreeModel,
|
||||||
)
|
)
|
||||||
from bedit.settings_dialog import SettingsDialog
|
from bedit.gui.dialogs.settings import SettingsDialog
|
||||||
from bedit.port_options_dialog import PortOptionsDialog
|
from bedit.gui.dialogs.port_options import PortOptionsDialog
|
||||||
from bedit.ui_main_window import Ui_MainWindow
|
from bedit.gui.preferences import application_settings
|
||||||
|
from bedit.gui.generated.ui_main_window import Ui_MainWindow
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
@@ -31,12 +39,7 @@ class MainWindow(QMainWindow):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.ui = Ui_MainWindow()
|
self.ui = Ui_MainWindow()
|
||||||
self.ui.setupUi(self)
|
self.ui.setupUi(self)
|
||||||
self.ui.emptyPage.setStyleSheet("background-color: #9a9a9a;")
|
self.settings = application_settings()
|
||||||
self.ui.emptyWorkspaceLabel.setStyleSheet(
|
|
||||||
"background: transparent; color: #202020;"
|
|
||||||
)
|
|
||||||
self._create_camera_toolbar()
|
|
||||||
self.settings = QSettings()
|
|
||||||
|
|
||||||
self.libraries = LibraryRepository(self)
|
self.libraries = LibraryRepository(self)
|
||||||
self.document_controller = DocumentController(self)
|
self.document_controller = DocumentController(self)
|
||||||
@@ -55,11 +58,11 @@ class MainWindow(QMainWindow):
|
|||||||
self.ui.leftDockHost.show()
|
self.ui.leftDockHost.show()
|
||||||
self.ui.panel_libraries.show()
|
self.ui.panel_libraries.show()
|
||||||
self.ui.panel_document.show()
|
self.ui.panel_document.show()
|
||||||
self.ui.leftDockHost.splitDockWidget(
|
self.ui.leftDockHost.setTabPosition(
|
||||||
self.ui.panel_document,
|
Qt.DockWidgetArea.LeftDockWidgetArea, QTabWidget.TabPosition.North
|
||||||
self.ui.panel_libraries,
|
|
||||||
Qt.Orientation.Vertical,
|
|
||||||
)
|
)
|
||||||
|
self.ui.leftDockHost.tabifyDockWidget(self.ui.panel_document, self.ui.panel_libraries)
|
||||||
|
self.ui.panel_document.raise_()
|
||||||
self.ui.workspaceSplitter.setSizes([280, 720])
|
self.ui.workspaceSplitter.setSizes([280, 720])
|
||||||
self.reload_libraries()
|
self.reload_libraries()
|
||||||
self._active_graph_changed()
|
self._active_graph_changed()
|
||||||
@@ -67,29 +70,18 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
def _configure_models(self) -> None:
|
def _configure_models(self) -> None:
|
||||||
self.ui.treeView.setModel(self.library_tree_model)
|
self.ui.treeView.setModel(self.library_tree_model)
|
||||||
self.ui.treeView.setIconSize(QSize(28, 28))
|
|
||||||
self.ui.treeView.setStyleSheet("QTreeView::item { height: 32px; }")
|
|
||||||
self.ui.treeView.setHeaderHidden(True)
|
self.ui.treeView.setHeaderHidden(True)
|
||||||
self.ui.treeView.setDragEnabled(True)
|
self.ui.treeView.setDragEnabled(True)
|
||||||
self.ui.treeView.setDragDropMode(self.ui.treeView.DragDropMode.DragOnly)
|
self.ui.treeView.setDragDropMode(self.ui.treeView.DragDropMode.DragOnly)
|
||||||
self.ui.treeView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
self.ui.treeView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||||
self.ui.treeView.customContextMenuRequested.connect(
|
self.ui.treeView.customContextMenuRequested.connect(self.show_external_library_context_menu)
|
||||||
self.show_external_library_context_menu
|
|
||||||
)
|
|
||||||
self.library_tree_model.rebuilt.connect(self.ui.treeView.expandAll)
|
self.library_tree_model.rebuilt.connect(self.ui.treeView.expandAll)
|
||||||
self.ui.documentTreeView.setModel(self.document_tree_model)
|
self.ui.documentTreeView.setModel(self.document_tree_model)
|
||||||
self.ui.documentTreeView.setIconSize(QSize(16, 16))
|
|
||||||
self.ui.documentTreeView.setHeaderHidden(True)
|
self.ui.documentTreeView.setHeaderHidden(True)
|
||||||
self.ui.documentTreeView.setDragEnabled(True)
|
self.ui.documentTreeView.setDragEnabled(True)
|
||||||
self.ui.documentTreeView.setDragDropMode(
|
self.ui.documentTreeView.setDragDropMode(self.ui.documentTreeView.DragDropMode.DragOnly)
|
||||||
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.setContextMenuPolicy(
|
|
||||||
Qt.ContextMenuPolicy.CustomContextMenu
|
|
||||||
)
|
|
||||||
self.ui.documentTreeView.customContextMenuRequested.connect(
|
|
||||||
self.show_library_context_menu
|
|
||||||
)
|
|
||||||
self.ui.documentTreeView.doubleClicked.connect(self.activate_tree_component)
|
self.ui.documentTreeView.doubleClicked.connect(self.activate_tree_component)
|
||||||
self.document_tree_model.rebuilt.connect(self.ui.documentTreeView.expandAll)
|
self.document_tree_model.rebuilt.connect(self.ui.documentTreeView.expandAll)
|
||||||
self.ui.graphView.set_model(self.document_controller)
|
self.ui.graphView.set_model(self.document_controller)
|
||||||
@@ -100,27 +92,35 @@ class MainWindow(QMainWindow):
|
|||||||
self.ui.graphView.selectionAvailabilityChanged.connect(
|
self.ui.graphView.selectionAvailabilityChanged.connect(
|
||||||
lambda _available: self._update_edit_actions()
|
lambda _available: self._update_edit_actions()
|
||||||
)
|
)
|
||||||
|
self.mode_button_group = QButtonGroup(self)
|
||||||
|
self.mode_button_group.setExclusive(True)
|
||||||
|
self.mode_button_group.addButton(self.ui.pointerToolButton)
|
||||||
|
self.mode_button_group.addButton(self.ui.connectToolButton)
|
||||||
|
self.mode_button_group.addButton(self.ui.boxToolButton)
|
||||||
|
self.mode_button_group.addButton(self.ui.lineToolButton)
|
||||||
|
self.mode_button_group.addButton(self.ui.textToolButton)
|
||||||
|
self.routing_button_group = QButtonGroup(self)
|
||||||
|
self.routing_button_group.setExclusive(True)
|
||||||
|
for button in (
|
||||||
|
self.ui.directRoutingButton,
|
||||||
|
self.ui.angledRoutingButton,
|
||||||
|
self.ui.splineRoutingButton,
|
||||||
|
):
|
||||||
|
self.routing_button_group.addButton(button)
|
||||||
self.ui.navigateUpButton.clicked.connect(self.navigate_up)
|
self.ui.navigateUpButton.clicked.connect(self.navigate_up)
|
||||||
|
self.ui.navigateDownButton.clicked.connect(self.navigate_down)
|
||||||
self.ui.pointerToolButton.clicked.connect(lambda: self.set_graph_tool("pointer"))
|
self.ui.pointerToolButton.clicked.connect(lambda: self.set_graph_tool("pointer"))
|
||||||
self.ui.inputToolButton.hide()
|
self.ui.connectToolButton.clicked.connect(lambda: self.set_graph_tool("connect"))
|
||||||
self.ui.outputToolButton.hide()
|
self.ui.boxToolButton.clicked.connect(lambda: self.set_graph_tool("box"))
|
||||||
|
self.ui.lineToolButton.clicked.connect(lambda: self.set_graph_tool("line"))
|
||||||
|
self.ui.textToolButton.clicked.connect(lambda: self.set_graph_tool("text"))
|
||||||
|
self.ui.rotateToolButton.clicked.connect(self.ui.graphView.rotate_selected)
|
||||||
|
self.ui.directRoutingButton.clicked.connect(lambda: self.set_connection_routing("direct"))
|
||||||
|
self.ui.angledRoutingButton.clicked.connect(lambda: self.set_connection_routing("angled"))
|
||||||
|
self.ui.splineRoutingButton.clicked.connect(lambda: self.set_connection_routing("spline"))
|
||||||
self.ui.applyJsonButton.clicked.connect(self.apply_json)
|
self.ui.applyJsonButton.clicked.connect(self.apply_json)
|
||||||
self.document_controller.activeGraphChanged.connect(self._active_graph_changed)
|
self.document_controller.activeGraphChanged.connect(self._active_graph_changed)
|
||||||
|
|
||||||
def _create_camera_toolbar(self) -> None:
|
|
||||||
self.cameraToolbar = QToolBar("Camera", self)
|
|
||||||
self.cameraToolbar.setObjectName("cameraToolbar")
|
|
||||||
self.actionZoomIn = QAction("Zoom In", self)
|
|
||||||
self.actionZoomIn.setShortcut("Ctrl++")
|
|
||||||
self.actionZoomOut = QAction("Zoom Out", self)
|
|
||||||
self.actionZoomOut.setShortcut("Ctrl+-")
|
|
||||||
self.actionCenterView = QAction("Center", self)
|
|
||||||
self.actionCenterView.setShortcut("Ctrl+0")
|
|
||||||
self.cameraToolbar.addActions(
|
|
||||||
(self.actionZoomIn, self.actionZoomOut, self.actionCenterView)
|
|
||||||
)
|
|
||||||
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.cameraToolbar)
|
|
||||||
|
|
||||||
def _connect_actions(self) -> None:
|
def _connect_actions(self) -> None:
|
||||||
self.ui.actionNew.triggered.connect(self.new_document)
|
self.ui.actionNew.triggered.connect(self.new_document)
|
||||||
self.ui.actionOpen.triggered.connect(self.open_document)
|
self.ui.actionOpen.triggered.connect(self.open_document)
|
||||||
@@ -141,9 +141,9 @@ class MainWindow(QMainWindow):
|
|||||||
self.ui.actionPaste.triggered.connect(self.ui.graphView.paste_selection)
|
self.ui.actionPaste.triggered.connect(self.ui.graphView.paste_selection)
|
||||||
self.ui.actionSelectAll.triggered.connect(self.ui.graphView.select_all)
|
self.ui.actionSelectAll.triggered.connect(self.ui.graphView.select_all)
|
||||||
self.ui.actionRotateClockwise.triggered.connect(self.ui.graphView.rotate_selected)
|
self.ui.actionRotateClockwise.triggered.connect(self.ui.graphView.rotate_selected)
|
||||||
self.actionZoomIn.triggered.connect(self.ui.graphView.zoom_in)
|
self.ui.actionZoomIn.triggered.connect(self.ui.graphView.zoom_in)
|
||||||
self.actionZoomOut.triggered.connect(self.ui.graphView.zoom_out)
|
self.ui.actionZoomOut.triggered.connect(self.ui.graphView.zoom_out)
|
||||||
self.actionCenterView.triggered.connect(self.ui.graphView.center_workspace)
|
self.ui.actionCenterView.triggered.connect(self.ui.graphView.center_workspace)
|
||||||
self.document_controller.undo_stack.canUndoChanged.connect(self.ui.actionUndo.setEnabled)
|
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.undo_stack.canRedoChanged.connect(self.ui.actionRedo.setEnabled)
|
||||||
self.document_controller.modifiedChanged.connect(lambda _modified: self._update_title())
|
self.document_controller.modifiedChanged.connect(lambda _modified: self._update_title())
|
||||||
@@ -160,8 +160,7 @@ class MainWindow(QMainWindow):
|
|||||||
for toolbar in (
|
for toolbar in (
|
||||||
self.ui.fileToolbar,
|
self.ui.fileToolbar,
|
||||||
self.ui.editToolbar,
|
self.ui.editToolbar,
|
||||||
self.ui.transformToolbar,
|
self.ui.cameraToolbar,
|
||||||
self.cameraToolbar,
|
|
||||||
):
|
):
|
||||||
self.ui.menuToolbars.addAction(toolbar.toggleViewAction())
|
self.ui.menuToolbars.addAction(toolbar.toggleViewAction())
|
||||||
|
|
||||||
@@ -184,7 +183,11 @@ class MainWindow(QMainWindow):
|
|||||||
if self.document_controller.document is None:
|
if self.document_controller.document is None:
|
||||||
self.setWindowTitle("BEdit")
|
self.setWindowTitle("BEdit")
|
||||||
return
|
return
|
||||||
name = self.document_controller.file_path.name if self.document_controller.file_path else "Untitled"
|
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 ""
|
modified = "*" if not self.document_controller.undo_stack.isClean() else ""
|
||||||
self.setWindowTitle(f"{modified}{name} — BEdit")
|
self.setWindowTitle(f"{modified}{name} — BEdit")
|
||||||
|
|
||||||
@@ -196,12 +199,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.ui.workspaceModeLabel.setText("")
|
self.ui.workspaceModeLabel.setText("")
|
||||||
self.ui.workspaceStack.setCurrentWidget(self.ui.emptyPage)
|
self.ui.workspaceStack.setCurrentWidget(self.ui.emptyPage)
|
||||||
self.ui.applyJsonButton.setVisible(False)
|
self.ui.applyJsonButton.setVisible(False)
|
||||||
for button in (
|
self._set_graph_controls_visible(False)
|
||||||
self.ui.pointerToolButton,
|
|
||||||
self.ui.inputToolButton,
|
|
||||||
self.ui.outputToolButton,
|
|
||||||
):
|
|
||||||
button.setVisible(False)
|
|
||||||
self._update_edit_actions()
|
self._update_edit_actions()
|
||||||
return
|
return
|
||||||
self.ui.graphBreadcrumbLabel.setText(" › ".join(self.document_controller.breadcrumb()))
|
self.ui.graphBreadcrumbLabel.setText(" › ".join(self.document_controller.breadcrumb()))
|
||||||
@@ -212,9 +210,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.ui.workspaceModeLabel.setText("Graph" if is_graph else "Text")
|
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.workspaceStack.setCurrentWidget(self.ui.graphPage if is_graph else self.ui.jsonPage)
|
||||||
self.ui.applyJsonButton.setVisible(not is_graph)
|
self.ui.applyJsonButton.setVisible(not is_graph)
|
||||||
self.ui.pointerToolButton.setVisible(is_graph)
|
self._set_graph_controls_visible(is_graph)
|
||||||
self.ui.inputToolButton.hide()
|
|
||||||
self.ui.outputToolButton.hide()
|
|
||||||
if is_graph:
|
if is_graph:
|
||||||
self.set_graph_tool("pointer")
|
self.set_graph_tool("pointer")
|
||||||
else:
|
else:
|
||||||
@@ -224,23 +220,66 @@ class MainWindow(QMainWindow):
|
|||||||
def _update_edit_actions(self) -> None:
|
def _update_edit_actions(self) -> None:
|
||||||
component = self.document_controller.active_component
|
component = self.document_controller.active_component
|
||||||
is_graph = component is not None and component.implementation_kind == "graph"
|
is_graph = component is not None and component.implementation_kind == "graph"
|
||||||
has_selection = bool(self.ui.graphView.scene() and self.ui.graphView.scene().selectedItems())
|
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):
|
for action in (self.ui.actionCut, self.ui.actionCopy, self.ui.actionDelete):
|
||||||
action.setEnabled(is_graph and has_selection)
|
action.setEnabled(is_graph and has_selection)
|
||||||
self.ui.actionRotateClockwise.setEnabled(
|
self.ui.actionRotateClockwise.setEnabled(
|
||||||
is_graph and self.ui.graphView.has_selected_components()
|
is_graph and self.ui.graphView.has_selected_components()
|
||||||
)
|
)
|
||||||
|
self.ui.rotateToolButton.setEnabled(
|
||||||
|
is_graph and self.ui.graphView.has_selected_components()
|
||||||
|
)
|
||||||
self.ui.actionSelectAll.setEnabled(is_graph)
|
self.ui.actionSelectAll.setEnabled(is_graph)
|
||||||
self.ui.actionPaste.setEnabled(is_graph)
|
self.ui.actionPaste.setEnabled(is_graph)
|
||||||
|
self.ui.navigateDownButton.setEnabled(
|
||||||
|
is_graph and self.ui.graphView.has_single_selected_component()
|
||||||
|
)
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def navigate_up(self) -> None:
|
def navigate_up(self) -> None:
|
||||||
if self._resolve_source_edits():
|
if self._resolve_source_edits():
|
||||||
self.document_controller.navigate_up()
|
self.document_controller.navigate_up()
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def navigate_down(self) -> None:
|
||||||
|
if self._resolve_source_edits():
|
||||||
|
self.ui.graphView.open_selected_component()
|
||||||
|
|
||||||
|
def _set_graph_controls_visible(self, visible: bool) -> None:
|
||||||
|
for widget in (
|
||||||
|
self.ui.pointerToolButton,
|
||||||
|
self.ui.connectToolButton,
|
||||||
|
self.ui.boxToolButton,
|
||||||
|
self.ui.lineToolButton,
|
||||||
|
self.ui.textToolButton,
|
||||||
|
self.ui.rotateToolButton,
|
||||||
|
self.ui.routingLabel,
|
||||||
|
self.ui.directRoutingButton,
|
||||||
|
self.ui.angledRoutingButton,
|
||||||
|
self.ui.splineRoutingButton,
|
||||||
|
):
|
||||||
|
widget.setVisible(visible)
|
||||||
|
|
||||||
def set_graph_tool(self, mode: str) -> None:
|
def set_graph_tool(self, mode: str) -> None:
|
||||||
self.ui.graphView.set_tool_mode("pointer")
|
self.ui.graphView.set_tool_mode(mode)
|
||||||
self.ui.pointerToolButton.setChecked(True)
|
{
|
||||||
|
"pointer": self.ui.pointerToolButton,
|
||||||
|
"connect": self.ui.connectToolButton,
|
||||||
|
"box": self.ui.boxToolButton,
|
||||||
|
"line": self.ui.lineToolButton,
|
||||||
|
"text": self.ui.textToolButton,
|
||||||
|
}[mode].setChecked(True)
|
||||||
|
|
||||||
|
def set_connection_routing(self, routing: str) -> None:
|
||||||
|
self.ui.graphView.set_connection_routing(routing)
|
||||||
|
buttons = {
|
||||||
|
"direct": self.ui.directRoutingButton,
|
||||||
|
"angled": self.ui.angledRoutingButton,
|
||||||
|
"spline": self.ui.splineRoutingButton,
|
||||||
|
}
|
||||||
|
buttons[routing].setChecked(True)
|
||||||
|
|
||||||
def _load_source_json(self) -> None:
|
def _load_source_json(self) -> None:
|
||||||
component = self.document_controller.active_component
|
component = self.document_controller.active_component
|
||||||
@@ -291,9 +330,7 @@ class MainWindow(QMainWindow):
|
|||||||
raise ValueError("'source' must be an object")
|
raise ValueError("'source' must be an object")
|
||||||
inputs = [Port.from_dict(item) for item in data["inputs"]]
|
inputs = [Port.from_dict(item) for item in data["inputs"]]
|
||||||
outputs = [Port.from_dict(item) for item in data["outputs"]]
|
outputs = [Port.from_dict(item) for item in data["outputs"]]
|
||||||
self.document_controller.replace_active_text_definition(
|
self.document_controller.replace_active_text_definition(inputs, outputs, data["source"])
|
||||||
inputs, outputs, data["source"]
|
|
||||||
)
|
|
||||||
except (TypeError, ValueError, json.JSONDecodeError) as error:
|
except (TypeError, ValueError, json.JSONDecodeError) as error:
|
||||||
QMessageBox.critical(self, "Invalid text component JSON", str(error))
|
QMessageBox.critical(self, "Invalid text component JSON", str(error))
|
||||||
return False
|
return False
|
||||||
@@ -476,9 +513,7 @@ class MainWindow(QMainWindow):
|
|||||||
try:
|
try:
|
||||||
if library is not None:
|
if library is not None:
|
||||||
library.document.validate()
|
library.document.validate()
|
||||||
JsonDocumentSerializer.save(
|
JsonDocumentSerializer.save(library.document, Path(library.source_path))
|
||||||
library.document, Path(library.source_path)
|
|
||||||
)
|
|
||||||
except (OSError, ValueError) as error:
|
except (OSError, ValueError) as error:
|
||||||
component.inputs, component.outputs = old_inputs, old_outputs
|
component.inputs, component.outputs = old_inputs, old_outputs
|
||||||
QMessageBox.warning(self, "Cannot change library ports", str(error))
|
QMessageBox.warning(self, "Cannot change library ports", str(error))
|
||||||
@@ -538,9 +573,7 @@ class MainWindow(QMainWindow):
|
|||||||
connection = owner.graph.connections.get(connection_id)
|
connection = owner.graph.connections.get(connection_id)
|
||||||
if connection is None:
|
if connection is None:
|
||||||
return
|
return
|
||||||
dialog = ItemOptionsDialog(
|
dialog = ItemOptionsDialog("Connection Options", connection.name, self, name_required=False)
|
||||||
"Connection Options", connection.name, self, name_required=False
|
|
||||||
)
|
|
||||||
if dialog.exec() == dialog.DialogCode.Accepted:
|
if dialog.exec() == dialog.DialogCode.Accepted:
|
||||||
self.document_controller.rename_connection(connection_id, dialog.name)
|
self.document_controller.rename_connection(connection_id, dialog.name)
|
||||||
|
|
||||||
1
BEdit/src/bedit/gui/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Qt model/view adapters and repositories."""
|
||||||
31
BEdit/src/bedit/gui/models/library_repository.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PySide6.QtCore import QObject, Signal
|
||||||
|
|
||||||
|
from bedit.core.libraries import LibraryDocument, library_candidates, load_library_file
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
for candidate in library_candidates(path):
|
||||||
|
try:
|
||||||
|
libraries.append(load_library_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)
|
||||||
@@ -3,10 +3,10 @@ import json
|
|||||||
from PySide6.QtCore import QByteArray, QMimeData, QModelIndex, Qt, Signal
|
from PySide6.QtCore import QByteArray, QMimeData, QModelIndex, Qt, Signal
|
||||||
from PySide6.QtGui import QStandardItem, QStandardItemModel
|
from PySide6.QtGui import QStandardItem, QStandardItemModel
|
||||||
|
|
||||||
from bedit.document.controller import DocumentController
|
from bedit.core.model import Component
|
||||||
from bedit.document.model import Component
|
from bedit.gui.controllers.document import DocumentController
|
||||||
from bedit.icon_renderer import library_icon
|
from bedit.gui.graphics.icon_renderer import library_icon
|
||||||
from bedit.library.repository import LibraryRepository
|
from bedit.gui.models.library_repository import LibraryRepository
|
||||||
|
|
||||||
|
|
||||||
COMPONENT_ROLE = Qt.ItemDataRole.UserRole + 1
|
COMPONENT_ROLE = Qt.ItemDataRole.UserRole + 1
|
||||||
6
BEdit/src/bedit/gui/preferences.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from PySide6.QtCore import QSettings
|
||||||
|
|
||||||
|
|
||||||
|
def application_settings() -> QSettings:
|
||||||
|
"""Return BEdit's explicit, disk-backed settings store."""
|
||||||
|
return QSettings("BEdit", "BEdit")
|
||||||
@@ -1,380 +0,0 @@
|
|||||||
from copy import deepcopy
|
|
||||||
|
|
||||||
from PySide6.QtCore import QPointF, QRectF, QSettings, Qt
|
|
||||||
from PySide6.QtGui import QColor, QPainter, QPen, QPolygonF
|
|
||||||
from PySide6.QtWidgets import (
|
|
||||||
QColorDialog,
|
|
||||||
QComboBox,
|
|
||||||
QDialog,
|
|
||||||
QDialogButtonBox,
|
|
||||||
QDoubleSpinBox,
|
|
||||||
QFormLayout,
|
|
||||||
QGraphicsEllipseItem,
|
|
||||||
QGraphicsItem,
|
|
||||||
QGraphicsObject,
|
|
||||||
QGraphicsScene,
|
|
||||||
QGraphicsSceneContextMenuEvent,
|
|
||||||
QGraphicsSceneMouseEvent,
|
|
||||||
QGraphicsView,
|
|
||||||
QHBoxLayout,
|
|
||||||
QInputDialog,
|
|
||||||
QLabel,
|
|
||||||
QLineEdit,
|
|
||||||
QMenu,
|
|
||||||
QPushButton,
|
|
||||||
QToolButton,
|
|
||||||
QVBoxLayout,
|
|
||||||
QWidget,
|
|
||||||
)
|
|
||||||
|
|
||||||
from bedit.document.model import Component, Icon, Port
|
|
||||||
from bedit.icon_renderer import _pen
|
|
||||||
|
|
||||||
|
|
||||||
def _icon_grid_size() -> int:
|
|
||||||
return QSettings().value("grid/iconSize", 8, type=int)
|
|
||||||
|
|
||||||
|
|
||||||
def _snap(value: float) -> float:
|
|
||||||
grid = _icon_grid_size()
|
|
||||||
return round(value / grid) * grid
|
|
||||||
|
|
||||||
|
|
||||||
class IconEditorView(QGraphicsView):
|
|
||||||
def drawBackground(self, painter: QPainter, rect: QRectF) -> None: # noqa: N802
|
|
||||||
painter.fillRect(rect, QColor("#ffffff"))
|
|
||||||
grid = _icon_grid_size()
|
|
||||||
painter.setPen(QPen(QColor("#dbeafe"), 0))
|
|
||||||
left = int(rect.left()) - int(rect.left()) % grid
|
|
||||||
top = int(rect.top()) - int(rect.top()) % grid
|
|
||||||
for x in range(left, int(rect.right()) + grid, grid):
|
|
||||||
painter.drawLine(x, rect.top(), x, rect.bottom())
|
|
||||||
for y in range(top, int(rect.bottom()) + grid, grid):
|
|
||||||
painter.drawLine(rect.left(), y, rect.right(), y)
|
|
||||||
|
|
||||||
|
|
||||||
class ResizeHandle(QGraphicsEllipseItem):
|
|
||||||
def __init__(self, owner: "ShapeItem") -> None:
|
|
||||||
super().__init__(-4, -4, 8, 8, owner)
|
|
||||||
self.owner = owner
|
|
||||||
self.setBrush(QColor("#ffffff"))
|
|
||||||
self.setPen(QPen(QColor("#2563eb"), 1.5))
|
|
||||||
self.setCursor(Qt.CursorShape.SizeFDiagCursor)
|
|
||||||
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable)
|
|
||||||
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
|
|
||||||
self.setZValue(20)
|
|
||||||
|
|
||||||
def itemChange(self, change, value): # noqa: N802
|
|
||||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
|
|
||||||
maximum = (
|
|
||||||
self.owner.scene().sceneRect().bottomRight() - self.owner.pos()
|
|
||||||
if self.owner.scene()
|
|
||||||
else QPointF(128, 128)
|
|
||||||
)
|
|
||||||
value = QPointF(
|
|
||||||
min(maximum.x(), max(_icon_grid_size(), _snap(value.x()))),
|
|
||||||
min(maximum.y(), max(_icon_grid_size(), _snap(value.y()))),
|
|
||||||
)
|
|
||||||
if self.owner.element.get("type") == "circle":
|
|
||||||
side = min(maximum.x(), maximum.y(), max(value.x(), value.y()))
|
|
||||||
value = QPointF(side, side)
|
|
||||||
self.owner.prepareGeometryChange()
|
|
||||||
self.owner.element["width"] = value.x()
|
|
||||||
self.owner.element["height"] = value.y()
|
|
||||||
self.owner.update()
|
|
||||||
return super().itemChange(change, value)
|
|
||||||
|
|
||||||
|
|
||||||
class ColorButton(QPushButton):
|
|
||||||
def __init__(self, color: str, allow_none: bool = False, parent=None) -> None:
|
|
||||||
super().__init__(parent)
|
|
||||||
self.color = color
|
|
||||||
self.allow_none = allow_none
|
|
||||||
self.clicked.connect(self.choose)
|
|
||||||
self._refresh()
|
|
||||||
|
|
||||||
def _refresh(self) -> None:
|
|
||||||
self.setText("No fill" if self.color == "none" else self.color)
|
|
||||||
swatch = "transparent" if self.color == "none" else self.color
|
|
||||||
self.setStyleSheet(f"QPushButton {{ background: {swatch}; }}")
|
|
||||||
|
|
||||||
def choose(self) -> None:
|
|
||||||
initial = QColor("#ffffff" if self.color == "none" else self.color)
|
|
||||||
color = QColorDialog.getColor(initial, self, "Choose colour", QColorDialog.ColorDialogOption.ShowAlphaChannel)
|
|
||||||
if color.isValid():
|
|
||||||
self.color = color.name(QColor.NameFormat.HexArgb) if color.alpha() < 255 else color.name()
|
|
||||||
self._refresh()
|
|
||||||
|
|
||||||
|
|
||||||
class ShapeOptionsDialog(QDialog):
|
|
||||||
def __init__(self, element: dict, parent=None) -> None:
|
|
||||||
super().__init__(parent)
|
|
||||||
self.element = deepcopy(element)
|
|
||||||
self.setWindowTitle("Shape Options")
|
|
||||||
layout = QVBoxLayout(self)
|
|
||||||
form = QFormLayout()
|
|
||||||
self.line_style = QComboBox()
|
|
||||||
self.line_style.addItems(["solid", "dash", "dot", "dash-dot", "none"])
|
|
||||||
self.line_style.setCurrentText(self.element.get("lineStyle", "solid"))
|
|
||||||
self.line_width = QDoubleSpinBox()
|
|
||||||
self.line_width.setRange(0.1, 20.0)
|
|
||||||
self.line_width.setValue(float(self.element.get("lineWidth", 1.5)))
|
|
||||||
self.stroke = ColorButton(self.element.get("stroke", "#303030"))
|
|
||||||
self.fill_type = QComboBox()
|
|
||||||
self.fill_type.addItems(["solid", "none"])
|
|
||||||
fill = self.element.get("fill", "#ffffff")
|
|
||||||
self.fill_type.setCurrentText("none" if fill in {"none", "transparent", ""} else "solid")
|
|
||||||
self.fill = ColorButton("#ffffff" if fill in {"none", "transparent", ""} else fill)
|
|
||||||
self.width = QDoubleSpinBox()
|
|
||||||
self.width.setRange(1, 500)
|
|
||||||
self.width.setValue(float(self.element.get("width", 20)))
|
|
||||||
self.height = QDoubleSpinBox()
|
|
||||||
self.height.setRange(1, 500)
|
|
||||||
self.height.setValue(float(self.element.get("height", 20)))
|
|
||||||
form.addRow("Width:", self.width)
|
|
||||||
form.addRow("Height:", self.height)
|
|
||||||
form.addRow("Line style:", self.line_style)
|
|
||||||
form.addRow("Line width:", self.line_width)
|
|
||||||
form.addRow("Line colour:", self.stroke)
|
|
||||||
if self.element.get("type") != "line":
|
|
||||||
form.addRow("Fill type:", self.fill_type)
|
|
||||||
form.addRow("Fill colour:", self.fill)
|
|
||||||
self.radius = None
|
|
||||||
if self.element.get("type") == "rectangle":
|
|
||||||
self.radius = QDoubleSpinBox()
|
|
||||||
self.radius.setRange(0, 50)
|
|
||||||
self.radius.setValue(float(self.element.get("cornerRadius", 0)))
|
|
||||||
form.addRow("Corner radius:", self.radius)
|
|
||||||
self.text_edit = None
|
|
||||||
self.font_size = None
|
|
||||||
if self.element.get("type") == "text":
|
|
||||||
self.text_edit = QLineEdit(str(self.element.get("text", "Text")))
|
|
||||||
self.font_size = QDoubleSpinBox()
|
|
||||||
self.font_size.setRange(4, 96)
|
|
||||||
self.font_size.setValue(float(self.element.get("fontSize", 12)))
|
|
||||||
form.addRow("Text:", self.text_edit)
|
|
||||||
form.addRow("Font size:", self.font_size)
|
|
||||||
layout.addLayout(form)
|
|
||||||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
|
||||||
buttons.accepted.connect(self.accept)
|
|
||||||
buttons.rejected.connect(self.reject)
|
|
||||||
layout.addWidget(buttons)
|
|
||||||
|
|
||||||
def accept(self) -> None:
|
|
||||||
self.element["lineStyle"] = self.line_style.currentText()
|
|
||||||
self.element["lineWidth"] = self.line_width.value()
|
|
||||||
self.element["stroke"] = self.stroke.color
|
|
||||||
self.element["width"] = self.width.value()
|
|
||||||
self.element["height"] = self.height.value()
|
|
||||||
if self.element.get("type") != "line":
|
|
||||||
self.element["fill"] = self.fill.color if self.fill_type.currentText() == "solid" else "none"
|
|
||||||
if self.radius is not None:
|
|
||||||
self.element["cornerRadius"] = self.radius.value()
|
|
||||||
if self.text_edit is not None:
|
|
||||||
self.element["text"] = self.text_edit.text()
|
|
||||||
self.element["fontSize"] = self.font_size.value()
|
|
||||||
self.element["color"] = self.stroke.color
|
|
||||||
super().accept()
|
|
||||||
|
|
||||||
|
|
||||||
class ShapeItem(QGraphicsObject):
|
|
||||||
def __init__(self, element: dict) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.element = element
|
|
||||||
self.setPos(float(element.get("x", 0)), float(element.get("y", 0)))
|
|
||||||
self.setFlags(QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
|
|
||||||
self.resize_handle = ResizeHandle(self)
|
|
||||||
self.resize_handle.setPos(float(element.get("width", 20)), float(element.get("height", 20)))
|
|
||||||
self.resize_handle.hide()
|
|
||||||
|
|
||||||
def boundingRect(self) -> QRectF: # noqa: N802
|
|
||||||
margin = max(3.0, float(self.element.get("lineWidth", 1.5)))
|
|
||||||
return QRectF(0, 0, float(self.element.get("width", 20)), float(self.element.get("height", 20))).adjusted(-margin, -margin, margin, margin)
|
|
||||||
|
|
||||||
def paint(self, painter: QPainter, option, widget=None) -> None:
|
|
||||||
del option, widget
|
|
||||||
rect = QRectF(0, 0, float(self.element.get("width", 20)), float(self.element.get("height", 20)))
|
|
||||||
painter.setPen(_pen(self.element))
|
|
||||||
fill = self.element.get("fill", "none")
|
|
||||||
painter.setBrush(Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill))
|
|
||||||
kind = self.element.get("type")
|
|
||||||
if kind == "rectangle":
|
|
||||||
radius = float(self.element.get("cornerRadius", 0))
|
|
||||||
painter.drawRoundedRect(rect, radius, radius)
|
|
||||||
elif kind in {"circle", "ellipse"}:
|
|
||||||
painter.drawEllipse(rect)
|
|
||||||
elif kind == "line":
|
|
||||||
painter.drawLine(rect.topLeft(), rect.bottomRight())
|
|
||||||
elif kind == "triangle":
|
|
||||||
painter.drawPolygon(QPolygonF([QPointF(rect.center().x(), 0), rect.bottomRight(), rect.bottomLeft()]))
|
|
||||||
elif kind == "text":
|
|
||||||
painter.setPen(QColor(self.element.get("color", "#202020")))
|
|
||||||
font = painter.font()
|
|
||||||
font.setPointSizeF(float(self.element.get("fontSize", 12)))
|
|
||||||
painter.setFont(font)
|
|
||||||
painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, str(self.element.get("text", "Text")))
|
|
||||||
if self.isSelected():
|
|
||||||
painter.setBrush(Qt.BrushStyle.NoBrush)
|
|
||||||
painter.setPen(QPen(QColor("#2563eb"), 1, Qt.PenStyle.DashLine))
|
|
||||||
painter.drawRect(rect)
|
|
||||||
|
|
||||||
def itemChange(self, change, value): # noqa: N802
|
|
||||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
|
|
||||||
bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128)
|
|
||||||
value = QPointF(
|
|
||||||
max(
|
|
||||||
bounds.left(),
|
|
||||||
min(
|
|
||||||
bounds.right() - float(self.element.get("width", 20)),
|
|
||||||
_snap(value.x()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
max(
|
|
||||||
bounds.top(),
|
|
||||||
min(
|
|
||||||
bounds.bottom() - float(self.element.get("height", 20)),
|
|
||||||
_snap(value.y()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
elif change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
|
||||||
self.element["x"], self.element["y"] = value.x(), value.y()
|
|
||||||
elif change == QGraphicsItem.GraphicsItemChange.ItemSelectedHasChanged:
|
|
||||||
self.resize_handle.setVisible(bool(value))
|
|
||||||
return super().itemChange(change, value)
|
|
||||||
|
|
||||||
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
|
||||||
super().mouseReleaseEvent(event)
|
|
||||||
bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128)
|
|
||||||
self.setPos(
|
|
||||||
max(bounds.left(), min(bounds.right() - float(self.element.get("width", 20)), _snap(self.pos().x()))),
|
|
||||||
max(bounds.top(), min(bounds.bottom() - float(self.element.get("height", 20)), _snap(self.pos().y()))),
|
|
||||||
)
|
|
||||||
|
|
||||||
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
|
|
||||||
menu = QMenu()
|
|
||||||
options = menu.addAction("Shape Options…")
|
|
||||||
delete = menu.addAction("Delete Shape")
|
|
||||||
chosen = menu.exec(event.screenPos())
|
|
||||||
if chosen is options:
|
|
||||||
dialog = ShapeOptionsDialog(self.element)
|
|
||||||
if dialog.exec() == dialog.DialogCode.Accepted:
|
|
||||||
self.prepareGeometryChange()
|
|
||||||
self.element.clear()
|
|
||||||
self.element.update(dialog.element)
|
|
||||||
self.resize_handle.setPos(
|
|
||||||
float(self.element.get("width", 20)),
|
|
||||||
float(self.element.get("height", 20)),
|
|
||||||
)
|
|
||||||
self.update()
|
|
||||||
elif chosen is delete and self.scene() is not None:
|
|
||||||
self.scene().removeItem(self)
|
|
||||||
self.element["_deleted"] = True
|
|
||||||
event.accept()
|
|
||||||
|
|
||||||
|
|
||||||
class PortHandle(QGraphicsEllipseItem):
|
|
||||||
def __init__(self, port: Port, direction: str, position: QPointF) -> None:
|
|
||||||
super().__init__(-5, -5, 10, 10)
|
|
||||||
self.port, self.direction = port, direction
|
|
||||||
self.setPos(position)
|
|
||||||
self.setBrush(QColor("#16a34a" if direction == "input" else "#dc2626"))
|
|
||||||
self.setPen(QPen(QColor("#ffffff"), 1.5))
|
|
||||||
self.setToolTip(f"{direction.title()}: {port.name} (drag to position)")
|
|
||||||
self.setZValue(10)
|
|
||||||
self.setFlags(QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
|
|
||||||
|
|
||||||
def itemChange(self, change, value): # noqa: N802
|
|
||||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
|
|
||||||
bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128)
|
|
||||||
value = QPointF(
|
|
||||||
min(bounds.right(), max(bounds.left(), _snap(value.x()))),
|
|
||||||
min(bounds.bottom(), max(bounds.top(), _snap(value.y()))),
|
|
||||||
)
|
|
||||||
elif change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
|
||||||
self.port.properties["iconPosition"] = {"x": value.x(), "y": value.y()}
|
|
||||||
return super().itemChange(change, value)
|
|
||||||
|
|
||||||
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
|
||||||
super().mouseReleaseEvent(event)
|
|
||||||
bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128)
|
|
||||||
self.setPos(
|
|
||||||
min(bounds.right(), max(bounds.left(), _snap(self.pos().x()))),
|
|
||||||
min(bounds.bottom(), max(bounds.top(), _snap(self.pos().y()))),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class IconEditorDialog(QDialog):
|
|
||||||
def __init__(self, component: Component, parent=None) -> None:
|
|
||||||
super().__init__(parent)
|
|
||||||
self.setWindowTitle(f"Icon Editor — {component.name}")
|
|
||||||
self.resize(850, 600)
|
|
||||||
self.icon = Icon.from_dict(component.icon.to_dict())
|
|
||||||
self.inputs = deepcopy(component.inputs)
|
|
||||||
self.outputs = deepcopy(component.outputs)
|
|
||||||
layout = QVBoxLayout(self)
|
|
||||||
toolbar = QHBoxLayout()
|
|
||||||
toolbar.addWidget(QLabel("Add:"))
|
|
||||||
for kind in ("rectangle", "circle", "ellipse", "line", "triangle", "text"):
|
|
||||||
button = QToolButton()
|
|
||||||
button.setText(kind.title())
|
|
||||||
button.clicked.connect(lambda _checked=False, value=kind: self.add_shape(value))
|
|
||||||
toolbar.addWidget(button)
|
|
||||||
toolbar.addStretch()
|
|
||||||
delete = QPushButton("Delete selected")
|
|
||||||
delete.clicked.connect(self.delete_selected)
|
|
||||||
toolbar.addWidget(delete)
|
|
||||||
layout.addLayout(toolbar)
|
|
||||||
self.scene = QGraphicsScene(0, 0, self.icon.width, self.icon.height, self)
|
|
||||||
self.view = IconEditorView(self.scene)
|
|
||||||
self.view.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
||||||
self.view.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
|
|
||||||
self.scene.addRect(self.scene.sceneRect(), QPen(QColor("#64748b"), 0)).setZValue(-100)
|
|
||||||
layout.addWidget(self.view, 1)
|
|
||||||
layout.addWidget(QLabel("Green points are inputs; red points are outputs. Drag them to place connection anchors."))
|
|
||||||
for element in self.icon.elements:
|
|
||||||
self.scene.addItem(ShapeItem(element))
|
|
||||||
self._add_ports(self.inputs, "input", 0.0)
|
|
||||||
self._add_ports(self.outputs, "output", self.icon.width)
|
|
||||||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
|
||||||
buttons.accepted.connect(self.accept)
|
|
||||||
buttons.rejected.connect(self.reject)
|
|
||||||
layout.addWidget(buttons)
|
|
||||||
self.view.fitInView(self.scene.sceneRect().adjusted(-10, -10, 10, 10), Qt.AspectRatioMode.KeepAspectRatio)
|
|
||||||
|
|
||||||
def _add_ports(self, ports: list[Port], direction: str, default_x: float) -> None:
|
|
||||||
spacing = self.icon.height / (len(ports) + 1)
|
|
||||||
for index, port in enumerate(ports, 1):
|
|
||||||
saved = port.properties.get("iconPosition", {})
|
|
||||||
position = QPointF(float(saved.get("x", default_x)), float(saved.get("y", spacing * index)))
|
|
||||||
self.scene.addItem(PortHandle(port, direction, position))
|
|
||||||
|
|
||||||
def add_shape(self, kind: str) -> None:
|
|
||||||
count = len([item for item in self.scene.items() if isinstance(item, ShapeItem)])
|
|
||||||
x, y = 15 + (count * 5) % 40, 15 + (count * 4) % 25
|
|
||||||
element = {"type": kind, "x": x, "y": y, "width": 55, "height": 35, "fill": "#dbeafe", "stroke": "#303030", "lineWidth": 1.5, "lineStyle": "solid"}
|
|
||||||
if kind == "circle":
|
|
||||||
element["width"] = element["height"] = 35
|
|
||||||
if kind == "line":
|
|
||||||
element["fill"] = "none"
|
|
||||||
if kind == "rectangle":
|
|
||||||
element["cornerRadius"] = 0
|
|
||||||
if kind == "text":
|
|
||||||
text, accepted = QInputDialog.getText(self, "Add Text", "Text:", text="Text")
|
|
||||||
if not accepted:
|
|
||||||
return
|
|
||||||
element.update({"text": text, "fontSize": 12, "color": "#202020", "fill": "none", "lineStyle": "none"})
|
|
||||||
self.icon.elements.append(element)
|
|
||||||
item = ShapeItem(element)
|
|
||||||
self.scene.addItem(item)
|
|
||||||
item.setSelected(True)
|
|
||||||
|
|
||||||
def delete_selected(self) -> None:
|
|
||||||
for item in self.scene.selectedItems():
|
|
||||||
if isinstance(item, ShapeItem):
|
|
||||||
self.scene.removeItem(item)
|
|
||||||
item.element["_deleted"] = True
|
|
||||||
|
|
||||||
def accept(self) -> None:
|
|
||||||
self.icon.elements = [element for element in self.icon.elements if not element.pop("_deleted", False)]
|
|
||||||
super().accept()
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from bedit.library.repository import LibraryRepository, default_library_paths
|
|
||||||
|
|
||||||
__all__ = ["LibraryRepository", "default_library_paths"]
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
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))
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
from copy import deepcopy
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from PySide6.QtCore import Qt
|
|
||||||
from PySide6.QtWidgets import (
|
|
||||||
QComboBox,
|
|
||||||
QDialog,
|
|
||||||
QDialogButtonBox,
|
|
||||||
QFormLayout,
|
|
||||||
QHBoxLayout,
|
|
||||||
QLabel,
|
|
||||||
QLineEdit,
|
|
||||||
QListWidget,
|
|
||||||
QListWidgetItem,
|
|
||||||
QMessageBox,
|
|
||||||
QPushButton,
|
|
||||||
QSplitter,
|
|
||||||
QVBoxLayout,
|
|
||||||
QWidget,
|
|
||||||
)
|
|
||||||
|
|
||||||
from bedit.document.model import Component, Port
|
|
||||||
from bedit.document.port_types import PortTypeRegistry
|
|
||||||
|
|
||||||
|
|
||||||
PORT_ROLE = Qt.ItemDataRole.UserRole
|
|
||||||
|
|
||||||
|
|
||||||
class PortOptionsDialog(QDialog):
|
|
||||||
"""Unified editor for a component's typed, oriented ports."""
|
|
||||||
|
|
||||||
def __init__(self, component: Component, parent=None, *, read_only: bool = False) -> None:
|
|
||||||
super().__init__(parent)
|
|
||||||
self.setWindowTitle(f"Port Options — {component.name}")
|
|
||||||
self.resize(620, 380)
|
|
||||||
self.ports: list[tuple[Port, str]] = [
|
|
||||||
*((deepcopy(port), "input") for port in component.inputs),
|
|
||||||
*((deepcopy(port), "output") for port in component.outputs),
|
|
||||||
]
|
|
||||||
self._loading = False
|
|
||||||
layout = QVBoxLayout(self)
|
|
||||||
splitter = QSplitter()
|
|
||||||
left = QWidget()
|
|
||||||
left_layout = QVBoxLayout(left)
|
|
||||||
self.list = QListWidget()
|
|
||||||
self.list.currentRowChanged.connect(self._load_current)
|
|
||||||
left_layout.addWidget(self.list)
|
|
||||||
port_buttons = QHBoxLayout()
|
|
||||||
self.add_button = QPushButton("Add Port")
|
|
||||||
self.remove_button = QPushButton("Remove Port")
|
|
||||||
self.add_button.clicked.connect(self.add_port)
|
|
||||||
self.remove_button.clicked.connect(self.remove_port)
|
|
||||||
port_buttons.addWidget(self.add_button)
|
|
||||||
port_buttons.addWidget(self.remove_button)
|
|
||||||
left_layout.addLayout(port_buttons)
|
|
||||||
right = QWidget()
|
|
||||||
form = QFormLayout(right)
|
|
||||||
self.name_edit = QLineEdit()
|
|
||||||
self.type_combo = QComboBox()
|
|
||||||
for port_type in PortTypeRegistry.all():
|
|
||||||
self.type_combo.addItem(port_type.display_name, port_type.id)
|
|
||||||
self.orientation_combo = QComboBox()
|
|
||||||
self.orientation_combo.addItem("Input", "input")
|
|
||||||
self.orientation_combo.addItem("Output", "output")
|
|
||||||
form.addRow("Name:", self.name_edit)
|
|
||||||
form.addRow("Type:", self.type_combo)
|
|
||||||
form.addRow("Orientation:", self.orientation_combo)
|
|
||||||
form.addRow("", QLabel("New ports start at (0, 0) in the icon editor."))
|
|
||||||
self.name_edit.textEdited.connect(self._store_current)
|
|
||||||
self.type_combo.currentIndexChanged.connect(self._store_current)
|
|
||||||
self.orientation_combo.currentIndexChanged.connect(self._store_current)
|
|
||||||
splitter.addWidget(left)
|
|
||||||
splitter.addWidget(right)
|
|
||||||
splitter.setSizes([250, 370])
|
|
||||||
layout.addWidget(splitter)
|
|
||||||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
|
||||||
buttons.accepted.connect(self.accept)
|
|
||||||
buttons.rejected.connect(self.reject)
|
|
||||||
layout.addWidget(buttons)
|
|
||||||
if read_only:
|
|
||||||
self.add_button.setEnabled(False)
|
|
||||||
self.remove_button.setEnabled(False)
|
|
||||||
self.name_edit.setReadOnly(True)
|
|
||||||
self.type_combo.setEnabled(False)
|
|
||||||
self.orientation_combo.setEnabled(False)
|
|
||||||
buttons.button(QDialogButtonBox.StandardButton.Ok).setText("Close")
|
|
||||||
buttons.button(QDialogButtonBox.StandardButton.Cancel).hide()
|
|
||||||
self._rebuild_list(0 if self.ports else -1)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def inputs(self) -> list[Port]:
|
|
||||||
return [port for port, orientation in self.ports if orientation == "input"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def outputs(self) -> list[Port]:
|
|
||||||
return [port for port, orientation in self.ports if orientation == "output"]
|
|
||||||
|
|
||||||
def _rebuild_list(self, row: int = -1) -> None:
|
|
||||||
self.list.clear()
|
|
||||||
for port, orientation in self.ports:
|
|
||||||
item = QListWidgetItem(f"{port.name} [{orientation}, {port.type}]")
|
|
||||||
item.setData(PORT_ROLE, port.id)
|
|
||||||
self.list.addItem(item)
|
|
||||||
self.list.setCurrentRow(min(row, len(self.ports) - 1))
|
|
||||||
self._update_enabled()
|
|
||||||
|
|
||||||
def _load_current(self, row: int) -> None:
|
|
||||||
self._loading = True
|
|
||||||
enabled = 0 <= row < len(self.ports)
|
|
||||||
if enabled:
|
|
||||||
port, orientation = self.ports[row]
|
|
||||||
self.name_edit.setText(port.name)
|
|
||||||
self.type_combo.setCurrentIndex(self.type_combo.findData(port.type))
|
|
||||||
self.orientation_combo.setCurrentIndex(self.orientation_combo.findData(orientation))
|
|
||||||
else:
|
|
||||||
self.name_edit.clear()
|
|
||||||
self._loading = False
|
|
||||||
self._update_enabled()
|
|
||||||
|
|
||||||
def _update_enabled(self) -> None:
|
|
||||||
enabled = self.list.currentRow() >= 0
|
|
||||||
self.remove_button.setEnabled(enabled)
|
|
||||||
self.name_edit.setEnabled(enabled)
|
|
||||||
self.type_combo.setEnabled(enabled)
|
|
||||||
self.orientation_combo.setEnabled(enabled)
|
|
||||||
|
|
||||||
def _store_current(self) -> None:
|
|
||||||
row = self.list.currentRow()
|
|
||||||
if self._loading or not (0 <= row < len(self.ports)):
|
|
||||||
return
|
|
||||||
port, _orientation = self.ports[row]
|
|
||||||
port.name = self.name_edit.text()
|
|
||||||
port.type = self.type_combo.currentData()
|
|
||||||
self.ports[row] = (port, self.orientation_combo.currentData())
|
|
||||||
self.list.item(row).setText(
|
|
||||||
f"{port.name} [{self.ports[row][1]}, {port.type}]"
|
|
||||||
)
|
|
||||||
|
|
||||||
def add_port(self) -> None:
|
|
||||||
port = Port(
|
|
||||||
id=f"port-{uuid4().hex[:8]}",
|
|
||||||
name=f"Port {len(self.ports) + 1}",
|
|
||||||
properties={"iconPosition": {"x": 0.0, "y": 0.0}},
|
|
||||||
type="signal",
|
|
||||||
)
|
|
||||||
self.ports.append((port, "input"))
|
|
||||||
self._rebuild_list(len(self.ports) - 1)
|
|
||||||
self.name_edit.selectAll()
|
|
||||||
self.name_edit.setFocus()
|
|
||||||
|
|
||||||
def remove_port(self) -> None:
|
|
||||||
row = self.list.currentRow()
|
|
||||||
if row >= 0:
|
|
||||||
self.ports.pop(row)
|
|
||||||
self._rebuild_list(min(row, len(self.ports) - 1))
|
|
||||||
|
|
||||||
def accept(self) -> None:
|
|
||||||
self._store_current()
|
|
||||||
if any(not port.name.strip() for port, _orientation in self.ports):
|
|
||||||
QMessageBox.warning(self, "Invalid port", "Every port must have a name.")
|
|
||||||
return
|
|
||||||
super().accept()
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
from bedit.workspace.view import GraphWorkspaceView
|
|
||||||
|
|
||||||
__all__ = ["GraphWorkspaceView"]
|
|
||||||
|
|
||||||
@@ -1,611 +0,0 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
from PySide6.QtCore import QMimeData, QPointF, QRectF, QSettings, Qt, Signal
|
|
||||||
from PySide6.QtGui import (
|
|
||||||
QColor,
|
|
||||||
QDragEnterEvent,
|
|
||||||
QDropEvent,
|
|
||||||
QMouseEvent,
|
|
||||||
QWheelEvent,
|
|
||||||
QPainter,
|
|
||||||
QPainterPath,
|
|
||||||
QPen,
|
|
||||||
QTransform,
|
|
||||||
)
|
|
||||||
from PySide6.QtWidgets import (
|
|
||||||
QGraphicsEllipseItem,
|
|
||||||
QGraphicsItem,
|
|
||||||
QGraphicsObject,
|
|
||||||
QGraphicsPathItem,
|
|
||||||
QGraphicsScene,
|
|
||||||
QGraphicsSceneContextMenuEvent,
|
|
||||||
QGraphicsSceneMouseEvent,
|
|
||||||
QGraphicsView,
|
|
||||||
QApplication,
|
|
||||||
QMenu,
|
|
||||||
QStyleOptionGraphicsItem,
|
|
||||||
QToolTip,
|
|
||||||
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
|
|
||||||
from bedit.icon_renderer import icon_bounds, paint_icon
|
|
||||||
|
|
||||||
|
|
||||||
SELECTION_MIME_TYPE = "application/x-bedit-selection"
|
|
||||||
|
|
||||||
|
|
||||||
def _graph_grid_size() -> int:
|
|
||||||
return QSettings().value("grid/graphSize", 32, type=int)
|
|
||||||
|
|
||||||
|
|
||||||
def _snapped(position: QPointF) -> QPointF:
|
|
||||||
grid = _graph_grid_size()
|
|
||||||
return QPointF(round(position.x() / grid) * grid, round(position.y() / grid) * grid)
|
|
||||||
|
|
||||||
|
|
||||||
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 = 128.0
|
|
||||||
HEIGHT = 128.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.hitbox = icon_bounds(component.icon)
|
|
||||||
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.hitbox.center())
|
|
||||||
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)
|
|
||||||
position = port.properties.get("iconPosition", {})
|
|
||||||
item.setPos(
|
|
||||||
float(position.get("x", x)) * self.WIDTH / self.component.icon.width,
|
|
||||||
float(position.get("y", spacing * index)) * self.HEIGHT / self.component.icon.height,
|
|
||||||
)
|
|
||||||
result[port.id] = item
|
|
||||||
return result
|
|
||||||
|
|
||||||
def boundingRect(self) -> QRectF: # noqa: N802
|
|
||||||
return self.hitbox
|
|
||||||
|
|
||||||
def paint(
|
|
||||||
self,
|
|
||||||
painter: QPainter,
|
|
||||||
option: QStyleOptionGraphicsItem,
|
|
||||||
widget: QWidget | None = None,
|
|
||||||
) -> None:
|
|
||||||
del option, widget
|
|
||||||
paint_icon(
|
|
||||||
painter,
|
|
||||||
self.component.icon,
|
|
||||||
QRectF(0, 0, self.WIDTH, self.HEIGHT),
|
|
||||||
)
|
|
||||||
if self.isSelected():
|
|
||||||
painter.setBrush(Qt.BrushStyle.NoBrush)
|
|
||||||
painter.setPen(QPen(QColor("#2563eb"), 2, Qt.PenStyle.DashLine))
|
|
||||||
painter.drawRect(self.boundingRect())
|
|
||||||
|
|
||||||
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…")
|
|
||||||
ports_action = menu.addAction("Port Options…")
|
|
||||||
selected = menu.exec(event.screenPos())
|
|
||||||
if selected is options_action:
|
|
||||||
scene = self.scene()
|
|
||||||
if isinstance(scene, GraphScene):
|
|
||||||
scene.componentOptionsRequested.emit(self.component_id)
|
|
||||||
elif selected is ports_action:
|
|
||||||
scene = self.scene()
|
|
||||||
if isinstance(scene, GraphScene):
|
|
||||||
scene.componentPortOptionsRequested.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)
|
|
||||||
snapped = _snapped(self.pos())
|
|
||||||
self.setPos(snapped)
|
|
||||||
self.controller.move_component(self.component_id, self.drag_start, snapped)
|
|
||||||
|
|
||||||
def itemChange(self, change, value): # noqa: N802
|
|
||||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
|
|
||||||
value = _snapped(value)
|
|
||||||
elif 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)
|
|
||||||
snapped = _snapped(self.pos())
|
|
||||||
self.setPos(snapped)
|
|
||||||
self.controller.move_interface_port(self.port.id, self.drag_start, snapped)
|
|
||||||
|
|
||||||
def itemChange(self, change, value): # noqa: N802
|
|
||||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
|
|
||||||
value = _snapped(value)
|
|
||||||
elif 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)
|
|
||||||
componentPortOptionsRequested = 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
|
|
||||||
# The view paints the grid so it always covers the complete viewport.
|
|
||||||
del painter, rect
|
|
||||||
|
|
||||||
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:
|
|
||||||
try:
|
|
||||||
self.controller.connect(self.pending_source.endpoint, item.endpoint)
|
|
||||||
except ValueError as error:
|
|
||||||
QToolTip.showText(event.screenPos(), str(error))
|
|
||||||
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)
|
|
||||||
componentPortOptionsRequested = 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("#f7f7f7"))
|
|
||||||
self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
|
|
||||||
self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter)
|
|
||||||
|
|
||||||
def drawBackground(self, painter: QPainter, rect: QRectF) -> None: # noqa: N802
|
|
||||||
"""Paint the visible graph viewport in scene coordinates."""
|
|
||||||
painter.fillRect(rect, QColor("#f7f7f7"))
|
|
||||||
if (
|
|
||||||
self.controller is None
|
|
||||||
or self.controller.document is None
|
|
||||||
or self.controller.active_component is None
|
|
||||||
):
|
|
||||||
return
|
|
||||||
spacing = _graph_grid_size()
|
|
||||||
left = int(rect.left()) - (int(rect.left()) % spacing)
|
|
||||||
top = int(rect.top()) - (int(rect.top()) % spacing)
|
|
||||||
painter.setPen(QPen(QColor("#c5cbd1"), 0))
|
|
||||||
for x in range(left, int(rect.right()) + spacing, spacing):
|
|
||||||
painter.drawLine(QPointF(x, rect.top()), QPointF(x, rect.bottom()))
|
|
||||||
for y in range(top, int(rect.bottom()) + spacing, spacing):
|
|
||||||
painter.drawLine(QPointF(rect.left(), y), QPointF(rect.right(), y))
|
|
||||||
|
|
||||||
def _zoom(self, factor: float) -> None:
|
|
||||||
current = self.transform().m11()
|
|
||||||
target = current * factor
|
|
||||||
if 0.1 <= target <= 8.0:
|
|
||||||
self.scale(factor, factor)
|
|
||||||
|
|
||||||
def zoom_in(self) -> None:
|
|
||||||
self._zoom(1.2)
|
|
||||||
|
|
||||||
def zoom_out(self) -> None:
|
|
||||||
self._zoom(1 / 1.2)
|
|
||||||
|
|
||||||
def center_workspace(self) -> None:
|
|
||||||
scene = self.scene()
|
|
||||||
if scene is None:
|
|
||||||
return
|
|
||||||
bounds = scene.itemsBoundingRect()
|
|
||||||
if bounds.isEmpty():
|
|
||||||
self.resetTransform()
|
|
||||||
self.centerOn(0, 0)
|
|
||||||
else:
|
|
||||||
self.fitInView(bounds.adjusted(-80, -80, 80, 80), Qt.AspectRatioMode.KeepAspectRatio)
|
|
||||||
|
|
||||||
def wheelEvent(self, event: QWheelEvent) -> None: # noqa: N802
|
|
||||||
self._zoom(1.2 if event.angleDelta().y() > 0 else 1 / 1.2)
|
|
||||||
event.accept()
|
|
||||||
|
|
||||||
def set_model(self, controller: DocumentController) -> None:
|
|
||||||
self.controller = controller
|
|
||||||
scene = GraphScene(controller, self)
|
|
||||||
scene.componentOptionsRequested.connect(self.componentOptionsRequested)
|
|
||||||
scene.componentPortOptionsRequested.connect(self.componentPortOptionsRequested)
|
|
||||||
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
|
|
||||||
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, _snapped(self.mapToScene(event.position().toPoint()))
|
|
||||||
)
|
|
||||||
event.acceptProposedAction()
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
{
|
|
||||||
"format": "bedit-document",
|
|
||||||
"version": 1,
|
|
||||||
"metadata": {
|
|
||||||
"name": "Untitled"
|
|
||||||
},
|
|
||||||
"roots": [
|
|
||||||
{
|
|
||||||
"id": "5ee742db-b25c-4d86-b4ca-cc0e61c4002a",
|
|
||||||
"name": "New Graph Block 1",
|
|
||||||
"position": {
|
|
||||||
"x": 0.0,
|
|
||||||
"y": 0.0
|
|
||||||
},
|
|
||||||
"rotation": 0.0,
|
|
||||||
"interface": {
|
|
||||||
"inputs": [],
|
|
||||||
"outputs": []
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"shape": "rectangle",
|
|
||||||
"fill": "#f4f4f4",
|
|
||||||
"border": "#303030",
|
|
||||||
"text": "Graph",
|
|
||||||
"size": {
|
|
||||||
"width": 128.0,
|
|
||||||
"height": 128.0
|
|
||||||
},
|
|
||||||
"elements": [
|
|
||||||
{
|
|
||||||
"type": "rectangle",
|
|
||||||
"x": 1.0,
|
|
||||||
"y": 1.0,
|
|
||||||
"width": 126.0,
|
|
||||||
"height": 126.0,
|
|
||||||
"fill": "#f4f4f4",
|
|
||||||
"stroke": "#303030",
|
|
||||||
"lineWidth": 1.5,
|
|
||||||
"lineStyle": "solid",
|
|
||||||
"cornerRadius": 5.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"x": 8.0,
|
|
||||||
"y": 8.0,
|
|
||||||
"width": 112.0,
|
|
||||||
"height": 112.0,
|
|
||||||
"text": "Graph",
|
|
||||||
"color": "#202020",
|
|
||||||
"fontSize": 12.0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"properties": {},
|
|
||||||
"library": {
|
|
||||||
"showSubtree": true
|
|
||||||
},
|
|
||||||
"implementation": {
|
|
||||||
"kind": "graph",
|
|
||||||
"graph": {
|
|
||||||
"blocks": [
|
|
||||||
{
|
|
||||||
"id": "89944479-e73e-446a-8d58-ebf35ac4144b",
|
|
||||||
"name": "New Graph Block 1",
|
|
||||||
"position": {
|
|
||||||
"x": 0.0,
|
|
||||||
"y": -96.0
|
|
||||||
},
|
|
||||||
"rotation": 0.0,
|
|
||||||
"interface": {
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"id": "port-e5cb81a5",
|
|
||||||
"name": "Port 1",
|
|
||||||
"position": {
|
|
||||||
"x": 0.0,
|
|
||||||
"y": 0.0
|
|
||||||
},
|
|
||||||
"properties": {
|
|
||||||
"iconPosition": {
|
|
||||||
"x": 32.0,
|
|
||||||
"y": 64.0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "signal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"id": "port-eb0034af",
|
|
||||||
"name": "Port 2",
|
|
||||||
"position": {
|
|
||||||
"x": 0.0,
|
|
||||||
"y": 0.0
|
|
||||||
},
|
|
||||||
"properties": {
|
|
||||||
"iconPosition": {
|
|
||||||
"x": 96.0,
|
|
||||||
"y": 64.0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "signal"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"shape": "rectangle",
|
|
||||||
"fill": "#f4f4f4",
|
|
||||||
"border": "#303030",
|
|
||||||
"text": "Graph",
|
|
||||||
"size": {
|
|
||||||
"width": 128.0,
|
|
||||||
"height": 128.0
|
|
||||||
},
|
|
||||||
"elements": [
|
|
||||||
{
|
|
||||||
"type": "rectangle",
|
|
||||||
"x": 32.0,
|
|
||||||
"y": 32.0,
|
|
||||||
"width": 64.0,
|
|
||||||
"height": 64.0,
|
|
||||||
"fill": "#f4f4f4",
|
|
||||||
"stroke": "#303030",
|
|
||||||
"lineWidth": 1.5,
|
|
||||||
"lineStyle": "solid",
|
|
||||||
"cornerRadius": 5.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"x": 40.0,
|
|
||||||
"y": 40.0,
|
|
||||||
"width": 48.0,
|
|
||||||
"height": 48.0,
|
|
||||||
"text": "Graph",
|
|
||||||
"color": "#303030",
|
|
||||||
"fontSize": 12.0,
|
|
||||||
"lineStyle": "solid",
|
|
||||||
"lineWidth": 1.5,
|
|
||||||
"stroke": "#303030",
|
|
||||||
"fill": "#ffffff"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"properties": {},
|
|
||||||
"library": {
|
|
||||||
"showSubtree": true
|
|
||||||
},
|
|
||||||
"implementation": {
|
|
||||||
"kind": "graph",
|
|
||||||
"graph": {
|
|
||||||
"blocks": [],
|
|
||||||
"connections": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"connections": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -9,15 +9,9 @@
|
|||||||
<layout class="QFormLayout" name="optionsForm">
|
<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="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="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="0"><widget class="QLabel" name="iconLabel"><property name="text"><string>Icon:</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="1" column="1"><widget class="QPushButton" name="editIconButton"><property name="text"><string>Edit Icon…</string></property><property name="toolTip"><string>Open the vector icon and port-position editor</string></property></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="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>
|
||||||
<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>
|
</layout>
|
||||||
</item>
|
</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><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>
|
||||||
|
|||||||
36
BEdit/ui/icon_editor_dialog.ui
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>IconEditorDialog</class>
|
||||||
|
<widget class="QDialog" name="IconEditorDialog">
|
||||||
|
<property name="geometry"><rect><x>0</x><y>0</y><width>850</width><height>600</height></rect></property>
|
||||||
|
<property name="windowTitle"><string>Icon Editor</string></property>
|
||||||
|
<layout class="QVBoxLayout" name="dialogLayout">
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="shapeToolbarLayout">
|
||||||
|
<item><widget class="QToolButton" name="pointerButton"><property name="text"><string>Pointer</string></property><property name="checkable"><bool>true</bool></property><property name="checked"><bool>true</bool></property></widget></item>
|
||||||
|
<item><widget class="QLabel" name="addShapeLabel"><property name="text"><string>Add:</string></property></widget></item>
|
||||||
|
<item><widget class="QToolButton" name="addRectangleButton"><property name="text"><string>Rectangle</string></property><property name="checkable"><bool>true</bool></property></widget></item>
|
||||||
|
<item><widget class="QToolButton" name="addCircleButton"><property name="text"><string>Circle</string></property><property name="checkable"><bool>true</bool></property></widget></item>
|
||||||
|
<item><widget class="QToolButton" name="addEllipseButton"><property name="text"><string>Ellipse</string></property><property name="checkable"><bool>true</bool></property></widget></item>
|
||||||
|
<item><widget class="QToolButton" name="addLineButton"><property name="text"><string>Line</string></property><property name="checkable"><bool>true</bool></property></widget></item>
|
||||||
|
<item><widget class="QToolButton" name="addTriangleButton"><property name="text"><string>Triangle</string></property><property name="checkable"><bool>true</bool></property></widget></item>
|
||||||
|
<item><widget class="QToolButton" name="addTextButton"><property name="text"><string>Text</string></property><property name="checkable"><bool>true</bool></property></widget></item>
|
||||||
|
<item><spacer name="toolbarSpacer"><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="QToolButton" name="zoomInButton"><property name="text"><string>+</string></property><property name="toolTip"><string>Zoom in</string></property></widget></item>
|
||||||
|
<item><widget class="QToolButton" name="zoomOutButton"><property name="text"><string>−</string></property><property name="toolTip"><string>Zoom out</string></property></widget></item>
|
||||||
|
<item><widget class="QToolButton" name="centerButton"><property name="text"><string>Fit</string></property><property name="toolTip"><string>Fit and center the icon canvas</string></property></widget></item>
|
||||||
|
<item><widget class="QPushButton" name="deleteSelectedButton"><property name="text"><string>Delete selected</string></property></widget></item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item><widget class="IconCanvasView" name="iconView"><property name="dragMode"><enum>QGraphicsView::DragMode::RubberBandDrag</enum></property></widget></item>
|
||||||
|
<item><widget class="QLabel" name="portHintLabel"><property name="text"><string>Green points are inputs; red points are outputs. Drag them to place connection anchors.</string></property><property name="wordWrap"><bool>true</bool></property></widget></item>
|
||||||
|
<item><widget class="QDialogButtonBox" name="buttonBox"><property name="standardButtons"><set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set></property></widget></item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<customwidgets><customwidget><class>IconCanvasView</class><extends>QGraphicsView</extends><header>bedit.gui.graphics.icon_canvas</header></customwidget></customwidgets>
|
||||||
|
<resources/>
|
||||||
|
<connections>
|
||||||
|
<connection><sender>buttonBox</sender><signal>accepted()</signal><receiver>IconEditorDialog</receiver><slot>accept()</slot><hints/></connection>
|
||||||
|
<connection><sender>buttonBox</sender><signal>rejected()</signal><receiver>IconEditorDialog</receiver><slot>reject()</slot><hints/></connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
||||||
@@ -6,8 +6,8 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>1000</width>
|
<width>1209</width>
|
||||||
<height>700</height>
|
<height>777</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
@@ -83,9 +83,18 @@
|
|||||||
</property>
|
</property>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QTreeView" name="treeView">
|
<widget class="QTreeView" name="treeView">
|
||||||
|
<property name="styleSheet">
|
||||||
|
<string notr="true">QTreeView::item { height: 32px; }</string>
|
||||||
|
</property>
|
||||||
<property name="alternatingRowColors">
|
<property name="alternatingRowColors">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="iconSize">
|
||||||
|
<size>
|
||||||
|
<width>28</width>
|
||||||
|
<height>28</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
<property name="uniformRowHeights">
|
<property name="uniformRowHeights">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
@@ -96,21 +105,48 @@
|
|||||||
</widget>
|
</widget>
|
||||||
<widget class="QDockWidget" name="panel_document">
|
<widget class="QDockWidget" name="panel_document">
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
<size><width>220</width><height>91</height></size>
|
<size>
|
||||||
|
<width>220</width>
|
||||||
|
<height>91</height>
|
||||||
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle"><string>Document</string></property>
|
<property name="windowTitle">
|
||||||
<attribute name="dockWidgetArea"><number>1</number></attribute>
|
<string>Document</string>
|
||||||
|
</property>
|
||||||
|
<attribute name="dockWidgetArea">
|
||||||
|
<number>1</number>
|
||||||
|
</attribute>
|
||||||
<widget class="QWidget" name="documentDockContents">
|
<widget class="QWidget" name="documentDockContents">
|
||||||
<layout class="QVBoxLayout" name="documentPanelLayout">
|
<layout class="QVBoxLayout" name="documentPanelLayout">
|
||||||
<property name="spacing"><number>0</number></property>
|
<property name="spacing">
|
||||||
<property name="leftMargin"><number>0</number></property>
|
<number>0</number>
|
||||||
<property name="topMargin"><number>0</number></property>
|
</property>
|
||||||
<property name="rightMargin"><number>0</number></property>
|
<property name="leftMargin">
|
||||||
<property name="bottomMargin"><number>0</number></property>
|
<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>
|
<item>
|
||||||
<widget class="QTreeView" name="documentTreeView">
|
<widget class="QTreeView" name="documentTreeView">
|
||||||
<property name="alternatingRowColors"><bool>true</bool></property>
|
<property name="alternatingRowColors">
|
||||||
<property name="uniformRowHeights"><bool>true</bool></property>
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="iconSize">
|
||||||
|
<size>
|
||||||
|
<width>16</width>
|
||||||
|
<height>16</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="uniformRowHeights">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
@@ -125,53 +161,320 @@
|
|||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="workspaceEditorLayout">
|
<layout class="QVBoxLayout" name="workspaceEditorLayout">
|
||||||
<property name="spacing"><number>0</number></property>
|
<property name="spacing">
|
||||||
<property name="leftMargin"><number>0</number></property>
|
<number>0</number>
|
||||||
<property name="topMargin"><number>0</number></property>
|
</property>
|
||||||
<property name="rightMargin"><number>0</number></property>
|
<property name="leftMargin">
|
||||||
<property name="bottomMargin"><number>0</number></property>
|
<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>
|
<item>
|
||||||
<widget class="QFrame" name="workspaceHeader">
|
<widget class="QFrame" name="workspaceHeader">
|
||||||
<property name="minimumSize"><size><width>0</width><height>34</height></size></property>
|
<property name="minimumSize">
|
||||||
<property name="maximumSize"><size><width>16777215</width><height>34</height></size></property>
|
<size>
|
||||||
<property name="frameShape"><enum>QFrame::Shape::StyledPanel</enum></property>
|
<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">
|
<layout class="QHBoxLayout" name="workspaceHeaderLayout">
|
||||||
<property name="leftMargin"><number>6</number></property>
|
<property name="leftMargin">
|
||||||
<property name="topMargin"><number>2</number></property>
|
<number>6</number>
|
||||||
<property name="rightMargin"><number>6</number></property>
|
</property>
|
||||||
<property name="bottomMargin"><number>2</number></property>
|
<property name="topMargin">
|
||||||
<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>
|
<number>2</number>
|
||||||
<item><widget class="QLabel" name="graphBreadcrumbLabel"><property name="text"><string>Untitled</string></property></widget></item>
|
</property>
|
||||||
<item><widget class="QLabel" name="workspaceModeLabel"><property name="text"><string>Graph</string></property></widget></item>
|
<property name="rightMargin">
|
||||||
<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>
|
<number>6</number>
|
||||||
<item><widget class="QPushButton" name="applyJsonButton"><property name="text"><string>Apply JSON</string></property><property name="visible"><bool>false</bool></property></widget></item>
|
</property>
|
||||||
<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>
|
<property name="bottomMargin">
|
||||||
<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>
|
<number>2</number>
|
||||||
<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>
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="navigateUpButton">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Open the containing graph</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Up</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../resources/resources.qrc">
|
||||||
|
<normaloff>:/icons/icons/arrow-up.png</normaloff>:/icons/icons/arrow-up.png</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="navigateDownButton">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Open the selected block</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Down</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../resources/resources.qrc">
|
||||||
|
<normaloff>:/icons/icons/arrow-down.png</normaloff>:/icons/icons/arrow-down.png</iconset>
|
||||||
|
</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="visible">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Apply JSON</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="pointerToolButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>Pointer</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../resources/resources.qrc">
|
||||||
|
<normaloff>:/icons/icons/edit-select.png</normaloff>:/icons/icons/edit-select.png</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="checkable">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="checked">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="connectToolButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>Connect</string>
|
||||||
|
</property>
|
||||||
|
<property name="checkable">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="boxToolButton">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Draw a box annotation</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Box</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../resources/resources.qrc">
|
||||||
|
<normaloff>:/icons/icons/draw-rectangle.png</normaloff>:/icons/icons/draw-rectangle.png</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="checkable">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="lineToolButton">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Draw a line annotation</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Line</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../resources/resources.qrc">
|
||||||
|
<normaloff>:/icons/icons/draw-bezier-curves.png</normaloff>:/icons/icons/draw-bezier-curves.png</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="checkable">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="textToolButton">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Add a text annotation</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Text</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../resources/resources.qrc">
|
||||||
|
<normaloff>:/icons/icons/draw-text.png</normaloff>:/icons/icons/draw-text.png</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="checkable">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="rotateToolButton">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Rotate selected blocks clockwise</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Rotate</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../resources/resources.qrc">
|
||||||
|
<normaloff>:/icons/icons/transform-rotate.png</normaloff>:/icons/icons/transform-rotate.png</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="routingLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Line:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="directRoutingButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>Direct</string>
|
||||||
|
</property>
|
||||||
|
<property name="checkable">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="angledRoutingButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>Angled</string>
|
||||||
|
</property>
|
||||||
|
<property name="checkable">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="checked">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="splineRoutingButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>Spline</string>
|
||||||
|
</property>
|
||||||
|
<property name="checkable">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QStackedWidget" name="workspaceStack">
|
<widget class="QStackedWidget" name="workspaceStack">
|
||||||
<property name="currentIndex"><number>0</number></property>
|
<property name="currentIndex">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
<widget class="QWidget" name="graphPage">
|
<widget class="QWidget" name="graphPage">
|
||||||
<layout class="QVBoxLayout" name="graphPageLayout">
|
<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>
|
<property name="leftMargin">
|
||||||
<item><widget class="GraphWorkspaceView" name="graphView"/></item>
|
<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>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QWidget" name="jsonPage">
|
<widget class="QWidget" name="jsonPage">
|
||||||
<layout class="QVBoxLayout" name="jsonPageLayout">
|
<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>
|
<property name="leftMargin">
|
||||||
<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>
|
<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>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QWidget" name="emptyPage">
|
<widget class="QWidget" name="emptyPage">
|
||||||
|
<property name="styleSheet">
|
||||||
|
<string notr="true">background-color: #9a9a9a;</string>
|
||||||
|
</property>
|
||||||
<layout class="QVBoxLayout" name="emptyPageLayout">
|
<layout class="QVBoxLayout" name="emptyPageLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="emptyWorkspaceLabel">
|
<widget class="QLabel" name="emptyWorkspaceLabel">
|
||||||
<property name="text"><string>No document open</string></property>
|
<property name="styleSheet">
|
||||||
<property name="alignment"><set>Qt::AlignmentFlag::AlignCenter</set></property>
|
<string notr="true">background: transparent; color: #202020;</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>No document open</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
@@ -189,7 +492,7 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>1000</width>
|
<width>1209</width>
|
||||||
<height>24</height>
|
<height>24</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
@@ -305,11 +608,19 @@
|
|||||||
<addaction name="actionCut"/>
|
<addaction name="actionCut"/>
|
||||||
<addaction name="actionPaste"/>
|
<addaction name="actionPaste"/>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QToolBar" name="transformToolbar">
|
<widget class="QToolBar" name="cameraToolbar">
|
||||||
<property name="windowTitle"><string>Transform</string></property>
|
<property name="windowTitle">
|
||||||
<attribute name="toolBarArea"><enum>TopToolBarArea</enum></attribute>
|
<string>Camera</string>
|
||||||
<attribute name="toolBarBreak"><bool>false</bool></attribute>
|
</property>
|
||||||
<addaction name="actionRotateClockwise"/>
|
<attribute name="toolBarArea">
|
||||||
|
<enum>TopToolBarArea</enum>
|
||||||
|
</attribute>
|
||||||
|
<attribute name="toolBarBreak">
|
||||||
|
<bool>false</bool>
|
||||||
|
</attribute>
|
||||||
|
<addaction name="actionZoomIn"/>
|
||||||
|
<addaction name="actionZoomOut"/>
|
||||||
|
<addaction name="actionCenterView"/>
|
||||||
</widget>
|
</widget>
|
||||||
<action name="actionNew">
|
<action name="actionNew">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
@@ -331,9 +642,51 @@
|
|||||||
<iconset resource="../resources/resources.qrc">
|
<iconset resource="../resources/resources.qrc">
|
||||||
<normaloff>:/icons/icons/transform-rotate.png</normaloff>:/icons/icons/transform-rotate.png</iconset>
|
<normaloff>:/icons/icons/transform-rotate.png</normaloff>:/icons/icons/transform-rotate.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text"><string>Rotate Clockwise</string></property>
|
<property name="text">
|
||||||
<property name="toolTip"><string>Rotate selected blocks clockwise by 90 degrees</string></property>
|
<string>Rotate Clockwise</string>
|
||||||
<property name="shortcut"><string>Ctrl+R</string></property>
|
</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="actionZoomIn">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../resources/resources.qrc">
|
||||||
|
<normaloff>:/icons/icons/zoom-in.png</normaloff>:/icons/icons/zoom-in.png</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Zoom In</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>Ctrl++</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionZoomOut">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../resources/resources.qrc">
|
||||||
|
<normaloff>:/icons/icons/zoom-out.png</normaloff>:/icons/icons/zoom-out.png</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Zoom Out</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>Ctrl+-</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionCenterView">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../resources/resources.qrc">
|
||||||
|
<normaloff>:/icons/icons/zoom-original.png</normaloff>:/icons/icons/zoom-original.png</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Center</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>Ctrl+0</string>
|
||||||
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="actionOpen">
|
<action name="actionOpen">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
@@ -386,8 +739,12 @@
|
|||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="actionClose">
|
<action name="actionClose">
|
||||||
<property name="text"><string>&Close Document</string></property>
|
<property name="text">
|
||||||
<property name="shortcut"><string>Ctrl+W</string></property>
|
<string>&Close Document</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>Ctrl+W</string>
|
||||||
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="actionUndo">
|
<action name="actionUndo">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
@@ -458,8 +815,12 @@
|
|||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="actionDelete">
|
<action name="actionDelete">
|
||||||
<property name="text"><string>&Delete</string></property>
|
<property name="text">
|
||||||
<property name="shortcut"><string>Del</string></property>
|
<string>&Delete</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>Del</string>
|
||||||
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="actionAbout">
|
<action name="actionAbout">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
@@ -484,7 +845,7 @@
|
|||||||
<customwidget>
|
<customwidget>
|
||||||
<class>GraphWorkspaceView</class>
|
<class>GraphWorkspaceView</class>
|
||||||
<extends>QGraphicsView</extends>
|
<extends>QGraphicsView</extends>
|
||||||
<header>bedit.workspace.view</header>
|
<header>bedit.gui.graphics.workspace</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<resources>
|
<resources>
|
||||||
|
|||||||
43
BEdit/ui/port_options_dialog.ui
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>PortOptionsDialog</class>
|
||||||
|
<widget class="QDialog" name="PortOptionsDialog">
|
||||||
|
<property name="geometry"><rect><x>0</x><y>0</y><width>620</width><height>380</height></rect></property>
|
||||||
|
<property name="windowTitle"><string>Port Options</string></property>
|
||||||
|
<layout class="QVBoxLayout" name="dialogLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QSplitter" name="portSplitter">
|
||||||
|
<property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property>
|
||||||
|
<widget class="QWidget" name="portListPanel">
|
||||||
|
<layout class="QVBoxLayout" name="portListLayout">
|
||||||
|
<item><widget class="QListWidget" name="portList"/></item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="portButtonsLayout">
|
||||||
|
<item><widget class="QPushButton" name="addPortButton"><property name="text"><string>Add Port</string></property></widget></item>
|
||||||
|
<item><widget class="QPushButton" name="removePortButton"><property name="text"><string>Remove Port</string></property></widget></item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<widget class="QWidget" name="portDetailsPanel">
|
||||||
|
<layout class="QFormLayout" name="portDetailsForm">
|
||||||
|
<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="typeLabel"><property name="text"><string>Type:</string></property></widget></item>
|
||||||
|
<item row="1" column="1"><widget class="QComboBox" name="typeCombo"><item><property name="text"><string>Signal</string></property></item></widget></item>
|
||||||
|
<item row="2" column="0"><widget class="QLabel" name="orientationLabel"><property name="text"><string>Orientation:</string></property></widget></item>
|
||||||
|
<item row="2" column="1"><widget class="QComboBox" name="orientationCombo"><item><property name="text"><string>Input</string></property></item><item><property name="text"><string>Output</string></property></item></widget></item>
|
||||||
|
<item row="3" column="0" colspan="2"><widget class="QLabel" name="positionHintLabel"><property name="text"><string>New ports start at (0, 0) in the icon editor.</string></property><property name="wordWrap"><bool>true</bool></property></widget></item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
</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>PortOptionsDialog</receiver><slot>accept()</slot><hints/></connection>
|
||||||
|
<connection><sender>buttonBox</sender><signal>rejected()</signal><receiver>PortOptionsDialog</receiver><slot>reject()</slot><hints/></connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>480</width>
|
<width>480</width>
|
||||||
<height>300</height>
|
<height>420</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
@@ -57,6 +57,19 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="editorGridsGroupBox">
|
||||||
|
<property name="title"><string>Editor grids</string></property>
|
||||||
|
<layout class="QFormLayout" name="editorGridsLayout">
|
||||||
|
<item row="0" column="0"><widget class="QLabel" name="graphGridLabel"><property name="text"><string>Workspace grid size:</string></property></widget></item>
|
||||||
|
<item row="0" column="1"><widget class="QSpinBox" name="graphGridSpinBox"><property name="suffix"><string> units</string></property><property name="minimum"><number>8</number></property><property name="maximum"><number>512</number></property><property name="value"><number>64</number></property></widget></item>
|
||||||
|
<item row="1" column="0"><widget class="QLabel" name="graphSnapLabel"><property name="text"><string>Workspace snapping size:</string></property></widget></item>
|
||||||
|
<item row="1" column="1"><widget class="QSpinBox" name="graphSnapSpinBox"><property name="suffix"><string> units</string></property><property name="minimum"><number>1</number></property><property name="maximum"><number>128</number></property><property name="value"><number>8</number></property></widget></item>
|
||||||
|
<item row="2" column="0"><widget class="QLabel" name="iconGridLabel"><property name="text"><string>Icon grid size:</string></property></widget></item>
|
||||||
|
<item row="2" column="1"><widget class="QSpinBox" name="iconGridSpinBox"><property name="suffix"><string> units</string></property><property name="minimum"><number>1</number></property><property name="maximum"><number>64</number></property><property name="value"><number>8</number></property></widget></item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<spacer name="generalSpacer">
|
<spacer name="generalSpacer">
|
||||||
|
|||||||
41
BEdit/ui/shape_options_dialog.ui
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>ShapeOptionsDialog</class>
|
||||||
|
<widget class="QDialog" name="ShapeOptionsDialog">
|
||||||
|
<property name="geometry"><rect><x>0</x><y>0</y><width>420</width><height>440</height></rect></property>
|
||||||
|
<property name="windowTitle"><string>Shape Options</string></property>
|
||||||
|
<layout class="QVBoxLayout" name="dialogLayout">
|
||||||
|
<item>
|
||||||
|
<layout class="QFormLayout" name="optionsForm">
|
||||||
|
<item row="0" column="0"><widget class="QLabel" name="widthLabel"><property name="text"><string>Width:</string></property></widget></item>
|
||||||
|
<item row="0" column="1"><widget class="QDoubleSpinBox" name="widthSpin"><property name="minimum"><double>1.000000000000000</double></property><property name="maximum"><double>500.000000000000000</double></property></widget></item>
|
||||||
|
<item row="1" column="0"><widget class="QLabel" name="heightLabel"><property name="text"><string>Height:</string></property></widget></item>
|
||||||
|
<item row="1" column="1"><widget class="QDoubleSpinBox" name="heightSpin"><property name="minimum"><double>1.000000000000000</double></property><property name="maximum"><double>500.000000000000000</double></property></widget></item>
|
||||||
|
<item row="2" column="0"><widget class="QLabel" name="lineStyleLabel"><property name="text"><string>Line style:</string></property></widget></item>
|
||||||
|
<item row="2" column="1"><widget class="QComboBox" name="lineStyleCombo"><item><property name="text"><string>solid</string></property></item><item><property name="text"><string>dash</string></property></item><item><property name="text"><string>dot</string></property></item><item><property name="text"><string>dash-dot</string></property></item><item><property name="text"><string>none</string></property></item></widget></item>
|
||||||
|
<item row="3" column="0"><widget class="QLabel" name="lineWidthLabel"><property name="text"><string>Line width:</string></property></widget></item>
|
||||||
|
<item row="3" column="1"><widget class="QDoubleSpinBox" name="lineWidthSpin"><property name="minimum"><double>0.100000000000000</double></property><property name="maximum"><double>20.000000000000000</double></property><property name="singleStep"><double>0.500000000000000</double></property></widget></item>
|
||||||
|
<item row="4" column="0"><widget class="QLabel" name="strokeColorLabel"><property name="text"><string>Line colour:</string></property></widget></item>
|
||||||
|
<item row="4" column="1"><widget class="QPushButton" name="strokeColorButton"><property name="text"><string>Choose…</string></property></widget></item>
|
||||||
|
<item row="5" column="0"><widget class="QLabel" name="fillTypeLabel"><property name="text"><string>Fill type:</string></property></widget></item>
|
||||||
|
<item row="5" column="1"><widget class="QComboBox" name="fillTypeCombo"><item><property name="text"><string>solid</string></property></item><item><property name="text"><string>none</string></property></item></widget></item>
|
||||||
|
<item row="6" column="0"><widget class="QLabel" name="fillColorLabel"><property name="text"><string>Fill colour:</string></property></widget></item>
|
||||||
|
<item row="6" column="1"><widget class="QPushButton" name="fillColorButton"><property name="text"><string>Choose…</string></property></widget></item>
|
||||||
|
<item row="7" column="0"><widget class="QLabel" name="cornerRadiusLabel"><property name="text"><string>Corner radius:</string></property></widget></item>
|
||||||
|
<item row="7" column="1"><widget class="QDoubleSpinBox" name="cornerRadiusSpin"><property name="maximum"><double>50.000000000000000</double></property></widget></item>
|
||||||
|
<item row="8" column="0"><widget class="QLabel" name="textLabel"><property name="text"><string>Text:</string></property></widget></item>
|
||||||
|
<item row="8" column="1"><widget class="QLineEdit" name="textEdit"/></item>
|
||||||
|
<item row="9" column="0"><widget class="QLabel" name="fontSizeLabel"><property name="text"><string>Font size:</string></property></widget></item>
|
||||||
|
<item row="9" column="1"><widget class="QDoubleSpinBox" name="fontSizeSpin"><property name="minimum"><double>4.000000000000000</double></property><property name="maximum"><double>96.000000000000000</double></property></widget></item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item><spacer name="verticalSpacer"><property name="orientation"><enum>Qt::Orientation::Vertical</enum></property><property name="sizeHint" stdset="0"><size><width>20</width><height>20</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>ShapeOptionsDialog</receiver><slot>accept()</slot><hints/></connection>
|
||||||
|
<connection><sender>buttonBox</sender><signal>rejected()</signal><receiver>ShapeOptionsDialog</receiver><slot>reject()</slot><hints/></connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
||||||
355
BEdit/untitled.bedit.json
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
{
|
||||||
|
"format": "bedit-document",
|
||||||
|
"version": 1,
|
||||||
|
"metadata": {
|
||||||
|
"name": "Untitled"
|
||||||
|
},
|
||||||
|
"roots": [
|
||||||
|
{
|
||||||
|
"id": "f4a9c769-d49a-46e1-90d8-a8175bf42e9c",
|
||||||
|
"name": "New Graph Block 1",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 0.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"interface": {
|
||||||
|
"inputs": [],
|
||||||
|
"outputs": []
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"shape": "rectangle",
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"border": "#303030",
|
||||||
|
"text": "Graph",
|
||||||
|
"size": {
|
||||||
|
"width": 128.0,
|
||||||
|
"height": 128.0
|
||||||
|
},
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "rectangle",
|
||||||
|
"x": 32.0,
|
||||||
|
"y": 32.0,
|
||||||
|
"width": 64.0,
|
||||||
|
"height": 64.0,
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"stroke": "#303030",
|
||||||
|
"lineWidth": 1.5,
|
||||||
|
"lineStyle": "solid",
|
||||||
|
"cornerRadius": 5.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"x": 40.0,
|
||||||
|
"y": 40.0,
|
||||||
|
"width": 48.0,
|
||||||
|
"height": 48.0,
|
||||||
|
"text": "Graph",
|
||||||
|
"color": "#202020",
|
||||||
|
"fontSize": 12.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"properties": {},
|
||||||
|
"library": {
|
||||||
|
"showSubtree": true
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"kind": "graph",
|
||||||
|
"graph": {
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"id": "af73487a-4a2d-4908-b919-d0a2eaa75ce2",
|
||||||
|
"name": "A",
|
||||||
|
"position": {
|
||||||
|
"x": -192.0,
|
||||||
|
"y": -192.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"interface": {
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"id": "port-5c5d4695",
|
||||||
|
"name": "Port 1",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 0.0
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"iconPosition": {
|
||||||
|
"x": 32.0,
|
||||||
|
"y": 64.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "signal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"id": "port-de109124",
|
||||||
|
"name": "Port 2",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 0.0
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"iconPosition": {
|
||||||
|
"x": 96.0,
|
||||||
|
"y": 64.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "signal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"shape": "rectangle",
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"border": "#303030",
|
||||||
|
"text": "Text",
|
||||||
|
"size": {
|
||||||
|
"width": 128.0,
|
||||||
|
"height": 128.0
|
||||||
|
},
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"cornerRadius": 5.0,
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"height": 64.0,
|
||||||
|
"lineStyle": "solid",
|
||||||
|
"lineWidth": 1.5,
|
||||||
|
"stroke": "#303030",
|
||||||
|
"type": "rectangle",
|
||||||
|
"width": 64.0,
|
||||||
|
"x": 32.0,
|
||||||
|
"y": 32.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "#303030",
|
||||||
|
"fill": "#ffffff",
|
||||||
|
"fontSize": 12.0,
|
||||||
|
"height": 48.0,
|
||||||
|
"lineStyle": "solid",
|
||||||
|
"lineWidth": 1.5,
|
||||||
|
"stroke": "#303030",
|
||||||
|
"text": "A",
|
||||||
|
"type": "text",
|
||||||
|
"width": 48.0,
|
||||||
|
"x": 40.0,
|
||||||
|
"y": 40.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"properties": {},
|
||||||
|
"library": {
|
||||||
|
"showSubtree": true
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"kind": "text",
|
||||||
|
"source": {
|
||||||
|
"equations": [],
|
||||||
|
"parameters": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "b89a77a1-d773-4ee3-8c3f-74377c29b4b2",
|
||||||
|
"name": "B",
|
||||||
|
"position": {
|
||||||
|
"x": 128.0,
|
||||||
|
"y": -128.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"interface": {
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"id": "port-4f732b3e",
|
||||||
|
"name": "Port 1",
|
||||||
|
"position": {
|
||||||
|
"x": -176.0,
|
||||||
|
"y": -144.0
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"iconPosition": {
|
||||||
|
"x": 32.0,
|
||||||
|
"y": 48.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "signal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "port-ef7c4218",
|
||||||
|
"name": "Port 2",
|
||||||
|
"position": {
|
||||||
|
"x": -176.0,
|
||||||
|
"y": -16.0
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"iconPosition": {
|
||||||
|
"x": 32.0,
|
||||||
|
"y": 80.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "signal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"id": "port-b68679b2",
|
||||||
|
"name": "Port 3",
|
||||||
|
"position": {
|
||||||
|
"x": 128.0,
|
||||||
|
"y": -80.0
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"iconPosition": {
|
||||||
|
"x": 96.0,
|
||||||
|
"y": 64.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "signal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"shape": "rectangle",
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"border": "#303030",
|
||||||
|
"text": "Graph",
|
||||||
|
"size": {
|
||||||
|
"width": 128.0,
|
||||||
|
"height": 128.0
|
||||||
|
},
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"cornerRadius": 5.0,
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"height": 64.0,
|
||||||
|
"lineStyle": "solid",
|
||||||
|
"lineWidth": 1.5,
|
||||||
|
"stroke": "#303030",
|
||||||
|
"type": "rectangle",
|
||||||
|
"width": 64.0,
|
||||||
|
"x": 32.0,
|
||||||
|
"y": 32.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "#303030",
|
||||||
|
"fill": "#ffffff",
|
||||||
|
"fontSize": 12.0,
|
||||||
|
"height": 48.0,
|
||||||
|
"lineStyle": "solid",
|
||||||
|
"lineWidth": 1.5,
|
||||||
|
"stroke": "#303030",
|
||||||
|
"text": "B",
|
||||||
|
"type": "text",
|
||||||
|
"width": 48.0,
|
||||||
|
"x": 40.0,
|
||||||
|
"y": 40.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"properties": {},
|
||||||
|
"library": {
|
||||||
|
"showSubtree": true
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"kind": "graph",
|
||||||
|
"graph": {
|
||||||
|
"blocks": [],
|
||||||
|
"connections": [],
|
||||||
|
"annotations": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": [
|
||||||
|
{
|
||||||
|
"id": "c59eac1f-5f24-45de-90ee-bfd039894ed9",
|
||||||
|
"source": {
|
||||||
|
"block": "af73487a-4a2d-4908-b919-d0a2eaa75ce2",
|
||||||
|
"port": "port-de109124"
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"block": "b89a77a1-d773-4ee3-8c3f-74377c29b4b2",
|
||||||
|
"port": "port-4f732b3e"
|
||||||
|
},
|
||||||
|
"name": "",
|
||||||
|
"properties": {
|
||||||
|
"routing": "angled",
|
||||||
|
"waypoints": [
|
||||||
|
{
|
||||||
|
"x": -64.0,
|
||||||
|
"y": -128.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": -64.0,
|
||||||
|
"y": 64.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 64.0,
|
||||||
|
"y": 64.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 64.0,
|
||||||
|
"y": -80.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"id": "bfe7d48b-fe54-4378-ab1b-f9b46c2efa87",
|
||||||
|
"kind": "box",
|
||||||
|
"position": {
|
||||||
|
"x": 48.0,
|
||||||
|
"y": -328.0
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"width": 160.0,
|
||||||
|
"height": 112.0
|
||||||
|
},
|
||||||
|
"text": "",
|
||||||
|
"layer": -1,
|
||||||
|
"properties": {
|
||||||
|
"stroke": "#303030",
|
||||||
|
"lineWidth": 1.5,
|
||||||
|
"lineStyle": "solid",
|
||||||
|
"fill": "#dbeafe",
|
||||||
|
"cornerRadius": 0.0,
|
||||||
|
"routing": "angled",
|
||||||
|
"waypoints": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "aa69f7b3-f5af-426b-9eb3-c63a268346b5",
|
||||||
|
"kind": "text",
|
||||||
|
"position": {
|
||||||
|
"x": -272.0,
|
||||||
|
"y": -216.0
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"width": 400.0,
|
||||||
|
"height": 48.0
|
||||||
|
},
|
||||||
|
"text": "This is an annotation",
|
||||||
|
"layer": -1,
|
||||||
|
"properties": {
|
||||||
|
"stroke": "#023010",
|
||||||
|
"lineWidth": 3.0,
|
||||||
|
"lineStyle": "dash",
|
||||||
|
"fill": "none",
|
||||||
|
"fontSize": 18.0,
|
||||||
|
"color": "#023010",
|
||||||
|
"routing": "angled",
|
||||||
|
"waypoints": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||