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>
118 lines
4.3 KiB
Python
118 lines
4.3 KiB
Python
"""Right-dock property panel: grouped, type-aware editors for the selection."""
|
|
from __future__ import annotations
|
|
|
|
from typing import Optional
|
|
|
|
from PyQt6.QtCore import pyqtSignal
|
|
from PyQt6.QtWidgets import (
|
|
QDoubleSpinBox, QFormLayout, QGroupBox, QLabel, QScrollArea, QVBoxLayout, QWidget,
|
|
)
|
|
|
|
from ..model.catalogue import CatalogueRegistry
|
|
from ..model.edge import EdgeModel
|
|
from ..model.node import NodeModel
|
|
from .property_editors import PropertyEditor
|
|
|
|
|
|
class PropertyPanel(QWidget):
|
|
"""Displays and edits properties of the currently selected node/edge.
|
|
|
|
Editing does not mutate the model directly; it emits signals that the
|
|
controller turns into undoable commands.
|
|
"""
|
|
|
|
property_edited = pyqtSignal(str, str, str, object) # kind, owner_id, key, value
|
|
geometry_edited = pyqtSignal(str, str, object) # node_id, field, value
|
|
|
|
def __init__(self, catalogues: CatalogueRegistry, parent=None):
|
|
super().__init__(parent)
|
|
self.catalogues = catalogues
|
|
self._kind: Optional[str] = None
|
|
self._owner_id: Optional[str] = None
|
|
|
|
outer = QVBoxLayout(self)
|
|
outer.setContentsMargins(0, 0, 0, 0)
|
|
self._scroll = QScrollArea()
|
|
self._scroll.setWidgetResizable(True)
|
|
outer.addWidget(self._scroll)
|
|
self._placeholder()
|
|
|
|
# -- public API -------------------------------------------------------
|
|
def set_target(self, model) -> None:
|
|
if isinstance(model, NodeModel):
|
|
self._build_for_node(model)
|
|
elif isinstance(model, EdgeModel):
|
|
self._build_for_edge(model)
|
|
else:
|
|
self._placeholder()
|
|
|
|
def refresh(self, model) -> None:
|
|
"""Rebuild if the currently shown owner changed under us."""
|
|
self.set_target(model)
|
|
|
|
# -- builders ---------------------------------------------------------
|
|
def _new_container(self, title: str) -> QVBoxLayout:
|
|
body = QWidget()
|
|
layout = QVBoxLayout(body)
|
|
layout.setContentsMargins(6, 6, 6, 6)
|
|
header = QLabel(f"<b>{title}</b>")
|
|
layout.addWidget(header)
|
|
self._scroll.setWidget(body)
|
|
return layout
|
|
|
|
def _placeholder(self) -> None:
|
|
self._kind = None
|
|
self._owner_id = None
|
|
body = QWidget()
|
|
layout = QVBoxLayout(body)
|
|
layout.addWidget(QLabel("<i>Select a node or edge to edit its properties.</i>"))
|
|
layout.addStretch(1)
|
|
self._scroll.setWidget(body)
|
|
|
|
def _build_for_node(self, node: NodeModel) -> None:
|
|
self._kind = "node"
|
|
self._owner_id = node.node_id
|
|
layout = self._new_container(f"Node: {node.title or node.template_key}")
|
|
layout.addWidget(self._geometry_group(node))
|
|
for group in node.properties:
|
|
layout.addWidget(self._property_group("node", node.node_id, group))
|
|
layout.addStretch(1)
|
|
|
|
def _build_for_edge(self, edge: EdgeModel) -> None:
|
|
self._kind = "edge"
|
|
self._owner_id = edge.edge_id
|
|
layout = self._new_container(f"Edge: {edge.title or edge.edge_id}")
|
|
for group in edge.properties:
|
|
layout.addWidget(self._property_group("edge", edge.edge_id, group))
|
|
layout.addStretch(1)
|
|
|
|
def _geometry_group(self, node: NodeModel) -> QGroupBox:
|
|
box = QGroupBox("Geometry")
|
|
form = QFormLayout(box)
|
|
for field, label, mn, mx in (
|
|
("x", "X", -1e6, 1e6),
|
|
("y", "Y", -1e6, 1e6),
|
|
("width", "Width", 10, 1e4),
|
|
("height", "Height", 10, 1e4),
|
|
("rotation", "Rotation", -360, 360),
|
|
):
|
|
spin = QDoubleSpinBox()
|
|
spin.setDecimals(1)
|
|
spin.setRange(mn, mx)
|
|
spin.setValue(float(getattr(node, field)))
|
|
spin.valueChanged.connect(
|
|
lambda val, f=field: self.geometry_edited.emit(node.node_id, f, val))
|
|
form.addRow(label, spin)
|
|
return box
|
|
|
|
def _property_group(self, kind: str, owner_id: str, group) -> QGroupBox:
|
|
box = QGroupBox(group.name)
|
|
form = QFormLayout(box)
|
|
for prop in group:
|
|
editor = PropertyEditor(prop, self.catalogues)
|
|
editor.value_changed.connect(
|
|
lambda val, k=prop.key: self.property_edited.emit(kind, owner_id, k, val))
|
|
label = f"{prop.label}"
|
|
form.addRow(label, editor)
|
|
return box
|