Compare commits

..

15 Commits

Author SHA1 Message Date
8a6203d01e Fixed some bugs 2026-07-22 19:16:56 +02:00
2d49f65ec4 Fixed some bugs 2026-07-22 18:13:13 +02:00
c918a6a428 causality inference done 2026-07-22 18:04:07 +02:00
32154386e8 Start with causality inference 2026-07-22 13:20:49 +02:00
09248421d0 Bond graph ports and drawing added 2026-07-22 12:24:58 +02:00
0c578af85d port and param editor in and outside of text editor the same 2026-07-22 11:45:43 +02:00
edefb23edc More types and units 2026-07-22 11:38:07 +02:00
48ac9e2f59 proper modelica text model editing 2026-07-21 17:16:06 +02:00
2f6487e510 Added direct graph navigation without activating Matplotlib toolbar modes:
Mouse wheel zooms in and out around the cursor.
Left-click dragging pans the graph.
Matplotlib toolbar modes still work and take precedence when activated.
The existing Home button can reset the view.
2026-07-21 15:29:58 +02:00
eb8f77ce64 better signal graphing 2026-07-21 15:24:24 +02:00
35d933ccbb Added signal graphing 2026-07-21 15:14:12 +02:00
edd7bb98f2 Saving sim results 2026-07-21 14:40:03 +02:00
ddc004dee5 sim window save/load 2026-07-21 14:01:55 +02:00
cdfc891980 Start of a sim window 2026-07-21 13:54:57 +02:00
6fb2478589 basic runner setup changed 2026-07-21 13:21:52 +02:00
53 changed files with 6685 additions and 1416 deletions

View File

@@ -6,7 +6,6 @@
"type": "shell",
"command": "pyside6-designer",
"args": [
"${workspaceFolder}/ui/*.ui"
],
"options": {
"cwd": "${workspaceFolder}",
@@ -116,6 +115,14 @@
"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",
@@ -126,7 +133,8 @@
"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 Icon Editor UI to Python",
"Qt: Compile Simulation Window UI to Python"
],
"problemMatcher": [],
"group": {

View File

@@ -25,7 +25,7 @@ src/bedit/
│ ├── port_types.py # Port type definitions and compatibility
│ ├── serializer.py # JSON persistence
│ ├── libraries.py # Library file discovery and parsing
│ └── simulation/ # Qt-free simulation service and compiler/runtime code
│ └── 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
@@ -56,16 +56,33 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
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.
- Port orientation is presented as one unified list in the UI, while the model
indexes inputs and outputs separately for connection semantics.
- 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` is currently the only type.
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.
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
@@ -74,7 +91,8 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
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. Do not scatter those constants through painters.
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
@@ -94,31 +112,92 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
- 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.
- Simulation compilation lives in `core/simulation/compiler.py`; the simulation
service only owns application state and delegates compilation. Ports with
- 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.
- Blocking simulator integration belongs in `core/simulation/runner.py` and runs
on its daemon worker thread. Never perform OMPython startup or simulation work
directly on the Qt GUI thread; background failures must go to the application
log.
- 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
@@ -159,6 +238,9 @@ pyside6-uic --from-imports ui/text_definition_editor.ui \
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
@@ -250,6 +332,7 @@ 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

View File

@@ -159,12 +159,12 @@ Every component owns its ports, declarative icon, properties, and child graph:
"name": "My Component",
"position": {"x": 0, "y": 0},
"interface": {
"inputs": [{"id": "in", "name": "Input", "type": "signal", "properties": {
"iconPosition": {"x": 0, "y": 40}
}}],
"outputs": [{"id": "out", "name": "Output", "type": "signal", "properties": {
"iconPosition": {"x": 128, "y": 64}
}}]
"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},

View File

@@ -9,6 +9,7 @@ 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",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 877 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 B

View File

@@ -1,5 +1,9 @@
<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>

View File

@@ -862,6 +862,75 @@ N\xa7\xd3>==\xfd\xa1\xc3\xe1\x98\x92$\xe9I\x00\
\xbf\xc9\xd3\x01\x9f\xa3\x0f\x17Z/\xe3\x7f\xe1/\x17\x85\
\xd6\x06q(\x0e\x10\x00\x00\x00\x00IEND\xaeB\
`\x82\
\x00\x00\x04)\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
\x00\x00\x00\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\
\xa7\x93\x00\x00\x00\x09pHYs\x00\x007\x5c\x00\x00\
7\x5c\x01\xcb\xc7\xa4\xb9\x00\x00\x03\x9bIDATx\
\xda\xedT=o\x1bG\x10}\xbb\xb7w$\xf5A\x9e\
lJEB\x18\xb1\x0a\x1bv\x13\xdb\x85\xaa\x18P$\
\xa4\x89\x81\x94\xd6_\x08`\xb8r\x93 \x85\x02\x01\xa9\
\x9c\x1f\xe0>\x85\x81\x94F\x00\xa7\x11\xd88\x85\x1bK\
6@)\x94-\xc8\x96\x10\x18\x81\x22\xf3C\x22i\xf2\
\xf6v\xf3B.s\xd1\xb1H\x80\xb83\x07x\x98\xbd\
\xdd\x99y3\xb3\xb7\x83\xb1\x8ce,\xef\xbd\x88\x1f\x97\
\x97\xe1\x05\x01\x820D~~\x1e\xdb\xf7\xee\xa1st\
\x94\x188H\x07/\x05\xe5\xe0\x0f\xb5C\xf0\xb7N\xd6\
\xb9\xb3g\x11\xde\xba\x85\xf8\xd5+\xc4\xb5\x1a`\x0c\xc4\
\x83B\x01\x82\xe4\xe1\xcd\x9b\xf2\x8f'Ol.\x9f\xb7\
\x96\x07\xd1\xc9\x09\xbeX_\xc7\xbb\x90\xd7\xd7\xafCN\
M\x01RB7\x9bbraA4\xee\xdf7q\xa3\
\x01\xf1\xc3\xf9\xf3\xf0\xc3P\xac\x90\xfc\xd1\xda\x9a|v\
\xf7\xee4\xc9\xbdw\xda\x01\x87\xec\xf4t<\xb7\xba\xda\
\xcc\xdc\xb9c\xebW\xae\x88\xb8^\xb7\xa2|\xfb\xb6\xf8\
\xe0\xeaU\xdbl4>?\xda\xd8\xf8\xf6\xa0\x5c\xfe\xa8\
yp \xff=\x01G\x9eN\x22!\x1fM\xa0T2\
S\x8b\x8b{\xb9\x85\x85U\xe4r?G\x9b\x9bB\xd5\
\xaaU[\x7f\xfe|)\xbcp\xe1Avv\xd6\x93a\
h\xdb\x07\x07B\xfc\xe7\x04\x122\xe3`\x07p~\x09\
\x04c\xa3X\x9c\xd5;;?\x89\xdd\xdd\xcf\xa41e\
e\x94B\xb3Z\xfd\xca\xfa\xbeWZZ\x8a>^Y\
Q\xf3\xec\x88\xa7\x94#'\xac\x85L\x05\x1c\x09\x9e\xe8\
S~\x88cxR\x22\xc8\xe7\xa1\xe7\xe6`\x84\x88\xf4\
\xe3\xc7\xbe\xa8V\xbf6\x17/\x96\xd5\xeb\x87\x0f\x0bZ\
\xa9K\xad(\xc2\x87\xd7\xaey\xc1\x8b\x17b\xe6\xf8\x18\
\xca\xf3\xe0Y\x9bT<B\x98\x00B\x0c\xaa\xa6\xbd!\
bc\xfa\xdad2\x00\x89\xdbL`\xfb\xe9S\xfc\xba\
\xb5\x85Oo\xdc\xf0J\x95\x0a\xec\xfe\xfe\xa5\xdc\xcb\x97\
\x05\x15[\xabzQ\xe4\xd7\xf6\xf7qD\x833$\xb7\
LF1\xa8JZ\x9dJ\xc2\x81$\x18VJX\x12\
\x81\xa4rr\x12&\x9bE\xa7\xd3\xc1\xef\x9b\x9b\xf8m\
{\x1b\xf5\xb7o\xd1e\xcc\xe3j\x15zo\x0f\xf4\xf1\
\xd9\x0d\xa5\x0c\x00\x87~\xe6\xacf\x00\x0a\x83\x9e\xd6\xff\
l=\xc9$\xbb$\x95\x02\x82\x00\xa0\x8e\xe9\xd7k\xb7\
\xd1\x22A\x83o\xbd~x\x88\xb6\xf3\xcb\xf2\xacEm\
\xc8A\xe2~\xd2\xc6\x15\xa7\xb9\x88b\x00o8\x1c\xba\
t\x0a\xba]L\xb1\x82\xac\xef#\xabT\x9f\xcc#\x04\
\x1d\xe5\x10\x0c \xb4\x86\xe9\xf5`834\xdft\xaf\
^G\xf7\xafJ\x1di\x86v\x9a~]\xdaj\x12c\
b\x02\xbe\xbb\x22IN^\x99V\x87B4&\xac\xad\
\x00(\xedll\xe8\xe2\xb9s\xbeGbEC\x9fW\
\xe1\xc71|!\x86W\xd2\xdfW\x0c\xe0q_\x11\xc3\
\xffD\xce\xcc@\x16\x8b\x10\x1e\xbf\x5c\x85Y\x22\x00P\
\xa0\xd6\xd4>\x8b9\xb3\xbb\xab\xa3A\xcf*5r\x8b\
5\xf4\xe5\x93\x0e\xb0~\xc2\x03\x1eZ#%\xfd\x93\xb6\
\xb3\xda\xd3O\x8f\x04\xbe\xd3\xa7\xde<\xed2\xc0\x08\xb2\
\x0e\x19\x06\xa5\xbf\xe0\xbaG\xbfe\x00\x8f\xd4174\
\x17\x0c\xb0\x04\xe0;\x03\x5cf\x8b\xa4M?\xad\xd4\xcf\
G\x0dV\x9f\x9e\x0f\xce~ v\xf8o\x11zpn\
\x14\xb0\xc5\xbdo\xda\xc0/\x92\xa6j\xd2\xcd\x8c<7\
\x9e\x01\x8b4\xca\xd3X%\x09$\xc3%\x91\xd1\xa4b\
\x87\xd40J\xdbk\xee5\xc9\x09=\xd8\xb2\xea\x8dK\
\xb6\x05\xc8\x90Z\xd0\x00\xceY\x11\xdf\xe3\xffI%\xf5\
mI\xdc\x224`\xfaE~\xe9\x0e\xa4\xd3=B\xa7\
F\xe9\xe8\xe8\x1dE\x90B&\xd1)\x0c\xc4`,c\
\x19\xcbX\x06\xf2'\xe3\xdf\x9d\x06\x06\xf6\x92\x0e\x00\x00\
\x00\x22zTXtSoftware\x00\x00\
x\xda+//\xd7\xcb\xcc\xcb.NN,H\xd5\xcb\
/J\x07\x006\xd8\x06X\x10S\xca\x5c\x00\x00\x00\x00\
IEND\xaeB`\x82\
\x00\x00\x02V\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -2326,6 +2395,158 @@ K\x80@\x89\x15\x8d\xc04\xd5\xb5^\xaf\x1bx\xfa\x19\
\x84\xe7\x04\xcf\x88\xfd\xfd\xfd\xe4\xe8\xe8\xc8\x9dL&\x14\
\xc7\xcc\x7f5p\xf2g\x94\xf7\x1f\xdf\x9a\xd2\x93\xfbC\
\xb7\xa7\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x03m\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\
\x00\x00\x09pHYs\x00\x00\x1b\xaf\x00\x00\x1b\xaf\x01\
^\x1a\x91\x1c\x00\x00\x00\x07tIME\x07\xd9\x02\x10\
\x17\x22\x16\x993\xa5<\x00\x00\x02\xedIDATx\
\xda\xed\x97_H\xd3Q\x14\xc7\xcf\xd9\xef7\xcb\xca\x89\
B \x91\x0f\xc9\xcczha\xf6\xa8\xb4\x06M\x90f\
aiV$\x96V\x12\xf9P-S\x90d%Lh\
V/B.\x8d\xc2\xb2\xc0\x1erS\xc1\x84\x1cdo\
a\xa0\x0f>h\x0f\xfdy\xf0-\x9b\xa6\x0f\xb1{\xfb\
n\xf4\x8b\x91\xe2\x5cn\xae\x07\x0f\xdc\x9dq/\xfc>\
\xe7{\xce\xe1\xdc\xdf\x8f\xd6M\xb3\x86\x86\x86\xe2Dp\
u\xbf\xe19pG\x82>!\x01\x04\xe1\x9aOd\x09\
:\x13V\x02\xcd\xd6\x03H\x84\xa9\x04\xab\xaf\xaf\xef\x84\
\xcb\x8f\x07\xe0z\xcf\x82AU\xc8\xa6\xeax\xfc\xf6\xd1\
\x8dcKf\xa0\xa5\xa5\xa5\x0an$V\xd0\xea\xc7\xf3\
\x86\x9a\xae\xf9\xd3\xb5\xdd\xf3\xaf\x84\xa4\xcfRR\xa9\x5c\
&\x031\xb3\xf2\xf6\x1f\xb9P\xdb\x14\x90\xd2\xacH\xf6\
\x01\xda\x13\x10T\xd1Z\x96\xec\xa7x\x07P\xd26\xc7\
P\xdb!\x04\xbd\x10L\x15\xedg7-\x82\xc65\x00\
\xc0m\x02?\x82\xd9\xf5\xa4j\xb3\x5c\xd3&,\xba\x1f\
R\x8f\xd4\x93\xe3\xe5\xc5\x08\xf0X7\xa1\xa6> B\
\xde\xbb&s\xc0\xb3\xf3x\x81'\xbbt\x1f\xc1,w\
f\x19M\xd7$\xa0\xbe\xf7\xf2\x16\x19\xf5\x1c\x88\x0e|\
L!\xc9\x8d$u\x97X\xa8\x0b\xde\xac\x0aS\xab$\
3\x07\xd5\xb3\xd4\xd4\xc7'\x00\xa8\xce\x00\xfc)\xe0\xcc\
\xa4\xdbK2\xe9\x96\x94\xaa\x0bu\xcfc\xa8\x1f\xbc\x92\
\x22\xffq\x14GnB\xa4\xdbBRy\xcfRy\xcb\
\xa4XmS\xcf\xa7I\xe8\xed,\xf4\x85\xbb&\xc7\xb4\
\xda\xc7\xfe:\x06X\xf1d\x975y\x8c\xe5_\xbd\xc6\
\x93\x96\xf0\xb3\x5c\x87\x9f++\xdf\x8d\xf6m\xbf\xfa\xa9\
o[\x9d!\xe6\x97\x11\xc0\x198\x1e\x84\xea\x02\xa4|\
?T\xbf\x09?\x97\x92\x8a\xc7\xb3\xf6H)\x92^#\
\x1b\xae\xe5 \x03\x06\xb7e \xa5\xc3\xb4\xd2\x00\x00?\
\x91\x87Z\x87R\x0e_h\x9b\xea\x9e\x0e?\xdf\xdd\xf8\
\x9d\x03B\xde\x0cv>\x9a\xf1\x1aK\xbd\xb5\x7fk\xb3\
u1\xb8\xdd0`x\xe8F\xf9\x1e\xe19\xea\x8a\x03\
`\xa9\xfb\x09\xf8)\x80\x1d\xb6\x8f\xcf\x02\xda~\xa6}\
\x86v\xdc\x981\x06\x045\x02NB\x90\xf7\xf0t\xb3\
\x1f\x19\xb8\x80\xe5\xeeOo5\x84\xa9>\x040\x1aD\
!\xc0MEs\xe7F\x97diM\xc8\xcc\xf9N\xa7\
3'\xfc0\xb9\xe6\x1b\xe9\x98\x8c\xaaB\x07\x92\x142\
\xebU6\xeb\xf1\xbc$\x85}\xd8\xbb;\xd1\x9c\xfa\xe1\
\x0f0\xfd\x9e;\x04\x13\x1b\xecD\xec\x02\xdcJ\x92\xcf\
\x03<\x14\xf5$\x04\xd8\x84\xd5\x85\xbf_P\xeba,\
3\x96\x0f\x8a\x0fB}\xe6\xa43\xf5L\x08\x1enB\
\xb5\x13J\x01\xb5\x13a\xaa\x87V3\x07|X\x0eI\
4\xe5oK\x8b|\x1f\xcc\xd4\xfa\xd1h%\x00\xa7\x02\
<\xbc\xaaA\xb4\xf0 m\x0c\x0e+:+\x9a\xad\x1e\
\xfd\xaf\xdf\x091k\xea\xfe\xfe\xf8\xe1H\xdf\x05R\xca\
|f\x1e\x89\xd1\x9ef\xbdhx\x0fE4mL\xaf\
b/B\x06\x12o\xbf\x00\xa3\x17WYZq\xd9W\
\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x05\xcf\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\
\x00\x00\x00\x09pHYs\x00\x00\x03v\x00\x00\x03v\
\x01}\xd5\x82\xcc\x00\x00\x00\x19tEXtSof\
tware\x00www.inksca\
pe.org\x9b\xee<\x1a\x00\x00\x05LID\
ATx\xda\xc5\x97\xdboTE\x1c\xc7\xbf3s\xce\
\x9e\xeeni\x0b\xbd@\x01C1\x05B\x10\x0d\xf4\x22\
\x18\x95D0i\xc4\x17|21\xc6\x17\xc3\x93\x89\xd1\
\xc8\x8b\xfaP#oj\x8c\x89O\xfc\x03\xc6K\x0c\xf1\
\x01!B\xa8\xc1j\x88D\x90@)\x14\xac\xb5\x86K\
K\xaf\xdb\xdd={\xce\xcc\xef\xe7\xec\x9cficb\
\xd2\x98\x13f\xf3\xdd93\xbf\x93\x9d\xcf\xfengW\
03\x1e\xe6\xf0\xb0\xc2\xf1\xca\xe9\x17wok\xda~\
lh\xe6\xe2\xd6\xbf\x0a#\xab\x94\x92h\xcb\xad\x9d{\
z\xdd\xfe\x9b\xf9 \xf7\xce\xe1\xad\xef\x0d`\x05Cb\
\x05\xa3\xe7\xcb\xce\xc7\x0emz\xf9\xfc\xab\x9d\x87\xbb\xf6\
\xb4\xec[\x15k\x82\x8e\x09\x9d\xf9\xed\x8d\x07\xda\x0fu\
=\xd5\xd2w\xe6\x83K\x87{S\xf3@N\xe4>\x9d\
)M{>\xd5\xa1\xab\xad\x17\x1b\x1a\xda\xdd~V\xe6\
\xb1)\xb7\x05\xe7\xee\x9e\x94\xf3\xa5\xe2'\x00\x9eI\x05\
\xe0^a\xa2\xe7\xdc\xdf\x03\xe8\x5c\xb3\x05\xf3\xb9k\x10\
A\xc5\xed\x87\x98\xc4\x99\xdb\xdf\xe1\xd4\xd8\xb7\x18-\x0e\
\xefL\xcd\x03aTi\x1c\x1c\xff\x11\xdd\x1bw\xa3I\
\x99\xe5F3\x87+\xf7/\xc2W\xb215\x00\x133\
\x08\x11E(\xc7\xcb\x01<\xa3\x9d]2#5\x00\
\xd2\x04!$\x8c!\x94u\xb4\xccVG1\x8c6\x10\
@\x9a\x00\x0c\x08\x86!B\x18G\x80X\xea\x01\x03c\
\xed\x02\x94\x1e\x80Y\x04\xd0d`,\x80\x00j\x10\x01\
i\x18\xc3\x10\xd6\x9ej\x0e\x08\xc5 bD:^\xe6\
\x01M\xfa\xffy\xe0\xe8\xe0\xd1\x17\xaeN\x5c\xed\x1f\x18\
\x1d\xd869w\xafA\x92\x84d\xc0\xbeC\xb1\x80\x94\
\x02JHw81!2\x0e\xa0\xc6\xa0\x8dv9b\
\xd0ul\x03{\xbe\x84\xaf\x14<_\xc1\xf7\xecu\
F\xa15\xd76\xff\xc4\xda\x9e\xeb\x1d\xab\xb6\xf7\xbf\xb4\
\xf9\xf5\x13\xb5N\xd8\xf7E_\xefTi\xea\xf8\xc6\xfa\
\x8d=mA[\x03\x99$\xdel\xd8\xcd\xb48\x1bw\
M\xd0\xdaX\x00\x8d\xd8\x98%\x8a@D\xee^c\xc8\
\xddk(\xb9\xdfp\xb2W\xef564\xaa\xd6\x9e\xf9\
h\xea\xf8G\xc3o\xb8\x8e\xe9\x1eF\x1d\x9fw\xfc\xb0\
\xabm\xd7\x81\xbd\xeb\xf7\x02\xc2`.\x9aAu\x9f\x85\
\x15\x5c\xdc\x935\x0c\x84\x00r\xb9,\xee\x9a?!\x93\
$p{\x1b\xbcGQ\xa9\x84P\xd2\x83'\xfd\x9a\x94\
\xf2\x90\xb1{\x81\xca\xc2g\xcf\xda\x0d\xc6\xc2!\xdc\x89\
n\x9e\xf9\xec\xc9\xef\x0f\xb8\x10\x8c\xcf\x8dw#\x06z\
\xda\xbb0T\xb8\x00-4 Q\x93\x84\x80\x14\x02J\
J7\xcb(Y\x0b'\xc7\x87\xb1\xe8\x06X\x00L\x9c\
x\x81\xaa\x22\x042\x8b\xbcX\x8d\x91\x89Q\x9c\x1a:\
\x8d\xf7\xf7\xbd\x85\xdb4\x8cyLv\xd5r\x80\x88\x9a\
\xc6\xa6\xc602u\x1dEQN\x0eVp\xb3\x10H\
\x0e\x96\x02\xcc\xb2\x06\xc2\xe2\x01Dm0\x83\x00x\xc2\
\xc3\xba\xecz\xcc\x95\x8b\xb81}\x0bgG\xbfF\x1c\
\x11\x00\xd8\xf50\x8a\xc1md|\xd9T\x03\x00[\x91\
k\xb5\xa8\xa88\x01 \x07\x91$\x1fK+\x07\x90\x1c\
\x0e\x86\xaa\x02A8\xefdT\x1d\x9a\x83V\x17\x8e0\
\x0a1\xb10\x81o\xfe\xf8\x0a\x85J\x11\xa5(\xb6\xa0\
\x12\x90\xe42V\xb3v\xf9@\x9e\xc0R\x80Y\x10\x9a\
f\x0a\xb3\xb8^\xbc\x09?\xf0\x9c\x82L\x80l\x10\xc0\
>\xe7\x91\xf7\xb2\x8eI2\x01,]G\x0cD\x00\x22\
\x8d\xf9h\x1a\xc3\x93\xbf!\xd4\x15h\x22D\xc6\x00\xd2\
\xc0S\xca\xca@\x19W.N\x19\xcf\xc3\x02\xbb\xf0\xcc\
.-\xc3A\x10\x0e\x0e\x0e\xff\x82\x1d\xebw .\xc5\
@9\x09A$\x04H\x85(\x8b\x18\x9e'\xdc\x87\xd6\
5H\xdc\xc1-\x04\x9erkea\x1e\xf1;Q\xd1\
\x06\xbc\x987\xe43L\x96\xa09)[cU\xefe\
1e\x93\x97du\x8f\x7f~\x00\xa0q\x04\xc0\xfe\x85\
\xf2B\xdd\xf9k\xe7\x81x1\x04\x9cP\x0b+)\x92\
^ \x94\xc0\xb3{z\xe1\xafV\xc8T3\xbc\x0a!\
%\xc2B\x8c\x9f.\xffj!%\xaa=@YyK\
\xe4\xfb\x0a\x85\x8cB&c\xd7\x19\xaf\xacY\x1f\xa9\xf5\
\x01\xfe\x90\xaf\x81\xd0\x03\xe0,\x08\x0b0@M\x1a`\
;SU\x94\x5ck\x13\xbbC}%\x1d@\xe0{\xae\
D\x99\x01\x02\xbb\xba\xa7\x7f\x89\xacx\xce\xdaN\xda\xb9\
\xfbD\xdf\xc8\xd0\xb2N\xc8\x1f\xf3\x15\x00\xcf\xfdg\xdb\
|M2\x0b\xd7\xf5\x9cG\xbc*\x80\xb2\x00\x9e\x87J\
r\x00$\x0b\x8c\xbd;#R\xf9MH&\xa9\xf3\x98\
b\xf0b|\x94L\xbc\x00h\x07@)\xfe\x1ep\x87\
3\x0bT\xe2\x18\xc2\x18\x18J\xbe\xb5X\xecF\xc9+\
E\x80\x04\x02(\xeb\x0a8\xd2P2\xb2J\x1aR1\
\xa2\xa4\x9d\xa4\x0e \x18\xa5(D\x14F\xae\xe6+\xb1\
F!\xac ,\xf9 k\xb3<)\x02\x88D\x0bq\
\x08*VP\xac\xc4\xae\x17\xb8\xf2,f\xc0\x92\xacd\
z\x00^F\x15\x1b\xb2A\xbeE\x05\xb8Q\x9aK\xda\
\xb4U\xf5\xb5)\xee@=1B.\x15R\xfbg\x94\
\xad\xf7\x7f\xdf\xd9\xde\x82\x83\xcdk\xb03n\x86\x9cR\
\xc0\xb4D\xb7\xea\xc5V\xde\x82<\xea!H^H\x0d\
\xdf\x1c\xbc\xb9ym\xa0W7*\xb4\xe6\xf3 c\
\xc0Dx\xa2\xf9q\xc8@Add$`\xdeN\x0d\
\xe0N\xff\xec\x05\xd9\x1c=?\xee\xf3\xe5\xfb\x22Z\xa0\
,\x83\xeb\x18*\xab\xc2 \x9b\xb9\x12\xa0n\xffD\x7f\
\xf1\x12V0\x1e\xfa\xdf\xf3\x7f\x00j\xf0\xda\xe8\xbc\xba\
\xd0\x0a\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x04<\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -2396,6 +2617,35 @@ f\xa2\xd1\xa5\x5c\x22\xb4\x91SZ\xd5u\xd7\x0a\xd2]\
|\x01\x85\x09\x800\x7fss\xd3{\xf6\x7fABG\
Y\x01\x05l*\xfc\x00!\x00\x12\xf1%U\xb6\x0e\x00\
\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x01\xad\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00 \x00\x00\x00 \x08\x03\x00\x00\x00D\xa4\x8a\xc6\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01B(\
\x9bx\x00\x00\x00\x07tIME\x07\xd9\x0c\x1c\x03\x1c\
\x0e%S,b\x00\x00\x00uPLTE\x00\x00\x00\
\x13\x13\x13\x0a\x0a\x0a\x0b\x0b\x0b\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc6\xc6\xc8\xc7\xc7\
\xc9\xc9\xc9\xcb\xcc\xcc\xce\xce\xce\xd0\xd0\xd0\xd2\xd4\xd4\xd6\
\xd7\xd7\xd8\xd7\xd7\xd9\xd8\xd8\xda\xd9\xd9\xdb\xda\xda\xdb\xda\
\xda\xdc\xdb\xdb\xdc\xdb\xdb\xdd\xdd\xdd\xdf\xde\xde\xdf\xe2\xe2\
\xe3\xe3\xe3\xe5\xe3\xe3\xe6\xe4\xe4\xe6\xe6\xe6\xe7\xe7\xe7\xe9\
\xe9\xe9\xea\xeb\xeb\xec\xed\xed\xee\xf0\xf0\xf1\xf3\xf3\xf4\xff\
\xff\xff\xd3\x9b\xcc\x0e\x00\x00\x00\x0atRNS\x00\x09\
\x15\x15\x1825678\xb5\xcc\xc0\x1e\x00\x00\x00\x01\
bKGD&Z\x08\x98\xb5\x00\x00\x00\x9bIDA\
Tx\xda\xd5\x93\xcb\x0e\x820\x10\x00\x8b\x0aEP|\
u\xc1G)\x94\x02\xff\xff\x89.]\x0e\x18\xccr1\
F\xe72\xd9d\xd26\x9bT\xfc\x02\xc1\x86%\x10a\
\xcf\x12\x0a\xd9)\x86N\x0a\xd9^\x18Z\x0c\xdc\x91\xc1\
a\xd0\x1c\x18\x1a\x0c\xec\x9e\xc1bP'\x9et\x97\xce\
\x95\xd4\x18\x94=q#\xddIWR\x89\x81\x1e\x03E\
\x82\x97IO\x02\x98\x8b\x82G\x01\x9e\xec\x8d\x8aj\xf1\
\x84\xcf\xbc!W\x03\x90\x8d\x02oR^-^\xf1?\
{\xe0\x83\xc8\x1a=`\xce\xc6\xebd\xa6\x93\x8b\xc4*\
\xde2\xc4\xeb/|\xcd'\xec\xbfO\xbf\x90M\x1a\x0a\
\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x02\x92\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -2563,6 +2813,10 @@ qt_resource_name = b"\
\x0f\xba\x1e'\
\x00d\
\x00r\x00a\x00w\x00-\x00p\x00a\x00t\x00h\x00.\x00p\x00n\x00g\
\x00\x0f\
\x020\x8b\xe7\
\x00l\
\x00i\x00s\x00t\x00-\x00r\x00e\x00m\x00o\x00v\x00e\x00.\x00p\x00n\x00g\
\x00\x0d\
\x0b\xe6\x1f\xa7\
\x00d\
@@ -2632,10 +2886,24 @@ qt_resource_name = b"\
\x00d\
\x00o\x00c\x00u\x00m\x00e\x00n\x00t\x00-\x00s\x00a\x00v\x00e\x00.\x00p\x00n\x00g\
\
\x00\x15\
\x02\xb4\x1f\x07\
\x00o\
\x00f\x00f\x00i\x00c\x00e\x00-\x00c\x00h\x00a\x00r\x00t\x00-\x00l\x00i\x00n\x00e\
\x00.\x00p\x00n\x00g\
\x00\x0c\
\x09\xc6\x19'\
\x00l\
\x00i\x00s\x00t\x00-\x00a\x00d\x00d\x00.\x00p\x00n\x00g\
\x00\x10\
\x03\xe6\xd3g\
\x00d\
\x00r\x00a\x00w\x00-\x00e\x00l\x00l\x00i\x00p\x00s\x00e\x00.\x00p\x00n\x00g\
\x00\x13\
\x07\xd6O\x07\
\x00v\
\x00i\x00e\x00w\x00-\x00f\x00o\x00r\x00m\x00-\x00t\x00a\x00b\x00l\x00e\x00.\x00p\
\x00n\x00g\
\x00\x12\
\x09\xb3>\xc7\
\x00d\
@@ -2653,63 +2921,71 @@ qt_resource_struct = b"\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1c\x00\x00\x00\x03\
\x00\x00\x00\x00\x00\x02\x00\x00\x00 \x00\x00\x00\x03\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x01\xfe\x00\x00\x00\x00\x00\x01\x00\x00N\xe8\
\x00\x00\x02\x22\x00\x00\x00\x00\x00\x01\x00\x00S\x15\
\x00\x00\x01\x9f{C\xf1'\
\x00\x00\x02>\x00\x00\x00\x00\x00\x01\x00\x00Z\x1a\
\x00\x00\x02b\x00\x00\x00\x00\x00\x01\x00\x00^G\
\x00\x00\x01\x9f\x7f\xa8\xa8)\
\x00\x00\x02\xec\x00\x00\x00\x00\x00\x01\x00\x00tV\
\x00\x00\x03\x10\x00\x00\x00\x00\x00\x01\x00\x00x\x83\
\x00\x00\x01\x9f{0\xc99\
\x00\x00\x01\xc4\x00\x00\x00\x00\x00\x01\x00\x00D2\
\x00\x00\x01d\x00\x00\x00\x00\x00\x01\x00\x004B\
\x00\x00\x01\x9f\x84\xbd\x03\xb8\
\x00\x00\x03\xc8\x00\x00\x00\x00\x00\x01\x00\x00\x91\xd7\
\x00\x00\x01\x9f\x84\x8f\x00\xb9\
\x00\x00\x01\xe8\x00\x00\x00\x00\x00\x01\x00\x00H_\
\x00\x00\x01\x9f\x7f&\x83\xcd\
\x00\x00\x01\xa4\x00\x00\x00\x00\x00\x01\x00\x00<J\
\x00\x00\x01\xc8\x00\x00\x00\x00\x00\x01\x00\x00@w\
\x00\x00\x01\x9f{C\xf1\x18\
\x00\x00\x03\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x8d\xaa\
\x00\x00\x04\x16\x00\x00\x00\x00\x00\x01\x00\x00\x9b\x1b\
\x00\x00\x01\x9f\x7fY\xceg\
\x00\x00\x02\xa2\x00\x00\x00\x00\x00\x01\x00\x00f\xc8\
\x00\x00\x02\xc6\x00\x00\x00\x00\x00\x01\x00\x00j\xf5\
\x00\x00\x01\x9f\x7fV\xd5\xc0\
\x00\x00\x03Z\x00\x00\x00\x00\x00\x01\x00\x00\x84\xc5\
\x00\x00\x03~\x00\x00\x00\x00\x00\x01\x00\x00\x88\xf2\
\x00\x00\x01\x9f\x7f&\x83r\
\x00\x00\x02\xce\x00\x00\x00\x00\x00\x01\x00\x00m[\
\x00\x00\x02\xf2\x00\x00\x00\x00\x00\x01\x00\x00q\x88\
\x00\x00\x01\x9f\x7f&\x83\xb1\
\x00\x00\x01\x1c\x00\x00\x00\x00\x00\x01\x00\x00,\x08\
\x00\x00\x01\x9f\x7fY\xce\x82\
\x00\x00\x01\xe0\x00\x00\x00\x00\x00\x01\x00\x00Kh\
\x00\x00\x02\x04\x00\x00\x00\x00\x00\x01\x00\x00O\x95\
\x00\x00\x01\x9f{C\xf1.\
\x00\x00\x01\x84\x00\x00\x00\x00\x00\x01\x00\x006\x9c\
\x00\x00\x04<\x00\x00\x00\x00\x00\x01\x00\x00\x9f[\
\x00\x00\x01\x9f\x84\x8f\xa6\x10\
\x00\x00\x01\xa8\x00\x00\x00\x00\x00\x01\x00\x00:\xc9\
\x00\x00\x01\x9f{{\xa5\xd5\
\x00\x00\x03\xca\x00\x00\x00\x00\x00\x01\x00\x00\x91\xea\
\x00\x00\x04h\x00\x00\x00\x00\x00\x01\x00\x00\xa1\x0c\
\x00\x00\x01\x9f\x7f&\x83\x10\
\x00\x00\x03\xf8\x00\x00\x00\x00\x00\x01\x00\x00\x95H\
\x00\x00\x01\x9f\x84\xbd\x03\xde\
\x00\x00\x00\xfe\x00\x00\x00\x00\x00\x01\x00\x00(e\
\x00\x00\x01\x9f\x7f&\x83~\
\x00\x00\x036\x00\x00\x00\x00\x00\x01\x00\x00\x80\xe2\
\x00\x00\x03Z\x00\x00\x00\x00\x00\x01\x00\x00\x85\x0f\
\x00\x00\x01\x9f\x7f&\x83T\
\x00\x00\x00~\x00\x00\x00\x00\x00\x01\x00\x00\x14M\
\x00\x00\x01\x9f\x7fY\xce^\
\x00\x00\x00\xa2\x00\x00\x00\x00\x00\x01\x00\x00\x19Y\
\x00\x00\x01\x9f{0\xc9B\
\x00\x00\x01d\x00\x00\x00\x00\x00\x01\x00\x004B\
\x00\x00\x01\x88\x00\x00\x00\x00\x00\x01\x00\x008o\
\x00\x00\x01\x9f\x7f&\x83)\
\x00\x00\x03\x14\x00\x00\x00\x00\x00\x01\x00\x00{`\
\x00\x00\x038\x00\x00\x00\x00\x00\x01\x00\x00\x7f\x8d\
\x00\x00\x01\x9f{C\xf1N\
\x00\x00\x02p\x00\x00\x00\x00\x00\x01\x00\x00b\x99\
\x00\x00\x02\x94\x00\x00\x00\x00\x00\x01\x00\x00f\xc6\
\x00\x00\x01\x9f\x7f&\x82\xf2\
\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x01\x00\x00!\xc5\
\x00\x00\x01\x9f{\x8d\xf34\
\x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01\x9f{0\xc9+\
\x00\x00\x02\x1e\x00\x00\x00\x00\x00\x01\x00\x00RH\
\x00\x00\x02B\x00\x00\x00\x00\x00\x01\x00\x00Vu\
\x00\x00\x01\x9f{C\xf1=\
\x00\x00\x006\x00\x00\x00\x00\x00\x01\x00\x00\x05\x86\
\x00\x00\x01\x9f\x7f&\x83\x99\
\x00\x00\x03\xf4\x00\x00\x00\x00\x00\x01\x00\x00\x94\x80\
\x00\x00\x04\x92\x00\x00\x00\x00\x00\x01\x00\x00\xa3\xa2\
\x00\x00\x01\x9f{{\xa5\xe3\
\x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x00\x0c\xc1\
\x00\x00\x01\x9f\x7f\xac\xf2\xc6\
\x00\x00\x01D\x00\x00\x00\x00\x00\x01\x00\x00/{\
\x00\x00\x01\x9f\x7fV\xd5\xe5\
\x00\x00\x03|\x00\x00\x00\x00\x00\x01\x00\x00\x88\xb7\
\x00\x00\x03\xa0\x00\x00\x00\x00\x00\x01\x00\x00\x8c\xe4\
\x00\x00\x01\x9f{0\xc9R\
"

View File

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

View File

@@ -6,6 +6,24 @@ 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
@@ -17,6 +35,15 @@ class Port:
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 {
@@ -26,11 +53,20 @@ class Port:
"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]) -> "Port":
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"])),
@@ -39,6 +75,15 @@ class Port:
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")),
)
@@ -139,6 +184,8 @@ class Connection:
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 {
@@ -147,6 +194,8 @@ class Connection:
"target": self.target.to_dict(),
"name": self.name,
"properties": self.properties,
"type": self.type,
"causality": self.causality,
}
@classmethod
@@ -157,6 +206,8 @@ class Connection:
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")),
)
@@ -166,9 +217,23 @@ class Parameter:
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, str]:
return {"id": self.id, "name": self.name, "type": self.type, "value": self.value}
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":
@@ -176,11 +241,17 @@ class Parameter:
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", "")),
)
@@ -299,8 +370,7 @@ class Component:
x: float = 0.0
y: float = 0.0
rotation: float = 0.0
inputs: list[Port] = field(default_factory=list)
outputs: list[Port] = field(default_factory=list)
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)
@@ -320,10 +390,7 @@ class Component:
"name": self.name,
"position": {"x": self.x, "y": self.y},
"rotation": self.rotation,
"interface": {
"inputs": [port.to_dict() for port in self.inputs],
"outputs": [port.to_dict() for port in self.outputs],
},
"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,
@@ -344,11 +411,19 @@ class Component:
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")
@@ -358,8 +433,7 @@ class Component:
x=float(position.get("x", 0.0)),
y=float(position.get("y", 0.0)),
rotation=float(data.get("rotation", 0.0)),
inputs=[Port.from_dict(item) for item in interface.get("inputs", [])],
outputs=[Port.from_dict(item) for item in interface.get("outputs", [])],
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", {})),
@@ -445,6 +519,13 @@ class GraphDocument:
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)
@@ -456,16 +537,46 @@ class GraphDocument:
raise ValueError(
f"Component names inside {owner.name!r} must be unique"
)
input_ids = {port.id for port in owner.inputs}
output_ids = {port.id for port in owner.outputs}
if len(input_ids) != len(owner.inputs) or len(output_ids) != len(owner.outputs):
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.inputs, *owner.outputs):
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:
@@ -474,14 +585,30 @@ class GraphDocument:
)
source_port = Port(junction.id, "Junction", type=junction.type)
elif connection.source.interface is not None:
if connection.source.interface not in input_ids:
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(p for p in owner.inputs if p.id == connection.source.interface)
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 "")
if source is None or connection.source.port not in {p.id for p in source.outputs}:
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.outputs if p.id == connection.source.port)
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:
@@ -495,16 +622,44 @@ class GraphDocument:
allows_multiple_connections=False,
)
elif connection.target.interface is not None:
if connection.target.interface not in output_ids:
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(p for p in owner.outputs if p.id == connection.target.interface)
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 "")
if target is None or connection.target.port not in {p.id for p in target.inputs}:
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(p for p in target.inputs if p.id == connection.target.port)
if not PortTypeRegistry.compatible(source_port.type, target_port.type):
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
@@ -567,6 +722,8 @@ def clone_component(source: Component) -> Component:
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())]
@@ -599,30 +756,7 @@ def clone_component(source: Component) -> Component:
name=current.name,
x=current.x,
y=current.y,
inputs=[
Port(
port.id,
port.name,
port.x,
port.y,
deepcopy(port.properties),
port.type,
port.allows_multiple_connections,
)
for port in current.inputs
],
outputs=[
Port(
port.id,
port.name,
port.x,
port.y,
deepcopy(port.properties),
port.type,
port.allows_multiple_connections,
)
for port in current.outputs
],
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),

View File

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

View File

@@ -14,6 +14,7 @@ class PortType:
class PortTypeRegistry:
_types = {
"signal": PortType("signal", "Signal", "A scalar signal connection"),
"power": PortType("power", "Power", "A two-variable power connection"),
}
@classmethod
@@ -28,5 +29,13 @@ class PortTypeRegistry:
raise ValueError(f"Unknown port type: {type_id}") from error
@classmethod
def compatible(cls, first: str, second: str) -> bool:
return cls.get(first).accepts(cls.get(second))
def compatible(
cls,
first: str,
second: str,
first_domain: str = "",
second_domain: str = "",
) -> bool:
if not cls.get(first).accepts(cls.get(second)):
return False
return first != "power" or first_domain == second_domain

View File

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

View File

@@ -1,3 +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__ = ["Simulation"]
__all__ = [
"OpenModelicaInterface",
"ModelBuildResult",
"Simulation",
"SimulationResults",
"SimulationExecutionResult",
"SimulationGraph",
"SimulationResultsSerializer",
"SimulationTrace",
"BerSimulationResultsSerializer",
"JsonSimulationResultsSerializer",
]

View File

@@ -14,8 +14,8 @@ _BEVALUE_PATTERN = re.compile(r"\$([A-Za-z_][A-Za-z0-9_]*)\$")
@dataclass(frozen=True)
class CompilationResult:
"""The intermediate data and generated source produced by compilation."""
class CompositionResult:
"""The intermediate data and generated source produced by composition."""
graph: dict[str, Any]
objects_by_id: dict[str, Any]
@@ -23,16 +23,16 @@ class CompilationResult:
model_name: str
def compile_graph(graph: dict[str, Any]) -> CompilationResult:
"""Clean, index, and emit a serialized component tree."""
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 CompilationResult(
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)
model_name=model_name_for(cleaned_graph),
)
@@ -59,9 +59,7 @@ def build_id_list(graph: dict[str, Any]) -> dict[str, Any]:
def visit(component: dict[str, Any]) -> None:
add(component, "component")
interface = component.get("interface", {})
for port in interface.get("inputs", []):
add(port, "port")
for port in interface.get("outputs", []):
for port in interface.get("ports", []):
add(port, "port")
implementation = component.get("implementation", {})
@@ -82,10 +80,11 @@ def emit_model(
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 compiler extensions and inspection.
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)
@@ -94,33 +93,58 @@ def emit_model(
implementation_kind = implementation.get("kind")
nested_graph = implementation.get("graph", {})
port_counts = connection_counts or _interface_connection_counts(graph)
macros = _port_count_macros(graph, port_counts)
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_connection_counts(nested_graph, block["id"]),
block_counts,
block_signs,
)
.rstrip()
.splitlines()
)
interface = graph.get("interface", {})
for port in interface.get("inputs", []):
lines.append(_port_declaration(port, "input", indent + 1, macros))
for port in interface.get("outputs", []):
lines.append(_port_declaration(port, "output", indent + 1, macros))
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} = {value};"
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":
@@ -134,8 +158,9 @@ def emit_model(
f"{body_indent}{junction_type} {_junction_name(junction['id'])};"
)
lines.append(f"{indentation}equation")
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", [])
@@ -148,11 +173,26 @@ def emit_model(
target = _endpoint_expression(
connection["target"], graph, blocks, junctions, endpoint_indices
)
# Connector types may require different equations in future.
lines.append(f"{body_indent}{target} = {source};")
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()
@@ -161,7 +201,6 @@ def emit_model(
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."""
@@ -192,14 +231,52 @@ def cleanup_graph(graph: dict[str, Any]) -> dict[str, Any]:
def _port_declaration(
port: dict[str, Any], direction: str, indent: int, macros: dict[str, str]
) -> str:
port_type = modelica_type(port.get("type", "signal"))
indentation = "\t" * indent
port_name = identifier(port["name"])
dimension = f"[${port_name}_N$]" if port.get("multipleConnections", False) else ""
declaration = f"{indentation}{direction} {port_type} {port_name}{dimension};"
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],
@@ -244,16 +321,21 @@ def _index_array_endpoint(
return f"{expression}[{endpoint_indices[key]}]"
def _block_connection_counts(
def _block_connection_data(
graph: dict[str, Any], block_id: str
) -> dict[str, int]:
) -> 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 in (connection.get("source", {}), connection.get("target", {})):
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
return counts
signs.setdefault(port_id, []).append(sign)
return counts, signs
def _interface_connection_counts(component: dict[str, Any]) -> dict[str, int]:
@@ -269,21 +351,58 @@ def _interface_connection_counts(component: dict[str, Any]) -> dict[str, int]:
return counts
def _port_count_macros(
component: dict[str, Any], connection_counts: dict[str, int]
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("inputs", []), *interface.get("outputs", [])):
for port in interface.get("ports", []):
if port.get("multipleConnections", False):
macros[f"{identifier(port['name'])}_N"] = str(
connection_counts.get(port["id"], 0)
)
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 compiler values."""
"""Replace BEdit ``$name$`` macros and reject unresolved composer values."""
def replace(match: re.Match[str]) -> str:
name = match.group(1)
@@ -296,8 +415,7 @@ def expand_bevalues(text: str, values: dict[str, str]) -> str:
def _find_port(component: dict[str, Any], port_id: str) -> dict[str, Any]:
interface = component.get("interface", {})
ports = [*interface.get("inputs", []), *interface.get("outputs", [])]
for port in ports:
for port in interface.get("ports", []):
if port.get("id") == port_id:
return port
raise ValueError(

View File

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

View File

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

View File

@@ -1,162 +0,0 @@
import json
import os
from collections.abc import Callable
from pathlib import Path
from threading import Lock, Thread
from bedit.core.application_log import get_logger
log = get_logger(__name__)
_worker_lock = Lock()
_worker: Thread | None = None
def run_validation_async(
openmodelica_path: str = "",
model: str = "",
model_name: str = "",
task: Callable[[], None] | None = None,
) -> None:
"""Start one validation task in a background thread and return immediately."""
global _worker
with _worker_lock:
if _worker is not None and _worker.is_alive():
raise RuntimeError("An OpenModelica task is already running")
_worker = Thread(
target=_run_safely,
args=(
task
or (lambda: _run_validation(openmodelica_path, model, model_name)),
),
name="bedit-simulation",
daemon=True,
)
_worker.start()
def run_simulation_async(
openmodelica_path: str = "",
model: str = "",
model_name: str = "",
task: Callable[[], None] | None = None,
) -> None:
"""Start one validation-and-simulation task and return immediately."""
global _worker
with _worker_lock:
if _worker is not None and _worker.is_alive():
raise RuntimeError("An OpenModelica task is already running")
_worker = Thread(
target=_run_safely,
args=(
task
or (lambda: _run_openmodelica(openmodelica_path, model, model_name)),
),
name="bedit-simulation",
daemon=True,
)
_worker.start()
def simulation_is_running() -> bool:
"""Return whether the background simulation worker is active."""
with _worker_lock:
return _worker is not None and _worker.is_alive()
def _run_safely(task: Callable[[], None]) -> None:
global _worker
try:
task()
except Exception:
log.exception("Simulation run failed")
finally:
with _worker_lock:
_worker = None
def _run_validation(
openmodelica_path: str = "", model: str = "", model_name: str = ""
) -> None:
"""Create the OpenModelica session and perform a validation"""
from OMPython import OMCSessionZMQ
omhome = _openmodelica_home(openmodelica_path)
omc = OMCSessionZMQ(omhome=omhome)
_validate_model(omc, model, model_name)
def _run_openmodelica(
openmodelica_path: str = "", model: str = "", model_name: str = ""
) -> None:
"""Validate and run the current simulation stub in one OMC session."""
from OMPython import OMCSessionZMQ
omhome = _openmodelica_home(openmodelica_path)
omc = OMCSessionZMQ(omhome=omhome)
_validate_model(omc, model, model_name)
log.info("OpenModelica Version: %s", omc.sendExpression("getVersion()"))
loaded = omc.sendExpression(f"loadString({json.dumps(model)})")
if loaded is not True:
details = _get_error_string(omc)
raise RuntimeError(f"OpenModelica could not load the model: {details}")
result = omc.sendExpression(f"checkModel({model_name})")
details = _get_error_string(omc)
if not isinstance(result, str) or "completed successfully" not in result:
message = details or result or "Unknown OpenModelica validation error"
raise RuntimeError(f"OpenModelica model validation failed: {message}")
def _validate_model(omc, model: str, model_name: str) -> None:
if not model:
raise ValueError("There is no compiled Modelica source to validate")
if not model_name:
raise ValueError("The compiled model has no Modelica name")
loaded = omc.sendExpression(f"loadString({json.dumps(model)})")
if loaded is not True:
details = _get_error_string(omc)
raise RuntimeError(f"OpenModelica could not load the model: {details}")
result = omc.sendExpression(f"checkModel({model_name})")
details = _get_error_string(omc)
if not isinstance(result, str) or "completed successfully" not in result:
message = details or result or "Unknown OpenModelica validation error"
raise RuntimeError(f"OpenModelica model validation failed: {message}")
log.info("%s", result)
if details:
log.warning("OpenModelica validation messages: %s", details)
def _get_error_string(omc) -> str:
"""Read OMC's deliberately unparsed error response as plain text."""
raw = omc.sendExpression("getErrorString()", parsed=False)
if not isinstance(raw, str):
return str(raw or "")
raw = raw.strip()
if not raw:
return ""
try:
decoded = json.loads(raw)
except (TypeError, json.JSONDecodeError):
return raw
return decoded if isinstance(decoded, str) else str(decoded)
def _openmodelica_home(openmodelica_path: str) -> str | None:
"""Convert an optional omc executable path to the home expected by OMPython."""
if not openmodelica_path.strip():
return None
path = Path(os.path.expandvars(openmodelica_path)).expanduser()
if path.name.lower() in {"omc", "omc.exe"}:
return str(path.parent.parent)
return str(path)

View File

@@ -1,50 +1,196 @@
from collections.abc import Callable
from typing import Any
from bedit.core.application_log import get_logger
from bedit.core.simulation.compiler import compile_graph
from bedit.core.simulation.runner import run_simulation_async, run_validation_async
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 simulation state and compiler facade."""
"""Application-owned composition state and OpenModelica interface."""
def __init__(self, *, openmodelica_path: str = "") -> None:
self.state: dict[str, Any] = {}
self.last_compilation_input: dict[str, Any] | None = None
self.last_compilation_output: str | None = None
self.last_composition_input: dict[str, Any] | None = None
self.last_composition_output: str | None = None
self.id_list: dict[str, Any] = {}
self.openmodelica_path = openmodelica_path
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
def compile(self, graph: dict[str, Any]) -> None:
"""Compile a serialized component tree and retain the result."""
@property
def openmodelica_path(self) -> str:
return self._openmodelica_path
self._prepare_compilation(graph)
run_validation_async(
self.openmodelica_path,
self.last_compilation_output,
@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 _prepare_compilation(self, graph: dict[str, Any]) -> None:
"""Generate and retain Modelica without starting a background operation."""
def compose_source(self, graph: dict[str, Any]) -> tuple[str, str]:
"""Compose Modelica source without asking OpenModelica to build it."""
result = compile_graph(graph)
self.last_compilation_input = result.graph
infer_causality(graph)
result = compose_graph(graph)
self.last_composition_input = result.graph
self.id_list = result.objects_by_id
self.last_compilation_output = result.modelica
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]) -> None:
"""Start the simulation without blocking the calling UI thread."""
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."""
# Always regenerate before running, then validate and simulate as one
# background operation so the two phases cannot compete for the worker.
self._prepare_compilation(graph)
run_simulation_async(
self.openmodelica_path,
self.last_compilation_output,
self.model_name,
self.simulation_progress = None
def report_progress(progress: SimulationProgress) -> None:
self.simulation_progress = progress
if progress_callback is not None:
progress_callback(progress)
def run_model(model_path: str) -> None:
try:
arguments = self.build_simulation_arguments()
except Exception as error:
if error_callback is not None:
error_callback(error)
else:
log.exception("Could not prepare simulation arguments")
return
self.openmodelica.run_model(
model_path,
arguments,
report_progress,
message_callback,
callback,
error_callback,
)
self.compose(graph, run_model, error_callback, message_callback)
def get_progress(self) -> SimulationProgress | None:
"""Return the most recently received simulation status."""
return self.simulation_progress
def shutdown(self, *, wait: bool = True) -> None:
"""Close OpenModelica and clean its generated working directory."""
self.openmodelica.shutdown(wait=wait)
self.model_path = None
def build_simulation_arguments(self) -> list[str]:
opts = self.last_composition_input["implementation"]["graph"].get(
"simulation", {}
)
start_time = float(opts.get("startTime", 0.0))
stop_time = float(opts.get("stopTime", 1.0))
interval_mode = opts.get("intervalMode", "numberOfIntervals")
interval_time = float(opts.get("intervalTime", 0.002))
if interval_mode == "numberOfIntervals":
intervals = int(opts.get("numberOfIntervals", 500))
if intervals <= 0:
raise ValueError("Number of simulation intervals must be positive")
interval_time = (stop_time - start_time) / intervals
if interval_time <= 0:
raise ValueError("Simulation interval must be positive")
arguments = [
"-outputFormat=csv",
f"-startTime={start_time}",
f"-stopTime={stop_time}",
f"-stepSize={interval_time}",
]
log.info("Running model with: %s", arguments)
return arguments

Binary file not shown.

Binary file not shown.

View File

@@ -1,4 +1,6 @@
import sys
from collections.abc import Sequence
from pathlib import Path
from PySide6.QtCore import QCoreApplication, Qt
from PySide6.QtGui import QColor, QPalette
@@ -38,14 +40,26 @@ def apply_light_theme(app: QApplication) -> None:
app.setPalette(palette)
def main() -> int:
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(sys.argv)
app = QApplication(list(sys.argv if argv is None else argv))
app.setApplicationDisplayName("BEdit")
apply_light_theme(app)
window = MainWindow()
document_path = _document_argument(app.arguments())
if document_path is not None:
window.open_document_path(document_path, check_unsaved=False)
window.show()
return app.exec()

View File

@@ -304,6 +304,20 @@ class RenameConnectionCommand(QUndoCommand):
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,
@@ -311,24 +325,21 @@ class DeleteSelectionCommand(QUndoCommand):
owner_id: str | None,
blocks: dict[str, Component],
connections: dict[str, Connection],
inputs: list[Port],
outputs: list[Port],
ports: list[Port],
) -> None:
super().__init__("Delete selection")
self.controller = controller
self.owner_id = owner_id
self.blocks = blocks
self.connections = connections
self.inputs = inputs
self.outputs = outputs
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.inputs},
{port.id for port in self.outputs},
{port.id for port in self.ports},
)
def undo(self) -> None:
@@ -336,8 +347,7 @@ class DeleteSelectionCommand(QUndoCommand):
self.owner_id,
self.blocks,
self.connections,
self.inputs,
self.outputs,
self.ports,
)
@@ -345,7 +355,7 @@ class PasteSelectionCommand(QUndoCommand):
def __init__(
self,
controller,
owner_id: str,
owner_id: str | None,
blocks: dict[str, Component],
connections: dict[str, Connection],
) -> None:
@@ -361,7 +371,6 @@ class PasteSelectionCommand(QUndoCommand):
self.blocks,
self.connections,
[],
[],
)
def undo(self) -> None:
@@ -370,7 +379,6 @@ class PasteSelectionCommand(QUndoCommand):
set(self.blocks),
set(self.connections),
set(),
set(),
)

View File

@@ -23,6 +23,7 @@ from bedit.gui.controllers.commands import (
MoveInterfacePortCommand,
PasteSelectionCommand,
RenameConnectionCommand,
RenameDocumentCommand,
RenameInterfacePortCommand,
ReplaceSourceCommand,
RotateComponentsCommand,
@@ -40,14 +41,17 @@ from bedit.core.model import (
Port,
clone_component,
)
from bedit.core.bond_graph import infer_causality
from bedit.core.simulation import Simulation
from bedit.core.port_types import PortTypeRegistry
from bedit.core.serializer import DocumentSerializer
from bedit.gui.preferences import application_settings
class DocumentController(QObject):
documentReset = Signal()
documentOpenedChanged = Signal(bool)
documentNameChanged = Signal(str)
activeGraphChanged = Signal()
componentAdded = Signal(str)
componentRemoved = Signal(str)
@@ -168,7 +172,13 @@ class DocumentController(QObject):
name=self._available_component_name(base_name, self.document.roots.values(), number),
implementation_kind=kind,
icon=Icon(text="Graph" if kind == "graph" else "Text"),
source={"equations": ""} if kind == "text" else {},
source={
"declarations": "",
"initialEquations": "",
"equations": "",
}
if kind == "text"
else {},
)
self.undo_stack.push(AddComponentCommand(self, None, component))
self.activate_component(component.id)
@@ -187,7 +197,13 @@ class DocumentController(QObject):
name=self._available_component_name(base_name, owner.graph.blocks.values(), number),
implementation_kind=kind,
icon=Icon(text="Graph" if kind == "graph" else "Text"),
source={"equations": ""} if kind == "text" else {},
source={
"declarations": "",
"initialEquations": "",
"equations": "",
}
if kind == "text"
else {},
)
self.undo_stack.push(AddComponentCommand(self, owner_id, component))
return component.id
@@ -214,10 +230,19 @@ class DocumentController(QObject):
{component_id: component},
connections,
[],
[],
)
)
def rename_document(self, name: str) -> None:
if self.document is None:
return
name = name.strip()
if not name:
raise ValueError("The document name cannot be empty")
old = str(self.document.metadata.get("name") or "Current Document")
if name != old:
self.undo_stack.push(RenameDocumentCommand(self, old, name))
def add_component_copy(self, source: Component, position: QPointF) -> str:
if self.active_component is None or self.active_component.implementation_kind != "graph":
raise ValueError("Open a graph component before placing components")
@@ -273,7 +298,12 @@ class DocumentController(QObject):
target_port = self._port_for_endpoint(target, "target")
if source_port is None or target_port is None:
raise ValueError("A connection endpoint no longer exists")
if not PortTypeRegistry.compatible(source_port.type, target_port.type):
if not PortTypeRegistry.compatible(
source_port.type,
target_port.type,
source_port.domain,
target_port.domain,
):
raise ValueError(f"Cannot connect {source_port.type!r} to {target_port.type!r}")
if not self.endpoint_accepts_connection(source, "source"):
raise ValueError(
@@ -290,6 +320,7 @@ class DocumentController(QObject):
properties={
"waypoints": [{"x": point.x(), "y": point.y()} for point in (waypoints or [])],
},
type=source_port.type,
)
self.undo_stack.push(AddConnectionCommand(self, self.active_component_id, connection))
return connection.id
@@ -307,6 +338,8 @@ class DocumentController(QObject):
if original is None:
raise ValueError("The connection no longer exists")
port_type = self.connection_port_type(original)
if port_type == "power":
raise ValueError("Power bond connections cannot contain junctions")
junction = Junction(str(uuid4()), position.x(), position.y(), port_type)
first_properties = deepcopy(original.properties)
first_properties["waypoints"] = [
@@ -321,6 +354,8 @@ class DocumentController(QObject):
Endpoint(junction=junction.id),
original.name,
first_properties,
original.type,
original.causality,
)
second = Connection(
str(uuid4()),
@@ -328,6 +363,8 @@ class DocumentController(QObject):
original.target,
"",
second_properties,
original.type,
original.causality,
)
self.undo_stack.push(
SplitConnectionCommand(
@@ -421,17 +458,69 @@ class DocumentController(QObject):
if old != new:
self.undo_stack.push(EditGraphParametersCommand(self, old, new))
def compile_active_graph(self) -> None:
def compose_active_graph(self) -> None:
component = self.active_component
if component is None or component.implementation_kind != "graph":
raise ValueError("Open a graph component before compiling")
self.simulation.compile(component.to_dict())
raise ValueError("Open a graph component before composing")
self._infer_active_graph_causality(component)
self.simulation.compose(component.to_dict())
def run_simulation(self) -> None:
def compose_active_graph_source(self) -> tuple[str, str]:
component = self.active_component
if component is None or component.implementation_kind != "graph":
raise ValueError("Open a graph component before exporting a model")
self._infer_active_graph_causality(component)
return self.simulation.compose_source(component.to_dict())
def run_simulation(
self,
progress_callback=None,
message_callback=None,
callback=None,
error_callback=None,
) -> None:
component = self.active_component
if component is None or component.implementation_kind != "graph":
raise ValueError("Open a graph component before running a simulation")
self.simulation.run_simulation(component.to_dict())
self._infer_active_graph_causality(component)
self.simulation.run_simulation(
component.to_dict(),
progress_callback,
message_callback,
callback,
error_callback,
)
def _infer_active_graph_causality(
self, component: Component, *, emit_reset: bool = True
) -> None:
"""Infer causality and copy the derived values into the live model."""
inferred = infer_causality(component.to_dict())
causalities: dict[str, str] = {}
def collect(serialized_component: dict) -> None:
graph = serialized_component.get("implementation", {}).get("graph", {})
for connection in graph.get("connections", []):
causalities[str(connection["id"])] = str(
connection.get("causality", "none")
)
for block in graph.get("blocks", []):
collect(block)
collect(inferred)
changed = False
for nested_component in self._component_subtree(component):
for connection in nested_component.graph.connections.values():
causality = causalities.get(connection.id, "none")
if connection.causality != causality:
connection.causality = causality
changed = True
if changed:
if self.document is not None:
self.document.validate()
if emit_reset:
self.documentReset.emit()
def set_route_waypoints(self, item_kind: str, item_id: str, waypoints: list[QPointF]) -> None:
item = (
@@ -542,12 +631,26 @@ class DocumentController(QObject):
allows_multiple_connections=role == "source",
)
if endpoint.interface is not None:
ports = owner.inputs if role == "source" else owner.outputs
orientations = (
{"input", "indifferent"}
if role == "source"
else {"output", "indifferent"}
)
ports = [
port for port in owner.ports if port.orientation in orientations
]
else:
component = owner.graph.blocks.get(endpoint.block or "")
if component is None:
return None
ports = component.outputs if role == "source" else component.inputs
orientations = (
{"output", "indifferent"}
if role == "source"
else {"input", "indifferent"}
)
ports = [
port for port in component.ports if port.orientation in orientations
]
return next(
(port for port in ports if port.id == (endpoint.interface or endpoint.port)), None
)
@@ -571,12 +674,13 @@ class DocumentController(QObject):
component = self.active_component
if component is None or component.implementation_kind != "graph":
raise ValueError("Open a graph component before adding an interface")
ports = component.inputs if direction == "input" else component.outputs
ports = [port for port in component.ports if port.orientation == direction]
port = Port(
id=f"{direction}-{uuid4().hex[:8]}",
name=f"{direction.title()} {len(ports) + 1}",
x=position.x(),
y=position.y(),
orientation=direction,
)
self.undo_stack.push(AddInterfacePortCommand(self, component.id, direction, port))
return port.id
@@ -591,7 +695,7 @@ class DocumentController(QObject):
owner = self.active_component
if owner is None:
return
port = next((p for p in (*owner.inputs, *owner.outputs) if p.id == port_id), None)
port = next((port for port in owner.ports if port.id == port_id), None)
if port is not None and port.name != name:
self.undo_stack.push(
RenameInterfacePortCommand(self, owner.id, port_id, port.name, name)
@@ -622,19 +726,26 @@ class DocumentController(QObject):
def replace_active_text_definition(
self,
inputs: list[Port],
outputs: list[Port],
ports: list[Port],
declarations: str,
initial_equations: str,
equations: str,
parameters: list[Parameter],
) -> None:
component = self.active_component
if component is None or component.implementation_kind != "text":
raise ValueError("Only text-defined components can be edited here")
input_ids = [port.id for port in inputs]
output_ids = [port.id for port in outputs]
if len(set(input_ids)) != len(input_ids) or len(set(output_ids)) != len(output_ids):
raise ValueError("Input and output IDs must be unique")
if any(not port.name.strip() for port in (*inputs, *outputs)):
port_ids = [port.id for port in ports]
input_ids = {
port.id for port in ports if port.orientation in {"input", "indifferent"}
}
output_ids = {
port.id for port in ports if port.orientation in {"output", "indifferent"}
}
source_ids = output_ids
if len(set(port_ids)) != len(port_ids):
raise ValueError("Port IDs must be unique")
if any(not port.name.strip() for port in ports):
raise ValueError("Every port must have a name")
parameter_ids = [parameter.id for parameter in parameters]
if len(set(parameter_ids)) != len(parameter_ids):
@@ -654,30 +765,29 @@ class DocumentController(QObject):
)
if (
connection.source.block == component.id
and connection.source.port not in output_ids
and connection.source.port not in source_ids
):
raise ValueError(
f"Output {connection.source.port!r} is still connected in the containing graph"
)
old = {
"inputs": [port.to_dict() for port in component.inputs],
"outputs": [port.to_dict() for port in component.outputs],
"ports": [port.to_dict() for port in component.ports],
"source": deepcopy(component.source),
"parameters": [parameter.to_dict() for parameter in component.parameters],
}
new = {
"inputs": [port.to_dict() for port in inputs],
"outputs": [port.to_dict() for port in outputs],
"ports": [port.to_dict() for port in ports],
"source": {
"equations": equations,
"declarations": declarations,
"initialEquations": initial_equations,
},
"parameters": [parameter.to_dict() for parameter in parameters],
}
if old != new:
candidate = deepcopy(self.document)
candidate_component = candidate.find_component(component.id)
candidate_component.inputs = deepcopy(inputs)
candidate_component.outputs = deepcopy(outputs)
candidate_component.ports = deepcopy(ports)
candidate_component.source = deepcopy(new["source"])
candidate_component.parameters = deepcopy(parameters)
candidate.validate()
@@ -688,8 +798,7 @@ class DocumentController(QObject):
component_id: str,
name: str,
icon: Icon,
inputs: list[Port],
outputs: list[Port],
ports: list[Port],
show_subtree: bool,
show_name: bool,
) -> None:
@@ -704,8 +813,7 @@ class DocumentController(QObject):
old = {
"name": component.name,
"icon": component.icon.to_dict(),
"inputs": [port.to_dict() for port in component.inputs],
"outputs": [port.to_dict() for port in component.outputs],
"ports": [port.to_dict() for port in component.ports],
"show_subtree": component.show_subtree_in_library,
"properties": deepcopy(component.properties),
}
@@ -717,8 +825,7 @@ class DocumentController(QObject):
new = {
"name": name,
"icon": icon.to_dict(),
"inputs": [port.to_dict() for port in inputs],
"outputs": [port.to_dict() for port in outputs],
"ports": [port.to_dict() for port in ports],
"show_subtree": show_subtree,
"properties": properties,
}
@@ -727,8 +834,7 @@ class DocumentController(QObject):
candidate_component = candidate.find_component(component_id)
candidate_component.name = name
candidate_component.icon = Icon.from_dict(icon.to_dict())
candidate_component.inputs = deepcopy(inputs)
candidate_component.outputs = deepcopy(outputs)
candidate_component.ports = deepcopy(ports)
candidate_component.properties = deepcopy(properties)
candidate.validate()
self.undo_stack.push(EditComponentAppearanceCommand(self, component_id, old, new))
@@ -798,16 +904,19 @@ class DocumentController(QObject):
)
)
def edit_component_ports(
self, component_id: str, inputs: list[Port], outputs: list[Port]
) -> None:
def edit_component_ports(self, component_id: str, ports: list[Port]) -> None:
if self.document is None:
return
component = self.document.find_component(component_id)
if component is None:
return
input_ids = {port.id for port in inputs}
output_ids = {port.id for port in outputs}
input_ids = {
port.id for port in ports if port.orientation in {"input", "indifferent"}
}
output_ids = {
port.id for port in ports if port.orientation in {"output", "indifferent"}
}
source_ids = output_ids
parent = self.document.find_parent(component_id)
if parent is not None:
for connection in parent.graph.connections.values():
@@ -818,7 +927,7 @@ class DocumentController(QObject):
raise ValueError("An input cannot be removed or reoriented while connected")
if (
connection.source.block == component_id
and connection.source.port not in output_ids
and connection.source.port not in source_ids
):
raise ValueError("An output cannot be removed or reoriented while connected")
for connection in component.graph.connections.values():
@@ -828,21 +937,18 @@ class DocumentController(QObject):
raise ValueError("An interface output cannot be removed while connected")
candidate = deepcopy(self.document)
candidate_component = candidate.find_component(component_id)
candidate_component.inputs = deepcopy(inputs)
candidate_component.outputs = deepcopy(outputs)
candidate_component.ports = deepcopy(ports)
candidate.validate()
old = {
"name": component.name,
"icon": component.icon.to_dict(),
"inputs": [port.to_dict() for port in component.inputs],
"outputs": [port.to_dict() for port in component.outputs],
"ports": [port.to_dict() for port in component.ports],
"show_subtree": component.show_subtree_in_library,
"properties": deepcopy(component.properties),
}
new = {
**old,
"inputs": [port.to_dict() for port in inputs],
"outputs": [port.to_dict() for port in outputs],
"ports": [port.to_dict() for port in ports],
}
if old != new:
self.undo_stack.push(EditComponentAppearanceCommand(self, component_id, old, new))
@@ -872,8 +978,7 @@ class DocumentController(QObject):
self,
block_ids: set[str],
connection_ids: set[str],
input_ids: set[str],
output_ids: set[str],
port_ids: set[str],
) -> None:
component = self.active_component
if component is None or component.implementation_kind != "graph":
@@ -884,8 +989,8 @@ class DocumentController(QObject):
if (
connection.source.block in block_ids
or connection.target.block in block_ids
or connection.source.interface in input_ids
or connection.target.interface in output_ids
or connection.source.interface in port_ids
or connection.target.interface in port_ids
):
all_connection_ids.add(connection.id)
blocks = {
@@ -896,9 +1001,8 @@ class DocumentController(QObject):
for connection_id in all_connection_ids
if connection_id in graph.connections
}
inputs = [port for port in component.inputs if port.id in input_ids]
outputs = [port for port in component.outputs if port.id in output_ids]
if not (blocks or connections or inputs or outputs):
ports = [port for port in component.ports if port.id in port_ids]
if not (blocks or connections or ports):
return
self.undo_stack.push(
DeleteSelectionCommand(
@@ -906,8 +1010,7 @@ class DocumentController(QObject):
component.id,
blocks,
connections,
inputs,
outputs,
ports,
)
)
@@ -945,12 +1048,78 @@ class DocumentController(QObject):
target=Endpoint(block=id_map[source.target.block], port=source.target.port),
name=source.name,
properties=properties,
type=source.type,
causality=source.causality,
)
connections[connection.id] = connection
if blocks:
self.undo_stack.push(PasteSelectionCommand(self, owner.id, blocks, connections))
return list(blocks)
def paste_components_to(
self,
owner_id: str | None,
source_components: list[Component],
source_connections: list[Connection] | None = None,
) -> list[str]:
"""Clone components into the document root or a graph at origin."""
if self.document is None:
raise ValueError("Open or create a document before pasting components")
if owner_id is None:
siblings = self.document.roots.values()
else:
owner = self.document.find_component(owner_id)
if owner is None or owner.implementation_kind != "graph":
raise ValueError("Components can only be pasted into a graph")
siblings = owner.graph.blocks.values()
pairs = [(source, clone_component(source)) for source in source_components]
if not pairs:
return []
id_map = {source.id: clone.id for source, clone in pairs}
used = list(siblings)
minimum_x = min(source.x for source, _clone in pairs)
minimum_y = min(source.y for source, _clone in pairs)
blocks: dict[str, Component] = {}
for source, clone in pairs:
clone.name = self._available_component_name(source.name, used, 0)
if owner_id is not None:
clone.x = source.x - minimum_x
clone.y = source.y - minimum_y
blocks[clone.id] = clone
used.append(clone)
connections: dict[str, Connection] = {}
if owner_id is not None:
for source in source_connections or []:
if source.source.block not in id_map or source.target.block not in id_map:
continue
properties = deepcopy(source.properties)
for point in properties.get("waypoints", []):
if isinstance(point, dict):
point["x"] = float(point.get("x", 0)) - minimum_x
point["y"] = float(point.get("y", 0)) - minimum_y
connection = Connection(
id=str(uuid4()),
source=Endpoint(
block=id_map[source.source.block], port=source.source.port
),
target=Endpoint(
block=id_map[source.target.block], port=source.target.port
),
name=source.name,
properties=properties,
type=source.type,
causality=source.causality,
)
connections[connection.id] = connection
self.undo_stack.push(
PasteSelectionCommand(self, owner_id, blocks, connections)
)
return list(blocks)
def _graph_for(self, owner_id: str):
if self.document is None:
raise ValueError("There is no open document")
@@ -1021,6 +1190,10 @@ class DocumentController(QObject):
def _insert_connection(self, owner_id: str, connection: Connection) -> None:
self._graph_for(owner_id).connections[connection.id] = connection
if self.document is not None and self._infer_causality_on_connection_change():
owner = self.document.find_component(owner_id)
if owner is not None:
self._infer_active_graph_causality(owner, emit_reset=False)
if owner_id == self.active_component_id:
self.connectionAdded.emit(connection.id)
self.documentReset.emit()
@@ -1057,10 +1230,20 @@ class DocumentController(QObject):
def _remove_connection(self, owner_id: str, connection_id: str) -> None:
self._graph_for(owner_id).connections.pop(connection_id, None)
if self.document is not None and self._infer_causality_on_connection_change():
owner = self.document.find_component(owner_id)
if owner is not None:
self._infer_active_graph_causality(owner, emit_reset=False)
if owner_id == self.active_component_id:
self.connectionRemoved.emit(connection_id)
self.documentReset.emit()
@staticmethod
def _infer_causality_on_connection_change() -> bool:
return application_settings().value(
"bondGraph/inferCausalityOnConnectionChange", True, type=bool
)
def _insert_annotation(self, owner_id: str, annotation: Annotation) -> None:
self._graph_for(owner_id).annotations[annotation.id] = annotation
if owner_id == self.active_component_id:
@@ -1158,9 +1341,9 @@ class DocumentController(QObject):
owner = self.document.find_component(owner_id)
if owner is None:
return
ports = owner.inputs if direction == "input" else owner.outputs
if all(existing.id != port.id for existing in ports):
ports.append(port)
port.orientation = direction
if all(existing.id != port.id for existing in owner.ports):
owner.ports.append(port)
self.interfaceChanged.emit()
self.documentReset.emit()
@@ -1170,8 +1353,7 @@ class DocumentController(QObject):
owner = self.document.find_component(owner_id)
if owner is None:
return
ports = owner.inputs if direction == "input" else owner.outputs
ports[:] = [port for port in ports if port.id != port_id]
owner.ports[:] = [port for port in owner.ports if port.id != port_id]
self.interfaceChanged.emit()
self.documentReset.emit()
@@ -1181,7 +1363,7 @@ class DocumentController(QObject):
owner = self.document.find_component(owner_id)
if owner is None:
return
port = next((p for p in (*owner.inputs, *owner.outputs) if p.id == port_id), None)
port = next((port for port in owner.ports if port.id == port_id), None)
if port is not None:
port.x, port.y = position.x(), position.y()
self.interfaceChanged.emit()
@@ -1191,7 +1373,7 @@ class DocumentController(QObject):
owner = self.document.find_component(owner_id) if self.document else None
if owner is None:
return
port = next((p for p in (*owner.inputs, *owner.outputs) if p.id == port_id), None)
port = next((port for port in owner.ports if port.id == port_id), None)
if port is not None:
port.name = name
self.interfaceChanged.emit()
@@ -1203,6 +1385,12 @@ class DocumentController(QObject):
connection.name = name
self.documentReset.emit()
def _rename_document(self, name: str) -> None:
if self.document is None:
return
self.document.metadata["name"] = name
self.documentNameChanged.emit(name)
def _replace_source(self, component_id: str, source: dict) -> None:
if self.document is None:
return
@@ -1222,8 +1410,7 @@ class DocumentController(QObject):
return
component.name = values["name"]
component.icon = Icon.from_dict(values["icon"])
component.inputs = [Port.from_dict(port) for port in values["inputs"]]
component.outputs = [Port.from_dict(port) for port in values["outputs"]]
component.ports = [Port.from_dict(port) for port in values["ports"]]
component.show_subtree_in_library = values["show_subtree"]
component.properties = deepcopy(values["properties"])
self.documentReset.emit()
@@ -1254,8 +1441,7 @@ class DocumentController(QObject):
owner_id: str | None,
block_ids: set[str],
connection_ids: set[str],
input_ids: set[str],
output_ids: set[str],
port_ids: set[str],
) -> None:
if self.document is None:
return
@@ -1278,8 +1464,7 @@ class DocumentController(QObject):
owner.graph.blocks.pop(block_id, None)
for connection_id in connection_ids:
owner.graph.connections.pop(connection_id, None)
owner.inputs[:] = [port for port in owner.inputs if port.id not in input_ids]
owner.outputs[:] = [port for port in owner.outputs if port.id not in output_ids]
owner.ports[:] = [port for port in owner.ports if port.id not in port_ids]
if active_was_deleted:
self.active_component_id = owner_id or next(iter(self.document.roots), None)
self.activeGraphChanged.emit()
@@ -1290,8 +1475,7 @@ class DocumentController(QObject):
owner_id: str | None,
blocks: dict[str, Component],
connections: dict[str, Connection],
inputs: list[Port],
outputs: list[Port],
ports: list[Port],
) -> None:
if self.document is None:
return
@@ -1303,10 +1487,8 @@ class DocumentController(QObject):
return
owner.graph.blocks.update(blocks)
owner.graph.connections.update(connections)
existing_inputs = {port.id for port in owner.inputs}
existing_outputs = {port.id for port in owner.outputs}
owner.inputs.extend(port for port in inputs if port.id not in existing_inputs)
owner.outputs.extend(port for port in outputs if port.id not in existing_outputs)
existing_ports = {port.id for port in owner.ports}
owner.ports.extend(port for port in ports if port.id not in existing_ports)
self.documentReset.emit()
@staticmethod
@@ -1321,8 +1503,7 @@ class DocumentController(QObject):
component = self.document.find_component(component_id)
if component is None:
return
component.inputs = [Port.from_dict(item) for item in values["inputs"]]
component.outputs = [Port.from_dict(item) for item in values["outputs"]]
component.ports = [Port.from_dict(item) for item in values["ports"]]
component.source = deepcopy(values["source"])
component.parameters = [
Parameter.from_dict(item) for item in values.get("parameters", [])

View File

@@ -12,8 +12,7 @@ class ComponentOptionsDialog(QDialog):
self.ui.setupUi(self)
self.component = component
self.edited_icon = component.icon
self.edited_inputs = component.inputs
self.edited_outputs = component.outputs
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)
@@ -22,13 +21,11 @@ class ComponentOptionsDialog(QDialog):
def edit_icon(self) -> None:
working = Component.from_dict(self.component.to_dict())
working.icon = self.edited_icon
working.inputs = self.edited_inputs
working.outputs = self.edited_outputs
working.ports = self.edited_ports
dialog = IconEditorDialog(working, self)
if dialog.exec() == dialog.DialogCode.Accepted:
self.edited_icon = dialog.icon
self.edited_inputs = dialog.inputs
self.edited_outputs = dialog.outputs
self.edited_ports = dialog.ports
def accept(self) -> None:
if not self.ui.nameEdit.text().strip():

View File

@@ -1,10 +1,11 @@
from copy import deepcopy
from uuid import uuid4
from PySide6.QtCore import Qt
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
@@ -14,6 +15,8 @@ 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()
@@ -22,19 +25,31 @@ class ParameterOptionsDialog(QDialog):
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.typeEdit.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.typeEdit.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:
@@ -51,12 +66,20 @@ class ParameterOptionsDialog(QDialog):
if 0 <= row < len(self.parameters):
parameter = self.parameters[row]
self.ui.nameEdit.setText(parameter.name)
self.ui.typeEdit.setText(parameter.type)
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.typeEdit.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()
@@ -64,8 +87,13 @@ class ParameterOptionsDialog(QDialog):
enabled = self.ui.parameterList.currentRow() >= 0
self.ui.removeParameterButton.setEnabled(enabled and not self.read_only)
self.ui.nameEdit.setEnabled(enabled)
self.ui.typeEdit.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()
@@ -73,11 +101,23 @@ class ParameterOptionsDialog(QDialog):
return
parameter = self.parameters[row]
parameter.name = self.ui.nameEdit.text()
parameter.type = self.ui.typeEdit.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(
@@ -89,12 +129,14 @@ class ParameterOptionsDialog(QDialog):
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()
@@ -106,3 +148,12 @@ class ParameterOptionsDialog(QDialog):
QMessageBox.warning(self, "Invalid parameter", "Parameter names must be unique.")
return
super().accept()
class ParameterEditor(ParameterOptionsDialog):
"""The parameter options editor embedded without dialog buttons."""
def __init__(self, parent=None) -> None:
super().__init__(Component(id="embedded-parameter-editor", name="Parameters"), parent)
self.setWindowFlags(Qt.WindowType.Widget)
self.ui.buttonBox.hide()

View File

@@ -1,10 +1,12 @@
from copy import deepcopy
from uuid import uuid4
from PySide6.QtCore import Qt
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
@@ -15,28 +17,45 @@ 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.ports: list[tuple[Port, str]] = [
*((deepcopy(port), "input") for port in component.inputs),
*((deepcopy(port), "output") for port in component.outputs),
]
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._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._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)
@@ -45,22 +64,15 @@ class PortOptionsDialog(QDialog):
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)
@property
def inputs(self) -> list[Port]:
return [port for port, orientation in self.ports if orientation == "input"]
@property
def outputs(self) -> list[Port]:
return [port for port, orientation in self.ports if orientation == "output"]
def _rebuild_list(self, row: int = -1) -> None:
self.ui.portList.clear()
for port, orientation in self.ports:
item = QListWidgetItem(f"{port.name} [{orientation}, {port.type}]")
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))
@@ -70,38 +82,125 @@ class PortOptionsDialog(QDialog):
self._loading = True
enabled = 0 <= row < len(self.ports)
if enabled:
port, orientation = self.ports[row]
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(orientation))
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)
self.ui.removePortButton.setEnabled(enabled and not self.read_only)
self.ui.nameEdit.setEnabled(enabled)
self.ui.typeCombo.setEnabled(enabled)
self.ui.orientationCombo.setEnabled(enabled)
self.ui.multipleConnectionsCheckBox.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, _orientation = self.ports[row]
port = self.ports[row]
port.name = self.ui.nameEdit.text()
port.type = self.ui.typeCombo.currentData()
port.allows_multiple_connections = self.ui.multipleConnectionsCheckBox.isChecked()
self.ports[row] = (port, self.ui.orientationCombo.currentData())
self.ui.portList.item(row).setText(
f"{port.name} [{self.ports[row][1]}, {port.type}]"
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(
@@ -110,20 +209,31 @@ class PortOptionsDialog(QDialog):
properties={"iconPosition": {"x": 0.0, "y": 0.0}},
type="signal",
)
self.ports.append((port, "input"))
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, _orientation in self.ports):
if any(not port.name.strip() for port in self.ports):
QMessageBox.warning(self, "Invalid port", "Every port must have a name.")
return
super().accept()
class PortEditor(PortOptionsDialog):
"""The port options editor embedded without dialog confirmation buttons."""
def __init__(self, parent=None) -> None:
super().__init__(Component(id="embedded-port-editor", name="Ports"), parent)
self.setWindowFlags(Qt.WindowType.Widget)
self.ui.buttonBox.hide()

View File

@@ -47,6 +47,9 @@ class SettingsDialog(QDialog):
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()
@@ -132,6 +135,15 @@ class SettingsDialog(QDialog):
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()
@@ -197,6 +209,10 @@ class SettingsDialog(QDialog):
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(),

View File

