Compare commits

...

41 Commits

Author SHA1 Message Date
8a6203d01e Fixed some bugs 2026-07-22 19:16:56 +02:00
2d49f65ec4 Fixed some bugs 2026-07-22 18:13:13 +02:00
c918a6a428 causality inference done 2026-07-22 18:04:07 +02:00
32154386e8 Start with causality inference 2026-07-22 13:20:49 +02:00
09248421d0 Bond graph ports and drawing added 2026-07-22 12:24:58 +02:00
0c578af85d port and param editor in and outside of text editor the same 2026-07-22 11:45:43 +02:00
edefb23edc More types and units 2026-07-22 11:38:07 +02:00
48ac9e2f59 proper modelica text model editing 2026-07-21 17:16:06 +02:00
2f6487e510 Added direct graph navigation without activating Matplotlib toolbar modes:
Mouse wheel zooms in and out around the cursor.
Left-click dragging pans the graph.
Matplotlib toolbar modes still work and take precedence when activated.
The existing Home button can reset the view.
2026-07-21 15:29:58 +02:00
eb8f77ce64 better signal graphing 2026-07-21 15:24:24 +02:00
35d933ccbb Added signal graphing 2026-07-21 15:14:12 +02:00
edd7bb98f2 Saving sim results 2026-07-21 14:40:03 +02:00
ddc004dee5 sim window save/load 2026-07-21 14:01:55 +02:00
cdfc891980 Start of a sim window 2026-07-21 13:54:57 +02:00
6fb2478589 basic runner setup changed 2026-07-21 13:21:52 +02:00
dc770a0886 new runner layout, still todo 2026-07-20 22:03:43 +02:00
38e3d7aae9 Added run button and parameter dialog 2026-07-20 18:46:59 +02:00
4cfe7c3a0e Fixed om emission 2026-07-20 18:17:45 +02:00
d368b36b8c basic openmodelica model emitting 2026-07-20 17:34:31 +02:00
42bf09610b Added binary file format 2026-07-20 15:39:34 +02:00
9ed8e371ef added stub simulation entry and logs 2026-07-20 15:26:18 +02:00
495edd42f4 Added array ports and junctions 2026-07-20 15:00:06 +02:00
a067c85994 new text mode editor 2026-07-20 14:24:47 +02:00
bf2512995f labels 2026-07-20 14:02:22 +02:00
8f87a3b477 new connection editing 2026-07-20 13:50:33 +02:00
6a3e9f01d0 Fixed dragging corners 2026-07-20 13:24:37 +02:00
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
8df5c2a8f9 fix some things 2026-07-19 22:16:37 +02:00
dbca41640f Implemented the new port-management and grid workflow.
Right-click blocks in the graph, Document tree, or Libraries tree and choose “Port Options…”.
Unified port list with:Add/remove port
Name
Type (Signal currently)
Input/output orientation

New ports start at (0, 0) in the icon editor.
Connected ports cannot be removed, reoriented, or changed incompatibly.
Added a PortType registry; connections require matching port types.
Removed/hid the separate Add Input and Add Output workspace tools.
Library port changes are written back to the library JSON.
Added independent graph and icon grid-size settings.
Components, interface terminals, icon shapes, resize handles, and icon ports snap to their corresponding grid.
Icon canvases and component hitboxes are now fixed at 128×128.
Selected icon shapes show a bottom-right resize handle.
Circles preserve equal width and height while resizing.
Shapes and ports are constrained to the icon hitbox.

Fixed the icon-editor crash and grid behavior.
Renamed the resize handle’s shape attribute, which was overriding Qt’s required shape() method.
Icon shapes, resize handles, and port anchors now snap while dragging.
Graph blocks and interface terminals also snap while dragging.
Replaced the nearly invisible dotted graph grid with higher-contrast grid lines.
Retained final release-time snapping as a safety check.
Python compilation and diff validation pass.
2026-07-19 22:06:36 +02:00
09824195c9 Implemented the vector icon editor, library previews, and workspace camera controls.
Key changes:
Component Options now has an “Edit Icon…” button instead of direct shape/color fields.
Icon editor supports:Rectangles with configurable corner radius
Circles and ellipses
Lines
Triangles
Text
Movable/selectable shapes
Shape context menus
Line style and width
Solid or transparent fill
Stroke and fill colours through Qt’s colour picker
Editable shape dimensions
Deleting selected shapes

Input anchors are green and output anchors red; both can be dragged around the icon.
Icons use a 120×80 vector coordinate system and are stored as editable elements in document JSON.
Components render their vector icons directly in the workspace.
Library and document trees display 16×16 icon previews.
Added a Camera toolbar:Zoom In
Zoom Out
Center

Mouse-wheel zoom works directly over the workspace.
Icon changes and port positions support undo/redo.
2026-07-19 21:46:44 +02:00
76bd313f79 Basic document handling 2026-07-19 21:26:27 +02:00
c5ab3329ae Start of BEdit 2026-07-19 19:19:15 +02:00
da69acf919 Merge branch 'master' of ssh://git.joppeb.nl:222/joppe/BondGraph 2026-07-19 13:45:19 +02:00
c5ca585134 Added simple piston with gas test 2026-07-19 13:44:33 +02:00
cbe611916e Setup for position control 2026-03-11 19:08:49 +01:00
9e124a5e92 Working and exportable PID controller 2026-03-11 16:37:50 +01:00
71722127f1 Start met EmbeddedControl lib 2026-03-11 11:57:48 +01:00
128 changed files with 25058 additions and 1 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
build/
__pycache__*
__pycache__*
exporttest/out

12
BEdit/.gitignore vendored Normal file
View File

