Saving sim results

This commit is contained in:
2026-07-21 14:40:03 +02:00
parent ddc004dee5
commit edd7bb98f2
21 changed files with 987 additions and 226 deletions

View File

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

View File

@@ -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 - Avoid the QSettings group name `general`; Qt treats `General` specially in INI
files. Autosave keys live under `autosave/`. files. Autosave keys live under `autosave/`.
- User-visible document edits should participate in undo/redo. - 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 - The text-definition editor uses OpenModelica highlighting and completion from
`src/bedit/data/syntax/openmodelica.json`. Keep keywords, types, built-ins, and `src/bedit/data/syntax/openmodelica.json`. Keep keywords, types, built-ins, and
named BEdit `$name$` macro completions editable there; arbitrary `$name$` named BEdit `$name$` macro completions editable there; arbitrary `$name$`
@@ -126,9 +131,14 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
progress, log, and future result views. Extend graph presentation through its progress, log, and future result views. Extend graph presentation through its
Designer-owned `resultsLayout` and the Designer-owned `resultsLayout` and the
`clear_result_views()`/`load_result_views()` hooks. `clear_result_views()`/`load_result_views()` hooks.
- Standalone simulation-result JSON is modeled in `core/simulation/results.py`. - Standalone simulation results are modeled in `core/simulation/results.py`.
Its versioned schema retains model status, messages, metadata, and plottable Its versioned schema retains model status, messages, metadata, and plottable
traces so the simulation window can open results without an active document. traces so the simulation window can open results without an active document.
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 - The optional OpenModelica executable is persisted as
`simulation/openModelicaPath`. The GUI passes it into `Simulation`; core must `simulation/openModelicaPath`. The GUI passes it into `Simulation`; core must
not read `QSettings`. An empty path uses OMPython/PATH discovery, while an not read `QSettings`. An empty path uses OMPython/PATH discovery, while an

View File

