Start of a sim window

This commit is contained in:
2026-07-21 13:54:57 +02:00
parent 6fb2478589
commit cdfc891980
10 changed files with 646 additions and 74 deletions

View File

@@ -427,11 +427,23 @@ class DocumentController(QObject):
raise ValueError("Open a graph component before composing")
self.simulation.compose(component.to_dict())
def run_simulation(self) -> None:
def run_simulation(
self,
progress_callback=None,
message_callback=None,
callback=None,
error_callback=None,
) -> None:
component = self.active_component
if component is None or component.implementation_kind != "graph":
raise ValueError("Open a graph component before running a simulation")
self.simulation.run_simulation(component.to_dict())
self.simulation.run_simulation(
component.to_dict(),
progress_callback,
message_callback,
callback,
error_callback,
)
def set_route_waypoints(self, item_kind: str, item_id: str, waypoints: list[QPointF]) -> None:
item = (

View File

@@ -0,0 +1,94 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'simulation_window.ui'
##
## Created by: Qt User Interface Compiler version 6.11.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (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)
class Ui_SimulationWindow(object):
def setupUi(self, SimulationWindow):
if not SimulationWindow.objectName():
SimulationWindow.setObjectName(u"SimulationWindow")
SimulationWindow.resize(720, 480)
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.resultsLayout.setObjectName(u"resultsLayout")
self.resultsPlaceholder = QLabel(self.resultsTab)
self.resultsPlaceholder.setObjectName(u"resultsPlaceholder")
self.resultsPlaceholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.resultsLayout.addWidget(self.resultsPlaceholder)
self.resultsTabs.addTab(self.resultsTab, "")
self.windowLayout.addWidget(self.resultsTabs)
SimulationWindow.setCentralWidget(self.centralWidget)
self.retranslateUi(SimulationWindow)
self.resultsTabs.setCurrentIndex(0)
QMetaObject.connectSlotsByName(SimulationWindow)
# setupUi
def retranslateUi(self, SimulationWindow):
SimulationWindow.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Simulation", 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))
# retranslateUi

View File

@@ -32,6 +32,7 @@ from bedit.gui.dialogs.settings import SettingsDialog
from bedit.gui.dialogs.port_options import PortOptionsDialog
from bedit.gui.dialogs.parameter_options import ParameterOptionsDialog
from bedit.gui.dialogs.simulation_settings import SimulationSettingsDialog
from bedit.gui.simulation_window import SimulationWindow
from bedit.gui.preferences import application_settings
from bedit.gui.simulation_reload import reload_simulation
from bedit.gui.generated.ui_main_window import Ui_MainWindow
@@ -54,6 +55,7 @@ class MainWindow(QMainWindow):
self.log.info("BEdit started")
self.settings = application_settings()
self._applying_text_definition = False
self._simulation_window = SimulationWindow(self)
self.libraries = LibraryRepository(self)
self.simulation = Simulation(
@@ -248,11 +250,16 @@ class MainWindow(QMainWindow):
@Slot()
def run_simulation(self) -> None:
window = self._simulation_window
callbacks = window.begin_run()
window.show()
window.raise_()
window.activateWindow()
try:
self.document_controller.run_simulation()
self.document_controller.run_simulation(*callbacks)
except Exception as error:
self.log.exception("Simulation run failed")
QMessageBox.warning(self, "Cannot run simulation", str(error))
window.report_start_error(error)
def _restore_window_geometry(self) -> None:
geometry = self.settings.value("window/geometry")

View File

@@ -0,0 +1,102 @@
from PySide6.QtCore import Signal
from PySide6.QtWidgets import QMainWindow
from bedit.core.simulation.openmodelica import SimulationMessage, SimulationProgress
from bedit.gui.generated.ui_simulation_window import Ui_SimulationWindow
class SimulationWindow(QMainWindow):
"""Persistent, reusable view of simulation progress and results."""
progressReceived = Signal(object)
messageReceived = Signal(object)
simulationFinished = Signal(object)
simulationFailed = Signal(str)
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.ui = Ui_SimulationWindow()
self.ui.setupUi(self)
self._running = False
self._run_generation = 0
self.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."""
self._run_generation += 1
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),
lambda result: self._report_finished(generation, result),
lambda error: self._report_error(generation, error),
)
def clear_results(self) -> None:
"""Clear future plots and result models before a new run.
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 _report_progress(
self, generation: int, progress: SimulationProgress
) -> None:
if generation == self._run_generation:
self.progressReceived.emit(progress)
def _report_message(self, generation: int, message: SimulationMessage) -> None:
if generation == self._run_generation:
self.messageReceived.emit(message)
def _report_finished(self, generation: int, result) -> None:
if generation == self._run_generation:
self.simulationFinished.emit(result)
def _report_error(self, generation: int, error: Exception) -> None:
if generation == self._run_generation:
self.simulationFailed.emit(str(error))
def report_start_error(self, error: Exception) -> None:
"""Report an error raised before asynchronous callbacks were installed."""
self.simulationFailed.emit(str(error))
def _show_progress(self, progress: SimulationProgress) -> None:
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:
prefix = message.stream or message.type or "OpenModelica"
self.ui.messageList.addItem(f"{prefix}: {message.text}")
self.ui.messageList.scrollToBottom()
def _show_finished(self, _result) -> None:
self._running = False
self.ui.progressBar.setValue(10000)
self.ui.statusLabel.setText("Simulation finished")
self.load_results()
def _show_error(self, message: str) -> None:
self._running = False
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."""
@property
def is_running(self) -> bool:
return self._running