@@ -0,0 +1,12 @@
.venv/
__pycache__/
*.py[cod]
*.egg-info/
build/
dist/
.pytest_cache/
.idea/
.vscode/*
!.vscode/tasks.json
.ruff_cache
ui/*_ui.py

151
BEdit/.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,151 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Qt: Open Designer",
"type": "shell",
"command": "pyside6-designer",
"args": [
],
"options": {
"cwd": "${workspaceFolder}",
"env": {
"QT_QPA_PLATFORMTHEME" :"qt6ct",
"QT_QPA_PLATFORM": "xcb"
}
},
"problemMatcher": [],
"presentation": {
"reveal": "always",
"panel": "dedicated"
}
},
{
"label": "Qt: Compile Resources to Python",
"type": "shell",
"command": "pyside6-rcc",
"args": [
"${workspaceFolder}/resources/resources.qrc",
"-o",
"${workspaceFolder}/src/bedit/gui/generated/resources_rc.py"
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [],
"presentation": {
"reveal": "silent",
"panel": "shared",
"clear": true
}
},
{
"label": "Qt: Compile UI to Python",
"type": "shell",
"command": "pyside6-uic",
"args": [
"--from-imports",
"${workspaceFolder}/ui/main_window.ui",
"-o",
"${workspaceFolder}/src/bedit/gui/generated/ui_main_window.py"
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [],
"presentation": {
"reveal": "silent",
"panel": "shared"
}
},
{
"label": "Qt: Compile Settings UI to Python",
"type": "shell",
"command": "pyside6-uic",
"args": [
"--from-imports",
"${workspaceFolder}/ui/settings_dialog.ui",
"-o",
"${workspaceFolder}/src/bedit/gui/generated/ui_settings_dialog.py"
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [],
"presentation": {
"reveal": "silent",
"panel": "shared"
}
},
{
"label": "Qt: Compile Component Options UI to Python",
"type": "shell",
"command": "pyside6-uic",
"args": [
"--from-imports",
"${workspaceFolder}/ui/component_options_dialog.ui",
"-o",
"${workspaceFolder}/src/bedit/gui/generated/ui_component_options_dialog.py"
],
"options": {"cwd": "${workspaceFolder}"},
"problemMatcher": [],
"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: Compile Simulation Window UI to Python",
"type": "shell",
"command": "pyside6-uic",
"args": ["--from-imports", "${workspaceFolder}/ui/simulation_window.ui", "-o", "${workspaceFolder}/src/bedit/gui/generated/ui_simulation_window.py"],
"options": {"cwd": "${workspaceFolder}"},
"problemMatcher": []
},
{
"label": "Qt: Build Designer Files",
"dependsOrder": "sequence",
"dependsOn": [
"Qt: Compile Resources to Python",
"Qt: Compile UI to Python",
"Qt: Compile Settings UI to Python",
"Qt: Compile Component Options UI to Python",
"Qt: Compile Port Options UI to Python",
"Qt: Compile Shape Options UI to Python",
"Qt: Compile Icon Editor UI to Python",
"Qt: Compile Simulation Window UI to Python"
],
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"reveal": "silent",
"panel": "shared",
"clear": true
}
}
]
}

347
BEdit/AGENTS.md Normal file
View File

@@ -0,0 +1,347 @@
# 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
│ └── simulation/ # Qt-free composition and OpenModelica interface code
└── 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.
- Parameters belong to `Component` rather than a particular implementation kind,
so graph and text components share stable-ID name/type/value records.
- Components store one ordered `ports` list. Each port's `orientation` field is
authoritative (`input`, `output`, or power-only `indifferent`); connection and
presentation code derives any directional groupings when needed.
- Port types are registered in `core/port_types.py`. Only compatible types may be
connected. Signal ports connect by type; power ports additionally require the
same domain. Editable power domains and causalities live in
`core/power_domains.py`. The `single effort in` and `single flow in`
causalities are available only on power ports that allow multiple connections.
- Power ports may use `indifferent` orientation and can act as either connection
endpoint. For two indifferent ports, click order determines source/arrow
direction; otherwise output/input semantics determine direction.
- Port connector type and signal value type are separate. Signal ports and
parameters carry editable value type, quantity, unit, row/column dimensions,
and description metadata. Editable quantity/unit suggestions live in
`core/physical_types.py`; unlisted values remain valid.
- Port removal or reorientation must be rejected when it would invalidate an
existing connection.
- Connections reference port IDs, never port names.
- Connections store their port type and bond-graph causality explicitly.
`core/bond_graph.py` owns causality inference; the simulation service invokes
it before Modelica composition. The controller copies inferred values into the
live model and refreshes the workspace before compile, export, or run.
Causality values are `none`, `source`, `target`, `warn_source`, and `warn_target`.
- Connection junctions are explicit typed graph objects. Splitting a connection
creates one incoming and one outgoing segment; the junction can source further
branches without overlapping full connection paths. Power bond connections
cannot contain junctions.
- Graph interaction has separate Pointer and Connect modes. Port hints are only
visible in Connect mode. Connecting two blocks opens the compatible port-pair
chooser; explicit port clicks determine its default selection. Connections
use one freely angled polyline format with absolute `properties.waypoints`.
Semantic ports choose compatibility, while each rendered endpoint is the
intersection of the owning hitbox and its center-to-adjacent-route-point ray.
- Connection appearance is configured per port type in
`gui/graphics/connection_styles.py`, including color, width, pen style, and
source/target arrowheads. Arrow styles are `open`, `half`, or `filled`. 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 polyline 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.
- Component clipboard data is shared by the graph, document tree, and library
tree. Pasting into the document node creates roots; pasting into a graph node
creates children at an origin-normalized position. Always clone pasted trees
with fresh IDs, preserve connections between jointly copied graph blocks, and
assign unique sibling names.
- The text-definition editor uses OpenModelica highlighting and completion from
`src/bedit/data/syntax/openmodelica.json`. Keep keywords, types, built-ins, and
named BEdit `$name$` macro completions editable there; arbitrary `$name$`
expressions are highlighted as BEvalues. Highlight colors and bold/italic
styles are persisted under `syntax/<category>/` in application settings.
- Text component sources may contain private Modelica declarations in
`source.declarations`. The text-definition editor exposes declarations above
`source.initialEquations` and `source.equations`, with the same highlighting
and completion in all three fields. The composer emits each in its matching
Modelica section.
- The text-definition view embeds the same `PortEditor` and `ParameterEditor`
implementations used by the standalone options dialogs. Do not reintroduce
separate port/parameter tables in the text editor.
- Application-wide messages use `core.application_log.get_logger()`. The main
window installs the Qt log-panel handler; core code must only use standard
Python logging and must not import the GUI handler.
- File → Reload Simulation Code (`Ctrl+F5`) reloads modules under
`bedit.core.simulation`, replaces the shared application/controller service,
and preserves the previous instance attributes where possible.
- Modelica composition lives in `core/simulation/composer.py`; the simulation
service only owns application state and delegates composition. Ports with
`multipleConnections` are emitted as Modelica arrays. Their size is inferred
per component instance from graph connections and exposed while compiling as
`$portname_N$`; array connection endpoints receive stable one-based indices in
graph connection order.
- Simulation → Export Model composes through `Simulation.compose_source()` without
building the model, then writes the generated source as a `.mo` file.
- OpenModelica integration belongs in `core/simulation/openmodelica.py`. Its
persistent worker and OMC session start lazily on the first queued request.
Never perform OMPython work directly on the Qt GUI thread. Result and error
callbacks run on background threads and must use a Qt signal before touching UI.
One lazy temporary working directory is shared by all requests in the session.
Explicit application shutdown closes OMC and removes that directory plus the
current session's OMPython log and port files; `__del__` is only a fallback.
- Simulation runs start an ephemeral localhost TCP listener before launching the
generated model through OMC's `system()` function. OpenModelica's newline-delimited
`xmltcp` status and message records are parsed in the core and forwarded through
callbacks; the simulation service retains the latest progress for polling.
- The application owns one reusable `SimulationWindow`. Starting a run clears its
progress, log, and future result views. Extend graph presentation through its
Designer-owned `resultsLayout` and the
`clear_result_views()`/`load_result_views()` hooks.
- Simulation-window geometry, dock/toolbar state, and central-results visibility
persist under `simulationWindow/` through `application_settings()`.
- Standalone simulation results are modeled in `core/simulation/results.py`.
Its versioned schema retains model status, messages, metadata, and plottable
traces so the simulation window can open results without an active document.
Human-readable `.json` uses JSON, while the default `.ber` format uses the same
compressed MessagePack approach as `.beb` documents.
- After a successful OpenModelica run, `<model>_res.csv` is parsed on the worker
before temporary-directory cleanup. `SimulationResults.data` stores every CSV
column as a numeric array, including `time`, for later plotting and persistence.
- The simulation window's dockable Signals tree derives hierarchy from dot-separated
result-column names and bracketed array indices (`a[1]` becomes `a → 1`). Leaf
items retain the exact full column name in `UserRole`; plotting code should
consume `SimulationWindow.selected_signal_names()`.
- Simulation graph tabs persist as `SimulationResults.graphs`; every graph has a
stable ID, editable title, and its own `traces` list. Runtime graph widgets belong
in `GraphWorkspacePage.plot_layout`, not in the serialized core model. The
Signals tree checkboxes edit the active graph's traces, and each page embeds a
Matplotlib QtAgg canvas. Each graph persists its own `x_axis` signal (default
`time`), selectable from the Signals tree context menu.
- Embedded Matplotlib canvases provide cursor-centered wheel zoom and direct
left-button drag panning without activating navigation-toolbar modes. Their
custom navigation toolbar restores an explicit data-derived home view.
- Rerunning the same composed model retains graph tabs, ordering, titles, X axes,
and surviving trace settings while replacing numeric data. Missing signals are
pruned from traces/X-axis selection and newly returned columns appear in the tree.
- The optional OpenModelica executable is persisted as
`simulation/openModelicaPath`. The GUI passes it into `Simulation`; core must
not read `QSettings`. An empty path uses OMPython/PATH discovery, while an
explicit `.../bin/omc` path is converted to the OpenModelica home directory.
- Builds enable OpenModelica's `--unitChecking`. Completed simulation results
read per-column units from OMC's generated `<model>_init.xml`; BEdit must not
infer derivative units itself. This metadata is serialized with `.ber`/JSON
results and displayed by the simulation signal tree and plots.
- Result loading restores variables marked `alias` or `negatedAlias` in OMC's
initialization XML. Values come from OMC's result representative or a fixed
parameter/constant start value; BEdit does not infer aliases from graph edges.
- Compile requests call OMC `checkModel` before `buildModel` and log OMC's check
summary verbatim through the application-wide logger.
## 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
pyside6-uic --from-imports ui/text_definition_editor.ui \
-o src/bedit/gui/generated/ui_text_definition_editor.py
pyside6-uic --from-imports ui/simulation_settings_dialog.ui \
-o src/bedit/gui/generated/ui_simulation_settings_dialog.py
pyside6-uic --from-imports ui/simulation_window.ui \
-o src/bedit/gui/generated/ui_simulation_window.py
pyside6-uic --from-imports ui/graph_parameters_dialog.ui \
-o src/bedit/gui/generated/ui_graph_parameters_dialog.py
pyside6-uic --from-imports ui/parameter_options_dialog.ui \
-o src/bedit/gui/generated/ui_parameter_options_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.
- Documents can be stored as human-readable `.bedit.json`/`.json` through
`JsonDocumentSerializer`, or as compressed MessagePack `.beb` through
`BebDocumentSerializer`. UI document I/O dispatches via `DocumentSerializer`.
- `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`.
The first non-flag launch argument is opened as the initial document.
## 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.

214
BEdit/README.md Normal file
View File

@@ -0,0 +1,214 @@
# BEdit Qt starter
A small desktop application scaffold using Python and PySide6 (the official Qt
bindings). Its interface is maintained in Qt Designer and it includes a main
window, menu bar, blank central workspace, common keyboard shortcuts, and
persisted window geometry. The application forces Qt's light Fusion palette, so
it remains light even when the operating-system theme is dark.
## Run it
Python 3.10 or newer is required. From this directory:
```bash
python -m venv .venv
```
Activate the environment:
- Windows PowerShell: `.venv\Scripts\Activate.ps1`
- Windows Command Prompt: `.venv\Scripts\activate.bat`
- Linux/macOS: `source .venv/bin/activate`
Then install and launch:
```bash
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
python -m bedit
```
After installation, the `bedit` command also launches the application.
## Project layout
```text
.
├── pyproject.toml dependencies, package metadata, and `bedit` command
├── README.md
├── ui/ editable Qt Designer sources
└── src/bedit
├── __main__.py minimal `python -m bedit` entry point
├── core/ Qt-free domain model, port types, files, libraries
└── gui/ every PySide-dependent module
├── 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
The project now uses Qt Designer. It is included with PySide6 on most
installations. Open the existing form with:
```bash
pyside6-designer ui/main_window.ui
```
In Designer, use the Widget Box to add controls, the Object Inspector to select
them, and the Property Editor to name and configure them. Always put widgets in
a layout (horizontal, vertical, grid, or form) so the window resizes correctly.
Save the form, close the running application if necessary, then regenerate its
Python wrapper:
```bash
pyside6-uic --from-imports ui/main_window.ui \
-o src/bedit/gui/generated/ui_main_window.py
```
Do not hand-edit files in `gui/generated`; change the `.ui` source and regenerate
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`.
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**:
- **Qt: Open Main Window in Designer** opens the form for visual editing.
- **Qt: Build Designer Files** compiles resources and then regenerates the UI
wrapper. It is the default build task, available with `Ctrl+Shift+B`.
- The separate resource and UI compilation tasks remain available when only one
generated file needs rebuilding.
## Architecture rules
- `bedit.core` must remain importable without PySide6.
- `bedit.gui` may depend on `core`; `core` must never import `gui`.
- Designer output and resource output belong only in `gui/generated`.
- Domain serialization and library parsing stay in `core`; Qt signals, models,
undo integration, painting, and widgets stay in `gui`.
## Graph and library prototype
Documents and libraries use the same recursive format: a library is simply a
BEdit document used as a copy source. The built-in example defines A, B, and C.
- A document can own multiple independent top-level graph or text components.
Right-click **Current Document** to create one, and double-click a current
component in the tree to activate it.
- Drag a component from Libraries onto the workspace. Placement recursively
copies it with new IDs, leaving no link to the source.
- Drag components to move them; movement participates in undo and redo.
- Components, interface terminals, and connections are selectable. Use a rubber
band or Ctrl-click for multiple selection, Delete to remove items, and the
standard Cut/Copy/Paste shortcuts to duplicate selected component groups.
- Switch the graph header to **Connect** and click two blocks or their temporary
port hints. A chooser lists every compatible output/input pairing in both
directions and preselects the pairing implied by the clicks. Empty-canvas
clicks add freely angled, grid-snapped vertices; right-click cancels.
- 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. Visible wire endpoints are
projected onto each icon's hitbox where the ray from its center toward the
adjacent route point crosses the boundary. Hidden semantic port positions do
not constrain the wire.
- Use **Box**, **Line**, and **Text** to add persistent graph annotations. Lines
use the same freely angled polyline and draggable-node behavior as connections.
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
interface tool and click the canvas to add a visible internal terminal and a
corresponding external block port. Interface terminals can be moved afterward.
- Right-click a component on the canvas or in Current Document to edit its name,
icon shape, icon text, fill color, and border color. The same dialog can hide
that component's contained subtree from the Libraries tree.
- 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. Selected
rectangles expose draggable corner and edge resize handles in both editors.
- Double-click a text component to edit its input list, output list, and
`implementation.source` JSON.
- Right-click any graph component under Current Document to add nested graph or
text blocks. Any current-document component can also be deleted there.
- The left navigator presents the current Document and external Libraries as tabs.
- Select one or more blocks and press `Ctrl+R`, or use **Rotate** in the graph header, to
rotate them clockwise by 90 degrees. Rotation is saved and supports undo/redo.
**Apply JSON** updates that source and participates in undo/redo.
- File → Save writes the complete recursive document to JSON.
- File → Close Document removes the active document and returns to an empty
workspace. An open graph uses a light gray grid whose visible and snapping
spacing are configured separately.
- Edit → Settings → Libraries accepts JSON or `.beb` document files and folders.
Every component owns its ports, declarative icon, properties, and child graph:
```json
{
"format": "bedit-document",
"version": 1,
"roots": [{
"id": "my-component",
"name": "My Component",
"position": {"x": 0, "y": 0},
"interface": {
"ports": [
{"id": "in", "name": "Input", "type": "signal", "orientation": "input",
"properties": {"iconPosition": {"x": 0, "y": 40}}},
{"id": "out", "name": "Output", "type": "signal", "orientation": "output",
"properties": {"iconPosition": {"x": 128, "y": 64}}}
]
},
"icon": {
"size": {"width": 128, "height": 128},
"elements": [{
"type": "rectangle", "x": 32, "y": 32,
"width": 64, "height": 64,
"cornerRadius": 5,
"fill": "#dbeafe", "stroke": "#245c9c",
"lineWidth": 1.5, "lineStyle": "solid"
}]
},
"properties": {},
"implementation": {
"kind": "text",
"source": {
"equations": ["out = gain * in"],
"parameters": {"gain": 1.0}
}
}
}]
}
```
Graph components use `"implementation": {"kind": "graph", "graph": ...}`;
text components use `"implementation": {"kind": "text", "source": ...}` and
never own a graph. Vector icons can contain rectangles, circles, ellipses,
lines, triangles, and text. Each element owns its geometry, fill, stroke, and
line style; ports keep their icon anchor in `properties.iconPosition`. The
port type registry controls which types may connect (currently `signal` only).
The workspace grid size, its finer snapping size, and the icon grid size are
configured independently in Settings. The recursive model and file loading are
under `src/bedit/core/`; the live Qt trees and graphics are isolated under
`src/bedit/gui/models/` and `src/bedit/gui/graphics/`.
## Optional tools
You do not need another GUI framework. Useful additions are:
- **Qt Designer** for drag-and-drop form layout.
- **Ruff** for formatting/linting (`ruff check .`).
- **pytest-qt** later for GUI interaction tests.
- **PyInstaller** or **Nuitka** later to produce a Windows `.exe`.
The light colors are defined in `apply_light_theme()` in `src/bedit/app.py`.
Adjust that palette if you want different light colors. Avoid a large stylesheet
unless the application needs highly customized controls; palettes preserve more
of Qt's standard behavior.

39
BEdit/pyproject.toml Normal file
View File

@@ -0,0 +1,39 @@
[build-system]
requires = ["setuptools>=69"]
build-backend = "setuptools.build_meta"
[project]
name = "bedit"
version = "0.1.0"
description = "A starter Qt desktop application"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"matplotlib>=3.8,<4",
"msgpack>=1.0,<2",
"PySide6>=6.7,<7",
"OMPython>4.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8",
"ruff>=0.5",
]
[project.gui-scripts]
bedit = "bedit.gui.app:main"
[tool.setuptools.packages.find]
where = ["src"]
[tool.setuptools.package-data]
bedit = [
"data/libraries/*.json",
"data/libraries/*.beb",
"data/syntax/*.json",
]
[tool.ruff]
line-length = 100
target-version = "py310"

View File

@@ -0,0 +1,24 @@
Oxygen Icon Theme has been developed by The Oxygen Team.
Art Directors:
Nuno F. Pinheiro <nuno@nuno-icons.com>
David Vignoni <david@oxygen-icons.org>
Naming Coordinator
Jakob Petsovits <jpetso@gmx.at>
Designers:
David J. Miller <miller@oxygen-icons.org>
David Vignoni <david@oxygen-icons.org>
Johann Ollivier Lapeyre <johann@oxygen-icons.org>
Kenneth Wimer <ken@oxygen-icons.org>
Nuno F. Pinheiro <nuno@nuno-icons.com>
Riccardo Iaconelli <riccardo@oxygen-icons.org>
David J. Miller <miller@oxygen-icons.org>
Thanks to:
Lee Olson: Contributed drawing used in application-x-bittorent icon.
Marco Aurélio "Coré": Improved audio-input-microphone icon.
Matthias Kretz: Contributed "audio-input-line" device icon.
Mauricio Piacentini <piacentini@kde.org> : game icons mashup
Erlend Hamberg: "text-x-haskell" mimetype icon.

View File

@@ -0,0 +1,216 @@
The Oxygen Icon Theme
Copyright (C) 2007 Nuno Pinheiro <nuno@oxygen-icons.org>
Copyright (C) 2007 David Vignoni <david@icon-king.com>
Copyright (C) 2007 David Miller <miller@oxygen-icons.org>
Copyright (C) 2007 Johann Ollivier Lapeyre <johann@oxygen-icons.org>
Copyright (C) 2007 Kenneth Wimer <kwwii@bootsplash.org>
Copyright (C) 2007 Riccardo Iaconelli <riccardo@oxygen-icons.org>
and others
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <http://www.gnu.org/licenses/>.
Clarification:
The GNU Lesser General Public License or LGPL is written for
software libraries in the first place. We expressly want the LGPL to
be valid for this artwork library too.
KDE Oxygen theme icons is a special kind of software library, it is an
artwork library, it's elements can be used in a Graphical User Interface, or
GUI.
Source code, for this library means:
- where they exist, SVG;
- otherwise, if applicable, the multi-layered formats xcf or psd, or
otherwise png.
The LGPL in some sections obliges you to make the files carry
notices. With images this is in some cases impossible or hardly useful.
With this library a notice is placed at a prominent place in the directory
containing the elements. You may follow this practice.
The exception in section 5 of the GNU Lesser General Public License covers
the use of elements of this art library in a GUI.
kde-artists [at] kde.org
-----
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View File

@@ -0,0 +1 @@
Oxygen Icons is a freedesktop.org compatible icon theme originally developed for the KDE Plasma desktop environment in combination with the Oxygen Style. It features smooth gradients, soft shadows, and a slightly glossy look.

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.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 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: 879 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 892 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 991 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 877 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 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

@@ -0,0 +1,36 @@
<RCC>
<qresource prefix="icons">
<file>icons/list-remove.png</file>
<file>icons/list-add.png</file>
<file>icons/view-form-table.png</file>
<file>icons/office-chart-line.png</file>
<file>icons/run-build.png</file>
<file>icons/preferences-system.png</file>
<file>icons/draw-triangle.png</file>
<file>icons/draw-ellipse.png</file>
<file>icons/draw-circle.png</file>
<file>icons/draw-path.png</file>
<file>icons/network-connect.png</file>
<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-redo.png</file>
<file>icons/edit-select.png</file>
<file>icons/edit-undo.png</file>
<file>icons/media-playback-start.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>
</RCC>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
"""BEdit desktop application."""
__version__ = "0.1.0"

View File

@@ -0,0 +1,5 @@
from bedit.gui.app import main
if __name__ == "__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,9 @@
import logging
LOGGER_NAME = "bedit"
def get_logger(name: str | None = None) -> logging.Logger:
"""Return the shared application logger, optionally scoped to a module."""
return logging.getLogger(LOGGER_NAME if name is None else f"{LOGGER_NAME}.{name}")

View File

@@ -0,0 +1,321 @@
"""Bond-graph analysis hooks.
This module intentionally has no GUI or simulation-engine dependencies. The
causality inference algorithm can grow here without coupling the document model
to OpenModelica or Qt.
"""
from __future__ import annotations
from typing import Any
from bedit.core.application_log import get_logger
log = get_logger(__name__)
def infer_causality(component: dict[str, Any], toplevel: bool = True) -> dict[str, Any]:
"""Infer power-connection causality in a serialized component tree.
This is currently a traversal stub: it ensures every power connection has a
causality value, while preserving causality already supplied by callers.
Future inference rules should assign ``source``, ``target``,
``warn_source``, or ``warn_target`` here and return the same tree.
The input is mutated and returned so the composer receives the inferred
representation without needing a second document conversion.
"""
if toplevel:
# Reset causalities
reset_causality(component)
implementation = component.get("implementation", {})
graph = implementation.get("graph") if isinstance(implementation, dict) else None
if not isinstance(graph, dict):
return component
id_list = build_id_list(component)
# Fixed causalities
for block in graph.get("blocks", []):
for port in block.get('interface', {}).get('ports', []):
if port.get('type', '') != 'power':
continue
if port.get('causality', 'indifferent') == 'fixed effort out':
propagate_from_port(block, port, 'effort out', graph, id_list)
elif port.get('causality', 'indifferent') == 'fixed flow out':
propagate_from_port(block, port, 'flow out', graph, id_list)
# Preferred causalities
for block in graph.get("blocks", []):
for port in block.get('interface', {}).get('ports', []):
if port.get('type', '') != 'power':
continue
if is_port_fully_assigned(block, port, graph):
continue
if port.get('causality', 'indifferent') == 'preferred effort out':
propagate_from_port(block, port, 'effort out', graph, id_list)
elif port.get('causality', 'indifferent') == 'preferred flow out':
propagate_from_port(block, port, 'flow out', graph, id_list)
# Soft choices may need several passes when a junction has multiple
# unresolved bonds. Stop as soon as a pass makes no further progress.
while True:
unresolved_before = sum(
con.get('type') == 'power' and con.get('causality', 'none') == 'none'
for con in graph.get('connections', [])
)
for block in graph.get("blocks", []):
for port in block.get('interface', {}).get('ports', []):
if port.get('type', '') != 'power':
continue
if is_port_fully_assigned(block, port, graph):
continue
if port.get('causality', 'indifferent') == 'likes effort out':
propagate_from_port(block, port, 'effort out', graph, id_list)
elif port.get('causality', 'indifferent') == 'likes flow out':
propagate_from_port(block, port, 'flow out', graph, id_list)
elif port.get('causality', 'indifferent') == 'single effort in':
evaluate_junction_constraints(block, port, graph, id_list)
if not is_port_fully_assigned(block, port, graph):
propagate_from_port(block, port, 'flow out', graph, id_list)
elif port.get('causality', 'indifferent') == 'single flow in':
evaluate_junction_constraints(block, port, graph, id_list)
if not is_port_fully_assigned(block, port, graph):
propagate_from_port(block, port, 'effort out', graph, id_list)
elif port.get('causality', 'indifferent') == 'indifferent':
# Force an arbitrary assignment on the first unassigned bond connected to this port
propagate_from_port(block, port, 'effort out', graph, id_list)
unresolved_after = sum(
con.get('type') == 'power' and con.get('causality', 'none') == 'none'
for con in graph.get('connections', [])
)
if unresolved_after == 0 or unresolved_after >= unresolved_before:
break
# Last check
for con in graph.get('connections', []):
if con.get('type') == 'power' and con.get('causality', 'none') == 'none':
raise ValueError("System under-constrained: unresolved causal loops or disconnected elements remain")
for block in graph.get("blocks", []):
if isinstance(block, dict):
infer_causality(block, False)
return component
def reset_causality(component: dict[str, Any]) -> None:
implementation = component.get("implementation", {})
graph = implementation.get("graph") if isinstance(implementation, dict) else None
if not isinstance(graph, dict):
return
for connection in graph.get("connections", []):
if isinstance(connection, dict) and connection.get("type") == "power":
connection["causality"] = "none"
for block in graph.get("blocks", []):
if isinstance(block, dict):
reset_causality(block)
def build_id_list(graph: dict[str, Any]) -> dict[str, Any]:
"""Index all addressable objects in a component tree by their stable ID."""
id_list: dict[str, Any] = {}
id_kinds: dict[str, str] = {}
def add(item: dict[str, Any], description: str) -> None:
item_id = item.get("id")
if not item_id:
raise ValueError(f"{description} has no ID")
# Port IDs identify a port on a component definition and may therefore
# recur in cloned component instances. Component and junction IDs are
# document objects and must remain globally unique.
if item_id in id_list and not (
description == "port" and id_kinds[item_id] == "port"
):
raise ValueError(f"Duplicate simulation object ID: {item_id}")
id_list[item_id] = item
id_kinds[item_id] = description
def visit(component: dict[str, Any]) -> None:
add(component, "component")
interface = component.get("interface", {})
for port in interface.get("ports", []):
add(port, "port")
implementation = component.get("implementation", {})
if implementation.get("kind") != "graph":
return
nested_graph = implementation.get("graph", {})
for junction in nested_graph.get("junctions", []):
add(junction, "junction")
for block in nested_graph.get("blocks", []):
visit(block)
visit(graph)
return id_list
def is_port_fully_assigned(block: dict[str, Any], port: dict[str, Any], graph: dict[str, Any]) -> bool:
block_id = block.get('id')
port_id = port.get('id')
for connection in graph.get('connections', []):
if connection.get('type') != 'power':
continue
source = connection.get('source', {})
target = connection.get('target', {})
if ((source.get('block') == block_id and source.get('port') == port_id)
or (target.get('block') == block_id and target.get('port') == port_id)):
if connection.get('causality', 'none') == 'none':
return False
return True
def propagate_from_port(block: dict[str, Any], port: dict[str, Any], desired_causality: str, graph: dict[str, Any], id_list: dict[str, Any]) -> None:
# find all connection on this specific port
attached_connections = []
for con in graph.get('connections', []):
if con.get('type') == 'power':
if con.get('source', {}).get('block') == block.get('id') and con.get('source', {}).get('port') == port.get('id'):
attached_connections.append(con)
elif con.get('target', {}).get('block') == block.get('id') and con.get('target', {}).get('port') == port.get('id'):
attached_connections.append(con)
if not attached_connections:
# unconnected port
return
# find first unassigned connection on this port
target_connection = None
for con in attached_connections:
if con.get('causality', None) == 'none':
target_connection = con
break
if target_connection is None:
# Nothing left to resolve
return
# Update
if target_connection.get('source').get('block') == block.get('id'):
target_connection['causality'] = 'target' if (desired_causality == 'effort out') else 'source'
else:
target_connection['causality'] = 'source' if (desired_causality == 'effort out') else 'target'
propagate_to_neighbor_from_connection(block, target_connection, graph, id_list)
def evaluate_junction_constraints(block: dict[str, Any], port: dict[str, Any], graph: dict[str, Any], id_list: dict[str, Any]) -> None:
# find all connection on this specific port
attached_connections = []
for con in graph.get('connections', []):
if con.get('type') == 'power':
if con.get('source', {}).get('block') == block.get('id') and con.get('source', {}).get('port') == port.get('id'):
attached_connections.append(con)
elif con.get('target', {}).get('block') == block.get('id') and con.get('target', {}).get('port') == port.get('id'):
attached_connections.append(con)
if not attached_connections:
# unconnected port
return
efforts_in = 0
efforts_out = 0
unnassigned_conns = []
# Count current states relative to the junction port
for con in attached_connections:
if con.get('causality', 'none') == 'none':
unnassigned_conns.append(con)
continue
is_source = (con.get('source').get('block') == block.get('id'))
if (is_source and con.get('causality', 'none')=='source') or (not is_source and con.get('causality', 'none')=='target'):
efforts_in += 1
else:
efforts_out += 1
# Single effort in
if port.get('causality', 'indifferent') == 'single effort in':
# ERROR CHECK: Multiple sources/blocks trying to claim effort control on a 0-junction
if efforts_in>1:
raise ValueError(f"Critical causality conflict: Multiple blocks are dictating effort to 0 junction: {block.get('name')}")
# 1 effort is coming in so all other ports must be effort out
if efforts_in == 1 and len(unnassigned_conns) > 0:
for con in unnassigned_conns:
desired = 'target' if (con.get('source').get('block')==block.get('id')) else 'source'
con['causality'] = desired
propagate_to_neighbor_from_connection(block, con, graph, id_list)
# No effort is coming in yet and one left so must be effort in
if efforts_in == 0 and len(unnassigned_conns) == 1:
con = unnassigned_conns[0]
desired = 'source' if (con.get('source').get('block')==block.get('id')) else 'target'
con['causality'] = desired
propagate_to_neighbor_from_connection(block, con, graph, id_list)
# Single flow in
elif port.get('causality', 'indifferent') == 'single flow in':
# ERROR CHECK: Multiple sources/blocks trying to claim flow control on a 1-junction
if efforts_out>1:
raise ValueError(f"Critical causality conflict: Multiple blocks are dictating flow to 1 junction: {block.get('name')}")
# 1 flow is coming in so all other ports must be flow out
if efforts_out == 1 and len(unnassigned_conns) > 0:
for con in unnassigned_conns:
desired = 'source' if (con.get('source').get('block')==block.get('id')) else 'target'
con['causality'] = desired
propagate_to_neighbor_from_connection(block, con, graph, id_list)
# No flow is coming in yet and one left so must be flow in
if efforts_out == 0 and len(unnassigned_conns) == 1:
con = unnassigned_conns[0]
desired = 'target' if (con.get('source').get('block')==block.get('id')) else 'source'
con['causality'] = desired
propagate_to_neighbor_from_connection(block, con, graph, id_list)
def propagate_to_neighbor_from_connection(block: dict[str, Any], connection: dict[str, Any], graph: dict[str, Any], id_list: dict[str, Any]) -> None:
# Get the neighbor block and port on the other side of the bond
neighbor_id = connection.get('target').get('block') if connection.get('source').get('block') == block.get('id') else connection.get('source').get('block')
neighbor_port_id = connection.get('target').get('port') if connection.get('source').get('block') == block.get('id') else connection.get('source').get('port')
neighbor_block = id_list.get(neighbor_id, None)
if neighbor_block is None:
raise ValueError(f"No other side of the bond found on connection {connection.get('id')}")
neighbor_ports = neighbor_block.get('interface').get('ports', [])
neighbor_port = None
for p in neighbor_ports:
if p.get('id') == neighbor_port_id:
neighbor_port = p
break
if neighbor_port is None:
raise ValueError(f"No other side of the bond found on connection {connection.get('id')}")
# Trigger evaluation on the neighbor
if neighbor_port.get('causality', 'indifferent') in ['single effort in', 'single flow in']:
evaluate_junction_constraints(neighbor_block, neighbor_port, graph, id_list)
else:
verify_component_compatibility(neighbor_block, neighbor_port, graph)
def verify_component_compatibility(block: dict[str, Any], port: dict[str, Any], graph: dict[str, Any]) -> None:
# Find the connection we are evaluating
conn = None
for c in graph.get('connections', []):
if (c.get('source').get('block') == block.get('id') and c.get('source').get('port') == port.get('id')) or (c.get('target').get('block') == block.get('id') and c.get('target').get('port') == port.get('id')):
if c.get('causality', 'none') != 'none':
conn = c
break
if conn is None:
return
# Figure out what causality was pushed onto this port from the outside world
state = conn.get('causality', 'none')
is_source = conn.get('source', {}).get('block') == block.get('id')
effort_out = (is_source and state == 'target') or (not is_source and state == 'source')
port_causality = port.get('causality', 'indifferent')
if port_causality == 'fixed effort out' and not effort_out:
raise ValueError(f"Critical source conflict: Fixed effort source '{block.get('name')}' forced into an input state.")
if port_causality == 'fixed flow out' and effort_out:
raise ValueError(f"Critical source conflict: Fixed flow source '{block.get('name')}' forced into an input state.")
if port_causality == 'preferred effort out' and not effort_out:
conn['causality'] = f'warn_{state}'
# log.warning(f"Storage block '{block.get('name')}' was forced into derivative causality (dependent state).")
if port_causality == 'preferred flow out' and effort_out:
conn['causality'] = f'warn_{state}'
# log.warning(f"Storage block '{block.get('name')}' was forced into derivative causality (dependent state).")

View File

@@ -0,0 +1,32 @@
from dataclasses import dataclass
from pathlib import Path
from bedit.core.model import GraphDocument
from bedit.core.serializer import DocumentSerializer
@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" / "default.beb"
def default_library_paths() -> list[str]:
return [str(bundled_library_path())]
def load_library_file(path: Path) -> LibraryDocument:
document = DocumentSerializer.load(path)
name = str(document.metadata.get("name") or path.stem)
return LibraryDocument(name, document, str(path))
def library_candidates(path: Path) -> list[Path]:
if not path.is_dir():
return [path]
return sorted((*path.glob("*.json"), *path.glob("*.beb")))

View File

@@ -0,0 +1,769 @@
from __future__ import annotations
from copy import deepcopy
from dataclasses import dataclass, field
from typing import Any
from uuid import uuid4
from bedit.core.port_types import PortTypeRegistry
from bedit.core.power_domains import (
MULTI_CONNECTION_POWER_CAUSALITIES,
POWER_CAUSALITIES,
POWER_DOMAINS,
)
def _dimensions(data: Any, subject: str) -> tuple[int, int]:
if not isinstance(data, dict):
raise ValueError(f"{subject} dimensions must be an object")
try:
rows = int(data.get("rows", 1))
columns = int(data.get("columns", 1))
except (TypeError, ValueError) as error:
raise ValueError(f"{subject} dimensions must be whole numbers") from error
if rows < 1 or columns < 1:
raise ValueError(f"{subject} dimensions must be at least 1")
return rows, columns
@dataclass
class Port:
id: str
name: str
x: float = 0.0
y: float = 0.0
properties: dict[str, Any] = field(default_factory=dict)
type: str = "signal"
allows_multiple_connections: bool = False
value_type: str = "real"
quantity: str = ""
unit: str = ""
rows: int = 1
columns: int = 1
description: str = ""
orientation: str = "input"
domain: str = "power"
causality: str = "indifferent"
def to_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"name": self.name,
"position": {"x": self.x, "y": self.y},
"properties": self.properties,
"type": self.type,
"multipleConnections": self.allows_multiple_connections,
"valueType": self.value_type,
"quantity": self.quantity,
"unit": self.unit,
"dimensions": {"rows": self.rows, "columns": self.columns},
"description": self.description,
"orientation": self.orientation,
"domain": self.domain,
"causality": self.causality,
}
@classmethod
def from_dict(cls, data: dict[str, Any], default_orientation: str = "input") -> "Port":
position = data.get("position", {})
rows, columns = _dimensions(data.get("dimensions", {}), "Port")
return cls(
id=str(data["id"]),
name=str(data.get("name", data["id"])),
x=float(position.get("x", 0.0)),
y=float(position.get("y", 0.0)),
properties=dict(data.get("properties", {})),
type=str(data.get("type", "signal")),
allows_multiple_connections=bool(data.get("multipleConnections", False)),
value_type=str(data.get("valueType", "real")),
quantity=str(data.get("quantity", "")),
unit=str(data.get("unit", "")),
rows=rows,
columns=columns,
description=str(data.get("description", "")),
orientation=str(data.get("orientation", default_orientation)),
domain=str(data.get("domain", "power")),
causality=str(data.get("causality", "indifferent")),
)
@dataclass
class Icon:
shape: str = "rectangle"
fill: str = "#f4f4f4"
border: str = "#303030"
text: str = ""
width: float = 128.0
height: float = 128.0
elements: list[dict[str, Any]] = field(default_factory=list)
def __post_init__(self) -> None:
if not self.elements:
self.elements = [
{
"type": "ellipse" if self.shape == "ellipse" else "rectangle",
"x": 32.0,
"y": 32.0,
"width": 64.0,
"height": 64.0,
"fill": self.fill,
"stroke": self.border,
"lineWidth": 1.5,
"lineStyle": "solid",
"cornerRadius": 5.0,
}
]
if self.text:
self.elements.append(
{
"type": "text",
"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]:
return {
"shape": self.shape,
"fill": self.fill,
"border": self.border,
"text": self.text,
"size": {"width": self.width, "height": self.height},
"elements": deepcopy(self.elements),
}
@classmethod
def from_dict(cls, data: dict[str, Any] | None) -> "Icon":
data = data or {}
icon = cls(
shape=str(data.get("shape", "rectangle")),
fill=str(data.get("fill", "#f4f4f4")),
border=str(data.get("border", "#303030")),
text=str(data.get("text", "")),
width=128.0,
height=128.0,
elements=deepcopy(data.get("elements", [])),
)
return icon
@dataclass(frozen=True)
class Endpoint:
block: str | None = None
port: str | None = None
interface: str | None = None
junction: str | None = None
def to_dict(self) -> dict[str, str]:
if self.interface is not None:
return {"interface": self.interface}
if self.junction is not None:
return {"junction": self.junction}
if self.block is None or self.port is None:
raise ValueError("A block endpoint requires both block and port")
return {"block": self.block, "port": self.port}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "Endpoint":
if "interface" in data:
return cls(interface=str(data["interface"]))
if "junction" in data:
return cls(junction=str(data["junction"]))
return cls(block=str(data["block"]), port=str(data["port"]))
@dataclass
class Connection:
id: str
source: Endpoint
target: Endpoint
name: str = ""
properties: dict[str, Any] = field(default_factory=dict)
type: str = "signal"
causality: str = "none"
def to_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"source": self.source.to_dict(),
"target": self.target.to_dict(),
"name": self.name,
"properties": self.properties,
"type": self.type,
"causality": self.causality,
}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "Connection":
return cls(
id=str(data["id"]),
source=Endpoint.from_dict(data["source"]),
target=Endpoint.from_dict(data["target"]),
name=str(data.get("name", "")),
properties=dict(data.get("properties", {})),
type=str(data.get("type", "signal")),
causality=str(data.get("causality", "none")),
)
@dataclass
class Parameter:
id: str
name: str
type: str = "real"
value: str = "0"
quantity: str = ""
unit: str = ""
rows: int = 1
columns: int = 1
description: str = ""
def to_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"name": self.name,
"type": self.type,
"value": self.value,
"quantity": self.quantity,
"unit": self.unit,
"dimensions": {"rows": self.rows, "columns": self.columns},
"description": self.description,
}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "Parameter":
if not isinstance(data, dict):
raise ValueError("Each component parameter must be an object")
if "id" not in data:
raise ValueError("Each component parameter must have an ID")
rows, columns = _dimensions(data.get("dimensions", {}), "Parameter")
return cls(
id=str(data["id"]),
name=str(data.get("name", "")),
type=str(data.get("type", "real")),
value=str(data.get("value", "0")),
quantity=str(data.get("quantity", "")),
unit=str(data.get("unit", "")),
rows=rows,
columns=columns,
description=str(data.get("description", "")),
)
@dataclass
class Junction:
id: str
x: float
y: float
type: str = "signal"
def to_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"position": {"x": self.x, "y": self.y},
"type": self.type,
}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "Junction":
position = data.get("position", {})
return cls(
id=str(data["id"]),
x=float(position.get("x", 0)),
y=float(position.get("y", 0)),
type=str(data.get("type", "signal")),
)
@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
class Graph:
blocks: dict[str, Component] = field(default_factory=dict)
connections: dict[str, Connection] = field(default_factory=dict)
annotations: dict[str, Annotation] = field(default_factory=dict)
junctions: dict[str, Junction] = field(default_factory=dict)
simulation_settings: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return {
"blocks": [block.to_dict() for block in self.blocks.values()],
"connections": [connection.to_dict() for connection in self.connections.values()],
"annotations": [item.to_dict() for item in self.annotations.values()],
"junctions": [junction.to_dict() for junction in self.junctions.values()],
"simulation": deepcopy(self.simulation_settings),
}
@classmethod
def from_dict(cls, data: dict[str, Any] | None) -> "Graph":
data = data or {}
blocks = [Component.from_dict(item) for item in data.get("blocks", [])]
connections = [Connection.from_dict(item) for item in data.get("connections", [])]
annotations = [Annotation.from_dict(item) for item in data.get("annotations", [])]
junctions = [Junction.from_dict(item) for item in data.get("junctions", [])]
if len({block.id for block in blocks}) != len(blocks):
raise ValueError("A graph contains duplicate component IDs")
if len({connection.id for connection in connections}) != len(connections):
raise ValueError("A graph contains duplicate connection IDs")
if len({item.id for item in annotations}) != len(annotations):
raise ValueError("A graph contains duplicate annotation IDs")
if len({item.id for item in junctions}) != len(junctions):
raise ValueError("A graph contains duplicate junction 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(
blocks={block.id: block for block in blocks},
connections={connection.id: connection for connection in connections},
annotations={item.id: item for item in annotations},
junctions={junction.id: junction for junction in junctions},
simulation_settings=deepcopy(data.get("simulation", {})),
)
@dataclass
class Component:
id: str
name: str
x: float = 0.0
y: float = 0.0
rotation: float = 0.0
ports: list[Port] = field(default_factory=list)
parameters: list[Parameter] = field(default_factory=list)
icon: Icon = field(default_factory=Icon)
properties: dict[str, Any] = field(default_factory=dict)
implementation_kind: str = "graph"
graph: Graph = field(default_factory=Graph)
source: dict[str, Any] = field(default_factory=dict)
show_subtree_in_library: bool = True
def to_dict(self) -> dict[str, Any]:
implementation = {"kind": self.implementation_kind}
if self.implementation_kind == "text":
implementation["source"] = self.source
else:
implementation["graph"] = self.graph.to_dict()
return {
"id": self.id,
"name": self.name,
"position": {"x": self.x, "y": self.y},
"rotation": self.rotation,
"interface": {"ports": [port.to_dict() for port in self.ports]},
"parameters": [parameter.to_dict() for parameter in self.parameters],
"icon": self.icon.to_dict(),
"properties": self.properties,
"library": {"showSubtree": self.show_subtree_in_library},
"implementation": implementation,
}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "Component":
interface = data.get("interface", {})
position = data.get("position", {})
implementation = data["implementation"]
kind = str(implementation.get("kind", "graph"))
if kind not in {"graph", "text"}:
raise ValueError(f"Unknown component implementation kind: {kind}")
source: dict[str, Any] = {}
parameters = data.get("parameters", [])
if kind == "text":
raw_source = implementation.get("source", {})
equations = raw_source.get("equations", "")
declarations = raw_source.get("declarations", "")
initial_equations = raw_source.get("initialEquations", "")
parameters = data.get("parameters", raw_source.get("parameters", []))
if not isinstance(equations, str):
raise ValueError("Text component equations must be a string")
if not isinstance(declarations, str):
raise ValueError("Text component declarations must be a string")
if not isinstance(initial_equations, str):
raise ValueError("Text component initial equations must be a string")
source = {
"equations": equations,
"declarations": declarations,
"initialEquations": initial_equations,
}
if not isinstance(parameters, list):
raise ValueError("Component parameters must be a list")
return cls(
id=str(data["id"]),
name=str(data.get("name", "Unnamed")),
x=float(position.get("x", 0.0)),
y=float(position.get("y", 0.0)),
rotation=float(data.get("rotation", 0.0)),
ports=[Port.from_dict(item) for item in interface.get("ports", [])],
parameters=[Parameter.from_dict(item) for item in parameters],
icon=Icon.from_dict(data.get("icon")),
properties=dict(data.get("properties", {})),
show_subtree_in_library=bool(data.get("library", {}).get("showSubtree", True)),
implementation_kind=kind,
graph=Graph.from_dict(implementation.get("graph")) if kind == "graph" else Graph(),
source=source,
)
@dataclass
class GraphDocument:
roots: dict[str, Component] = field(default_factory=dict)
metadata: dict[str, Any] = field(default_factory=dict)
@classmethod
def empty(cls) -> "GraphDocument":
return cls(metadata={"name": "Current Document"})
def to_dict(self) -> dict[str, Any]:
return {
"format": "bedit-document",
"version": 1,
"metadata": self.metadata,
"roots": [root.to_dict() for root in self.roots.values()],
}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "GraphDocument":
if data.get("format") != "bedit-document":
raise ValueError("This is not a BEdit document")
if data.get("version") != 1:
raise ValueError(f"Unsupported BEdit document version: {data.get('version')}")
roots = [Component.from_dict(item) for item in data["roots"]]
if len({root.id for root in roots}) != len(roots):
raise ValueError("The document contains duplicate root IDs")
document = cls(
roots={root.id: root for root in roots},
metadata=dict(data.get("metadata", {})),
)
document.validate()
return document
def all_components(self):
def walk(component: Component):
yield component
if component.implementation_kind == "graph":
for child in component.graph.blocks.values():
yield from walk(child)
def all_roots():
for root in self.roots.values():
yield from walk(root)
return all_roots()
def find_component(self, component_id: str) -> Component | None:
return next(
(component for component in self.all_components() if component.id == component_id),
None,
)
def find_parent(self, component_id: str) -> Component | None:
for component in self.all_components():
if component_id in component.graph.blocks:
return component
return None
def validate(self) -> None:
seen: set[str] = set()
root_names = [component.name for component in self.roots.values()]
if len(set(root_names)) != len(root_names):
raise ValueError("Root component names must be unique")
for component in self.all_components():
if component.id in seen:
raise ValueError(f"Duplicate component ID: {component.id}")
seen.add(component.id)
parameter_ids = [parameter.id for parameter in component.parameters]
parameter_names = [parameter.name for parameter in component.parameters]
if len(set(parameter_ids)) != len(parameter_ids):
raise ValueError(f"Component {component.name!r} has duplicate parameter IDs")
if len(set(parameter_names)) != len(parameter_names):
raise ValueError(f"Component {component.name!r} has duplicate parameter names")
if any(not name.strip() for name in parameter_names):
raise ValueError(f"Component {component.name!r} has an unnamed parameter")
if any(not parameter.type.strip() for parameter in component.parameters):
raise ValueError(f"Component {component.name!r} has an untyped parameter")
if any(
parameter.rows < 1 or parameter.columns < 1
for parameter in component.parameters
):
raise ValueError(f"Component {component.name!r} has invalid parameter dimensions")
if component.implementation_kind == "text" and component.graph.blocks:
raise ValueError(f"Text component {component.name} cannot contain a graph")
self._validate_graph(component)
@staticmethod
def _validate_graph(owner: Component) -> None:
child_names = [component.name for component in owner.graph.blocks.values()]
if len(set(child_names)) != len(child_names):
raise ValueError(
f"Component names inside {owner.name!r} must be unique"
)
port_ids = {port.id for port in owner.ports}
if len(port_ids) != len(owner.ports):
raise ValueError(f"Component {owner.name} contains duplicate port IDs")
for port in owner.ports:
PortTypeRegistry.get(port.type)
if port.orientation not in {"input", "output", "indifferent"}:
raise ValueError(f"Port {port.name!r} has an invalid orientation")
if port.orientation == "indifferent" and port.type != "power":
raise ValueError("Only power ports may have indifferent orientation")
if port.type == "power":
if port.domain not in {domain.id for domain in POWER_DOMAINS}:
raise ValueError(f"Power port {port.name!r} has an unknown domain")
if port.causality not in POWER_CAUSALITIES:
raise ValueError(f"Power port {port.name!r} has invalid causality")
if (
port.causality in MULTI_CONNECTION_POWER_CAUSALITIES
and not port.allows_multiple_connections
):
raise ValueError(
f"Power port {port.name!r} requires multiple connections "
f"for causality {port.causality!r}"
)
if not port.value_type.strip():
raise ValueError(f"Port {port.name!r} has no value type")
if port.rows < 1 or port.columns < 1:
raise ValueError(f"Port {port.name!r} has invalid dimensions")
for junction in owner.graph.junctions.values():
PortTypeRegistry.get(junction.type)
if junction.type == "power":
raise ValueError("Power bond connections cannot contain junctions")
endpoint_counts: dict[tuple[str, str, str], int] = {}
for connection in owner.graph.connections.values():
if connection.causality not in {
"none",
"source",
"target",
"warn_source",
"warn_target",
}:
raise ValueError(f"Connection {connection.id} has invalid causality")
if connection.source.junction is not None:
junction = owner.graph.junctions.get(connection.source.junction)
if junction is None:
raise ValueError(
f"Connection {connection.id} uses an unknown source junction"
)
source_port = Port(junction.id, "Junction", type=junction.type)
elif connection.source.interface is not None:
source_ports = [
port
for port in owner.ports
if port.orientation in {"input", "indifferent"}
]
if connection.source.interface not in {port.id for port in source_ports}:
raise ValueError(f"Connection {connection.id} uses an unknown interface input")
source_port = next(
port for port in source_ports if port.id == connection.source.interface
)
else:
source = owner.graph.blocks.get(connection.source.block or "")
source_ports = (
[]
if source is None
else [
port
for port in source.ports
if port.orientation in {"output", "indifferent"}
]
)
if source is None or connection.source.port not in {p.id for p in source_ports}:
raise ValueError(f"Connection {connection.id} uses an unknown block output")
source_port = next(p for p in source_ports if p.id == connection.source.port)
if connection.target.junction is not None:
junction = owner.graph.junctions.get(connection.target.junction)
if junction is None:
raise ValueError(
f"Connection {connection.id} uses an unknown target junction"
)
target_port = Port(
junction.id,
"Junction",
type=junction.type,
allows_multiple_connections=False,
)
elif connection.target.interface is not None:
target_ports = [
port
for port in owner.ports
if port.orientation in {"output", "indifferent"}
]
if connection.target.interface not in {port.id for port in target_ports}:
raise ValueError(f"Connection {connection.id} uses an unknown interface output")
target_port = next(
port for port in target_ports if port.id == connection.target.interface
)
else:
target = owner.graph.blocks.get(connection.target.block or "")
target_ports = (
[]
if target is None
else [
port
for port in target.ports
if port.orientation in {"input", "indifferent"}
]
)
if target is None or connection.target.port not in {
port.id for port in target_ports
}:
raise ValueError(f"Connection {connection.id} uses an unknown block input")
target_port = next(
port for port in target_ports if port.id == connection.target.port
)
if not PortTypeRegistry.compatible(
source_port.type,
target_port.type,
source_port.domain,
target_port.domain,
):
raise ValueError(f"Connection {connection.id} joins incompatible port types")
# The endpoint ports remain authoritative. This also upgrades older
# documents whose connections predate the explicit type field.
connection.type = source_port.type
source_key = (
"source-junction"
if connection.source.junction is not None
else "source-interface"
if connection.source.interface is not None
else "source-block",
connection.source.block or "",
connection.source.junction
or connection.source.interface
or connection.source.port
or "",
)
target_key = (
"target-junction"
if connection.target.junction is not None
else "target-interface"
if connection.target.interface is not None
else "target-block",
connection.target.block or "",
connection.target.junction
or connection.target.interface
or connection.target.port
or "",
)
endpoint_counts[source_key] = endpoint_counts.get(source_key, 0) + 1
endpoint_counts[target_key] = endpoint_counts.get(target_key, 0) + 1
if (
endpoint_counts[target_key] > 1
and not target_port.allows_multiple_connections
):
raise ValueError(
f"Input {target_port.name!r} has multiple incoming connections "
"but does not allow them"
)
def clone_component(source: Component) -> Component:
"""Deep-copy a component tree and remap every owned object ID."""
def clone(current: Component) -> Component:
child_pairs = [(child, clone(child)) for child in current.graph.blocks.values()]
child_ids = {old.id: new.id for old, new in child_pairs}
junction_ids = {
junction.id: str(uuid4()) for junction in current.graph.junctions.values()
}
def remap(endpoint: Endpoint) -> Endpoint:
if endpoint.interface is not None:
return endpoint
if endpoint.junction is not None:
return Endpoint(junction=junction_ids[endpoint.junction])
return Endpoint(block=child_ids[endpoint.block or ""], port=endpoint.port)
graph = Graph(
blocks={new.id: new for _old, new in child_pairs},
connections={
new_id: Connection(
new_id,
remap(connection.source),
remap(connection.target),
connection.name,
deepcopy(connection.properties),
connection.type,
connection.causality,
)
for connection in current.graph.connections.values()
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())]
},
junctions={
junction_ids[junction.id]: Junction(
junction_ids[junction.id], junction.x, junction.y, junction.type
)
for junction in current.graph.junctions.values()
},
simulation_settings=deepcopy(current.graph.simulation_settings),
)
return Component(
id=str(uuid4()),
name=current.name,
x=current.x,
y=current.y,
ports=[Port.from_dict(port.to_dict()) for port in current.ports],
parameters=deepcopy(current.parameters),
icon=Icon.from_dict(current.icon.to_dict()),
properties=deepcopy(current.properties),
implementation_kind=current.implementation_kind,
graph=graph if current.implementation_kind == "graph" else Graph(),
source=deepcopy(current.source),
show_subtree_in_library=current.show_subtree_in_library,
)
return clone(source)

View File

@@ -0,0 +1,47 @@
"""Editable suggestions for signal and parameter physical metadata.
These values populate editable combo boxes; documents may use values not listed
here. They are suggestions rather than a validation registry.
"""
VALUE_TYPES = ("real", "integer", "boolean", "string")
QUANTITIES = (
"",
"Angle",
"AngularVelocity",
"Current",
"Energy",
"Force",
"Frequency",
"Length",
"Mass",
"Power",
"Pressure",
"Temperature",
"Time",
"Torque",
"Velocity",
"Voltage",
)
UNITS = (
"",
"1",
"A",
"Hz",
"J",
"K",
"N",
"N.m",
"Pa",
"V",
"W",
"deg",
"kg",
"m",
"m/s",
"rad",
"rad/s",
"s",
)

View File

@@ -0,0 +1,41 @@
from dataclasses import dataclass
@dataclass(frozen=True)
class PortType:
id: str
display_name: str
description: str = ""
def accepts(self, other: "PortType") -> bool:
return self.id == other.id
class PortTypeRegistry:
_types = {
"signal": PortType("signal", "Signal", "A scalar signal connection"),
"power": PortType("power", "Power", "A two-variable power connection"),
}
@classmethod
def all(cls) -> tuple[PortType, ...]:
return tuple(cls._types.values())
@classmethod
def get(cls, type_id: str) -> PortType:
try:
return cls._types[type_id]
except KeyError as error:
raise ValueError(f"Unknown port type: {type_id}") from error
@classmethod
def compatible(
cls,
first: str,
second: str,
first_domain: str = "",
second_domain: str = "",
) -> bool:
if not cls.get(first).accepts(cls.get(second)):
return False
return first != "power" or first_domain == second_domain

View File

@@ -0,0 +1,49 @@
"""Editable power-port domain definitions used by the port editor."""
from dataclasses import dataclass
@dataclass(frozen=True)
class PowerDomain:
id: str
display_name: str
effort: str
flow: str
POWER_DOMAINS: tuple[PowerDomain, ...] = (
PowerDomain("power", "Power", "p.e", "p.f"),
)
def power_domain(domain_id: str) -> PowerDomain:
return next((domain for domain in POWER_DOMAINS if domain.id == domain_id), POWER_DOMAINS[0])
STANDARD_POWER_CAUSALITIES = (
"fixed flow out",
"fixed effort out",
"preferred flow out",
"preferred effort out",
"likes flow out",
"likes effort out",
"indifferent",
)
MULTI_CONNECTION_POWER_CAUSALITIES = (
"single effort in",
"single flow in",
)
POWER_CAUSALITIES = (
*STANDARD_POWER_CAUSALITIES,
*MULTI_CONNECTION_POWER_CAUSALITIES,
)
def power_causalities(allows_multiple_connections: bool = False) -> tuple[str, ...]:
"""Return the causalities available for one power port configuration."""
if allows_multiple_connections:
return POWER_CAUSALITIES
return STANDARD_POWER_CAUSALITIES

View File

@@ -0,0 +1,76 @@
import json
import zlib
from pathlib import Path
import msgpack
from bedit.core.model import GraphDocument
class JsonDocumentSerializer:
@staticmethod
def load(path: Path) -> GraphDocument:
with path.open(encoding="utf-8") as file:
data = json.load(file)
if not isinstance(data, dict):
raise ValueError("The graph file must contain a JSON object")
return GraphDocument.from_dict(data)
@staticmethod
def save(document: GraphDocument, path: Path) -> None:
temporary_path = path.with_suffix(path.suffix + ".tmp")
with temporary_path.open("w", encoding="utf-8") as file:
json.dump(document.to_dict(), file, indent=2)
file.write("\n")
temporary_path.replace(path)
class BebDocumentSerializer:
"""Compressed MessagePack serializer for the binary .beb format."""
MAGIC = b"BEB\x00"
VERSION = 1
@classmethod
def load(cls, path: Path) -> GraphDocument:
payload = path.read_bytes()
header = cls.MAGIC + bytes([cls.VERSION])
if not payload.startswith(header):
raise ValueError("This is not a supported BEdit binary document")
try:
data = msgpack.unpackb(zlib.decompress(payload[len(header) :]), raw=False)
except (ValueError, zlib.error, msgpack.exceptions.MsgpackException) as error:
raise ValueError("The BEdit binary document is damaged") from error
if not isinstance(data, dict):
raise ValueError("The BEdit binary document has an invalid root value")
return GraphDocument.from_dict(data)
@classmethod
def save(cls, document: GraphDocument, path: Path) -> None:
packed = msgpack.packb(document.to_dict(), use_bin_type=True)
payload = cls.MAGIC + bytes([cls.VERSION]) + zlib.compress(packed, level=9)
temporary_path = path.with_suffix(path.suffix + ".tmp")
temporary_path.write_bytes(payload)
temporary_path.replace(path)
class DocumentSerializer:
"""Select the document serializer from its file extension."""
@staticmethod
def load(path: Path) -> GraphDocument:
serializer = (
BebDocumentSerializer
if path.suffix.lower() == ".beb"
else JsonDocumentSerializer
)
return serializer.load(path)
@staticmethod
def save(document: GraphDocument, path: Path) -> None:
serializer = (
BebDocumentSerializer
if path.suffix.lower() == ".beb"
else JsonDocumentSerializer
)
serializer.save(document, path)

View File

@@ -0,0 +1,24 @@
from bedit.core.simulation.service import Simulation
from bedit.core.simulation.openmodelica import ModelBuildResult, OpenModelicaInterface
from bedit.core.simulation.results import (
BerSimulationResultsSerializer,
JsonSimulationResultsSerializer,
SimulationExecutionResult,
SimulationGraph,
SimulationResults,
SimulationResultsSerializer,
SimulationTrace,
)
__all__ = [
"OpenModelicaInterface",
"ModelBuildResult",
"Simulation",
"SimulationResults",
"SimulationExecutionResult",
"SimulationGraph",
"SimulationResultsSerializer",
"SimulationTrace",
"BerSimulationResultsSerializer",
"JsonSimulationResultsSerializer",
]

View File

@@ -0,0 +1,452 @@
import re
from copy import deepcopy
from dataclasses import dataclass
from typing import Any
_MODELICA_TYPES = {
"real": "Real",
"integer": "Integer",
"boolean": "Boolean",
"string": "String",
}
_BEVALUE_PATTERN = re.compile(r"\$([A-Za-z_][A-Za-z0-9_]*)\$")
@dataclass(frozen=True)
class CompositionResult:
"""The intermediate data and generated source produced by composition."""
graph: dict[str, Any]
objects_by_id: dict[str, Any]
modelica: str
model_name: str
def compose_graph(graph: dict[str, Any]) -> CompositionResult:
"""Clean, index, and compose a serialized component tree."""
cleaned_graph = cleanup_graph(deepcopy(graph))
objects_by_id = build_id_list(cleaned_graph)
return CompositionResult(
graph=cleaned_graph,
objects_by_id=objects_by_id,
modelica=emit_model(cleaned_graph, objects_by_id),
model_name=model_name_for(cleaned_graph),
)
def build_id_list(graph: dict[str, Any]) -> dict[str, Any]:
"""Index all addressable objects in a component tree by their stable ID."""
id_list: dict[str, Any] = {}
id_kinds: dict[str, str] = {}
def add(item: dict[str, Any], description: str) -> None:
item_id = item.get("id")
if not item_id:
raise ValueError(f"{description} has no ID")
# Port IDs identify a port on a component definition and may therefore
# recur in cloned component instances. Component and junction IDs are
# document objects and must remain globally unique.
if item_id in id_list and not (
description == "port" and id_kinds[item_id] == "port"
):
raise ValueError(f"Duplicate simulation object ID: {item_id}")
id_list[item_id] = item
id_kinds[item_id] = description
def visit(component: dict[str, Any]) -> None:
add(component, "component")
interface = component.get("interface", {})
for port in interface.get("ports", []):
add(port, "port")
implementation = component.get("implementation", {})
if implementation.get("kind") != "graph":
return
nested_graph = implementation.get("graph", {})
for junction in nested_graph.get("junctions", []):
add(junction, "junction")
for block in nested_graph.get("blocks", []):
visit(block)
visit(graph)
return id_list
def emit_model(
graph: dict[str, Any],
id_list: dict[str, Any],
indent: int = 0,
connection_counts: dict[str, int] | None = None,
connection_signs: dict[str, list[float]] | None = None,
) -> str:
"""Emit a component and its nested definitions as Modelica source."""
del id_list # Kept in the public API for composer extensions and inspection.
indentation = "\t" * indent
body_indent = "\t" * (indent + 1)
model_name = model_name_for(graph)
lines = [f"{indentation}model {model_name}"]
implementation = graph.get("implementation", {})
implementation_kind = implementation.get("kind")
nested_graph = implementation.get("graph", {})
port_counts = connection_counts or _interface_connection_counts(graph)
port_signs = connection_signs or _interface_connection_signs(graph)
macros = _port_connection_macros(graph, port_counts, port_signs)
if indent == 0 and _contains_power_port(graph):
lines.extend(
(
f"{body_indent}connector BondPort \"Bond graph power port\"",
f"{body_indent}\tReal e \"Effort variable\";",
f"{body_indent}\tflow Real f \"Flow variable\";",
f"{body_indent}end BondPort;",
)
)
if implementation_kind == "graph":
for block in nested_graph.get("blocks", []):
block_counts, block_signs = _block_connection_data(
nested_graph, block["id"]
)
lines.extend(
emit_model(
block,
{},
indent + 1,
block_counts,
block_signs,
)
.rstrip()
.splitlines()
)
interface = graph.get("interface", {})
for port in interface.get("ports", []):
direction = "output" if port.get("orientation") == "output" else "input"
lines.append(_port_declaration(port, direction, indent + 1, macros))
for parameter in graph.get("parameters", []):
parameter_type = modelica_type(parameter.get("type", "real"))
parameter_name = identifier(parameter["name"])
value = expand_bevalues(str(parameter.get("value", "0")), macros)
dimensions = _fixed_dimensions(parameter)
attributes = _quantity_unit_attributes(parameter)
lines.append(
f"{body_indent}parameter {parameter_type} {parameter_name}"
f"{dimensions}{attributes} = {value};"
)
if implementation_kind == "text":
declarations = str(implementation.get("source", {}).get("declarations", ""))
declarations = expand_bevalues(declarations, macros)
lines.extend(
f"{body_indent}{line}" if line.strip() else ""
for line in declarations.splitlines()
)
if implementation_kind == "graph":
for block in nested_graph.get("blocks", []):
block_type = model_name_for(block)
block_name = identifier(block["name"])
lines.append(f"{body_indent}{block_type} {block_name};")
for junction in nested_graph.get("junctions", []):
junction_type = modelica_type(junction.get("type", "signal"))
lines.append(
f"{body_indent}{junction_type} {_junction_name(junction['id'])};"
)
if implementation_kind == "graph":
lines.append(f"{indentation}equation")
blocks = {block["id"]: block for block in nested_graph.get("blocks", [])}
junctions = {
junction["id"]: junction for junction in nested_graph.get("junctions", [])
}
endpoint_indices: dict[tuple[str, str, str], int] = {}
for connection in nested_graph.get("connections", []):
source = _endpoint_expression(
connection["source"], graph, blocks, junctions, endpoint_indices
)
target = _endpoint_expression(
connection["target"], graph, blocks, junctions, endpoint_indices
)
if connection.get("type") == "power":
lines.append(f"{body_indent}connect({source}, {target});")
else:
lines.append(f"{body_indent}{target} = {source};")
else:
initial_equations = str(
implementation.get("source", {}).get("initialEquations", "")
)
initial_equations = expand_bevalues(initial_equations, macros)
equations = str(implementation.get("source", {}).get("equations", ""))
equations = expand_bevalues(equations, macros)
if initial_equations.strip():
lines.append(f"{indentation}initial equation")
lines.extend(
f"{body_indent}{line}" if line.strip() else ""
for line in initial_equations.splitlines()
)
lines.append(f"{indentation}equation")
lines.extend(
f"{body_indent}{line}" if line.strip() else ""
for line in equations.splitlines()
)
lines.append(f"{indentation}end {model_name};")
return "\n".join(lines) + "\n"
def cleanup_graph(graph: dict[str, Any]) -> dict[str, Any]:
"""Remove annotations and UI-only data from a serialized component tree."""
def remove_key_with_lists(data: Any, target_key: str) -> None:
if isinstance(data, dict):
for key in list(data):
if key == target_key:
del data[key]
else:
remove_key_with_lists(data[key], target_key)
elif isinstance(data, list):
for item in data:
remove_key_with_lists(item, target_key)
for key in (
"position",
"rotation",
"iconPosition",
"icon",
"annotations",
"library",
"properties",
):
remove_key_with_lists(graph, key)
return graph
def _port_declaration(
port: dict[str, Any], direction: str, indent: int, macros: dict[str, str]
) -> str:
indentation = "\t" * indent
port_name = identifier(port["name"])
dimensions = _port_dimensions(port, port_name)
if port.get("type") == "power":
return expand_bevalues(
f"{indentation}BondPort {port_name}{dimensions};", macros
)
port_type = modelica_type(port.get("valueType", "real"))
attributes = _quantity_unit_attributes(port)
declaration = (
f"{indentation}{direction} {port_type} {port_name}{dimensions}{attributes};"
)
return expand_bevalues(declaration, macros)
def _fixed_dimensions(item: dict[str, Any]) -> str:
dimensions = item.get("dimensions", {})
rows = max(1, int(dimensions.get("rows", 1)))
columns = max(1, int(dimensions.get("columns", 1)))
if rows == columns == 1:
return ""
if columns == 1:
return f"[{rows}]"
return f"[{rows},{columns}]"
def _port_dimensions(port: dict[str, Any], port_name: str) -> str:
fixed = _fixed_dimensions(port)
if not port.get("multipleConnections", False):
return fixed
entries = [f"${port_name}_N$"]
if fixed:
entries.extend(fixed[1:-1].split(","))
return f"[{','.join(entries)}]"
def _quantity_unit_attributes(item: dict[str, Any]) -> str:
attributes = []
for key in ("quantity", "unit"):
value = str(item.get(key, "")).strip()
if value:
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
attributes.append(f'{key}="{escaped}"')
return f"({', '.join(attributes)})" if attributes else ""
def _endpoint_expression(
endpoint: dict[str, Any],
owner: dict[str, Any],
blocks: dict[str, dict[str, Any]],
junctions: dict[str, dict[str, Any]],
endpoint_indices: dict[tuple[str, str, str], int],
) -> str:
if "junction" in endpoint:
junction_id = endpoint["junction"]
if junction_id not in junctions:
raise ValueError(f"Connection references unknown junction {junction_id!r}")
return _junction_name(junction_id)
if "interface" in endpoint:
port = _find_port(owner, endpoint["interface"])
expression = identifier(port["name"])
return _index_array_endpoint(
expression, port, ("interface", owner["id"], port["id"]), endpoint_indices
)
block_id = endpoint.get("block")
port_id = endpoint.get("port")
block = blocks.get(block_id)
if block is None:
raise ValueError(f"Connection references unknown block {block_id!r}")
port = _find_port(block, port_id)
expression = f"{identifier(block['name'])}.{identifier(port['name'])}"
return _index_array_endpoint(
expression, port, ("block", block_id, port_id), endpoint_indices
)
def _index_array_endpoint(
expression: str,
port: dict[str, Any],
key: tuple[str, str, str],
endpoint_indices: dict[tuple[str, str, str], int],
) -> str:
if not port.get("multipleConnections", False):
return expression
endpoint_indices[key] = endpoint_indices.get(key, 0) + 1
return f"{expression}[{endpoint_indices[key]}]"
def _block_connection_data(
graph: dict[str, Any], block_id: str
) -> tuple[dict[str, int], dict[str, list[float]]]:
counts: dict[str, int] = {}
signs: dict[str, list[float]] = {}
for connection in graph.get("connections", []):
for endpoint, sign in (
(connection.get("source", {}), -1.0),
(connection.get("target", {}), 1.0),
):
if endpoint.get("block") == block_id and endpoint.get("port"):
port_id = endpoint["port"]
counts[port_id] = counts.get(port_id, 0) + 1
signs.setdefault(port_id, []).append(sign)
return counts, signs
def _interface_connection_counts(component: dict[str, Any]) -> dict[str, int]:
implementation = component.get("implementation", {})
if implementation.get("kind") != "graph":
return {}
counts: dict[str, int] = {}
for connection in implementation.get("graph", {}).get("connections", []):
for endpoint in (connection.get("source", {}), connection.get("target", {})):
if endpoint.get("interface"):
port_id = endpoint["interface"]
counts[port_id] = counts.get(port_id, 0) + 1
return counts
def _interface_connection_signs(component: dict[str, Any]) -> dict[str, list[float]]:
implementation = component.get("implementation", {})
if implementation.get("kind") != "graph":
return {}
signs: dict[str, list[float]] = {}
for connection in implementation.get("graph", {}).get("connections", []):
for endpoint, sign in (
(connection.get("source", {}), -1.0),
(connection.get("target", {}), 1.0),
):
port_id = endpoint.get("interface")
if port_id:
signs.setdefault(port_id, []).append(sign)
return signs
def _port_connection_macros(
component: dict[str, Any],
connection_counts: dict[str, int],
connection_signs: dict[str, list[float]],
) -> dict[str, str]:
macros: dict[str, str] = {}
interface = component.get("interface", {})
for port in interface.get("ports", []):
if port.get("multipleConnections", False):
macro_name = identifier(port["name"])
port_id = port["id"]
macros[f"{macro_name}_N"] = str(connection_counts.get(port_id, 0))
signs = connection_signs.get(port_id, [])
macros[f"{macro_name}_S"] = "{" + ", ".join(
f"{sign:.1f}" for sign in signs
) + "}"
return macros
def _contains_power_port(component: dict[str, Any]) -> bool:
if any(
port.get("type") == "power"
for port in component.get("interface", {}).get("ports", [])
):
return True
implementation = component.get("implementation", {})
if implementation.get("kind") != "graph":
return False
return any(
_contains_power_port(block)
for block in implementation.get("graph", {}).get("blocks", [])
)
def expand_bevalues(text: str, values: dict[str, str]) -> str:
"""Replace BEdit ``$name$`` macros and reject unresolved composer values."""
def replace(match: re.Match[str]) -> str:
name = match.group(1)
if name not in values:
raise ValueError(f"Unknown BEdit value ${name}$")
return values[name]
return _BEVALUE_PATTERN.sub(replace, text)
def _find_port(component: dict[str, Any], port_id: str) -> dict[str, Any]:
interface = component.get("interface", {})
for port in interface.get("ports", []):
if port.get("id") == port_id:
return port
raise ValueError(
f"Component {component.get('name', component.get('id', '?'))!r} "
f"has no port {port_id!r}"
)
def modelica_type(value: str) -> str:
normalized = str(value).strip().lower()
if normalized in {"signal", "signal array"}:
return "Real"
return _MODELICA_TYPES.get(normalized, identifier(str(value)))
def identifier(value: str) -> str:
"""Return a safe unquoted Modelica identifier."""
normalized = re.sub(r"[^A-Za-z0-9_]", "_", str(value).strip())
if not normalized:
raise ValueError("Modelica names cannot be empty")
if normalized[0].isdigit():
normalized = f"model_{normalized}"
return normalized
def model_name_for(component: dict[str, Any]) -> str:
"""Return the generated Modelica type name for a component."""
return identifier(f"m_{component['name']}")
def _junction_name(junction_id: str) -> str:
return identifier(f"junction_{junction_id}")

View File

@@ -0,0 +1,639 @@
import json
import logging
import os
import re
import shlex
import shutil
import socket
import tempfile
import time
import xml.etree.ElementTree as ET
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
from queue import Queue
from threading import Event, Lock, Thread, current_thread
from typing import Any
from bedit.core.application_log import get_logger
from bedit.core.simulation.results import (
SimulationExecutionResult,
load_openmodelica_csv,
)
log = get_logger(__name__)
ResultCallback = Callable[[Any], None]
ErrorCallback = Callable[[Exception], None]
@dataclass(frozen=True)
class _Request:
description: str
operation: Callable[[Any, Path], Any]
callback: ResultCallback | None
error_callback: ErrorCallback | None
@dataclass(frozen=True)
class SimulationProgress:
phase: str
current_step_size: float
time: float
progress: int
@dataclass(frozen=True)
class SimulationMessage:
stream: str
type: str
text: str
@dataclass(frozen=True)
class ModelBuildResult:
"""OMC's model check summary and subsequent build response."""
check_summary: str
build_result: Any
diagnostics: str = ""
class OpenModelicaInterface:
"""Asynchronous, persistent interface to one OpenModelica session.
The worker and OMC session are created lazily for the first request. Callbacks
execute on background threads and must not manipulate Qt widgets directly.
"""
def __init__(self, executable_path: str = "") -> None:
self._executable_path = executable_path
self._lifecycle_lock = Lock()
self._queue: Queue[_Request | None] | None = None
self._worker: Thread | None = None
self._temp_dir: Path | None = None
@property
def executable_path(self) -> str:
return self._executable_path
def configure(self, executable_path: str) -> None:
"""Use a new executable path for subsequent requests."""
if executable_path == self._executable_path:
return
self.shutdown(wait=True)
self._executable_path = executable_path
def get_version(
self,
callback: ResultCallback | None = None,
error_callback: ErrorCallback | None = None,
) -> None:
"""Request the OpenModelica version without blocking the caller."""
self.send_expression("getVersion()", callback, error_callback)
def build_model(
self,
model: str,
model_name: str,
callback: ResultCallback | None = None,
error_callback: ErrorCallback | None = None,
) -> None:
"""Load and build one composed model as an ordered worker operation."""
def operation(omc, _temp_dir: Path):
capture = _LogCapture()
ompython_logger = logging.getLogger("OMPython")
previous_propagate = ompython_logger.propagate
ompython_logger.addHandler(capture)
ompython_logger.propagate = False
try:
unit_checking = omc.sendExpression(
'setCommandLineOptions("--unitChecking")'
)
if unit_checking is not True:
raise RuntimeError("OpenModelica could not enable unit checking")
loaded = omc.sendExpression(f"loadString({json.dumps(model)})")
if loaded is not True:
raise RuntimeError("OpenModelica could not load the composed model")
diagnostics = [_read_omc_diagnostics(omc)]
check_summary = omc.sendExpression(f"checkModel({model_name})")
diagnostics.append(_read_omc_diagnostics(omc))
if not isinstance(check_summary, str) or not check_summary.strip():
raise RuntimeError(
"OpenModelica did not return a model-check summary"
)
raw_build_result = omc.sendExpression(
f"buildModel({model_name})", parsed=False
)
build_result = _parse_modelica_string_array(raw_build_result)
diagnostics.append(_read_omc_diagnostics(omc))
diagnostics.extend(capture.messages)
diagnostic_text = "\n".join(
item.strip()
for item in diagnostics
if isinstance(item, str) and item.strip()
)
return ModelBuildResult(check_summary, build_result, diagnostic_text)
finally:
ompython_logger.removeHandler(capture)
ompython_logger.propagate = previous_propagate
self._submit("build model", operation, callback, error_callback)
def run_model(
self,
executable: str,
arguments: list[str],
progress_callback: Callable[[SimulationProgress], None] | None = None,
message_callback: Callable[[SimulationMessage], None] | None = None,
callback: ResultCallback | None = None,
error_callback: ErrorCallback | None = None,
) -> None:
"""Run a built model and consume its XML/TCP status stream."""
def operation(omc, temp_dir: Path):
return _run_model_with_tcp(
omc,
executable,
arguments,
temp_dir,
progress_callback,
message_callback,
)
self._submit("run simulation", operation, callback, error_callback)
def send_expression(
self,
expression: str,
callback: ResultCallback | None = None,
error_callback: ErrorCallback | None = None,
*,
parsed: bool = True,
) -> None:
"""Queue an OMC expression for ordered execution on the worker thread."""
def operation(omc, _temp_dir: Path):
return omc.sendExpression(expression, parsed=parsed)
self._submit(expression, operation, callback, error_callback)
def _submit(
self,
description: str,
operation: Callable[[Any, Path], Any],
callback: ResultCallback | None,
error_callback: ErrorCallback | None,
) -> None:
request = _Request(description, operation, callback, error_callback)
self._ensure_worker().put(request)
def shutdown(self, *, wait: bool = True) -> None:
"""Ask the worker to close its OMC session after queued requests."""
with self._lifecycle_lock:
worker = self._worker
queue = self._queue
self._worker = None
self._queue = None
if queue is not None:
queue.put(None)
if wait and worker is not None and worker is not current_thread():
worker.join()
if worker is None:
self._cleanup_temp_dir()
def __del__(self) -> None:
"""Best-effort fallback; normal application shutdown is explicit."""
try:
self.shutdown(wait=False)
except Exception:
pass
def _ensure_worker(self) -> Queue[_Request | None]:
with self._lifecycle_lock:
if self._worker is not None and self._worker.is_alive():
return self._queue
temp_dir = self._ensure_temp_dir_locked()
queue: Queue[_Request | None] = Queue()
worker = Thread(
target=self._worker_main,
args=(queue, self._executable_path, temp_dir),
name="bedit-openmodelica",
daemon=True,
)
self._queue = queue
self._worker = worker
worker.start()
return queue
def _worker_main(
self,
queue: Queue[_Request | None],
executable_path: str,
temp_dir: Path,
) -> None:
omc = None
try:
while (request := queue.get()) is not None:
try:
if omc is None:
omc = _create_session(executable_path)
changed_directory = omc.sendExpression(
f"cd({json.dumps(str(temp_dir))})"
)
if not changed_directory:
raise RuntimeError(
f"OpenModelica could not use {str(temp_dir)!r}"
)
result = request.operation(omc, temp_dir)
except Exception as error:
if request.error_callback is None:
log.exception(
"OpenModelica request failed: %s", request.description
)
else:
_deliver_callback(request.error_callback, error)
else:
if request.callback is not None:
_deliver_callback(request.callback, result)
finally:
transport_files = _ompython_transport_files(omc)
if omc is not None:
try:
omc.sendExpression("quit()")
except Exception:
log.debug("Could not close OpenModelica session", exc_info=True)
for path in transport_files:
try:
path.unlink(missing_ok=True)
except OSError:
log.debug("Could not remove OMPython file %s", path, exc_info=True)
self._cleanup_temp_dir(temp_dir)
def _ensure_temp_dir_locked(self) -> Path:
if self._temp_dir is None:
self._temp_dir = Path(tempfile.mkdtemp(prefix="bedit-openmodelica-"))
return self._temp_dir
def _cleanup_temp_dir(self, expected: Path | None = None) -> None:
with self._lifecycle_lock:
if expected is not None and self._temp_dir != expected:
temp_dir = expected
else:
temp_dir = self._temp_dir
self._temp_dir = None
if temp_dir is not None:
shutil.rmtree(temp_dir, ignore_errors=True)
def _read_omc_diagnostics(omc) -> str:
"""Read OMC diagnostics without sending multiline text through OMPython's parser."""
raw = omc.sendExpression("getErrorString()", parsed=False)
if isinstance(raw, bytes):
raw = raw.decode("utf-8", errors="replace")
if not isinstance(raw, str):
return ""
text = raw.strip()
if not text:
return ""
try:
decoded = json.loads(text)
except json.JSONDecodeError:
return text
return decoded if isinstance(decoded, str) else text
class _LogCapture(logging.Handler):
"""Collect dependency log messages for forwarding through BEdit's logger."""
def __init__(self) -> None:
super().__init__(level=logging.WARNING)
self.messages: list[str] = []
def emit(self, record: logging.LogRecord) -> None:
self.messages.append(record.getMessage())
def _parse_modelica_string_array(raw: Any) -> tuple[str, ...]:
"""Parse the string array returned by ``buildModel`` in raw mode."""
if isinstance(raw, bytes):
raw = raw.decode("utf-8", errors="replace")
if not isinstance(raw, str):
raise RuntimeError(f"OpenModelica returned an invalid build result: {raw!r}")
values = tuple(
json.loads(token)
for token in re.findall(r'"(?:\\.|[^"\\])*"', raw)
)
if not values:
raise RuntimeError(f"OpenModelica returned an invalid build result: {raw!r}")
return values
def _deliver_callback(callback: Callable[[Any], None], value: Any) -> None:
try:
callback(value)
except Exception:
log.exception("OpenModelica callback failed")
def _create_session(executable_path: str):
from OMPython import OMCSessionZMQ
return OMCSessionZMQ(omhome=_openmodelica_home(executable_path))
def _run_model_with_tcp(
omc,
executable: str,
arguments: list[str],
temp_dir: Path,
progress_callback: Callable[[SimulationProgress], None] | None,
message_callback: Callable[[SimulationMessage], None] | None,
) -> SimulationExecutionResult:
executable_path = Path(executable)
executable_command = executable
if not executable_path.is_absolute() and executable_path.parent == Path("."):
executable_command = f"./{executable}"
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("127.0.0.1", 0))
server.listen(1)
server.settimeout(0.25)
port = server.getsockname()[1]
command = shlex.join([
executable_command,
*arguments,
f"-port={port}",
"-logFormat=xmltcp",
])
output_path = temp_dir / "simulation-output.txt"
command_finished = Event()
reader_finished = Event()
reader_errors: list[Exception] = []
def read_progress() -> None:
try:
connection = _accept_simulation_connection(server, command_finished)
with connection, connection.makefile(
"r", encoding="utf-8"
) as stream:
for line in stream:
_handle_simulation_xml(
line, progress_callback, message_callback
)
except Exception as error:
reader_errors.append(error)
finally:
reader_finished.set()
reader = Thread(
target=read_progress,
name="bedit-simulation-progress",
daemon=True,
)
reader.start()
log.info("Starting simulation through OpenModelica: %s", command)
try:
return_code = omc.sendExpression(
f"system({json.dumps(command)}, {json.dumps(str(output_path))})"
)
finally:
command_finished.set()
if not reader_finished.wait(20.0):
raise TimeoutError("Simulation progress connection did not close")
if reader_errors:
raise reader_errors[0]
if return_code != 0:
output = output_path.read_text(errors="replace") if output_path.exists() else ""
detail = f": {output.strip()}" if output.strip() else ""
raise RuntimeError(
f"Simulation process exited with status {return_code}{detail}"
)
result_path = temp_dir / f"{Path(executable).stem}_res.csv"
if not result_path.is_file():
raise RuntimeError(
f"OpenModelica did not create the expected result file {result_path.name!r}"
)
data = load_openmodelica_csv(result_path)
init_path = temp_dir / f"{Path(executable).stem}_init.xml"
csv_column_count = len(data)
data = _add_openmodelica_alias_signals(init_path, data)
signal_metadata = _load_openmodelica_signal_metadata(init_path, data)
log.info(
"Loaded %d result columns, restored %d OMC aliases, and found %d "
"unit definitions from %s",
csv_column_count,
len(data) - csv_column_count,
len(signal_metadata),
result_path.name,
)
return SimulationExecutionResult(
return_code=int(return_code),
result_file=str(result_path),
data=data,
signal_metadata=signal_metadata,
)
def _add_openmodelica_alias_signals(
path: Path, data: dict[str, list[float]]
) -> dict[str, list[float]]:
"""Restore result aliases exactly as described by OMC's init XML."""
try:
root = ET.parse(path).getroot()
except (OSError, ET.ParseError) as error:
raise RuntimeError(
f"Could not read OpenModelica aliases from {path.name}: {error}"
) from error
variables: dict[str, dict[str, str]] = {}
for scalar in root.findall(".//ScalarVariable"):
name = scalar.get("name", "")
if not name:
continue
value = next(iter(scalar), None)
variables[name] = {
"alias": scalar.get("alias", "noAlias"),
"target": scalar.get("aliasVariable", ""),
"variability": scalar.get("variability", ""),
"start": value.get("start", "") if value is not None else "",
}
restored = {name: list(values) for name, values in data.items()}
sample_count = len(next(iter(data.values()), []))
def resolve(name: str, visited: set[str] | None = None) -> list[float] | None:
if name in restored:
return restored[name]
variable = variables.get(name)
if variable is None:
return None
visited = set() if visited is None else visited
if name in visited:
return None
visited.add(name)
alias_kind = variable["alias"]
target = variable["target"]
if alias_kind in {"alias", "negatedAlias"} and target:
values = resolve(target, visited)
if values is None:
return None
return [-value for value in values] if alias_kind == "negatedAlias" else list(values)
if variable["variability"] not in {"parameter", "constant"}:
return None
try:
value = float(variable["start"])
except ValueError:
return None
return [value] * sample_count
for name, variable in variables.items():
if variable["alias"] not in {"alias", "negatedAlias"} or name in restored:
continue
values = resolve(name)
if values is not None:
restored[name] = values
return restored
def _load_openmodelica_signal_metadata(
path: Path, signal_names: dict[str, list[float]]
) -> dict[str, dict[str, str]]:
"""Read units reported by OMC in its generated initialization XML."""
try:
root = ET.parse(path).getroot()
except (OSError, ET.ParseError) as error:
raise RuntimeError(
f"Could not read OpenModelica unit metadata from {path.name}: {error}"
) from error
variables: dict[str, tuple[dict[str, str], str]] = {}
for scalar in root.findall(".//ScalarVariable"):
name = scalar.get("name", "")
if not name:
continue
value = next(iter(scalar), None)
metadata: dict[str, str] = {}
if value is not None:
for key in ("unit", "displayUnit", "quantity"):
attribute = value.get(key, "")
if attribute:
metadata[key] = attribute
description = scalar.get("description", "")
if description:
metadata["description"] = description
variables[name] = (metadata, scalar.get("aliasVariable", ""))
def resolve(name: str, visited: set[str] | None = None) -> dict[str, str]:
metadata, alias = variables.get(name, ({}, ""))
if metadata.get("unit") or not alias:
return dict(metadata)
visited = set() if visited is None else visited
if name in visited:
return dict(metadata)
visited.add(name)
inherited = resolve(alias, visited)
return {**inherited, **metadata}
metadata: dict[str, dict[str, str]] = {}
for name in signal_names:
if name not in variables:
continue
resolved = resolve(name)
if resolved:
metadata[name] = resolved
if "time" in signal_names:
metadata["time"] = {"unit": "s", "quantity": "Time"}
return metadata
def _accept_simulation_connection(
server: socket.socket, command_finished: Event
) -> socket.socket:
deadline = time.monotonic() + 15.0
while time.monotonic() < deadline:
try:
connection, _address = server.accept()
return connection
except socket.timeout:
if command_finished.is_set():
raise RuntimeError(
"Simulation command finished before opening its progress connection"
)
raise TimeoutError("Simulation did not connect to the progress server")
def _handle_simulation_xml(
line: str,
progress_callback: Callable[[SimulationProgress], None] | None,
message_callback: Callable[[SimulationMessage], None] | None,
) -> None:
text = line.strip()
if not text:
return
try:
element = ET.fromstring(text)
except ET.ParseError:
log.warning("Invalid simulation status XML: %s", text)
return
if element.tag == "status" and progress_callback is not None:
_deliver_callback(
progress_callback,
SimulationProgress(
phase=element.get("phase", ""),
current_step_size=float(element.get("currentStepSize", 0)),
time=float(element.get("time", 0)),
progress=int(float(element.get("progress", 0))),
),
)
elif element.tag == "message":
message = SimulationMessage(
stream=element.get("stream", ""),
type=element.get("type", ""),
text=element.get("text", ""),
)
log.info("OpenModelica %s: %s", message.stream, message.text)
if message_callback is not None:
_deliver_callback(message_callback, message)
def _ompython_transport_files(omc) -> set[Path]:
"""Return only the log and port files owned by this OMPython session."""
if omc is None:
return set()
process = getattr(omc, "omc_process", None)
if process is None:
return set()
files: set[Path] = set()
temp_dir = getattr(process, "_temp_dir", None)
file_base = getattr(process, "_omc_filebase", None)
if temp_dir is not None and file_base:
files.add(Path(temp_dir) / f"{file_base}.log")
try:
port_file = process._get_portfile_path()
except Exception:
port_file = None
if port_file is not None:
files.add(Path(port_file))
return files
def _openmodelica_home(executable_path: str) -> str | None:
"""Convert an optional omc executable path to the home expected by OMPython."""
if not executable_path.strip():
return None
path = Path(os.path.expandvars(executable_path)).expanduser()
if path.name.lower() in {"omc", "omc.exe"}:
return str(path.parent.parent)
return str(path)

View File

@@ -0,0 +1,237 @@
import csv
import json
import zlib
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any
from uuid import uuid4
import msgpack
RESULTS_FORMAT = "bedit-simulation-results"
RESULTS_VERSION = 1
@dataclass
class SimulationTrace:
"""One plottable series; samples can be filled by a future result importer."""
name: str
x_values: list[float] = field(default_factory=list)
y_values: list[float] = field(default_factory=list)
x_label: str = "time"
y_label: str = ""
unit: str = ""
properties: dict[str, Any] = field(default_factory=dict)
@dataclass
class SimulationGraph:
"""One graph workspace tab and its configured traces."""
id: str = field(default_factory=lambda: str(uuid4()))
title: str = "Graph 1"
x_axis: str = "time"
traces: list[SimulationTrace] = field(default_factory=list)
@dataclass
class SimulationResults:
"""Serializable state displayed by the standalone simulation window."""
model_name: str = ""
status: dict[str, Any] = field(default_factory=dict)
messages: list[dict[str, str]] = field(default_factory=list)
data: dict[str, list[float]] = field(default_factory=dict)
signal_metadata: dict[str, dict[str, str]] = field(default_factory=dict)
graphs: list[SimulationGraph] = field(
default_factory=lambda: [SimulationGraph()]
)
metadata: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return {
"format": RESULTS_FORMAT,
"version": RESULTS_VERSION,
"modelName": self.model_name,
"status": dict(self.status),
"messages": [dict(message) for message in self.messages],
"data": {name: list(values) for name, values in self.data.items()},
"signalMetadata": {
name: dict(metadata)
for name, metadata in self.signal_metadata.items()
},
"graphs": [asdict(graph) for graph in self.graphs],
"metadata": dict(self.metadata),
}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "SimulationResults":
if data.get("format") != RESULTS_FORMAT:
raise ValueError("Not a BEdit simulation-results file")
if data.get("version") != RESULTS_VERSION:
raise ValueError(f"Unsupported simulation-results version: {data.get('version')!r}")
try:
graphs = [
SimulationGraph(
id=str(graph["id"]),
title=str(graph["title"]),
x_axis=str(graph.get("x_axis", "time")),
traces=[
SimulationTrace(**trace) for trace in graph.get("traces", [])
],
)
for graph in data.get("graphs", [])
]
return cls(
model_name=str(data.get("modelName", "")),
status=dict(data.get("status", {})),
messages=[dict(message) for message in data.get("messages", [])],
data={
str(name): [float(value) for value in values]
for name, values in dict(data.get("data", {})).items()
},
signal_metadata={
str(name): {
str(key): str(value)
for key, value in dict(metadata).items()
}
for name, metadata in dict(
data.get("signalMetadata", {})
).items()
},
graphs=graphs,
metadata=dict(data.get("metadata", {})),
)
except (KeyError, TypeError, ValueError) as error:
raise ValueError("Malformed simulation-results data") from error
@dataclass(frozen=True)
class SimulationExecutionResult:
"""Completed process information delivered before its temp files disappear."""
return_code: int
result_file: str
data: dict[str, list[float]]
signal_metadata: dict[str, dict[str, str]] = field(default_factory=dict)
def load_openmodelica_csv(path: str | Path) -> dict[str, list[float]]:
"""Read an OpenModelica CSV result as one numeric array per column."""
source = Path(path)
try:
with source.open(newline="", encoding="utf-8") as file:
reader = csv.reader(file)
headers = next(reader)
if not headers or any(not header for header in headers):
raise ValueError("The result CSV has an invalid header")
if len(set(headers)) != len(headers):
raise ValueError("The result CSV contains duplicate column names")
columns = {header: [] for header in headers}
for row_number, row in enumerate(reader, start=2):
if len(row) != len(headers):
raise ValueError(
f"Result CSV row {row_number} has {len(row)} values; "
f"expected {len(headers)}"
)
for header, value in zip(headers, row, strict=True):
columns[header].append(float(value))
except OSError as error:
raise ValueError(f"Could not read OpenModelica results: {error}") from error
except StopIteration as error:
raise ValueError("The OpenModelica result CSV is empty") from error
except ValueError as error:
if str(error).startswith(("The result CSV", "Result CSV")):
raise
raise ValueError(f"The OpenModelica result CSV is not numeric: {error}") from error
return columns
class JsonSimulationResultsSerializer:
@staticmethod
def load(path: Path) -> SimulationResults:
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as error:
raise ValueError(f"Could not read simulation results: {error}") from error
if not isinstance(data, dict):
raise ValueError("Simulation-results root must be an object")
return SimulationResults.from_dict(data)
@staticmethod
def save(results: SimulationResults, path: Path) -> None:
temporary_path = path.with_suffix(path.suffix + ".tmp")
temporary_path.write_text(
json.dumps(results.to_dict(), indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
temporary_path.replace(path)
class BerSimulationResultsSerializer:
"""Compressed MessagePack serializer for binary simulation results."""
MAGIC = b"BER\x00"
VERSION = 1
@classmethod
def load(cls, path: Path) -> SimulationResults:
try:
payload = path.read_bytes()
except OSError as error:
raise ValueError(f"Could not read simulation results: {error}") from error
header = cls.MAGIC + bytes([cls.VERSION])
if not payload.startswith(header):
raise ValueError("This is not a supported BEdit binary results file")
try:
data = msgpack.unpackb(
zlib.decompress(payload[len(header) :]), raw=False
)
except (ValueError, zlib.error, msgpack.exceptions.MsgpackException) as error:
raise ValueError("The BEdit binary results file is damaged") from error
if not isinstance(data, dict):
raise ValueError("The BEdit binary results file has an invalid root value")
return SimulationResults.from_dict(data)
@classmethod
def save(cls, results: SimulationResults, path: Path) -> None:
packed = msgpack.packb(results.to_dict(), use_bin_type=True)
payload = cls.MAGIC + bytes([cls.VERSION]) + zlib.compress(packed, level=9)
temporary_path = path.with_suffix(path.suffix + ".tmp")
temporary_path.write_bytes(payload)
temporary_path.replace(path)
class SimulationResultsSerializer:
"""Select JSON or compressed MessagePack based on the file extension."""
@staticmethod
def load(path: str | Path) -> SimulationResults:
target = Path(path)
serializer = (
BerSimulationResultsSerializer
if target.suffix.lower() == ".ber"
else JsonSimulationResultsSerializer
)
return serializer.load(target)
@staticmethod
def save(results: SimulationResults, path: str | Path) -> None:
target = Path(path)
serializer = (
BerSimulationResultsSerializer
if target.suffix.lower() == ".ber"
else JsonSimulationResultsSerializer
)
serializer.save(results, target)
def save_simulation_results(path: str | Path, results: SimulationResults) -> None:
SimulationResultsSerializer.save(results, path)
def load_simulation_results(path: str | Path) -> SimulationResults:
return SimulationResultsSerializer.load(path)

View File

@@ -0,0 +1,196 @@
from collections.abc import Callable
from typing import Any
from bedit.core.application_log import get_logger
from bedit.core.bond_graph import infer_causality
from bedit.core.simulation.composer import compose_graph
from bedit.core.simulation.openmodelica import (
ErrorCallback,
ModelBuildResult,
OpenModelicaInterface,
ResultCallback,
SimulationMessage,
SimulationProgress,
)
log = get_logger(__name__)
class Simulation:
"""Application-owned composition state and OpenModelica interface."""
def __init__(self, *, openmodelica_path: str = "") -> None:
self.state: dict[str, Any] = {}
self.last_composition_input: dict[str, Any] | None = None
self.last_composition_output: str | None = None
self.id_list: dict[str, Any] = {}
self.model_name: str | None = None
self._openmodelica_path = openmodelica_path
self.openmodelica = OpenModelicaInterface(openmodelica_path)
self.model_path: str | None = None
self.simulation_progress: SimulationProgress | None = None
@property
def openmodelica_path(self) -> str:
return self._openmodelica_path
@openmodelica_path.setter
def openmodelica_path(self, value: str) -> None:
self._openmodelica_path = value
self.openmodelica.configure(value)
def compose(
self,
graph: dict[str, Any],
callback: Callable[[str], None] | None = None,
error_callback: ErrorCallback | None = None,
message_callback: Callable[[SimulationMessage], None] | None = None,
) -> None:
"""Compose and retain the active graph's Modelica representation."""
self.model_path = None
self.compose_source(graph)
def _model_compiled(result):
if not isinstance(result, ModelBuildResult):
failure = RuntimeError(
f"OpenModelica returned an invalid build response: {result!r}"
)
if error_callback is not None:
error_callback(failure)
else:
log.error("%s", failure)
return
log.info("%s", result.check_summary.strip())
if message_callback is not None:
message_callback(
SimulationMessage(
stream="build",
type="info",
text=result.check_summary.strip(),
)
)
if result.diagnostics:
log.warning("OpenModelica build diagnostics:\n%s", result.diagnostics)
if message_callback is not None:
message_callback(
SimulationMessage(
stream="build",
type="warning",
text=result.diagnostics,
)
)
log.info("Compiling OK: %s", result.build_result)
try:
self.model_path = str(result.build_result[0])
except (IndexError, TypeError) as error:
failure = RuntimeError(
f"OpenModelica returned an invalid build result: {result!r}"
)
failure.__cause__ = error
if error_callback is not None:
error_callback(failure)
else:
log.error("%s", failure)
return
if message_callback is not None:
message_callback(
SimulationMessage(
stream="build",
type="info",
text=f"Compiling OK: {result.build_result}",
)
)
if callback is not None:
callback(self.model_path)
self.openmodelica.build_model(
self.last_composition_output,
self.model_name,
_model_compiled,
error_callback,
)
def compose_source(self, graph: dict[str, Any]) -> tuple[str, str]:
"""Compose Modelica source without asking OpenModelica to build it."""
infer_causality(graph)
result = compose_graph(graph)
self.last_composition_input = result.graph
self.id_list = result.objects_by_id
self.last_composition_output = result.modelica
self.model_name = result.model_name
# log.info("Composed OpenModelica model:\n%s", result.modelica)
return result.model_name, result.modelica
def run_simulation(
self,
graph: dict[str, Any],
progress_callback: Callable[[SimulationProgress], None] | None = None,
message_callback: Callable[[SimulationMessage], None] | None = None,
callback: ResultCallback | None = None,
error_callback: ErrorCallback | None = None,
) -> None:
"""Compose, build, and asynchronously run the current graph."""
self.simulation_progress = None
def report_progress(progress: SimulationProgress) -> None:
self.simulation_progress = progress
if progress_callback is not None:
progress_callback(progress)
def run_model(model_path: str) -> None:
try:
arguments = self.build_simulation_arguments()
except Exception as error:
if error_callback is not None:
error_callback(error)
else:
log.exception("Could not prepare simulation arguments")
return
self.openmodelica.run_model(
model_path,
arguments,
report_progress,
message_callback,
callback,
error_callback,
)
self.compose(graph, run_model, error_callback, message_callback)
def get_progress(self) -> SimulationProgress | None:
"""Return the most recently received simulation status."""
return self.simulation_progress
def shutdown(self, *, wait: bool = True) -> None:
"""Close OpenModelica and clean its generated working directory."""
self.openmodelica.shutdown(wait=wait)
self.model_path = None
def build_simulation_arguments(self) -> list[str]:
opts = self.last_composition_input["implementation"]["graph"].get(
"simulation", {}
)
start_time = float(opts.get("startTime", 0.0))
stop_time = float(opts.get("stopTime", 1.0))
interval_mode = opts.get("intervalMode", "numberOfIntervals")
interval_time = float(opts.get("intervalTime", 0.002))
if interval_mode == "numberOfIntervals":
intervals = int(opts.get("numberOfIntervals", 500))
if intervals <= 0:
raise ValueError("Number of simulation intervals must be positive")
interval_time = (stop_time - start_time) / intervals
if interval_time <= 0:
raise ValueError("Simulation interval must be positive")
arguments = [
"-outputFormat=csv",
f"-startTime={start_time}",
f"-stopTime={stop_time}",
f"-stepSize={interval_time}",
]
log.info("Running model with: %s", arguments)
return arguments

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,22 @@
{
"keywords": [
"algorithm", "and", "annotation", "block", "break", "class", "connect",
"connector", "constant", "constrainedby", "der", "discrete", "each", "else",
"elseif", "elsewhen", "encapsulated", "end", "enumeration", "equation",
"expandable", "extends", "external", "false", "final", "flow", "for",
"function", "if", "import", "impure", "in", "initial", "inner", "input",
"loop", "model", "not", "operator", "or", "outer", "output", "package",
"parameter", "partial", "protected", "public", "pure", "record", "redeclare",
"replaceable", "return", "stream", "then", "true", "type", "when", "while",
"within"
],
"types": ["Boolean", "Integer", "Real", "String"],
"builtins": [
"abs", "acos", "actualStream", "asin", "assert", "atan", "atan2", "cardinality",
"ceil", "change", "cos", "cosh", "delay", "div", "edge", "exp", "floor",
"homotopy", "inStream", "integer", "log", "log10", "max", "min", "mod",
"noEvent", "pre", "reinit", "rem", "sample", "semiLinear", "sign", "sin",
"sinh", "smooth", "sqrt", "sum", "tan", "tanh", "terminal", "terminate"
],
"bevalues": []
}

View File

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

View File

@@ -0,0 +1,65 @@
import sys
from collections.abc import Sequence
from pathlib import Path
from PySide6.QtCore import QCoreApplication, Qt
from PySide6.QtGui import QColor, QPalette
from PySide6.QtWidgets import QApplication, QStyleFactory
from bedit.gui.main_window import MainWindow
def apply_light_theme(app: QApplication) -> None:
"""Use a predictable light Qt theme, independent of the desktop theme."""
app.setStyle(QStyleFactory.create("Fusion"))
palette = QPalette()
palette.setColor(QPalette.ColorRole.Window, QColor(240, 240, 240))
palette.setColor(QPalette.ColorRole.WindowText, Qt.GlobalColor.black)
palette.setColor(QPalette.ColorRole.Base, Qt.GlobalColor.white)
palette.setColor(QPalette.ColorRole.AlternateBase, QColor(233, 233, 233))
palette.setColor(QPalette.ColorRole.ToolTipBase, Qt.GlobalColor.white)
palette.setColor(QPalette.ColorRole.ToolTipText, Qt.GlobalColor.black)
palette.setColor(QPalette.ColorRole.Text, Qt.GlobalColor.black)
palette.setColor(QPalette.ColorRole.Button, QColor(240, 240, 240))
palette.setColor(QPalette.ColorRole.ButtonText, Qt.GlobalColor.black)
palette.setColor(QPalette.ColorRole.BrightText, Qt.GlobalColor.red)
palette.setColor(QPalette.ColorRole.Link, QColor(0, 102, 204))
palette.setColor(QPalette.ColorRole.Highlight, QColor(0, 120, 215))
palette.setColor(QPalette.ColorRole.HighlightedText, Qt.GlobalColor.white)
palette.setColor(
QPalette.ColorGroup.Disabled,
QPalette.ColorRole.Text,
QColor(109, 109, 109),
)
palette.setColor(
QPalette.ColorGroup.Disabled,
QPalette.ColorRole.ButtonText,
QColor(109, 109, 109),
)
app.setPalette(palette)
def _document_argument(arguments: Sequence[str]) -> Path | None:
"""Return the first positional application argument, if present."""
return next(
(Path(argument) for argument in arguments[1:] if not argument.startswith("-")),
None,
)
def main(argv: Sequence[str] | None = None) -> int:
QCoreApplication.setApplicationName("BEdit")
QCoreApplication.setOrganizationName("BEdit")
QCoreApplication.setApplicationVersion("0.1.0")
app = QApplication(list(sys.argv if argv is None else argv))
app.setApplicationDisplayName("BEdit")
apply_light_theme(app)
window = MainWindow()
document_path = _document_argument(app.arguments())
if document_path is not None:
window.open_document_path(document_path, check_unsaved=False)
window.show()
return app.exec()

View File

@@ -0,0 +1,26 @@
import logging
from PySide6.QtCore import QObject, Signal
from PySide6.QtWidgets import QPlainTextEdit
class _LogEmitter(QObject):
lineReady = Signal(str)
class ApplicationLogHandler(logging.Handler):
"""Thread-safe bridge from Python logging into a Qt text view."""
def __init__(self, output: QPlainTextEdit) -> None:
super().__init__()
self.emitter = _LogEmitter(output)
self.emitter.lineReady.connect(output.appendPlainText)
self.setFormatter(
logging.Formatter("%(asctime)s %(levelname)s %(message)s", "%H:%M:%S")
)
def emit(self, record: logging.LogRecord) -> None:
try:
self.emitter.lineReady.emit(self.format(record))
except Exception:
self.handleError(record)

View File

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

View File

@@ -0,0 +1,408 @@
from PySide6.QtCore import QPointF
from PySide6.QtGui import QUndoCommand
from bedit.core.model import Annotation, Component, Connection, Junction, Port
class AddComponentCommand(QUndoCommand):
def __init__(self, controller, owner_id: str | None, component: Component) -> None:
super().__init__(f"Add {component.name}")
self.controller = controller
self.owner_id = owner_id
self.component = component
def redo(self) -> None:
self.controller._insert_component(self.owner_id, self.component)
def undo(self) -> None:
self.controller._remove_component(self.owner_id, self.component.id)
class MoveComponentCommand(QUndoCommand):
def __init__(
self,
controller,
owner_id: str,
component_id: str,
old: QPointF,
new: QPointF,
) -> None:
super().__init__("Move component")
self.controller = controller
self.owner_id = owner_id
self.component_id = component_id
self.old = old
self.new = new
def redo(self) -> None:
self.controller._move_component(self.owner_id, self.component_id, self.new)
def undo(self) -> None:
self.controller._move_component(self.owner_id, self.component_id, self.old)
class RotateComponentsCommand(QUndoCommand):
def __init__(
self,
controller,
owner_id: str,
rotations: dict[str, tuple[float, float]],
) -> None:
super().__init__("Rotate components")
self.controller = controller
self.owner_id = owner_id
self.rotations = rotations
def redo(self) -> None:
for component_id, (_old, new) in self.rotations.items():
self.controller._rotate_component(self.owner_id, component_id, new)
def undo(self) -> None:
for component_id, (old, _new) in self.rotations.items():
self.controller._rotate_component(self.owner_id, component_id, old)
class AddConnectionCommand(QUndoCommand):
def __init__(self, controller, owner_id: str, connection: Connection) -> None:
super().__init__("Connect components")
self.controller = controller
self.owner_id = owner_id
self.connection = connection
def redo(self) -> None:
self.controller._insert_connection(self.owner_id, self.connection)
def undo(self) -> None:
self.controller._remove_connection(self.owner_id, self.connection.id)
class SplitConnectionCommand(QUndoCommand):
def __init__(
self,
controller,
owner_id: str,
original: Connection,
junction: Junction,
first: Connection,
second: Connection,
) -> None:
super().__init__("Add connection junction")
self.controller, self.owner_id = controller, owner_id
self.original, self.junction = original, junction
self.first, self.second = first, second
def redo(self) -> None:
self.controller._split_connection(
self.owner_id, self.original.id, self.junction, self.first, self.second
)
def undo(self) -> None:
self.controller._restore_split_connection(
self.owner_id, self.original, self.junction.id, self.first.id, self.second.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 EditSimulationSettingsCommand(QUndoCommand):
def __init__(self, controller, owner_id: str, old: dict, new: dict) -> None:
super().__init__("Edit simulation settings")
self.controller, self.owner_id = controller, owner_id
self.old, self.new = old, new
def redo(self) -> None:
self.controller._set_simulation_settings(self.owner_id, self.new)
def undo(self) -> None:
self.controller._set_simulation_settings(self.owner_id, self.old)
class EditGraphParametersCommand(QUndoCommand):
def __init__(self, controller, old: dict, new: dict) -> None:
super().__init__("Edit graph parameters")
self.controller = controller
self.old, self.new = old, new
def redo(self) -> None:
self.controller._set_graph_parameters(self.new)
def undo(self) -> None:
self.controller._set_graph_parameters(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):
def __init__(self, controller, old: Component, new: Component) -> None:
super().__init__("Apply JSON changes")
self.controller = controller
self.old = old
self.new = new
def redo(self) -> None:
self.controller._replace_component(self.old.id, self.new)
def undo(self) -> None:
self.controller._replace_component(self.new.id, self.old)
class AddInterfacePortCommand(QUndoCommand):
def __init__(self, controller, owner_id: str, direction: str, port: Port) -> None:
super().__init__(f"Add {direction}")
self.controller = controller
self.owner_id = owner_id
self.direction = direction
self.port = port
def redo(self) -> None:
self.controller._insert_interface_port(self.owner_id, self.direction, self.port)
def undo(self) -> None:
self.controller._remove_interface_port(self.owner_id, self.direction, self.port.id)
class ReplaceSourceCommand(QUndoCommand):
def __init__(self, controller, component_id: str, old: dict, new: dict) -> None:
super().__init__("Apply source JSON")
self.controller = controller
self.component_id = component_id
self.old = old
self.new = new
def redo(self) -> None:
self.controller._replace_source(self.component_id, self.new)
def undo(self) -> None:
self.controller._replace_source(self.component_id, self.old)
class MoveInterfacePortCommand(QUndoCommand):
def __init__(self, controller, owner_id: str, port_id: str, old: QPointF, new: QPointF) -> None:
super().__init__("Move interface terminal")
self.controller = controller
self.owner_id = owner_id
self.port_id = port_id
self.old = old
self.new = new
def redo(self) -> None:
self.controller._move_interface_port(self.owner_id, self.port_id, self.new)
def undo(self) -> None:
self.controller._move_interface_port(self.owner_id, self.port_id, self.old)
class EditComponentAppearanceCommand(QUndoCommand):
def __init__(self, controller, component_id: str, old: dict, new: dict) -> None:
super().__init__("Edit component appearance")
self.controller = controller
self.component_id = component_id
self.old = old
self.new = new
def redo(self) -> None:
self.controller._set_component_appearance(self.component_id, self.new)
def undo(self) -> None:
self.controller._set_component_appearance(self.component_id, self.old)
class EditComponentPropertiesCommand(QUndoCommand):
def __init__(self, controller, component_id: str, old: dict, new: dict, text: str) -> None:
super().__init__(text)
self.controller, self.component_id = controller, component_id
self.old, self.new = old, new
def redo(self) -> None:
self.controller._set_component_properties(self.component_id, self.new)
def undo(self) -> None:
self.controller._set_component_properties(self.component_id, self.old)
class EditComponentParametersCommand(QUndoCommand):
def __init__(self, controller, component_id: str, old: list, new: list) -> None:
super().__init__("Edit component parameters")
self.controller, self.component_id = controller, component_id
self.old, self.new = old, new
def redo(self) -> None:
self.controller._set_component_parameters(self.component_id, self.new)
def undo(self) -> None:
self.controller._set_component_parameters(self.component_id, self.old)
class RenameInterfacePortCommand(QUndoCommand):
def __init__(self, controller, owner_id: str, port_id: str, old: str, new: str) -> None:
super().__init__("Rename interface port")
self.controller, self.owner_id, self.port_id = controller, owner_id, port_id
self.old, self.new = old, new
def redo(self) -> None:
self.controller._rename_interface_port(self.owner_id, self.port_id, self.new)
def undo(self) -> None:
self.controller._rename_interface_port(self.owner_id, self.port_id, self.old)
class RenameConnectionCommand(QUndoCommand):
def __init__(self, controller, owner_id: str, connection_id: str, old: str, new: str) -> None:
super().__init__("Rename connection")
self.controller, self.owner_id, self.connection_id = controller, owner_id, connection_id
self.old, self.new = old, new
def redo(self) -> None:
self.controller._rename_connection(self.owner_id, self.connection_id, self.new)
def undo(self) -> None:
self.controller._rename_connection(self.owner_id, self.connection_id, self.old)
class RenameDocumentCommand(QUndoCommand):
def __init__(self, controller, old: str, new: str) -> None:
super().__init__("Rename document")
self.controller = controller
self.old = old
self.new = new
def redo(self) -> None:
self.controller._rename_document(self.new)
def undo(self) -> None:
self.controller._rename_document(self.old)
class DeleteSelectionCommand(QUndoCommand):
def __init__(
self,
controller,
owner_id: str | None,
blocks: dict[str, Component],
connections: dict[str, Connection],
ports: list[Port],
) -> None:
super().__init__("Delete selection")
self.controller = controller
self.owner_id = owner_id
self.blocks = blocks
self.connections = connections
self.ports = ports
def redo(self) -> None:
self.controller._delete_items(
self.owner_id,
set(self.blocks),
set(self.connections),
{port.id for port in self.ports},
)
def undo(self) -> None:
self.controller._restore_items(
self.owner_id,
self.blocks,
self.connections,
self.ports,
)
class PasteSelectionCommand(QUndoCommand):
def __init__(
self,
controller,
owner_id: str | None,
blocks: dict[str, Component],
connections: dict[str, Connection],
) -> None:
super().__init__("Paste selection")
self.controller = controller
self.owner_id = owner_id
self.blocks = blocks
self.connections = connections
def redo(self) -> None:
self.controller._restore_items(
self.owner_id,
self.blocks,
self.connections,
[],
)
def undo(self) -> None:
self.controller._delete_items(
self.owner_id,
set(self.blocks),
set(self.connections),
set(),
)
class EditTextDefinitionCommand(QUndoCommand):
def __init__(self, controller, component_id: str, old: dict, new: dict) -> None:
super().__init__("Edit text component")
self.controller = controller
self.component_id = component_id
self.old = old
self.new = new
def redo(self) -> None:
self.controller._set_text_definition(self.component_id, self.new)
def undo(self) -> None:
self.controller._set_text_definition(self.component_id, self.old)
def id(self) -> int:
return 1001
def mergeWith(self, other: QUndoCommand) -> bool: # noqa: N802
if not isinstance(other, EditTextDefinitionCommand):
return False
if other.component_id != self.component_id:
return False
self.new = other.new
return True

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,34 @@
from PySide6.QtWidgets import QDialog, QMessageBox
from bedit.core.model import Component
from bedit.gui.graphics.icon_editor import IconEditorDialog
from bedit.gui.generated.ui_component_options_dialog import Ui_ComponentOptionsDialog
class ComponentOptionsDialog(QDialog):
def __init__(self, component: Component, parent=None) -> None:
super().__init__(parent)
self.ui = Ui_ComponentOptionsDialog()
self.ui.setupUi(self)
self.component = component
self.edited_icon = component.icon
self.edited_ports = component.ports
self.ui.nameEdit.setText(component.name)
self.ui.editIconButton.clicked.connect(self.edit_icon)
self.ui.showSubtreeCheckBox.setChecked(component.show_subtree_in_library)
self.ui.showNameCheckBox.setChecked(bool(component.properties.get("showName", False)))
def edit_icon(self) -> None:
working = Component.from_dict(self.component.to_dict())
working.icon = self.edited_icon
working.ports = self.edited_ports
dialog = IconEditorDialog(working, self)
if dialog.exec() == dialog.DialogCode.Accepted:
self.edited_icon = dialog.icon
self.edited_ports = dialog.ports
def accept(self) -> None:
if not self.ui.nameEdit.text().strip():
QMessageBox.warning(self, "Invalid name", "The component name cannot be empty.")
return
super().accept()

View File

@@ -0,0 +1,18 @@
from PySide6.QtWidgets import QDialog
from bedit.gui.generated.ui_connection_chooser_dialog import Ui_ConnectionChooserDialog
class ConnectionChooserDialog(QDialog):
def __init__(self, labels: list[str], default_index: int = 0, parent=None) -> None:
super().__init__(parent)
self.ui = Ui_ConnectionChooserDialog()
self.ui.setupUi(self)
self.ui.connectionList.addItems(labels)
if labels:
self.ui.connectionList.setCurrentRow(max(0, min(default_index, len(labels) - 1)))
self.ui.buttonBox.button(self.ui.buttonBox.StandardButton.Ok).setEnabled(bool(labels))
@property
def selected_index(self) -> int:
return self.ui.connectionList.currentRow()

View File

@@ -0,0 +1,93 @@
from copy import deepcopy
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QDialog, QStyledItemDelegate, QTreeWidgetItem
from bedit.core.model import Component
from bedit.gui.generated.ui_graph_parameters_dialog import Ui_GraphParametersDialog
COMPONENT_ID_ROLE = Qt.ItemDataRole.UserRole
PARAMETER_ID_ROLE = Qt.ItemDataRole.UserRole + 1
class ValueColumnDelegate(QStyledItemDelegate):
"""Allow editing only in the parameter value column."""
def createEditor(self, parent, option, index): # noqa: N802 (Qt API name)
parameter_id = index.siblingAtColumn(0).data(PARAMETER_ID_ROLE)
if index.column() != 2 or parameter_id is None:
return None
return super().createEditor(parent, option, index)
class GraphParametersDialog(QDialog):
"""Edit every parameter value in an active component subtree."""
def __init__(self, root: Component, parent=None) -> None:
super().__init__(parent)
self.ui = Ui_GraphParametersDialog()
self.ui.setupUi(self)
self.ui.parameterTree.setItemDelegate(ValueColumnDelegate(self.ui.parameterTree))
self.setWindowTitle(f"Graph Parameters — {root.name}")
self._parameters = {
component.id: deepcopy(component.parameters)
for component in self._walk(root)
}
self._populate(root)
self.ui.parameterTree.expandAll()
self.ui.parameterTree.resizeColumnToContents(0)
self.ui.parameterTree.resizeColumnToContents(1)
@staticmethod
def _walk(component: Component):
yield component
if component.implementation_kind == "graph":
for child in component.graph.blocks.values():
yield from GraphParametersDialog._walk(child)
def _populate(self, root: Component) -> None:
self.ui.parameterTree.clear()
def add_component(component: Component, parent: QTreeWidgetItem | None) -> None:
item = QTreeWidgetItem([component.name, "", ""])
item.setData(0, COMPONENT_ID_ROLE, component.id)
item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable)
if parent is None:
self.ui.parameterTree.addTopLevelItem(item)
else:
parent.addChild(item)
for parameter in self._parameters[component.id]:
parameter_item = QTreeWidgetItem(
[parameter.name, parameter.type, parameter.value]
)
parameter_item.setData(0, COMPONENT_ID_ROLE, component.id)
parameter_item.setData(0, PARAMETER_ID_ROLE, parameter.id)
parameter_item.setFlags(
parameter_item.flags() | Qt.ItemFlag.ItemIsEditable
)
item.addChild(parameter_item)
if component.implementation_kind == "graph":
for child in component.graph.blocks.values():
add_component(child, item)
add_component(root, None)
@property
def parameter_values(self) -> dict[str, dict[str, str]]:
values: dict[str, dict[str, str]] = {}
iterator = self.ui.parameterTree.invisibleRootItem()
def collect(parent: QTreeWidgetItem) -> None:
for index in range(parent.childCount()):
item = parent.child(index)
parameter_id = item.data(0, PARAMETER_ID_ROLE)
if parameter_id is not None:
component_id = item.data(0, COMPONENT_ID_ROLE)
values.setdefault(component_id, {})[parameter_id] = item.text(2)
collect(item)
collect(iterator)
return values

View File

@@ -0,0 +1,60 @@
from PySide6.QtWidgets import (
QDialog,
QDialogButtonBox,
QCheckBox,
QFormLayout,
QLineEdit,
QMessageBox,
QVBoxLayout,
)
class ItemOptionsDialog(QDialog):
"""Small, extensible options dialog shared by ports and connections."""
def __init__(
self,
title: str,
name: str,
parent=None,
*,
name_required: bool = True,
show_name: bool | None = None,
) -> None:
super().__init__(parent)
self.name_required = name_required
self.setWindowTitle(title)
self.resize(380, 120)
layout = QVBoxLayout(self)
self.form = QFormLayout()
self.name_edit = QLineEdit(name, self)
self.form.addRow("Name:", self.name_edit)
self.show_name_check = None
if show_name is not None:
self.show_name_check = QCheckBox("Show name below connection", self)
self.show_name_check.setChecked(show_name)
self.form.addRow("", self.show_name_check)
layout.addLayout(self.form)
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel,
parent=self,
)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
@property
def name(self) -> str:
return self.name_edit.text().strip()
@property
def show_name(self) -> bool:
return bool(self.show_name_check and self.show_name_check.isChecked())
def accept(self) -> None:
if self.name_required and not self.name:
QMessageBox.warning(self, "Invalid name", "The name cannot be empty.")
return
super().accept()

View File

@@ -0,0 +1,159 @@
from copy import deepcopy
from uuid import uuid4
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import QDialog, QListWidgetItem, QMessageBox
from bedit.core.model import Component, Parameter
from bedit.core.physical_types import QUANTITIES, UNITS
from bedit.gui.generated.ui_parameter_options_dialog import Ui_ParameterOptionsDialog
PARAMETER_ROLE = Qt.ItemDataRole.UserRole
class ParameterOptionsDialog(QDialog):
"""Editor for parameters shared by graph and text components."""
edited = Signal()
def __init__(self, component: Component, parent=None, *, read_only: bool = False) -> None:
super().__init__(parent)
self.ui = Ui_ParameterOptionsDialog()
self.ui.setupUi(self)
self.setWindowTitle(f"Parameter Options — {component.name}")
self.parameters = deepcopy(component.parameters)
self.read_only = read_only
self._loading = False
self.ui.quantityCombo.addItems(QUANTITIES)
self.ui.unitCombo.addItems(UNITS)
self.ui.parameterList.currentRowChanged.connect(self._load_current)
self.ui.addParameterButton.clicked.connect(self.add_parameter)
self.ui.removeParameterButton.clicked.connect(self.remove_parameter)
self.ui.nameEdit.textEdited.connect(self._store_current)
self.ui.typeCombo.currentTextChanged.connect(self._store_current)
self.ui.valueEdit.textEdited.connect(self._store_current)
self.ui.quantityCombo.currentTextChanged.connect(self._store_current)
self.ui.unitCombo.currentTextChanged.connect(self._store_current)
self.ui.rowsSpin.valueChanged.connect(self._store_current)
self.ui.columnsSpin.valueChanged.connect(self._store_current)
self.ui.descriptionEdit.textChanged.connect(self._store_current)
self.ui.parameterSplitter.setSizes([250, 370])
if read_only:
self.ui.addParameterButton.setEnabled(False)
self.ui.removeParameterButton.setEnabled(False)
self.ui.nameEdit.setReadOnly(True)
self.ui.typeCombo.setEnabled(False)
self.ui.valueEdit.setReadOnly(True)
self.ui.quantityCombo.setEnabled(False)
self.ui.unitCombo.setEnabled(False)
self.ui.rowsSpin.setEnabled(False)
self.ui.columnsSpin.setEnabled(False)
self.ui.descriptionEdit.setReadOnly(True)
self._rebuild_list(0 if self.parameters else -1)
def _rebuild_list(self, row: int = -1) -> None:
self.ui.parameterList.clear()
for parameter in self.parameters:
item = QListWidgetItem(f"{parameter.name} [{parameter.type}] = {parameter.value}")
item.setData(PARAMETER_ROLE, parameter.id)
self.ui.parameterList.addItem(item)
self.ui.parameterList.setCurrentRow(min(row, len(self.parameters) - 1))
self._update_enabled()
def _load_current(self, row: int) -> None:
self._loading = True
if 0 <= row < len(self.parameters):
parameter = self.parameters[row]
self.ui.nameEdit.setText(parameter.name)
self.ui.typeCombo.setCurrentText(parameter.type)
self.ui.valueEdit.setText(parameter.value)
self.ui.quantityCombo.setCurrentText(parameter.quantity)
self.ui.unitCombo.setCurrentText(parameter.unit)
self.ui.rowsSpin.setValue(parameter.rows)
self.ui.columnsSpin.setValue(parameter.columns)
self.ui.descriptionEdit.setPlainText(parameter.description)
else:
self.ui.nameEdit.clear()
self.ui.typeCombo.setCurrentText("")
self.ui.valueEdit.clear()
self.ui.quantityCombo.setCurrentText("")
self.ui.unitCombo.setCurrentText("")
self.ui.descriptionEdit.clear()
self._loading = False
self._update_enabled()
def _update_enabled(self) -> None:
enabled = self.ui.parameterList.currentRow() >= 0
self.ui.removeParameterButton.setEnabled(enabled and not self.read_only)
self.ui.nameEdit.setEnabled(enabled)
self.ui.typeCombo.setEnabled(enabled and not self.read_only)
self.ui.valueEdit.setEnabled(enabled)
self.ui.quantityCombo.setEnabled(enabled and not self.read_only)
self.ui.unitCombo.setEnabled(enabled and not self.read_only)
self.ui.rowsSpin.setEnabled(enabled and not self.read_only)
self.ui.columnsSpin.setEnabled(enabled and not self.read_only)
self.ui.descriptionEdit.setEnabled(enabled)
def _store_current(self) -> None:
row = self.ui.parameterList.currentRow()
if self._loading or not (0 <= row < len(self.parameters)):
return
parameter = self.parameters[row]
parameter.name = self.ui.nameEdit.text()
parameter.type = self.ui.typeCombo.currentText().strip() or "real"
parameter.value = self.ui.valueEdit.text()
parameter.quantity = self.ui.quantityCombo.currentText().strip()
parameter.unit = self.ui.unitCombo.currentText().strip()
parameter.rows = self.ui.rowsSpin.value()
parameter.columns = self.ui.columnsSpin.value()
parameter.description = self.ui.descriptionEdit.toPlainText()
self.ui.parameterList.item(row).setText(
f"{parameter.name} [{parameter.type}] = {parameter.value}"
)
self.edited.emit()
def set_parameters(self, parameters: list[Parameter]) -> None:
self._loading = True
self.parameters = deepcopy(parameters)
self._loading = False
self._rebuild_list(0 if self.parameters else -1)
def add_parameter(self) -> None:
self.parameters.append(
Parameter(
id=f"parameter-{uuid4().hex[:8]}",
name=f"Parameter {len(self.parameters) + 1}",
)
)
self._rebuild_list(len(self.parameters) - 1)
self.ui.nameEdit.selectAll()
self.ui.nameEdit.setFocus()
self.edited.emit()
def remove_parameter(self) -> None:
row = self.ui.parameterList.currentRow()
if row >= 0:
self.parameters.pop(row)
self._rebuild_list(min(row, len(self.parameters) - 1))
self.edited.emit()
def accept(self) -> None:
self._store_current()
if any(not parameter.name.strip() for parameter in self.parameters):
QMessageBox.warning(self, "Invalid parameter", "Every parameter needs a name.")
return
names = [parameter.name for parameter in self.parameters]
if len(set(names)) != len(names):
QMessageBox.warning(self, "Invalid parameter", "Parameter names must be unique.")
return
super().accept()
class ParameterEditor(ParameterOptionsDialog):
"""The parameter options editor embedded without dialog buttons."""
def __init__(self, parent=None) -> None:
super().__init__(Component(id="embedded-parameter-editor", name="Parameters"), parent)
self.setWindowFlags(Qt.WindowType.Widget)
self.ui.buttonBox.hide()

View File

@@ -0,0 +1,239 @@
from copy import deepcopy
from uuid import uuid4
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import QDialog, QDialogButtonBox, QListWidgetItem, QMessageBox
from bedit.core.model import Component, Port
from bedit.core.physical_types import QUANTITIES, UNITS
from bedit.core.power_domains import POWER_DOMAINS, power_causalities, power_domain
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."""
edited = Signal()
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.read_only = read_only
self.ports = deepcopy(component.ports)
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.quantityCombo.addItems(QUANTITIES)
self.ui.unitCombo.addItems(UNITS)
self.ui.orientationCombo.setItemData(0, "input")
self.ui.orientationCombo.setItemData(1, "output")
self.ui.orientationCombo.setItemData(2, "indifferent")
for domain in POWER_DOMAINS:
self.ui.domainCombo.addItem(domain.display_name, domain.id)
self._refresh_causality_options()
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._port_type_changed)
self.ui.orientationCombo.currentIndexChanged.connect(self._store_current)
self.ui.multipleConnectionsCheckBox.toggled.connect(
self._multiple_connections_changed
)
self.ui.valueTypeCombo.currentTextChanged.connect(self._store_current)
self.ui.quantityCombo.currentTextChanged.connect(self._store_current)
self.ui.unitCombo.currentTextChanged.connect(self._store_current)
self.ui.rowsSpin.valueChanged.connect(self._store_current)
self.ui.columnsSpin.valueChanged.connect(self._store_current)
self.ui.descriptionEdit.textChanged.connect(self._store_current)
self.ui.domainCombo.currentIndexChanged.connect(self._power_domain_changed)
self.ui.causalityCombo.currentTextChanged.connect(self._store_current)
self.ui.powerDescriptionEdit.textChanged.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.multipleConnectionsCheckBox.setEnabled(False)
self.ui.typeOptionsStack.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)
def _rebuild_list(self, row: int = -1) -> None:
self.ui.portList.clear()
for port in self.ports:
item = QListWidgetItem(f"{port.name} [{port.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 = 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(port.orientation)
)
self.ui.multipleConnectionsCheckBox.setChecked(
port.allows_multiple_connections
)
self.ui.valueTypeCombo.setCurrentText(port.value_type)
self.ui.quantityCombo.setCurrentText(port.quantity)
self.ui.unitCombo.setCurrentText(port.unit)
self.ui.rowsSpin.setValue(port.rows)
self.ui.columnsSpin.setValue(port.columns)
self.ui.descriptionEdit.setPlainText(port.description)
self.ui.domainCombo.setCurrentIndex(self.ui.domainCombo.findData(port.domain))
self._refresh_causality_options(
port.causality,
port_type=port.type,
allows_multiple_connections=port.allows_multiple_connections,
)
self.ui.causalityCombo.setCurrentText(port.causality)
self.ui.powerDescriptionEdit.setPlainText(port.description)
self._show_type_options(port.type)
else:
self.ui.nameEdit.clear()
self.ui.descriptionEdit.clear()
self._loading = False
self._update_enabled()
def _update_enabled(self) -> None:
enabled = self.ui.portList.currentRow() >= 0
self.ui.removePortButton.setEnabled(enabled and not self.read_only)
self.ui.nameEdit.setEnabled(enabled)
self.ui.typeCombo.setEnabled(enabled and not self.read_only)
self.ui.orientationCombo.setEnabled(enabled and not self.read_only)
self.ui.multipleConnectionsCheckBox.setEnabled(enabled and not self.read_only)
self.ui.typeOptionsStack.setEnabled(enabled and not self.read_only)
def _port_type_changed(self) -> None:
port_type = self.ui.typeCombo.currentData()
indifferent_item = self.ui.orientationCombo.model().item(2)
indifferent_item.setEnabled(port_type == "power")
if port_type != "power" and self.ui.orientationCombo.currentData() == "indifferent":
self.ui.orientationCombo.setCurrentIndex(0)
self._refresh_causality_options(port_type=port_type)
self._show_type_options(port_type)
self._store_current()
def _multiple_connections_changed(self) -> None:
self._refresh_causality_options()
self._store_current()
def _refresh_causality_options(
self,
selected: str | None = None,
*,
port_type: str | None = None,
allows_multiple_connections: bool | None = None,
) -> None:
selected = selected or self.ui.causalityCombo.currentText() or "indifferent"
port_type = port_type or self.ui.typeCombo.currentData()
if allows_multiple_connections is None:
allows_multiple_connections = self.ui.multipleConnectionsCheckBox.isChecked()
choices = power_causalities(
port_type == "power" and allows_multiple_connections
)
self.ui.causalityCombo.blockSignals(True)
self.ui.causalityCombo.clear()
self.ui.causalityCombo.addItems(choices)
self.ui.causalityCombo.setCurrentText(
selected if selected in choices else "indifferent"
)
self.ui.causalityCombo.blockSignals(False)
def _show_type_options(self, port_type: str) -> None:
self.ui.orientationCombo.model().item(2).setEnabled(port_type == "power")
page = {
"signal": self.ui.signalOptionsPage,
"power": self.ui.powerOptionsPage,
}.get(port_type, self.ui.unsupportedTypePage)
self.ui.typeOptionsStack.setCurrentWidget(page)
def _power_domain_changed(self) -> None:
domain = power_domain(self.ui.domainCombo.currentData())
self.ui.effortValueLabel.setText(domain.effort)
self.ui.flowValueLabel.setText(domain.flow)
self._store_current()
def _store_current(self) -> None:
row = self.ui.portList.currentRow()
if self._loading or not (0 <= row < len(self.ports)):
return
port = self.ports[row]
port.name = self.ui.nameEdit.text()
port.type = self.ui.typeCombo.currentData()
port.allows_multiple_connections = self.ui.multipleConnectionsCheckBox.isChecked()
port.value_type = self.ui.valueTypeCombo.currentText().strip() or "real"
port.quantity = self.ui.quantityCombo.currentText().strip()
port.unit = self.ui.unitCombo.currentText().strip()
port.rows = self.ui.rowsSpin.value()
port.columns = self.ui.columnsSpin.value()
port.domain = self.ui.domainCombo.currentData() or "power"
port.causality = self.ui.causalityCombo.currentText()
port.description = (
self.ui.powerDescriptionEdit.toPlainText()
if port.type == "power"
else self.ui.descriptionEdit.toPlainText()
)
orientation = self.ui.orientationCombo.currentData()
port.orientation = orientation
self.ui.portList.item(row).setText(f"{port.name} [{port.orientation}, {port.type}]")
self.edited.emit()
def set_ports(self, ports: list[Port]) -> None:
self._loading = True
self.ports = deepcopy(ports)
self._loading = False
self._rebuild_list(0 if self.ports else -1)
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)
self._rebuild_list(len(self.ports) - 1)
self.ui.nameEdit.selectAll()
self.ui.nameEdit.setFocus()
self.edited.emit()
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))
self.edited.emit()
def accept(self) -> None:
self._store_current()
if any(not port.name.strip() for port in self.ports):
QMessageBox.warning(self, "Invalid port", "Every port must have a name.")
return
super().accept()
class PortEditor(PortOptionsDialog):
"""The port options editor embedded without dialog confirmation buttons."""
def __init__(self, parent=None) -> None:
super().__init__(Component(id="embedded-port-editor", name="Ports"), parent)
self.setWindowFlags(Qt.WindowType.Widget)
self.ui.buttonBox.hide()

View File

@@ -0,0 +1,240 @@
from pathlib import Path
from PySide6.QtCore import QSettings, Qt, Signal
from PySide6.QtGui import QColor
from PySide6.QtWidgets import QColorDialog, QDialog, QFileDialog, QHeaderView, QTableWidgetItem
from bedit.core.libraries import bundled_library_path, default_library_paths
from bedit.gui.preferences import application_settings
from bedit.gui.generated.ui_settings_dialog import Ui_SettingsDialog
from bedit.gui.editors.openmodelica import HIGHLIGHT_STYLES
class SettingsDialog(QDialog):
"""Edit application preferences defined in the Designer form."""
settingsChanged = Signal()
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.ui = Ui_SettingsDialog()
self.ui.setupUi(self)
self.ui.graphGridSpinBox.valueChanged.connect(
self.ui.graphSnapSpinBox.setMaximum
)
self.settings = application_settings()
self.ui.addLibraryFileButton.clicked.connect(self._add_library_file)
self.ui.addLibraryFolderButton.clicked.connect(self._add_library_folder)
self.ui.removeLibraryPathButton.clicked.connect(self._remove_library_path)
self.ui.browseOpenModelicaButton.clicked.connect(self._browse_openmodelica)
self.ui.libraryPathsList.itemSelectionChanged.connect(self._update_remove_button)
self.ui.syntaxStylesTable.cellDoubleClicked.connect(self._choose_syntax_color)
self._load_settings()
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._as_bool(enabled)
)
self.ui.autosaveIntervalSpinBox.setValue(int(interval))
self.ui.libraryPathsList.clear()
self.ui.libraryPathsList.addItems(self.library_paths(self.settings))
self.ui.graphGridSpinBox.setValue(self.graph_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.ui.automaticCausalityCheckBox.setChecked(
self.infer_causality_on_connection_change(self.settings)
)
self.ui.openModelicaPathEdit.setText(self.openmodelica_path(self.settings))
self._load_syntax_styles()
self._update_remove_button()
def _load_syntax_styles(self) -> None:
table = self.ui.syntaxStylesTable
table.setRowCount(len(HIGHLIGHT_STYLES))
table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
for row, (category, defaults) in enumerate(HIGHLIGHT_STYLES.items()):
label, default_color, default_bold, default_italic = defaults
name_item = QTableWidgetItem(label)
name_item.setData(Qt.ItemDataRole.UserRole, category)
name_item.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable)
table.setItem(row, 0, name_item)
color = str(self.settings.value(f"syntax/{category}/color", default_color))
color_item = QTableWidgetItem(color)
color_item.setBackground(QColor(color))
color_item.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable)
table.setItem(row, 1, color_item)
for column, key, default in (
(2, "bold", default_bold),
(3, "italic", default_italic),
):
item = QTableWidgetItem()
item.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsUserCheckable)
enabled = self._as_bool(
self.settings.value(f"syntax/{category}/{key}", default)
)
item.setCheckState(
Qt.CheckState.Checked if enabled else Qt.CheckState.Unchecked
)
table.setItem(row, column, item)
def _choose_syntax_color(self, row: int, column: int) -> None:
if column != 1:
return
item = self.ui.syntaxStylesTable.item(row, column)
color = QColorDialog.getColor(QColor(item.text()), self, "Highlight colour")
if color.isValid():
item.setText(color.name())
item.setBackground(color)
@staticmethod
def _as_bool(value) -> bool:
if isinstance(value, str):
return value.strip().lower() in {"1", "true", "yes", "on"}
return bool(value)
@staticmethod
def library_paths(settings: QSettings | None = None) -> list[str]:
settings = settings if settings is not None else application_settings()
value = settings.value("libraries/paths", default_library_paths())
if isinstance(value, str):
paths = [value]
else:
paths = [str(path) for path in value]
bundled = bundled_library_path()
migrated = [
str(bundled)
if not Path(path).exists()
and Path(path).parent == bundled.parent
and Path(path).suffix.lower() == ".json"
else path
for path in paths
]
if migrated != paths:
settings.setValue("libraries/paths", migrated)
settings.sync()
return migrated
@staticmethod
def graph_grid_size(settings: QSettings | None = None) -> 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
def icon_grid_size(settings: QSettings | None = None) -> int:
settings = settings if settings is not None else application_settings()
return settings.value("grid/iconSize", 8, type=int)
@staticmethod
def infer_causality_on_connection_change(
settings: QSettings | None = None,
) -> bool:
settings = settings if settings is not None else application_settings()
return settings.value(
"bondGraph/inferCausalityOnConnectionChange", True, type=bool
)
@staticmethod
def openmodelica_path(settings: QSettings | None = None) -> str:
settings = settings if settings is not None else application_settings()
return str(settings.value("simulation/openModelicaPath", "") or "")
def _browse_openmodelica(self) -> None:
current = self.ui.openModelicaPathEdit.text().strip()
start = str(Path(current).expanduser().parent) if current else ""
path, _ = QFileDialog.getOpenFileName(
self,
"Select OpenModelica executable",
start,
"OpenModelica compiler (omc omc.exe);;All files (*)",
)
if path:
# Keep symlink paths such as ~/.local/bin/omc intact; resolving them
# could turn the selected launcher into an unrelated container script.
self.ui.openModelicaPathEdit.setText(str(Path(path).expanduser().absolute()))
def _add_library_file(self) -> None:
path, _ = QFileDialog.getOpenFileName(
self,
"Add library",
"",
"BEdit libraries (*.beb *.bedit.json *.json);;All files (*)",
)
if path:
self._append_unique_path(path)
def _add_library_folder(self) -> None:
path = QFileDialog.getExistingDirectory(self, "Add library folder")
if path:
self._append_unique_path(path)
def _append_unique_path(self, path: str) -> None:
normalized = str(Path(path).expanduser().resolve())
existing = {
self.ui.libraryPathsList.item(row).text()
for row in range(self.ui.libraryPathsList.count())
}
if normalized not in existing:
self.ui.libraryPathsList.addItem(normalized)
def _remove_library_path(self) -> None:
for item in self.ui.libraryPathsList.selectedItems():
self.ui.libraryPathsList.takeItem(self.ui.libraryPathsList.row(item))
def _update_remove_button(self) -> None:
self.ui.removeLibraryPathButton.setEnabled(bool(self.ui.libraryPathsList.selectedItems()))
def accept(self) -> None:
self.settings.setValue("autosave/enabled", self.ui.autosaveGroupBox.isChecked())
self.settings.setValue(
"autosave/intervalMinutes", self.ui.autosaveIntervalSpinBox.value()
)
self.settings.remove("General/autosaveEnabled")
self.settings.remove("General/autosaveInterval")
paths = [
self.ui.libraryPathsList.item(row).text()
for row in range(self.ui.libraryPathsList.count())
]
self.settings.setValue("libraries/paths", paths)
self.settings.setValue("grid/graphSize", self.ui.graphGridSpinBox.value())
self.settings.setValue("grid/graphSnapSize", self.ui.graphSnapSpinBox.value())
self.settings.setValue("grid/iconSize", self.ui.iconGridSpinBox.value())
self.settings.setValue(
"bondGraph/inferCausalityOnConnectionChange",
self.ui.automaticCausalityCheckBox.isChecked(),
)
self.settings.setValue(
"simulation/openModelicaPath",
self.ui.openModelicaPathEdit.text().strip(),
)
for row in range(self.ui.syntaxStylesTable.rowCount()):
category = self.ui.syntaxStylesTable.item(row, 0).data(
Qt.ItemDataRole.UserRole
)
self.settings.setValue(
f"syntax/{category}/color",
self.ui.syntaxStylesTable.item(row, 1).text(),
)
self.settings.setValue(
f"syntax/{category}/bold",
self.ui.syntaxStylesTable.item(row, 2).checkState()
== Qt.CheckState.Checked,
)
self.settings.setValue(
f"syntax/{category}/italic",
self.ui.syntaxStylesTable.item(row, 3).checkState()
== Qt.CheckState.Checked,
)
self.settings.sync()
self.settingsChanged.emit()
super().accept()

View File

@@ -0,0 +1,60 @@
from copy import deepcopy
from PySide6.QtWidgets import QButtonGroup, QDialog
from bedit.gui.generated.ui_simulation_settings_dialog import Ui_SimulationSettingsDialog
class SimulationSettingsDialog(QDialog):
def __init__(self, settings: dict, parent=None) -> None:
super().__init__(parent)
self.ui = Ui_SimulationSettingsDialog()
self.ui.setupUi(self)
self._settings = deepcopy(settings)
self.interval_mode_group = QButtonGroup(self)
self.interval_mode_group.setExclusive(True)
self.interval_mode_group.addButton(self.ui.numberOfIntervalsRadioButton)
self.interval_mode_group.addButton(self.ui.intervalTimeRadioButton)
self.ui.startTimeSpinBox.setValue(float(settings.get("startTime", 0.0)))
self.ui.stopTimeSpinBox.setValue(float(settings.get("stopTime", 1.0)))
self.ui.numberOfIntervalsSpinBox.setValue(
int(settings.get("numberOfIntervals", 500))
)
self.ui.intervalTimeSpinBox.setValue(float(settings.get("intervalTime", 0.002)))
interval_mode = settings.get("intervalMode", "numberOfIntervals")
if interval_mode == "intervalTime":
self.ui.intervalTimeRadioButton.setChecked(True)
else:
self.ui.numberOfIntervalsRadioButton.setChecked(True)
self.ui.numberOfIntervalsRadioButton.toggled.connect(
self._update_interval_fields
)
self.ui.intervalTimeRadioButton.toggled.connect(self._update_interval_fields)
self._update_interval_fields()
def _update_interval_fields(self) -> None:
use_number = self.ui.numberOfIntervalsRadioButton.isChecked()
self.ui.numberOfIntervalsSpinBox.setEnabled(use_number)
self.ui.intervalTimeSpinBox.setEnabled(not use_number)
@property
def settings(self) -> dict:
values = deepcopy(self._settings)
values.update(
{
"startTime": self.ui.startTimeSpinBox.value(),
"stopTime": self.ui.stopTimeSpinBox.value(),
"intervalMode": (
"numberOfIntervals"
if self.ui.numberOfIntervalsRadioButton.isChecked()
else "intervalTime"
),
"numberOfIntervals": self.ui.numberOfIntervalsSpinBox.value(),
"intervalTime": self.ui.intervalTimeSpinBox.value(),
}
)
return values

View File

@@ -0,0 +1 @@
"""Reusable editing widgets."""

View File

@@ -0,0 +1,252 @@
import json
from importlib.resources import files
from PySide6.QtCore import QRegularExpression, QStringListModel, Qt
from PySide6.QtGui import (
QColor,
QFont,
QKeyEvent,
QSyntaxHighlighter,
QTextCharFormat,
QTextCursor,
)
from PySide6.QtWidgets import QCompleter, QPlainTextEdit
from bedit.gui.preferences import application_settings
HIGHLIGHT_STYLES = {
"keywords": ("Keywords", "#7c3aed", True, False),
"types": ("Types", "#0369a1", True, False),
"builtins": ("Built-ins", "#0f766e", False, False),
"bevalues": ("BEvalues", "#c026d3", True, False),
"inputs": ("Inputs", "#1d4ed8", False, False),
"outputs": ("Outputs", "#be123c", False, False),
"parameters": ("Parameters", "#a16207", False, False),
"numbers": ("Numbers", "#b45309", False, False),
"strings": ("Strings", "#15803d", False, False),
"comments": ("Comments", "#6b7280", False, True),
}
def load_openmodelica_syntax() -> dict[str, list[str]]:
resource = files("bedit").joinpath("data/syntax/openmodelica.json")
with resource.open(encoding="utf-8") as file:
values = json.load(file)
return {
category: [str(word) for word in values.get(category, [])]
for category in ("keywords", "types", "builtins", "bevalues")
}
def _format(color: str, *, bold: bool = False, italic: bool = False) -> QTextCharFormat:
value = QTextCharFormat()
value.setForeground(QColor(color))
value.setFontWeight(QFont.Weight.Bold if bold else QFont.Weight.Normal)
value.setFontItalic(italic)
return value
def _as_bool(value) -> bool:
if isinstance(value, str):
return value.strip().lower() in {"1", "true", "yes", "on"}
return bool(value)
def highlighting_style(category: str) -> tuple[str, bool, bool]:
_label, default_color, default_bold, default_italic = HIGHLIGHT_STYLES[category]
settings = application_settings()
prefix = f"syntax/{category}"
return (
str(settings.value(f"{prefix}/color", default_color)),
_as_bool(settings.value(f"{prefix}/bold", default_bold)),
_as_bool(settings.value(f"{prefix}/italic", default_italic)),
)
class OpenModelicaHighlighter(QSyntaxHighlighter):
"""Syntax highlighter driven by the editable OpenModelica word list."""
def __init__(
self,
document,
syntax: dict[str, list[str]],
symbols: dict[str, list[str]],
) -> None:
super().__init__(document)
self.rules: list[tuple[QRegularExpression, QTextCharFormat]] = []
for category in (
"keywords",
"types",
"builtins",
"inputs",
"outputs",
"parameters",
):
words = syntax.get(category, symbols.get(category, []))
if words:
pattern = r"\b(?:" + "|".join(map(QRegularExpression.escape, words)) + r")\b"
color, bold, italic = highlighting_style(category)
self.rules.append(
(QRegularExpression(pattern), _format(color, bold=bold, italic=italic))
)
bevalue_style = highlighting_style("bevalues")
number_style = highlighting_style("numbers")
string_style = highlighting_style("strings")
self.rules.extend(
[
(
QRegularExpression(r"\$[A-Za-z_][A-Za-z0-9_]*\$"),
_format(
bevalue_style[0],
bold=bevalue_style[1],
italic=bevalue_style[2],
),
),
(
QRegularExpression(r"\b(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?\b"),
_format(number_style[0], bold=number_style[1], italic=number_style[2]),
),
(
QRegularExpression(r'"(?:\\.|[^"\\])*"'),
_format(string_style[0], bold=string_style[1], italic=string_style[2]),
),
]
)
comment_style = highlighting_style("comments")
self.comment_format = _format(
comment_style[0], bold=comment_style[1], italic=comment_style[2]
)
self.rules.append((QRegularExpression(r"//.*$"), self.comment_format))
self.comment_start = QRegularExpression(r"/\*")
self.comment_end = QRegularExpression(r"\*/")
def highlightBlock(self, text: str) -> None: # noqa: N802
for expression, text_format in self.rules:
match = expression.globalMatch(text)
while match.hasNext():
result = match.next()
self.setFormat(result.capturedStart(), result.capturedLength(), text_format)
self.setCurrentBlockState(0)
start = (
0
if self.previousBlockState() == 1
else self.comment_start.match(text).capturedStart()
)
while start >= 0:
end_match = self.comment_end.match(text, start + 2)
if end_match.hasMatch():
length = end_match.capturedEnd() - start
else:
self.setCurrentBlockState(1)
length = len(text) - start
self.setFormat(start, length, self.comment_format)
if not end_match.hasMatch():
break
start = self.comment_start.match(text, start + length).capturedStart()
class OpenModelicaEditor(QPlainTextEdit):
"""OpenModelica text editor with syntax highlighting and completion."""
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
self.setPlaceholderText("Enter OpenModelica equations here…")
self.syntax = load_openmodelica_syntax()
self.symbols = {"inputs": [], "outputs": [], "parameters": []}
self.highlighter = OpenModelicaHighlighter(
self.document(), self.syntax, self.symbols
)
self.completer = QCompleter(self)
self.completer.setWidget(self)
self.completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
self.completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
self.completer.activated.connect(self._insert_completion)
self._rebuild_completions()
def set_symbols(
self,
inputs: list[str],
outputs: list[str],
parameters: list[str],
) -> None:
self.symbols = {
"inputs": [name for name in inputs if name],
"outputs": [name for name in outputs if name],
"parameters": [name for name in parameters if name],
}
self.reload_highlighting()
def reload_highlighting(self) -> None:
self.highlighter.setDocument(None)
self.highlighter = OpenModelicaHighlighter(
self.document(), self.syntax, self.symbols
)
self._rebuild_completions()
def _rebuild_completions(self) -> None:
words = sorted(
{
*self.syntax["keywords"],
*self.syntax["types"],
*self.syntax["builtins"],
*(f"${name.strip('$')}$" for name in self.syntax["bevalues"]),
*self.symbols["inputs"],
*self.symbols["outputs"],
*self.symbols["parameters"],
},
key=str.casefold,
)
self.completer.setModel(QStringListModel(words, self.completer))
def _completion_prefix(self) -> str:
cursor = self.textCursor()
text = cursor.block().text()[: cursor.positionInBlock()]
index = len(text)
while index > 0 and (text[index - 1].isalnum() or text[index - 1] in "_$"):
index -= 1
return text[index:]
def _insert_completion(self, completion: str) -> None:
prefix = self._completion_prefix()
cursor = self.textCursor()
cursor.movePosition(
QTextCursor.MoveOperation.Left,
QTextCursor.MoveMode.KeepAnchor,
len(prefix),
)
cursor.insertText(completion)
self.setTextCursor(cursor)
def keyPressEvent(self, event: QKeyEvent) -> None: # noqa: N802
popup = self.completer.popup()
if popup.isVisible() and event.key() in {
Qt.Key.Key_Enter,
Qt.Key.Key_Return,
Qt.Key.Key_Escape,
Qt.Key.Key_Tab,
Qt.Key.Key_Backtab,
}:
event.ignore()
return
explicit = (
event.modifiers() == Qt.KeyboardModifier.ControlModifier
and event.key() == Qt.Key.Key_Space
)
if not explicit:
super().keyPressEvent(event)
prefix = self._completion_prefix()
if not explicit and (len(prefix) < 2 or event.text() == ""):
popup.hide()
return
self.completer.setCompletionPrefix(prefix)
if self.completer.completionCount() == 0:
popup.hide()
return
rectangle = self.cursorRect()
rectangle.setWidth(
popup.sizeHintForColumn(0) + popup.verticalScrollBar().sizeHint().width()
)
self.completer.complete(rectangle)

