diff --git a/.vscode/launch.json b/.vscode/launch.json index e975683..37552a9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,7 @@ "version": "0.2.0", "configurations": [ { - "name": "Python Debugger: Module", + "name": "BEdit debug", "type": "debugpy", "request": "launch", "module": "bedit_gui", @@ -18,6 +18,22 @@ "args": [ "-f", "${workspaceFolder}/untitled.bedit.json" ] + }, + { + "name": "BEsim debug", + "type": "debugpy", + "request": "launch", + "module": "bedit_gui.simulation_application", + "preLaunchTask": "Qt: Generate files", + "cwd": "${workspaceFolder}", + "env": { + "QT_QPA_PLATFORMTHEME": "qt6ct", + "QT_QPA_PLATFORM": "xcb" + }, + "args": [ + "-s", "run_bondgraph", + "${workspaceFolder}/untitled.besim.json" + ] } ] } \ No newline at end of file diff --git a/src/bedit_gui/controllers/log_controller.py b/src/bedit_gui/controllers/log_controller.py index 9c0124d..55752b8 100644 --- a/src/bedit_gui/controllers/log_controller.py +++ b/src/bedit_gui/controllers/log_controller.py @@ -6,6 +6,7 @@ from PySide6.QtCore import QObject, Signal from bedit_gui.services.application_logging import configure_logging from bedit_gui.views.main_window import MainWindow +from bedit_gui.views.simulation_window import SimulationWindow from bedit_gui.views.models import LogListModel @@ -30,7 +31,7 @@ class LogController(QObject): def __init__( self, - window: MainWindow, + window: MainWindow | SimulationWindow, level: int | str = logging.INFO, ) -> None: super().__init__(window) diff --git a/src/bedit_gui/controllers/simulation_file_controller.py b/src/bedit_gui/controllers/simulation_file_controller.py index 7b03b31..341c36f 100644 --- a/src/bedit_gui/controllers/simulation_file_controller.py +++ b/src/bedit_gui/controllers/simulation_file_controller.py @@ -3,13 +3,18 @@ from __future__ import annotations from pathlib import Path from PySide6.QtCore import QObject, Qt -from PySide6.QtWidgets import QApplication, QFileDialog, QInputDialog, QMessageBox +from PySide6.QtWidgets import QApplication, QDialog, QFileDialog, QInputDialog, QMessageBox from bedit_gui.services import document_files, simulation_files from bedit_gui.services.simulation_loader import compile_simulation_root, component_choices, load_and_compile_bedit +from bedit_gui.models import SimulationDatabase, SimulationID +from bedit_gui.services.application_logging import get_logger from bedit_gui.simulation_models import CompiledModel, SimulationRoot +from bedit_gui.views.dialogs.simulation_settings_dialog import SimulationSettingsDialog from bedit_gui.views.simulation_window import SimulationWindow +logger = get_logger(__name__) + class SimulationFileController(QObject): def __init__(self, window: SimulationWindow) -> None: @@ -21,6 +26,7 @@ class SimulationFileController(QObject): window.ui.actionNew_Simulation_Run.triggered.connect(self.new) window.ui.actionOpen_Simulation_Run.triggered.connect(self.open_dialog) window.ui.actionSave_Simulation_Run.triggered.connect(self.save) + window.ui.actionSimulation_Options.triggered.connect(self.open_simulation_settings) self._update_window() def new(self) -> None: @@ -28,17 +34,23 @@ class SimulationFileController(QObject): self.compiled_model = None self.path = None self._update_window() + logger.info("Created new simulation") def open(self, path: str | Path, *, component: str | None = None, simulation: str | None = None, backed_by_file: bool = True, compiled_model: CompiledModel | None = None) -> None: file_path = Path(path) - if file_path.suffix.lower() == ".bes" or simulation_files.is_simulation_json(file_path): - self.root = simulation_files.load(file_path) - self.compiled_model = compiled_model or self._recompile_with_wait_cursor(self.root) - self.path = file_path if backed_by_file else None - else: - self.root, self.compiled_model = self._compile_with_wait_cursor(file_path, component=component, simulation=simulation) - self.path = None + try: + if file_path.suffix.lower() == ".bes" or simulation_files.is_simulation_json(file_path): + self.root = simulation_files.load(file_path) + self.compiled_model = compiled_model or self._recompile_with_wait_cursor(self.root) + self.path = file_path if backed_by_file else None + else: + self.root, self.compiled_model = self._compile_with_wait_cursor(file_path, component=component, simulation=simulation) + self.path = None + except (OSError, RuntimeError, TypeError, ValueError): + logger.exception("Could not open simulation %s", file_path) + raise self._update_window() + logger.info("Opened simulation: %s", file_path) def open_dialog(self) -> None: filename, _ = QFileDialog.getOpenFileName(self.window, "Open Simulation", "", "Simulation and BEdit files (*.bes *.beb *.json)") @@ -75,11 +87,43 @@ class SimulationFileController(QObject): path = path.with_suffix(".bes") try: simulation_files.save(self.root, path) - except (OSError, ValueError) as exc: + except (OSError, TypeError, ValueError) as exc: + logger.exception("Could not save simulation %s", path) QMessageBox.critical(self.window, "Could not save simulation", str(exc)) return self.path = path self._update_window() + logger.info("Saved simulation: %s", path) + + def open_simulation_settings(self) -> None: + if self.root is None: + return + simulation_id = SimulationID() + database = SimulationDatabase(simulations={simulation_id: self.root.settings}, active_simulation=simulation_id) + components = self._source_components() + dialog = SimulationSettingsDialog(database, [(component_id, path) for component_id, _component, path in components], self.window, show_simulation_list=False) + if dialog.exec() != QDialog.DialogCode.Accepted: + return + updated = dialog.database().simulations[simulation_id] + component_changed = updated.component != self.root.component + self.root.settings = updated + self.root.component = updated.component + self.root.component_path = next((path for component_id, _component, path in components if component_id == updated.component), self.root.component_path) + self.root.settings_name = updated.name + if component_changed: + self.compiled_model = None + self._update_window() + logger.info("Updated simulation settings: %s", updated.name) + + def _source_components(self) -> list[tuple]: + if self.root is not None and self.root.source_document is not None: + try: + return component_choices(document_files.load(self.root.source_document)) + except (OSError, TypeError, ValueError): + logger.warning("Could not load source components from %s", self.root.source_document, exc_info=True) + if self.root is None: + return [] + return [(self.root.component, None, self.root.component_path)] def _compile_with_wait_cursor(self, path: Path, *, component: str | None, simulation: str | None) -> tuple[SimulationRoot, CompiledModel]: QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor) @@ -100,8 +144,10 @@ class SimulationFileController(QObject): self.window.setWindowTitle("BEdit Simulator") self.window.statusBar().showMessage("No simulation loaded") self.window.ui.actionSave_Simulation_Run.setEnabled(False) + self.window.ui.actionSimulation_Options.setEnabled(False) return self.window.setWindowTitle(f"{self.root.settings.name} — BEdit Simulator") compiled = self.compiled_model.executable if self.compiled_model is not None else "not compiled" self.window.statusBar().showMessage(f"{self.root.component_path} · {compiled}") self.window.ui.actionSave_Simulation_Run.setEnabled(True) + self.window.ui.actionSimulation_Options.setEnabled(True) diff --git a/src/bedit_gui/services/application_settings.py b/src/bedit_gui/services/application_settings.py index fe823f3..35e2330 100644 --- a/src/bedit_gui/services/application_settings.py +++ b/src/bedit_gui/services/application_settings.py @@ -25,3 +25,24 @@ class ApplicationSettings: @log_level.setter def log_level(self, level: int) -> None: self._settings.setValue(self.LOG_LEVEL_KEY, level) + +class SimulationApplicationSettings: + """Typed access to persistent BEsim application settings.""" + + LOG_LEVEL_KEY = "logging/level" + DEFAULT_LOG_LEVEL = logging.INFO + + def __init__(self, settings: QSettings | None = None) -> None: + self._settings = settings if settings is not None else QSettings() + + @property + def log_level(self) -> int: + return self._settings.value( + self.LOG_LEVEL_KEY, + self.DEFAULT_LOG_LEVEL, + type=int, + ) + + @log_level.setter + def log_level(self, level: int) -> None: + self._settings.setValue(self.LOG_LEVEL_KEY, level) diff --git a/src/bedit_gui/services/simulation_files.py b/src/bedit_gui/services/simulation_files.py index 91ca959..314b9b1 100644 --- a/src/bedit_gui/services/simulation_files.py +++ b/src/bedit_gui/services/simulation_files.py @@ -67,7 +67,7 @@ def _load_bes(path: Path) -> Mapping[str, Any]: if version != FILE_FORMAT_VERSION: raise ValueError(f"unsupported BES file version {version}") data = msgpack.unpackb(zlib.decompress(payload[header_end:]), raw=False, strict_map_key=False) - except (OSError, ValueError, zlib.error, msgpack.exceptions.MsgpackException) as exc: + except (OSError, ValueError, zlib.error, msgpack.exceptions.UnpackException) as exc: raise ValueError(f"could not read BES simulation {path}: {exc}") from exc if not isinstance(data, Mapping): raise TypeError(f"BES simulation {path} must contain a map") diff --git a/src/bedit_gui/simulation_application.py b/src/bedit_gui/simulation_application.py index d10d252..fc9aabe 100644 --- a/src/bedit_gui/simulation_application.py +++ b/src/bedit_gui/simulation_application.py @@ -6,7 +6,9 @@ import sys from PySide6.QtWidgets import QApplication, QMessageBox from bedit_gui.controllers.simulation_file_controller import SimulationFileController +from bedit_gui.controllers.log_controller import LogController from bedit_gui.simulation_models import CompiledModel +from bedit_gui.services.application_settings import SimulationApplicationSettings from bedit_gui.views.simulation_window import SimulationWindow @@ -31,11 +33,18 @@ def parse_arguments(arguments: list[str] | None = None) -> argparse.Namespace: def main(arguments: list[str] | None = None) -> int: args = parse_arguments(arguments) app = QApplication(sys.argv if arguments is None else [sys.argv[0], *arguments]) - app.setOrganizationName("BEdit") - app.setApplicationName("BEdit Simulator") + + app.setOrganizationName("BEsim") + app.setApplicationName("BEsim") + window = SimulationWindow() controller = SimulationFileController(window) + + settings = SimulationApplicationSettings() + LogController(window, settings.log_level) + window.showMaximized() + if args.file: try: compiled_model = CompiledModel(args.model_name, args.executable, args.working_directory) if args.handoff else None diff --git a/src/bedit_gui/ui/forms/simulation_window.ui b/src/bedit_gui/ui/forms/simulation_window.ui index e857e8f..c3d0eed 100644 --- a/src/bedit_gui/ui/forms/simulation_window.ui +++ b/src/bedit_gui/ui/forms/simulation_window.ui @@ -136,7 +136,7 @@ - + diff --git a/src/bedit_gui/views/dialogs/simulation_settings_dialog.py b/src/bedit_gui/views/dialogs/simulation_settings_dialog.py index a2d8d24..ce1c1ff 100644 --- a/src/bedit_gui/views/dialogs/simulation_settings_dialog.py +++ b/src/bedit_gui/views/dialogs/simulation_settings_dialog.py @@ -4,7 +4,7 @@ from copy import deepcopy from PySide6.QtCore import Qt from PySide6.QtGui import QDoubleValidator -from PySide6.QtWidgets import QDialog, QListWidgetItem, QMessageBox, QWidget +from PySide6.QtWidgets import QDialog, QLayout, QListWidgetItem, QMessageBox, QWidget from bedit_core.models import ComponentID from bedit_gui.models import Simulation, SimulationDatabase, SimulationID, SimulationMethod @@ -14,7 +14,7 @@ from bedit_gui.ui.generated.ui_simulation_settings import Ui_Dialog class SimulationSettingsDialog(QDialog): """Editor for the detached simulation database of a document.""" - def __init__(self, database: SimulationDatabase, components: list[tuple[ComponentID, str]], parent: QWidget | None = None) -> None: + def __init__(self, database: SimulationDatabase, components: list[tuple[ComponentID, str]], parent: QWidget | None = None, *, show_simulation_list: bool = True) -> None: super().__init__(parent) self.ui = Ui_Dialog() @@ -25,6 +25,9 @@ class SimulationSettingsDialog(QDialog): self._components = components self._loading = False + if not show_simulation_list: + self._set_layout_visible(self.ui.simulationListLayout, False) + self.ui.startTimeSpinBox.setRange(-1e12, 1e12) self.ui.simLengthSpinBox.setRange(0.0, 1e12) self.ui.stepSizeSpinBox.setRange(1e-9, 1e12) @@ -196,3 +199,14 @@ class SimulationSettingsDialog(QDialog): self.ui.frame.setEnabled(enabled) self.ui.removeSimulationButton.setEnabled(enabled) self.ui.addSimulationButton.setEnabled(bool(self._components)) + + @classmethod + def _set_layout_visible(cls, layout: QLayout, visible: bool) -> None: + for index in range(layout.count()): + item = layout.itemAt(index) + widget = item.widget() + child_layout = item.layout() + if widget is not None: + widget.setVisible(visible) + elif child_layout is not None: + cls._set_layout_visible(child_layout, visible) diff --git a/src/bedit_gui/views/simulation_window.py b/src/bedit_gui/views/simulation_window.py index 2528ffa..c6e946b 100644 --- a/src/bedit_gui/views/simulation_window.py +++ b/src/bedit_gui/views/simulation_window.py @@ -10,4 +10,4 @@ class SimulationWindow(QMainWindow): super().__init__() self.ui = Ui_SimulationWindow() self.ui.setupUi(self) - self.setWindowTitle("BEdit Simulator") + self.setWindowTitle("BEsim") diff --git a/simulation.besim.json b/untitled.besim.json similarity index 100% rename from simulation.besim.json rename to untitled.besim.json