"""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