@@ -1,20 +1,12 @@
from copy import deepcopy
from uuid import uuid4
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import QCheckBox, QComboBox, QHeaderView, QTableWidgetItem, QWidget
from PySide6.QtCore import Signal
from PySide6.QtWidgets import QWidget
from bedit.core.model import Parameter, Port
from bedit.core.port_types import PortTypeRegistry
from bedit.gui.generated.ui_text_definition_editor import Ui_TextDefinitionEditor
ID_ROLE = Qt.ItemDataRole.UserRole
PROPERTIES_ROLE = Qt.ItemDataRole.UserRole + 1
class TextDefinitionEditor(QWidget):
"""Editor for a text component's equations, ports, and parameters."""
"""Editor for a text component's Modelica source, ports, and parameters."""
modifiedChanged = Signal(bool)
definitionEdited = Signal()
@@ -25,24 +17,14 @@ class TextDefinitionEditor(QWidget):
self.ui.setupUi(self)
self._modified = False
self._loading = False
self.ui.portsTable.horizontalHeader().setSectionResizeMode(
QHeaderView.ResizeMode.Stretch
)
self.ui.parametersTable.horizontalHeader().setSectionResizeMode(
QHeaderView.ResizeMode.Stretch
)
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.portsTable.cellChanged.connect(self._symbols_modified)
self.ui.parametersTable.cellChanged.connect(self._symbols_modified)
self.ui.addPortButton.clicked.connect(self.add_port)
self.ui.removePortButton.clicked.connect(self.remove_port)
self.ui.addParameterButton.clicked.connect(self.add_parameter)
self.ui.removeParameterButton.clicked.connect(self.remove_parameter)
self.ui.portsTable.itemSelectionChanged.connect(self._update_buttons)
self.ui.parametersTable.itemSelectionChanged.connect(self._update_buttons)
self._update_buttons()
self.ui.portEditor.edited.connect(self._symbols_modified)
self.ui.parameterEditor.edited.connect(self._symbols_modified)
@property
def is_modified(self) -> bool:
@@ -53,63 +35,38 @@ class TextDefinitionEditor(QWidget):
return self.ui.equationsEdit.toPlainText()
@property
def ports(self) -> tuple[list[Port], list[Port]]:
inputs: list[Port] = []
outputs: list[Port] = []
for row in range(self.ui.portsTable.rowCount()):
name_item = self.ui.portsTable.item(row, 0)
type_combo = self.ui.portsTable.cellWidget(row, 1)
orientation_combo = self.ui.portsTable.cellWidget(row, 2)
multiple_check = self.ui.portsTable.cellWidget(row, 3)
port = Port(
id=name_item.data(ID_ROLE),
name=name_item.text().strip(),
type=type_combo.currentData(),
properties=deepcopy(name_item.data(PROPERTIES_ROLE) or {}),
allows_multiple_connections=multiple_check.isChecked(),
)
target = inputs if orientation_combo.currentData() == "input" else outputs
target.append(port)
return inputs, outputs
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]:
table = self.ui.parametersTable
return [
Parameter(
id=table.item(row, 0).data(ID_ROLE),
name=table.item(row, 0).text().strip(),
type=table.item(row, 1).text().strip(),
value=table.item(row, 2).text(),
)
for row in range(table.rowCount())
]
return self.ui.parameterEditor.parameters
def set_definition(
self,
declarations: str,
initial_equations: str,
equations: str,
inputs: list[Port],
outputs: list[Port],
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.portsTable.setRowCount(0)
for port in inputs:
self._append_port(port, "input")
for port in outputs:
self._append_port(port, "output")
self.ui.parametersTable.setRowCount(0)
for parameter in parameters:
self._append_parameter(parameter)
self.ui.equationsEdit.set_symbols(
[port.name for port in inputs],
[port.name for port in outputs],
[parameter.name for parameter in parameters],
)
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)
self._update_buttons()
def set_modified(self, modified: bool) -> None:
if self._modified != modified:
@@ -127,91 +84,27 @@ class TextDefinitionEditor(QWidget):
self._mark_modified()
def _refresh_editor_symbols(self) -> None:
inputs, outputs = self.ports
self.ui.equationsEdit.set_symbols(
[port.name for port in inputs],
[port.name for port in outputs],
[parameter.name for parameter in self.parameters],
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 _new_combo(self, values: list[tuple[str, str]], current: str) -> QComboBox:
combo = QComboBox(self)
for label, value in values:
combo.addItem(label, value)
combo.setCurrentIndex(max(0, combo.findData(current)))
combo.currentIndexChanged.connect(self._symbols_modified)
return combo
def _append_port(self, port: Port, orientation: str) -> None:
table = self.ui.portsTable
row = table.rowCount()
table.insertRow(row)
name = QTableWidgetItem(port.name)
name.setData(ID_ROLE, port.id)
name.setData(PROPERTIES_ROLE, deepcopy(port.properties))
table.setItem(row, 0, name)
types = [(item.display_name, item.id) for item in PortTypeRegistry.all()]
table.setCellWidget(row, 1, self._new_combo(types, port.type))
table.setCellWidget(
row,
2,
self._new_combo([("Input", "input"), ("Output", "output")], orientation),
)
multiple = QCheckBox("Any", self)
multiple.setChecked(port.allows_multiple_connections)
multiple.toggled.connect(self._symbols_modified)
table.setCellWidget(row, 3, multiple)
def _append_parameter(self, parameter: Parameter) -> None:
table = self.ui.parametersTable
row = table.rowCount()
table.insertRow(row)
name = QTableWidgetItem(parameter.name)
name.setData(ID_ROLE, parameter.id)
table.setItem(row, 0, name)
table.setItem(row, 1, QTableWidgetItem(parameter.type))
table.setItem(row, 2, QTableWidgetItem(parameter.value))
def add_port(self) -> None:
port = Port(
id=f"port-{uuid4().hex[:8]}",
name=f"Port {self.ui.portsTable.rowCount() + 1}",
type="signal",
properties={"iconPosition": {"x": 0.0, "y": 0.0}},
)
self._loading = True
self._append_port(port, "input")
self._loading = False
self.ui.portsTable.selectRow(self.ui.portsTable.rowCount() - 1)
self._symbols_modified()
def remove_port(self) -> None:
row = self.ui.portsTable.currentRow()
if row >= 0:
self.ui.portsTable.removeRow(row)
self._symbols_modified()
self._update_buttons()
def add_parameter(self) -> None:
parameter = Parameter(
id=f"parameter-{uuid4().hex[:8]}",
name=f"Parameter {self.ui.parametersTable.rowCount() + 1}",
)
self._loading = True
self._append_parameter(parameter)
self._loading = False
self.ui.parametersTable.selectRow(self.ui.parametersTable.rowCount() - 1)
self._symbols_modified()
def remove_parameter(self) -> None:
row = self.ui.parametersTable.currentRow()
if row >= 0:
self.ui.parametersTable.removeRow(row)
self._symbols_modified()
self._update_buttons()
def _update_buttons(self) -> None:
self.ui.removePortButton.setEnabled(self.ui.portsTable.currentRow() >= 0)
self.ui.removeParameterButton.setEnabled(
self.ui.parametersTable.currentRow() >= 0
)
def _apply_editor_symbols(
self, inputs: list[str], outputs: list[str], parameters: list[str]
) -> None:
symbols = (inputs, outputs, parameters)
self.ui.declarationsEdit.set_symbols(*symbols)
self.ui.initialEquationsEdit.set_symbols(*symbols)
self.ui.equationsEdit.set_symbols(*symbols)

View File

@@ -862,6 +862,75 @@ N\xa7\xd3>==\xfd\xa1\xc3\xe1\x98\x92$\xe9I\x00\
\xbf\xc9\xd3\x01\x9f\xa3\x0f\x17Z/\xe3\x7f\xe1/\x17\x85\
\xd6\x06q(\x0e\x10\x00\x00\x00\x00IEND\xaeB\
`\x82\
\x00\x00\x04)\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
\x00\x00\x00\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\
\xa7\x93\x00\x00\x00\x09pHYs\x00\x007\x5c\x00\x00\
7\x5c\x01\xcb\xc7\xa4\xb9\x00\x00\x03\x9bIDATx\
\xda\xedT=o\x1bG\x10}\xbb\xb7w$\xf5A\x9e\
lJEB\x18\xb1\x0a\x1bv\x13\xdb\x85\xaa\x18P$\
\xa4\x89\x81\x94\xd6_\x08`\xb8r\x93 \x85\x02\x01\xa9\
\x9c\x1f\xe0>\x85\x81\x94F\x00\xa7\x11\xd88\x85\x1bK\
6@)\x94-\xc8\x96\x10\x18\x81\x22\xf3C\x22i\xf2\
\xf6v\xf3B.s\xd1\xb1H\x80\xb83\x07x\x98\xbd\
\xdd\x99y3\xb3\xb7\x83\xb1\x8ce,\xef\xbd\x88\x1f\x97\
\x97\xe1\x05\x01\x820D~~\x1e\xdb\xf7\xee\xa1st\
\x94\x188H\x07/\x05\xe5\xe0\x0f\xb5C\xf0\xb7N\xd6\
\xb9\xb3g\x11\xde\xba\x85\xf8\xd5+\xc4\xb5\x1a`\x0c\xc4\
\x83B\x01\x82\xe4\xe1\xcd\x9b\xf2\x8f'Ol.\x9f\xb7\
\x96\x07\xd1\xc9\x09\xbeX_\xc7\xbb\x90\xd7\xd7\xafCN\
M\x01RB7\x9bbraA4\xee\xdf7q\xa3\
\x01\xf1\xc3\xf9\xf3\xf0\xc3P\xac\x90\xfc\xd1\xda\x9a|v\
\xf7\xee4\xc9\xbdw\xda\x01\x87\xec\xf4t<\xb7\xba\xda\
\xcc\xdc\xb9c\xebW\xae\x88\xb8^\xb7\xa2|\xfb\xb6\xf8\
\xe0\xeaU\xdbl4>?\xda\xd8\xf8\xf6\xa0\x5c\xfe\xa8\
yp \xff=\x01G\x9eN\x22!\x1fM\xa0T2\
S\x8b\x8b{\xb9\x85\x85U\xe4r?G\x9b\x9bB\xd5\
\xaaU[\x7f\xfe|)\xbcp\xe1Avv\xd6\x93a\
h\xdb\x07\x07B\xfc\xe7\x04\x122\xe3`\x07p~\x09\
\x04c\xa3X\x9c\xd5;;?\x89\xdd\xdd\xcf\xa41e\
e\x94B\xb3Z\xfd\xca\xfa\xbeWZZ\x8a>^Y\
Q\xf3\xec\x88\xa7\x94#'\xac\x85L\x05\x1c\x09\x9e\xe8\
S~\x88cxR\x22\xc8\xe7\xa1\xe7\xe6`\x84\x88\xf4\
\xe3\xc7\xbe\xa8V\xbf6\x17/\x96\xd5\xeb\x87\x0f\x0bZ\
\xa9K\xad(\xc2\x87\xd7\xaey\xc1\x8b\x17b\xe6\xf8\x18\
\xca\xf3\xe0Y\x9bT<B\x98\x00B\x0c\xaa\xa6\xbd!\
bc\xfa\xdad2\x00\x89\xdbL`\xfb\xe9S\xfc\xba\
\xb5\x85Oo\xdc\xf0J\x95\x0a\xec\xfe\xfe\xa5\xdc\xcb\x97\
\x05\x15[\xabzQ\xe4\xd7\xf6\xf7qD\x833$\xb7\
LF1\xa8JZ\x9dJ\xc2\x81$\x18VJX\x12\
\x81\xa4rr\x12&\x9bE\xa7\xd3\xc1\xef\x9b\x9b\xf8m\
{\x1b\xf5\xb7o\xd1e\xcc\xe3j\x15zo\x0f\xf4\xf1\
\xd9\x0d\xa5\x0c\x00\x87~\xe6\xacf\x00\x0a\x83\x9e\xd6\xff\
l=\xc9$\xbb$\x95\x02\x82\x00\xa0\x8e\xe9\xd7k\xb7\
\xd1\x22A\x83o\xbd~x\x88\xb6\xf3\xcb\xf2\xacEm\
\xc8A\xe2~\xd2\xc6\x15\xa7\xb9\x88b\x00o8\x1c\xba\
t\x0a\xba]L\xb1\x82\xac\xef#\xabT\x9f\xcc#\x04\
\x1d\xe5\x10\x0c \xb4\x86\xe9\xf5`834\xdft\xaf\
^G\xf7\xafJ\x1di\x86v\x9a~]\xdaj\x12c\
b\x02\xbe\xbb\x22IN^\x99V\x87B4&\xac\xad\
\x00(\xedll\xe8\xe2\xb9s\xbeGbEC\x9fW\
\xe1\xc71|!\x86W\xd2\xdfW\x0c\xe0q_\x11\xc3\
\xffD\xce\xcc@\x16\x8b\x10\x1e\xbf\x5c\x85Y\x22\x00P\
\xa0\xd6\xd4>\x8b9\xb3\xbb\xab\xa3A\xcf*5r\x8b\
5\xf4\xe5\x93\x0e\xb0~\xc2\x03\x1eZ#%\xfd\x93\xb6\
\xb3\xda\xd3O\x8f\x04\xbe\xd3\xa7\xde<\xed2\xc0\x08\xb2\
\x0e\x19\x06\xa5\xbf\xe0\xbaG\xbfe\x00\x8f\xd4174\
\x17\x0c\xb0\x04\xe0;\x03\x5cf\x8b\xa4M?\xad\xd4\xcf\
G\x0dV\x9f\x9e\x0f\xce~ v\xf8o\x11zpn\
\x14\xb0\xc5\xbdo\xda\xc0/\x92\xa6j\xd2\xcd\x8c<7\
\x9e\x01\x8b4\xca\xd3X%\x09$\xc3%\x91\xd1\xa4b\
\x87\xd40J\xdbk\xee5\xc9\x09=\xd8\xb2\xea\x8dK\
\xb6\x05\xc8\x90Z\xd0\x00\xceY\x11\xdf\xe3\xffI%\xf5\
mI\xdc\x224`\xfaE~\xe9\x0e\xa4\xd3=B\xa7\
F\xe9\xe8\xe8\x1dE\x90B&\xd1)\x0c\xc4`,c\
\x19\xcbX\x06\xf2'\xe3\xdf\x9d\x06\x06\xf6\x92\x0e\x00\x00\
\x00\x22zTXtSoftware\x00\x00\
x\xda+//\xd7\xcb\xcc\xcb.NN,H\xd5\xcb\
/J\x07\x006\xd8\x06X\x10S\xca\x5c\x00\x00\x00\x00\
IEND\xaeB`\x82\
\x00\x00\x02V\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -2326,6 +2395,158 @@ K\x80@\x89\x15\x8d\xc04\xd5\xb5^\xaf\x1bx\xfa\x19\
\x84\xe7\x04\xcf\x88\xfd\xfd\xfd\xe4\xe8\xe8\xc8\x9dL&\x14\
\xc7\xcc\x7f5p\xf2g\x94\xf7\x1f\xdf\x9a\xd2\x93\xfbC\
\xb7\xa7\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x03m\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\
\x00\x00\x09pHYs\x00\x00\x1b\xaf\x00\x00\x1b\xaf\x01\
^\x1a\x91\x1c\x00\x00\x00\x07tIME\x07\xd9\x02\x10\
\x17\x22\x16\x993\xa5<\x00\x00\x02\xedIDATx\
\xda\xed\x97_H\xd3Q\x14\xc7\xcf\xd9\xef7\xcb\xca\x89\
B \x91\x0f\xc9\xcczha\xf6\xa8\xb4\x06M\x90f\
aiV$\x96V\x12\xf9P-S\x90d%Lh\
V/B.\x8d\xc2\xb2\xc0\x1erS\xc1\x84\x1cdo\
a\xa0\x0f>h\x0f\xfdy\xf0-\x9b\xa6\x0f\xb1{\xfb\
n\xf4\x8b\x91\xe2\x5cn\xae\x07\x0f\xdc\x9dq/\xfc>\
\xe7{\xce\xe1\xdc\xdf\x8f\xd6M\xb3\x86\x86\x86\xe2Dp\
u\xbf\xe19pG\x82>!\x01\x04\xe1\x9aOd\x09\
:\x13V\x02\xcd\xd6\x03H\x84\xa9\x04\xab\xaf\xaf\xef\x84\
\xcb\x8f\x07\xe0z\xcf\x82AU\xc8\xa6\xeax\xfc\xf6\xd1\
\x8dcKf\xa0\xa5\xa5\xa5\x0an$V\xd0\xea\xc7\xf3\
\x86\x9a\xae\xf9\xd3\xb5\xdd\xf3\xaf\x84\xa4\xcfRR\xa9\x5c\
&\x031\xb3\xf2\xf6\x1f\xb9P\xdb\x14\x90\xd2\xacH\xf6\
\x01\xda\x13\x10T\xd1Z\x96\xec\xa7x\x07P\xd26\xc7\
P\xdb!\x04\xbd\x10L\x15\xedg7-\x82\xc65\x00\
\xc0m\x02?\x82\xd9\xf5\xa4j\xb3\x5c\xd3&,\xba\x1f\
R\x8f\xd4\x93\xe3\xe5\xc5\x08\xf0X7\xa1\xa6> B\
\xde\xbb&s\xc0\xb3\xf3x\x81'\xbbt\x1f\xc1,w\
f\x19M\xd7$\xa0\xbe\xf7\xf2\x16\x19\xf5\x1c\x88\x0e|\
L!\xc9\x8d$u\x97X\xa8\x0b\xde\xac\x0aS\xab$\
3\x07\xd5\xb3\xd4\xd4\xc7'\x00\xa8\xce\x00\xfc)\xe0\xcc\
\xa4\xdbK2\xe9\x96\x94\xaa\x0bu\xcfc\xa8\x1f\xbc\x92\
\x22\xffq\x14GnB\xa4\xdbBRy\xcfRy\xcb\
\xa4XmS\xcf\xa7I\xe8\xed,\xf4\x85\xbb&\xc7\xb4\
\xda\xc7\xfe:\x06X\xf1d\x975y\x8c\xe5_\xbd\xc6\
\x93\x96\xf0\xb3\x5c\x87\x9f++\xdf\x8d\xf6m\xbf\xfa\xa9\
o[\x9d!\xe6\x97\x11\xc0\x198\x1e\x84\xea\x02\xa4|\
?T\xbf\x09?\x97\x92\x8a\xc7\xb3\xf6H)\x92^#\
\x1b\xae\xe5 \x03\x06\xb7e \xa5\xc3\xb4\xd2\x00\x00?\
\x91\x87Z\x87R\x0e_h\x9b\xea\x9e\x0e?\xdf\xdd\xf8\
\x9d\x03B\xde\x0cv>\x9a\xf1\x1aK\xbd\xb5\x7fk\xb3\
u1\xb8\xdd0`x\xe8F\xf9\x1e\xe19\xea\x8a\x03\
`\xa9\xfb\x09\xf8)\x80\x1d\xb6\x8f\xcf\x02\xda~\xa6}\
\x86v\xdc\x981\x06\x045\x02NB\x90\xf7\xf0t\xb3\
\x1f\x19\xb8\x80\xe5\xeeOo5\x84\xa9>\x040\x1aD\
!\xc0MEs\xe7F\x97diM\xc8\xcc\xf9N\xa7\
3'\xfc0\xb9\xe6\x1b\xe9\x98\x8c\xaaB\x07\x92\x142\
\xebU6\xeb\xf1\xbc$\x85}\xd8\xbb;\xd1\x9c\xfa\xe1\
\x0f0\xfd\x9e;\x04\x13\x1b\xecD\xec\x02\xdcJ\x92\xcf\
\x03<\x14\xf5$\x04\xd8\x84\xd5\x85\xbf_P\xeba,\
3\x96\x0f\x8a\x0fB}\xe6\xa43\xf5L\x08\x1enB\
\xb5\x13J\x01\xb5\x13a\xaa\x87V3\x07|X\x0eI\
4\xe5oK\x8b|\x1f\xcc\xd4\xfa\xd1h%\x00\xa7\x02\
<\xbc\xaaA\xb4\xf0 m\x0c\x0e+:+\x9a\xad\x1e\
\xfd\xaf\xdf\x091k\xea\xfe\xfe\xf8\xe1H\xdf\x05R\xca\
|f\x1e\x89\xd1\x9ef\xbdhx\x0fE4mL\xaf\
b/B\x06\x12o\xbf\x00\xa3\x17WYZq\xd9W\
\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x05\xcf\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\
\x00\x00\x00\x09pHYs\x00\x00\x03v\x00\x00\x03v\
\x01}\xd5\x82\xcc\x00\x00\x00\x19tEXtSof\
tware\x00www.inksca\
pe.org\x9b\xee<\x1a\x00\x00\x05LID\
ATx\xda\xc5\x97\xdboTE\x1c\xc7\xbf3s\xce\
\x9e\xeeni\x0b\xbd@\x01C1\x05B\x10\x0d\xf4\x22\
\x18\x95D0i\xc4\x17|21\xc6\x17\xc3\x93\x89\xd1\
\xc8\x8b\xfaP#oj\x8c\x89O\xfc\x03\xc6K\x0c\xf1\
\x01!B\xa8\xc1j\x88D\x90@)\x14\xac\xb5\x86K\
K\xaf\xdb\xdd={\xce\xcc\xef\xe7\xec\x9cficb\
\xd2\x98\x13f\xf3\xdd93\xbf\x93\x9d\xcf\xfengW\
03\x1e\xe6\xf0\xb0\xc2\xf1\xca\xe9\x17wok\xda~\
lh\xe6\xe2\xd6\xbf\x0a#\xab\x94\x92h\xcb\xad\x9d{\
z\xdd\xfe\x9b\xf9 \xf7\xce\xe1\xad\xef\x0d`\x05Cb\
\x05\xa3\xe7\xcb\xce\xc7\x0emz\xf9\xfc\xab\x9d\x87\xbb\xf6\
\xb4\xec[\x15k\x82\x8e\x09\x9d\xf9\xed\x8d\x07\xda\x0fu\
=\xd5\xd2w\xe6\x83K\x87{S\xf3@N\xe4>\x9d\
)M{>\xd5\xa1\xab\xad\x17\x1b\x1a\xda\xdd~V\xe6\
\xb1)\xb7\x05\xe7\xee\x9e\x94\xf3\xa5\xe2'\x00\x9eI\x05\
\xe0^a\xa2\xe7\xdc\xdf\x03\xe8\x5c\xb3\x05\xf3\xb9k\x10\
A\xc5\xed\x87\x98\xc4\x99\xdb\xdf\xe1\xd4\xd8\xb7\x18-\x0e\
\xefL\xcd\x03aTi\x1c\x1c\xff\x11\xdd\x1bw\xa3I\
\x99\xe5F3\x87+\xf7/\xc2W\xb215\x00\x133\
\x08\x11E(\xc7\xcb\x01<\xa3\x9d]2#5\x00\
\xd2\x04!$\x8c!\x94u\xb4\xccVG1\x8c6\x10\
@\x9a\x00\x0c\x08\x86!B\x18G\x80X\xea\x01\x03c\
\xed\x02\x94\x1e\x80Y\x04\xd0d`,\x80\x00j\x10\x01\
i\x18\xc3\x10\xd6\x9ej\x0e\x08\xc5 bD:^\xe6\
\x01M\xfa\xffy\xe0\xe8\xe0\xd1\x17\xaeN\x5c\xed\x1f\x18\
\x1d\xd869w\xafA\x92\x84d\xc0\xbeC\xb1\x80\x94\
\x02JHw81!2\x0e\xa0\xc6\xa0\x8dv9b\
\xd0ul\x03{\xbe\x84\xaf\x14<_\xc1\xf7\xecu\
F\xa15\xd76\xff\xc4\xda\x9e\xeb\x1d\xab\xb6\xf7\xbf\xb4\
\xf9\xf5\x13\xb5N\xd8\xf7E_\xefTi\xea\xf8\xc6\xfa\
\x8d=mA[\x03\x99$\xdel\xd8\xcd\xb48\x1bw\
M\xd0\xdaX\x00\x8d\xd8\x98%\x8a@D\xee^c\xc8\
\xddk(\xb9\xdfp\xb2W\xef564\xaa\xd6\x9e\xf9\
h\xea\xf8G\xc3o\xb8\x8e\xe9\x1eF\x1d\x9fw\xfc\xb0\
\xabm\xd7\x81\xbd\xeb\xf7\x02\xc2`.\x9aAu\x9f\x85\
\x15\x5c\xdc\x935\x0c\x84\x00r\xb9,\xee\x9a?!\x93\
$p{\x1b\xbcGQ\xa9\x84P\xd2\x83'\xfd\x9a\x94\
\xf2\x90\xb1{\x81\xca\xc2g\xcf\xda\x0d\xc6\xc2!\xdc\x89\
n\x9e\xf9\xec\xc9\xef\x0f\xb8\x10\x8c\xcf\x8dw#\x06z\
\xda\xbb0T\xb8\x00-4 Q\x93\x84\x80\x14\x02J\
J7\xcb(Y\x0b'\xc7\x87\xb1\xe8\x06X\x00L\x9c\
x\x81\xaa\x22\x042\x8b\xbcX\x8d\x91\x89Q\x9c\x1a:\
\x8d\xf7\xf7\xbd\x85\xdb4\x8cyLv\xd5r\x80\x88\x9a\
\xc6\xa6\xc602u\x1dEQN\x0eVp\xb3\x10H\
\x0e\x96\x02\xcc\xb2\x06\xc2\xe2\x01Dm0\x83\x00x\xc2\
\xc3\xba\xecz\xcc\x95\x8b\xb81}\x0bgG\xbfF\x1c\
\x11\x00\xd8\xf50\x8a\xc1md|\xd9T\x03\x00[\x91\
k\xb5\xa8\xa88\x01 \x07\x91$\x1fK+\x07\x90\x1c\
\x0e\x86\xaa\x02A8\xefdT\x1d\x9a\x83V\x17\x8e0\
\x0a1\xb10\x81o\xfe\xf8\x0a\x85J\x11\xa5(\xb6\xa0\
\x12\x90\xe42V\xb3v\xf9@\x9e\xc0R\x80Y\x10\x9a\
f\x0a\xb3\xb8^\xbc\x09?\xf0\x9c\x82L\x80l\x10\xc0\
>\xe7\x91\xf7\xb2\x8eI2\x01,]G\x0cD\x00\x22\
\x8d\xf9h\x1a\xc3\x93\xbf!\xd4\x15h\x22D\xc6\x00\xd2\
\xc0S\xca\xca@\x19W.N\x19\xcf\xc3\x02\xbb\xf0\xcc\
.-\xc3A\x10\x0e\x0e\x0e\xff\x82\x1d\xebw .\xc5\
@9\x09A$\x04H\x85(\x8b\x18\x9e'\xdc\x87\xd6\
5H\xdc\xc1-\x04\x9erkea\x1e\xf1;Q\xd1\
\x06\xbc\x987\xe43L\x96\xa09)[cU\xefe\
1e\x93\x97du\x8f\x7f~\x00\xa0q\x04\xc0\xfe\x85\
\xf2B\xdd\xf9k\xe7\x81x1\x04\x9cP\x0b+)\x92\
^ \x94\xc0\xb3{z\xe1\xafV\xc8T3\xbc\x0a!\
%\xc2B\x8c\x9f.\xffj!%\xaa=@YyK\
\xe4\xfb\x0a\x85\x8cB&c\xd7\x19\xaf\xacY\x1f\xa9\xf5\
\x01\xfe\x90\xaf\x81\xd0\x03\xe0,\x08\x0b0@M\x1a`\
;SU\x94\x5ck\x13\xbbC}%\x1d@\xe0{\xae\
D\x99\x01\x02\xbb\xba\xa7\x7f\x89\xacx\xce\xdaN\xda\xb9\
\xfbD\xdf\xc8\xd0\xb2N\xc8\x1f\xf3\x15\x00\xcf\xfdg\xdb\
|M2\x0b\xd7\xf5\x9cG\xbc*\x80\xb2\x00\x9e\x87J\
r\x00$\x0b\x8c\xbd;#R\xf9MH&\xa9\xf3\x98\
b\xf0b|\x94L\xbc\x00h\x07@)\xfe\x1ep\x87\
3\x0bT\xe2\x18\xc2\x18\x18J\xbe\xb5X\xecF\xc9+\
E\x80\x04\x02(\xeb\x0a8\xd2P2\xb2J\x1aR1\
\xa2\xa4\x9d\xa4\x0e \x18\xa5(D\x14F\xae\xe6+\xb1\
F!\xac ,\xf9 k\xb3<)\x02\x88D\x0bq\
\x08*VP\xac\xc4\xae\x17\xb8\xf2,f\xc0\x92\xacd\
z\x00^F\x15\x1b\xb2A\xbeE\x05\xb8Q\x9aK\xda\
\xb4U\xf5\xb5)\xee@=1B.\x15R\xfbg\x94\
\xad\xf7\x7f\xdf\xd9\xde\x82\x83\xcdk\xb03n\x86\x9cR\
\xc0\xb4D\xb7\xea\xc5V\xde\x82<\xea!H^H\x0d\
\xdf\x1c\xbc\xb9ym\xa0W7*\xb4\xe6\xf3 c\
\xc0Dx\xa2\xf9q\xc8@Add$`\xdeN\x0d\
\xe0N\xff\xec\x05\xd9\x1c=?\xee\xf3\xe5\xfb\x22Z\xa0\
,\x83\xeb\x18*\xab\xc2 \x9b\xb9\x12\xa0n\xffD\x7f\
\xf1\x12V0\x1e\xfa\xdf\xf3\x7f\x00j\xf0\xda\xe8\xbc\xba\
\xd0\x0a\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x04<\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -2396,6 +2617,35 @@ f\xa2\xd1\xa5\x5c\x22\xb4\x91SZ\xd5u\xd7\x0a\xd2]\
|\x01\x85\x09\x800\x7fss\xd3{\xf6\x7fABG\
Y\x01\x05l*\xfc\x00!\x00\x12\xf1%U\xb6\x0e\x00\
\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x01\xad\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00 \x00\x00\x00 \x08\x03\x00\x00\x00D\xa4\x8a\xc6\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01B(\
\x9bx\x00\x00\x00\x07tIME\x07\xd9\x0c\x1c\x03\x1c\
\x0e%S,b\x00\x00\x00uPLTE\x00\x00\x00\
\x13\x13\x13\x0a\x0a\x0a\x0b\x0b\x0b\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc6\xc6\xc8\xc7\xc7\
\xc9\xc9\xc9\xcb\xcc\xcc\xce\xce\xce\xd0\xd0\xd0\xd2\xd4\xd4\xd6\
\xd7\xd7\xd8\xd7\xd7\xd9\xd8\xd8\xda\xd9\xd9\xdb\xda\xda\xdb\xda\
\xda\xdc\xdb\xdb\xdc\xdb\xdb\xdd\xdd\xdd\xdf\xde\xde\xdf\xe2\xe2\
\xe3\xe3\xe3\xe5\xe3\xe3\xe6\xe4\xe4\xe6\xe6\xe6\xe7\xe7\xe7\xe9\
\xe9\xe9\xea\xeb\xeb\xec\xed\xed\xee\xf0\xf0\xf1\xf3\xf3\xf4\xff\
\xff\xff\xd3\x9b\xcc\x0e\x00\x00\x00\x0atRNS\x00\x09\
\x15\x15\x1825678\xb5\xcc\xc0\x1e\x00\x00\x00\x01\
bKGD&Z\x08\x98\xb5\x00\x00\x00\x9bIDA\
Tx\xda\xd5\x93\xcb\x0e\x820\x10\x00\x8b\x0aEP|\
u\xc1G)\x94\x02\xff\xff\x89.]\x0e\x18\xccr1\
F\xe72\xd9d\xd26\x9bT\xfc\x02\xc1\x86%\x10a\
\xcf\x12\x0a\xd9)\x86N\x0a\xd9^\x18Z\x0c\xdc\x91\xc1\
a\xd0\x1c\x18\x1a\x0c\xec\x9e\xc1bP'\x9et\x97\xce\
\x95\xd4\x18\x94=q#\xddIWR\x89\x81\x1e\x03E\
\x82\x97IO\x02\x98\x8b\x82G\x01\x9e\xec\x8d\x8aj\xf1\
\x84\xcf\xbc!W\x03\x90\x8d\x02oR^-^\xf1?\
{\xe0\x83\xc8\x1a=`\xce\xc6\xebd\xa6\x93\x8b\xc4*\
\xde2\xc4\xeb/|\xcd'\xec\xbfO\xbf\x90M\x1a\x0a\
\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x02\x92\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -2563,6 +2813,10 @@ qt_resource_name = b"\
\x0f\xba\x1e'\
\x00d\
\x00r\x00a\x00w\x00-\x00p\x00a\x00t\x00h\x00.\x00p\x00n\x00g\
\x00\x0f\
\x020\x8b\xe7\
\x00l\
\x00i\x00s\x00t\x00-\x00r\x00e\x00m\x00o\x00v\x00e\x00.\x00p\x00n\x00g\
\x00\x0d\
\x0b\xe6\x1f\xa7\
\x00d\
@@ -2632,10 +2886,24 @@ qt_resource_name = b"\
\x00d\
\x00o\x00c\x00u\x00m\x00e\x00n\x00t\x00-\x00s\x00a\x00v\x00e\x00.\x00p\x00n\x00g\
\
\x00\x15\
\x02\xb4\x1f\x07\
\x00o\
\x00f\x00f\x00i\x00c\x00e\x00-\x00c\x00h\x00a\x00r\x00t\x00-\x00l\x00i\x00n\x00e\
\x00.\x00p\x00n\x00g\
\x00\x0c\
\x09\xc6\x19'\
\x00l\
\x00i\x00s\x00t\x00-\x00a\x00d\x00d\x00.\x00p\x00n\x00g\
\x00\x10\
\x03\xe6\xd3g\
\x00d\
\x00r\x00a\x00w\x00-\x00e\x00l\x00l\x00i\x00p\x00s\x00e\x00.\x00p\x00n\x00g\
\x00\x13\
\x07\xd6O\x07\
\x00v\
\x00i\x00e\x00w\x00-\x00f\x00o\x00r\x00m\x00-\x00t\x00a\x00b\x00l\x00e\x00.\x00p\
\x00n\x00g\
\x00\x12\
\x09\xb3>\xc7\
\x00d\
@@ -2653,63 +2921,71 @@ qt_resource_struct = b"\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1c\x00\x00\x00\x03\
\x00\x00\x00\x00\x00\x02\x00\x00\x00 \x00\x00\x00\x03\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x01\xfe\x00\x00\x00\x00\x00\x01\x00\x00N\xe8\
\x00\x00\x02\x22\x00\x00\x00\x00\x00\x01\x00\x00S\x15\
\x00\x00\x01\x9f{C\xf1'\
\x00\x00\x02>\x00\x00\x00\x00\x00\x01\x00\x00Z\x1a\
\x00\x00\x02b\x00\x00\x00\x00\x00\x01\x00\x00^G\
\x00\x00\x01\x9f\x7f\xa8\xa8)\
\x00\x00\x02\xec\x00\x00\x00\x00\x00\x01\x00\x00tV\
\x00\x00\x03\x10\x00\x00\x00\x00\x00\x01\x00\x00x\x83\
\x00\x00\x01\x9f{0\xc99\
\x00\x00\x01\xc4\x00\x00\x00\x00\x00\x01\x00\x00D2\
\x00\x00\x01d\x00\x00\x00\x00\x00\x01\x00\x004B\
\x00\x00\x01\x9f\x84\xbd\x03\xb8\
\x00\x00\x03\xc8\x00\x00\x00\x00\x00\x01\x00\x00\x91\xd7\
\x00\x00\x01\x9f\x84\x8f\x00\xb9\
\x00\x00\x01\xe8\x00\x00\x00\x00\x00\x01\x00\x00H_\
\x00\x00\x01\x9f\x7f&\x83\xcd\
\x00\x00\x01\xa4\x00\x00\x00\x00\x00\x01\x00\x00<J\
\x00\x00\x01\xc8\x00\x00\x00\x00\x00\x01\x00\x00@w\
\x00\x00\x01\x9f{C\xf1\x18\
\x00\x00\x03\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x8d\xaa\
\x00\x00\x04\x16\x00\x00\x00\x00\x00\x01\x00\x00\x9b\x1b\
\x00\x00\x01\x9f\x7fY\xceg\
\x00\x00\x02\xa2\x00\x00\x00\x00\x00\x01\x00\x00f\xc8\
\x00\x00\x02\xc6\x00\x00\x00\x00\x00\x01\x00\x00j\xf5\
\x00\x00\x01\x9f\x7fV\xd5\xc0\
\x00\x00\x03Z\x00\x00\x00\x00\x00\x01\x00\x00\x84\xc5\
\x00\x00\x03~\x00\x00\x00\x00\x00\x01\x00\x00\x88\xf2\
\x00\x00\x01\x9f\x7f&\x83r\
\x00\x00\x02\xce\x00\x00\x00\x00\x00\x01\x00\x00m[\
\x00\x00\x02\xf2\x00\x00\x00\x00\x00\x01\x00\x00q\x88\
\x00\x00\x01\x9f\x7f&\x83\xb1\
\x00\x00\x01\x1c\x00\x00\x00\x00\x00\x01\x00\x00,\x08\
\x00\x00\x01\x9f\x7fY\xce\x82\
\x00\x00\x01\xe0\x00\x00\x00\x00\x00\x01\x00\x00Kh\
\x00\x00\x02\x04\x00\x00\x00\x00\x00\x01\x00\x00O\x95\
\x00\x00\x01\x9f{C\xf1.\
\x00\x00\x01\x84\x00\x00\x00\x00\x00\x01\x00\x006\x9c\
\x00\x00\x04<\x00\x00\x00\x00\x00\x01\x00\x00\x9f[\
\x00\x00\x01\x9f\x84\x8f\xa6\x10\
\x00\x00\x01\xa8\x00\x00\x00\x00\x00\x01\x00\x00:\xc9\
\x00\x00\x01\x9f{{\xa5\xd5\
\x00\x00\x03\xca\x00\x00\x00\x00\x00\x01\x00\x00\x91\xea\
\x00\x00\x04h\x00\x00\x00\x00\x00\x01\x00\x00\xa1\x0c\
\x00\x00\x01\x9f\x7f&\x83\x10\
\x00\x00\x03\xf8\x00\x00\x00\x00\x00\x01\x00\x00\x95H\
\x00\x00\x01\x9f\x84\xbd\x03\xde\
\x00\x00\x00\xfe\x00\x00\x00\x00\x00\x01\x00\x00(e\
\x00\x00\x01\x9f\x7f&\x83~\
\x00\x00\x036\x00\x00\x00\x00\x00\x01\x00\x00\x80\xe2\
\x00\x00\x03Z\x00\x00\x00\x00\x00\x01\x00\x00\x85\x0f\
\x00\x00\x01\x9f\x7f&\x83T\
\x00\x00\x00~\x00\x00\x00\x00\x00\x01\x00\x00\x14M\
\x00\x00\x01\x9f\x7fY\xce^\
\x00\x00\x00\xa2\x00\x00\x00\x00\x00\x01\x00\x00\x19Y\
\x00\x00\x01\x9f{0\xc9B\
\x00\x00\x01d\x00\x00\x00\x00\x00\x01\x00\x004B\
\x00\x00\x01\x88\x00\x00\x00\x00\x00\x01\x00\x008o\
\x00\x00\x01\x9f\x7f&\x83)\
\x00\x00\x03\x14\x00\x00\x00\x00\x00\x01\x00\x00{`\
\x00\x00\x038\x00\x00\x00\x00\x00\x01\x00\x00\x7f\x8d\
\x00\x00\x01\x9f{C\xf1N\
\x00\x00\x02p\x00\x00\x00\x00\x00\x01\x00\x00b\x99\
\x00\x00\x02\x94\x00\x00\x00\x00\x00\x01\x00\x00f\xc6\
\x00\x00\x01\x9f\x7f&\x82\xf2\
\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x01\x00\x00!\xc5\
\x00\x00\x01\x9f{\x8d\xf34\
\x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01\x9f{0\xc9+\
\x00\x00\x02\x1e\x00\x00\x00\x00\x00\x01\x00\x00RH\
\x00\x00\x02B\x00\x00\x00\x00\x00\x01\x00\x00Vu\
\x00\x00\x01\x9f{C\xf1=\
\x00\x00\x006\x00\x00\x00\x00\x00\x01\x00\x00\x05\x86\
\x00\x00\x01\x9f\x7f&\x83\x99\
\x00\x00\x03\xf4\x00\x00\x00\x00\x00\x01\x00\x00\x94\x80\
\x00\x00\x04\x92\x00\x00\x00\x00\x00\x01\x00\x00\xa3\xa2\
\x00\x00\x01\x9f{{\xa5\xe3\
\x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x00\x0c\xc1\
\x00\x00\x01\x9f\x7f\xac\xf2\xc6\
\x00\x00\x01D\x00\x00\x00\x00\x00\x01\x00\x00/{\
\x00\x00\x01\x9f\x7fV\xd5\xe5\
\x00\x00\x03|\x00\x00\x00\x00\x00\x01\x00\x00\x88\xb7\
\x00\x00\x03\xa0\x00\x00\x00\x00\x00\x01\x00\x00\x8c\xe4\
\x00\x00\x01\x9f{0\xc9R\
"

View File

@@ -39,91 +39,99 @@ class Ui_MainWindow(object):
self.actionGraphParameters = QAction(MainWindow)
self.actionGraphParameters.setObjectName(u"actionGraphParameters")
icon1 = QIcon()
icon1.addFile(u":/icons/icons/configure.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
icon1.addFile(u":/icons/icons/view-form-table.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionGraphParameters.setIcon(icon1)
self.actionCompile = QAction(MainWindow)
self.actionCompile.setObjectName(u"actionCompile")
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.actionCompile.setIcon(icon2)
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")
icon4 = QIcon()
icon4.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionNew.setIcon(icon4)
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")
icon5 = QIcon()
icon5.addFile(u":/icons/icons/transform-rotate.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRotateClockwise.setIcon(icon5)
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")
icon6 = QIcon()
icon6.addFile(u":/icons/icons/zoom-in.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionZoomIn.setIcon(icon6)
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")
icon7 = QIcon()
icon7.addFile(u":/icons/icons/zoom-out.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionZoomOut.setIcon(icon7)
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")
icon8 = QIcon()
icon8.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCenterView.setIcon(icon8)
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")
icon9 = QIcon()
icon9.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionOpen.setIcon(icon9)
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")
icon10 = QIcon()
icon10.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSave.setIcon(icon10)
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")
icon11 = QIcon()
icon11.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSaveAs.setIcon(icon11)
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")
icon12 = QIcon()
icon12.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionUndo.setIcon(icon12)
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")
icon13 = QIcon()
icon13.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRedo.setIcon(icon13)
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")
icon14 = QIcon()
icon14.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCut.setIcon(icon14)
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")
icon15 = QIcon()
icon15.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCopy.setIcon(icon15)
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")
icon16 = QIcon()
icon16.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionPaste.setIcon(icon16)
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)
@@ -213,18 +221,18 @@ class Ui_MainWindow(object):
self.workspaceHeaderLayout.setContentsMargins(6, 2, 6, 2)
self.navigateUpButton = QToolButton(self.workspaceHeader)
self.navigateUpButton.setObjectName(u"navigateUpButton")
icon17 = QIcon()
icon17.addFile(u":/icons/icons/arrow-up.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.navigateUpButton.setIcon(icon17)
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)
icon18 = QIcon()
icon18.addFile(u":/icons/icons/arrow-down.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.navigateDownButton.setIcon(icon18)
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)
@@ -244,9 +252,9 @@ class Ui_MainWindow(object):
self.pointerToolButton = QToolButton(self.workspaceHeader)
self.pointerToolButton.setObjectName(u"pointerToolButton")
icon19 = QIcon()
icon19.addFile(u":/icons/icons/edit-select.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.pointerToolButton.setIcon(icon19)
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)
@@ -254,43 +262,43 @@ class Ui_MainWindow(object):
self.connectToolButton = QToolButton(self.workspaceHeader)
self.connectToolButton.setObjectName(u"connectToolButton")
icon20 = QIcon()
icon20.addFile(u":/icons/icons/network-connect.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.connectToolButton.setIcon(icon20)
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")
icon21 = QIcon()
icon21.addFile(u":/icons/icons/draw-rectangle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.boxToolButton.setIcon(icon21)
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")
icon22 = QIcon()
icon22.addFile(u":/icons/icons/draw-bezier-curves.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.lineToolButton.setIcon(icon22)
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")
icon23 = QIcon()
icon23.addFile(u":/icons/icons/draw-text.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.textToolButton.setIcon(icon23)
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(icon5)
self.rotateToolButton.setIcon(icon7)
self.workspaceHeaderLayout.addWidget(self.rotateToolButton)
@@ -435,7 +443,9 @@ class Ui_MainWindow(object):
self.menuHelp.addAction(self.actionAboutQt)
self.menuSimulation.addAction(self.actionSimulationSettings)
self.menuSimulation.addAction(self.actionGraphParameters)
self.menuSimulation.addAction(self.actionCompile)
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)
@@ -451,7 +461,8 @@ class Ui_MainWindow(object):
self.cameraToolbar.addAction(self.actionCenterView)
self.simulationToolbar.addAction(self.actionSimulationSettings)
self.simulationToolbar.addAction(self.actionGraphParameters)
self.simulationToolbar.addAction(self.actionCompile)
self.simulationToolbar.addAction(self.actionCompose)
self.simulationToolbar.addAction(self.actionSimulationWindow)
self.simulationToolbar.addAction(self.actionRunSimulation)
self.retranslateUi(MainWindow)
@@ -472,12 +483,12 @@ class Ui_MainWindow(object):
#if QT_CONFIG(statustip)
self.actionGraphParameters.setStatusTip(QCoreApplication.translate("MainWindow", u"Edit parameters throughout the active graph", None))
#endif // QT_CONFIG(statustip)
self.actionCompile.setText(QCoreApplication.translate("MainWindow", u"Compile", None))
self.actionCompose.setText(QCoreApplication.translate("MainWindow", u"Compose", None))
#if QT_CONFIG(statustip)
self.actionCompile.setStatusTip(QCoreApplication.translate("MainWindow", u"Compile the active graph for simulation", None))
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.actionCompile.setShortcut(QCoreApplication.translate("MainWindow", u"F5", None))
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)
@@ -486,6 +497,14 @@ class Ui_MainWindow(object):
#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))

View File

@@ -15,16 +15,17 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox,
QFormLayout, QHBoxLayout, QLabel, QLineEdit,
QListWidget, QListWidgetItem, QPushButton, QSizePolicy,
QSplitter, QVBoxLayout, QWidget)
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(620, 380)
ParameterOptionsDialog.resize(720, 500)
self.dialogLayout = QVBoxLayout(ParameterOptionsDialog)
self.dialogLayout.setObjectName(u"dialogLayout")
self.parameterSplitter = QSplitter(ParameterOptionsDialog)
@@ -76,20 +77,82 @@ class Ui_ParameterOptionsDialog(object):
self.parameterDetailsForm.setWidget(1, QFormLayout.ItemRole.LabelRole, self.typeLabel)
self.typeEdit = QLineEdit(self.parameterDetailsPanel)
self.typeEdit.setObjectName(u"typeEdit")
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.typeEdit)
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(2, QFormLayout.ItemRole.LabelRole, self.valueLabel)
self.parameterDetailsForm.setWidget(4, QFormLayout.ItemRole.LabelRole, self.valueLabel)
self.valueEdit = QLineEdit(self.parameterDetailsPanel)
self.valueEdit.setObjectName(u"valueEdit")
self.parameterDetailsForm.setWidget(2, QFormLayout.ItemRole.FieldRole, self.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)
@@ -115,6 +178,16 @@ class Ui_ParameterOptionsDialog(object):
self.removeParameterButton.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Remove Parameter", None))
self.nameLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Name:", None))
self.typeLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Type:", None))
self.typeCombo.setItemText(0, QCoreApplication.translate("ParameterOptionsDialog", u"real", None))
self.typeCombo.setItemText(1, QCoreApplication.translate("ParameterOptionsDialog", u"integer", None))
self.typeCombo.setItemText(2, QCoreApplication.translate("ParameterOptionsDialog", u"boolean", None))
self.typeCombo.setItemText(3, QCoreApplication.translate("ParameterOptionsDialog", u"string", None))
self.rowsLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Rows:", None))
self.columnsLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Columns:", None))
self.valueLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Value:", None))
self.quantityLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Quantity:", None))
self.unitLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Unit:", None))
self.descriptionLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Description:", None))
# retranslateUi

View File

@@ -18,14 +18,14 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
from PySide6.QtWidgets import (QAbstractButton, QApplication, QCheckBox, QComboBox,
QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout,
QLabel, QLineEdit, QListWidget, QListWidgetItem,
QPushButton, QSizePolicy, QSplitter, QVBoxLayout,
QWidget)
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(620, 380)
PortOptionsDialog.resize(760, 421)
self.dialogLayout = QVBoxLayout(PortOptionsDialog)
self.dialogLayout.setObjectName(u"dialogLayout")
self.portSplitter = QSplitter(PortOptionsDialog)
@@ -61,6 +61,7 @@ class Ui_PortOptionsDialog(object):
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")
@@ -91,6 +92,7 @@ class Ui_PortOptionsDialog(object):
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)
@@ -104,7 +106,154 @@ class Ui_PortOptionsDialog(object):
self.positionHintLabel.setObjectName(u"positionHintLabel")
self.positionHintLabel.setWordWrap(True)
self.portDetailsForm.setWidget(4, QFormLayout.ItemRole.SpanningRole, self.positionHintLabel)
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)
@@ -129,14 +278,33 @@ class Ui_PortOptionsDialog(object):
self.addPortButton.setText(QCoreApplication.translate("PortOptionsDialog", u"Add Port", None))
self.removePortButton.setText(QCoreApplication.translate("PortOptionsDialog", u"Remove Port", None))
self.nameLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Name:", None))
self.typeLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Type:", None))
self.typeLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Port type:", None))
self.typeCombo.setItemText(0, QCoreApplication.translate("PortOptionsDialog", u"Signal", None))
self.orientationLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Orientation:", None))
self.orientationCombo.setItemText(0, QCoreApplication.translate("PortOptionsDialog", u"Input", None))
self.orientationCombo.setItemText(1, QCoreApplication.translate("PortOptionsDialog", u"Output", None))
self.orientationCombo.setItemText(2, QCoreApplication.translate("PortOptionsDialog", u"Indifferent", None))
self.multipleConnectionsCheckBox.setText(QCoreApplication.translate("PortOptionsDialog", u"Allow multiple connections", None))
self.positionHintLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"New ports start at (0, 0) in the icon editor.", None))
self.valueTypeLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Type:", None))
self.valueTypeCombo.setItemText(0, QCoreApplication.translate("PortOptionsDialog", u"real", None))
self.valueTypeCombo.setItemText(1, QCoreApplication.translate("PortOptionsDialog", u"integer", None))
self.valueTypeCombo.setItemText(2, QCoreApplication.translate("PortOptionsDialog", u"boolean", None))
self.valueTypeCombo.setItemText(3, QCoreApplication.translate("PortOptionsDialog", u"string", None))
self.quantityLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Quantity:", None))
self.unitLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Unit:", None))
self.rowsColumnsLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Matrix size:", None))
self.descriptionLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Description:", None))
self.domainLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Domain:", None))
self.effortLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Effort:", None))
self.effortValueLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"p.e", None))
self.flowLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Flow:", None))
self.flowValueLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"p.f", None))
self.causalityLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Causality:", None))
self.powerDescriptionLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Description:", None))
self.unsupportedTypeLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"This port type does not have an options editor yet.", None))
# retranslateUi

View File

@@ -15,12 +15,12 @@ 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, QDialog,
QDialogButtonBox, QFormLayout, QGroupBox, QHBoxLayout,
QHeaderView, QLabel, QLineEdit, QListWidget,
QListWidgetItem, QPushButton, QSizePolicy, QSpacerItem,
QSpinBox, QTabWidget, QTableWidget, QTableWidgetItem,
QVBoxLayout, QWidget)
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):
@@ -104,6 +104,34 @@ class Ui_SettingsDialog(object):
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)
@@ -247,6 +275,10 @@ class Ui_SettingsDialog(object):
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))

View File

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

View File

@@ -15,11 +15,11 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractItemView, QApplication, QGroupBox, QHBoxLayout,
QHeaderView, QPushButton, QSizePolicy, QSpacerItem,
QSplitter, QTableWidget, QTableWidgetItem, QVBoxLayout,
QWidget)
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):
@@ -34,7 +34,31 @@ class Ui_TextDefinitionEditor(object):
self.columnSplitter.setObjectName(u"columnSplitter")
self.columnSplitter.setOrientation(Qt.Orientation.Horizontal)
self.columnSplitter.setChildrenCollapsible(False)
self.equationsGroup = QGroupBox(self.columnSplitter)
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")
@@ -43,7 +67,8 @@ class Ui_TextDefinitionEditor(object):
self.equationsLayout.addWidget(self.equationsEdit)
self.columnSplitter.addWidget(self.equationsGroup)
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)
@@ -52,82 +77,20 @@ class Ui_TextDefinitionEditor(object):
self.portsGroup.setObjectName(u"portsGroup")
self.portsLayout = QVBoxLayout(self.portsGroup)
self.portsLayout.setObjectName(u"portsLayout")
self.portsTable = QTableWidget(self.portsGroup)
if (self.portsTable.columnCount() < 4):
self.portsTable.setColumnCount(4)
__qtablewidgetitem = QTableWidgetItem()
self.portsTable.setHorizontalHeaderItem(0, __qtablewidgetitem)
__qtablewidgetitem1 = QTableWidgetItem()
self.portsTable.setHorizontalHeaderItem(1, __qtablewidgetitem1)
__qtablewidgetitem2 = QTableWidgetItem()
self.portsTable.setHorizontalHeaderItem(2, __qtablewidgetitem2)
__qtablewidgetitem3 = QTableWidgetItem()
self.portsTable.setHorizontalHeaderItem(3, __qtablewidgetitem3)
self.portsTable.setObjectName(u"portsTable")
self.portsTable.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.portsTable.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.portsTable.setColumnCount(4)
self.portEditor = PortEditor(self.portsGroup)
self.portEditor.setObjectName(u"portEditor")
self.portsLayout.addWidget(self.portsTable)
self.portButtonsLayout = QHBoxLayout()
self.portButtonsLayout.setObjectName(u"portButtonsLayout")
self.addPortButton = QPushButton(self.portsGroup)
self.addPortButton.setObjectName(u"addPortButton")
self.portButtonsLayout.addWidget(self.addPortButton)
self.removePortButton = QPushButton(self.portsGroup)
self.removePortButton.setObjectName(u"removePortButton")
self.portButtonsLayout.addWidget(self.removePortButton)
self.portButtonSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.portButtonsLayout.addItem(self.portButtonSpacer)
self.portsLayout.addLayout(self.portButtonsLayout)
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.parametersTable = QTableWidget(self.parametersGroup)
if (self.parametersTable.columnCount() < 3):
self.parametersTable.setColumnCount(3)
__qtablewidgetitem4 = QTableWidgetItem()
self.parametersTable.setHorizontalHeaderItem(0, __qtablewidgetitem4)
__qtablewidgetitem5 = QTableWidgetItem()
self.parametersTable.setHorizontalHeaderItem(1, __qtablewidgetitem5)
__qtablewidgetitem6 = QTableWidgetItem()
self.parametersTable.setHorizontalHeaderItem(2, __qtablewidgetitem6)
self.parametersTable.setObjectName(u"parametersTable")
self.parametersTable.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.parametersTable.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.parametersTable.setColumnCount(3)
self.parameterEditor = ParameterEditor(self.parametersGroup)
self.parameterEditor.setObjectName(u"parameterEditor")
self.parametersLayout.addWidget(self.parametersTable)
self.parameterButtonsLayout = QHBoxLayout()
self.parameterButtonsLayout.setObjectName(u"parameterButtonsLayout")
self.addParameterButton = QPushButton(self.parametersGroup)
self.addParameterButton.setObjectName(u"addParameterButton")
self.parameterButtonsLayout.addWidget(self.addParameterButton)
self.removeParameterButton = QPushButton(self.parametersGroup)
self.removeParameterButton.setObjectName(u"removeParameterButton")
self.parameterButtonsLayout.addWidget(self.removeParameterButton)
self.parameterButtonSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.parameterButtonsLayout.addItem(self.parameterButtonSpacer)
self.parametersLayout.addLayout(self.parameterButtonsLayout)
self.parametersLayout.addWidget(self.parameterEditor)
self.definitionSplitter.addWidget(self.parametersGroup)
self.columnSplitter.addWidget(self.definitionSplitter)
@@ -141,27 +104,11 @@ class Ui_TextDefinitionEditor(object):
# 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))
___qtablewidgetitem = self.portsTable.horizontalHeaderItem(0)
___qtablewidgetitem.setText(QCoreApplication.translate("TextDefinitionEditor", u"Name", None))
___qtablewidgetitem1 = self.portsTable.horizontalHeaderItem(1)
___qtablewidgetitem1.setText(QCoreApplication.translate("TextDefinitionEditor", u"Type", None))
___qtablewidgetitem2 = self.portsTable.horizontalHeaderItem(2)
___qtablewidgetitem2.setText(QCoreApplication.translate("TextDefinitionEditor", u"Orientation", None))
___qtablewidgetitem3 = self.portsTable.horizontalHeaderItem(3)
___qtablewidgetitem3.setText(QCoreApplication.translate("TextDefinitionEditor", u"Multiple", None))
self.addPortButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Add Port", None))
self.removePortButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Remove Port", None))
self.parametersGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Parameters", None))
___qtablewidgetitem4 = self.parametersTable.horizontalHeaderItem(0)
___qtablewidgetitem4.setText(QCoreApplication.translate("TextDefinitionEditor", u"Name", None))
___qtablewidgetitem5 = self.parametersTable.horizontalHeaderItem(1)
___qtablewidgetitem5.setText(QCoreApplication.translate("TextDefinitionEditor", u"Type", None))
___qtablewidgetitem6 = self.parametersTable.horizontalHeaderItem(2)
___qtablewidgetitem6.setText(QCoreApplication.translate("TextDefinitionEditor", u"Value", None))
self.addParameterButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Add Parameter", None))
self.removeParameterButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Remove Parameter", None))
pass
# retranslateUi

View File

@@ -1,8 +1,12 @@
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"
@@ -13,11 +17,17 @@ class ConnectionStyle:
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),
}

View File

@@ -1,6 +1,6 @@
from copy import deepcopy
from PySide6.QtCore import QPointF, QRectF, QSizeF, Qt
from PySide6.QtCore import QPointF, QRectF, QSizeF, Qt, QTimer
from PySide6.QtGui import QColor, QPainter, QPainterPath, QPen, QPolygonF
from PySide6.QtWidgets import (
QColorDialog,
@@ -458,8 +458,7 @@ class IconEditorDialog(QDialog):
self.ui.setupUi(self)
self.setWindowTitle(f"Icon Editor — {component.name}")
self.icon = Icon.from_dict(component.icon.to_dict())
self.inputs = deepcopy(component.inputs)
self.outputs = deepcopy(component.outputs)
self.ports = deepcopy(component.ports)
self.tool_group = QButtonGroup(self)
self.tool_group.setExclusive(True)
self.tool_group.addButton(self.ui.pointerButton)
@@ -485,9 +484,17 @@ class IconEditorDialog(QDialog):
self.scene.addRect(self.scene.sceneRect(), QPen(QColor("#64748b"), 0)).setZValue(-100)
for element in self.icon.elements:
self.scene.addItem(ShapeItem(element))
self._add_ports(self.inputs, "input", 0.0)
self._add_ports(self.outputs, "output", self.icon.width)
self.view.center_icon()
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)

View File

@@ -173,8 +173,16 @@ class ComponentGraphicsItem(QGraphicsObject):
| QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
)
self.input_ports = self._create_ports(component.inputs, "target", 0.0)
self.output_ports = self._create_ports(component.outputs, "source", self.WIDTH)
self.input_ports = self._create_ports(
[port for port in component.ports if port.orientation != "output"],
"target",
0.0,
)
self.output_ports = self._create_ports(
[port for port in component.ports if port.orientation == "output"],
"source",
self.WIDTH,
)
self.setTransformOriginPoint(self.hitbox.center())
self.setRotation(component.rotation)
self.name_label: NameLabelItem | None = None
@@ -371,8 +379,10 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
super().__init__()
self.connection_id = connection.id
self.name = connection.name
self.connection_type = connection.type
self.source_is_junction = connection.source.junction is not None
self.target_is_junction = connection.target.junction is not None
self.causality = connection.causality
self.controller = controller
self.style = style or ConnectionStyle()
self.start = QPointF()
@@ -387,6 +397,19 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
self.name_label: NameLabelItem | None = None
self.sync_name_label(connection)
def boundingRect(self) -> QRectF: # noqa: N802
"""Include custom arrowheads and causality marks in Qt's repaint area."""
decoration_margin = (
max(self.style.arrow_size, 6.0) + self.style.selected_width / 2 + 1.0
)
return super().boundingRect().adjusted(
-decoration_margin,
-decoration_margin,
decoration_margin,
decoration_margin,
)
def sync_name_label(self, connection: Connection) -> None:
visible = bool(connection.properties.get("showName", False))
if not visible:
@@ -417,7 +440,11 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
self.setSelected(True)
menu = QMenu()
add_node_action = menu.addAction("Add Node")
add_junction_action = menu.addAction("Add Junction")
add_junction_action = (
menu.addAction("Add Junction")
if self.connection_type != "power"
else None
)
menu.addSeparator()
options_action = menu.addAction("Connection Options…")
selected = menu.exec(event.screenPos())
@@ -425,7 +452,7 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
scene = self.scene()
if isinstance(scene, GraphScene):
scene.add_route_node("connection", self.connection_id, event.scenePos())
elif selected is add_junction_action:
elif add_junction_action is not None and selected is add_junction_action:
scene = self.scene()
if isinstance(scene, GraphScene):
scene.add_connection_junction(self.connection_id, event.scenePos())
@@ -485,22 +512,62 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
handle.setVisible(self.isSelected())
@staticmethod
def _arrow(end: QPointF, direction: QPointF, size: float) -> QPolygonF:
def _arrow_points(
end: QPointF, direction: QPointF, size: float
) -> tuple[QPointF, QPointF, QPointF]:
length = max(0.001, (direction.x() ** 2 + direction.y() ** 2) ** 0.5)
unit = QPointF(direction.x() / length, direction.y() / length)
normal = QPointF(-unit.y(), unit.x())
base = end - unit * size
return QPolygonF([end, base + normal * size * 0.45, base - normal * size * 0.45])
return end, base + normal * size * 0.45, base - normal * size * 0.45
def _draw_arrow(
self, painter: QPainter, end: QPointF, direction: QPointF
) -> None:
tip, left, right = self._arrow_points(
end, direction, self.style.arrow_size
)
color = self.pen().color()
if self.style.arrow_style == "filled":
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(color)
painter.drawPolygon(QPolygonF([tip, left, right]))
return
painter.setPen(QPen(color, self.pen().widthF(), Qt.PenStyle.SolidLine))
painter.setBrush(Qt.BrushStyle.NoBrush)
painter.drawLine(tip, left)
if self.style.arrow_style == "open":
painter.drawLine(tip, right)
def _draw_causality_mark(
self, painter: QPainter, point: QPointF, direction: QPointF, *, warning: bool
) -> None:
length = max(0.001, (direction.x() ** 2 + direction.y() ** 2) ** 0.5)
normal = QPointF(-direction.y() / length, direction.x() / length)
half_length = 6.0
color = QColor("#c25a00") if warning else self.pen().color()
painter.setPen(QPen(color, max(2.0, self.pen().widthF()), Qt.PenStyle.SolidLine))
painter.drawLine(point - normal * half_length, point + normal * half_length)
def paint(self, painter: QPainter, option, widget=None) -> None:
super().paint(painter, option, widget)
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(self.pen().color())
if self.style.arrow_at_target and not self.target_is_junction:
painter.drawPolygon(self._arrow(self.end, self.end_direction, self.style.arrow_size))
self._draw_arrow(painter, self.end, self.end_direction)
if self.style.arrow_at_source and not self.source_is_junction:
painter.drawPolygon(
self._arrow(self.start, -self.start_direction, self.style.arrow_size)
self._draw_arrow(painter, self.start, -self.start_direction)
if self.causality in {"source", "warn_source"}:
self._draw_causality_mark(
painter,
self.start,
self.start_direction,
warning=self.causality == "warn_source",
)
elif self.causality in {"target", "warn_target"}:
self._draw_causality_mark(
painter,
self.end,
self.end_direction,
warning=self.causality == "warn_target",
)
@@ -882,12 +949,12 @@ class GraphScene(QGraphicsScene):
owner = self.controller.active_component
if owner is None or owner.implementation_kind != "graph":
return
for port in owner.inputs:
for port in (port for port in owner.ports if port.orientation != "output"):
item = InterfaceTerminalItem(port, "input", self.controller)
self.addItem(item)
item.setPos(port.x, port.y)
self.input_items[port.id] = item
for port in owner.outputs:
for port in (port for port in owner.ports if port.orientation == "output"):
item = InterfaceTerminalItem(port, "output", self.controller)
self.addItem(item)
item.setPos(port.x, port.y)
@@ -1207,9 +1274,23 @@ class GraphScene(QGraphicsScene):
def add_pairs(
source_item: ComponentGraphicsItem, target_item: ComponentGraphicsItem
) -> None:
for output in source_item.component.outputs:
for input_port in target_item.component.inputs:
if not PortTypeRegistry.compatible(output.type, input_port.type):
source_ports = [
port
for port in source_item.component.ports
if port.orientation in {"output", "indifferent"}
]
for output in source_ports:
for input_port in (
port
for port in target_item.component.ports
if port.orientation in {"input", "indifferent"}
):
if not PortTypeRegistry.compatible(
output.type,
input_port.type,
output.domain,
input_port.domain,
):
continue
source = Endpoint(block=source_item.component_id, port=output.id)
target = Endpoint(block=target_item.component_id, port=input_port.id)
@@ -1238,9 +1319,15 @@ class GraphScene(QGraphicsScene):
choices: list[ConnectionChoice] = []
interface = Endpoint(interface=terminal.port.id)
if terminal.direction == "input":
for port in component.component.inputs:
for port in (
port
for port in component.component.ports
if port.orientation in {"input", "indifferent"}
):
target = Endpoint(block=component.component_id, port=port.id)
if not PortTypeRegistry.compatible(terminal.port.type, port.type):
if not PortTypeRegistry.compatible(
terminal.port.type, port.type, terminal.port.domain, port.domain
):
continue
if not self.controller.endpoint_accepts_connection(interface, "source"):
continue
@@ -1254,9 +1341,16 @@ class GraphScene(QGraphicsScene):
)
)
else:
for port in component.component.outputs:
ports = [
port
for port in component.component.ports
if port.orientation in {"output", "indifferent"}
]
for port in ports:
source = Endpoint(block=component.component_id, port=port.id)
if not PortTypeRegistry.compatible(port.type, terminal.port.type):
if not PortTypeRegistry.compatible(
port.type, terminal.port.type, port.domain, terminal.port.domain
):
continue
if not self.controller.endpoint_accepts_connection(source, "source"):
continue
@@ -1278,7 +1372,11 @@ class GraphScene(QGraphicsScene):
) -> list[ConnectionChoice]:
choices: list[ConnectionChoice] = []
source = junction.endpoint
for port in component.component.inputs:
for port in (
port
for port in component.component.ports
if port.orientation in {"input", "indifferent"}
):
target = Endpoint(block=component.component_id, port=port.id)
if not PortTypeRegistry.compatible(junction.junction.type, port.type):
continue
@@ -1559,6 +1657,8 @@ class GraphScene(QGraphicsScene):
graphics = self.connection_items.get(connection_id)
if connection is None or graphics is None:
return
if connection.type == "power":
return
snapped = _snapped(position)
anchors = [graphics.start, *points, graphics.end]
@@ -1809,8 +1909,7 @@ class GraphWorkspaceView(QGraphicsView):
return
blocks: set[str] = set()
connections: set[str] = set()
inputs: set[str] = set()
outputs: set[str] = set()
ports: set[str] = set()
annotations: set[str] = set()
for item in self.scene().selectedItems():
if isinstance(item, ComponentGraphicsItem):
@@ -1818,10 +1917,10 @@ class GraphWorkspaceView(QGraphicsView):
elif isinstance(item, ConnectionGraphicsItem):
connections.add(item.connection_id)
elif isinstance(item, InterfaceTerminalItem):
(inputs if item.direction == "input" else outputs).add(item.port.id)
ports.add(item.port.id)
elif isinstance(item, (AnnotationGraphicsItem, LineAnnotationGraphicsItem)):
annotations.add(item.annotation_id)
self.controller.delete_selection(blocks, connections, inputs, outputs)
self.controller.delete_selection(blocks, connections, ports)
self.controller.delete_annotations(annotations)
def has_selected_components(self) -> bool:
@@ -1892,6 +1991,22 @@ class GraphWorkspaceView(QGraphicsView):
if self.controller is None:
return
mime_data = QApplication.clipboard().mimeData()
if mime_data.hasFormat(COMPONENT_MIME_TYPE):
try:
payload = json.loads(
bytes(mime_data.data(COMPONENT_MIME_TYPE)).decode("utf-8")
)
source = Component.from_dict(payload)
except (KeyError, TypeError, ValueError, json.JSONDecodeError):
return
component_id = self.controller.add_component_copy(source, QPointF(0, 0))
scene = self.scene()
if isinstance(scene, GraphScene):
scene.clearSelection()
item = scene.component_items.get(component_id)
if item is not None:
item.setSelected(True)
return
if not mime_data.hasFormat(SELECTION_MIME_TYPE):
return
try:

View File

