sim window save/load

This commit is contained in:
2026-07-21 14:01:55 +02:00
parent cdfc891980
commit ddc004dee5
12 changed files with 532 additions and 164 deletions

View File

@@ -51,6 +51,9 @@ class Ui_MainWindow(object):
icon3 = QIcon()
icon3.addFile(u":/icons/icons/media-playback-start.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRunSimulation.setIcon(icon3)
self.actionSimulationWindow = QAction(MainWindow)
self.actionSimulationWindow.setObjectName(u"actionSimulationWindow")
self.actionSimulationWindow.setIcon(icon1)
self.actionNew = QAction(MainWindow)
self.actionNew.setObjectName(u"actionNew")
icon4 = QIcon()
@@ -436,6 +439,7 @@ class Ui_MainWindow(object):
self.menuSimulation.addAction(self.actionSimulationSettings)
self.menuSimulation.addAction(self.actionGraphParameters)
self.menuSimulation.addAction(self.actionCompose)
self.menuSimulation.addAction(self.actionSimulationWindow)
self.menuSimulation.addAction(self.actionRunSimulation)
self.fileToolbar.addAction(self.actionNew)
self.fileToolbar.addAction(self.actionOpen)
@@ -452,6 +456,7 @@ class Ui_MainWindow(object):
self.simulationToolbar.addAction(self.actionSimulationSettings)
self.simulationToolbar.addAction(self.actionGraphParameters)
self.simulationToolbar.addAction(self.actionCompose)
self.simulationToolbar.addAction(self.actionSimulationWindow)
self.simulationToolbar.addAction(self.actionRunSimulation)
self.retranslateUi(MainWindow)
@@ -486,6 +491,10 @@ class Ui_MainWindow(object):
#if QT_CONFIG(shortcut)
self.actionRunSimulation.setShortcut(QCoreApplication.translate("MainWindow", u"F6", None))
#endif // QT_CONFIG(shortcut)
self.actionSimulationWindow.setText(QCoreApplication.translate("MainWindow", u"Simulation Window", None))
#if QT_CONFIG(statustip)
self.actionSimulationWindow.setStatusTip(QCoreApplication.translate("MainWindow", u"Show the simulation results window", None))
#endif // QT_CONFIG(statustip)
self.actionNew.setText(QCoreApplication.translate("MainWindow", u"&New", None))
#if QT_CONFIG(statustip)
self.actionNew.setStatusTip(QCoreApplication.translate("MainWindow", u"Create a new document", None))

View File

@@ -11,84 +11,169 @@
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QLabel, QListWidget, QListWidgetItem,
QMainWindow, QProgressBar, QSizePolicy, QTabWidget,
QVBoxLayout, QWidget)
from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
QCursor, QFont, QFontDatabase, QGradient,
QIcon, QImage, QKeySequence, QLinearGradient,
QPainter, QPalette, QPixmap, QRadialGradient,
QTransform)
from PySide6.QtWidgets import (QApplication, QDockWidget, QLabel, QListWidget,
QListWidgetItem, QMainWindow, QMenu, QMenuBar,
QProgressBar, QSizePolicy, QToolBar, QVBoxLayout,
QWidget)
from . import resources_rc
class Ui_SimulationWindow(object):
def setupUi(self, SimulationWindow):
if not SimulationWindow.objectName():
SimulationWindow.setObjectName(u"SimulationWindow")
SimulationWindow.resize(720, 480)
SimulationWindow.resize(900, 650)
self.actionOpen = QAction(SimulationWindow)
self.actionOpen.setObjectName(u"actionOpen")
icon = QIcon()
icon.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionOpen.setIcon(icon)
self.actionSave = QAction(SimulationWindow)
self.actionSave.setObjectName(u"actionSave")
icon1 = QIcon()
icon1.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSave.setIcon(icon1)
self.actionClear = QAction(SimulationWindow)
self.actionClear.setObjectName(u"actionClear")
icon2 = QIcon()
icon2.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionClear.setIcon(icon2)
self.actionExit = QAction(SimulationWindow)
self.actionExit.setObjectName(u"actionExit")
self.actionAbout = QAction(SimulationWindow)
self.actionAbout.setObjectName(u"actionAbout")
self.actionAboutQt = QAction(SimulationWindow)
self.actionAboutQt.setObjectName(u"actionAboutQt")
self.centralWidget = QWidget(SimulationWindow)
self.centralWidget.setObjectName(u"centralWidget")
self.windowLayout = QVBoxLayout(self.centralWidget)
self.windowLayout.setObjectName(u"windowLayout")
self.statusLabel = QLabel(self.centralWidget)
self.statusLabel.setObjectName(u"statusLabel")
self.windowLayout.addWidget(self.statusLabel)
self.progressBar = QProgressBar(self.centralWidget)
self.progressBar.setObjectName(u"progressBar")
self.progressBar.setMaximum(10000)
self.progressBar.setValue(0)
self.windowLayout.addWidget(self.progressBar)
self.timeLabel = QLabel(self.centralWidget)
self.timeLabel.setObjectName(u"timeLabel")
self.windowLayout.addWidget(self.timeLabel)
self.resultsTabs = QTabWidget(self.centralWidget)
self.resultsTabs.setObjectName(u"resultsTabs")
self.logTab = QWidget()
self.logTab.setObjectName(u"logTab")
self.logLayout = QVBoxLayout(self.logTab)
self.logLayout.setObjectName(u"logLayout")
self.messageList = QListWidget(self.logTab)
self.messageList.setObjectName(u"messageList")
self.messageList.setAlternatingRowColors(True)
self.logLayout.addWidget(self.messageList)
self.resultsTabs.addTab(self.logTab, "")
self.resultsTab = QWidget()
self.resultsTab.setObjectName(u"resultsTab")
self.resultsLayout = QVBoxLayout(self.resultsTab)
self.centralWidget.setMaximumSize(QSize(0, 0))
SimulationWindow.setCentralWidget(self.centralWidget)
self.menuBar = QMenuBar(SimulationWindow)
self.menuBar.setObjectName(u"menuBar")
self.menuFile = QMenu(self.menuBar)
self.menuFile.setObjectName(u"menuFile")
self.menuView = QMenu(self.menuBar)
self.menuView.setObjectName(u"menuView")
self.menuPanels = QMenu(self.menuView)
self.menuPanels.setObjectName(u"menuPanels")
self.menuToolbars = QMenu(self.menuView)
self.menuToolbars.setObjectName(u"menuToolbars")
self.menuHelp = QMenu(self.menuBar)
self.menuHelp.setObjectName(u"menuHelp")
SimulationWindow.setMenuBar(self.menuBar)
self.fileToolbar = QToolBar(SimulationWindow)
self.fileToolbar.setObjectName(u"fileToolbar")
self.fileToolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
SimulationWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.fileToolbar)
self.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.resultsTab)
self.resultsPlaceholder = QLabel(self.resultsDockContents)
self.resultsPlaceholder.setObjectName(u"resultsPlaceholder")
self.resultsPlaceholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.resultsLayout.addWidget(self.resultsPlaceholder)
self.resultsTabs.addTab(self.resultsTab, "")
self.resultsDock.setWidget(self.resultsDockContents)
SimulationWindow.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.resultsDock)
self.statusDock = QDockWidget(SimulationWindow)
self.statusDock.setObjectName(u"statusDock")
self.statusDockContents = QWidget()
self.statusDockContents.setObjectName(u"statusDockContents")
self.statusLayout = QVBoxLayout(self.statusDockContents)
self.statusLayout.setObjectName(u"statusLayout")
self.statusLabel = QLabel(self.statusDockContents)
self.statusLabel.setObjectName(u"statusLabel")
self.windowLayout.addWidget(self.resultsTabs)
self.statusLayout.addWidget(self.statusLabel)
SimulationWindow.setCentralWidget(self.centralWidget)
self.progressBar = QProgressBar(self.statusDockContents)
self.progressBar.setObjectName(u"progressBar")
self.progressBar.setMaximum(10000)
self.progressBar.setValue(0)
self.statusLayout.addWidget(self.progressBar)
self.timeLabel = QLabel(self.statusDockContents)
self.timeLabel.setObjectName(u"timeLabel")
self.statusLayout.addWidget(self.timeLabel)
self.statusDock.setWidget(self.statusDockContents)
SimulationWindow.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.statusDock)
self.logDock = QDockWidget(SimulationWindow)
self.logDock.setObjectName(u"logDock")
self.logDockContents = QWidget()
self.logDockContents.setObjectName(u"logDockContents")
self.logLayout = QVBoxLayout(self.logDockContents)
self.logLayout.setObjectName(u"logLayout")
self.messageList = QListWidget(self.logDockContents)
self.messageList.setObjectName(u"messageList")
self.messageList.setAlternatingRowColors(True)
self.logLayout.addWidget(self.messageList)
self.logDock.setWidget(self.logDockContents)
SimulationWindow.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.logDock)
self.menuBar.addAction(self.menuFile.menuAction())
self.menuBar.addAction(self.menuView.menuAction())
self.menuBar.addAction(self.menuHelp.menuAction())
self.menuFile.addAction(self.actionOpen)
self.menuFile.addAction(self.actionSave)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionClear)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionExit)
self.menuView.addAction(self.menuPanels.menuAction())
self.menuView.addAction(self.menuToolbars.menuAction())
self.menuHelp.addAction(self.actionAbout)
self.menuHelp.addAction(self.actionAboutQt)
self.fileToolbar.addAction(self.actionOpen)
self.fileToolbar.addAction(self.actionSave)
self.fileToolbar.addAction(self.actionClear)
self.retranslateUi(SimulationWindow)
self.resultsTabs.setCurrentIndex(0)
QMetaObject.connectSlotsByName(SimulationWindow)
# setupUi
def retranslateUi(self, SimulationWindow):
SimulationWindow.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Simulation", None))
self.actionOpen.setText(QCoreApplication.translate("SimulationWindow", u"&Open\u2026", None))
#if QT_CONFIG(shortcut)
self.actionOpen.setShortcut(QCoreApplication.translate("SimulationWindow", u"Ctrl+O", None))
#endif // QT_CONFIG(shortcut)
self.actionSave.setText(QCoreApplication.translate("SimulationWindow", u"&Save\u2026", None))
#if QT_CONFIG(shortcut)
self.actionSave.setShortcut(QCoreApplication.translate("SimulationWindow", u"Ctrl+S", None))
#endif // QT_CONFIG(shortcut)
self.actionClear.setText(QCoreApplication.translate("SimulationWindow", u"&Clear", None))
self.actionExit.setText(QCoreApplication.translate("SimulationWindow", u"E&xit", None))
#if QT_CONFIG(shortcut)
self.actionExit.setShortcut(QCoreApplication.translate("SimulationWindow", u"Ctrl+W", None))
#endif // QT_CONFIG(shortcut)
self.actionAbout.setText(QCoreApplication.translate("SimulationWindow", u"&About Simulation Window", None))
self.actionAboutQt.setText(QCoreApplication.translate("SimulationWindow", u"About &Qt", None))
self.menuFile.setTitle(QCoreApplication.translate("SimulationWindow", u"&File", None))
self.menuView.setTitle(QCoreApplication.translate("SimulationWindow", u"&View", None))
self.menuPanels.setTitle(QCoreApplication.translate("SimulationWindow", u"&Panels", None))
self.menuToolbars.setTitle(QCoreApplication.translate("SimulationWindow", u"&Toolbars", None))
self.menuHelp.setTitle(QCoreApplication.translate("SimulationWindow", u"&Help", None))
self.fileToolbar.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"File", None))
self.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.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.resultsTabs.setTabText(self.resultsTabs.indexOf(self.logTab), QCoreApplication.translate("SimulationWindow", u"Simulation Log", None))
self.resultsPlaceholder.setText(QCoreApplication.translate("SimulationWindow", u"Simulation graphs and result controls can be added here.", None))
self.resultsTabs.setTabText(self.resultsTabs.indexOf(self.resultsTab), QCoreApplication.translate("SimulationWindow", u"Results", None))
self.logDock.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Log", None))
# retranslateUi

View File

@@ -169,6 +169,7 @@ class MainWindow(QMainWindow):
)
self.ui.actionGraphParameters.triggered.connect(self.show_graph_parameters)
self.ui.actionCompose.triggered.connect(self.compose_active_graph)
self.ui.actionSimulationWindow.triggered.connect(self.show_simulation_window)
self.ui.actionRunSimulation.triggered.connect(self.run_simulation)
self.document_controller.undo_stack.canUndoChanged.connect(self.ui.actionUndo.setEnabled)
self.document_controller.undo_stack.canRedoChanged.connect(self.ui.actionRedo.setEnabled)
@@ -248,15 +249,20 @@ class MainWindow(QMainWindow):
self.log.error("Composition failed: %s", error)
QMessageBox.warning(self, "Cannot compose", str(error))
@Slot()
def show_simulation_window(self) -> None:
self._simulation_window.show()
self._simulation_window.raise_()
self._simulation_window.activateWindow()
@Slot()
def run_simulation(self) -> None:
window = self._simulation_window
callbacks = window.begin_run()
window.show()
window.raise_()
window.activateWindow()
self.show_simulation_window()
try:
self.document_controller.run_simulation(*callbacks)
window.set_model_name(self.simulation.model_name)
except Exception as error:
self.log.exception("Simulation run failed")
window.report_start_error(error)

