This commit is contained in:
Joppe Blondel
2026-08-17 13:03:29 +02:00
parent 4591e6b7b0
commit a61c8e6003
6 changed files with 204 additions and 7 deletions
+88 -2
View File
@@ -6,10 +6,10 @@ 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.QtWidgets import QGraphicsEllipseItem, QGraphicsItem, QGraphicsPathItem, QGraphicsPixmapItem, QGraphicsScene, QGraphicsSceneMouseEvent, QGraphicsView, QMenu, QWidget
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, GraphConnection, Icon
from bedit_gui.models import Graph, GraphComponentLabel, GraphConnection, Icon
from bedit_gui.ui.generated.ui_graph_editor_widget import Ui_graphEditorWidget
from bedit_gui.utils.icon import get_pixmap_bounding_box, render_icon
@@ -20,6 +20,7 @@ MAX_ZOOM = 4.0
ZOOM_STEP = 1.15
ZOOM_TO_FIT_PADDING = 32.0
COMPONENT_ICON_SIZE = QSize(128, 128)
COMPONENT_LABEL_FONT_SIZE = 24.0
FALLBACK_COMPONENT_SPACING = 128
CONNECTION_WIDTH = 4.0
CONNECTION_BOUNDING_BOX_SPACING = 16.0
@@ -184,6 +185,58 @@ class GraphComponentItem(QGraphicsPixmapItem):
event.accept()
class GraphComponentLabelItem(QGraphicsTextItem):
def __init__(self, component_id: ComponentID, text: str, label: GraphComponentLabel, component_item: GraphComponentItem, editor: GraphEditorWidget) -> None:
super().__init__(text, component_item)
self.component_id = component_id
self.editor = editor
self._dragging = False
self._drag_start = label.relative_position
font = self.font()
font.setItalic(True)
font.setPointSizeF(COMPONENT_LABEL_FONT_SIZE)
self.setFont(font)
self.setDefaultTextColor(QColor("#202020"))
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, editor.mode is GraphEditorMode.NORMAL)
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
self.set_relative_position(label.relative_position)
def relative_position(self) -> tuple[int, int]:
component_bounds = self.editor._component_bounds[self.component_id]
return round(self.pos().x() + self.boundingRect().width() / 2), round(self.pos().y() - component_bounds.bottom())
def set_relative_position(self, position: tuple[int, int]) -> None:
component_bounds = self.editor._component_bounds[self.component_id]
self.setPos(position[0] - self.boundingRect().width() / 2, component_bounds.bottom() + position[1])
def itemChange(self, change: QGraphicsItem.GraphicsItemChange, value: object) -> object:
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange and self._dragging and isinstance(value, QPointF):
component_bounds = self.editor._component_bounds[self.component_id]
size = self.editor.snap_to_grid_size
relative_x = value.x() + self.boundingRect().width() / 2
relative_y = value.y() - component_bounds.bottom()
value = QPointF(round(relative_x / size) * size - self.boundingRect().width() / 2, component_bounds.bottom() + round(relative_y / size) * size)
return super().itemChange(change, value)
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
self._drag_start = self.relative_position()
self._dragging = True
super().mousePressEvent(event)
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:
super().mouseReleaseEvent(event)
self._dragging = False
position = self.relative_position()
self.set_relative_position(position)
if position != self._drag_start:
self.editor.finish_component_label_move(self.component_id, position)
def contextMenuEvent(self, event) -> None:
self.editor.component_context_menu_requested.emit(self.component_id, event.screenPos())
event.accept()
class GraphConnectionPointItem(QGraphicsEllipseItem):
def __init__(self, connection_id: ConnectionID, index: int, position: tuple[int, int], editor: GraphEditorWidget) -> None:
radius = CONNECTION_WIDTH
@@ -234,6 +287,7 @@ class GraphEditorWidget(QWidget):
component_move_requested = Signal(object, object, object)
component_context_menu_requested = Signal(object, object)
component_open_requested = Signal(object)
component_label_move_requested = Signal(object, object, object)
connection_points_change_requested = Signal(object, object, object, str)
connection_add_requested = Signal(object, object)
connections_delete_requested = Signal(object, object)
@@ -251,6 +305,7 @@ class GraphEditorWidget(QWidget):
self._connection_preview: QGraphicsPathItem | None = None
self._component_items: dict[ComponentID, GraphComponentItem] = {}
self._component_bounds: dict[ComponentID, QRectF] = {}
self._component_label_items: dict[ComponentID, GraphComponentLabelItem] = {}
self._connection_items: dict[ConnectionID, GraphConnectionItem] = {}
self._connection_point_items: dict[ConnectionID, list[GraphConnectionPointItem]] = {}
self.set_snap_to_grid_size(snap_to_grid_size)
@@ -310,6 +365,7 @@ class GraphEditorWidget(QWidget):
self._icons = icons or {}
self._component_items = {}
self._component_bounds = {}
self._component_label_items = {}
self._connection_items = {}
self._connection_point_items = {}
self.scene.clear()
@@ -331,6 +387,11 @@ class GraphEditorWidget(QWidget):
self._component_bounds[component_id] = bounds.translated(-pixmap.width() / 2, -pixmap.height() / 2)
self.scene.addItem(item)
for component_id, child in component.implementation.graph.components.items():
label = graph.component_labels.get(component_id, GraphComponentLabel())
if label.visible:
self._create_component_label_item(component_id, child.name, label)
for connection_id, connection in component.implementation.graph.connections.items():
if not isinstance(connection, (SignalConnection, BondConnection)):
continue
@@ -459,6 +520,31 @@ class GraphEditorWidget(QWidget):
if self._component is not None:
self.component_move_requested.emit(self._component, component_id, position)
def finish_component_label_move(self, component_id: ComponentID, relative_position: tuple[int, int]) -> None:
if self._component is not None:
self.component_label_move_requested.emit(self._component, component_id, relative_position)
def component_label_visible(self, component_id: ComponentID) -> bool:
return self._graph.component_labels.get(component_id, GraphComponentLabel()).visible
def set_component_label(self, component_id: ComponentID, label: GraphComponentLabel | None) -> None:
if label is None:
self._graph.component_labels.pop(component_id, None)
label = GraphComponentLabel()
else:
self._graph.component_labels[component_id] = label
item = self._component_label_items.pop(component_id, None)
if item is not None:
item.setParentItem(None)
self.scene.removeItem(item)
component = self._component
if component is not None and label.visible and component_id in component.implementation.graph.components:
self._create_component_label_item(component_id, component.implementation.graph.components[component_id].name, label)
def _create_component_label_item(self, component_id: ComponentID, text: str, label: GraphComponentLabel) -> None:
item = GraphComponentLabelItem(component_id, text, label, self._component_items[component_id], self)
self._component_label_items[component_id] = item
def set_component_position(self, component_id: ComponentID, position: tuple[int, int] | None) -> None:
item = self._component_items.get(component_id)
if item is None: