From 38e3d7aae9e2c29eaac595ae2f441ca6e3ef32e7 Mon Sep 17 00:00:00 2001 From: Joppe Blondel Date: Mon, 20 Jul 2026 18:46:59 +0200 Subject: [PATCH] Added run button and parameter dialog --- BEdit/AGENTS.md | 11 ++ BEdit/pyproject.toml | 1 + BEdit/src/bedit/core/simulation/runner.py | 68 ++++++++ BEdit/src/bedit/core/simulation/service.py | 10 +- BEdit/src/bedit/gui/controllers/commands.py | 13 ++ BEdit/src/bedit/gui/controllers/document.py | 50 ++++++ .../src/bedit/gui/dialogs/graph_parameters.py | 93 +++++++++++ BEdit/src/bedit/gui/dialogs/settings.py | 25 +++ .../generated/ui_graph_parameters_dialog.py | 64 ++++++++ .../src/bedit/gui/generated/ui_main_window.py | 151 ++++++++++-------- .../bedit/gui/generated/ui_settings_dialog.py | 56 ++++++- BEdit/src/bedit/gui/main_window.py | 40 ++++- BEdit/ui/graph_parameters_dialog.ui | 35 ++++ BEdit/ui/main_window.ui | 19 +++ BEdit/ui/settings_dialog.ui | 21 +++ BEdit/untitled.bedit.json | 2 +- 16 files changed, 587 insertions(+), 72 deletions(-) create mode 100644 BEdit/src/bedit/core/simulation/runner.py create mode 100644 BEdit/src/bedit/gui/dialogs/graph_parameters.py create mode 100644 BEdit/src/bedit/gui/generated/ui_graph_parameters_dialog.py create mode 100644 BEdit/ui/graph_parameters_dialog.ui diff --git a/BEdit/AGENTS.md b/BEdit/AGENTS.md index 1be9003..010b527 100644 --- a/BEdit/AGENTS.md +++ b/BEdit/AGENTS.md @@ -111,6 +111,14 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant per component instance from graph connections and exposed while compiling as `$portname_N$`; array connection endpoints receive stable one-based indices in graph connection order. +- Blocking simulator integration belongs in `core/simulation/runner.py` and runs + on its daemon worker thread. Never perform OMPython startup or simulation work + directly on the Qt GUI thread; background failures must go to the application + log. +- The optional OpenModelica executable is persisted as + `simulation/openModelicaPath`. The GUI passes it into `Simulation`; core must + not read `QSettings`. An empty path uses OMPython/PATH discovery, while an + explicit `.../bin/omc` path is converted to the OpenModelica home directory. ## Qt Designer and generated files @@ -151,6 +159,9 @@ pyside6-uic --from-imports ui/text_definition_editor.ui \ pyside6-uic --from-imports ui/simulation_settings_dialog.ui \ -o src/bedit/gui/generated/ui_simulation_settings_dialog.py +pyside6-uic --from-imports ui/graph_parameters_dialog.ui \ + -o src/bedit/gui/generated/ui_graph_parameters_dialog.py + pyside6-uic --from-imports ui/parameter_options_dialog.ui \ -o src/bedit/gui/generated/ui_parameter_options_dialog.py ``` diff --git a/BEdit/pyproject.toml b/BEdit/pyproject.toml index 790c93e..7d1bc00 100644 --- a/BEdit/pyproject.toml +++ b/BEdit/pyproject.toml @@ -11,6 +11,7 @@ requires-python = ">=3.10" dependencies = [ "msgpack>=1.0,<2", "PySide6>=6.7,<7", + "OMPython>4.0", ] [project.optional-dependencies] diff --git a/BEdit/src/bedit/core/simulation/runner.py b/BEdit/src/bedit/core/simulation/runner.py new file mode 100644 index 0000000..9661922 --- /dev/null +++ b/BEdit/src/bedit/core/simulation/runner.py @@ -0,0 +1,68 @@ +import os +from collections.abc import Callable +from pathlib import Path +from threading import Lock, Thread + +from bedit.core.application_log import get_logger + + +log = get_logger(__name__) +_worker_lock = Lock() +_worker: Thread | None = None + + +def run_simulation_async( + openmodelica_path: str = "", task: Callable[[], None] | None = None +) -> None: + """Start one simulation task in a background thread and return immediately.""" + + global _worker + with _worker_lock: + if _worker is not None and _worker.is_alive(): + raise RuntimeError("A simulation is already running") + _worker = Thread( + target=_run_safely, + args=(task or (lambda: _run_openmodelica(openmodelica_path)),), + name="bedit-simulation", + daemon=True, + ) + _worker.start() + + +def simulation_is_running() -> bool: + """Return whether the background simulation worker is active.""" + + with _worker_lock: + return _worker is not None and _worker.is_alive() + + +def _run_safely(task: Callable[[], None]) -> None: + global _worker + try: + task() + except Exception: + log.exception("Simulation run failed") + finally: + with _worker_lock: + _worker = None + + +def _run_openmodelica(openmodelica_path: str = "") -> None: + """Create the OpenModelica session and perform the current runner stub work.""" + + from OMPython import OMCSessionZMQ + + omhome = _openmodelica_home(openmodelica_path) + omc = OMCSessionZMQ(omhome=omhome) + log.info("OpenModelica Version: %s", omc.sendExpression("getVersion()")) + + +def _openmodelica_home(openmodelica_path: str) -> str | None: + """Convert an optional omc executable path to the home expected by OMPython.""" + + if not openmodelica_path.strip(): + return None + path = Path(os.path.expandvars(openmodelica_path)).expanduser() + if path.name.lower() in {"omc", "omc.exe"}: + return str(path.parent.parent) + return str(path) diff --git a/BEdit/src/bedit/core/simulation/service.py b/BEdit/src/bedit/core/simulation/service.py index fddb3a0..389eb87 100644 --- a/BEdit/src/bedit/core/simulation/service.py +++ b/BEdit/src/bedit/core/simulation/service.py @@ -2,6 +2,7 @@ from typing import Any from bedit.core.application_log import get_logger from bedit.core.simulation.compiler import compile_graph +from bedit.core.simulation.runner import run_simulation_async log = get_logger(__name__) @@ -10,11 +11,12 @@ log = get_logger(__name__) class Simulation: """Application-owned simulation state and compiler facade.""" - def __init__(self) -> None: + def __init__(self, *, openmodelica_path: str = "") -> None: self.state: dict[str, Any] = {} self.last_compilation_input: dict[str, Any] | None = None self.last_compilation_output: str | None = None self.id_list: dict[str, Any] = {} + self.openmodelica_path = openmodelica_path def compile(self, graph: dict[str, Any]) -> None: """Compile a serialized component tree and retain the result.""" @@ -24,4 +26,8 @@ class Simulation: self.id_list = result.objects_by_id self.last_compilation_output = result.modelica log.info("Generated Modelica model:\n%s", self.last_compilation_output) - log.info("Simulation settings:\n%s", self.last_compilation_input['implementation']['graph']['simulation']) + + def run_simulation(self) -> None: + """Start the simulation without blocking the calling UI thread.""" + + run_simulation_async(self.openmodelica_path) diff --git a/BEdit/src/bedit/gui/controllers/commands.py b/BEdit/src/bedit/gui/controllers/commands.py index 155a805..b1a36f4 100644 --- a/BEdit/src/bedit/gui/controllers/commands.py +++ b/BEdit/src/bedit/gui/controllers/commands.py @@ -150,6 +150,19 @@ class EditSimulationSettingsCommand(QUndoCommand): self.controller._set_simulation_settings(self.owner_id, self.old) +class EditGraphParametersCommand(QUndoCommand): + def __init__(self, controller, old: dict, new: dict) -> None: + super().__init__("Edit graph parameters") + self.controller = controller + self.old, self.new = old, new + + def redo(self) -> None: + self.controller._set_graph_parameters(self.new) + + def undo(self) -> None: + self.controller._set_graph_parameters(self.old) + + class DeleteAnnotationsCommand(QUndoCommand): def __init__(self, controller, owner_id: str, annotations: dict[str, Annotation]) -> None: super().__init__("Delete annotations") diff --git a/BEdit/src/bedit/gui/controllers/document.py b/BEdit/src/bedit/gui/controllers/document.py index ce12725..21c94e1 100644 --- a/BEdit/src/bedit/gui/controllers/document.py +++ b/BEdit/src/bedit/gui/controllers/document.py @@ -13,6 +13,7 @@ from bedit.gui.controllers.commands import ( DeleteSelectionCommand, DeleteAnnotationsCommand, EditGraphItemCommand, + EditGraphParametersCommand, EditSimulationSettingsCommand, EditTextDefinitionCommand, EditComponentAppearanceCommand, @@ -393,12 +394,45 @@ class DocumentController(QObject): EditSimulationSettingsCommand(self, component.id, old, new) ) + def edit_graph_parameter_values( + self, root_id: str, values: dict[str, dict[str, str]] + ) -> None: + root = self.document.find_component(root_id) if self.document else None + if root is None: + return + subtree_ids = {component.id for component in self._component_subtree(root)} + if not set(values) <= subtree_ids: + raise ValueError("Parameter changes contain a component outside the active graph") + + old: dict[str, list[dict]] = {} + new: dict[str, list[dict]] = {} + for component_id, parameter_values in values.items(): + component = self.document.find_component(component_id) + known_ids = {parameter.id for parameter in component.parameters} + if not set(parameter_values) <= known_ids: + raise ValueError(f"Component {component.name!r} contains an unknown parameter") + updated = deepcopy(component.parameters) + for parameter in updated: + if parameter.id in parameter_values: + parameter.value = parameter_values[parameter.id] + old[component_id] = [parameter.to_dict() for parameter in component.parameters] + new[component_id] = [parameter.to_dict() for parameter in updated] + + if old != new: + self.undo_stack.push(EditGraphParametersCommand(self, old, new)) + def compile_active_graph(self) -> None: component = self.active_component if component is None or component.implementation_kind != "graph": raise ValueError("Open a graph component before compiling") self.simulation.compile(component.to_dict()) + def run_simulation(self) -> 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() + def set_route_waypoints(self, item_kind: str, item_id: str, waypoints: list[QPointF]) -> None: item = ( self.active_graph.connections @@ -1082,6 +1116,22 @@ class DocumentController(QObject): owner.graph.simulation_settings = deepcopy(settings) self.documentReset.emit() + def _set_graph_parameters(self, values: dict[str, list[dict]]) -> None: + if self.document is None: + return + changed_text_components: list[str] = [] + for component_id, parameters in values.items(): + component = self.document.find_component(component_id) + if component is None: + continue + component.parameters = [Parameter.from_dict(item) for item in parameters] + if component.implementation_kind == "text": + changed_text_components.append(component_id) + self.document.validate() + self.documentReset.emit() + for component_id in changed_text_components: + self.textDefinitionChanged.emit(component_id) + def _replace_component(self, old_id: str, replacement: Component) -> None: if self.document is None: return diff --git a/BEdit/src/bedit/gui/dialogs/graph_parameters.py b/BEdit/src/bedit/gui/dialogs/graph_parameters.py new file mode 100644 index 0000000..b068eea --- /dev/null +++ b/BEdit/src/bedit/gui/dialogs/graph_parameters.py @@ -0,0 +1,93 @@ +from copy import deepcopy + +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QDialog, QStyledItemDelegate, QTreeWidgetItem + +from bedit.core.model import Component +from bedit.gui.generated.ui_graph_parameters_dialog import Ui_GraphParametersDialog + + +COMPONENT_ID_ROLE = Qt.ItemDataRole.UserRole +PARAMETER_ID_ROLE = Qt.ItemDataRole.UserRole + 1 + + +class ValueColumnDelegate(QStyledItemDelegate): + """Allow editing only in the parameter value column.""" + + def createEditor(self, parent, option, index): # noqa: N802 (Qt API name) + parameter_id = index.siblingAtColumn(0).data(PARAMETER_ID_ROLE) + if index.column() != 2 or parameter_id is None: + return None + return super().createEditor(parent, option, index) + + +class GraphParametersDialog(QDialog): + """Edit every parameter value in an active component subtree.""" + + def __init__(self, root: Component, parent=None) -> None: + super().__init__(parent) + self.ui = Ui_GraphParametersDialog() + self.ui.setupUi(self) + self.ui.parameterTree.setItemDelegate(ValueColumnDelegate(self.ui.parameterTree)) + self.setWindowTitle(f"Graph Parameters — {root.name}") + self._parameters = { + component.id: deepcopy(component.parameters) + for component in self._walk(root) + } + self._populate(root) + self.ui.parameterTree.expandAll() + self.ui.parameterTree.resizeColumnToContents(0) + self.ui.parameterTree.resizeColumnToContents(1) + + @staticmethod + def _walk(component: Component): + yield component + if component.implementation_kind == "graph": + for child in component.graph.blocks.values(): + yield from GraphParametersDialog._walk(child) + + def _populate(self, root: Component) -> None: + self.ui.parameterTree.clear() + + def add_component(component: Component, parent: QTreeWidgetItem | None) -> None: + item = QTreeWidgetItem([component.name, "", ""]) + item.setData(0, COMPONENT_ID_ROLE, component.id) + item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable) + if parent is None: + self.ui.parameterTree.addTopLevelItem(item) + else: + parent.addChild(item) + + for parameter in self._parameters[component.id]: + parameter_item = QTreeWidgetItem( + [parameter.name, parameter.type, parameter.value] + ) + parameter_item.setData(0, COMPONENT_ID_ROLE, component.id) + parameter_item.setData(0, PARAMETER_ID_ROLE, parameter.id) + parameter_item.setFlags( + parameter_item.flags() | Qt.ItemFlag.ItemIsEditable + ) + item.addChild(parameter_item) + + if component.implementation_kind == "graph": + for child in component.graph.blocks.values(): + add_component(child, item) + + add_component(root, None) + + @property + def parameter_values(self) -> dict[str, dict[str, str]]: + values: dict[str, dict[str, str]] = {} + iterator = self.ui.parameterTree.invisibleRootItem() + + def collect(parent: QTreeWidgetItem) -> None: + for index in range(parent.childCount()): + item = parent.child(index) + parameter_id = item.data(0, PARAMETER_ID_ROLE) + if parameter_id is not None: + component_id = item.data(0, COMPONENT_ID_ROLE) + values.setdefault(component_id, {})[parameter_id] = item.text(2) + collect(item) + + collect(iterator) + return values diff --git a/BEdit/src/bedit/gui/dialogs/settings.py b/BEdit/src/bedit/gui/dialogs/settings.py index c1bee8f..d6edea9 100644 --- a/BEdit/src/bedit/gui/dialogs/settings.py +++ b/BEdit/src/bedit/gui/dialogs/settings.py @@ -26,6 +26,7 @@ class SettingsDialog(QDialog): self.ui.addLibraryFileButton.clicked.connect(self._add_library_file) self.ui.addLibraryFolderButton.clicked.connect(self._add_library_folder) self.ui.removeLibraryPathButton.clicked.connect(self._remove_library_path) + self.ui.browseOpenModelicaButton.clicked.connect(self._browse_openmodelica) self.ui.libraryPathsList.itemSelectionChanged.connect(self._update_remove_button) self.ui.syntaxStylesTable.cellDoubleClicked.connect(self._choose_syntax_color) self._load_settings() @@ -46,6 +47,7 @@ class SettingsDialog(QDialog): self.ui.graphGridSpinBox.setValue(self.graph_grid_size(self.settings)) self.ui.graphSnapSpinBox.setValue(self.graph_snap_size(self.settings)) self.ui.iconGridSpinBox.setValue(self.icon_grid_size(self.settings)) + self.ui.openModelicaPathEdit.setText(self.openmodelica_path(self.settings)) self._load_syntax_styles() self._update_remove_button() @@ -130,6 +132,25 @@ class SettingsDialog(QDialog): settings = settings if settings is not None else application_settings() return settings.value("grid/iconSize", 8, type=int) + @staticmethod + def openmodelica_path(settings: QSettings | None = None) -> str: + settings = settings if settings is not None else application_settings() + return str(settings.value("simulation/openModelicaPath", "") or "") + + def _browse_openmodelica(self) -> None: + current = self.ui.openModelicaPathEdit.text().strip() + start = str(Path(current).expanduser().parent) if current else "" + path, _ = QFileDialog.getOpenFileName( + self, + "Select OpenModelica executable", + start, + "OpenModelica compiler (omc omc.exe);;All files (*)", + ) + if path: + # Keep symlink paths such as ~/.local/bin/omc intact; resolving them + # could turn the selected launcher into an unrelated container script. + self.ui.openModelicaPathEdit.setText(str(Path(path).expanduser().absolute())) + def _add_library_file(self) -> None: path, _ = QFileDialog.getOpenFileName( self, @@ -176,6 +197,10 @@ class SettingsDialog(QDialog): self.settings.setValue("grid/graphSize", self.ui.graphGridSpinBox.value()) self.settings.setValue("grid/graphSnapSize", self.ui.graphSnapSpinBox.value()) self.settings.setValue("grid/iconSize", self.ui.iconGridSpinBox.value()) + self.settings.setValue( + "simulation/openModelicaPath", + self.ui.openModelicaPathEdit.text().strip(), + ) for row in range(self.ui.syntaxStylesTable.rowCount()): category = self.ui.syntaxStylesTable.item(row, 0).data( Qt.ItemDataRole.UserRole diff --git a/BEdit/src/bedit/gui/generated/ui_graph_parameters_dialog.py b/BEdit/src/bedit/gui/generated/ui_graph_parameters_dialog.py new file mode 100644 index 0000000..2c081ec --- /dev/null +++ b/BEdit/src/bedit/gui/generated/ui_graph_parameters_dialog.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'graph_parameters_dialog.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 (QAbstractButton, QApplication, QDialog, QDialogButtonBox, + QHeaderView, QLabel, QSizePolicy, QTreeWidget, + QTreeWidgetItem, QVBoxLayout, QWidget) + +class Ui_GraphParametersDialog(object): + def setupUi(self, GraphParametersDialog): + if not GraphParametersDialog.objectName(): + GraphParametersDialog.setObjectName(u"GraphParametersDialog") + GraphParametersDialog.resize(620, 480) + self.dialogLayout = QVBoxLayout(GraphParametersDialog) + self.dialogLayout.setObjectName(u"dialogLayout") + self.descriptionLabel = QLabel(GraphParametersDialog) + self.descriptionLabel.setObjectName(u"descriptionLabel") + + self.dialogLayout.addWidget(self.descriptionLabel) + + self.parameterTree = QTreeWidget(GraphParametersDialog) + self.parameterTree.setObjectName(u"parameterTree") + self.parameterTree.setAlternatingRowColors(True) + self.parameterTree.setRootIsDecorated(True) + self.parameterTree.setColumnCount(3) + + self.dialogLayout.addWidget(self.parameterTree) + + self.buttonBox = QDialogButtonBox(GraphParametersDialog) + self.buttonBox.setObjectName(u"buttonBox") + self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok) + + self.dialogLayout.addWidget(self.buttonBox) + + + self.retranslateUi(GraphParametersDialog) + self.buttonBox.accepted.connect(GraphParametersDialog.accept) + self.buttonBox.rejected.connect(GraphParametersDialog.reject) + + QMetaObject.connectSlotsByName(GraphParametersDialog) + # setupUi + + def retranslateUi(self, GraphParametersDialog): + GraphParametersDialog.setWindowTitle(QCoreApplication.translate("GraphParametersDialog", u"Graph Parameters", None)) + self.descriptionLabel.setText(QCoreApplication.translate("GraphParametersDialog", u"Edit parameter values throughout the active component tree.", None)) + ___qtreewidgetitem = self.parameterTree.headerItem() + ___qtreewidgetitem.setText(2, QCoreApplication.translate("GraphParametersDialog", u"Value", None)) + ___qtreewidgetitem.setText(1, QCoreApplication.translate("GraphParametersDialog", u"Type", None)) + ___qtreewidgetitem.setText(0, QCoreApplication.translate("GraphParametersDialog", u"Component / Parameter", None)) + # retranslateUi + diff --git a/BEdit/src/bedit/gui/generated/ui_main_window.py b/BEdit/src/bedit/gui/generated/ui_main_window.py index c0eae2d..41d0d2c 100644 --- a/BEdit/src/bedit/gui/generated/ui_main_window.py +++ b/BEdit/src/bedit/gui/generated/ui_main_window.py @@ -36,84 +36,94 @@ class Ui_MainWindow(object): icon = QIcon() icon.addFile(u":/icons/icons/preferences-system.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) self.actionSimulationSettings.setIcon(icon) + self.actionGraphParameters = QAction(MainWindow) + self.actionGraphParameters.setObjectName(u"actionGraphParameters") + icon1 = QIcon() + icon1.addFile(u":/icons/icons/configure.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionGraphParameters.setIcon(icon1) self.actionCompile = QAction(MainWindow) self.actionCompile.setObjectName(u"actionCompile") - icon1 = QIcon() - icon1.addFile(u":/icons/icons/run-build.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionCompile.setIcon(icon1) + icon2 = QIcon() + icon2.addFile(u":/icons/icons/run-build.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionCompile.setIcon(icon2) + self.actionRunSimulation = QAction(MainWindow) + self.actionRunSimulation.setObjectName(u"actionRunSimulation") + icon3 = QIcon() + icon3.addFile(u":/icons/icons/media-playback-start.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionRunSimulation.setIcon(icon3) self.actionNew = QAction(MainWindow) self.actionNew.setObjectName(u"actionNew") - icon2 = QIcon() - icon2.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionNew.setIcon(icon2) + icon4 = QIcon() + icon4.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionNew.setIcon(icon4) self.actionRotateClockwise = QAction(MainWindow) self.actionRotateClockwise.setObjectName(u"actionRotateClockwise") - icon3 = QIcon() - icon3.addFile(u":/icons/icons/transform-rotate.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionRotateClockwise.setIcon(icon3) + icon5 = QIcon() + icon5.addFile(u":/icons/icons/transform-rotate.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionRotateClockwise.setIcon(icon5) self.actionZoomIn = QAction(MainWindow) self.actionZoomIn.setObjectName(u"actionZoomIn") - icon4 = QIcon() - icon4.addFile(u":/icons/icons/zoom-in.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionZoomIn.setIcon(icon4) + icon6 = QIcon() + icon6.addFile(u":/icons/icons/zoom-in.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionZoomIn.setIcon(icon6) self.actionZoomOut = QAction(MainWindow) self.actionZoomOut.setObjectName(u"actionZoomOut") - icon5 = QIcon() - icon5.addFile(u":/icons/icons/zoom-out.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionZoomOut.setIcon(icon5) + icon7 = QIcon() + icon7.addFile(u":/icons/icons/zoom-out.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionZoomOut.setIcon(icon7) self.actionCenterView = QAction(MainWindow) self.actionCenterView.setObjectName(u"actionCenterView") - icon6 = QIcon() - icon6.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionCenterView.setIcon(icon6) + icon8 = QIcon() + icon8.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionCenterView.setIcon(icon8) self.actionOpen = QAction(MainWindow) self.actionOpen.setObjectName(u"actionOpen") - icon7 = QIcon() - icon7.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionOpen.setIcon(icon7) + icon9 = QIcon() + icon9.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionOpen.setIcon(icon9) self.actionReloadLibraries = QAction(MainWindow) self.actionReloadLibraries.setObjectName(u"actionReloadLibraries") self.actionReloadSimulation = QAction(MainWindow) self.actionReloadSimulation.setObjectName(u"actionReloadSimulation") self.actionSave = QAction(MainWindow) self.actionSave.setObjectName(u"actionSave") - icon8 = QIcon() - icon8.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionSave.setIcon(icon8) + icon10 = QIcon() + icon10.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionSave.setIcon(icon10) self.actionSaveAs = QAction(MainWindow) self.actionSaveAs.setObjectName(u"actionSaveAs") - icon9 = QIcon() - icon9.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionSaveAs.setIcon(icon9) + icon11 = QIcon() + icon11.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionSaveAs.setIcon(icon11) self.actionExit = QAction(MainWindow) self.actionExit.setObjectName(u"actionExit") self.actionClose = QAction(MainWindow) self.actionClose.setObjectName(u"actionClose") self.actionUndo = QAction(MainWindow) self.actionUndo.setObjectName(u"actionUndo") - icon10 = QIcon() - icon10.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionUndo.setIcon(icon10) + icon12 = QIcon() + icon12.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionUndo.setIcon(icon12) self.actionRedo = QAction(MainWindow) self.actionRedo.setObjectName(u"actionRedo") - icon11 = QIcon() - icon11.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionRedo.setIcon(icon11) + icon13 = QIcon() + icon13.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionRedo.setIcon(icon13) self.actionCut = QAction(MainWindow) self.actionCut.setObjectName(u"actionCut") - icon12 = QIcon() - icon12.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionCut.setIcon(icon12) + icon14 = QIcon() + icon14.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionCut.setIcon(icon14) self.actionCopy = QAction(MainWindow) self.actionCopy.setObjectName(u"actionCopy") - icon13 = QIcon() - icon13.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionCopy.setIcon(icon13) + icon15 = QIcon() + icon15.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionCopy.setIcon(icon15) self.actionPaste = QAction(MainWindow) self.actionPaste.setObjectName(u"actionPaste") - icon14 = QIcon() - icon14.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionPaste.setIcon(icon14) + icon16 = QIcon() + icon16.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionPaste.setIcon(icon16) self.actionSelectAll = QAction(MainWindow) self.actionSelectAll.setObjectName(u"actionSelectAll") self.actionDelete = QAction(MainWindow) @@ -203,18 +213,18 @@ class Ui_MainWindow(object): self.workspaceHeaderLayout.setContentsMargins(6, 2, 6, 2) self.navigateUpButton = QToolButton(self.workspaceHeader) self.navigateUpButton.setObjectName(u"navigateUpButton") - icon15 = QIcon() - icon15.addFile(u":/icons/icons/arrow-up.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.navigateUpButton.setIcon(icon15) + icon17 = QIcon() + icon17.addFile(u":/icons/icons/arrow-up.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.navigateUpButton.setIcon(icon17) self.workspaceHeaderLayout.addWidget(self.navigateUpButton) self.navigateDownButton = QToolButton(self.workspaceHeader) self.navigateDownButton.setObjectName(u"navigateDownButton") self.navigateDownButton.setEnabled(False) - icon16 = QIcon() - icon16.addFile(u":/icons/icons/arrow-down.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.navigateDownButton.setIcon(icon16) + icon18 = QIcon() + icon18.addFile(u":/icons/icons/arrow-down.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.navigateDownButton.setIcon(icon18) self.workspaceHeaderLayout.addWidget(self.navigateDownButton) @@ -234,9 +244,9 @@ class Ui_MainWindow(object): self.pointerToolButton = QToolButton(self.workspaceHeader) self.pointerToolButton.setObjectName(u"pointerToolButton") - icon17 = QIcon() - icon17.addFile(u":/icons/icons/edit-select.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.pointerToolButton.setIcon(icon17) + icon19 = QIcon() + icon19.addFile(u":/icons/icons/edit-select.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.pointerToolButton.setIcon(icon19) self.pointerToolButton.setCheckable(True) self.pointerToolButton.setChecked(True) @@ -244,43 +254,43 @@ class Ui_MainWindow(object): self.connectToolButton = QToolButton(self.workspaceHeader) self.connectToolButton.setObjectName(u"connectToolButton") - icon18 = QIcon() - icon18.addFile(u":/icons/icons/network-connect.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.connectToolButton.setIcon(icon18) + icon20 = QIcon() + icon20.addFile(u":/icons/icons/network-connect.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.connectToolButton.setIcon(icon20) self.connectToolButton.setCheckable(True) self.workspaceHeaderLayout.addWidget(self.connectToolButton) self.boxToolButton = QToolButton(self.workspaceHeader) self.boxToolButton.setObjectName(u"boxToolButton") - icon19 = QIcon() - icon19.addFile(u":/icons/icons/draw-rectangle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.boxToolButton.setIcon(icon19) + icon21 = QIcon() + icon21.addFile(u":/icons/icons/draw-rectangle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.boxToolButton.setIcon(icon21) self.boxToolButton.setCheckable(True) self.workspaceHeaderLayout.addWidget(self.boxToolButton) self.lineToolButton = QToolButton(self.workspaceHeader) self.lineToolButton.setObjectName(u"lineToolButton") - icon20 = QIcon() - icon20.addFile(u":/icons/icons/draw-bezier-curves.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.lineToolButton.setIcon(icon20) + icon22 = QIcon() + icon22.addFile(u":/icons/icons/draw-bezier-curves.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.lineToolButton.setIcon(icon22) self.lineToolButton.setCheckable(True) self.workspaceHeaderLayout.addWidget(self.lineToolButton) self.textToolButton = QToolButton(self.workspaceHeader) self.textToolButton.setObjectName(u"textToolButton") - icon21 = QIcon() - icon21.addFile(u":/icons/icons/draw-text.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.textToolButton.setIcon(icon21) + icon23 = QIcon() + icon23.addFile(u":/icons/icons/draw-text.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.textToolButton.setIcon(icon23) self.textToolButton.setCheckable(True) self.workspaceHeaderLayout.addWidget(self.textToolButton) self.rotateToolButton = QToolButton(self.workspaceHeader) self.rotateToolButton.setObjectName(u"rotateToolButton") - self.rotateToolButton.setIcon(icon3) + self.rotateToolButton.setIcon(icon5) self.workspaceHeaderLayout.addWidget(self.rotateToolButton) @@ -433,7 +443,9 @@ class Ui_MainWindow(object): self.cameraToolbar.addAction(self.actionZoomOut) self.cameraToolbar.addAction(self.actionCenterView) self.simulationToolbar.addAction(self.actionSimulationSettings) + self.simulationToolbar.addAction(self.actionGraphParameters) self.simulationToolbar.addAction(self.actionCompile) + self.simulationToolbar.addAction(self.actionRunSimulation) self.retranslateUi(MainWindow) @@ -448,6 +460,10 @@ class Ui_MainWindow(object): self.actionSimulationSettings.setText(QCoreApplication.translate("MainWindow", u"Simulation Settings", None)) #if QT_CONFIG(statustip) self.actionSimulationSettings.setStatusTip(QCoreApplication.translate("MainWindow", u"Edit settings stored in the active graph", None)) +#endif // QT_CONFIG(statustip) + self.actionGraphParameters.setText(QCoreApplication.translate("MainWindow", u"Graph Parameters", None)) +#if QT_CONFIG(statustip) + self.actionGraphParameters.setStatusTip(QCoreApplication.translate("MainWindow", u"Edit parameters throughout the active graph", None)) #endif // QT_CONFIG(statustip) self.actionCompile.setText(QCoreApplication.translate("MainWindow", u"Compile", None)) #if QT_CONFIG(statustip) @@ -455,6 +471,13 @@ class Ui_MainWindow(object): #endif // QT_CONFIG(statustip) #if QT_CONFIG(shortcut) self.actionCompile.setShortcut(QCoreApplication.translate("MainWindow", u"F5", None)) +#endif // QT_CONFIG(shortcut) + self.actionRunSimulation.setText(QCoreApplication.translate("MainWindow", u"Run", None)) +#if QT_CONFIG(statustip) + self.actionRunSimulation.setStatusTip(QCoreApplication.translate("MainWindow", u"Run the simulation", None)) +#endif // QT_CONFIG(statustip) +#if QT_CONFIG(shortcut) + self.actionRunSimulation.setShortcut(QCoreApplication.translate("MainWindow", u"F6", None)) #endif // QT_CONFIG(shortcut) self.actionNew.setText(QCoreApplication.translate("MainWindow", u"&New", None)) #if QT_CONFIG(statustip) diff --git a/BEdit/src/bedit/gui/generated/ui_settings_dialog.py b/BEdit/src/bedit/gui/generated/ui_settings_dialog.py index 7e276a1..3c46d41 100644 --- a/BEdit/src/bedit/gui/generated/ui_settings_dialog.py +++ b/BEdit/src/bedit/gui/generated/ui_settings_dialog.py @@ -17,10 +17,10 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, QPalette, QPixmap, QRadialGradient, QTransform) from PySide6.QtWidgets import (QAbstractButton, QAbstractItemView, QApplication, QDialog, QDialogButtonBox, QFormLayout, QGroupBox, QHBoxLayout, - QHeaderView, QLabel, QListWidget, QListWidgetItem, - QPushButton, QSizePolicy, QSpacerItem, QSpinBox, - QTabWidget, QTableWidget, QTableWidgetItem, QVBoxLayout, - QWidget) + QHeaderView, QLabel, QLineEdit, QListWidget, + QListWidgetItem, QPushButton, QSizePolicy, QSpacerItem, + QSpinBox, QTabWidget, QTableWidget, QTableWidgetItem, + QVBoxLayout, QWidget) class Ui_SettingsDialog(object): def setupUi(self, SettingsDialog): @@ -104,6 +104,48 @@ class Ui_SettingsDialog(object): self.generalLayout.addItem(self.generalSpacer) self.settingsTabs.addTab(self.generalTab, "") + self.simulationTab = QWidget() + self.simulationTab.setObjectName(u"simulationTab") + self.simulationTabLayout = QVBoxLayout(self.simulationTab) + self.simulationTabLayout.setObjectName(u"simulationTabLayout") + self.openModelicaGroupBox = QGroupBox(self.simulationTab) + self.openModelicaGroupBox.setObjectName(u"openModelicaGroupBox") + self.openModelicaLayout = QVBoxLayout(self.openModelicaGroupBox) + self.openModelicaLayout.setObjectName(u"openModelicaLayout") + self.openModelicaPathLabel = QLabel(self.openModelicaGroupBox) + self.openModelicaPathLabel.setObjectName(u"openModelicaPathLabel") + + self.openModelicaLayout.addWidget(self.openModelicaPathLabel) + + self.openModelicaPathLayout = QHBoxLayout() + self.openModelicaPathLayout.setObjectName(u"openModelicaPathLayout") + self.openModelicaPathEdit = QLineEdit(self.openModelicaGroupBox) + self.openModelicaPathEdit.setObjectName(u"openModelicaPathEdit") + + self.openModelicaPathLayout.addWidget(self.openModelicaPathEdit) + + self.browseOpenModelicaButton = QPushButton(self.openModelicaGroupBox) + self.browseOpenModelicaButton.setObjectName(u"browseOpenModelicaButton") + + self.openModelicaPathLayout.addWidget(self.browseOpenModelicaButton) + + + self.openModelicaLayout.addLayout(self.openModelicaPathLayout) + + self.openModelicaHintLabel = QLabel(self.openModelicaGroupBox) + self.openModelicaHintLabel.setObjectName(u"openModelicaHintLabel") + self.openModelicaHintLabel.setWordWrap(True) + + self.openModelicaLayout.addWidget(self.openModelicaHintLabel) + + + self.simulationTabLayout.addWidget(self.openModelicaGroupBox) + + self.simulationSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.simulationTabLayout.addItem(self.simulationSpacer) + + self.settingsTabs.addTab(self.simulationTab, "") self.syntaxTab = QWidget() self.syntaxTab.setObjectName(u"syntaxTab") self.syntaxTabLayout = QVBoxLayout(self.syntaxTab) @@ -205,6 +247,12 @@ class Ui_SettingsDialog(object): self.iconGridLabel.setText(QCoreApplication.translate("SettingsDialog", u"Icon grid size:", None)) self.iconGridSpinBox.setSuffix(QCoreApplication.translate("SettingsDialog", u" units", None)) self.settingsTabs.setTabText(self.settingsTabs.indexOf(self.generalTab), QCoreApplication.translate("SettingsDialog", u"General", None)) + self.openModelicaGroupBox.setTitle(QCoreApplication.translate("SettingsDialog", u"OpenModelica", None)) + self.openModelicaPathLabel.setText(QCoreApplication.translate("SettingsDialog", u"OpenModelica executable:", None)) + self.openModelicaPathEdit.setPlaceholderText(QCoreApplication.translate("SettingsDialog", u"Leave empty to find omc on PATH", None)) + self.browseOpenModelicaButton.setText(QCoreApplication.translate("SettingsDialog", u"Browse\u2026", None)) + self.openModelicaHintLabel.setText(QCoreApplication.translate("SettingsDialog", u"Select the omc executable, for example ~/.local/bin/omc.", None)) + self.settingsTabs.setTabText(self.settingsTabs.indexOf(self.simulationTab), QCoreApplication.translate("SettingsDialog", u"Simulation", None)) self.syntaxHintLabel.setText(QCoreApplication.translate("SettingsDialog", u"Double-click a colour cell to choose a colour. Word lists remain editable in data/syntax/openmodelica.json.", None)) ___qtablewidgetitem = self.syntaxStylesTable.horizontalHeaderItem(0) ___qtablewidgetitem.setText(QCoreApplication.translate("SettingsDialog", u"Expression type", None)) diff --git a/BEdit/src/bedit/gui/main_window.py b/BEdit/src/bedit/gui/main_window.py index 95ad7de..b3cd7c2 100644 --- a/BEdit/src/bedit/gui/main_window.py +++ b/BEdit/src/bedit/gui/main_window.py @@ -18,6 +18,7 @@ from bedit.core.serializer import DocumentSerializer from bedit.core.simulation import Simulation from bedit.gui.controllers.document import DocumentController from bedit.gui.dialogs.component_options import ComponentOptionsDialog +from bedit.gui.dialogs.graph_parameters import GraphParametersDialog from bedit.gui.dialogs.item_options import ItemOptionsDialog from bedit.gui.models.library_repository import LibraryRepository from bedit.gui.models.library_tree import ( @@ -55,7 +56,9 @@ class MainWindow(QMainWindow): self._applying_text_definition = False self.libraries = LibraryRepository(self) - self.simulation = Simulation() + self.simulation = Simulation( + openmodelica_path=SettingsDialog.openmodelica_path(self.settings) + ) self.document_controller = DocumentController(self, simulation=self.simulation) self.library_tree_model = LibraryTreeModel( self.libraries, @@ -162,7 +165,9 @@ class MainWindow(QMainWindow): self.ui.actionSimulationSettings.triggered.connect( self.show_simulation_settings ) + self.ui.actionGraphParameters.triggered.connect(self.show_graph_parameters) self.ui.actionCompile.triggered.connect(self.compile_active_graph) + 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) self.document_controller.modifiedChanged.connect(lambda _modified: self._update_title()) @@ -218,6 +223,21 @@ class MainWindow(QMainWindow): if dialog.exec() == dialog.DialogCode.Accepted: self.document_controller.edit_simulation_settings(dialog.settings) + @Slot() + def show_graph_parameters(self) -> None: + component = self.document_controller.active_component + if component is None or component.implementation_kind != "graph": + return + dialog = GraphParametersDialog(component, self) + if dialog.exec() == dialog.DialogCode.Accepted: + try: + self.document_controller.edit_graph_parameter_values( + component.id, dialog.parameter_values + ) + except ValueError as error: + self.log.error("Could not change graph parameters: %s", error) + QMessageBox.warning(self, "Cannot change graph parameters", str(error)) + @Slot() def compile_active_graph(self) -> None: try: @@ -226,6 +246,14 @@ class MainWindow(QMainWindow): self.log.error("Compile failed: %s", error) QMessageBox.warning(self, "Cannot compile", str(error)) + @Slot() + def run_simulation(self) -> None: + try: + self.document_controller.run_simulation() + except Exception as error: + self.log.exception("Simulation run failed") + QMessageBox.warning(self, "Cannot run simulation", str(error)) + def _restore_window_geometry(self) -> None: geometry = self.settings.value("window/geometry") if geometry is not None: @@ -252,7 +280,9 @@ class MainWindow(QMainWindow): self.ui.workspaceStack.setCurrentWidget(self.ui.emptyPage) self._set_graph_controls_visible(False) self.ui.actionSimulationSettings.setEnabled(False) + self.ui.actionGraphParameters.setEnabled(False) self.ui.actionCompile.setEnabled(False) + self.ui.actionRunSimulation.setEnabled(False) self._update_edit_actions() return self.ui.graphBreadcrumbLabel.setText(" › ".join(self.document_controller.breadcrumb())) @@ -261,7 +291,9 @@ class MainWindow(QMainWindow): ) is_graph = component.implementation_kind == "graph" self.ui.actionSimulationSettings.setEnabled(is_graph) + self.ui.actionGraphParameters.setEnabled(is_graph) self.ui.actionCompile.setEnabled(is_graph) + self.ui.actionRunSimulation.setEnabled(is_graph) self.ui.workspaceModeLabel.setText("Graph" if is_graph else "Text") self.ui.workspaceStack.setCurrentWidget( self.ui.graphPage if is_graph else self.ui.textPage @@ -483,8 +515,14 @@ class MainWindow(QMainWindow): dialog = SettingsDialog(self) dialog.settingsChanged.connect(self.reload_libraries) dialog.settingsChanged.connect(self.refresh_editor_settings) + dialog.settingsChanged.connect(self.refresh_simulation_preferences) dialog.exec() + def refresh_simulation_preferences(self) -> None: + self.simulation.openmodelica_path = SettingsDialog.openmodelica_path( + self.settings + ) + def refresh_editor_settings(self) -> None: scene = self.ui.graphView.scene() if scene is not None: diff --git a/BEdit/ui/graph_parameters_dialog.ui b/BEdit/ui/graph_parameters_dialog.ui new file mode 100644 index 0000000..c595d44 --- /dev/null +++ b/BEdit/ui/graph_parameters_dialog.ui @@ -0,0 +1,35 @@ + + + GraphParametersDialog + + 00620480 + Graph Parameters + + + + Edit parameter values throughout the active component tree. + + + + + true + true + 3 + Component / Parameter + Type + Value + + + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + + + + + buttonBoxaccepted()GraphParametersDialogaccept() + buttonBoxrejected()GraphParametersDialogreject() + + diff --git a/BEdit/ui/main_window.ui b/BEdit/ui/main_window.ui index 13c6069..884311c 100644 --- a/BEdit/ui/main_window.ui +++ b/BEdit/ui/main_window.ui @@ -633,7 +633,9 @@ false + + @@ -647,6 +649,14 @@ Edit settings stored in the active graph + + + + :/icons/icons/configure.png:/icons/icons/configure.png + + Graph Parameters + Edit parameters throughout the active graph + @@ -662,6 +672,15 @@ F5 + + + + :/icons/icons/media-playback-start.png:/icons/icons/media-playback-start.png + + Run + Run the simulation + F6 + diff --git a/BEdit/ui/settings_dialog.ui b/BEdit/ui/settings_dialog.ui index 80544b8..ae31987 100644 --- a/BEdit/ui/settings_dialog.ui +++ b/BEdit/ui/settings_dialog.ui @@ -86,6 +86,27 @@ + + Simulation + + + + OpenModelica + + OpenModelica executable: + + + Leave empty to find omc on PATH + Browse… + + + Select the omc executable, for example ~/.local/bin/omc.true + + + + Qt::Orientation::Vertical2040 + + Text highlighting diff --git a/BEdit/untitled.bedit.json b/BEdit/untitled.bedit.json index 958e5a8..6602f5f 100644 --- a/BEdit/untitled.bedit.json +++ b/BEdit/untitled.bedit.json @@ -96,7 +96,7 @@ "id": "parameter-022177fc", "name": "v", "type": "real", - "value": "1" + "value": "2" } ], "icon": {