Add left node-library dock (grouped, searchable, drag-and-drop vector symbols), right property dock with type-aware grouped editors (incl. geometry, catalogue single/multi, color, value list), zoom/pan/fit canvas view accepting node drops, flow animator, configuration dialog, and the MainWindow wiring menus/toolbar, undo/redo, delete/duplicate, calculate flow, save/open (JSON), and PNG/SVG export. Includes a demo sample network, entry points, and panel + app integration tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
101 lines
3.5 KiB
Python
101 lines
3.5 KiB
Python
"""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)
|