Copy/paste on canvas

This commit is contained in:
2026-08-17 12:12:57 +02:00
parent fcee2ac8b6
commit 0bb2141673
8 changed files with 182 additions and 49 deletions

View File

@@ -5,7 +5,7 @@ import argparse
from PySide6.QtWidgets import QApplication, QMessageBox from PySide6.QtWidgets import QApplication, QMessageBox
from bedit_gui.controllers.clipboard_controller import ClipboardController, DocumentTreeClipboardHandler, TextClipboardHandler from bedit_gui.controllers.clipboard_controller import ClipboardController, DocumentTreeClipboardHandler, GraphEditorClipboardHandler, TextClipboardHandler
from bedit_gui.controllers.document_controller import DocumentController from bedit_gui.controllers.document_controller import DocumentController
from bedit_gui.controllers.log_controller import LogController from bedit_gui.controllers.log_controller import LogController
from bedit_gui.controllers.settings_controller import SettingsController from bedit_gui.controllers.settings_controller import SettingsController
@@ -59,7 +59,7 @@ def main() -> int:
ViewMenuController(window) ViewMenuController(window)
document_tree_controller = DocumentTreeController(document, window) document_tree_controller = DocumentTreeController(document, window)
clipboard = ClipboardService(app) clipboard = ClipboardService(app)
ClipboardController(window, clipboard, [TextClipboardHandler(clipboard), DocumentTreeClipboardHandler(document, window.ui.documentTree, document_tree_controller.model, clipboard)]) ClipboardController(window, clipboard, [TextClipboardHandler(clipboard), DocumentTreeClipboardHandler(document, window.ui.documentTree, document_tree_controller.model, clipboard), GraphEditorClipboardHandler(document, window.graph_editor, clipboard)])
window_state_controller = WindowStateController(app, window) window_state_controller = WindowStateController(app, window)
window_state_controller.restore() window_state_controller.restore()

View File

@@ -108,15 +108,20 @@ class DeleteComponent(QUndoCommand):
class PasteComponents(QUndoCommand): class PasteComponents(QUndoCommand):
def __init__(self, document: object, target: dict[ComponentID, Component], components: dict[ComponentID, Component], icons: dict[ComponentID, Icon]) -> None: def __init__(self, document: object, target: dict[ComponentID, Component], components: dict[ComponentID, Component], icons: dict[ComponentID, Icon], graph_id: ComponentID | None = None, positions: dict[ComponentID, tuple[int, int]] | None = None) -> None:
super().__init__("Paste components") super().__init__("Paste components")
self.document = document self.document = document
self.target = target self.target = target
self.components = components self.components = components
self.icons = icons self.icons = icons
self.graph_id = graph_id
self.positions = positions or {}
def redo(self) -> None: def redo(self) -> None:
self.target.update(self.components) self.target.update(self.components)
if self.graph_id is not None:
for component_id, position in self.positions.items():
self.document._set_graph_component_position(self.graph_id, component_id, position)
self.document.model_changed.emit(self.document.model) self.document.model_changed.emit(self.document.model)
for component_id, icon in self.icons.items(): for component_id, icon in self.icons.items():
self.document._set_component_icon(component_id, icon) self.document._set_component_icon(component_id, icon)
@@ -124,6 +129,9 @@ class PasteComponents(QUndoCommand):
def undo(self) -> None: def undo(self) -> None:
for component_id in self.components: for component_id in self.components:
self.target.pop(component_id, None) self.target.pop(component_id, None)
if self.graph_id is not None:
for component_id in self.positions:
self.document._set_graph_component_position(self.graph_id, component_id, None)
self.document.model_changed.emit(self.document.model) self.document.model_changed.emit(self.document.model)
for component_id in self.icons: for component_id in self.icons:
self.document._set_component_icon(component_id, None) self.document._set_component_icon(component_id, None)

View File

