"""NodeItem: renders a node's SVG symbol, title and ports; draggable/selectable.""" from __future__ import annotations from PyQt6.QtCore import QRectF, Qt, QPointF from PyQt6.QtGui import QColor, QFont, QPen from PyQt6.QtWidgets import QGraphicsObject, QGraphicsItem, QStyle from ..model.node import NodeModel from . import svg_cache from .port_item import PortItem LABEL_HEIGHT = 18.0 class NodeItem(QGraphicsObject): """Graphics item bound to a :class:`NodeModel`.""" def __init__(self, scene_ref, node: NodeModel) -> None: super().__init__() self._scene = scene_ref self.node = node self.setFlags( QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges ) self.setAcceptHoverEvents(True) self.setZValue(1) self.setPos(node.x, node.y) self._renderer = svg_cache.renderer(node.svg_name) self._port_items: dict[str, PortItem] = {} self._press_pos: QPointF | None = None self._build_ports() # -- helpers used by children ---------------------------------------- def scene_relations(self): return getattr(self._scene, "document", None) and self._scene.document.relations def port_item(self, port_id: str) -> PortItem | None: return self._port_items.get(port_id) def port_items(self): return self._port_items.values() def _build_ports(self) -> None: for p in self.node.ports: self._port_items[p.port_id] = PortItem(self, p) def refresh_ports(self) -> None: for pi in self._port_items.values(): pi.update_position() # -- geometry --------------------------------------------------------- def body_rect(self) -> QRectF: return QRectF(0, 0, self.node.width, self.node.height) def boundingRect(self) -> QRectF: m = 6 return QRectF(-m, -m, self.node.width + 2 * m, self.node.height + LABEL_HEIGHT + 2 * m) def set_model_size(self, w: float, h: float) -> None: self.prepareGeometryChange() self.node.width = w self.node.height = h self.refresh_ports() self.update() # -- interaction ------------------------------------------------------ def mousePressEvent(self, event): self._press_pos = self.pos() super().mousePressEvent(event) def mouseReleaseEvent(self, event): super().mouseReleaseEvent(event) if self._press_pos is not None and self.pos() != self._press_pos: old = (self._press_pos.x(), self._press_pos.y()) new = (self.pos().x(), self.pos().y()) self._scene.on_node_move_finished(self.node.node_id, old, new) self._press_pos = None def itemChange(self, change, value): if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange and self.scene(): cfg = getattr(self._scene, "config", None) if cfg is not None and cfg.snap_to_grid: value = QPointF(cfg.snap(value.x()), cfg.snap(value.y())) return value if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged: self.node.x = self.pos().x() self.node.y = self.pos().y() self._scene.reroute_edges_for_node(self.node.node_id) if change == QGraphicsItem.GraphicsItemChange.ItemSelectedHasChanged: self.update() return super().itemChange(change, value) # -- painting --------------------------------------------------------- def paint(self, painter, option, widget=None): painter.setRenderHint(painter.RenderHint.Antialiasing, True) body = self.body_rect() selected = bool(option.state & QStyle.StateFlag.State_Selected) if selected: pen = QPen(QColor("#1976d2"), 1.5, Qt.PenStyle.DashLine) painter.setPen(pen) painter.setBrush(QColor(25, 118, 210, 30)) painter.drawRoundedRect(body.adjusted(-3, -3, 3, 3), 4, 4) if self._renderer is not None: self._renderer.render(painter, body) else: painter.setPen(QPen(QColor("#555"), 2)) painter.setBrush(QColor("#eceff1")) painter.drawRoundedRect(body, 6, 6) # title label title = self.node.title if title: painter.setPen(QColor("#263238")) f = QFont() f.setPointSizeF(8.5) painter.setFont(f) rect = QRectF(-10, self.node.height + 2, self.node.width + 20, LABEL_HEIGHT) painter.drawText(rect, Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop, title)