SyntheticMDProductions's picture
ADAM October 2026 source release: PixelRow, INRFlow, Wan Video, Oasis player and field guide
f8c73f9 verified
Raw History Blame Contribute Delete
11.5 kB
from __future__ import annotations
from pathlib import Path
from typing import Any
from PySide6.QtWidgets import (
QCheckBox,
QComboBox,
QDoubleSpinBox,
QFileDialog,
QFrame,
QGridLayout,
QGroupBox,
QHBoxLayout,
QLabel,
QLineEdit,
QPlainTextEdit,
QPushButton,
QScrollArea,
QSlider,
QSpinBox,
QVBoxLayout,
QWidget,
)
from PySide6.QtCore import Qt, Signal
class SettingsForm(QWidget):
"""Builds editable PySide controls from a model plugin setting schema."""
changed = Signal()
def __init__(
self,
schema: dict[str, dict[str, Any]] | None = None,
parent: QWidget | None = None,
) -> None:
super().__init__(parent)
self.schema: dict[str, dict[str, Any]] = {}
self.widgets: dict[str, QWidget] = {}
self.rows: dict[str, tuple[QLabel, QWidget]] = {}
self.show_advanced = QCheckBox("Show advanced settings")
self.show_advanced.toggled.connect(self._advanced_changed)
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 4, 0, 0)
layout.setSpacing(10)
layout.addWidget(self.show_advanced)
self.scroll = QScrollArea()
self.scroll.setWidgetResizable(True)
self.scroll.setFrameShape(QFrame.NoFrame)
self.scroll.setMinimumHeight(180)
self.scroll.setMaximumHeight(390)
self.content = QWidget()
self.content_layout = QVBoxLayout(self.content)
self.content_layout.setContentsMargins(4, 4, 4, 4)
self.content_layout.setSpacing(10)
self.scroll.setWidget(self.content)
layout.addWidget(self.scroll)
if schema:
self.set_schema(schema)
def set_schema(self, schema: dict[str, dict[str, Any]]) -> None:
self.schema = {str(key): dict(value) for key, value in schema.items()}
self.widgets.clear()
self.rows.clear()
while self.content_layout.count():
item = self.content_layout.takeAt(0)
if item.widget():
item.widget().deleteLater()
grouped: dict[str, list[tuple[str, dict[str, Any]]]] = {}
for key, spec in self.schema.items():
grouped.setdefault(str(spec.get("group", "Basic")), []).append((key, spec))
has_advanced = any(bool(spec.get("advanced")) for spec in self.schema.values())
self.show_advanced.setVisible(has_advanced)
for group, items in grouped.items():
box = QGroupBox()
grid = QGridLayout(box)
grid.setContentsMargins(12, 12, 12, 12)
grid.setHorizontalSpacing(12)
grid.setVerticalSpacing(8)
grid.setColumnStretch(0, 0)
grid.setColumnStretch(1, 1)
group_title = QLabel(group)
group_title.setProperty("sectionTitle", True)
grid.addWidget(group_title, 0, 0, 1, 2)
for row, (key, spec) in enumerate(items):
required = " *" if spec.get("required") else ""
label = QLabel(str(spec.get("label", key)) + required)
label.setMinimumWidth(128)
label.setMaximumWidth(210)
label.setWordWrap(True)
widget = self._build_widget(key, spec)
tooltip = self._tooltip_for(key, spec)
if tooltip:
label.setToolTip(tooltip)
widget.setToolTip(tooltip)
if str(spec.get("type", "text")) not in {"path", "folder", "multiline_text"}:
widget.setMaximumWidth(420)
grid.addWidget(label, row + 1, 0, Qt.AlignLeft | Qt.AlignVCenter)
grid.addWidget(widget, row + 1, 1, Qt.AlignLeft | Qt.AlignVCenter)
self.widgets[key] = widget
self.rows[key] = (label, widget)
self.content_layout.addWidget(box)
self.content_layout.addStretch()
self._advanced_changed(self.show_advanced.isChecked())
def _build_widget(self, key: str, spec: dict[str, Any]) -> QWidget:
kind = str(spec.get("type", "text"))
default = spec.get("default", "")
if kind == "bool":
widget = QCheckBox()
widget.setChecked(bool(default))
widget.setMaximumWidth(420)
widget.toggled.connect(self.changed)
return widget
if kind == "choice":
widget = QComboBox()
option_labels = spec.get("option_labels", {})
for option in spec.get("options", []):
label = option_labels.get(str(option), str(option)) if isinstance(option_labels, dict) else str(option)
widget.addItem(str(label), option)
index = widget.findData(default)
if index < 0:
index = widget.findText(str(default))
widget.setCurrentIndex(max(0, index))
widget.currentIndexChanged.connect(self.changed)
widget.setMinimumWidth(170)
return widget
if kind in {"int", "slider"}:
if kind == "slider":
container = QWidget()
row = QHBoxLayout(container)
row.setContentsMargins(0, 0, 0, 0)
slider = QSlider(Qt.Horizontal)
slider.setRange(int(spec.get("min", 0)), int(spec.get("max", 100)))
value = QSpinBox()
value.setRange(slider.minimum(), slider.maximum())
slider.setValue(int(default or slider.minimum()))
value.setValue(slider.value())
slider.valueChanged.connect(value.setValue)
value.valueChanged.connect(slider.setValue)
slider.valueChanged.connect(self.changed)
row.addWidget(slider, 1)
row.addWidget(value)
container.setProperty("value_widget", value)
container.setMaximumWidth(420)
return container
widget = QSpinBox()
widget.setRange(int(spec.get("min", -2_147_000_000)), int(spec.get("max", 2_147_000_000)))
widget.setValue(int(default or 0))
widget.valueChanged.connect(self.changed)
widget.setMinimumWidth(120)
return widget
if kind == "float":
widget = QDoubleSpinBox()
widget.setDecimals(int(spec.get("decimals", 7)))
widget.setRange(float(spec.get("min", -1_000_000.0)), float(spec.get("max", 1_000_000.0)))
widget.setSingleStep(float(spec.get("step", 0.0001)))
widget.setValue(float(default or 0.0))
widget.valueChanged.connect(self.changed)
widget.setMinimumWidth(140)
return widget
if kind == "multiline_text":
widget = QPlainTextEdit()
widget.setMaximumHeight(int(spec.get("height", 76)))
widget.setPlainText(str(default or ""))
widget.textChanged.connect(self.changed)
widget.setMinimumWidth(260)
return widget
if kind in {"path", "folder"}:
container = QWidget()
row = QHBoxLayout(container)
row.setContentsMargins(0, 0, 0, 0)
edit = QLineEdit(str(default or ""))
button = QPushButton("Browse")
button.setFixedWidth(82)
button.clicked.connect(lambda _checked=False, e=edit, k=kind: self._browse(e, k))
edit.editingFinished.connect(self.changed)
row.addWidget(edit, 1)
row.addWidget(button)
container.setProperty("value_widget", edit)
container.setMinimumWidth(260)
return container
widget = QLineEdit(str(default or ""))
widget.editingFinished.connect(self.changed)
widget.setMinimumWidth(180)
return widget
@staticmethod
def _tooltip_for(key: str, spec: dict[str, Any]) -> str:
parts = []
description = str(spec.get("description", "")).strip()
if description:
parts.append(description)
if spec.get("required"):
parts.append("Required.")
if "min" in spec or "max" in spec:
low = spec.get("min", "—")
high = spec.get("max", "—")
parts.append(f"Range: {low} to {high}.")
if str(spec.get("type", "")) == "choice" and spec.get("options"):
parts.append("Choices: " + ", ".join(str(option) for option in spec.get("options", [])) + ".")
if spec.get("advanced"):
parts.append("Advanced setting.")
if not parts:
parts.append(key.replace("_", " ").title())
return "\n".join(parts)
def _browse(self, edit: QLineEdit, kind: str) -> None:
current = Path(edit.text()).expanduser() if edit.text().strip() else Path.home()
if kind == "folder":
selected = QFileDialog.getExistingDirectory(self, "Select folder", str(current))
else:
selected, _filter = QFileDialog.getOpenFileName(self, "Select file", str(current))
if selected:
edit.setText(selected)
self.changed.emit()
def _advanced_changed(self, enabled: bool) -> None:
for key, spec in self.schema.items():
label, widget = self.rows[key]
visible = enabled or not bool(spec.get("advanced"))
label.setVisible(visible)
widget.setVisible(visible)
def values(self) -> dict[str, Any]:
return {key: self.value(key) for key in self.schema}
def value(self, key: str) -> Any:
widget = self.widgets[key]
spec = self.schema[key]
kind = str(spec.get("type", "text"))
value_widget = widget.property("value_widget")
if value_widget:
widget = value_widget
if kind == "bool":
return bool(widget.isChecked())
if kind == "choice":
data = widget.currentData()
return data if data is not None else widget.currentText()
if kind in {"int", "slider"}:
return int(widget.value())
if kind == "float":
return float(widget.value())
if kind == "multiline_text":
return widget.toPlainText()
return widget.text()
def set_values(self, values: dict[str, Any]) -> None:
for key, value in values.items():
if key not in self.widgets:
continue
self._set_value(key, value)
def _set_value(self, key: str, value: Any) -> None:
widget = self.widgets[key]
spec = self.schema[key]
kind = str(spec.get("type", "text"))
value_widget = widget.property("value_widget")
if value_widget:
widget = value_widget
try:
if kind == "bool":
widget.setChecked(bool(value))
elif kind == "choice":
index = widget.findData(value)
if index < 0:
index = widget.findText(str(value))
if index >= 0:
widget.setCurrentIndex(index)
elif kind in {"int", "slider"}:
widget.setValue(int(value))
elif kind == "float":
widget.setValue(float(value))
elif kind == "multiline_text":
widget.setPlainText(str(value))
else:
widget.setText(str(value))
except (TypeError, ValueError):
return