diff --git a/src/bedit_gui/commands/component_command.py b/src/bedit_gui/commands/component_command.py new file mode 100644 index 0000000..847cf27 --- /dev/null +++ b/src/bedit_gui/commands/component_command.py @@ -0,0 +1,106 @@ +from PySide6.QtGui import QUndoCommand + +from bedit_core.models import Component, ComponentID, Connection, ConnectionID, EquationImplementation, Graph, GraphImplementation, Interface + + +class AddEmptyGraphComponent(QUndoCommand): + def __init__(self, document: object, parent: Component) -> None: + super().__init__("Add graph component") + if not isinstance(parent.implementation, GraphImplementation): + raise TypeError("parent component must have a graph implementation") + self.document = document + self.graph = parent.implementation.graph + self.component_id = ComponentID() + self.component = Component(name="New Graph Component", interface=Interface(), parameters={}, implementation=GraphImplementation(Graph())) + + def redo(self) -> None: + self.graph.components[self.component_id] = self.component + self.document.model_changed.emit(self.document.model) + + def undo(self) -> None: + del self.graph.components[self.component_id] + self.document.model_changed.emit(self.document.model) + + +class AddEmptyEquationComponent(QUndoCommand): + def __init__(self, document: object, parent: Component) -> None: + super().__init__("Add equation component") + if not isinstance(parent.implementation, GraphImplementation): + raise TypeError("parent component must have a graph implementation") + self.document = document + self.graph = parent.implementation.graph + self.component_id = ComponentID() + self.component = Component(name="New Equation Component", interface=Interface(), parameters={}, implementation=EquationImplementation()) + + def redo(self) -> None: + self.graph.components[self.component_id] = self.component + self.document.model_changed.emit(self.document.model) + + def undo(self) -> None: + del self.graph.components[self.component_id] + self.document.model_changed.emit(self.document.model) + + +class DeleteComponent(QUndoCommand): + def __init__(self, document: object, component: Component) -> None: + super().__init__("Delete component") + self.document = document + self.component = component + self.components, self.component_id, self.parent_graph = self._find_component(document.model.root, component) + self.component_index = list(self.components).index(self.component_id) + port_ids = set(component.interface.ports) + self.connections: list[tuple[int, ConnectionID, Connection]] = [] + if self.parent_graph is not None: + for index, (connection_id, connection) in enumerate(self.parent_graph.connections.items()): + if connection.source in port_ids or connection.target in port_ids: + self.connections.append((index, connection_id, connection)) + self.icons = {} + for component_id in self._component_ids(self.component_id, component): + icon = document.stored_component_icon(component_id) + if icon is not None: + self.icons[component_id] = icon + + def redo(self) -> None: + if self.parent_graph is not None: + for _, connection_id, _ in self.connections: + self.parent_graph.connections.pop(connection_id, None) + self.components.pop(self.component_id, None) + self.document.model_changed.emit(self.document.model) + for component_id in self.icons: + self.document._set_component_icon(component_id, None) + + def undo(self) -> None: + self._restore_item(self.components, self.component_id, self.component, self.component_index) + if self.parent_graph is not None: + for index, connection_id, connection in self.connections: + self._restore_item(self.parent_graph.connections, connection_id, connection, index) + self.document.model_changed.emit(self.document.model) + for component_id, icon in self.icons.items(): + self.document._set_component_icon(component_id, icon) + + @classmethod + def _find_component(cls, components: dict[ComponentID, Component], target: Component, parent_graph: Graph | None = None) -> tuple[dict[ComponentID, Component], ComponentID, Graph | None]: + for component_id, component in components.items(): + if component is target: + return components, component_id, parent_graph + if isinstance(component.implementation, GraphImplementation): + try: + return cls._find_component(component.implementation.graph.components, target, component.implementation.graph) + except ValueError: + pass + raise ValueError("component is not part of this document") + + @classmethod + def _component_ids(cls, component_id: ComponentID, component: Component) -> list[ComponentID]: + component_ids = [component_id] + if isinstance(component.implementation, GraphImplementation): + for child_id, child in component.implementation.graph.components.items(): + component_ids.extend(cls._component_ids(child_id, child)) + return component_ids + + @staticmethod + def _restore_item(items: dict, item_id: object, item: object, index: int) -> None: + values = list(items.items()) + values.insert(index, (item_id, item)) + items.clear() + items.update(values) diff --git a/src/bedit_gui/controllers/document_tree_controller.py b/src/bedit_gui/controllers/document_tree_controller.py index 68ca35a..1059c24 100644 --- a/src/bedit_gui/controllers/document_tree_controller.py +++ b/src/bedit_gui/controllers/document_tree_controller.py @@ -112,6 +112,11 @@ class DocumentTreeController(QObject): edit_interface = menu.addAction("Edit Interface") edit_params = menu.addAction("Edit Parameters") edit_icon = menu.addAction("Edit Icon") + menu.addSeparator() + add_graph_component = menu.addAction("Add Graph Component") + add_equation_component = menu.addAction("Add Equation Component") + menu.addSeparator() + delete_component = menu.addAction("Delete Component") selected = menu.exec( self.window.ui.documentTree.viewport().mapToGlobal(position) ) @@ -121,6 +126,12 @@ class DocumentTreeController(QObject): self._edit_params(component) elif selected is edit_icon: self._edit_icon(component) + elif selected is add_graph_component: + self._add_graph_component(component) + elif selected is add_equation_component: + self._add_equation_component(component) + elif selected is delete_component: + self._delete_component(component) def _edit_interface(self, component: Component) -> None: dialog = self.interface_editor_factory( @@ -149,3 +160,12 @@ class DocumentTreeController(QObject): def _icon_editor_closed(self, editor: IconEditorWindow, *_args: object) -> None: if editor in self._icon_editors: self._icon_editors.remove(editor) + + def _add_graph_component(self, component: Component) -> None: + self.document.add_empty_graph_component(component) + + def _add_equation_component(self, component: Component) -> None: + self.document.add_empty_equation_component(component) + + def _delete_component(self, component: Component) -> None: + self.document.delete_component(component) diff --git a/src/bedit_gui/documents/document.py b/src/bedit_gui/documents/document.py index f0b2f33..11322a8 100644 --- a/src/bedit_gui/documents/document.py +++ b/src/bedit_gui/documents/document.py @@ -6,13 +6,14 @@ from pathlib import Path from PySide6.QtCore import QObject, Signal from PySide6.QtGui import QUndoStack -from bedit_core.models import ID, Component, ComponentID, GraphImplementation, Port, PortID, Parameter, ParameterID +from bedit_core.models import ID, Component, ComponentID, GraphImplementation, Port, PortID, Parameter, ParameterID, EquationImplementation from bedit_core.models import Document as CoreDocument from bedit_gui.commands.change_icon_command import ChangeIconCommand 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.commands.component_command import AddEmptyGraphComponent, AddEmptyEquationComponent, DeleteComponent from bedit_gui.models import Icon, IconDatabase from bedit_gui.services import document_files @@ -195,3 +196,14 @@ class Document(QObject): for command in commands: self.undo_stack.push(command) self.undo_stack.endMacro() + + def add_empty_graph_component(self, component: Component) -> None: + if isinstance(component.implementation, GraphImplementation): + self.undo_stack.push(AddEmptyGraphComponent(self, component)) + + def add_empty_equation_component(self, component: Component) -> None: + if isinstance(component.implementation, GraphImplementation): + self.undo_stack.push(AddEmptyEquationComponent(self, component)) + + def delete_component(self, component: Component) -> None: + self.undo_stack.push(DeleteComponent(self, component))