From 713d094b08da9810432a1030f0486000e931b344 Mon Sep 17 00:00:00 2001 From: Joppe Blondel Date: Mon, 27 Jul 2026 12:07:47 +0200 Subject: [PATCH] Parameter editor added --- src/bedit_core/models.py | 2 +- src/bedit_gui/application.py | 4 + src/bedit_gui/commands/param_commands.py | 76 ++++++++ .../controllers/document_tree_controller.py | 31 +++- src/bedit_gui/documents/document.py | 34 +++- src/bedit_gui/ui/forms/param_editor_widget.ui | 169 ++++++++++++++++++ .../views/dialogs/param_editor_dialog.py | 36 ++++ src/bedit_gui/views/param_editor_widget.py | 134 ++++++++++++++ 8 files changed, 474 insertions(+), 12 deletions(-) create mode 100644 src/bedit_gui/commands/param_commands.py create mode 100644 src/bedit_gui/ui/forms/param_editor_widget.ui create mode 100644 src/bedit_gui/views/dialogs/param_editor_dialog.py create mode 100644 src/bedit_gui/views/param_editor_widget.py diff --git a/src/bedit_core/models.py b/src/bedit_core/models.py index 66b8b25..7b7027c 100644 --- a/src/bedit_core/models.py +++ b/src/bedit_core/models.py @@ -95,7 +95,7 @@ class BondPort(Port): @dataclass class Parameter: name: str - value: Any = 1.0 + value: Any = "1.0" value_type: ValueType = ValueType.REAL matrix_size: Annotated[list[int], 2] = field(default_factory=lambda: [1, 1]) quantity: str | None = None diff --git a/src/bedit_gui/application.py b/src/bedit_gui/application.py index b6fd4a1..a687706 100644 --- a/src/bedit_gui/application.py +++ b/src/bedit_gui/application.py @@ -25,14 +25,18 @@ def main() -> int: settings = ApplicationSettings() document = Document(app) window = MainWindow() + LogController(window, settings.log_level) DocumentController(document, window) SettingsController(window, settings) UndoController(document, window) ViewMenuController(window) DocumentTreeController(document, window) + window_state_controller = WindowStateController(app, window) window_state_controller.restore() + + document.new() window.showMaximized() return app.exec() diff --git a/src/bedit_gui/commands/param_commands.py b/src/bedit_gui/commands/param_commands.py new file mode 100644 index 0000000..b8beb3f --- /dev/null +++ b/src/bedit_gui/commands/param_commands.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +from copy import deepcopy + +from PySide6.QtGui import QUndoCommand + +from bedit_core.models import Component, Parameter, ParameterID + + +class AddParamCommand(QUndoCommand): + def __init__( + self, + document: object, + component: Component, + param_id: ParameterID, + param: Parameter, + ) -> None: + super().__init__("Add port") + self.document = document + self.component = component + self.param_id = param_id + self.param = deepcopy(param) + + def redo(self) -> None: + self.component.parameters[self.param_id] = deepcopy(self.param) + self.document.model_changed.emit(self.document.model) + + def undo(self) -> None: + del self.component.parameters[self.param_id] + self.document.model_changed.emit(self.document.model) + + +class RemoveParamCommand(QUndoCommand): + def __init__( + self, + document: object, + component: Component, + param_id: ParameterID, + ) -> None: + super().__init__("Remove parameter") + self.document = document + self.component = component + self.param_id = param_id + self.param = deepcopy(component.parameters[param_id]) + + def redo(self) -> None: + del self.component.parameters[self.param_id] + self.document.model_changed.emit(self.document.model) + + def undo(self) -> None: + self.component.parameters[self.param_id] = deepcopy(self.param) + self.document.model_changed.emit(self.document.model) + + +class ChangeParamCommand(QUndoCommand): + def __init__( + self, + document: object, + component: Component, + param_id: ParameterID, + param: Parameter, + ) -> None: + super().__init__("Change parameter") + self.document = document + self.component = component + self.param_id = param_id + self.old_param = deepcopy(component.parameters[param_id]) + self.new_param = deepcopy(param) + + def redo(self) -> None: + self.component.parameters[self.param_id] = deepcopy(self.new_param) + self.document.model_changed.emit(self.document.model) + + def undo(self) -> None: + self.component.parameters[self.param_id] = deepcopy(self.old_param) + self.document.model_changed.emit(self.document.model) diff --git a/src/bedit_gui/controllers/document_tree_controller.py b/src/bedit_gui/controllers/document_tree_controller.py index 2073b5a..49c6a87 100644 --- a/src/bedit_gui/controllers/document_tree_controller.py +++ b/src/bedit_gui/controllers/document_tree_controller.py @@ -4,27 +4,34 @@ from typing import Protocol from PySide6.QtCore import QObject, QPoint, Qt from PySide6.QtWidgets import QDialog, QMenu -from bedit_core.models import Component, Port, PortID +from bedit_core.models import Component, Port, PortID, Parameter, ParameterID from bedit_core.models import Document as CoreDocument from bedit_gui.documents import Document -from bedit_gui.views.dialogs.interface_editor_dialog import ( - InterfaceEditorDialog, -) +from bedit_gui.views.dialogs.interface_editor_dialog import InterfaceEditorDialog +from bedit_gui.views.dialogs.param_editor_dialog import ParamEditorDialog from bedit_gui.views.main_window import MainWindow from bedit_gui.views.models.document_tree_model import DocumentTreeModel class InterfaceEditorLike(Protocol): def exec(self) -> int: ... - def ports(self) -> dict[PortID, Port]: ... +class ParamEditorLike(Protocol): + def exec(self) -> int: ... + def params(self) -> dict[ParameterID, Parameter]: ... + InterfaceEditorFactory = Callable[ [dict[PortID, Port], MainWindow], InterfaceEditorLike, ] +ParamEditorFactory = Callable[ + [dict[ParameterID, Parameter], MainWindow], + ParamEditorLike +] + class DocumentTreeController(QObject): def __init__( @@ -32,6 +39,7 @@ class DocumentTreeController(QObject): document: Document, window: MainWindow, interface_editor_factory: InterfaceEditorFactory = InterfaceEditorDialog, + param_editor_factory: ParamEditorFactory = ParamEditorDialog, ) -> None: super().__init__(window) @@ -39,6 +47,7 @@ class DocumentTreeController(QObject): self.window = window self.model = DocumentTreeModel() self.interface_editor_factory = interface_editor_factory + self.param_editor_factory = param_editor_factory window.ui.documentTree.setModel(self.model) document.model_changed.connect(self._on_document_changed) @@ -71,11 +80,14 @@ class DocumentTreeController(QObject): menu = QMenu(self.window.ui.documentTree) edit_interface = menu.addAction("Edit Interface") + edit_params = menu.addAction("Edit Parameters") selected = menu.exec( self.window.ui.documentTree.viewport().mapToGlobal(position) ) if selected is edit_interface: self._edit_interface(component) + elif selected is edit_params: + self._edit_params(component) def _edit_interface(self, component: Component) -> None: dialog = self.interface_editor_factory( @@ -84,3 +96,12 @@ class DocumentTreeController(QObject): ) if dialog.exec() == QDialog.DialogCode.Accepted: self.document.update_component_ports(component, dialog.ports()) + + def _edit_params(self, component: Component) -> None: + dialog = self.param_editor_factory( + component.parameters, + self.window + ) + if dialog.exec() == QDialog.DialogCode.Accepted: + self.document.update_component_params(component, dialog.params()) + diff --git a/src/bedit_gui/documents/document.py b/src/bedit_gui/documents/document.py index 49a8011..be95abe 100644 --- a/src/bedit_gui/documents/document.py +++ b/src/bedit_gui/documents/document.py @@ -5,9 +5,10 @@ from pathlib import Path from PySide6.QtCore import QObject, Signal from PySide6.QtGui import QUndoStack -from bedit_core.models import ID, Component, Port, PortID +from bedit_core.models import ID, Component, Port, PortID, Parameter, ParameterID from bedit_core.models import Document as CoreDocument from bedit_gui.commands.port_commands import AddPortCommand, ChangePortCommand, RemovePortCommand +from bedit_gui.commands.param_commands import AddParamCommand, ChangeParamCommand, RemoveParamCommand from bedit_gui.commands.rename_component_command import RenameComponentCommand from bedit_gui.commands.rename_document_command import RenameDocumentCommand from bedit_gui.services import document_files @@ -88,11 +89,7 @@ class Document(QObject): def rename_component(self, component: Component, name: str) -> None: self.undo_stack.push(RenameComponentCommand(self, component, name)) - def update_component_ports( - self, - component: Component, - ports: dict[PortID, Port], - ) -> None: + def update_component_ports(self, component: Component, ports: dict[PortID, Port]) -> None: current = component.interface.ports removed = [ RemovePortCommand(self, component, port_id) @@ -115,3 +112,28 @@ class Document(QObject): for command in commands: self.undo_stack.push(command) self.undo_stack.endMacro() + + def update_component_params(self, component: Component, params: dict[ParameterID, Parameter]) -> None: + current = component.parameters + removed = [ + RemoveParamCommand(self, component, param_id) + for param_id in current.keys() - params.keys() + ] + added = [ + AddParamCommand(self, component, param_id, params[param_id]) + for param_id in params.keys() - current.keys() + ] + changed = [ + ChangeParamCommand(self, component, param_id, params[param_id]) + for param_id in current.keys() & params.keys() + if current[param_id] != params[param_id] + ] + commands = [*removed, *added, *changed] + if not commands: + return + + self.undo_stack.beginMacro("Edit parameter") + for command in commands: + self.undo_stack.push(command) + self.undo_stack.endMacro() + diff --git a/src/bedit_gui/ui/forms/param_editor_widget.ui b/src/bedit_gui/ui/forms/param_editor_widget.ui new file mode 100644 index 0000000..df35f2c --- /dev/null +++ b/src/bedit_gui/ui/forms/param_editor_widget.ui @@ -0,0 +1,169 @@ + + + ParamEditor + + + + 0 + 0 + 541 + 241 + + + + Form + + + + + + + + + + + + + Add Parameter + + + + + + + Remove Parameter + + + + + + + + + + + + + + + Name: + + + + + + + + + + Type: + + + + + + + + + + + + + + Size: + + + + + + + + + rows + + + 1 + + + + + + + columns + + + 1 + + + + + + + + + Quantity: + + + + + + + Unit: + + + + + + + + + + Value: + + + + + + + + + QFormLayout::FieldGrowthPolicy::AllNonFixedFieldsGrow + + + + + Description: + + + + + + + + 0 + 0 + + + + + 0 + 20 + + + + + 16777215 + 60 + + + + + + + + + + + + + diff --git a/src/bedit_gui/views/dialogs/param_editor_dialog.py b/src/bedit_gui/views/dialogs/param_editor_dialog.py new file mode 100644 index 0000000..f1d30ec --- /dev/null +++ b/src/bedit_gui/views/dialogs/param_editor_dialog.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from PySide6.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QWidget + +from bedit_core.models import ParameterID, Parameter +from bedit_gui.views.param_editor_widget import ParamEditorWidget + + +class ParamEditorDialog(QDialog): + """Modal wrapper around the reusable param editor widget.""" + + def __init__( + self, + params: dict[ParameterID, Parameter], + parent: QWidget | None = None, + ) -> None: + super().__init__(parent) + self.setWindowTitle("Edit Parameters") + self.resize(550, 280) + + layout = QVBoxLayout(self) + self.editor = ParamEditorWidget(self) + self.editor.set_params(params) + layout.addWidget(self.editor) + + buttons = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, + self, + ) + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + layout.addWidget(buttons) + + def params(self) -> dict[ParameterID, Parameter]: + return self.editor.params() + diff --git a/src/bedit_gui/views/param_editor_widget.py b/src/bedit_gui/views/param_editor_widget.py new file mode 100644 index 0000000..f4837a1 --- /dev/null +++ b/src/bedit_gui/views/param_editor_widget.py @@ -0,0 +1,134 @@ +from __future__ import annotations + +from copy import deepcopy + +from PySide6.QtCore import QItemSelection, Signal +from PySide6.QtGui import QStandardItem, QStandardItemModel +from PySide6.QtWidgets import QWidget + +from bedit_core.models import Parameter, ParameterID, ValueType +from bedit_gui.ui.generated.ui_param_editor_widget import Ui_ParamEditor + + +class ParamEditorWidget(QWidget): + """Reusable editor for a detached draft of a component's parameters.""" + + params_changed = Signal() + + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + + self.ui = Ui_ParamEditor() + self.ui.setupUi(self) + self._params: dict[ParameterID, Parameter] = {} + self._param_ids: list[ParameterID] = [] + self._loading = False + + self.ui.typeComboBox.addItem("Real", ValueType.REAL) + self.ui.typeComboBox.addItem("Integer", ValueType.INT) + self.ui.typeComboBox.addItem("Boolean", ValueType.BOOL) + + self._list_model = QStandardItemModel(self) + self.ui.portList.setModel(self._list_model) + self.ui.portList.selectionModel().selectionChanged.connect(self._selection_changed) + self.ui.addParam.clicked.connect(self._add_param) + self.ui.removeParam.clicked.connect(self._remove_param) + + self.ui.nameEdit.textEdited.connect(self._form_changed) + self.ui.typeComboBox.currentIndexChanged.connect(self._form_changed) + self.ui.valueEdit.textEdited.connect(self._form_changed) + self.ui.widthSize.valueChanged.connect(self._form_changed) + self.ui.heightSize.valueChanged.connect(self._form_changed) + self.ui.descriptionEdit.textChanged.connect(self._form_changed) + + self._set_editor_enabled(False) + + def set_params(self, params: dict[ParameterID, Parameter]) -> None: + self._params = deepcopy(params) + self._param_ids = list(self._params) + self._rebuild_list() + + def params(self) -> dict[ParameterID, Parameter]: + return deepcopy(self._params) + + def _rebuild_list(self, selected_id: ParameterID | None = None) -> None: + self._list_model.clear() + for param_id in self._param_ids: + self._list_model.appendRow(QStandardItem(self._params[param_id].name)) + + if self._param_ids: + if selected_id not in self._params: + selected_id = self._param_ids[0] + index = self._list_model.index(self._param_ids.index(selected_id), 0) + self.ui.portList.setCurrentIndex(index) + else: + self._set_editor_enabled(False) + self.ui.removeParam.setEnabled(False) + + def _selection_changed(self, _selected: QItemSelection, _deselected: QItemSelection) -> None: + param_id = self._selected_param_id() + self.ui.removeParam.setEnabled(param_id is not None) + self._set_editor_enabled(param_id is not None) + if param_id is not None: + self._load_param(self._params[param_id]) + + def _selected_param_id(self) -> ParameterID | None: + index = self.ui.portList.currentIndex() + if not index.isValid() or index.row() >= len(self._param_ids): + return None + return self._param_ids[index.row()] + + def _load_param(self, param: Parameter) -> None: + self._loading = True + self.ui.nameEdit.setText(param.name) + self.ui.typeComboBox.setCurrentIndex(self.ui.typeComboBox.findData(param.value_type)) + self.ui.valueEdit.setText(param.value) + self.ui.widthSize.setValue(param.matrix_size[0]) + self.ui.heightSize.setValue(param.matrix_size[1]) + self.ui.descriptionEdit.setPlainText(param.description or "") + self._loading = False + + def _form_changed(self, *_args: object) -> None: + if self._loading: + return + + param_id = self._selected_param_id() + if param_id is None: + return + + old_param = self._params[param_id] + self._params[param_id] = Parameter( + name=self.ui.nameEdit.text().strip(), + value_type=self.ui.typeComboBox.currentData(), + value=self.ui.valueEdit.text().strip(), + matrix_size=[self.ui.widthSize.value(), self.ui.heightSize.value()], + quantity=old_param.quantity, + unit=old_param.unit, + description=self.ui.descriptionEdit.toPlainText() or None, + ) + self._list_model.item(self._param_ids.index(param_id)).setText(self._params[param_id].name) + self.params_changed.emit() + + def _add_param(self) -> None: + param_id = ParameterID() + param = Parameter(name=f"Parameter {len(self._params) + 1}") + self._params[param_id] = param + self._param_ids.append(param_id) + self._rebuild_list(param_id) + self.params_changed.emit() + + def _remove_param(self) -> None: + param_id = self._selected_param_id() + if param_id is None: + return + + row = self._param_ids.index(param_id) + del self._params[param_id] + self._param_ids.remove(param_id) + selected = self._param_ids[min(row, len(self._param_ids) - 1)] if self._param_ids else None + self._rebuild_list(selected) + self.params_changed.emit() + + def _set_editor_enabled(self, enabled: bool) -> None: + for widget in (self.ui.nameEdit, self.ui.typeComboBox, self.ui.widthSize, self.ui.heightSize, self.ui.descriptionEdit): + widget.setEnabled(enabled)