From 748bb08531bde1cf90b29d090e4414ea843d88dd Mon Sep 17 00:00:00 2001 From: Joppe Blondel Date: Wed, 29 Jul 2026 18:40:50 +0200 Subject: [PATCH] Equation editor undo --- .../commands/equation_text_command.py | 53 +++++++++++++++ .../controllers/document_tree_controller.py | 6 ++ src/bedit_gui/controllers/undo_controller.py | 2 + src/bedit_gui/documents/document.py | 7 ++ src/bedit_gui/views/equation_editor_widget.py | 64 ++++++++++++++++--- 5 files changed, 122 insertions(+), 10 deletions(-) create mode 100644 src/bedit_gui/commands/equation_text_command.py diff --git a/src/bedit_gui/commands/equation_text_command.py b/src/bedit_gui/commands/equation_text_command.py new file mode 100644 index 0000000..68a1d26 --- /dev/null +++ b/src/bedit_gui/commands/equation_text_command.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from PySide6.QtGui import QUndoCommand + +from bedit_core.models import Component, EquationImplementation + + +class ChangeEquationTextCommand(QUndoCommand): + COMMAND_ID = 1001 + + def __init__(self, document: object, component: Component, section: str, text: list[str], edit_id: int) -> None: + super().__init__(self._command_text(section)) + implementation = component.implementation + if not isinstance(implementation, EquationImplementation): + raise TypeError("equation text can only be changed on an equation component") + + self.document = document + self.component = component + self.section = section + self.old_text = list(getattr(implementation, section)) + self.new_text = list(text) + self.edit_id = edit_id + + def id(self) -> int: + return self.COMMAND_ID + + def mergeWith(self, other: QUndoCommand) -> bool: + if not isinstance(other, ChangeEquationTextCommand): + return False + if other.component is not self.component or other.section != self.section or other.edit_id != self.edit_id: + return False + self.new_text = list(other.new_text) + return True + + def redo(self) -> None: + self._set_text(self.new_text) + + def undo(self) -> None: + self._set_text(self.old_text) + + def _set_text(self, text: list[str]) -> None: + implementation = self.component.implementation + assert isinstance(implementation, EquationImplementation) + setattr(implementation, self.section, list(text)) + self.document.equation_text_changed.emit(self.component, self.section) + + @staticmethod + def _command_text(section: str) -> str: + return { + "declarations": "Edit declarations", + "initial_equations": "Edit initial equations", + "equations": "Edit equations", + }[section] diff --git a/src/bedit_gui/controllers/document_tree_controller.py b/src/bedit_gui/controllers/document_tree_controller.py index 2f92ed5..7f46ea2 100644 --- a/src/bedit_gui/controllers/document_tree_controller.py +++ b/src/bedit_gui/controllers/document_tree_controller.py @@ -58,8 +58,10 @@ class DocumentTreeController(QObject): window.ui.documentTree.setModel(self.model) window.ui.documentTree.selectionModel().selectionChanged.connect(self._selection_changed) + window.equation_editor.equation_text_change_requested.connect(self.document.update_component_equation_text) document.model_changed.connect(self._on_document_changed) document.icon_changed.connect(self._on_icon_changed) + document.equation_text_changed.connect(self._on_equation_text_changed) self.model.rename_document_requested.connect(self.document.rename) self.model.rename_component_requested.connect(self.document.rename_component) window.ui.actionDelete.triggered.connect(self.delete_selected_component) @@ -113,6 +115,10 @@ class DocumentTreeController(QObject): self.window.equation_editor.set_component(None) self.window.equation_editor.hide() + def _on_equation_text_changed(self, component: Component, section: str) -> None: + if self.window.equation_editor.component() is component: + self.window.equation_editor.refresh_text(section) + def _on_icon_changed(self, component_id: ComponentID, icon: object) -> None: component = self._components.get(component_id) if component is None: diff --git a/src/bedit_gui/controllers/undo_controller.py b/src/bedit_gui/controllers/undo_controller.py index fb31862..7825ad0 100644 --- a/src/bedit_gui/controllers/undo_controller.py +++ b/src/bedit_gui/controllers/undo_controller.py @@ -31,11 +31,13 @@ class UndoController(QObject): redo_action.setEnabled(undo_stack.canRedo()) def undo(self) -> None: + self.window.equation_editor.finish_text_edit() command = self.document.undo_stack.undoText() self.document.undo_stack.undo() logger.info("Undo: %s", command) def redo(self) -> None: + self.window.equation_editor.finish_text_edit() command = self.document.undo_stack.redoText() self.document.undo_stack.redo() logger.info("Redo: %s", command) diff --git a/src/bedit_gui/documents/document.py b/src/bedit_gui/documents/document.py index 4295fe8..2cfd3a9 100644 --- a/src/bedit_gui/documents/document.py +++ b/src/bedit_gui/documents/document.py @@ -9,6 +9,7 @@ from PySide6.QtGui import QUndoStack from bedit_core.models import ID, Component, ComponentID, GraphImplementation, Port, PortID, Parameter, ParameterID from bedit_core.models import Document as CoreDocument from bedit_gui.commands.change_icon_command import ChangeIconCommand +from bedit_gui.commands.equation_text_command import ChangeEquationTextCommand 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 @@ -25,6 +26,7 @@ class Document(QObject): path_changed = Signal(object) modified_changed = Signal(bool) icon_changed = Signal(object, object) + equation_text_changed = Signal(object, str) def __init__(self, parent: QObject | None = None) -> None: super().__init__(parent) @@ -197,6 +199,11 @@ class Document(QObject): self.undo_stack.push(command) self.undo_stack.endMacro() + def update_component_equation_text(self, component: Component, section: str, text: list[str], edit_id: int) -> None: + command = ChangeEquationTextCommand(self, component, section, text, edit_id) + if command.old_text != command.new_text: + self.undo_stack.push(command) + def add_empty_graph_component(self, component: Component) -> None: if isinstance(component.implementation, GraphImplementation): command = AddEmptyGraphComponent(self, component) diff --git a/src/bedit_gui/views/equation_editor_widget.py b/src/bedit_gui/views/equation_editor_widget.py index 8e5a75d..0ae8a6f 100644 --- a/src/bedit_gui/views/equation_editor_widget.py +++ b/src/bedit_gui/views/equation_editor_widget.py @@ -1,6 +1,6 @@ from __future__ import annotations -from PySide6.QtCore import Qt, Signal +from PySide6.QtCore import QEvent, QObject, QTimer, Qt, Signal from PySide6.QtWidgets import QWidget from bedit_core.models import Component, EquationImplementation @@ -13,6 +13,7 @@ class EquationEditorWidget(QWidget): """Editor that keeps an equation component synchronized with its fields.""" component_changed = Signal(object) + equation_text_change_requested = Signal(object, str, object, int) sidebar_visible_changed = Signal(bool) def __init__(self, parent: QWidget | None = None) -> None: @@ -22,6 +23,11 @@ class EquationEditorWidget(QWidget): self.ui.setupUi(self) self._component: Component | None = None self._loading = False + self._edit_id = 0 + self._edit_timer = QTimer(self) + self._edit_timer.setInterval(750) + self._edit_timer.setSingleShot(True) + self._edit_timer.timeout.connect(self.finish_text_edit) self.param_editor = ParamEditorWidget(self) self.port_editor = PortEditorWidget(self) @@ -30,9 +36,10 @@ class EquationEditorWidget(QWidget): self.ui.sidebarButton.clicked.connect(self.toggle_sidebar) self.set_sidebar_visible(True) - self.ui.declarationsTextEdit.textChanged.connect(self._text_changed) - self.ui.initialEquationsTextEdit.textChanged.connect(self._text_changed) - self.ui.equationsTextEdit.textChanged.connect(self._text_changed) + for editor in self._text_editors(): + editor.setUndoRedoEnabled(False) + editor.installEventFilter(self) + editor.textChanged.connect(self._text_changed) self.param_editor.params_changed.connect(self._params_changed) self.port_editor.ports_changed.connect(self._ports_changed) @@ -48,6 +55,10 @@ class EquationEditorWidget(QWidget): def component(self) -> Component | None: return self._component + def finish_text_edit(self) -> None: + self._edit_timer.stop() + self._edit_id += 1 + def toggle_sidebar(self) -> None: self.set_sidebar_visible(not self.param_editor.isVisibleTo(self)) @@ -81,16 +92,49 @@ class EquationEditorWidget(QWidget): self._loading = False self._set_editors_enabled(component is not None) + def refresh_text(self, section: str) -> None: + if self._component is None: + return + implementation = self._component.implementation + assert isinstance(implementation, EquationImplementation) + editor = self._editor_for_section(section) + text = "\n".join(getattr(implementation, section)) + if editor.toPlainText() == text: + return + self._loading = True + editor.setPlainText(text) + self._loading = False + def _text_changed(self) -> None: if self._loading or self._component is None: return - implementation = self._component.implementation - assert isinstance(implementation, EquationImplementation) - implementation.declarations = self.ui.declarationsTextEdit.toPlainText().splitlines() - implementation.initial_equations = self.ui.initialEquationsTextEdit.toPlainText().splitlines() - implementation.equations = self.ui.equationsTextEdit.toPlainText().splitlines() - self.component_changed.emit(self._component) + editor = self.sender() + section = self._section_for_editor(editor) + self.equation_text_change_requested.emit(self._component, section, editor.toPlainText().splitlines(), self._edit_id) + self._edit_timer.start() + + def eventFilter(self, watched: QObject, event: QEvent) -> bool: + if watched in self._text_editors() and event.type() in (QEvent.Type.FocusIn, QEvent.Type.FocusOut): + self.finish_text_edit() + return super().eventFilter(watched, event) + + def _text_editors(self) -> tuple: + return (self.ui.declarationsTextEdit, self.ui.initialEquationsTextEdit, self.ui.equationsTextEdit) + + def _section_for_editor(self, editor: QObject) -> str: + if editor is self.ui.declarationsTextEdit: + return "declarations" + if editor is self.ui.initialEquationsTextEdit: + return "initial_equations" + return "equations" + + def _editor_for_section(self, section: str): + return { + "declarations": self.ui.declarationsTextEdit, + "initial_equations": self.ui.initialEquationsTextEdit, + "equations": self.ui.equationsTextEdit, + }[section] def _params_changed(self) -> None: if self._loading or self._component is None: