Added new connection styles

This commit is contained in:
2026-07-20 12:45:47 +02:00
parent 8fa450d734
commit ee859c4311
9 changed files with 348 additions and 182 deletions

View File

@@ -0,0 +1,25 @@
from dataclasses import dataclass
from PySide6.QtCore import Qt
@dataclass(frozen=True)
class ConnectionStyle:
color: str = "#285f9e"
selected_color: str = "#f59e0b"
width: float = 2.5
selected_width: float = 4.0
line_style: Qt.PenStyle = Qt.PenStyle.SolidLine
arrow_at_source: bool = False
arrow_at_target: bool = True
arrow_size: float = 10.0
# This is the intentional code-level styling point for every port/connection type.
CONNECTION_STYLES: dict[str, ConnectionStyle] = {
"signal": ConnectionStyle(),
}
def connection_style(port_type: str) -> ConnectionStyle:
return CONNECTION_STYLES.get(port_type, ConnectionStyle())

View File

@@ -10,6 +10,7 @@ from PySide6.QtGui import (
QPainter,
QPainterPath,
QPen,
QPolygonF,
QTransform,
)
from PySide6.QtWidgets import (
@@ -32,6 +33,7 @@ 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.graphics.connection_styles import ConnectionStyle, connection_style
from bedit.gui.preferences import application_settings
@@ -235,10 +237,20 @@ class InterfaceTerminalItem(QGraphicsObject):
class ConnectionGraphicsItem(QGraphicsPathItem):
def __init__(self, connection_id: str, name: str = "") -> None:
def __init__(
self,
connection_id: str,
name: str = "",
style: ConnectionStyle | None = None,
) -> None:
super().__init__()
self.connection_id = connection_id
self.name = name
self.style = style or ConnectionStyle()
self.start = QPointF()
self.end = QPointF()
self.start_direction = QPointF(1, 0)
self.end_direction = QPointF(1, 0)
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
self._update_pen()
self.setZValue(-1)
@@ -262,11 +274,40 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
def _update_pen(self) -> None:
self.setPen(
QPen(
QColor("#f59e0b") if self.isSelected() else QColor("#285f9e"),
4.0 if self.isSelected() else 2.5,
QColor(self.style.selected_color if self.isSelected() else self.style.color),
self.style.selected_width if self.isSelected() else self.style.width,
self.style.line_style,
)
)
def set_connection_path(
self,
path: QPainterPath,
points: list[QPointF],
) -> None:
self.setPath(path)
self.start, self.end = points[0], points[-1]
if len(points) > 1:
self.start_direction = points[1] - points[0]
self.end_direction = points[-1] - points[-2]
@staticmethod
def _arrow(end: QPointF, direction: QPointF, size: float) -> QPolygonF:
length = max(0.001, (direction.x() ** 2 + direction.y() ** 2) ** 0.5)
unit = QPointF(direction.x() / length, direction.y() / length)
normal = QPointF(-unit.y(), unit.x())
base = end - unit * size
return QPolygonF([end, base + normal * size * 0.45, base - normal * size * 0.45])
def paint(self, painter: QPainter, option, widget=None) -> None:
super().paint(painter, option, widget)
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(self.pen().color())
if self.style.arrow_at_target:
painter.drawPolygon(self._arrow(self.end, self.end_direction, self.style.arrow_size))
if self.style.arrow_at_source:
painter.drawPolygon(self._arrow(self.start, -self.start_direction, self.style.arrow_size))
class GraphScene(QGraphicsScene):
componentOptionsRequested = Signal(str)
@@ -282,6 +323,10 @@ class GraphScene(QGraphicsScene):
self.output_items: dict[str, InterfaceTerminalItem] = {}
self.connection_items: dict[str, ConnectionGraphicsItem] = {}
self.pending_source: ConnectionPortItem | None = None
self.pending_waypoints: list[QPointF] = []
self.pending_preview: QGraphicsPathItem | None = None
self.interaction_mode = "pointer"
self.connection_routing = "spline"
self.setSceneRect(-2000, -2000, 4000, 4000)
controller.documentReset.connect(self.rebuild)
@@ -297,6 +342,8 @@ class GraphScene(QGraphicsScene):
self.output_items.clear()
self.connection_items.clear()
self.pending_source = None
self.pending_waypoints.clear()
self.pending_preview = None
owner = self.controller.active_component
if owner is None or owner.implementation_kind != "graph":
return
@@ -316,7 +363,11 @@ class GraphScene(QGraphicsScene):
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)
item = ConnectionGraphicsItem(
connection.id,
connection.name,
connection_style(self.controller.connection_port_type(connection)),
)
self.addItem(item)
self.connection_items[connection.id] = item
self.update_connection(connection.id)
@@ -367,34 +418,115 @@ class GraphScene(QGraphicsScene):
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)
routing = str(connection.properties.get("routing", "spline"))
waypoints = [
QPointF(float(point["x"]), float(point["y"]))
for point in connection.properties.get("waypoints", [])
if isinstance(point, dict) and "x" in point and "y" in point
]
path, direction_points = self._route_path(start, end, routing, waypoints)
graphics.set_connection_path(path, direction_points)
@staticmethod
def _route_path(
start: QPointF,
end: QPointF,
routing: str,
waypoints: list[QPointF] | None = None,
) -> tuple[QPainterPath, list[QPointF]]:
waypoints = waypoints or []
path = QPainterPath(start)
path.cubicTo(start + QPointF(distance, 0), end - QPointF(distance, 0), end)
graphics.setPath(path)
if routing == "angled":
points = [start, *waypoints, end]
for point in points[1:]:
path.lineTo(point)
return path, points
if routing == "direct":
path.lineTo(end)
return path, [start, end]
distance = max(50.0, abs(end.x() - start.x()) * 0.5)
first_control = start + QPointF(distance, 0)
second_control = end - QPointF(distance, 0)
path.cubicTo(first_control, second_control, end)
return path, [start, first_control, second_control, end]
def set_interaction_mode(self, mode: str) -> None:
self.interaction_mode = mode
if mode != "connect":
self._clear_pending_source()
def set_connection_routing(self, routing: str) -> None:
self.connection_routing = routing
self._clear_pending_source()
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
if self.interaction_mode != "connect":
self._clear_pending_source()
super().mousePressEvent(event)
return
if event.button() == Qt.MouseButton.RightButton:
self._clear_pending_source()
event.accept()
return
item = self.itemAt(event.scenePos(), QTransform())
if isinstance(item, ConnectionPortItem):
if item.role == "source":
self._clear_pending_source()
self.pending_source = item
self.pending_waypoints = []
item.setBrush(QColor("#f5b642"))
self._create_preview()
elif self.pending_source is not None:
if self.pending_source.endpoint != item.endpoint:
try:
self.controller.connect(self.pending_source.endpoint, item.endpoint)
self.controller.connect(
self.pending_source.endpoint,
item.endpoint,
routing=self.connection_routing,
waypoints=self.pending_waypoints,
)
except ValueError as error:
QToolTip.showText(event.screenPos(), str(error))
self._clear_pending_source()
event.accept()
return
self._clear_pending_source()
super().mousePressEvent(event)
if self.pending_source is not None and self.connection_routing == "angled":
self.pending_waypoints.append(_snapped(event.scenePos()))
self._update_preview(event.scenePos())
event.accept()
def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
if self.interaction_mode == "connect" and self.pending_source is not None:
self._update_preview(event.scenePos())
event.accept()
return
super().mouseMoveEvent(event)
def _create_preview(self) -> None:
self.pending_preview = QGraphicsPathItem()
self.pending_preview.setPen(QPen(QColor("#64748b"), 1.5, Qt.PenStyle.DashLine))
self.pending_preview.setZValue(-0.5)
self.addItem(self.pending_preview)
def _update_preview(self, cursor: QPointF) -> None:
if self.pending_source is None or self.pending_preview is None:
return
path, _points = self._route_path(
self.pending_source.scenePos(),
cursor,
self.connection_routing,
self.pending_waypoints,
)
self.pending_preview.setPath(path)
def _clear_pending_source(self) -> None:
if self.pending_source is not None:
self.pending_source.setBrush(QColor("#ffffff"))
if self.pending_preview is not None:
self.removeItem(self.pending_preview)
self.pending_source = None
self.pending_waypoints.clear()
self.pending_preview = None
class GraphWorkspaceView(QGraphicsView):
@@ -411,6 +543,7 @@ class GraphWorkspaceView(QGraphicsView):
self.tool_mode = "pointer"
self.paste_count = 0
self.setAcceptDrops(True)
self.setMouseTracking(True)
self.setRenderHint(QPainter.RenderHint.Antialiasing)
self.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
self.setBackgroundBrush(QColor("#f7f7f7"))
@@ -505,6 +638,29 @@ class GraphWorkspaceView(QGraphicsView):
and any(isinstance(item, ComponentGraphicsItem) for item in scene.selectedItems())
)
def has_single_selected_component(self) -> bool:
scene = self.scene()
return bool(
scene
and sum(
isinstance(item, ComponentGraphicsItem) for item in scene.selectedItems()
)
== 1
)
def open_selected_component(self) -> bool:
if self.controller is None or self.scene() is None:
return False
selected = [
item
for item in self.scene().selectedItems()
if isinstance(item, ComponentGraphicsItem)
]
if len(selected) != 1:
return False
self.controller.activate_component(selected[0].component_id)
return True
def rotate_selected(self) -> None:
if self.controller is None or self.scene() is None:
return
@@ -578,6 +734,14 @@ class GraphWorkspaceView(QGraphicsView):
if mode == "pointer"
else QGraphicsView.DragMode.NoDrag
)
scene = self.scene()
if isinstance(scene, GraphScene):
scene.set_interaction_mode(mode)
def set_connection_routing(self, routing: str) -> None:
scene = self.scene()
if isinstance(scene, GraphScene):
scene.set_connection_routing(routing)
def mousePressEvent(self, event: QMouseEvent) -> None: # noqa: N802
super().mousePressEvent(event)