Compare commits

...

5 Commits

Author SHA1 Message Date
4ab74f64e2 Better drawing and added more icons 2026-07-20 13:18:16 +02:00
ee859c4311 Added new connection styles 2026-07-20 12:45:47 +02:00
8fa450d734 more ui files instead of python generated ui 2026-07-20 12:27:29 +02:00
48a2b4c8d0 Reorganized the application around a clear frontend/backend boundary.
src/bedit/
├── __main__.py
├── core/                   # Pure Python, no PySide
│   ├── model.py
│   ├── port_types.py
│   ├── serializer.py
│   └── libraries.py
└── gui/                    # All Qt-dependent code
    ├── app.py
    ├── main_window.py
    ├── preferences.py
    ├── controllers/
    ├── dialogs/
    ├── graphics/
    ├── models/
    └── generated/          # Designer/resource output only
Notable improvements:
Domain models, serialization, port types, and library parsing are now Qt-free.
Qt signals and undo infrastructure are explicitly isolated under gui/controllers.
Library parsing is separated from the Qt repository and tree models.
All generated Python is contained in gui/generated.
Designer build tasks now write to the generated directory.
The application entry point and package metadata use the new paths.
README now documents the structure and dependency rules.
Removed the old mixed document, library, and workspace packages.
2026-07-20 12:09:00 +02:00
1a47952358 Fixed some small stuff 2026-07-20 11:52:34 +02:00
68 changed files with 6745 additions and 1987 deletions

2
BEdit/.gitignore vendored
View File

@@ -8,3 +8,5 @@ dist/
.idea/ .idea/
.vscode/* .vscode/*
!.vscode/tasks.json !.vscode/tasks.json
.ruff_cache
ui/*_ui.py

View File

@@ -2,31 +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": {
"cwd": "${workspaceFolder}",
"env": {
"QT_QPA_PLATFORMTHEME" :"qt6ct",
"QT_QPA_PLATFORM": "xcb"
}
},
"problemMatcher": [],
"presentation": {
"reveal": "always",
"panel": "dedicated"
}
},
{
"label": "Qt: Open Settings Dialog in Designer",
"type": "shell",
"command": "pyside6-designer",
"args": [
"${workspaceFolder}/ui/settings_dialog.ui"
], ],
"options": { "options": {
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
@@ -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
View 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.

View File

@@ -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

View File

@@ -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": []
}
}
}
]
}

View File

@@ -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"]

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 991 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -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>

File diff suppressed because it is too large Load Diff

View File

@@ -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())

View 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"]

View 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]

View File

@@ -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,

View File

@@ -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)

View 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": []
}
}
}
]
}

View File

@@ -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",
]

View File

@@ -0,0 +1 @@
"""Qt user interface and application adapters."""

View File

@@ -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()

View File

@@ -0,0 +1,5 @@
"""Qt-aware application controllers and undo commands."""
from bedit.gui.controllers.document import DocumentController
__all__ = ["DocumentController"]

View File

@@ -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")

View File

@@ -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

View File

@@ -0,0 +1 @@
"""Application dialogs."""

View File

@@ -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:

View 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()

View File

@@ -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()

View File

@@ -0,0 +1 @@
"""Generated Qt code. Do not edit these modules by hand."""

View File

@@ -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

View 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

View File

@@ -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

View 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

View File

@@ -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))

View 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

View File

@@ -0,0 +1,5 @@
"""Graphics scenes, views, editors, and renderers."""
from bedit.gui.graphics.workspace import GraphWorkspaceView
__all__ = ["GraphWorkspaceView"]

View 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())

View 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()

View 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()

View File

@@ -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()

File diff suppressed because it is too large Load Diff

View 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)

View File

@@ -0,0 +1 @@
"""Qt model/view adapters and repositories."""

View 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)

View File

@@ -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

View 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")

View File

@@ -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()

View File

@@ -1,3 +0,0 @@
from bedit.library.repository import LibraryRepository, default_library_paths
__all__ = ["LibraryRepository", "default_library_paths"]

View File

@@ -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))

View File

@@ -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()

View File

@@ -1,4 +0,0 @@
from bedit.workspace.view import GraphWorkspaceView
__all__ = ["GraphWorkspaceView"]

View File

@@ -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()

View File

@@ -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": []
}
}
}
]
}

View File

@@ -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>

View 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>

View File

@@ -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>&amp;Close Document</string></property> <property name="text">
<property name="shortcut"><string>Ctrl+W</string></property> <string>&amp;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>&amp;Delete</string></property> <property name="text">
<property name="shortcut"><string>Del</string></property> <string>&amp;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>

View 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>

View File

@@ -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">

View 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
View 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": []
}
}
]
}
}
}
]
}