View File

@@ -1,12 +1,20 @@
import re
from pathlib import Path
from PySide6.QtCore import Signal
from PySide6.QtWidgets import QMainWindow
from PySide6.QtWidgets import QFileDialog, QMainWindow, QMessageBox
from bedit.core.simulation.openmodelica import SimulationMessage, SimulationProgress
from bedit.core.simulation.results import (
SimulationResults,
load_simulation_results,
save_simulation_results,
)
from bedit.gui.generated.ui_simulation_window import Ui_SimulationWindow
class SimulationWindow(QMainWindow):
"""Persistent, reusable view of simulation progress and results."""
"""Persistent viewer for live and previously saved simulation results."""
progressReceived = Signal(object)
messageReceived = Signal(object)
@@ -19,22 +27,37 @@ class SimulationWindow(QMainWindow):
self.ui.setupUi(self)
self._running = False
self._run_generation = 0
self._file_path: Path | None = None
self.results = SimulationResults()
self._connect_actions()
self._populate_view_menu()
self.progressReceived.connect(self._show_progress)
self.messageReceived.connect(self._show_message)
self.simulationFinished.connect(self._show_finished)
self.simulationFailed.connect(self._show_error)
def begin_run(self) -> tuple:
"""Reset the view and return callbacks bound to this particular run."""
def _connect_actions(self) -> None:
self.ui.actionOpen.triggered.connect(self.open_results)
self.ui.actionSave.triggered.connect(self.save_results)
self.ui.actionClear.triggered.connect(self.clear)
self.ui.actionExit.triggered.connect(self.close)
self.ui.actionAbout.triggered.connect(self.show_about)
self.ui.actionAboutQt.triggered.connect(
lambda: QMessageBox.aboutQt(self, "About Qt Framework")
)
self._run_generation += 1
def _populate_view_menu(self) -> None:
for panel in (self.ui.statusDock, self.ui.logDock, self.ui.resultsDock):
self.ui.menuPanels.addAction(panel.toggleViewAction())
self.ui.menuToolbars.addAction(self.ui.fileToolbar.toggleViewAction())
def begin_run(self, model_name: str = "") -> tuple:
"""Reset the viewer and return callbacks bound to this run."""
self.clear(model_name=model_name)
generation = self._run_generation
self._running = True
self.ui.statusLabel.setText("Preparing simulation…")
self.ui.progressBar.setValue(0)
self.ui.timeLabel.setText("Time: 0 s")
self.ui.messageList.clear()
self.clear_results()
return (
lambda progress: self._report_progress(generation, progress),
lambda message: self._report_message(generation, message),
@@ -42,17 +65,110 @@ class SimulationWindow(QMainWindow):
lambda error: self._report_error(generation, error),
)
def clear_results(self) -> None:
"""Clear future plots and result models before a new run.
def set_model_name(self, model_name: str | None) -> None:
if model_name:
self.results.model_name = model_name
self._update_title()
Add graph widgets and their clearing logic here when simulation result
loading and plotting are implemented. The Designer-owned `resultsLayout`
is the intended container for those widgets.
def clear(self, checked: bool = False, *, model_name: str = "") -> None:
"""Discard the displayed run and prepare an empty results document."""
del checked
self._run_generation += 1
self._running = False
self._file_path = None
self.results = SimulationResults(model_name=model_name)
self.ui.statusLabel.setText("No simulation has been run yet.")
self.ui.progressBar.setValue(0)
self.ui.timeLabel.setText("Time: 0 s")
self.ui.messageList.clear()
self.clear_result_views()
self._update_title()
def clear_result_views(self) -> None:
"""Clear custom plots before a run or loaded document is displayed.
Future graph widgets should be placed in the Designer-owned
``resultsLayout`` and reset here.
"""
def _report_progress(
self, generation: int, progress: SimulationProgress
) -> None:
self.ui.resultsPlaceholder.setText(
"Simulation graphs and result controls can be added here."
)
def load_result_views(self) -> None:
"""Populate custom plots from ``self.results.traces``.
This is the intended integration point for a future plotting widget.
"""
trace_count = len(self.results.traces)
if trace_count:
self.ui.resultsPlaceholder.setText(
f"{trace_count} trace(s) loaded; add graph rendering here."
)
def open_results(self) -> None:
file_name, _selected_filter = QFileDialog.getOpenFileName(
self,
"Open Simulation Results",
"",
"BEdit Simulation Results (*.json);;JSON Files (*.json)",
)
if not file_name:
return
try:
results = load_simulation_results(file_name)
except ValueError as error:
QMessageBox.warning(self, "Cannot Open Simulation Results", str(error))
return
self._run_generation += 1
self._running = False
self._file_path = Path(file_name)
self.results = results
self._display_results()
def save_results(self) -> None:
file_path = self._file_path
if file_path is None:
default_name = f"{_safe_file_stem(self.results.model_name)}-results.json"
file_name, _selected_filter = QFileDialog.getSaveFileName(
self,
"Save Simulation Results",
default_name,
"BEdit Simulation Results (*.json);;JSON Files (*.json)",
)
if not file_name:
return
file_path = Path(file_name)
if file_path.suffix.lower() != ".json":
file_path = file_path.with_suffix(".json")
try:
save_simulation_results(file_path, self.results)
except OSError as error:
QMessageBox.warning(self, "Cannot Save Simulation Results", str(error))
return
self._file_path = file_path
self._update_title()
def _display_results(self) -> None:
status = self.results.status
self.ui.statusLabel.setText(str(status.get("phase", "Loaded results")))
self.ui.progressBar.setValue(int(status.get("progress", 0)))
self.ui.timeLabel.setText(f"Time: {float(status.get('time', 0)):g} s")
self.ui.messageList.clear()
for message in self.results.messages:
prefix = message.get("stream") or message.get("type") or "OpenModelica"
self.ui.messageList.addItem(f"{prefix}: {message.get('text', '')}")
self.clear_result_views()
self.load_result_views()
self._update_title()
def _update_title(self) -> None:
name = self.results.model_name or "Simulation"
self.setWindowTitle(f"{name} — Simulation")
def _report_progress(self, generation: int, progress: SimulationProgress) -> None:
if generation == self._run_generation:
self.progressReceived.emit(progress)
@@ -69,34 +185,57 @@ class SimulationWindow(QMainWindow):
self.simulationFailed.emit(str(error))
def report_start_error(self, error: Exception) -> None:
"""Report an error raised before asynchronous callbacks were installed."""
self.simulationFailed.emit(str(error))
def _show_progress(self, progress: SimulationProgress) -> None:
self.results.status = {
"phase": progress.phase,
"currentStepSize": progress.current_step_size,
"time": progress.time,
"progress": progress.progress,
}
self.ui.statusLabel.setText(progress.phase or "Running")
self.ui.timeLabel.setText(f"Time: {progress.time:g} s")
self.ui.progressBar.setValue(max(0, min(10000, progress.progress)))
def _show_message(self, message: SimulationMessage) -> None:
self.results.messages.append(
{"stream": message.stream, "type": message.type, "text": message.text}
)
prefix = message.stream or message.type or "OpenModelica"
self.ui.messageList.addItem(f"{prefix}: {message.text}")
self.ui.messageList.scrollToBottom()
def _show_finished(self, _result) -> None:
def _show_finished(self, result) -> None:
self._running = False
self.results.status.update(phase="Simulation finished", progress=10000)
self.results.metadata["processResult"] = result
self.ui.progressBar.setValue(10000)
self.ui.statusLabel.setText("Simulation finished")
self.load_results()
self.load_result_views()
def _show_error(self, message: str) -> None:
self._running = False
self.results.status["phase"] = "Simulation failed"
self.results.messages.append(
{"stream": "BEdit", "type": "error", "text": message}
)
self.ui.statusLabel.setText("Simulation failed")
self.ui.messageList.addItem(f"Error: {message}")
def load_results(self) -> None:
"""Populate future plots and result controls after a successful run."""
def show_about(self) -> None:
QMessageBox.about(
self,
"About BEdit Simulation",
"<h3>BEdit Simulation</h3>"
"<p>View live progress and open or save simulation results.</p>",
)
@property
def is_running(self) -> bool:
return self._running
def _safe_file_stem(model_name: str) -> str:
stem = re.sub(r"[^A-Za-z0-9_.-]+", "_", model_name).strip("._")
return stem or "simulation"