Compare commits

..

4 Commits

Author SHA1 Message Date
edd7bb98f2 Saving sim results 2026-07-21 14:40:03 +02:00
ddc004dee5 sim window save/load 2026-07-21 14:01:55 +02:00
cdfc891980 Start of a sim window 2026-07-21 13:54:57 +02:00
6fb2478589 basic runner setup changed 2026-07-21 13:21:52 +02:00
26 changed files with 2164 additions and 318 deletions

View File

@@ -6,7 +6,6 @@
"type": "shell",
"command": "pyside6-designer",
"args": [
"${workspaceFolder}/ui/*.ui"
],
"options": {
"cwd": "${workspaceFolder}",

View File

@@ -25,7 +25,7 @@ src/bedit/
│ ├── port_types.py # Port type definitions and compatibility
│ ├── serializer.py # JSON persistence
│ ├── libraries.py # Library file discovery and parsing
│ └── simulation/ # Qt-free simulation service and compiler/runtime code
│ └── simulation/ # Qt-free composition and OpenModelica interface code
└── gui/ # All Qt-dependent code
├── app.py # QApplication startup and palette
├── main_window.py # Top-level UI orchestration
@@ -94,6 +94,11 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
- Avoid the QSettings group name `general`; Qt treats `General` specially in INI
files. Autosave keys live under `autosave/`.
- User-visible document edits should participate in undo/redo.
- Component clipboard data is shared by the graph, document tree, and library
tree. Pasting into the document node creates roots; pasting into a graph node
creates children at an origin-normalized position. Always clone pasted trees
with fresh IDs, preserve connections between jointly copied graph blocks, and
assign unique sibling names.
- The text-definition editor uses OpenModelica highlighting and completion from
`src/bedit/data/syntax/openmodelica.json`. Keep keywords, types, built-ins, and
named BEdit `$name$` macro completions editable there; arbitrary `$name$`
@@ -105,16 +110,35 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
- File → Reload Simulation Code (`Ctrl+F5`) reloads modules under
`bedit.core.simulation`, replaces the shared application/controller service,
and preserves the previous instance attributes where possible.
- Simulation compilation lives in `core/simulation/compiler.py`; the simulation
service only owns application state and delegates compilation. Ports with
- Modelica composition lives in `core/simulation/composer.py`; the simulation
service only owns application state and delegates composition. Ports with
`multipleConnections` are emitted as Modelica arrays. Their size is inferred
per component instance from graph connections and exposed while compiling as
`$portname_N$`; array connection endpoints receive stable one-based indices in
graph connection order.
- Blocking simulator integration belongs in `core/simulation/runner.py` and runs
on its daemon worker thread. Never perform OMPython startup or simulation work
directly on the Qt GUI thread; background failures must go to the application
log.
- OpenModelica integration belongs in `core/simulation/openmodelica.py`. Its
persistent worker and OMC session start lazily on the first queued request.
Never perform OMPython work directly on the Qt GUI thread. Result and error
callbacks run on background threads and must use a Qt signal before touching UI.
One lazy temporary working directory is shared by all requests in the session.
Explicit application shutdown closes OMC and removes that directory plus the
current session's OMPython log and port files; `__del__` is only a fallback.
- Simulation runs start an ephemeral localhost TCP listener before launching the
generated model through OMC's `system()` function. OpenModelica's newline-delimited
`xmltcp` status and message records are parsed in the core and forwarded through
callbacks; the simulation service retains the latest progress for polling.
- The application owns one reusable `SimulationWindow`. Starting a run clears its
progress, log, and future result views. Extend graph presentation through its
Designer-owned `resultsLayout` and the
`clear_result_views()`/`load_result_views()` hooks.
- Standalone simulation results are modeled in `core/simulation/results.py`.
Its versioned schema retains model status, messages, metadata, and plottable
traces so the simulation window can open results without an active document.
Human-readable `.json` uses JSON, while the default `.ber` format uses the same
compressed MessagePack approach as `.beb` documents.
- After a successful OpenModelica run, `<model>_res.csv` is parsed on the worker
before temporary-directory cleanup. `SimulationResults.data` stores every CSV
column as a numeric array, including `time`, for later plotting and persistence.
- The optional OpenModelica executable is persisted as
`simulation/openModelicaPath`. The GUI passes it into `Simulation`; core must
not read `QSettings`. An empty path uses OMPython/PATH discovery, while an
@@ -159,6 +183,9 @@ pyside6-uic --from-imports ui/text_definition_editor.ui \
pyside6-uic --from-imports ui/simulation_settings_dialog.ui \
-o src/bedit/gui/generated/ui_simulation_settings_dialog.py
pyside6-uic --from-imports ui/simulation_window.ui \
-o src/bedit/gui/generated/ui_simulation_window.py
pyside6-uic --from-imports ui/graph_parameters_dialog.ui \
-o src/bedit/gui/generated/ui_graph_parameters_dialog.py

Binary file not shown.

After

Width:  |  Height:  |  Size: 877 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 B

View File

@@ -1,5 +1,7 @@
<RCC>
<qresource prefix="icons">
<file>icons/view-form-table.png</file>
<file>icons/office-chart-line.png</file>
<file>icons/run-build.png</file>
<file>icons/preferences-system.png</file>
<file>icons/draw-triangle.png</file>

View File

@@ -2326,6 +2326,63 @@ K\x80@\x89\x15\x8d\xc04\xd5\xb5^\xaf\x1bx\xfa\x19\
\x84\xe7\x04\xcf\x88\xfd\xfd\xfd\xe4\xe8\xe8\xc8\x9dL&\x14\
\xc7\xcc\x7f5p\xf2g\x94\xf7\x1f\xdf\x9a\xd2\x93\xfbC\
\xb7\xa7\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x03m\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\
\x00\x00\x09pHYs\x00\x00\x1b\xaf\x00\x00\x1b\xaf\x01\
^\x1a\x91\x1c\x00\x00\x00\x07tIME\x07\xd9\x02\x10\
\x17\x22\x16\x993\xa5<\x00\x00\x02\xedIDATx\
\xda\xed\x97_H\xd3Q\x14\xc7\xcf\xd9\xef7\xcb\xca\x89\
B \x91\x0f\xc9\xcczha\xf6\xa8\xb4\x06M\x90f\
aiV$\x96V\x12\xf9P-S\x90d%Lh\
V/B.\x8d\xc2\xb2\xc0\x1erS\xc1\x84\x1cdo\
a\xa0\x0f>h\x0f\xfdy\xf0-\x9b\xa6\x0f\xb1{\xfb\
n\xf4\x8b\x91\xe2\x5cn\xae\x07\x0f\xdc\x9dq/\xfc>\
\xe7{\xce\xe1\xdc\xdf\x8f\xd6M\xb3\x86\x86\x86\xe2Dp\
u\xbf\xe19pG\x82>!\x01\x04\xe1\x9aOd\x09\
:\x13V\x02\xcd\xd6\x03H\x84\xa9\x04\xab\xaf\xaf\xef\x84\
\xcb\x8f\x07\xe0z\xcf\x82AU\xc8\xa6\xeax\xfc\xf6\xd1\
\x8dcKf\xa0\xa5\xa5\xa5\x0an$V\xd0\xea\xc7\xf3\
\x86\x9a\xae\xf9\xd3\xb5\xdd\xf3\xaf\x84\xa4\xcfRR\xa9\x5c\
&\x031\xb3\xf2\xf6\x1f\xb9P\xdb\x14\x90\xd2\xacH\xf6\
\x01\xda\x13\x10T\xd1Z\x96\xec\xa7x\x07P\xd26\xc7\
P\xdb!\x04\xbd\x10L\x15\xedg7-\x82\xc65\x00\
\xc0m\x02?\x82\xd9\xf5\xa4j\xb3\x5c\xd3&,\xba\x1f\
R\x8f\xd4\x93\xe3\xe5\xc5\x08\xf0X7\xa1\xa6> B\
\xde\xbb&s\xc0\xb3\xf3x\x81'\xbbt\x1f\xc1,w\
f\x19M\xd7$\xa0\xbe\xf7\xf2\x16\x19\xf5\x1c\x88\x0e|\
L!\xc9\x8d$u\x97X\xa8\x0b\xde\xac\x0aS\xab$\
3\x07\xd5\xb3\xd4\xd4\xc7'\x00\xa8\xce\x00\xfc)\xe0\xcc\
\xa4\xdbK2\xe9\x96\x94\xaa\x0bu\xcfc\xa8\x1f\xbc\x92\
\x22\xffq\x14GnB\xa4\xdbBRy\xcfRy\xcb\
\xa4XmS\xcf\xa7I\xe8\xed,\xf4\x85\xbb&\xc7\xb4\
\xda\xc7\xfe:\x06X\xf1d\x975y\x8c\xe5_\xbd\xc6\
\x93\x96\xf0\xb3\x5c\x87\x9f++\xdf\x8d\xf6m\xbf\xfa\xa9\
o[\x9d!\xe6\x97\x11\xc0\x198\x1e\x84\xea\x02\xa4|\
?T\xbf\x09?\x97\x92\x8a\xc7\xb3\xf6H)\x92^#\
\x1b\xae\xe5 \x03\x06\xb7e \xa5\xc3\xb4\xd2\x00\x00?\
\x91\x87Z\x87R\x0e_h\x9b\xea\x9e\x0e?\xdf\xdd\xf8\
\x9d\x03B\xde\x0cv>\x9a\xf1\x1aK\xbd\xb5\x7fk\xb3\
u1\xb8\xdd0`x\xe8F\xf9\x1e\xe19\xea\x8a\x03\
`\xa9\xfb\x09\xf8)\x80\x1d\xb6\x8f\xcf\x02\xda~\xa6}\
\x86v\xdc\x981\x06\x045\x02NB\x90\xf7\xf0t\xb3\
\x1f\x19\xb8\x80\xe5\xeeOo5\x84\xa9>\x040\x1aD\
!\xc0MEs\xe7F\x97diM\xc8\xcc\xf9N\xa7\
3'\xfc0\xb9\xe6\x1b\xe9\x98\x8c\xaaB\x07\x92\x142\
\xebU6\xeb\xf1\xbc$\x85}\xd8\xbb;\xd1\x9c\xfa\xe1\
\x0f0\xfd\x9e;\x04\x13\x1b\xecD\xec\x02\xdcJ\x92\xcf\
\x03<\x14\xf5$\x04\xd8\x84\xd5\x85\xbf_P\xeba,\
3\x96\x0f\x8a\x0fB}\xe6\xa43\xf5L\x08\x1enB\
\xb5\x13J\x01\xb5\x13a\xaa\x87V3\x07|X\x0eI\
4\xe5oK\x8b|\x1f\xcc\xd4\xfa\xd1h%\x00\xa7\x02\
<\xbc\xaaA\xb4\xf0 m\x0c\x0e+:+\x9a\xad\x1e\
\xfd\xaf\xdf\x091k\xea\xfe\xfe\xf8\xe1H\xdf\x05R\xca\
|f\x1e\x89\xd1\x9ef\xbdhx\x0fE4mL\xaf\
b/B\x06\x12o\xbf\x00\xa3\x17WYZq\xd9W\
\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x04<\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -2396,6 +2453,35 @@ f\xa2\xd1\xa5\x5c\x22\xb4\x91SZ\xd5u\xd7\x0a\xd2]\
|\x01\x85\x09\x800\x7fss\xd3{\xf6\x7fABG\
Y\x01\x05l*\xfc\x00!\x00\x12\xf1%U\xb6\x0e\x00\
\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x01\xad\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00 \x00\x00\x00 \x08\x03\x00\x00\x00D\xa4\x8a\xc6\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01B(\
\x9bx\x00\x00\x00\x07tIME\x07\xd9\x0c\x1c\x03\x1c\
\x0e%S,b\x00\x00\x00uPLTE\x00\x00\x00\
\x13\x13\x13\x0a\x0a\x0a\x0b\x0b\x0b\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc6\xc6\xc8\xc7\xc7\
\xc9\xc9\xc9\xcb\xcc\xcc\xce\xce\xce\xd0\xd0\xd0\xd2\xd4\xd4\xd6\
\xd7\xd7\xd8\xd7\xd7\xd9\xd8\xd8\xda\xd9\xd9\xdb\xda\xda\xdb\xda\
\xda\xdc\xdb\xdb\xdc\xdb\xdb\xdd\xdd\xdd\xdf\xde\xde\xdf\xe2\xe2\
\xe3\xe3\xe3\xe5\xe3\xe3\xe6\xe4\xe4\xe6\xe6\xe6\xe7\xe7\xe7\xe9\
\xe9\xe9\xea\xeb\xeb\xec\xed\xed\xee\xf0\xf0\xf1\xf3\xf3\xf4\xff\
\xff\xff\xd3\x9b\xcc\x0e\x00\x00\x00\x0atRNS\x00\x09\
\x15\x15\x1825678\xb5\xcc\xc0\x1e\x00\x00\x00\x01\
bKGD&Z\x08\x98\xb5\x00\x00\x00\x9bIDA\
Tx\xda\xd5\x93\xcb\x0e\x820\x10\x00\x8b\x0aEP|\
u\xc1G)\x94\x02\xff\xff\x89.]\x0e\x18\xccr1\
F\xe72\xd9d\xd26\x9bT\xfc\x02\xc1\x86%\x10a\
\xcf\x12\x0a\xd9)\x86N\x0a\xd9^\x18Z\x0c\xdc\x91\xc1\
a\xd0\x1c\x18\x1a\x0c\xec\x9e\xc1bP'\x9et\x97\xce\
\x95\xd4\x18\x94=q#\xddIWR\x89\x81\x1e\x03E\
\x82\x97IO\x02\x98\x8b\x82G\x01\x9e\xec\x8d\x8aj\xf1\
\x84\xcf\xbc!W\x03\x90\x8d\x02oR^-^\xf1?\
{\xe0\x83\xc8\x1a=`\xce\xc6\xebd\xa6\x93\x8b\xc4*\
\xde2\xc4\xeb/|\xcd'\xec\xbfO\xbf\x90M\x1a\x0a\
\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x02\x92\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -2632,10 +2718,20 @@ qt_resource_name = b"\
\x00d\
\x00o\x00c\x00u\x00m\x00e\x00n\x00t\x00-\x00s\x00a\x00v\x00e\x00.\x00p\x00n\x00g\
\
\x00\x15\
\x02\xb4\x1f\x07\
\x00o\
\x00f\x00f\x00i\x00c\x00e\x00-\x00c\x00h\x00a\x00r\x00t\x00-\x00l\x00i\x00n\x00e\
\x00.\x00p\x00n\x00g\
\x00\x10\
\x03\xe6\xd3g\
\x00d\
\x00r\x00a\x00w\x00-\x00e\x00l\x00l\x00i\x00p\x00s\x00e\x00.\x00p\x00n\x00g\
\x00\x13\
\x07\xd6O\x07\
\x00v\
\x00i\x00e\x00w\x00-\x00f\x00o\x00r\x00m\x00-\x00t\x00a\x00b\x00l\x00e\x00.\x00p\
\x00n\x00g\
\x00\x12\
\x09\xb3>\xc7\
\x00d\
@@ -2653,7 +2749,7 @@ qt_resource_struct = b"\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1c\x00\x00\x00\x03\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1e\x00\x00\x00\x03\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x01\xfe\x00\x00\x00\x00\x00\x01\x00\x00N\xe8\
\x00\x00\x01\x9f{C\xf1'\
@@ -2661,11 +2757,13 @@ qt_resource_struct = b"\
\x00\x00\x01\x9f\x7f\xa8\xa8)\
\x00\x00\x02\xec\x00\x00\x00\x00\x00\x01\x00\x00tV\
\x00\x00\x01\x9f{0\xc99\
\x00\x00\x03\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x8d\xaa\
\x00\x00\x01\x9f\x84\x8f\x00\xb9\
\x00\x00\x01\xc4\x00\x00\x00\x00\x00\x01\x00\x00D2\
\x00\x00\x01\x9f\x7f&\x83\xcd\
\x00\x00\x01\xa4\x00\x00\x00\x00\x00\x01\x00\x00<J\
\x00\x00\x01\x9f{C\xf1\x18\
\x00\x00\x03\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x8d\xaa\
\x00\x00\x03\xd4\x00\x00\x00\x00\x00\x01\x00\x00\x91\x1b\
\x00\x00\x01\x9f\x7fY\xceg\
\x00\x00\x02\xa2\x00\x00\x00\x00\x00\x01\x00\x00f\xc8\
\x00\x00\x01\x9f\x7fV\xd5\xc0\
@@ -2677,9 +2775,11 @@ qt_resource_struct = b"\
\x00\x00\x01\x9f\x7fY\xce\x82\
\x00\x00\x01\xe0\x00\x00\x00\x00\x00\x01\x00\x00Kh\
\x00\x00\x01\x9f{C\xf1.\
\x00\x00\x03\xfa\x00\x00\x00\x00\x00\x01\x00\x00\x95[\
\x00\x00\x01\x9f\x84\x8f\xa6\x10\
\x00\x00\x01\x84\x00\x00\x00\x00\x00\x01\x00\x006\x9c\
\x00\x00\x01\x9f{{\xa5\xd5\
\x00\x00\x03\xca\x00\x00\x00\x00\x00\x01\x00\x00\x91\xea\
\x00\x00\x04&\x00\x00\x00\x00\x00\x01\x00\x00\x97\x0c\
\x00\x00\x01\x9f\x7f&\x83\x10\
\x00\x00\x00\xfe\x00\x00\x00\x00\x00\x01\x00\x00(e\
\x00\x00\x01\x9f\x7f&\x83~\
@@ -2703,7 +2803,7 @@ qt_resource_struct = b"\
\x00\x00\x01\x9f{C\xf1=\
\x00\x00\x006\x00\x00\x00\x00\x00\x01\x00\x00\x05\x86\
\x00\x00\x01\x9f\x7f&\x83\x99\
\x00\x00\x03\xf4\x00\x00\x00\x00\x00\x01\x00\x00\x94\x80\
\x00\x00\x04P\x00\x00\x00\x00\x00\x01\x00\x00\x99\xa2\
\x00\x00\x01\x9f{{\xa5\xe3\
\x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x00\x0c\xc1\
\x00\x00\x01\x9f\x7f\xac\xf2\xc6\

View File

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

View File

@@ -14,8 +14,8 @@ _BEVALUE_PATTERN = re.compile(r"\$([A-Za-z_][A-Za-z0-9_]*)\$")
@dataclass(frozen=True)
class CompilationResult:
"""The intermediate data and generated source produced by compilation."""
class CompositionResult:
"""The intermediate data and generated source produced by composition."""
graph: dict[str, Any]
objects_by_id: dict[str, Any]
@@ -23,16 +23,16 @@ class CompilationResult:
model_name: str
def compile_graph(graph: dict[str, Any]) -> CompilationResult:
"""Clean, index, and emit a serialized component tree."""
def compose_graph(graph: dict[str, Any]) -> CompositionResult:
"""Clean, index, and compose a serialized component tree."""
cleaned_graph = cleanup_graph(deepcopy(graph))
objects_by_id = build_id_list(cleaned_graph)
return CompilationResult(
return CompositionResult(
graph=cleaned_graph,
objects_by_id=objects_by_id,
modelica=emit_model(cleaned_graph, objects_by_id),
model_name=model_name_for(cleaned_graph)
model_name=model_name_for(cleaned_graph),
)
@@ -85,7 +85,7 @@ def emit_model(
) -> str:
"""Emit a component and its nested definitions as Modelica source."""
del id_list # Kept in the public API for compiler extensions and inspection.
del id_list # Kept in the public API for composer extensions and inspection.
indentation = "\t" * indent
body_indent = "\t" * (indent + 1)
model_name = model_name_for(graph)
@@ -283,7 +283,7 @@ def _port_count_macros(
def expand_bevalues(text: str, values: dict[str, str]) -> str:
"""Replace BEdit ``$name$`` macros and reject unresolved compiler values."""
"""Replace BEdit ``$name$`` macros and reject unresolved composer values."""
def replace(match: re.Match[str]) -> str:
name = match.group(1)

View File

@@ -0,0 +1,427 @@
import json
import os
import shlex
import shutil
import socket
import tempfile
import time
import xml.etree.ElementTree as ET
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
from queue import Queue
from threading import Event, Lock, Thread, current_thread
from typing import Any
from bedit.core.application_log import get_logger
from bedit.core.simulation.results import (
SimulationExecutionResult,
load_openmodelica_csv,
)
log = get_logger(__name__)
ResultCallback = Callable[[Any], None]
ErrorCallback = Callable[[Exception], None]
@dataclass(frozen=True)
class _Request:
description: str
operation: Callable[[Any, Path], Any]
callback: ResultCallback | None
error_callback: ErrorCallback | None
@dataclass(frozen=True)
class SimulationProgress:
phase: str
current_step_size: float
time: float
progress: int
@dataclass(frozen=True)
class SimulationMessage:
stream: str
type: str
text: str
class OpenModelicaInterface:
"""Asynchronous, persistent interface to one OpenModelica session.
The worker and OMC session are created lazily for the first request. Callbacks
execute on background threads and must not manipulate Qt widgets directly.
"""
def __init__(self, executable_path: str = "") -> None:
self._executable_path = executable_path
self._lifecycle_lock = Lock()
self._queue: Queue[_Request | None] | None = None
self._worker: Thread | None = None
self._temp_dir: Path | None = None
@property
def executable_path(self) -> str:
return self._executable_path
def configure(self, executable_path: str) -> None:
"""Use a new executable path for subsequent requests."""
if executable_path == self._executable_path:
return
self.shutdown(wait=True)
self._executable_path = executable_path
def get_version(
self,
callback: ResultCallback | None = None,
error_callback: ErrorCallback | None = None,
) -> None:
"""Request the OpenModelica version without blocking the caller."""
self.send_expression("getVersion()", callback, error_callback)
def build_model(
self,
model: str,
model_name: str,
callback: ResultCallback | None = None,
error_callback: ErrorCallback | None = None,
) -> None:
"""Load and build one composed model as an ordered worker operation."""
def operation(omc, _temp_dir: Path):
loaded = omc.sendExpression(f"loadString({json.dumps(model)})")
if loaded is not True:
raise RuntimeError("OpenModelica could not load the composed model")
return omc.sendExpression(f"buildModel({model_name})")
self._submit("build model", operation, callback, error_callback)
def run_model(
self,
executable: str,
arguments: list[str],
progress_callback: Callable[[SimulationProgress], None] | None = None,
message_callback: Callable[[SimulationMessage], None] | None = None,
callback: ResultCallback | None = None,
error_callback: ErrorCallback | None = None,
) -> None:
"""Run a built model and consume its XML/TCP status stream."""
def operation(omc, temp_dir: Path):
return _run_model_with_tcp(
omc,
executable,
arguments,
temp_dir,
progress_callback,
message_callback,
)
self._submit("run simulation", operation, callback, error_callback)
def send_expression(
self,
expression: str,
callback: ResultCallback | None = None,
error_callback: ErrorCallback | None = None,
*,
parsed: bool = True,
) -> None:
"""Queue an OMC expression for ordered execution on the worker thread."""
def operation(omc, _temp_dir: Path):
return omc.sendExpression(expression, parsed=parsed)
self._submit(expression, operation, callback, error_callback)
def _submit(
self,
description: str,
operation: Callable[[Any, Path], Any],
callback: ResultCallback | None,
error_callback: ErrorCallback | None,
) -> None:
request = _Request(description, operation, callback, error_callback)
self._ensure_worker().put(request)
def shutdown(self, *, wait: bool = True) -> None:
"""Ask the worker to close its OMC session after queued requests."""
with self._lifecycle_lock:
worker = self._worker
queue = self._queue
self._worker = None
self._queue = None
if queue is not None:
queue.put(None)
if wait and worker is not None and worker is not current_thread():
worker.join()
if worker is None:
self._cleanup_temp_dir()
def __del__(self) -> None:
"""Best-effort fallback; normal application shutdown is explicit."""
try:
self.shutdown(wait=False)
except Exception:
pass
def _ensure_worker(self) -> Queue[_Request | None]:
with self._lifecycle_lock:
if self._worker is not None and self._worker.is_alive():
return self._queue
temp_dir = self._ensure_temp_dir_locked()
queue: Queue[_Request | None] = Queue()
worker = Thread(
target=self._worker_main,
args=(queue, self._executable_path, temp_dir),
name="bedit-openmodelica",
daemon=True,
)
self._queue = queue
self._worker = worker
worker.start()
return queue
def _worker_main(
self,
queue: Queue[_Request | None],
executable_path: str,
temp_dir: Path,
) -> None:
omc = None
try:
while (request := queue.get()) is not None:
try:
if omc is None:
omc = _create_session(executable_path)
changed_directory = omc.sendExpression(
f"cd({json.dumps(str(temp_dir))})"
)
if not changed_directory:
raise RuntimeError(
f"OpenModelica could not use {str(temp_dir)!r}"
)
result = request.operation(omc, temp_dir)
except Exception as error:
if request.error_callback is None:
log.exception(
"OpenModelica request failed: %s", request.description
)
else:
_deliver_callback(request.error_callback, error)
else:
if request.callback is not None:
_deliver_callback(request.callback, result)
finally:
transport_files = _ompython_transport_files(omc)
if omc is not None:
try:
omc.sendExpression("quit()")
except Exception:
log.debug("Could not close OpenModelica session", exc_info=True)
for path in transport_files:
try:
path.unlink(missing_ok=True)
except OSError:
log.debug("Could not remove OMPython file %s", path, exc_info=True)
self._cleanup_temp_dir(temp_dir)
def _ensure_temp_dir_locked(self) -> Path:
if self._temp_dir is None:
self._temp_dir = Path(tempfile.mkdtemp(prefix="bedit-openmodelica-"))
return self._temp_dir
def _cleanup_temp_dir(self, expected: Path | None = None) -> None:
with self._lifecycle_lock:
if expected is not None and self._temp_dir != expected:
temp_dir = expected
else:
temp_dir = self._temp_dir
self._temp_dir = None
if temp_dir is not None:
shutil.rmtree(temp_dir, ignore_errors=True)
def _deliver_callback(callback: Callable[[Any], None], value: Any) -> None:
try:
callback(value)
except Exception:
log.exception("OpenModelica callback failed")
def _create_session(executable_path: str):
from OMPython import OMCSessionZMQ
return OMCSessionZMQ(omhome=_openmodelica_home(executable_path))
def _run_model_with_tcp(
omc,
executable: str,
arguments: list[str],
temp_dir: Path,
progress_callback: Callable[[SimulationProgress], None] | None,
message_callback: Callable[[SimulationMessage], None] | None,
) -> SimulationExecutionResult:
executable_path = Path(executable)
executable_command = executable
if not executable_path.is_absolute() and executable_path.parent == Path("."):
executable_command = f"./{executable}"
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("127.0.0.1", 0))
server.listen(1)
server.settimeout(0.25)
port = server.getsockname()[1]
command = shlex.join([
executable_command,
*arguments,
f"-port={port}",
"-logFormat=xmltcp",
])
output_path = temp_dir / "simulation-output.txt"
command_finished = Event()
reader_finished = Event()
reader_errors: list[Exception] = []
def read_progress() -> None:
try:
connection = _accept_simulation_connection(server, command_finished)
with connection, connection.makefile(
"r", encoding="utf-8"
) as stream:
for line in stream:
_handle_simulation_xml(
line, progress_callback, message_callback
)
except Exception as error:
reader_errors.append(error)
finally:
reader_finished.set()
reader = Thread(
target=read_progress,
name="bedit-simulation-progress",
daemon=True,
)
reader.start()
log.info("Starting simulation through OpenModelica: %s", command)
try:
return_code = omc.sendExpression(
f"system({json.dumps(command)}, {json.dumps(str(output_path))})"
)
finally:
command_finished.set()
if not reader_finished.wait(20.0):
raise TimeoutError("Simulation progress connection did not close")
if reader_errors:
raise reader_errors[0]
if return_code != 0:
output = output_path.read_text(errors="replace") if output_path.exists() else ""
detail = f": {output.strip()}" if output.strip() else ""
raise RuntimeError(
f"Simulation process exited with status {return_code}{detail}"
)
result_path = temp_dir / f"{Path(executable).stem}_res.csv"
if not result_path.is_file():
raise RuntimeError(
f"OpenModelica did not create the expected result file {result_path.name!r}"
)
data = load_openmodelica_csv(result_path)
log.info(
"Loaded %d result columns from %s", len(data), result_path.name
)
return SimulationExecutionResult(
return_code=int(return_code),
result_file=str(result_path),
data=data,
)
def _accept_simulation_connection(
server: socket.socket, command_finished: Event
) -> socket.socket:
deadline = time.monotonic() + 15.0
while time.monotonic() < deadline:
try:
connection, _address = server.accept()
return connection
except socket.timeout:
if command_finished.is_set():
raise RuntimeError(
"Simulation command finished before opening its progress connection"
)
raise TimeoutError("Simulation did not connect to the progress server")
def _handle_simulation_xml(
line: str,
progress_callback: Callable[[SimulationProgress], None] | None,
message_callback: Callable[[SimulationMessage], None] | None,
) -> None:
text = line.strip()
if not text:
return
try:
element = ET.fromstring(text)
except ET.ParseError:
log.warning("Invalid simulation status XML: %s", text)
return
if element.tag == "status" and progress_callback is not None:
_deliver_callback(
progress_callback,
SimulationProgress(
phase=element.get("phase", ""),
current_step_size=float(element.get("currentStepSize", 0)),
time=float(element.get("time", 0)),
progress=int(float(element.get("progress", 0))),
),
)
elif element.tag == "message":
message = SimulationMessage(
stream=element.get("stream", ""),
type=element.get("type", ""),
text=element.get("text", ""),
)
log.info("OpenModelica %s: %s", message.stream, message.text)
if message_callback is not None:
_deliver_callback(message_callback, message)
def _ompython_transport_files(omc) -> set[Path]:
"""Return only the log and port files owned by this OMPython session."""
if omc is None:
return set()
process = getattr(omc, "omc_process", None)
if process is None:
return set()
files: set[Path] = set()
temp_dir = getattr(process, "_temp_dir", None)
file_base = getattr(process, "_omc_filebase", None)
if temp_dir is not None and file_base:
files.add(Path(temp_dir) / f"{file_base}.log")
try:
port_file = process._get_portfile_path()
except Exception:
port_file = None
if port_file is not None:
files.add(Path(port_file))
return files
def _openmodelica_home(executable_path: str) -> str | None:
"""Convert an optional omc executable path to the home expected by OMPython."""
if not executable_path.strip():
return None
path = Path(os.path.expandvars(executable_path)).expanduser()
if path.name.lower() in {"omc", "omc.exe"}:
return str(path.parent.parent)
return str(path)

View File

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

View File

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

View File

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

Binary file not shown.

View File

@@ -345,7 +345,7 @@ class PasteSelectionCommand(QUndoCommand):
def __init__(
self,
controller,
owner_id: str,
owner_id: str | None,
blocks: dict[str, Component],
connections: dict[str, Connection],
) -> None:

View File

@@ -421,17 +421,29 @@ class DocumentController(QObject):
if old != new:
self.undo_stack.push(EditGraphParametersCommand(self, old, new))
def compile_active_graph(self) -> None:
def compose_active_graph(self) -> None:
component = self.active_component
if component is None or component.implementation_kind != "graph":
raise ValueError("Open a graph component before compiling")
self.simulation.compile(component.to_dict())
raise ValueError("Open a graph component before composing")
self.simulation.compose(component.to_dict())
def run_simulation(self) -> None:
def run_simulation(
self,
progress_callback=None,
message_callback=None,
callback=None,
error_callback=None,
) -> None:
component = self.active_component
if component is None or component.implementation_kind != "graph":
raise ValueError("Open a graph component before running a simulation")
self.simulation.run_simulation(component.to_dict())
self.simulation.run_simulation(
component.to_dict(),
progress_callback,
message_callback,
callback,
error_callback,
)
def set_route_waypoints(self, item_kind: str, item_id: str, waypoints: list[QPointF]) -> None:
item = (
@@ -951,6 +963,68 @@ class DocumentController(QObject):
self.undo_stack.push(PasteSelectionCommand(self, owner.id, blocks, connections))
return list(blocks)
def paste_components_to(
self,
owner_id: str | None,
source_components: list[Component],
source_connections: list[Connection] | None = None,
) -> list[str]:
"""Clone components into the document root or a graph at origin."""
if self.document is None:
raise ValueError("Open or create a document before pasting components")
if owner_id is None:
siblings = self.document.roots.values()
else:
owner = self.document.find_component(owner_id)
if owner is None or owner.implementation_kind != "graph":
raise ValueError("Components can only be pasted into a graph")
siblings = owner.graph.blocks.values()
pairs = [(source, clone_component(source)) for source in source_components]
if not pairs:
return []
id_map = {source.id: clone.id for source, clone in pairs}
used = list(siblings)
minimum_x = min(source.x for source, _clone in pairs)
minimum_y = min(source.y for source, _clone in pairs)
blocks: dict[str, Component] = {}
for source, clone in pairs:
clone.name = self._available_component_name(source.name, used, 0)
if owner_id is not None:
clone.x = source.x - minimum_x
clone.y = source.y - minimum_y
blocks[clone.id] = clone
used.append(clone)
connections: dict[str, Connection] = {}
if owner_id is not None:
for source in source_connections or []:
if source.source.block not in id_map or source.target.block not in id_map:
continue
properties = deepcopy(source.properties)
for point in properties.get("waypoints", []):
if isinstance(point, dict):
point["x"] = float(point.get("x", 0)) - minimum_x
point["y"] = float(point.get("y", 0)) - minimum_y
connection = Connection(
id=str(uuid4()),
source=Endpoint(
block=id_map[source.source.block], port=source.source.port
),
target=Endpoint(
block=id_map[source.target.block], port=source.target.port
),
name=source.name,
properties=properties,
)
connections[connection.id] = connection
self.undo_stack.push(
PasteSelectionCommand(self, owner_id, blocks, connections)
)
return list(blocks)
def _graph_for(self, owner_id: str):
if self.document is None:
raise ValueError("There is no open document")

View File

@@ -2326,6 +2326,63 @@ K\x80@\x89\x15\x8d\xc04\xd5\xb5^\xaf\x1bx\xfa\x19\
\x84\xe7\x04\xcf\x88\xfd\xfd\xfd\xe4\xe8\xe8\xc8\x9dL&\x14\
\xc7\xcc\x7f5p\xf2g\x94\xf7\x1f\xdf\x9a\xd2\x93\xfbC\
\xb7\xa7\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x03m\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\
\x00\x00\x09pHYs\x00\x00\x1b\xaf\x00\x00\x1b\xaf\x01\
^\x1a\x91\x1c\x00\x00\x00\x07tIME\x07\xd9\x02\x10\
\x17\x22\x16\x993\xa5<\x00\x00\x02\xedIDATx\
\xda\xed\x97_H\xd3Q\x14\xc7\xcf\xd9\xef7\xcb\xca\x89\
B \x91\x0f\xc9\xcczha\xf6\xa8\xb4\x06M\x90f\
aiV$\x96V\x12\xf9P-S\x90d%Lh\
V/B.\x8d\xc2\xb2\xc0\x1erS\xc1\x84\x1cdo\
a\xa0\x0f>h\x0f\xfdy\xf0-\x9b\xa6\x0f\xb1{\xfb\
n\xf4\x8b\x91\xe2\x5cn\xae\x07\x0f\xdc\x9dq/\xfc>\
\xe7{\xce\xe1\xdc\xdf\x8f\xd6M\xb3\x86\x86\x86\xe2Dp\
u\xbf\xe19pG\x82>!\x01\x04\xe1\x9aOd\x09\
:\x13V\x02\xcd\xd6\x03H\x84\xa9\x04\xab\xaf\xaf\xef\x84\
\xcb\x8f\x07\xe0z\xcf\x82AU\xc8\xa6\xeax\xfc\xf6\xd1\
\x8dcKf\xa0\xa5\xa5\xa5\x0an$V\xd0\xea\xc7\xf3\
\x86\x9a\xae\xf9\xd3\xb5\xdd\xf3\xaf\x84\xa4\xcfRR\xa9\x5c\
&\x031\xb3\xf2\xf6\x1f\xb9P\xdb\x14\x90\xd2\xacH\xf6\
\x01\xda\x13\x10T\xd1Z\x96\xec\xa7x\x07P\xd26\xc7\
P\xdb!\x04\xbd\x10L\x15\xedg7-\x82\xc65\x00\
\xc0m\x02?\x82\xd9\xf5\xa4j\xb3\x5c\xd3&,\xba\x1f\
R\x8f\xd4\x93\xe3\xe5\xc5\x08\xf0X7\xa1\xa6> B\
\xde\xbb&s\xc0\xb3\xf3x\x81'\xbbt\x1f\xc1,w\
f\x19M\xd7$\xa0\xbe\xf7\xf2\x16\x19\xf5\x1c\x88\x0e|\
L!\xc9\x8d$u\x97X\xa8\x0b\xde\xac\x0aS\xab$\
3\x07\xd5\xb3\xd4\xd4\xc7'\x00\xa8\xce\x00\xfc)\xe0\xcc\
\xa4\xdbK2\xe9\x96\x94\xaa\x0bu\xcfc\xa8\x1f\xbc\x92\
\x22\xffq\x14GnB\xa4\xdbBRy\xcfRy\xcb\
\xa4XmS\xcf\xa7I\xe8\xed,\xf4\x85\xbb&\xc7\xb4\
\xda\xc7\xfe:\x06X\xf1d\x975y\x8c\xe5_\xbd\xc6\
\x93\x96\xf0\xb3\x5c\x87\x9f++\xdf\x8d\xf6m\xbf\xfa\xa9\
o[\x9d!\xe6\x97\x11\xc0\x198\x1e\x84\xea\x02\xa4|\
?T\xbf\x09?\x97\x92\x8a\xc7\xb3\xf6H)\x92^#\
\x1b\xae\xe5 \x03\x06\xb7e \xa5\xc3\xb4\xd2\x00\x00?\
\x91\x87Z\x87R\x0e_h\x9b\xea\x9e\x0e?\xdf\xdd\xf8\
\x9d\x03B\xde\x0cv>\x9a\xf1\x1aK\xbd\xb5\x7fk\xb3\
u1\xb8\xdd0`x\xe8F\xf9\x1e\xe19\xea\x8a\x03\
`\xa9\xfb\x09\xf8)\x80\x1d\xb6\x8f\xcf\x02\xda~\xa6}\
\x86v\xdc\x981\x06\x045\x02NB\x90\xf7\xf0t\xb3\
\x1f\x19\xb8\x80\xe5\xeeOo5\x84\xa9>\x040\x1aD\
!\xc0MEs\xe7F\x97diM\xc8\xcc\xf9N\xa7\
3'\xfc0\xb9\xe6\x1b\xe9\x98\x8c\xaaB\x07\x92\x142\
\xebU6\xeb\xf1\xbc$\x85}\xd8\xbb;\xd1\x9c\xfa\xe1\
\x0f0\xfd\x9e;\x04\x13\x1b\xecD\xec\x02\xdcJ\x92\xcf\
\x03<\x14\xf5$\x04\xd8\x84\xd5\x85\xbf_P\xeba,\
3\x96\x0f\x8a\x0fB}\xe6\xa43\xf5L\x08\x1enB\
\xb5\x13J\x01\xb5\x13a\xaa\x87V3\x07|X\x0eI\
4\xe5oK\x8b|\x1f\xcc\xd4\xfa\xd1h%\x00\xa7\x02\
<\xbc\xaaA\xb4\xf0 m\x0c\x0e+:+\x9a\xad\x1e\
\xfd\xaf\xdf\x091k\xea\xfe\xfe\xf8\xe1H\xdf\x05R\xca\
|f\x1e\x89\xd1\x9ef\xbdhx\x0fE4mL\xaf\
b/B\x06\x12o\xbf\x00\xa3\x17WYZq\xd9W\
\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x04<\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -2396,6 +2453,35 @@ f\xa2\xd1\xa5\x5c\x22\xb4\x91SZ\xd5u\xd7\x0a\xd2]\
|\x01\x85\x09\x800\x7fss\xd3{\xf6\x7fABG\
Y\x01\x05l*\xfc\x00!\x00\x12\xf1%U\xb6\x0e\x00\
\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x01\xad\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00 \x00\x00\x00 \x08\x03\x00\x00\x00D\xa4\x8a\xc6\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01B(\
\x9bx\x00\x00\x00\x07tIME\x07\xd9\x0c\x1c\x03\x1c\
\x0e%S,b\x00\x00\x00uPLTE\x00\x00\x00\
\x13\x13\x13\x0a\x0a\x0a\x0b\x0b\x0b\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc6\xc6\xc8\xc7\xc7\
\xc9\xc9\xc9\xcb\xcc\xcc\xce\xce\xce\xd0\xd0\xd0\xd2\xd4\xd4\xd6\
\xd7\xd7\xd8\xd7\xd7\xd9\xd8\xd8\xda\xd9\xd9\xdb\xda\xda\xdb\xda\
\xda\xdc\xdb\xdb\xdc\xdb\xdb\xdd\xdd\xdd\xdf\xde\xde\xdf\xe2\xe2\
\xe3\xe3\xe3\xe5\xe3\xe3\xe6\xe4\xe4\xe6\xe6\xe6\xe7\xe7\xe7\xe9\
\xe9\xe9\xea\xeb\xeb\xec\xed\xed\xee\xf0\xf0\xf1\xf3\xf3\xf4\xff\
\xff\xff\xd3\x9b\xcc\x0e\x00\x00\x00\x0atRNS\x00\x09\
\x15\x15\x1825678\xb5\xcc\xc0\x1e\x00\x00\x00\x01\
bKGD&Z\x08\x98\xb5\x00\x00\x00\x9bIDA\
Tx\xda\xd5\x93\xcb\x0e\x820\x10\x00\x8b\x0aEP|\
u\xc1G)\x94\x02\xff\xff\x89.]\x0e\x18\xccr1\
F\xe72\xd9d\xd26\x9bT\xfc\x02\xc1\x86%\x10a\
\xcf\x12\x0a\xd9)\x86N\x0a\xd9^\x18Z\x0c\xdc\x91\xc1\
a\xd0\x1c\x18\x1a\x0c\xec\x9e\xc1bP'\x9et\x97\xce\
\x95\xd4\x18\x94=q#\xddIWR\x89\x81\x1e\x03E\
\x82\x97IO\x02\x98\x8b\x82G\x01\x9e\xec\x8d\x8aj\xf1\
\x84\xcf\xbc!W\x03\x90\x8d\x02oR^-^\xf1?\
{\xe0\x83\xc8\x1a=`\xce\xc6\xebd\xa6\x93\x8b\xc4*\
\xde2\xc4\xeb/|\xcd'\xec\xbfO\xbf\x90M\x1a\x0a\
\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x02\x92\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -2632,10 +2718,20 @@ qt_resource_name = b"\
\x00d\
\x00o\x00c\x00u\x00m\x00e\x00n\x00t\x00-\x00s\x00a\x00v\x00e\x00.\x00p\x00n\x00g\
\
\x00\x15\
\x02\xb4\x1f\x07\
\x00o\
\x00f\x00f\x00i\x00c\x00e\x00-\x00c\x00h\x00a\x00r\x00t\x00-\x00l\x00i\x00n\x00e\
\x00.\x00p\x00n\x00g\
\x00\x10\
\x03\xe6\xd3g\
\x00d\
\x00r\x00a\x00w\x00-\x00e\x00l\x00l\x00i\x00p\x00s\x00e\x00.\x00p\x00n\x00g\
\x00\x13\
\x07\xd6O\x07\
\x00v\
\x00i\x00e\x00w\x00-\x00f\x00o\x00r\x00m\x00-\x00t\x00a\x00b\x00l\x00e\x00.\x00p\
\x00n\x00g\
\x00\x12\
\x09\xb3>\xc7\
\x00d\
@@ -2653,7 +2749,7 @@ qt_resource_struct = b"\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1c\x00\x00\x00\x03\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1e\x00\x00\x00\x03\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x01\xfe\x00\x00\x00\x00\x00\x01\x00\x00N\xe8\
\x00\x00\x01\x9f{C\xf1'\
@@ -2661,11 +2757,13 @@ qt_resource_struct = b"\
\x00\x00\x01\x9f\x7f\xa8\xa8)\
\x00\x00\x02\xec\x00\x00\x00\x00\x00\x01\x00\x00tV\
\x00\x00\x01\x9f{0\xc99\
\x00\x00\x03\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x8d\xaa\
\x00\x00\x01\x9f\x84\x8f\x00\xb9\
\x00\x00\x01\xc4\x00\x00\x00\x00\x00\x01\x00\x00D2\
\x00\x00\x01\x9f\x7f&\x83\xcd\
\x00\x00\x01\xa4\x00\x00\x00\x00\x00\x01\x00\x00<J\
\x00\x00\x01\x9f{C\xf1\x18\
\x00\x00\x03\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x8d\xaa\
\x00\x00\x03\xd4\x00\x00\x00\x00\x00\x01\x00\x00\x91\x1b\
\x00\x00\x01\x9f\x7fY\xceg\
\x00\x00\x02\xa2\x00\x00\x00\x00\x00\x01\x00\x00f\xc8\
\x00\x00\x01\x9f\x7fV\xd5\xc0\
@@ -2677,9 +2775,11 @@ qt_resource_struct = b"\
\x00\x00\x01\x9f\x7fY\xce\x82\
\x00\x00\x01\xe0\x00\x00\x00\x00\x00\x01\x00\x00Kh\
\x00\x00\x01\x9f{C\xf1.\
\x00\x00\x03\xfa\x00\x00\x00\x00\x00\x01\x00\x00\x95[\
\x00\x00\x01\x9f\x84\x8f\xa6\x10\
\x00\x00\x01\x84\x00\x00\x00\x00\x00\x01\x00\x006\x9c\
\x00\x00\x01\x9f{{\xa5\xd5\
\x00\x00\x03\xca\x00\x00\x00\x00\x00\x01\x00\x00\x91\xea\
\x00\x00\x04&\x00\x00\x00\x00\x00\x01\x00\x00\x97\x0c\
\x00\x00\x01\x9f\x7f&\x83\x10\
\x00\x00\x00\xfe\x00\x00\x00\x00\x00\x01\x00\x00(e\
\x00\x00\x01\x9f\x7f&\x83~\
@@ -2703,7 +2803,7 @@ qt_resource_struct = b"\
\x00\x00\x01\x9f{C\xf1=\
\x00\x00\x006\x00\x00\x00\x00\x00\x01\x00\x00\x05\x86\
\x00\x00\x01\x9f\x7f&\x83\x99\
\x00\x00\x03\xf4\x00\x00\x00\x00\x00\x01\x00\x00\x94\x80\
\x00\x00\x04P\x00\x00\x00\x00\x00\x01\x00\x00\x99\xa2\
\x00\x00\x01\x9f{{\xa5\xe3\
\x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x00\x0c\xc1\
\x00\x00\x01\x9f\x7f\xac\xf2\xc6\

View File

@@ -39,91 +39,96 @@ class Ui_MainWindow(object):
self.actionGraphParameters = QAction(MainWindow)
self.actionGraphParameters.setObjectName(u"actionGraphParameters")
icon1 = QIcon()
icon1.addFile(u":/icons/icons/configure.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
icon1.addFile(u":/icons/icons/view-form-table.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionGraphParameters.setIcon(icon1)
self.actionCompile = QAction(MainWindow)
self.actionCompile.setObjectName(u"actionCompile")
self.actionCompose = QAction(MainWindow)
self.actionCompose.setObjectName(u"actionCompose")
icon2 = QIcon()
icon2.addFile(u":/icons/icons/run-build.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCompile.setIcon(icon2)
self.actionCompose.setIcon(icon2)
self.actionRunSimulation = QAction(MainWindow)
self.actionRunSimulation.setObjectName(u"actionRunSimulation")
icon3 = QIcon()
icon3.addFile(u":/icons/icons/media-playback-start.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRunSimulation.setIcon(icon3)
self.actionSimulationWindow = QAction(MainWindow)
self.actionSimulationWindow.setObjectName(u"actionSimulationWindow")
icon4 = QIcon()
icon4.addFile(u":/icons/icons/office-chart-line.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSimulationWindow.setIcon(icon4)
self.actionNew = QAction(MainWindow)
self.actionNew.setObjectName(u"actionNew")
icon4 = QIcon()
icon4.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionNew.setIcon(icon4)
icon5 = QIcon()
icon5.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionNew.setIcon(icon5)
self.actionRotateClockwise = QAction(MainWindow)
self.actionRotateClockwise.setObjectName(u"actionRotateClockwise")
icon5 = QIcon()
icon5.addFile(u":/icons/icons/transform-rotate.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRotateClockwise.setIcon(icon5)
icon6 = QIcon()
icon6.addFile(u":/icons/icons/transform-rotate.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRotateClockwise.setIcon(icon6)
self.actionZoomIn = QAction(MainWindow)
self.actionZoomIn.setObjectName(u"actionZoomIn")
icon6 = QIcon()
icon6.addFile(u":/icons/icons/zoom-in.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionZoomIn.setIcon(icon6)
icon7 = QIcon()
icon7.addFile(u":/icons/icons/zoom-in.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionZoomIn.setIcon(icon7)
self.actionZoomOut = QAction(MainWindow)
self.actionZoomOut.setObjectName(u"actionZoomOut")
icon7 = QIcon()
icon7.addFile(u":/icons/icons/zoom-out.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionZoomOut.setIcon(icon7)
icon8 = QIcon()
icon8.addFile(u":/icons/icons/zoom-out.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionZoomOut.setIcon(icon8)
self.actionCenterView = QAction(MainWindow)
self.actionCenterView.setObjectName(u"actionCenterView")
icon8 = QIcon()
icon8.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCenterView.setIcon(icon8)
icon9 = QIcon()
icon9.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCenterView.setIcon(icon9)
self.actionOpen = QAction(MainWindow)
self.actionOpen.setObjectName(u"actionOpen")
icon9 = QIcon()
icon9.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionOpen.setIcon(icon9)
icon10 = QIcon()
icon10.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionOpen.setIcon(icon10)
self.actionReloadLibraries = QAction(MainWindow)
self.actionReloadLibraries.setObjectName(u"actionReloadLibraries")
self.actionReloadSimulation = QAction(MainWindow)
self.actionReloadSimulation.setObjectName(u"actionReloadSimulation")
self.actionSave = QAction(MainWindow)
self.actionSave.setObjectName(u"actionSave")
icon10 = QIcon()
icon10.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSave.setIcon(icon10)
icon11 = QIcon()
icon11.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSave.setIcon(icon11)
self.actionSaveAs = QAction(MainWindow)
self.actionSaveAs.setObjectName(u"actionSaveAs")
icon11 = QIcon()
icon11.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSaveAs.setIcon(icon11)
icon12 = QIcon()
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.setObjectName(u"actionExit")
self.actionClose = QAction(MainWindow)
self.actionClose.setObjectName(u"actionClose")
self.actionUndo = QAction(MainWindow)
self.actionUndo.setObjectName(u"actionUndo")
icon12 = QIcon()
icon12.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionUndo.setIcon(icon12)
icon13 = QIcon()
icon13.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionUndo.setIcon(icon13)
self.actionRedo = QAction(MainWindow)
self.actionRedo.setObjectName(u"actionRedo")
icon13 = QIcon()
icon13.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRedo.setIcon(icon13)
icon14 = QIcon()
icon14.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRedo.setIcon(icon14)
self.actionCut = QAction(MainWindow)
self.actionCut.setObjectName(u"actionCut")
icon14 = QIcon()
icon14.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCut.setIcon(icon14)
icon15 = QIcon()
icon15.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCut.setIcon(icon15)
self.actionCopy = QAction(MainWindow)
self.actionCopy.setObjectName(u"actionCopy")
icon15 = QIcon()
icon15.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCopy.setIcon(icon15)
icon16 = QIcon()
icon16.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCopy.setIcon(icon16)
self.actionPaste = QAction(MainWindow)
self.actionPaste.setObjectName(u"actionPaste")
icon16 = QIcon()
icon16.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionPaste.setIcon(icon16)
icon17 = QIcon()
icon17.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionPaste.setIcon(icon17)
self.actionSelectAll = QAction(MainWindow)
self.actionSelectAll.setObjectName(u"actionSelectAll")
self.actionDelete = QAction(MainWindow)
@@ -213,18 +218,18 @@ class Ui_MainWindow(object):
self.workspaceHeaderLayout.setContentsMargins(6, 2, 6, 2)
self.navigateUpButton = QToolButton(self.workspaceHeader)
self.navigateUpButton.setObjectName(u"navigateUpButton")
icon17 = QIcon()
icon17.addFile(u":/icons/icons/arrow-up.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.navigateUpButton.setIcon(icon17)
icon18 = QIcon()
icon18.addFile(u":/icons/icons/arrow-up.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.navigateUpButton.setIcon(icon18)
self.workspaceHeaderLayout.addWidget(self.navigateUpButton)
self.navigateDownButton = QToolButton(self.workspaceHeader)
self.navigateDownButton.setObjectName(u"navigateDownButton")
self.navigateDownButton.setEnabled(False)
icon18 = QIcon()
icon18.addFile(u":/icons/icons/arrow-down.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.navigateDownButton.setIcon(icon18)
icon19 = QIcon()
icon19.addFile(u":/icons/icons/arrow-down.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.navigateDownButton.setIcon(icon19)
self.workspaceHeaderLayout.addWidget(self.navigateDownButton)
@@ -244,9 +249,9 @@ class Ui_MainWindow(object):
self.pointerToolButton = QToolButton(self.workspaceHeader)
self.pointerToolButton.setObjectName(u"pointerToolButton")
icon19 = QIcon()
icon19.addFile(u":/icons/icons/edit-select.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.pointerToolButton.setIcon(icon19)
icon20 = QIcon()
icon20.addFile(u":/icons/icons/edit-select.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.pointerToolButton.setIcon(icon20)
self.pointerToolButton.setCheckable(True)
self.pointerToolButton.setChecked(True)
@@ -254,43 +259,43 @@ class Ui_MainWindow(object):
self.connectToolButton = QToolButton(self.workspaceHeader)
self.connectToolButton.setObjectName(u"connectToolButton")
icon20 = QIcon()
icon20.addFile(u":/icons/icons/network-connect.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.connectToolButton.setIcon(icon20)
icon21 = QIcon()
icon21.addFile(u":/icons/icons/network-connect.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.connectToolButton.setIcon(icon21)
self.connectToolButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.connectToolButton)
self.boxToolButton = QToolButton(self.workspaceHeader)
self.boxToolButton.setObjectName(u"boxToolButton")
icon21 = QIcon()
icon21.addFile(u":/icons/icons/draw-rectangle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.boxToolButton.setIcon(icon21)
icon22 = QIcon()
icon22.addFile(u":/icons/icons/draw-rectangle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.boxToolButton.setIcon(icon22)
self.boxToolButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.boxToolButton)
self.lineToolButton = QToolButton(self.workspaceHeader)
self.lineToolButton.setObjectName(u"lineToolButton")
icon22 = QIcon()
icon22.addFile(u":/icons/icons/draw-bezier-curves.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.lineToolButton.setIcon(icon22)
icon23 = QIcon()
icon23.addFile(u":/icons/icons/draw-bezier-curves.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.lineToolButton.setIcon(icon23)
self.lineToolButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.lineToolButton)
self.textToolButton = QToolButton(self.workspaceHeader)
self.textToolButton.setObjectName(u"textToolButton")
icon23 = QIcon()
icon23.addFile(u":/icons/icons/draw-text.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.textToolButton.setIcon(icon23)
icon24 = QIcon()
icon24.addFile(u":/icons/icons/draw-text.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.textToolButton.setIcon(icon24)
self.textToolButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.textToolButton)
self.rotateToolButton = QToolButton(self.workspaceHeader)
self.rotateToolButton.setObjectName(u"rotateToolButton")
self.rotateToolButton.setIcon(icon5)
self.rotateToolButton.setIcon(icon6)
self.workspaceHeaderLayout.addWidget(self.rotateToolButton)
@@ -435,7 +440,8 @@ class Ui_MainWindow(object):
self.menuHelp.addAction(self.actionAboutQt)
self.menuSimulation.addAction(self.actionSimulationSettings)
self.menuSimulation.addAction(self.actionGraphParameters)
self.menuSimulation.addAction(self.actionCompile)
self.menuSimulation.addAction(self.actionCompose)
self.menuSimulation.addAction(self.actionSimulationWindow)
self.menuSimulation.addAction(self.actionRunSimulation)
self.fileToolbar.addAction(self.actionNew)
self.fileToolbar.addAction(self.actionOpen)
@@ -451,7 +457,8 @@ class Ui_MainWindow(object):
self.cameraToolbar.addAction(self.actionCenterView)
self.simulationToolbar.addAction(self.actionSimulationSettings)
self.simulationToolbar.addAction(self.actionGraphParameters)
self.simulationToolbar.addAction(self.actionCompile)
self.simulationToolbar.addAction(self.actionCompose)
self.simulationToolbar.addAction(self.actionSimulationWindow)
self.simulationToolbar.addAction(self.actionRunSimulation)
self.retranslateUi(MainWindow)
@@ -472,12 +479,12 @@ class Ui_MainWindow(object):
#if QT_CONFIG(statustip)
self.actionGraphParameters.setStatusTip(QCoreApplication.translate("MainWindow", u"Edit parameters throughout the active graph", None))
#endif // QT_CONFIG(statustip)
self.actionCompile.setText(QCoreApplication.translate("MainWindow", u"Compile", None))
self.actionCompose.setText(QCoreApplication.translate("MainWindow", u"Compose", None))
#if QT_CONFIG(statustip)
self.actionCompile.setStatusTip(QCoreApplication.translate("MainWindow", u"Compile the active graph for simulation", None))
self.actionCompose.setStatusTip(QCoreApplication.translate("MainWindow", u"Compose the active graph as an OpenModelica model", None))
#endif // QT_CONFIG(statustip)
#if QT_CONFIG(shortcut)
self.actionCompile.setShortcut(QCoreApplication.translate("MainWindow", u"F5", None))
self.actionCompose.setShortcut(QCoreApplication.translate("MainWindow", u"F5", None))
#endif // QT_CONFIG(shortcut)
self.actionRunSimulation.setText(QCoreApplication.translate("MainWindow", u"Run", None))
#if QT_CONFIG(statustip)
@@ -486,6 +493,10 @@ class Ui_MainWindow(object):
#if QT_CONFIG(shortcut)
self.actionRunSimulation.setShortcut(QCoreApplication.translate("MainWindow", u"F6", None))
#endif // QT_CONFIG(shortcut)
self.actionSimulationWindow.setText(QCoreApplication.translate("MainWindow", u"Simulation Window", None))
#if QT_CONFIG(statustip)
self.actionSimulationWindow.setStatusTip(QCoreApplication.translate("MainWindow", u"Show the simulation results window", None))
#endif // QT_CONFIG(statustip)
self.actionNew.setText(QCoreApplication.translate("MainWindow", u"&New", None))
#if QT_CONFIG(statustip)
self.actionNew.setStatusTip(QCoreApplication.translate("MainWindow", u"Create a new document", None))

View File

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

View File

@@ -1892,6 +1892,22 @@ class GraphWorkspaceView(QGraphicsView):
if self.controller is None:
return
mime_data = QApplication.clipboard().mimeData()
if mime_data.hasFormat(COMPONENT_MIME_TYPE):
try:
payload = json.loads(
bytes(mime_data.data(COMPONENT_MIME_TYPE)).decode("utf-8")
)
source = Component.from_dict(payload)
except (KeyError, TypeError, ValueError, json.JSONDecodeError):
return
component_id = self.controller.add_component_copy(source, QPointF(0, 0))
scene = self.scene()
if isinstance(scene, GraphScene):
scene.clearSelection()
item = scene.component_items.get(component_id)
if item is not None:
item.setSelected(True)
return
if not mime_data.hasFormat(SELECTION_MIME_TYPE):
return
try:

View File

@@ -1,10 +1,12 @@
import json
from copy import deepcopy
from pathlib import Path
from PySide6.QtCore import Qt, Slot
from PySide6.QtCore import QByteArray, QMimeData, Qt, Slot
from PySide6.QtGui import QCloseEvent
from PySide6.QtWidgets import (
QButtonGroup,
QApplication,
QFileDialog,
QMainWindow,
QMenu,
@@ -12,7 +14,7 @@ from PySide6.QtWidgets import (
QTabWidget,
)
from bedit.core.model import Component
from bedit.core.model import Component, Connection
from bedit.core.application_log import get_logger
from bedit.core.serializer import DocumentSerializer
from bedit.core.simulation import Simulation
@@ -22,16 +24,20 @@ from bedit.gui.dialogs.graph_parameters import GraphParametersDialog
from bedit.gui.dialogs.item_options import ItemOptionsDialog
from bedit.gui.models.library_repository import LibraryRepository
from bedit.gui.models.library_tree import (
COMPONENT_MIME_TYPE,
COMPONENT_ROLE,
COMPONENT_ID_ROLE,
COMPONENT_INSTANCE_ROLE,
ITEM_KIND_ROLE,
DocumentTreeModel,
LibraryTreeModel,
)
from bedit.gui.graphics.workspace import SELECTION_MIME_TYPE
from bedit.gui.dialogs.settings import SettingsDialog
from bedit.gui.dialogs.port_options import PortOptionsDialog
from bedit.gui.dialogs.parameter_options import ParameterOptionsDialog
from bedit.gui.dialogs.simulation_settings import SimulationSettingsDialog
from bedit.gui.simulation_window import SimulationWindow
from bedit.gui.preferences import application_settings
from bedit.gui.simulation_reload import reload_simulation
from bedit.gui.generated.ui_main_window import Ui_MainWindow
@@ -54,6 +60,9 @@ class MainWindow(QMainWindow):
self.log.info("BEdit started")
self.settings = application_settings()
self._applying_text_definition = False
# Keep a Python-owned top-level window. Giving it MainWindow as its Qt
# parent makes some Linux window managers inherit the BEdit window icon.
self._simulation_window = SimulationWindow()
self.libraries = LibraryRepository(self)
self.simulation = Simulation(
@@ -67,6 +76,7 @@ class MainWindow(QMainWindow):
)
self.document_tree_model = DocumentTreeModel(self.document_controller, self)
self._configure_models()
QApplication.clipboard().dataChanged.connect(self._update_edit_actions)
self._connect_actions()
self._populate_view_menu()
self._restore_window_geometry()
@@ -100,6 +110,10 @@ class MainWindow(QMainWindow):
self.ui.documentTreeView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.ui.documentTreeView.customContextMenuRequested.connect(self.show_library_context_menu)
self.ui.documentTreeView.doubleClicked.connect(self.activate_tree_component)
self.ui.documentTreeView.clicked.connect(
lambda _index: self._update_edit_actions()
)
self.ui.treeView.clicked.connect(lambda _index: self._update_edit_actions())
self.document_tree_model.rebuilt.connect(self.ui.documentTreeView.expandAll)
self.ui.graphView.set_model(self.document_controller)
self.ui.graphView.componentOptionsRequested.connect(self.show_component_options)
@@ -153,9 +167,9 @@ class MainWindow(QMainWindow):
self.ui.actionUndo.triggered.connect(self.document_controller.undo_stack.undo)
self.ui.actionRedo.triggered.connect(self.document_controller.undo_stack.redo)
self.ui.actionDelete.triggered.connect(self.ui.graphView.delete_selected)
self.ui.actionCopy.triggered.connect(self.ui.graphView.copy_selection)
self.ui.actionCut.triggered.connect(self.ui.graphView.cut_selection)
self.ui.actionPaste.triggered.connect(self.ui.graphView.paste_selection)
self.ui.actionCopy.triggered.connect(self.copy_selection)
self.ui.actionCut.triggered.connect(self.cut_selection)
self.ui.actionPaste.triggered.connect(self.paste_selection)
self.ui.actionSelectAll.triggered.connect(self.ui.graphView.select_all)
self.ui.actionRotateClockwise.triggered.connect(self.ui.graphView.rotate_selected)
self.addAction(self.ui.actionRotateClockwise)
@@ -166,7 +180,8 @@ class MainWindow(QMainWindow):
self.show_simulation_settings
)
self.ui.actionGraphParameters.triggered.connect(self.show_graph_parameters)
self.ui.actionCompile.triggered.connect(self.compile_active_graph)
self.ui.actionCompose.triggered.connect(self.compose_active_graph)
self.ui.actionSimulationWindow.triggered.connect(self.show_simulation_window)
self.ui.actionRunSimulation.triggered.connect(self.run_simulation)
self.document_controller.undo_stack.canUndoChanged.connect(self.ui.actionUndo.setEnabled)
self.document_controller.undo_stack.canRedoChanged.connect(self.ui.actionRedo.setEnabled)
@@ -239,20 +254,30 @@ class MainWindow(QMainWindow):
QMessageBox.warning(self, "Cannot change graph parameters", str(error))
@Slot()
def compile_active_graph(self) -> None:
def compose_active_graph(self) -> None:
try:
self.document_controller.compile_active_graph()
self.document_controller.compose_active_graph()
except ValueError as error:
self.log.error("Compile failed: %s", error)
QMessageBox.warning(self, "Cannot compile", str(error))
self.log.error("Composition failed: %s", error)
QMessageBox.warning(self, "Cannot compose", str(error))
@Slot()
def show_simulation_window(self) -> None:
self._simulation_window.show()
self._simulation_window.raise_()
self._simulation_window.activateWindow()
@Slot()
def run_simulation(self) -> None:
window = self._simulation_window
callbacks = window.begin_run()
self.show_simulation_window()
try:
self.document_controller.run_simulation()
self.document_controller.run_simulation(*callbacks)
window.set_model_name(self.simulation.model_name)
except Exception as error:
self.log.exception("Simulation run failed")
QMessageBox.warning(self, "Cannot run simulation", str(error))
window.report_start_error(error)
def _restore_window_geometry(self) -> None:
geometry = self.settings.value("window/geometry")
@@ -281,7 +306,7 @@ class MainWindow(QMainWindow):
self._set_graph_controls_visible(False)
self.ui.actionSimulationSettings.setEnabled(False)
self.ui.actionGraphParameters.setEnabled(False)
self.ui.actionCompile.setEnabled(False)
self.ui.actionCompose.setEnabled(False)
self.ui.actionRunSimulation.setEnabled(False)
self._update_edit_actions()
return
@@ -292,7 +317,7 @@ class MainWindow(QMainWindow):
is_graph = component.implementation_kind == "graph"
self.ui.actionSimulationSettings.setEnabled(is_graph)
self.ui.actionGraphParameters.setEnabled(is_graph)
self.ui.actionCompile.setEnabled(is_graph)
self.ui.actionCompose.setEnabled(is_graph)
self.ui.actionRunSimulation.setEnabled(is_graph)
self.ui.workspaceModeLabel.setText("Graph" if is_graph else "Text")
self.ui.workspaceStack.setCurrentWidget(
@@ -311,8 +336,24 @@ class MainWindow(QMainWindow):
has_selection = bool(
self.ui.graphView.scene() and self.ui.graphView.scene().selectedItems()
)
for action in (self.ui.actionCut, self.ui.actionCopy, self.ui.actionDelete):
action.setEnabled(is_graph and has_selection)
document_component_selected = bool(
self.ui.documentTreeView.currentIndex().data(COMPONENT_ID_ROLE)
)
library_component_selected = isinstance(
self.ui.treeView.currentIndex().data(COMPONENT_ROLE), dict
)
document_has_focus = self._view_has_focus(self.ui.documentTreeView)
library_has_focus = self._view_has_focus(self.ui.treeView)
self.ui.actionCopy.setEnabled(
(is_graph and has_selection)
or (document_has_focus and document_component_selected)
or (library_has_focus and library_component_selected)
)
self.ui.actionCut.setEnabled(
(is_graph and has_selection)
or (document_has_focus and document_component_selected)
)
self.ui.actionDelete.setEnabled(is_graph and has_selection)
self.ui.actionRotateClockwise.setEnabled(
is_graph and self.ui.graphView.has_selected_components()
)
@@ -320,11 +361,114 @@ class MainWindow(QMainWindow):
is_graph and self.ui.graphView.has_selected_components()
)
self.ui.actionSelectAll.setEnabled(is_graph)
self.ui.actionPaste.setEnabled(is_graph)
can_paste_component = QApplication.clipboard().mimeData().hasFormat(
COMPONENT_MIME_TYPE
) or QApplication.clipboard().mimeData().hasFormat(SELECTION_MIME_TYPE)
self.ui.actionPaste.setEnabled(
(is_graph and not document_has_focus and not library_has_focus)
or (
document_has_focus
and self.document_controller.document is not None
and can_paste_component
)
)
self.ui.navigateDownButton.setEnabled(
is_graph and self.ui.graphView.has_single_selected_component()
)
def copy_selection(self) -> bool:
if self._view_has_focus(self.ui.documentTreeView):
return self._copy_tree_component(self.ui.documentTreeView)
if self._view_has_focus(self.ui.treeView):
return self._copy_tree_component(self.ui.treeView)
return self.ui.graphView.copy_selection()
def cut_selection(self) -> None:
if self._view_has_focus(self.ui.documentTreeView):
index = self.ui.documentTreeView.currentIndex()
component_id = index.data(COMPONENT_ID_ROLE)
if component_id and self._copy_tree_component(self.ui.documentTreeView):
self.document_controller.delete_component(component_id)
return
if self._view_has_focus(self.ui.treeView):
self._copy_tree_component(self.ui.treeView)
return
self.ui.graphView.cut_selection()
def paste_selection(self) -> None:
if self._view_has_focus(self.ui.documentTreeView):
self._paste_into_document_tree()
return
self.ui.graphView.paste_selection()
def _copy_tree_component(self, tree) -> bool:
component = tree.currentIndex().data(COMPONENT_ROLE)
if not isinstance(component, dict):
return False
mime_data = QMimeData()
mime_data.setData(
COMPONENT_MIME_TYPE,
QByteArray(json.dumps(component).encode("utf-8")),
)
QApplication.clipboard().setMimeData(mime_data)
return True
def _paste_into_document_tree(self) -> None:
index = self.ui.documentTreeView.currentIndex()
kind = index.data(ITEM_KIND_ROLE)
owner_id = None
if kind == "current-component":
component_id = index.data(COMPONENT_ID_ROLE)
component = (
self.document_controller.document.find_component(component_id)
if self.document_controller.document
else None
)
if component is None or component.implementation_kind != "graph":
QMessageBox.warning(
self, "Cannot Paste", "Select a graph component or the document root."
)
return
owner_id = component.id
elif kind != "current-document":
QMessageBox.warning(
self, "Cannot Paste", "Select a graph component or the document root."
)
return
try:
components, connections = self._clipboard_components()
self.document_controller.paste_components_to(
owner_id, components, connections
)
except ValueError as error:
QMessageBox.warning(self, "Cannot Paste", str(error))
@staticmethod
def _clipboard_components() -> tuple[list[Component], list]:
mime_data = QApplication.clipboard().mimeData()
try:
if mime_data.hasFormat(SELECTION_MIME_TYPE):
payload = json.loads(
bytes(mime_data.data(SELECTION_MIME_TYPE)).decode("utf-8")
)
return (
[Component.from_dict(item) for item in payload.get("components", [])],
[Connection.from_dict(item) for item in payload.get("connections", [])],
)
if mime_data.hasFormat(COMPONENT_MIME_TYPE):
payload = json.loads(
bytes(mime_data.data(COMPONENT_MIME_TYPE)).decode("utf-8")
)
return [Component.from_dict(payload)], []
except (KeyError, TypeError, ValueError, json.JSONDecodeError) as error:
raise ValueError("The clipboard does not contain a valid component") from error
raise ValueError("The clipboard does not contain a BEdit component")
@staticmethod
def _view_has_focus(view) -> bool:
focus = QApplication.focusWidget()
return focus is view or (focus is not None and view.isAncestorOf(focus))
@Slot()
def navigate_up(self) -> None:
if self._resolve_source_edits():
@@ -547,6 +691,8 @@ class MainWindow(QMainWindow):
def show_library_context_menu(self, position) -> None:
tree_view = self.ui.documentTreeView
index = tree_view.indexAt(position)
if index.isValid():
tree_view.setCurrentIndex(index)
kind = index.data(ITEM_KIND_ROLE)
if kind == "current-component":
component_id = index.data(COMPONENT_ID_ROLE)
@@ -564,6 +710,11 @@ class MainWindow(QMainWindow):
options_action = menu.addAction("Component Options…")
ports_action = menu.addAction("Port Options…")
parameters_action = menu.addAction("Parameter Options…")
menu.addSeparator()
copy_action = menu.addAction("Copy")
cut_action = menu.addAction("Cut")
paste_action = menu.addAction("Paste")
paste_action.setEnabled(component.implementation_kind == "graph")
delete_action = menu.addAction("Delete")
selected = menu.exec(tree_view.viewport().mapToGlobal(position))
if graph_action is not None and selected is graph_action:
@@ -576,6 +727,13 @@ class MainWindow(QMainWindow):
self.show_component_port_options(component_id)
elif selected is parameters_action:
self.show_component_parameter_options(component_id)
elif selected is copy_action:
self._copy_tree_component(tree_view)
elif selected is cut_action:
if self._copy_tree_component(tree_view):
self.document_controller.delete_component(component_id)
elif selected is paste_action:
self._paste_into_document_tree()
elif selected is delete_action:
answer = QMessageBox.question(
self,
@@ -591,24 +749,34 @@ class MainWindow(QMainWindow):
menu = QMenu(self)
graph_action = menu.addAction("New Graph Block")
text_action = menu.addAction("New Text Block")
menu.addSeparator()
paste_action = menu.addAction("Paste")
selected = menu.exec(tree_view.viewport().mapToGlobal(position))
if selected is graph_action:
self.document_controller.add_root("graph")
elif selected is text_action:
self.document_controller.add_root("text")
elif selected is paste_action:
self._paste_into_document_tree()
@Slot(object)
def show_external_library_context_menu(self, position) -> None:
tree = self.ui.treeView
index = tree.indexAt(position)
if index.isValid():
tree.setCurrentIndex(index)
component = index.data(COMPONENT_INSTANCE_ROLE)
if not isinstance(component, Component):
return
menu = QMenu(self)
copy_action = menu.addAction("Copy")
menu.addSeparator()
ports_action = menu.addAction("Port Options…")
parameters_action = menu.addAction("Parameter Options…")
selected = menu.exec(tree.viewport().mapToGlobal(position))
if selected is ports_action:
if selected is copy_action:
self._copy_tree_component(tree)
elif selected is ports_action:
dialog = PortOptionsDialog(component, self)
if dialog.exec() == dialog.DialogCode.Accepted:
old_inputs, old_outputs = deepcopy(component.inputs), deepcopy(component.outputs)
@@ -760,6 +928,8 @@ class MainWindow(QMainWindow):
event.ignore()
return
self.settings.setValue("window/geometry", self.saveGeometry())
self._simulation_window.close()
self.simulation.shutdown()
self.log.info("BEdit closed")
self.application_logger.removeHandler(self.log_handler)
event.accept()

View File

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

View File

@@ -0,0 +1,266 @@
import re
from pathlib import Path
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import QFileDialog, QMainWindow, QMessageBox
from bedit.core.simulation.openmodelica import SimulationMessage, SimulationProgress
from bedit.core.simulation.results import (
SimulationExecutionResult,
SimulationResults,
load_simulation_results,
save_simulation_results,
)
from bedit.gui.generated.ui_simulation_window import Ui_SimulationWindow
class SimulationWindow(QMainWindow):
"""Persistent viewer for live and previously saved simulation results."""
progressReceived = Signal(object)
messageReceived = Signal(object)
simulationFinished = Signal(object)
simulationFailed = Signal(str)
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.ui = Ui_SimulationWindow()
self.ui.setupUi(self)
self._running = False
self._run_generation = 0
self._file_path: Path | None = None
self.results = SimulationResults()
self._connect_actions()
self._populate_view_menu()
self.splitDockWidget(
self.ui.statusDock, self.ui.logDock, Qt.Orientation.Vertical
)
self.ui.statusDock.setFixedHeight(self.ui.statusDock.sizeHint().height())
self.progressReceived.connect(self._show_progress)
self.messageReceived.connect(self._show_message)
self.simulationFinished.connect(self._show_finished)
self.simulationFailed.connect(self._show_error)
def _connect_actions(self) -> None:
self.ui.actionOpen.triggered.connect(self.open_results)
self.ui.actionSave.triggered.connect(self.save_results)
self.ui.actionSaveAs.triggered.connect(self.save_results_as)
self.ui.actionClear.triggered.connect(self.clear)
self.ui.actionExit.triggered.connect(self.close)
self.ui.actionAbout.triggered.connect(self.show_about)
self.ui.actionAboutQt.triggered.connect(
lambda: QMessageBox.aboutQt(self, "About Qt Framework")
)
self.ui.actionToggleResults.toggled.connect(self.ui.centralWidget.setVisible)
def _populate_view_menu(self) -> None:
self.ui.menuPanels.addAction(self.ui.actionToggleResults)
for panel in (self.ui.statusDock, self.ui.logDock):
self.ui.menuPanels.addAction(panel.toggleViewAction())
self.ui.menuToolbars.addAction(self.ui.fileToolbar.toggleViewAction())
def begin_run(self, model_name: str = "") -> tuple:
"""Reset the viewer and return callbacks bound to this run."""
self.clear(model_name=model_name)
generation = self._run_generation
self._running = True
self.ui.statusLabel.setText("Preparing simulation…")
return (
lambda progress: self._report_progress(generation, progress),
lambda message: self._report_message(generation, message),
lambda result: self._report_finished(generation, result),
lambda error: self._report_error(generation, error),
)
def set_model_name(self, model_name: str | None) -> None:
if model_name:
self.results.model_name = model_name
self._update_title()
def clear(self, checked: bool = False, *, model_name: str = "") -> None:
"""Discard the displayed run and prepare an empty results document."""
del checked
self._run_generation += 1
self._running = False
self._file_path = None
self.results = SimulationResults(model_name=model_name)
self.ui.statusLabel.setText("No simulation has been run yet.")
self.ui.progressBar.setValue(0)
self.ui.timeLabel.setText("Time: 0 s")
self.ui.messageList.clear()
self.clear_result_views()
self._update_title()
def clear_result_views(self) -> None:
"""Clear custom plots before a run or loaded document is displayed.
Future graph widgets should be placed in the Designer-owned
``resultsLayout`` and reset here.
"""
self.ui.resultsPlaceholder.setText(
"Simulation graphs and result controls can be added here."
)
def load_result_views(self) -> None:
"""Populate custom plots from ``self.results.data`` and traces.
This is the intended integration point for a future plotting widget.
"""
data_count = len(self.results.data)
trace_count = len(self.results.traces)
if data_count or trace_count:
sample_count = len(next(iter(self.results.data.values()), []))
self.ui.resultsPlaceholder.setText(
f"{data_count} data column(s), {sample_count} sample(s), and "
f"{trace_count} configured trace(s) loaded; add graph rendering here."
)
def open_results(self) -> None:
file_name, _selected_filter = QFileDialog.getOpenFileName(
self,
"Open Simulation Results",
"",
"BEdit Binary Simulation Results (*.ber);;JSON Simulation Results (*.json)",
)
if not file_name:
return
try:
results = load_simulation_results(file_name)
except ValueError as error:
QMessageBox.warning(self, "Cannot Open Simulation Results", str(error))
return
self._run_generation += 1
self._running = False
self._file_path = Path(file_name)
self.results = results
self._display_results()
def save_results(self) -> None:
file_path = self._file_path
if file_path is None:
self.save_results_as()
return
self._save_results_to(file_path)
def save_results_as(self) -> None:
default_name = f"{_safe_file_stem(self.results.model_name)}-results.ber"
file_name, selected_filter = QFileDialog.getSaveFileName(
self,
"Save Simulation Results As",
default_name,
"BEdit Binary Simulation Results (*.ber);;JSON Simulation Results (*.json)",
)
if not file_name:
return
file_path = Path(file_name)
if file_path.suffix.lower() not in {".ber", ".json"}:
suffix = ".json" if selected_filter.startswith("JSON") else ".ber"
file_path = file_path.with_suffix(suffix)
self._save_results_to(file_path)
def _save_results_to(self, file_path: Path) -> None:
try:
save_simulation_results(file_path, self.results)
except OSError as error:
QMessageBox.warning(self, "Cannot Save Simulation Results", str(error))
return
self._file_path = file_path
self._update_title()
def _display_results(self) -> None:
status = self.results.status
self.ui.statusLabel.setText(str(status.get("phase", "Loaded results")))
self.ui.progressBar.setValue(int(status.get("progress", 0)))
self.ui.timeLabel.setText(f"Time: {float(status.get('time', 0)):g} s")
self.ui.messageList.clear()
for message in self.results.messages:
prefix = message.get("stream") or message.get("type") or "OpenModelica"
self.ui.messageList.addItem(f"{prefix}: {message.get('text', '')}")
self.clear_result_views()
self.load_result_views()
self._update_title()
def _update_title(self) -> None:
name = self.results.model_name or "Simulation"
self.setWindowTitle(f"{name} — Simulation")
def _report_progress(self, generation: int, progress: SimulationProgress) -> None:
if generation == self._run_generation:
self.progressReceived.emit(progress)
def _report_message(self, generation: int, message: SimulationMessage) -> None:
if generation == self._run_generation:
self.messageReceived.emit(message)
def _report_finished(self, generation: int, result) -> None:
if generation == self._run_generation:
self.simulationFinished.emit(result)
def _report_error(self, generation: int, error: Exception) -> None:
if generation == self._run_generation:
self.simulationFailed.emit(str(error))
def report_start_error(self, error: Exception) -> None:
self.simulationFailed.emit(str(error))
def _show_progress(self, progress: SimulationProgress) -> None:
self.results.status = {
"phase": progress.phase,
"currentStepSize": progress.current_step_size,
"time": progress.time,
"progress": progress.progress,
}
self.ui.statusLabel.setText(progress.phase or "Running")
self.ui.timeLabel.setText(f"Time: {progress.time:g} s")
self.ui.progressBar.setValue(max(0, min(10000, progress.progress)))
def _show_message(self, message: SimulationMessage) -> None:
self.results.messages.append(
{"stream": message.stream, "type": message.type, "text": message.text}
)
prefix = message.stream or message.type or "OpenModelica"
self.ui.messageList.addItem(f"{prefix}: {message.text}")
self.ui.messageList.scrollToBottom()
def _show_finished(self, result) -> None:
self._running = False
self.results.status.update(phase="Simulation finished", progress=10000)
if isinstance(result, SimulationExecutionResult):
self.results.data = result.data
self.results.metadata["processReturnCode"] = result.return_code
self.results.metadata["sourceResultFile"] = Path(result.result_file).name
else:
self.results.metadata["processResult"] = result
self.ui.progressBar.setValue(10000)
self.ui.statusLabel.setText("Simulation finished")
self.load_result_views()
def _show_error(self, message: str) -> None:
self._running = False
self.results.status["phase"] = "Simulation failed"
self.results.messages.append(
{"stream": "BEdit", "type": "error", "text": message}
)
self.ui.statusLabel.setText("Simulation failed")
self.ui.messageList.addItem(f"Error: {message}")
def show_about(self) -> None:
QMessageBox.about(
self,
"About BEdit Simulation",
"<h3>BEdit Simulation</h3>"
"<p>View live progress and open or save simulation results.</p>",
)
@property
def is_running(self) -> bool:
return self._running
def _safe_file_stem(model_name: str) -> str:
stem = re.sub(r"[^A-Za-z0-9_.-]+", "_", model_name).strip("._")
return stem or "simulation"

View File

@@ -551,7 +551,8 @@
</property>
<addaction name="actionSimulationSettings"/>
<addaction name="actionGraphParameters"/>
<addaction name="actionCompile"/>
<addaction name="actionCompose"/>
<addaction name="actionSimulationWindow"/>
<addaction name="actionRunSimulation"/>
</widget>
<addaction name="menuFile"/>
@@ -644,7 +645,8 @@
</attribute>
<addaction name="actionSimulationSettings"/>
<addaction name="actionGraphParameters"/>
<addaction name="actionCompile"/>
<addaction name="actionCompose"/>
<addaction name="actionSimulationWindow"/>
<addaction name="actionRunSimulation"/>
</widget>
<action name="actionSimulationSettings">
@@ -662,7 +664,7 @@
<action name="actionGraphParameters">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/configure.png</normaloff>:/icons/icons/configure.png</iconset>
<normaloff>:/icons/icons/view-form-table.png</normaloff>:/icons/icons/view-form-table.png</iconset>
</property>
<property name="text">
<string>Graph Parameters</string>
@@ -671,16 +673,16 @@
<string>Edit parameters throughout the active graph</string>
</property>
</action>
<action name="actionCompile">
<action name="actionCompose">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/run-build.png</normaloff>:/icons/icons/run-build.png</iconset>
</property>
<property name="text">
<string>Compile</string>
<string>Compose</string>
</property>
<property name="statusTip">
<string>Compile the active graph for simulation</string>
<string>Compose the active graph as an OpenModelica model</string>
</property>
<property name="shortcut">
<string>F5</string>
@@ -701,6 +703,18 @@
<string>F6</string>
</property>
</action>
<action name="actionSimulationWindow">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/office-chart-line.png</normaloff>:/icons/icons/office-chart-line.png</iconset>
</property>
<property name="text">
<string>Simulation Window</string>
</property>
<property name="statusTip">
<string>Show the simulation results window</string>
</property>
</action>
<action name="actionNew">
<property name="icon">
<iconset resource="../resources/resources.qrc">

View File

@@ -0,0 +1,244 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SimulationWindow</class>
<widget class="QMainWindow" name="SimulationWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>900</width>
<height>650</height>
</rect>
</property>
<property name="windowTitle">
<string>Simulation</string>
</property>
<property name="windowIcon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/office-chart-line.png</normaloff>:/icons/icons/office-chart-line.png</iconset>
</property>
<widget class="QWidget" name="centralWidget">
<layout class="QVBoxLayout" name="resultsLayout">
<item>
<widget class="QLabel" name="resultsPlaceholder">
<property name="text">
<string>Simulation graphs and result controls can be added here.</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menuBar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>900</width>
<height>24</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
<property name="title">
<string>&amp;File</string>
</property>
<addaction name="actionOpen"/>
<addaction name="actionSave"/>
<addaction name="actionSaveAs"/>
<addaction name="separator"/>
<addaction name="actionClear"/>
<addaction name="separator"/>
<addaction name="actionExit"/>
</widget>
<widget class="QMenu" name="menuView">
<property name="title">
<string>&amp;View</string>
</property>
<widget class="QMenu" name="menuPanels">
<property name="title">
<string>&amp;Panels</string>
</property>
</widget>
<widget class="QMenu" name="menuToolbars">
<property name="title">
<string>&amp;Toolbars</string>
</property>
</widget>
<addaction name="menuPanels"/>
<addaction name="menuToolbars"/>
</widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
<string>&amp;Help</string>
</property>
<addaction name="actionAbout"/>
<addaction name="actionAboutQt"/>
</widget>
<addaction name="menuFile"/>
<addaction name="menuView"/>
<addaction name="menuHelp"/>
</widget>
<widget class="QToolBar" name="fileToolbar">
<property name="windowTitle">
<string>File</string>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonStyle::ToolButtonIconOnly</enum>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionOpen"/>
<addaction name="actionSave"/>
<addaction name="actionSaveAs"/>
<addaction name="actionClear"/>
</widget>
<widget class="QDockWidget" name="statusDock">
<property name="windowTitle">
<string>Status</string>
</property>
<attribute name="dockWidgetArea">
<number>8</number>
</attribute>
<widget class="QWidget" name="statusDockContents">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="timeLabel">
<property name="text">
<string>Time: 0 s</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="maximum">
<number>10000</number>
</property>
<property name="value">
<number>0</number>
</property>
<property name="format">
<string>%p%</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="statusLabel">
<property name="text">
<string>No simulation has been run yet.</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<widget class="QDockWidget" name="logDock">
<property name="floating">
<bool>false</bool>
</property>
<property name="features">
<set>QDockWidget::DockWidgetFeature::DockWidgetFloatable|QDockWidget::DockWidgetFeature::DockWidgetMovable</set>
</property>
<property name="windowTitle">
<string>Log</string>
</property>
<attribute name="dockWidgetArea">
<number>8</number>
</attribute>
<widget class="QWidget" name="logDockContents">
<layout class="QVBoxLayout" name="logLayout">
<item>
<widget class="QListWidget" name="messageList">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<action name="actionOpen">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/document-open.png</normaloff>:/icons/icons/document-open.png</iconset>
</property>
<property name="text">
<string>&amp;Open…</string>
</property>
<property name="shortcut">
<string>Ctrl+O</string>
</property>
</action>
<action name="actionSave">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/document-save.png</normaloff>:/icons/icons/document-save.png</iconset>
</property>
<property name="text">
<string>&amp;Save…</string>
</property>
<property name="shortcut">
<string>Ctrl+S</string>
</property>
</action>
<action name="actionClear">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/document-new.png</normaloff>:/icons/icons/document-new.png</iconset>
</property>
<property name="text">
<string>&amp;Clear</string>
</property>
</action>
<action name="actionSaveAs">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/document-save-as.png</normaloff>:/icons/icons/document-save-as.png</iconset>
</property>
<property name="text">
<string>Save &amp;As…</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+S</string>
</property>
</action>
<action name="actionExit">
<property name="text">
<string>E&amp;xit</string>
</property>
<property name="shortcut">
<string>Ctrl+W</string>
</property>
</action>
<action name="actionAbout">
<property name="text">
<string>&amp;About Simulation Window</string>
</property>
</action>
<action name="actionAboutQt">
<property name="text">
<string>About &amp;Qt</string>
</property>
</action>
<action name="actionToggleResults">
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="text">
<string>Results</string>
</property>
</action>
</widget>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -56,7 +56,7 @@
"showName": false
},
"library": {
"showSubtree": false
"showSubtree": true
},
"implementation": {
"kind": "graph",
@@ -377,7 +377,7 @@
"implementation": {
"kind": "text",
"source": {
"equations": "y = v+time;"
"equations": "y = v+sin(time);"
}
}
},
@@ -652,8 +652,8 @@
"enabled": false,
"startTime": 0.0,
"stopTime": 10.0,
"intervalMode": "numberOfIntervals",
"numberOfIntervals": 5000,
"intervalMode": "intervalTime",
"numberOfIntervals": 50000,
"intervalTime": 0.002
}
}

44
m_Test.mo Normal file
View File

@@ -0,0 +1,44 @@
model m_Test
model m_Constant0
output Real y;
parameter Real v = 2;
equation
y = v;
end m_Constant0;
model m_gain0
input Real u;
output Real y;
parameter Real k = 2.5;
equation
y = k*u;
end m_gain0;
model m_const_and_time
output Real y;
parameter Real v = 4.8;
equation
y = v+sin(time);
end m_const_and_time;
model m_gain1
input Real u;
output Real y;
parameter Real k = -5;
equation
y = k*u;
end m_gain1;
model m_add0
input Real u[2];
output Real y;
equation
y = sum(u[i] for i in 1:2 );
end m_add0;
m_Constant0 Constant0;
m_gain0 gain0;
m_const_and_time const_and_time;
m_gain1 gain1;
m_add0 add0;
equation
gain0.u = Constant0.y;
gain1.u = const_and_time.y;
add0.u[1] = gain0.y;
add0.u[2] = gain1.y;
end m_Test;