Add the QGraphicsView layer: NodeItem (SVG symbol + title + draggable, snap-to-grid), PortItem (relation-colored anchors with compatibility hints), EdgeItem (orthogonal routing, arc hops on crossings, line styles, head/tail decorations, animated flow dashes), and DiagramScene binding the document to items with edge-drawing interaction and a grid background. Add EditorController with a QUndoStack (add/connect/delete/move/edit) and scene interaction tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
75 lines
2.5 KiB
Python
75 lines
2.5 KiB
Python
"""PortItem: a clickable, relation-colored connection anchor on a node."""
|
|
from __future__ import annotations
|
|
|
|
from PyQt6.QtCore import QPointF, QRectF, Qt
|
|
from PyQt6.QtGui import QBrush, QColor, QPen
|
|
from PyQt6.QtWidgets import QGraphicsItem
|
|
|
|
from ..model.port import Port
|
|
|
|
PORT_RADIUS = 6.0
|
|
|
|
|
|
class PortItem(QGraphicsItem):
|
|
"""Rendered anchor for a single :class:`Port`, child of its NodeItem."""
|
|
|
|
def __init__(self, node_item, port: Port) -> None:
|
|
super().__init__(node_item)
|
|
self.node_item = node_item
|
|
self.port = port
|
|
self.setAcceptHoverEvents(True)
|
|
self.setZValue(2)
|
|
self._hover = False
|
|
# Visual affordance flags set by the scene during an edge drag.
|
|
self.compatible_hint = False
|
|
self.setToolTip(f"{port.name} ({port.relation})")
|
|
self.update_position()
|
|
|
|
# -- geometry ---------------------------------------------------------
|
|
def update_position(self) -> None:
|
|
n = self.node_item.node
|
|
lx, ly = self.port.local_point(n.width, n.height)
|
|
self.setPos(lx, ly)
|
|
|
|
def boundingRect(self) -> QRectF:
|
|
r = PORT_RADIUS + 3
|
|
return QRectF(-r, -r, 2 * r, 2 * r)
|
|
|
|
def scene_center(self) -> QPointF:
|
|
return self.mapToScene(QPointF(0, 0))
|
|
|
|
def relation_color(self) -> QColor:
|
|
reg = self.node_item.scene_relations()
|
|
return QColor(reg.color(self.port.relation)) if reg else QColor("#888")
|
|
|
|
# -- interaction ------------------------------------------------------
|
|
def hoverEnterEvent(self, event):
|
|
self._hover = True
|
|
self.update()
|
|
super().hoverEnterEvent(event)
|
|
|
|
def hoverLeaveEvent(self, event):
|
|
self._hover = False
|
|
self.update()
|
|
super().hoverLeaveEvent(event)
|
|
|
|
def set_compatible_hint(self, value: bool) -> None:
|
|
if self.compatible_hint != value:
|
|
self.compatible_hint = value
|
|
self.update()
|
|
|
|
# -- painting ---------------------------------------------------------
|
|
def paint(self, painter, option, widget=None):
|
|
color = self.relation_color()
|
|
r = PORT_RADIUS
|
|
if self._hover or self.compatible_hint:
|
|
r = PORT_RADIUS + 2
|
|
painter.setRenderHint(painter.RenderHint.Antialiasing, True)
|
|
pen = QPen(color.darker(140), 1.5)
|
|
if self.compatible_hint:
|
|
pen = QPen(QColor("#000000"), 2)
|
|
painter.setPen(pen)
|
|
fill = color.lighter(120) if self._hover else color
|
|
painter.setBrush(QBrush(fill))
|
|
painter.drawEllipse(QPointF(0, 0), r, r)
|