View File

@@ -0,0 +1,110 @@
from PySide6.QtCore import Signal
from PySide6.QtWidgets import QWidget
from bedit.core.model import Parameter, Port
from bedit.gui.generated.ui_text_definition_editor import Ui_TextDefinitionEditor
class TextDefinitionEditor(QWidget):
"""Editor for a text component's Modelica source, ports, and parameters."""
modifiedChanged = Signal(bool)
definitionEdited = Signal()
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.ui = Ui_TextDefinitionEditor()
self.ui.setupUi(self)
self._modified = False
self._loading = False
self.ui.columnSplitter.setSizes([560, 340])
self.ui.sourceSplitter.setSizes([180, 180, 240])
self.ui.definitionSplitter.setSizes([300, 300])
self.ui.declarationsEdit.textChanged.connect(self._mark_modified)
self.ui.initialEquationsEdit.textChanged.connect(self._mark_modified)
self.ui.equationsEdit.textChanged.connect(self._mark_modified)
self.ui.portEditor.edited.connect(self._symbols_modified)
self.ui.parameterEditor.edited.connect(self._symbols_modified)
@property
def is_modified(self) -> bool:
return self._modified
@property
def equations(self) -> str:
return self.ui.equationsEdit.toPlainText()
@property
def declarations(self) -> str:
return self.ui.declarationsEdit.toPlainText()
@property
def initial_equations(self) -> str:
return self.ui.initialEquationsEdit.toPlainText()
@property
def ports(self) -> list[Port]:
return self.ui.portEditor.ports
@property
def parameters(self) -> list[Parameter]:
return self.ui.parameterEditor.parameters
def set_definition(
self,
declarations: str,
initial_equations: str,
equations: str,
ports: list[Port],
parameters: list[Parameter],
) -> None:
self._loading = True
self.ui.declarationsEdit.setPlainText(declarations)
self.ui.initialEquationsEdit.setPlainText(initial_equations)
self.ui.equationsEdit.setPlainText(equations)
self.ui.portEditor.set_ports(ports)
self.ui.parameterEditor.set_parameters(parameters)
self._set_editor_symbols(ports, parameters)
self._loading = False
self.set_modified(False)
def set_modified(self, modified: bool) -> None:
if self._modified != modified:
self._modified = modified
self.modifiedChanged.emit(modified)
def _mark_modified(self, *_args) -> None:
if not self._loading:
self.set_modified(True)
self.definitionEdited.emit()
def _symbols_modified(self, *_args) -> None:
if not self._loading:
self._refresh_editor_symbols()
self._mark_modified()
def _refresh_editor_symbols(self) -> None:
self._set_editor_symbols(self.ports, self.parameters)
def _set_editor_symbols(
self,
ports: list[Port],
parameters: list[Parameter],
) -> None:
self._apply_editor_symbols(
[
port.name
for port in ports
if port.orientation in {"input", "indifferent"}
],
[port.name for port in ports if port.orientation == "output"],
[parameter.name for parameter in parameters],
)
def _apply_editor_symbols(
self, inputs: list[str], outputs: list[str], parameters: list[str]
) -> None:
symbols = (inputs, outputs, parameters)
self.ui.declarationsEdit.set_symbols(*symbols)
self.ui.initialEquationsEdit.set_symbols(*symbols)
self.ui.equationsEdit.set_symbols(*symbols)

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'component_options_dialog.ui'
##
## Created by: Qt User Interface Compiler version 6.11.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractButton, QApplication, QCheckBox, QDialog,
QDialogButtonBox, QFormLayout, QLabel, QLineEdit,
QPushButton, QSizePolicy, QSpacerItem, QVBoxLayout,
QWidget)
class Ui_ComponentOptionsDialog(object):
def setupUi(self, ComponentOptionsDialog):
if not ComponentOptionsDialog.objectName():
ComponentOptionsDialog.setObjectName(u"ComponentOptionsDialog")
ComponentOptionsDialog.resize(420, 260)
self.dialogLayout = QVBoxLayout(ComponentOptionsDialog)
self.dialogLayout.setObjectName(u"dialogLayout")
self.optionsForm = QFormLayout()
self.optionsForm.setObjectName(u"optionsForm")
self.nameLabel = QLabel(ComponentOptionsDialog)
self.nameLabel.setObjectName(u"nameLabel")
self.optionsForm.setWidget(0, QFormLayout.ItemRole.LabelRole, self.nameLabel)
self.nameEdit = QLineEdit(ComponentOptionsDialog)
self.nameEdit.setObjectName(u"nameEdit")
self.optionsForm.setWidget(0, QFormLayout.ItemRole.FieldRole, self.nameEdit)
self.iconLabel = QLabel(ComponentOptionsDialog)
self.iconLabel.setObjectName(u"iconLabel")
self.optionsForm.setWidget(1, QFormLayout.ItemRole.LabelRole, self.iconLabel)
self.editIconButton = QPushButton(ComponentOptionsDialog)
self.editIconButton.setObjectName(u"editIconButton")
self.optionsForm.setWidget(1, QFormLayout.ItemRole.FieldRole, self.editIconButton)
self.showSubtreeCheckBox = QCheckBox(ComponentOptionsDialog)
self.showSubtreeCheckBox.setObjectName(u"showSubtreeCheckBox")
self.showSubtreeCheckBox.setChecked(True)
self.optionsForm.setWidget(2, QFormLayout.ItemRole.SpanningRole, self.showSubtreeCheckBox)
self.showNameCheckBox = QCheckBox(ComponentOptionsDialog)
self.showNameCheckBox.setObjectName(u"showNameCheckBox")
self.optionsForm.setWidget(3, QFormLayout.ItemRole.SpanningRole, self.showNameCheckBox)
self.dialogLayout.addLayout(self.optionsForm)
self.optionsSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
self.dialogLayout.addItem(self.optionsSpacer)
self.buttonBox = QDialogButtonBox(ComponentOptionsDialog)
self.buttonBox.setObjectName(u"buttonBox")
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
self.dialogLayout.addWidget(self.buttonBox)
self.retranslateUi(ComponentOptionsDialog)
self.buttonBox.accepted.connect(ComponentOptionsDialog.accept)
self.buttonBox.rejected.connect(ComponentOptionsDialog.reject)
QMetaObject.connectSlotsByName(ComponentOptionsDialog)
# setupUi
def retranslateUi(self, ComponentOptionsDialog):
ComponentOptionsDialog.setWindowTitle(QCoreApplication.translate("ComponentOptionsDialog", u"Component Options", None))
self.nameLabel.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Name:", None))
self.iconLabel.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Icon:", None))
self.editIconButton.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Edit Icon\u2026", None))
#if QT_CONFIG(tooltip)
self.editIconButton.setToolTip(QCoreApplication.translate("ComponentOptionsDialog", u"Open the vector icon and port-position editor", None))
#endif // QT_CONFIG(tooltip)
self.showSubtreeCheckBox.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Show contained components in the Libraries tree", None))
self.showNameCheckBox.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Show name below component", None))
# retranslateUi