@@ -1,27 +0,0 @@
{
"format": "bedit-simulation-results",
"version": 1,
"modelName": "m_Test",
"status": {
"phase": "Simulation finished",
"currentStepSize": 0.0,
"time": 10.0,
"progress": 10000
},
"messages": [
{
"stream": "LOG_SUCCESS",
"type": "info",
"text": "The initialization finished successfully without homotopy method."
},
{
"stream": "LOG_SUCCESS",
"type": "info",
"text": "The simulation finished successfully."
}
],
"traces": [],
"metadata": {
"processResult": 0
}
}

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> <RCC>
<qresource prefix="icons"> <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/run-build.png</file>
<file>icons/preferences-system.png</file> <file>icons/preferences-system.png</file>
<file>icons/draw-triangle.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\ \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\ \xc7\xcc\x7f5p\xf2g\x94\xf7\x1f\xdf\x9a\xd2\x93\xfbC\
\xb7\xa7\x00\x00\x00\x00IEND\xaeB`\x82\ \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<\ \x00\x00\x04<\
\x89\ \x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -2396,6 +2453,35 @@ f\xa2\xd1\xa5\x5c\x22\xb4\x91SZ\xd5u\xd7\x0a\xd2]\
|\x01\x85\x09\x800\x7fss\xd3{\xf6\x7fABG\ |\x01\x85\x09\x800\x7fss\xd3{\xf6\x7fABG\
Y\x01\x05l*\xfc\x00!\x00\x12\xf1%U\xb6\x0e\x00\ Y\x01\x05l*\xfc\x00!\x00\x12\xf1%U\xb6\x0e\x00\
\x00\x00\x00IEND\xaeB`\x82\ \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\ \x00\x00\x02\x92\
\x89\ \x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -2632,10 +2718,20 @@ qt_resource_name = b"\
\x00d\ \x00d\
\x00o\x00c\x00u\x00m\x00e\x00n\x00t\x00-\x00s\x00a\x00v\x00e\x00.\x00p\x00n\x00g\ \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\ \x00\x10\
\x03\xe6\xd3g\ \x03\xe6\xd3g\
\x00d\ \x00d\
\x00r\x00a\x00w\x00-\x00e\x00l\x00l\x00i\x00p\x00s\x00e\x00.\x00p\x00n\x00g\ \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\ \x00\x12\
\x09\xb3>\xc7\ \x09\xb3>\xc7\
\x00d\ \x00d\
@@ -2653,7 +2749,7 @@ qt_resource_struct = b"\
\x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\
\x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\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\x00\x00\x00\x00\x00\x00\
\x00\x00\x01\xfe\x00\x00\x00\x00\x00\x01\x00\x00N\xe8\ \x00\x00\x01\xfe\x00\x00\x00\x00\x00\x01\x00\x00N\xe8\
\x00\x00\x01\x9f{C\xf1'\ \x00\x00\x01\x9f{C\xf1'\
@@ -2661,11 +2757,13 @@ qt_resource_struct = b"\
\x00\x00\x01\x9f\x7f\xa8\xa8)\ \x00\x00\x01\x9f\x7f\xa8\xa8)\
\x00\x00\x02\xec\x00\x00\x00\x00\x00\x01\x00\x00tV\ \x00\x00\x02\xec\x00\x00\x00\x00\x00\x01\x00\x00tV\
\x00\x00\x01\x9f{0\xc99\ \x00\x00\x01\x9f{0\xc99\
\x00\x00\x03\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x8d\xaa\
\x00\x00\x01\x9f\x84\x8f\x00\xb9\
\x00\x00\x01\xc4\x00\x00\x00\x00\x00\x01\x00\x00D2\ \x00\x00\x01\xc4\x00\x00\x00\x00\x00\x01\x00\x00D2\
\x00\x00\x01\x9f\x7f&\x83\xcd\ \x00\x00\x01\x9f\x7f&\x83\xcd\
\x00\x00\x01\xa4\x00\x00\x00\x00\x00\x01\x00\x00<J\ \x00\x00\x01\xa4\x00\x00\x00\x00\x00\x01\x00\x00<J\
\x00\x00\x01\x9f{C\xf1\x18\ \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\x01\x9f\x7fY\xceg\
\x00\x00\x02\xa2\x00\x00\x00\x00\x00\x01\x00\x00f\xc8\ \x00\x00\x02\xa2\x00\x00\x00\x00\x00\x01\x00\x00f\xc8\
\x00\x00\x01\x9f\x7fV\xd5\xc0\ \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\x9f\x7fY\xce\x82\
\x00\x00\x01\xe0\x00\x00\x00\x00\x00\x01\x00\x00Kh\ \x00\x00\x01\xe0\x00\x00\x00\x00\x00\x01\x00\x00Kh\
\x00\x00\x01\x9f{C\xf1.\ \x00\x00\x01\x9f{C\xf1.\
\x00\x00\x03\xfa\x00\x00\x00\x00\x00\x01\x00\x00\x95[\
\x00\x00\x01\x9f\x84\x8f\xa6\x10\
\x00\x00\x01\x84\x00\x00\x00\x00\x00\x01\x00\x006\x9c\ \x00\x00\x01\x84\x00\x00\x00\x00\x00\x01\x00\x006\x9c\
\x00\x00\x01\x9f{{\xa5\xd5\ \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\x01\x9f\x7f&\x83\x10\
\x00\x00\x00\xfe\x00\x00\x00\x00\x00\x01\x00\x00(e\ \x00\x00\x00\xfe\x00\x00\x00\x00\x00\x01\x00\x00(e\
\x00\x00\x01\x9f\x7f&\x83~\ \x00\x00\x01\x9f\x7f&\x83~\
@@ -2703,7 +2803,7 @@ qt_resource_struct = b"\
\x00\x00\x01\x9f{C\xf1=\ \x00\x00\x01\x9f{C\xf1=\
\x00\x00\x006\x00\x00\x00\x00\x00\x01\x00\x00\x05\x86\ \x00\x00\x006\x00\x00\x00\x00\x00\x01\x00\x00\x05\x86\
\x00\x00\x01\x9f\x7f&\x83\x99\ \x00\x00\x01\x9f\x7f&\x83\x99\
\x00\x00\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\x01\x9f{{\xa5\xe3\
\x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x00\x0c\xc1\ \x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x00\x0c\xc1\
\x00\x00\x01\x9f\x7f\xac\xf2\xc6\ \x00\x00\x01\x9f\x7f\xac\xf2\xc6\

View File

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

View File

@@ -14,6 +14,10 @@ from threading import Event, Lock, Thread, current_thread
from typing import Any from typing import Any
from bedit.core.application_log import get_logger from bedit.core.application_log import get_logger
from bedit.core.simulation.results import (
SimulationExecutionResult,
load_openmodelica_csv,
)
log = get_logger(__name__) log = get_logger(__name__)
@@ -263,7 +267,7 @@ def _run_model_with_tcp(
temp_dir: Path, temp_dir: Path,
progress_callback: Callable[[SimulationProgress], None] | None, progress_callback: Callable[[SimulationProgress], None] | None,
message_callback: Callable[[SimulationMessage], None] | None, message_callback: Callable[[SimulationMessage], None] | None,
) -> int: ) -> SimulationExecutionResult:
executable_path = Path(executable) executable_path = Path(executable)
executable_command = executable executable_command = executable
if not executable_path.is_absolute() and executable_path.parent == Path("."): if not executable_path.is_absolute() and executable_path.parent == Path("."):
@@ -324,7 +328,20 @@ def _run_model_with_tcp(
raise RuntimeError( raise RuntimeError(
f"Simulation process exited with status {return_code}{detail}" f"Simulation process exited with status {return_code}{detail}"
) )
return int(return_code) 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( def _accept_simulation_connection(

View File

@@ -1,8 +1,12 @@
import csv
import json import json
import zlib
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
import msgpack
RESULTS_FORMAT = "bedit-simulation-results" RESULTS_FORMAT = "bedit-simulation-results"
RESULTS_VERSION = 1 RESULTS_VERSION = 1
@@ -28,6 +32,7 @@ class SimulationResults:
model_name: str = "" model_name: str = ""
status: dict[str, Any] = field(default_factory=dict) status: dict[str, Any] = field(default_factory=dict)
messages: list[dict[str, str]] = field(default_factory=list) messages: list[dict[str, str]] = field(default_factory=list)
data: dict[str, list[float]] = field(default_factory=dict)
traces: list[SimulationTrace] = field(default_factory=list) traces: list[SimulationTrace] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict) metadata: dict[str, Any] = field(default_factory=dict)
@@ -38,6 +43,7 @@ class SimulationResults:
"modelName": self.model_name, "modelName": self.model_name,
"status": dict(self.status), "status": dict(self.status),
"messages": [dict(message) for message in self.messages], "messages": [dict(message) for message in self.messages],
"data": {name: list(values) for name, values in self.data.items()},
"traces": [asdict(trace) for trace in self.traces], "traces": [asdict(trace) for trace in self.traces],
"metadata": dict(self.metadata), "metadata": dict(self.metadata),
} }
@@ -54,6 +60,10 @@ class SimulationResults:
model_name=str(data.get("modelName", "")), model_name=str(data.get("modelName", "")),
status=dict(data.get("status", {})), status=dict(data.get("status", {})),
messages=[dict(message) for message in data.get("messages", [])], 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, traces=traces,
metadata=dict(data.get("metadata", {})), metadata=dict(data.get("metadata", {})),
) )
@@ -61,18 +71,129 @@ class SimulationResults:
raise ValueError("Malformed simulation-results data") from 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: def save_simulation_results(path: str | Path, results: SimulationResults) -> None:
Path(path).write_text( SimulationResultsSerializer.save(results, path)
json.dumps(results.to_dict(), indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
def load_simulation_results(path: str | Path) -> SimulationResults: def load_simulation_results(path: str | Path) -> SimulationResults:
try: return SimulationResultsSerializer.load(path)
data = json.loads(Path(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)

View File

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

View File

@@ -963,6 +963,68 @@ class DocumentController(QObject):
self.undo_stack.push(PasteSelectionCommand(self, owner.id, blocks, connections)) self.undo_stack.push(PasteSelectionCommand(self, owner.id, blocks, connections))
return list(blocks) 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): def _graph_for(self, owner_id: str):
if self.document is None: if self.document is None:
raise ValueError("There is no open document") 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\ \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\ \xc7\xcc\x7f5p\xf2g\x94\xf7\x1f\xdf\x9a\xd2\x93\xfbC\
\xb7\xa7\x00\x00\x00\x00IEND\xaeB`\x82\ \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<\ \x00\x00\x04<\
\x89\ \x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -2396,6 +2453,35 @@ f\xa2\xd1\xa5\x5c\x22\xb4\x91SZ\xd5u\xd7\x0a\xd2]\
|\x01\x85\x09\x800\x7fss\xd3{\xf6\x7fABG\ |\x01\x85\x09\x800\x7fss\xd3{\xf6\x7fABG\
Y\x01\x05l*\xfc\x00!\x00\x12\xf1%U\xb6\x0e\x00\ Y\x01\x05l*\xfc\x00!\x00\x12\xf1%U\xb6\x0e\x00\
\x00\x00\x00IEND\xaeB`\x82\ \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\ \x00\x00\x02\x92\
\x89\ \x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -2632,10 +2718,20 @@ qt_resource_name = b"\
\x00d\ \x00d\
\x00o\x00c\x00u\x00m\x00e\x00n\x00t\x00-\x00s\x00a\x00v\x00e\x00.\x00p\x00n\x00g\ \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\ \x00\x10\
\x03\xe6\xd3g\ \x03\xe6\xd3g\
\x00d\ \x00d\
\x00r\x00a\x00w\x00-\x00e\x00l\x00l\x00i\x00p\x00s\x00e\x00.\x00p\x00n\x00g\ \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\ \x00\x12\
\x09\xb3>\xc7\ \x09\xb3>\xc7\
\x00d\ \x00d\
@@ -2653,7 +2749,7 @@ qt_resource_struct = b"\
\x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\
\x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\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\x00\x00\x00\x00\x00\x00\
\x00\x00\x01\xfe\x00\x00\x00\x00\x00\x01\x00\x00N\xe8\ \x00\x00\x01\xfe\x00\x00\x00\x00\x00\x01\x00\x00N\xe8\
\x00\x00\x01\x9f{C\xf1'\ \x00\x00\x01\x9f{C\xf1'\
@@ -2661,11 +2757,13 @@ qt_resource_struct = b"\
\x00\x00\x01\x9f\x7f\xa8\xa8)\ \x00\x00\x01\x9f\x7f\xa8\xa8)\
\x00\x00\x02\xec\x00\x00\x00\x00\x00\x01\x00\x00tV\ \x00\x00\x02\xec\x00\x00\x00\x00\x00\x01\x00\x00tV\
\x00\x00\x01\x9f{0\xc99\ \x00\x00\x01\x9f{0\xc99\
\x00\x00\x03\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x8d\xaa\
\x00\x00\x01\x9f\x84\x8f\x00\xb9\
\x00\x00\x01\xc4\x00\x00\x00\x00\x00\x01\x00\x00D2\ \x00\x00\x01\xc4\x00\x00\x00\x00\x00\x01\x00\x00D2\
\x00\x00\x01\x9f\x7f&\x83\xcd\ \x00\x00\x01\x9f\x7f&\x83\xcd\
\x00\x00\x01\xa4\x00\x00\x00\x00\x00\x01\x00\x00<J\ \x00\x00\x01\xa4\x00\x00\x00\x00\x00\x01\x00\x00<J\
\x00\x00\x01\x9f{C\xf1\x18\ \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\x01\x9f\x7fY\xceg\
\x00\x00\x02\xa2\x00\x00\x00\x00\x00\x01\x00\x00f\xc8\ \x00\x00\x02\xa2\x00\x00\x00\x00\x00\x01\x00\x00f\xc8\
\x00\x00\x01\x9f\x7fV\xd5\xc0\ \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\x9f\x7fY\xce\x82\
\x00\x00\x01\xe0\x00\x00\x00\x00\x00\x01\x00\x00Kh\ \x00\x00\x01\xe0\x00\x00\x00\x00\x00\x01\x00\x00Kh\
\x00\x00\x01\x9f{C\xf1.\ \x00\x00\x01\x9f{C\xf1.\
\x00\x00\x03\xfa\x00\x00\x00\x00\x00\x01\x00\x00\x95[\
\x00\x00\x01\x9f\x84\x8f\xa6\x10\
\x00\x00\x01\x84\x00\x00\x00\x00\x00\x01\x00\x006\x9c\ \x00\x00\x01\x84\x00\x00\x00\x00\x00\x01\x00\x006\x9c\
\x00\x00\x01\x9f{{\xa5\xd5\ \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\x01\x9f\x7f&\x83\x10\
\x00\x00\x00\xfe\x00\x00\x00\x00\x00\x01\x00\x00(e\ \x00\x00\x00\xfe\x00\x00\x00\x00\x00\x01\x00\x00(e\
\x00\x00\x01\x9f\x7f&\x83~\ \x00\x00\x01\x9f\x7f&\x83~\
@@ -2703,7 +2803,7 @@ qt_resource_struct = b"\
\x00\x00\x01\x9f{C\xf1=\ \x00\x00\x01\x9f{C\xf1=\
\x00\x00\x006\x00\x00\x00\x00\x00\x01\x00\x00\x05\x86\ \x00\x00\x006\x00\x00\x00\x00\x00\x01\x00\x00\x05\x86\
\x00\x00\x01\x9f\x7f&\x83\x99\ \x00\x00\x01\x9f\x7f&\x83\x99\
\x00\x00\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\x01\x9f{{\xa5\xe3\
\x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x00\x0c\xc1\ \x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x00\x0c\xc1\
\x00\x00\x01\x9f\x7f\xac\xf2\xc6\ \x00\x00\x01\x9f\x7f\xac\xf2\xc6\

View File

@@ -39,7 +39,7 @@ class Ui_MainWindow(object):
self.actionGraphParameters = QAction(MainWindow) self.actionGraphParameters = QAction(MainWindow)
self.actionGraphParameters.setObjectName(u"actionGraphParameters") self.actionGraphParameters.setObjectName(u"actionGraphParameters")
icon1 = QIcon() 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.actionGraphParameters.setIcon(icon1)
self.actionCompose = QAction(MainWindow) self.actionCompose = QAction(MainWindow)
self.actionCompose.setObjectName(u"actionCompose") self.actionCompose.setObjectName(u"actionCompose")
@@ -53,80 +53,82 @@ class Ui_MainWindow(object):
self.actionRunSimulation.setIcon(icon3) self.actionRunSimulation.setIcon(icon3)
self.actionSimulationWindow = QAction(MainWindow) self.actionSimulationWindow = QAction(MainWindow)
self.actionSimulationWindow.setObjectName(u"actionSimulationWindow") self.actionSimulationWindow.setObjectName(u"actionSimulationWindow")
self.actionSimulationWindow.setIcon(icon1) 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 = QAction(MainWindow)
self.actionNew.setObjectName(u"actionNew") self.actionNew.setObjectName(u"actionNew")
icon4 = QIcon() icon5 = QIcon()
icon4.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon5.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionNew.setIcon(icon4) self.actionNew.setIcon(icon5)
self.actionRotateClockwise = QAction(MainWindow) self.actionRotateClockwise = QAction(MainWindow)
self.actionRotateClockwise.setObjectName(u"actionRotateClockwise") self.actionRotateClockwise.setObjectName(u"actionRotateClockwise")
icon5 = QIcon() icon6 = QIcon()
icon5.addFile(u":/icons/icons/transform-rotate.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon6.addFile(u":/icons/icons/transform-rotate.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRotateClockwise.setIcon(icon5) self.actionRotateClockwise.setIcon(icon6)
self.actionZoomIn = QAction(MainWindow) self.actionZoomIn = QAction(MainWindow)
self.actionZoomIn.setObjectName(u"actionZoomIn") self.actionZoomIn.setObjectName(u"actionZoomIn")
icon6 = QIcon() icon7 = QIcon()
icon6.addFile(u":/icons/icons/zoom-in.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon7.addFile(u":/icons/icons/zoom-in.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionZoomIn.setIcon(icon6) self.actionZoomIn.setIcon(icon7)
self.actionZoomOut = QAction(MainWindow) self.actionZoomOut = QAction(MainWindow)
self.actionZoomOut.setObjectName(u"actionZoomOut") self.actionZoomOut.setObjectName(u"actionZoomOut")
icon7 = QIcon() icon8 = QIcon()
icon7.addFile(u":/icons/icons/zoom-out.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon8.addFile(u":/icons/icons/zoom-out.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionZoomOut.setIcon(icon7) self.actionZoomOut.setIcon(icon8)
self.actionCenterView = QAction(MainWindow) self.actionCenterView = QAction(MainWindow)
self.actionCenterView.setObjectName(u"actionCenterView") self.actionCenterView.setObjectName(u"actionCenterView")
icon8 = QIcon() icon9 = QIcon()
icon8.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon9.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCenterView.setIcon(icon8) self.actionCenterView.setIcon(icon9)
self.actionOpen = QAction(MainWindow) self.actionOpen = QAction(MainWindow)
self.actionOpen.setObjectName(u"actionOpen") self.actionOpen.setObjectName(u"actionOpen")
icon9 = QIcon() icon10 = QIcon()
icon9.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon10.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionOpen.setIcon(icon9) self.actionOpen.setIcon(icon10)
self.actionReloadLibraries = QAction(MainWindow) self.actionReloadLibraries = QAction(MainWindow)
self.actionReloadLibraries.setObjectName(u"actionReloadLibraries") self.actionReloadLibraries.setObjectName(u"actionReloadLibraries")
self.actionReloadSimulation = QAction(MainWindow) self.actionReloadSimulation = QAction(MainWindow)
self.actionReloadSimulation.setObjectName(u"actionReloadSimulation") self.actionReloadSimulation.setObjectName(u"actionReloadSimulation")
self.actionSave = QAction(MainWindow) self.actionSave = QAction(MainWindow)
self.actionSave.setObjectName(u"actionSave") self.actionSave.setObjectName(u"actionSave")
icon10 = QIcon() icon11 = QIcon()
icon10.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon11.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSave.setIcon(icon10) self.actionSave.setIcon(icon11)
self.actionSaveAs = QAction(MainWindow) self.actionSaveAs = QAction(MainWindow)
self.actionSaveAs.setObjectName(u"actionSaveAs") self.actionSaveAs.setObjectName(u"actionSaveAs")
icon11 = QIcon() icon12 = QIcon()
icon11.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon12.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSaveAs.setIcon(icon11) self.actionSaveAs.setIcon(icon12)
self.actionExit = QAction(MainWindow) self.actionExit = QAction(MainWindow)
self.actionExit.setObjectName(u"actionExit") self.actionExit.setObjectName(u"actionExit")
self.actionClose = QAction(MainWindow) self.actionClose = QAction(MainWindow)
self.actionClose.setObjectName(u"actionClose") self.actionClose.setObjectName(u"actionClose")
self.actionUndo = QAction(MainWindow) self.actionUndo = QAction(MainWindow)
self.actionUndo.setObjectName(u"actionUndo") self.actionUndo.setObjectName(u"actionUndo")
icon12 = QIcon() icon13 = QIcon()
icon12.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon13.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionUndo.setIcon(icon12) self.actionUndo.setIcon(icon13)
self.actionRedo = QAction(MainWindow) self.actionRedo = QAction(MainWindow)
self.actionRedo.setObjectName(u"actionRedo") self.actionRedo.setObjectName(u"actionRedo")
icon13 = QIcon() icon14 = QIcon()
icon13.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon14.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRedo.setIcon(icon13) self.actionRedo.setIcon(icon14)
self.actionCut = QAction(MainWindow) self.actionCut = QAction(MainWindow)
self.actionCut.setObjectName(u"actionCut") self.actionCut.setObjectName(u"actionCut")
icon14 = QIcon() icon15 = QIcon()
icon14.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon15.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCut.setIcon(icon14) self.actionCut.setIcon(icon15)
self.actionCopy = QAction(MainWindow) self.actionCopy = QAction(MainWindow)
self.actionCopy.setObjectName(u"actionCopy") self.actionCopy.setObjectName(u"actionCopy")
icon15 = QIcon() icon16 = QIcon()
icon15.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon16.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCopy.setIcon(icon15) self.actionCopy.setIcon(icon16)
self.actionPaste = QAction(MainWindow) self.actionPaste = QAction(MainWindow)
self.actionPaste.setObjectName(u"actionPaste") self.actionPaste.setObjectName(u"actionPaste")
icon16 = QIcon() icon17 = QIcon()
icon16.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon17.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionPaste.setIcon(icon16) self.actionPaste.setIcon(icon17)
self.actionSelectAll = QAction(MainWindow) self.actionSelectAll = QAction(MainWindow)
self.actionSelectAll.setObjectName(u"actionSelectAll") self.actionSelectAll.setObjectName(u"actionSelectAll")
self.actionDelete = QAction(MainWindow) self.actionDelete = QAction(MainWindow)
@@ -216,18 +218,18 @@ class Ui_MainWindow(object):
self.workspaceHeaderLayout.setContentsMargins(6, 2, 6, 2) self.workspaceHeaderLayout.setContentsMargins(6, 2, 6, 2)
self.navigateUpButton = QToolButton(self.workspaceHeader) self.navigateUpButton = QToolButton(self.workspaceHeader)
self.navigateUpButton.setObjectName(u"navigateUpButton") self.navigateUpButton.setObjectName(u"navigateUpButton")
icon17 = QIcon() icon18 = QIcon()
icon17.addFile(u":/icons/icons/arrow-up.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon18.addFile(u":/icons/icons/arrow-up.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.navigateUpButton.setIcon(icon17) self.navigateUpButton.setIcon(icon18)
self.workspaceHeaderLayout.addWidget(self.navigateUpButton) self.workspaceHeaderLayout.addWidget(self.navigateUpButton)
self.navigateDownButton = QToolButton(self.workspaceHeader) self.navigateDownButton = QToolButton(self.workspaceHeader)
self.navigateDownButton.setObjectName(u"navigateDownButton") self.navigateDownButton.setObjectName(u"navigateDownButton")
self.navigateDownButton.setEnabled(False) self.navigateDownButton.setEnabled(False)
icon18 = QIcon() icon19 = QIcon()
icon18.addFile(u":/icons/icons/arrow-down.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon19.addFile(u":/icons/icons/arrow-down.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.navigateDownButton.setIcon(icon18) self.navigateDownButton.setIcon(icon19)
self.workspaceHeaderLayout.addWidget(self.navigateDownButton) self.workspaceHeaderLayout.addWidget(self.navigateDownButton)
@@ -247,9 +249,9 @@ class Ui_MainWindow(object):
self.pointerToolButton = QToolButton(self.workspaceHeader) self.pointerToolButton = QToolButton(self.workspaceHeader)
self.pointerToolButton.setObjectName(u"pointerToolButton") self.pointerToolButton.setObjectName(u"pointerToolButton")
icon19 = QIcon() icon20 = QIcon()
icon19.addFile(u":/icons/icons/edit-select.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon20.addFile(u":/icons/icons/edit-select.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.pointerToolButton.setIcon(icon19) self.pointerToolButton.setIcon(icon20)
self.pointerToolButton.setCheckable(True) self.pointerToolButton.setCheckable(True)
self.pointerToolButton.setChecked(True) self.pointerToolButton.setChecked(True)
@@ -257,43 +259,43 @@ class Ui_MainWindow(object):
self.connectToolButton = QToolButton(self.workspaceHeader) self.connectToolButton = QToolButton(self.workspaceHeader)
self.connectToolButton.setObjectName(u"connectToolButton") self.connectToolButton.setObjectName(u"connectToolButton")
icon20 = QIcon() icon21 = QIcon()
icon20.addFile(u":/icons/icons/network-connect.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon21.addFile(u":/icons/icons/network-connect.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.connectToolButton.setIcon(icon20) self.connectToolButton.setIcon(icon21)
self.connectToolButton.setCheckable(True) self.connectToolButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.connectToolButton) self.workspaceHeaderLayout.addWidget(self.connectToolButton)
self.boxToolButton = QToolButton(self.workspaceHeader) self.boxToolButton = QToolButton(self.workspaceHeader)
self.boxToolButton.setObjectName(u"boxToolButton") self.boxToolButton.setObjectName(u"boxToolButton")
icon21 = QIcon() icon22 = QIcon()
icon21.addFile(u":/icons/icons/draw-rectangle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon22.addFile(u":/icons/icons/draw-rectangle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.boxToolButton.setIcon(icon21) self.boxToolButton.setIcon(icon22)
self.boxToolButton.setCheckable(True) self.boxToolButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.boxToolButton) self.workspaceHeaderLayout.addWidget(self.boxToolButton)
self.lineToolButton = QToolButton(self.workspaceHeader) self.lineToolButton = QToolButton(self.workspaceHeader)
self.lineToolButton.setObjectName(u"lineToolButton") self.lineToolButton.setObjectName(u"lineToolButton")
icon22 = QIcon() icon23 = QIcon()
icon22.addFile(u":/icons/icons/draw-bezier-curves.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon23.addFile(u":/icons/icons/draw-bezier-curves.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.lineToolButton.setIcon(icon22) self.lineToolButton.setIcon(icon23)
self.lineToolButton.setCheckable(True) self.lineToolButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.lineToolButton) self.workspaceHeaderLayout.addWidget(self.lineToolButton)
self.textToolButton = QToolButton(self.workspaceHeader) self.textToolButton = QToolButton(self.workspaceHeader)
self.textToolButton.setObjectName(u"textToolButton") self.textToolButton.setObjectName(u"textToolButton")
icon23 = QIcon() icon24 = QIcon()
icon23.addFile(u":/icons/icons/draw-text.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon24.addFile(u":/icons/icons/draw-text.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.textToolButton.setIcon(icon23) self.textToolButton.setIcon(icon24)
self.textToolButton.setCheckable(True) self.textToolButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.textToolButton) self.workspaceHeaderLayout.addWidget(self.textToolButton)
self.rotateToolButton = QToolButton(self.workspaceHeader) self.rotateToolButton = QToolButton(self.workspaceHeader)
self.rotateToolButton.setObjectName(u"rotateToolButton") self.rotateToolButton.setObjectName(u"rotateToolButton")
self.rotateToolButton.setIcon(icon5) self.rotateToolButton.setIcon(icon6)
self.workspaceHeaderLayout.addWidget(self.rotateToolButton) self.workspaceHeaderLayout.addWidget(self.rotateToolButton)

View File

@@ -16,10 +16,10 @@ from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
QIcon, QImage, QKeySequence, QLinearGradient, QIcon, QImage, QKeySequence, QLinearGradient,
QPainter, QPalette, QPixmap, QRadialGradient, QPainter, QPalette, QPixmap, QRadialGradient,
QTransform) QTransform)
from PySide6.QtWidgets import (QApplication, QDockWidget, QLabel, QListWidget, from PySide6.QtWidgets import (QApplication, QDockWidget, QHBoxLayout, QLabel,
QListWidgetItem, QMainWindow, QMenu, QMenuBar, QListWidget, QListWidgetItem, QMainWindow, QMenu,
QProgressBar, QSizePolicy, QToolBar, QVBoxLayout, QMenuBar, QProgressBar, QSizePolicy, QToolBar,
QWidget) QVBoxLayout, QWidget)
from . import resources_rc from . import resources_rc
class Ui_SimulationWindow(object): class Ui_SimulationWindow(object):
@@ -27,33 +27,53 @@ class Ui_SimulationWindow(object):
if not SimulationWindow.objectName(): if not SimulationWindow.objectName():
SimulationWindow.setObjectName(u"SimulationWindow") SimulationWindow.setObjectName(u"SimulationWindow")
SimulationWindow.resize(900, 650) 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 = QAction(SimulationWindow)
self.actionOpen.setObjectName(u"actionOpen") self.actionOpen.setObjectName(u"actionOpen")
icon = QIcon() icon1 = QIcon()
icon.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon1.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionOpen.setIcon(icon) self.actionOpen.setIcon(icon1)
self.actionSave = QAction(SimulationWindow) self.actionSave = QAction(SimulationWindow)
self.actionSave.setObjectName(u"actionSave") self.actionSave.setObjectName(u"actionSave")
icon1 = QIcon() icon2 = QIcon()
icon1.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon2.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSave.setIcon(icon1) self.actionSave.setIcon(icon2)
self.actionClear = QAction(SimulationWindow) self.actionClear = QAction(SimulationWindow)
self.actionClear.setObjectName(u"actionClear") self.actionClear.setObjectName(u"actionClear")
icon2 = QIcon() icon3 = QIcon()
icon2.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon3.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionClear.setIcon(icon2) 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 = QAction(SimulationWindow)
self.actionExit.setObjectName(u"actionExit") self.actionExit.setObjectName(u"actionExit")
self.actionAbout = QAction(SimulationWindow) self.actionAbout = QAction(SimulationWindow)
self.actionAbout.setObjectName(u"actionAbout") self.actionAbout.setObjectName(u"actionAbout")
self.actionAboutQt = QAction(SimulationWindow) self.actionAboutQt = QAction(SimulationWindow)
self.actionAboutQt.setObjectName(u"actionAboutQt") 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 = QWidget(SimulationWindow)
self.centralWidget.setObjectName(u"centralWidget") self.centralWidget.setObjectName(u"centralWidget")
self.centralWidget.setMaximumSize(QSize(0, 0)) 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) SimulationWindow.setCentralWidget(self.centralWidget)
self.menuBar = QMenuBar(SimulationWindow) self.menuBar = QMenuBar(SimulationWindow)
self.menuBar.setObjectName(u"menuBar") self.menuBar.setObjectName(u"menuBar")
self.menuBar.setGeometry(QRect(0, 0, 900, 24))
self.menuFile = QMenu(self.menuBar) self.menuFile = QMenu(self.menuBar)
self.menuFile.setObjectName(u"menuFile") self.menuFile.setObjectName(u"menuFile")
self.menuView = QMenu(self.menuBar) self.menuView = QMenu(self.menuBar)
@@ -69,47 +89,35 @@ class Ui_SimulationWindow(object):
self.fileToolbar.setObjectName(u"fileToolbar") self.fileToolbar.setObjectName(u"fileToolbar")
self.fileToolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) self.fileToolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
SimulationWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.fileToolbar) SimulationWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.fileToolbar)
self.resultsDock = QDockWidget(SimulationWindow)
self.resultsDock.setObjectName(u"resultsDock")
self.resultsDockContents = QWidget()
self.resultsDockContents.setObjectName(u"resultsDockContents")
self.resultsLayout = QVBoxLayout(self.resultsDockContents)
self.resultsLayout.setObjectName(u"resultsLayout")
self.resultsPlaceholder = QLabel(self.resultsDockContents)
self.resultsPlaceholder.setObjectName(u"resultsPlaceholder")
self.resultsPlaceholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.resultsLayout.addWidget(self.resultsPlaceholder)
self.resultsDock.setWidget(self.resultsDockContents)
SimulationWindow.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.resultsDock)
self.statusDock = QDockWidget(SimulationWindow) self.statusDock = QDockWidget(SimulationWindow)
self.statusDock.setObjectName(u"statusDock") self.statusDock.setObjectName(u"statusDock")
self.statusDockContents = QWidget() self.statusDockContents = QWidget()
self.statusDockContents.setObjectName(u"statusDockContents") self.statusDockContents.setObjectName(u"statusDockContents")
self.statusLayout = QVBoxLayout(self.statusDockContents) self.horizontalLayout = QHBoxLayout(self.statusDockContents)
self.statusLayout.setObjectName(u"statusLayout") self.horizontalLayout.setObjectName(u"horizontalLayout")
self.statusLabel = QLabel(self.statusDockContents) self.timeLabel = QLabel(self.statusDockContents)
self.statusLabel.setObjectName(u"statusLabel") self.timeLabel.setObjectName(u"timeLabel")
self.statusLayout.addWidget(self.statusLabel) self.horizontalLayout.addWidget(self.timeLabel)
self.progressBar = QProgressBar(self.statusDockContents) self.progressBar = QProgressBar(self.statusDockContents)
self.progressBar.setObjectName(u"progressBar") self.progressBar.setObjectName(u"progressBar")
self.progressBar.setMaximum(10000) self.progressBar.setMaximum(10000)
self.progressBar.setValue(0) self.progressBar.setValue(0)
self.statusLayout.addWidget(self.progressBar) self.horizontalLayout.addWidget(self.progressBar)
self.timeLabel = QLabel(self.statusDockContents) self.statusLabel = QLabel(self.statusDockContents)
self.timeLabel.setObjectName(u"timeLabel") self.statusLabel.setObjectName(u"statusLabel")
self.statusLayout.addWidget(self.timeLabel) self.horizontalLayout.addWidget(self.statusLabel)
self.statusDock.setWidget(self.statusDockContents) self.statusDock.setWidget(self.statusDockContents)
SimulationWindow.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.statusDock) SimulationWindow.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.statusDock)
self.logDock = QDockWidget(SimulationWindow) self.logDock = QDockWidget(SimulationWindow)
self.logDock.setObjectName(u"logDock") self.logDock.setObjectName(u"logDock")
self.logDock.setFloating(False)
self.logDock.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetFloatable|QDockWidget.DockWidgetFeature.DockWidgetMovable)
self.logDockContents = QWidget() self.logDockContents = QWidget()
self.logDockContents.setObjectName(u"logDockContents") self.logDockContents.setObjectName(u"logDockContents")
self.logLayout = QVBoxLayout(self.logDockContents) self.logLayout = QVBoxLayout(self.logDockContents)
@@ -128,6 +136,7 @@ class Ui_SimulationWindow(object):
self.menuBar.addAction(self.menuHelp.menuAction()) self.menuBar.addAction(self.menuHelp.menuAction())
self.menuFile.addAction(self.actionOpen) self.menuFile.addAction(self.actionOpen)
self.menuFile.addAction(self.actionSave) self.menuFile.addAction(self.actionSave)
self.menuFile.addAction(self.actionSaveAs)
self.menuFile.addSeparator() self.menuFile.addSeparator()
self.menuFile.addAction(self.actionClear) self.menuFile.addAction(self.actionClear)
self.menuFile.addSeparator() self.menuFile.addSeparator()
@@ -138,6 +147,7 @@ class Ui_SimulationWindow(object):
self.menuHelp.addAction(self.actionAboutQt) self.menuHelp.addAction(self.actionAboutQt)
self.fileToolbar.addAction(self.actionOpen) self.fileToolbar.addAction(self.actionOpen)
self.fileToolbar.addAction(self.actionSave) self.fileToolbar.addAction(self.actionSave)
self.fileToolbar.addAction(self.actionSaveAs)
self.fileToolbar.addAction(self.actionClear) self.fileToolbar.addAction(self.actionClear)
self.retranslateUi(SimulationWindow) self.retranslateUi(SimulationWindow)
@@ -156,24 +166,28 @@ class Ui_SimulationWindow(object):
self.actionSave.setShortcut(QCoreApplication.translate("SimulationWindow", u"Ctrl+S", None)) self.actionSave.setShortcut(QCoreApplication.translate("SimulationWindow", u"Ctrl+S", None))
#endif // QT_CONFIG(shortcut) #endif // QT_CONFIG(shortcut)
self.actionClear.setText(QCoreApplication.translate("SimulationWindow", u"&Clear", None)) 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)) self.actionExit.setText(QCoreApplication.translate("SimulationWindow", u"E&xit", None))
#if QT_CONFIG(shortcut) #if QT_CONFIG(shortcut)
self.actionExit.setShortcut(QCoreApplication.translate("SimulationWindow", u"Ctrl+W", None)) self.actionExit.setShortcut(QCoreApplication.translate("SimulationWindow", u"Ctrl+W", None))
#endif // QT_CONFIG(shortcut) #endif // QT_CONFIG(shortcut)
self.actionAbout.setText(QCoreApplication.translate("SimulationWindow", u"&About Simulation Window", None)) self.actionAbout.setText(QCoreApplication.translate("SimulationWindow", u"&About Simulation Window", None))
self.actionAboutQt.setText(QCoreApplication.translate("SimulationWindow", u"About &Qt", None)) self.actionAboutQt.setText(QCoreApplication.translate("SimulationWindow", u"About &Qt", None))
self.actionToggleResults.setText(QCoreApplication.translate("SimulationWindow", u"Results", None))
self.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.menuFile.setTitle(QCoreApplication.translate("SimulationWindow", u"&File", None))
self.menuView.setTitle(QCoreApplication.translate("SimulationWindow", u"&View", None)) self.menuView.setTitle(QCoreApplication.translate("SimulationWindow", u"&View", None))
self.menuPanels.setTitle(QCoreApplication.translate("SimulationWindow", u"&Panels", None)) self.menuPanels.setTitle(QCoreApplication.translate("SimulationWindow", u"&Panels", None))
self.menuToolbars.setTitle(QCoreApplication.translate("SimulationWindow", u"&Toolbars", None)) self.menuToolbars.setTitle(QCoreApplication.translate("SimulationWindow", u"&Toolbars", None))
self.menuHelp.setTitle(QCoreApplication.translate("SimulationWindow", u"&Help", None)) self.menuHelp.setTitle(QCoreApplication.translate("SimulationWindow", u"&Help", None))
self.fileToolbar.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"File", None)) self.fileToolbar.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"File", None))
self.resultsDock.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Results", None))
self.resultsPlaceholder.setText(QCoreApplication.translate("SimulationWindow", u"Simulation graphs and result controls can be added here.", None))
self.statusDock.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Status", None)) self.statusDock.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Status", None))
self.statusLabel.setText(QCoreApplication.translate("SimulationWindow", u"No simulation has been run yet.", None))
self.progressBar.setFormat(QCoreApplication.translate("SimulationWindow", u"%p%", None))
self.timeLabel.setText(QCoreApplication.translate("SimulationWindow", u"Time: 0 s", None)) self.timeLabel.setText(QCoreApplication.translate("SimulationWindow", u"Time: 0 s", None))
self.progressBar.setFormat(QCoreApplication.translate("SimulationWindow", u"%p%", None))
self.statusLabel.setText(QCoreApplication.translate("SimulationWindow", u"No simulation has been run yet.", None))
self.logDock.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Log", None)) self.logDock.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Log", None))
# retranslateUi # retranslateUi

View File

@@ -1892,6 +1892,22 @@ class GraphWorkspaceView(QGraphicsView):
if self.controller is None: if self.controller is None:
return return
mime_data = QApplication.clipboard().mimeData() 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): if not mime_data.hasFormat(SELECTION_MIME_TYPE):
return return
try: try:

View File

@@ -1,10 +1,12 @@
import json
from copy import deepcopy from copy import deepcopy
from pathlib import Path 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.QtGui import QCloseEvent
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QButtonGroup, QButtonGroup,
QApplication,
QFileDialog, QFileDialog,
QMainWindow, QMainWindow,
QMenu, QMenu,
@@ -12,7 +14,7 @@ from PySide6.QtWidgets import (
QTabWidget, 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.application_log import get_logger
from bedit.core.serializer import DocumentSerializer from bedit.core.serializer import DocumentSerializer
from bedit.core.simulation import Simulation from bedit.core.simulation import Simulation
@@ -22,12 +24,15 @@ from bedit.gui.dialogs.graph_parameters import GraphParametersDialog
from bedit.gui.dialogs.item_options import ItemOptionsDialog from bedit.gui.dialogs.item_options import ItemOptionsDialog
from bedit.gui.models.library_repository import LibraryRepository from bedit.gui.models.library_repository import LibraryRepository
from bedit.gui.models.library_tree import ( from bedit.gui.models.library_tree import (
COMPONENT_MIME_TYPE,
COMPONENT_ROLE,
COMPONENT_ID_ROLE, COMPONENT_ID_ROLE,
COMPONENT_INSTANCE_ROLE, COMPONENT_INSTANCE_ROLE,
ITEM_KIND_ROLE, ITEM_KIND_ROLE,
DocumentTreeModel, DocumentTreeModel,
LibraryTreeModel, LibraryTreeModel,
) )
from bedit.gui.graphics.workspace import SELECTION_MIME_TYPE
from bedit.gui.dialogs.settings import SettingsDialog from bedit.gui.dialogs.settings import SettingsDialog
from bedit.gui.dialogs.port_options import PortOptionsDialog from bedit.gui.dialogs.port_options import PortOptionsDialog
from bedit.gui.dialogs.parameter_options import ParameterOptionsDialog from bedit.gui.dialogs.parameter_options import ParameterOptionsDialog
@@ -55,7 +60,9 @@ class MainWindow(QMainWindow):
self.log.info("BEdit started") self.log.info("BEdit started")
self.settings = application_settings() self.settings = application_settings()
self._applying_text_definition = False self._applying_text_definition = False
self._simulation_window = SimulationWindow(self) # 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.libraries = LibraryRepository(self)
self.simulation = Simulation( self.simulation = Simulation(
@@ -69,6 +76,7 @@ class MainWindow(QMainWindow):
) )
self.document_tree_model = DocumentTreeModel(self.document_controller, self) self.document_tree_model = DocumentTreeModel(self.document_controller, self)
self._configure_models() self._configure_models()
QApplication.clipboard().dataChanged.connect(self._update_edit_actions)
self._connect_actions() self._connect_actions()
self._populate_view_menu() self._populate_view_menu()
self._restore_window_geometry() self._restore_window_geometry()
@@ -102,6 +110,10 @@ class MainWindow(QMainWindow):
self.ui.documentTreeView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.ui.documentTreeView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.ui.documentTreeView.customContextMenuRequested.connect(self.show_library_context_menu) self.ui.documentTreeView.customContextMenuRequested.connect(self.show_library_context_menu)
self.ui.documentTreeView.doubleClicked.connect(self.activate_tree_component) 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.document_tree_model.rebuilt.connect(self.ui.documentTreeView.expandAll)
self.ui.graphView.set_model(self.document_controller) self.ui.graphView.set_model(self.document_controller)
self.ui.graphView.componentOptionsRequested.connect(self.show_component_options) self.ui.graphView.componentOptionsRequested.connect(self.show_component_options)
@@ -155,9 +167,9 @@ class MainWindow(QMainWindow):
self.ui.actionUndo.triggered.connect(self.document_controller.undo_stack.undo) self.ui.actionUndo.triggered.connect(self.document_controller.undo_stack.undo)
self.ui.actionRedo.triggered.connect(self.document_controller.undo_stack.redo) self.ui.actionRedo.triggered.connect(self.document_controller.undo_stack.redo)
self.ui.actionDelete.triggered.connect(self.ui.graphView.delete_selected) self.ui.actionDelete.triggered.connect(self.ui.graphView.delete_selected)
self.ui.actionCopy.triggered.connect(self.ui.graphView.copy_selection) self.ui.actionCopy.triggered.connect(self.copy_selection)
self.ui.actionCut.triggered.connect(self.ui.graphView.cut_selection) self.ui.actionCut.triggered.connect(self.cut_selection)
self.ui.actionPaste.triggered.connect(self.ui.graphView.paste_selection) self.ui.actionPaste.triggered.connect(self.paste_selection)
self.ui.actionSelectAll.triggered.connect(self.ui.graphView.select_all) self.ui.actionSelectAll.triggered.connect(self.ui.graphView.select_all)
self.ui.actionRotateClockwise.triggered.connect(self.ui.graphView.rotate_selected) self.ui.actionRotateClockwise.triggered.connect(self.ui.graphView.rotate_selected)
self.addAction(self.ui.actionRotateClockwise) self.addAction(self.ui.actionRotateClockwise)
@@ -324,8 +336,24 @@ class MainWindow(QMainWindow):
has_selection = bool( has_selection = bool(
self.ui.graphView.scene() and self.ui.graphView.scene().selectedItems() self.ui.graphView.scene() and self.ui.graphView.scene().selectedItems()
) )
for action in (self.ui.actionCut, self.ui.actionCopy, self.ui.actionDelete): document_component_selected = bool(
action.setEnabled(is_graph and has_selection) 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( self.ui.actionRotateClockwise.setEnabled(
is_graph and self.ui.graphView.has_selected_components() is_graph and self.ui.graphView.has_selected_components()
) )
@@ -333,11 +361,114 @@ class MainWindow(QMainWindow):
is_graph and self.ui.graphView.has_selected_components() is_graph and self.ui.graphView.has_selected_components()
) )
self.ui.actionSelectAll.setEnabled(is_graph) 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( self.ui.navigateDownButton.setEnabled(
is_graph and self.ui.graphView.has_single_selected_component() 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() @Slot()
def navigate_up(self) -> None: def navigate_up(self) -> None:
if self._resolve_source_edits(): if self._resolve_source_edits():
@@ -560,6 +691,8 @@ class MainWindow(QMainWindow):
def show_library_context_menu(self, position) -> None: def show_library_context_menu(self, position) -> None:
tree_view = self.ui.documentTreeView tree_view = self.ui.documentTreeView
index = tree_view.indexAt(position) index = tree_view.indexAt(position)
if index.isValid():
tree_view.setCurrentIndex(index)
kind = index.data(ITEM_KIND_ROLE) kind = index.data(ITEM_KIND_ROLE)
if kind == "current-component": if kind == "current-component":
component_id = index.data(COMPONENT_ID_ROLE) component_id = index.data(COMPONENT_ID_ROLE)
@@ -577,6 +710,11 @@ class MainWindow(QMainWindow):
options_action = menu.addAction("Component Options…") options_action = menu.addAction("Component Options…")
ports_action = menu.addAction("Port Options…") ports_action = menu.addAction("Port Options…")
parameters_action = menu.addAction("Parameter 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") delete_action = menu.addAction("Delete")
selected = menu.exec(tree_view.viewport().mapToGlobal(position)) selected = menu.exec(tree_view.viewport().mapToGlobal(position))
if graph_action is not None and selected is graph_action: if graph_action is not None and selected is graph_action:
@@ -589,6 +727,13 @@ class MainWindow(QMainWindow):
self.show_component_port_options(component_id) self.show_component_port_options(component_id)
elif selected is parameters_action: elif selected is parameters_action:
self.show_component_parameter_options(component_id) 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: elif selected is delete_action:
answer = QMessageBox.question( answer = QMessageBox.question(
self, self,
@@ -604,24 +749,34 @@ class MainWindow(QMainWindow):
menu = QMenu(self) menu = QMenu(self)
graph_action = menu.addAction("New Graph Block") graph_action = menu.addAction("New Graph Block")
text_action = menu.addAction("New Text Block") text_action = menu.addAction("New Text Block")
menu.addSeparator()
paste_action = menu.addAction("Paste")
selected = menu.exec(tree_view.viewport().mapToGlobal(position)) selected = menu.exec(tree_view.viewport().mapToGlobal(position))
if selected is graph_action: if selected is graph_action:
self.document_controller.add_root("graph") self.document_controller.add_root("graph")
elif selected is text_action: elif selected is text_action:
self.document_controller.add_root("text") self.document_controller.add_root("text")
elif selected is paste_action:
self._paste_into_document_tree()
@Slot(object) @Slot(object)
def show_external_library_context_menu(self, position) -> None: def show_external_library_context_menu(self, position) -> None:
tree = self.ui.treeView tree = self.ui.treeView
index = tree.indexAt(position) index = tree.indexAt(position)
if index.isValid():
tree.setCurrentIndex(index)
component = index.data(COMPONENT_INSTANCE_ROLE) component = index.data(COMPONENT_INSTANCE_ROLE)
if not isinstance(component, Component): if not isinstance(component, Component):
return return
menu = QMenu(self) menu = QMenu(self)
copy_action = menu.addAction("Copy")
menu.addSeparator()
ports_action = menu.addAction("Port Options…") ports_action = menu.addAction("Port Options…")
parameters_action = menu.addAction("Parameter Options…") parameters_action = menu.addAction("Parameter Options…")
selected = menu.exec(tree.viewport().mapToGlobal(position)) 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) dialog = PortOptionsDialog(component, self)
if dialog.exec() == dialog.DialogCode.Accepted: if dialog.exec() == dialog.DialogCode.Accepted:
old_inputs, old_outputs = deepcopy(component.inputs), deepcopy(component.outputs) old_inputs, old_outputs = deepcopy(component.inputs), deepcopy(component.outputs)
@@ -773,6 +928,7 @@ class MainWindow(QMainWindow):
event.ignore() event.ignore()
return return
self.settings.setValue("window/geometry", self.saveGeometry()) self.settings.setValue("window/geometry", self.saveGeometry())
self._simulation_window.close()
self.simulation.shutdown() self.simulation.shutdown()
self.log.info("BEdit closed") self.log.info("BEdit closed")
self.application_logger.removeHandler(self.log_handler) self.application_logger.removeHandler(self.log_handler)

View File

@@ -1,11 +1,12 @@
import re import re
from pathlib import Path from pathlib import Path
from PySide6.QtCore import Signal from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import QFileDialog, QMainWindow, QMessageBox from PySide6.QtWidgets import QFileDialog, QMainWindow, QMessageBox
from bedit.core.simulation.openmodelica import SimulationMessage, SimulationProgress from bedit.core.simulation.openmodelica import SimulationMessage, SimulationProgress
from bedit.core.simulation.results import ( from bedit.core.simulation.results import (
SimulationExecutionResult,
SimulationResults, SimulationResults,
load_simulation_results, load_simulation_results,
save_simulation_results, save_simulation_results,
@@ -31,6 +32,10 @@ class SimulationWindow(QMainWindow):
self.results = SimulationResults() self.results = SimulationResults()
self._connect_actions() self._connect_actions()
self._populate_view_menu() 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.progressReceived.connect(self._show_progress)
self.messageReceived.connect(self._show_message) self.messageReceived.connect(self._show_message)
self.simulationFinished.connect(self._show_finished) self.simulationFinished.connect(self._show_finished)
@@ -39,15 +44,18 @@ class SimulationWindow(QMainWindow):
def _connect_actions(self) -> None: def _connect_actions(self) -> None:
self.ui.actionOpen.triggered.connect(self.open_results) self.ui.actionOpen.triggered.connect(self.open_results)
self.ui.actionSave.triggered.connect(self.save_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.actionClear.triggered.connect(self.clear)
self.ui.actionExit.triggered.connect(self.close) self.ui.actionExit.triggered.connect(self.close)
self.ui.actionAbout.triggered.connect(self.show_about) self.ui.actionAbout.triggered.connect(self.show_about)
self.ui.actionAboutQt.triggered.connect( self.ui.actionAboutQt.triggered.connect(
lambda: QMessageBox.aboutQt(self, "About Qt Framework") lambda: QMessageBox.aboutQt(self, "About Qt Framework")
) )
self.ui.actionToggleResults.toggled.connect(self.ui.centralWidget.setVisible)
def _populate_view_menu(self) -> None: def _populate_view_menu(self) -> None:
for panel in (self.ui.statusDock, self.ui.logDock, self.ui.resultsDock): self.ui.menuPanels.addAction(self.ui.actionToggleResults)
for panel in (self.ui.statusDock, self.ui.logDock):
self.ui.menuPanels.addAction(panel.toggleViewAction()) self.ui.menuPanels.addAction(panel.toggleViewAction())
self.ui.menuToolbars.addAction(self.ui.fileToolbar.toggleViewAction()) self.ui.menuToolbars.addAction(self.ui.fileToolbar.toggleViewAction())
@@ -97,15 +105,18 @@ class SimulationWindow(QMainWindow):
) )
def load_result_views(self) -> None: def load_result_views(self) -> None:
"""Populate custom plots from ``self.results.traces``. """Populate custom plots from ``self.results.data`` and traces.
This is the intended integration point for a future plotting widget. This is the intended integration point for a future plotting widget.
""" """
data_count = len(self.results.data)
trace_count = len(self.results.traces) trace_count = len(self.results.traces)
if trace_count: if data_count or trace_count:
sample_count = len(next(iter(self.results.data.values()), []))
self.ui.resultsPlaceholder.setText( self.ui.resultsPlaceholder.setText(
f"{trace_count} trace(s) loaded; add graph rendering here." 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: def open_results(self) -> None:
@@ -113,7 +124,7 @@ class SimulationWindow(QMainWindow):
self, self,
"Open Simulation Results", "Open Simulation Results",
"", "",
"BEdit Simulation Results (*.json);;JSON Files (*.json)", "BEdit Binary Simulation Results (*.ber);;JSON Simulation Results (*.json)",
) )
if not file_name: if not file_name:
return return
@@ -131,18 +142,27 @@ class SimulationWindow(QMainWindow):
def save_results(self) -> None: def save_results(self) -> None:
file_path = self._file_path file_path = self._file_path
if file_path is None: if file_path is None:
default_name = f"{_safe_file_stem(self.results.model_name)}-results.json" self.save_results_as()
file_name, _selected_filter = QFileDialog.getSaveFileName( return
self, self._save_results_to(file_path)
"Save Simulation Results",
default_name, def save_results_as(self) -> None:
"BEdit Simulation Results (*.json);;JSON Files (*.json)", default_name = f"{_safe_file_stem(self.results.model_name)}-results.ber"
) file_name, selected_filter = QFileDialog.getSaveFileName(
if not file_name: self,
return "Save Simulation Results As",
file_path = Path(file_name) default_name,
if file_path.suffix.lower() != ".json": "BEdit Binary Simulation Results (*.ber);;JSON Simulation Results (*.json)",
file_path = file_path.with_suffix(".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: try:
save_simulation_results(file_path, self.results) save_simulation_results(file_path, self.results)
except OSError as error: except OSError as error:
@@ -209,7 +229,12 @@ class SimulationWindow(QMainWindow):
def _show_finished(self, result) -> None: def _show_finished(self, result) -> None:
self._running = False self._running = False
self.results.status.update(phase="Simulation finished", progress=10000) self.results.status.update(phase="Simulation finished", progress=10000)
self.results.metadata["processResult"] = result 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.progressBar.setValue(10000)
self.ui.statusLabel.setText("Simulation finished") self.ui.statusLabel.setText("Simulation finished")
self.load_result_views() self.load_result_views()

View File

@@ -664,7 +664,7 @@
<action name="actionGraphParameters"> <action name="actionGraphParameters">
<property name="icon"> <property name="icon">
<iconset resource="../resources/resources.qrc"> <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>
<property name="text"> <property name="text">
<string>Graph Parameters</string> <string>Graph Parameters</string>
@@ -706,7 +706,7 @@
<action name="actionSimulationWindow"> <action name="actionSimulationWindow">
<property name="icon"> <property name="icon">
<iconset resource="../resources/resources.qrc"> <iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/configure.png</normaloff>:/icons/icons/configure.png</iconset> <normaloff>:/icons/icons/office-chart-line.png</normaloff>:/icons/icons/office-chart-line.png</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>Simulation Window</string> <string>Simulation Window</string>

View File

@@ -2,30 +2,77 @@
<ui version="4.0"> <ui version="4.0">
<class>SimulationWindow</class> <class>SimulationWindow</class>
<widget class="QMainWindow" name="SimulationWindow"> <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="geometry">
<property name="windowTitle"><string>Simulation</string></property> <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"> <widget class="QWidget" name="centralWidget">
<property name="maximumSize"><size><width>0</width><height>0</height></size></property> <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>
<widget class="QMenuBar" name="menuBar"> <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"> <widget class="QMenu" name="menuFile">
<property name="title"><string>&amp;File</string></property> <property name="title">
<string>&amp;File</string>
</property>
<addaction name="actionOpen"/> <addaction name="actionOpen"/>
<addaction name="actionSave"/> <addaction name="actionSave"/>
<addaction name="actionSaveAs"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionClear"/> <addaction name="actionClear"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionExit"/> <addaction name="actionExit"/>
</widget> </widget>
<widget class="QMenu" name="menuView"> <widget class="QMenu" name="menuView">
<property name="title"><string>&amp;View</string></property> <property name="title">
<widget class="QMenu" name="menuPanels"><property name="title"><string>&amp;Panels</string></property></widget> <string>&amp;View</string>
<widget class="QMenu" name="menuToolbars"><property name="title"><string>&amp;Toolbars</string></property></widget> </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="menuPanels"/>
<addaction name="menuToolbars"/> <addaction name="menuToolbars"/>
</widget> </widget>
<widget class="QMenu" name="menuHelp"> <widget class="QMenu" name="menuHelp">
<property name="title"><string>&amp;Help</string></property> <property name="title">
<string>&amp;Help</string>
</property>
<addaction name="actionAbout"/> <addaction name="actionAbout"/>
<addaction name="actionAboutQt"/> <addaction name="actionAboutQt"/>
</widget> </widget>
@@ -34,58 +81,164 @@
<addaction name="menuHelp"/> <addaction name="menuHelp"/>
</widget> </widget>
<widget class="QToolBar" name="fileToolbar"> <widget class="QToolBar" name="fileToolbar">
<property name="windowTitle"><string>File</string></property> <property name="windowTitle">
<property name="toolButtonStyle"><enum>Qt::ToolButtonStyle::ToolButtonIconOnly</enum></property> <string>File</string>
<attribute name="toolBarArea"><enum>TopToolBarArea</enum></attribute> </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="actionOpen"/>
<addaction name="actionSave"/> <addaction name="actionSave"/>
<addaction name="actionSaveAs"/>
<addaction name="actionClear"/> <addaction name="actionClear"/>
</widget> </widget>
<widget class="QDockWidget" name="resultsDock">
<property name="windowTitle"><string>Results</string></property>
<attribute name="dockWidgetArea"><number>2</number></attribute>
<widget class="QWidget" name="resultsDockContents">
<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>
<widget class="QDockWidget" name="statusDock"> <widget class="QDockWidget" name="statusDock">
<property name="windowTitle"><string>Status</string></property> <property name="windowTitle">
<attribute name="dockWidgetArea"><number>8</number></attribute> <string>Status</string>
</property>
<attribute name="dockWidgetArea">
<number>8</number>
</attribute>
<widget class="QWidget" name="statusDockContents"> <widget class="QWidget" name="statusDockContents">
<layout class="QVBoxLayout" name="statusLayout"> <layout class="QHBoxLayout" name="horizontalLayout">
<item><widget class="QLabel" name="statusLabel"><property name="text"><string>No simulation has been run yet.</string></property></widget></item> <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> <widget class="QLabel" name="timeLabel">
<item><widget class="QLabel" name="timeLabel"><property name="text"><string>Time: 0 s</string></property></widget></item> <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> </layout>
</widget> </widget>
</widget> </widget>
<widget class="QDockWidget" name="logDock"> <widget class="QDockWidget" name="logDock">
<property name="windowTitle"><string>Log</string></property> <property name="floating">
<attribute name="dockWidgetArea"><number>8</number></attribute> <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"> <widget class="QWidget" name="logDockContents">
<layout class="QVBoxLayout" name="logLayout"> <layout class="QVBoxLayout" name="logLayout">
<item><widget class="QListWidget" name="messageList"><property name="alternatingRowColors"><bool>true</bool></property></widget></item> <item>
<widget class="QListWidget" name="messageList">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</widget> </widget>
<action name="actionOpen"> <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="icon">
<property name="text"><string>&amp;Open…</string></property><property name="shortcut"><string>Ctrl+O</string></property> <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>
<action name="actionSave"> <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="icon">
<property name="text"><string>&amp;Save…</string></property><property name="shortcut"><string>Ctrl+S</string></property> <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>
<action name="actionClear"> <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="icon">
<property name="text"><string>&amp;Clear</string></property> <iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/document-new.png</normaloff>:/icons/icons/document-new.png</iconset>
</property>
<property name="text">
<string>&amp;Clear</string>
</property>
</action>
<action name="actionSaveAs">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/document-save-as.png</normaloff>:/icons/icons/document-save-as.png</iconset>
</property>
<property name="text">
<string>Save &amp;As…</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+S</string>
</property>
</action>
<action name="actionExit">
<property name="text">
<string>E&amp;xit</string>
</property>
<property name="shortcut">
<string>Ctrl+W</string>
</property>
</action>
<action name="actionAbout">
<property name="text">
<string>&amp;About Simulation Window</string>
</property>
</action>
<action name="actionAboutQt">
<property name="text">
<string>About &amp;Qt</string>
</property>
</action>
<action name="actionToggleResults">
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="text">
<string>Results</string>
</property>
</action> </action>
<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>
</widget> </widget>
<resources><include location="../resources/resources.qrc"/></resources> <resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections/> <connections/>
</ui> </ui>