Skip to content

View

Base classes

Bases: PView, ABC

Base view class.

Parameters:

Name Type Description Default
name str

Identity key of the view. Passed as positional-only argument.

required
kwargs Any

Additional keyword arguments for view subclasses.

{}
Source code in src/redsun/view/_base.py
class View(PView, ABC):
    """Base view class.

    Parameters
    ----------
    name : str
        Identity key of the view. Passed as positional-only argument.
    kwargs : Any, optional
        Additional keyword arguments for view subclasses.
    """

    @abstractmethod
    def __init__(
        self,
        name: str,
        /,
        **kwargs: Any,
    ) -> None:
        self.name = name
        super().__init__(**kwargs)

    @property
    @abstractmethod
    def view_position(self) -> ViewPosition:
        """Position of the view component in the main view of the UI."""

view_position abstractmethod property

view_position: ViewPosition

Position of the view component in the main view of the UI.

Bases: Protocol

Minimal protocol a view component should implement.

Attributes:

Name Type Description
name str

Identity key of the view.

Notes

Access to the virtual container is optional and should be acquired by implementing :class:~redsun.virtual.IsInjectable.

Source code in src/redsun/view/_base.py
@runtime_checkable
class PView(Protocol):
    """Minimal protocol a view component should implement.

    Attributes
    ----------
    name : str
        Identity key of the view.

    Notes
    -----
    Access to the virtual container is optional and should be acquired
    by implementing :class:`~redsun.virtual.IsInjectable`.
    """

    name: str

    @property
    @abstractmethod
    def view_position(self) -> ViewPosition:
        """Position of the view component in the main view of the UI."""

view_position abstractmethod property

view_position: ViewPosition

Position of the view component in the main view of the UI.

Bases: str, Enum

Supported view positions.

Used to define the position of a view component in the main view of the UI.

Warning

These values are based on how Qt manages dock widgets. They may change in the future.

Attributes:

Name Type Description
CENTER str

Center view position.

LEFT str

Left view position.

RIGHT str

Right view position.

TOP str

Top view position.

BOTTOM str

Bottom view position.

Source code in src/redsun/view/__init__.py
@unique
class ViewPosition(str, Enum):
    """Supported view positions.

    Used to define the position of a view component in the main view of the UI.

    !!! warning
        These values are based on how Qt manages dock widgets.
        They may change in the future.

    Attributes
    ----------
    CENTER : str
        Center view position.
    LEFT : str
        Left view position.
    RIGHT : str
        Right view position.
    TOP : str
        Top view position.
    BOTTOM : str
        Bottom view position.
    """

    CENTER = "center"
    LEFT = "left"
    RIGHT = "right"
    TOP = "top"
    BOTTOM = "bottom"

Qt widgets

Qt widget utilities for plan-based UIs.

This module provides the building blocks for a plan parameter form:

  • ActionButton — a QPushButton that carries Action metadata and auto-updates its label when toggled.
  • PlanWidget — a frozen dataclass owning all Qt widgets for a single plan (parameter form, run/pause buttons, action buttons).
  • create_plan_widget — factory that builds a complete PlanWidget from a PlanSpec and wires up the caller-supplied callbacks.
  • PlanInfoDialog — a simple Markdown-rendering dialog for displaying the plan docstring (if available).
  • create_param_widget — re-exported from _widget_factory for convenience.

ActionButton

Bases: QPushButton

A QPushButton that carries Action metadata.

Automatically updates its label based on toggle state using the action's toggle_states attribute.

Parameters:

Name Type Description Default
action Action

The action to associate with this button.

required
parent QWidget | None

The parent widget. Default is None.

None

Attributes:

Name Type Description
action Action

The action associated with this button.

Source code in src/redsun/view/qt/utils.py
class ActionButton(QtW.QPushButton):
    """A ``QPushButton`` that carries ``Action`` metadata.

    Automatically updates its label based on toggle state using the action's
    ``toggle_states`` attribute.

    Parameters
    ----------
    action : Action
        The action to associate with this button.
    parent : QtWidgets.QWidget | None, optional
        The parent widget. Default is ``None``.

    Attributes
    ----------
    action : Action
        The action associated with this button.
    """

    def __init__(self, action: Action, parent: QtW.QWidget | None = None) -> None:
        self.name_capital = action.name.capitalize()
        super().__init__(self.name_capital, parent)
        self.action = action

        if action.description:
            self.setToolTip(action.description)

        if action.togglable:
            self.setCheckable(True)
            self.toggled.connect(self._update_text)
            self._update_text(False)

    def _update_text(self, checked: bool) -> None:
        """Update button text based on toggle state."""
        state_text = (
            self.action.toggle_states[1] if checked else self.action.toggle_states[0]
        )
        self.setText(f"{self.name_capital} ({state_text})")

PlanInfoDialog

Bases: QDialog

Dialog to provide information to the user.

Parameters:

Name Type Description Default
title str

The title of the dialog window.

required
text str

The text to display in the text edit area (rendered as Markdown).

required
parent QWidget | None

The parent widget, by default None.

None
Source code in src/redsun/view/qt/utils.py
class PlanInfoDialog(QtW.QDialog):
    """Dialog to provide information to the user.

    Parameters
    ----------
    title : str
        The title of the dialog window.
    text : str
        The text to display in the text edit area (rendered as Markdown).
    parent : QtWidgets.QWidget | None, optional
        The parent widget, by default ``None``.
    """

    def __init__(
        self,
        title: str,
        text: str,
        parent: QtW.QWidget | None = None,
    ) -> None:
        super().__init__(parent)

        self.setWindowTitle(title)
        self.resize(500, 300)

        layout = QtW.QVBoxLayout(self)

        self.text_edit = QtW.QTextEdit()
        self.text_edit.setReadOnly(True)
        self.text_edit.setMarkdown(text)
        layout.addWidget(self.text_edit)

        self.ok_button = QtW.QPushButton("OK")
        self.ok_button.setDefault(True)
        self.ok_button.clicked.connect(self.accept)

        button_layout = QtW.QHBoxLayout()
        button_layout.addStretch()
        button_layout.addWidget(self.ok_button)
        layout.addLayout(button_layout)

        self.setLayout(layout)

    @classmethod
    def show_dialog(
        cls, title: str, text: str, parent: QtW.QWidget | None = None
    ) -> int:
        """Create and show the dialog in one step.

        Parameters
        ----------
        title : str
            The title of the dialog window.
        text : str
            The text to display in the text edit area.
        parent : QtWidgets.QWidget | None, optional
            The parent widget, by default ``None``.

        Returns
        -------
        int
            Dialog result code (``QDialog.Accepted`` or ``QDialog.Rejected``).
        """
        dialog = cls(title, text, parent)
        return dialog.exec()

show_dialog classmethod

show_dialog(
    title: str, text: str, parent: QWidget | None = None
) -> int

Create and show the dialog in one step.

Parameters:

Name Type Description Default
title str

The title of the dialog window.

required
text str

The text to display in the text edit area.

required
parent QWidget | None

The parent widget, by default None.

None

Returns:

Type Description
int

Dialog result code (QDialog.Accepted or QDialog.Rejected).

Source code in src/redsun/view/qt/utils.py
@classmethod
def show_dialog(
    cls, title: str, text: str, parent: QtW.QWidget | None = None
) -> int:
    """Create and show the dialog in one step.

    Parameters
    ----------
    title : str
        The title of the dialog window.
    text : str
        The text to display in the text edit area.
    parent : QtWidgets.QWidget | None, optional
        The parent widget, by default ``None``.

    Returns
    -------
    int
        Dialog result code (``QDialog.Accepted`` or ``QDialog.Rejected``).
    """
    dialog = cls(title, text, parent)
    return dialog.exec()

create_plan_widget

create_plan_widget(
    spec: PlanSpec,
    run_callback: Callable[[], None] | None = None,
    toggle_callback: Callable[[bool], None] | None = None,
    pause_callback: Callable[[bool], None] | None = None,
    action_clicked_callback: Callable[[str], None]
    | None = None,
    action_toggled_callback: Callable[[bool, str], None]
    | None = None,
) -> PlanWidget

Build a complete PlanWidget for spec.

Parameters:

Name Type Description Default
spec PlanSpec

The plan specification to build a widget for.

required
run_callback Callable[[], None] | None

Connected to run_button.clicked for non-togglable plans.

None
toggle_callback Callable[[bool], None] | None

Connected to run_button.toggled for togglable plans.

None
pause_callback Callable[[bool], None] | None

Connected to pause_button.toggled for pausable plans.

None
action_clicked_callback Callable[[str], None] | None

Called with action_name when a non-togglable action fires.

None
action_toggled_callback Callable[[bool, str], None] | None

Called with (checked, action_name) when a togglable action fires.

None

Returns:

Type Description
PlanWidget

Fully constructed widget, ready to be added to a QStackedWidget.

Source code in src/redsun/view/qt/utils.py
def create_plan_widget(
    spec: PlanSpec,
    run_callback: Callable[[], None] | None = None,
    toggle_callback: Callable[[bool], None] | None = None,
    pause_callback: Callable[[bool], None] | None = None,
    action_clicked_callback: Callable[[str], None] | None = None,
    action_toggled_callback: Callable[[bool, str], None] | None = None,
) -> PlanWidget:
    """Build a complete ``PlanWidget`` for *spec*.

    Parameters
    ----------
    spec : PlanSpec
        The plan specification to build a widget for.
    run_callback : Callable[[], None] | None, optional
        Connected to ``run_button.clicked`` for non-togglable plans.
    toggle_callback : Callable[[bool], None] | None, optional
        Connected to ``run_button.toggled`` for togglable plans.
    pause_callback : Callable[[bool], None] | None, optional
        Connected to ``pause_button.toggled`` for pausable plans.
    action_clicked_callback : Callable[[str], None] | None, optional
        Called with ``action_name`` when a non-togglable action fires.
    action_toggled_callback : Callable[[bool, str], None] | None, optional
        Called with ``(checked, action_name)`` when a togglable action fires.

    Returns
    -------
    PlanWidget
        Fully constructed widget, ready to be added to a ``QStackedWidget``.
    """
    page = QtW.QWidget()
    page_layout = QtW.QVBoxLayout(page)
    page_layout.setContentsMargins(4, 4, 4, 4)
    page_layout.setSpacing(4)

    # Build device + parameter widgets and assemble the two group boxes
    device_widgets, param_widgets = _build_param_widgets(spec)

    # Combine into one flat list for the container (preserves .parameters access)
    all_widgets = device_widgets + param_widgets
    container = mgw.Container(widgets=all_widgets)

    devices_group = _build_devices_group(device_widgets)
    params_group = _build_params_group(param_widgets)

    params_widget = QtW.QWidget()
    params_layout = QtW.QVBoxLayout(params_widget)
    params_layout.setContentsMargins(0, 0, 0, 0)
    params_layout.setSpacing(4)
    if devices_group is not None:
        params_layout.addWidget(devices_group)
    if params_group is not None:
        params_layout.addWidget(params_group)
    page_layout.addWidget(params_widget)

    run_button, pause_button = _build_run_buttons(
        spec,
        page,
        page_layout,
        run_callback or (lambda: None),
        toggle_callback or (lambda checked: None),
        pause_callback or (lambda paused: None),
    )

    actions_group, action_buttons = _build_actions_group(
        spec,
        page_layout,
        action_clicked_callback or (lambda name: None),
        action_toggled_callback or (lambda checked, name: None),
    )

    return PlanWidget(
        spec=spec,
        group_box=page,
        run_button=run_button,
        pause_button=pause_button,
        container=container,
        device_widgets=device_widgets,
        params_widget=params_widget,
        actions_group=actions_group,
        action_buttons=action_buttons,
    )

Container for all Qt widgets that represent a single plan.

Source code in src/redsun/view/qt/utils.py
@dataclass(frozen=True)
class PlanWidget:
    """Container for all Qt widgets that represent a single plan."""

    spec: PlanSpec
    """The plan specification that this widget represents."""

    group_box: QtW.QWidget
    """The top-level page widget suitable for stacking in a QStackedWidget."""

    run_button: QtW.QPushButton
    """The button to run (or stop) the plan."""

    container: mgw.Container[mgw_bases.ValueWidget[Any]]
    """The magicgui Container holding the parameter input widgets."""

    device_widgets: list[mgw_bases.ValueWidget[Any]]
    """Device parameter widgets (``DeviceSequenceEdit`` or ``ComboBox``).

    Exposed so callers can connect validation logic directly to each
    device widget's ``changed`` signal.
    """

    params_widget: QtW.QWidget
    """Widget wrapping devices_group + params_group; disable this to lock
    all parameter inputs without affecting the run/stop/pause buttons.
    """

    action_buttons: dict[str, ActionButton]
    """Mapping of action names to their buttons for direct access."""

    actions_group: QtW.QGroupBox | None = None
    """The group box containing action buttons, or None if the plan has no actions."""

    pause_button: QtW.QPushButton | None = None
    """The pause/resume button, or None if the plan is not pausable."""

    def toggle(self, status: bool) -> None:
        """Update UI state when a togglable plan starts or stops.

        Parameters
        ----------
        status : bool
            `True` when the plan is starting; `False` when stopping.
        """
        self.run_button.setText("Stop" if status else "Run")
        if self.pause_button:
            self.pause_button.setEnabled(status)
        if self.actions_group:
            self.actions_group.setEnabled(status)
        self.params_widget.setEnabled(not status)

    def pause(self, status: bool) -> None:
        """Update UI state when a plan is paused or resumed.

        Parameters
        ----------
        status : bool
            `True` when pausing; `False` when resuming.
        """
        if self.pause_button:
            self.pause_button.setText("Resume" if status else "Pause")
            self.run_button.setEnabled(not status)

    def setEnabled(self, enabled: bool) -> None:  # noqa: N802
        """Enable or disable the entire plan widget.

        Parameters
        ----------
        enabled : bool
            ``True`` to enable; ``False`` to disable.
        """
        self.group_box.setEnabled(enabled)
        self.run_button.setEnabled(enabled)
        self.params_widget.setEnabled(enabled)

    def enable_actions(self, enabled: bool = True) -> None:
        """Enable or disable the actions group box.

        Parameters
        ----------
        enabled : bool, optional
            ``True`` to enable; ``False`` to disable. Default is ``True``.
        """
        if self.actions_group:
            self.actions_group.setEnabled(enabled)

    def get_action_button(self, action_name: str) -> ActionButton | None:
        """Return the `ActionButton` for `action_name`, or `None` if absent.

        Parameters
        ----------
        action_name : str
            The name of the action.
        """
        return self.action_buttons.get(action_name)

    def has_actions(self) -> bool:
        """Return `True` if this plan has at least one action button."""
        return bool(self.action_buttons)

    @property
    def parameters(self) -> dict[str, Any]:
        """Current parameter values keyed by parameter name.

        The presenter is responsible for routing these into positional args
        and keyword args via ``collect_arguments`` / ``resolve_arguments``.
        """
        return {w.name: w.value for w in self.container}

spec instance-attribute

spec: PlanSpec

The plan specification that this widget represents.

group_box instance-attribute

group_box: QWidget

The top-level page widget suitable for stacking in a QStackedWidget.

run_button instance-attribute

run_button: QPushButton

The button to run (or stop) the plan.

container instance-attribute

container: Container[ValueWidget[Any]]

The magicgui Container holding the parameter input widgets.

device_widgets instance-attribute

device_widgets: list[ValueWidget[Any]]

Device parameter widgets (DeviceSequenceEdit or ComboBox).

Exposed so callers can connect validation logic directly to each device widget's changed signal.

params_widget instance-attribute

params_widget: QWidget

Widget wrapping devices_group + params_group; disable this to lock all parameter inputs without affecting the run/stop/pause buttons.

action_buttons instance-attribute

action_buttons: dict[str, ActionButton]

Mapping of action names to their buttons for direct access.

actions_group class-attribute instance-attribute

actions_group: QGroupBox | None = None

The group box containing action buttons, or None if the plan has no actions.

pause_button class-attribute instance-attribute

pause_button: QPushButton | None = None

The pause/resume button, or None if the plan is not pausable.

parameters property

parameters: dict[str, Any]

Current parameter values keyed by parameter name.

The presenter is responsible for routing these into positional args and keyword args via collect_arguments / resolve_arguments.

toggle

toggle(status: bool) -> None

Update UI state when a togglable plan starts or stops.

Source code in src/redsun/view/qt/utils.py
def toggle(self, status: bool) -> None:
    """Update UI state when a togglable plan starts or stops.

    Parameters
    ----------
    status : bool
        `True` when the plan is starting; `False` when stopping.
    """
    self.run_button.setText("Stop" if status else "Run")
    if self.pause_button:
        self.pause_button.setEnabled(status)
    if self.actions_group:
        self.actions_group.setEnabled(status)
    self.params_widget.setEnabled(not status)

pause

pause(status: bool) -> None

Update UI state when a plan is paused or resumed.

Source code in src/redsun/view/qt/utils.py
def pause(self, status: bool) -> None:
    """Update UI state when a plan is paused or resumed.

    Parameters
    ----------
    status : bool
        `True` when pausing; `False` when resuming.
    """
    if self.pause_button:
        self.pause_button.setText("Resume" if status else "Pause")
        self.run_button.setEnabled(not status)

setEnabled

setEnabled(enabled: bool) -> None

Enable or disable the entire plan widget.

Source code in src/redsun/view/qt/utils.py
def setEnabled(self, enabled: bool) -> None:  # noqa: N802
    """Enable or disable the entire plan widget.

    Parameters
    ----------
    enabled : bool
        ``True`` to enable; ``False`` to disable.
    """
    self.group_box.setEnabled(enabled)
    self.run_button.setEnabled(enabled)
    self.params_widget.setEnabled(enabled)

enable_actions

enable_actions(enabled: bool = True) -> None

Enable or disable the actions group box.

Source code in src/redsun/view/qt/utils.py
def enable_actions(self, enabled: bool = True) -> None:
    """Enable or disable the actions group box.

    Parameters
    ----------
    enabled : bool, optional
        ``True`` to enable; ``False`` to disable. Default is ``True``.
    """
    if self.actions_group:
        self.actions_group.setEnabled(enabled)

get_action_button

get_action_button(action_name: str) -> ActionButton | None

Return the ActionButton for action_name, or None if absent.

Source code in src/redsun/view/qt/utils.py
def get_action_button(self, action_name: str) -> ActionButton | None:
    """Return the `ActionButton` for `action_name`, or `None` if absent.

    Parameters
    ----------
    action_name : str
        The name of the action.
    """
    return self.action_buttons.get(action_name)

has_actions

has_actions() -> bool

Return True if this plan has at least one action button.

Source code in src/redsun/view/qt/utils.py
def has_actions(self) -> bool:
    """Return `True` if this plan has at least one action button."""
    return bool(self.action_buttons)

Widget factory for plan parameter forms.

Maps a ParamDescription to an appropriate magicgui widget via a table-driven factory registry (_WIDGET_FACTORY_MAP).

create_param_widget is the public entry point. It walks _WIDGET_FACTORY_MAP — an ordered list of (predicate, factory) pairs — and calls the first factory whose predicate matches the given ParamDescription.

Extending the system

To add support for a new annotation shape, define a predicate and a factory function and insert a (predicate, factory) tuple at the right priority position in _WIDGET_FACTORY_MAP. Nothing else needs to change.

Unresolvable annotations

create_plan_spec pre-validates that every required parameter can be mapped to a widget. Plans with unresolvable required parameters raise UnresolvableAnnotationError and are skipped by the presenter. create_param_widget therefore raises RuntimeError (not a silent fallback) if all factory entries fail.

create_param_widget

create_param_widget(param: ParamDescription) -> mgw.Widget

Create a magicgui widget for param via the factory registry.

Parameters:

Name Type Description Default
param ParamDescription

The parameter specification.

required

Returns:

Type Description
Widget

The created widget.

Raises:

Type Description
RuntimeError

If every entry in _WIDGET_FACTORY_MAP fails.

Source code in src/redsun/view/qt/_widget_factory.py
def create_param_widget(param: ParamDescription) -> mgw.Widget:
    """Create a magicgui widget for *param* via the factory registry.

    Parameters
    ----------
    param : ParamDescription
        The parameter specification.

    Returns
    -------
    mgw.Widget
        The created widget.

    Raises
    ------
    RuntimeError
        If every entry in ``_WIDGET_FACTORY_MAP`` fails.
    """
    for predicate, factory in _WIDGET_FACTORY_MAP:
        widget = _try_factory_entry(predicate, factory, param)
        if widget is not None:
            return widget
    raise RuntimeError(
        f"No widget factory matched parameter {param.name!r} "
        f"(annotation: {param.annotation!r}). "
        f"This is a bug — create_plan_spec should have caught this."
    )

Descriptor-driven tree view for displaying and editing device settings.

DescriptorTreeView is a self-contained QTreeWidget-based widget that renders Bluesky-compatible describe_configuration / read_configuration dicts as a two-column property tree.

The design is inspired by the ParameterTree widget from the pyqtgraph library (MIT licence, © 2012 University of North Carolina at Chapel Hill, Luke Campagnola).

Layout (two columns: Setting | Value):

▾ source          <- GROUP row, spans both columns, bold
    property  [widget]

The source field of a Bluesky Descriptor is used as the group label. When it carries the :readonly suffix (e.g. "settings:readonly") the value cell is rendered as a greyed QLabel and cannot be edited.

The device-name root level is omitted — present one DescriptorTreeView per device, e.g. inside a QTabWidget.

Supported dtype → widget mappings
dtype Widget
"integer" QSpinBox
"number" QDoubleSpinBox
"string" QLineEdit, or QComboBox when choices are present
"boolean" QComboBox (True / False)
"array" read-only QLabel

DescriptorTreeView

Bases: QTreeWidget

Two-column property tree for browsing and editing device settings.

Modelled after the pyqtgraph ParameterTree: uses setItemWidget to place editor/display widgets permanently in the Value column, so there are no popup editors and no delegate geometry issues.

The device-name root level is omitted — use one widget per device, e.g. inside a QTabWidget.

Example
    view = DescriptorTreeView(
        device.describe_configuration(),
        device.read_configuration(),
        parent,
    )
    view.sigPropertyChanged.connect(on_property_changed)

Parameters:

Name Type Description Default
descriptors dict[str, Descriptor]

Flat describe_configuration() dict keyed by name-property canonical keys.

required
readings dict[str, Reading[Any]]

Flat read_configuration() dict matching the same keys.

required
parent QWidget | None

Optional parent widget.

None
Signals

sigPropertyChanged: Signal[str, Any] Emitted when the user commits an edit. Carries (key: str, value: Any).

Source code in src/redsun/view/qt/treeview.py
class DescriptorTreeView(QtWidgets.QTreeWidget):
    """Two-column property tree for browsing and editing device settings.

    Modelled after the pyqtgraph ``ParameterTree``: uses ``setItemWidget``
    to place editor/display widgets permanently in the *Value* column, so
    there are no popup editors and no delegate geometry issues.

    The device-name root level is omitted — use one widget per device,
    e.g. inside a ``QTabWidget``.

    Example
    -------

    ```python
        view = DescriptorTreeView(
            device.describe_configuration(),
            device.read_configuration(),
            parent,
        )
        view.sigPropertyChanged.connect(on_property_changed)
    ```

    Parameters
    ----------
    descriptors: dict[str, Descriptor]
        Flat ``describe_configuration()`` dict keyed by
        ``name-property`` canonical keys.
    readings: dict[str, Reading[Any]]
        Flat ``read_configuration()`` dict matching the same keys.
    parent: QtWidgets.QWidget | None
        Optional parent widget.

    Signals
    -------
    sigPropertyChanged: Signal[str, Any]
        Emitted when the user commits an edit.
        Carries ``(key: str, value: Any)``.
    """

    sigPropertyChanged: Signal = Signal(str, object)

    def __init__(
        self,
        descriptors: dict[str, Descriptor],
        readings: dict[str, Reading[Any]],
        parent: QtWidgets.QWidget | None = None,
    ) -> None:
        super().__init__(parent)

        self._descriptors = descriptors
        self._readings: dict[str, Any] = {k: v["value"] for k, v in readings.items()}
        self._pending: dict[str, Any] = {}  # key -> old value
        # key -> the widget embedded in the Value column
        self._widgets: dict[str, QtWidgets.QWidget] = {}

        self.setColumnCount(2)
        self.setHeaderLabels(["Setting", "Value"])
        self.setHeaderHidden(True)
        _hdr = self.header()
        if _hdr is not None:
            _hdr.setSectionResizeMode(
                0, QtWidgets.QHeaderView.ResizeMode.ResizeToContents
            )
            _hdr.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeMode.Stretch)
        self.setRootIsDecorated(False)
        self.setIndentation(12)
        self.setAlternatingRowColors(True)
        self.setVerticalScrollMode(
            QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel
        )
        self.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.NoSelection)
        self.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus)

        self._build()

    def update_reading(self, key: str, reading: Reading[Any]) -> None:
        """Push a live value update for *key* into the corresponding widget.

        Parameters
        ----------
        key:
            Canonical ``name-property`` key.
        reading:
            New reading dict; only ``reading["value"]`` is used.
        """
        value = reading["value"]
        self._readings[key] = value
        widget = self._widgets.get(key)
        if widget is not None:
            desc = self._descriptors.get(key)
            if desc is not None:
                _update_widget_value(widget, value, desc)

    def confirm_change(self, key: str, success: bool) -> None:
        """Confirm or revert a pending user edit.

        Parameters
        ----------
        key:
            Canonical key of the setting that was attempted.
        success:
            ``True`` → keep the new value; ``False`` → revert to the
            pre-edit value and refresh the widget.
        """
        old = self._pending.pop(key, None)
        if old is None:
            return
        if not success:
            self._readings[key] = old
            widget = self._widgets.get(key)
            desc = self._descriptors.get(key)
            if widget is not None and desc is not None:
                _update_widget_value(widget, old, desc)
            _log.info("Reverted '%s' to previous value.", key)

    def get_keys(self) -> set[str]:
        """Return the set of all descriptor keys in this view."""
        return set(self._descriptors.keys())

    def _on_changed(self, key: str, value: Any) -> None:
        """Slot wired to every editor widget's change signal."""
        self._pending[key] = self._readings.get(key)
        self._readings[key] = value
        self.sigPropertyChanged.emit(key, value)

    def _build(self) -> None:
        """Populate the tree from ``self._descriptors`` and ``self._readings``.

        Groups descriptors by ``source`` (stripping the optional
        ``:readonly`` suffix), then creates one bold top-level
        ``QTreeWidgetItem`` per group and one child item per setting.
        """
        self.clear()
        self._widgets.clear()

        # group by source: source -> [(full_key, prop_name, descriptor, readonly)]
        groups: dict[str, list[tuple[str, str, Descriptor, bool]]] = {}
        for full_key, desc in self._descriptors.items():
            # strip the device prefix  (name\\property → prop)
            prop = full_key.split("-", 1)[-1] if "-" in full_key else full_key

            source_raw: str = desc.get("source", "unknown")
            parts = source_raw.split(":", 1)
            source = parts[0]
            readonly = len(parts) > 1 and parts[1] == "readonly"

            groups.setdefault(source, []).append((full_key, prop, desc, readonly))

        for source, leaves in groups.items():
            # --- group header row ---
            group_item = QtWidgets.QTreeWidgetItem([source.title()])
            group_item.setFirstColumnSpanned(True)
            font = group_item.font(0)
            font.setBold(True)
            group_item.setFont(0, font)
            group_item.setExpanded(True)
            self.addTopLevelItem(group_item)

            # --- leaf rows ---
            for full_key, prop, desc, readonly in leaves:
                child = QtWidgets.QTreeWidgetItem()
                child.setText(0, prop)
                child.setTextAlignment(
                    0,
                    QtCore.Qt.AlignmentFlag.AlignLeft
                    | QtCore.Qt.AlignmentFlag.AlignVCenter,
                )
                # tooltip
                tip_parts = [f"dtype: {desc.get('dtype', '?')}"]
                if "units" in desc:
                    tip_parts.append(f"units: {desc['units']}")
                if readonly:
                    tip_parts.append("(read-only)")
                child.setToolTip(0, " | ".join(tip_parts))
                child.setToolTip(1, " | ".join(tip_parts))

                group_item.addChild(child)

                # build the value widget
                initial = self._readings.get(full_key)
                widget = _make_value_widget(
                    full_key,
                    desc,
                    initial,
                    self._on_changed,
                    readonly,
                    self,
                )
                self.setItemWidget(child, 1, widget)
                self._widgets[full_key] = widget

        self.expandAll()
        self.resizeColumnToContents(0)