View File

@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'connection_chooser_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,
QLabel, QListWidget, QListWidgetItem, QSizePolicy,
QVBoxLayout, QWidget)
class Ui_ConnectionChooserDialog(object):
def setupUi(self, ConnectionChooserDialog):
if not ConnectionChooserDialog.objectName():
ConnectionChooserDialog.setObjectName(u"ConnectionChooserDialog")
ConnectionChooserDialog.resize(460, 280)
self.dialogLayout = QVBoxLayout(ConnectionChooserDialog)
self.dialogLayout.setObjectName(u"dialogLayout")
self.promptLabel = QLabel(ConnectionChooserDialog)
self.promptLabel.setObjectName(u"promptLabel")
self.dialogLayout.addWidget(self.promptLabel)
self.connectionList = QListWidget(ConnectionChooserDialog)
self.connectionList.setObjectName(u"connectionList")
self.connectionList.setAlternatingRowColors(True)
self.dialogLayout.addWidget(self.connectionList)
self.buttonBox = QDialogButtonBox(ConnectionChooserDialog)
self.buttonBox.setObjectName(u"buttonBox")
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
self.dialogLayout.addWidget(self.buttonBox)
self.retranslateUi(ConnectionChooserDialog)
self.buttonBox.accepted.connect(ConnectionChooserDialog.accept)
self.buttonBox.rejected.connect(ConnectionChooserDialog.reject)
self.connectionList.itemDoubleClicked.connect(ConnectionChooserDialog.accept)
QMetaObject.connectSlotsByName(ConnectionChooserDialog)
# setupUi
def retranslateUi(self, ConnectionChooserDialog):
ConnectionChooserDialog.setWindowTitle(QCoreApplication.translate("ConnectionChooserDialog", u"Select a Connection", None))
self.promptLabel.setText(QCoreApplication.translate("ConnectionChooserDialog", u"Select a connection:", None))
# retranslateUi

View File

@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'graph_parameters_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,
QHeaderView, QLabel, QSizePolicy, QTreeWidget,
QTreeWidgetItem, QVBoxLayout, QWidget)
class Ui_GraphParametersDialog(object):
def setupUi(self, GraphParametersDialog):
if not GraphParametersDialog.objectName():
GraphParametersDialog.setObjectName(u"GraphParametersDialog")
GraphParametersDialog.resize(620, 480)
self.dialogLayout = QVBoxLayout(GraphParametersDialog)
self.dialogLayout.setObjectName(u"dialogLayout")
self.descriptionLabel = QLabel(GraphParametersDialog)
self.descriptionLabel.setObjectName(u"descriptionLabel")
self.dialogLayout.addWidget(self.descriptionLabel)
self.parameterTree = QTreeWidget(GraphParametersDialog)
self.parameterTree.setObjectName(u"parameterTree")
self.parameterTree.setAlternatingRowColors(True)
self.parameterTree.setRootIsDecorated(True)
self.parameterTree.setColumnCount(3)
self.dialogLayout.addWidget(self.parameterTree)
self.buttonBox = QDialogButtonBox(GraphParametersDialog)
self.buttonBox.setObjectName(u"buttonBox")
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
self.dialogLayout.addWidget(self.buttonBox)
self.retranslateUi(GraphParametersDialog)
self.buttonBox.accepted.connect(GraphParametersDialog.accept)
self.buttonBox.rejected.connect(GraphParametersDialog.reject)
QMetaObject.connectSlotsByName(GraphParametersDialog)
# setupUi
def retranslateUi(self, GraphParametersDialog):
GraphParametersDialog.setWindowTitle(QCoreApplication.translate("GraphParametersDialog", u"Graph Parameters", None))
self.descriptionLabel.setText(QCoreApplication.translate("GraphParametersDialog", u"Edit parameter values throughout the active component tree.", None))
___qtreewidgetitem = self.parameterTree.headerItem()
___qtreewidgetitem.setText(2, QCoreApplication.translate("GraphParametersDialog", u"Value", None))
___qtreewidgetitem.setText(1, QCoreApplication.translate("GraphParametersDialog", u"Type", None))
___qtreewidgetitem.setText(0, QCoreApplication.translate("GraphParametersDialog", u"Component / Parameter", None))
# retranslateUi

