AI'ed clipboard
This commit is contained in:
216
src/bedit_gui/controllers/clipboard_controller.py
Normal file
216
src/bedit_gui/controllers/clipboard_controller.py
Normal file
@@ -0,0 +1,216 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import QEvent, QObject, QTimer, Signal
|
||||
from PySide6.QtWidgets import QApplication, QLineEdit, QMainWindow, QMessageBox, QPlainTextEdit, QTextEdit, QTreeView, QWidget
|
||||
|
||||
from bedit_core.models import Component, GraphImplementation
|
||||
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.models.document_tree_model import DocumentTreeModel
|
||||
|
||||
|
||||
class ClipboardHandler(QObject):
|
||||
"""Base implementation for adding clipboard support to another editor."""
|
||||
|
||||
availability_changed = Signal()
|
||||
|
||||
def owns_focus(self, _widget: QWidget) -> bool:
|
||||
return False
|
||||
|
||||
def can_copy(self) -> bool:
|
||||
return False
|
||||
|
||||
def can_cut(self) -> bool:
|
||||
return False
|
||||
|
||||
def can_paste(self) -> bool:
|
||||
return False
|
||||
|
||||
def copy(self) -> None:
|
||||
pass
|
||||
|
||||
def cut(self) -> None:
|
||||
pass
|
||||
|
||||
def paste(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class TextClipboardHandler(ClipboardHandler):
|
||||
def __init__(self, clipboard: ClipboardService, parent: QObject | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.clipboard = clipboard
|
||||
|
||||
def owns_focus(self, widget: QWidget) -> bool:
|
||||
return isinstance(widget, (QLineEdit, QTextEdit, QPlainTextEdit))
|
||||
|
||||
def can_copy(self) -> bool:
|
||||
return self._has_selection()
|
||||
|
||||
def can_cut(self) -> bool:
|
||||
widget = self._widget()
|
||||
return widget is not None and not widget.isReadOnly() and self._has_selection()
|
||||
|
||||
def can_paste(self) -> bool:
|
||||
widget = self._widget()
|
||||
return widget is not None and not widget.isReadOnly() and self.clipboard.has_text()
|
||||
|
||||
def copy(self) -> None:
|
||||
widget = self._widget()
|
||||
if widget is not None:
|
||||
widget.copy()
|
||||
|
||||
def cut(self) -> None:
|
||||
widget = self._widget()
|
||||
if widget is not None and not widget.isReadOnly():
|
||||
widget.cut()
|
||||
|
||||
def paste(self) -> None:
|
||||
widget = self._widget()
|
||||
if widget is not None and not widget.isReadOnly():
|
||||
widget.paste()
|
||||
|
||||
def _has_selection(self) -> bool:
|
||||
widget = self._widget()
|
||||
if isinstance(widget, QLineEdit):
|
||||
return widget.hasSelectedText()
|
||||
return widget.textCursor().hasSelection() if widget is not None else False
|
||||
|
||||
@staticmethod
|
||||
def _widget() -> QLineEdit | QTextEdit | QPlainTextEdit | None:
|
||||
widget = QApplication.focusWidget()
|
||||
return widget if isinstance(widget, (QLineEdit, QTextEdit, QPlainTextEdit)) else None
|
||||
|
||||
|
||||
class DocumentTreeClipboardHandler(ClipboardHandler):
|
||||
def __init__(self, document: Document, tree: QTreeView, model: DocumentTreeModel, clipboard: ClipboardService, parent: QObject | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.document = document
|
||||
self.tree = tree
|
||||
self.model = model
|
||||
self.clipboard = clipboard
|
||||
self.tree.selectionModel().selectionChanged.connect(self.availability_changed)
|
||||
self.tree.selectionModel().currentChanged.connect(self.availability_changed)
|
||||
|
||||
def owns_focus(self, widget: QWidget) -> bool:
|
||||
return widget is self.tree or self.tree.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 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:
|
||||
target = self._target()
|
||||
payload = self.clipboard.get_json(ClipboardService.COMPONENTS_MIME)
|
||||
if target is None or payload is None:
|
||||
return
|
||||
try:
|
||||
components, icons = import_components(payload)
|
||||
except (TypeError, ValueError) as exc:
|
||||
QMessageBox.critical(self.tree, "Could not paste components", str(exc))
|
||||
return
|
||||
self.document.paste_components(target, components, icons)
|
||||
|
||||
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]:
|
||||
indexes = self.tree.selectionModel().selectedRows(0)
|
||||
selected = {id(component) for index in indexes if isinstance(component := self.model.value(index), Component)}
|
||||
components: list[Component] = []
|
||||
for index in indexes:
|
||||
component = self.model.value(index)
|
||||
if not isinstance(component, Component):
|
||||
continue
|
||||
parent = index.parent()
|
||||
nested = False
|
||||
while parent.isValid():
|
||||
value = self.model.value(parent)
|
||||
if isinstance(value, Component) and id(value) in selected:
|
||||
nested = True
|
||||
break
|
||||
parent = parent.parent()
|
||||
if not nested:
|
||||
components.append(component)
|
||||
return components
|
||||
|
||||
def _target(self) -> dict | None:
|
||||
value = self.model.value(self.tree.currentIndex())
|
||||
if isinstance(value, CoreDocument):
|
||||
return value.root
|
||||
if isinstance(value, Component) and isinstance(value.implementation, GraphImplementation):
|
||||
return value.implementation.graph.components
|
||||
return None
|
||||
|
||||
|
||||
class ClipboardController(QObject):
|
||||
def __init__(self, window: QMainWindow, clipboard: ClipboardService, handlers: list[ClipboardHandler]) -> None:
|
||||
super().__init__(window)
|
||||
self.window = window
|
||||
self.clipboard = clipboard
|
||||
self.handlers = handlers
|
||||
window.ui.actionCopy.triggered.connect(self.copy)
|
||||
window.ui.actionCut.triggered.connect(self.cut)
|
||||
window.ui.actionPaste.triggered.connect(self.paste)
|
||||
application = QApplication.instance()
|
||||
application.focusChanged.connect(self.update_actions)
|
||||
application.installEventFilter(self)
|
||||
clipboard.changed.connect(self.update_actions)
|
||||
for handler in handlers:
|
||||
handler.setParent(self)
|
||||
handler.availability_changed.connect(self.update_actions)
|
||||
self.update_actions()
|
||||
|
||||
def eventFilter(self, watched: QObject, event: QEvent) -> bool:
|
||||
if event.type() in (QEvent.Type.KeyRelease, QEvent.Type.MouseButtonRelease):
|
||||
QTimer.singleShot(0, self.update_actions)
|
||||
return super().eventFilter(watched, event)
|
||||
|
||||
def active_handler(self) -> ClipboardHandler | None:
|
||||
widget = QApplication.focusWidget()
|
||||
if widget is None:
|
||||
return None
|
||||
return next((handler for handler in self.handlers if handler.owns_focus(widget)), None)
|
||||
|
||||
def copy(self) -> None:
|
||||
handler = self.active_handler()
|
||||
if handler is not None and handler.can_copy():
|
||||
handler.copy()
|
||||
self.update_actions()
|
||||
|
||||
def cut(self) -> None:
|
||||
handler = self.active_handler()
|
||||
if handler is not None and handler.can_cut():
|
||||
handler.cut()
|
||||
self.update_actions()
|
||||
|
||||
def paste(self) -> None:
|
||||
handler = self.active_handler()
|
||||
if handler is not None and handler.can_paste():
|
||||
handler.paste()
|
||||
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())
|
||||
@@ -3,7 +3,7 @@ from functools import partial
|
||||
from typing import Protocol
|
||||
|
||||
from PySide6.QtCore import QObject, QPoint, QSize, Qt
|
||||
from PySide6.QtWidgets import QDialog, QHeaderView, QMenu
|
||||
from PySide6.QtWidgets import QAbstractItemView, QDialog, QHeaderView, QMenu
|
||||
|
||||
from bedit_core.models import Component, ComponentID, GraphImplementation, Port, PortID, Parameter, ParameterID
|
||||
from bedit_core.models import Document as CoreDocument
|
||||
@@ -69,6 +69,8 @@ class DocumentTreeController(QObject):
|
||||
window.ui.actionEscape.triggered.connect(self.deselect)
|
||||
|
||||
window.ui.documentTree.setHeaderHidden(True)
|
||||
window.ui.documentTree.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
window.ui.documentTree.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
window.ui.documentTree.setIconSize(QSize(48, 48))
|
||||
window.ui.documentTree.header().setStretchLastSection(False)
|
||||
window.ui.documentTree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
||||
@@ -177,11 +179,29 @@ class DocumentTreeController(QObject):
|
||||
self.window.ui.documentTree.selectionModel().clear()
|
||||
|
||||
def delete_selected_component(self) -> None:
|
||||
index = self.window.ui.documentTree.currentIndex()
|
||||
component = self.model.value(index)
|
||||
focused = self.window.ui.documentTree.hasFocus()
|
||||
if focused and isinstance(component, Component):
|
||||
self._delete_component(component)
|
||||
if focused:
|
||||
self.document.delete_components(self._selected_components())
|
||||
|
||||
def _delete_component(self, component: Component) -> None:
|
||||
self.document.delete_component(component)
|
||||
|
||||
def _selected_components(self) -> list[Component]:
|
||||
indexes = self.window.ui.documentTree.selectionModel().selectedRows(0)
|
||||
selected = {id(component) for index in indexes if isinstance(component := self.model.value(index), Component)}
|
||||
components: list[Component] = []
|
||||
for index in indexes:
|
||||
component = self.model.value(index)
|
||||
if not isinstance(component, Component):
|
||||
continue
|
||||
parent = index.parent()
|
||||
nested = False
|
||||
while parent.isValid():
|
||||
value = self.model.value(parent)
|
||||
if isinstance(value, Component) and id(value) in selected:
|
||||
nested = True
|
||||
break
|
||||
parent = parent.parent()
|
||||
if not nested:
|
||||
components.append(component)
|
||||
return components
|
||||
|
||||
Reference in New Issue
Block a user