Compare commits
41 Commits
a23e051024
...
bondgraph
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a6203d01e | |||
| 2d49f65ec4 | |||
| c918a6a428 | |||
| 32154386e8 | |||
| 09248421d0 | |||
| 0c578af85d | |||
| edefb23edc | |||
| 48ac9e2f59 | |||
| 2f6487e510 | |||
| eb8f77ce64 | |||
| 35d933ccbb | |||
| edd7bb98f2 | |||
| ddc004dee5 | |||
| cdfc891980 | |||
| 6fb2478589 | |||
| dc770a0886 | |||
| 38e3d7aae9 | |||
| 4cfe7c3a0e | |||
| d368b36b8c | |||
| 42bf09610b | |||
| 9ed8e371ef | |||
| 495edd42f4 | |||
| a067c85994 | |||
| bf2512995f | |||
| 8f87a3b477 | |||
| 6a3e9f01d0 | |||
| 4ab74f64e2 | |||
| ee859c4311 | |||
| 8fa450d734 | |||
| 48a2b4c8d0 | |||
| 1a47952358 | |||
| 8df5c2a8f9 | |||
| dbca41640f | |||
| 09824195c9 | |||
| 76bd313f79 | |||
| c5ab3329ae | |||
| da69acf919 | |||
| c5ca585134 | |||
| cbe611916e | |||
| 9e124a5e92 | |||
| 71722127f1 |
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
build/
|
||||
__pycache__*
|
||||
__pycache__*
|
||||
exporttest/out
|
||||
12
BEdit/.gitignore
vendored
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
24
BEdit/resources/icons/AUTHORS
Normal file
@@ -0,0 +1,24 @@
|
||||
Oxygen Icon Theme has been developed by The Oxygen Team.
|
||||
|
||||
Art Directors:
|
||||
Nuno F. Pinheiro <nuno@nuno-icons.com>
|
||||
David Vignoni <david@oxygen-icons.org>
|
||||
|
||||
Naming Coordinator
|
||||
Jakob Petsovits <jpetso@gmx.at>
|
||||
|
||||
Designers:
|
||||
David J. Miller <miller@oxygen-icons.org>
|
||||
David Vignoni <david@oxygen-icons.org>
|
||||
Johann Ollivier Lapeyre <johann@oxygen-icons.org>
|
||||
Kenneth Wimer <ken@oxygen-icons.org>
|
||||
Nuno F. Pinheiro <nuno@nuno-icons.com>
|
||||
Riccardo Iaconelli <riccardo@oxygen-icons.org>
|
||||
David J. Miller <miller@oxygen-icons.org>
|
||||
|
||||
Thanks to:
|
||||
Lee Olson: Contributed drawing used in application-x-bittorent icon.
|
||||
Marco Aurélio "Coré": Improved audio-input-microphone icon.
|
||||
Matthias Kretz: Contributed "audio-input-line" device icon.
|
||||
Mauricio Piacentini <piacentini@kde.org> : game icons mashup
|
||||
Erlend Hamberg: "text-x-haskell" mimetype icon.
|
||||
216
BEdit/resources/icons/COPYING
Normal file
@@ -0,0 +1,216 @@
|
||||
The Oxygen Icon Theme
|
||||
Copyright (C) 2007 Nuno Pinheiro <nuno@oxygen-icons.org>
|
||||
Copyright (C) 2007 David Vignoni <david@icon-king.com>
|
||||
Copyright (C) 2007 David Miller <miller@oxygen-icons.org>
|
||||
Copyright (C) 2007 Johann Ollivier Lapeyre <johann@oxygen-icons.org>
|
||||
Copyright (C) 2007 Kenneth Wimer <kwwii@bootsplash.org>
|
||||
Copyright (C) 2007 Riccardo Iaconelli <riccardo@oxygen-icons.org>
|
||||
|
||||
|
||||
and others
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 3 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Clarification:
|
||||
|
||||
The GNU Lesser General Public License or LGPL is written for
|
||||
software libraries in the first place. We expressly want the LGPL to
|
||||
be valid for this artwork library too.
|
||||
|
||||
KDE Oxygen theme icons is a special kind of software library, it is an
|
||||
artwork library, it's elements can be used in a Graphical User Interface, or
|
||||
GUI.
|
||||
|
||||
Source code, for this library means:
|
||||
- where they exist, SVG;
|
||||
- otherwise, if applicable, the multi-layered formats xcf or psd, or
|
||||
otherwise png.
|
||||
|
||||
The LGPL in some sections obliges you to make the files carry
|
||||
notices. With images this is in some cases impossible or hardly useful.
|
||||
|
||||
With this library a notice is placed at a prominent place in the directory
|
||||
containing the elements. You may follow this practice.
|
||||
|
||||
The exception in section 5 of the GNU Lesser General Public License covers
|
||||
the use of elements of this art library in a GUI.
|
||||
|
||||
kde-artists [at] kde.org
|
||||
|
||||
-----
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
1
BEdit/resources/icons/README
Normal file
@@ -0,0 +1 @@
|
||||
Oxygen Icons is a freedesktop.org compatible icon theme originally developed for the KDE Plasma desktop environment in combination with the Oxygen Style. It features smooth gradients, soft shadows, and a slightly glossy look.
|
||||
BIN
BEdit/resources/icons/arrow-down.png
Normal file
|
After Width: | Height: | Size: 1006 B |
BIN
BEdit/resources/icons/arrow-up.png
Normal file
|
After Width: | Height: | Size: 927 B |
BIN
BEdit/resources/icons/configure.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
BEdit/resources/icons/document-new.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
BEdit/resources/icons/document-open.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
BEdit/resources/icons/document-save-as.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
BEdit/resources/icons/document-save.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
BEdit/resources/icons/draw-bezier-curves.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
BEdit/resources/icons/draw-circle.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
BEdit/resources/icons/draw-ellipse.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
BEdit/resources/icons/draw-path.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
BEdit/resources/icons/draw-rectangle.png
Normal file
|
After Width: | Height: | Size: 658 B |
BIN
BEdit/resources/icons/draw-text.png
Normal file
|
After Width: | Height: | Size: 598 B |
BIN
BEdit/resources/icons/draw-triangle.png
Normal file
|
After Width: | Height: | Size: 879 B |
BIN
BEdit/resources/icons/edit-copy.png
Normal file
|
After Width: | Height: | Size: 860 B |
BIN
BEdit/resources/icons/edit-cut.png
Normal file
|
After Width: | Height: | Size: 892 B |
BIN
BEdit/resources/icons/edit-paste.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
BEdit/resources/icons/edit-redo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
BEdit/resources/icons/edit-select.png
Normal file
|
After Width: | Height: | Size: 991 B |
BIN
BEdit/resources/icons/edit-undo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
BEdit/resources/icons/list-add.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
BEdit/resources/icons/list-remove.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
BEdit/resources/icons/media-playback-start.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
BEdit/resources/icons/network-connect.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
BEdit/resources/icons/office-chart-line.png
Normal file
|
After Width: | Height: | Size: 877 B |
BIN
BEdit/resources/icons/preferences-system.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
BEdit/resources/icons/run-build.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
BEdit/resources/icons/transform-rotate.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
BEdit/resources/icons/view-form-table.png
Normal file
|
After Width: | Height: | Size: 429 B |
BIN
BEdit/resources/icons/zoom-in.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
BEdit/resources/icons/zoom-original.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
BEdit/resources/icons/zoom-out.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
36
BEdit/resources/resources.qrc
Normal 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>
|
||||
2998
BEdit/resources/resources_rc.py
Normal file
4
BEdit/src/bedit/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""BEdit desktop application."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
5
BEdit/src/bedit/__main__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from bedit.gui.app import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
5
BEdit/src/bedit/core/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Pure BEdit domain model and persistence; this package has no Qt dependency."""
|
||||
|
||||
from bedit.core.model import Component, Connection, Endpoint, Graph, GraphDocument, Icon, Port
|
||||
|
||||
__all__ = ["Component", "Connection", "Endpoint", "Graph", "GraphDocument", "Icon", "Port"]
|
||||
9
BEdit/src/bedit/core/application_log.py
Normal 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}")
|
||||
321
BEdit/src/bedit/core/bond_graph.py
Normal 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).")
|
||||
32
BEdit/src/bedit/core/libraries.py
Normal 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")))
|
||||
769
BEdit/src/bedit/core/model.py
Normal 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)
|
||||
47
BEdit/src/bedit/core/physical_types.py
Normal 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",
|
||||
)
|
||||
41
BEdit/src/bedit/core/port_types.py
Normal 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
|
||||
49
BEdit/src/bedit/core/power_domains.py
Normal 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
|
||||
76
BEdit/src/bedit/core/serializer.py
Normal 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)
|
||||
24
BEdit/src/bedit/core/simulation/__init__.py
Normal 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",
|
||||
]
|
||||
452
BEdit/src/bedit/core/simulation/composer.py
Normal 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}")
|
||||
639
BEdit/src/bedit/core/simulation/openmodelica.py
Normal 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)
|
||||
237
BEdit/src/bedit/core/simulation/results.py
Normal 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)
|
||||
196
BEdit/src/bedit/core/simulation/service.py
Normal 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
|
||||
BIN
BEdit/src/bedit/data/libraries/bondgraph.beb
Normal file
BIN
BEdit/src/bedit/data/libraries/default.beb
Normal file
22
BEdit/src/bedit/data/syntax/openmodelica.json
Normal 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": []
|
||||
}
|
||||
1
BEdit/src/bedit/gui/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Qt user interface and application adapters."""
|
||||
65
BEdit/src/bedit/gui/app.py
Normal 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()
|
||||
26
BEdit/src/bedit/gui/application_log.py
Normal 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)
|
||||
5
BEdit/src/bedit/gui/controllers/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Qt-aware application controllers and undo commands."""
|
||||
|
||||
from bedit.gui.controllers.document import DocumentController
|
||||
|
||||
__all__ = ["DocumentController"]
|
||||
408
BEdit/src/bedit/gui/controllers/commands.py
Normal 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
|
||||
1513
BEdit/src/bedit/gui/controllers/document.py
Normal file
1
BEdit/src/bedit/gui/dialogs/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Application dialogs."""
|
||||
34
BEdit/src/bedit/gui/dialogs/component_options.py
Normal 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()
|
||||
18
BEdit/src/bedit/gui/dialogs/connection_chooser.py
Normal 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()
|
||||
93
BEdit/src/bedit/gui/dialogs/graph_parameters.py
Normal 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
|
||||
60
BEdit/src/bedit/gui/dialogs/item_options.py
Normal 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()
|
||||
159
BEdit/src/bedit/gui/dialogs/parameter_options.py
Normal 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()
|
||||
239
BEdit/src/bedit/gui/dialogs/port_options.py
Normal 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()
|
||||
240
BEdit/src/bedit/gui/dialogs/settings.py
Normal 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()
|
||||
60
BEdit/src/bedit/gui/dialogs/simulation_settings.py
Normal 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
|
||||
1
BEdit/src/bedit/gui/editors/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Reusable editing widgets."""
|
||||
252
BEdit/src/bedit/gui/editors/openmodelica.py
Normal 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)
|
||||
110
BEdit/src/bedit/gui/editors/text_definition.py
Normal 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)
|
||||
1
BEdit/src/bedit/gui/generated/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Generated Qt code. Do not edit these modules by hand."""
|
||||
2998
BEdit/src/bedit/gui/generated/resources_rc.py
Normal file
95
BEdit/src/bedit/gui/generated/ui_component_options_dialog.py
Normal 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
|
||||
|
||||
@@ -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
|
||||
|
||||
64
BEdit/src/bedit/gui/generated/ui_graph_parameters_dialog.py
Normal 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
|
||||
|
||||
191
BEdit/src/bedit/gui/generated/ui_icon_editor_dialog.py
Normal 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
|
||||
|
||||
653
BEdit/src/bedit/gui/generated/ui_main_window.py
Normal 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
|
||||
|
||||
193
BEdit/src/bedit/gui/generated/ui_parameter_options_dialog.py
Normal 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
|
||||
|
||||
310
BEdit/src/bedit/gui/generated/ui_port_options_dialog.py
Normal 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
|
||||
|
||||
304
BEdit/src/bedit/gui/generated/ui_settings_dialog.py
Normal 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
|
||||
|
||||
194
BEdit/src/bedit/gui/generated/ui_shape_options_dialog.py
Normal file
@@ -0,0 +1,194 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
################################################################################
|
||||
## Form generated from reading UI file 'shape_options_dialog.ui'
|
||||
##
|
||||
## Created by: Qt User Interface Compiler version 6.11.1
|
||||
##
|
||||
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||
################################################################################
|
||||
|
||||
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
||||
QMetaObject, QObject, QPoint, QRect,
|
||||
QSize, QTime, QUrl, Qt)
|
||||
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
||||
QFont, QFontDatabase, QGradient, QIcon,
|
||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||
from PySide6.QtWidgets import (QAbstractButton, QApplication, QComboBox, QDialog,
|
||||
QDialogButtonBox, QDoubleSpinBox, QFormLayout, QLabel,
|
||||
QLineEdit, QPushButton, QSizePolicy, QSpacerItem,
|
||||
QVBoxLayout, QWidget)
|
||||
|
||||
class Ui_ShapeOptionsDialog(object):
|
||||
def setupUi(self, ShapeOptionsDialog):
|
||||
if not ShapeOptionsDialog.objectName():
|
||||
ShapeOptionsDialog.setObjectName(u"ShapeOptionsDialog")
|
||||
ShapeOptionsDialog.resize(420, 440)
|
||||
self.dialogLayout = QVBoxLayout(ShapeOptionsDialog)
|
||||
self.dialogLayout.setObjectName(u"dialogLayout")
|
||||
self.optionsForm = QFormLayout()
|
||||
self.optionsForm.setObjectName(u"optionsForm")
|
||||
self.widthLabel = QLabel(ShapeOptionsDialog)
|
||||
self.widthLabel.setObjectName(u"widthLabel")
|
||||
|
||||
self.optionsForm.setWidget(0, QFormLayout.ItemRole.LabelRole, self.widthLabel)
|
||||
|
||||
self.widthSpin = QDoubleSpinBox(ShapeOptionsDialog)
|
||||
self.widthSpin.setObjectName(u"widthSpin")
|
||||
self.widthSpin.setMinimum(1.000000000000000)
|
||||
self.widthSpin.setMaximum(500.000000000000000)
|
||||
|
||||
self.optionsForm.setWidget(0, QFormLayout.ItemRole.FieldRole, self.widthSpin)
|
||||
|
||||
self.heightLabel = QLabel(ShapeOptionsDialog)
|
||||
self.heightLabel.setObjectName(u"heightLabel")
|
||||
|
||||
self.optionsForm.setWidget(1, QFormLayout.ItemRole.LabelRole, self.heightLabel)
|
||||
|
||||
self.heightSpin = QDoubleSpinBox(ShapeOptionsDialog)
|
||||
self.heightSpin.setObjectName(u"heightSpin")
|
||||
self.heightSpin.setMinimum(1.000000000000000)
|
||||
self.heightSpin.setMaximum(500.000000000000000)
|
||||
|
||||
self.optionsForm.setWidget(1, QFormLayout.ItemRole.FieldRole, self.heightSpin)
|
||||
|
||||
self.lineStyleLabel = QLabel(ShapeOptionsDialog)
|
||||
self.lineStyleLabel.setObjectName(u"lineStyleLabel")
|
||||
|
||||
self.optionsForm.setWidget(2, QFormLayout.ItemRole.LabelRole, self.lineStyleLabel)
|
||||
|
||||
self.lineStyleCombo = QComboBox(ShapeOptionsDialog)
|
||||
self.lineStyleCombo.addItem("")
|
||||
self.lineStyleCombo.addItem("")
|
||||
self.lineStyleCombo.addItem("")
|
||||
self.lineStyleCombo.addItem("")
|
||||
self.lineStyleCombo.addItem("")
|
||||
self.lineStyleCombo.setObjectName(u"lineStyleCombo")
|
||||
|
||||
self.optionsForm.setWidget(2, QFormLayout.ItemRole.FieldRole, self.lineStyleCombo)
|
||||
|
||||
self.lineWidthLabel = QLabel(ShapeOptionsDialog)
|
||||
self.lineWidthLabel.setObjectName(u"lineWidthLabel")
|
||||
|
||||
self.optionsForm.setWidget(3, QFormLayout.ItemRole.LabelRole, self.lineWidthLabel)
|
||||
|
||||
self.lineWidthSpin = QDoubleSpinBox(ShapeOptionsDialog)
|
||||
self.lineWidthSpin.setObjectName(u"lineWidthSpin")
|
||||
self.lineWidthSpin.setMinimum(0.100000000000000)
|
||||
self.lineWidthSpin.setMaximum(20.000000000000000)
|
||||
self.lineWidthSpin.setSingleStep(0.500000000000000)
|
||||
|
||||
self.optionsForm.setWidget(3, QFormLayout.ItemRole.FieldRole, self.lineWidthSpin)
|
||||
|
||||
self.strokeColorLabel = QLabel(ShapeOptionsDialog)
|
||||
self.strokeColorLabel.setObjectName(u"strokeColorLabel")
|
||||
|
||||
self.optionsForm.setWidget(4, QFormLayout.ItemRole.LabelRole, self.strokeColorLabel)
|
||||
|
||||
self.strokeColorButton = QPushButton(ShapeOptionsDialog)
|
||||
self.strokeColorButton.setObjectName(u"strokeColorButton")
|
||||
|
||||
self.optionsForm.setWidget(4, QFormLayout.ItemRole.FieldRole, self.strokeColorButton)
|
||||
|
||||
self.fillTypeLabel = QLabel(ShapeOptionsDialog)
|
||||
self.fillTypeLabel.setObjectName(u"fillTypeLabel")
|
||||
|
||||
self.optionsForm.setWidget(5, QFormLayout.ItemRole.LabelRole, self.fillTypeLabel)
|
||||
|
||||
self.fillTypeCombo = QComboBox(ShapeOptionsDialog)
|
||||
self.fillTypeCombo.addItem("")
|
||||
self.fillTypeCombo.addItem("")
|
||||
self.fillTypeCombo.setObjectName(u"fillTypeCombo")
|
||||
|
||||
self.optionsForm.setWidget(5, QFormLayout.ItemRole.FieldRole, self.fillTypeCombo)
|
||||
|
||||
self.fillColorLabel = QLabel(ShapeOptionsDialog)
|
||||
self.fillColorLabel.setObjectName(u"fillColorLabel")
|
||||
|
||||
self.optionsForm.setWidget(6, QFormLayout.ItemRole.LabelRole, self.fillColorLabel)
|
||||
|
||||
self.fillColorButton = QPushButton(ShapeOptionsDialog)
|
||||
self.fillColorButton.setObjectName(u"fillColorButton")
|
||||
|
||||
self.optionsForm.setWidget(6, QFormLayout.ItemRole.FieldRole, self.fillColorButton)
|
||||
|
||||
self.cornerRadiusLabel = QLabel(ShapeOptionsDialog)
|
||||
self.cornerRadiusLabel.setObjectName(u"cornerRadiusLabel")
|
||||
|
||||
self.optionsForm.setWidget(7, QFormLayout.ItemRole.LabelRole, self.cornerRadiusLabel)
|
||||
|
||||
self.cornerRadiusSpin = QDoubleSpinBox(ShapeOptionsDialog)
|
||||
self.cornerRadiusSpin.setObjectName(u"cornerRadiusSpin")
|
||||
self.cornerRadiusSpin.setMaximum(50.000000000000000)
|
||||
|
||||
self.optionsForm.setWidget(7, QFormLayout.ItemRole.FieldRole, self.cornerRadiusSpin)
|
||||
|
||||
self.textLabel = QLabel(ShapeOptionsDialog)
|
||||
self.textLabel.setObjectName(u"textLabel")
|
||||
|
||||
self.optionsForm.setWidget(8, QFormLayout.ItemRole.LabelRole, self.textLabel)
|
||||
|
||||
self.textEdit = QLineEdit(ShapeOptionsDialog)
|
||||
self.textEdit.setObjectName(u"textEdit")
|
||||
|
||||
self.optionsForm.setWidget(8, QFormLayout.ItemRole.FieldRole, self.textEdit)
|
||||
|
||||
self.fontSizeLabel = QLabel(ShapeOptionsDialog)
|
||||
self.fontSizeLabel.setObjectName(u"fontSizeLabel")
|
||||
|
||||
self.optionsForm.setWidget(9, QFormLayout.ItemRole.LabelRole, self.fontSizeLabel)
|
||||
|
||||
self.fontSizeSpin = QDoubleSpinBox(ShapeOptionsDialog)
|
||||
self.fontSizeSpin.setObjectName(u"fontSizeSpin")
|
||||
self.fontSizeSpin.setMinimum(4.000000000000000)
|
||||
self.fontSizeSpin.setMaximum(96.000000000000000)
|
||||
|
||||
self.optionsForm.setWidget(9, QFormLayout.ItemRole.FieldRole, self.fontSizeSpin)
|
||||
|
||||
|
||||
self.dialogLayout.addLayout(self.optionsForm)
|
||||
|
||||
self.verticalSpacer = QSpacerItem(20, 20, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
|
||||
|
||||
self.dialogLayout.addItem(self.verticalSpacer)
|
||||
|
||||
self.buttonBox = QDialogButtonBox(ShapeOptionsDialog)
|
||||
self.buttonBox.setObjectName(u"buttonBox")
|
||||
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
|
||||
|
||||
self.dialogLayout.addWidget(self.buttonBox)
|
||||
|
||||
|
||||
self.retranslateUi(ShapeOptionsDialog)
|
||||
self.buttonBox.accepted.connect(ShapeOptionsDialog.accept)
|
||||
self.buttonBox.rejected.connect(ShapeOptionsDialog.reject)
|
||||
|
||||
QMetaObject.connectSlotsByName(ShapeOptionsDialog)
|
||||
# setupUi
|
||||
|
||||
def retranslateUi(self, ShapeOptionsDialog):
|
||||
ShapeOptionsDialog.setWindowTitle(QCoreApplication.translate("ShapeOptionsDialog", u"Shape Options", None))
|
||||
self.widthLabel.setText(QCoreApplication.translate("ShapeOptionsDialog", u"Width:", None))
|
||||
self.heightLabel.setText(QCoreApplication.translate("ShapeOptionsDialog", u"Height:", None))
|
||||
self.lineStyleLabel.setText(QCoreApplication.translate("ShapeOptionsDialog", u"Line style:", None))
|
||||
self.lineStyleCombo.setItemText(0, QCoreApplication.translate("ShapeOptionsDialog", u"solid", None))
|
||||
self.lineStyleCombo.setItemText(1, QCoreApplication.translate("ShapeOptionsDialog", u"dash", None))
|
||||
self.lineStyleCombo.setItemText(2, QCoreApplication.translate("ShapeOptionsDialog", u"dot", None))
|
||||
self.lineStyleCombo.setItemText(3, QCoreApplication.translate("ShapeOptionsDialog", u"dash-dot", None))
|
||||
self.lineStyleCombo.setItemText(4, QCoreApplication.translate("ShapeOptionsDialog", u"none", None))
|
||||
|
||||
self.lineWidthLabel.setText(QCoreApplication.translate("ShapeOptionsDialog", u"Line width:", None))
|
||||
self.strokeColorLabel.setText(QCoreApplication.translate("ShapeOptionsDialog", u"Line colour:", None))
|
||||
self.strokeColorButton.setText(QCoreApplication.translate("ShapeOptionsDialog", u"Choose\u2026", None))
|
||||
self.fillTypeLabel.setText(QCoreApplication.translate("ShapeOptionsDialog", u"Fill type:", None))
|
||||
self.fillTypeCombo.setItemText(0, QCoreApplication.translate("ShapeOptionsDialog", u"solid", None))
|
||||
self.fillTypeCombo.setItemText(1, QCoreApplication.translate("ShapeOptionsDialog", u"none", None))
|
||||
|
||||
self.fillColorLabel.setText(QCoreApplication.translate("ShapeOptionsDialog", u"Fill colour:", None))
|
||||
self.fillColorButton.setText(QCoreApplication.translate("ShapeOptionsDialog", u"Choose\u2026", None))
|
||||
self.cornerRadiusLabel.setText(QCoreApplication.translate("ShapeOptionsDialog", u"Corner radius:", None))
|
||||
self.textLabel.setText(QCoreApplication.translate("ShapeOptionsDialog", u"Text:", None))
|
||||
self.fontSizeLabel.setText(QCoreApplication.translate("ShapeOptionsDialog", u"Font size:", None))
|
||||
# retranslateUi
|
||||
|
||||
123
BEdit/src/bedit/gui/generated/ui_simulation_settings_dialog.py
Normal 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
|
||||
|
||||
244
BEdit/src/bedit/gui/generated/ui_simulation_window.py
Normal 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
|
||||
|
||||
114
BEdit/src/bedit/gui/generated/ui_text_definition_editor.py
Normal 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
|
||||
|
||||
5
BEdit/src/bedit/gui/graphics/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Graphics scenes, views, editors, and renderers."""
|
||||
|
||||
from bedit.gui.graphics.workspace import GraphWorkspaceView
|
||||
|
||||
__all__ = ["GraphWorkspaceView"]
|
||||
35
BEdit/src/bedit/gui/graphics/connection_styles.py
Normal 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())
|
||||
47
BEdit/src/bedit/gui/graphics/icon_canvas.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from PySide6.QtCore import QPointF, QRectF
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QColor, QPainter, QPen, QWheelEvent
|
||||
from PySide6.QtWidgets import QGraphicsView
|
||||
|
||||
from bedit.gui.preferences import application_settings
|
||||
|
||||
|
||||
def icon_grid_size() -> int:
|
||||
return application_settings().value("grid/iconSize", 8, type=int)
|
||||
|
||||
|
||||
class IconCanvasView(QGraphicsView):
|
||||
"""Designer-promotable view that paints the icon editor grid."""
|
||||
|
||||
def drawBackground(self, painter: QPainter, rect: QRectF) -> None: # noqa: N802
|
||||
painter.fillRect(rect, QColor("#ffffff"))
|
||||
grid = icon_grid_size()
|
||||
painter.setPen(QPen(QColor("#dbeafe"), 0))
|
||||
left = int(rect.left()) - int(rect.left()) % grid
|
||||
top = int(rect.top()) - int(rect.top()) % grid
|
||||
for x in range(left, int(rect.right()) + grid, grid):
|
||||
painter.drawLine(QPointF(x, rect.top()), QPointF(x, rect.bottom()))
|
||||
for y in range(top, int(rect.bottom()) + grid, grid):
|
||||
painter.drawLine(QPointF(rect.left(), y), QPointF(rect.right(), y))
|
||||
|
||||
def _zoom(self, factor: float) -> None:
|
||||
target = self.transform().m11() * factor
|
||||
if 0.25 <= target <= 16.0:
|
||||
self.scale(factor, factor)
|
||||
|
||||
def zoom_in(self) -> None:
|
||||
self._zoom(1.2)
|
||||
|
||||
def zoom_out(self) -> None:
|
||||
self._zoom(1 / 1.2)
|
||||
|
||||
def center_icon(self) -> None:
|
||||
if self.scene() is not None:
|
||||
self.fitInView(
|
||||
self.scene().sceneRect().adjusted(-10, -10, 10, 10),
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
)
|
||||
|
||||
def wheelEvent(self, event: QWheelEvent) -> None: # noqa: N802
|
||||
self._zoom(1.2 if event.angleDelta().y() > 0 else 1 / 1.2)
|
||||
event.accept()
|
||||
561
BEdit/src/bedit/gui/graphics/icon_editor.py
Normal 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()
|
||||
106
BEdit/src/bedit/gui/graphics/icon_renderer.py
Normal 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))
|
||||