View File

@@ -0,0 +1,191 @@
# -*- 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
from . import resources_rc
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")
icon = QIcon()
icon.addFile(u":/icons/icons/edit-select.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.pointerButton.setIcon(icon)
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")
icon1 = QIcon()
icon1.addFile(u":/icons/icons/draw-rectangle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.addRectangleButton.setIcon(icon1)
self.addRectangleButton.setCheckable(True)
self.shapeToolbarLayout.addWidget(self.addRectangleButton)
self.addCircleButton = QToolButton(IconEditorDialog)
self.addCircleButton.setObjectName(u"addCircleButton")
icon2 = QIcon()
icon2.addFile(u":/icons/icons/draw-circle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.addCircleButton.setIcon(icon2)
self.addCircleButton.setCheckable(True)
self.shapeToolbarLayout.addWidget(self.addCircleButton)
self.addEllipseButton = QToolButton(IconEditorDialog)
self.addEllipseButton.setObjectName(u"addEllipseButton")
icon3 = QIcon()
icon3.addFile(u":/icons/icons/draw-ellipse.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.addEllipseButton.setIcon(icon3)
self.addEllipseButton.setCheckable(True)
self.shapeToolbarLayout.addWidget(self.addEllipseButton)
self.addLineButton = QToolButton(IconEditorDialog)
self.addLineButton.setObjectName(u"addLineButton")
icon4 = QIcon()
icon4.addFile(u":/icons/icons/draw-bezier-curves.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.addLineButton.setIcon(icon4)
self.addLineButton.setCheckable(True)
self.shapeToolbarLayout.addWidget(self.addLineButton)
self.addTriangleButton = QToolButton(IconEditorDialog)
self.addTriangleButton.setObjectName(u"addTriangleButton")
icon5 = QIcon()
icon5.addFile(u":/icons/icons/draw-triangle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.addTriangleButton.setIcon(icon5)
self.addTriangleButton.setCheckable(True)
self.shapeToolbarLayout.addWidget(self.addTriangleButton)
self.addTextButton = QToolButton(IconEditorDialog)
self.addTextButton.setObjectName(u"addTextButton")
icon6 = QIcon()
icon6.addFile(u":/icons/icons/draw-text.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.addTextButton.setIcon(icon6)
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")
icon7 = QIcon()
icon7.addFile(u":/icons/icons/zoom-in.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.zoomInButton.setIcon(icon7)
self.shapeToolbarLayout.addWidget(self.zoomInButton)
self.zoomOutButton = QToolButton(IconEditorDialog)
self.zoomOutButton.setObjectName(u"zoomOutButton")
icon8 = QIcon()
icon8.addFile(u":/icons/icons/zoom-out.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.zoomOutButton.setIcon(icon8)
self.shapeToolbarLayout.addWidget(self.zoomOutButton)
self.centerButton = QToolButton(IconEditorDialog)
self.centerButton.setObjectName(u"centerButton")
icon9 = QIcon()
icon9.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.centerButton.setIcon(icon9)
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))
#if QT_CONFIG(tooltip)
self.zoomInButton.setToolTip(QCoreApplication.translate("IconEditorDialog", u"Zoom in", None))
#endif // QT_CONFIG(tooltip)
self.zoomInButton.setText(QCoreApplication.translate("IconEditorDialog", u"+", None))
#if QT_CONFIG(tooltip)
self.zoomOutButton.setToolTip(QCoreApplication.translate("IconEditorDialog", u"Zoom out", None))
#endif // QT_CONFIG(tooltip)
self.zoomOutButton.setText(QCoreApplication.translate("IconEditorDialog", u"\u2212", None))
#if QT_CONFIG(tooltip)
self.centerButton.setToolTip(QCoreApplication.translate("IconEditorDialog", u"Fit and center the icon canvas", None))
#endif // QT_CONFIG(tooltip)
self.centerButton.setText(QCoreApplication.translate("IconEditorDialog", u"Fit", None))
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

@@ -0,0 +1,653 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'main_window.ui'
##
## Created by: Qt User Interface Compiler version 6.11.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
QCursor, QFont, QFontDatabase, QGradient,
QIcon, QImage, QKeySequence, QLinearGradient,
QPainter, QPalette, QPixmap, QRadialGradient,
QTransform)
from PySide6.QtWidgets import (QApplication, QDockWidget, QFrame, QHBoxLayout,
QHeaderView, QLabel, QMainWindow, QMenu,
QMenuBar, QPlainTextEdit, QSizePolicy, QSpacerItem,
QSplitter, QStackedWidget, QTabWidget, QToolBar,
QToolButton, QTreeView, QVBoxLayout, QWidget)
from bedit.gui.editors.text_definition import TextDefinitionEditor
from bedit.gui.graphics.workspace import GraphWorkspaceView
from . import resources_rc
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
if not MainWindow.objectName():
MainWindow.setObjectName(u"MainWindow")
MainWindow.resize(1209, 777)
self.actionSimulationSettings = QAction(MainWindow)
self.actionSimulationSettings.setObjectName(u"actionSimulationSettings")
icon = QIcon()
icon.addFile(u":/icons/icons/preferences-system.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSimulationSettings.setIcon(icon)
self.actionGraphParameters = QAction(MainWindow)
self.actionGraphParameters.setObjectName(u"actionGraphParameters")
icon1 = QIcon()
icon1.addFile(u":/icons/icons/view-form-table.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionGraphParameters.setIcon(icon1)
self.actionCompose = QAction(MainWindow)
self.actionCompose.setObjectName(u"actionCompose")
icon2 = QIcon()
icon2.addFile(u":/icons/icons/run-build.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCompose.setIcon(icon2)
self.actionRunSimulation = QAction(MainWindow)
self.actionRunSimulation.setObjectName(u"actionRunSimulation")
icon3 = QIcon()
icon3.addFile(u":/icons/icons/media-playback-start.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRunSimulation.setIcon(icon3)
self.actionSimulationWindow = QAction(MainWindow)
self.actionSimulationWindow.setObjectName(u"actionSimulationWindow")
icon4 = QIcon()
icon4.addFile(u":/icons/icons/office-chart-line.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSimulationWindow.setIcon(icon4)
self.actionExportModel = QAction(MainWindow)
self.actionExportModel.setObjectName(u"actionExportModel")
icon5 = QIcon()
icon5.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionExportModel.setIcon(icon5)
self.actionNew = QAction(MainWindow)
self.actionNew.setObjectName(u"actionNew")
icon6 = QIcon()
icon6.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionNew.setIcon(icon6)
self.actionRotateClockwise = QAction(MainWindow)
self.actionRotateClockwise.setObjectName(u"actionRotateClockwise")
icon7 = QIcon()
icon7.addFile(u":/icons/icons/transform-rotate.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRotateClockwise.setIcon(icon7)
self.actionZoomIn = QAction(MainWindow)
self.actionZoomIn.setObjectName(u"actionZoomIn")
icon8 = QIcon()
icon8.addFile(u":/icons/icons/zoom-in.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionZoomIn.setIcon(icon8)
self.actionZoomOut = QAction(MainWindow)
self.actionZoomOut.setObjectName(u"actionZoomOut")
icon9 = QIcon()
icon9.addFile(u":/icons/icons/zoom-out.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionZoomOut.setIcon(icon9)
self.actionCenterView = QAction(MainWindow)
self.actionCenterView.setObjectName(u"actionCenterView")
icon10 = QIcon()
icon10.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCenterView.setIcon(icon10)
self.actionOpen = QAction(MainWindow)
self.actionOpen.setObjectName(u"actionOpen")
icon11 = QIcon()
icon11.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionOpen.setIcon(icon11)
self.actionReloadLibraries = QAction(MainWindow)
self.actionReloadLibraries.setObjectName(u"actionReloadLibraries")
self.actionReloadSimulation = QAction(MainWindow)
self.actionReloadSimulation.setObjectName(u"actionReloadSimulation")
self.actionSave = QAction(MainWindow)
self.actionSave.setObjectName(u"actionSave")
icon12 = QIcon()
icon12.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSave.setIcon(icon12)
self.actionSaveAs = QAction(MainWindow)
self.actionSaveAs.setObjectName(u"actionSaveAs")
self.actionSaveAs.setIcon(icon5)
self.actionExit = QAction(MainWindow)
self.actionExit.setObjectName(u"actionExit")
self.actionClose = QAction(MainWindow)
self.actionClose.setObjectName(u"actionClose")
self.actionUndo = QAction(MainWindow)
self.actionUndo.setObjectName(u"actionUndo")
icon13 = QIcon()
icon13.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionUndo.setIcon(icon13)
self.actionRedo = QAction(MainWindow)
self.actionRedo.setObjectName(u"actionRedo")
icon14 = QIcon()
icon14.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRedo.setIcon(icon14)
self.actionCut = QAction(MainWindow)
self.actionCut.setObjectName(u"actionCut")
icon15 = QIcon()
icon15.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCut.setIcon(icon15)
self.actionCopy = QAction(MainWindow)
self.actionCopy.setObjectName(u"actionCopy")
icon16 = QIcon()
icon16.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCopy.setIcon(icon16)
self.actionPaste = QAction(MainWindow)
self.actionPaste.setObjectName(u"actionPaste")
icon17 = QIcon()
icon17.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionPaste.setIcon(icon17)
self.actionSelectAll = QAction(MainWindow)
self.actionSelectAll.setObjectName(u"actionSelectAll")
self.actionDelete = QAction(MainWindow)
self.actionDelete.setObjectName(u"actionDelete")
self.actionAbout = QAction(MainWindow)
self.actionAbout.setObjectName(u"actionAbout")
self.actionSettings = QAction(MainWindow)
self.actionSettings.setObjectName(u"actionSettings")
self.actionAboutQt = QAction(MainWindow)
self.actionAboutQt.setObjectName(u"actionAboutQt")
self.centralwidget = QWidget(MainWindow)
self.centralwidget.setObjectName(u"centralwidget")
self.workspaceLayout = QHBoxLayout(self.centralwidget)
self.workspaceLayout.setSpacing(0)
self.workspaceLayout.setObjectName(u"workspaceLayout")
self.workspaceLayout.setContentsMargins(0, 0, 0, 0)
self.workspaceSplitter = QSplitter(self.centralwidget)
self.workspaceSplitter.setObjectName(u"workspaceSplitter")
sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.workspaceSplitter.sizePolicy().hasHeightForWidth())
self.workspaceSplitter.setSizePolicy(sizePolicy)
self.workspaceSplitter.setOrientation(Qt.Orientation.Horizontal)
self.workspaceSplitter.setChildrenCollapsible(False)
self.leftDockHost = QMainWindow(self.workspaceSplitter)
self.leftDockHost.setObjectName(u"leftDockHost")
self.leftDockHost.setMinimumSize(QSize(220, 0))
self.panel_libraries = QDockWidget(self.leftDockHost)
self.panel_libraries.setObjectName(u"panel_libraries")
self.panel_libraries.setMinimumSize(QSize(220, 91))
self.dockWidgetContents = QWidget()
self.dockWidgetContents.setObjectName(u"dockWidgetContents")
self.librariesLayout = QVBoxLayout(self.dockWidgetContents)
self.librariesLayout.setSpacing(0)
self.librariesLayout.setObjectName(u"librariesLayout")
self.librariesLayout.setContentsMargins(0, 0, 0, 0)
self.treeView = QTreeView(self.dockWidgetContents)
self.treeView.setObjectName(u"treeView")
self.treeView.setStyleSheet(u"QTreeView::item { height: 32px; }")
self.treeView.setAlternatingRowColors(True)
self.treeView.setIconSize(QSize(28, 28))
self.treeView.setUniformRowHeights(True)
self.librariesLayout.addWidget(self.treeView)
self.panel_libraries.setWidget(self.dockWidgetContents)
self.leftDockHost.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.panel_libraries)
self.panel_document = QDockWidget(self.leftDockHost)
self.panel_document.setObjectName(u"panel_document")
self.panel_document.setMinimumSize(QSize(220, 91))
self.documentDockContents = QWidget()
self.documentDockContents.setObjectName(u"documentDockContents")
self.documentPanelLayout = QVBoxLayout(self.documentDockContents)
self.documentPanelLayout.setSpacing(0)
self.documentPanelLayout.setObjectName(u"documentPanelLayout")
self.documentPanelLayout.setContentsMargins(0, 0, 0, 0)
self.documentTreeView = QTreeView(self.documentDockContents)
self.documentTreeView.setObjectName(u"documentTreeView")
self.documentTreeView.setAlternatingRowColors(True)
self.documentTreeView.setIconSize(QSize(16, 16))
self.documentTreeView.setUniformRowHeights(True)
self.documentPanelLayout.addWidget(self.documentTreeView)
self.panel_document.setWidget(self.documentDockContents)
self.leftDockHost.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.panel_document)
self.workspaceSplitter.addWidget(self.leftDockHost)
self.workspace = QWidget(self.workspaceSplitter)
self.workspace.setObjectName(u"workspace")
sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
sizePolicy1.setHorizontalStretch(1)
sizePolicy1.setVerticalStretch(0)
sizePolicy1.setHeightForWidth(self.workspace.sizePolicy().hasHeightForWidth())
self.workspace.setSizePolicy(sizePolicy1)
self.workspaceEditorLayout = QVBoxLayout(self.workspace)
self.workspaceEditorLayout.setSpacing(0)
self.workspaceEditorLayout.setObjectName(u"workspaceEditorLayout")
self.workspaceEditorLayout.setContentsMargins(0, 0, 0, 0)
self.workspaceHeader = QFrame(self.workspace)
self.workspaceHeader.setObjectName(u"workspaceHeader")
self.workspaceHeader.setMinimumSize(QSize(0, 34))
self.workspaceHeader.setMaximumSize(QSize(16777215, 34))
self.workspaceHeader.setFrameShape(QFrame.Shape.StyledPanel)
self.workspaceHeaderLayout = QHBoxLayout(self.workspaceHeader)
self.workspaceHeaderLayout.setObjectName(u"workspaceHeaderLayout")
self.workspaceHeaderLayout.setContentsMargins(6, 2, 6, 2)
self.navigateUpButton = QToolButton(self.workspaceHeader)
self.navigateUpButton.setObjectName(u"navigateUpButton")
icon18 = QIcon()
icon18.addFile(u":/icons/icons/arrow-up.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.navigateUpButton.setIcon(icon18)
self.workspaceHeaderLayout.addWidget(self.navigateUpButton)
self.navigateDownButton = QToolButton(self.workspaceHeader)
self.navigateDownButton.setObjectName(u"navigateDownButton")
self.navigateDownButton.setEnabled(False)
icon19 = QIcon()
icon19.addFile(u":/icons/icons/arrow-down.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.navigateDownButton.setIcon(icon19)
self.workspaceHeaderLayout.addWidget(self.navigateDownButton)
self.graphBreadcrumbLabel = QLabel(self.workspaceHeader)
self.graphBreadcrumbLabel.setObjectName(u"graphBreadcrumbLabel")
self.workspaceHeaderLayout.addWidget(self.graphBreadcrumbLabel)
self.workspaceModeLabel = QLabel(self.workspaceHeader)
self.workspaceModeLabel.setObjectName(u"workspaceModeLabel")
self.workspaceHeaderLayout.addWidget(self.workspaceModeLabel)
self.workspaceHeaderSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.workspaceHeaderLayout.addItem(self.workspaceHeaderSpacer)
self.pointerToolButton = QToolButton(self.workspaceHeader)
self.pointerToolButton.setObjectName(u"pointerToolButton")
icon20 = QIcon()
icon20.addFile(u":/icons/icons/edit-select.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.pointerToolButton.setIcon(icon20)
self.pointerToolButton.setCheckable(True)
self.pointerToolButton.setChecked(True)
self.workspaceHeaderLayout.addWidget(self.pointerToolButton)
self.connectToolButton = QToolButton(self.workspaceHeader)
self.connectToolButton.setObjectName(u"connectToolButton")
icon21 = QIcon()
icon21.addFile(u":/icons/icons/network-connect.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.connectToolButton.setIcon(icon21)
self.connectToolButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.connectToolButton)
self.boxToolButton = QToolButton(self.workspaceHeader)
self.boxToolButton.setObjectName(u"boxToolButton")
icon22 = QIcon()
icon22.addFile(u":/icons/icons/draw-rectangle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.boxToolButton.setIcon(icon22)
self.boxToolButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.boxToolButton)
self.lineToolButton = QToolButton(self.workspaceHeader)
self.lineToolButton.setObjectName(u"lineToolButton")
icon23 = QIcon()
icon23.addFile(u":/icons/icons/draw-bezier-curves.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.lineToolButton.setIcon(icon23)
self.lineToolButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.lineToolButton)
self.textToolButton = QToolButton(self.workspaceHeader)
self.textToolButton.setObjectName(u"textToolButton")
icon24 = QIcon()
icon24.addFile(u":/icons/icons/draw-text.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.textToolButton.setIcon(icon24)
self.textToolButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.textToolButton)
self.rotateToolButton = QToolButton(self.workspaceHeader)
self.rotateToolButton.setObjectName(u"rotateToolButton")
self.rotateToolButton.setIcon(icon7)
self.workspaceHeaderLayout.addWidget(self.rotateToolButton)
self.workspaceEditorLayout.addWidget(self.workspaceHeader)
self.workspaceVerticalSplitter = QSplitter(self.workspace)
self.workspaceVerticalSplitter.setObjectName(u"workspaceVerticalSplitter")
self.workspaceVerticalSplitter.setOrientation(Qt.Orientation.Vertical)
self.workspaceVerticalSplitter.setChildrenCollapsible(False)
self.workspaceStack = QStackedWidget(self.workspaceVerticalSplitter)
self.workspaceStack.setObjectName(u"workspaceStack")
self.graphPage = QWidget()
self.graphPage.setObjectName(u"graphPage")
self.graphPageLayout = QVBoxLayout(self.graphPage)
self.graphPageLayout.setObjectName(u"graphPageLayout")
self.graphPageLayout.setContentsMargins(0, 0, 0, 0)
self.graphView = GraphWorkspaceView(self.graphPage)
self.graphView.setObjectName(u"graphView")
self.graphPageLayout.addWidget(self.graphView)
self.workspaceStack.addWidget(self.graphPage)
self.textPage = QWidget()
self.textPage.setObjectName(u"textPage")
self.textPageLayout = QVBoxLayout(self.textPage)
self.textPageLayout.setObjectName(u"textPageLayout")
self.textPageLayout.setContentsMargins(0, 0, 0, 0)
self.textDefinitionEditor = TextDefinitionEditor(self.textPage)
self.textDefinitionEditor.setObjectName(u"textDefinitionEditor")
self.textPageLayout.addWidget(self.textDefinitionEditor)
self.workspaceStack.addWidget(self.textPage)
self.emptyPage = QWidget()
self.emptyPage.setObjectName(u"emptyPage")
self.emptyPage.setStyleSheet(u"background-color: #9a9a9a;")
self.emptyPageLayout = QVBoxLayout(self.emptyPage)
self.emptyPageLayout.setObjectName(u"emptyPageLayout")
self.emptyWorkspaceLabel = QLabel(self.emptyPage)
self.emptyWorkspaceLabel.setObjectName(u"emptyWorkspaceLabel")
self.emptyWorkspaceLabel.setStyleSheet(u"background: transparent; color: #202020;")
self.emptyWorkspaceLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.emptyPageLayout.addWidget(self.emptyWorkspaceLabel)
self.workspaceStack.addWidget(self.emptyPage)
self.workspaceVerticalSplitter.addWidget(self.workspaceStack)
self.outputTabs = QTabWidget(self.workspaceVerticalSplitter)
self.outputTabs.setObjectName(u"outputTabs")
self.outputTabs.setMinimumSize(QSize(0, 100))
self.logTab = QWidget()
self.logTab.setObjectName(u"logTab")
self.logTabLayout = QVBoxLayout(self.logTab)
self.logTabLayout.setObjectName(u"logTabLayout")
self.logTabLayout.setContentsMargins(0, 0, 0, 0)
self.logOutput = QPlainTextEdit(self.logTab)
self.logOutput.setObjectName(u"logOutput")
self.logOutput.setReadOnly(True)
self.logOutput.setMaximumBlockCount(5000)
self.logTabLayout.addWidget(self.logOutput)
self.outputTabs.addTab(self.logTab, "")
self.workspaceVerticalSplitter.addWidget(self.outputTabs)
self.workspaceEditorLayout.addWidget(self.workspaceVerticalSplitter)
self.workspaceSplitter.addWidget(self.workspace)
self.workspaceLayout.addWidget(self.workspaceSplitter)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QMenuBar(MainWindow)
self.menubar.setObjectName(u"menubar")
self.menubar.setGeometry(QRect(0, 0, 1209, 24))
self.menuFile = QMenu(self.menubar)
self.menuFile.setObjectName(u"menuFile")
self.menuEdit = QMenu(self.menubar)
self.menuEdit.setObjectName(u"menuEdit")
self.menuView = QMenu(self.menubar)
self.menuView.setObjectName(u"menuView")
self.menuPanels = QMenu(self.menuView)
self.menuPanels.setObjectName(u"menuPanels")
self.menuToolbars = QMenu(self.menuView)
self.menuToolbars.setObjectName(u"menuToolbars")
self.menuHelp = QMenu(self.menubar)
self.menuHelp.setObjectName(u"menuHelp")
self.menuSimulation = QMenu(self.menubar)
self.menuSimulation.setObjectName(u"menuSimulation")
MainWindow.setMenuBar(self.menubar)
self.fileToolbar = QToolBar(MainWindow)
self.fileToolbar.setObjectName(u"fileToolbar")
sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
sizePolicy2.setHorizontalStretch(0)
sizePolicy2.setVerticalStretch(0)
sizePolicy2.setHeightForWidth(self.fileToolbar.sizePolicy().hasHeightForWidth())
self.fileToolbar.setSizePolicy(sizePolicy2)
self.fileToolbar.setMinimumSize(QSize(0, 40))
self.fileToolbar.setMaximumSize(QSize(16777215, 40))
self.fileToolbar.setIconSize(QSize(24, 24))
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.fileToolbar)
self.editToolbar = QToolBar(MainWindow)
self.editToolbar.setObjectName(u"editToolbar")
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.editToolbar)
self.cameraToolbar = QToolBar(MainWindow)
self.cameraToolbar.setObjectName(u"cameraToolbar")
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.cameraToolbar)
self.simulationToolbar = QToolBar(MainWindow)
self.simulationToolbar.setObjectName(u"simulationToolbar")
self.simulationToolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.simulationToolbar)
self.menubar.addAction(self.menuFile.menuAction())
self.menubar.addAction(self.menuEdit.menuAction())
self.menubar.addAction(self.menuView.menuAction())
self.menubar.addAction(self.menuSimulation.menuAction())
self.menubar.addAction(self.menuHelp.menuAction())
self.menuFile.addAction(self.actionNew)
self.menuFile.addAction(self.actionOpen)
self.menuFile.addAction(self.actionReloadLibraries)
self.menuFile.addAction(self.actionReloadSimulation)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionSave)
self.menuFile.addAction(self.actionSaveAs)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionClose)
self.menuFile.addAction(self.actionExit)
self.menuEdit.addAction(self.actionUndo)
self.menuEdit.addAction(self.actionRedo)
self.menuEdit.addSeparator()
self.menuEdit.addAction(self.actionCopy)
self.menuEdit.addAction(self.actionCut)
self.menuEdit.addAction(self.actionPaste)
self.menuEdit.addAction(self.actionDelete)
self.menuEdit.addAction(self.actionSelectAll)
self.menuEdit.addSeparator()
self.menuEdit.addAction(self.actionSettings)
self.menuView.addAction(self.menuPanels.menuAction())
self.menuView.addAction(self.menuToolbars.menuAction())
self.menuHelp.addAction(self.actionAbout)
self.menuHelp.addAction(self.actionAboutQt)
self.menuSimulation.addAction(self.actionSimulationSettings)
self.menuSimulation.addAction(self.actionGraphParameters)
self.menuSimulation.addAction(self.actionCompose)
self.menuSimulation.addAction(self.actionExportModel)
self.menuSimulation.addAction(self.actionSimulationWindow)
self.menuSimulation.addAction(self.actionRunSimulation)
self.fileToolbar.addAction(self.actionNew)
self.fileToolbar.addAction(self.actionOpen)
self.fileToolbar.addAction(self.actionSave)
self.fileToolbar.addAction(self.actionSaveAs)
self.editToolbar.addAction(self.actionUndo)
self.editToolbar.addAction(self.actionRedo)
self.editToolbar.addAction(self.actionCopy)
self.editToolbar.addAction(self.actionCut)
self.editToolbar.addAction(self.actionPaste)
self.cameraToolbar.addAction(self.actionZoomIn)
self.cameraToolbar.addAction(self.actionZoomOut)
self.cameraToolbar.addAction(self.actionCenterView)
self.simulationToolbar.addAction(self.actionSimulationSettings)
self.simulationToolbar.addAction(self.actionGraphParameters)
self.simulationToolbar.addAction(self.actionCompose)
self.simulationToolbar.addAction(self.actionSimulationWindow)
self.simulationToolbar.addAction(self.actionRunSimulation)
self.retranslateUi(MainWindow)
self.workspaceStack.setCurrentIndex(0)
QMetaObject.connectSlotsByName(MainWindow)
# setupUi
def retranslateUi(self, MainWindow):
MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"BEdit", None))
self.actionSimulationSettings.setText(QCoreApplication.translate("MainWindow", u"Simulation Settings", None))
#if QT_CONFIG(statustip)
self.actionSimulationSettings.setStatusTip(QCoreApplication.translate("MainWindow", u"Edit settings stored in the active graph", None))
#endif // QT_CONFIG(statustip)
self.actionGraphParameters.setText(QCoreApplication.translate("MainWindow", u"Graph Parameters", None))
#if QT_CONFIG(statustip)
self.actionGraphParameters.setStatusTip(QCoreApplication.translate("MainWindow", u"Edit parameters throughout the active graph", None))
#endif // QT_CONFIG(statustip)
self.actionCompose.setText(QCoreApplication.translate("MainWindow", u"Compose", None))
#if QT_CONFIG(statustip)
self.actionCompose.setStatusTip(QCoreApplication.translate("MainWindow", u"Compose the active graph as an OpenModelica model", None))
#endif // QT_CONFIG(statustip)
#if QT_CONFIG(shortcut)
self.actionCompose.setShortcut(QCoreApplication.translate("MainWindow", u"F5", None))
#endif // QT_CONFIG(shortcut)
self.actionRunSimulation.setText(QCoreApplication.translate("MainWindow", u"Run", None))
#if QT_CONFIG(statustip)
self.actionRunSimulation.setStatusTip(QCoreApplication.translate("MainWindow", u"Run the simulation", None))
#endif // QT_CONFIG(statustip)
#if QT_CONFIG(shortcut)
self.actionRunSimulation.setShortcut(QCoreApplication.translate("MainWindow", u"F6", None))
#endif // QT_CONFIG(shortcut)
self.actionSimulationWindow.setText(QCoreApplication.translate("MainWindow", u"Simulation Window", None))
#if QT_CONFIG(statustip)
self.actionSimulationWindow.setStatusTip(QCoreApplication.translate("MainWindow", u"Show the simulation results window", None))
#endif // QT_CONFIG(statustip)
self.actionExportModel.setText(QCoreApplication.translate("MainWindow", u"Export Model\u2026", None))
#if QT_CONFIG(statustip)
self.actionExportModel.setStatusTip(QCoreApplication.translate("MainWindow", u"Save the composed OpenModelica model to a file", None))
#endif // QT_CONFIG(statustip)
self.actionNew.setText(QCoreApplication.translate("MainWindow", u"&New", None))
#if QT_CONFIG(statustip)
self.actionNew.setStatusTip(QCoreApplication.translate("MainWindow", u"Create a new document", None))
#endif // QT_CONFIG(statustip)
#if QT_CONFIG(shortcut)
self.actionNew.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+N", None))
#endif // QT_CONFIG(shortcut)
self.actionRotateClockwise.setText(QCoreApplication.translate("MainWindow", u"Rotate Clockwise", None))
#if QT_CONFIG(tooltip)
self.actionRotateClockwise.setToolTip(QCoreApplication.translate("MainWindow", u"Rotate selected blocks clockwise by 90 degrees", None))
#endif // QT_CONFIG(tooltip)
#if QT_CONFIG(shortcut)
self.actionRotateClockwise.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+R", None))
#endif // QT_CONFIG(shortcut)
self.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)
self.actionOpen.setText(QCoreApplication.translate("MainWindow", u"&Open\u2026", None))
#if QT_CONFIG(statustip)
self.actionOpen.setStatusTip(QCoreApplication.translate("MainWindow", u"Open a document", None))
#endif // QT_CONFIG(statustip)
#if QT_CONFIG(shortcut)
self.actionOpen.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+O", None))
#endif // QT_CONFIG(shortcut)
self.actionReloadLibraries.setText(QCoreApplication.translate("MainWindow", u"Reload &Libraries", None))
#if QT_CONFIG(statustip)
self.actionReloadLibraries.setStatusTip(QCoreApplication.translate("MainWindow", u"Reload configured library files from disk", None))
#endif // QT_CONFIG(statustip)
#if QT_CONFIG(shortcut)
self.actionReloadLibraries.setShortcut(QCoreApplication.translate("MainWindow", u"Shift+F5", None))
#endif // QT_CONFIG(shortcut)
self.actionReloadSimulation.setText(QCoreApplication.translate("MainWindow", u"Reload &Simulation Code", None))
#if QT_CONFIG(statustip)
self.actionReloadSimulation.setStatusTip(QCoreApplication.translate("MainWindow", u"Reload the simulation package while preserving runtime state", None))
#endif // QT_CONFIG(statustip)
#if QT_CONFIG(shortcut)
self.actionReloadSimulation.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+F5", None))
#endif // QT_CONFIG(shortcut)
self.actionSave.setText(QCoreApplication.translate("MainWindow", u"&Save", None))
#if QT_CONFIG(statustip)
self.actionSave.setStatusTip(QCoreApplication.translate("MainWindow", u"Save the current document", None))
#endif // QT_CONFIG(statustip)
#if QT_CONFIG(shortcut)
self.actionSave.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+S", None))
#endif // QT_CONFIG(shortcut)
self.actionSaveAs.setText(QCoreApplication.translate("MainWindow", u"Save &As\u2026", None))
#if QT_CONFIG(shortcut)
self.actionSaveAs.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+Shift+S", None))
#endif // QT_CONFIG(shortcut)
self.actionExit.setText(QCoreApplication.translate("MainWindow", u"E&xit", None))
#if QT_CONFIG(shortcut)
self.actionExit.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+Q", None))
#endif // QT_CONFIG(shortcut)
self.actionClose.setText(QCoreApplication.translate("MainWindow", u"&Close Document", None))
#if QT_CONFIG(shortcut)
self.actionClose.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+W", None))
#endif // QT_CONFIG(shortcut)
self.actionUndo.setText(QCoreApplication.translate("MainWindow", u"&Undo", None))
#if QT_CONFIG(shortcut)
self.actionUndo.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+Z", None))
#endif // QT_CONFIG(shortcut)
self.actionRedo.setText(QCoreApplication.translate("MainWindow", u"&Redo", None))
#if QT_CONFIG(shortcut)
self.actionRedo.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+Y", None))
#endif // QT_CONFIG(shortcut)
self.actionCut.setText(QCoreApplication.translate("MainWindow", u"Cu&t", None))
#if QT_CONFIG(shortcut)
self.actionCut.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+X", None))
#endif // QT_CONFIG(shortcut)
self.actionCopy.setText(QCoreApplication.translate("MainWindow", u"&Copy", None))
#if QT_CONFIG(shortcut)
self.actionCopy.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+C", None))
#endif // QT_CONFIG(shortcut)
self.actionPaste.setText(QCoreApplication.translate("MainWindow", u"&Paste", None))
#if QT_CONFIG(shortcut)
self.actionPaste.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+V", None))
#endif // QT_CONFIG(shortcut)
self.actionSelectAll.setText(QCoreApplication.translate("MainWindow", u"Select &All", None))
#if QT_CONFIG(shortcut)
self.actionSelectAll.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+A", None))
#endif // QT_CONFIG(shortcut)
self.actionDelete.setText(QCoreApplication.translate("MainWindow", u"&Delete", None))
#if QT_CONFIG(shortcut)
self.actionDelete.setShortcut(QCoreApplication.translate("MainWindow", u"Del", None))
#endif // QT_CONFIG(shortcut)
self.actionAbout.setText(QCoreApplication.translate("MainWindow", u"&About BEdit", None))
self.actionSettings.setText(QCoreApplication.translate("MainWindow", u"&Settings\u2026", None))
#if QT_CONFIG(statustip)
self.actionSettings.setStatusTip(QCoreApplication.translate("MainWindow", u"Configure BEdit", None))
#endif // QT_CONFIG(statustip)
self.actionAboutQt.setText(QCoreApplication.translate("MainWindow", u"About &Qt", None))
self.panel_libraries.setWindowTitle(QCoreApplication.translate("MainWindow", u"Libraries", None))
self.panel_document.setWindowTitle(QCoreApplication.translate("MainWindow", u"Document", None))
#if QT_CONFIG(tooltip)
self.navigateUpButton.setToolTip(QCoreApplication.translate("MainWindow", u"Open the containing graph", None))
#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.workspaceModeLabel.setText(QCoreApplication.translate("MainWindow", u"Graph", None))
self.pointerToolButton.setText(QCoreApplication.translate("MainWindow", u"Pointer", None))
self.connectToolButton.setText(QCoreApplication.translate("MainWindow", u"Connect", None))
#if QT_CONFIG(tooltip)
self.boxToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Draw a box annotation", None))
#endif // QT_CONFIG(tooltip)
self.boxToolButton.setText(QCoreApplication.translate("MainWindow", u"Box", None))
#if QT_CONFIG(tooltip)
self.lineToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Draw a line annotation", None))
#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.emptyWorkspaceLabel.setText(QCoreApplication.translate("MainWindow", u"No document open", None))
self.logOutput.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Application messages appear here.", None))
self.outputTabs.setTabText(self.outputTabs.indexOf(self.logTab), QCoreApplication.translate("MainWindow", u"Log", None))
self.menuFile.setTitle(QCoreApplication.translate("MainWindow", u"&File", None))
self.menuEdit.setTitle(QCoreApplication.translate("MainWindow", u"&Edit", None))
self.menuView.setTitle(QCoreApplication.translate("MainWindow", u"&View", None))
self.menuPanels.setTitle(QCoreApplication.translate("MainWindow", u"&Panels", None))
self.menuToolbars.setTitle(QCoreApplication.translate("MainWindow", u"&Toolbars", None))
self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"&Help", None))
self.menuSimulation.setTitle(QCoreApplication.translate("MainWindow", u"Simulation", None))
self.fileToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"File", None))
self.editToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Edit", None))
self.cameraToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Camera", None))
self.simulationToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Simulation", None))
# retranslateUi

View File

@@ -0,0 +1,193 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'parameter_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, QPlainTextEdit,
QPushButton, QSizePolicy, QSpinBox, QSplitter,
QVBoxLayout, QWidget)
class Ui_ParameterOptionsDialog(object):
def setupUi(self, ParameterOptionsDialog):
if not ParameterOptionsDialog.objectName():
ParameterOptionsDialog.setObjectName(u"ParameterOptionsDialog")
ParameterOptionsDialog.resize(720, 500)
self.dialogLayout = QVBoxLayout(ParameterOptionsDialog)
self.dialogLayout.setObjectName(u"dialogLayout")
self.parameterSplitter = QSplitter(ParameterOptionsDialog)
self.parameterSplitter.setObjectName(u"parameterSplitter")
self.parameterSplitter.setOrientation(Qt.Orientation.Horizontal)
self.parameterListPanel = QWidget(self.parameterSplitter)
self.parameterListPanel.setObjectName(u"parameterListPanel")
self.parameterListLayout = QVBoxLayout(self.parameterListPanel)
self.parameterListLayout.setObjectName(u"parameterListLayout")
self.parameterListLayout.setContentsMargins(0, 0, 0, 0)
self.parameterList = QListWidget(self.parameterListPanel)
self.parameterList.setObjectName(u"parameterList")
self.parameterListLayout.addWidget(self.parameterList)
self.parameterButtonsLayout = QHBoxLayout()
self.parameterButtonsLayout.setObjectName(u"parameterButtonsLayout")
self.addParameterButton = QPushButton(self.parameterListPanel)
self.addParameterButton.setObjectName(u"addParameterButton")
self.parameterButtonsLayout.addWidget(self.addParameterButton)
self.removeParameterButton = QPushButton(self.parameterListPanel)
self.removeParameterButton.setObjectName(u"removeParameterButton")
self.parameterButtonsLayout.addWidget(self.removeParameterButton)
self.parameterListLayout.addLayout(self.parameterButtonsLayout)
self.parameterSplitter.addWidget(self.parameterListPanel)
self.parameterDetailsPanel = QWidget(self.parameterSplitter)
self.parameterDetailsPanel.setObjectName(u"parameterDetailsPanel")
self.parameterDetailsForm = QFormLayout(self.parameterDetailsPanel)
self.parameterDetailsForm.setObjectName(u"parameterDetailsForm")
self.parameterDetailsForm.setContentsMargins(0, 0, 0, 0)
self.nameLabel = QLabel(self.parameterDetailsPanel)
self.nameLabel.setObjectName(u"nameLabel")
self.parameterDetailsForm.setWidget(0, QFormLayout.ItemRole.LabelRole, self.nameLabel)
self.nameEdit = QLineEdit(self.parameterDetailsPanel)
self.nameEdit.setObjectName(u"nameEdit")
self.parameterDetailsForm.setWidget(0, QFormLayout.ItemRole.FieldRole, self.nameEdit)
self.typeLabel = QLabel(self.parameterDetailsPanel)
self.typeLabel.setObjectName(u"typeLabel")
self.parameterDetailsForm.setWidget(1, QFormLayout.ItemRole.LabelRole, self.typeLabel)
self.typeCombo = QComboBox(self.parameterDetailsPanel)
self.typeCombo.addItem("")
self.typeCombo.addItem("")
self.typeCombo.addItem("")
self.typeCombo.addItem("")
self.typeCombo.setObjectName(u"typeCombo")
self.typeCombo.setEditable(True)
self.parameterDetailsForm.setWidget(1, QFormLayout.ItemRole.FieldRole, self.typeCombo)
self.rowsLabel = QLabel(self.parameterDetailsPanel)
self.rowsLabel.setObjectName(u"rowsLabel")
self.parameterDetailsForm.setWidget(2, QFormLayout.ItemRole.LabelRole, self.rowsLabel)
self.rowsSpin = QSpinBox(self.parameterDetailsPanel)
self.rowsSpin.setObjectName(u"rowsSpin")
self.rowsSpin.setMinimum(1)
self.rowsSpin.setMaximum(9999)
self.parameterDetailsForm.setWidget(2, QFormLayout.ItemRole.FieldRole, self.rowsSpin)
self.columnsLabel = QLabel(self.parameterDetailsPanel)
self.columnsLabel.setObjectName(u"columnsLabel")
self.parameterDetailsForm.setWidget(3, QFormLayout.ItemRole.LabelRole, self.columnsLabel)
self.columnsSpin = QSpinBox(self.parameterDetailsPanel)
self.columnsSpin.setObjectName(u"columnsSpin")
self.columnsSpin.setMinimum(1)
self.columnsSpin.setMaximum(9999)
self.parameterDetailsForm.setWidget(3, QFormLayout.ItemRole.FieldRole, self.columnsSpin)
self.valueLabel = QLabel(self.parameterDetailsPanel)
self.valueLabel.setObjectName(u"valueLabel")
self.parameterDetailsForm.setWidget(4, QFormLayout.ItemRole.LabelRole, self.valueLabel)
self.valueEdit = QLineEdit(self.parameterDetailsPanel)
self.valueEdit.setObjectName(u"valueEdit")
self.parameterDetailsForm.setWidget(4, QFormLayout.ItemRole.FieldRole, self.valueEdit)
self.quantityLabel = QLabel(self.parameterDetailsPanel)
self.quantityLabel.setObjectName(u"quantityLabel")
self.parameterDetailsForm.setWidget(5, QFormLayout.ItemRole.LabelRole, self.quantityLabel)
self.quantityCombo = QComboBox(self.parameterDetailsPanel)
self.quantityCombo.setObjectName(u"quantityCombo")
self.quantityCombo.setEditable(True)
self.parameterDetailsForm.setWidget(5, QFormLayout.ItemRole.FieldRole, self.quantityCombo)
self.unitLabel = QLabel(self.parameterDetailsPanel)
self.unitLabel.setObjectName(u"unitLabel")
self.parameterDetailsForm.setWidget(6, QFormLayout.ItemRole.LabelRole, self.unitLabel)
self.unitCombo = QComboBox(self.parameterDetailsPanel)
self.unitCombo.setObjectName(u"unitCombo")
self.unitCombo.setEditable(True)
self.parameterDetailsForm.setWidget(6, QFormLayout.ItemRole.FieldRole, self.unitCombo)
self.descriptionLabel = QLabel(self.parameterDetailsPanel)
self.descriptionLabel.setObjectName(u"descriptionLabel")
self.parameterDetailsForm.setWidget(7, QFormLayout.ItemRole.LabelRole, self.descriptionLabel)
self.descriptionEdit = QPlainTextEdit(self.parameterDetailsPanel)
self.descriptionEdit.setObjectName(u"descriptionEdit")
self.descriptionEdit.setMaximumHeight(100)
self.parameterDetailsForm.setWidget(7, QFormLayout.ItemRole.FieldRole, self.descriptionEdit)
self.parameterSplitter.addWidget(self.parameterDetailsPanel)
self.dialogLayout.addWidget(self.parameterSplitter)
self.buttonBox = QDialogButtonBox(ParameterOptionsDialog)
self.buttonBox.setObjectName(u"buttonBox")
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
self.dialogLayout.addWidget(self.buttonBox)
self.retranslateUi(ParameterOptionsDialog)
self.buttonBox.accepted.connect(ParameterOptionsDialog.accept)
self.buttonBox.rejected.connect(ParameterOptionsDialog.reject)
QMetaObject.connectSlotsByName(ParameterOptionsDialog)
# setupUi
def retranslateUi(self, ParameterOptionsDialog):
ParameterOptionsDialog.setWindowTitle(QCoreApplication.translate("ParameterOptionsDialog", u"Parameter Options", None))
self.addParameterButton.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Add Parameter", None))
self.removeParameterButton.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Remove Parameter", None))
self.nameLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Name:", None))
self.typeLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Type:", None))
self.typeCombo.setItemText(0, QCoreApplication.translate("ParameterOptionsDialog", u"real", None))
self.typeCombo.setItemText(1, QCoreApplication.translate("ParameterOptionsDialog", u"integer", None))
self.typeCombo.setItemText(2, QCoreApplication.translate("ParameterOptionsDialog", u"boolean", None))
self.typeCombo.setItemText(3, QCoreApplication.translate("ParameterOptionsDialog", u"string", None))
self.rowsLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Rows:", None))
self.columnsLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Columns:", None))
self.valueLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Value:", None))
self.quantityLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Quantity:", None))
self.unitLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Unit:", None))
self.descriptionLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Description:", None))
# retranslateUi

