"""Left-dock node library panel: grouped, draggable vector node symbols.""" from __future__ import annotations from PyQt6.QtCore import QByteArray, QMimeData, QRectF, Qt from PyQt6.QtGui import QDrag, QIcon, QPainter, QPixmap from PyQt6.QtWidgets import ( QLineEdit, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, ) from ..model.node_library import NodeLibrary from ..view import svg_cache NODE_MIME = "application/x-pipeline-node" def _icon_for(svg_name: str, size: int = 40) -> QIcon: renderer = svg_cache.renderer(svg_name) pm = QPixmap(size, size) pm.fill(Qt.GlobalColor.transparent) if renderer is not None: painter = QPainter(pm) painter.setRenderHint(QPainter.RenderHint.Antialiasing, True) renderer.render(painter, QRectF(2, 2, size - 4, size - 4)) painter.end() return QIcon(pm) class _LibraryTree(QTreeWidget): """Tree whose leaf items start a drag carrying the template key.""" def __init__(self, parent=None): super().__init__(parent) self.setHeaderHidden(True) self.setDragEnabled(True) self.setIconSize_default() def setIconSize_default(self): from PyQt6.QtCore import QSize self.setIconSize(QSize(36, 36)) def startDrag(self, supported_actions): item = self.currentItem() if item is None: return key = item.data(0, Qt.ItemDataRole.UserRole) if not key: return mime = QMimeData() mime.setData(NODE_MIME, QByteArray(key.encode("utf-8"))) drag = QDrag(self) drag.setMimeData(mime) icon = item.icon(0) if not icon.isNull(): drag.setPixmap(icon.pixmap(40, 40)) drag.exec(Qt.DropAction.CopyAction) class NodePanel(QWidget): """Search box + grouped tree of node templates.""" def __init__(self, library: NodeLibrary, parent=None): super().__init__(parent) self.library = library layout = QVBoxLayout(self) layout.setContentsMargins(4, 4, 4, 4) self._search = QLineEdit() self._search.setPlaceholderText("Filter nodes…") self._search.textChanged.connect(self._apply_filter) layout.addWidget(self._search) self._tree = _LibraryTree() layout.addWidget(self._tree) self._populate() def _populate(self) -> None: self._tree.clear() for group_name, templates in sorted(self.library.groups().items()): group_item = QTreeWidgetItem([group_name]) group_item.setFlags(group_item.flags() & ~Qt.ItemFlag.ItemIsSelectable) self._tree.addTopLevelItem(group_item) for tmpl in sorted(templates, key=lambda t: t.label): leaf = QTreeWidgetItem([tmpl.label]) leaf.setData(0, Qt.ItemDataRole.UserRole, tmpl.key) leaf.setIcon(0, _icon_for(tmpl.svg_name)) leaf.setToolTip(0, f"{tmpl.label} — drag onto the canvas") group_item.addChild(leaf) group_item.setExpanded(True) def _apply_filter(self, text: str) -> None: text = text.strip().lower() for i in range(self._tree.topLevelItemCount()): group = self._tree.topLevelItem(i) visible_children = 0 for j in range(group.childCount()): leaf = group.child(j) match = text in leaf.text(0).lower() leaf.setHidden(bool(text) and not match) visible_children += int(match or not text) group.setHidden(bool(text) and visible_children == 0)