Source code for pySimBlocks.gui.widgets.toolbar_view

# ******************************************************************************
#                                  pySimBlocks
#                     Copyright (c) 2026 Université de Lille & INRIA
# ******************************************************************************
#  This program is free software: you can redistribute it and/or modify it
#  under the terms of the GNU Lesser General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or (at your
#  option) any later version.
#
#  This program is distributed in the hope that it will be useful, but WITHOUT
#  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
#  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
#  for more details.
#
#  You should have received a copy of the GNU Lesser General Public License
#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
# ******************************************************************************
#  Authors: see Authors.txt
# ******************************************************************************

from __future__ import annotations

from PySide6.QtWidgets import QToolBar, QMessageBox, QProgressDialog, QApplication, QToolButton
from PySide6.QtGui import QAction
from PySide6.QtCore import Qt
from shiboken6 import isValid

from pySimBlocks.gui.dialogs.display_yaml_dialog import DisplayYamlDialog
from pySimBlocks.gui.dialogs.settings_dialog import SettingsDialog
from pySimBlocks.gui.project_controller import ProjectController
from pySimBlocks.gui.services.project_saver import ProjectSaver
from pySimBlocks.gui.services.simulation_runner import SimulationRunner

# Add ons
from pySimBlocks.gui.addons.sofa.sofa_dialog import SofaDialog
from pySimBlocks.gui.addons.sofa.sofa_service import SofaService