View File

@@ -0,0 +1,310 @@
# -*- 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, QCheckBox, QComboBox,
QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout,
QLabel, QLineEdit, QListWidget, QListWidgetItem,
QPlainTextEdit, QPushButton, QSizePolicy, QSpinBox,
QSplitter, QStackedWidget, QVBoxLayout, QWidget)
class Ui_PortOptionsDialog(object):
def setupUi(self, PortOptionsDialog):
if not PortOptionsDialog.objectName():
PortOptionsDialog.setObjectName(u"PortOptionsDialog")
PortOptionsDialog.resize(760, 421)
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.setVerticalSpacing(3)
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.addItem("")
self.orientationCombo.setObjectName(u"orientationCombo")
self.portDetailsForm.setWidget(2, QFormLayout.ItemRole.FieldRole, self.orientationCombo)
self.multipleConnectionsCheckBox = QCheckBox(self.portDetailsPanel)
self.multipleConnectionsCheckBox.setObjectName(u"multipleConnectionsCheckBox")
self.portDetailsForm.setWidget(3, QFormLayout.ItemRole.SpanningRole, self.multipleConnectionsCheckBox)
self.positionHintLabel = QLabel(self.portDetailsPanel)
self.positionHintLabel.setObjectName(u"positionHintLabel")
self.positionHintLabel.setWordWrap(True)
self.portDetailsForm.setWidget(5, QFormLayout.ItemRole.SpanningRole, self.positionHintLabel)
self.typeOptionsStack = QStackedWidget(self.portDetailsPanel)
self.typeOptionsStack.setObjectName(u"typeOptionsStack")
self.signalOptionsPage = QWidget()
self.signalOptionsPage.setObjectName(u"signalOptionsPage")
self.signalOptionsForm = QFormLayout(self.signalOptionsPage)
self.signalOptionsForm.setObjectName(u"signalOptionsForm")
self.valueTypeLabel = QLabel(self.signalOptionsPage)
self.valueTypeLabel.setObjectName(u"valueTypeLabel")
self.signalOptionsForm.setWidget(0, QFormLayout.ItemRole.LabelRole, self.valueTypeLabel)
self.valueTypeCombo = QComboBox(self.signalOptionsPage)
self.valueTypeCombo.addItem("")
self.valueTypeCombo.addItem("")
self.valueTypeCombo.addItem("")
self.valueTypeCombo.addItem("")
self.valueTypeCombo.setObjectName(u"valueTypeCombo")
self.valueTypeCombo.setEditable(True)
self.signalOptionsForm.setWidget(0, QFormLayout.ItemRole.FieldRole, self.valueTypeCombo)
self.quantityLabel = QLabel(self.signalOptionsPage)
self.quantityLabel.setObjectName(u"quantityLabel")
self.signalOptionsForm.setWidget(1, QFormLayout.ItemRole.LabelRole, self.quantityLabel)
self.quantityCombo = QComboBox(self.signalOptionsPage)
self.quantityCombo.setObjectName(u"quantityCombo")
self.quantityCombo.setEditable(True)
self.signalOptionsForm.setWidget(1, QFormLayout.ItemRole.FieldRole, self.quantityCombo)
self.unitCombo = QComboBox(self.signalOptionsPage)
self.unitCombo.setObjectName(u"unitCombo")
self.unitCombo.setEditable(True)
self.signalOptionsForm.setWidget(2, QFormLayout.ItemRole.FieldRole, self.unitCombo)
self.unitLabel = QLabel(self.signalOptionsPage)
self.unitLabel.setObjectName(u"unitLabel")
self.signalOptionsForm.setWidget(2, QFormLayout.ItemRole.LabelRole, self.unitLabel)
self.rowsColumnsLabel = QLabel(self.signalOptionsPage)
self.rowsColumnsLabel.setObjectName(u"rowsColumnsLabel")
self.signalOptionsForm.setWidget(3, QFormLayout.ItemRole.LabelRole, self.rowsColumnsLabel)
self.horizontalLayout = QHBoxLayout()
self.horizontalLayout.setObjectName(u"horizontalLayout")
self.rowsSpin = QSpinBox(self.signalOptionsPage)
self.rowsSpin.setObjectName(u"rowsSpin")
self.rowsSpin.setMinimum(1)
self.rowsSpin.setMaximum(9999)
self.horizontalLayout.addWidget(self.rowsSpin)
self.columnsSpin = QSpinBox(self.signalOptionsPage)
self.columnsSpin.setObjectName(u"columnsSpin")
self.columnsSpin.setMinimum(1)
self.columnsSpin.setMaximum(9999)
self.horizontalLayout.addWidget(self.columnsSpin)
self.signalOptionsForm.setLayout(3, QFormLayout.ItemRole.FieldRole, self.horizontalLayout)
self.descriptionLabel = QLabel(self.signalOptionsPage)
self.descriptionLabel.setObjectName(u"descriptionLabel")
self.signalOptionsForm.setWidget(4, QFormLayout.ItemRole.LabelRole, self.descriptionLabel)
self.descriptionEdit = QPlainTextEdit(self.signalOptionsPage)
self.descriptionEdit.setObjectName(u"descriptionEdit")
self.signalOptionsForm.setWidget(4, QFormLayout.ItemRole.FieldRole, self.descriptionEdit)
self.typeOptionsStack.addWidget(self.signalOptionsPage)
self.powerOptionsPage = QWidget()
self.powerOptionsPage.setObjectName(u"powerOptionsPage")
self.powerOptionsForm = QFormLayout(self.powerOptionsPage)
self.powerOptionsForm.setObjectName(u"powerOptionsForm")
self.domainLabel = QLabel(self.powerOptionsPage)
self.domainLabel.setObjectName(u"domainLabel")
self.powerOptionsForm.setWidget(0, QFormLayout.ItemRole.LabelRole, self.domainLabel)
self.domainCombo = QComboBox(self.powerOptionsPage)
self.domainCombo.setObjectName(u"domainCombo")
self.powerOptionsForm.setWidget(0, QFormLayout.ItemRole.FieldRole, self.domainCombo)
self.effortLabel = QLabel(self.powerOptionsPage)
self.effortLabel.setObjectName(u"effortLabel")
self.powerOptionsForm.setWidget(1, QFormLayout.ItemRole.LabelRole, self.effortLabel)
self.effortValueLabel = QLabel(self.powerOptionsPage)
self.effortValueLabel.setObjectName(u"effortValueLabel")
self.powerOptionsForm.setWidget(1, QFormLayout.ItemRole.FieldRole, self.effortValueLabel)
self.flowLabel = QLabel(self.powerOptionsPage)
self.flowLabel.setObjectName(u"flowLabel")
self.powerOptionsForm.setWidget(2, QFormLayout.ItemRole.LabelRole, self.flowLabel)
self.flowValueLabel = QLabel(self.powerOptionsPage)
self.flowValueLabel.setObjectName(u"flowValueLabel")
self.powerOptionsForm.setWidget(2, QFormLayout.ItemRole.FieldRole, self.flowValueLabel)
self.causalityLabel = QLabel(self.powerOptionsPage)
self.causalityLabel.setObjectName(u"causalityLabel")
self.powerOptionsForm.setWidget(3, QFormLayout.ItemRole.LabelRole, self.causalityLabel)
self.causalityCombo = QComboBox(self.powerOptionsPage)
self.causalityCombo.setObjectName(u"causalityCombo")
self.powerOptionsForm.setWidget(3, QFormLayout.ItemRole.FieldRole, self.causalityCombo)
self.powerDescriptionLabel = QLabel(self.powerOptionsPage)
self.powerDescriptionLabel.setObjectName(u"powerDescriptionLabel")
self.powerOptionsForm.setWidget(4, QFormLayout.ItemRole.LabelRole, self.powerDescriptionLabel)
self.powerDescriptionEdit = QPlainTextEdit(self.powerOptionsPage)
self.powerDescriptionEdit.setObjectName(u"powerDescriptionEdit")
self.powerOptionsForm.setWidget(4, QFormLayout.ItemRole.FieldRole, self.powerDescriptionEdit)
self.typeOptionsStack.addWidget(self.powerOptionsPage)
self.unsupportedTypePage = QWidget()
self.unsupportedTypePage.setObjectName(u"unsupportedTypePage")
self.unsupportedTypeLayout = QVBoxLayout(self.unsupportedTypePage)
self.unsupportedTypeLayout.setObjectName(u"unsupportedTypeLayout")
self.unsupportedTypeLabel = QLabel(self.unsupportedTypePage)
self.unsupportedTypeLabel.setObjectName(u"unsupportedTypeLabel")
self.unsupportedTypeLabel.setWordWrap(True)
self.unsupportedTypeLayout.addWidget(self.unsupportedTypeLabel)
self.typeOptionsStack.addWidget(self.unsupportedTypePage)
self.portDetailsForm.setWidget(4, QFormLayout.ItemRole.SpanningRole, self.typeOptionsStack)
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"Port 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.orientationCombo.setItemText(2, QCoreApplication.translate("PortOptionsDialog", u"Indifferent", None))
self.multipleConnectionsCheckBox.setText(QCoreApplication.translate("PortOptionsDialog", u"Allow multiple connections", None))
self.positionHintLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"New ports start at (0, 0) in the icon editor.", None))
self.valueTypeLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Type:", None))
self.valueTypeCombo.setItemText(0, QCoreApplication.translate("PortOptionsDialog", u"real", None))
self.valueTypeCombo.setItemText(1, QCoreApplication.translate("PortOptionsDialog", u"integer", None))
self.valueTypeCombo.setItemText(2, QCoreApplication.translate("PortOptionsDialog", u"boolean", None))
self.valueTypeCombo.setItemText(3, QCoreApplication.translate("PortOptionsDialog", u"string", None))
self.quantityLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Quantity:", None))
self.unitLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Unit:", None))
self.rowsColumnsLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Matrix size:", None))
self.descriptionLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Description:", None))
self.domainLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Domain:", None))
self.effortLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Effort:", None))
self.effortValueLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"p.e", None))
self.flowLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Flow:", None))
self.flowValueLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"p.f", None))
self.causalityLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Causality:", None))
self.powerDescriptionLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Description:", None))
self.unsupportedTypeLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"This port type does not have an options editor yet.", None))
# retranslateUi

View File

@@ -0,0 +1,304 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'settings_dialog.ui'
##
## Created by: Qt User Interface Compiler version 6.11.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractButton, QAbstractItemView, QApplication, QCheckBox,
QDialog, QDialogButtonBox, QFormLayout, QGroupBox,
QHBoxLayout, QHeaderView, QLabel, QLineEdit,
QListWidget, QListWidgetItem, QPushButton, QSizePolicy,
QSpacerItem, QSpinBox, QTabWidget, QTableWidget,
QTableWidgetItem, QVBoxLayout, QWidget)
class Ui_SettingsDialog(object):
def setupUi(self, SettingsDialog):
if not SettingsDialog.objectName():
SettingsDialog.setObjectName(u"SettingsDialog")
SettingsDialog.resize(480, 520)
SettingsDialog.setModal(True)
self.dialogLayout = QVBoxLayout(SettingsDialog)
self.dialogLayout.setObjectName(u"dialogLayout")
self.settingsTabs = QTabWidget(SettingsDialog)
self.settingsTabs.setObjectName(u"settingsTabs")
self.generalTab = QWidget()
self.generalTab.setObjectName(u"generalTab")
self.generalLayout = QVBoxLayout(self.generalTab)
self.generalLayout.setObjectName(u"generalLayout")
self.autosaveGroupBox = QGroupBox(self.generalTab)
self.autosaveGroupBox.setObjectName(u"autosaveGroupBox")
self.autosaveGroupBox.setCheckable(True)
self.autosaveGroupBox.setChecked(False)
self.autosaveLayout = QFormLayout(self.autosaveGroupBox)
self.autosaveLayout.setObjectName(u"autosaveLayout")
self.autosaveIntervalSpinBox = QSpinBox(self.autosaveGroupBox)
self.autosaveIntervalSpinBox.setObjectName(u"autosaveIntervalSpinBox")
self.autosaveIntervalSpinBox.setMinimum(1)
self.autosaveIntervalSpinBox.setMaximum(120)
self.autosaveIntervalSpinBox.setValue(5)
self.autosaveLayout.setWidget(0, QFormLayout.ItemRole.FieldRole, self.autosaveIntervalSpinBox)
self.generalLayout.addWidget(self.autosaveGroupBox)
self.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.generalLayout.addItem(self.generalSpacer)
self.settingsTabs.addTab(self.generalTab, "")
self.bondGraphTab = QWidget()
self.bondGraphTab.setObjectName(u"bondGraphTab")
self.bondGraphLayout = QVBoxLayout(self.bondGraphTab)
self.bondGraphLayout.setObjectName(u"bondGraphLayout")
self.causalityGroupBox = QGroupBox(self.bondGraphTab)
self.causalityGroupBox.setObjectName(u"causalityGroupBox")
self.causalityLayout = QVBoxLayout(self.causalityGroupBox)
self.causalityLayout.setObjectName(u"causalityLayout")
self.automaticCausalityCheckBox = QCheckBox(self.causalityGroupBox)
self.automaticCausalityCheckBox.setObjectName(u"automaticCausalityCheckBox")
self.automaticCausalityCheckBox.setChecked(True)
self.causalityLayout.addWidget(self.automaticCausalityCheckBox)
self.automaticCausalityHintLabel = QLabel(self.causalityGroupBox)
self.automaticCausalityHintLabel.setObjectName(u"automaticCausalityHintLabel")
self.automaticCausalityHintLabel.setWordWrap(True)
self.causalityLayout.addWidget(self.automaticCausalityHintLabel)
self.bondGraphLayout.addWidget(self.causalityGroupBox)
self.bondGraphSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
self.bondGraphLayout.addItem(self.bondGraphSpacer)
self.settingsTabs.addTab(self.bondGraphTab, "")
self.simulationTab = QWidget()
self.simulationTab.setObjectName(u"simulationTab")
self.simulationTabLayout = QVBoxLayout(self.simulationTab)
self.simulationTabLayout.setObjectName(u"simulationTabLayout")
self.openModelicaGroupBox = QGroupBox(self.simulationTab)
self.openModelicaGroupBox.setObjectName(u"openModelicaGroupBox")
self.openModelicaLayout = QVBoxLayout(self.openModelicaGroupBox)
self.openModelicaLayout.setObjectName(u"openModelicaLayout")
self.openModelicaPathLabel = QLabel(self.openModelicaGroupBox)
self.openModelicaPathLabel.setObjectName(u"openModelicaPathLabel")
self.openModelicaLayout.addWidget(self.openModelicaPathLabel)
self.openModelicaPathLayout = QHBoxLayout()
self.openModelicaPathLayout.setObjectName(u"openModelicaPathLayout")
self.openModelicaPathEdit = QLineEdit(self.openModelicaGroupBox)
self.openModelicaPathEdit.setObjectName(u"openModelicaPathEdit")
self.openModelicaPathLayout.addWidget(self.openModelicaPathEdit)
self.browseOpenModelicaButton = QPushButton(self.openModelicaGroupBox)
self.browseOpenModelicaButton.setObjectName(u"browseOpenModelicaButton")
self.openModelicaPathLayout.addWidget(self.browseOpenModelicaButton)
self.openModelicaLayout.addLayout(self.openModelicaPathLayout)
self.openModelicaHintLabel = QLabel(self.openModelicaGroupBox)
self.openModelicaHintLabel.setObjectName(u"openModelicaHintLabel")
self.openModelicaHintLabel.setWordWrap(True)
self.openModelicaLayout.addWidget(self.openModelicaHintLabel)
self.simulationTabLayout.addWidget(self.openModelicaGroupBox)
self.simulationSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
self.simulationTabLayout.addItem(self.simulationSpacer)
self.settingsTabs.addTab(self.simulationTab, "")
self.syntaxTab = QWidget()
self.syntaxTab.setObjectName(u"syntaxTab")
self.syntaxTabLayout = QVBoxLayout(self.syntaxTab)
self.syntaxTabLayout.setObjectName(u"syntaxTabLayout")
self.syntaxHintLabel = QLabel(self.syntaxTab)
self.syntaxHintLabel.setObjectName(u"syntaxHintLabel")
self.syntaxHintLabel.setWordWrap(True)
self.syntaxTabLayout.addWidget(self.syntaxHintLabel)
self.syntaxStylesTable = QTableWidget(self.syntaxTab)
if (self.syntaxStylesTable.columnCount() < 4):
self.syntaxStylesTable.setColumnCount(4)
__qtablewidgetitem = QTableWidgetItem()
self.syntaxStylesTable.setHorizontalHeaderItem(0, __qtablewidgetitem)
__qtablewidgetitem1 = QTableWidgetItem()
self.syntaxStylesTable.setHorizontalHeaderItem(1, __qtablewidgetitem1)
__qtablewidgetitem2 = QTableWidgetItem()
self.syntaxStylesTable.setHorizontalHeaderItem(2, __qtablewidgetitem2)
__qtablewidgetitem3 = QTableWidgetItem()
self.syntaxStylesTable.setHorizontalHeaderItem(3, __qtablewidgetitem3)
self.syntaxStylesTable.setObjectName(u"syntaxStylesTable")
self.syntaxStylesTable.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.syntaxStylesTable.setColumnCount(4)
self.syntaxTabLayout.addWidget(self.syntaxStylesTable)
self.settingsTabs.addTab(self.syntaxTab, "")
self.librariesTab = QWidget()
self.librariesTab.setObjectName(u"librariesTab")
self.librariesTabLayout = QVBoxLayout(self.librariesTab)
self.librariesTabLayout.setObjectName(u"librariesTabLayout")
self.libraryPathsLabel = QLabel(self.librariesTab)
self.libraryPathsLabel.setObjectName(u"libraryPathsLabel")
self.libraryPathsLabel.setWordWrap(True)
self.librariesTabLayout.addWidget(self.libraryPathsLabel)
self.libraryPathsList = QListWidget(self.librariesTab)
self.libraryPathsList.setObjectName(u"libraryPathsList")
self.librariesTabLayout.addWidget(self.libraryPathsList)
self.libraryPathButtonsLayout = QHBoxLayout()
self.libraryPathButtonsLayout.setObjectName(u"libraryPathButtonsLayout")
self.addLibraryFileButton = QPushButton(self.librariesTab)
self.addLibraryFileButton.setObjectName(u"addLibraryFileButton")
self.libraryPathButtonsLayout.addWidget(self.addLibraryFileButton)
self.addLibraryFolderButton = QPushButton(self.librariesTab)
self.addLibraryFolderButton.setObjectName(u"addLibraryFolderButton")
self.libraryPathButtonsLayout.addWidget(self.addLibraryFolderButton)
self.removeLibraryPathButton = QPushButton(self.librariesTab)
self.removeLibraryPathButton.setObjectName(u"removeLibraryPathButton")
self.libraryPathButtonsLayout.addWidget(self.removeLibraryPathButton)
self.libraryButtonsSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.libraryPathButtonsLayout.addItem(self.libraryButtonsSpacer)
self.librariesTabLayout.addLayout(self.libraryPathButtonsLayout)
self.settingsTabs.addTab(self.librariesTab, "")
self.dialogLayout.addWidget(self.settingsTabs)
self.buttonBox = QDialogButtonBox(SettingsDialog)
self.buttonBox.setObjectName(u"buttonBox")
self.buttonBox.setOrientation(Qt.Orientation.Horizontal)
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
self.dialogLayout.addWidget(self.buttonBox)
self.retranslateUi(SettingsDialog)
self.buttonBox.accepted.connect(SettingsDialog.accept)
self.buttonBox.rejected.connect(SettingsDialog.reject)
self.settingsTabs.setCurrentIndex(0)
QMetaObject.connectSlotsByName(SettingsDialog)
# setupUi
def retranslateUi(self, SettingsDialog):
SettingsDialog.setWindowTitle(QCoreApplication.translate("SettingsDialog", u"Settings", None))
self.autosaveGroupBox.setTitle(QCoreApplication.translate("SettingsDialog", u"Automatic saving", None))
self.autosaveIntervalSpinBox.setSuffix(QCoreApplication.translate("SettingsDialog", u" minutes", None))
self.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.causalityGroupBox.setTitle(QCoreApplication.translate("SettingsDialog", u"Causality", None))
self.automaticCausalityCheckBox.setText(QCoreApplication.translate("SettingsDialog", u"Infer causality when connections are added or removed", None))
self.automaticCausalityHintLabel.setText(QCoreApplication.translate("SettingsDialog", u"When disabled, displayed causalities are updated only when compiling, exporting, or running the model.", None))
self.settingsTabs.setTabText(self.settingsTabs.indexOf(self.bondGraphTab), QCoreApplication.translate("SettingsDialog", u"Bond graph", None))
self.openModelicaGroupBox.setTitle(QCoreApplication.translate("SettingsDialog", u"OpenModelica", None))
self.openModelicaPathLabel.setText(QCoreApplication.translate("SettingsDialog", u"OpenModelica executable:", None))
self.openModelicaPathEdit.setPlaceholderText(QCoreApplication.translate("SettingsDialog", u"Leave empty to find omc on PATH", None))
self.browseOpenModelicaButton.setText(QCoreApplication.translate("SettingsDialog", u"Browse\u2026", None))
self.openModelicaHintLabel.setText(QCoreApplication.translate("SettingsDialog", u"Select the omc executable, for example ~/.local/bin/omc.", None))
self.settingsTabs.setTabText(self.settingsTabs.indexOf(self.simulationTab), QCoreApplication.translate("SettingsDialog", u"Simulation", None))
self.syntaxHintLabel.setText(QCoreApplication.translate("SettingsDialog", u"Double-click a colour cell to choose a colour. Word lists remain editable in data/syntax/openmodelica.json.", None))
___qtablewidgetitem = self.syntaxStylesTable.horizontalHeaderItem(0)
___qtablewidgetitem.setText(QCoreApplication.translate("SettingsDialog", u"Expression type", None))
___qtablewidgetitem1 = self.syntaxStylesTable.horizontalHeaderItem(1)
___qtablewidgetitem1.setText(QCoreApplication.translate("SettingsDialog", u"Colour", None))
___qtablewidgetitem2 = self.syntaxStylesTable.horizontalHeaderItem(2)
___qtablewidgetitem2.setText(QCoreApplication.translate("SettingsDialog", u"Bold", None))
___qtablewidgetitem3 = self.syntaxStylesTable.horizontalHeaderItem(3)
___qtablewidgetitem3.setText(QCoreApplication.translate("SettingsDialog", u"Italic", None))
self.settingsTabs.setTabText(self.settingsTabs.indexOf(self.syntaxTab), QCoreApplication.translate("SettingsDialog", u"Text highlighting", None))
self.libraryPathsLabel.setText(QCoreApplication.translate("SettingsDialog", u"Load library JSON or BEdit Binary files from these files or folders at startup:", None))
self.addLibraryFileButton.setText(QCoreApplication.translate("SettingsDialog", u"Add File\u2026", None))
self.addLibraryFolderButton.setText(QCoreApplication.translate("SettingsDialog", u"Add Folder\u2026", None))
self.removeLibraryPathButton.setText(QCoreApplication.translate("SettingsDialog", u"Remove", None))
self.settingsTabs.setTabText(self.settingsTabs.indexOf(self.librariesTab), QCoreApplication.translate("SettingsDialog", u"Libraries", None))
# retranslateUi

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,123 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'simulation_settings_dialog.ui'
##
## Created by: Qt User Interface Compiler version 6.11.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractButton, QAbstractSpinBox, QApplication, QDialog,
QDialogButtonBox, QDoubleSpinBox, QFormLayout, QGroupBox,
QLabel, QRadioButton, QSizePolicy, QSpacerItem,
QSpinBox, QVBoxLayout, QWidget)
class Ui_SimulationSettingsDialog(object):
def setupUi(self, SimulationSettingsDialog):
if not SimulationSettingsDialog.objectName():
SimulationSettingsDialog.setObjectName(u"SimulationSettingsDialog")
SimulationSettingsDialog.resize(420, 384)
self.dialogLayout = QVBoxLayout(SimulationSettingsDialog)
self.dialogLayout.setObjectName(u"dialogLayout")
self.settingsGroup = QGroupBox(SimulationSettingsDialog)
self.settingsGroup.setObjectName(u"settingsGroup")
self.settingsLayout = QVBoxLayout(self.settingsGroup)
self.settingsLayout.setObjectName(u"settingsLayout")
self.simulationInterval = QGroupBox(self.settingsGroup)
self.simulationInterval.setObjectName(u"simulationInterval")
self.simulationInterval.setEnabled(True)
self.formLayout = QFormLayout(self.simulationInterval)
self.formLayout.setObjectName(u"formLayout")
self.label = QLabel(self.simulationInterval)
self.label.setObjectName(u"label")
self.formLayout.setWidget(0, QFormLayout.ItemRole.LabelRole, self.label)
self.startTimeSpinBox = QDoubleSpinBox(self.simulationInterval)
self.startTimeSpinBox.setObjectName(u"startTimeSpinBox")
self.startTimeSpinBox.setEnabled(True)
self.formLayout.setWidget(0, QFormLayout.ItemRole.FieldRole, self.startTimeSpinBox)
self.label_2 = QLabel(self.simulationInterval)
self.label_2.setObjectName(u"label_2")
self.formLayout.setWidget(1, QFormLayout.ItemRole.LabelRole, self.label_2)
self.stopTimeSpinBox = QDoubleSpinBox(self.simulationInterval)
self.stopTimeSpinBox.setObjectName(u"stopTimeSpinBox")
self.stopTimeSpinBox.setValue(1.000000000000000)
self.formLayout.setWidget(1, QFormLayout.ItemRole.FieldRole, self.stopTimeSpinBox)
self.numberOfIntervalsRadioButton = QRadioButton(self.simulationInterval)
self.numberOfIntervalsRadioButton.setObjectName(u"numberOfIntervalsRadioButton")
self.formLayout.setWidget(2, QFormLayout.ItemRole.LabelRole, self.numberOfIntervalsRadioButton)
self.intervalTimeRadioButton = QRadioButton(self.simulationInterval)
self.intervalTimeRadioButton.setObjectName(u"intervalTimeRadioButton")
self.formLayout.setWidget(3, QFormLayout.ItemRole.LabelRole, self.intervalTimeRadioButton)
self.numberOfIntervalsSpinBox = QSpinBox(self.simulationInterval)
self.numberOfIntervalsSpinBox.setObjectName(u"numberOfIntervalsSpinBox")
self.numberOfIntervalsSpinBox.setMaximum(999999999)
self.numberOfIntervalsSpinBox.setValue(500)
self.formLayout.setWidget(2, QFormLayout.ItemRole.FieldRole, self.numberOfIntervalsSpinBox)
self.intervalTimeSpinBox = QDoubleSpinBox(self.simulationInterval)
self.intervalTimeSpinBox.setObjectName(u"intervalTimeSpinBox")
self.intervalTimeSpinBox.setDecimals(5)
self.intervalTimeSpinBox.setStepType(QAbstractSpinBox.StepType.AdaptiveDecimalStepType)
self.intervalTimeSpinBox.setValue(0.002000000000000)
self.formLayout.setWidget(3, QFormLayout.ItemRole.FieldRole, self.intervalTimeSpinBox)
self.settingsLayout.addWidget(self.simulationInterval)
self.settingsSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
self.settingsLayout.addItem(self.settingsSpacer)
self.dialogLayout.addWidget(self.settingsGroup)
self.buttonBox = QDialogButtonBox(SimulationSettingsDialog)
self.buttonBox.setObjectName(u"buttonBox")
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
self.dialogLayout.addWidget(self.buttonBox)
self.retranslateUi(SimulationSettingsDialog)
self.buttonBox.accepted.connect(SimulationSettingsDialog.accept)
self.buttonBox.rejected.connect(SimulationSettingsDialog.reject)
QMetaObject.connectSlotsByName(SimulationSettingsDialog)
# setupUi
def retranslateUi(self, SimulationSettingsDialog):
SimulationSettingsDialog.setWindowTitle(QCoreApplication.translate("SimulationSettingsDialog", u"Simulation Settings", None))
self.settingsGroup.setTitle(QCoreApplication.translate("SimulationSettingsDialog", u"Settings", None))
self.simulationInterval.setTitle(QCoreApplication.translate("SimulationSettingsDialog", u"Simulation Interval", None))
self.label.setText(QCoreApplication.translate("SimulationSettingsDialog", u"Start time:", None))
self.startTimeSpinBox.setPrefix("")
self.startTimeSpinBox.setSuffix(QCoreApplication.translate("SimulationSettingsDialog", u"s", None))
self.label_2.setText(QCoreApplication.translate("SimulationSettingsDialog", u"Stop time:", None))
self.stopTimeSpinBox.setSuffix(QCoreApplication.translate("SimulationSettingsDialog", u"s", None))
self.numberOfIntervalsRadioButton.setText(QCoreApplication.translate("SimulationSettingsDialog", u"Number of intervals:", None))
self.intervalTimeRadioButton.setText(QCoreApplication.translate("SimulationSettingsDialog", u"Interval:", None))
self.intervalTimeSpinBox.setSuffix(QCoreApplication.translate("SimulationSettingsDialog", u"s", None))
# retranslateUi

