More fixes and extended lib
This commit is contained in:
@@ -10,7 +10,7 @@ from PySide6.QtGui import QActionGroup, QBrush, QColor, QCursor, QDragEnterEvent
|
||||
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.models import Graph, GraphComponentLabel, GraphConnection, Icon, PortMetadata
|
||||
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_natural_icon_size, get_pixmap_bounding_box, render_icon
|
||||
@@ -34,6 +34,9 @@ ARROW_HALF_WIDTH = 16.0
|
||||
SIGNAL_ARROW_LENGTH = ARROW_LENGTH / 2
|
||||
SIGNAL_ARROW_HALF_WIDTH = ARROW_HALF_WIDTH / 2
|
||||
CAUSALITY_TICK_HALF_LENGTH = 16.0
|
||||
CONNECTION_ANNOTATION_FONT_SIZE = 18.0
|
||||
CONNECTION_ANNOTATION_BACK_OFFSET = 24.0
|
||||
CONNECTION_ANNOTATION_SIDE_OFFSET = 14.0
|
||||
|
||||
|
||||
class GraphEditorMode(Enum):
|
||||
@@ -152,7 +155,7 @@ class GraphComponentItem(QGraphicsPixmapItem):
|
||||
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
|
||||
|
||||
def itemChange(self, change: QGraphicsItem.GraphicsItemChange, value: object) -> object:
|
||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange and self._dragging and isinstance(value, QPointF):
|
||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange and self.component_id in self.editor._component_drag_starts and isinstance(value, QPointF):
|
||||
grid_size = self.editor.snap_to_grid_size
|
||||
value = QPointF(round(value.x() / grid_size) * grid_size, round(value.y() / grid_size) * grid_size)
|
||||
result = super().itemChange(change, value)
|
||||
@@ -167,6 +170,7 @@ class GraphComponentItem(QGraphicsPixmapItem):
|
||||
self._drag_start = QPointF(self.pos())
|
||||
self._dragging = True
|
||||
super().mousePressEvent(event)
|
||||
self.editor.begin_component_move()
|
||||
|
||||
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
||||
if self.editor.mode is GraphEditorMode.CONNECTION:
|
||||
@@ -176,10 +180,7 @@ class GraphComponentItem(QGraphicsPixmapItem):
|
||||
return
|
||||
super().mouseReleaseEvent(event)
|
||||
self._dragging = False
|
||||
position = (round(self.pos().x()), round(self.pos().y()))
|
||||
self.setPos(*position)
|
||||
if self.pos() != self._drag_start:
|
||||
self.editor.finish_component_move(self.component_id, position)
|
||||
self.editor.finish_component_moves()
|
||||
|
||||
def mouseDoubleClickEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
@@ -295,7 +296,7 @@ class GraphConnectionPointItem(QGraphicsEllipseItem):
|
||||
|
||||
|
||||
class GraphEditorWidget(QWidget):
|
||||
component_move_requested = Signal(object, object, object)
|
||||
component_moves_requested = Signal(object, object)
|
||||
component_context_menu_requested = Signal(object, object)
|
||||
component_open_requested = Signal(object)
|
||||
component_label_move_requested = Signal(object, object, object)
|
||||
@@ -312,14 +313,17 @@ class GraphEditorWidget(QWidget):
|
||||
self._component: Component | None = None
|
||||
self._graph = Graph()
|
||||
self._icons: dict[ComponentID, Icon] = {}
|
||||
self._port_metadata: dict[PortID, PortMetadata] = {}
|
||||
self._mode = GraphEditorMode.NORMAL
|
||||
self._connection_start: ComponentID | None = None
|
||||
self._connection_preview: QGraphicsPathItem | None = None
|
||||
self._component_items: dict[ComponentID, GraphComponentItem] = {}
|
||||
self._component_drag_starts: dict[ComponentID, tuple[int, int]] = {}
|
||||
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._connection_annotation_items: dict[ConnectionID, QGraphicsTextItem] = {}
|
||||
self.set_snap_to_grid_size(snap_to_grid_size)
|
||||
self.scene = GraphGraphicsScene(self)
|
||||
self.scene.setSceneRect(-SCENE_SIZE / 2, -SCENE_SIZE / 2, SCENE_SIZE, SCENE_SIZE)
|
||||
@@ -364,12 +368,12 @@ class GraphEditorWidget(QWidget):
|
||||
self._clear_connection_start()
|
||||
component = self._component
|
||||
if component is not None:
|
||||
self.set_component(component, self._graph, self._icons)
|
||||
self.set_component(component, self._graph, self._icons, self._port_metadata)
|
||||
|
||||
def toggle_mode(self) -> None:
|
||||
self.set_mode(GraphEditorMode.CONNECTION if self._mode is GraphEditorMode.NORMAL else GraphEditorMode.NORMAL)
|
||||
|
||||
def set_component(self, component: Component | None, graph: Graph | None = None, icons: dict[ComponentID, Icon] | None = None) -> None:
|
||||
def set_component(self, component: Component | None, graph: Graph | None = None, icons: dict[ComponentID, Icon] | None = None, port_metadata: dict[PortID, PortMetadata] | None = None) -> None:
|
||||
if component is not None and not isinstance(component.implementation, GraphImplementation):
|
||||
raise TypeError("GraphEditorWidget only supports components with a graph implementation")
|
||||
component_changed = component is not self._component
|
||||
@@ -377,11 +381,14 @@ class GraphEditorWidget(QWidget):
|
||||
self._component = component
|
||||
self._graph = graph or Graph()
|
||||
self._icons = icons or {}
|
||||
self._port_metadata = port_metadata or {}
|
||||
self._component_drag_starts = {}
|
||||
self._component_items = {}
|
||||
self._component_bounds = {}
|
||||
self._component_label_items = {}
|
||||
self._connection_items = {}
|
||||
self._connection_point_items = {}
|
||||
self._connection_annotation_items = {}
|
||||
self.scene.clear()
|
||||
if component is None:
|
||||
return
|
||||
@@ -420,6 +427,17 @@ class GraphEditorWidget(QWidget):
|
||||
connection_item.setData(0, str(connection_id))
|
||||
self._connection_items[connection_id] = connection_item
|
||||
self.scene.addItem(connection_item)
|
||||
annotation = self._port_metadata.get(connection.target, PortMetadata()).connection_annotation if isinstance(connection, SignalConnection) else None
|
||||
if annotation:
|
||||
annotation_item = QGraphicsTextItem(annotation)
|
||||
font = annotation_item.font()
|
||||
font.setBold(True)
|
||||
font.setPointSizeF(CONNECTION_ANNOTATION_FONT_SIZE)
|
||||
annotation_item.setFont(font)
|
||||
annotation_item.setDefaultTextColor(QColor(SIGNAL_CONNECTION_COLOR))
|
||||
annotation_item.setZValue(1)
|
||||
self._connection_annotation_items[connection_id] = annotation_item
|
||||
self.scene.addItem(annotation_item)
|
||||
visual_connection = graph.connections.get(connection_id)
|
||||
self._create_connection_point_items(connection_id, visual_connection.points[1:-1] if visual_connection is not None and len(visual_connection.points) >= 2 else [])
|
||||
self.refresh_connections()
|
||||
@@ -452,6 +470,23 @@ class GraphEditorWidget(QWidget):
|
||||
if isinstance(connection, BondConnection):
|
||||
tick_at_source = False if connection.causality is BondCausality.EFFORT_OUT else True if connection.causality is BondCausality.FLOW_OUT else None
|
||||
item.setPath(item._connection_path(points, isinstance(connection, BondConnection), tick_at_source))
|
||||
annotation_item = self._connection_annotation_items.get(connection_id)
|
||||
if annotation_item is not None:
|
||||
self._position_connection_annotation(annotation_item, points)
|
||||
|
||||
@staticmethod
|
||||
def _position_connection_annotation(item: QGraphicsTextItem, points: list[tuple[float, float]]) -> None:
|
||||
target = QPointF(*points[-1])
|
||||
previous = next((QPointF(*point) for point in reversed(points[:-1]) if QPointF(*point) != target), None)
|
||||
if previous is None:
|
||||
return
|
||||
dx = target.x() - previous.x()
|
||||
dy = target.y() - previous.y()
|
||||
length = hypot(dx, dy)
|
||||
x = target.x() - CONNECTION_ANNOTATION_BACK_OFFSET * dx / length - CONNECTION_ANNOTATION_SIDE_OFFSET * dy / length
|
||||
y = target.y() - CONNECTION_ANNOTATION_BACK_OFFSET * dy / length + CONNECTION_ANNOTATION_SIDE_OFFSET * dx / length
|
||||
bounds = item.boundingRect()
|
||||
item.setPos(x - bounds.width() / 2, y - bounds.height() / 2)
|
||||
|
||||
def add_connection_point(self, connection_id: ConnectionID, scene_position: QPointF) -> None:
|
||||
points = self._connection_metadata_points(connection_id)
|
||||
@@ -477,6 +512,9 @@ class GraphEditorWidget(QWidget):
|
||||
connection_item = self._connection_items.pop(connection_id, None)
|
||||
if connection_item is not None:
|
||||
self.scene.removeItem(connection_item)
|
||||
annotation_item = self._connection_annotation_items.pop(connection_id, None)
|
||||
if annotation_item is not None:
|
||||
self.scene.removeItem(annotation_item)
|
||||
return
|
||||
self._graph.connections[connection_id] = GraphConnection(points=list(points))
|
||||
interior_points = points[1:-1]
|
||||
@@ -532,9 +570,23 @@ class GraphEditorWidget(QWidget):
|
||||
best_distance = distance
|
||||
return best_index
|
||||
|
||||
def finish_component_move(self, component_id: ComponentID, position: tuple[int, int]) -> None:
|
||||
if self._component is not None:
|
||||
self.component_move_requested.emit(self._component, component_id, position)
|
||||
def begin_component_move(self) -> None:
|
||||
self._component_drag_starts = {item.component_id: (round(item.pos().x()), round(item.pos().y())) for item in self.scene.selectedItems() if isinstance(item, GraphComponentItem)}
|
||||
|
||||
def finish_component_moves(self) -> None:
|
||||
starts = self._component_drag_starts
|
||||
self._component_drag_starts = {}
|
||||
if self._component is None:
|
||||
return
|
||||
positions = {}
|
||||
for component_id, old_position in starts.items():
|
||||
item = self._component_items[component_id]
|
||||
position = (round(item.pos().x()), round(item.pos().y()))
|
||||
item.setPos(*position)
|
||||
if position != old_position:
|
||||
positions[component_id] = position
|
||||
if positions:
|
||||
self.component_moves_requested.emit(self._component, positions)
|
||||
|
||||
def finish_component_label_move(self, component_id: ComponentID, relative_position: tuple[int, int]) -> None:
|
||||
if self._component is not None:
|
||||
|
||||
Reference in New Issue
Block a user