Library drag-in
This commit is contained in:
@@ -182,6 +182,7 @@ class GraphEditorClipboardHandler(ClipboardHandler):
|
||||
self.editor = editor
|
||||
self.clipboard = clipboard
|
||||
editor.scene.selectionChanged.connect(self.availability_changed)
|
||||
editor.component_drop_requested.connect(self.drop_components)
|
||||
|
||||
def owns_focus(self, widget: QWidget) -> bool:
|
||||
return widget is self.editor or self.editor.isAncestorOf(widget)
|
||||
@@ -214,12 +215,19 @@ class GraphEditorClipboardHandler(ClipboardHandler):
|
||||
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
|
||||
x, y = self.editor.paste_position()
|
||||
self._paste_payload(graph_component, payload, (x, y))
|
||||
|
||||
def drop_components(self, graph_component: Component, payload: dict, position: tuple[int, int]) -> None:
|
||||
self._paste_payload(graph_component, payload, position)
|
||||
|
||||
def _paste_payload(self, graph_component: Component, payload: dict, position: tuple[int, int]) -> None:
|
||||
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()
|
||||
x, y = 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)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
from PySide6.QtCore import QObject, QSize
|
||||
from PySide6.QtCore import QObject, QSize, Qt
|
||||
from PySide6.QtWidgets import QAbstractItemView, QHeaderView
|
||||
|
||||
from bedit_core.models import Component, ComponentID, GraphImplementation
|
||||
from bedit_gui.models import Icon, IconDatabase
|
||||
from bedit_gui.services.application_settings import ApplicationSettings
|
||||
from bedit_gui.services.component_clipboard import export_component_data
|
||||
from bedit_gui.services.libraries import load_library_documents
|
||||
from bedit_gui.utils.icon import render_fitted_icon
|
||||
from bedit_gui.views.main_window import MainWindow
|
||||
from bedit_gui.views.models.document_tree_model import DocumentTreeModel
|
||||
from bedit_gui.views.models.library_tree_model import LibraryTreeModel
|
||||
|
||||
ICON_SIZE = QSize(32, 32)
|
||||
|
||||
@@ -17,13 +18,17 @@ class LibraryController(QObject):
|
||||
super().__init__(window)
|
||||
self.window = window
|
||||
self.settings = settings
|
||||
self.model = DocumentTreeModel(editable=False)
|
||||
self._component_sources: dict[int, tuple[ComponentID, dict[ComponentID, Icon]]] = {}
|
||||
self.model = LibraryTreeModel(self._component_payload)
|
||||
|
||||
tree = window.ui.libraryTree
|
||||
tree.setModel(self.model)
|
||||
tree.setHeaderHidden(True)
|
||||
tree.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
tree.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
tree.setDragEnabled(True)
|
||||
tree.setDragDropMode(QAbstractItemView.DragDropMode.DragOnly)
|
||||
tree.setDefaultDropAction(Qt.DropAction.CopyAction)
|
||||
tree.setIconSize(QSize(48, 48))
|
||||
tree.header().setStretchLastSection(False)
|
||||
tree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
||||
@@ -34,6 +39,11 @@ class LibraryController(QObject):
|
||||
|
||||
def reload(self) -> None:
|
||||
libraries = load_library_documents(self.settings.library_paths)
|
||||
self._component_sources = {}
|
||||
for library in libraries:
|
||||
database = library.document.metadata.get("icon_database") if library.document.metadata is not None else None
|
||||
icons = database.icons if isinstance(database, IconDatabase) else {}
|
||||
self._collect_component_sources(library.document.root, icons)
|
||||
self.model.set_documents([library.document for library in libraries])
|
||||
for library in libraries:
|
||||
database = library.document.metadata.get("icon_database") if library.document.metadata is not None else None
|
||||
@@ -41,6 +51,24 @@ class LibraryController(QObject):
|
||||
self._set_component_icons(library.document.root, icons)
|
||||
self.window.ui.libraryTree.expandAll()
|
||||
|
||||
def _component_payload(self, components: list[Component]) -> dict:
|
||||
roots = {}
|
||||
icons = {}
|
||||
for component in components:
|
||||
source = self._component_sources.get(id(component))
|
||||
if source is None:
|
||||
continue
|
||||
component_id, source_icons = source
|
||||
roots[component_id] = component
|
||||
icons.update(source_icons)
|
||||
return export_component_data(roots, icons)
|
||||
|
||||
def _collect_component_sources(self, components: dict[ComponentID, Component], icons: dict[ComponentID, Icon]) -> None:
|
||||
for component_id, component in components.items():
|
||||
self._component_sources[id(component)] = (component_id, icons)
|
||||
if isinstance(component.implementation, GraphImplementation):
|
||||
self._collect_component_sources(component.implementation.graph.components, icons)
|
||||
|
||||
def _set_component_icons(self, components: dict[ComponentID, Component], icons: dict[ComponentID, Icon]) -> None:
|
||||
for component_id, component in components.items():
|
||||
self.model.set_component_icon(component_id, render_fitted_icon(icons.get(component_id, Icon()), component.interface.ports, ICON_SIZE))
|
||||
|
||||
@@ -6,9 +6,11 @@ from typing import Any
|
||||
from PySide6.QtCore import QMimeData, QObject, Signal
|
||||
from PySide6.QtGui import QClipboard, QGuiApplication
|
||||
|
||||
from bedit_gui.services.component_clipboard import COMPONENTS_MIME
|
||||
|
||||
|
||||
class ClipboardService(QObject):
|
||||
COMPONENTS_MIME = "application/x-bedit-components+json"
|
||||
COMPONENTS_MIME = COMPONENTS_MIME
|
||||
changed = Signal()
|
||||
|
||||
def __init__(self, parent: QObject | None = None) -> None:
|
||||
|
||||
@@ -9,18 +9,25 @@ from bedit_gui.documents import Document as GuiDocument
|
||||
from bedit_gui.models import Icon, ShapeID
|
||||
|
||||
FORMAT_VERSION = 1
|
||||
COMPONENTS_MIME = "application/x-bedit-components+json"
|
||||
|
||||
|
||||
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}
|
||||
icons[component_id] = icon
|
||||
return export_component_data(roots, icons)
|
||||
|
||||
|
||||
def export_component_data(components: dict[ComponentID, Component], icons: dict[ComponentID, Icon]) -> dict[str, Any]:
|
||||
serialized = document_to_data(Document(format_version=1, id=ID(), name="Clipboard", root=components))
|
||||
component_ids = set(_all_component_ids(components))
|
||||
icon_data = {str(component_id): icon.to_data() for component_id, icon in icons.items() if component_id in component_ids}
|
||||
return {"format_version": FORMAT_VERSION, "type": "components", "components": serialized["root"], "icons": icon_data}
|
||||
|
||||
|
||||
def import_components(payload: dict[str, Any]) -> tuple[dict[ComponentID, Component], dict[ComponentID, Icon]]:
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from enum import Enum
|
||||
from itertools import pairwise
|
||||
from math import hypot
|
||||
|
||||
from PySide6.QtCore import QEvent, QObject, QPointF, QRectF, QSize, QTimer, Qt, Signal
|
||||
from PySide6.QtGui import QActionGroup, QBrush, QColor, QCursor, QKeySequence, QMouseEvent, QPainter, QPainterPath, QPen, QShortcut, QWheelEvent
|
||||
from PySide6.QtGui import QActionGroup, QBrush, QColor, QCursor, QDragEnterEvent, QDragMoveEvent, QDropEvent, QKeySequence, QMouseEvent, QPainter, QPainterPath, QPen, QShortcut, QWheelEvent
|
||||
from PySide6.QtWidgets import QGraphicsEllipseItem, QGraphicsItem, QGraphicsPathItem, QGraphicsPixmapItem, QGraphicsScene, QGraphicsSceneMouseEvent, QGraphicsTextItem, QGraphicsView, QMenu, QWidget
|
||||
|
||||
from bedit_core.models import BondCausality, BondConnection, BondPort, Component, ComponentID, Connection, ConnectionID, GraphImplementation, Port, PortID, SignalConnection, SignalDirection, SignalPort
|
||||
from bedit_gui.models import Graph, GraphComponentLabel, GraphConnection, Icon
|
||||
from bedit_gui.services.component_clipboard import COMPONENTS_MIME
|
||||
from bedit_gui.ui.generated.ui_graph_editor_widget import Ui_graphEditorWidget
|
||||
from bedit_gui.utils.icon import get_pixmap_bounding_box, render_icon
|
||||
|
||||
@@ -291,6 +293,7 @@ class GraphEditorWidget(QWidget):
|
||||
connection_points_change_requested = Signal(object, object, object, str)
|
||||
connection_add_requested = Signal(object, object)
|
||||
connections_delete_requested = Signal(object, object)
|
||||
component_drop_requested = Signal(object, object, object)
|
||||
|
||||
def __init__(self, parent: QWidget | None = None, snap_to_grid_size: int = 4) -> None:
|
||||
super().__init__(parent)
|
||||
@@ -314,6 +317,7 @@ class GraphEditorWidget(QWidget):
|
||||
self.ui.graphicsView.setScene(self.scene)
|
||||
self.ui.graphicsView.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
|
||||
self.ui.graphicsView.viewport().installEventFilter(self)
|
||||
self.ui.graphicsView.viewport().setAcceptDrops(True)
|
||||
self._mode_actions = QActionGroup(self)
|
||||
self._mode_actions.setExclusive(True)
|
||||
self._mode_actions.addAction(self.ui.actionMouseMode)
|
||||
@@ -736,6 +740,25 @@ class GraphEditorWidget(QWidget):
|
||||
self.ui.graphicsView.centerOn(bounds.center())
|
||||
|
||||
def eventFilter(self, watched: QObject, event: QEvent) -> bool:
|
||||
if watched is self.ui.graphicsView.viewport() and event.type() in (QEvent.Type.DragEnter, QEvent.Type.DragMove):
|
||||
assert isinstance(event, (QDragEnterEvent, QDragMoveEvent))
|
||||
if self._component is not None and event.mimeData().hasFormat(COMPONENTS_MIME):
|
||||
event.setDropAction(Qt.DropAction.CopyAction)
|
||||
event.accept()
|
||||
return True
|
||||
if watched is self.ui.graphicsView.viewport() and event.type() == QEvent.Type.Drop:
|
||||
assert isinstance(event, QDropEvent)
|
||||
if self._component is not None and event.mimeData().hasFormat(COMPONENTS_MIME):
|
||||
try:
|
||||
payload = json.loads(bytes(event.mimeData().data(COMPONENTS_MIME)).decode("utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError):
|
||||
return True
|
||||
if isinstance(payload, dict):
|
||||
position = self._snap_position(self.ui.graphicsView.mapToScene(event.position().toPoint()))
|
||||
self.component_drop_requested.emit(self._component, payload, position)
|
||||
event.setDropAction(Qt.DropAction.CopyAction)
|
||||
event.accept()
|
||||
return True
|
||||
if watched is self.ui.graphicsView.viewport() and event.type() == QEvent.Type.MouseMove:
|
||||
assert isinstance(event, QMouseEvent)
|
||||
self._update_connection_preview(self.ui.graphicsView.mapToScene(event.position().toPoint()))
|
||||
|
||||
55
src/bedit_gui/views/models/library_tree_model.py
Normal file
55
src/bedit_gui/views/models/library_tree_model.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections.abc import Callable
|
||||
|
||||
from PySide6.QtCore import QMimeData, QModelIndex, Qt
|
||||
|
||||
from bedit_core.models import Component
|
||||
from bedit_gui.services.component_clipboard import COMPONENTS_MIME
|
||||
from bedit_gui.views.models.document_tree_model import DocumentTreeModel
|
||||
|
||||
PayloadFactory = Callable[[list[Component]], dict]
|
||||
|
||||
|
||||
class LibraryTreeModel(DocumentTreeModel):
|
||||
def __init__(self, payload_factory: PayloadFactory) -> None:
|
||||
super().__init__(editable=False)
|
||||
self._payload_factory = payload_factory
|
||||
|
||||
def flags(self, index: QModelIndex) -> Qt.ItemFlag:
|
||||
flags = super().flags(index)
|
||||
if isinstance(self.value(index), Component):
|
||||
flags |= Qt.ItemFlag.ItemIsDragEnabled
|
||||
return flags
|
||||
|
||||
def mimeTypes(self) -> list[str]:
|
||||
return [COMPONENTS_MIME]
|
||||
|
||||
def mimeData(self, indexes: list[QModelIndex]) -> QMimeData:
|
||||
rows = [index for index in indexes if index.column() == 0 and isinstance(self.value(index), Component)]
|
||||
selected = {id(self.value(index)) for index in rows}
|
||||
components = []
|
||||
added = set()
|
||||
for index in rows:
|
||||
parent = index.parent()
|
||||
if any(id(self.value(parent_index)) in selected for parent_index in self._parents(parent)):
|
||||
continue
|
||||
component = self.value(index)
|
||||
if isinstance(component, Component) and id(component) not in added:
|
||||
components.append(component)
|
||||
added.add(id(component))
|
||||
mime = QMimeData()
|
||||
if components:
|
||||
mime.setData(COMPONENTS_MIME, json.dumps(self._payload_factory(components)).encode("utf-8"))
|
||||
mime.setText("\n".join(component.name for component in components))
|
||||
return mime
|
||||
|
||||
def supportedDragActions(self) -> Qt.DropAction:
|
||||
return Qt.DropAction.CopyAction
|
||||
|
||||
@staticmethod
|
||||
def _parents(index: QModelIndex):
|
||||
while index.isValid():
|
||||
yield index
|
||||
index = index.parent()
|
||||
Reference in New Issue
Block a user