@@ -8,6 +8,7 @@ from bedit_core.models import Document as CoreDocument
from bedit_gui.documents import Document from bedit_gui.documents import Document
from bedit_gui.services.clipboard import ClipboardService from bedit_gui.services.clipboard import ClipboardService
from bedit_gui.services.component_clipboard import export_components, import_components 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 from bedit_gui.views.models.document_tree_model import DocumentTreeModel
@@ -28,6 +29,9 @@ class ClipboardHandler(QObject):
def can_paste(self) -> bool: def can_paste(self) -> bool:
return False return False
def can_delete(self) -> bool:
return False
def copy(self) -> None: def copy(self) -> None:
pass pass
@@ -37,6 +41,9 @@ class ClipboardHandler(QObject):
def paste(self) -> None: def paste(self) -> None:
pass pass
def delete(self) -> None:
pass
class TextClipboardHandler(ClipboardHandler): class TextClipboardHandler(ClipboardHandler):
def __init__(self, clipboard: ClipboardService, parent: QObject | None = None) -> None: def __init__(self, clipboard: ClipboardService, parent: QObject | None = None) -> None:
@@ -106,6 +113,9 @@ class DocumentTreeClipboardHandler(ClipboardHandler):
def can_paste(self) -> bool: def can_paste(self) -> bool:
return self._target() is not None and self.clipboard.has_format(ClipboardService.COMPONENTS_MIME) 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: def copy(self) -> None:
components = self._selected_components() components = self._selected_components()
if components: if components:
@@ -129,6 +139,9 @@ class DocumentTreeClipboardHandler(ClipboardHandler):
return return
self.document.paste_components(target, components, icons) 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: def _write_components(self, components: list[Component]) -> None:
payload = export_components(self.document, components) payload = export_components(self.document, components)
self.clipboard.set_json(ClipboardService.COMPONENTS_MIME, payload, "\n".join(component.name for component in 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 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): class ClipboardController(QObject):
def __init__(self, window: QMainWindow, clipboard: ClipboardService, handlers: list[ClipboardHandler]) -> None: def __init__(self, window: QMainWindow, clipboard: ClipboardService, handlers: list[ClipboardHandler]) -> None:
super().__init__(window) super().__init__(window)
@@ -171,6 +251,7 @@ class ClipboardController(QObject):
window.ui.actionCopy.triggered.connect(self.copy) window.ui.actionCopy.triggered.connect(self.copy)
window.ui.actionCut.triggered.connect(self.cut) window.ui.actionCut.triggered.connect(self.cut)
window.ui.actionPaste.triggered.connect(self.paste) window.ui.actionPaste.triggered.connect(self.paste)
window.ui.actionDelete.triggered.connect(self.delete)
application = QApplication.instance() application = QApplication.instance()
application.focusChanged.connect(self.update_actions) application.focusChanged.connect(self.update_actions)
application.installEventFilter(self) application.installEventFilter(self)
@@ -209,8 +290,15 @@ class ClipboardController(QObject):
handler.paste() handler.paste()
self.update_actions() 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: def update_actions(self, *_args: object) -> None:
handler = self.active_handler() handler = self.active_handler()
self.window.ui.actionCopy.setEnabled(handler is not None and handler.can_copy()) 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.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.actionPaste.setEnabled(handler is not None and handler.can_paste())
self.window.ui.actionDelete.setEnabled(handler is not None and handler.can_delete())

View File

@@ -66,8 +66,8 @@ class DocumentTreeController(QObject):
document.equation_text_changed.connect(self._on_equation_text_changed) document.equation_text_changed.connect(self._on_equation_text_changed)
self.model.rename_document_requested.connect(self.document.rename) self.model.rename_document_requested.connect(self.document.rename)
self.model.rename_component_requested.connect(self.document.rename_component) 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_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 # Add deselection with esc to this widget
window.ui.actionEscape.setShortcutContext(Qt.ShortcutContext.WidgetWithChildrenShortcut) window.ui.actionEscape.setShortcutContext(Qt.ShortcutContext.WidgetWithChildrenShortcut)
@@ -98,13 +98,14 @@ class DocumentTreeController(QObject):
def _on_document_changed(self, model: CoreDocument) -> None: def _on_document_changed(self, model: CoreDocument) -> None:
"""Rebuild the tree whenever New/Open replaces the core document.""" """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.model.set_document(model)
self._components = {} self._components = {}
self._collect_components(model.root) self._collect_components(model.root)
for component_id, component in self._components.items(): for component_id, component in self._components.items():
icon = self.document.component_icon(component_id) icon = self.document.component_icon(component_id)
self.model.set_component_icon(component_id, render_fitted_icon(icon, component.interface.ports, ICON_SIZE)) 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 # Optional presentation behavior. Later, you could instead remember
# expanded component IDs and restore only those nodes. # expanded component IDs and restore only those nodes.
@@ -161,27 +162,36 @@ class DocumentTreeController(QObject):
if not isinstance(component, Component): if not isinstance(component, Component):
return 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) menu = QMenu(self.window.ui.documentTree)
edit_interface = menu.addAction("Edit Interface") edit_interface = menu.addAction("Edit Interface")
edit_params = menu.addAction("Edit Parameters") edit_params = menu.addAction("Edit Parameters")
edit_icon = menu.addAction("Edit Icon") edit_icon = menu.addAction("Edit Icon")
menu.addSeparator() menu.addSeparator()
add_graph_component = None
add_equation_component = None
if isinstance(component.implementation, GraphImplementation):
add_graph_component = menu.addAction("Add Graph Component") add_graph_component = menu.addAction("Add Graph Component")
add_equation_component = menu.addAction("Add Equation Component") add_equation_component = menu.addAction("Add Equation Component")
menu.addSeparator() menu.addSeparator()
delete_component = menu.addAction("Delete Component") delete_component = menu.addAction("Delete Component")
selected = menu.exec( selected = menu.exec(global_position)
self.window.ui.documentTree.viewport().mapToGlobal(position)
)
if selected is edit_interface: if selected is edit_interface:
self._edit_interface(component) self._edit_interface(component)
elif selected is edit_params: elif selected is edit_params:
self._edit_params(component) self._edit_params(component)
elif selected is edit_icon: elif selected is edit_icon:
self._edit_icon(component) 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) 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) self._add_equation_component(component)
elif selected is delete_component: elif selected is delete_component:
self._delete_component(component) self._delete_component(component)

View File

@@ -322,6 +322,16 @@ class Document(QObject):
names.add(component.name) names.add(component.name)
self.undo_stack.push(PasteComponents(self, target, components, icons)) self.undo_stack.push(PasteComponents(self, target, components, icons))
def paste_graph_components(self, graph_component: Component, components: dict[ComponentID, Component], icons: dict[ComponentID, Icon], positions: dict[ComponentID, tuple[int, int]]) -> None:
if not isinstance(graph_component.implementation, GraphImplementation) or not components:
return
target = graph_component.implementation.graph.components
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, self.component_id(graph_component), positions))
@staticmethod @staticmethod
def _unique_component_name(components: dict[ComponentID, Component], name: str) -> str: def _unique_component_name(components: dict[ComponentID, Component], name: str) -> str:
return Document._unique_name({component.name for component in components.values()}, name) return Document._unique_name({component.name for component in components.values()}, name)

View File

@@ -41,9 +41,8 @@
<bool>false</bool> <bool>false</bool>
</property> </property>
<addaction name="actionZoomToFit"/> <addaction name="actionZoomToFit"/>
<addaction name="actionAddComponent"/> <addaction name="actionMouseMode"/>
<addaction name="actionAddConnection"/> <addaction name="actionConnectionMode"/>
<addaction name="actionDelete"/>
</widget> </widget>
</item> </item>
<item> <item>
@@ -72,28 +71,28 @@
<string>Zoom canvas to fit</string> <string>Zoom canvas to fit</string>
</property> </property>
</action> </action>
<action name="actionAddComponent"> <action name="actionMouseMode">
<property name="icon">
<iconset resource="../../resources/resources.qrc">
<normaloff>:/icons/icons/edit-select.png</normaloff>:/icons/icons/edit-select.png</iconset>
</property>
<property name="text"> <property name="text">
<string>Add Component</string> <string>Mouse Mode</string>
</property> </property>
<property name="toolTip"> <property name="toolTip">
<string>Add a component</string> <string>Mouse Mode</string>
</property> </property>
</action> </action>
<action name="actionAddConnection"> <action name="actionConnectionMode">
<property name="icon">
<iconset resource="../../resources/resources.qrc">
<normaloff>:/icons/icons/network-connect.png</normaloff>:/icons/icons/network-connect.png</iconset>
</property>
<property name="text"> <property name="text">
<string>Add Connection</string> <string>Connection Mode</string>
</property> </property>
<property name="toolTip"> <property name="toolTip">
<string>Add a connection</string> <string>Connection Mode</string>
</property>
</action>
<action name="actionDelete">
<property name="text">
<string>Delete</string>
</property>
<property name="toolTip">
<string>Delete selected graph items</string>
</property> </property>
</action> </action>
</widget> </widget>

View File

@@ -30,12 +30,16 @@ class Ui_graphEditorWidget(object):
icon = QIcon() icon = QIcon()
icon.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionZoomToFit.setIcon(icon) self.actionZoomToFit.setIcon(icon)
self.actionAddComponent = QAction(graphEditorWidget) self.actionMouseMode = QAction(graphEditorWidget)
self.actionAddComponent.setObjectName(u"actionAddComponent") self.actionMouseMode.setObjectName(u"actionMouseMode")
self.actionAddConnection = QAction(graphEditorWidget) icon1 = QIcon()
self.actionAddConnection.setObjectName(u"actionAddConnection") icon1.addFile(u":/icons/icons/edit-select.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionDelete = QAction(graphEditorWidget) self.actionMouseMode.setIcon(icon1)
self.actionDelete.setObjectName(u"actionDelete") self.actionConnectionMode = QAction(graphEditorWidget)
self.actionConnectionMode.setObjectName(u"actionConnectionMode")
icon2 = QIcon()
icon2.addFile(u":/icons/icons/network-connect.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionConnectionMode.setIcon(icon2)
self.verticalLayout = QVBoxLayout(graphEditorWidget) self.verticalLayout = QVBoxLayout(graphEditorWidget)
self.verticalLayout.setSpacing(0) self.verticalLayout.setSpacing(0)
self.verticalLayout.setObjectName(u"verticalLayout") self.verticalLayout.setObjectName(u"verticalLayout")
@@ -57,9 +61,8 @@ class Ui_graphEditorWidget(object):
self.graphToolBar.addAction(self.actionZoomToFit) self.graphToolBar.addAction(self.actionZoomToFit)
self.graphToolBar.addAction(self.actionAddComponent) self.graphToolBar.addAction(self.actionMouseMode)
self.graphToolBar.addAction(self.actionAddConnection) self.graphToolBar.addAction(self.actionConnectionMode)
self.graphToolBar.addAction(self.actionDelete)
self.retranslateUi(graphEditorWidget) self.retranslateUi(graphEditorWidget)
@@ -72,17 +75,13 @@ class Ui_graphEditorWidget(object):
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.actionZoomToFit.setToolTip(QCoreApplication.translate("graphEditorWidget", u"Zoom canvas to fit", None)) self.actionZoomToFit.setToolTip(QCoreApplication.translate("graphEditorWidget", u"Zoom canvas to fit", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.actionAddComponent.setText(QCoreApplication.translate("graphEditorWidget", u"Add Component", None)) self.actionMouseMode.setText(QCoreApplication.translate("graphEditorWidget", u"Mouse Mode", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.actionAddComponent.setToolTip(QCoreApplication.translate("graphEditorWidget", u"Add a component", None)) self.actionMouseMode.setToolTip(QCoreApplication.translate("graphEditorWidget", u"Mouse Mode", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.actionAddConnection.setText(QCoreApplication.translate("graphEditorWidget", u"Add Connection", None)) self.actionConnectionMode.setText(QCoreApplication.translate("graphEditorWidget", u"Connection Mode", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.actionAddConnection.setToolTip(QCoreApplication.translate("graphEditorWidget", u"Add a connection", None)) self.actionConnectionMode.setToolTip(QCoreApplication.translate("graphEditorWidget", u"Connection Mode", None))
#endif // QT_CONFIG(tooltip)
self.actionDelete.setText(QCoreApplication.translate("graphEditorWidget", u"Delete", None))
#if QT_CONFIG(tooltip)
self.actionDelete.setToolTip(QCoreApplication.translate("graphEditorWidget", u"Delete selected graph items", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.graphToolBar.setWindowTitle(QCoreApplication.translate("graphEditorWidget", u"Graph tools", None)) self.graphToolBar.setWindowTitle(QCoreApplication.translate("graphEditorWidget", u"Graph tools", None))
# retranslateUi # retranslateUi

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from math import hypot from math import hypot
from PySide6.QtCore import QEvent, QObject, QPointF, QRectF, QSize, QTimer, Qt, Signal from PySide6.QtCore import QEvent, QObject, QPointF, QRectF, QSize, QTimer, Qt, Signal
from PySide6.QtGui import QColor, QPainter, QPainterPath, QPen, QWheelEvent from PySide6.QtGui import QColor, QCursor, QPainter, QPainterPath, QPen, QWheelEvent
from PySide6.QtWidgets import QGraphicsItem, QGraphicsPathItem, QGraphicsPixmapItem, QGraphicsScene, QGraphicsSceneMouseEvent, QGraphicsView, QWidget from PySide6.QtWidgets import QGraphicsItem, QGraphicsPathItem, QGraphicsPixmapItem, QGraphicsScene, QGraphicsSceneMouseEvent, QGraphicsView, QWidget
from bedit_core.models import BondCausality, BondConnection, Component, ComponentID, ConnectionID, GraphImplementation, SignalConnection from bedit_core.models import BondCausality, BondConnection, Component, ComponentID, ConnectionID, GraphImplementation, SignalConnection
@@ -55,7 +55,6 @@ class GraphConnectionItem(QGraphicsPathItem):
super().__init__() super().__init__()
self.setPath(self._connection_path(points, half_arrow, tick_at_source)) self.setPath(self._connection_path(points, half_arrow, tick_at_source))
pen = QPen(QColor("#202020"), CONNECTION_WIDTH) pen = QPen(QColor("#202020"), CONNECTION_WIDTH)
pen.setCosmetic(True)
self.setPen(pen) self.setPen(pen)
self.setZValue(-1) self.setZValue(-1)
@@ -137,9 +136,17 @@ class GraphComponentItem(QGraphicsPixmapItem):
if self.pos() != self._drag_start: if self.pos() != self._drag_start:
self.editor.finish_component_move(self.component_id, position) self.editor.finish_component_move(self.component_id, position)
def contextMenuEvent(self, event) -> None:
if not self.isSelected():
self.scene().clearSelection()
self.setSelected(True)
self.editor.component_context_menu_requested.emit(self.component_id, event.screenPos())
event.accept()
class GraphEditorWidget(QWidget): class GraphEditorWidget(QWidget):
component_move_requested = Signal(object, object, object) component_move_requested = Signal(object, object, object)
component_context_menu_requested = Signal(object, object)
def __init__(self, parent: QWidget | None = None, snap_to_grid_size: int = 4) -> None: def __init__(self, parent: QWidget | None = None, snap_to_grid_size: int = 4) -> None:
super().__init__(parent) super().__init__(parent)
@@ -298,6 +305,18 @@ class GraphEditorWidget(QWidget):
def component(self) -> Component | None: def component(self) -> Component | None:
return self._component return self._component
def selected_component_ids(self) -> list[ComponentID]:
return [item.component_id for item in self.scene.selectedItems() if isinstance(item, GraphComponentItem)]
def paste_position(self) -> tuple[int, int]:
viewport = self.ui.graphicsView.viewport()
viewport_position = viewport.mapFromGlobal(QCursor.pos())
if not viewport.rect().contains(viewport_position):
viewport_position = viewport.rect().center()
scene_position = self.ui.graphicsView.mapToScene(viewport_position)
size = self.snap_to_grid_size
return round(scene_position.x() / size) * size, round(scene_position.y() / size) * size
def zoom_to_fit(self) -> None: def zoom_to_fit(self) -> None:
bounds = self.scene.itemsBoundingRect() bounds = self.scene.itemsBoundingRect()
if bounds.isEmpty(): if bounds.isEmpty():