AI'ed clipboard
This commit is contained in:
@@ -5,6 +5,7 @@ import argparse
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from bedit_gui.controllers.clipboard_controller import ClipboardController, DocumentTreeClipboardHandler, TextClipboardHandler
|
||||
from bedit_gui.controllers.document_controller import DocumentController
|
||||
from bedit_gui.controllers.log_controller import LogController
|
||||
from bedit_gui.controllers.settings_controller import SettingsController
|
||||
@@ -14,6 +15,7 @@ from bedit_gui.controllers.window_state_controller import WindowStateController
|
||||
from bedit_gui.controllers.document_tree_controller import DocumentTreeController
|
||||
from bedit_gui.documents import Document
|
||||
from bedit_gui.services.application_settings import ApplicationSettings
|
||||
from bedit_gui.services.clipboard import ClipboardService
|
||||
from bedit_gui.views.main_window import MainWindow
|
||||
|
||||
def parse_arguments():
|
||||
@@ -46,7 +48,9 @@ def main() -> int:
|
||||
SettingsController(window, settings)
|
||||
UndoController(document, window)
|
||||
ViewMenuController(window)
|
||||
DocumentTreeController(document, window)
|
||||
document_tree_controller = DocumentTreeController(document, window)
|
||||
clipboard = ClipboardService(app)
|
||||
ClipboardController(window, clipboard, [TextClipboardHandler(clipboard), DocumentTreeClipboardHandler(document, window.ui.documentTree, document_tree_controller.model, clipboard)])
|
||||
|
||||
window_state_controller = WindowStateController(app, window)
|
||||
window_state_controller.restore()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from PySide6.QtGui import QUndoCommand
|
||||
|
||||
from bedit_core.models import Component, ComponentID, Connection, ConnectionID, EquationImplementation, Graph, GraphImplementation, Interface
|
||||
from bedit_gui.models import Icon
|
||||
|
||||
|
||||
class AddEmptyGraphComponent(QUndoCommand):
|
||||
@@ -104,3 +105,25 @@ class DeleteComponent(QUndoCommand):
|
||||
values.insert(index, (item_id, item))
|
||||
items.clear()
|
||||
items.update(values)
|
||||
|
||||
|
||||
class PasteComponents(QUndoCommand):
|
||||
def __init__(self, document: object, target: dict[ComponentID, Component], components: dict[ComponentID, Component], icons: dict[ComponentID, Icon]) -> None:
|
||||
super().__init__("Paste components")
|
||||
self.document = document
|
||||
self.target = target
|
||||
self.components = components
|
||||
self.icons = icons
|
||||
|
||||
def redo(self) -> None:
|
||||
self.target.update(self.components)
|
||||
self.document.model_changed.emit(self.document.model)
|
||||
for component_id, icon in self.icons.items():
|
||||
self.document._set_component_icon(component_id, icon)
|
||||
|
||||
def undo(self) -> None:
|
||||
for component_id in self.components:
|
||||
self.target.pop(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)
|
||||
|
||||
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
|
||||
|
||||
@@ -6,14 +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, EquationImplementation
|
||||
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.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.commands.component_command import AddEmptyEquationComponent, AddEmptyGraphComponent, DeleteComponent, PasteComponents
|
||||
from bedit_gui.models import Icon, IconDatabase
|
||||
from bedit_gui.services import document_files
|
||||
|
||||
@@ -199,11 +199,45 @@ class Document(QObject):
|
||||
|
||||
def add_empty_graph_component(self, component: Component) -> None:
|
||||
if isinstance(component.implementation, GraphImplementation):
|
||||
self.undo_stack.push(AddEmptyGraphComponent(self, component))
|
||||
command = AddEmptyGraphComponent(self, component)
|
||||
command.component.name = self._unique_component_name(component.implementation.graph.components, command.component.name)
|
||||
self.undo_stack.push(command)
|
||||
|
||||
def add_empty_equation_component(self, component: Component) -> None:
|
||||
if isinstance(component.implementation, GraphImplementation):
|
||||
self.undo_stack.push(AddEmptyEquationComponent(self, component))
|
||||
command = AddEmptyEquationComponent(self, component)
|
||||
command.component.name = self._unique_component_name(component.implementation.graph.components, command.component.name)
|
||||
self.undo_stack.push(command)
|
||||
|
||||
def delete_component(self, component: Component) -> None:
|
||||
self.undo_stack.push(DeleteComponent(self, component))
|
||||
|
||||
def delete_components(self, components: list[Component]) -> None:
|
||||
if not components:
|
||||
return
|
||||
self.undo_stack.beginMacro("Delete components")
|
||||
for component in components:
|
||||
self.undo_stack.push(DeleteComponent(self, component))
|
||||
self.undo_stack.endMacro()
|
||||
|
||||
def paste_components(self, target: dict[ComponentID, Component], components: dict[ComponentID, Component], icons: dict[ComponentID, Icon]) -> None:
|
||||
if not components:
|
||||
return
|
||||
names = {component.name for component in target.values()}
|
||||
for component in components.values():
|
||||
component.name = self._unique_name(names, component.name)
|
||||
names.add(component.name)
|
||||
self.undo_stack.push(PasteComponents(self, target, components, icons))
|
||||
|
||||
@staticmethod
|
||||
def _unique_component_name(components: dict[ComponentID, Component], name: str) -> str:
|
||||
return Document._unique_name({component.name for component in components.values()}, name)
|
||||
|
||||
@staticmethod
|
||||
def _unique_name(names: set[str], name: str) -> str:
|
||||
if name not in names:
|
||||
return name
|
||||
index = 0
|
||||
while f"{name}_{index}" in names:
|
||||
index += 1
|
||||
return f"{name}_{index}"
|
||||
|
||||
42
src/bedit_gui/services/clipboard.py
Normal file
42
src/bedit_gui/services/clipboard.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from PySide6.QtCore import QMimeData, QObject, Signal
|
||||
from PySide6.QtGui import QClipboard, QGuiApplication
|
||||
|
||||
|
||||
class ClipboardService(QObject):
|
||||
COMPONENTS_MIME = "application/x-bedit-components+json"
|
||||
changed = Signal()
|
||||
|
||||
def __init__(self, parent: QObject | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._clipboard = QGuiApplication.clipboard()
|
||||
self._clipboard.dataChanged.connect(self.changed)
|
||||
|
||||
def set_json(self, mime_type: str, data: dict[str, Any], text: str = "") -> None:
|
||||
mime = QMimeData()
|
||||
mime.setData(mime_type, json.dumps(data).encode("utf-8"))
|
||||
if text:
|
||||
mime.setText(text)
|
||||
self._clipboard.setMimeData(mime, QClipboard.Mode.Clipboard)
|
||||
|
||||
def get_json(self, mime_type: str) -> dict[str, Any] | None:
|
||||
mime = self._clipboard.mimeData(QClipboard.Mode.Clipboard)
|
||||
if mime is None or not mime.hasFormat(mime_type):
|
||||
return None
|
||||
try:
|
||||
data = json.loads(bytes(mime.data(mime_type)).decode("utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError):
|
||||
return None
|
||||
return data if isinstance(data, dict) else None
|
||||
|
||||
def has_format(self, mime_type: str) -> bool:
|
||||
mime = self._clipboard.mimeData(QClipboard.Mode.Clipboard)
|
||||
return mime is not None and mime.hasFormat(mime_type)
|
||||
|
||||
def has_text(self) -> bool:
|
||||
mime = self._clipboard.mimeData(QClipboard.Mode.Clipboard)
|
||||
return mime is not None and mime.hasText()
|
||||
82
src/bedit_gui/services/component_clipboard.py
Normal file
82
src/bedit_gui/services/component_clipboard.py
Normal file
@@ -0,0 +1,82 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from typing import Any
|
||||
|
||||
from bedit_core.models import Component, ComponentID, ConnectionID, Document, GraphImplementation, ID, ParameterID, PortID
|
||||
from bedit_core.serialization.schema import document_from_data, document_to_data
|
||||
from bedit_gui.documents import Document as GuiDocument
|
||||
from bedit_gui.models import Icon, ShapeID
|
||||
|
||||
FORMAT_VERSION = 1
|
||||
|
||||
|
||||
def export_components(document: GuiDocument, components: list[Component]) -> dict[str, Any]:
|
||||
roots = {document.component_id(component): component for component in components}
|
||||
serialized = document_to_data(Document(format_version=1, id=ID(), name="Clipboard", root=roots))
|
||||
component_ids = _all_component_ids(roots)
|
||||
icons = {}
|
||||
for component_id in component_ids:
|
||||
icon = document.stored_component_icon(component_id)
|
||||
if icon is not None:
|
||||
icons[str(component_id)] = icon.to_data()
|
||||
return {"format_version": FORMAT_VERSION, "type": "components", "components": serialized["root"], "icons": icons}
|
||||
|
||||
|
||||
def import_components(payload: dict[str, Any]) -> tuple[dict[ComponentID, Component], dict[ComponentID, Icon]]:
|
||||
if payload.get("format_version") != FORMAT_VERSION or payload.get("type") != "components":
|
||||
raise ValueError("unsupported component clipboard format")
|
||||
components = payload.get("components")
|
||||
if not isinstance(components, dict):
|
||||
raise TypeError("component clipboard payload must contain a components object")
|
||||
clipboard_document = document_from_data({"format_version": 1, "id": str(ID()), "name": "Clipboard", "root": components, "metadata": None})
|
||||
component_map: dict[ComponentID, ComponentID] = {}
|
||||
port_map: dict[PortID, PortID] = {}
|
||||
remapped = _remap_components(clipboard_document.root, component_map, port_map)
|
||||
icon_data = payload.get("icons", {})
|
||||
if not isinstance(icon_data, dict):
|
||||
raise TypeError("component clipboard icons must be an object")
|
||||
icons: dict[ComponentID, Icon] = {}
|
||||
for old_id, data in icon_data.items():
|
||||
new_component_id = component_map.get(ComponentID(old_id))
|
||||
if new_component_id is None or not isinstance(data, dict):
|
||||
continue
|
||||
icon = Icon.from_data(data)
|
||||
icon.shapes = {ShapeID(): shape for shape in icon.shapes.values()}
|
||||
icon.port_positions = {port_map[port_id]: position for port_id, position in icon.port_positions.items() if port_id in port_map}
|
||||
icons[new_component_id] = icon
|
||||
return remapped, icons
|
||||
|
||||
|
||||
def _remap_components(components: dict[ComponentID, Component], component_map: dict[ComponentID, ComponentID], port_map: dict[PortID, PortID]) -> dict[ComponentID, Component]:
|
||||
remapped: dict[ComponentID, Component] = {}
|
||||
for old_component_id, original in components.items():
|
||||
component = deepcopy(original)
|
||||
new_component_id = ComponentID()
|
||||
component_map[old_component_id] = new_component_id
|
||||
component.interface.ports = {_new_port_id(old_id, port_map): port for old_id, port in component.interface.ports.items()}
|
||||
component.parameters = {ParameterID(): parameter for parameter in component.parameters.values()}
|
||||
if isinstance(component.implementation, GraphImplementation):
|
||||
graph = component.implementation.graph
|
||||
graph.components = _remap_components(graph.components, component_map, port_map)
|
||||
graph.connections = {ConnectionID(): connection for connection in graph.connections.values()}
|
||||
for connection in graph.connections.values():
|
||||
connection.source = port_map.get(connection.source, connection.source)
|
||||
connection.target = port_map.get(connection.target, connection.target)
|
||||
remapped[new_component_id] = component
|
||||
return remapped
|
||||
|
||||
|
||||
def _new_port_id(old_id: PortID, port_map: dict[PortID, PortID]) -> PortID:
|
||||
new_id = PortID()
|
||||
port_map[old_id] = new_id
|
||||
return new_id
|
||||
|
||||
|
||||
def _all_component_ids(components: dict[ComponentID, Component]) -> list[ComponentID]:
|
||||
component_ids: list[ComponentID] = []
|
||||
for component_id, component in components.items():
|
||||
component_ids.append(component_id)
|
||||
if isinstance(component.implementation, GraphImplementation):
|
||||
component_ids.extend(_all_component_ids(component.implementation.graph.components))
|
||||
return component_ids
|
||||
@@ -52,6 +52,10 @@
|
||||
<addaction name="actionUndo"/>
|
||||
<addaction name="actionRedo"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionCopy"/>
|
||||
<addaction name="actionCut"/>
|
||||
<addaction name="actionPaste"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionDelete"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionSettings"/>
|
||||
@@ -103,6 +107,9 @@
|
||||
</attribute>
|
||||
<addaction name="actionUndo"/>
|
||||
<addaction name="actionRedo"/>
|
||||
<addaction name="actionCopy"/>
|
||||
<addaction name="actionCut"/>
|
||||
<addaction name="actionPaste"/>
|
||||
</widget>
|
||||
<widget class="QDockWidget" name="documentTreeWidget">
|
||||
<property name="minimumSize">
|
||||
@@ -325,6 +332,60 @@
|
||||
<enum>QAction::MenuRole::NoRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionCopy">
|
||||
<property name="icon">
|
||||
<iconset resource="../../resources/resources.qrc">
|
||||
<normaloff>:/icons/icons/edit-copy.png</normaloff>:/icons/icons/edit-copy.png</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Copy</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Copy selected</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+C</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::NoRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionPaste">
|
||||
<property name="icon">
|
||||
<iconset resource="../../resources/resources.qrc">
|
||||
<normaloff>:/icons/icons/edit-paste.png</normaloff>:/icons/icons/edit-paste.png</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Paste</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Paste selected</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+V</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::NoRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionCut">
|
||||
<property name="icon">
|
||||
<iconset resource="../../resources/resources.qrc">
|
||||
<normaloff>:/icons/icons/edit-cut.png</normaloff>:/icons/icons/edit-cut.png</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Cut</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Cut selected</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+X</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::NoRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="../../resources/resources.qrc"/>
|
||||
|
||||
Reference in New Issue
Block a user