diff --git a/BEdit/.vscode/tasks.json b/BEdit/.vscode/tasks.json index a5b5d1b..e618f1b 100644 --- a/BEdit/.vscode/tasks.json +++ b/BEdit/.vscode/tasks.json @@ -115,6 +115,14 @@ "options": {"cwd": "${workspaceFolder}"}, "problemMatcher": [] }, + { + "label": "Qt: Compile Simulation Window UI to Python", + "type": "shell", + "command": "pyside6-uic", + "args": ["--from-imports", "${workspaceFolder}/ui/simulation_window.ui", "-o", "${workspaceFolder}/src/bedit/gui/generated/ui_simulation_window.py"], + "options": {"cwd": "${workspaceFolder}"}, + "problemMatcher": [] + }, { "label": "Qt: Build Designer Files", "dependsOrder": "sequence", @@ -125,7 +133,8 @@ "Qt: Compile Component Options UI to Python", "Qt: Compile Port Options UI to Python", "Qt: Compile Shape Options UI to Python", - "Qt: Compile Icon Editor UI to Python" + "Qt: Compile Icon Editor UI to Python", + "Qt: Compile Simulation Window UI to Python" ], "problemMatcher": [], "group": { diff --git a/BEdit/AGENTS.md b/BEdit/AGENTS.md index 4376fd9..15762e2 100644 --- a/BEdit/AGENTS.md +++ b/BEdit/AGENTS.md @@ -139,6 +139,16 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant - After a successful OpenModelica run, `_res.csv` is parsed on the worker before temporary-directory cleanup. `SimulationResults.data` stores every CSV column as a numeric array, including `time`, for later plotting and persistence. +- The simulation window's dockable Signals tree derives hierarchy from dot-separated + result-column names and bracketed array indices (`a[1]` becomes `a → 1`). Leaf + items retain the exact full column name in `UserRole`; plotting code should + consume `SimulationWindow.selected_signal_names()`. +- Simulation graph tabs persist as `SimulationResults.graphs`; every graph has a + stable ID, editable title, and its own `traces` list. Runtime graph widgets belong + in `GraphWorkspacePage.plot_layout`, not in the serialized core model. The + Signals tree checkboxes edit the active graph's traces, and each page embeds a + Matplotlib QtAgg canvas. Each graph persists its own `x_axis` signal (default + `time`), selectable from the Signals tree context menu. - The optional OpenModelica executable is persisted as `simulation/openModelicaPath`. The GUI passes it into `Simulation`; core must not read `QSettings`. An empty path uses OMPython/PATH discovery, while an diff --git a/BEdit/pyproject.toml b/BEdit/pyproject.toml index 7d1bc00..876acbf 100644 --- a/BEdit/pyproject.toml +++ b/BEdit/pyproject.toml @@ -9,6 +9,7 @@ description = "A starter Qt desktop application" readme = "README.md" requires-python = ">=3.10" dependencies = [ + "matplotlib>=3.8,<4", "msgpack>=1.0,<2", "PySide6>=6.7,<7", "OMPython>4.0", diff --git a/BEdit/resources/icons/list-add.png b/BEdit/resources/icons/list-add.png new file mode 100644 index 0000000..5724694 Binary files /dev/null and b/BEdit/resources/icons/list-add.png differ diff --git a/BEdit/resources/icons/list-remove.png b/BEdit/resources/icons/list-remove.png new file mode 100644 index 0000000..53506b0 Binary files /dev/null and b/BEdit/resources/icons/list-remove.png differ diff --git a/BEdit/resources/resources.qrc b/BEdit/resources/resources.qrc index 1806ce0..6cc4b06 100644 --- a/BEdit/resources/resources.qrc +++ b/BEdit/resources/resources.qrc @@ -1,5 +1,7 @@ + icons/list-remove.png + icons/list-add.png icons/view-form-table.png icons/office-chart-line.png icons/run-build.png diff --git a/BEdit/resources/resources_rc.py b/BEdit/resources/resources_rc.py index e3fafc1..1975a3b 100644 --- a/BEdit/resources/resources_rc.py +++ b/BEdit/resources/resources_rc.py @@ -862,6 +862,75 @@ N\xa7\xd3>==\xfd\xa1\xc3\xe1\x98\x92$\xe9I\x00\ \xbf\xc9\xd3\x01\x9f\xa3\x0f\x17Z/\xe3\x7f\xe1/\x17\x85\ \xd6\x06q(\x0e\x10\x00\x00\x00\x00IEND\xaeB\ `\x82\ +\x00\x00\x04)\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\ +\x00\x00\x00\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\ +\xa7\x93\x00\x00\x00\x09pHYs\x00\x007\x5c\x00\x00\ +7\x5c\x01\xcb\xc7\xa4\xb9\x00\x00\x03\x9bIDATx\ +\xda\xedT=o\x1bG\x10}\xbb\xb7w$\xf5A\x9e\ +lJEB\x18\xb1\x0a\x1bv\x13\xdb\x85\xaa\x18P$\ +\xa4\x89\x81\x94\xd6_\x08`\xb8r\x93 \x85\x02\x01\xa9\ +\x9c\x1f\xe0>\x85\x81\x94F\x00\xa7\x11\xd88\x85\x1bK\ +6@)\x94-\xc8\x96\x10\x18\x81\x22\xf3C\x22i\xf2\ +\xf6v\xf3B.s\xd1\xb1H\x80\xb83\x07x\x98\xbd\ +\xdd\x99y3\xb3\xb7\x83\xb1\x8ce,\xef\xbd\x88\x1f\x97\ +\x97\xe1\x05\x01\x820D~~\x1e\xdb\xf7\xee\xa1st\ +\x94\x188H\x07/\x05\xe5\xe0\x0f\xb5C\xf0\xb7N\xd6\ +\xb9\xb3g\x11\xde\xba\x85\xf8\xd5+\xc4\xb5\x1a`\x0c\xc4\ +\x83B\x01\x82\xe4\xe1\xcd\x9b\xf2\x8f'Ol.\x9f\xb7\ +\x96\x07\xd1\xc9\x09\xbeX_\xc7\xbb\x90\xd7\xd7\xafCN\ +M\x01RB7\x9bbraA4\xee\xdf7q\xa3\ +\x01\xf1\xc3\xf9\xf3\xf0\xc3P\xac\x90\xfc\xd1\xda\x9a|v\ +\xf7\xee4\xc9\xbdw\xda\x01\x87\xec\xf4t<\xb7\xba\xda\ +\xcc\xdc\xb9c\xebW\xae\x88\xb8^\xb7\xa2|\xfb\xb6\xf8\ +\xe0\xeaU\xdbl4>?\xda\xd8\xf8\xf6\xa0\x5c\xfe\xa8\ +yp \xff=\x01G\x9eN\x22!\x1fM\xa0T2\ +S\x8b\x8b{\xb9\x85\x85U\xe4r?G\x9b\x9bB\xd5\ +\xaaU[\x7f\xfe|)\xbcp\xe1Avv\xd6\x93a\ +h\xdb\x07\x07B\xfc\xe7\x04\x122\xe3`\x07p~\x09\ +\x04c\xa3X\x9c\xd5;;?\x89\xdd\xdd\xcf\xa41e\ +e\x94B\xb3Z\xfd\xca\xfa\xbeWZZ\x8a>^Y\ +Q\xf3\xec\x88\xa7\x94#'\xac\x85L\x05\x1c\x09\x9e\xe8\ +S~\x88cxR\x22\xc8\xe7\xa1\xe7\xe6`\x84\x88\xf4\ +\xe3\xc7\xbe\xa8V\xbf6\x17/\x96\xd5\xeb\x87\x0f\x0bZ\ +\xa9K\xad(\xc2\x87\xd7\xaey\xc1\x8b\x17b\xe6\xf8\x18\ +\xca\xf3\xe0Y\x9bT\x8b9\xb3\xbb\xab\xa3A\xcf*5r\x8b\ +5\xf4\xe5\x93\x0e\xb0~\xc2\x03\x1eZ#%\xfd\x93\xb6\ +\xb3\xda\xd3O\x8f\x04\xbe\xd3\xa7\xde<\xed2\xc0\x08\xb2\ +\x0e\x19\x06\xa5\xbf\xe0\xbaG\xbfe\x00\x8f\xd4174\ +\x17\x0c\xb0\x04\xe0;\x03\x5cf\x8b\xa4M?\xad\xd4\xcf\ +G\x0dV\x9f\x9e\x0f\xce~ v\xf8o\x11zpn\ +\x14\xb0\xc5\xbdo\xda\xc0/\x92\xa6j\xd2\xcd\x8c<7\ +\x9e\x01\x8b4\xca\xd3X%\x09$\xc3%\x91\xd1\xa4b\ +\x87\xd40J\xdbk\xee5\xc9\x09=\xd8\xb2\xea\x8dK\ +\xb6\x05\xc8\x90Z\xd0\x00\xceY\x11\xdf\xe3\xffI%\xf5\ +mI\xdc\x224`\xfaE~\xe9\x0e\xa4\xd3=B\xa7\ +F\xe9\xe8\xe8\x1dE\x90B&\xd1)\x0c\xc4`,c\ +\x19\xcbX\x06\xf2'\xe3\xdf\x9d\x06\x06\xf6\x92\x0e\x00\x00\ +\x00\x22zTXtSoftware\x00\x00\ +x\xda+//\xd7\xcb\xcc\xcb.NN,H\xd5\xcb\ +/J\x07\x006\xd8\x06X\x10S\xca\x5c\x00\x00\x00\x00\ +IEND\xaeB`\x82\ \x00\x00\x02V\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -2383,6 +2452,101 @@ u1\xb8\xdd0`x\xe8F\xf9\x1e\xe19\xea\x8a\x03\ |f\x1e\x89\xd1\x9ef\xbdhx\x0fE4mL\xaf\ b/B\x06\x12o\xbf\x00\xa3\x17WYZq\xd9W\ \x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x05\xcf\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\ +\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\ +\x00\x00\x00\x09pHYs\x00\x00\x03v\x00\x00\x03v\ +\x01}\xd5\x82\xcc\x00\x00\x00\x19tEXtSof\ +tware\x00www.inksca\ +pe.org\x9b\xee<\x1a\x00\x00\x05LID\ +ATx\xda\xc5\x97\xdboTE\x1c\xc7\xbf3s\xce\ +\x9e\xeeni\x0b\xbd@\x01C1\x05B\x10\x0d\xf4\x22\ +\x18\x95D0i\xc4\x17|21\xc6\x17\xc3\x93\x89\xd1\ +\xc8\x8b\xfaP#oj\x8c\x89O\xfc\x03\xc6K\x0c\xf1\ +\x01!B\xa8\xc1j\x88D\x90@)\x14\xac\xb5\x86K\ +K\xaf\xdb\xdd={\xce\xcc\xef\xe7\xec\x9cficb\ +\xd2\x98\x13f\xf3\xdd93\xbf\x93\x9d\xcf\xfengW\ +03\x1e\xe6\xf0\xb0\xc2\xf1\xca\xe9\x17wok\xda~\ +lh\xe6\xe2\xd6\xbf\x0a#\xab\x94\x92h\xcb\xad\x9d{\ +z\xdd\xfe\x9b\xf9 \xf7\xce\xe1\xad\xef\x0d`\x05Cb\ +\x05\xa3\xe7\xcb\xce\xc7\x0emz\xf9\xfc\xab\x9d\x87\xbb\xf6\ +\xb4\xec[\x15k\x82\x8e\x09\x9d\xf9\xed\x8d\x07\xda\x0fu\ +=\xd5\xd2w\xe6\x83K\x87{S\xf3@N\xe4>\x9d\ +)M{>\xd5\xa1\xab\xad\x17\x1b\x1a\xda\xdd~V\xe6\ +\xb1)\xb7\x05\xe7\xee\x9e\x94\xf3\xa5\xe2'\x00\x9eI\x05\ +\xe0^a\xa2\xe7\xdc\xdf\x03\xe8\x5c\xb3\x05\xf3\xb9k\x10\ +A\xc5\xed\x87\x98\xc4\x99\xdb\xdf\xe1\xd4\xd8\xb7\x18-\x0e\ +\xefL\xcd\x03aTi\x1c\x1c\xff\x11\xdd\x1bw\xa3I\ +\x99\xe5F3\x87+\xf7/\xc2W\xb215\x00\x133\ + \x08\x11E(\xc7\xcb\x01<\xa3\x9d]2#5\x00\ +\xd2\x04!$\x8c!\x94u\xb4\xccVG1\x8c6\x10\ +@\x9a\x00\x0c\x08\x86!B\x18G\x80X\xea\x01\x03c\ +\xed\x02\x94\x1e\x80Y\x04\xd0d`,\x80\x00j\x10\x01\ +i\x18\xc3\x10\xd6\x9ej\x0e\x08\xc5 bD:^\xe6\ +\x01M\xfa\xffy\xe0\xe8\xe0\xd1\x17\xaeN\x5c\xed\x1f\x18\ +\x1d\xd869w\xafA\x92\x84d\xc0\xbeC\xb1\x80\x94\ +\x02JHw81!2\x0e\xa0\xc6\xa0\x8dv9b\ + \xd0ul\x03{\xbe\x84\xaf\x14<_\xc1\xf7\xecu\ +F\xa15\xd76\xff\xc4\xda\x9e\xeb\x1d\xab\xb6\xf7\xbf\xb4\ +\xf9\xf5\x13\xb5N\xd8\xf7E_\xefTi\xea\xf8\xc6\xfa\ +\x8d=mA[\x03\x99$\xdel\xd8\xcd\xb48\x1bw\ +M\xd0\xdaX\x00\x8d\xd8\x98%\x8a@D\xee^c\xc8\ +\xddk(\xb9\xdfp\xb2W\xef564\xaa\xd6\x9e\xf9\ +h\xea\xf8G\xc3o\xb8\x8e\xe9\x1eF\x1d\x9fw\xfc\xb0\ +\xabm\xd7\x81\xbd\xeb\xf7\x02\xc2`.\x9aAu\x9f\x85\ +\x15\x5c\xdc\x935\x0c\x84\x00r\xb9,\xee\x9a?!\x93\ +$p{\x1b\xbcGQ\xa9\x84P\xd2\x83'\xfd\x9a\x94\ +\xf2\x90\xb1{\x81\xca\xc2g\xcf\xda\x0d\xc6\xc2!\xdc\x89\ +n\x9e\xf9\xec\xc9\xef\x0f\xb8\x10\x8c\xcf\x8dw#\x06z\ +\xda\xbb0T\xb8\x00-4 Q\x93\x84\x80\x14\x02J\ +J7\xcb(Y\x0b'\xc7\x87\xb1\xe8\x06X\x00L\x9c\ +x\x81\xaa\x22\x042\x8b\xbcX\x8d\x91\x89Q\x9c\x1a:\ +\x8d\xf7\xf7\xbd\x85\xdb4\x8cyLv\xd5r\x80\x88\x9a\ +\xc6\xa6\xc602u\x1dEQN\x0eVp\xb3\x10H\ +\x0e\x96\x02\xcc\xb2\x06\xc2\xe2\x01Dm0\x83\x00x\xc2\ +\xc3\xba\xecz\xcc\x95\x8b\xb81}\x0bgG\xbfF\x1c\ +\x11\x00\xd8\xf50\x8a\xc1md|\xd9T\x03\x00[\x91\ +k\xb5\xa8\xa88\x01 \x07\x91$\x1fK+\x07\x90\x1c\ +\x0e\x86\xaa\x02A8\xefdT\x1d\x9a\x83V\x17\x8e0\ +\x0a1\xb10\x81o\xfe\xf8\x0a\x85J\x11\xa5(\xb6\xa0\ +\x12\x90\xe42V\xb3v\xf9@\x9e\xc0R\x80Y\x10\x9a\ +f\x0a\xb3\xb8^\xbc\x09?\xf0\x9c\x82L\x80l\x10\xc0\ +>\xe7\x91\xf7\xb2\x8eI2\x01,]G\x0cD\x00\x22\ +\x8d\xf9h\x1a\xc3\x93\xbf!\xd4\x15h\x22D\xc6\x00\xd2\ +\xc0S\xca\xca@\x19W.N\x19\xcf\xc3\x02\xbb\xf0\xcc\ +.-\xc3A\x10\x0e\x0e\x0e\xff\x82\x1d\xebw .\xc5\ +@9\x09A$\x04H\x85(\x8b\x18\x9e'\xdc\x87\xd6\ +5H\xdc\xc1-\x04\x9erkea\x1e\xf1;Q\xd1\ +\x06\xbc\x987\xe43L\x96\xa09)[cU\xefe\ +1e\x93\x97du\x8f\x7f~\x00\xa0q\x04\xc0\xfe\x85\ +\xf2B\xdd\xf9k\xe7\x81x1\x04\x9cP\x0b+)\x92\ +^ \x94\xc0\xb3{z\xe1\xafV\xc8T3\xbc\x0a!\ +%\xc2B\x8c\x9f.\xffj!%\xaa=@YyK\ +\xe4\xfb\x0a\x85\x8cB&c\xd7\x19\xaf\xacY\x1f\xa9\xf5\ +\x01\xfe\x90\xaf\x81\xd0\x03\xe0,\x08\x0b0@M\x1a`\ +;SU\x94\x5ck\x13\xbbC}%\x1d@\xe0{\xae\ +D\x99\x01\x02\xbb\xba\xa7\x7f\x89\xacx\xce\xdaN\xda\xb9\ +\xfbD\xdf\xc8\xd0\xb2N\xc8\x1f\xf3\x15\x00\xcf\xfdg\xdb\ +|M2\x0b\xd7\xf5\x9cG\xbc*\x80\xb2\x00\x9e\x87J\ +r\x00$\x0b\x8c\xbd;#R\xf9MH&\xa9\xf3\x98\ +b\xf0b|\x94L\xbc\x00h\x07@)\xfe\x1ep\x87\ +3\x0bT\xe2\x18\xc2\x18\x18J\xbe\xb5X\xecF\xc9+\ +E\x80\x04\x02(\xeb\x0a8\xd2P2\xb2J\x1aR1\ +\xa2\xa4\x9d\xa4\x0e \x18\xa5(D\x14F\xae\xe6+\xb1\ +F!\xac ,\xf9 k\xb3<)\x02\x88D\x0bq\ +\x08*VP\xac\xc4\xae\x17\xb8\xf2,f\xc0\x92\xacd\ +z\x00^F\x15\x1b\xb2A\xbeE\x05\xb8Q\x9aK\xda\ +\xb4U\xf5\xb5)\xee@=1B.\x15R\xfbg\x94\ +\xad\xf7\x7f\xdf\xd9\xde\x82\x83\xcdk\xb03n\x86\x9cR\ +\xc0\xb4D\xb7\xea\xc5V\xde\x82<\xea!H^H\x0d\ + \xdf\x1c\xbc\xb9ym\xa0W7*\xb4\xe6\xf3 c\ +\xc0Dx\xa2\xf9q\xc8@Add$`\xdeN\x0d\ +\xe0N\xff\xec\x05\xd9\x1c=?\xee\xf3\xe5\xfb\x22Z\xa0\ +,\x83\xeb\x18*\xab\xc2 \x9b\xb9\x12\xa0n\xffD\x7f\ +\xf1\x12V0\x1e\xfa\xdf\xf3\x7f\x00j\xf0\xda\xe8\xbc\xba\ +\xd0\x0a\x00\x00\x00\x00IEND\xaeB`\x82\ \x00\x00\x04<\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -2649,6 +2813,10 @@ qt_resource_name = b"\ \x0f\xba\x1e'\ \x00d\ \x00r\x00a\x00w\x00-\x00p\x00a\x00t\x00h\x00.\x00p\x00n\x00g\ +\x00\x0f\ +\x020\x8b\xe7\ +\x00l\ +\x00i\x00s\x00t\x00-\x00r\x00e\x00m\x00o\x00v\x00e\x00.\x00p\x00n\x00g\ \x00\x0d\ \x0b\xe6\x1f\xa7\ \x00d\ @@ -2723,6 +2891,10 @@ qt_resource_name = b"\ \x00o\ \x00f\x00f\x00i\x00c\x00e\x00-\x00c\x00h\x00a\x00r\x00t\x00-\x00l\x00i\x00n\x00e\ \x00.\x00p\x00n\x00g\ +\x00\x0c\ +\x09\xc6\x19'\ +\x00l\ +\x00i\x00s\x00t\x00-\x00a\x00d\x00d\x00.\x00p\x00n\x00g\ \x00\x10\ \x03\xe6\xd3g\ \x00d\ @@ -2749,67 +2921,71 @@ qt_resource_struct = b"\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1e\x00\x00\x00\x03\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00 \x00\x00\x00\x03\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\xfe\x00\x00\x00\x00\x00\x01\x00\x00N\xe8\ +\x00\x00\x02\x22\x00\x00\x00\x00\x00\x01\x00\x00S\x15\ \x00\x00\x01\x9f{C\xf1'\ -\x00\x00\x02>\x00\x00\x00\x00\x00\x01\x00\x00Z\x1a\ +\x00\x00\x02b\x00\x00\x00\x00\x00\x01\x00\x00^G\ \x00\x00\x01\x9f\x7f\xa8\xa8)\ -\x00\x00\x02\xec\x00\x00\x00\x00\x00\x01\x00\x00tV\ +\x00\x00\x03\x10\x00\x00\x00\x00\x00\x01\x00\x00x\x83\ \x00\x00\x01\x9f{0\xc99\ -\x00\x00\x03\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x8d\xaa\ +\x00\x00\x01d\x00\x00\x00\x00\x00\x01\x00\x004B\ +\x00\x00\x01\x9f\x84\xbd\x03\xb8\ +\x00\x00\x03\xc8\x00\x00\x00\x00\x00\x01\x00\x00\x91\xd7\ \x00\x00\x01\x9f\x84\x8f\x00\xb9\ -\x00\x00\x01\xc4\x00\x00\x00\x00\x00\x01\x00\x00D2\ +\x00\x00\x01\xe8\x00\x00\x00\x00\x00\x01\x00\x00H_\ \x00\x00\x01\x9f\x7f&\x83\xcd\ -\x00\x00\x01\xa4\x00\x00\x00\x00\x00\x01\x00\x00 dict[str, Any]: @@ -44,7 +57,7 @@ class SimulationResults: "status": dict(self.status), "messages": [dict(message) for message in self.messages], "data": {name: list(values) for name, values in self.data.items()}, - "traces": [asdict(trace) for trace in self.traces], + "graphs": [asdict(graph) for graph in self.graphs], "metadata": dict(self.metadata), } @@ -55,7 +68,17 @@ class SimulationResults: if data.get("version") != RESULTS_VERSION: raise ValueError(f"Unsupported simulation-results version: {data.get('version')!r}") try: - traces = [SimulationTrace(**trace) for trace in data.get("traces", [])] + graphs = [ + SimulationGraph( + id=str(graph["id"]), + title=str(graph["title"]), + x_axis=str(graph.get("x_axis", "time")), + traces=[ + SimulationTrace(**trace) for trace in graph.get("traces", []) + ], + ) + for graph in data.get("graphs", []) + ] return cls( model_name=str(data.get("modelName", "")), status=dict(data.get("status", {})), @@ -64,10 +87,10 @@ class SimulationResults: str(name): [float(value) for value in values] for name, values in dict(data.get("data", {})).items() }, - traces=traces, + graphs=graphs, metadata=dict(data.get("metadata", {})), ) - except (TypeError, ValueError) as error: + except (KeyError, TypeError, ValueError) as error: raise ValueError("Malformed simulation-results data") from error diff --git a/BEdit/src/bedit/gui/generated/resources_rc.py b/BEdit/src/bedit/gui/generated/resources_rc.py index e3fafc1..1975a3b 100644 --- a/BEdit/src/bedit/gui/generated/resources_rc.py +++ b/BEdit/src/bedit/gui/generated/resources_rc.py @@ -862,6 +862,75 @@ N\xa7\xd3>==\xfd\xa1\xc3\xe1\x98\x92$\xe9I\x00\ \xbf\xc9\xd3\x01\x9f\xa3\x0f\x17Z/\xe3\x7f\xe1/\x17\x85\ \xd6\x06q(\x0e\x10\x00\x00\x00\x00IEND\xaeB\ `\x82\ +\x00\x00\x04)\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\ +\x00\x00\x00\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\ +\xa7\x93\x00\x00\x00\x09pHYs\x00\x007\x5c\x00\x00\ +7\x5c\x01\xcb\xc7\xa4\xb9\x00\x00\x03\x9bIDATx\ +\xda\xedT=o\x1bG\x10}\xbb\xb7w$\xf5A\x9e\ +lJEB\x18\xb1\x0a\x1bv\x13\xdb\x85\xaa\x18P$\ +\xa4\x89\x81\x94\xd6_\x08`\xb8r\x93 \x85\x02\x01\xa9\ +\x9c\x1f\xe0>\x85\x81\x94F\x00\xa7\x11\xd88\x85\x1bK\ +6@)\x94-\xc8\x96\x10\x18\x81\x22\xf3C\x22i\xf2\ +\xf6v\xf3B.s\xd1\xb1H\x80\xb83\x07x\x98\xbd\ +\xdd\x99y3\xb3\xb7\x83\xb1\x8ce,\xef\xbd\x88\x1f\x97\ +\x97\xe1\x05\x01\x820D~~\x1e\xdb\xf7\xee\xa1st\ +\x94\x188H\x07/\x05\xe5\xe0\x0f\xb5C\xf0\xb7N\xd6\ +\xb9\xb3g\x11\xde\xba\x85\xf8\xd5+\xc4\xb5\x1a`\x0c\xc4\ +\x83B\x01\x82\xe4\xe1\xcd\x9b\xf2\x8f'Ol.\x9f\xb7\ +\x96\x07\xd1\xc9\x09\xbeX_\xc7\xbb\x90\xd7\xd7\xafCN\ +M\x01RB7\x9bbraA4\xee\xdf7q\xa3\ +\x01\xf1\xc3\xf9\xf3\xf0\xc3P\xac\x90\xfc\xd1\xda\x9a|v\ +\xf7\xee4\xc9\xbdw\xda\x01\x87\xec\xf4t<\xb7\xba\xda\ +\xcc\xdc\xb9c\xebW\xae\x88\xb8^\xb7\xa2|\xfb\xb6\xf8\ +\xe0\xeaU\xdbl4>?\xda\xd8\xf8\xf6\xa0\x5c\xfe\xa8\ +yp \xff=\x01G\x9eN\x22!\x1fM\xa0T2\ +S\x8b\x8b{\xb9\x85\x85U\xe4r?G\x9b\x9bB\xd5\ +\xaaU[\x7f\xfe|)\xbcp\xe1Avv\xd6\x93a\ +h\xdb\x07\x07B\xfc\xe7\x04\x122\xe3`\x07p~\x09\ +\x04c\xa3X\x9c\xd5;;?\x89\xdd\xdd\xcf\xa41e\ +e\x94B\xb3Z\xfd\xca\xfa\xbeWZZ\x8a>^Y\ +Q\xf3\xec\x88\xa7\x94#'\xac\x85L\x05\x1c\x09\x9e\xe8\ +S~\x88cxR\x22\xc8\xe7\xa1\xe7\xe6`\x84\x88\xf4\ +\xe3\xc7\xbe\xa8V\xbf6\x17/\x96\xd5\xeb\x87\x0f\x0bZ\ +\xa9K\xad(\xc2\x87\xd7\xaey\xc1\x8b\x17b\xe6\xf8\x18\ +\xca\xf3\xe0Y\x9bT\x8b9\xb3\xbb\xab\xa3A\xcf*5r\x8b\ +5\xf4\xe5\x93\x0e\xb0~\xc2\x03\x1eZ#%\xfd\x93\xb6\ +\xb3\xda\xd3O\x8f\x04\xbe\xd3\xa7\xde<\xed2\xc0\x08\xb2\ +\x0e\x19\x06\xa5\xbf\xe0\xbaG\xbfe\x00\x8f\xd4174\ +\x17\x0c\xb0\x04\xe0;\x03\x5cf\x8b\xa4M?\xad\xd4\xcf\ +G\x0dV\x9f\x9e\x0f\xce~ v\xf8o\x11zpn\ +\x14\xb0\xc5\xbdo\xda\xc0/\x92\xa6j\xd2\xcd\x8c<7\ +\x9e\x01\x8b4\xca\xd3X%\x09$\xc3%\x91\xd1\xa4b\ +\x87\xd40J\xdbk\xee5\xc9\x09=\xd8\xb2\xea\x8dK\ +\xb6\x05\xc8\x90Z\xd0\x00\xceY\x11\xdf\xe3\xffI%\xf5\ +mI\xdc\x224`\xfaE~\xe9\x0e\xa4\xd3=B\xa7\ +F\xe9\xe8\xe8\x1dE\x90B&\xd1)\x0c\xc4`,c\ +\x19\xcbX\x06\xf2'\xe3\xdf\x9d\x06\x06\xf6\x92\x0e\x00\x00\ +\x00\x22zTXtSoftware\x00\x00\ +x\xda+//\xd7\xcb\xcc\xcb.NN,H\xd5\xcb\ +/J\x07\x006\xd8\x06X\x10S\xca\x5c\x00\x00\x00\x00\ +IEND\xaeB`\x82\ \x00\x00\x02V\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -2383,6 +2452,101 @@ u1\xb8\xdd0`x\xe8F\xf9\x1e\xe19\xea\x8a\x03\ |f\x1e\x89\xd1\x9ef\xbdhx\x0fE4mL\xaf\ b/B\x06\x12o\xbf\x00\xa3\x17WYZq\xd9W\ \x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x05\xcf\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\ +\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\ +\x00\x00\x00\x09pHYs\x00\x00\x03v\x00\x00\x03v\ +\x01}\xd5\x82\xcc\x00\x00\x00\x19tEXtSof\ +tware\x00www.inksca\ +pe.org\x9b\xee<\x1a\x00\x00\x05LID\ +ATx\xda\xc5\x97\xdboTE\x1c\xc7\xbf3s\xce\ +\x9e\xeeni\x0b\xbd@\x01C1\x05B\x10\x0d\xf4\x22\ +\x18\x95D0i\xc4\x17|21\xc6\x17\xc3\x93\x89\xd1\ +\xc8\x8b\xfaP#oj\x8c\x89O\xfc\x03\xc6K\x0c\xf1\ +\x01!B\xa8\xc1j\x88D\x90@)\x14\xac\xb5\x86K\ +K\xaf\xdb\xdd={\xce\xcc\xef\xe7\xec\x9cficb\ +\xd2\x98\x13f\xf3\xdd93\xbf\x93\x9d\xcf\xfengW\ +03\x1e\xe6\xf0\xb0\xc2\xf1\xca\xe9\x17wok\xda~\ +lh\xe6\xe2\xd6\xbf\x0a#\xab\x94\x92h\xcb\xad\x9d{\ +z\xdd\xfe\x9b\xf9 \xf7\xce\xe1\xad\xef\x0d`\x05Cb\ +\x05\xa3\xe7\xcb\xce\xc7\x0emz\xf9\xfc\xab\x9d\x87\xbb\xf6\ +\xb4\xec[\x15k\x82\x8e\x09\x9d\xf9\xed\x8d\x07\xda\x0fu\ +=\xd5\xd2w\xe6\x83K\x87{S\xf3@N\xe4>\x9d\ +)M{>\xd5\xa1\xab\xad\x17\x1b\x1a\xda\xdd~V\xe6\ +\xb1)\xb7\x05\xe7\xee\x9e\x94\xf3\xa5\xe2'\x00\x9eI\x05\ +\xe0^a\xa2\xe7\xdc\xdf\x03\xe8\x5c\xb3\x05\xf3\xb9k\x10\ +A\xc5\xed\x87\x98\xc4\x99\xdb\xdf\xe1\xd4\xd8\xb7\x18-\x0e\ +\xefL\xcd\x03aTi\x1c\x1c\xff\x11\xdd\x1bw\xa3I\ +\x99\xe5F3\x87+\xf7/\xc2W\xb215\x00\x133\ + \x08\x11E(\xc7\xcb\x01<\xa3\x9d]2#5\x00\ +\xd2\x04!$\x8c!\x94u\xb4\xccVG1\x8c6\x10\ +@\x9a\x00\x0c\x08\x86!B\x18G\x80X\xea\x01\x03c\ +\xed\x02\x94\x1e\x80Y\x04\xd0d`,\x80\x00j\x10\x01\ +i\x18\xc3\x10\xd6\x9ej\x0e\x08\xc5 bD:^\xe6\ +\x01M\xfa\xffy\xe0\xe8\xe0\xd1\x17\xaeN\x5c\xed\x1f\x18\ +\x1d\xd869w\xafA\x92\x84d\xc0\xbeC\xb1\x80\x94\ +\x02JHw81!2\x0e\xa0\xc6\xa0\x8dv9b\ + \xd0ul\x03{\xbe\x84\xaf\x14<_\xc1\xf7\xecu\ +F\xa15\xd76\xff\xc4\xda\x9e\xeb\x1d\xab\xb6\xf7\xbf\xb4\ +\xf9\xf5\x13\xb5N\xd8\xf7E_\xefTi\xea\xf8\xc6\xfa\ +\x8d=mA[\x03\x99$\xdel\xd8\xcd\xb48\x1bw\ +M\xd0\xdaX\x00\x8d\xd8\x98%\x8a@D\xee^c\xc8\ +\xddk(\xb9\xdfp\xb2W\xef564\xaa\xd6\x9e\xf9\ +h\xea\xf8G\xc3o\xb8\x8e\xe9\x1eF\x1d\x9fw\xfc\xb0\ +\xabm\xd7\x81\xbd\xeb\xf7\x02\xc2`.\x9aAu\x9f\x85\ +\x15\x5c\xdc\x935\x0c\x84\x00r\xb9,\xee\x9a?!\x93\ +$p{\x1b\xbcGQ\xa9\x84P\xd2\x83'\xfd\x9a\x94\ +\xf2\x90\xb1{\x81\xca\xc2g\xcf\xda\x0d\xc6\xc2!\xdc\x89\ +n\x9e\xf9\xec\xc9\xef\x0f\xb8\x10\x8c\xcf\x8dw#\x06z\ +\xda\xbb0T\xb8\x00-4 Q\x93\x84\x80\x14\x02J\ +J7\xcb(Y\x0b'\xc7\x87\xb1\xe8\x06X\x00L\x9c\ +x\x81\xaa\x22\x042\x8b\xbcX\x8d\x91\x89Q\x9c\x1a:\ +\x8d\xf7\xf7\xbd\x85\xdb4\x8cyLv\xd5r\x80\x88\x9a\ +\xc6\xa6\xc602u\x1dEQN\x0eVp\xb3\x10H\ +\x0e\x96\x02\xcc\xb2\x06\xc2\xe2\x01Dm0\x83\x00x\xc2\ +\xc3\xba\xecz\xcc\x95\x8b\xb81}\x0bgG\xbfF\x1c\ +\x11\x00\xd8\xf50\x8a\xc1md|\xd9T\x03\x00[\x91\ +k\xb5\xa8\xa88\x01 \x07\x91$\x1fK+\x07\x90\x1c\ +\x0e\x86\xaa\x02A8\xefdT\x1d\x9a\x83V\x17\x8e0\ +\x0a1\xb10\x81o\xfe\xf8\x0a\x85J\x11\xa5(\xb6\xa0\ +\x12\x90\xe42V\xb3v\xf9@\x9e\xc0R\x80Y\x10\x9a\ +f\x0a\xb3\xb8^\xbc\x09?\xf0\x9c\x82L\x80l\x10\xc0\ +>\xe7\x91\xf7\xb2\x8eI2\x01,]G\x0cD\x00\x22\ +\x8d\xf9h\x1a\xc3\x93\xbf!\xd4\x15h\x22D\xc6\x00\xd2\ +\xc0S\xca\xca@\x19W.N\x19\xcf\xc3\x02\xbb\xf0\xcc\ +.-\xc3A\x10\x0e\x0e\x0e\xff\x82\x1d\xebw .\xc5\ +@9\x09A$\x04H\x85(\x8b\x18\x9e'\xdc\x87\xd6\ +5H\xdc\xc1-\x04\x9erkea\x1e\xf1;Q\xd1\ +\x06\xbc\x987\xe43L\x96\xa09)[cU\xefe\ +1e\x93\x97du\x8f\x7f~\x00\xa0q\x04\xc0\xfe\x85\ +\xf2B\xdd\xf9k\xe7\x81x1\x04\x9cP\x0b+)\x92\ +^ \x94\xc0\xb3{z\xe1\xafV\xc8T3\xbc\x0a!\ +%\xc2B\x8c\x9f.\xffj!%\xaa=@YyK\ +\xe4\xfb\x0a\x85\x8cB&c\xd7\x19\xaf\xacY\x1f\xa9\xf5\ +\x01\xfe\x90\xaf\x81\xd0\x03\xe0,\x08\x0b0@M\x1a`\ +;SU\x94\x5ck\x13\xbbC}%\x1d@\xe0{\xae\ +D\x99\x01\x02\xbb\xba\xa7\x7f\x89\xacx\xce\xdaN\xda\xb9\ +\xfbD\xdf\xc8\xd0\xb2N\xc8\x1f\xf3\x15\x00\xcf\xfdg\xdb\ +|M2\x0b\xd7\xf5\x9cG\xbc*\x80\xb2\x00\x9e\x87J\ +r\x00$\x0b\x8c\xbd;#R\xf9MH&\xa9\xf3\x98\ +b\xf0b|\x94L\xbc\x00h\x07@)\xfe\x1ep\x87\ +3\x0bT\xe2\x18\xc2\x18\x18J\xbe\xb5X\xecF\xc9+\ +E\x80\x04\x02(\xeb\x0a8\xd2P2\xb2J\x1aR1\ +\xa2\xa4\x9d\xa4\x0e \x18\xa5(D\x14F\xae\xe6+\xb1\ +F!\xac ,\xf9 k\xb3<)\x02\x88D\x0bq\ +\x08*VP\xac\xc4\xae\x17\xb8\xf2,f\xc0\x92\xacd\ +z\x00^F\x15\x1b\xb2A\xbeE\x05\xb8Q\x9aK\xda\ +\xb4U\xf5\xb5)\xee@=1B.\x15R\xfbg\x94\ +\xad\xf7\x7f\xdf\xd9\xde\x82\x83\xcdk\xb03n\x86\x9cR\ +\xc0\xb4D\xb7\xea\xc5V\xde\x82<\xea!H^H\x0d\ + \xdf\x1c\xbc\xb9ym\xa0W7*\xb4\xe6\xf3 c\ +\xc0Dx\xa2\xf9q\xc8@Add$`\xdeN\x0d\ +\xe0N\xff\xec\x05\xd9\x1c=?\xee\xf3\xe5\xfb\x22Z\xa0\ +,\x83\xeb\x18*\xab\xc2 \x9b\xb9\x12\xa0n\xffD\x7f\ +\xf1\x12V0\x1e\xfa\xdf\xf3\x7f\x00j\xf0\xda\xe8\xbc\xba\ +\xd0\x0a\x00\x00\x00\x00IEND\xaeB`\x82\ \x00\x00\x04<\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -2649,6 +2813,10 @@ qt_resource_name = b"\ \x0f\xba\x1e'\ \x00d\ \x00r\x00a\x00w\x00-\x00p\x00a\x00t\x00h\x00.\x00p\x00n\x00g\ +\x00\x0f\ +\x020\x8b\xe7\ +\x00l\ +\x00i\x00s\x00t\x00-\x00r\x00e\x00m\x00o\x00v\x00e\x00.\x00p\x00n\x00g\ \x00\x0d\ \x0b\xe6\x1f\xa7\ \x00d\ @@ -2723,6 +2891,10 @@ qt_resource_name = b"\ \x00o\ \x00f\x00f\x00i\x00c\x00e\x00-\x00c\x00h\x00a\x00r\x00t\x00-\x00l\x00i\x00n\x00e\ \x00.\x00p\x00n\x00g\ +\x00\x0c\ +\x09\xc6\x19'\ +\x00l\ +\x00i\x00s\x00t\x00-\x00a\x00d\x00d\x00.\x00p\x00n\x00g\ \x00\x10\ \x03\xe6\xd3g\ \x00d\ @@ -2749,67 +2921,71 @@ qt_resource_struct = b"\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1e\x00\x00\x00\x03\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00 \x00\x00\x00\x03\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\xfe\x00\x00\x00\x00\x00\x01\x00\x00N\xe8\ +\x00\x00\x02\x22\x00\x00\x00\x00\x00\x01\x00\x00S\x15\ \x00\x00\x01\x9f{C\xf1'\ -\x00\x00\x02>\x00\x00\x00\x00\x00\x01\x00\x00Z\x1a\ +\x00\x00\x02b\x00\x00\x00\x00\x00\x01\x00\x00^G\ \x00\x00\x01\x9f\x7f\xa8\xa8)\ -\x00\x00\x02\xec\x00\x00\x00\x00\x00\x01\x00\x00tV\ +\x00\x00\x03\x10\x00\x00\x00\x00\x00\x01\x00\x00x\x83\ \x00\x00\x01\x9f{0\xc99\ -\x00\x00\x03\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x8d\xaa\ +\x00\x00\x01d\x00\x00\x00\x00\x00\x01\x00\x004B\ +\x00\x00\x01\x9f\x84\xbd\x03\xb8\ +\x00\x00\x03\xc8\x00\x00\x00\x00\x00\x01\x00\x00\x91\xd7\ \x00\x00\x01\x9f\x84\x8f\x00\xb9\ -\x00\x00\x01\xc4\x00\x00\x00\x00\x00\x01\x00\x00D2\ +\x00\x00\x01\xe8\x00\x00\x00\x00\x00\x01\x00\x00H_\ \x00\x00\x01\x9f\x7f&\x83\xcd\ -\x00\x00\x01\xa4\x00\x00\x00\x00\x00\x01\x00\x00 None: self.ui.menuPanels.addAction(self.ui.actionToggleResults) - for panel in (self.ui.statusDock, self.ui.logDock): + for panel in (self.ui.statusDock, self.ui.logDock, self.ui.signalsDock): self.ui.menuPanels.addAction(panel.toggleViewAction()) self.ui.menuToolbars.addAction(self.ui.fileToolbar.toggleViewAction()) + self.ui.menuToolbars.addAction(self.ui.workspaceToolbar.toggleViewAction()) def begin_run(self, model_name: str = "") -> tuple: """Reset the viewer and return callbacks bound to this run.""" @@ -91,6 +116,7 @@ class SimulationWindow(QMainWindow): self.ui.timeLabel.setText("Time: 0 s") self.ui.messageList.clear() self.clear_result_views() + self.load_result_views() self._update_title() def clear_result_views(self) -> None: @@ -100,9 +126,10 @@ class SimulationWindow(QMainWindow): ``resultsLayout`` and reset here. """ - self.ui.resultsPlaceholder.setText( - "Simulation graphs and result controls can be added here." - ) + self._rebuilding_graph_tabs = True + self._clear_graph_tab_widgets() + self._rebuilding_graph_tabs = False + self.ui.signalsTree.clear() def load_result_views(self) -> None: """Populate custom plots from ``self.results.data`` and traces. @@ -110,14 +137,195 @@ class SimulationWindow(QMainWindow): This is the intended integration point for a future plotting widget. """ - data_count = len(self.results.data) - trace_count = len(self.results.traces) - if data_count or trace_count: - sample_count = len(next(iter(self.results.data.values()), [])) - self.ui.resultsPlaceholder.setText( - f"{data_count} data column(s), {sample_count} sample(s), and " - f"{trace_count} configured trace(s) loaded; add graph rendering here." + self._rebuild_graph_tabs() + self._rebuild_signal_tree() + + def _rebuild_graph_tabs(self) -> None: + self._rebuilding_graph_tabs = True + self._clear_graph_tab_widgets() + for graph in self.results.graphs: + self.ui.graphTabs.addTab( + GraphWorkspacePage(graph, self.results), graph.title ) + self._rebuilding_graph_tabs = False + self._update_graph_actions() + self._sync_signal_checks() + + def _clear_graph_tab_widgets(self) -> None: + while self.ui.graphTabs.count(): + page = self.ui.graphTabs.widget(0) + self.ui.graphTabs.removeTab(0) + page.deleteLater() + + def add_graph_tab(self) -> None: + used_titles = {graph.title for graph in self.results.graphs} + number = 1 + while f"Graph {number}" in used_titles: + number += 1 + graph = SimulationGraph(title=f"Graph {number}") + self.results.graphs.append(graph) + index = self.ui.graphTabs.addTab( + GraphWorkspacePage(graph, self.results), graph.title + ) + self.ui.graphTabs.setCurrentIndex(index) + self._update_graph_actions() + + def remove_current_graph_tab(self) -> None: + index = self.ui.graphTabs.currentIndex() + if index < 0 or index >= len(self.results.graphs): + return + self.results.graphs.pop(index) + page = self.ui.graphTabs.widget(index) + self.ui.graphTabs.removeTab(index) + page.deleteLater() + self._update_graph_actions() + self._sync_signal_checks() + + def rename_graph_tab(self, index: int) -> None: + if index < 0 or index >= len(self.results.graphs): + return + graph = self.results.graphs[index] + title, accepted = QInputDialog.getText( + self, "Rename Graph", "Title:", text=graph.title + ) + title = title.strip() + if accepted and title: + graph.title = title + self.ui.graphTabs.setTabText(index, title) + page = self.ui.graphTabs.widget(index) + if isinstance(page, GraphWorkspacePage): + page.refresh_chart() + + def _move_graph_tab(self, old_index: int, new_index: int) -> None: + if self._rebuilding_graph_tabs or old_index == new_index: + return + graph = self.results.graphs.pop(old_index) + self.results.graphs.insert(new_index, graph) + self._sync_signal_checks() + + def _update_graph_actions(self) -> None: + self.ui.actionRemoveGraph.setEnabled(bool(self.results.graphs)) + + def _rebuild_signal_tree(self) -> None: + """Build a hierarchy while retaining each leaf's complete signal name.""" + + tree = self.ui.signalsTree + self._updating_signal_checks = True + try: + tree.clear() + items: dict[tuple[str, ...], QTreeWidgetItem] = {} + for signal_name in sorted(self.results.data, key=str.casefold): + parts = _signal_tree_parts(signal_name) + if not parts: + continue + parent = tree.invisibleRootItem() + for depth, part in enumerate(parts, start=1): + path = parts[:depth] + item = items.get(path) + if item is None: + item = QTreeWidgetItem(parent, [part]) + items[path] = item + parent = item + parent.setData(0, Qt.ItemDataRole.UserRole, signal_name) + parent.setToolTip(0, signal_name) + parent.setFlags(parent.flags() | Qt.ItemFlag.ItemIsUserCheckable) + parent.setCheckState(0, Qt.CheckState.Unchecked) + tree.expandToDepth(0) + finally: + self._updating_signal_checks = False + self._sync_signal_checks() + + def _current_graph_changed(self, _index: int) -> None: + if not self._rebuilding_graph_tabs: + self._sync_signal_checks() + + def _current_graph(self) -> SimulationGraph | None: + index = self.ui.graphTabs.currentIndex() + if 0 <= index < len(self.results.graphs): + return self.results.graphs[index] + return None + + def _sync_signal_checks(self) -> None: + graph = self._current_graph() + enabled = {trace.name for trace in graph.traces} if graph else set() + self._updating_signal_checks = True + try: + root = self.ui.signalsTree.invisibleRootItem() + pending = [root.child(index) for index in range(root.childCount())] + while pending: + item = pending.pop() + pending.extend( + item.child(index) for index in range(item.childCount()) + ) + signal_name = item.data(0, Qt.ItemDataRole.UserRole) + if isinstance(signal_name, str): + item.setCheckState( + 0, + Qt.CheckState.Checked + if signal_name in enabled + else Qt.CheckState.Unchecked, + ) + finally: + self._updating_signal_checks = False + self.ui.signalsTree.setEnabled(graph is not None) + + def _signal_check_changed(self, item: QTreeWidgetItem, _column: int) -> None: + if self._updating_signal_checks: + return + signal_name = item.data(0, Qt.ItemDataRole.UserRole) + graph = self._current_graph() + if not isinstance(signal_name, str) or graph is None: + return + enabled = item.checkState(0) == Qt.CheckState.Checked + existing = next( + (trace for trace in graph.traces if trace.name == signal_name), None + ) + if enabled and existing is None: + graph.traces.append(SimulationTrace(name=signal_name)) + elif not enabled and existing is not None: + graph.traces.remove(existing) + page = self.ui.graphTabs.currentWidget() + if isinstance(page, GraphWorkspacePage): + page.refresh_chart() + + def selected_signal_names(self) -> list[str]: + """Return full column names selected for future plotting actions.""" + + names = [] + for item in self.ui.signalsTree.selectedItems(): + name = item.data(0, Qt.ItemDataRole.UserRole) + if isinstance(name, str): + names.append(name) + return names + + def show_signal_context_menu(self, position) -> None: + item = self.ui.signalsTree.itemAt(position) + signal_name = ( + item.data(0, Qt.ItemDataRole.UserRole) if item is not None else None + ) + graph = self._current_graph() + if not isinstance(signal_name, str) or graph is None: + return + menu = QMenu(self) + use_as_x_action = menu.addAction("Use as X Axis") + use_as_x_action.setCheckable(True) + use_as_x_action.setChecked(graph.x_axis == signal_name) + selected = menu.exec( + self.ui.signalsTree.viewport().mapToGlobal(position) + ) + if selected is use_as_x_action: + self.set_x_axis_signal(signal_name) + + def set_x_axis_signal(self, signal_name: str) -> None: + """Set the current graph's persisted horizontal data column.""" + + graph = self._current_graph() + if graph is None or signal_name not in self.results.data: + return + graph.x_axis = signal_name + page = self.ui.graphTabs.currentWidget() + if isinstance(page, GraphWorkspacePage): + page.refresh_chart() def open_results(self) -> None: file_name, _selected_filter = QFileDialog.getOpenFileName( @@ -261,6 +469,65 @@ class SimulationWindow(QMainWindow): return self._running +class GraphWorkspacePage(QWidget): + """Matplotlib view of one persisted simulation graph definition.""" + + def __init__( + self, graph: SimulationGraph, results: SimulationResults, parent=None + ) -> None: + super().__init__(parent) + self.graph_id = graph.id + self.graph = graph + self.results = results + self.plot_layout = QVBoxLayout(self) + self.figure = Figure(layout="constrained") + self.canvas = FigureCanvasQTAgg(self.figure) + self.axes = self.figure.add_subplot(111) + self.navigation_toolbar = NavigationToolbar2QT(self.canvas, self) + self.plot_layout.addWidget(self.navigation_toolbar) + self.plot_layout.addWidget(self.canvas) + self.refresh_chart() + + def refresh_chart(self) -> None: + self.axes.clear() + x_values = self.results.data.get(self.graph.x_axis) + for trace in self.graph.traces: + y_values = self.results.data.get(trace.name) + if y_values is None: + continue + horizontal = x_values if x_values is not None else range(len(y_values)) + sample_count = min(len(horizontal), len(y_values)) + color = trace.properties.get("color") + self.axes.plot( + list(horizontal)[:sample_count], + y_values[:sample_count], + label=trace.name, + color=color if isinstance(color, str) and color else None, + ) + self.axes.set_title(self.graph.title) + self.axes.set_xlabel(self.graph.x_axis if x_values is not None else "sample") + self.axes.grid(True, alpha=0.25) + if self.axes.lines: + self.axes.legend() + self.canvas.draw_idle() + + def _safe_file_stem(model_name: str) -> str: stem = re.sub(r"[^A-Za-z0-9_.-]+", "_", model_name).strip("._") return stem or "simulation" + + +def _signal_tree_parts(signal_name: str) -> tuple[str, ...]: + """Split dots and array indices into display hierarchy segments.""" + + parts: list[str] = [] + for segment in signal_name.split("."): + if not segment: + continue + match = re.fullmatch(r"([^\[]+)((?:\[[^\]]+\])+)", segment) + if match is None: + parts.append(segment) + continue + parts.append(match.group(1)) + parts.extend(re.findall(r"\[([^\]]+)\]", match.group(2))) + return tuple(parts) diff --git a/BEdit/ui/simulation_window.ui b/BEdit/ui/simulation_window.ui index 8f4dc70..816d9b7 100644 --- a/BEdit/ui/simulation_window.ui +++ b/BEdit/ui/simulation_window.ui @@ -20,12 +20,12 @@ - - - Simulation graphs and result controls can be added here. + + + false - - Qt::AlignmentFlag::AlignCenter + + true @@ -76,10 +76,34 @@ + + + &Graph + + + + + + + + Workspace + + + Qt::ToolButtonStyle::ToolButtonIconOnly + + + TopToolBarArea + + + false + + + + File @@ -93,10 +117,10 @@ false + - @@ -162,6 +186,36 @@ + + + Signals + + + 2 + + + + + + + Qt::ContextMenuPolicy::CustomContextMenu + + + QAbstractItemView::SelectionMode::ExtendedSelection + + + true + + + + Signal + + + + + + + @@ -236,6 +290,30 @@ Results + + + + :/icons/icons/list-add.png:/icons/icons/list-add.png + + + Add Graph + + + Add a graph workspace tab + + + + + + :/icons/icons/list-remove.png:/icons/icons/list-remove.png + + + Remove Current Graph + + + Remove the current graph workspace tab + +