[docs] class ToolBarView(QToolBar): """Application toolbar providing save, run, plot, and add-on actions. Attributes: saver: Service used to persist the project to disk. runner: Service used to launch a simulation. project_controller: Controller coordinating model and view mutations. sofa_service: Service managing SOFA-specific operations. sofa_action: Toolbar action for opening the SOFA dialog. """ def __init__( self, saver: ProjectSaver, runner: SimulationRunner, project_controller: ProjectController, ): """Initialize the ToolBarView and register all toolbar actions. Args: saver: Service used to save the project to YAML. runner: Service used to launch a simulation. project_controller: Controller coordinating model and view mutations. Raises: None. """ super().__init__() self.setToolButtonStyle(Qt.ToolButtonTextOnly) self.saver = saver self.runner = runner self.project_controller = project_controller self._plot_dialog = None save_action = QAction("Save", self) save_action.triggered.connect(self.on_save) self.addAction(save_action) self.addSeparator() self.undo_button = QToolButton(self) self.undo_button.setText("Undo") self.undo_button.setToolButtonStyle(Qt.ToolButtonTextOnly) self.undo_button.clicked.connect(self.project_controller.undo_manager.undo) self.undo_button.clicked.connect(self._focus_view_after_history_action) self.addWidget(self.undo_button) self.redo_button = QToolButton(self) self.redo_button.setText("Redo") self.redo_button.setToolButtonStyle(Qt.ToolButtonTextOnly) self.redo_button.clicked.connect(self.project_controller.undo_manager.redo) self.redo_button.clicked.connect(self._focus_view_after_history_action) self.addWidget(self.redo_button) self.project_controller.undo_manager.stack.canUndoChanged.connect( self.undo_button.setEnabled ) self.project_controller.undo_manager.stack.canRedoChanged.connect( self.redo_button.setEnabled ) self.undo_button.setEnabled(self.project_controller.undo_manager.stack.canUndo()) self.redo_button.setEnabled(self.project_controller.undo_manager.stack.canRedo()) self.addSeparator() export_action = QAction("Export", self) export_action.triggered.connect(self.on_export_project) self.addAction(export_action) display_action = QAction("Project YAML", self) display_action.triggered.connect(self.on_open_display_yaml) self.addAction(display_action) sim_settings_action = QAction("Settings", self) sim_settings_action.triggered.connect(self.on_open_simulation_settings) self.addAction(sim_settings_action) run_action = QAction("Run", self) run_action.triggered.connect(self.on_run_sim) self.addAction(run_action) plot_action = QAction("Plot", self) plot_action.triggered.connect(self.on_plot_logs) self.addAction(plot_action) # add ons self.sofa_service = SofaService(self.project_controller.project_state, self.project_controller) self.sofa_action = QAction("Sofa", self) self.sofa_action.triggered.connect(self.on_open_sofa_dialog) self.addAction(self.sofa_action) # -------------------------------------------------------------------------- # Public Methods # --------------------------------------------------------------------------
[docs] def on_save(self) -> None: """Save the project and clear the dirty flag.""" window = self.window() if hasattr(window, "save_project"): window.save_project() return self.saver.save(self.project_controller.project_state, self.project_controller.view.block_items) self.project_controller.clear_dirty()
[docs] def on_export_project(self) -> None: """Prompt to save if dirty, then export the project.""" window = self.parent() if window.confirm_discard_or_save("exporting"): self.saver.export(self.project_controller.project_state, self.project_controller.view.block_items)
[docs] def on_open_display_yaml(self) -> None: """Open the YAML viewer dialog for the current project.""" dialog = DisplayYamlDialog(self.project_controller.project_state, self.project_controller.view) dialog.exec()
[docs] def on_open_simulation_settings(self) -> None: """Open the simulation settings dialog.""" dialog = SettingsDialog(self.project_controller.project_state, self.project_controller, self.parent()) dialog.exec()
[docs] def on_run_sim(self) -> None: """Run the simulation with a busy progress dialog.""" dlg = QProgressDialog(self) dlg.setWindowTitle("Simulation") dlg.setLabelText("Running simulation...\nPlease wait.") dlg.setRange(0, 0) # busy indicator dlg.setCancelButton(None) dlg.setWindowModality(Qt.ApplicationModal) dlg.setMinimumWidth(300) dlg.setMinimumHeight(120) dlg.show() QApplication.processEvents() self.set_running(True) logs, flag, msg = self.runner.run(self.project_controller.project_state) dlg.close() self.set_running(False) self.project_controller.project_state.logs = logs if not flag: QMessageBox.warning( self, "Simulation failed with error", msg, QMessageBox.Ok, ) return dlg = self._plot_dialog if dlg is not None and isValid(dlg): dlg.present()
[docs] def discard_plot_dialog(self) -> None: """Destroy the plot window (e.g. after loading another project).""" dlg = self._plot_dialog self._plot_dialog = None if dlg is not None and isValid(dlg): dlg.close() dlg.deleteLater()
[docs] def on_plot_logs(self) -> None: """Open or restore the plot dialog if simulation logs are available.""" flag, msg = self.project_controller.project_state.can_plot() if not flag: QMessageBox.warning( self, "Plot Error", msg, QMessageBox.Ok, ) return dlg = self._plot_dialog if dlg is None or not isValid(dlg): from pySimBlocks.gui.dialogs.plot_dialog import PlotDialog dlg = PlotDialog( self.project_controller.project_state, self.project_controller, None, ) self._plot_dialog = dlg dlg.present()
[docs] def set_running(self, running: bool) -> None: """Enable or disable all toolbar actions based on the running state. Args: running: True to disable all actions, False to re-enable them. """ for action in self.actions(): action.setEnabled(not running)
[docs] def refresh_sofa_button(self) -> None: """Show or hide the SOFA toolbar button based on project contents.""" if self.project_controller.has_sofa_block(): if self.sofa_action not in self.actions(): self.addAction(self.sofa_action) else: if self.sofa_action in self.actions(): self.removeAction(self.sofa_action)
def _focus_view_after_history_action(self) -> None: """Return keyboard focus to the canvas after undo/redo from toolbar.""" self.project_controller.view.setFocus()
[docs] def on_open_sofa_dialog(self) -> None: """Open the SOFA dialog if SOFA prerequisites are satisfied.""" ok, msg, details = self.sofa_service.can_use_sofa() if not ok: QMessageBox.warning( self, msg, details, QMessageBox.Ok ) return dialog = SofaDialog(self.sofa_service, self.parent()) dialog.exec()