Ilya ae3f752b6f feat(ui): panels, canvas view, animation, and main window
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>
2026-07-02 23:25:14 +02:00

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)