Reorganized the application around a clear frontend/backend boundary.
src/bedit/
├── __main__.py
├── core/ # Pure Python, no PySide
│ ├── model.py
│ ├── port_types.py
│ ├── serializer.py
│ └── libraries.py
└── gui/ # All Qt-dependent code
├── app.py
├── main_window.py
├── preferences.py
├── controllers/
├── dialogs/
├── graphics/
├── models/
└── generated/ # Designer/resource output only
Notable improvements:
Domain models, serialization, port types, and library parsing are now Qt-free.
Qt signals and undo infrastructure are explicitly isolated under gui/controllers.
Library parsing is separated from the Qt repository and tree models.
All generated Python is contained in gui/generated.
Designer build tasks now write to the generated directory.
The application entry point and package metadata use the new paths.
README now documents the structure and dependency rules.
Removed the old mixed document, library, and workspace packages.
This commit is contained in:
616
BEdit/src/bedit/gui/graphics/workspace.py
Normal file
616
BEdit/src/bedit/gui/graphics/workspace.py
Normal file
@@ -0,0 +1,616 @@
|
||||
import json
|
||||
|
||||
from PySide6.QtCore import QMimeData, QPointF, QRectF, Qt, Signal
|
||||
from PySide6.QtGui import (
|
||||
QColor,
|
||||
QDragEnterEvent,
|
||||
QDropEvent,
|
||||
QMouseEvent,
|
||||
QWheelEvent,
|
||||
QPainter,
|
||||
QPainterPath,
|
||||
QPen,
|
||||
QTransform,
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QGraphicsEllipseItem,
|
||||
QGraphicsItem,
|
||||
QGraphicsObject,
|
||||
QGraphicsPathItem,
|
||||
QGraphicsScene,
|
||||
QGraphicsSceneContextMenuEvent,
|
||||
QGraphicsSceneMouseEvent,
|
||||
QGraphicsView,
|
||||
QApplication,
|
||||
QMenu,
|
||||
QStyleOptionGraphicsItem,
|
||||
QToolTip,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from bedit.core.model import Component, Connection, Endpoint, Port
|
||||
from bedit.gui.controllers.document import DocumentController
|
||||
from bedit.gui.models.library_tree import COMPONENT_MIME_TYPE
|
||||
from bedit.gui.graphics.icon_renderer import icon_bounds, paint_icon
|
||||
from bedit.gui.preferences import application_settings
|
||||
|
||||
|
||||
SELECTION_MIME_TYPE = "application/x-bedit-selection"
|
||||
|
||||
|
||||
def _graph_snap_size() -> int:
|
||||
return application_settings().value("grid/graphSnapSize", 8, type=int)
|
||||
|
||||
|
||||
def _graph_grid_size() -> int:
|
||||
return application_settings().value("grid/graphSize", 64, type=int)
|
||||
|
||||
|
||||
def _snapped(position: QPointF) -> QPointF:
|
||||
grid = _graph_snap_size()
|
||||
return QPointF(round(position.x() / grid) * grid, round(position.y() / grid) * grid)
|
||||
|
||||
|
||||
class ConnectionPortItem(QGraphicsEllipseItem):
|
||||
def __init__(self, endpoint: Endpoint, role: str, label: str, parent: QGraphicsItem) -> None:
|
||||
super().__init__(-6, -6, 12, 12, parent)
|
||||
self.endpoint = endpoint
|
||||
self.role = role
|
||||
self.setBrush(QColor("#ffffff"))
|
||||
self.setPen(QPen(QColor("#303030"), 1.5))
|
||||
self.setZValue(2)
|
||||
self.setToolTip(label)
|
||||
|
||||
|
||||
class ComponentGraphicsItem(QGraphicsObject):
|
||||
WIDTH = 128.0
|
||||
HEIGHT = 128.0
|
||||
|
||||
def __init__(self, component: Component, controller: DocumentController) -> None:
|
||||
super().__init__()
|
||||
self.component_id = component.id
|
||||
self.component = component
|
||||
self.controller = controller
|
||||
self.drag_start = QPointF()
|
||||
self.hitbox = icon_bounds(component.icon)
|
||||
self.setFlags(
|
||||
QGraphicsItem.GraphicsItemFlag.ItemIsMovable
|
||||
| QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
|
||||
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
|
||||
)
|
||||
self.input_ports = self._create_ports(component.inputs, "target", 0.0)
|
||||
self.output_ports = self._create_ports(component.outputs, "source", self.WIDTH)
|
||||
self.setTransformOriginPoint(self.hitbox.center())
|
||||
self.setRotation(component.rotation)
|
||||
|
||||
def _create_ports(self, ports, role: str, x: float) -> dict[str, ConnectionPortItem]:
|
||||
result = {}
|
||||
spacing = self.HEIGHT / (len(ports) + 1)
|
||||
for index, port in enumerate(ports, start=1):
|
||||
endpoint = Endpoint(block=self.component_id, port=port.id)
|
||||
item = ConnectionPortItem(endpoint, role, port.name, self)
|
||||
position = port.properties.get("iconPosition", {})
|
||||
item.setPos(
|
||||
float(position.get("x", x)) * self.WIDTH / self.component.icon.width,
|
||||
float(position.get("y", spacing * index)) * self.HEIGHT / self.component.icon.height,
|
||||
)
|
||||
result[port.id] = item
|
||||
return result
|
||||
|
||||
def boundingRect(self) -> QRectF: # noqa: N802
|
||||
return self.hitbox
|
||||
|
||||
def paint(
|
||||
self,
|
||||
painter: QPainter,
|
||||
option: QStyleOptionGraphicsItem,
|
||||
widget: QWidget | None = None,
|
||||
) -> None:
|
||||
del option, widget
|
||||
paint_icon(
|
||||
painter,
|
||||
self.component.icon,
|
||||
QRectF(0, 0, self.WIDTH, self.HEIGHT),
|
||||
)
|
||||
if self.isSelected():
|
||||
painter.setBrush(Qt.BrushStyle.NoBrush)
|
||||
painter.setPen(QPen(QColor("#2563eb"), 2, Qt.PenStyle.DashLine))
|
||||
painter.drawRect(self.boundingRect())
|
||||
|
||||
def mouseDoubleClickEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
self.controller.activate_component(self.component_id)
|
||||
event.accept()
|
||||
|
||||
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
|
||||
if not self.isSelected():
|
||||
scene = self.scene()
|
||||
if scene is not None:
|
||||
scene.clearSelection()
|
||||
self.setSelected(True)
|
||||
menu = QMenu()
|
||||
options_action = menu.addAction("Component Options…")
|
||||
ports_action = menu.addAction("Port Options…")
|
||||
selected = menu.exec(event.screenPos())
|
||||
if selected is options_action:
|
||||
scene = self.scene()
|
||||
if isinstance(scene, GraphScene):
|
||||
scene.componentOptionsRequested.emit(self.component_id)
|
||||
elif selected is ports_action:
|
||||
scene = self.scene()
|
||||
if isinstance(scene, GraphScene):
|
||||
scene.componentPortOptionsRequested.emit(self.component_id)
|
||||
event.accept()
|
||||
|
||||
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
self.drag_start = self.pos()
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
super().mouseReleaseEvent(event)
|
||||
snapped = _snapped(self.pos())
|
||||
self.setPos(snapped)
|
||||
self.controller.move_component(self.component_id, self.drag_start, snapped)
|
||||
|
||||
def itemChange(self, change, value): # noqa: N802
|
||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
|
||||
value = _snapped(value)
|
||||
elif change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
||||
scene = self.scene()
|
||||
if isinstance(scene, GraphScene):
|
||||
scene.update_connections_for_block(self.component_id)
|
||||
return super().itemChange(change, value)
|
||||
|
||||
|
||||
class InterfaceTerminalItem(QGraphicsObject):
|
||||
WIDTH = 110.0
|
||||
HEIGHT = 36.0
|
||||
|
||||
def __init__(self, port: Port, direction: str, controller: DocumentController) -> None:
|
||||
super().__init__()
|
||||
self.port = port
|
||||
self.direction = direction
|
||||
self.controller = controller
|
||||
self.drag_start = QPointF()
|
||||
role = "source" if direction == "input" else "target"
|
||||
self.connection_port = ConnectionPortItem(
|
||||
Endpoint(interface=port.id), role, port.name, self
|
||||
)
|
||||
connection_x = self.WIDTH if direction == "input" else 0.0
|
||||
self.connection_port.setPos(connection_x, self.HEIGHT / 2)
|
||||
self.setFlags(
|
||||
QGraphicsItem.GraphicsItemFlag.ItemIsMovable
|
||||
| QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
|
||||
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
|
||||
)
|
||||
self.setToolTip(f"Component {direction}: {port.name}")
|
||||
|
||||
def boundingRect(self) -> QRectF: # noqa: N802
|
||||
return QRectF(0, 0, self.WIDTH, self.HEIGHT)
|
||||
|
||||
def paint(
|
||||
self,
|
||||
painter: QPainter,
|
||||
option: QStyleOptionGraphicsItem,
|
||||
widget: QWidget | None = None,
|
||||
) -> None:
|
||||
del option, widget
|
||||
painter.setBrush(QColor("#e5e7eb"))
|
||||
painter.setPen(QPen(QColor("#4b5563"), 1.5))
|
||||
painter.drawRoundedRect(self.boundingRect(), 4, 4)
|
||||
painter.setPen(QColor("#202020"))
|
||||
marker = "IN" if self.direction == "input" else "OUT"
|
||||
painter.drawText(
|
||||
self.boundingRect().adjusted(8, 0, -8, 0),
|
||||
Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft,
|
||||
f"{marker} {self.port.name}",
|
||||
)
|
||||
|
||||
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
self.drag_start = self.pos()
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
|
||||
menu = QMenu()
|
||||
options_action = menu.addAction(f"{self.direction.title()} Options…")
|
||||
if menu.exec(event.screenPos()) is options_action:
|
||||
scene = self.scene()
|
||||
if isinstance(scene, GraphScene):
|
||||
scene.portOptionsRequested.emit(self.port.id, self.direction)
|
||||
event.accept()
|
||||
|
||||
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
super().mouseReleaseEvent(event)
|
||||
snapped = _snapped(self.pos())
|
||||
self.setPos(snapped)
|
||||
self.controller.move_interface_port(self.port.id, self.drag_start, snapped)
|
||||
|
||||
def itemChange(self, change, value): # noqa: N802
|
||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
|
||||
value = _snapped(value)
|
||||
elif change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
||||
scene = self.scene()
|
||||
if isinstance(scene, GraphScene):
|
||||
scene.update_connections_for_interface(self.port.id)
|
||||
return super().itemChange(change, value)
|
||||
|
||||
|
||||
class ConnectionGraphicsItem(QGraphicsPathItem):
|
||||
def __init__(self, connection_id: str, name: str = "") -> None:
|
||||
super().__init__()
|
||||
self.connection_id = connection_id
|
||||
self.name = name
|
||||
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
|
||||
self._update_pen()
|
||||
self.setZValue(-1)
|
||||
self.setToolTip(name or "Connection")
|
||||
|
||||
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
|
||||
menu = QMenu()
|
||||
options_action = menu.addAction("Connection Options…")
|
||||
if menu.exec(event.screenPos()) is options_action:
|
||||
scene = self.scene()
|
||||
if isinstance(scene, GraphScene):
|
||||
scene.connectionOptionsRequested.emit(self.connection_id)
|
||||
event.accept()
|
||||
|
||||
def itemChange(self, change, value): # noqa: N802
|
||||
result = super().itemChange(change, value)
|
||||
if change == QGraphicsItem.GraphicsItemChange.ItemSelectedHasChanged:
|
||||
self._update_pen()
|
||||
return result
|
||||
|
||||
def _update_pen(self) -> None:
|
||||
self.setPen(
|
||||
QPen(
|
||||
QColor("#f59e0b") if self.isSelected() else QColor("#285f9e"),
|
||||
4.0 if self.isSelected() else 2.5,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class GraphScene(QGraphicsScene):
|
||||
componentOptionsRequested = Signal(str)
|
||||
componentPortOptionsRequested = Signal(str)
|
||||
portOptionsRequested = Signal(str, str)
|
||||
connectionOptionsRequested = Signal(str)
|
||||
|
||||
def __init__(self, controller: DocumentController, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.controller = controller
|
||||
self.component_items: dict[str, ComponentGraphicsItem] = {}
|
||||
self.input_items: dict[str, InterfaceTerminalItem] = {}
|
||||
self.output_items: dict[str, InterfaceTerminalItem] = {}
|
||||
self.connection_items: dict[str, ConnectionGraphicsItem] = {}
|
||||
self.pending_source: ConnectionPortItem | None = None
|
||||
self.setSceneRect(-2000, -2000, 4000, 4000)
|
||||
|
||||
controller.documentReset.connect(self.rebuild)
|
||||
controller.activeGraphChanged.connect(self.rebuild)
|
||||
controller.componentMoved.connect(self.set_component_position)
|
||||
controller.componentRotated.connect(self.set_component_rotation)
|
||||
self.rebuild()
|
||||
|
||||
def rebuild(self) -> None:
|
||||
self.clear()
|
||||
self.component_items.clear()
|
||||
self.input_items.clear()
|
||||
self.output_items.clear()
|
||||
self.connection_items.clear()
|
||||
self.pending_source = None
|
||||
owner = self.controller.active_component
|
||||
if owner is None or owner.implementation_kind != "graph":
|
||||
return
|
||||
for port in owner.inputs:
|
||||
item = InterfaceTerminalItem(port, "input", self.controller)
|
||||
self.addItem(item)
|
||||
item.setPos(port.x, port.y)
|
||||
self.input_items[port.id] = item
|
||||
for port in owner.outputs:
|
||||
item = InterfaceTerminalItem(port, "output", self.controller)
|
||||
self.addItem(item)
|
||||
item.setPos(port.x, port.y)
|
||||
self.output_items[port.id] = item
|
||||
for component in owner.graph.blocks.values():
|
||||
item = ComponentGraphicsItem(component, self.controller)
|
||||
self.addItem(item)
|
||||
item.setPos(component.x, component.y)
|
||||
self.component_items[component.id] = item
|
||||
for connection in owner.graph.connections.values():
|
||||
item = ConnectionGraphicsItem(connection.id, connection.name)
|
||||
self.addItem(item)
|
||||
self.connection_items[connection.id] = item
|
||||
self.update_connection(connection.id)
|
||||
|
||||
def set_component_position(self, component_id: str, position: QPointF) -> None:
|
||||
item = self.component_items.get(component_id)
|
||||
if item is not None and item.pos() != position:
|
||||
item.setPos(position)
|
||||
|
||||
def set_component_rotation(self, component_id: str, rotation: float) -> None:
|
||||
item = self.component_items.get(component_id)
|
||||
if item is not None:
|
||||
item.setRotation(rotation)
|
||||
self.update_connections_for_block(component_id)
|
||||
|
||||
def update_connections_for_block(self, component_id: str) -> None:
|
||||
for connection in self.controller.active_graph.connections.values():
|
||||
if component_id in (connection.source.block, connection.target.block):
|
||||
self.update_connection(connection.id)
|
||||
|
||||
def update_connections_for_interface(self, port_id: str) -> None:
|
||||
for connection in self.controller.active_graph.connections.values():
|
||||
if port_id in (connection.source.interface, connection.target.interface):
|
||||
self.update_connection(connection.id)
|
||||
|
||||
def drawBackground(self, painter: QPainter, rect: QRectF) -> None: # noqa: N802
|
||||
# The view paints the grid so it always covers the complete viewport.
|
||||
del painter, rect
|
||||
|
||||
def _endpoint_item(self, endpoint: Endpoint, role: str) -> ConnectionPortItem | None:
|
||||
if endpoint.interface is not None:
|
||||
terminals = self.input_items if role == "source" else self.output_items
|
||||
terminal = terminals.get(endpoint.interface)
|
||||
return terminal.connection_port if terminal else None
|
||||
component = self.component_items.get(endpoint.block or "")
|
||||
if component is None:
|
||||
return None
|
||||
ports = component.output_ports if role == "source" else component.input_ports
|
||||
return ports.get(endpoint.port or "")
|
||||
|
||||
def update_connection(self, connection_id: str) -> None:
|
||||
connection = self.controller.active_graph.connections.get(connection_id)
|
||||
graphics = self.connection_items.get(connection_id)
|
||||
if connection is None or graphics is None:
|
||||
return
|
||||
source = self._endpoint_item(connection.source, "source")
|
||||
target = self._endpoint_item(connection.target, "target")
|
||||
if source is None or target is None:
|
||||
return
|
||||
start, end = source.scenePos(), target.scenePos()
|
||||
distance = max(50.0, abs(end.x() - start.x()) * 0.5)
|
||||
path = QPainterPath(start)
|
||||
path.cubicTo(start + QPointF(distance, 0), end - QPointF(distance, 0), end)
|
||||
graphics.setPath(path)
|
||||
|
||||
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
item = self.itemAt(event.scenePos(), QTransform())
|
||||
if isinstance(item, ConnectionPortItem):
|
||||
if item.role == "source":
|
||||
self._clear_pending_source()
|
||||
self.pending_source = item
|
||||
item.setBrush(QColor("#f5b642"))
|
||||
elif self.pending_source is not None:
|
||||
if self.pending_source.endpoint != item.endpoint:
|
||||
try:
|
||||
self.controller.connect(self.pending_source.endpoint, item.endpoint)
|
||||
except ValueError as error:
|
||||
QToolTip.showText(event.screenPos(), str(error))
|
||||
self._clear_pending_source()
|
||||
event.accept()
|
||||
return
|
||||
self._clear_pending_source()
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def _clear_pending_source(self) -> None:
|
||||
if self.pending_source is not None:
|
||||
self.pending_source.setBrush(QColor("#ffffff"))
|
||||
self.pending_source = None
|
||||
|
||||
|
||||
class GraphWorkspaceView(QGraphicsView):
|
||||
toolUsed = Signal()
|
||||
componentOptionsRequested = Signal(str)
|
||||
componentPortOptionsRequested = Signal(str)
|
||||
portOptionsRequested = Signal(str, str)
|
||||
connectionOptionsRequested = Signal(str)
|
||||
selectionAvailabilityChanged = Signal(bool)
|
||||
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.controller: DocumentController | None = None
|
||||
self.tool_mode = "pointer"
|
||||
self.paste_count = 0
|
||||
self.setAcceptDrops(True)
|
||||
self.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
self.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
|
||||
self.setBackgroundBrush(QColor("#f7f7f7"))
|
||||
self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
|
||||
self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter)
|
||||
|
||||
def drawBackground(self, painter: QPainter, rect: QRectF) -> None: # noqa: N802
|
||||
"""Paint the visible graph viewport in scene coordinates."""
|
||||
painter.fillRect(rect, QColor("#f7f7f7"))
|
||||
if (
|
||||
self.controller is None
|
||||
or self.controller.document is None
|
||||
or self.controller.active_component is None
|
||||
):
|
||||
return
|
||||
spacing = _graph_grid_size()
|
||||
left = int(rect.left()) - (int(rect.left()) % spacing)
|
||||
top = int(rect.top()) - (int(rect.top()) % spacing)
|
||||
painter.setPen(QPen(QColor("#c5cbd1"), 0))
|
||||
for x in range(left, int(rect.right()) + spacing, spacing):
|
||||
painter.drawLine(QPointF(x, rect.top()), QPointF(x, rect.bottom()))
|
||||
for y in range(top, int(rect.bottom()) + spacing, spacing):
|
||||
painter.drawLine(QPointF(rect.left(), y), QPointF(rect.right(), y))
|
||||
|
||||
def _zoom(self, factor: float) -> None:
|
||||
current = self.transform().m11()
|
||||
target = current * factor
|
||||
if 0.1 <= target <= 8.0:
|
||||
self.scale(factor, factor)
|
||||
|
||||
def zoom_in(self) -> None:
|
||||
self._zoom(1.2)
|
||||
|
||||
def zoom_out(self) -> None:
|
||||
self._zoom(1 / 1.2)
|
||||
|
||||
def center_workspace(self) -> None:
|
||||
scene = self.scene()
|
||||
if scene is None:
|
||||
return
|
||||
bounds = scene.itemsBoundingRect()
|
||||
if bounds.isEmpty():
|
||||
self.resetTransform()
|
||||
self.centerOn(0, 0)
|
||||
else:
|
||||
self.fitInView(bounds.adjusted(-80, -80, 80, 80), Qt.AspectRatioMode.KeepAspectRatio)
|
||||
|
||||
def wheelEvent(self, event: QWheelEvent) -> None: # noqa: N802
|
||||
self._zoom(1.2 if event.angleDelta().y() > 0 else 1 / 1.2)
|
||||
event.accept()
|
||||
|
||||
def set_model(self, controller: DocumentController) -> None:
|
||||
self.controller = controller
|
||||
scene = GraphScene(controller, self)
|
||||
scene.componentOptionsRequested.connect(self.componentOptionsRequested)
|
||||
scene.componentPortOptionsRequested.connect(self.componentPortOptionsRequested)
|
||||
scene.portOptionsRequested.connect(self.portOptionsRequested)
|
||||
scene.connectionOptionsRequested.connect(self.connectionOptionsRequested)
|
||||
scene.selectionChanged.connect(
|
||||
lambda: self.selectionAvailabilityChanged.emit(bool(scene.selectedItems()))
|
||||
)
|
||||
self.setScene(scene)
|
||||
|
||||
def select_all(self) -> None:
|
||||
scene = self.scene()
|
||||
if scene is None:
|
||||
return
|
||||
for item in scene.items():
|
||||
if item.flags() & QGraphicsItem.GraphicsItemFlag.ItemIsSelectable:
|
||||
item.setSelected(True)
|
||||
|
||||
def delete_selected(self) -> None:
|
||||
if self.controller is None or not isinstance(self.scene(), GraphScene):
|
||||
return
|
||||
blocks: set[str] = set()
|
||||
connections: set[str] = set()
|
||||
inputs: set[str] = set()
|
||||
outputs: set[str] = set()
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, ComponentGraphicsItem):
|
||||
blocks.add(item.component_id)
|
||||
elif isinstance(item, ConnectionGraphicsItem):
|
||||
connections.add(item.connection_id)
|
||||
elif isinstance(item, InterfaceTerminalItem):
|
||||
(inputs if item.direction == "input" else outputs).add(item.port.id)
|
||||
self.controller.delete_selection(blocks, connections, inputs, outputs)
|
||||
|
||||
def has_selected_components(self) -> bool:
|
||||
scene = self.scene()
|
||||
return bool(
|
||||
scene
|
||||
and any(isinstance(item, ComponentGraphicsItem) for item in scene.selectedItems())
|
||||
)
|
||||
|
||||
def rotate_selected(self) -> None:
|
||||
if self.controller is None or self.scene() is None:
|
||||
return
|
||||
component_ids = {
|
||||
item.component_id
|
||||
for item in self.scene().selectedItems()
|
||||
if isinstance(item, ComponentGraphicsItem)
|
||||
}
|
||||
self.controller.rotate_components(component_ids)
|
||||
|
||||
def copy_selection(self) -> bool:
|
||||
if self.controller is None or not isinstance(self.scene(), GraphScene):
|
||||
return False
|
||||
selected_ids = {
|
||||
item.component_id
|
||||
for item in self.scene().selectedItems()
|
||||
if isinstance(item, ComponentGraphicsItem)
|
||||
}
|
||||
if not selected_ids:
|
||||
return False
|
||||
graph = self.controller.active_graph
|
||||
components = [graph.blocks[component_id].to_dict() for component_id in selected_ids]
|
||||
connections = [
|
||||
connection.to_dict()
|
||||
for connection in graph.connections.values()
|
||||
if connection.source.block in selected_ids and connection.target.block in selected_ids
|
||||
]
|
||||
mime_data = QMimeData()
|
||||
mime_data.setData(
|
||||
SELECTION_MIME_TYPE,
|
||||
json.dumps({"components": components, "connections": connections}).encode("utf-8"),
|
||||
)
|
||||
QApplication.clipboard().setMimeData(mime_data)
|
||||
self.paste_count = 0
|
||||
return True
|
||||
|
||||
def cut_selection(self) -> None:
|
||||
if self.copy_selection():
|
||||
self.delete_selected()
|
||||
|
||||
def paste_selection(self) -> None:
|
||||
if self.controller is None:
|
||||
return
|
||||
mime_data = QApplication.clipboard().mimeData()
|
||||
if not mime_data.hasFormat(SELECTION_MIME_TYPE):
|
||||
return
|
||||
try:
|
||||
payload = json.loads(bytes(mime_data.data(SELECTION_MIME_TYPE)).decode("utf-8"))
|
||||
components = [Component.from_dict(item) for item in payload.get("components", [])]
|
||||
connections = [Connection.from_dict(item) for item in payload.get("connections", [])]
|
||||
except (KeyError, TypeError, ValueError, json.JSONDecodeError):
|
||||
return
|
||||
self.paste_count += 1
|
||||
new_ids = self.controller.paste_selection(
|
||||
components,
|
||||
connections,
|
||||
QPointF(32 * self.paste_count, 32 * self.paste_count),
|
||||
)
|
||||
scene = self.scene()
|
||||
if isinstance(scene, GraphScene):
|
||||
scene.clearSelection()
|
||||
for component_id in new_ids:
|
||||
item = scene.component_items.get(component_id)
|
||||
if item is not None:
|
||||
item.setSelected(True)
|
||||
|
||||
def set_tool_mode(self, mode: str) -> None:
|
||||
self.tool_mode = mode
|
||||
self.setDragMode(
|
||||
QGraphicsView.DragMode.RubberBandDrag
|
||||
if mode == "pointer"
|
||||
else QGraphicsView.DragMode.NoDrag
|
||||
)
|
||||
|
||||
def mousePressEvent(self, event: QMouseEvent) -> None: # noqa: N802
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def dragEnterEvent(self, event: QDragEnterEvent) -> None: # noqa: N802
|
||||
if (
|
||||
event.mimeData().hasFormat(COMPONENT_MIME_TYPE)
|
||||
and self.controller is not None
|
||||
and self.controller.active_component is not None
|
||||
and self.controller.active_component.implementation_kind == "graph"
|
||||
):
|
||||
event.acceptProposedAction()
|
||||
return
|
||||
super().dragEnterEvent(event)
|
||||
|
||||
def dragMoveEvent(self, event) -> None: # noqa: N802
|
||||
if (
|
||||
event.mimeData().hasFormat(COMPONENT_MIME_TYPE)
|
||||
and self.controller is not None
|
||||
and self.controller.active_component is not None
|
||||
and self.controller.active_component.implementation_kind == "graph"
|
||||
):
|
||||
event.acceptProposedAction()
|
||||
return
|
||||
super().dragMoveEvent(event)
|
||||
|
||||
def dropEvent(self, event: QDropEvent) -> None: # noqa: N802
|
||||
if self.controller is None or not event.mimeData().hasFormat(COMPONENT_MIME_TYPE):
|
||||
super().dropEvent(event)
|
||||
return
|
||||
data = json.loads(bytes(event.mimeData().data(COMPONENT_MIME_TYPE)).decode("utf-8"))
|
||||
source = Component.from_dict(data)
|
||||
self.controller.add_component_copy(
|
||||
source, _snapped(self.mapToScene(event.position().toPoint()))
|
||||
)
|
||||
event.acceptProposedAction()
|
||||
Reference in New Issue
Block a user