Skip to content

View

Base classes

Bases: ABC

Base view class.

Deliberately does not inherit PView: the protocol's read-only name property descriptor would shadow the instance attribute assigned here. Instances satisfy the protocol structurally - which is also how any non-ABC view is expected to comply.

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(ABC):
    """Base view class.

    Deliberately does **not** inherit [`PView`][redsun.view.PView]: the
    protocol's read-only ``name`` property descriptor would shadow the
    instance attribute assigned here. Instances satisfy the protocol
    structurally - which is also how any non-ABC view is expected to comply.

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

    name: str

    @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.

name is declared as a read-only property: the framework only reads it, so implementers may satisfy the protocol with a plain instance attribute, a class attribute, or a property.

Notes

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

Compliance is enforced at build time via isinstance - class-level checks cannot see attributes assigned in __init__.

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

    ``name`` is declared as a **read-only property**: the framework only
    reads it, so implementers may satisfy the protocol with a plain
    instance attribute, a class attribute, or a property.

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

    Compliance is enforced at build time via ``isinstance`` - class-level
    checks cannot see attributes assigned in ``__init__``.
    """

    @property
    def name(self) -> str:
        """Identity key of the view."""
        ...

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

name property

name: str

Identity key of the view.

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.

Where a view component sits in the main window.

Warning

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

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

    Where a view component sits in the main window.

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

    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)

    device_widgets, param_widgets = _build_param_widgets(spec)

    # a flat list is what keeps `Container.parameters` reachable
    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:
        """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:
    """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() / read() 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).

DescriptorTreeView

Bases: QTreeWidget

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

Rows are grouped by the source field of each descriptor (one header per device, leaf labels strip the device-name prefix).

Parameters:

Name Type Description Default
descriptors dict[str, Descriptor]

Mapping of canonical name-property keys to descriptors.

required
readings dict[str, Reading[Any]]

Initial readings for the same keys; only reading["value"] is used.

required
parent QWidget

Optional parent widget.

None
Signals

sig_property_changed : Signal[str, str, Any] Emitted when the user commits an edit to a setting. - str: object name - str: property name - Any: new value

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

    Rows are grouped by the ``source`` field of each descriptor (one
    header per device, leaf labels strip the device-name prefix).

    Parameters
    ----------
    descriptors : dict[str, Descriptor]
        Mapping of canonical ``name-property`` keys to descriptors.
    readings : dict[str, Reading[Any]]
        Initial readings for the same keys; only ``reading["value"]``
        is used.
    parent : QtWidgets.QWidget, optional
        Optional parent widget.

    Signals
    -------
    sig_property_changed : Signal[str, str, Any]
        Emitted when the user commits an edit to a setting.
        - str: object name
        - str: property name
        - Any: new value
    """

    sig_property_changed: Signal = Signal(str, 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 = {k: v["value"] for k, v in readings.items()}
        self._pending: dict[str, Any] = {}
        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 : str
            Canonical ``name-property`` key.
        reading : Reading[Any]
            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)

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

        Parameters
        ----------
        key : str
            Canonical key of the setting that was attempted.
        success : bool
            ``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)
            _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
        owner, property = key.split("-", 1)
        self.sig_property_changed.emit(owner, property, value)

    def _add_leaf(
        self,
        group_item: QtWidgets.QTreeWidgetItem,
        full_key: str,
        prop: str,
        desc: Descriptor,
        readonly: bool,
    ) -> None:
        """Append one leaf row (setting + value widget) to *group_item*."""
        child = QtWidgets.QTreeWidgetItem()
        units = desc.get("units", "") or ""
        label = f"{prop} ({units})" if units else prop
        child.setText(0, label)
        child.setTextAlignment(
            0,
            QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignVCenter,
        )
        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)")
        tip = " | ".join(tip_parts)
        child.setToolTip(0, tip)
        child.setToolTip(1, tip)
        group_item.addChild(child)
        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

    def _make_group_item(self, label: str) -> QtWidgets.QTreeWidgetItem:
        """Create and register a bold top-level group header item."""
        item = QtWidgets.QTreeWidgetItem([label])
        item.setFirstColumnSpanned(True)
        font = item.font(0)
        font.setBold(True)
        item.setFont(0, font)
        item.setExpanded(True)
        self.addTopLevelItem(item)
        return item

    def _build(self) -> None:
        """Populate the tree."""
        self.clear()
        self._widgets.clear()
        self._build_from_sources()
        self.expandAll()
        self.resizeColumnToContents(0)

    def _build_from_sources(self) -> None:
        """Build tree grouped by the ``source`` field prefix of each descriptor."""
        groups: dict[str, list[tuple[str, str, Descriptor, bool]]] = {}
        for full_key, desc in self._descriptors.items():
            prop = full_key.split("-", 1)[-1] if "-" in full_key else full_key
            source_raw = 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_item = self._make_group_item(source)
            for full_key, prop, desc, readonly in leaves:
                self._add_leaf(group_item, full_key, prop, desc, readonly)

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 : str
        Canonical ``name-property`` key.
    reading : Reading[Any]
        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)

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 : str
        Canonical key of the setting that was attempted.
    success : bool
        ``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)
        _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())

Built-ins

Bases: QtView, Loggable

View for configuring the output storage location.

Displays and edits the root output directory of the session path provider shared under PATH_PROVIDER. Paths generated by that provider have the form::

<base_dir>/<session>/<YYYY-MM-DD>/<plan>_<counter>

The view never owns a provider: when nothing bound the key it degrades to a read-only placeholder.

Parameters:

Name Type Description Default
name str

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

required
kwargs Any

Additional keyword arguments (unused).

{}
Source code in src/redsun/view/qt/builtins.py
class StorageView(QtView, Loggable):
    """View for configuring the output storage location.

    Displays and edits the root output directory of the session path provider
    shared under [`PATH_PROVIDER`][redsun.storage.PATH_PROVIDER]. Paths
    generated by that provider have the form::

        <base_dir>/<session>/<YYYY-MM-DD>/<plan>_<counter>

    The view never owns a provider: when nothing bound the key it degrades to a
    read-only placeholder.

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

    @property
    def view_position(self) -> ViewPosition:
        """The position in the main view."""
        return ViewPosition.LEFT

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

        self._provider: SessionPathProvider | None = None

        self._root_dir_edit = QtW.QLineEdit()
        self._root_dir_edit.setReadOnly(True)
        self._root_dir_btn = QtW.QPushButton("Browse...")
        self._root_dir_btn.clicked.connect(self._on_browse_clicked)

        base_dir_row = QtW.QHBoxLayout()
        base_dir_row.addWidget(self._root_dir_edit)
        base_dir_row.addWidget(self._root_dir_btn)

        self._open_dir_btn = QtW.QPushButton("Open root directory")
        self._open_dir_btn.clicked.connect(self._on_open_dir_clicked)

        form = QtW.QFormLayout()
        form.addRow("Root directory", base_dir_row)

        root = QtW.QVBoxLayout(self)
        root.addLayout(form)
        root.addWidget(self._open_dir_btn)
        root.addStretch()
        self.setLayout(root)

    def inject_dependencies(self, container: VirtualContainer) -> None:
        """Resolve the session path provider and mirror its base directory."""
        provider = container.try_require(PATH_PROVIDER)
        if provider is None:
            self.logger.warning("No session path provider was shared.")
            self._root_dir_edit.setText(_NO_ROOT_DIR)
            self._root_dir_btn.setEnabled(False)
            return

        self._provider = provider
        self._root_dir_edit.setText(
            run_coro(provider.signals.base_dir.get_value()) or _NO_ROOT_DIR
        )
        container.subscribe(provider.signals.base_dir, self.update_base_dir)

    @slot
    def update_base_dir(self, reading: dict[str, Reading[str]]) -> None:
        """Show the base directory carried by a reading of the provider's signal."""
        value = next(iter(reading.values()))["value"]
        self._root_dir_edit.setText(value or _NO_ROOT_DIR)

    def _on_browse_clicked(self) -> None:
        if self._provider is None:
            return
        chosen = QtW.QFileDialog.getExistingDirectory(
            self,
            "Select output directory",
            self._root_dir_edit.text(),
        )
        if not chosen:
            return
        self._provider.set_base_dir(Path(chosen))

    def _on_open_dir_clicked(self) -> None:
        path = self._root_dir_edit.text()
        if path == _NO_ROOT_DIR:
            return
        QDesktopServices.openUrl(QUrl.fromLocalFile(path))

view_position property

view_position: ViewPosition

The position in the main view.

inject_dependencies

inject_dependencies(container: VirtualContainer) -> None

Resolve the session path provider and mirror its base directory.

Source code in src/redsun/view/qt/builtins.py
def inject_dependencies(self, container: VirtualContainer) -> None:
    """Resolve the session path provider and mirror its base directory."""
    provider = container.try_require(PATH_PROVIDER)
    if provider is None:
        self.logger.warning("No session path provider was shared.")
        self._root_dir_edit.setText(_NO_ROOT_DIR)
        self._root_dir_btn.setEnabled(False)
        return

    self._provider = provider
    self._root_dir_edit.setText(
        run_coro(provider.signals.base_dir.get_value()) or _NO_ROOT_DIR
    )
    container.subscribe(provider.signals.base_dir, self.update_base_dir)

update_base_dir

update_base_dir(reading: dict[str, Reading[str]]) -> None

Show the base directory carried by a reading of the provider's signal.

Source code in src/redsun/view/qt/builtins.py
@slot
def update_base_dir(self, reading: dict[str, Reading[str]]) -> None:
    """Show the base directory carried by a reading of the provider's signal."""
    value = next(iter(reading.values()))["value"]
    self._root_dir_edit.setText(value or _NO_ROOT_DIR)