from __future__ import annotations from pathlib import Path from PySide6.QtCore import QObject, Signal, Qt 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): runtime_changed = Signal() def __init__(self, window: SimulationWindow) -> None: super().__init__(window) self.window = window self.root: SimulationRoot | None = None self.compiled_model: CompiledModel | None = None self.path: Path | None = None 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: self.root = None self.compiled_model = None self.path = None self._update_window() self.runtime_changed.emit() 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) 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._log_compilation() self._update_window() self.runtime_changed.emit() 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)") if not filename: return try: path = Path(filename) if path.suffix.lower() == ".bes" or simulation_files.is_simulation_json(path): self.open(path) return document = document_files.load(path) database = document.metadata.get("simulation_database") if document.metadata is not None else None entries = [(f"Simulation: {simulation.name}", None, simulation.name) for simulation in getattr(database, "simulations", {}).values()] entries.extend((f"Component: {component_path}", component_path, None) for _component_id, _component, component_path in component_choices(document)) if not entries: raise ValueError("the BEdit document contains no components or simulation settings") label, accepted = QInputDialog.getItem(self.window, "Simulation Target", "Compile:", [entry[0] for entry in entries], 0, False) if accepted: _display, component, simulation = next(entry for entry in entries if entry[0] == label) self.open(path, component=component, simulation=simulation) except (OSError, RuntimeError, ValueError) as exc: QMessageBox.critical(self.window, "Could not open simulation", str(exc)) def save(self) -> None: if self.root is None: return path = self.path if path is None: filename, _ = QFileDialog.getSaveFileName(self.window, "Save Simulation", "simulation.bes", "BEdit simulation (*.bes);;Simulation JSON (*.json)") if not filename: return path = Path(filename) if path.suffix.lower() not in (".bes", ".json"): path = path.with_suffix(".bes") try: simulation_files.save(self.root, path) 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 self.root.current_end_time = updated.start_time self.root.results.clear() if component_changed: self.compiled_model = None self._update_window() self.runtime_changed.emit() 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) try: return load_and_compile_bedit(path, component_selector=component, simulation_selector=simulation) finally: QApplication.restoreOverrideCursor() def _recompile_with_wait_cursor(self, root: SimulationRoot) -> CompiledModel: QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor) try: return compile_simulation_root(root) finally: QApplication.restoreOverrideCursor() def _update_window(self) -> None: if self.root is None: 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) def _log_compilation(self) -> None: if self.compiled_model is None: return logger.info("Compiled model %s: %s", self.compiled_model.model_name, self.compiled_model.executable) if self.compiled_model.output.strip(): logger.info("Compiler output:\n%s", self.compiled_model.output.strip()) if self.compiled_model.errors.strip(): logger.warning("Compiler errors:\n%s", self.compiled_model.errors.strip())