View File

@@ -0,0 +1,244 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'simulation_window.ui'
##
## Created by: Qt User Interface Compiler version 6.11.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
QCursor, QFont, QFontDatabase, QGradient,
QIcon, QImage, QKeySequence, QLinearGradient,
QPainter, QPalette, QPixmap, QRadialGradient,
QTransform)
from PySide6.QtWidgets import (QAbstractItemView, QApplication, QDockWidget, QHBoxLayout,
QHeaderView, QLabel, QListWidget, QListWidgetItem,
QMainWindow, QMenu, QMenuBar, QProgressBar,
QSizePolicy, QTabWidget, QToolBar, QTreeWidget,
QTreeWidgetItem, QVBoxLayout, QWidget)
from . import resources_rc
class Ui_SimulationWindow(object):
def setupUi(self, SimulationWindow):
if not SimulationWindow.objectName():
SimulationWindow.setObjectName(u"SimulationWindow")
SimulationWindow.resize(900, 650)
icon = QIcon()
icon.addFile(u":/icons/icons/office-chart-line.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
SimulationWindow.setWindowIcon(icon)
self.actionOpen = QAction(SimulationWindow)
self.actionOpen.setObjectName(u"actionOpen")
icon1 = QIcon()
icon1.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionOpen.setIcon(icon1)
self.actionSave = QAction(SimulationWindow)
self.actionSave.setObjectName(u"actionSave")
icon2 = QIcon()
icon2.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSave.setIcon(icon2)
self.actionClear = QAction(SimulationWindow)
self.actionClear.setObjectName(u"actionClear")
icon3 = QIcon()
icon3.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionClear.setIcon(icon3)
self.actionSaveAs = QAction(SimulationWindow)
self.actionSaveAs.setObjectName(u"actionSaveAs")
icon4 = QIcon()
icon4.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSaveAs.setIcon(icon4)
self.actionExit = QAction(SimulationWindow)
self.actionExit.setObjectName(u"actionExit")
self.actionAbout = QAction(SimulationWindow)
self.actionAbout.setObjectName(u"actionAbout")
self.actionAboutQt = QAction(SimulationWindow)
self.actionAboutQt.setObjectName(u"actionAboutQt")
self.actionToggleResults = QAction(SimulationWindow)
self.actionToggleResults.setObjectName(u"actionToggleResults")
self.actionToggleResults.setCheckable(True)
self.actionToggleResults.setChecked(True)
self.actionAddGraph = QAction(SimulationWindow)
self.actionAddGraph.setObjectName(u"actionAddGraph")
icon5 = QIcon()
icon5.addFile(u":/icons/icons/list-add.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionAddGraph.setIcon(icon5)
self.actionRemoveGraph = QAction(SimulationWindow)
self.actionRemoveGraph.setObjectName(u"actionRemoveGraph")
icon6 = QIcon()
icon6.addFile(u":/icons/icons/list-remove.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRemoveGraph.setIcon(icon6)
self.centralWidget = QWidget(SimulationWindow)
self.centralWidget.setObjectName(u"centralWidget")
self.resultsLayout = QVBoxLayout(self.centralWidget)
self.resultsLayout.setObjectName(u"resultsLayout")
self.graphTabs = QTabWidget(self.centralWidget)
self.graphTabs.setObjectName(u"graphTabs")
self.graphTabs.setTabsClosable(False)
self.graphTabs.setMovable(True)
self.resultsLayout.addWidget(self.graphTabs)
SimulationWindow.setCentralWidget(self.centralWidget)
self.menuBar = QMenuBar(SimulationWindow)
self.menuBar.setObjectName(u"menuBar")
self.menuBar.setGeometry(QRect(0, 0, 900, 24))
self.menuFile = QMenu(self.menuBar)
self.menuFile.setObjectName(u"menuFile")
self.menuView = QMenu(self.menuBar)
self.menuView.setObjectName(u"menuView")
self.menuPanels = QMenu(self.menuView)
self.menuPanels.setObjectName(u"menuPanels")
self.menuToolbars = QMenu(self.menuView)
self.menuToolbars.setObjectName(u"menuToolbars")
self.menuHelp = QMenu(self.menuBar)
self.menuHelp.setObjectName(u"menuHelp")
self.menuGraph = QMenu(self.menuBar)
self.menuGraph.setObjectName(u"menuGraph")
SimulationWindow.setMenuBar(self.menuBar)
self.workspaceToolbar = QToolBar(SimulationWindow)
self.workspaceToolbar.setObjectName(u"workspaceToolbar")
self.workspaceToolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
SimulationWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.workspaceToolbar)
self.fileToolbar = QToolBar(SimulationWindow)
self.fileToolbar.setObjectName(u"fileToolbar")
self.fileToolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
SimulationWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.fileToolbar)
self.statusDock = QDockWidget(SimulationWindow)
self.statusDock.setObjectName(u"statusDock")
self.statusDockContents = QWidget()
self.statusDockContents.setObjectName(u"statusDockContents")
self.horizontalLayout = QHBoxLayout(self.statusDockContents)
self.horizontalLayout.setObjectName(u"horizontalLayout")
self.timeLabel = QLabel(self.statusDockContents)
self.timeLabel.setObjectName(u"timeLabel")
self.horizontalLayout.addWidget(self.timeLabel)
self.progressBar = QProgressBar(self.statusDockContents)
self.progressBar.setObjectName(u"progressBar")
self.progressBar.setMaximum(10000)
self.progressBar.setValue(0)
self.horizontalLayout.addWidget(self.progressBar)
self.statusLabel = QLabel(self.statusDockContents)
self.statusLabel.setObjectName(u"statusLabel")
self.horizontalLayout.addWidget(self.statusLabel)
self.statusDock.setWidget(self.statusDockContents)
SimulationWindow.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.statusDock)
self.logDock = QDockWidget(SimulationWindow)
self.logDock.setObjectName(u"logDock")
self.logDock.setFloating(False)
self.logDock.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetFloatable|QDockWidget.DockWidgetFeature.DockWidgetMovable)
self.logDockContents = QWidget()
self.logDockContents.setObjectName(u"logDockContents")
self.logLayout = QVBoxLayout(self.logDockContents)
self.logLayout.setObjectName(u"logLayout")
self.messageList = QListWidget(self.logDockContents)
self.messageList.setObjectName(u"messageList")
self.messageList.setAlternatingRowColors(True)
self.logLayout.addWidget(self.messageList)
self.logDock.setWidget(self.logDockContents)
SimulationWindow.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.logDock)
self.signalsDock = QDockWidget(SimulationWindow)
self.signalsDock.setObjectName(u"signalsDock")
self.signalsDockContents = QWidget()
self.signalsDockContents.setObjectName(u"signalsDockContents")
self.signalsLayout = QVBoxLayout(self.signalsDockContents)
self.signalsLayout.setObjectName(u"signalsLayout")
self.signalsTree = QTreeWidget(self.signalsDockContents)
self.signalsTree.setObjectName(u"signalsTree")
self.signalsTree.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.signalsTree.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
self.signalsTree.setHeaderHidden(True)
self.signalsLayout.addWidget(self.signalsTree)
self.signalsDock.setWidget(self.signalsDockContents)
SimulationWindow.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.signalsDock)
self.menuBar.addAction(self.menuFile.menuAction())
self.menuBar.addAction(self.menuView.menuAction())
self.menuBar.addAction(self.menuGraph.menuAction())
self.menuBar.addAction(self.menuHelp.menuAction())
self.menuFile.addAction(self.actionOpen)
self.menuFile.addAction(self.actionSave)
self.menuFile.addAction(self.actionSaveAs)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionClear)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionExit)
self.menuView.addAction(self.menuPanels.menuAction())
self.menuView.addAction(self.menuToolbars.menuAction())
self.menuHelp.addAction(self.actionAbout)
self.menuHelp.addAction(self.actionAboutQt)
self.menuGraph.addAction(self.actionAddGraph)
self.menuGraph.addAction(self.actionRemoveGraph)
self.workspaceToolbar.addAction(self.actionAddGraph)
self.workspaceToolbar.addAction(self.actionRemoveGraph)
self.fileToolbar.addAction(self.actionClear)
self.fileToolbar.addAction(self.actionOpen)
self.fileToolbar.addAction(self.actionSave)
self.fileToolbar.addAction(self.actionSaveAs)
self.retranslateUi(SimulationWindow)
QMetaObject.connectSlotsByName(SimulationWindow)
# setupUi
def retranslateUi(self, SimulationWindow):
SimulationWindow.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Simulation", None))
self.actionOpen.setText(QCoreApplication.translate("SimulationWindow", u"&Open\u2026", None))
#if QT_CONFIG(shortcut)
self.actionOpen.setShortcut(QCoreApplication.translate("SimulationWindow", u"Ctrl+O", None))
#endif // QT_CONFIG(shortcut)
self.actionSave.setText(QCoreApplication.translate("SimulationWindow", u"&Save\u2026", None))
#if QT_CONFIG(shortcut)
self.actionSave.setShortcut(QCoreApplication.translate("SimulationWindow", u"Ctrl+S", None))
#endif // QT_CONFIG(shortcut)
self.actionClear.setText(QCoreApplication.translate("SimulationWindow", u"&Clear", None))
self.actionSaveAs.setText(QCoreApplication.translate("SimulationWindow", u"Save &As\u2026", None))
#if QT_CONFIG(shortcut)
self.actionSaveAs.setShortcut(QCoreApplication.translate("SimulationWindow", u"Ctrl+Shift+S", None))
#endif // QT_CONFIG(shortcut)
self.actionExit.setText(QCoreApplication.translate("SimulationWindow", u"E&xit", None))
#if QT_CONFIG(shortcut)
self.actionExit.setShortcut(QCoreApplication.translate("SimulationWindow", u"Ctrl+W", None))
#endif // QT_CONFIG(shortcut)
self.actionAbout.setText(QCoreApplication.translate("SimulationWindow", u"&About Simulation Window", None))
self.actionAboutQt.setText(QCoreApplication.translate("SimulationWindow", u"About &Qt", None))
self.actionToggleResults.setText(QCoreApplication.translate("SimulationWindow", u"Results", None))
self.actionAddGraph.setText(QCoreApplication.translate("SimulationWindow", u"Add Graph", None))
#if QT_CONFIG(statustip)
self.actionAddGraph.setStatusTip(QCoreApplication.translate("SimulationWindow", u"Add a graph workspace tab", None))
#endif // QT_CONFIG(statustip)
self.actionRemoveGraph.setText(QCoreApplication.translate("SimulationWindow", u"Remove Current Graph", None))
#if QT_CONFIG(statustip)
self.actionRemoveGraph.setStatusTip(QCoreApplication.translate("SimulationWindow", u"Remove the current graph workspace tab", None))
#endif // QT_CONFIG(statustip)
self.menuFile.setTitle(QCoreApplication.translate("SimulationWindow", u"&File", None))
self.menuView.setTitle(QCoreApplication.translate("SimulationWindow", u"&View", None))
self.menuPanels.setTitle(QCoreApplication.translate("SimulationWindow", u"&Panels", None))
self.menuToolbars.setTitle(QCoreApplication.translate("SimulationWindow", u"&Toolbars", None))
self.menuHelp.setTitle(QCoreApplication.translate("SimulationWindow", u"&Help", None))
self.menuGraph.setTitle(QCoreApplication.translate("SimulationWindow", u"&Graph", None))
self.workspaceToolbar.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Workspace", None))
self.fileToolbar.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"File", None))
self.statusDock.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Status", None))
self.timeLabel.setText(QCoreApplication.translate("SimulationWindow", u"Time: 0 s", None))
self.progressBar.setFormat(QCoreApplication.translate("SimulationWindow", u"%p%", None))
self.statusLabel.setText(QCoreApplication.translate("SimulationWindow", u"No simulation has been run yet.", None))
self.logDock.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Log", None))
self.signalsDock.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Signals", None))
___qtreewidgetitem = self.signalsTree.headerItem()
___qtreewidgetitem.setText(0, QCoreApplication.translate("SimulationWindow", u"Signal", None))
# retranslateUi

View File

@@ -0,0 +1,114 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'text_definition_editor.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 (QApplication, QGroupBox, QHBoxLayout, QSizePolicy,
QSplitter, QVBoxLayout, QWidget)
from bedit.gui.dialogs.parameter_options import ParameterEditor
from bedit.gui.dialogs.port_options import PortEditor
from bedit.gui.editors.openmodelica import OpenModelicaEditor
class Ui_TextDefinitionEditor(object):
def setupUi(self, TextDefinitionEditor):
if not TextDefinitionEditor.objectName():
TextDefinitionEditor.setObjectName(u"TextDefinitionEditor")
TextDefinitionEditor.resize(900, 600)
self.editorLayout = QHBoxLayout(TextDefinitionEditor)
self.editorLayout.setObjectName(u"editorLayout")
self.editorLayout.setContentsMargins(6, 6, 6, 6)
self.columnSplitter = QSplitter(TextDefinitionEditor)
self.columnSplitter.setObjectName(u"columnSplitter")
self.columnSplitter.setOrientation(Qt.Orientation.Horizontal)
self.columnSplitter.setChildrenCollapsible(False)
self.sourceSplitter = QSplitter(self.columnSplitter)
self.sourceSplitter.setObjectName(u"sourceSplitter")
self.sourceSplitter.setOrientation(Qt.Orientation.Vertical)
self.sourceSplitter.setChildrenCollapsible(False)
self.declarationsGroup = QGroupBox(self.sourceSplitter)
self.declarationsGroup.setObjectName(u"declarationsGroup")
self.declarationsLayout = QVBoxLayout(self.declarationsGroup)
self.declarationsLayout.setObjectName(u"declarationsLayout")
self.declarationsEdit = OpenModelicaEditor(self.declarationsGroup)
self.declarationsEdit.setObjectName(u"declarationsEdit")
self.declarationsLayout.addWidget(self.declarationsEdit)
self.sourceSplitter.addWidget(self.declarationsGroup)
self.initialEquationsGroup = QGroupBox(self.sourceSplitter)
self.initialEquationsGroup.setObjectName(u"initialEquationsGroup")
self.initialEquationsLayout = QVBoxLayout(self.initialEquationsGroup)
self.initialEquationsLayout.setObjectName(u"initialEquationsLayout")
self.initialEquationsEdit = OpenModelicaEditor(self.initialEquationsGroup)
self.initialEquationsEdit.setObjectName(u"initialEquationsEdit")
self.initialEquationsLayout.addWidget(self.initialEquationsEdit)
self.sourceSplitter.addWidget(self.initialEquationsGroup)
self.equationsGroup = QGroupBox(self.sourceSplitter)
self.equationsGroup.setObjectName(u"equationsGroup")
self.equationsLayout = QVBoxLayout(self.equationsGroup)
self.equationsLayout.setObjectName(u"equationsLayout")
self.equationsEdit = OpenModelicaEditor(self.equationsGroup)
self.equationsEdit.setObjectName(u"equationsEdit")
self.equationsLayout.addWidget(self.equationsEdit)
self.sourceSplitter.addWidget(self.equationsGroup)
self.columnSplitter.addWidget(self.sourceSplitter)
self.definitionSplitter = QSplitter(self.columnSplitter)
self.definitionSplitter.setObjectName(u"definitionSplitter")
self.definitionSplitter.setOrientation(Qt.Orientation.Vertical)
self.definitionSplitter.setChildrenCollapsible(False)
self.portsGroup = QGroupBox(self.definitionSplitter)
self.portsGroup.setObjectName(u"portsGroup")
self.portsLayout = QVBoxLayout(self.portsGroup)
self.portsLayout.setObjectName(u"portsLayout")
self.portEditor = PortEditor(self.portsGroup)
self.portEditor.setObjectName(u"portEditor")
self.portsLayout.addWidget(self.portEditor)
self.definitionSplitter.addWidget(self.portsGroup)
self.parametersGroup = QGroupBox(self.definitionSplitter)
self.parametersGroup.setObjectName(u"parametersGroup")
self.parametersLayout = QVBoxLayout(self.parametersGroup)
self.parametersLayout.setObjectName(u"parametersLayout")
self.parameterEditor = ParameterEditor(self.parametersGroup)
self.parameterEditor.setObjectName(u"parameterEditor")
self.parametersLayout.addWidget(self.parameterEditor)
self.definitionSplitter.addWidget(self.parametersGroup)
self.columnSplitter.addWidget(self.definitionSplitter)
self.editorLayout.addWidget(self.columnSplitter)
self.retranslateUi(TextDefinitionEditor)
QMetaObject.connectSlotsByName(TextDefinitionEditor)
# setupUi
def retranslateUi(self, TextDefinitionEditor):
self.declarationsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Declarations", None))
self.initialEquationsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Initial Equations", None))
self.equationsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Equations", None))
self.portsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Ports", None))
self.parametersGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Parameters", None))
pass
# 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,35 @@
from dataclasses import dataclass
from typing import Literal
from PySide6.QtCore import Qt
ArrowStyle = Literal["open", "half", "filled"]
@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
arrow_style: ArrowStyle = "filled"
def __post_init__(self) -> None:
if self.arrow_style not in {"open", "half", "filled"}:
raise ValueError(f"Unknown connection arrow style: {self.arrow_style}")
# This is the intentional code-level styling point for every port/connection type.
CONNECTION_STYLES: dict[str, ConnectionStyle] = {
"signal": ConnectionStyle(),
"power": ConnectionStyle(width=3.0, arrow_style="half", color="#000000", arrow_size=20.0),
}
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,561 @@
from copy import deepcopy
from PySide6.QtCore import QPointF, QRectF, QSizeF, Qt, QTimer
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):
CURSORS = {
"n": Qt.CursorShape.SizeVerCursor,
"s": Qt.CursorShape.SizeVerCursor,
"e": Qt.CursorShape.SizeHorCursor,
"w": Qt.CursorShape.SizeHorCursor,
"nw": Qt.CursorShape.SizeFDiagCursor,
"se": Qt.CursorShape.SizeFDiagCursor,
"ne": Qt.CursorShape.SizeBDiagCursor,
"sw": Qt.CursorShape.SizeBDiagCursor,
}
def __init__(self, owner: "ShapeItem", role: str = "se") -> None:
super().__init__(-4, -4, 8, 8, owner)
self.owner = owner
self.role = role
self.setBrush(QColor("#ffffff"))
self.setPen(QPen(QColor("#2563eb"), 1.5))
self.setCursor(self.CURSORS[role])
self.setZValue(20)
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
self.owner.begin_resize()
event.accept()
def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
self.owner.resize_from_handle(self.role, event.scenePos())
event.accept()
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
self.owner.finish_resize()
event.accept()
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
)
roles = (
("n", "ne", "e", "se", "s", "sw", "w", "nw")
if element.get("type") == "rectangle"
else ("se",)
)
self.resize_handles = [ResizeHandle(self, role) for role in roles]
self.resize_start: dict | None = None
self._position_resize_handles()
for handle in self.resize_handles:
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:
for handle in self.resize_handles:
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 _position_resize_handles(self) -> None:
width = float(self.element.get("width", 20))
height = float(self.element.get("height", 20))
positions = {
"n": QPointF(width / 2, 0),
"ne": QPointF(width, 0),
"e": QPointF(width, height / 2),
"se": QPointF(width, height),
"s": QPointF(width / 2, height),
"sw": QPointF(0, height),
"w": QPointF(0, height / 2),
"nw": QPointF(0, 0),
}
for handle in self.resize_handles:
handle.setPos(positions[handle.role])
def begin_resize(self) -> None:
self.resize_start = {
"left": self.pos().x(),
"top": self.pos().y(),
"right": self.pos().x() + float(self.element.get("width", 20)),
"bottom": self.pos().y() + float(self.element.get("height", 20)),
}
def resize_from_handle(self, role: str, scene_position: QPointF) -> None:
if self.resize_start is None:
self.begin_resize()
if self.element.get("type") == "line":
bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128)
endpoint = QPointF(
min(bounds.right(), max(bounds.left(), _snap(scene_position.x()))),
min(bounds.bottom(), max(bounds.top(), _snap(scene_position.y()))),
)
self.prepareGeometryChange()
self.element["width"] = endpoint.x() - self.pos().x()
self.element["height"] = endpoint.y() - self.pos().y()
self._position_resize_handles()
self.update()
return
edges = dict(self.resize_start)
point = QPointF(_snap(scene_position.x()), _snap(scene_position.y()))
if "w" in role:
edges["left"] = min(point.x(), edges["right"] - _icon_grid_size())
if "e" in role:
edges["right"] = max(point.x(), edges["left"] + _icon_grid_size())
if "n" in role:
edges["top"] = min(point.y(), edges["bottom"] - _icon_grid_size())
if "s" in role:
edges["bottom"] = max(point.y(), edges["top"] + _icon_grid_size())
bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128)
edges["left"] = max(bounds.left(), edges["left"])
edges["top"] = max(bounds.top(), edges["top"])
edges["right"] = min(bounds.right(), edges["right"])
edges["bottom"] = min(bounds.bottom(), edges["bottom"])
if self.element.get("type") == "circle":
side = min(edges["right"] - edges["left"], edges["bottom"] - edges["top"])
edges["right"] = edges["left"] + side
edges["bottom"] = edges["top"] + side
self.prepareGeometryChange()
self.element["width"] = edges["right"] - edges["left"]
self.element["height"] = edges["bottom"] - edges["top"]
self.setPos(edges["left"], edges["top"])
self._position_resize_handles()
self.update()
def finish_resize(self) -> None:
self.resize_start = None
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._position_resize_handles()
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.ports = deepcopy(component.ports)
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))
input_side = [port for port in self.ports if port.orientation != "output"]
output_side = [port for port in self.ports if port.orientation == "output"]
self._add_ports(input_side, "input", 0.0)
self._add_ports(output_side, "output", self.icon.width)
self._initial_fit_pending = True
def showEvent(self, event) -> None: # noqa: N802 (Qt API name)
super().showEvent(event)
if self._initial_fit_pending:
self._initial_fit_pending = False
QTimer.singleShot(0, 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

@@ -0,0 +1,106 @@
from PySide6.QtCore import QPointF, QRectF, Qt
from PySide6.QtGui import QColor, QFont, QIcon, QPainter, QPen, QPixmap, QPolygonF
from bedit.core.model import Icon
def icon_bounds(icon: Icon) -> QRectF:
"""Return the automatic hitbox of all visible vector elements."""
bounds = QRectF()
for element in icon.elements:
if element.get("_deleted"):
continue
rect = QRectF(
float(element.get("x", 0)),
float(element.get("y", 0)),
float(element.get("width", 0)),
float(element.get("height", 0)),
).normalized()
bounds = rect if bounds.isNull() else bounds.united(rect)
return bounds if not bounds.isNull() else QRectF(32, 32, 64, 64)
def shape_pen(element: dict) -> QPen:
styles = {
"solid": Qt.PenStyle.SolidLine,
"dash": Qt.PenStyle.DashLine,
"dot": Qt.PenStyle.DotLine,
"dash-dot": Qt.PenStyle.DashDotLine,
"none": Qt.PenStyle.NoPen,
}
return QPen(
QColor(element.get("stroke", "#303030")),
float(element.get("lineWidth", 1.5)),
styles.get(element.get("lineStyle", "solid"), Qt.PenStyle.SolidLine),
)
def paint_icon(
painter: QPainter,
icon: Icon,
target: QRectF,
source: QRectF | None = None,
) -> None:
painter.save()
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
if source is None:
source = QRectF(0, 0, icon.width, icon.height)
painter.translate(target.topLeft())
painter.scale(target.width() / source.width(), target.height() / source.height())
painter.translate(-source.left(), -source.top())
for element in icon.elements:
kind = element.get("type", "rectangle")
rect = QRectF(
float(element.get("x", 0)),
float(element.get("y", 0)),
float(element.get("width", 20)),
float(element.get("height", 20)),
)
painter.setPen(shape_pen(element))
fill = element.get("fill", "#ffffff")
painter.setBrush(
Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill)
)
if kind == "rectangle":
radius = float(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(), rect.top()), rect.bottomRight(), rect.bottomLeft()]
)
)
elif kind == "text":
painter.setPen(QColor(element.get("color", element.get("stroke", "#202020"))))
font = QFont()
font.setPointSizeF(float(element.get("fontSize", 12)))
painter.setFont(font)
painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, str(element.get("text", "Text")))
painter.restore()
def icon_pixmap(icon: Icon, size: int = 16) -> QPixmap:
pixmap = QPixmap(size, size)
pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap)
bounds = icon_bounds(icon)
ratio = min(size / bounds.width(), size / bounds.height())
width, height = bounds.width() * ratio, bounds.height() * ratio
paint_icon(
painter,
icon,
QRectF((size - width) / 2, (size - height) / 2, width, height),
bounds,
)
painter.end()
return pixmap
def library_icon(icon: Icon) -> QIcon:
# Render large enough for the tall Libraries rows. icon_pixmap crops to the
# vector hitbox first, so a 32x32 drawing is shown as large as a 128x128 one.
return QIcon(icon_pixmap(icon, 28))

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More