"""Per-type property editor widgets used by the property panel.""" from __future__ import annotations from PyQt6.QtCore import pyqtSignal from PyQt6.QtGui import QColor from PyQt6.QtWidgets import ( QCheckBox, QColorDialog, QComboBox, QDoubleSpinBox, QHBoxLayout, QLineEdit, QListWidget, QListWidgetItem, QPushButton, QSpinBox, QWidget, ) from PyQt6.QtCore import Qt from ..model.catalogue import CatalogueRegistry from ..model.properties import Property, PropertyType class PropertyEditor(QWidget): """A single labelled control bound to a :class:`Property`. Emits :attr:`value_changed` with the new (coerced) value on user edits. """ value_changed = pyqtSignal(object) def __init__(self, prop: Property, catalogues: CatalogueRegistry, parent=None): super().__init__(parent) self.prop = prop self.catalogues = catalogues self._control: QWidget layout = QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) self._build() layout.addWidget(self._control) self.setEnabled(prop.editable) # -- construction ----------------------------------------------------- def _build(self) -> None: t = self.prop.ptype v = self.prop.value if t == PropertyType.STRING: w = QLineEdit(str(v or "")) w.editingFinished.connect(lambda: self.value_changed.emit(w.text())) self._control = w elif t == PropertyType.INT: w = QSpinBox() w.setRange(int(self.prop.minimum) if self.prop.minimum is not None else -10**6, int(self.prop.maximum) if self.prop.maximum is not None else 10**6) w.setValue(int(v or 0)) if self.prop.unit: w.setSuffix(f" {self.prop.unit}") w.valueChanged.connect(lambda val: self.value_changed.emit(val)) self._control = w elif t == PropertyType.FLOAT: w = QDoubleSpinBox() w.setDecimals(3) w.setRange(self.prop.minimum if self.prop.minimum is not None else -1e9, self.prop.maximum if self.prop.maximum is not None else 1e9) w.setValue(float(v or 0.0)) if self.prop.unit: w.setSuffix(f" {self.prop.unit}") w.valueChanged.connect(lambda val: self.value_changed.emit(val)) self._control = w elif t == PropertyType.BOOL: w = QCheckBox() w.setChecked(bool(v)) w.toggled.connect(lambda val: self.value_changed.emit(val)) self._control = w elif t == PropertyType.ENUM: w = QComboBox() w.addItems([str(o) for o in self.prop.options]) if v is not None and str(v) in [str(o) for o in self.prop.options]: w.setCurrentText(str(v)) w.currentTextChanged.connect(lambda val: self.value_changed.emit(val)) self._control = w elif t == PropertyType.COLOR: self._control = self._build_color(v) elif t == PropertyType.CATALOGUE_ITEM: self._control = self._build_catalogue_single(v) elif t == PropertyType.CATALOGUE_ITEMS: self._control = self._build_catalogue_multi(v) elif t == PropertyType.VALUE_LIST: w = QLineEdit(", ".join(str(x) for x in (v or []))) w.setPlaceholderText("comma-separated values") w.editingFinished.connect( lambda: self.value_changed.emit(self._parse_list(w.text()))) self._control = w else: # pragma: no cover - defensive self._control = QLineEdit(str(v)) def _build_color(self, value) -> QWidget: btn = QPushButton(str(value or "#ffffff")) self._color = str(value or "#ffffff") def paint(): c = QColor(self._color) fg = "#000000" if c.lightnessF() > 0.5 else "#ffffff" btn.setStyleSheet(f"background:{self._color}; color:{fg};") btn.setText(self._color) def pick(): c = QColorDialog.getColor(QColor(self._color)) if c.isValid(): self._color = c.name() paint() self.value_changed.emit(self._color) btn.clicked.connect(pick) paint() return btn def _build_catalogue_single(self, value) -> QWidget: w = QComboBox() cat = self.catalogues.get(self.prop.catalogue) if self.prop.catalogue else None self._ids: list[str] = [] if cat is not None: for item in cat: w.addItem(item.name, item.item_id) self._ids.append(item.item_id) if value in self._ids: w.setCurrentIndex(self._ids.index(value)) w.currentIndexChanged.connect( lambda idx: self.value_changed.emit(w.itemData(idx))) return w def _build_catalogue_multi(self, value) -> QWidget: w = QListWidget() w.setMaximumHeight(90) selected = set(value or []) cat = self.catalogues.get(self.prop.catalogue) if self.prop.catalogue else None if cat is not None: for item in cat: it = QListWidgetItem(item.name) it.setData(Qt.ItemDataRole.UserRole, item.item_id) it.setFlags(it.flags() | Qt.ItemFlag.ItemIsUserCheckable) it.setCheckState(Qt.CheckState.Checked if item.item_id in selected else Qt.CheckState.Unchecked) w.addItem(it) def changed(_item): ids = [w.item(i).data(Qt.ItemDataRole.UserRole) for i in range(w.count()) if w.item(i).checkState() == Qt.CheckState.Checked] self.value_changed.emit(ids) w.itemChanged.connect(changed) return w @staticmethod def _parse_list(text: str) -> list: out = [] for token in text.split(","): token = token.strip() if not token: continue try: out.append(int(token)) except ValueError: try: out.append(float(token)) except ValueError: out.append(token) return out