"""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"{title}") 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("Select a node or edge to edit its properties.")) 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