Equation editor undo

This commit is contained in:
2026-07-29 18:40:50 +02:00
parent 9df352f6b7
commit 748bb08531
5 changed files with 122 additions and 10 deletions

View File

@@ -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]

View File

@@ -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:

View File

@@ -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)

View File

@@ -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)

View File

@@ -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: