Source code for ezmsg.qt.settings_panel
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from qtpy import QtCore
from qtpy import QtWidgets
from .chain import ProcessorGraph
from .chain import ProcessorSettingsBinding
from .publisher import EzPublisher
from .session import EzSession
from .settings_form import SettingsForm
_DIRTY_SECTION_STYLE = (
"QGroupBox {"
" border: 1px solid #c77d00;"
" border-radius: 6px;"
" margin-top: 8px;"
" padding-top: 8px;"
" background: rgba(199, 125, 0, 0.08);"
"}"
"QGroupBox::title {"
" subcontrol-origin: margin;"
" left: 10px;"
" padding: 0 4px;"
" color: #8a5a00;"
"}"
)
_DIRTY_BUTTON_STYLE = (
"QPushButton { background: #fff2cc; border: 1px solid #c77d00; }"
"QPushButton:disabled { color: #888888; background: #f0f0f0; "
"border: 1px solid #cccccc; }"
)
@dataclass
class _SettingsSection:
binding: ProcessorSettingsBinding
group: QtWidgets.QGroupBox
form: SettingsForm
apply_button: QtWidgets.QPushButton
publisher: EzPublisher
last_applied: Any | None
base_title: str
[docs]
class ProcessorSettingsPanel(QtWidgets.QWidget):
"""Settings forms for runtime-configurable processors in a graph."""
[docs]
def __init__(
self,
graph: ProcessorGraph,
session: EzSession,
parent: QtWidgets.QWidget | None = None,
):
super().__init__(parent)
self._graph = graph
self._session = session
self._sections: list[_SettingsSection] = []
self.setSizePolicy(
QtWidgets.QSizePolicy.Policy.Maximum,
QtWidgets.QSizePolicy.Policy.Preferred,
)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(10)
layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop)
bindings = graph.settings_bindings()
if not bindings:
empty = QtWidgets.QLabel("No runtime settings available.")
empty.setWordWrap(True)
layout.addWidget(empty)
layout.addStretch()
return
for binding in bindings:
section = self._create_section(binding)
self._sections.append(section)
layout.addWidget(section.group)
layout.addStretch()
[docs]
@classmethod
def from_graph(
cls,
graph: ProcessorGraph,
*,
session: EzSession | None = None,
parent: QtWidgets.QWidget | None = None,
) -> ProcessorSettingsPanel:
resolved_session = graph.session if session is None else session
if resolved_session is None:
raise RuntimeError(
"ProcessorGraph must be attached to a session "
"before building settings UI"
)
if graph.session is not None and graph.session is not resolved_session:
raise RuntimeError(
"ProcessorSettingsPanel session does not match graph session"
)
if graph.session is None:
raise RuntimeError(
"ProcessorGraph must be attached to a session "
"before building settings UI"
)
return cls(graph, resolved_session, parent=parent)
@property
def sections(self) -> tuple[QtWidgets.QGroupBox, ...]:
return tuple(section.group for section in self._sections)
def _create_section(self, binding: ProcessorSettingsBinding) -> _SettingsSection:
title = binding.title
group = QtWidgets.QGroupBox(title, self)
group.setSizePolicy(
QtWidgets.QSizePolicy.Policy.Maximum,
QtWidgets.QSizePolicy.Policy.Maximum,
)
layout = QtWidgets.QVBoxLayout(group)
layout.setContentsMargins(12, 12, 12, 12)
layout.setSpacing(8)
form = SettingsForm(
binding.settings_type, initial=binding.initial_settings, parent=group
)
layout.addWidget(form)
controls = QtWidgets.QHBoxLayout()
controls.addStretch()
apply_button = QtWidgets.QPushButton("Apply", group)
apply_button.setEnabled(False)
controls.addWidget(apply_button)
layout.addLayout(controls)
publisher = EzPublisher(binding.topic, parent=group, session=self._session)
section = _SettingsSection(
binding=binding,
group=group,
form=form,
apply_button=apply_button,
publisher=publisher,
last_applied=form.get_settings(),
base_title=title,
)
form.settings_changed.connect(
lambda _section=section: self._update_dirty_state(_section)
)
apply_button.clicked.connect(
lambda _checked=False, _section=section: self._apply_section(_section)
)
self._update_dirty_state(section)
return section
def _is_dirty(self, section: _SettingsSection) -> bool:
return section.form.get_settings() != section.last_applied
def _update_dirty_state(self, section: _SettingsSection) -> None:
dirty = self._is_dirty(section)
section.apply_button.setEnabled(dirty)
section.group.setTitle(
f"{section.base_title} *" if dirty else section.base_title
)
section.group.setStyleSheet(_DIRTY_SECTION_STYLE if dirty else "")
section.apply_button.setStyleSheet(_DIRTY_BUTTON_STYLE if dirty else "")
def _apply_section(self, section: _SettingsSection) -> None:
settings = section.form.get_settings()
section.publisher.emit(settings)
section.last_applied = settings
self._update_dirty_state(section)