Copy/paste on canvas
This commit is contained in:
@@ -8,6 +8,7 @@ from bedit_core.models import Document as CoreDocument
|
||||
from bedit_gui.documents import Document
|
||||
from bedit_gui.services.clipboard import ClipboardService
|
||||
from bedit_gui.services.component_clipboard import export_components, import_components
|
||||
from bedit_gui.views.graph_editor_widget import GraphEditorWidget
|
||||
from bedit_gui.views.models.document_tree_model import DocumentTreeModel
|
||||
|
||||
|
||||
@@ -28,6 +29,9 @@ class ClipboardHandler(QObject):
|
||||
def can_paste(self) -> bool:
|
||||
return False
|
||||
|
||||
def can_delete(self) -> bool:
|
||||
return False
|
||||
|
||||
def copy(self) -> None:
|
||||
pass
|
||||
|
||||
@@ -37,6 +41,9 @@ class ClipboardHandler(QObject):
|
||||
def paste(self) -> None:
|
||||
pass
|
||||
|
||||
def delete(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class TextClipboardHandler(ClipboardHandler):
|
||||
def __init__(self, clipboard: ClipboardService, parent: QObject | None = None) -> None:
|
||||
@@ -106,6 +113,9 @@ class DocumentTreeClipboardHandler(ClipboardHandler):
|
||||
def can_paste(self) -> bool:
|
||||
return self._target() is not None and self.clipboard.has_format(ClipboardService.COMPONENTS_MIME)
|
||||
|
||||
def can_delete(self) -> bool:
|
||||
return bool(self._selected_components())
|
||||
|
||||
def copy(self) -> None:
|
||||
components = self._selected_components()
|
||||
if components:
|
||||
@@ -129,6 +139,9 @@ class DocumentTreeClipboardHandler(ClipboardHandler):
|
||||
return
|
||||
self.document.paste_components(target, components, icons)
|
||||
|
||||
def delete(self) -> None:
|
||||
self.document.delete_components(self._selected_components())
|
||||
|
||||
def _write_components(self, components: list[Component]) -> None:
|
||||
payload = export_components(self.document, components)
|
||||
self.clipboard.set_json(ClipboardService.COMPONENTS_MIME, payload, "\n".join(component.name for component in components))
|
||||
@@ -162,6 +175,73 @@ class DocumentTreeClipboardHandler(ClipboardHandler):
|
||||
return None
|
||||
|
||||
|
||||
class GraphEditorClipboardHandler(ClipboardHandler):
|
||||
def __init__(self, document: Document, editor: GraphEditorWidget, clipboard: ClipboardService, parent: QObject | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.document = document
|
||||
self.editor = editor
|
||||
self.clipboard = clipboard
|
||||
editor.scene.selectionChanged.connect(self.availability_changed)
|
||||
|
||||
def owns_focus(self, widget: QWidget) -> bool:
|
||||
return widget is self.editor or self.editor.isAncestorOf(widget)
|
||||
|
||||
def can_copy(self) -> bool:
|
||||
return bool(self._selected_components())
|
||||
|
||||
def can_cut(self) -> bool:
|
||||
return self.can_copy()
|
||||
|
||||
def can_paste(self) -> bool:
|
||||
return self._target() is not None and self.clipboard.has_format(ClipboardService.COMPONENTS_MIME)
|
||||
|
||||
def can_delete(self) -> bool:
|
||||
return self.can_copy()
|
||||
|
||||
def copy(self) -> None:
|
||||
components = self._selected_components()
|
||||
if components:
|
||||
self._write_components(components)
|
||||
|
||||
def cut(self) -> None:
|
||||
components = self._selected_components()
|
||||
if components:
|
||||
self._write_components(components)
|
||||
self.document.delete_components(components)
|
||||
|
||||
def paste(self) -> None:
|
||||
graph_component = self.editor.component()
|
||||
payload = self.clipboard.get_json(ClipboardService.COMPONENTS_MIME)
|
||||
if graph_component is None or not isinstance(graph_component.implementation, GraphImplementation) or payload is None:
|
||||
return
|
||||
try:
|
||||
components, icons = import_components(payload)
|
||||
except (TypeError, ValueError) as exc:
|
||||
QMessageBox.critical(self.editor, "Could not paste components", str(exc))
|
||||
return
|
||||
x, y = self.editor.paste_position()
|
||||
spacing = self.editor.snap_to_grid_size * 4
|
||||
positions = {component_id: (x + index * spacing, y + index * spacing) for index, component_id in enumerate(components)}
|
||||
self.document.paste_graph_components(graph_component, components, icons, positions)
|
||||
|
||||
def delete(self) -> None:
|
||||
self.document.delete_components(self._selected_components())
|
||||
|
||||
def _write_components(self, components: list[Component]) -> None:
|
||||
payload = export_components(self.document, components)
|
||||
self.clipboard.set_json(ClipboardService.COMPONENTS_MIME, payload, "\n".join(component.name for component in components))
|
||||
|
||||
def _selected_components(self) -> list[Component]:
|
||||
target = self._target()
|
||||
if target is None:
|
||||
return []
|
||||
return [target[component_id] for component_id in self.editor.selected_component_ids() if component_id in target]
|
||||
|
||||
def _target(self) -> dict | None:
|
||||
component = self.editor.component()
|
||||
return component.implementation.graph.components if component is not None and isinstance(component.implementation, GraphImplementation) else None
|
||||
|
||||
|
||||
class ClipboardController(QObject):
|
||||
def __init__(self, window: QMainWindow, clipboard: ClipboardService, handlers: list[ClipboardHandler]) -> None:
|
||||
super().__init__(window)
|
||||
@@ -171,6 +251,7 @@ class ClipboardController(QObject):
|
||||
window.ui.actionCopy.triggered.connect(self.copy)
|
||||
window.ui.actionCut.triggered.connect(self.cut)
|
||||
window.ui.actionPaste.triggered.connect(self.paste)
|
||||
window.ui.actionDelete.triggered.connect(self.delete)
|
||||
application = QApplication.instance()
|
||||
application.focusChanged.connect(self.update_actions)
|
||||
application.installEventFilter(self)
|
||||
@@ -209,8 +290,15 @@ class ClipboardController(QObject):
|
||||
handler.paste()
|
||||
self.update_actions()
|
||||
|
||||
def delete(self) -> None:
|
||||
handler = self.active_handler()
|
||||
if handler is not None and handler.can_delete():
|
||||
handler.delete()
|
||||
self.update_actions()
|
||||
|
||||
def update_actions(self, *_args: object) -> None:
|
||||
handler = self.active_handler()
|
||||
self.window.ui.actionCopy.setEnabled(handler is not None and handler.can_copy())
|
||||
self.window.ui.actionCut.setEnabled(handler is not None and handler.can_cut())
|
||||
self.window.ui.actionPaste.setEnabled(handler is not None and handler.can_paste())
|
||||
self.window.ui.actionDelete.setEnabled(handler is not None and handler.can_delete())
|
||||
|
||||
@@ -66,8 +66,8 @@ class DocumentTreeController(QObject):
|
||||
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)
|
||||
window.graph_editor.component_move_requested.connect(self.document.move_graph_component)
|
||||
window.graph_editor.component_context_menu_requested.connect(self._show_graph_component_context_menu)
|
||||
|
||||
# Add deselection with esc to this widget
|
||||
window.ui.actionEscape.setShortcutContext(Qt.ShortcutContext.WidgetWithChildrenShortcut)
|
||||
@@ -98,13 +98,14 @@ class DocumentTreeController(QObject):
|
||||
|
||||
def _on_document_changed(self, model: CoreDocument) -> None:
|
||||
"""Rebuild the tree whenever New/Open replaces the core document."""
|
||||
self._show_component(None)
|
||||
displayed_component = self.window.graph_editor.component() or self.window.equation_editor.component()
|
||||
self.model.set_document(model)
|
||||
self._components = {}
|
||||
self._collect_components(model.root)
|
||||
for component_id, component in self._components.items():
|
||||
icon = self.document.component_icon(component_id)
|
||||
self.model.set_component_icon(component_id, render_fitted_icon(icon, component.interface.ports, ICON_SIZE))
|
||||
self._show_component(displayed_component if any(component is displayed_component for component in self._components.values()) else None)
|
||||
|
||||
# Optional presentation behavior. Later, you could instead remember
|
||||
# expanded component IDs and restore only those nodes.
|
||||
@@ -161,27 +162,36 @@ class DocumentTreeController(QObject):
|
||||
if not isinstance(component, Component):
|
||||
return
|
||||
|
||||
self._show_component_context_menu(component, self.window.ui.documentTree.viewport().mapToGlobal(position))
|
||||
|
||||
def _show_graph_component_context_menu(self, component_id: ComponentID, global_position: QPoint) -> None:
|
||||
component = self._components.get(component_id)
|
||||
if component is not None:
|
||||
self._show_component_context_menu(component, global_position)
|
||||
|
||||
def _show_component_context_menu(self, component: Component, global_position: QPoint) -> None:
|
||||
menu = QMenu(self.window.ui.documentTree)
|
||||
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()
|
||||
add_graph_component = None
|
||||
add_equation_component = None
|
||||
if isinstance(component.implementation, GraphImplementation):
|
||||
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)
|
||||
)
|
||||
selected = menu.exec(global_position)
|
||||
if selected is edit_interface:
|
||||
self._edit_interface(component)
|
||||
elif selected is edit_params:
|
||||
self._edit_params(component)
|
||||
elif selected is edit_icon:
|
||||
self._edit_icon(component)
|
||||
elif selected is add_graph_component:
|
||||
elif add_graph_component is not None and selected is add_graph_component:
|
||||
self._add_graph_component(component)
|
||||
elif selected is add_equation_component:
|
||||
elif add_equation_component is not None and selected is add_equation_component:
|
||||
self._add_equation_component(component)
|
||||
elif selected is delete_component:
|
||||
self._delete_component(component)
|
||||
|
||||
Reference in New Issue
Block a user