Compare commits
11 Commits
edd7bb98f2
...
bondgraph
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a6203d01e | |||
| 2d49f65ec4 | |||
| c918a6a428 | |||
| 32154386e8 | |||
| 09248421d0 | |||
| 0c578af85d | |||
| edefb23edc | |||
| 48ac9e2f59 | |||
| 2f6487e510 | |||
| eb8f77ce64 | |||
| 35d933ccbb |
11
BEdit/.vscode/tasks.json
vendored
11
BEdit/.vscode/tasks.json
vendored
@@ -115,6 +115,14 @@
|
|||||||
"options": {"cwd": "${workspaceFolder}"},
|
"options": {"cwd": "${workspaceFolder}"},
|
||||||
"problemMatcher": []
|
"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",
|
"label": "Qt: Build Designer Files",
|
||||||
"dependsOrder": "sequence",
|
"dependsOrder": "sequence",
|
||||||
@@ -125,7 +133,8 @@
|
|||||||
"Qt: Compile Component Options UI to Python",
|
"Qt: Compile Component Options UI to Python",
|
||||||
"Qt: Compile Port Options UI to Python",
|
"Qt: Compile Port Options UI to Python",
|
||||||
"Qt: Compile Shape 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": [],
|
"problemMatcher": [],
|
||||||
"group": {
|
"group": {
|
||||||
|
|||||||
@@ -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.
|
anchors may change without changing IDs.
|
||||||
- Parameters belong to `Component` rather than a particular implementation kind,
|
- Parameters belong to `Component` rather than a particular implementation kind,
|
||||||
so graph and text components share stable-ID name/type/value records.
|
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
|
- Components store one ordered `ports` list. Each port's `orientation` field is
|
||||||
indexes inputs and outputs separately for connection semantics.
|
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
|
- 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
|
- Port removal or reorientation must be rejected when it would invalidate an
|
||||||
existing connection.
|
existing connection.
|
||||||
- Connections reference port IDs, never port names.
|
- 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
|
- Connection junctions are explicit typed graph objects. Splitting a connection
|
||||||
creates one incoming and one outgoing segment; the junction can source further
|
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
|
- Graph interaction has separate Pointer and Connect modes. Port hints are only
|
||||||
visible in Connect mode. Connecting two blocks opens the compatible port-pair
|
visible in Connect mode. Connecting two blocks opens the compatible port-pair
|
||||||
chooser; explicit port clicks determine its default selection. Connections
|
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.
|
intersection of the owning hitbox and its center-to-adjacent-route-point ray.
|
||||||
- Connection appearance is configured per port type in
|
- Connection appearance is configured per port type in
|
||||||
`gui/graphics/connection_styles.py`, including color, width, pen style, and
|
`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`.
|
- Graph annotations are `box`, `line`, or `text` objects in `Graph.annotations`.
|
||||||
They use integer layers below or above graph layer 0. Annotation lines reuse
|
They use integer layers below or above graph layer 0. Annotation lines reuse
|
||||||
connection polyline and absolute `properties.waypoints` semantics. Graph
|
connection polyline and absolute `properties.waypoints` semantics. Graph
|
||||||
@@ -104,6 +122,14 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
|
|||||||
named BEdit `$name$` macro completions editable there; arbitrary `$name$`
|
named BEdit `$name$` macro completions editable there; arbitrary `$name$`
|
||||||
expressions are highlighted as BEvalues. Highlight colors and bold/italic
|
expressions are highlighted as BEvalues. Highlight colors and bold/italic
|
||||||
styles are persisted under `syntax/<category>/` in application settings.
|
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
|
- Application-wide messages use `core.application_log.get_logger()`. The main
|
||||||
window installs the Qt log-panel handler; core code must only use standard
|
window installs the Qt log-panel handler; core code must only use standard
|
||||||
Python logging and must not import the GUI handler.
|
Python logging and must not import the GUI handler.
|
||||||
@@ -116,6 +142,8 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
|
|||||||
per component instance from graph connections and exposed while compiling as
|
per component instance from graph connections and exposed while compiling as
|
||||||
`$portname_N$`; array connection endpoints receive stable one-based indices in
|
`$portname_N$`; array connection endpoints receive stable one-based indices in
|
||||||
graph connection order.
|
graph connection order.
|
||||||
|
- Simulation → Export Model composes through `Simulation.compose_source()` without
|
||||||
|
building the model, then writes the generated source as a `.mo` file.
|
||||||
- OpenModelica integration belongs in `core/simulation/openmodelica.py`. Its
|
- OpenModelica integration belongs in `core/simulation/openmodelica.py`. Its
|
||||||
persistent worker and OMC session start lazily on the first queued request.
|
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
|
Never perform OMPython work directly on the Qt GUI thread. Result and error
|
||||||
@@ -131,6 +159,8 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
|
|||||||
progress, log, and future result views. Extend graph presentation through its
|
progress, log, and future result views. Extend graph presentation through its
|
||||||
Designer-owned `resultsLayout` and the
|
Designer-owned `resultsLayout` and the
|
||||||
`clear_result_views()`/`load_result_views()` hooks.
|
`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`.
|
- Standalone simulation results are modeled in `core/simulation/results.py`.
|
||||||
Its versioned schema retains model status, messages, metadata, and plottable
|
Its versioned schema retains model status, messages, metadata, and plottable
|
||||||
traces so the simulation window can open results without an active document.
|
traces so the simulation window can open results without an active document.
|
||||||
@@ -139,10 +169,35 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
|
|||||||
- After a successful OpenModelica run, `<model>_res.csv` is parsed on the worker
|
- After a successful OpenModelica run, `<model>_res.csv` is parsed on the worker
|
||||||
before temporary-directory cleanup. `SimulationResults.data` stores every CSV
|
before temporary-directory cleanup. `SimulationResults.data` stores every CSV
|
||||||
column as a numeric array, including `time`, for later plotting and persistence.
|
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
|
- The optional OpenModelica executable is persisted as
|
||||||
`simulation/openModelicaPath`. The GUI passes it into `Simulation`; core must
|
`simulation/openModelicaPath`. The GUI passes it into `Simulation`; core must
|
||||||
not read `QSettings`. An empty path uses OMPython/PATH discovery, while an
|
not read `QSettings`. An empty path uses OMPython/PATH discovery, while an
|
||||||
explicit `.../bin/omc` path is converted to the OpenModelica home directory.
|
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
|
## Qt Designer and generated files
|
||||||
|
|
||||||
@@ -277,6 +332,7 @@ PYTHONPATH=src python3 -m bedit
|
|||||||
```
|
```
|
||||||
|
|
||||||
The installed GUI entry point is `bedit.gui.app:main`.
|
The installed GUI entry point is `bedit.gui.app:main`.
|
||||||
|
The first non-flag launch argument is opened as the initial document.
|
||||||
|
|
||||||
## Completion checklist
|
## Completion checklist
|
||||||
|
|
||||||
|
|||||||
@@ -159,12 +159,12 @@ Every component owns its ports, declarative icon, properties, and child graph:
|
|||||||
"name": "My Component",
|
"name": "My Component",
|
||||||
"position": {"x": 0, "y": 0},
|
"position": {"x": 0, "y": 0},
|
||||||
"interface": {
|
"interface": {
|
||||||
"inputs": [{"id": "in", "name": "Input", "type": "signal", "properties": {
|
"ports": [
|
||||||
"iconPosition": {"x": 0, "y": 40}
|
{"id": "in", "name": "Input", "type": "signal", "orientation": "input",
|
||||||
}}],
|
"properties": {"iconPosition": {"x": 0, "y": 40}}},
|
||||||
"outputs": [{"id": "out", "name": "Output", "type": "signal", "properties": {
|
{"id": "out", "name": "Output", "type": "signal", "orientation": "output",
|
||||||
"iconPosition": {"x": 128, "y": 64}
|
"properties": {"iconPosition": {"x": 128, "y": 64}}}
|
||||||
}}]
|
]
|
||||||
},
|
},
|
||||||
"icon": {
|
"icon": {
|
||||||
"size": {"width": 128, "height": 128},
|
"size": {"width": 128, "height": 128},
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ description = "A starter Qt desktop application"
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"matplotlib>=3.8,<4",
|
||||||
"msgpack>=1.0,<2",
|
"msgpack>=1.0,<2",
|
||||||
"PySide6>=6.7,<7",
|
"PySide6>=6.7,<7",
|
||||||
"OMPython>4.0",
|
"OMPython>4.0",
|
||||||
|
|||||||
BIN
BEdit/resources/icons/list-add.png
Normal file
BIN
BEdit/resources/icons/list-add.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
BIN
BEdit/resources/icons/list-remove.png
Normal file
BIN
BEdit/resources/icons/list-remove.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
@@ -1,5 +1,7 @@
|
|||||||
<RCC>
|
<RCC>
|
||||||
<qresource prefix="icons">
|
<qresource prefix="icons">
|
||||||
|
<file>icons/list-remove.png</file>
|
||||||
|
<file>icons/list-add.png</file>
|
||||||
<file>icons/view-form-table.png</file>
|
<file>icons/view-form-table.png</file>
|
||||||
<file>icons/office-chart-line.png</file>
|
<file>icons/office-chart-line.png</file>
|
||||||
<file>icons/run-build.png</file>
|
<file>icons/run-build.png</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\
|
\xbf\xc9\xd3\x01\x9f\xa3\x0f\x17Z/\xe3\x7f\xe1/\x17\x85\
|
||||||
\xd6\x06q(\x0e\x10\x00\x00\x00\x00IEND\xaeB\
|
\xd6\x06q(\x0e\x10\x00\x00\x00\x00IEND\xaeB\
|
||||||
`\x82\
|
`\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\
|
\x00\x00\x02V\
|
||||||
\x89\
|
\x89\
|
||||||
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
|
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
|
||||||
@@ -2383,6 +2452,101 @@ u1\xb8\xdd0`x\xe8F\xf9\x1e\xe19\xea\x8a\x03\
|
|||||||
|f\x1e\x89\xd1\x9ef\xbdhx\x0fE4mL\xaf\
|
|f\x1e\x89\xd1\x9ef\xbdhx\x0fE4mL\xaf\
|
||||||
b/B\x06\x12o\xbf\x00\xa3\x17WYZq\xd9W\
|
b/B\x06\x12o\xbf\x00\xa3\x17WYZq\xd9W\
|
||||||
\x00\x00\x00\x00IEND\xaeB`\x82\
|
\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<\
|
\x00\x00\x04<\
|
||||||
\x89\
|
\x89\
|
||||||
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
|
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
|
||||||
@@ -2649,6 +2813,10 @@ qt_resource_name = b"\
|
|||||||
\x0f\xba\x1e'\
|
\x0f\xba\x1e'\
|
||||||
\x00d\
|
\x00d\
|
||||||
\x00r\x00a\x00w\x00-\x00p\x00a\x00t\x00h\x00.\x00p\x00n\x00g\
|
\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\
|
\x00\x0d\
|
||||||
\x0b\xe6\x1f\xa7\
|
\x0b\xe6\x1f\xa7\
|
||||||
\x00d\
|
\x00d\
|
||||||
@@ -2723,6 +2891,10 @@ qt_resource_name = b"\
|
|||||||
\x00o\
|
\x00o\
|
||||||
\x00f\x00f\x00i\x00c\x00e\x00-\x00c\x00h\x00a\x00r\x00t\x00-\x00l\x00i\x00n\x00e\
|
\x00f\x00f\x00i\x00c\x00e\x00-\x00c\x00h\x00a\x00r\x00t\x00-\x00l\x00i\x00n\x00e\
|
||||||
\x00.\x00p\x00n\x00g\
|
\x00.\x00p\x00n\x00g\
|
||||||
|
\x00\x0c\
|
||||||
|
\x09\xc6\x19'\
|
||||||
|
\x00l\
|
||||||
|
\x00i\x00s\x00t\x00-\x00a\x00d\x00d\x00.\x00p\x00n\x00g\
|
||||||
\x00\x10\
|
\x00\x10\
|
||||||
\x03\xe6\xd3g\
|
\x03\xe6\xd3g\
|
||||||
\x00d\
|
\x00d\
|
||||||
@@ -2749,67 +2921,71 @@ qt_resource_struct = b"\
|
|||||||
\x00\x00\x00\x00\x00\x00\x00\x00\
|
\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\x02\x00\x00\x00\x01\x00\x00\x00\x02\
|
||||||
\x00\x00\x00\x00\x00\x00\x00\x00\
|
\x00\x00\x00\x00\x00\x00\x00\x00\
|
||||||
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1e\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\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\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\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\x9f{0\xc99\
|
||||||
\x00\x00\x03\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x8d\xaa\
|
\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\x9f\x84\x8f\x00\xb9\
|
||||||
\x00\x00\x01\xc4\x00\x00\x00\x00\x00\x01\x00\x00D2\
|
\x00\x00\x01\xe8\x00\x00\x00\x00\x00\x01\x00\x00H_\
|
||||||
\x00\x00\x01\x9f\x7f&\x83\xcd\
|
\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\x01\x9f{C\xf1\x18\
|
||||||
\x00\x00\x03\xd4\x00\x00\x00\x00\x00\x01\x00\x00\x91\x1b\
|
\x00\x00\x04\x16\x00\x00\x00\x00\x00\x01\x00\x00\x9b\x1b\
|
||||||
\x00\x00\x01\x9f\x7fY\xceg\
|
\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\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\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\x9f\x7f&\x83\xb1\
|
||||||
\x00\x00\x01\x1c\x00\x00\x00\x00\x00\x01\x00\x00,\x08\
|
\x00\x00\x01\x1c\x00\x00\x00\x00\x00\x01\x00\x00,\x08\
|
||||||
\x00\x00\x01\x9f\x7fY\xce\x82\
|
\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\x9f{C\xf1.\
|
||||||
\x00\x00\x03\xfa\x00\x00\x00\x00\x00\x01\x00\x00\x95[\
|
\x00\x00\x04<\x00\x00\x00\x00\x00\x01\x00\x00\x9f[\
|
||||||
\x00\x00\x01\x9f\x84\x8f\xa6\x10\
|
\x00\x00\x01\x9f\x84\x8f\xa6\x10\
|
||||||
\x00\x00\x01\x84\x00\x00\x00\x00\x00\x01\x00\x006\x9c\
|
\x00\x00\x01\xa8\x00\x00\x00\x00\x00\x01\x00\x00:\xc9\
|
||||||
\x00\x00\x01\x9f{{\xa5\xd5\
|
\x00\x00\x01\x9f{{\xa5\xd5\
|
||||||
\x00\x00\x04&\x00\x00\x00\x00\x00\x01\x00\x00\x97\x0c\
|
\x00\x00\x04h\x00\x00\x00\x00\x00\x01\x00\x00\xa1\x0c\
|
||||||
\x00\x00\x01\x9f\x7f&\x83\x10\
|
\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\x00\xfe\x00\x00\x00\x00\x00\x01\x00\x00(e\
|
||||||
\x00\x00\x01\x9f\x7f&\x83~\
|
\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\x01\x9f\x7f&\x83T\
|
||||||
\x00\x00\x00~\x00\x00\x00\x00\x00\x01\x00\x00\x14M\
|
\x00\x00\x00~\x00\x00\x00\x00\x00\x01\x00\x00\x14M\
|
||||||
\x00\x00\x01\x9f\x7fY\xce^\
|
\x00\x00\x01\x9f\x7fY\xce^\
|
||||||
\x00\x00\x00\xa2\x00\x00\x00\x00\x00\x01\x00\x00\x19Y\
|
\x00\x00\x00\xa2\x00\x00\x00\x00\x00\x01\x00\x00\x19Y\
|
||||||
\x00\x00\x01\x9f{0\xc9B\
|
\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\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\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\x01\x9f\x7f&\x82\xf2\
|
||||||
\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x01\x00\x00!\xc5\
|
\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x01\x00\x00!\xc5\
|
||||||
\x00\x00\x01\x9f{\x8d\xf34\
|
\x00\x00\x01\x9f{\x8d\xf34\
|
||||||
\x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
|
\x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
|
||||||
\x00\x00\x01\x9f{0\xc9+\
|
\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\x01\x9f{C\xf1=\
|
||||||
\x00\x00\x006\x00\x00\x00\x00\x00\x01\x00\x00\x05\x86\
|
\x00\x00\x006\x00\x00\x00\x00\x00\x01\x00\x00\x05\x86\
|
||||||
\x00\x00\x01\x9f\x7f&\x83\x99\
|
\x00\x00\x01\x9f\x7f&\x83\x99\
|
||||||
\x00\x00\x04P\x00\x00\x00\x00\x00\x01\x00\x00\x99\xa2\
|
\x00\x00\x04\x92\x00\x00\x00\x00\x00\x01\x00\x00\xa3\xa2\
|
||||||
\x00\x00\x01\x9f{{\xa5\xe3\
|
\x00\x00\x01\x9f{{\xa5\xe3\
|
||||||
\x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x00\x0c\xc1\
|
\x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x00\x0c\xc1\
|
||||||
\x00\x00\x01\x9f\x7f\xac\xf2\xc6\
|
\x00\x00\x01\x9f\x7f\xac\xf2\xc6\
|
||||||
\x00\x00\x01D\x00\x00\x00\x00\x00\x01\x00\x00/{\
|
\x00\x00\x01D\x00\x00\x00\x00\x00\x01\x00\x00/{\
|
||||||
\x00\x00\x01\x9f\x7fV\xd5\xe5\
|
\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\
|
\x00\x00\x01\x9f{0\xc9R\
|
||||||
"
|
"
|
||||||
|
|
||||||
|
|||||||
321
BEdit/src/bedit/core/bond_graph.py
Normal file
321
BEdit/src/bedit/core/bond_graph.py
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
"""Bond-graph analysis hooks.
|
||||||
|
|
||||||
|
This module intentionally has no GUI or simulation-engine dependencies. The
|
||||||
|
causality inference algorithm can grow here without coupling the document model
|
||||||
|
to OpenModelica or Qt.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from bedit.core.application_log import get_logger
|
||||||
|
|
||||||
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
def infer_causality(component: dict[str, Any], toplevel: bool = True) -> dict[str, Any]:
|
||||||
|
"""Infer power-connection causality in a serialized component tree.
|
||||||
|
|
||||||
|
This is currently a traversal stub: it ensures every power connection has a
|
||||||
|
causality value, while preserving causality already supplied by callers.
|
||||||
|
Future inference rules should assign ``source``, ``target``,
|
||||||
|
``warn_source``, or ``warn_target`` here and return the same tree.
|
||||||
|
|
||||||
|
The input is mutated and returned so the composer receives the inferred
|
||||||
|
representation without needing a second document conversion.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if toplevel:
|
||||||
|
# Reset causalities
|
||||||
|
reset_causality(component)
|
||||||
|
|
||||||
|
implementation = component.get("implementation", {})
|
||||||
|
graph = implementation.get("graph") if isinstance(implementation, dict) else None
|
||||||
|
if not isinstance(graph, dict):
|
||||||
|
return component
|
||||||
|
|
||||||
|
id_list = build_id_list(component)
|
||||||
|
|
||||||
|
# Fixed causalities
|
||||||
|
for block in graph.get("blocks", []):
|
||||||
|
for port in block.get('interface', {}).get('ports', []):
|
||||||
|
if port.get('type', '') != 'power':
|
||||||
|
continue
|
||||||
|
if port.get('causality', 'indifferent') == 'fixed effort out':
|
||||||
|
propagate_from_port(block, port, 'effort out', graph, id_list)
|
||||||
|
elif port.get('causality', 'indifferent') == 'fixed flow out':
|
||||||
|
propagate_from_port(block, port, 'flow out', graph, id_list)
|
||||||
|
|
||||||
|
# Preferred causalities
|
||||||
|
for block in graph.get("blocks", []):
|
||||||
|
for port in block.get('interface', {}).get('ports', []):
|
||||||
|
if port.get('type', '') != 'power':
|
||||||
|
continue
|
||||||
|
if is_port_fully_assigned(block, port, graph):
|
||||||
|
continue
|
||||||
|
if port.get('causality', 'indifferent') == 'preferred effort out':
|
||||||
|
propagate_from_port(block, port, 'effort out', graph, id_list)
|
||||||
|
elif port.get('causality', 'indifferent') == 'preferred flow out':
|
||||||
|
propagate_from_port(block, port, 'flow out', graph, id_list)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Soft choices may need several passes when a junction has multiple
|
||||||
|
# unresolved bonds. Stop as soon as a pass makes no further progress.
|
||||||
|
while True:
|
||||||
|
unresolved_before = sum(
|
||||||
|
con.get('type') == 'power' and con.get('causality', 'none') == 'none'
|
||||||
|
for con in graph.get('connections', [])
|
||||||
|
)
|
||||||
|
for block in graph.get("blocks", []):
|
||||||
|
for port in block.get('interface', {}).get('ports', []):
|
||||||
|
if port.get('type', '') != 'power':
|
||||||
|
continue
|
||||||
|
if is_port_fully_assigned(block, port, graph):
|
||||||
|
continue
|
||||||
|
if port.get('causality', 'indifferent') == 'likes effort out':
|
||||||
|
propagate_from_port(block, port, 'effort out', graph, id_list)
|
||||||
|
elif port.get('causality', 'indifferent') == 'likes flow out':
|
||||||
|
propagate_from_port(block, port, 'flow out', graph, id_list)
|
||||||
|
elif port.get('causality', 'indifferent') == 'single effort in':
|
||||||
|
evaluate_junction_constraints(block, port, graph, id_list)
|
||||||
|
if not is_port_fully_assigned(block, port, graph):
|
||||||
|
propagate_from_port(block, port, 'flow out', graph, id_list)
|
||||||
|
elif port.get('causality', 'indifferent') == 'single flow in':
|
||||||
|
evaluate_junction_constraints(block, port, graph, id_list)
|
||||||
|
if not is_port_fully_assigned(block, port, graph):
|
||||||
|
propagate_from_port(block, port, 'effort out', graph, id_list)
|
||||||
|
elif port.get('causality', 'indifferent') == 'indifferent':
|
||||||
|
# Force an arbitrary assignment on the first unassigned bond connected to this port
|
||||||
|
propagate_from_port(block, port, 'effort out', graph, id_list)
|
||||||
|
unresolved_after = sum(
|
||||||
|
con.get('type') == 'power' and con.get('causality', 'none') == 'none'
|
||||||
|
for con in graph.get('connections', [])
|
||||||
|
)
|
||||||
|
if unresolved_after == 0 or unresolved_after >= unresolved_before:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Last check
|
||||||
|
for con in graph.get('connections', []):
|
||||||
|
if con.get('type') == 'power' and con.get('causality', 'none') == 'none':
|
||||||
|
raise ValueError("System under-constrained: unresolved causal loops or disconnected elements remain")
|
||||||
|
|
||||||
|
for block in graph.get("blocks", []):
|
||||||
|
if isinstance(block, dict):
|
||||||
|
infer_causality(block, False)
|
||||||
|
|
||||||
|
return component
|
||||||
|
|
||||||
|
def reset_causality(component: dict[str, Any]) -> None:
|
||||||
|
implementation = component.get("implementation", {})
|
||||||
|
graph = implementation.get("graph") if isinstance(implementation, dict) else None
|
||||||
|
if not isinstance(graph, dict):
|
||||||
|
return
|
||||||
|
for connection in graph.get("connections", []):
|
||||||
|
if isinstance(connection, dict) and connection.get("type") == "power":
|
||||||
|
connection["causality"] = "none"
|
||||||
|
for block in graph.get("blocks", []):
|
||||||
|
if isinstance(block, dict):
|
||||||
|
reset_causality(block)
|
||||||
|
|
||||||
|
def build_id_list(graph: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Index all addressable objects in a component tree by their stable ID."""
|
||||||
|
|
||||||
|
id_list: dict[str, Any] = {}
|
||||||
|
id_kinds: dict[str, str] = {}
|
||||||
|
|
||||||
|
def add(item: dict[str, Any], description: str) -> None:
|
||||||
|
item_id = item.get("id")
|
||||||
|
if not item_id:
|
||||||
|
raise ValueError(f"{description} has no ID")
|
||||||
|
# Port IDs identify a port on a component definition and may therefore
|
||||||
|
# recur in cloned component instances. Component and junction IDs are
|
||||||
|
# document objects and must remain globally unique.
|
||||||
|
if item_id in id_list and not (
|
||||||
|
description == "port" and id_kinds[item_id] == "port"
|
||||||
|
):
|
||||||
|
raise ValueError(f"Duplicate simulation object ID: {item_id}")
|
||||||
|
id_list[item_id] = item
|
||||||
|
id_kinds[item_id] = description
|
||||||
|
|
||||||
|
def visit(component: dict[str, Any]) -> None:
|
||||||
|
add(component, "component")
|
||||||
|
interface = component.get("interface", {})
|
||||||
|
for port in interface.get("ports", []):
|
||||||
|
add(port, "port")
|
||||||
|
|
||||||
|
implementation = component.get("implementation", {})
|
||||||
|
if implementation.get("kind") != "graph":
|
||||||
|
return
|
||||||
|
nested_graph = implementation.get("graph", {})
|
||||||
|
for junction in nested_graph.get("junctions", []):
|
||||||
|
add(junction, "junction")
|
||||||
|
for block in nested_graph.get("blocks", []):
|
||||||
|
visit(block)
|
||||||
|
|
||||||
|
visit(graph)
|
||||||
|
return id_list
|
||||||
|
|
||||||
|
def is_port_fully_assigned(block: dict[str, Any], port: dict[str, Any], graph: dict[str, Any]) -> bool:
|
||||||
|
block_id = block.get('id')
|
||||||
|
port_id = port.get('id')
|
||||||
|
for connection in graph.get('connections', []):
|
||||||
|
if connection.get('type') != 'power':
|
||||||
|
continue
|
||||||
|
source = connection.get('source', {})
|
||||||
|
target = connection.get('target', {})
|
||||||
|
if ((source.get('block') == block_id and source.get('port') == port_id)
|
||||||
|
or (target.get('block') == block_id and target.get('port') == port_id)):
|
||||||
|
if connection.get('causality', 'none') == 'none':
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def propagate_from_port(block: dict[str, Any], port: dict[str, Any], desired_causality: str, graph: dict[str, Any], id_list: dict[str, Any]) -> None:
|
||||||
|
# find all connection on this specific port
|
||||||
|
attached_connections = []
|
||||||
|
for con in graph.get('connections', []):
|
||||||
|
if con.get('type') == 'power':
|
||||||
|
if con.get('source', {}).get('block') == block.get('id') and con.get('source', {}).get('port') == port.get('id'):
|
||||||
|
attached_connections.append(con)
|
||||||
|
elif con.get('target', {}).get('block') == block.get('id') and con.get('target', {}).get('port') == port.get('id'):
|
||||||
|
attached_connections.append(con)
|
||||||
|
if not attached_connections:
|
||||||
|
# unconnected port
|
||||||
|
return
|
||||||
|
|
||||||
|
# find first unassigned connection on this port
|
||||||
|
target_connection = None
|
||||||
|
for con in attached_connections:
|
||||||
|
if con.get('causality', None) == 'none':
|
||||||
|
target_connection = con
|
||||||
|
break
|
||||||
|
if target_connection is None:
|
||||||
|
# Nothing left to resolve
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update
|
||||||
|
if target_connection.get('source').get('block') == block.get('id'):
|
||||||
|
target_connection['causality'] = 'target' if (desired_causality == 'effort out') else 'source'
|
||||||
|
else:
|
||||||
|
target_connection['causality'] = 'source' if (desired_causality == 'effort out') else 'target'
|
||||||
|
|
||||||
|
propagate_to_neighbor_from_connection(block, target_connection, graph, id_list)
|
||||||
|
|
||||||
|
def evaluate_junction_constraints(block: dict[str, Any], port: dict[str, Any], graph: dict[str, Any], id_list: dict[str, Any]) -> None:
|
||||||
|
# find all connection on this specific port
|
||||||
|
attached_connections = []
|
||||||
|
for con in graph.get('connections', []):
|
||||||
|
if con.get('type') == 'power':
|
||||||
|
if con.get('source', {}).get('block') == block.get('id') and con.get('source', {}).get('port') == port.get('id'):
|
||||||
|
attached_connections.append(con)
|
||||||
|
elif con.get('target', {}).get('block') == block.get('id') and con.get('target', {}).get('port') == port.get('id'):
|
||||||
|
attached_connections.append(con)
|
||||||
|
if not attached_connections:
|
||||||
|
# unconnected port
|
||||||
|
return
|
||||||
|
|
||||||
|
efforts_in = 0
|
||||||
|
efforts_out = 0
|
||||||
|
unnassigned_conns = []
|
||||||
|
# Count current states relative to the junction port
|
||||||
|
for con in attached_connections:
|
||||||
|
if con.get('causality', 'none') == 'none':
|
||||||
|
unnassigned_conns.append(con)
|
||||||
|
continue
|
||||||
|
|
||||||
|
is_source = (con.get('source').get('block') == block.get('id'))
|
||||||
|
if (is_source and con.get('causality', 'none')=='source') or (not is_source and con.get('causality', 'none')=='target'):
|
||||||
|
efforts_in += 1
|
||||||
|
else:
|
||||||
|
efforts_out += 1
|
||||||
|
|
||||||
|
# Single effort in
|
||||||
|
if port.get('causality', 'indifferent') == 'single effort in':
|
||||||
|
# ERROR CHECK: Multiple sources/blocks trying to claim effort control on a 0-junction
|
||||||
|
if efforts_in>1:
|
||||||
|
raise ValueError(f"Critical causality conflict: Multiple blocks are dictating effort to 0 junction: {block.get('name')}")
|
||||||
|
|
||||||
|
# 1 effort is coming in so all other ports must be effort out
|
||||||
|
if efforts_in == 1 and len(unnassigned_conns) > 0:
|
||||||
|
for con in unnassigned_conns:
|
||||||
|
desired = 'target' if (con.get('source').get('block')==block.get('id')) else 'source'
|
||||||
|
con['causality'] = desired
|
||||||
|
propagate_to_neighbor_from_connection(block, con, graph, id_list)
|
||||||
|
|
||||||
|
# No effort is coming in yet and one left so must be effort in
|
||||||
|
if efforts_in == 0 and len(unnassigned_conns) == 1:
|
||||||
|
con = unnassigned_conns[0]
|
||||||
|
desired = 'source' if (con.get('source').get('block')==block.get('id')) else 'target'
|
||||||
|
con['causality'] = desired
|
||||||
|
propagate_to_neighbor_from_connection(block, con, graph, id_list)
|
||||||
|
|
||||||
|
# Single flow in
|
||||||
|
elif port.get('causality', 'indifferent') == 'single flow in':
|
||||||
|
# ERROR CHECK: Multiple sources/blocks trying to claim flow control on a 1-junction
|
||||||
|
if efforts_out>1:
|
||||||
|
raise ValueError(f"Critical causality conflict: Multiple blocks are dictating flow to 1 junction: {block.get('name')}")
|
||||||
|
|
||||||
|
# 1 flow is coming in so all other ports must be flow out
|
||||||
|
if efforts_out == 1 and len(unnassigned_conns) > 0:
|
||||||
|
for con in unnassigned_conns:
|
||||||
|
desired = 'source' if (con.get('source').get('block')==block.get('id')) else 'target'
|
||||||
|
con['causality'] = desired
|
||||||
|
propagate_to_neighbor_from_connection(block, con, graph, id_list)
|
||||||
|
|
||||||
|
# No flow is coming in yet and one left so must be flow in
|
||||||
|
if efforts_out == 0 and len(unnassigned_conns) == 1:
|
||||||
|
con = unnassigned_conns[0]
|
||||||
|
desired = 'target' if (con.get('source').get('block')==block.get('id')) else 'source'
|
||||||
|
con['causality'] = desired
|
||||||
|
propagate_to_neighbor_from_connection(block, con, graph, id_list)
|
||||||
|
|
||||||
|
def propagate_to_neighbor_from_connection(block: dict[str, Any], connection: dict[str, Any], graph: dict[str, Any], id_list: dict[str, Any]) -> None:
|
||||||
|
# Get the neighbor block and port on the other side of the bond
|
||||||
|
neighbor_id = connection.get('target').get('block') if connection.get('source').get('block') == block.get('id') else connection.get('source').get('block')
|
||||||
|
neighbor_port_id = connection.get('target').get('port') if connection.get('source').get('block') == block.get('id') else connection.get('source').get('port')
|
||||||
|
neighbor_block = id_list.get(neighbor_id, None)
|
||||||
|
if neighbor_block is None:
|
||||||
|
raise ValueError(f"No other side of the bond found on connection {connection.get('id')}")
|
||||||
|
neighbor_ports = neighbor_block.get('interface').get('ports', [])
|
||||||
|
neighbor_port = None
|
||||||
|
for p in neighbor_ports:
|
||||||
|
if p.get('id') == neighbor_port_id:
|
||||||
|
neighbor_port = p
|
||||||
|
break
|
||||||
|
if neighbor_port is None:
|
||||||
|
raise ValueError(f"No other side of the bond found on connection {connection.get('id')}")
|
||||||
|
|
||||||
|
# Trigger evaluation on the neighbor
|
||||||
|
if neighbor_port.get('causality', 'indifferent') in ['single effort in', 'single flow in']:
|
||||||
|
evaluate_junction_constraints(neighbor_block, neighbor_port, graph, id_list)
|
||||||
|
else:
|
||||||
|
verify_component_compatibility(neighbor_block, neighbor_port, graph)
|
||||||
|
|
||||||
|
def verify_component_compatibility(block: dict[str, Any], port: dict[str, Any], graph: dict[str, Any]) -> None:
|
||||||
|
# Find the connection we are evaluating
|
||||||
|
conn = None
|
||||||
|
for c in graph.get('connections', []):
|
||||||
|
if (c.get('source').get('block') == block.get('id') and c.get('source').get('port') == port.get('id')) or (c.get('target').get('block') == block.get('id') and c.get('target').get('port') == port.get('id')):
|
||||||
|
if c.get('causality', 'none') != 'none':
|
||||||
|
conn = c
|
||||||
|
break
|
||||||
|
|
||||||
|
if conn is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Figure out what causality was pushed onto this port from the outside world
|
||||||
|
state = conn.get('causality', 'none')
|
||||||
|
is_source = conn.get('source', {}).get('block') == block.get('id')
|
||||||
|
effort_out = (is_source and state == 'target') or (not is_source and state == 'source')
|
||||||
|
port_causality = port.get('causality', 'indifferent')
|
||||||
|
|
||||||
|
if port_causality == 'fixed effort out' and not effort_out:
|
||||||
|
raise ValueError(f"Critical source conflict: Fixed effort source '{block.get('name')}' forced into an input state.")
|
||||||
|
if port_causality == 'fixed flow out' and effort_out:
|
||||||
|
raise ValueError(f"Critical source conflict: Fixed flow source '{block.get('name')}' forced into an input state.")
|
||||||
|
|
||||||
|
if port_causality == 'preferred effort out' and not effort_out:
|
||||||
|
conn['causality'] = f'warn_{state}'
|
||||||
|
# log.warning(f"Storage block '{block.get('name')}' was forced into derivative causality (dependent state).")
|
||||||
|
if port_causality == 'preferred flow out' and effort_out:
|
||||||
|
conn['causality'] = f'warn_{state}'
|
||||||
|
# log.warning(f"Storage block '{block.get('name')}' was forced into derivative causality (dependent state).")
|
||||||
@@ -6,6 +6,24 @@ from typing import Any
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from bedit.core.port_types import PortTypeRegistry
|
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
|
@dataclass
|
||||||
@@ -17,6 +35,15 @@ class Port:
|
|||||||
properties: dict[str, Any] = field(default_factory=dict)
|
properties: dict[str, Any] = field(default_factory=dict)
|
||||||
type: str = "signal"
|
type: str = "signal"
|
||||||
allows_multiple_connections: bool = False
|
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]:
|
def to_dict(self) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
@@ -26,11 +53,20 @@ class Port:
|
|||||||
"properties": self.properties,
|
"properties": self.properties,
|
||||||
"type": self.type,
|
"type": self.type,
|
||||||
"multipleConnections": self.allows_multiple_connections,
|
"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
|
@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", {})
|
position = data.get("position", {})
|
||||||
|
rows, columns = _dimensions(data.get("dimensions", {}), "Port")
|
||||||
return cls(
|
return cls(
|
||||||
id=str(data["id"]),
|
id=str(data["id"]),
|
||||||
name=str(data.get("name", data["id"])),
|
name=str(data.get("name", data["id"])),
|
||||||
@@ -39,6 +75,15 @@ class Port:
|
|||||||
properties=dict(data.get("properties", {})),
|
properties=dict(data.get("properties", {})),
|
||||||
type=str(data.get("type", "signal")),
|
type=str(data.get("type", "signal")),
|
||||||
allows_multiple_connections=bool(data.get("multipleConnections", False)),
|
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
|
target: Endpoint
|
||||||
name: str = ""
|
name: str = ""
|
||||||
properties: dict[str, Any] = field(default_factory=dict)
|
properties: dict[str, Any] = field(default_factory=dict)
|
||||||
|
type: str = "signal"
|
||||||
|
causality: str = "none"
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, Any]:
|
def to_dict(self) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
@@ -147,6 +194,8 @@ class Connection:
|
|||||||
"target": self.target.to_dict(),
|
"target": self.target.to_dict(),
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"properties": self.properties,
|
"properties": self.properties,
|
||||||
|
"type": self.type,
|
||||||
|
"causality": self.causality,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -157,6 +206,8 @@ class Connection:
|
|||||||
target=Endpoint.from_dict(data["target"]),
|
target=Endpoint.from_dict(data["target"]),
|
||||||
name=str(data.get("name", "")),
|
name=str(data.get("name", "")),
|
||||||
properties=dict(data.get("properties", {})),
|
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
|
name: str
|
||||||
type: str = "real"
|
type: str = "real"
|
||||||
value: str = "0"
|
value: str = "0"
|
||||||
|
quantity: str = ""
|
||||||
|
unit: str = ""
|
||||||
|
rows: int = 1
|
||||||
|
columns: int = 1
|
||||||
|
description: str = ""
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, str]:
|
def to_dict(self) -> dict[str, Any]:
|
||||||
return {"id": self.id, "name": self.name, "type": self.type, "value": self.value}
|
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
|
@classmethod
|
||||||
def from_dict(cls, data: dict[str, Any]) -> "Parameter":
|
def from_dict(cls, data: dict[str, Any]) -> "Parameter":
|
||||||
@@ -176,11 +241,17 @@ class Parameter:
|
|||||||
raise ValueError("Each component parameter must be an object")
|
raise ValueError("Each component parameter must be an object")
|
||||||
if "id" not in data:
|
if "id" not in data:
|
||||||
raise ValueError("Each component parameter must have an ID")
|
raise ValueError("Each component parameter must have an ID")
|
||||||
|
rows, columns = _dimensions(data.get("dimensions", {}), "Parameter")
|
||||||
return cls(
|
return cls(
|
||||||
id=str(data["id"]),
|
id=str(data["id"]),
|
||||||
name=str(data.get("name", "")),
|
name=str(data.get("name", "")),
|
||||||
type=str(data.get("type", "real")),
|
type=str(data.get("type", "real")),
|
||||||
value=str(data.get("value", "0")),
|
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
|
x: float = 0.0
|
||||||
y: float = 0.0
|
y: float = 0.0
|
||||||
rotation: float = 0.0
|
rotation: float = 0.0
|
||||||
inputs: list[Port] = field(default_factory=list)
|
ports: list[Port] = field(default_factory=list)
|
||||||
outputs: list[Port] = field(default_factory=list)
|
|
||||||
parameters: list[Parameter] = field(default_factory=list)
|
parameters: list[Parameter] = field(default_factory=list)
|
||||||
icon: Icon = field(default_factory=Icon)
|
icon: Icon = field(default_factory=Icon)
|
||||||
properties: dict[str, Any] = field(default_factory=dict)
|
properties: dict[str, Any] = field(default_factory=dict)
|
||||||
@@ -320,10 +390,7 @@ class Component:
|
|||||||
"name": self.name,
|
"name": self.name,
|
||||||
"position": {"x": self.x, "y": self.y},
|
"position": {"x": self.x, "y": self.y},
|
||||||
"rotation": self.rotation,
|
"rotation": self.rotation,
|
||||||
"interface": {
|
"interface": {"ports": [port.to_dict() for port in self.ports]},
|
||||||
"inputs": [port.to_dict() for port in self.inputs],
|
|
||||||
"outputs": [port.to_dict() for port in self.outputs],
|
|
||||||
},
|
|
||||||
"parameters": [parameter.to_dict() for parameter in self.parameters],
|
"parameters": [parameter.to_dict() for parameter in self.parameters],
|
||||||
"icon": self.icon.to_dict(),
|
"icon": self.icon.to_dict(),
|
||||||
"properties": self.properties,
|
"properties": self.properties,
|
||||||
@@ -344,11 +411,19 @@ class Component:
|
|||||||
if kind == "text":
|
if kind == "text":
|
||||||
raw_source = implementation.get("source", {})
|
raw_source = implementation.get("source", {})
|
||||||
equations = raw_source.get("equations", "")
|
equations = raw_source.get("equations", "")
|
||||||
|
declarations = raw_source.get("declarations", "")
|
||||||
|
initial_equations = raw_source.get("initialEquations", "")
|
||||||
parameters = data.get("parameters", raw_source.get("parameters", []))
|
parameters = data.get("parameters", raw_source.get("parameters", []))
|
||||||
if not isinstance(equations, str):
|
if not isinstance(equations, str):
|
||||||
raise ValueError("Text component equations must be a string")
|
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 = {
|
source = {
|
||||||
"equations": equations,
|
"equations": equations,
|
||||||
|
"declarations": declarations,
|
||||||
|
"initialEquations": initial_equations,
|
||||||
}
|
}
|
||||||
if not isinstance(parameters, list):
|
if not isinstance(parameters, list):
|
||||||
raise ValueError("Component parameters must be a list")
|
raise ValueError("Component parameters must be a list")
|
||||||
@@ -358,8 +433,7 @@ class Component:
|
|||||||
x=float(position.get("x", 0.0)),
|
x=float(position.get("x", 0.0)),
|
||||||
y=float(position.get("y", 0.0)),
|
y=float(position.get("y", 0.0)),
|
||||||
rotation=float(data.get("rotation", 0.0)),
|
rotation=float(data.get("rotation", 0.0)),
|
||||||
inputs=[Port.from_dict(item) for item in interface.get("inputs", [])],
|
ports=[Port.from_dict(item) for item in interface.get("ports", [])],
|
||||||
outputs=[Port.from_dict(item) for item in interface.get("outputs", [])],
|
|
||||||
parameters=[Parameter.from_dict(item) for item in parameters],
|
parameters=[Parameter.from_dict(item) for item in parameters],
|
||||||
icon=Icon.from_dict(data.get("icon")),
|
icon=Icon.from_dict(data.get("icon")),
|
||||||
properties=dict(data.get("properties", {})),
|
properties=dict(data.get("properties", {})),
|
||||||
@@ -445,6 +519,13 @@ class GraphDocument:
|
|||||||
raise ValueError(f"Component {component.name!r} has duplicate parameter names")
|
raise ValueError(f"Component {component.name!r} has duplicate parameter names")
|
||||||
if any(not name.strip() for name in parameter_names):
|
if any(not name.strip() for name in parameter_names):
|
||||||
raise ValueError(f"Component {component.name!r} has an unnamed parameter")
|
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:
|
if component.implementation_kind == "text" and component.graph.blocks:
|
||||||
raise ValueError(f"Text component {component.name} cannot contain a graph")
|
raise ValueError(f"Text component {component.name} cannot contain a graph")
|
||||||
self._validate_graph(component)
|
self._validate_graph(component)
|
||||||
@@ -456,16 +537,46 @@ class GraphDocument:
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Component names inside {owner.name!r} must be unique"
|
f"Component names inside {owner.name!r} must be unique"
|
||||||
)
|
)
|
||||||
input_ids = {port.id for port in owner.inputs}
|
port_ids = {port.id for port in owner.ports}
|
||||||
output_ids = {port.id for port in owner.outputs}
|
if len(port_ids) != len(owner.ports):
|
||||||
if len(input_ids) != len(owner.inputs) or len(output_ids) != len(owner.outputs):
|
|
||||||
raise ValueError(f"Component {owner.name} contains duplicate port IDs")
|
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)
|
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():
|
for junction in owner.graph.junctions.values():
|
||||||
PortTypeRegistry.get(junction.type)
|
PortTypeRegistry.get(junction.type)
|
||||||
|
if junction.type == "power":
|
||||||
|
raise ValueError("Power bond connections cannot contain junctions")
|
||||||
endpoint_counts: dict[tuple[str, str, str], int] = {}
|
endpoint_counts: dict[tuple[str, str, str], int] = {}
|
||||||
for connection in owner.graph.connections.values():
|
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:
|
if connection.source.junction is not None:
|
||||||
junction = owner.graph.junctions.get(connection.source.junction)
|
junction = owner.graph.junctions.get(connection.source.junction)
|
||||||
if junction is None:
|
if junction is None:
|
||||||
@@ -474,14 +585,30 @@ class GraphDocument:
|
|||||||
)
|
)
|
||||||
source_port = Port(junction.id, "Junction", type=junction.type)
|
source_port = Port(junction.id, "Junction", type=junction.type)
|
||||||
elif connection.source.interface is not None:
|
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")
|
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:
|
else:
|
||||||
source = owner.graph.blocks.get(connection.source.block or "")
|
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")
|
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:
|
if connection.target.junction is not None:
|
||||||
junction = owner.graph.junctions.get(connection.target.junction)
|
junction = owner.graph.junctions.get(connection.target.junction)
|
||||||
if junction is None:
|
if junction is None:
|
||||||
@@ -495,16 +622,44 @@ class GraphDocument:
|
|||||||
allows_multiple_connections=False,
|
allows_multiple_connections=False,
|
||||||
)
|
)
|
||||||
elif connection.target.interface is not None:
|
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")
|
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:
|
else:
|
||||||
target = owner.graph.blocks.get(connection.target.block or "")
|
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")
|
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)
|
target_port = next(
|
||||||
if not PortTypeRegistry.compatible(source_port.type, target_port.type):
|
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")
|
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_key = (
|
||||||
"source-junction"
|
"source-junction"
|
||||||
if connection.source.junction is not None
|
if connection.source.junction is not None
|
||||||
@@ -567,6 +722,8 @@ def clone_component(source: Component) -> Component:
|
|||||||
remap(connection.target),
|
remap(connection.target),
|
||||||
connection.name,
|
connection.name,
|
||||||
deepcopy(connection.properties),
|
deepcopy(connection.properties),
|
||||||
|
connection.type,
|
||||||
|
connection.causality,
|
||||||
)
|
)
|
||||||
for connection in current.graph.connections.values()
|
for connection in current.graph.connections.values()
|
||||||
for new_id in [str(uuid4())]
|
for new_id in [str(uuid4())]
|
||||||
@@ -599,30 +756,7 @@ def clone_component(source: Component) -> Component:
|
|||||||
name=current.name,
|
name=current.name,
|
||||||
x=current.x,
|
x=current.x,
|
||||||
y=current.y,
|
y=current.y,
|
||||||
inputs=[
|
ports=[Port.from_dict(port.to_dict()) for port in current.ports],
|
||||||
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
|
|
||||||
],
|
|
||||||
parameters=deepcopy(current.parameters),
|
parameters=deepcopy(current.parameters),
|
||||||
icon=Icon.from_dict(current.icon.to_dict()),
|
icon=Icon.from_dict(current.icon.to_dict()),
|
||||||
properties=deepcopy(current.properties),
|
properties=deepcopy(current.properties),
|
||||||
|
|||||||
47
BEdit/src/bedit/core/physical_types.py
Normal file
47
BEdit/src/bedit/core/physical_types.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
"""Editable suggestions for signal and parameter physical metadata.
|
||||||
|
|
||||||
|
These values populate editable combo boxes; documents may use values not listed
|
||||||
|
here. They are suggestions rather than a validation registry.
|
||||||
|
"""
|
||||||
|
|
||||||
|
VALUE_TYPES = ("real", "integer", "boolean", "string")
|
||||||
|
|
||||||
|
QUANTITIES = (
|
||||||
|
"",
|
||||||
|
"Angle",
|
||||||
|
"AngularVelocity",
|
||||||
|
"Current",
|
||||||
|
"Energy",
|
||||||
|
"Force",
|
||||||
|
"Frequency",
|
||||||
|
"Length",
|
||||||
|
"Mass",
|
||||||
|
"Power",
|
||||||
|
"Pressure",
|
||||||
|
"Temperature",
|
||||||
|
"Time",
|
||||||
|
"Torque",
|
||||||
|
"Velocity",
|
||||||
|
"Voltage",
|
||||||
|
)
|
||||||
|
|
||||||
|
UNITS = (
|
||||||
|
"",
|
||||||
|
"1",
|
||||||
|
"A",
|
||||||
|
"Hz",
|
||||||
|
"J",
|
||||||
|
"K",
|
||||||
|
"N",
|
||||||
|
"N.m",
|
||||||
|
"Pa",
|
||||||
|
"V",
|
||||||
|
"W",
|
||||||
|
"deg",
|
||||||
|
"kg",
|
||||||
|
"m",
|
||||||
|
"m/s",
|
||||||
|
"rad",
|
||||||
|
"rad/s",
|
||||||
|
"s",
|
||||||
|
)
|
||||||
@@ -14,6 +14,7 @@ class PortType:
|
|||||||
class PortTypeRegistry:
|
class PortTypeRegistry:
|
||||||
_types = {
|
_types = {
|
||||||
"signal": PortType("signal", "Signal", "A scalar signal connection"),
|
"signal": PortType("signal", "Signal", "A scalar signal connection"),
|
||||||
|
"power": PortType("power", "Power", "A two-variable power connection"),
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -28,5 +29,13 @@ class PortTypeRegistry:
|
|||||||
raise ValueError(f"Unknown port type: {type_id}") from error
|
raise ValueError(f"Unknown port type: {type_id}") from error
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def compatible(cls, first: str, second: str) -> bool:
|
def compatible(
|
||||||
return cls.get(first).accepts(cls.get(second))
|
cls,
|
||||||
|
first: str,
|
||||||
|
second: str,
|
||||||
|
first_domain: str = "",
|
||||||
|
second_domain: str = "",
|
||||||
|
) -> bool:
|
||||||
|
if not cls.get(first).accepts(cls.get(second)):
|
||||||
|
return False
|
||||||
|
return first != "power" or first_domain == second_domain
|
||||||
|
|||||||
49
BEdit/src/bedit/core/power_domains.py
Normal file
49
BEdit/src/bedit/core/power_domains.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
"""Editable power-port domain definitions used by the port editor."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PowerDomain:
|
||||||
|
id: str
|
||||||
|
display_name: str
|
||||||
|
effort: str
|
||||||
|
flow: str
|
||||||
|
|
||||||
|
|
||||||
|
POWER_DOMAINS: tuple[PowerDomain, ...] = (
|
||||||
|
PowerDomain("power", "Power", "p.e", "p.f"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def power_domain(domain_id: str) -> PowerDomain:
|
||||||
|
return next((domain for domain in POWER_DOMAINS if domain.id == domain_id), POWER_DOMAINS[0])
|
||||||
|
|
||||||
|
|
||||||
|
STANDARD_POWER_CAUSALITIES = (
|
||||||
|
"fixed flow out",
|
||||||
|
"fixed effort out",
|
||||||
|
"preferred flow out",
|
||||||
|
"preferred effort out",
|
||||||
|
"likes flow out",
|
||||||
|
"likes effort out",
|
||||||
|
"indifferent",
|
||||||
|
)
|
||||||
|
|
||||||
|
MULTI_CONNECTION_POWER_CAUSALITIES = (
|
||||||
|
"single effort in",
|
||||||
|
"single flow in",
|
||||||
|
)
|
||||||
|
|
||||||
|
POWER_CAUSALITIES = (
|
||||||
|
*STANDARD_POWER_CAUSALITIES,
|
||||||
|
*MULTI_CONNECTION_POWER_CAUSALITIES,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def power_causalities(allows_multiple_connections: bool = False) -> tuple[str, ...]:
|
||||||
|
"""Return the causalities available for one power port configuration."""
|
||||||
|
|
||||||
|
if allows_multiple_connections:
|
||||||
|
return POWER_CAUSALITIES
|
||||||
|
return STANDARD_POWER_CAUSALITIES
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
from bedit.core.simulation.service import Simulation
|
from bedit.core.simulation.service import Simulation
|
||||||
from bedit.core.simulation.openmodelica import OpenModelicaInterface
|
from bedit.core.simulation.openmodelica import ModelBuildResult, OpenModelicaInterface
|
||||||
from bedit.core.simulation.results import (
|
from bedit.core.simulation.results import (
|
||||||
BerSimulationResultsSerializer,
|
BerSimulationResultsSerializer,
|
||||||
JsonSimulationResultsSerializer,
|
JsonSimulationResultsSerializer,
|
||||||
SimulationExecutionResult,
|
SimulationExecutionResult,
|
||||||
|
SimulationGraph,
|
||||||
SimulationResults,
|
SimulationResults,
|
||||||
SimulationResultsSerializer,
|
SimulationResultsSerializer,
|
||||||
SimulationTrace,
|
SimulationTrace,
|
||||||
@@ -11,9 +12,11 @@ from bedit.core.simulation.results import (
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"OpenModelicaInterface",
|
"OpenModelicaInterface",
|
||||||
|
"ModelBuildResult",
|
||||||
"Simulation",
|
"Simulation",
|
||||||
"SimulationResults",
|
"SimulationResults",
|
||||||
"SimulationExecutionResult",
|
"SimulationExecutionResult",
|
||||||
|
"SimulationGraph",
|
||||||
"SimulationResultsSerializer",
|
"SimulationResultsSerializer",
|
||||||
"SimulationTrace",
|
"SimulationTrace",
|
||||||
"BerSimulationResultsSerializer",
|
"BerSimulationResultsSerializer",
|
||||||
|
|||||||
@@ -59,9 +59,7 @@ def build_id_list(graph: dict[str, Any]) -> dict[str, Any]:
|
|||||||
def visit(component: dict[str, Any]) -> None:
|
def visit(component: dict[str, Any]) -> None:
|
||||||
add(component, "component")
|
add(component, "component")
|
||||||
interface = component.get("interface", {})
|
interface = component.get("interface", {})
|
||||||
for port in interface.get("inputs", []):
|
for port in interface.get("ports", []):
|
||||||
add(port, "port")
|
|
||||||
for port in interface.get("outputs", []):
|
|
||||||
add(port, "port")
|
add(port, "port")
|
||||||
|
|
||||||
implementation = component.get("implementation", {})
|
implementation = component.get("implementation", {})
|
||||||
@@ -82,6 +80,7 @@ def emit_model(
|
|||||||
id_list: dict[str, Any],
|
id_list: dict[str, Any],
|
||||||
indent: int = 0,
|
indent: int = 0,
|
||||||
connection_counts: dict[str, int] | None = None,
|
connection_counts: dict[str, int] | None = None,
|
||||||
|
connection_signs: dict[str, list[float]] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Emit a component and its nested definitions as Modelica source."""
|
"""Emit a component and its nested definitions as Modelica source."""
|
||||||
|
|
||||||
@@ -94,33 +93,58 @@ def emit_model(
|
|||||||
implementation_kind = implementation.get("kind")
|
implementation_kind = implementation.get("kind")
|
||||||
nested_graph = implementation.get("graph", {})
|
nested_graph = implementation.get("graph", {})
|
||||||
port_counts = connection_counts or _interface_connection_counts(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":
|
if implementation_kind == "graph":
|
||||||
for block in nested_graph.get("blocks", []):
|
for block in nested_graph.get("blocks", []):
|
||||||
|
block_counts, block_signs = _block_connection_data(
|
||||||
|
nested_graph, block["id"]
|
||||||
|
)
|
||||||
lines.extend(
|
lines.extend(
|
||||||
emit_model(
|
emit_model(
|
||||||
block,
|
block,
|
||||||
{},
|
{},
|
||||||
indent + 1,
|
indent + 1,
|
||||||
_block_connection_counts(nested_graph, block["id"]),
|
block_counts,
|
||||||
|
block_signs,
|
||||||
)
|
)
|
||||||
.rstrip()
|
.rstrip()
|
||||||
.splitlines()
|
.splitlines()
|
||||||
)
|
)
|
||||||
|
|
||||||
interface = graph.get("interface", {})
|
interface = graph.get("interface", {})
|
||||||
for port in interface.get("inputs", []):
|
for port in interface.get("ports", []):
|
||||||
lines.append(_port_declaration(port, "input", indent + 1, macros))
|
direction = "output" if port.get("orientation") == "output" else "input"
|
||||||
for port in interface.get("outputs", []):
|
lines.append(_port_declaration(port, direction, indent + 1, macros))
|
||||||
lines.append(_port_declaration(port, "output", indent + 1, macros))
|
|
||||||
|
|
||||||
for parameter in graph.get("parameters", []):
|
for parameter in graph.get("parameters", []):
|
||||||
parameter_type = modelica_type(parameter.get("type", "real"))
|
parameter_type = modelica_type(parameter.get("type", "real"))
|
||||||
parameter_name = identifier(parameter["name"])
|
parameter_name = identifier(parameter["name"])
|
||||||
value = expand_bevalues(str(parameter.get("value", "0")), macros)
|
value = expand_bevalues(str(parameter.get("value", "0")), macros)
|
||||||
|
dimensions = _fixed_dimensions(parameter)
|
||||||
|
attributes = _quantity_unit_attributes(parameter)
|
||||||
lines.append(
|
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":
|
if implementation_kind == "graph":
|
||||||
@@ -134,8 +158,9 @@ def emit_model(
|
|||||||
f"{body_indent}{junction_type} {_junction_name(junction['id'])};"
|
f"{body_indent}{junction_type} {_junction_name(junction['id'])};"
|
||||||
)
|
)
|
||||||
|
|
||||||
lines.append(f"{indentation}equation")
|
|
||||||
if implementation_kind == "graph":
|
if implementation_kind == "graph":
|
||||||
|
lines.append(f"{indentation}equation")
|
||||||
|
|
||||||
blocks = {block["id"]: block for block in nested_graph.get("blocks", [])}
|
blocks = {block["id"]: block for block in nested_graph.get("blocks", [])}
|
||||||
junctions = {
|
junctions = {
|
||||||
junction["id"]: junction for junction in nested_graph.get("junctions", [])
|
junction["id"]: junction for junction in nested_graph.get("junctions", [])
|
||||||
@@ -148,11 +173,26 @@ def emit_model(
|
|||||||
target = _endpoint_expression(
|
target = _endpoint_expression(
|
||||||
connection["target"], graph, blocks, junctions, endpoint_indices
|
connection["target"], graph, blocks, junctions, endpoint_indices
|
||||||
)
|
)
|
||||||
# Connector types may require different equations in future.
|
if connection.get("type") == "power":
|
||||||
lines.append(f"{body_indent}{target} = {source};")
|
lines.append(f"{body_indent}connect({source}, {target});")
|
||||||
|
else:
|
||||||
|
lines.append(f"{body_indent}{target} = {source};")
|
||||||
else:
|
else:
|
||||||
|
initial_equations = str(
|
||||||
|
implementation.get("source", {}).get("initialEquations", "")
|
||||||
|
)
|
||||||
|
initial_equations = expand_bevalues(initial_equations, macros)
|
||||||
equations = str(implementation.get("source", {}).get("equations", ""))
|
equations = str(implementation.get("source", {}).get("equations", ""))
|
||||||
equations = expand_bevalues(equations, macros)
|
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(
|
lines.extend(
|
||||||
f"{body_indent}{line}" if line.strip() else ""
|
f"{body_indent}{line}" if line.strip() else ""
|
||||||
for line in equations.splitlines()
|
for line in equations.splitlines()
|
||||||
@@ -161,7 +201,6 @@ def emit_model(
|
|||||||
lines.append(f"{indentation}end {model_name};")
|
lines.append(f"{indentation}end {model_name};")
|
||||||
return "\n".join(lines) + "\n"
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
def cleanup_graph(graph: dict[str, Any]) -> dict[str, Any]:
|
def cleanup_graph(graph: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Remove annotations and UI-only data from a serialized component tree."""
|
"""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(
|
def _port_declaration(
|
||||||
port: dict[str, Any], direction: str, indent: int, macros: dict[str, str]
|
port: dict[str, Any], direction: str, indent: int, macros: dict[str, str]
|
||||||
) -> str:
|
) -> str:
|
||||||
port_type = modelica_type(port.get("type", "signal"))
|
|
||||||
indentation = "\t" * indent
|
indentation = "\t" * indent
|
||||||
port_name = identifier(port["name"])
|
port_name = identifier(port["name"])
|
||||||
dimension = f"[${port_name}_N$]" if port.get("multipleConnections", False) else ""
|
dimensions = _port_dimensions(port, port_name)
|
||||||
declaration = f"{indentation}{direction} {port_type} {port_name}{dimension};"
|
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)
|
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(
|
def _endpoint_expression(
|
||||||
endpoint: dict[str, Any],
|
endpoint: dict[str, Any],
|
||||||
owner: dict[str, Any],
|
owner: dict[str, Any],
|
||||||
@@ -244,16 +321,21 @@ def _index_array_endpoint(
|
|||||||
return f"{expression}[{endpoint_indices[key]}]"
|
return f"{expression}[{endpoint_indices[key]}]"
|
||||||
|
|
||||||
|
|
||||||
def _block_connection_counts(
|
def _block_connection_data(
|
||||||
graph: dict[str, Any], block_id: str
|
graph: dict[str, Any], block_id: str
|
||||||
) -> dict[str, int]:
|
) -> tuple[dict[str, int], dict[str, list[float]]]:
|
||||||
counts: dict[str, int] = {}
|
counts: dict[str, int] = {}
|
||||||
|
signs: dict[str, list[float]] = {}
|
||||||
for connection in graph.get("connections", []):
|
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"):
|
if endpoint.get("block") == block_id and endpoint.get("port"):
|
||||||
port_id = endpoint["port"]
|
port_id = endpoint["port"]
|
||||||
counts[port_id] = counts.get(port_id, 0) + 1
|
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]:
|
def _interface_connection_counts(component: dict[str, Any]) -> dict[str, int]:
|
||||||
@@ -269,19 +351,56 @@ def _interface_connection_counts(component: dict[str, Any]) -> dict[str, int]:
|
|||||||
return counts
|
return counts
|
||||||
|
|
||||||
|
|
||||||
def _port_count_macros(
|
def _interface_connection_signs(component: dict[str, Any]) -> dict[str, list[float]]:
|
||||||
component: dict[str, Any], connection_counts: dict[str, int]
|
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]:
|
) -> dict[str, str]:
|
||||||
macros: dict[str, str] = {}
|
macros: dict[str, str] = {}
|
||||||
interface = component.get("interface", {})
|
interface = component.get("interface", {})
|
||||||
for port in (*interface.get("inputs", []), *interface.get("outputs", [])):
|
for port in interface.get("ports", []):
|
||||||
if port.get("multipleConnections", False):
|
if port.get("multipleConnections", False):
|
||||||
macros[f"{identifier(port['name'])}_N"] = str(
|
macro_name = identifier(port["name"])
|
||||||
connection_counts.get(port["id"], 0)
|
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
|
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:
|
def expand_bevalues(text: str, values: dict[str, str]) -> str:
|
||||||
"""Replace BEdit ``$name$`` macros and reject unresolved composer values."""
|
"""Replace BEdit ``$name$`` macros and reject unresolved composer values."""
|
||||||
|
|
||||||
@@ -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]:
|
def _find_port(component: dict[str, Any], port_id: str) -> dict[str, Any]:
|
||||||
interface = component.get("interface", {})
|
interface = component.get("interface", {})
|
||||||
ports = [*interface.get("inputs", []), *interface.get("outputs", [])]
|
for port in interface.get("ports", []):
|
||||||
for port in ports:
|
|
||||||
if port.get("id") == port_id:
|
if port.get("id") == port_id:
|
||||||
return port
|
return port
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
@@ -48,6 +50,15 @@ class SimulationMessage:
|
|||||||
text: 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:
|
class OpenModelicaInterface:
|
||||||
"""Asynchronous, persistent interface to one OpenModelica session.
|
"""Asynchronous, persistent interface to one OpenModelica session.
|
||||||
|
|
||||||
@@ -92,10 +103,42 @@ class OpenModelicaInterface:
|
|||||||
"""Load and build one composed model as an ordered worker operation."""
|
"""Load and build one composed model as an ordered worker operation."""
|
||||||
|
|
||||||
def operation(omc, _temp_dir: Path):
|
def operation(omc, _temp_dir: Path):
|
||||||
loaded = omc.sendExpression(f"loadString({json.dumps(model)})")
|
capture = _LogCapture()
|
||||||
if loaded is not True:
|
ompython_logger = logging.getLogger("OMPython")
|
||||||
raise RuntimeError("OpenModelica could not load the composed model")
|
previous_propagate = ompython_logger.propagate
|
||||||
return omc.sendExpression(f"buildModel({model_name})")
|
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)
|
self._submit("build model", operation, callback, error_callback)
|
||||||
|
|
||||||
@@ -247,6 +290,51 @@ class OpenModelicaInterface:
|
|||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
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:
|
def _deliver_callback(callback: Callable[[Any], None], value: Any) -> None:
|
||||||
try:
|
try:
|
||||||
callback(value)
|
callback(value)
|
||||||
@@ -334,16 +422,140 @@ def _run_model_with_tcp(
|
|||||||
f"OpenModelica did not create the expected result file {result_path.name!r}"
|
f"OpenModelica did not create the expected result file {result_path.name!r}"
|
||||||
)
|
)
|
||||||
data = load_openmodelica_csv(result_path)
|
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(
|
log.info(
|
||||||
"Loaded %d result columns from %s", len(data), result_path.name
|
"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 SimulationExecutionResult(
|
||||||
return_code=int(return_code),
|
return_code=int(return_code),
|
||||||
result_file=str(result_path),
|
result_file=str(result_path),
|
||||||
data=data,
|
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(
|
def _accept_simulation_connection(
|
||||||
server: socket.socket, command_finished: Event
|
server: socket.socket, command_finished: Event
|
||||||
) -> socket.socket:
|
) -> socket.socket:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import zlib
|
|||||||
from dataclasses import asdict, dataclass, field
|
from dataclasses import asdict, dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
import msgpack
|
import msgpack
|
||||||
|
|
||||||
@@ -25,6 +26,16 @@ class SimulationTrace:
|
|||||||
properties: dict[str, Any] = field(default_factory=dict)
|
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
|
@dataclass
|
||||||
class SimulationResults:
|
class SimulationResults:
|
||||||
"""Serializable state displayed by the standalone simulation window."""
|
"""Serializable state displayed by the standalone simulation window."""
|
||||||
@@ -33,7 +44,10 @@ class SimulationResults:
|
|||||||
status: dict[str, Any] = field(default_factory=dict)
|
status: dict[str, Any] = field(default_factory=dict)
|
||||||
messages: list[dict[str, str]] = field(default_factory=list)
|
messages: list[dict[str, str]] = field(default_factory=list)
|
||||||
data: dict[str, list[float]] = field(default_factory=dict)
|
data: dict[str, list[float]] = field(default_factory=dict)
|
||||||
traces: list[SimulationTrace] = field(default_factory=list)
|
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)
|
metadata: dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, Any]:
|
def to_dict(self) -> dict[str, Any]:
|
||||||
@@ -44,7 +58,11 @@ class SimulationResults:
|
|||||||
"status": dict(self.status),
|
"status": dict(self.status),
|
||||||
"messages": [dict(message) for message in self.messages],
|
"messages": [dict(message) for message in self.messages],
|
||||||
"data": {name: list(values) for name, values in self.data.items()},
|
"data": {name: list(values) for name, values in self.data.items()},
|
||||||
"traces": [asdict(trace) for trace in self.traces],
|
"signalMetadata": {
|
||||||
|
name: dict(metadata)
|
||||||
|
for name, metadata in self.signal_metadata.items()
|
||||||
|
},
|
||||||
|
"graphs": [asdict(graph) for graph in self.graphs],
|
||||||
"metadata": dict(self.metadata),
|
"metadata": dict(self.metadata),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +73,17 @@ class SimulationResults:
|
|||||||
if data.get("version") != RESULTS_VERSION:
|
if data.get("version") != RESULTS_VERSION:
|
||||||
raise ValueError(f"Unsupported simulation-results version: {data.get('version')!r}")
|
raise ValueError(f"Unsupported simulation-results version: {data.get('version')!r}")
|
||||||
try:
|
try:
|
||||||
traces = [SimulationTrace(**trace) for trace in data.get("traces", [])]
|
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(
|
return cls(
|
||||||
model_name=str(data.get("modelName", "")),
|
model_name=str(data.get("modelName", "")),
|
||||||
status=dict(data.get("status", {})),
|
status=dict(data.get("status", {})),
|
||||||
@@ -64,10 +92,19 @@ class SimulationResults:
|
|||||||
str(name): [float(value) for value in values]
|
str(name): [float(value) for value in values]
|
||||||
for name, values in dict(data.get("data", {})).items()
|
for name, values in dict(data.get("data", {})).items()
|
||||||
},
|
},
|
||||||
traces=traces,
|
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", {})),
|
metadata=dict(data.get("metadata", {})),
|
||||||
)
|
)
|
||||||
except (TypeError, ValueError) as error:
|
except (KeyError, TypeError, ValueError) as error:
|
||||||
raise ValueError("Malformed simulation-results data") from error
|
raise ValueError("Malformed simulation-results data") from error
|
||||||
|
|
||||||
|
|
||||||
@@ -78,6 +115,7 @@ class SimulationExecutionResult:
|
|||||||
return_code: int
|
return_code: int
|
||||||
result_file: str
|
result_file: str
|
||||||
data: dict[str, list[float]]
|
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]]:
|
def load_openmodelica_csv(path: str | Path) -> dict[str, list[float]]:
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ from collections.abc import Callable
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from bedit.core.application_log import get_logger
|
from bedit.core.application_log import get_logger
|
||||||
|
from bedit.core.bond_graph import infer_causality
|
||||||
from bedit.core.simulation.composer import compose_graph
|
from bedit.core.simulation.composer import compose_graph
|
||||||
from bedit.core.simulation.openmodelica import (
|
from bedit.core.simulation.openmodelica import (
|
||||||
ErrorCallback,
|
ErrorCallback,
|
||||||
|
ModelBuildResult,
|
||||||
OpenModelicaInterface,
|
OpenModelicaInterface,
|
||||||
ResultCallback,
|
ResultCallback,
|
||||||
SimulationMessage,
|
SimulationMessage,
|
||||||
@@ -43,22 +45,45 @@ class Simulation:
|
|||||||
graph: dict[str, Any],
|
graph: dict[str, Any],
|
||||||
callback: Callable[[str], None] | None = None,
|
callback: Callable[[str], None] | None = None,
|
||||||
error_callback: ErrorCallback | None = None,
|
error_callback: ErrorCallback | None = None,
|
||||||
|
message_callback: Callable[[SimulationMessage], None] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Compose and retain the active graph's Modelica representation."""
|
"""Compose and retain the active graph's Modelica representation."""
|
||||||
|
|
||||||
self.model_path = None
|
self.model_path = None
|
||||||
|
self.compose_source(graph)
|
||||||
# Create openmodelica model
|
|
||||||
result = compose_graph(graph)
|
|
||||||
self.last_composition_input = result.graph
|
|
||||||
self.id_list = result.objects_by_id
|
|
||||||
self.last_composition_output = result.modelica
|
|
||||||
self.model_name = result.model_name
|
|
||||||
|
|
||||||
def _model_compiled(result):
|
def _model_compiled(result):
|
||||||
log.info("Compiling OK: %s", 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:
|
try:
|
||||||
self.model_path = str(result[0])
|
self.model_path = str(result.build_result[0])
|
||||||
except (IndexError, TypeError) as error:
|
except (IndexError, TypeError) as error:
|
||||||
failure = RuntimeError(
|
failure = RuntimeError(
|
||||||
f"OpenModelica returned an invalid build result: {result!r}"
|
f"OpenModelica returned an invalid build result: {result!r}"
|
||||||
@@ -69,6 +94,14 @@ class Simulation:
|
|||||||
else:
|
else:
|
||||||
log.error("%s", failure)
|
log.error("%s", failure)
|
||||||
return
|
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:
|
if callback is not None:
|
||||||
callback(self.model_path)
|
callback(self.model_path)
|
||||||
|
|
||||||
@@ -79,6 +112,18 @@ class Simulation:
|
|||||||
error_callback,
|
error_callback,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def compose_source(self, graph: dict[str, Any]) -> tuple[str, str]:
|
||||||
|
"""Compose Modelica source without asking OpenModelica to build it."""
|
||||||
|
|
||||||
|
infer_causality(graph)
|
||||||
|
result = compose_graph(graph)
|
||||||
|
self.last_composition_input = result.graph
|
||||||
|
self.id_list = result.objects_by_id
|
||||||
|
self.last_composition_output = result.modelica
|
||||||
|
self.model_name = result.model_name
|
||||||
|
# log.info("Composed OpenModelica model:\n%s", result.modelica)
|
||||||
|
return result.model_name, result.modelica
|
||||||
|
|
||||||
def run_simulation(
|
def run_simulation(
|
||||||
self,
|
self,
|
||||||
graph: dict[str, Any],
|
graph: dict[str, Any],
|
||||||
@@ -114,7 +159,7 @@ class Simulation:
|
|||||||
error_callback,
|
error_callback,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.compose(graph, run_model, error_callback)
|
self.compose(graph, run_model, error_callback, message_callback)
|
||||||
|
|
||||||
def get_progress(self) -> SimulationProgress | None:
|
def get_progress(self) -> SimulationProgress | None:
|
||||||
"""Return the most recently received simulation status."""
|
"""Return the most recently received simulation status."""
|
||||||
|
|||||||
BIN
BEdit/src/bedit/data/libraries/bondgraph.beb
Normal file
BIN
BEdit/src/bedit/data/libraries/bondgraph.beb
Normal file
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,6 @@
|
|||||||
import sys
|
import sys
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from PySide6.QtCore import QCoreApplication, Qt
|
from PySide6.QtCore import QCoreApplication, Qt
|
||||||
from PySide6.QtGui import QColor, QPalette
|
from PySide6.QtGui import QColor, QPalette
|
||||||
@@ -38,14 +40,26 @@ def apply_light_theme(app: QApplication) -> None:
|
|||||||
app.setPalette(palette)
|
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.setApplicationName("BEdit")
|
||||||
QCoreApplication.setOrganizationName("BEdit")
|
QCoreApplication.setOrganizationName("BEdit")
|
||||||
QCoreApplication.setApplicationVersion("0.1.0")
|
QCoreApplication.setApplicationVersion("0.1.0")
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(list(sys.argv if argv is None else argv))
|
||||||
app.setApplicationDisplayName("BEdit")
|
app.setApplicationDisplayName("BEdit")
|
||||||
|
|
||||||
apply_light_theme(app)
|
apply_light_theme(app)
|
||||||
window = MainWindow()
|
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()
|
window.show()
|
||||||
return app.exec()
|
return app.exec()
|
||||||
|
|||||||
@@ -304,6 +304,20 @@ class RenameConnectionCommand(QUndoCommand):
|
|||||||
self.controller._rename_connection(self.owner_id, self.connection_id, self.old)
|
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):
|
class DeleteSelectionCommand(QUndoCommand):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -311,24 +325,21 @@ class DeleteSelectionCommand(QUndoCommand):
|
|||||||
owner_id: str | None,
|
owner_id: str | None,
|
||||||
blocks: dict[str, Component],
|
blocks: dict[str, Component],
|
||||||
connections: dict[str, Connection],
|
connections: dict[str, Connection],
|
||||||
inputs: list[Port],
|
ports: list[Port],
|
||||||
outputs: list[Port],
|
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__("Delete selection")
|
super().__init__("Delete selection")
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.owner_id = owner_id
|
self.owner_id = owner_id
|
||||||
self.blocks = blocks
|
self.blocks = blocks
|
||||||
self.connections = connections
|
self.connections = connections
|
||||||
self.inputs = inputs
|
self.ports = ports
|
||||||
self.outputs = outputs
|
|
||||||
|
|
||||||
def redo(self) -> None:
|
def redo(self) -> None:
|
||||||
self.controller._delete_items(
|
self.controller._delete_items(
|
||||||
self.owner_id,
|
self.owner_id,
|
||||||
set(self.blocks),
|
set(self.blocks),
|
||||||
set(self.connections),
|
set(self.connections),
|
||||||
{port.id for port in self.inputs},
|
{port.id for port in self.ports},
|
||||||
{port.id for port in self.outputs},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def undo(self) -> None:
|
def undo(self) -> None:
|
||||||
@@ -336,8 +347,7 @@ class DeleteSelectionCommand(QUndoCommand):
|
|||||||
self.owner_id,
|
self.owner_id,
|
||||||
self.blocks,
|
self.blocks,
|
||||||
self.connections,
|
self.connections,
|
||||||
self.inputs,
|
self.ports,
|
||||||
self.outputs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -361,7 +371,6 @@ class PasteSelectionCommand(QUndoCommand):
|
|||||||
self.blocks,
|
self.blocks,
|
||||||
self.connections,
|
self.connections,
|
||||||
[],
|
[],
|
||||||
[],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def undo(self) -> None:
|
def undo(self) -> None:
|
||||||
@@ -370,7 +379,6 @@ class PasteSelectionCommand(QUndoCommand):
|
|||||||
set(self.blocks),
|
set(self.blocks),
|
||||||
set(self.connections),
|
set(self.connections),
|
||||||
set(),
|
set(),
|
||||||
set(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from bedit.gui.controllers.commands import (
|
|||||||
MoveInterfacePortCommand,
|
MoveInterfacePortCommand,
|
||||||
PasteSelectionCommand,
|
PasteSelectionCommand,
|
||||||
RenameConnectionCommand,
|
RenameConnectionCommand,
|
||||||
|
RenameDocumentCommand,
|
||||||
RenameInterfacePortCommand,
|
RenameInterfacePortCommand,
|
||||||
ReplaceSourceCommand,
|
ReplaceSourceCommand,
|
||||||
RotateComponentsCommand,
|
RotateComponentsCommand,
|
||||||
@@ -40,14 +41,17 @@ from bedit.core.model import (
|
|||||||
Port,
|
Port,
|
||||||
clone_component,
|
clone_component,
|
||||||
)
|
)
|
||||||
|
from bedit.core.bond_graph import infer_causality
|
||||||
from bedit.core.simulation import Simulation
|
from bedit.core.simulation import Simulation
|
||||||
from bedit.core.port_types import PortTypeRegistry
|
from bedit.core.port_types import PortTypeRegistry
|
||||||
from bedit.core.serializer import DocumentSerializer
|
from bedit.core.serializer import DocumentSerializer
|
||||||
|
from bedit.gui.preferences import application_settings
|
||||||
|
|
||||||
|
|
||||||
class DocumentController(QObject):
|
class DocumentController(QObject):
|
||||||
documentReset = Signal()
|
documentReset = Signal()
|
||||||
documentOpenedChanged = Signal(bool)
|
documentOpenedChanged = Signal(bool)
|
||||||
|
documentNameChanged = Signal(str)
|
||||||
activeGraphChanged = Signal()
|
activeGraphChanged = Signal()
|
||||||
componentAdded = Signal(str)
|
componentAdded = Signal(str)
|
||||||
componentRemoved = Signal(str)
|
componentRemoved = Signal(str)
|
||||||
@@ -168,7 +172,13 @@ class DocumentController(QObject):
|
|||||||
name=self._available_component_name(base_name, self.document.roots.values(), number),
|
name=self._available_component_name(base_name, self.document.roots.values(), number),
|
||||||
implementation_kind=kind,
|
implementation_kind=kind,
|
||||||
icon=Icon(text="Graph" if kind == "graph" else "Text"),
|
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.undo_stack.push(AddComponentCommand(self, None, component))
|
||||||
self.activate_component(component.id)
|
self.activate_component(component.id)
|
||||||
@@ -187,7 +197,13 @@ class DocumentController(QObject):
|
|||||||
name=self._available_component_name(base_name, owner.graph.blocks.values(), number),
|
name=self._available_component_name(base_name, owner.graph.blocks.values(), number),
|
||||||
implementation_kind=kind,
|
implementation_kind=kind,
|
||||||
icon=Icon(text="Graph" if kind == "graph" else "Text"),
|
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))
|
self.undo_stack.push(AddComponentCommand(self, owner_id, component))
|
||||||
return component.id
|
return component.id
|
||||||
@@ -214,10 +230,19 @@ class DocumentController(QObject):
|
|||||||
{component_id: component},
|
{component_id: component},
|
||||||
connections,
|
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:
|
def add_component_copy(self, source: Component, position: QPointF) -> str:
|
||||||
if self.active_component is None or self.active_component.implementation_kind != "graph":
|
if self.active_component is None or self.active_component.implementation_kind != "graph":
|
||||||
raise ValueError("Open a graph component before placing components")
|
raise ValueError("Open a graph component before placing components")
|
||||||
@@ -273,7 +298,12 @@ class DocumentController(QObject):
|
|||||||
target_port = self._port_for_endpoint(target, "target")
|
target_port = self._port_for_endpoint(target, "target")
|
||||||
if source_port is None or target_port is None:
|
if source_port is None or target_port is None:
|
||||||
raise ValueError("A connection endpoint no longer exists")
|
raise ValueError("A connection endpoint no longer exists")
|
||||||
if not PortTypeRegistry.compatible(source_port.type, target_port.type):
|
if not PortTypeRegistry.compatible(
|
||||||
|
source_port.type,
|
||||||
|
target_port.type,
|
||||||
|
source_port.domain,
|
||||||
|
target_port.domain,
|
||||||
|
):
|
||||||
raise ValueError(f"Cannot connect {source_port.type!r} to {target_port.type!r}")
|
raise ValueError(f"Cannot connect {source_port.type!r} to {target_port.type!r}")
|
||||||
if not self.endpoint_accepts_connection(source, "source"):
|
if not self.endpoint_accepts_connection(source, "source"):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -290,6 +320,7 @@ class DocumentController(QObject):
|
|||||||
properties={
|
properties={
|
||||||
"waypoints": [{"x": point.x(), "y": point.y()} for point in (waypoints or [])],
|
"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))
|
self.undo_stack.push(AddConnectionCommand(self, self.active_component_id, connection))
|
||||||
return connection.id
|
return connection.id
|
||||||
@@ -307,6 +338,8 @@ class DocumentController(QObject):
|
|||||||
if original is None:
|
if original is None:
|
||||||
raise ValueError("The connection no longer exists")
|
raise ValueError("The connection no longer exists")
|
||||||
port_type = self.connection_port_type(original)
|
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)
|
junction = Junction(str(uuid4()), position.x(), position.y(), port_type)
|
||||||
first_properties = deepcopy(original.properties)
|
first_properties = deepcopy(original.properties)
|
||||||
first_properties["waypoints"] = [
|
first_properties["waypoints"] = [
|
||||||
@@ -321,6 +354,8 @@ class DocumentController(QObject):
|
|||||||
Endpoint(junction=junction.id),
|
Endpoint(junction=junction.id),
|
||||||
original.name,
|
original.name,
|
||||||
first_properties,
|
first_properties,
|
||||||
|
original.type,
|
||||||
|
original.causality,
|
||||||
)
|
)
|
||||||
second = Connection(
|
second = Connection(
|
||||||
str(uuid4()),
|
str(uuid4()),
|
||||||
@@ -328,6 +363,8 @@ class DocumentController(QObject):
|
|||||||
original.target,
|
original.target,
|
||||||
"",
|
"",
|
||||||
second_properties,
|
second_properties,
|
||||||
|
original.type,
|
||||||
|
original.causality,
|
||||||
)
|
)
|
||||||
self.undo_stack.push(
|
self.undo_stack.push(
|
||||||
SplitConnectionCommand(
|
SplitConnectionCommand(
|
||||||
@@ -425,8 +462,16 @@ class DocumentController(QObject):
|
|||||||
component = self.active_component
|
component = self.active_component
|
||||||
if component is None or component.implementation_kind != "graph":
|
if component is None or component.implementation_kind != "graph":
|
||||||
raise ValueError("Open a graph component before composing")
|
raise ValueError("Open a graph component before composing")
|
||||||
|
self._infer_active_graph_causality(component)
|
||||||
self.simulation.compose(component.to_dict())
|
self.simulation.compose(component.to_dict())
|
||||||
|
|
||||||
|
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(
|
def run_simulation(
|
||||||
self,
|
self,
|
||||||
progress_callback=None,
|
progress_callback=None,
|
||||||
@@ -437,6 +482,7 @@ class DocumentController(QObject):
|
|||||||
component = self.active_component
|
component = self.active_component
|
||||||
if component is None or component.implementation_kind != "graph":
|
if component is None or component.implementation_kind != "graph":
|
||||||
raise ValueError("Open a graph component before running a simulation")
|
raise ValueError("Open a graph component before running a simulation")
|
||||||
|
self._infer_active_graph_causality(component)
|
||||||
self.simulation.run_simulation(
|
self.simulation.run_simulation(
|
||||||
component.to_dict(),
|
component.to_dict(),
|
||||||
progress_callback,
|
progress_callback,
|
||||||
@@ -445,6 +491,37 @@ class DocumentController(QObject):
|
|||||||
error_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:
|
def set_route_waypoints(self, item_kind: str, item_id: str, waypoints: list[QPointF]) -> None:
|
||||||
item = (
|
item = (
|
||||||
self.active_graph.connections
|
self.active_graph.connections
|
||||||
@@ -554,12 +631,26 @@ class DocumentController(QObject):
|
|||||||
allows_multiple_connections=role == "source",
|
allows_multiple_connections=role == "source",
|
||||||
)
|
)
|
||||||
if endpoint.interface is not None:
|
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:
|
else:
|
||||||
component = owner.graph.blocks.get(endpoint.block or "")
|
component = owner.graph.blocks.get(endpoint.block or "")
|
||||||
if component is None:
|
if component is None:
|
||||||
return 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(
|
return next(
|
||||||
(port for port in ports if port.id == (endpoint.interface or endpoint.port)), None
|
(port for port in ports if port.id == (endpoint.interface or endpoint.port)), None
|
||||||
)
|
)
|
||||||
@@ -583,12 +674,13 @@ class DocumentController(QObject):
|
|||||||
component = self.active_component
|
component = self.active_component
|
||||||
if component is None or component.implementation_kind != "graph":
|
if component is None or component.implementation_kind != "graph":
|
||||||
raise ValueError("Open a graph component before adding an interface")
|
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(
|
port = Port(
|
||||||
id=f"{direction}-{uuid4().hex[:8]}",
|
id=f"{direction}-{uuid4().hex[:8]}",
|
||||||
name=f"{direction.title()} {len(ports) + 1}",
|
name=f"{direction.title()} {len(ports) + 1}",
|
||||||
x=position.x(),
|
x=position.x(),
|
||||||
y=position.y(),
|
y=position.y(),
|
||||||
|
orientation=direction,
|
||||||
)
|
)
|
||||||
self.undo_stack.push(AddInterfacePortCommand(self, component.id, direction, port))
|
self.undo_stack.push(AddInterfacePortCommand(self, component.id, direction, port))
|
||||||
return port.id
|
return port.id
|
||||||
@@ -603,7 +695,7 @@ class DocumentController(QObject):
|
|||||||
owner = self.active_component
|
owner = self.active_component
|
||||||
if owner is None:
|
if owner is None:
|
||||||
return
|
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:
|
if port is not None and port.name != name:
|
||||||
self.undo_stack.push(
|
self.undo_stack.push(
|
||||||
RenameInterfacePortCommand(self, owner.id, port_id, port.name, name)
|
RenameInterfacePortCommand(self, owner.id, port_id, port.name, name)
|
||||||
@@ -634,19 +726,26 @@ class DocumentController(QObject):
|
|||||||
|
|
||||||
def replace_active_text_definition(
|
def replace_active_text_definition(
|
||||||
self,
|
self,
|
||||||
inputs: list[Port],
|
ports: list[Port],
|
||||||
outputs: list[Port],
|
declarations: str,
|
||||||
|
initial_equations: str,
|
||||||
equations: str,
|
equations: str,
|
||||||
parameters: list[Parameter],
|
parameters: list[Parameter],
|
||||||
) -> None:
|
) -> None:
|
||||||
component = self.active_component
|
component = self.active_component
|
||||||
if component is None or component.implementation_kind != "text":
|
if component is None or component.implementation_kind != "text":
|
||||||
raise ValueError("Only text-defined components can be edited here")
|
raise ValueError("Only text-defined components can be edited here")
|
||||||
input_ids = [port.id for port in inputs]
|
port_ids = [port.id for port in ports]
|
||||||
output_ids = [port.id for port in outputs]
|
input_ids = {
|
||||||
if len(set(input_ids)) != len(input_ids) or len(set(output_ids)) != len(output_ids):
|
port.id for port in ports if port.orientation in {"input", "indifferent"}
|
||||||
raise ValueError("Input and output IDs must be unique")
|
}
|
||||||
if any(not port.name.strip() for port in (*inputs, *outputs)):
|
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")
|
raise ValueError("Every port must have a name")
|
||||||
parameter_ids = [parameter.id for parameter in parameters]
|
parameter_ids = [parameter.id for parameter in parameters]
|
||||||
if len(set(parameter_ids)) != len(parameter_ids):
|
if len(set(parameter_ids)) != len(parameter_ids):
|
||||||
@@ -666,30 +765,29 @@ class DocumentController(QObject):
|
|||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
connection.source.block == component.id
|
connection.source.block == component.id
|
||||||
and connection.source.port not in output_ids
|
and connection.source.port not in source_ids
|
||||||
):
|
):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Output {connection.source.port!r} is still connected in the containing graph"
|
f"Output {connection.source.port!r} is still connected in the containing graph"
|
||||||
)
|
)
|
||||||
old = {
|
old = {
|
||||||
"inputs": [port.to_dict() for port in component.inputs],
|
"ports": [port.to_dict() for port in component.ports],
|
||||||
"outputs": [port.to_dict() for port in component.outputs],
|
|
||||||
"source": deepcopy(component.source),
|
"source": deepcopy(component.source),
|
||||||
"parameters": [parameter.to_dict() for parameter in component.parameters],
|
"parameters": [parameter.to_dict() for parameter in component.parameters],
|
||||||
}
|
}
|
||||||
new = {
|
new = {
|
||||||
"inputs": [port.to_dict() for port in inputs],
|
"ports": [port.to_dict() for port in ports],
|
||||||
"outputs": [port.to_dict() for port in outputs],
|
|
||||||
"source": {
|
"source": {
|
||||||
"equations": equations,
|
"equations": equations,
|
||||||
|
"declarations": declarations,
|
||||||
|
"initialEquations": initial_equations,
|
||||||
},
|
},
|
||||||
"parameters": [parameter.to_dict() for parameter in parameters],
|
"parameters": [parameter.to_dict() for parameter in parameters],
|
||||||
}
|
}
|
||||||
if old != new:
|
if old != new:
|
||||||
candidate = deepcopy(self.document)
|
candidate = deepcopy(self.document)
|
||||||
candidate_component = candidate.find_component(component.id)
|
candidate_component = candidate.find_component(component.id)
|
||||||
candidate_component.inputs = deepcopy(inputs)
|
candidate_component.ports = deepcopy(ports)
|
||||||
candidate_component.outputs = deepcopy(outputs)
|
|
||||||
candidate_component.source = deepcopy(new["source"])
|
candidate_component.source = deepcopy(new["source"])
|
||||||
candidate_component.parameters = deepcopy(parameters)
|
candidate_component.parameters = deepcopy(parameters)
|
||||||
candidate.validate()
|
candidate.validate()
|
||||||
@@ -700,8 +798,7 @@ class DocumentController(QObject):
|
|||||||
component_id: str,
|
component_id: str,
|
||||||
name: str,
|
name: str,
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
inputs: list[Port],
|
ports: list[Port],
|
||||||
outputs: list[Port],
|
|
||||||
show_subtree: bool,
|
show_subtree: bool,
|
||||||
show_name: bool,
|
show_name: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -716,8 +813,7 @@ class DocumentController(QObject):
|
|||||||
old = {
|
old = {
|
||||||
"name": component.name,
|
"name": component.name,
|
||||||
"icon": component.icon.to_dict(),
|
"icon": component.icon.to_dict(),
|
||||||
"inputs": [port.to_dict() for port in component.inputs],
|
"ports": [port.to_dict() for port in component.ports],
|
||||||
"outputs": [port.to_dict() for port in component.outputs],
|
|
||||||
"show_subtree": component.show_subtree_in_library,
|
"show_subtree": component.show_subtree_in_library,
|
||||||
"properties": deepcopy(component.properties),
|
"properties": deepcopy(component.properties),
|
||||||
}
|
}
|
||||||
@@ -729,8 +825,7 @@ class DocumentController(QObject):
|
|||||||
new = {
|
new = {
|
||||||
"name": name,
|
"name": name,
|
||||||
"icon": icon.to_dict(),
|
"icon": icon.to_dict(),
|
||||||
"inputs": [port.to_dict() for port in inputs],
|
"ports": [port.to_dict() for port in ports],
|
||||||
"outputs": [port.to_dict() for port in outputs],
|
|
||||||
"show_subtree": show_subtree,
|
"show_subtree": show_subtree,
|
||||||
"properties": properties,
|
"properties": properties,
|
||||||
}
|
}
|
||||||
@@ -739,8 +834,7 @@ class DocumentController(QObject):
|
|||||||
candidate_component = candidate.find_component(component_id)
|
candidate_component = candidate.find_component(component_id)
|
||||||
candidate_component.name = name
|
candidate_component.name = name
|
||||||
candidate_component.icon = Icon.from_dict(icon.to_dict())
|
candidate_component.icon = Icon.from_dict(icon.to_dict())
|
||||||
candidate_component.inputs = deepcopy(inputs)
|
candidate_component.ports = deepcopy(ports)
|
||||||
candidate_component.outputs = deepcopy(outputs)
|
|
||||||
candidate_component.properties = deepcopy(properties)
|
candidate_component.properties = deepcopy(properties)
|
||||||
candidate.validate()
|
candidate.validate()
|
||||||
self.undo_stack.push(EditComponentAppearanceCommand(self, component_id, old, new))
|
self.undo_stack.push(EditComponentAppearanceCommand(self, component_id, old, new))
|
||||||
@@ -810,16 +904,19 @@ class DocumentController(QObject):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def edit_component_ports(
|
def edit_component_ports(self, component_id: str, ports: list[Port]) -> None:
|
||||||
self, component_id: str, inputs: list[Port], outputs: list[Port]
|
|
||||||
) -> None:
|
|
||||||
if self.document is None:
|
if self.document is None:
|
||||||
return
|
return
|
||||||
component = self.document.find_component(component_id)
|
component = self.document.find_component(component_id)
|
||||||
if component is None:
|
if component is None:
|
||||||
return
|
return
|
||||||
input_ids = {port.id for port in inputs}
|
input_ids = {
|
||||||
output_ids = {port.id for port in outputs}
|
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)
|
parent = self.document.find_parent(component_id)
|
||||||
if parent is not None:
|
if parent is not None:
|
||||||
for connection in parent.graph.connections.values():
|
for connection in parent.graph.connections.values():
|
||||||
@@ -830,7 +927,7 @@ class DocumentController(QObject):
|
|||||||
raise ValueError("An input cannot be removed or reoriented while connected")
|
raise ValueError("An input cannot be removed or reoriented while connected")
|
||||||
if (
|
if (
|
||||||
connection.source.block == component_id
|
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")
|
raise ValueError("An output cannot be removed or reoriented while connected")
|
||||||
for connection in component.graph.connections.values():
|
for connection in component.graph.connections.values():
|
||||||
@@ -840,21 +937,18 @@ class DocumentController(QObject):
|
|||||||
raise ValueError("An interface output cannot be removed while connected")
|
raise ValueError("An interface output cannot be removed while connected")
|
||||||
candidate = deepcopy(self.document)
|
candidate = deepcopy(self.document)
|
||||||
candidate_component = candidate.find_component(component_id)
|
candidate_component = candidate.find_component(component_id)
|
||||||
candidate_component.inputs = deepcopy(inputs)
|
candidate_component.ports = deepcopy(ports)
|
||||||
candidate_component.outputs = deepcopy(outputs)
|
|
||||||
candidate.validate()
|
candidate.validate()
|
||||||
old = {
|
old = {
|
||||||
"name": component.name,
|
"name": component.name,
|
||||||
"icon": component.icon.to_dict(),
|
"icon": component.icon.to_dict(),
|
||||||
"inputs": [port.to_dict() for port in component.inputs],
|
"ports": [port.to_dict() for port in component.ports],
|
||||||
"outputs": [port.to_dict() for port in component.outputs],
|
|
||||||
"show_subtree": component.show_subtree_in_library,
|
"show_subtree": component.show_subtree_in_library,
|
||||||
"properties": deepcopy(component.properties),
|
"properties": deepcopy(component.properties),
|
||||||
}
|
}
|
||||||
new = {
|
new = {
|
||||||
**old,
|
**old,
|
||||||
"inputs": [port.to_dict() for port in inputs],
|
"ports": [port.to_dict() for port in ports],
|
||||||
"outputs": [port.to_dict() for port in outputs],
|
|
||||||
}
|
}
|
||||||
if old != new:
|
if old != new:
|
||||||
self.undo_stack.push(EditComponentAppearanceCommand(self, component_id, old, new))
|
self.undo_stack.push(EditComponentAppearanceCommand(self, component_id, old, new))
|
||||||
@@ -884,8 +978,7 @@ class DocumentController(QObject):
|
|||||||
self,
|
self,
|
||||||
block_ids: set[str],
|
block_ids: set[str],
|
||||||
connection_ids: set[str],
|
connection_ids: set[str],
|
||||||
input_ids: set[str],
|
port_ids: set[str],
|
||||||
output_ids: set[str],
|
|
||||||
) -> None:
|
) -> None:
|
||||||
component = self.active_component
|
component = self.active_component
|
||||||
if component is None or component.implementation_kind != "graph":
|
if component is None or component.implementation_kind != "graph":
|
||||||
@@ -896,8 +989,8 @@ class DocumentController(QObject):
|
|||||||
if (
|
if (
|
||||||
connection.source.block in block_ids
|
connection.source.block in block_ids
|
||||||
or connection.target.block in block_ids
|
or connection.target.block in block_ids
|
||||||
or connection.source.interface in input_ids
|
or connection.source.interface in port_ids
|
||||||
or connection.target.interface in output_ids
|
or connection.target.interface in port_ids
|
||||||
):
|
):
|
||||||
all_connection_ids.add(connection.id)
|
all_connection_ids.add(connection.id)
|
||||||
blocks = {
|
blocks = {
|
||||||
@@ -908,9 +1001,8 @@ class DocumentController(QObject):
|
|||||||
for connection_id in all_connection_ids
|
for connection_id in all_connection_ids
|
||||||
if connection_id in graph.connections
|
if connection_id in graph.connections
|
||||||
}
|
}
|
||||||
inputs = [port for port in component.inputs if port.id in input_ids]
|
ports = [port for port in component.ports if port.id in port_ids]
|
||||||
outputs = [port for port in component.outputs if port.id in output_ids]
|
if not (blocks or connections or ports):
|
||||||
if not (blocks or connections or inputs or outputs):
|
|
||||||
return
|
return
|
||||||
self.undo_stack.push(
|
self.undo_stack.push(
|
||||||
DeleteSelectionCommand(
|
DeleteSelectionCommand(
|
||||||
@@ -918,8 +1010,7 @@ class DocumentController(QObject):
|
|||||||
component.id,
|
component.id,
|
||||||
blocks,
|
blocks,
|
||||||
connections,
|
connections,
|
||||||
inputs,
|
ports,
|
||||||
outputs,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -957,6 +1048,8 @@ class DocumentController(QObject):
|
|||||||
target=Endpoint(block=id_map[source.target.block], port=source.target.port),
|
target=Endpoint(block=id_map[source.target.block], port=source.target.port),
|
||||||
name=source.name,
|
name=source.name,
|
||||||
properties=properties,
|
properties=properties,
|
||||||
|
type=source.type,
|
||||||
|
causality=source.causality,
|
||||||
)
|
)
|
||||||
connections[connection.id] = connection
|
connections[connection.id] = connection
|
||||||
if blocks:
|
if blocks:
|
||||||
@@ -1017,6 +1110,8 @@ class DocumentController(QObject):
|
|||||||
),
|
),
|
||||||
name=source.name,
|
name=source.name,
|
||||||
properties=properties,
|
properties=properties,
|
||||||
|
type=source.type,
|
||||||
|
causality=source.causality,
|
||||||
)
|
)
|
||||||
connections[connection.id] = connection
|
connections[connection.id] = connection
|
||||||
|
|
||||||
@@ -1095,6 +1190,10 @@ class DocumentController(QObject):
|
|||||||
|
|
||||||
def _insert_connection(self, owner_id: str, connection: Connection) -> None:
|
def _insert_connection(self, owner_id: str, connection: Connection) -> None:
|
||||||
self._graph_for(owner_id).connections[connection.id] = connection
|
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:
|
if owner_id == self.active_component_id:
|
||||||
self.connectionAdded.emit(connection.id)
|
self.connectionAdded.emit(connection.id)
|
||||||
self.documentReset.emit()
|
self.documentReset.emit()
|
||||||
@@ -1131,10 +1230,20 @@ class DocumentController(QObject):
|
|||||||
|
|
||||||
def _remove_connection(self, owner_id: str, connection_id: str) -> None:
|
def _remove_connection(self, owner_id: str, connection_id: str) -> None:
|
||||||
self._graph_for(owner_id).connections.pop(connection_id, 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:
|
if owner_id == self.active_component_id:
|
||||||
self.connectionRemoved.emit(connection_id)
|
self.connectionRemoved.emit(connection_id)
|
||||||
self.documentReset.emit()
|
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:
|
def _insert_annotation(self, owner_id: str, annotation: Annotation) -> None:
|
||||||
self._graph_for(owner_id).annotations[annotation.id] = annotation
|
self._graph_for(owner_id).annotations[annotation.id] = annotation
|
||||||
if owner_id == self.active_component_id:
|
if owner_id == self.active_component_id:
|
||||||
@@ -1232,9 +1341,9 @@ class DocumentController(QObject):
|
|||||||
owner = self.document.find_component(owner_id)
|
owner = self.document.find_component(owner_id)
|
||||||
if owner is None:
|
if owner is None:
|
||||||
return
|
return
|
||||||
ports = owner.inputs if direction == "input" else owner.outputs
|
port.orientation = direction
|
||||||
if all(existing.id != port.id for existing in ports):
|
if all(existing.id != port.id for existing in owner.ports):
|
||||||
ports.append(port)
|
owner.ports.append(port)
|
||||||
self.interfaceChanged.emit()
|
self.interfaceChanged.emit()
|
||||||
self.documentReset.emit()
|
self.documentReset.emit()
|
||||||
|
|
||||||
@@ -1244,8 +1353,7 @@ class DocumentController(QObject):
|
|||||||
owner = self.document.find_component(owner_id)
|
owner = self.document.find_component(owner_id)
|
||||||
if owner is None:
|
if owner is None:
|
||||||
return
|
return
|
||||||
ports = owner.inputs if direction == "input" else owner.outputs
|
owner.ports[:] = [port for port in owner.ports if port.id != port_id]
|
||||||
ports[:] = [port for port in ports if port.id != port_id]
|
|
||||||
self.interfaceChanged.emit()
|
self.interfaceChanged.emit()
|
||||||
self.documentReset.emit()
|
self.documentReset.emit()
|
||||||
|
|
||||||
@@ -1255,7 +1363,7 @@ class DocumentController(QObject):
|
|||||||
owner = self.document.find_component(owner_id)
|
owner = self.document.find_component(owner_id)
|
||||||
if owner is None:
|
if owner is None:
|
||||||
return
|
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:
|
if port is not None:
|
||||||
port.x, port.y = position.x(), position.y()
|
port.x, port.y = position.x(), position.y()
|
||||||
self.interfaceChanged.emit()
|
self.interfaceChanged.emit()
|
||||||
@@ -1265,7 +1373,7 @@ class DocumentController(QObject):
|
|||||||
owner = self.document.find_component(owner_id) if self.document else None
|
owner = self.document.find_component(owner_id) if self.document else None
|
||||||
if owner is None:
|
if owner is None:
|
||||||
return
|
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:
|
if port is not None:
|
||||||
port.name = name
|
port.name = name
|
||||||
self.interfaceChanged.emit()
|
self.interfaceChanged.emit()
|
||||||
@@ -1277,6 +1385,12 @@ class DocumentController(QObject):
|
|||||||
connection.name = name
|
connection.name = name
|
||||||
self.documentReset.emit()
|
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:
|
def _replace_source(self, component_id: str, source: dict) -> None:
|
||||||
if self.document is None:
|
if self.document is None:
|
||||||
return
|
return
|
||||||
@@ -1296,8 +1410,7 @@ class DocumentController(QObject):
|
|||||||
return
|
return
|
||||||
component.name = values["name"]
|
component.name = values["name"]
|
||||||
component.icon = Icon.from_dict(values["icon"])
|
component.icon = Icon.from_dict(values["icon"])
|
||||||
component.inputs = [Port.from_dict(port) for port in values["inputs"]]
|
component.ports = [Port.from_dict(port) for port in values["ports"]]
|
||||||
component.outputs = [Port.from_dict(port) for port in values["outputs"]]
|
|
||||||
component.show_subtree_in_library = values["show_subtree"]
|
component.show_subtree_in_library = values["show_subtree"]
|
||||||
component.properties = deepcopy(values["properties"])
|
component.properties = deepcopy(values["properties"])
|
||||||
self.documentReset.emit()
|
self.documentReset.emit()
|
||||||
@@ -1328,8 +1441,7 @@ class DocumentController(QObject):
|
|||||||
owner_id: str | None,
|
owner_id: str | None,
|
||||||
block_ids: set[str],
|
block_ids: set[str],
|
||||||
connection_ids: set[str],
|
connection_ids: set[str],
|
||||||
input_ids: set[str],
|
port_ids: set[str],
|
||||||
output_ids: set[str],
|
|
||||||
) -> None:
|
) -> None:
|
||||||
if self.document is None:
|
if self.document is None:
|
||||||
return
|
return
|
||||||
@@ -1352,8 +1464,7 @@ class DocumentController(QObject):
|
|||||||
owner.graph.blocks.pop(block_id, None)
|
owner.graph.blocks.pop(block_id, None)
|
||||||
for connection_id in connection_ids:
|
for connection_id in connection_ids:
|
||||||
owner.graph.connections.pop(connection_id, None)
|
owner.graph.connections.pop(connection_id, None)
|
||||||
owner.inputs[:] = [port for port in owner.inputs if port.id not in input_ids]
|
owner.ports[:] = [port for port in owner.ports if port.id not in port_ids]
|
||||||
owner.outputs[:] = [port for port in owner.outputs if port.id not in output_ids]
|
|
||||||
if active_was_deleted:
|
if active_was_deleted:
|
||||||
self.active_component_id = owner_id or next(iter(self.document.roots), None)
|
self.active_component_id = owner_id or next(iter(self.document.roots), None)
|
||||||
self.activeGraphChanged.emit()
|
self.activeGraphChanged.emit()
|
||||||
@@ -1364,8 +1475,7 @@ class DocumentController(QObject):
|
|||||||
owner_id: str | None,
|
owner_id: str | None,
|
||||||
blocks: dict[str, Component],
|
blocks: dict[str, Component],
|
||||||
connections: dict[str, Connection],
|
connections: dict[str, Connection],
|
||||||
inputs: list[Port],
|
ports: list[Port],
|
||||||
outputs: list[Port],
|
|
||||||
) -> None:
|
) -> None:
|
||||||
if self.document is None:
|
if self.document is None:
|
||||||
return
|
return
|
||||||
@@ -1377,10 +1487,8 @@ class DocumentController(QObject):
|
|||||||
return
|
return
|
||||||
owner.graph.blocks.update(blocks)
|
owner.graph.blocks.update(blocks)
|
||||||
owner.graph.connections.update(connections)
|
owner.graph.connections.update(connections)
|
||||||
existing_inputs = {port.id for port in owner.inputs}
|
existing_ports = {port.id for port in owner.ports}
|
||||||
existing_outputs = {port.id for port in owner.outputs}
|
owner.ports.extend(port for port in ports if port.id not in existing_ports)
|
||||||
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)
|
|
||||||
self.documentReset.emit()
|
self.documentReset.emit()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -1395,8 +1503,7 @@ class DocumentController(QObject):
|
|||||||
component = self.document.find_component(component_id)
|
component = self.document.find_component(component_id)
|
||||||
if component is None:
|
if component is None:
|
||||||
return
|
return
|
||||||
component.inputs = [Port.from_dict(item) for item in values["inputs"]]
|
component.ports = [Port.from_dict(item) for item in values["ports"]]
|
||||||
component.outputs = [Port.from_dict(item) for item in values["outputs"]]
|
|
||||||
component.source = deepcopy(values["source"])
|
component.source = deepcopy(values["source"])
|
||||||
component.parameters = [
|
component.parameters = [
|
||||||
Parameter.from_dict(item) for item in values.get("parameters", [])
|
Parameter.from_dict(item) for item in values.get("parameters", [])
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ class ComponentOptionsDialog(QDialog):
|
|||||||
self.ui.setupUi(self)
|
self.ui.setupUi(self)
|
||||||
self.component = component
|
self.component = component
|
||||||
self.edited_icon = component.icon
|
self.edited_icon = component.icon
|
||||||
self.edited_inputs = component.inputs
|
self.edited_ports = component.ports
|
||||||
self.edited_outputs = component.outputs
|
|
||||||
self.ui.nameEdit.setText(component.name)
|
self.ui.nameEdit.setText(component.name)
|
||||||
self.ui.editIconButton.clicked.connect(self.edit_icon)
|
self.ui.editIconButton.clicked.connect(self.edit_icon)
|
||||||
self.ui.showSubtreeCheckBox.setChecked(component.show_subtree_in_library)
|
self.ui.showSubtreeCheckBox.setChecked(component.show_subtree_in_library)
|
||||||
@@ -22,13 +21,11 @@ class ComponentOptionsDialog(QDialog):
|
|||||||
def edit_icon(self) -> None:
|
def edit_icon(self) -> None:
|
||||||
working = Component.from_dict(self.component.to_dict())
|
working = Component.from_dict(self.component.to_dict())
|
||||||
working.icon = self.edited_icon
|
working.icon = self.edited_icon
|
||||||
working.inputs = self.edited_inputs
|
working.ports = self.edited_ports
|
||||||
working.outputs = self.edited_outputs
|
|
||||||
dialog = IconEditorDialog(working, self)
|
dialog = IconEditorDialog(working, self)
|
||||||
if dialog.exec() == dialog.DialogCode.Accepted:
|
if dialog.exec() == dialog.DialogCode.Accepted:
|
||||||
self.edited_icon = dialog.icon
|
self.edited_icon = dialog.icon
|
||||||
self.edited_inputs = dialog.inputs
|
self.edited_ports = dialog.ports
|
||||||
self.edited_outputs = dialog.outputs
|
|
||||||
|
|
||||||
def accept(self) -> None:
|
def accept(self) -> None:
|
||||||
if not self.ui.nameEdit.text().strip():
|
if not self.ui.nameEdit.text().strip():
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt, Signal
|
||||||
from PySide6.QtWidgets import QDialog, QListWidgetItem, QMessageBox
|
from PySide6.QtWidgets import QDialog, QListWidgetItem, QMessageBox
|
||||||
|
|
||||||
from bedit.core.model import Component, Parameter
|
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
|
from bedit.gui.generated.ui_parameter_options_dialog import Ui_ParameterOptionsDialog
|
||||||
|
|
||||||
|
|
||||||
@@ -14,6 +15,8 @@ PARAMETER_ROLE = Qt.ItemDataRole.UserRole
|
|||||||
class ParameterOptionsDialog(QDialog):
|
class ParameterOptionsDialog(QDialog):
|
||||||
"""Editor for parameters shared by graph and text components."""
|
"""Editor for parameters shared by graph and text components."""
|
||||||
|
|
||||||
|
edited = Signal()
|
||||||
|
|
||||||
def __init__(self, component: Component, parent=None, *, read_only: bool = False) -> None:
|
def __init__(self, component: Component, parent=None, *, read_only: bool = False) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.ui = Ui_ParameterOptionsDialog()
|
self.ui = Ui_ParameterOptionsDialog()
|
||||||
@@ -22,19 +25,31 @@ class ParameterOptionsDialog(QDialog):
|
|||||||
self.parameters = deepcopy(component.parameters)
|
self.parameters = deepcopy(component.parameters)
|
||||||
self.read_only = read_only
|
self.read_only = read_only
|
||||||
self._loading = False
|
self._loading = False
|
||||||
|
self.ui.quantityCombo.addItems(QUANTITIES)
|
||||||
|
self.ui.unitCombo.addItems(UNITS)
|
||||||
self.ui.parameterList.currentRowChanged.connect(self._load_current)
|
self.ui.parameterList.currentRowChanged.connect(self._load_current)
|
||||||
self.ui.addParameterButton.clicked.connect(self.add_parameter)
|
self.ui.addParameterButton.clicked.connect(self.add_parameter)
|
||||||
self.ui.removeParameterButton.clicked.connect(self.remove_parameter)
|
self.ui.removeParameterButton.clicked.connect(self.remove_parameter)
|
||||||
self.ui.nameEdit.textEdited.connect(self._store_current)
|
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.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])
|
self.ui.parameterSplitter.setSizes([250, 370])
|
||||||
if read_only:
|
if read_only:
|
||||||
self.ui.addParameterButton.setEnabled(False)
|
self.ui.addParameterButton.setEnabled(False)
|
||||||
self.ui.removeParameterButton.setEnabled(False)
|
self.ui.removeParameterButton.setEnabled(False)
|
||||||
self.ui.nameEdit.setReadOnly(True)
|
self.ui.nameEdit.setReadOnly(True)
|
||||||
self.ui.typeEdit.setReadOnly(True)
|
self.ui.typeCombo.setEnabled(False)
|
||||||
self.ui.valueEdit.setReadOnly(True)
|
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)
|
self._rebuild_list(0 if self.parameters else -1)
|
||||||
|
|
||||||
def _rebuild_list(self, row: int = -1) -> None:
|
def _rebuild_list(self, row: int = -1) -> None:
|
||||||
@@ -51,12 +66,20 @@ class ParameterOptionsDialog(QDialog):
|
|||||||
if 0 <= row < len(self.parameters):
|
if 0 <= row < len(self.parameters):
|
||||||
parameter = self.parameters[row]
|
parameter = self.parameters[row]
|
||||||
self.ui.nameEdit.setText(parameter.name)
|
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.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:
|
else:
|
||||||
self.ui.nameEdit.clear()
|
self.ui.nameEdit.clear()
|
||||||
self.ui.typeEdit.clear()
|
self.ui.typeCombo.setCurrentText("")
|
||||||
self.ui.valueEdit.clear()
|
self.ui.valueEdit.clear()
|
||||||
|
self.ui.quantityCombo.setCurrentText("")
|
||||||
|
self.ui.unitCombo.setCurrentText("")
|
||||||
|
self.ui.descriptionEdit.clear()
|
||||||
self._loading = False
|
self._loading = False
|
||||||
self._update_enabled()
|
self._update_enabled()
|
||||||
|
|
||||||
@@ -64,8 +87,13 @@ class ParameterOptionsDialog(QDialog):
|
|||||||
enabled = self.ui.parameterList.currentRow() >= 0
|
enabled = self.ui.parameterList.currentRow() >= 0
|
||||||
self.ui.removeParameterButton.setEnabled(enabled and not self.read_only)
|
self.ui.removeParameterButton.setEnabled(enabled and not self.read_only)
|
||||||
self.ui.nameEdit.setEnabled(enabled)
|
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.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:
|
def _store_current(self) -> None:
|
||||||
row = self.ui.parameterList.currentRow()
|
row = self.ui.parameterList.currentRow()
|
||||||
@@ -73,11 +101,23 @@ class ParameterOptionsDialog(QDialog):
|
|||||||
return
|
return
|
||||||
parameter = self.parameters[row]
|
parameter = self.parameters[row]
|
||||||
parameter.name = self.ui.nameEdit.text()
|
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.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(
|
self.ui.parameterList.item(row).setText(
|
||||||
f"{parameter.name} [{parameter.type}] = {parameter.value}"
|
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:
|
def add_parameter(self) -> None:
|
||||||
self.parameters.append(
|
self.parameters.append(
|
||||||
@@ -89,12 +129,14 @@ class ParameterOptionsDialog(QDialog):
|
|||||||
self._rebuild_list(len(self.parameters) - 1)
|
self._rebuild_list(len(self.parameters) - 1)
|
||||||
self.ui.nameEdit.selectAll()
|
self.ui.nameEdit.selectAll()
|
||||||
self.ui.nameEdit.setFocus()
|
self.ui.nameEdit.setFocus()
|
||||||
|
self.edited.emit()
|
||||||
|
|
||||||
def remove_parameter(self) -> None:
|
def remove_parameter(self) -> None:
|
||||||
row = self.ui.parameterList.currentRow()
|
row = self.ui.parameterList.currentRow()
|
||||||
if row >= 0:
|
if row >= 0:
|
||||||
self.parameters.pop(row)
|
self.parameters.pop(row)
|
||||||
self._rebuild_list(min(row, len(self.parameters) - 1))
|
self._rebuild_list(min(row, len(self.parameters) - 1))
|
||||||
|
self.edited.emit()
|
||||||
|
|
||||||
def accept(self) -> None:
|
def accept(self) -> None:
|
||||||
self._store_current()
|
self._store_current()
|
||||||
@@ -106,3 +148,12 @@ class ParameterOptionsDialog(QDialog):
|
|||||||
QMessageBox.warning(self, "Invalid parameter", "Parameter names must be unique.")
|
QMessageBox.warning(self, "Invalid parameter", "Parameter names must be unique.")
|
||||||
return
|
return
|
||||||
super().accept()
|
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()
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt, Signal
|
||||||
from PySide6.QtWidgets import QDialog, QDialogButtonBox, QListWidgetItem, QMessageBox
|
from PySide6.QtWidgets import QDialog, QDialogButtonBox, QListWidgetItem, QMessageBox
|
||||||
|
|
||||||
from bedit.core.model import Component, Port
|
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.core.port_types import PortTypeRegistry
|
||||||
from bedit.gui.generated.ui_port_options_dialog import Ui_PortOptionsDialog
|
from bedit.gui.generated.ui_port_options_dialog import Ui_PortOptionsDialog
|
||||||
|
|
||||||
@@ -15,28 +17,45 @@ PORT_ROLE = Qt.ItemDataRole.UserRole
|
|||||||
class PortOptionsDialog(QDialog):
|
class PortOptionsDialog(QDialog):
|
||||||
"""Unified editor for a component's typed, oriented ports."""
|
"""Unified editor for a component's typed, oriented ports."""
|
||||||
|
|
||||||
|
edited = Signal()
|
||||||
|
|
||||||
def __init__(self, component: Component, parent=None, *, read_only: bool = False) -> None:
|
def __init__(self, component: Component, parent=None, *, read_only: bool = False) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.ui = Ui_PortOptionsDialog()
|
self.ui = Ui_PortOptionsDialog()
|
||||||
self.ui.setupUi(self)
|
self.ui.setupUi(self)
|
||||||
self.setWindowTitle(f"Port Options — {component.name}")
|
self.setWindowTitle(f"Port Options — {component.name}")
|
||||||
self.ports: list[tuple[Port, str]] = [
|
self.read_only = read_only
|
||||||
*((deepcopy(port), "input") for port in component.inputs),
|
self.ports = deepcopy(component.ports)
|
||||||
*((deepcopy(port), "output") for port in component.outputs),
|
|
||||||
]
|
|
||||||
self._loading = False
|
self._loading = False
|
||||||
self.ui.typeCombo.clear()
|
self.ui.typeCombo.clear()
|
||||||
for port_type in PortTypeRegistry.all():
|
for port_type in PortTypeRegistry.all():
|
||||||
self.ui.typeCombo.addItem(port_type.display_name, port_type.id)
|
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(0, "input")
|
||||||
self.ui.orientationCombo.setItemData(1, "output")
|
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.portList.currentRowChanged.connect(self._load_current)
|
||||||
self.ui.addPortButton.clicked.connect(self.add_port)
|
self.ui.addPortButton.clicked.connect(self.add_port)
|
||||||
self.ui.removePortButton.clicked.connect(self.remove_port)
|
self.ui.removePortButton.clicked.connect(self.remove_port)
|
||||||
self.ui.nameEdit.textEdited.connect(self._store_current)
|
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.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])
|
self.ui.portSplitter.setSizes([250, 370])
|
||||||
if read_only:
|
if read_only:
|
||||||
self.ui.addPortButton.setEnabled(False)
|
self.ui.addPortButton.setEnabled(False)
|
||||||
@@ -45,22 +64,15 @@ class PortOptionsDialog(QDialog):
|
|||||||
self.ui.typeCombo.setEnabled(False)
|
self.ui.typeCombo.setEnabled(False)
|
||||||
self.ui.orientationCombo.setEnabled(False)
|
self.ui.orientationCombo.setEnabled(False)
|
||||||
self.ui.multipleConnectionsCheckBox.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.Ok).setText("Close")
|
||||||
self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).hide()
|
self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).hide()
|
||||||
self._rebuild_list(0 if self.ports else -1)
|
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:
|
def _rebuild_list(self, row: int = -1) -> None:
|
||||||
self.ui.portList.clear()
|
self.ui.portList.clear()
|
||||||
for port, orientation in self.ports:
|
for port in self.ports:
|
||||||
item = QListWidgetItem(f"{port.name} [{orientation}, {port.type}]")
|
item = QListWidgetItem(f"{port.name} [{port.orientation}, {port.type}]")
|
||||||
item.setData(PORT_ROLE, port.id)
|
item.setData(PORT_ROLE, port.id)
|
||||||
self.ui.portList.addItem(item)
|
self.ui.portList.addItem(item)
|
||||||
self.ui.portList.setCurrentRow(min(row, len(self.ports) - 1))
|
self.ui.portList.setCurrentRow(min(row, len(self.ports) - 1))
|
||||||
@@ -70,38 +82,125 @@ class PortOptionsDialog(QDialog):
|
|||||||
self._loading = True
|
self._loading = True
|
||||||
enabled = 0 <= row < len(self.ports)
|
enabled = 0 <= row < len(self.ports)
|
||||||
if enabled:
|
if enabled:
|
||||||
port, orientation = self.ports[row]
|
port = self.ports[row]
|
||||||
self.ui.nameEdit.setText(port.name)
|
self.ui.nameEdit.setText(port.name)
|
||||||
self.ui.typeCombo.setCurrentIndex(self.ui.typeCombo.findData(port.type))
|
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(
|
self.ui.multipleConnectionsCheckBox.setChecked(
|
||||||
port.allows_multiple_connections
|
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:
|
else:
|
||||||
self.ui.nameEdit.clear()
|
self.ui.nameEdit.clear()
|
||||||
|
self.ui.descriptionEdit.clear()
|
||||||
self._loading = False
|
self._loading = False
|
||||||
self._update_enabled()
|
self._update_enabled()
|
||||||
|
|
||||||
def _update_enabled(self) -> None:
|
def _update_enabled(self) -> None:
|
||||||
enabled = self.ui.portList.currentRow() >= 0
|
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.nameEdit.setEnabled(enabled)
|
||||||
self.ui.typeCombo.setEnabled(enabled)
|
self.ui.typeCombo.setEnabled(enabled and not self.read_only)
|
||||||
self.ui.orientationCombo.setEnabled(enabled)
|
self.ui.orientationCombo.setEnabled(enabled and not self.read_only)
|
||||||
self.ui.multipleConnectionsCheckBox.setEnabled(enabled)
|
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:
|
def _store_current(self) -> None:
|
||||||
row = self.ui.portList.currentRow()
|
row = self.ui.portList.currentRow()
|
||||||
if self._loading or not (0 <= row < len(self.ports)):
|
if self._loading or not (0 <= row < len(self.ports)):
|
||||||
return
|
return
|
||||||
port, _orientation = self.ports[row]
|
port = self.ports[row]
|
||||||
port.name = self.ui.nameEdit.text()
|
port.name = self.ui.nameEdit.text()
|
||||||
port.type = self.ui.typeCombo.currentData()
|
port.type = self.ui.typeCombo.currentData()
|
||||||
port.allows_multiple_connections = self.ui.multipleConnectionsCheckBox.isChecked()
|
port.allows_multiple_connections = self.ui.multipleConnectionsCheckBox.isChecked()
|
||||||
self.ports[row] = (port, self.ui.orientationCombo.currentData())
|
port.value_type = self.ui.valueTypeCombo.currentText().strip() or "real"
|
||||||
self.ui.portList.item(row).setText(
|
port.quantity = self.ui.quantityCombo.currentText().strip()
|
||||||
f"{port.name} [{self.ports[row][1]}, {port.type}]"
|
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:
|
def add_port(self) -> None:
|
||||||
port = Port(
|
port = Port(
|
||||||
@@ -110,20 +209,31 @@ class PortOptionsDialog(QDialog):
|
|||||||
properties={"iconPosition": {"x": 0.0, "y": 0.0}},
|
properties={"iconPosition": {"x": 0.0, "y": 0.0}},
|
||||||
type="signal",
|
type="signal",
|
||||||
)
|
)
|
||||||
self.ports.append((port, "input"))
|
self.ports.append(port)
|
||||||
self._rebuild_list(len(self.ports) - 1)
|
self._rebuild_list(len(self.ports) - 1)
|
||||||
self.ui.nameEdit.selectAll()
|
self.ui.nameEdit.selectAll()
|
||||||
self.ui.nameEdit.setFocus()
|
self.ui.nameEdit.setFocus()
|
||||||
|
self.edited.emit()
|
||||||
|
|
||||||
def remove_port(self) -> None:
|
def remove_port(self) -> None:
|
||||||
row = self.ui.portList.currentRow()
|
row = self.ui.portList.currentRow()
|
||||||
if row >= 0:
|
if row >= 0:
|
||||||
self.ports.pop(row)
|
self.ports.pop(row)
|
||||||
self._rebuild_list(min(row, len(self.ports) - 1))
|
self._rebuild_list(min(row, len(self.ports) - 1))
|
||||||
|
self.edited.emit()
|
||||||
|
|
||||||
def accept(self) -> None:
|
def accept(self) -> None:
|
||||||
self._store_current()
|
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.")
|
QMessageBox.warning(self, "Invalid port", "Every port must have a name.")
|
||||||
return
|
return
|
||||||
super().accept()
|
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()
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ class SettingsDialog(QDialog):
|
|||||||
self.ui.graphGridSpinBox.setValue(self.graph_grid_size(self.settings))
|
self.ui.graphGridSpinBox.setValue(self.graph_grid_size(self.settings))
|
||||||
self.ui.graphSnapSpinBox.setValue(self.graph_snap_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.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.ui.openModelicaPathEdit.setText(self.openmodelica_path(self.settings))
|
||||||
self._load_syntax_styles()
|
self._load_syntax_styles()
|
||||||
self._update_remove_button()
|
self._update_remove_button()
|
||||||
@@ -132,6 +135,15 @@ class SettingsDialog(QDialog):
|
|||||||
settings = settings if settings is not None else application_settings()
|
settings = settings if settings is not None else application_settings()
|
||||||
return settings.value("grid/iconSize", 8, type=int)
|
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
|
@staticmethod
|
||||||
def openmodelica_path(settings: QSettings | None = None) -> str:
|
def openmodelica_path(settings: QSettings | None = None) -> str:
|
||||||
settings = settings if settings is not None else application_settings()
|
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/graphSize", self.ui.graphGridSpinBox.value())
|
||||||
self.settings.setValue("grid/graphSnapSize", self.ui.graphSnapSpinBox.value())
|
self.settings.setValue("grid/graphSnapSize", self.ui.graphSnapSpinBox.value())
|
||||||
self.settings.setValue("grid/iconSize", self.ui.iconGridSpinBox.value())
|
self.settings.setValue("grid/iconSize", self.ui.iconGridSpinBox.value())
|
||||||
|
self.settings.setValue(
|
||||||
|
"bondGraph/inferCausalityOnConnectionChange",
|
||||||
|
self.ui.automaticCausalityCheckBox.isChecked(),
|
||||||
|
)
|
||||||
self.settings.setValue(
|
self.settings.setValue(
|
||||||
"simulation/openModelicaPath",
|
"simulation/openModelicaPath",
|
||||||
self.ui.openModelicaPathEdit.text().strip(),
|
self.ui.openModelicaPathEdit.text().strip(),
|
||||||
|
|||||||
@@ -1,20 +1,12 @@
|
|||||||
from copy import deepcopy
|
from PySide6.QtCore import Signal
|
||||||
from uuid import uuid4
|
from PySide6.QtWidgets import QWidget
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, Signal
|
|
||||||
from PySide6.QtWidgets import QCheckBox, QComboBox, QHeaderView, QTableWidgetItem, QWidget
|
|
||||||
|
|
||||||
from bedit.core.model import Parameter, Port
|
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
|
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):
|
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)
|
modifiedChanged = Signal(bool)
|
||||||
definitionEdited = Signal()
|
definitionEdited = Signal()
|
||||||
@@ -25,24 +17,14 @@ class TextDefinitionEditor(QWidget):
|
|||||||
self.ui.setupUi(self)
|
self.ui.setupUi(self)
|
||||||
self._modified = False
|
self._modified = False
|
||||||
self._loading = 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.columnSplitter.setSizes([560, 340])
|
||||||
|
self.ui.sourceSplitter.setSizes([180, 180, 240])
|
||||||
self.ui.definitionSplitter.setSizes([300, 300])
|
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.equationsEdit.textChanged.connect(self._mark_modified)
|
||||||
self.ui.portsTable.cellChanged.connect(self._symbols_modified)
|
self.ui.portEditor.edited.connect(self._symbols_modified)
|
||||||
self.ui.parametersTable.cellChanged.connect(self._symbols_modified)
|
self.ui.parameterEditor.edited.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()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_modified(self) -> bool:
|
def is_modified(self) -> bool:
|
||||||
@@ -53,63 +35,38 @@ class TextDefinitionEditor(QWidget):
|
|||||||
return self.ui.equationsEdit.toPlainText()
|
return self.ui.equationsEdit.toPlainText()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ports(self) -> tuple[list[Port], list[Port]]:
|
def declarations(self) -> str:
|
||||||
inputs: list[Port] = []
|
return self.ui.declarationsEdit.toPlainText()
|
||||||
outputs: list[Port] = []
|
|
||||||
for row in range(self.ui.portsTable.rowCount()):
|
@property
|
||||||
name_item = self.ui.portsTable.item(row, 0)
|
def initial_equations(self) -> str:
|
||||||
type_combo = self.ui.portsTable.cellWidget(row, 1)
|
return self.ui.initialEquationsEdit.toPlainText()
|
||||||
orientation_combo = self.ui.portsTable.cellWidget(row, 2)
|
|
||||||
multiple_check = self.ui.portsTable.cellWidget(row, 3)
|
@property
|
||||||
port = Port(
|
def ports(self) -> list[Port]:
|
||||||
id=name_item.data(ID_ROLE),
|
return self.ui.portEditor.ports
|
||||||
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
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parameters(self) -> list[Parameter]:
|
def parameters(self) -> list[Parameter]:
|
||||||
table = self.ui.parametersTable
|
return self.ui.parameterEditor.parameters
|
||||||
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())
|
|
||||||
]
|
|
||||||
|
|
||||||
def set_definition(
|
def set_definition(
|
||||||
self,
|
self,
|
||||||
|
declarations: str,
|
||||||
|
initial_equations: str,
|
||||||
equations: str,
|
equations: str,
|
||||||
inputs: list[Port],
|
ports: list[Port],
|
||||||
outputs: list[Port],
|
|
||||||
parameters: list[Parameter],
|
parameters: list[Parameter],
|
||||||
) -> None:
|
) -> None:
|
||||||
self._loading = True
|
self._loading = True
|
||||||
|
self.ui.declarationsEdit.setPlainText(declarations)
|
||||||
|
self.ui.initialEquationsEdit.setPlainText(initial_equations)
|
||||||
self.ui.equationsEdit.setPlainText(equations)
|
self.ui.equationsEdit.setPlainText(equations)
|
||||||
self.ui.portsTable.setRowCount(0)
|
self.ui.portEditor.set_ports(ports)
|
||||||
for port in inputs:
|
self.ui.parameterEditor.set_parameters(parameters)
|
||||||
self._append_port(port, "input")
|
self._set_editor_symbols(ports, parameters)
|
||||||
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._loading = False
|
self._loading = False
|
||||||
self.set_modified(False)
|
self.set_modified(False)
|
||||||
self._update_buttons()
|
|
||||||
|
|
||||||
def set_modified(self, modified: bool) -> None:
|
def set_modified(self, modified: bool) -> None:
|
||||||
if self._modified != modified:
|
if self._modified != modified:
|
||||||
@@ -127,91 +84,27 @@ class TextDefinitionEditor(QWidget):
|
|||||||
self._mark_modified()
|
self._mark_modified()
|
||||||
|
|
||||||
def _refresh_editor_symbols(self) -> None:
|
def _refresh_editor_symbols(self) -> None:
|
||||||
inputs, outputs = self.ports
|
self._set_editor_symbols(self.ports, self.parameters)
|
||||||
self.ui.equationsEdit.set_symbols(
|
|
||||||
[port.name for port in inputs],
|
def _set_editor_symbols(
|
||||||
[port.name for port in outputs],
|
self,
|
||||||
[parameter.name for parameter in self.parameters],
|
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:
|
def _apply_editor_symbols(
|
||||||
combo = QComboBox(self)
|
self, inputs: list[str], outputs: list[str], parameters: list[str]
|
||||||
for label, value in values:
|
) -> None:
|
||||||
combo.addItem(label, value)
|
symbols = (inputs, outputs, parameters)
|
||||||
combo.setCurrentIndex(max(0, combo.findData(current)))
|
self.ui.declarationsEdit.set_symbols(*symbols)
|
||||||
combo.currentIndexChanged.connect(self._symbols_modified)
|
self.ui.initialEquationsEdit.set_symbols(*symbols)
|
||||||
return combo
|
self.ui.equationsEdit.set_symbols(*symbols)
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -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\
|
\xbf\xc9\xd3\x01\x9f\xa3\x0f\x17Z/\xe3\x7f\xe1/\x17\x85\
|
||||||
\xd6\x06q(\x0e\x10\x00\x00\x00\x00IEND\xaeB\
|
\xd6\x06q(\x0e\x10\x00\x00\x00\x00IEND\xaeB\
|
||||||
`\x82\
|
`\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\
|
\x00\x00\x02V\
|
||||||
\x89\
|
\x89\
|
||||||
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
|
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
|
||||||
@@ -2383,6 +2452,101 @@ u1\xb8\xdd0`x\xe8F\xf9\x1e\xe19\xea\x8a\x03\
|
|||||||
|f\x1e\x89\xd1\x9ef\xbdhx\x0fE4mL\xaf\
|
|f\x1e\x89\xd1\x9ef\xbdhx\x0fE4mL\xaf\
|
||||||
b/B\x06\x12o\xbf\x00\xa3\x17WYZq\xd9W\
|
b/B\x06\x12o\xbf\x00\xa3\x17WYZq\xd9W\
|
||||||
\x00\x00\x00\x00IEND\xaeB`\x82\
|
\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<\
|
\x00\x00\x04<\
|
||||||
\x89\
|
\x89\
|
||||||
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
|
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
|
||||||
@@ -2649,6 +2813,10 @@ qt_resource_name = b"\
|
|||||||
\x0f\xba\x1e'\
|
\x0f\xba\x1e'\
|
||||||
\x00d\
|
\x00d\
|
||||||
\x00r\x00a\x00w\x00-\x00p\x00a\x00t\x00h\x00.\x00p\x00n\x00g\
|
\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\
|
\x00\x0d\
|
||||||
\x0b\xe6\x1f\xa7\
|
\x0b\xe6\x1f\xa7\
|
||||||
\x00d\
|
\x00d\
|
||||||
@@ -2723,6 +2891,10 @@ qt_resource_name = b"\
|
|||||||
\x00o\
|
\x00o\
|
||||||
\x00f\x00f\x00i\x00c\x00e\x00-\x00c\x00h\x00a\x00r\x00t\x00-\x00l\x00i\x00n\x00e\
|
\x00f\x00f\x00i\x00c\x00e\x00-\x00c\x00h\x00a\x00r\x00t\x00-\x00l\x00i\x00n\x00e\
|
||||||
\x00.\x00p\x00n\x00g\
|
\x00.\x00p\x00n\x00g\
|
||||||
|
\x00\x0c\
|
||||||
|
\x09\xc6\x19'\
|
||||||
|
\x00l\
|
||||||
|
\x00i\x00s\x00t\x00-\x00a\x00d\x00d\x00.\x00p\x00n\x00g\
|
||||||
\x00\x10\
|
\x00\x10\
|
||||||
\x03\xe6\xd3g\
|
\x03\xe6\xd3g\
|
||||||
\x00d\
|
\x00d\
|
||||||
@@ -2749,67 +2921,71 @@ qt_resource_struct = b"\
|
|||||||
\x00\x00\x00\x00\x00\x00\x00\x00\
|
\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\x02\x00\x00\x00\x01\x00\x00\x00\x02\
|
||||||
\x00\x00\x00\x00\x00\x00\x00\x00\
|
\x00\x00\x00\x00\x00\x00\x00\x00\
|
||||||
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1e\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\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\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\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\x9f{0\xc99\
|
||||||
\x00\x00\x03\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x8d\xaa\
|
\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\x9f\x84\x8f\x00\xb9\
|
||||||
\x00\x00\x01\xc4\x00\x00\x00\x00\x00\x01\x00\x00D2\
|
\x00\x00\x01\xe8\x00\x00\x00\x00\x00\x01\x00\x00H_\
|
||||||
\x00\x00\x01\x9f\x7f&\x83\xcd\
|
\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\x01\x9f{C\xf1\x18\
|
||||||
\x00\x00\x03\xd4\x00\x00\x00\x00\x00\x01\x00\x00\x91\x1b\
|
\x00\x00\x04\x16\x00\x00\x00\x00\x00\x01\x00\x00\x9b\x1b\
|
||||||
\x00\x00\x01\x9f\x7fY\xceg\
|
\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\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\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\x9f\x7f&\x83\xb1\
|
||||||
\x00\x00\x01\x1c\x00\x00\x00\x00\x00\x01\x00\x00,\x08\
|
\x00\x00\x01\x1c\x00\x00\x00\x00\x00\x01\x00\x00,\x08\
|
||||||
\x00\x00\x01\x9f\x7fY\xce\x82\
|
\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\x9f{C\xf1.\
|
||||||
\x00\x00\x03\xfa\x00\x00\x00\x00\x00\x01\x00\x00\x95[\
|
\x00\x00\x04<\x00\x00\x00\x00\x00\x01\x00\x00\x9f[\
|
||||||
\x00\x00\x01\x9f\x84\x8f\xa6\x10\
|
\x00\x00\x01\x9f\x84\x8f\xa6\x10\
|
||||||
\x00\x00\x01\x84\x00\x00\x00\x00\x00\x01\x00\x006\x9c\
|
\x00\x00\x01\xa8\x00\x00\x00\x00\x00\x01\x00\x00:\xc9\
|
||||||
\x00\x00\x01\x9f{{\xa5\xd5\
|
\x00\x00\x01\x9f{{\xa5\xd5\
|
||||||
\x00\x00\x04&\x00\x00\x00\x00\x00\x01\x00\x00\x97\x0c\
|
\x00\x00\x04h\x00\x00\x00\x00\x00\x01\x00\x00\xa1\x0c\
|
||||||
\x00\x00\x01\x9f\x7f&\x83\x10\
|
\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\x00\xfe\x00\x00\x00\x00\x00\x01\x00\x00(e\
|
||||||
\x00\x00\x01\x9f\x7f&\x83~\
|
\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\x01\x9f\x7f&\x83T\
|
||||||
\x00\x00\x00~\x00\x00\x00\x00\x00\x01\x00\x00\x14M\
|
\x00\x00\x00~\x00\x00\x00\x00\x00\x01\x00\x00\x14M\
|
||||||
\x00\x00\x01\x9f\x7fY\xce^\
|
\x00\x00\x01\x9f\x7fY\xce^\
|
||||||
\x00\x00\x00\xa2\x00\x00\x00\x00\x00\x01\x00\x00\x19Y\
|
\x00\x00\x00\xa2\x00\x00\x00\x00\x00\x01\x00\x00\x19Y\
|
||||||
\x00\x00\x01\x9f{0\xc9B\
|
\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\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\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\x01\x9f\x7f&\x82\xf2\
|
||||||
\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x01\x00\x00!\xc5\
|
\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x01\x00\x00!\xc5\
|
||||||
\x00\x00\x01\x9f{\x8d\xf34\
|
\x00\x00\x01\x9f{\x8d\xf34\
|
||||||
\x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
|
\x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
|
||||||
\x00\x00\x01\x9f{0\xc9+\
|
\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\x01\x9f{C\xf1=\
|
||||||
\x00\x00\x006\x00\x00\x00\x00\x00\x01\x00\x00\x05\x86\
|
\x00\x00\x006\x00\x00\x00\x00\x00\x01\x00\x00\x05\x86\
|
||||||
\x00\x00\x01\x9f\x7f&\x83\x99\
|
\x00\x00\x01\x9f\x7f&\x83\x99\
|
||||||
\x00\x00\x04P\x00\x00\x00\x00\x00\x01\x00\x00\x99\xa2\
|
\x00\x00\x04\x92\x00\x00\x00\x00\x00\x01\x00\x00\xa3\xa2\
|
||||||
\x00\x00\x01\x9f{{\xa5\xe3\
|
\x00\x00\x01\x9f{{\xa5\xe3\
|
||||||
\x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x00\x0c\xc1\
|
\x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x00\x0c\xc1\
|
||||||
\x00\x00\x01\x9f\x7f\xac\xf2\xc6\
|
\x00\x00\x01\x9f\x7f\xac\xf2\xc6\
|
||||||
\x00\x00\x01D\x00\x00\x00\x00\x00\x01\x00\x00/{\
|
\x00\x00\x01D\x00\x00\x00\x00\x00\x01\x00\x00/{\
|
||||||
\x00\x00\x01\x9f\x7fV\xd5\xe5\
|
\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\
|
\x00\x00\x01\x9f{0\xc9R\
|
||||||
"
|
"
|
||||||
|
|
||||||
|
|||||||
@@ -56,50 +56,53 @@ class Ui_MainWindow(object):
|
|||||||
icon4 = QIcon()
|
icon4 = QIcon()
|
||||||
icon4.addFile(u":/icons/icons/office-chart-line.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon4.addFile(u":/icons/icons/office-chart-line.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.actionSimulationWindow.setIcon(icon4)
|
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 = QAction(MainWindow)
|
||||||
self.actionNew.setObjectName(u"actionNew")
|
self.actionNew.setObjectName(u"actionNew")
|
||||||
icon5 = QIcon()
|
icon6 = QIcon()
|
||||||
icon5.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon6.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.actionNew.setIcon(icon5)
|
self.actionNew.setIcon(icon6)
|
||||||
self.actionRotateClockwise = QAction(MainWindow)
|
self.actionRotateClockwise = QAction(MainWindow)
|
||||||
self.actionRotateClockwise.setObjectName(u"actionRotateClockwise")
|
self.actionRotateClockwise.setObjectName(u"actionRotateClockwise")
|
||||||
icon6 = QIcon()
|
icon7 = QIcon()
|
||||||
icon6.addFile(u":/icons/icons/transform-rotate.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon7.addFile(u":/icons/icons/transform-rotate.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.actionRotateClockwise.setIcon(icon6)
|
self.actionRotateClockwise.setIcon(icon7)
|
||||||
self.actionZoomIn = QAction(MainWindow)
|
self.actionZoomIn = QAction(MainWindow)
|
||||||
self.actionZoomIn.setObjectName(u"actionZoomIn")
|
self.actionZoomIn.setObjectName(u"actionZoomIn")
|
||||||
icon7 = QIcon()
|
icon8 = QIcon()
|
||||||
icon7.addFile(u":/icons/icons/zoom-in.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon8.addFile(u":/icons/icons/zoom-in.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.actionZoomIn.setIcon(icon7)
|
self.actionZoomIn.setIcon(icon8)
|
||||||
self.actionZoomOut = QAction(MainWindow)
|
self.actionZoomOut = QAction(MainWindow)
|
||||||
self.actionZoomOut.setObjectName(u"actionZoomOut")
|
self.actionZoomOut.setObjectName(u"actionZoomOut")
|
||||||
icon8 = QIcon()
|
icon9 = QIcon()
|
||||||
icon8.addFile(u":/icons/icons/zoom-out.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon9.addFile(u":/icons/icons/zoom-out.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.actionZoomOut.setIcon(icon8)
|
self.actionZoomOut.setIcon(icon9)
|
||||||
self.actionCenterView = QAction(MainWindow)
|
self.actionCenterView = QAction(MainWindow)
|
||||||
self.actionCenterView.setObjectName(u"actionCenterView")
|
self.actionCenterView.setObjectName(u"actionCenterView")
|
||||||
icon9 = QIcon()
|
icon10 = QIcon()
|
||||||
icon9.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon10.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.actionCenterView.setIcon(icon9)
|
self.actionCenterView.setIcon(icon10)
|
||||||
self.actionOpen = QAction(MainWindow)
|
self.actionOpen = QAction(MainWindow)
|
||||||
self.actionOpen.setObjectName(u"actionOpen")
|
self.actionOpen.setObjectName(u"actionOpen")
|
||||||
icon10 = QIcon()
|
icon11 = QIcon()
|
||||||
icon10.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon11.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.actionOpen.setIcon(icon10)
|
self.actionOpen.setIcon(icon11)
|
||||||
self.actionReloadLibraries = QAction(MainWindow)
|
self.actionReloadLibraries = QAction(MainWindow)
|
||||||
self.actionReloadLibraries.setObjectName(u"actionReloadLibraries")
|
self.actionReloadLibraries.setObjectName(u"actionReloadLibraries")
|
||||||
self.actionReloadSimulation = QAction(MainWindow)
|
self.actionReloadSimulation = QAction(MainWindow)
|
||||||
self.actionReloadSimulation.setObjectName(u"actionReloadSimulation")
|
self.actionReloadSimulation.setObjectName(u"actionReloadSimulation")
|
||||||
self.actionSave = QAction(MainWindow)
|
self.actionSave = QAction(MainWindow)
|
||||||
self.actionSave.setObjectName(u"actionSave")
|
self.actionSave.setObjectName(u"actionSave")
|
||||||
icon11 = QIcon()
|
icon12 = QIcon()
|
||||||
icon11.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon12.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.actionSave.setIcon(icon11)
|
self.actionSave.setIcon(icon12)
|
||||||
self.actionSaveAs = QAction(MainWindow)
|
self.actionSaveAs = QAction(MainWindow)
|
||||||
self.actionSaveAs.setObjectName(u"actionSaveAs")
|
self.actionSaveAs.setObjectName(u"actionSaveAs")
|
||||||
icon12 = QIcon()
|
self.actionSaveAs.setIcon(icon5)
|
||||||
icon12.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
|
||||||
self.actionSaveAs.setIcon(icon12)
|
|
||||||
self.actionExit = QAction(MainWindow)
|
self.actionExit = QAction(MainWindow)
|
||||||
self.actionExit.setObjectName(u"actionExit")
|
self.actionExit.setObjectName(u"actionExit")
|
||||||
self.actionClose = QAction(MainWindow)
|
self.actionClose = QAction(MainWindow)
|
||||||
@@ -295,7 +298,7 @@ class Ui_MainWindow(object):
|
|||||||
|
|
||||||
self.rotateToolButton = QToolButton(self.workspaceHeader)
|
self.rotateToolButton = QToolButton(self.workspaceHeader)
|
||||||
self.rotateToolButton.setObjectName(u"rotateToolButton")
|
self.rotateToolButton.setObjectName(u"rotateToolButton")
|
||||||
self.rotateToolButton.setIcon(icon6)
|
self.rotateToolButton.setIcon(icon7)
|
||||||
|
|
||||||
self.workspaceHeaderLayout.addWidget(self.rotateToolButton)
|
self.workspaceHeaderLayout.addWidget(self.rotateToolButton)
|
||||||
|
|
||||||
@@ -441,6 +444,7 @@ class Ui_MainWindow(object):
|
|||||||
self.menuSimulation.addAction(self.actionSimulationSettings)
|
self.menuSimulation.addAction(self.actionSimulationSettings)
|
||||||
self.menuSimulation.addAction(self.actionGraphParameters)
|
self.menuSimulation.addAction(self.actionGraphParameters)
|
||||||
self.menuSimulation.addAction(self.actionCompose)
|
self.menuSimulation.addAction(self.actionCompose)
|
||||||
|
self.menuSimulation.addAction(self.actionExportModel)
|
||||||
self.menuSimulation.addAction(self.actionSimulationWindow)
|
self.menuSimulation.addAction(self.actionSimulationWindow)
|
||||||
self.menuSimulation.addAction(self.actionRunSimulation)
|
self.menuSimulation.addAction(self.actionRunSimulation)
|
||||||
self.fileToolbar.addAction(self.actionNew)
|
self.fileToolbar.addAction(self.actionNew)
|
||||||
@@ -496,6 +500,10 @@ class Ui_MainWindow(object):
|
|||||||
self.actionSimulationWindow.setText(QCoreApplication.translate("MainWindow", u"Simulation Window", None))
|
self.actionSimulationWindow.setText(QCoreApplication.translate("MainWindow", u"Simulation Window", None))
|
||||||
#if QT_CONFIG(statustip)
|
#if QT_CONFIG(statustip)
|
||||||
self.actionSimulationWindow.setStatusTip(QCoreApplication.translate("MainWindow", u"Show the simulation results window", None))
|
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)
|
#endif // QT_CONFIG(statustip)
|
||||||
self.actionNew.setText(QCoreApplication.translate("MainWindow", u"&New", None))
|
self.actionNew.setText(QCoreApplication.translate("MainWindow", u"&New", None))
|
||||||
#if QT_CONFIG(statustip)
|
#if QT_CONFIG(statustip)
|
||||||
|
|||||||
@@ -15,16 +15,17 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
|||||||
QFont, QFontDatabase, QGradient, QIcon,
|
QFont, QFontDatabase, QGradient, QIcon,
|
||||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||||
from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox,
|
from PySide6.QtWidgets import (QAbstractButton, QApplication, QComboBox, QDialog,
|
||||||
QFormLayout, QHBoxLayout, QLabel, QLineEdit,
|
QDialogButtonBox, QFormLayout, QHBoxLayout, QLabel,
|
||||||
QListWidget, QListWidgetItem, QPushButton, QSizePolicy,
|
QLineEdit, QListWidget, QListWidgetItem, QPlainTextEdit,
|
||||||
QSplitter, QVBoxLayout, QWidget)
|
QPushButton, QSizePolicy, QSpinBox, QSplitter,
|
||||||
|
QVBoxLayout, QWidget)
|
||||||
|
|
||||||
class Ui_ParameterOptionsDialog(object):
|
class Ui_ParameterOptionsDialog(object):
|
||||||
def setupUi(self, ParameterOptionsDialog):
|
def setupUi(self, ParameterOptionsDialog):
|
||||||
if not ParameterOptionsDialog.objectName():
|
if not ParameterOptionsDialog.objectName():
|
||||||
ParameterOptionsDialog.setObjectName(u"ParameterOptionsDialog")
|
ParameterOptionsDialog.setObjectName(u"ParameterOptionsDialog")
|
||||||
ParameterOptionsDialog.resize(620, 380)
|
ParameterOptionsDialog.resize(720, 500)
|
||||||
self.dialogLayout = QVBoxLayout(ParameterOptionsDialog)
|
self.dialogLayout = QVBoxLayout(ParameterOptionsDialog)
|
||||||
self.dialogLayout.setObjectName(u"dialogLayout")
|
self.dialogLayout.setObjectName(u"dialogLayout")
|
||||||
self.parameterSplitter = QSplitter(ParameterOptionsDialog)
|
self.parameterSplitter = QSplitter(ParameterOptionsDialog)
|
||||||
@@ -76,20 +77,82 @@ class Ui_ParameterOptionsDialog(object):
|
|||||||
|
|
||||||
self.parameterDetailsForm.setWidget(1, QFormLayout.ItemRole.LabelRole, self.typeLabel)
|
self.parameterDetailsForm.setWidget(1, QFormLayout.ItemRole.LabelRole, self.typeLabel)
|
||||||
|
|
||||||
self.typeEdit = QLineEdit(self.parameterDetailsPanel)
|
self.typeCombo = QComboBox(self.parameterDetailsPanel)
|
||||||
self.typeEdit.setObjectName(u"typeEdit")
|
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 = QLabel(self.parameterDetailsPanel)
|
||||||
self.valueLabel.setObjectName(u"valueLabel")
|
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 = QLineEdit(self.parameterDetailsPanel)
|
||||||
self.valueEdit.setObjectName(u"valueEdit")
|
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)
|
self.parameterSplitter.addWidget(self.parameterDetailsPanel)
|
||||||
|
|
||||||
@@ -115,6 +178,16 @@ class Ui_ParameterOptionsDialog(object):
|
|||||||
self.removeParameterButton.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Remove Parameter", None))
|
self.removeParameterButton.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Remove Parameter", None))
|
||||||
self.nameLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Name:", None))
|
self.nameLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Name:", None))
|
||||||
self.typeLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Type:", 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.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
|
# retranslateUi
|
||||||
|
|
||||||
|
|||||||
@@ -18,14 +18,14 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
|||||||
from PySide6.QtWidgets import (QAbstractButton, QApplication, QCheckBox, QComboBox,
|
from PySide6.QtWidgets import (QAbstractButton, QApplication, QCheckBox, QComboBox,
|
||||||
QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout,
|
QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout,
|
||||||
QLabel, QLineEdit, QListWidget, QListWidgetItem,
|
QLabel, QLineEdit, QListWidget, QListWidgetItem,
|
||||||
QPushButton, QSizePolicy, QSplitter, QVBoxLayout,
|
QPlainTextEdit, QPushButton, QSizePolicy, QSpinBox,
|
||||||
QWidget)
|
QSplitter, QStackedWidget, QVBoxLayout, QWidget)
|
||||||
|
|
||||||
class Ui_PortOptionsDialog(object):
|
class Ui_PortOptionsDialog(object):
|
||||||
def setupUi(self, PortOptionsDialog):
|
def setupUi(self, PortOptionsDialog):
|
||||||
if not PortOptionsDialog.objectName():
|
if not PortOptionsDialog.objectName():
|
||||||
PortOptionsDialog.setObjectName(u"PortOptionsDialog")
|
PortOptionsDialog.setObjectName(u"PortOptionsDialog")
|
||||||
PortOptionsDialog.resize(620, 380)
|
PortOptionsDialog.resize(760, 421)
|
||||||
self.dialogLayout = QVBoxLayout(PortOptionsDialog)
|
self.dialogLayout = QVBoxLayout(PortOptionsDialog)
|
||||||
self.dialogLayout.setObjectName(u"dialogLayout")
|
self.dialogLayout.setObjectName(u"dialogLayout")
|
||||||
self.portSplitter = QSplitter(PortOptionsDialog)
|
self.portSplitter = QSplitter(PortOptionsDialog)
|
||||||
@@ -61,6 +61,7 @@ class Ui_PortOptionsDialog(object):
|
|||||||
self.portDetailsPanel.setObjectName(u"portDetailsPanel")
|
self.portDetailsPanel.setObjectName(u"portDetailsPanel")
|
||||||
self.portDetailsForm = QFormLayout(self.portDetailsPanel)
|
self.portDetailsForm = QFormLayout(self.portDetailsPanel)
|
||||||
self.portDetailsForm.setObjectName(u"portDetailsForm")
|
self.portDetailsForm.setObjectName(u"portDetailsForm")
|
||||||
|
self.portDetailsForm.setVerticalSpacing(3)
|
||||||
self.portDetailsForm.setContentsMargins(0, 0, 0, 0)
|
self.portDetailsForm.setContentsMargins(0, 0, 0, 0)
|
||||||
self.nameLabel = QLabel(self.portDetailsPanel)
|
self.nameLabel = QLabel(self.portDetailsPanel)
|
||||||
self.nameLabel.setObjectName(u"nameLabel")
|
self.nameLabel.setObjectName(u"nameLabel")
|
||||||
@@ -91,6 +92,7 @@ class Ui_PortOptionsDialog(object):
|
|||||||
self.orientationCombo = QComboBox(self.portDetailsPanel)
|
self.orientationCombo = QComboBox(self.portDetailsPanel)
|
||||||
self.orientationCombo.addItem("")
|
self.orientationCombo.addItem("")
|
||||||
self.orientationCombo.addItem("")
|
self.orientationCombo.addItem("")
|
||||||
|
self.orientationCombo.addItem("")
|
||||||
self.orientationCombo.setObjectName(u"orientationCombo")
|
self.orientationCombo.setObjectName(u"orientationCombo")
|
||||||
|
|
||||||
self.portDetailsForm.setWidget(2, QFormLayout.ItemRole.FieldRole, self.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.setObjectName(u"positionHintLabel")
|
||||||
self.positionHintLabel.setWordWrap(True)
|
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)
|
self.portSplitter.addWidget(self.portDetailsPanel)
|
||||||
|
|
||||||
@@ -129,14 +278,33 @@ class Ui_PortOptionsDialog(object):
|
|||||||
self.addPortButton.setText(QCoreApplication.translate("PortOptionsDialog", u"Add Port", None))
|
self.addPortButton.setText(QCoreApplication.translate("PortOptionsDialog", u"Add Port", None))
|
||||||
self.removePortButton.setText(QCoreApplication.translate("PortOptionsDialog", u"Remove Port", None))
|
self.removePortButton.setText(QCoreApplication.translate("PortOptionsDialog", u"Remove Port", None))
|
||||||
self.nameLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Name:", 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.typeCombo.setItemText(0, QCoreApplication.translate("PortOptionsDialog", u"Signal", None))
|
||||||
|
|
||||||
self.orientationLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Orientation:", None))
|
self.orientationLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Orientation:", None))
|
||||||
self.orientationCombo.setItemText(0, QCoreApplication.translate("PortOptionsDialog", u"Input", None))
|
self.orientationCombo.setItemText(0, QCoreApplication.translate("PortOptionsDialog", u"Input", None))
|
||||||
self.orientationCombo.setItemText(1, QCoreApplication.translate("PortOptionsDialog", u"Output", 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.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.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
|
# retranslateUi
|
||||||
|
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
|||||||
QFont, QFontDatabase, QGradient, QIcon,
|
QFont, QFontDatabase, QGradient, QIcon,
|
||||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||||
from PySide6.QtWidgets import (QAbstractButton, QAbstractItemView, QApplication, QDialog,
|
from PySide6.QtWidgets import (QAbstractButton, QAbstractItemView, QApplication, QCheckBox,
|
||||||
QDialogButtonBox, QFormLayout, QGroupBox, QHBoxLayout,
|
QDialog, QDialogButtonBox, QFormLayout, QGroupBox,
|
||||||
QHeaderView, QLabel, QLineEdit, QListWidget,
|
QHBoxLayout, QHeaderView, QLabel, QLineEdit,
|
||||||
QListWidgetItem, QPushButton, QSizePolicy, QSpacerItem,
|
QListWidget, QListWidgetItem, QPushButton, QSizePolicy,
|
||||||
QSpinBox, QTabWidget, QTableWidget, QTableWidgetItem,
|
QSpacerItem, QSpinBox, QTabWidget, QTableWidget,
|
||||||
QVBoxLayout, QWidget)
|
QTableWidgetItem, QVBoxLayout, QWidget)
|
||||||
|
|
||||||
class Ui_SettingsDialog(object):
|
class Ui_SettingsDialog(object):
|
||||||
def setupUi(self, SettingsDialog):
|
def setupUi(self, SettingsDialog):
|
||||||
@@ -104,6 +104,34 @@ class Ui_SettingsDialog(object):
|
|||||||
self.generalLayout.addItem(self.generalSpacer)
|
self.generalLayout.addItem(self.generalSpacer)
|
||||||
|
|
||||||
self.settingsTabs.addTab(self.generalTab, "")
|
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 = QWidget()
|
||||||
self.simulationTab.setObjectName(u"simulationTab")
|
self.simulationTab.setObjectName(u"simulationTab")
|
||||||
self.simulationTabLayout = QVBoxLayout(self.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.iconGridLabel.setText(QCoreApplication.translate("SettingsDialog", u"Icon grid size:", None))
|
||||||
self.iconGridSpinBox.setSuffix(QCoreApplication.translate("SettingsDialog", u" units", None))
|
self.iconGridSpinBox.setSuffix(QCoreApplication.translate("SettingsDialog", u" units", None))
|
||||||
self.settingsTabs.setTabText(self.settingsTabs.indexOf(self.generalTab), QCoreApplication.translate("SettingsDialog", u"General", None))
|
self.settingsTabs.setTabText(self.settingsTabs.indexOf(self.generalTab), QCoreApplication.translate("SettingsDialog", u"General", None))
|
||||||
|
self.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.openModelicaGroupBox.setTitle(QCoreApplication.translate("SettingsDialog", u"OpenModelica", None))
|
||||||
self.openModelicaPathLabel.setText(QCoreApplication.translate("SettingsDialog", u"OpenModelica executable:", None))
|
self.openModelicaPathLabel.setText(QCoreApplication.translate("SettingsDialog", u"OpenModelica executable:", None))
|
||||||
self.openModelicaPathEdit.setPlaceholderText(QCoreApplication.translate("SettingsDialog", u"Leave empty to find omc on PATH", None))
|
self.openModelicaPathEdit.setPlaceholderText(QCoreApplication.translate("SettingsDialog", u"Leave empty to find omc on PATH", None))
|
||||||
|
|||||||
@@ -16,10 +16,11 @@ from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
|
|||||||
QIcon, QImage, QKeySequence, QLinearGradient,
|
QIcon, QImage, QKeySequence, QLinearGradient,
|
||||||
QPainter, QPalette, QPixmap, QRadialGradient,
|
QPainter, QPalette, QPixmap, QRadialGradient,
|
||||||
QTransform)
|
QTransform)
|
||||||
from PySide6.QtWidgets import (QApplication, QDockWidget, QHBoxLayout, QLabel,
|
from PySide6.QtWidgets import (QAbstractItemView, QApplication, QDockWidget, QHBoxLayout,
|
||||||
QListWidget, QListWidgetItem, QMainWindow, QMenu,
|
QHeaderView, QLabel, QListWidget, QListWidgetItem,
|
||||||
QMenuBar, QProgressBar, QSizePolicy, QToolBar,
|
QMainWindow, QMenu, QMenuBar, QProgressBar,
|
||||||
QVBoxLayout, QWidget)
|
QSizePolicy, QTabWidget, QToolBar, QTreeWidget,
|
||||||
|
QTreeWidgetItem, QVBoxLayout, QWidget)
|
||||||
from . import resources_rc
|
from . import resources_rc
|
||||||
|
|
||||||
class Ui_SimulationWindow(object):
|
class Ui_SimulationWindow(object):
|
||||||
@@ -60,15 +61,26 @@ class Ui_SimulationWindow(object):
|
|||||||
self.actionToggleResults.setObjectName(u"actionToggleResults")
|
self.actionToggleResults.setObjectName(u"actionToggleResults")
|
||||||
self.actionToggleResults.setCheckable(True)
|
self.actionToggleResults.setCheckable(True)
|
||||||
self.actionToggleResults.setChecked(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 = QWidget(SimulationWindow)
|
||||||
self.centralWidget.setObjectName(u"centralWidget")
|
self.centralWidget.setObjectName(u"centralWidget")
|
||||||
self.resultsLayout = QVBoxLayout(self.centralWidget)
|
self.resultsLayout = QVBoxLayout(self.centralWidget)
|
||||||
self.resultsLayout.setObjectName(u"resultsLayout")
|
self.resultsLayout.setObjectName(u"resultsLayout")
|
||||||
self.resultsPlaceholder = QLabel(self.centralWidget)
|
self.graphTabs = QTabWidget(self.centralWidget)
|
||||||
self.resultsPlaceholder.setObjectName(u"resultsPlaceholder")
|
self.graphTabs.setObjectName(u"graphTabs")
|
||||||
self.resultsPlaceholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
self.graphTabs.setTabsClosable(False)
|
||||||
|
self.graphTabs.setMovable(True)
|
||||||
|
|
||||||
self.resultsLayout.addWidget(self.resultsPlaceholder)
|
self.resultsLayout.addWidget(self.graphTabs)
|
||||||
|
|
||||||
SimulationWindow.setCentralWidget(self.centralWidget)
|
SimulationWindow.setCentralWidget(self.centralWidget)
|
||||||
self.menuBar = QMenuBar(SimulationWindow)
|
self.menuBar = QMenuBar(SimulationWindow)
|
||||||
@@ -84,7 +96,13 @@ class Ui_SimulationWindow(object):
|
|||||||
self.menuToolbars.setObjectName(u"menuToolbars")
|
self.menuToolbars.setObjectName(u"menuToolbars")
|
||||||
self.menuHelp = QMenu(self.menuBar)
|
self.menuHelp = QMenu(self.menuBar)
|
||||||
self.menuHelp.setObjectName(u"menuHelp")
|
self.menuHelp.setObjectName(u"menuHelp")
|
||||||
|
self.menuGraph = QMenu(self.menuBar)
|
||||||
|
self.menuGraph.setObjectName(u"menuGraph")
|
||||||
SimulationWindow.setMenuBar(self.menuBar)
|
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 = QToolBar(SimulationWindow)
|
||||||
self.fileToolbar.setObjectName(u"fileToolbar")
|
self.fileToolbar.setObjectName(u"fileToolbar")
|
||||||
self.fileToolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
|
self.fileToolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
|
||||||
@@ -130,9 +148,26 @@ class Ui_SimulationWindow(object):
|
|||||||
|
|
||||||
self.logDock.setWidget(self.logDockContents)
|
self.logDock.setWidget(self.logDockContents)
|
||||||
SimulationWindow.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.logDock)
|
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.menuFile.menuAction())
|
||||||
self.menuBar.addAction(self.menuView.menuAction())
|
self.menuBar.addAction(self.menuView.menuAction())
|
||||||
|
self.menuBar.addAction(self.menuGraph.menuAction())
|
||||||
self.menuBar.addAction(self.menuHelp.menuAction())
|
self.menuBar.addAction(self.menuHelp.menuAction())
|
||||||
self.menuFile.addAction(self.actionOpen)
|
self.menuFile.addAction(self.actionOpen)
|
||||||
self.menuFile.addAction(self.actionSave)
|
self.menuFile.addAction(self.actionSave)
|
||||||
@@ -145,10 +180,14 @@ class Ui_SimulationWindow(object):
|
|||||||
self.menuView.addAction(self.menuToolbars.menuAction())
|
self.menuView.addAction(self.menuToolbars.menuAction())
|
||||||
self.menuHelp.addAction(self.actionAbout)
|
self.menuHelp.addAction(self.actionAbout)
|
||||||
self.menuHelp.addAction(self.actionAboutQt)
|
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.actionOpen)
|
||||||
self.fileToolbar.addAction(self.actionSave)
|
self.fileToolbar.addAction(self.actionSave)
|
||||||
self.fileToolbar.addAction(self.actionSaveAs)
|
self.fileToolbar.addAction(self.actionSaveAs)
|
||||||
self.fileToolbar.addAction(self.actionClear)
|
|
||||||
|
|
||||||
self.retranslateUi(SimulationWindow)
|
self.retranslateUi(SimulationWindow)
|
||||||
|
|
||||||
@@ -177,17 +216,29 @@ class Ui_SimulationWindow(object):
|
|||||||
self.actionAbout.setText(QCoreApplication.translate("SimulationWindow", u"&About Simulation Window", None))
|
self.actionAbout.setText(QCoreApplication.translate("SimulationWindow", u"&About Simulation Window", None))
|
||||||
self.actionAboutQt.setText(QCoreApplication.translate("SimulationWindow", u"About &Qt", None))
|
self.actionAboutQt.setText(QCoreApplication.translate("SimulationWindow", u"About &Qt", None))
|
||||||
self.actionToggleResults.setText(QCoreApplication.translate("SimulationWindow", u"Results", None))
|
self.actionToggleResults.setText(QCoreApplication.translate("SimulationWindow", u"Results", None))
|
||||||
self.resultsPlaceholder.setText(QCoreApplication.translate("SimulationWindow", u"Simulation graphs and result controls can be added here.", 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.menuFile.setTitle(QCoreApplication.translate("SimulationWindow", u"&File", None))
|
||||||
self.menuView.setTitle(QCoreApplication.translate("SimulationWindow", u"&View", None))
|
self.menuView.setTitle(QCoreApplication.translate("SimulationWindow", u"&View", None))
|
||||||
self.menuPanels.setTitle(QCoreApplication.translate("SimulationWindow", u"&Panels", None))
|
self.menuPanels.setTitle(QCoreApplication.translate("SimulationWindow", u"&Panels", None))
|
||||||
self.menuToolbars.setTitle(QCoreApplication.translate("SimulationWindow", u"&Toolbars", None))
|
self.menuToolbars.setTitle(QCoreApplication.translate("SimulationWindow", u"&Toolbars", None))
|
||||||
self.menuHelp.setTitle(QCoreApplication.translate("SimulationWindow", u"&Help", 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.fileToolbar.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"File", None))
|
||||||
self.statusDock.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Status", None))
|
self.statusDock.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Status", None))
|
||||||
self.timeLabel.setText(QCoreApplication.translate("SimulationWindow", u"Time: 0 s", None))
|
self.timeLabel.setText(QCoreApplication.translate("SimulationWindow", u"Time: 0 s", None))
|
||||||
self.progressBar.setFormat(QCoreApplication.translate("SimulationWindow", u"%p%", 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.statusLabel.setText(QCoreApplication.translate("SimulationWindow", u"No simulation has been run yet.", None))
|
||||||
self.logDock.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Log", 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
|
# retranslateUi
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
|||||||
QFont, QFontDatabase, QGradient, QIcon,
|
QFont, QFontDatabase, QGradient, QIcon,
|
||||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||||
from PySide6.QtWidgets import (QAbstractItemView, QApplication, QGroupBox, QHBoxLayout,
|
from PySide6.QtWidgets import (QApplication, QGroupBox, QHBoxLayout, QSizePolicy,
|
||||||
QHeaderView, QPushButton, QSizePolicy, QSpacerItem,
|
QSplitter, QVBoxLayout, QWidget)
|
||||||
QSplitter, QTableWidget, QTableWidgetItem, 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
|
from bedit.gui.editors.openmodelica import OpenModelicaEditor
|
||||||
|
|
||||||
class Ui_TextDefinitionEditor(object):
|
class Ui_TextDefinitionEditor(object):
|
||||||
@@ -34,7 +34,31 @@ class Ui_TextDefinitionEditor(object):
|
|||||||
self.columnSplitter.setObjectName(u"columnSplitter")
|
self.columnSplitter.setObjectName(u"columnSplitter")
|
||||||
self.columnSplitter.setOrientation(Qt.Orientation.Horizontal)
|
self.columnSplitter.setOrientation(Qt.Orientation.Horizontal)
|
||||||
self.columnSplitter.setChildrenCollapsible(False)
|
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.equationsGroup.setObjectName(u"equationsGroup")
|
||||||
self.equationsLayout = QVBoxLayout(self.equationsGroup)
|
self.equationsLayout = QVBoxLayout(self.equationsGroup)
|
||||||
self.equationsLayout.setObjectName(u"equationsLayout")
|
self.equationsLayout.setObjectName(u"equationsLayout")
|
||||||
@@ -43,7 +67,8 @@ class Ui_TextDefinitionEditor(object):
|
|||||||
|
|
||||||
self.equationsLayout.addWidget(self.equationsEdit)
|
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 = QSplitter(self.columnSplitter)
|
||||||
self.definitionSplitter.setObjectName(u"definitionSplitter")
|
self.definitionSplitter.setObjectName(u"definitionSplitter")
|
||||||
self.definitionSplitter.setOrientation(Qt.Orientation.Vertical)
|
self.definitionSplitter.setOrientation(Qt.Orientation.Vertical)
|
||||||
@@ -52,82 +77,20 @@ class Ui_TextDefinitionEditor(object):
|
|||||||
self.portsGroup.setObjectName(u"portsGroup")
|
self.portsGroup.setObjectName(u"portsGroup")
|
||||||
self.portsLayout = QVBoxLayout(self.portsGroup)
|
self.portsLayout = QVBoxLayout(self.portsGroup)
|
||||||
self.portsLayout.setObjectName(u"portsLayout")
|
self.portsLayout.setObjectName(u"portsLayout")
|
||||||
self.portsTable = QTableWidget(self.portsGroup)
|
self.portEditor = PortEditor(self.portsGroup)
|
||||||
if (self.portsTable.columnCount() < 4):
|
self.portEditor.setObjectName(u"portEditor")
|
||||||
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.portsLayout.addWidget(self.portsTable)
|
self.portsLayout.addWidget(self.portEditor)
|
||||||
|
|
||||||
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.definitionSplitter.addWidget(self.portsGroup)
|
self.definitionSplitter.addWidget(self.portsGroup)
|
||||||
self.parametersGroup = QGroupBox(self.definitionSplitter)
|
self.parametersGroup = QGroupBox(self.definitionSplitter)
|
||||||
self.parametersGroup.setObjectName(u"parametersGroup")
|
self.parametersGroup.setObjectName(u"parametersGroup")
|
||||||
self.parametersLayout = QVBoxLayout(self.parametersGroup)
|
self.parametersLayout = QVBoxLayout(self.parametersGroup)
|
||||||
self.parametersLayout.setObjectName(u"parametersLayout")
|
self.parametersLayout.setObjectName(u"parametersLayout")
|
||||||
self.parametersTable = QTableWidget(self.parametersGroup)
|
self.parameterEditor = ParameterEditor(self.parametersGroup)
|
||||||
if (self.parametersTable.columnCount() < 3):
|
self.parameterEditor.setObjectName(u"parameterEditor")
|
||||||
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.parametersLayout.addWidget(self.parametersTable)
|
self.parametersLayout.addWidget(self.parameterEditor)
|
||||||
|
|
||||||
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.definitionSplitter.addWidget(self.parametersGroup)
|
self.definitionSplitter.addWidget(self.parametersGroup)
|
||||||
self.columnSplitter.addWidget(self.definitionSplitter)
|
self.columnSplitter.addWidget(self.definitionSplitter)
|
||||||
@@ -141,27 +104,11 @@ class Ui_TextDefinitionEditor(object):
|
|||||||
# setupUi
|
# setupUi
|
||||||
|
|
||||||
def retranslateUi(self, TextDefinitionEditor):
|
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.equationsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Equations", None))
|
||||||
self.portsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Ports", 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))
|
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
|
pass
|
||||||
# retranslateUi
|
# retranslateUi
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
|
|
||||||
|
|
||||||
|
ArrowStyle = Literal["open", "half", "filled"]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ConnectionStyle:
|
class ConnectionStyle:
|
||||||
color: str = "#285f9e"
|
color: str = "#285f9e"
|
||||||
@@ -13,11 +17,17 @@ class ConnectionStyle:
|
|||||||
arrow_at_source: bool = False
|
arrow_at_source: bool = False
|
||||||
arrow_at_target: bool = True
|
arrow_at_target: bool = True
|
||||||
arrow_size: float = 10.0
|
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.
|
# This is the intentional code-level styling point for every port/connection type.
|
||||||
CONNECTION_STYLES: dict[str, ConnectionStyle] = {
|
CONNECTION_STYLES: dict[str, ConnectionStyle] = {
|
||||||
"signal": ConnectionStyle(),
|
"signal": ConnectionStyle(),
|
||||||
|
"power": ConnectionStyle(width=3.0, arrow_style="half", color="#000000", arrow_size=20.0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from copy import deepcopy
|
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.QtGui import QColor, QPainter, QPainterPath, QPen, QPolygonF
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QColorDialog,
|
QColorDialog,
|
||||||
@@ -458,8 +458,7 @@ class IconEditorDialog(QDialog):
|
|||||||
self.ui.setupUi(self)
|
self.ui.setupUi(self)
|
||||||
self.setWindowTitle(f"Icon Editor — {component.name}")
|
self.setWindowTitle(f"Icon Editor — {component.name}")
|
||||||
self.icon = Icon.from_dict(component.icon.to_dict())
|
self.icon = Icon.from_dict(component.icon.to_dict())
|
||||||
self.inputs = deepcopy(component.inputs)
|
self.ports = deepcopy(component.ports)
|
||||||
self.outputs = deepcopy(component.outputs)
|
|
||||||
self.tool_group = QButtonGroup(self)
|
self.tool_group = QButtonGroup(self)
|
||||||
self.tool_group.setExclusive(True)
|
self.tool_group.setExclusive(True)
|
||||||
self.tool_group.addButton(self.ui.pointerButton)
|
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)
|
self.scene.addRect(self.scene.sceneRect(), QPen(QColor("#64748b"), 0)).setZValue(-100)
|
||||||
for element in self.icon.elements:
|
for element in self.icon.elements:
|
||||||
self.scene.addItem(ShapeItem(element))
|
self.scene.addItem(ShapeItem(element))
|
||||||
self._add_ports(self.inputs, "input", 0.0)
|
input_side = [port for port in self.ports if port.orientation != "output"]
|
||||||
self._add_ports(self.outputs, "output", self.icon.width)
|
output_side = [port for port in self.ports if port.orientation == "output"]
|
||||||
self.view.center_icon()
|
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:
|
def _add_ports(self, ports: list[Port], direction: str, default_x: float) -> None:
|
||||||
spacing = self.icon.height / (len(ports) + 1)
|
spacing = self.icon.height / (len(ports) + 1)
|
||||||
|
|||||||
@@ -173,8 +173,16 @@ class ComponentGraphicsItem(QGraphicsObject):
|
|||||||
| QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
|
| QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
|
||||||
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
|
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
|
||||||
)
|
)
|
||||||
self.input_ports = self._create_ports(component.inputs, "target", 0.0)
|
self.input_ports = self._create_ports(
|
||||||
self.output_ports = self._create_ports(component.outputs, "source", self.WIDTH)
|
[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.setTransformOriginPoint(self.hitbox.center())
|
||||||
self.setRotation(component.rotation)
|
self.setRotation(component.rotation)
|
||||||
self.name_label: NameLabelItem | None = None
|
self.name_label: NameLabelItem | None = None
|
||||||
@@ -371,8 +379,10 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.connection_id = connection.id
|
self.connection_id = connection.id
|
||||||
self.name = connection.name
|
self.name = connection.name
|
||||||
|
self.connection_type = connection.type
|
||||||
self.source_is_junction = connection.source.junction is not None
|
self.source_is_junction = connection.source.junction is not None
|
||||||
self.target_is_junction = connection.target.junction is not None
|
self.target_is_junction = connection.target.junction is not None
|
||||||
|
self.causality = connection.causality
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.style = style or ConnectionStyle()
|
self.style = style or ConnectionStyle()
|
||||||
self.start = QPointF()
|
self.start = QPointF()
|
||||||
@@ -387,6 +397,19 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
|
|||||||
self.name_label: NameLabelItem | None = None
|
self.name_label: NameLabelItem | None = None
|
||||||
self.sync_name_label(connection)
|
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:
|
def sync_name_label(self, connection: Connection) -> None:
|
||||||
visible = bool(connection.properties.get("showName", False))
|
visible = bool(connection.properties.get("showName", False))
|
||||||
if not visible:
|
if not visible:
|
||||||
@@ -417,7 +440,11 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
|
|||||||
self.setSelected(True)
|
self.setSelected(True)
|
||||||
menu = QMenu()
|
menu = QMenu()
|
||||||
add_node_action = menu.addAction("Add Node")
|
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()
|
menu.addSeparator()
|
||||||
options_action = menu.addAction("Connection Options…")
|
options_action = menu.addAction("Connection Options…")
|
||||||
selected = menu.exec(event.screenPos())
|
selected = menu.exec(event.screenPos())
|
||||||
@@ -425,7 +452,7 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
|
|||||||
scene = self.scene()
|
scene = self.scene()
|
||||||
if isinstance(scene, GraphScene):
|
if isinstance(scene, GraphScene):
|
||||||
scene.add_route_node("connection", self.connection_id, event.scenePos())
|
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()
|
scene = self.scene()
|
||||||
if isinstance(scene, GraphScene):
|
if isinstance(scene, GraphScene):
|
||||||
scene.add_connection_junction(self.connection_id, event.scenePos())
|
scene.add_connection_junction(self.connection_id, event.scenePos())
|
||||||
@@ -485,22 +512,62 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
|
|||||||
handle.setVisible(self.isSelected())
|
handle.setVisible(self.isSelected())
|
||||||
|
|
||||||
@staticmethod
|
@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)
|
length = max(0.001, (direction.x() ** 2 + direction.y() ** 2) ** 0.5)
|
||||||
unit = QPointF(direction.x() / length, direction.y() / length)
|
unit = QPointF(direction.x() / length, direction.y() / length)
|
||||||
normal = QPointF(-unit.y(), unit.x())
|
normal = QPointF(-unit.y(), unit.x())
|
||||||
base = end - unit * size
|
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:
|
def paint(self, painter: QPainter, option, widget=None) -> None:
|
||||||
super().paint(painter, option, widget)
|
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:
|
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:
|
if self.style.arrow_at_source and not self.source_is_junction:
|
||||||
painter.drawPolygon(
|
self._draw_arrow(painter, self.start, -self.start_direction)
|
||||||
self._arrow(self.start, -self.start_direction, self.style.arrow_size)
|
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
|
owner = self.controller.active_component
|
||||||
if owner is None or owner.implementation_kind != "graph":
|
if owner is None or owner.implementation_kind != "graph":
|
||||||
return
|
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)
|
item = InterfaceTerminalItem(port, "input", self.controller)
|
||||||
self.addItem(item)
|
self.addItem(item)
|
||||||
item.setPos(port.x, port.y)
|
item.setPos(port.x, port.y)
|
||||||
self.input_items[port.id] = item
|
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)
|
item = InterfaceTerminalItem(port, "output", self.controller)
|
||||||
self.addItem(item)
|
self.addItem(item)
|
||||||
item.setPos(port.x, port.y)
|
item.setPos(port.x, port.y)
|
||||||
@@ -1207,9 +1274,23 @@ class GraphScene(QGraphicsScene):
|
|||||||
def add_pairs(
|
def add_pairs(
|
||||||
source_item: ComponentGraphicsItem, target_item: ComponentGraphicsItem
|
source_item: ComponentGraphicsItem, target_item: ComponentGraphicsItem
|
||||||
) -> None:
|
) -> None:
|
||||||
for output in source_item.component.outputs:
|
source_ports = [
|
||||||
for input_port in target_item.component.inputs:
|
port
|
||||||
if not PortTypeRegistry.compatible(output.type, input_port.type):
|
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
|
continue
|
||||||
source = Endpoint(block=source_item.component_id, port=output.id)
|
source = Endpoint(block=source_item.component_id, port=output.id)
|
||||||
target = Endpoint(block=target_item.component_id, port=input_port.id)
|
target = Endpoint(block=target_item.component_id, port=input_port.id)
|
||||||
@@ -1238,9 +1319,15 @@ class GraphScene(QGraphicsScene):
|
|||||||
choices: list[ConnectionChoice] = []
|
choices: list[ConnectionChoice] = []
|
||||||
interface = Endpoint(interface=terminal.port.id)
|
interface = Endpoint(interface=terminal.port.id)
|
||||||
if terminal.direction == "input":
|
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)
|
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
|
continue
|
||||||
if not self.controller.endpoint_accepts_connection(interface, "source"):
|
if not self.controller.endpoint_accepts_connection(interface, "source"):
|
||||||
continue
|
continue
|
||||||
@@ -1254,9 +1341,16 @@ class GraphScene(QGraphicsScene):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
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)
|
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
|
continue
|
||||||
if not self.controller.endpoint_accepts_connection(source, "source"):
|
if not self.controller.endpoint_accepts_connection(source, "source"):
|
||||||
continue
|
continue
|
||||||
@@ -1278,7 +1372,11 @@ class GraphScene(QGraphicsScene):
|
|||||||
) -> list[ConnectionChoice]:
|
) -> list[ConnectionChoice]:
|
||||||
choices: list[ConnectionChoice] = []
|
choices: list[ConnectionChoice] = []
|
||||||
source = junction.endpoint
|
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)
|
target = Endpoint(block=component.component_id, port=port.id)
|
||||||
if not PortTypeRegistry.compatible(junction.junction.type, port.type):
|
if not PortTypeRegistry.compatible(junction.junction.type, port.type):
|
||||||
continue
|
continue
|
||||||
@@ -1559,6 +1657,8 @@ class GraphScene(QGraphicsScene):
|
|||||||
graphics = self.connection_items.get(connection_id)
|
graphics = self.connection_items.get(connection_id)
|
||||||
if connection is None or graphics is None:
|
if connection is None or graphics is None:
|
||||||
return
|
return
|
||||||
|
if connection.type == "power":
|
||||||
|
return
|
||||||
snapped = _snapped(position)
|
snapped = _snapped(position)
|
||||||
anchors = [graphics.start, *points, graphics.end]
|
anchors = [graphics.start, *points, graphics.end]
|
||||||
|
|
||||||
@@ -1809,8 +1909,7 @@ class GraphWorkspaceView(QGraphicsView):
|
|||||||
return
|
return
|
||||||
blocks: set[str] = set()
|
blocks: set[str] = set()
|
||||||
connections: set[str] = set()
|
connections: set[str] = set()
|
||||||
inputs: set[str] = set()
|
ports: set[str] = set()
|
||||||
outputs: set[str] = set()
|
|
||||||
annotations: set[str] = set()
|
annotations: set[str] = set()
|
||||||
for item in self.scene().selectedItems():
|
for item in self.scene().selectedItems():
|
||||||
if isinstance(item, ComponentGraphicsItem):
|
if isinstance(item, ComponentGraphicsItem):
|
||||||
@@ -1818,10 +1917,10 @@ class GraphWorkspaceView(QGraphicsView):
|
|||||||
elif isinstance(item, ConnectionGraphicsItem):
|
elif isinstance(item, ConnectionGraphicsItem):
|
||||||
connections.add(item.connection_id)
|
connections.add(item.connection_id)
|
||||||
elif isinstance(item, InterfaceTerminalItem):
|
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)):
|
elif isinstance(item, (AnnotationGraphicsItem, LineAnnotationGraphicsItem)):
|
||||||
annotations.add(item.annotation_id)
|
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)
|
self.controller.delete_annotations(annotations)
|
||||||
|
|
||||||
def has_selected_components(self) -> bool:
|
def has_selected_components(self) -> bool:
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ class MainWindow(QMainWindow):
|
|||||||
)
|
)
|
||||||
self.ui.actionGraphParameters.triggered.connect(self.show_graph_parameters)
|
self.ui.actionGraphParameters.triggered.connect(self.show_graph_parameters)
|
||||||
self.ui.actionCompose.triggered.connect(self.compose_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.actionSimulationWindow.triggered.connect(self.show_simulation_window)
|
||||||
self.ui.actionRunSimulation.triggered.connect(self.run_simulation)
|
self.ui.actionRunSimulation.triggered.connect(self.run_simulation)
|
||||||
self.document_controller.undo_stack.canUndoChanged.connect(self.ui.actionUndo.setEnabled)
|
self.document_controller.undo_stack.canUndoChanged.connect(self.ui.actionUndo.setEnabled)
|
||||||
@@ -261,6 +262,35 @@ class MainWindow(QMainWindow):
|
|||||||
self.log.error("Composition failed: %s", error)
|
self.log.error("Composition failed: %s", error)
|
||||||
QMessageBox.warning(self, "Cannot compose", str(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()
|
@Slot()
|
||||||
def show_simulation_window(self) -> None:
|
def show_simulation_window(self) -> None:
|
||||||
self._simulation_window.show()
|
self._simulation_window.show()
|
||||||
@@ -274,7 +304,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.show_simulation_window()
|
self.show_simulation_window()
|
||||||
try:
|
try:
|
||||||
self.document_controller.run_simulation(*callbacks)
|
self.document_controller.run_simulation(*callbacks)
|
||||||
window.set_model_name(self.simulation.model_name)
|
window.prepare_run_model(self.simulation.model_name)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
self.log.exception("Simulation run failed")
|
self.log.exception("Simulation run failed")
|
||||||
window.report_start_error(error)
|
window.report_start_error(error)
|
||||||
@@ -307,6 +337,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.ui.actionSimulationSettings.setEnabled(False)
|
self.ui.actionSimulationSettings.setEnabled(False)
|
||||||
self.ui.actionGraphParameters.setEnabled(False)
|
self.ui.actionGraphParameters.setEnabled(False)
|
||||||
self.ui.actionCompose.setEnabled(False)
|
self.ui.actionCompose.setEnabled(False)
|
||||||
|
self.ui.actionExportModel.setEnabled(False)
|
||||||
self.ui.actionRunSimulation.setEnabled(False)
|
self.ui.actionRunSimulation.setEnabled(False)
|
||||||
self._update_edit_actions()
|
self._update_edit_actions()
|
||||||
return
|
return
|
||||||
@@ -318,6 +349,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.ui.actionSimulationSettings.setEnabled(is_graph)
|
self.ui.actionSimulationSettings.setEnabled(is_graph)
|
||||||
self.ui.actionGraphParameters.setEnabled(is_graph)
|
self.ui.actionGraphParameters.setEnabled(is_graph)
|
||||||
self.ui.actionCompose.setEnabled(is_graph)
|
self.ui.actionCompose.setEnabled(is_graph)
|
||||||
|
self.ui.actionExportModel.setEnabled(is_graph)
|
||||||
self.ui.actionRunSimulation.setEnabled(is_graph)
|
self.ui.actionRunSimulation.setEnabled(is_graph)
|
||||||
self.ui.workspaceModeLabel.setText("Graph" if is_graph else "Text")
|
self.ui.workspaceModeLabel.setText("Graph" if is_graph else "Text")
|
||||||
self.ui.workspaceStack.setCurrentWidget(
|
self.ui.workspaceStack.setCurrentWidget(
|
||||||
@@ -505,9 +537,10 @@ class MainWindow(QMainWindow):
|
|||||||
if component is None:
|
if component is None:
|
||||||
return
|
return
|
||||||
self.ui.textDefinitionEditor.set_definition(
|
self.ui.textDefinitionEditor.set_definition(
|
||||||
|
component.source.get("declarations", ""),
|
||||||
|
component.source.get("initialEquations", ""),
|
||||||
component.source.get("equations", ""),
|
component.source.get("equations", ""),
|
||||||
component.inputs,
|
component.ports,
|
||||||
component.outputs,
|
|
||||||
component.parameters,
|
component.parameters,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -522,7 +555,8 @@ class MainWindow(QMainWindow):
|
|||||||
answer = QMessageBox.question(
|
answer = QMessageBox.question(
|
||||||
self,
|
self,
|
||||||
"Apply text component changes?",
|
"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.Apply
|
||||||
| QMessageBox.StandardButton.Discard
|
| QMessageBox.StandardButton.Discard
|
||||||
| QMessageBox.StandardButton.Cancel,
|
| QMessageBox.StandardButton.Cancel,
|
||||||
@@ -534,12 +568,13 @@ class MainWindow(QMainWindow):
|
|||||||
@Slot()
|
@Slot()
|
||||||
def apply_text_definition(self) -> bool:
|
def apply_text_definition(self) -> bool:
|
||||||
try:
|
try:
|
||||||
inputs, outputs = self.ui.textDefinitionEditor.ports
|
ports = self.ui.textDefinitionEditor.ports
|
||||||
self._applying_text_definition = True
|
self._applying_text_definition = True
|
||||||
try:
|
try:
|
||||||
self.document_controller.replace_active_text_definition(
|
self.document_controller.replace_active_text_definition(
|
||||||
inputs,
|
ports,
|
||||||
outputs,
|
self.ui.textDefinitionEditor.declarations,
|
||||||
|
self.ui.textDefinitionEditor.initial_equations,
|
||||||
self.ui.textDefinitionEditor.equations,
|
self.ui.textDefinitionEditor.equations,
|
||||||
self.ui.textDefinitionEditor.parameters,
|
self.ui.textDefinitionEditor.parameters,
|
||||||
)
|
)
|
||||||
@@ -606,12 +641,23 @@ class MainWindow(QMainWindow):
|
|||||||
)
|
)
|
||||||
if not filename:
|
if not filename:
|
||||||
return
|
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:
|
try:
|
||||||
self.document_controller.load(Path(filename))
|
self.document_controller.load(path)
|
||||||
self.log.info("Opened document %s", filename)
|
self.log.info("Opened document %s", path)
|
||||||
except (OSError, ValueError) as error:
|
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))
|
QMessageBox.critical(self, "Could not open graph", str(error))
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def save_document(self) -> bool:
|
def save_document(self) -> bool:
|
||||||
@@ -672,6 +718,8 @@ class MainWindow(QMainWindow):
|
|||||||
if scene is not None:
|
if scene is not None:
|
||||||
scene.update()
|
scene.update()
|
||||||
self.ui.graphView.viewport().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()
|
self.ui.textDefinitionEditor.ui.equationsEdit.reload_highlighting()
|
||||||
|
|
||||||
def _document_opened_changed(self, opened: bool) -> None:
|
def _document_opened_changed(self, opened: bool) -> None:
|
||||||
@@ -779,9 +827,8 @@ class MainWindow(QMainWindow):
|
|||||||
elif selected is ports_action:
|
elif selected is ports_action:
|
||||||
dialog = PortOptionsDialog(component, self)
|
dialog = PortOptionsDialog(component, self)
|
||||||
if dialog.exec() == dialog.DialogCode.Accepted:
|
if dialog.exec() == dialog.DialogCode.Accepted:
|
||||||
old_inputs, old_outputs = deepcopy(component.inputs), deepcopy(component.outputs)
|
old_ports = deepcopy(component.ports)
|
||||||
component.inputs = dialog.inputs
|
component.ports = dialog.ports
|
||||||
component.outputs = dialog.outputs
|
|
||||||
library = next(
|
library = next(
|
||||||
(
|
(
|
||||||
library
|
library
|
||||||
@@ -795,7 +842,7 @@ class MainWindow(QMainWindow):
|
|||||||
library.document.validate()
|
library.document.validate()
|
||||||
DocumentSerializer.save(library.document, Path(library.source_path))
|
DocumentSerializer.save(library.document, Path(library.source_path))
|
||||||
except (OSError, ValueError) as error:
|
except (OSError, ValueError) as error:
|
||||||
component.inputs, component.outputs = old_inputs, old_outputs
|
component.ports = old_ports
|
||||||
self.log.error("Could not change library ports: %s", error)
|
self.log.error("Could not change library ports: %s", error)
|
||||||
QMessageBox.warning(self, "Cannot change library ports", str(error))
|
QMessageBox.warning(self, "Cannot change library ports", str(error))
|
||||||
self.library_tree_model.rebuild()
|
self.library_tree_model.rebuild()
|
||||||
@@ -837,7 +884,7 @@ class MainWindow(QMainWindow):
|
|||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
self.document_controller.edit_component_ports(
|
self.document_controller.edit_component_ports(
|
||||||
component_id, dialog.inputs, dialog.outputs
|
component_id, dialog.ports
|
||||||
)
|
)
|
||||||
except ValueError as error:
|
except ValueError as error:
|
||||||
self.log.error("Could not change component ports: %s", error)
|
self.log.error("Could not change component ports: %s", error)
|
||||||
@@ -873,8 +920,7 @@ class MainWindow(QMainWindow):
|
|||||||
component_id,
|
component_id,
|
||||||
dialog.ui.nameEdit.text().strip(),
|
dialog.ui.nameEdit.text().strip(),
|
||||||
dialog.edited_icon,
|
dialog.edited_icon,
|
||||||
dialog.edited_inputs,
|
dialog.edited_ports,
|
||||||
dialog.edited_outputs,
|
|
||||||
dialog.ui.showSubtreeCheckBox.isChecked(),
|
dialog.ui.showSubtreeCheckBox.isChecked(),
|
||||||
dialog.ui.showNameCheckBox.isChecked(),
|
dialog.ui.showNameCheckBox.isChecked(),
|
||||||
)
|
)
|
||||||
@@ -887,8 +933,7 @@ class MainWindow(QMainWindow):
|
|||||||
owner = self.document_controller.active_component
|
owner = self.document_controller.active_component
|
||||||
if owner is None:
|
if owner is None:
|
||||||
return
|
return
|
||||||
ports = owner.inputs if direction == "input" else owner.outputs
|
port = next((item for item in owner.ports if item.id == port_id), None)
|
||||||
port = next((item for item in ports if item.id == port_id), None)
|
|
||||||
if port is None:
|
if port is None:
|
||||||
return
|
return
|
||||||
dialog = ItemOptionsDialog(f"{direction.title()} Options", port.name, self)
|
dialog = ItemOptionsDialog(f"{direction.title()} Options", port.name, self)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import json
|
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 PySide6.QtGui import QStandardItem, QStandardItemModel
|
||||||
|
|
||||||
from bedit.core.model import Component
|
from bedit.core.model import Component
|
||||||
@@ -80,8 +80,24 @@ class DocumentTreeModel(LibraryTreeModel):
|
|||||||
controller.componentMoved.connect(lambda _component_id, _position: self.rebuild())
|
controller.componentMoved.connect(lambda _component_id, _position: self.rebuild())
|
||||||
controller.connectionAdded.connect(lambda _connection_id: self.rebuild())
|
controller.connectionAdded.connect(lambda _connection_id: self.rebuild())
|
||||||
controller.connectionRemoved.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()
|
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:
|
def rebuild(self) -> None:
|
||||||
self.clear()
|
self.clear()
|
||||||
self.setHorizontalHeaderLabels(["Document"])
|
self.setHorizontalHeaderLabels(["Document"])
|
||||||
|
|||||||
@@ -1,17 +1,33 @@
|
|||||||
import re
|
import re
|
||||||
from pathlib import Path
|
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.QtCore import Qt, Signal
|
||||||
from PySide6.QtWidgets import QFileDialog, QMainWindow, QMessageBox
|
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.openmodelica import SimulationMessage, SimulationProgress
|
||||||
from bedit.core.simulation.results import (
|
from bedit.core.simulation.results import (
|
||||||
SimulationExecutionResult,
|
SimulationExecutionResult,
|
||||||
|
SimulationGraph,
|
||||||
SimulationResults,
|
SimulationResults,
|
||||||
|
SimulationTrace,
|
||||||
load_simulation_results,
|
load_simulation_results,
|
||||||
save_simulation_results,
|
save_simulation_results,
|
||||||
)
|
)
|
||||||
from bedit.gui.generated.ui_simulation_window import Ui_SimulationWindow
|
from bedit.gui.generated.ui_simulation_window import Ui_SimulationWindow
|
||||||
|
from bedit.gui.preferences import application_settings
|
||||||
|
|
||||||
|
|
||||||
class SimulationWindow(QMainWindow):
|
class SimulationWindow(QMainWindow):
|
||||||
@@ -26,9 +42,12 @@ class SimulationWindow(QMainWindow):
|
|||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.ui = Ui_SimulationWindow()
|
self.ui = Ui_SimulationWindow()
|
||||||
self.ui.setupUi(self)
|
self.ui.setupUi(self)
|
||||||
|
self.settings = application_settings()
|
||||||
self._running = False
|
self._running = False
|
||||||
self._run_generation = 0
|
self._run_generation = 0
|
||||||
self._file_path: Path | None = None
|
self._file_path: Path | None = None
|
||||||
|
self._rebuilding_graph_tabs = False
|
||||||
|
self._updating_signal_checks = False
|
||||||
self.results = SimulationResults()
|
self.results = SimulationResults()
|
||||||
self._connect_actions()
|
self._connect_actions()
|
||||||
self._populate_view_menu()
|
self._populate_view_menu()
|
||||||
@@ -36,6 +55,7 @@ class SimulationWindow(QMainWindow):
|
|||||||
self.ui.statusDock, self.ui.logDock, Qt.Orientation.Vertical
|
self.ui.statusDock, self.ui.logDock, Qt.Orientation.Vertical
|
||||||
)
|
)
|
||||||
self.ui.statusDock.setFixedHeight(self.ui.statusDock.sizeHint().height())
|
self.ui.statusDock.setFixedHeight(self.ui.statusDock.sizeHint().height())
|
||||||
|
self._restore_window_layout()
|
||||||
self.progressReceived.connect(self._show_progress)
|
self.progressReceived.connect(self._show_progress)
|
||||||
self.messageReceived.connect(self._show_message)
|
self.messageReceived.connect(self._show_message)
|
||||||
self.simulationFinished.connect(self._show_finished)
|
self.simulationFinished.connect(self._show_finished)
|
||||||
@@ -52,19 +72,57 @@ class SimulationWindow(QMainWindow):
|
|||||||
lambda: QMessageBox.aboutQt(self, "About Qt Framework")
|
lambda: QMessageBox.aboutQt(self, "About Qt Framework")
|
||||||
)
|
)
|
||||||
self.ui.actionToggleResults.toggled.connect(self.ui.centralWidget.setVisible)
|
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:
|
def _populate_view_menu(self) -> None:
|
||||||
self.ui.menuPanels.addAction(self.ui.actionToggleResults)
|
self.ui.menuPanels.addAction(self.ui.actionToggleResults)
|
||||||
for panel in (self.ui.statusDock, self.ui.logDock):
|
for panel in (self.ui.statusDock, self.ui.logDock, self.ui.signalsDock):
|
||||||
self.ui.menuPanels.addAction(panel.toggleViewAction())
|
self.ui.menuPanels.addAction(panel.toggleViewAction())
|
||||||
self.ui.menuToolbars.addAction(self.ui.fileToolbar.toggleViewAction())
|
self.ui.menuToolbars.addAction(self.ui.fileToolbar.toggleViewAction())
|
||||||
|
self.ui.menuToolbars.addAction(self.ui.workspaceToolbar.toggleViewAction())
|
||||||
|
|
||||||
def begin_run(self, model_name: str = "") -> tuple:
|
def _restore_window_layout(self) -> None:
|
||||||
"""Reset the viewer and return callbacks bound to this run."""
|
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)
|
||||||
|
|
||||||
self.clear(model_name=model_name)
|
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
|
generation = self._run_generation
|
||||||
self._running = True
|
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…")
|
self.ui.statusLabel.setText("Preparing simulation…")
|
||||||
return (
|
return (
|
||||||
lambda progress: self._report_progress(generation, progress),
|
lambda progress: self._report_progress(generation, progress),
|
||||||
@@ -73,10 +131,19 @@ class SimulationWindow(QMainWindow):
|
|||||||
lambda error: self._report_error(generation, error),
|
lambda error: self._report_error(generation, error),
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_model_name(self, model_name: str | None) -> None:
|
def prepare_run_model(self, model_name: str | None) -> None:
|
||||||
if model_name:
|
"""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.results.model_name = model_name
|
||||||
self._update_title()
|
self.ui.statusLabel.setText("Preparing simulation…")
|
||||||
|
self._update_title()
|
||||||
|
|
||||||
def clear(self, checked: bool = False, *, model_name: str = "") -> None:
|
def clear(self, checked: bool = False, *, model_name: str = "") -> None:
|
||||||
"""Discard the displayed run and prepare an empty results document."""
|
"""Discard the displayed run and prepare an empty results document."""
|
||||||
@@ -91,6 +158,7 @@ class SimulationWindow(QMainWindow):
|
|||||||
self.ui.timeLabel.setText("Time: 0 s")
|
self.ui.timeLabel.setText("Time: 0 s")
|
||||||
self.ui.messageList.clear()
|
self.ui.messageList.clear()
|
||||||
self.clear_result_views()
|
self.clear_result_views()
|
||||||
|
self.load_result_views()
|
||||||
self._update_title()
|
self._update_title()
|
||||||
|
|
||||||
def clear_result_views(self) -> None:
|
def clear_result_views(self) -> None:
|
||||||
@@ -100,9 +168,10 @@ class SimulationWindow(QMainWindow):
|
|||||||
``resultsLayout`` and reset here.
|
``resultsLayout`` and reset here.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.ui.resultsPlaceholder.setText(
|
self._rebuilding_graph_tabs = True
|
||||||
"Simulation graphs and result controls can be added here."
|
self._clear_graph_tab_widgets()
|
||||||
)
|
self._rebuilding_graph_tabs = False
|
||||||
|
self.ui.signalsTree.clear()
|
||||||
|
|
||||||
def load_result_views(self) -> None:
|
def load_result_views(self) -> None:
|
||||||
"""Populate custom plots from ``self.results.data`` and traces.
|
"""Populate custom plots from ``self.results.data`` and traces.
|
||||||
@@ -110,14 +179,226 @@ class SimulationWindow(QMainWindow):
|
|||||||
This is the intended integration point for a future plotting widget.
|
This is the intended integration point for a future plotting widget.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
data_count = len(self.results.data)
|
self._rebuild_graph_tabs()
|
||||||
trace_count = len(self.results.traces)
|
self._rebuild_signal_tree()
|
||||||
if data_count or trace_count:
|
|
||||||
sample_count = len(next(iter(self.results.data.values()), []))
|
def _rebuild_graph_tabs(self) -> None:
|
||||||
self.ui.resultsPlaceholder.setText(
|
current_page = self.ui.graphTabs.currentWidget()
|
||||||
f"{data_count} data column(s), {sample_count} sample(s), and "
|
current_graph_id = (
|
||||||
f"{trace_count} configured trace(s) loaded; add graph rendering here."
|
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:
|
def open_results(self) -> None:
|
||||||
file_name, _selected_filter = QFileDialog.getOpenFileName(
|
file_name, _selected_filter = QFileDialog.getOpenFileName(
|
||||||
@@ -231,6 +512,24 @@ class SimulationWindow(QMainWindow):
|
|||||||
self.results.status.update(phase="Simulation finished", progress=10000)
|
self.results.status.update(phase="Simulation finished", progress=10000)
|
||||||
if isinstance(result, SimulationExecutionResult):
|
if isinstance(result, SimulationExecutionResult):
|
||||||
self.results.data = result.data
|
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["processReturnCode"] = result.return_code
|
||||||
self.results.metadata["sourceResultFile"] = Path(result.result_file).name
|
self.results.metadata["sourceResultFile"] = Path(result.result_file).name
|
||||||
else:
|
else:
|
||||||
@@ -256,11 +555,221 @@ class SimulationWindow(QMainWindow):
|
|||||||
"<p>View live progress and open or save simulation results.</p>",
|
"<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
|
@property
|
||||||
def is_running(self) -> bool:
|
def is_running(self) -> bool:
|
||||||
return self._running
|
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:
|
def _safe_file_stem(model_name: str) -> str:
|
||||||
stem = re.sub(r"[^A-Za-z0-9_.-]+", "_", model_name).strip("._")
|
stem = re.sub(r"[^A-Za-z0-9_.-]+", "_", model_name).strip("._")
|
||||||
return stem or "simulation"
|
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)
|
||||||
|
|||||||
@@ -552,6 +552,7 @@
|
|||||||
<addaction name="actionSimulationSettings"/>
|
<addaction name="actionSimulationSettings"/>
|
||||||
<addaction name="actionGraphParameters"/>
|
<addaction name="actionGraphParameters"/>
|
||||||
<addaction name="actionCompose"/>
|
<addaction name="actionCompose"/>
|
||||||
|
<addaction name="actionExportModel"/>
|
||||||
<addaction name="actionSimulationWindow"/>
|
<addaction name="actionSimulationWindow"/>
|
||||||
<addaction name="actionRunSimulation"/>
|
<addaction name="actionRunSimulation"/>
|
||||||
</widget>
|
</widget>
|
||||||
@@ -715,6 +716,18 @@
|
|||||||
<string>Show the simulation results window</string>
|
<string>Show the simulation results window</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</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">
|
<action name="actionNew">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="../resources/resources.qrc">
|
<iconset resource="../resources/resources.qrc">
|
||||||
|
|||||||
@@ -2,13 +2,222 @@
|
|||||||
<ui version="4.0">
|
<ui version="4.0">
|
||||||
<class>ParameterOptionsDialog</class>
|
<class>ParameterOptionsDialog</class>
|
||||||
<widget class="QDialog" name="ParameterOptionsDialog">
|
<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="geometry">
|
||||||
<property name="windowTitle"><string>Parameter Options</string></property>
|
<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">
|
<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>
|
||||||
<item><widget class="QDialogButtonBox" name="buttonBox"><property name="standardButtons"><set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set></property></widget></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>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<resources/>
|
<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>
|
</ui>
|
||||||
|
|||||||
@@ -2,43 +2,307 @@
|
|||||||
<ui version="4.0">
|
<ui version="4.0">
|
||||||
<class>PortOptionsDialog</class>
|
<class>PortOptionsDialog</class>
|
||||||
<widget class="QDialog" name="PortOptionsDialog">
|
<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="geometry">
|
||||||
<property name="windowTitle"><string>Port Options</string></property>
|
<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">
|
<layout class="QVBoxLayout" name="dialogLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QSplitter" name="portSplitter">
|
<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">
|
<widget class="QWidget" name="portListPanel">
|
||||||
<layout class="QVBoxLayout" name="portListLayout">
|
<layout class="QVBoxLayout" name="portListLayout">
|
||||||
<item><widget class="QListWidget" name="portList"/></item>
|
<item>
|
||||||
|
<widget class="QListWidget" name="portList"/>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="portButtonsLayout">
|
<layout class="QHBoxLayout" name="portButtonsLayout">
|
||||||
<item><widget class="QPushButton" name="addPortButton"><property name="text"><string>Add Port</string></property></widget></item>
|
<item>
|
||||||
<item><widget class="QPushButton" name="removePortButton"><property name="text"><string>Remove Port</string></property></widget></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>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QWidget" name="portDetailsPanel">
|
<widget class="QWidget" name="portDetailsPanel">
|
||||||
<layout class="QFormLayout" name="portDetailsForm">
|
<layout class="QFormLayout" name="portDetailsForm">
|
||||||
<item row="0" column="0"><widget class="QLabel" name="nameLabel"><property name="text"><string>Name:</string></property></widget></item>
|
<property name="verticalSpacing">
|
||||||
<item row="0" column="1"><widget class="QLineEdit" name="nameEdit"/></item>
|
<number>3</number>
|
||||||
<item row="1" column="0"><widget class="QLabel" name="typeLabel"><property name="text"><string>Type:</string></property></widget></item>
|
</property>
|
||||||
<item row="1" column="1"><widget class="QComboBox" name="typeCombo"><item><property name="text"><string>Signal</string></property></item></widget></item>
|
<item row="0" column="0">
|
||||||
<item row="2" column="0"><widget class="QLabel" name="orientationLabel"><property name="text"><string>Orientation:</string></property></widget></item>
|
<widget class="QLabel" name="nameLabel">
|
||||||
<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>
|
<property name="text">
|
||||||
<item row="3" column="0" colspan="2"><widget class="QCheckBox" name="multipleConnectionsCheckBox"><property name="text"><string>Allow multiple connections</string></property></widget></item>
|
<string>Name:</string>
|
||||||
<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>
|
||||||
|
</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>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</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>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<resources/>
|
<resources/>
|
||||||
<connections>
|
<connections>
|
||||||
<connection><sender>buttonBox</sender><signal>accepted()</signal><receiver>PortOptionsDialog</receiver><slot>accept()</slot><hints/></connection>
|
<connection>
|
||||||
<connection><sender>buttonBox</sender><signal>rejected()</signal><receiver>PortOptionsDialog</receiver><slot>reject()</slot><hints/></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>
|
</connections>
|
||||||
</ui>
|
</ui>
|
||||||
|
|||||||
@@ -86,6 +86,55 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</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">
|
<widget class="QWidget" name="simulationTab">
|
||||||
<attribute name="title"><string>Simulation</string></attribute>
|
<attribute name="title"><string>Simulation</string></attribute>
|
||||||
<layout class="QVBoxLayout" name="simulationTabLayout">
|
<layout class="QVBoxLayout" name="simulationTabLayout">
|
||||||
|
|||||||
@@ -20,12 +20,12 @@
|
|||||||
<widget class="QWidget" name="centralWidget">
|
<widget class="QWidget" name="centralWidget">
|
||||||
<layout class="QVBoxLayout" name="resultsLayout">
|
<layout class="QVBoxLayout" name="resultsLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="resultsPlaceholder">
|
<widget class="QTabWidget" name="graphTabs">
|
||||||
<property name="text">
|
<property name="tabsClosable">
|
||||||
<string>Simulation graphs and result controls can be added here.</string>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="alignment">
|
<property name="movable">
|
||||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -76,10 +76,34 @@
|
|||||||
<addaction name="actionAbout"/>
|
<addaction name="actionAbout"/>
|
||||||
<addaction name="actionAboutQt"/>
|
<addaction name="actionAboutQt"/>
|
||||||
</widget>
|
</widget>
|
||||||
|
<widget class="QMenu" name="menuGraph">
|
||||||
|
<property name="title">
|
||||||
|
<string>&Graph</string>
|
||||||
|
</property>
|
||||||
|
<addaction name="actionAddGraph"/>
|
||||||
|
<addaction name="actionRemoveGraph"/>
|
||||||
|
</widget>
|
||||||
<addaction name="menuFile"/>
|
<addaction name="menuFile"/>
|
||||||
<addaction name="menuView"/>
|
<addaction name="menuView"/>
|
||||||
|
<addaction name="menuGraph"/>
|
||||||
<addaction name="menuHelp"/>
|
<addaction name="menuHelp"/>
|
||||||
</widget>
|
</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">
|
<widget class="QToolBar" name="fileToolbar">
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
<string>File</string>
|
<string>File</string>
|
||||||
@@ -93,10 +117,10 @@
|
|||||||
<attribute name="toolBarBreak">
|
<attribute name="toolBarBreak">
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</attribute>
|
</attribute>
|
||||||
|
<addaction name="actionClear"/>
|
||||||
<addaction name="actionOpen"/>
|
<addaction name="actionOpen"/>
|
||||||
<addaction name="actionSave"/>
|
<addaction name="actionSave"/>
|
||||||
<addaction name="actionSaveAs"/>
|
<addaction name="actionSaveAs"/>
|
||||||
<addaction name="actionClear"/>
|
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QDockWidget" name="statusDock">
|
<widget class="QDockWidget" name="statusDock">
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
@@ -162,6 +186,36 @@
|
|||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</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">
|
<action name="actionOpen">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="../resources/resources.qrc">
|
<iconset resource="../resources/resources.qrc">
|
||||||
@@ -236,6 +290,30 @@
|
|||||||
<string>Results</string>
|
<string>Results</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</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>
|
</widget>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="../resources/resources.qrc"/>
|
<include location="../resources/resources.qrc"/>
|
||||||
|
|||||||
@@ -12,11 +12,27 @@
|
|||||||
<widget class="QSplitter" name="columnSplitter">
|
<widget class="QSplitter" name="columnSplitter">
|
||||||
<property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property>
|
<property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property>
|
||||||
<property name="childrenCollapsible"><bool>false</bool></property>
|
<property name="childrenCollapsible"><bool>false</bool></property>
|
||||||
<widget class="QGroupBox" name="equationsGroup">
|
<widget class="QSplitter" name="sourceSplitter">
|
||||||
<property name="title"><string>Equations</string></property>
|
<property name="orientation"><enum>Qt::Orientation::Vertical</enum></property>
|
||||||
<layout class="QVBoxLayout" name="equationsLayout">
|
<property name="childrenCollapsible"><bool>false</bool></property>
|
||||||
<item><widget class="OpenModelicaEditor" name="equationsEdit"/></item>
|
<widget class="QGroupBox" name="declarationsGroup">
|
||||||
</layout>
|
<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>
|
||||||
<widget class="QSplitter" name="definitionSplitter">
|
<widget class="QSplitter" name="definitionSplitter">
|
||||||
<property name="orientation"><enum>Qt::Orientation::Vertical</enum></property>
|
<property name="orientation"><enum>Qt::Orientation::Vertical</enum></property>
|
||||||
@@ -24,15 +40,13 @@
|
|||||||
<widget class="QGroupBox" name="portsGroup">
|
<widget class="QGroupBox" name="portsGroup">
|
||||||
<property name="title"><string>Ports</string></property>
|
<property name="title"><string>Ports</string></property>
|
||||||
<layout class="QVBoxLayout" name="portsLayout">
|
<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><widget class="PortEditor" name="portEditor"/></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>
|
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QGroupBox" name="parametersGroup">
|
<widget class="QGroupBox" name="parametersGroup">
|
||||||
<property name="title"><string>Parameters</string></property>
|
<property name="title"><string>Parameters</string></property>
|
||||||
<layout class="QVBoxLayout" name="parametersLayout">
|
<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><widget class="ParameterEditor" name="parameterEditor"/></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>
|
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</widget>
|
</widget>
|
||||||
@@ -46,6 +60,8 @@
|
|||||||
<extends>QPlainTextEdit</extends>
|
<extends>QPlainTextEdit</extends>
|
||||||
<header>bedit.gui.editors.openmodelica</header>
|
<header>bedit.gui.editors.openmodelica</header>
|
||||||
</customwidget>
|
</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>
|
</customwidgets>
|
||||||
<resources/>
|
<resources/>
|
||||||
<connections/>
|
<connections/>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
33
m_Test.mo
33
m_Test.mo
@@ -6,7 +6,7 @@ model m_Test
|
|||||||
y = v;
|
y = v;
|
||||||
end m_Constant0;
|
end m_Constant0;
|
||||||
model m_gain0
|
model m_gain0
|
||||||
input Real u;
|
input Real u(quantity="Velocity", unit="m/s");
|
||||||
output Real y;
|
output Real y;
|
||||||
parameter Real k = 2.5;
|
parameter Real k = 2.5;
|
||||||
equation
|
equation
|
||||||
@@ -16,7 +16,7 @@ model m_Test
|
|||||||
output Real y;
|
output Real y;
|
||||||
parameter Real v = 4.8;
|
parameter Real v = 4.8;
|
||||||
equation
|
equation
|
||||||
y = v+sin(time);
|
y = v+sin(10*time);
|
||||||
end m_const_and_time;
|
end m_const_and_time;
|
||||||
model m_gain1
|
model m_gain1
|
||||||
input Real u;
|
input Real u;
|
||||||
@@ -31,14 +31,43 @@ model m_Test
|
|||||||
equation
|
equation
|
||||||
y = sum(u[i] for i in 1:2 );
|
y = sum(u[i] for i in 1:2 );
|
||||||
end m_add0;
|
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_Constant0 Constant0;
|
||||||
m_gain0 gain0;
|
m_gain0 gain0;
|
||||||
m_const_and_time const_and_time;
|
m_const_and_time const_and_time;
|
||||||
m_gain1 gain1;
|
m_gain1 gain1;
|
||||||
m_add0 add0;
|
m_add0 add0;
|
||||||
|
m_Integrate0 Integrate0;
|
||||||
|
m_Differentiate0 Differentiate0;
|
||||||
equation
|
equation
|
||||||
gain0.u = Constant0.y;
|
gain0.u = Constant0.y;
|
||||||
gain1.u = const_and_time.y;
|
gain1.u = const_and_time.y;
|
||||||
add0.u[1] = gain0.y;
|
add0.u[1] = gain0.y;
|
||||||
add0.u[2] = gain1.y;
|
add0.u[2] = gain1.y;
|
||||||
|
Integrate0.u = add0.y;
|
||||||
|
Differentiate0.u = add0.y;
|
||||||
end m_Test;
|
end m_Test;
|
||||||
Reference in New Issue
Block a user