update_reading

update_reading(key: str, reading: Reading[Any]) -> None

Push a live value update for key into the corresponding widget.

Parameters:

Name Type Description Default
key str

Canonical name-property key.

required
reading Reading[Any]

New reading dict; only reading["value"] is used.

required
Source code in src/redsun/view/qt/treeview.py
def update_reading(self, key: str, reading: Reading[Any]) -> None:
    """Push a live value update for *key* into the corresponding widget.

    Parameters
    ----------
    key:
        Canonical ``name-property`` key.
    reading:
        New reading dict; only ``reading["value"]`` is used.
    """
    value = reading["value"]
    self._readings[key] = value
    widget = self._widgets.get(key)
    if widget is not None:
        desc = self._descriptors.get(key)
        if desc is not None:
            _update_widget_value(widget, value, desc)

confirm_change

confirm_change(key: str, success: bool) -> None

Confirm or revert a pending user edit.

Parameters:

Name Type Description Default
key str

Canonical key of the setting that was attempted.

required
success bool

True → keep the new value; False → revert to the pre-edit value and refresh the widget.

required
Source code in src/redsun/view/qt/treeview.py
def confirm_change(self, key: str, success: bool) -> None:
    """Confirm or revert a pending user edit.

    Parameters
    ----------
    key:
        Canonical key of the setting that was attempted.
    success:
        ``True`` → keep the new value; ``False`` → revert to the
        pre-edit value and refresh the widget.
    """
    old = self._pending.pop(key, None)
    if old is None:
        return
    if not success:
        self._readings[key] = old
        widget = self._widgets.get(key)
        desc = self._descriptors.get(key)
        if widget is not None and desc is not None:
            _update_widget_value(widget, old, desc)
        _log.info("Reverted '%s' to previous value.", key)

get_keys

get_keys() -> set[str]

Return the set of all descriptor keys in this view.

Source code in src/redsun/view/qt/treeview.py
def get_keys(self) -> set[str]:
    """Return the set of all descriptor keys in this view."""
    return set(self._descriptors.keys())