@@ -1,10 +1,12 @@
import json
from copy import deepcopy
from pathlib import Path
from PySide6.QtCore import Qt, Slot
from PySide6.QtCore import QByteArray, QMimeData, Qt, Slot
from PySide6.QtGui import QCloseEvent
from PySide6.QtWidgets import (
QButtonGroup,
QApplication,
QFileDialog,
QMainWindow,
QMenu,
@@ -12,7 +14,7 @@ from PySide6.QtWidgets import (
QTabWidget,
)
from bedit.core.model import Component
from bedit.core.model import Component, Connection
from bedit.core.application_log import get_logger
from bedit.core.serializer import DocumentSerializer
from bedit.core.simulation import Simulation
@@ -22,16 +24,20 @@ from bedit.gui.dialogs.graph_parameters import GraphParametersDialog
from bedit.gui.dialogs.item_options import ItemOptionsDialog
from bedit.gui.models.library_repository import LibraryRepository
from bedit.gui.models.library_tree import (
COMPONENT_MIME_TYPE,
COMPONENT_ROLE,
COMPONENT_ID_ROLE,
COMPONENT_INSTANCE_ROLE,
ITEM_KIND_ROLE,
DocumentTreeModel,
LibraryTreeModel,
)
from bedit.gui.graphics.workspace import SELECTION_MIME_TYPE
from bedit.gui.dialogs.settings import SettingsDialog
from bedit.gui.dialogs.port_options import PortOptionsDialog
from bedit.gui.dialogs.parameter_options import ParameterOptionsDialog
from bedit.gui.dialogs.simulation_settings import SimulationSettingsDialog
from bedit.gui.simulation_window import SimulationWindow
from bedit.gui.preferences import application_settings
from bedit.gui.simulation_reload import reload_simulation
from bedit.gui.generated.ui_main_window import Ui_MainWindow
@@ -54,6 +60,9 @@ class MainWindow(QMainWindow):
self.log.info("BEdit started")
self.settings = application_settings()
self._applying_text_definition = False
# Keep a Python-owned top-level window. Giving it MainWindow as its Qt
# parent makes some Linux window managers inherit the BEdit window icon.
self._simulation_window = SimulationWindow()
self.libraries = LibraryRepository(self)
self.simulation = Simulation(
@@ -67,6 +76,7 @@ class MainWindow(QMainWindow):
)
self.document_tree_model = DocumentTreeModel(self.document_controller, self)
self._configure_models()
QApplication.clipboard().dataChanged.connect(self._update_edit_actions)
self._connect_actions()
self._populate_view_menu()
self._restore_window_geometry()
@@ -100,6 +110,10 @@ class MainWindow(QMainWindow):
self.ui.documentTreeView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.ui.documentTreeView.customContextMenuRequested.connect(self.show_library_context_menu)
self.ui.documentTreeView.doubleClicked.connect(self.activate_tree_component)
self.ui.documentTreeView.clicked.connect(
lambda _index: self._update_edit_actions()
)
self.ui.treeView.clicked.connect(lambda _index: self._update_edit_actions())
self.document_tree_model.rebuilt.connect(self.ui.documentTreeView.expandAll)
self.ui.graphView.set_model(self.document_controller)
self.ui.graphView.componentOptionsRequested.connect(self.show_component_options)
@@ -153,9 +167,9 @@ class MainWindow(QMainWindow):
self.ui.actionUndo.triggered.connect(self.document_controller.undo_stack.undo)
self.ui.actionRedo.triggered.connect(self.document_controller.undo_stack.redo)
self.ui.actionDelete.triggered.connect(self.ui.graphView.delete_selected)
self.ui.actionCopy.triggered.connect(self.ui.graphView.copy_selection)
self.ui.actionCut.triggered.connect(self.ui.graphView.cut_selection)
self.ui.actionPaste.triggered.connect(self.ui.graphView.paste_selection)
self.ui.actionCopy.triggered.connect(self.copy_selection)
self.ui.actionCut.triggered.connect(self.cut_selection)
self.ui.actionPaste.triggered.connect(self.paste_selection)
self.ui.actionSelectAll.triggered.connect(self.ui.graphView.select_all)
self.ui.actionRotateClockwise.triggered.connect(self.ui.graphView.rotate_selected)
self.addAction(self.ui.actionRotateClockwise)
@@ -166,7 +180,9 @@ class MainWindow(QMainWindow):
self.show_simulation_settings
)
self.ui.actionGraphParameters.triggered.connect(self.show_graph_parameters)
self.ui.actionCompile.triggered.connect(self.compile_active_graph)
self.ui.actionCompose.triggered.connect(self.compose_active_graph)
self.ui.actionExportModel.triggered.connect(self.export_model)
self.ui.actionSimulationWindow.triggered.connect(self.show_simulation_window)
self.ui.actionRunSimulation.triggered.connect(self.run_simulation)
self.document_controller.undo_stack.canUndoChanged.connect(self.ui.actionUndo.setEnabled)
self.document_controller.undo_stack.canRedoChanged.connect(self.ui.actionRedo.setEnabled)
@@ -239,20 +255,59 @@ class MainWindow(QMainWindow):
QMessageBox.warning(self, "Cannot change graph parameters", str(error))
@Slot()
def compile_active_graph(self) -> None:
def compose_active_graph(self) -> None:
try:
self.document_controller.compile_active_graph()
self.document_controller.compose_active_graph()
except ValueError as error:
self.log.error("Compile failed: %s", error)
QMessageBox.warning(self, "Cannot compile", str(error))
self.log.error("Composition failed: %s", error)
QMessageBox.warning(self, "Cannot compose", str(error))
@Slot()
def export_model(self) -> None:
try:
model_name, source = self.document_controller.compose_active_graph_source()
except ValueError as error:
self.log.error("Model export composition failed: %s", error)
QMessageBox.warning(self, "Cannot Export Model", str(error))
return
file_name, _selected_filter = QFileDialog.getSaveFileName(
self,
"Export OpenModelica Model",
f"{model_name}.mo",
"Modelica Models (*.mo);;All Files (*)",
)
if not file_name:
return
path = Path(file_name)
if not path.suffix:
path = path.with_suffix(".mo")
temporary_path = path.with_suffix(path.suffix + ".tmp")
try:
temporary_path.write_text(source, encoding="utf-8")
temporary_path.replace(path)
except OSError as error:
self.log.error("Could not export model %s: %s", path, error)
QMessageBox.critical(self, "Cannot Export Model", str(error))
return
self.log.info("Exported OpenModelica model to %s", path)
@Slot()
def show_simulation_window(self) -> None:
self._simulation_window.show()
self._simulation_window.raise_()
self._simulation_window.activateWindow()
@Slot()
def run_simulation(self) -> None:
window = self._simulation_window
callbacks = window.begin_run()
self.show_simulation_window()
try:
self.document_controller.run_simulation()
self.document_controller.run_simulation(*callbacks)
window.prepare_run_model(self.simulation.model_name)
except Exception as error:
self.log.exception("Simulation run failed")
QMessageBox.warning(self, "Cannot run simulation", str(error))
window.report_start_error(error)
def _restore_window_geometry(self) -> None:
geometry = self.settings.value("window/geometry")
@@ -281,7 +336,8 @@ class MainWindow(QMainWindow):
self._set_graph_controls_visible(False)
self.ui.actionSimulationSettings.setEnabled(False)
self.ui.actionGraphParameters.setEnabled(False)
self.ui.actionCompile.setEnabled(False)
self.ui.actionCompose.setEnabled(False)
self.ui.actionExportModel.setEnabled(False)
self.ui.actionRunSimulation.setEnabled(False)
self._update_edit_actions()
return
@@ -292,7 +348,8 @@ class MainWindow(QMainWindow):
is_graph = component.implementation_kind == "graph"
self.ui.actionSimulationSettings.setEnabled(is_graph)
self.ui.actionGraphParameters.setEnabled(is_graph)
self.ui.actionCompile.setEnabled(is_graph)
self.ui.actionCompose.setEnabled(is_graph)
self.ui.actionExportModel.setEnabled(is_graph)
self.ui.actionRunSimulation.setEnabled(is_graph)
self.ui.workspaceModeLabel.setText("Graph" if is_graph else "Text")
self.ui.workspaceStack.setCurrentWidget(
@@ -311,8 +368,24 @@ class MainWindow(QMainWindow):
has_selection = bool(
self.ui.graphView.scene() and self.ui.graphView.scene().selectedItems()
)
for action in (self.ui.actionCut, self.ui.actionCopy, self.ui.actionDelete):
action.setEnabled(is_graph and has_selection)
document_component_selected = bool(
self.ui.documentTreeView.currentIndex().data(COMPONENT_ID_ROLE)
)
library_component_selected = isinstance(
self.ui.treeView.currentIndex().data(COMPONENT_ROLE), dict
)
document_has_focus = self._view_has_focus(self.ui.documentTreeView)
library_has_focus = self._view_has_focus(self.ui.treeView)
self.ui.actionCopy.setEnabled(
(is_graph and has_selection)
or (document_has_focus and document_component_selected)
or (library_has_focus and library_component_selected)
)
self.ui.actionCut.setEnabled(
(is_graph and has_selection)
or (document_has_focus and document_component_selected)
)
self.ui.actionDelete.setEnabled(is_graph and has_selection)
self.ui.actionRotateClockwise.setEnabled(
is_graph and self.ui.graphView.has_selected_components()
)
@@ -320,11 +393,114 @@ class MainWindow(QMainWindow):
is_graph and self.ui.graphView.has_selected_components()
)
self.ui.actionSelectAll.setEnabled(is_graph)
self.ui.actionPaste.setEnabled(is_graph)
can_paste_component = QApplication.clipboard().mimeData().hasFormat(
COMPONENT_MIME_TYPE
) or QApplication.clipboard().mimeData().hasFormat(SELECTION_MIME_TYPE)
self.ui.actionPaste.setEnabled(
(is_graph and not document_has_focus and not library_has_focus)
or (
document_has_focus
and self.document_controller.document is not None
and can_paste_component
)
)
self.ui.navigateDownButton.setEnabled(
is_graph and self.ui.graphView.has_single_selected_component()
)
def copy_selection(self) -> bool:
if self._view_has_focus(self.ui.documentTreeView):
return self._copy_tree_component(self.ui.documentTreeView)
if self._view_has_focus(self.ui.treeView):
return self._copy_tree_component(self.ui.treeView)
return self.ui.graphView.copy_selection()
def cut_selection(self) -> None:
if self._view_has_focus(self.ui.documentTreeView):
index = self.ui.documentTreeView.currentIndex()
component_id = index.data(COMPONENT_ID_ROLE)
if component_id and self._copy_tree_component(self.ui.documentTreeView):
self.document_controller.delete_component(component_id)
return
if self._view_has_focus(self.ui.treeView):
self._copy_tree_component(self.ui.treeView)
return
self.ui.graphView.cut_selection()
def paste_selection(self) -> None:
if self._view_has_focus(self.ui.documentTreeView):
self._paste_into_document_tree()
return
self.ui.graphView.paste_selection()
def _copy_tree_component(self, tree) -> bool:
component = tree.currentIndex().data(COMPONENT_ROLE)
if not isinstance(component, dict):
return False
mime_data = QMimeData()
mime_data.setData(
COMPONENT_MIME_TYPE,
QByteArray(json.dumps(component).encode("utf-8")),
)
QApplication.clipboard().setMimeData(mime_data)
return True
def _paste_into_document_tree(self) -> None:
index = self.ui.documentTreeView.currentIndex()
kind = index.data(ITEM_KIND_ROLE)
owner_id = None
if kind == "current-component":
component_id = index.data(COMPONENT_ID_ROLE)
component = (
self.document_controller.document.find_component(component_id)
if self.document_controller.document
else None
)
if component is None or component.implementation_kind != "graph":
QMessageBox.warning(
self, "Cannot Paste", "Select a graph component or the document root."
)
return
owner_id = component.id
elif kind != "current-document":
QMessageBox.warning(
self, "Cannot Paste", "Select a graph component or the document root."
)
return
try:
components, connections = self._clipboard_components()
self.document_controller.paste_components_to(
owner_id, components, connections
)
except ValueError as error:
QMessageBox.warning(self, "Cannot Paste", str(error))
@staticmethod
def _clipboard_components() -> tuple[list[Component], list]:
mime_data = QApplication.clipboard().mimeData()
try:
if mime_data.hasFormat(SELECTION_MIME_TYPE):
payload = json.loads(
bytes(mime_data.data(SELECTION_MIME_TYPE)).decode("utf-8")
)
return (
[Component.from_dict(item) for item in payload.get("components", [])],
[Connection.from_dict(item) for item in payload.get("connections", [])],
)
if mime_data.hasFormat(COMPONENT_MIME_TYPE):
payload = json.loads(
bytes(mime_data.data(COMPONENT_MIME_TYPE)).decode("utf-8")
)
return [Component.from_dict(payload)], []
except (KeyError, TypeError, ValueError, json.JSONDecodeError) as error:
raise ValueError("The clipboard does not contain a valid component") from error
raise ValueError("The clipboard does not contain a BEdit component")
@staticmethod
def _view_has_focus(view) -> bool:
focus = QApplication.focusWidget()
return focus is view or (focus is not None and view.isAncestorOf(focus))
@Slot()
def navigate_up(self) -> None:
if self._resolve_source_edits():
@@ -361,9 +537,10 @@ class MainWindow(QMainWindow):
if component is None:
return
self.ui.textDefinitionEditor.set_definition(
component.source.get("declarations", ""),
component.source.get("initialEquations", ""),
component.source.get("equations", ""),
component.inputs,
component.outputs,
component.ports,
component.parameters,
)
@@ -378,7 +555,8 @@ class MainWindow(QMainWindow):
answer = QMessageBox.question(
self,
"Apply text component changes?",
"The text component has unapplied equation, port, or parameter changes.",
"The text component has unapplied declaration, initial-equation, "
"equation, port, or parameter changes.",
QMessageBox.StandardButton.Apply
| QMessageBox.StandardButton.Discard
| QMessageBox.StandardButton.Cancel,
@@ -390,12 +568,13 @@ class MainWindow(QMainWindow):
@Slot()
def apply_text_definition(self) -> bool:
try:
inputs, outputs = self.ui.textDefinitionEditor.ports
ports = self.ui.textDefinitionEditor.ports
self._applying_text_definition = True
try:
self.document_controller.replace_active_text_definition(
inputs,
outputs,
ports,
self.ui.textDefinitionEditor.declarations,
self.ui.textDefinitionEditor.initial_equations,
self.ui.textDefinitionEditor.equations,
self.ui.textDefinitionEditor.parameters,
)
@@ -462,12 +641,23 @@ class MainWindow(QMainWindow):
)
if not filename:
return
self.open_document_path(Path(filename), check_unsaved=False)
def open_document_path(self, path: Path, *, check_unsaved: bool = True) -> bool:
"""Open a document path selected by the UI or supplied at startup."""
if check_unsaved and (
not self._resolve_source_edits() or not self._maybe_save()
):
return False
try:
self.document_controller.load(Path(filename))
self.log.info("Opened document %s", filename)
self.document_controller.load(path)
self.log.info("Opened document %s", path)
except (OSError, ValueError) as error:
self.log.error("Could not open document %s: %s", filename, error)
self.log.error("Could not open document %s: %s", path, error)
QMessageBox.critical(self, "Could not open graph", str(error))
return False
return True
@Slot()
def save_document(self) -> bool:
@@ -528,6 +718,8 @@ class MainWindow(QMainWindow):
if scene is not None:
scene.update()
self.ui.graphView.viewport().update()
self.ui.textDefinitionEditor.ui.declarationsEdit.reload_highlighting()
self.ui.textDefinitionEditor.ui.initialEquationsEdit.reload_highlighting()
self.ui.textDefinitionEditor.ui.equationsEdit.reload_highlighting()
def _document_opened_changed(self, opened: bool) -> None:
@@ -547,6 +739,8 @@ class MainWindow(QMainWindow):
def show_library_context_menu(self, position) -> None:
tree_view = self.ui.documentTreeView
index = tree_view.indexAt(position)
if index.isValid():
tree_view.setCurrentIndex(index)
kind = index.data(ITEM_KIND_ROLE)
if kind == "current-component":
component_id = index.data(COMPONENT_ID_ROLE)
@@ -564,6 +758,11 @@ class MainWindow(QMainWindow):
options_action = menu.addAction("Component Options…")
ports_action = menu.addAction("Port Options…")
parameters_action = menu.addAction("Parameter Options…")
menu.addSeparator()
copy_action = menu.addAction("Copy")
cut_action = menu.addAction("Cut")
paste_action = menu.addAction("Paste")
paste_action.setEnabled(component.implementation_kind == "graph")
delete_action = menu.addAction("Delete")
selected = menu.exec(tree_view.viewport().mapToGlobal(position))
if graph_action is not None and selected is graph_action:
@@ -576,6 +775,13 @@ class MainWindow(QMainWindow):
self.show_component_port_options(component_id)
elif selected is parameters_action:
self.show_component_parameter_options(component_id)
elif selected is copy_action:
self._copy_tree_component(tree_view)
elif selected is cut_action:
if self._copy_tree_component(tree_view):
self.document_controller.delete_component(component_id)
elif selected is paste_action:
self._paste_into_document_tree()
elif selected is delete_action:
answer = QMessageBox.question(
self,
@@ -591,29 +797,38 @@ class MainWindow(QMainWindow):
menu = QMenu(self)
graph_action = menu.addAction("New Graph Block")
text_action = menu.addAction("New Text Block")
menu.addSeparator()
paste_action = menu.addAction("Paste")
selected = menu.exec(tree_view.viewport().mapToGlobal(position))
if selected is graph_action:
self.document_controller.add_root("graph")
elif selected is text_action:
self.document_controller.add_root("text")
elif selected is paste_action:
self._paste_into_document_tree()
@Slot(object)
def show_external_library_context_menu(self, position) -> None:
tree = self.ui.treeView
index = tree.indexAt(position)
if index.isValid():
tree.setCurrentIndex(index)
component = index.data(COMPONENT_INSTANCE_ROLE)
if not isinstance(component, Component):
return
menu = QMenu(self)
copy_action = menu.addAction("Copy")
menu.addSeparator()
ports_action = menu.addAction("Port Options…")
parameters_action = menu.addAction("Parameter Options…")
selected = menu.exec(tree.viewport().mapToGlobal(position))
if selected is ports_action:
if selected is copy_action:
self._copy_tree_component(tree)
elif selected is ports_action:
dialog = PortOptionsDialog(component, self)
if dialog.exec() == dialog.DialogCode.Accepted:
old_inputs, old_outputs = deepcopy(component.inputs), deepcopy(component.outputs)
component.inputs = dialog.inputs
component.outputs = dialog.outputs
old_ports = deepcopy(component.ports)
component.ports = dialog.ports
library = next(
(
library
@@ -627,7 +842,7 @@ class MainWindow(QMainWindow):
library.document.validate()
DocumentSerializer.save(library.document, Path(library.source_path))
except (OSError, ValueError) as error:
component.inputs, component.outputs = old_inputs, old_outputs
component.ports = old_ports
self.log.error("Could not change library ports: %s", error)
QMessageBox.warning(self, "Cannot change library ports", str(error))
self.library_tree_model.rebuild()
@@ -669,7 +884,7 @@ class MainWindow(QMainWindow):
return
try:
self.document_controller.edit_component_ports(
component_id, dialog.inputs, dialog.outputs
component_id, dialog.ports
)
except ValueError as error:
self.log.error("Could not change component ports: %s", error)
@@ -705,8 +920,7 @@ class MainWindow(QMainWindow):
component_id,
dialog.ui.nameEdit.text().strip(),
dialog.edited_icon,
dialog.edited_inputs,
dialog.edited_outputs,
dialog.edited_ports,
dialog.ui.showSubtreeCheckBox.isChecked(),
dialog.ui.showNameCheckBox.isChecked(),
)
@@ -719,8 +933,7 @@ class MainWindow(QMainWindow):
owner = self.document_controller.active_component
if owner is None:
return
ports = owner.inputs if direction == "input" else owner.outputs
port = next((item for item in ports if item.id == port_id), None)
port = next((item for item in owner.ports if item.id == port_id), None)
if port is None:
return
dialog = ItemOptionsDialog(f"{direction.title()} Options", port.name, self)
@@ -760,6 +973,8 @@ class MainWindow(QMainWindow):
event.ignore()
return
self.settings.setValue("window/geometry", self.saveGeometry())
self._simulation_window.close()
self.simulation.shutdown()
self.log.info("BEdit closed")
self.application_logger.removeHandler(self.log_handler)
event.accept()

View File

@@ -1,6 +1,6 @@
import json
from PySide6.QtCore import QByteArray, QMimeData, QModelIndex, Qt, Signal
from PySide6.QtCore import QByteArray, QMimeData, QModelIndex, QSignalBlocker, Qt, Signal
from PySide6.QtGui import QStandardItem, QStandardItemModel
from bedit.core.model import Component
@@ -80,8 +80,24 @@ class DocumentTreeModel(LibraryTreeModel):
controller.componentMoved.connect(lambda _component_id, _position: self.rebuild())
controller.connectionAdded.connect(lambda _connection_id: self.rebuild())
controller.connectionRemoved.connect(lambda _connection_id: self.rebuild())
controller.documentNameChanged.connect(self._document_name_changed)
self.itemChanged.connect(self._document_item_changed)
self.rebuild()
def _document_item_changed(self, item: QStandardItem) -> None:
if item.data(ITEM_KIND_ROLE) != "current-document":
return
try:
self.controller.rename_document(item.text())
except ValueError:
self.rebuild()
def _document_name_changed(self, name: str) -> None:
root = self.item(0)
if root is not None:
with QSignalBlocker(self):
root.setText(name)
def rebuild(self) -> None:
self.clear()
self.setHorizontalHeaderLabels(["Document"])

View File

@@ -20,6 +20,8 @@ def _saved_instance_state(instance) -> dict:
def reload_simulation(current):
"""Reload the simulation package and return a fresh state-preserving instance."""
saved_state = _saved_instance_state(current)
saved_state.pop("openmodelica", None)
current.shutdown(wait=True)
importlib.invalidate_caches()
package = importlib.import_module(SIMULATION_PACKAGE)
discovered: list[ModuleType] = []
@@ -30,4 +32,5 @@ def reload_simulation(current):
package = importlib.reload(package)
replacement = package.Simulation()
vars(replacement).update(saved_state)
replacement.openmodelica.configure(replacement.openmodelica_path)
return replacement

View File

@@ -0,0 +1,775 @@
import re
from pathlib import Path
from matplotlib.backend_bases import MouseButton
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg, NavigationToolbar2QT
from matplotlib.figure import Figure
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QCloseEvent
from PySide6.QtWidgets import (
QFileDialog,
QInputDialog,
QMainWindow,
QMenu,
QMessageBox,
QTreeWidgetItem,
QVBoxLayout,
QWidget,
)
from bedit.core.simulation.openmodelica import SimulationMessage, SimulationProgress
from bedit.core.simulation.results import (
SimulationExecutionResult,
SimulationGraph,
SimulationResults,
SimulationTrace,
load_simulation_results,
save_simulation_results,
)
from bedit.gui.generated.ui_simulation_window import Ui_SimulationWindow
from bedit.gui.preferences import application_settings
class SimulationWindow(QMainWindow):
"""Persistent viewer for live and previously saved simulation results."""
progressReceived = Signal(object)
messageReceived = Signal(object)
simulationFinished = Signal(object)
simulationFailed = Signal(str)
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.ui = Ui_SimulationWindow()
self.ui.setupUi(self)
self.settings = application_settings()
self._running = False
self._run_generation = 0
self._file_path: Path | None = None
self._rebuilding_graph_tabs = False
self._updating_signal_checks = False
self.results = SimulationResults()
self._connect_actions()
self._populate_view_menu()
self.splitDockWidget(
self.ui.statusDock, self.ui.logDock, Qt.Orientation.Vertical
)
self.ui.statusDock.setFixedHeight(self.ui.statusDock.sizeHint().height())
self._restore_window_layout()
self.progressReceived.connect(self._show_progress)
self.messageReceived.connect(self._show_message)
self.simulationFinished.connect(self._show_finished)
self.simulationFailed.connect(self._show_error)
def _connect_actions(self) -> None:
self.ui.actionOpen.triggered.connect(self.open_results)
self.ui.actionSave.triggered.connect(self.save_results)
self.ui.actionSaveAs.triggered.connect(self.save_results_as)
self.ui.actionClear.triggered.connect(self.clear)
self.ui.actionExit.triggered.connect(self.close)
self.ui.actionAbout.triggered.connect(self.show_about)
self.ui.actionAboutQt.triggered.connect(
lambda: QMessageBox.aboutQt(self, "About Qt Framework")
)
self.ui.actionToggleResults.toggled.connect(self.ui.centralWidget.setVisible)
self.ui.actionAddGraph.triggered.connect(self.add_graph_tab)
self.ui.actionRemoveGraph.triggered.connect(self.remove_current_graph_tab)
self.ui.graphTabs.tabBarDoubleClicked.connect(self.rename_graph_tab)
self.ui.graphTabs.tabBar().tabMoved.connect(self._move_graph_tab)
self.ui.graphTabs.currentChanged.connect(self._current_graph_changed)
self.ui.signalsTree.itemChanged.connect(self._signal_check_changed)
self.ui.signalsTree.customContextMenuRequested.connect(
self.show_signal_context_menu
)
def _populate_view_menu(self) -> None:
self.ui.menuPanels.addAction(self.ui.actionToggleResults)
for panel in (self.ui.statusDock, self.ui.logDock, self.ui.signalsDock):
self.ui.menuPanels.addAction(panel.toggleViewAction())
self.ui.menuToolbars.addAction(self.ui.fileToolbar.toggleViewAction())
self.ui.menuToolbars.addAction(self.ui.workspaceToolbar.toggleViewAction())
def _restore_window_layout(self) -> None:
geometry = self.settings.value("simulationWindow/geometry")
if geometry is not None:
self.restoreGeometry(geometry)
state = self.settings.value("simulationWindow/state")
if state is not None:
self.restoreState(state)
results_visible = self.settings.value(
"simulationWindow/resultsVisible", True, type=bool
)
self.ui.actionToggleResults.setChecked(results_visible)
def _save_window_layout(self) -> None:
self.settings.setValue("simulationWindow/geometry", self.saveGeometry())
self.settings.setValue("simulationWindow/state", self.saveState())
self.settings.setValue(
"simulationWindow/resultsVisible",
self.ui.actionToggleResults.isChecked(),
)
self.settings.sync()
def begin_run(self) -> tuple:
"""Reset transient run state while retaining the current workspace."""
self._run_generation += 1
generation = self._run_generation
self._running = True
self._file_path = None
self.results.status = {}
self.results.messages.clear()
self.results.metadata.clear()
self.ui.progressBar.setValue(0)
self.ui.timeLabel.setText("Time: 0 s")
self.ui.messageList.clear()
self.ui.statusLabel.setText("Preparing simulation…")
return (
lambda progress: self._report_progress(generation, progress),
lambda message: self._report_message(generation, message),
lambda result: self._report_finished(generation, result),
lambda error: self._report_error(generation, error),
)
def prepare_run_model(self, model_name: str | None) -> None:
"""Retain graph configuration only when rerunning the same model."""
if not model_name:
return
if self.results.model_name != model_name:
self.results = SimulationResults(model_name=model_name)
self.clear_result_views()
self.load_result_views()
else:
self.results.model_name = model_name
self.ui.statusLabel.setText("Preparing simulation…")
self._update_title()
def clear(self, checked: bool = False, *, model_name: str = "") -> None:
"""Discard the displayed run and prepare an empty results document."""
del checked
self._run_generation += 1
self._running = False
self._file_path = None
self.results = SimulationResults(model_name=model_name)
self.ui.statusLabel.setText("No simulation has been run yet.")
self.ui.progressBar.setValue(0)
self.ui.timeLabel.setText("Time: 0 s")
self.ui.messageList.clear()
self.clear_result_views()
self.load_result_views()
self._update_title()
def clear_result_views(self) -> None:
"""Clear custom plots before a run or loaded document is displayed.
Future graph widgets should be placed in the Designer-owned
``resultsLayout`` and reset here.
"""
self._rebuilding_graph_tabs = True
self._clear_graph_tab_widgets()
self._rebuilding_graph_tabs = False
self.ui.signalsTree.clear()
def load_result_views(self) -> None:
"""Populate custom plots from ``self.results.data`` and traces.
This is the intended integration point for a future plotting widget.
"""
self._rebuild_graph_tabs()
self._rebuild_signal_tree()
def _rebuild_graph_tabs(self) -> None:
current_page = self.ui.graphTabs.currentWidget()
current_graph_id = (
current_page.graph_id
if isinstance(current_page, GraphWorkspacePage)
else None
)
self._rebuilding_graph_tabs = True
self._clear_graph_tab_widgets()
for graph in self.results.graphs:
self.ui.graphTabs.addTab(
GraphWorkspacePage(graph, self.results), graph.title
)
if current_graph_id is not None:
for index, graph in enumerate(self.results.graphs):
if graph.id == current_graph_id:
self.ui.graphTabs.setCurrentIndex(index)
break
self._rebuilding_graph_tabs = False
self._update_graph_actions()
self._sync_signal_checks()
def _clear_graph_tab_widgets(self) -> None:
while self.ui.graphTabs.count():
page = self.ui.graphTabs.widget(0)
self.ui.graphTabs.removeTab(0)
page.deleteLater()
def add_graph_tab(self) -> None:
used_titles = {graph.title for graph in self.results.graphs}
number = 1
while f"Graph {number}" in used_titles:
number += 1
graph = SimulationGraph(title=f"Graph {number}")
self.results.graphs.append(graph)
index = self.ui.graphTabs.addTab(
GraphWorkspacePage(graph, self.results), graph.title
)
self.ui.graphTabs.setCurrentIndex(index)
self._update_graph_actions()
def remove_current_graph_tab(self) -> None:
index = self.ui.graphTabs.currentIndex()
if index < 0 or index >= len(self.results.graphs):
return
self.results.graphs.pop(index)
page = self.ui.graphTabs.widget(index)
self.ui.graphTabs.removeTab(index)
page.deleteLater()
self._update_graph_actions()
self._sync_signal_checks()
def rename_graph_tab(self, index: int) -> None:
if index < 0 or index >= len(self.results.graphs):
return
graph = self.results.graphs[index]
title, accepted = QInputDialog.getText(
self, "Rename Graph", "Title:", text=graph.title
)
title = title.strip()
if accepted and title:
graph.title = title
self.ui.graphTabs.setTabText(index, title)
page = self.ui.graphTabs.widget(index)
if isinstance(page, GraphWorkspacePage):
page.refresh_chart()
def _move_graph_tab(self, old_index: int, new_index: int) -> None:
if self._rebuilding_graph_tabs or old_index == new_index:
return
graph = self.results.graphs.pop(old_index)
self.results.graphs.insert(new_index, graph)
self._sync_signal_checks()
def _update_graph_actions(self) -> None:
self.ui.actionRemoveGraph.setEnabled(bool(self.results.graphs))
def _rebuild_signal_tree(self) -> None:
"""Build a hierarchy while retaining each leaf's complete signal name."""
tree = self.ui.signalsTree
self._updating_signal_checks = True
try:
tree.clear()
items: dict[tuple[str, ...], QTreeWidgetItem] = {}
for signal_name in sorted(self.results.data, key=str.casefold):
parts = _signal_tree_parts(signal_name)
if not parts:
continue
parent = tree.invisibleRootItem()
for depth, part in enumerate(parts, start=1):
path = parts[:depth]
item = items.get(path)
if item is None:
item = QTreeWidgetItem(parent, [part])
items[path] = item
parent = item
parent.setData(0, Qt.ItemDataRole.UserRole, signal_name)
metadata = self.results.signal_metadata.get(signal_name, {})
unit = metadata.get("unit", "")
if unit:
parent.setText(0, f"{parts[-1]} [{unit}]")
tooltip = signal_name
details = [
metadata.get("quantity", ""),
f"Unit: {unit}" if unit else "",
metadata.get("description", ""),
]
details = [detail for detail in details if detail]
if details:
tooltip += "\n" + "\n".join(details)
parent.setToolTip(0, tooltip)
parent.setFlags(parent.flags() | Qt.ItemFlag.ItemIsUserCheckable)
parent.setCheckState(0, Qt.CheckState.Unchecked)
tree.expandToDepth(0)
finally:
self._updating_signal_checks = False
self._sync_signal_checks()
def _current_graph_changed(self, _index: int) -> None:
if not self._rebuilding_graph_tabs:
self._sync_signal_checks()
def _current_graph(self) -> SimulationGraph | None:
index = self.ui.graphTabs.currentIndex()
if 0 <= index < len(self.results.graphs):
return self.results.graphs[index]
return None
def _sync_signal_checks(self) -> None:
graph = self._current_graph()
enabled = {trace.name for trace in graph.traces} if graph else set()
self._updating_signal_checks = True
try:
root = self.ui.signalsTree.invisibleRootItem()
pending = [root.child(index) for index in range(root.childCount())]
while pending:
item = pending.pop()
pending.extend(
item.child(index) for index in range(item.childCount())
)
signal_name = item.data(0, Qt.ItemDataRole.UserRole)
if isinstance(signal_name, str):
item.setCheckState(
0,
Qt.CheckState.Checked
if signal_name in enabled
else Qt.CheckState.Unchecked,
)
finally:
self._updating_signal_checks = False
self.ui.signalsTree.setEnabled(graph is not None)
def _signal_check_changed(self, item: QTreeWidgetItem, _column: int) -> None:
if self._updating_signal_checks:
return
signal_name = item.data(0, Qt.ItemDataRole.UserRole)
graph = self._current_graph()
if not isinstance(signal_name, str) or graph is None:
return
enabled = item.checkState(0) == Qt.CheckState.Checked
existing = next(
(trace for trace in graph.traces if trace.name == signal_name), None
)
if enabled and existing is None:
metadata = self.results.signal_metadata.get(signal_name, {})
graph.traces.append(
SimulationTrace(
name=signal_name,
y_label=metadata.get("quantity", ""),
unit=metadata.get("unit", ""),
)
)
elif not enabled and existing is not None:
graph.traces.remove(existing)
page = self.ui.graphTabs.currentWidget()
if isinstance(page, GraphWorkspacePage):
page.refresh_chart()
def selected_signal_names(self) -> list[str]:
"""Return full column names selected for future plotting actions."""
names = []
for item in self.ui.signalsTree.selectedItems():
name = item.data(0, Qt.ItemDataRole.UserRole)
if isinstance(name, str):
names.append(name)
return names
def show_signal_context_menu(self, position) -> None:
item = self.ui.signalsTree.itemAt(position)
signal_name = (
item.data(0, Qt.ItemDataRole.UserRole) if item is not None else None
)
graph = self._current_graph()
if not isinstance(signal_name, str) or graph is None:
return
menu = QMenu(self)
use_as_x_action = menu.addAction("Use as X Axis")
use_as_x_action.setCheckable(True)
use_as_x_action.setChecked(graph.x_axis == signal_name)
selected = menu.exec(
self.ui.signalsTree.viewport().mapToGlobal(position)
)
if selected is use_as_x_action:
self.set_x_axis_signal(signal_name)
def set_x_axis_signal(self, signal_name: str) -> None:
"""Set the current graph's persisted horizontal data column."""
graph = self._current_graph()
if graph is None or signal_name not in self.results.data:
return
graph.x_axis = signal_name
page = self.ui.graphTabs.currentWidget()
if isinstance(page, GraphWorkspacePage):
page.refresh_chart()
def open_results(self) -> None:
file_name, _selected_filter = QFileDialog.getOpenFileName(
self,
"Open Simulation Results",
"",
"BEdit Binary Simulation Results (*.ber);;JSON Simulation Results (*.json)",
)
if not file_name:
return
try:
results = load_simulation_results(file_name)
except ValueError as error:
QMessageBox.warning(self, "Cannot Open Simulation Results", str(error))
return
self._run_generation += 1
self._running = False
self._file_path = Path(file_name)
self.results = results
self._display_results()
def save_results(self) -> None:
file_path = self._file_path
if file_path is None:
self.save_results_as()
return
self._save_results_to(file_path)
def save_results_as(self) -> None:
default_name = f"{_safe_file_stem(self.results.model_name)}-results.ber"
file_name, selected_filter = QFileDialog.getSaveFileName(
self,
"Save Simulation Results As",
default_name,
"BEdit Binary Simulation Results (*.ber);;JSON Simulation Results (*.json)",
)
if not file_name:
return
file_path = Path(file_name)
if file_path.suffix.lower() not in {".ber", ".json"}:
suffix = ".json" if selected_filter.startswith("JSON") else ".ber"
file_path = file_path.with_suffix(suffix)
self._save_results_to(file_path)
def _save_results_to(self, file_path: Path) -> None:
try:
save_simulation_results(file_path, self.results)
except OSError as error:
QMessageBox.warning(self, "Cannot Save Simulation Results", str(error))
return
self._file_path = file_path
self._update_title()
def _display_results(self) -> None:
status = self.results.status
self.ui.statusLabel.setText(str(status.get("phase", "Loaded results")))
self.ui.progressBar.setValue(int(status.get("progress", 0)))
self.ui.timeLabel.setText(f"Time: {float(status.get('time', 0)):g} s")
self.ui.messageList.clear()
for message in self.results.messages:
prefix = message.get("stream") or message.get("type") or "OpenModelica"
self.ui.messageList.addItem(f"{prefix}: {message.get('text', '')}")
self.clear_result_views()
self.load_result_views()
self._update_title()
def _update_title(self) -> None:
name = self.results.model_name or "Simulation"
self.setWindowTitle(f"{name} — Simulation")
def _report_progress(self, generation: int, progress: SimulationProgress) -> None:
if generation == self._run_generation:
self.progressReceived.emit(progress)
def _report_message(self, generation: int, message: SimulationMessage) -> None:
if generation == self._run_generation:
self.messageReceived.emit(message)
def _report_finished(self, generation: int, result) -> None:
if generation == self._run_generation:
self.simulationFinished.emit(result)
def _report_error(self, generation: int, error: Exception) -> None:
if generation == self._run_generation:
self.simulationFailed.emit(str(error))
def report_start_error(self, error: Exception) -> None:
self.simulationFailed.emit(str(error))
def _show_progress(self, progress: SimulationProgress) -> None:
self.results.status = {
"phase": progress.phase,
"currentStepSize": progress.current_step_size,
"time": progress.time,
"progress": progress.progress,
}
self.ui.statusLabel.setText(progress.phase or "Running")
self.ui.timeLabel.setText(f"Time: {progress.time:g} s")
self.ui.progressBar.setValue(max(0, min(10000, progress.progress)))
def _show_message(self, message: SimulationMessage) -> None:
self.results.messages.append(
{"stream": message.stream, "type": message.type, "text": message.text}
)
prefix = message.stream or message.type or "OpenModelica"
self.ui.messageList.addItem(f"{prefix}: {message.text}")
self.ui.messageList.scrollToBottom()
def _show_finished(self, result) -> None:
self._running = False
self.results.status.update(phase="Simulation finished", progress=10000)
if isinstance(result, SimulationExecutionResult):
self.results.data = result.data
self.results.signal_metadata = result.signal_metadata
available_signals = set(result.data)
for graph in self.results.graphs:
graph.traces = [
trace
for trace in graph.traces
if trace.name in available_signals
]
for trace in graph.traces:
metadata = result.signal_metadata.get(trace.name, {})
trace.unit = metadata.get("unit", "")
trace.y_label = metadata.get("quantity", "")
if graph.x_axis not in available_signals:
graph.x_axis = (
"time"
if "time" in available_signals
else next(iter(result.data), "")
)
self.results.metadata["processReturnCode"] = result.return_code
self.results.metadata["sourceResultFile"] = Path(result.result_file).name
else:
self.results.metadata["processResult"] = result
self.ui.progressBar.setValue(10000)
self.ui.statusLabel.setText("Simulation finished")
self.load_result_views()
def _show_error(self, message: str) -> None:
self._running = False
self.results.status["phase"] = "Simulation failed"
self.results.messages.append(
{"stream": "BEdit", "type": "error", "text": message}
)
self.ui.statusLabel.setText("Simulation failed")
self.ui.messageList.addItem(f"Error: {message}")
def show_about(self) -> None:
QMessageBox.about(
self,
"About BEdit Simulation",
"<h3>BEdit Simulation</h3>"
"<p>View live progress and open or save simulation results.</p>",
)
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802 (Qt API name)
self._save_window_layout()
super().closeEvent(event)
@property
def is_running(self) -> bool:
return self._running
class GraphWorkspacePage(QWidget):
"""Matplotlib view of one persisted simulation graph definition."""
def __init__(
self, graph: SimulationGraph, results: SimulationResults, parent=None
) -> None:
super().__init__(parent)
self.graph_id = graph.id
self.graph = graph
self.results = results
self.plot_layout = QVBoxLayout(self)
self.figure = Figure(layout="constrained")
self.canvas = InteractiveFigureCanvas(self.figure)
self.axes = self.figure.add_subplot(111)
self.canvas.axes = self.axes
self.navigation_toolbar = GraphNavigationToolbar(self.canvas, self)
self.canvas.navigation_toolbar = self.navigation_toolbar
self.plot_layout.addWidget(self.navigation_toolbar)
self.plot_layout.addWidget(self.canvas)
self.refresh_chart()
def refresh_chart(self) -> None:
self.axes.clear()
x_values = self.results.data.get(self.graph.x_axis)
for trace in self.graph.traces:
y_values = self.results.data.get(trace.name)
if y_values is None:
continue
horizontal = x_values if x_values is not None else range(len(y_values))
sample_count = min(len(horizontal), len(y_values))
color = trace.properties.get("color")
unit = trace.unit or self.results.signal_metadata.get(
trace.name, {}
).get("unit", "")
label = f"{trace.name} [{unit}]" if unit else trace.name
self.axes.plot(
list(horizontal)[:sample_count],
y_values[:sample_count],
label=label,
color=color if isinstance(color, str) and color else None,
)
self.axes.set_title(self.graph.title)
if x_values is not None:
x_unit = self.results.signal_metadata.get(
self.graph.x_axis, {}
).get("unit", "")
x_label = (
f"{self.graph.x_axis} [{x_unit}]"
if x_unit
else self.graph.x_axis
)
else:
x_label = "sample"
self.axes.set_xlabel(x_label)
units = {
trace.unit
or self.results.signal_metadata.get(trace.name, {}).get("unit", "")
for trace in self.graph.traces
if self.results.data.get(trace.name) is not None
}
units.discard("")
if len(units) == 1:
self.axes.set_ylabel(next(iter(units)))
self.axes.grid(True, alpha=0.25)
if self.axes.lines:
self.axes.legend()
self.axes.relim()
self.axes.autoscale_view()
self.canvas.set_home_view()
self.canvas.draw_idle()
class GraphNavigationToolbar(NavigationToolbar2QT):
"""Navigation toolbar whose Home action includes direct canvas navigation."""
def home(self, *args) -> None:
del args
self.canvas.reset_home_view()
class InteractiveFigureCanvas(FigureCanvasQTAgg):
"""Matplotlib canvas with always-available wheel zoom and drag pan."""
def __init__(self, figure: Figure) -> None:
super().__init__(figure)
self.axes = None
self.navigation_toolbar = None
self._pan_start = None
self._home_view = None
self.mpl_connect("scroll_event", self._zoom_at_cursor)
self.mpl_connect("button_press_event", self._start_pan)
self.mpl_connect("motion_notify_event", self._pan)
self.mpl_connect("button_release_event", self._finish_pan)
def _toolbar_is_active(self) -> bool:
return bool(
self.navigation_toolbar is not None
and self.navigation_toolbar.mode
)
def set_home_view(self) -> None:
if self.axes is not None:
self._home_view = (self.axes.get_xlim(), self.axes.get_ylim())
def reset_home_view(self) -> None:
if self.axes is None or self._home_view is None:
return
x_limits, y_limits = self._home_view
self.axes.set_xlim(x_limits)
self.axes.set_ylim(y_limits)
self.draw_idle()
def _zoom_at_cursor(self, event) -> None:
if (
self.axes is None
or event.inaxes is not self.axes
or event.xdata is None
or event.ydata is None
or self._toolbar_is_active()
):
return
scale = 0.8 if event.button == "up" else 1.25
left, right = self.axes.get_xlim()
bottom, top = self.axes.get_ylim()
self.axes.set_xlim(
event.xdata - (event.xdata - left) * scale,
event.xdata + (right - event.xdata) * scale,
)
self.axes.set_ylim(
event.ydata - (event.ydata - bottom) * scale,
event.ydata + (top - event.ydata) * scale,
)
self.draw_idle()
def _start_pan(self, event) -> None:
if (
self.axes is None
or event.inaxes is not self.axes
or event.button != MouseButton.LEFT
or self._toolbar_is_active()
):
return
self._pan_start = (
event.x,
event.y,
self.axes.get_xlim(),
self.axes.get_ylim(),
)
def _pan(self, event) -> None:
if (
self.axes is None
or self._pan_start is None
or event.x is None
or event.y is None
):
return
start_x, start_y, x_limits, y_limits = self._pan_start
width = max(self.axes.bbox.width, 1.0)
height = max(self.axes.bbox.height, 1.0)
delta_x = (event.x - start_x) * (x_limits[1] - x_limits[0]) / width
delta_y = (event.y - start_y) * (y_limits[1] - y_limits[0]) / height
self.axes.set_xlim(x_limits[0] - delta_x, x_limits[1] - delta_x)
self.axes.set_ylim(y_limits[0] - delta_y, y_limits[1] - delta_y)
self.draw_idle()
def _finish_pan(self, _event) -> None:
self._pan_start = None
def _safe_file_stem(model_name: str) -> str:
stem = re.sub(r"[^A-Za-z0-9_.-]+", "_", model_name).strip("._")
return stem or "simulation"
def _signal_tree_parts(signal_name: str) -> tuple[str, ...]:
"""Split a result name into readable component, variable, and index levels.
OpenModelica emits state derivatives as names such as ``der(block.x)``.
Keep those traces available, but display them below a ``Derivatives`` group
on their owning component instead of creating a misleading top-level
``der(block`` branch.
"""
derivative = re.fullmatch(r"der\((.+)\)", signal_name)
if derivative is not None:
inner_name = derivative.group(1)
inner_parts = _signal_tree_parts(inner_name)
if inner_parts:
index_count = len(re.findall(r"\[[^\]]+\]", inner_name.rsplit(".", 1)[-1]))
variable_index = max(0, len(inner_parts) - index_count - 1)
return (
*inner_parts[:variable_index],
"Derivatives",
*inner_parts[variable_index:],
)
return ("Derivatives", signal_name)
parts: list[str] = []
for segment in signal_name.split("."):
if not segment:
continue
match = re.fullmatch(r"([^\[]+)((?:\[[^\]]+\])+)", segment)
if match is None:
parts.append(segment)
continue
parts.append(match.group(1))
parts.extend(re.findall(r"\[([^\]]+)\]", match.group(2)))
return tuple(parts)

View File

@@ -551,7 +551,9 @@
</property>
<addaction name="actionSimulationSettings"/>
<addaction name="actionGraphParameters"/>
<addaction name="actionCompile"/>
<addaction name="actionCompose"/>
<addaction name="actionExportModel"/>
<addaction name="actionSimulationWindow"/>
<addaction name="actionRunSimulation"/>
</widget>
<addaction name="menuFile"/>
@@ -644,7 +646,8 @@
</attribute>
<addaction name="actionSimulationSettings"/>
<addaction name="actionGraphParameters"/>
<addaction name="actionCompile"/>
<addaction name="actionCompose"/>
<addaction name="actionSimulationWindow"/>
<addaction name="actionRunSimulation"/>
</widget>
<action name="actionSimulationSettings">
@@ -662,7 +665,7 @@
<action name="actionGraphParameters">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/configure.png</normaloff>:/icons/icons/configure.png</iconset>
<normaloff>:/icons/icons/view-form-table.png</normaloff>:/icons/icons/view-form-table.png</iconset>
</property>
<property name="text">
<string>Graph Parameters</string>
@@ -671,16 +674,16 @@
<string>Edit parameters throughout the active graph</string>
</property>
</action>
<action name="actionCompile">
<action name="actionCompose">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/run-build.png</normaloff>:/icons/icons/run-build.png</iconset>
</property>
<property name="text">
<string>Compile</string>
<string>Compose</string>
</property>
<property name="statusTip">
<string>Compile the active graph for simulation</string>
<string>Compose the active graph as an OpenModelica model</string>
</property>
<property name="shortcut">
<string>F5</string>
@@ -701,6 +704,30 @@
<string>F6</string>
</property>
</action>
<action name="actionSimulationWindow">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/office-chart-line.png</normaloff>:/icons/icons/office-chart-line.png</iconset>
</property>
<property name="text">
<string>Simulation Window</string>
</property>
<property name="statusTip">
<string>Show the simulation results window</string>
</property>
</action>
<action name="actionExportModel">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/document-save-as.png</normaloff>:/icons/icons/document-save-as.png</iconset>
</property>
<property name="text">
<string>Export Model…</string>
</property>
<property name="statusTip">
<string>Save the composed OpenModelica model to a file</string>
</property>
</action>
<action name="actionNew">
<property name="icon">
<iconset resource="../resources/resources.qrc">

View File

@@ -2,13 +2,222 @@
<ui version="4.0">
<class>ParameterOptionsDialog</class>
<widget class="QDialog" name="ParameterOptionsDialog">
<property name="geometry"><rect><x>0</x><y>0</y><width>620</width><height>380</height></rect></property>
<property name="windowTitle"><string>Parameter Options</string></property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>720</width>
<height>328</height>
</rect>
</property>
<property name="windowTitle">
<string>Parameter Options</string>
</property>
<layout class="QVBoxLayout" name="dialogLayout">
<item><widget class="QSplitter" name="parameterSplitter"><property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property><widget class="QWidget" name="parameterListPanel"><layout class="QVBoxLayout" name="parameterListLayout"><item><widget class="QListWidget" name="parameterList"/></item><item><layout class="QHBoxLayout" name="parameterButtonsLayout"><item><widget class="QPushButton" name="addParameterButton"><property name="text"><string>Add Parameter</string></property></widget></item><item><widget class="QPushButton" name="removeParameterButton"><property name="text"><string>Remove Parameter</string></property></widget></item></layout></item></layout></widget><widget class="QWidget" name="parameterDetailsPanel"><layout class="QFormLayout" name="parameterDetailsForm"><item row="0" column="0"><widget class="QLabel" name="nameLabel"><property name="text"><string>Name:</string></property></widget></item><item row="0" column="1"><widget class="QLineEdit" name="nameEdit"/></item><item row="1" column="0"><widget class="QLabel" name="typeLabel"><property name="text"><string>Type:</string></property></widget></item><item row="1" column="1"><widget class="QLineEdit" name="typeEdit"/></item><item row="2" column="0"><widget class="QLabel" name="valueLabel"><property name="text"><string>Value:</string></property></widget></item><item row="2" column="1"><widget class="QLineEdit" name="valueEdit"/></item></layout></widget></widget></item>
<item><widget class="QDialogButtonBox" name="buttonBox"><property name="standardButtons"><set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set></property></widget></item>
<item>
<widget class="QSplitter" name="parameterSplitter">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<widget class="QWidget" name="parameterListPanel">
<layout class="QVBoxLayout" name="parameterListLayout">
<item>
<widget class="QListWidget" name="parameterList"/>
</item>
<item>
<layout class="QHBoxLayout" name="parameterButtonsLayout">
<item>
<widget class="QPushButton" name="addParameterButton">
<property name="text">
<string>Add Parameter</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeParameterButton">
<property name="text">
<string>Remove Parameter</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="parameterDetailsPanel">
<layout class="QFormLayout" name="parameterDetailsForm">
<property name="verticalSpacing">
<number>3</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="nameLabel">
<property name="text">
<string>Name:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="nameEdit"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="typeLabel">
<property name="text">
<string>Type:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="typeCombo">
<property name="editable">
<bool>true</bool>
</property>
<item>
<property name="text">
<string>real</string>
</property>
</item>
<item>
<property name="text">
<string>integer</string>
</property>
</item>
<item>
<property name="text">
<string>boolean</string>
</property>
</item>
<item>
<property name="text">
<string>string</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="columnsLabel">
<property name="text">
<string>Matrix size:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QSpinBox" name="columnsSpin">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>9999</number>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="rowsSpin">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>9999</number>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="valueEdit"/>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="quantityCombo">
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QComboBox" name="unitCombo">
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QPlainTextEdit" name="descriptionEdit"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="valueLabel">
<property name="text">
<string>Value:</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="quantityLabel">
<property name="text">
<string>Quantity:</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="unitLabel">
<property name="text">
<string>Unit:</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="descriptionLabel">
<property name="text">
<string>Description:</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections><connection><sender>buttonBox</sender><signal>accepted()</signal><receiver>ParameterOptionsDialog</receiver><slot>accept()</slot><hints/></connection><connection><sender>buttonBox</sender><signal>rejected()</signal><receiver>ParameterOptionsDialog</receiver><slot>reject()</slot><hints/></connection></connections>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>ParameterOptionsDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>ParameterOptionsDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -2,43 +2,307 @@
<ui version="4.0">
<class>PortOptionsDialog</class>
<widget class="QDialog" name="PortOptionsDialog">
<property name="geometry"><rect><x>0</x><y>0</y><width>620</width><height>380</height></rect></property>
<property name="windowTitle"><string>Port Options</string></property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>760</width>
<height>421</height>
</rect>
</property>
<property name="windowTitle">
<string>Port Options</string>
</property>
<layout class="QVBoxLayout" name="dialogLayout">
<item>
<widget class="QSplitter" name="portSplitter">
<property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<widget class="QWidget" name="portListPanel">
<layout class="QVBoxLayout" name="portListLayout">
<item><widget class="QListWidget" name="portList"/></item>
<item>
<widget class="QListWidget" name="portList"/>
</item>
<item>
<layout class="QHBoxLayout" name="portButtonsLayout">
<item><widget class="QPushButton" name="addPortButton"><property name="text"><string>Add Port</string></property></widget></item>
<item><widget class="QPushButton" name="removePortButton"><property name="text"><string>Remove Port</string></property></widget></item>
<item>
<widget class="QPushButton" name="addPortButton">
<property name="text">
<string>Add Port</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removePortButton">
<property name="text">
<string>Remove Port</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="portDetailsPanel">
<layout class="QFormLayout" name="portDetailsForm">
<item row="0" column="0"><widget class="QLabel" name="nameLabel"><property name="text"><string>Name:</string></property></widget></item>
<item row="0" column="1"><widget class="QLineEdit" name="nameEdit"/></item>
<item row="1" column="0"><widget class="QLabel" name="typeLabel"><property name="text"><string>Type:</string></property></widget></item>
<item row="1" column="1"><widget class="QComboBox" name="typeCombo"><item><property name="text"><string>Signal</string></property></item></widget></item>
<item row="2" column="0"><widget class="QLabel" name="orientationLabel"><property name="text"><string>Orientation:</string></property></widget></item>
<item row="2" column="1"><widget class="QComboBox" name="orientationCombo"><item><property name="text"><string>Input</string></property></item><item><property name="text"><string>Output</string></property></item></widget></item>
<item row="3" column="0" colspan="2"><widget class="QCheckBox" name="multipleConnectionsCheckBox"><property name="text"><string>Allow multiple connections</string></property></widget></item>
<item row="4" column="0" colspan="2"><widget class="QLabel" name="positionHintLabel"><property name="text"><string>New ports start at (0, 0) in the icon editor.</string></property><property name="wordWrap"><bool>true</bool></property></widget></item>
<property name="verticalSpacing">
<number>3</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="nameLabel">
<property name="text">
<string>Name:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="nameEdit"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="typeLabel">
<property name="text">
<string>Port type:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="typeCombo">
<item>
<property name="text">
<string>Signal</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="orientationLabel">
<property name="text">
<string>Orientation:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="orientationCombo">
<item>
<property name="text">
<string>Input</string>
</property>
</item>
<item>
<property name="text">
<string>Output</string>
</property>
</item>
<item>
<property name="text">
<string>Indifferent</string>
</property>
</item>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="multipleConnectionsCheckBox">
<property name="text">
<string>Allow multiple connections</string>
</property>
</widget>
</item>
<item row="5" column="0" colspan="2">
<widget class="QLabel" name="positionHintLabel">
<property name="text">
<string>New ports start at (0, 0) in the icon editor.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="4" column="0" colspan="2">
<widget class="QStackedWidget" name="typeOptionsStack">
<widget class="QWidget" name="signalOptionsPage">
<layout class="QFormLayout" name="signalOptionsForm">
<item row="0" column="0">
<widget class="QLabel" name="valueTypeLabel">
<property name="text">
<string>Type:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="valueTypeCombo">
<property name="editable">
<bool>true</bool>
</property>
<item>
<property name="text">
<string>real</string>
</property>
</item>
<item>
<property name="text">
<string>integer</string>
</property>
</item>
<item>
<property name="text">
<string>boolean</string>
</property>
</item>
<item>
<property name="text">
<string>string</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="quantityLabel">
<property name="text">
<string>Quantity:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="quantityCombo">
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="unitCombo">
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="unitLabel">
<property name="text">
<string>Unit:</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="rowsColumnsLabel">
<property name="text">
<string>Matrix size:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QSpinBox" name="rowsSpin">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>9999</number>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="columnsSpin">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>9999</number>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="0">
<widget class="QLabel" name="descriptionLabel">
<property name="text">
<string>Description:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QPlainTextEdit" name="descriptionEdit"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="powerOptionsPage">
<layout class="QFormLayout" name="powerOptionsForm">
<item row="0" column="0"><widget class="QLabel" name="domainLabel"><property name="text"><string>Domain:</string></property></widget></item>
<item row="0" column="1"><widget class="QComboBox" name="domainCombo"/></item>
<item row="1" column="0"><widget class="QLabel" name="effortLabel"><property name="text"><string>Effort:</string></property></widget></item>
<item row="1" column="1"><widget class="QLabel" name="effortValueLabel"><property name="text"><string>p.e</string></property></widget></item>
<item row="2" column="0"><widget class="QLabel" name="flowLabel"><property name="text"><string>Flow:</string></property></widget></item>
<item row="2" column="1"><widget class="QLabel" name="flowValueLabel"><property name="text"><string>p.f</string></property></widget></item>
<item row="3" column="0"><widget class="QLabel" name="causalityLabel"><property name="text"><string>Causality:</string></property></widget></item>
<item row="3" column="1"><widget class="QComboBox" name="causalityCombo"/></item>
<item row="4" column="0"><widget class="QLabel" name="powerDescriptionLabel"><property name="text"><string>Description:</string></property></widget></item>
<item row="4" column="1"><widget class="QPlainTextEdit" name="powerDescriptionEdit"/></item>
</layout>
</widget>
<widget class="QWidget" name="unsupportedTypePage">
<layout class="QVBoxLayout" name="unsupportedTypeLayout">
<item>
<widget class="QLabel" name="unsupportedTypeLabel">
<property name="text">
<string>This port type does not have an options editor yet.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item><widget class="QDialogButtonBox" name="buttonBox"><property name="standardButtons"><set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set></property></widget></item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection><sender>buttonBox</sender><signal>accepted()</signal><receiver>PortOptionsDialog</receiver><slot>accept()</slot><hints/></connection>
<connection><sender>buttonBox</sender><signal>rejected()</signal><receiver>PortOptionsDialog</receiver><slot>reject()</slot><hints/></connection>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>PortOptionsDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>PortOptionsDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -86,6 +86,55 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="bondGraphTab">
<attribute name="title">
<string>Bond graph</string>
</attribute>
<layout class="QVBoxLayout" name="bondGraphLayout">
<item>
<widget class="QGroupBox" name="causalityGroupBox">
<property name="title">
<string>Causality</string>
</property>
<layout class="QVBoxLayout" name="causalityLayout">
<item>
<widget class="QCheckBox" name="automaticCausalityCheckBox">
<property name="text">
<string>Infer causality when connections are added or removed</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="automaticCausalityHintLabel">
<property name="text">
<string>When disabled, displayed causalities are updated only when compiling, exporting, or running the model.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="bondGraphSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="simulationTab">
<attribute name="title"><string>Simulation</string></attribute>
<layout class="QVBoxLayout" name="simulationTabLayout">

View File

@@ -0,0 +1,322 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SimulationWindow</class>
<widget class="QMainWindow" name="SimulationWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>900</width>
<height>650</height>
</rect>
</property>
<property name="windowTitle">
<string>Simulation</string>
</property>
<property name="windowIcon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/office-chart-line.png</normaloff>:/icons/icons/office-chart-line.png</iconset>
</property>
<widget class="QWidget" name="centralWidget">
<layout class="QVBoxLayout" name="resultsLayout">
<item>
<widget class="QTabWidget" name="graphTabs">
<property name="tabsClosable">
<bool>false</bool>
</property>
<property name="movable">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menuBar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>900</width>
<height>24</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
<property name="title">
<string>&amp;File</string>
</property>
<addaction name="actionOpen"/>
<addaction name="actionSave"/>
<addaction name="actionSaveAs"/>
<addaction name="separator"/>
<addaction name="actionClear"/>
<addaction name="separator"/>
<addaction name="actionExit"/>
</widget>
<widget class="QMenu" name="menuView">
<property name="title">
<string>&amp;View</string>
</property>
<widget class="QMenu" name="menuPanels">
<property name="title">
<string>&amp;Panels</string>
</property>
</widget>
<widget class="QMenu" name="menuToolbars">
<property name="title">
<string>&amp;Toolbars</string>
</property>
</widget>
<addaction name="menuPanels"/>
<addaction name="menuToolbars"/>
</widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
<string>&amp;Help</string>
</property>
<addaction name="actionAbout"/>
<addaction name="actionAboutQt"/>
</widget>
<widget class="QMenu" name="menuGraph">
<property name="title">
<string>&amp;Graph</string>
</property>
<addaction name="actionAddGraph"/>
<addaction name="actionRemoveGraph"/>
</widget>
<addaction name="menuFile"/>
<addaction name="menuView"/>
<addaction name="menuGraph"/>
<addaction name="menuHelp"/>
</widget>
<widget class="QToolBar" name="workspaceToolbar">
<property name="windowTitle">
<string>Workspace</string>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonStyle::ToolButtonIconOnly</enum>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionAddGraph"/>
<addaction name="actionRemoveGraph"/>
</widget>
<widget class="QToolBar" name="fileToolbar">
<property name="windowTitle">
<string>File</string>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonStyle::ToolButtonIconOnly</enum>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionClear"/>
<addaction name="actionOpen"/>
<addaction name="actionSave"/>
<addaction name="actionSaveAs"/>
</widget>
<widget class="QDockWidget" name="statusDock">
<property name="windowTitle">
<string>Status</string>
</property>
<attribute name="dockWidgetArea">
<number>8</number>
</attribute>
<widget class="QWidget" name="statusDockContents">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="timeLabel">
<property name="text">
<string>Time: 0 s</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="maximum">
<number>10000</number>
</property>
<property name="value">
<number>0</number>
</property>
<property name="format">
<string>%p%</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="statusLabel">
<property name="text">
<string>No simulation has been run yet.</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<widget class="QDockWidget" name="logDock">
<property name="floating">
<bool>false</bool>
</property>
<property name="features">
<set>QDockWidget::DockWidgetFeature::DockWidgetFloatable|QDockWidget::DockWidgetFeature::DockWidgetMovable</set>
</property>
<property name="windowTitle">
<string>Log</string>
</property>
<attribute name="dockWidgetArea">
<number>8</number>
</attribute>
<widget class="QWidget" name="logDockContents">
<layout class="QVBoxLayout" name="logLayout">
<item>
<widget class="QListWidget" name="messageList">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<widget class="QDockWidget" name="signalsDock">
<property name="windowTitle">
<string>Signals</string>
</property>
<attribute name="dockWidgetArea">
<number>2</number>
</attribute>
<widget class="QWidget" name="signalsDockContents">
<layout class="QVBoxLayout" name="signalsLayout">
<item>
<widget class="QTreeWidget" name="signalsTree">
<property name="contextMenuPolicy">
<enum>Qt::ContextMenuPolicy::CustomContextMenu</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SelectionMode::ExtendedSelection</enum>
</property>
<property name="headerHidden">
<bool>true</bool>
</property>
<column>
<property name="text">
<string>Signal</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</widget>
<action name="actionOpen">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/document-open.png</normaloff>:/icons/icons/document-open.png</iconset>
</property>
<property name="text">
<string>&amp;Open…</string>
</property>
<property name="shortcut">
<string>Ctrl+O</string>
</property>
</action>
<action name="actionSave">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/document-save.png</normaloff>:/icons/icons/document-save.png</iconset>
</property>
<property name="text">
<string>&amp;Save…</string>
</property>
<property name="shortcut">
<string>Ctrl+S</string>
</property>
</action>
<action name="actionClear">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/document-new.png</normaloff>:/icons/icons/document-new.png</iconset>
</property>
<property name="text">
<string>&amp;Clear</string>
</property>
</action>
<action name="actionSaveAs">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/document-save-as.png</normaloff>:/icons/icons/document-save-as.png</iconset>
</property>
<property name="text">
<string>Save &amp;As…</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+S</string>
</property>
</action>
<action name="actionExit">
<property name="text">
<string>E&amp;xit</string>
</property>
<property name="shortcut">
<string>Ctrl+W</string>
</property>
</action>
<action name="actionAbout">
<property name="text">
<string>&amp;About Simulation Window</string>
</property>
</action>
<action name="actionAboutQt">
<property name="text">
<string>About &amp;Qt</string>
</property>
</action>
<action name="actionToggleResults">
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="text">
<string>Results</string>
</property>
</action>
<action name="actionAddGraph">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/list-add.png</normaloff>:/icons/icons/list-add.png</iconset>
</property>
<property name="text">
<string>Add Graph</string>
</property>
<property name="statusTip">
<string>Add a graph workspace tab</string>
</property>
</action>
<action name="actionRemoveGraph">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/list-remove.png</normaloff>:/icons/icons/list-remove.png</iconset>
</property>
<property name="text">
<string>Remove Current Graph</string>
</property>
<property name="statusTip">
<string>Remove the current graph workspace tab</string>
</property>
</action>
</widget>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -12,11 +12,27 @@
<widget class="QSplitter" name="columnSplitter">
<property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property>
<property name="childrenCollapsible"><bool>false</bool></property>
<widget class="QGroupBox" name="equationsGroup">
<property name="title"><string>Equations</string></property>
<layout class="QVBoxLayout" name="equationsLayout">
<item><widget class="OpenModelicaEditor" name="equationsEdit"/></item>
</layout>
<widget class="QSplitter" name="sourceSplitter">
<property name="orientation"><enum>Qt::Orientation::Vertical</enum></property>
<property name="childrenCollapsible"><bool>false</bool></property>
<widget class="QGroupBox" name="declarationsGroup">
<property name="title"><string>Declarations</string></property>
<layout class="QVBoxLayout" name="declarationsLayout">
<item><widget class="OpenModelicaEditor" name="declarationsEdit"/></item>
</layout>
</widget>
<widget class="QGroupBox" name="initialEquationsGroup">
<property name="title"><string>Initial Equations</string></property>
<layout class="QVBoxLayout" name="initialEquationsLayout">
<item><widget class="OpenModelicaEditor" name="initialEquationsEdit"/></item>
</layout>
</widget>
<widget class="QGroupBox" name="equationsGroup">
<property name="title"><string>Equations</string></property>
<layout class="QVBoxLayout" name="equationsLayout">
<item><widget class="OpenModelicaEditor" name="equationsEdit"/></item>
</layout>
</widget>
</widget>
<widget class="QSplitter" name="definitionSplitter">
<property name="orientation"><enum>Qt::Orientation::Vertical</enum></property>
@@ -24,15 +40,13 @@
<widget class="QGroupBox" name="portsGroup">
<property name="title"><string>Ports</string></property>
<layout class="QVBoxLayout" name="portsLayout">
<item><widget class="QTableWidget" name="portsTable"><property name="selectionBehavior"><enum>QAbstractItemView::SelectionBehavior::SelectRows</enum></property><property name="selectionMode"><enum>QAbstractItemView::SelectionMode::SingleSelection</enum></property><property name="columnCount"><number>4</number></property><column><property name="text"><string>Name</string></property></column><column><property name="text"><string>Type</string></property></column><column><property name="text"><string>Orientation</string></property></column><column><property name="text"><string>Multiple</string></property></column></widget></item>
<item><layout class="QHBoxLayout" name="portButtonsLayout"><item><widget class="QPushButton" name="addPortButton"><property name="text"><string>Add Port</string></property></widget></item><item><widget class="QPushButton" name="removePortButton"><property name="text"><string>Remove Port</string></property></widget></item><item><spacer name="portButtonSpacer"><property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item></layout></item>
<item><widget class="PortEditor" name="portEditor"/></item>
</layout>
</widget>
<widget class="QGroupBox" name="parametersGroup">
<property name="title"><string>Parameters</string></property>
<layout class="QVBoxLayout" name="parametersLayout">
<item><widget class="QTableWidget" name="parametersTable"><property name="selectionBehavior"><enum>QAbstractItemView::SelectionBehavior::SelectRows</enum></property><property name="selectionMode"><enum>QAbstractItemView::SelectionMode::SingleSelection</enum></property><property name="columnCount"><number>3</number></property><column><property name="text"><string>Name</string></property></column><column><property name="text"><string>Type</string></property></column><column><property name="text"><string>Value</string></property></column></widget></item>
<item><layout class="QHBoxLayout" name="parameterButtonsLayout"><item><widget class="QPushButton" name="addParameterButton"><property name="text"><string>Add Parameter</string></property></widget></item><item><widget class="QPushButton" name="removeParameterButton"><property name="text"><string>Remove Parameter</string></property></widget></item><item><spacer name="parameterButtonSpacer"><property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item></layout></item>
<item><widget class="ParameterEditor" name="parameterEditor"/></item>
</layout>
</widget>
</widget>
@@ -46,6 +60,8 @@
<extends>QPlainTextEdit</extends>
<header>bedit.gui.editors.openmodelica</header>
</customwidget>
<customwidget><class>PortEditor</class><extends>QWidget</extends><header>bedit.gui.dialogs.port_options</header></customwidget>
<customwidget><class>ParameterEditor</class><extends>QWidget</extends><header>bedit.gui.dialogs.parameter_options</header></customwidget>
</customwidgets>
<resources/>
<connections/>

File diff suppressed because it is too large Load Diff

73
m_Test.mo Normal file
View File

@@ -0,0 +1,73 @@
model m_Test
model m_Constant0
output Real y;
parameter Real v = 2;
equation
y = v;
end m_Constant0;
model m_gain0
input Real u(quantity="Velocity", unit="m/s");
output Real y;
parameter Real k = 2.5;
equation
y = k*u;
end m_gain0;
model m_const_and_time
output Real y;
parameter Real v = 4.8;
equation
y = v+sin(10*time);
end m_const_and_time;
model m_gain1
input Real u;
output Real y;
parameter Real k = -5;
equation
y = k*u;
end m_gain1;
model m_add0
input Real u[2];
output Real y;
equation
y = sum(u[i] for i in 1:2 );
end m_add0;
model m_Integrate0
input Real u;
output Real y;
parameter Real y_start = 0;
initial equation
y = y_start;
equation
der(y) = u;
end m_Integrate0;
model m_Differentiate0
input Real u;
output Real y;
parameter Real x_start = 0;
parameter Real y_start = 0;
parameter Real k = 1;
parameter Real T = 0.01;
Real x(start=x_start);
initial equation
y = y_start;
equation
assert(T > 0, "Differentiate time constant T must be positive");
der(x) = (u - x)/T;
y = (k/T)*(u - x);
end m_Differentiate0;
m_Constant0 Constant0;
m_gain0 gain0;
m_const_and_time const_and_time;
m_gain1 gain1;
m_add0 add0;
m_Integrate0 Integrate0;
m_Differentiate0 Differentiate0;
equation
gain0.u = Constant0.y;
gain1.u = const_and_time.y;
add0.u[1] = gain0.y;
add0.u[2] = gain1.y;
Integrate0.u = add0.y;
Differentiate0.u = add0.y;
end m_Test;