# Orca
#
# Copyright 2026 Igalia, S.L.
# Author: Joanmarie Diggs <jdiggs@igalia.com>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA  02110-1301 USA.

# pylint: disable=too-many-arguments
# pylint: disable=too-many-lines
# pylint: disable=too-many-locals
# pylint: disable=too-many-nested-blocks
# pylint: disable=too-many-positional-arguments
# pylint: disable=too-many-instance-attributes

"""Base class for preference grid UI components."""

from __future__ import annotations

import contextlib
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

import gi

gi.require_version("Atk", "1.0")
gi.require_version("Gdk", "3.0")
gi.require_version("Gtk", "3.0")
from gi.repository import Atk, Gdk, GLib, Gtk  # pylint: disable=no-name-in-module

from . import orca_gui_helpers

if TYPE_CHECKING:
    from collections.abc import Callable, Sequence


class CategoryListBoxRow(Gtk.ListBoxRow):
    """ListBoxRow for category selection with category identifier."""

    def __init__(self, category: str) -> None:
        super().__init__()
        self._category = category

    @property
    def category(self) -> str:
        """Get the category identifier."""
        return self._category


class RadioButtonWithActions(Gtk.RadioButton):
    """RadioButton with associated action buttons for navigation."""

    def __init__(self, label: str | None = None, group: Gtk.RadioButton | None = None) -> None:
        super().__init__(label=label, group=group)
        self._action_buttons: list[Gtk.Button] = []

    @property
    def action_buttons(self) -> list[Gtk.Button]:
        """Get the list of action buttons."""
        return self._action_buttons

    @action_buttons.setter
    def action_buttons(self, value: list[Gtk.Button]) -> None:
        """Set the list of action buttons."""
        self._action_buttons = value


class ListDetailPreferencesStackHelper:
    """Helper for managing list/detail preferences stack."""

    def __init__(self) -> None:
        self.stack: Gtk.Stack | None = None
        self.categories_listbox: Gtk.ListBox | None = None
        self.detail_listbox: Gtk.ListBox | None = None
        self.disable_widgets: list[Gtk.Widget] = []
        self.on_category_activated_callback: Callable[[Gtk.ListBoxRow], None] | None = None

    def show_categories(self) -> None:
        """Switch to categories view and show registered widgets."""

        if self.stack:
            self.stack.set_visible_child_name("categories")

        for widget in self.disable_widgets:
            widget.show()

    def show_detail(self) -> None:
        """Switch to detail view and hide registered widgets."""

        if self.stack:
            self.stack.set_visible_child_name("detail")

        for widget in self.disable_widgets:
            widget.hide()

    def register_disable_widgets(self, *widgets: Gtk.Widget) -> None:
        """Register widgets that should be disabled when in detail view."""

        self.disable_widgets.extend(widgets)


@dataclass
class BooleanPreferenceControl:  # pylint: disable=too-many-instance-attributes
    """Represents a boolean preference with its UI label and getters/setters."""

    label: str
    getter: Callable[[], bool | None]
    setter: Callable[[bool], Any]
    prefs_key: str | None = None
    member_of: str | None = None
    determine_sensitivity: Callable[[], bool] | None = None
    apply_immediately: bool = True


@dataclass
class IntRangePreferenceControl:  # pylint: disable=too-many-instance-attributes
    """Represents an integer range preference with its UI label and getters/setters."""

    label: str
    minimum: int
    maximum: int
    getter: Callable[[], int]
    setter: Callable[[int], Any]
    prefs_key: str | None = None
    member_of: str | None = None
    determine_sensitivity: Callable[[], bool] | None = None
    apply_immediately: bool = False


@dataclass
class FloatRangePreferenceControl:  # pylint: disable=too-many-instance-attributes
    """Represents a float range preference with its UI label and getters/setters."""

    label: str
    minimum: float
    maximum: float
    getter: Callable[[], float]
    setter: Callable[[float], Any]
    prefs_key: str | None = None
    member_of: str | None = None
    determine_sensitivity: Callable[[], bool] | None = None


@dataclass
class EnumPreferenceControl:  # pylint: disable=too-many-instance-attributes
    """Represents an enumerated preference with its UI label and getters/setters."""

    label: str
    options: list[str]
    getter: Callable[[], Any]
    setter: Callable[[Any], Any]
    values: list[Any] | None = None
    prefs_key: str | None = None
    member_of: str | None = None
    determine_sensitivity: Callable[[], bool] | None = None


@dataclass
class ColorPreferenceControl:
    """Represents a color preference with its UI label and getters/setters."""

    label: str
    getter: Callable[[], str]
    setter: Callable[[str], Any]
    prefs_key: str | None = None
    member_of: str | None = None
    determine_sensitivity: Callable[[], bool] | None = None


@dataclass
class SelectionPreferenceControl:  # pylint: disable=too-many-instance-attributes
    """Control for selection-based preferences with optional actions."""

    label: str
    options: list[str]
    getter: Callable[[], Any]
    setter: Callable[[Any], Any] | None
    values: list[Any] | None = None
    prefs_key: str | None = None
    member_of: str | None = None
    get_actions_for_option: Callable[[Any], list[tuple[str, str, Callable[[], None]]]] | None = None
    determine_sensitivity: Callable[[], bool] | None = None
    apply_immediately: bool = True
    tracks_changes: bool = True


ControlType = (
    BooleanPreferenceControl
    | IntRangePreferenceControl
    | FloatRangePreferenceControl
    | EnumPreferenceControl
    | ColorPreferenceControl
    | SelectionPreferenceControl
)


@dataclass(frozen=True)
class PreferenceValueDoc:
    """Documentation metadata for one preference value."""

    label: str
    value: str = ""
    summary: str = ""


@dataclass(frozen=True)
class PreferenceActionDoc:
    """Documentation metadata for an action associated with a preference control."""

    label: str
    summary: str = ""
    placement: str = ""


@dataclass(frozen=True)
class PreferenceControlDoc:
    """Documentation metadata for a preference control."""

    label: str
    kind: str
    summary: str = ""
    schema: str = ""
    key: str = ""
    values: tuple[str, ...] = ()
    value_docs: tuple[PreferenceValueDoc, ...] = ()
    actions: tuple[PreferenceActionDoc, ...] = ()
    item_actions: tuple[PreferenceActionDoc, ...] = ()
    controls: tuple[PreferenceControlDoc, ...] = ()
    dynamic_values: bool = False
    minimum: str = ""
    maximum: str = ""


@dataclass(frozen=True)
class PreferencePanelDoc:
    """Documentation metadata for a preference panel."""

    title: str
    panel_id: str = ""
    summary: str = ""
    description: str = ""
    schema: str = ""
    controls: tuple[PreferenceControlDoc, ...] = ()
    show_available_settings: bool = True


# pylint: disable=no-member
class PreferencesFocusManagedListBox(orca_gui_helpers.FocusManagedListBox):
    """Focus-managed listbox with preferences-window Left arrow behavior."""

    def _navigate_left_from_widget(self, widget: Gtk.Widget) -> bool:
        """Exit nested group or move to sidebar on Left arrow."""

        if isinstance(widget, (Gtk.Scale, Gtk.SpinButton)):
            return False
        parent = self.get_parent()
        first_grid = None
        while parent is not None:
            if isinstance(parent, PreferencesGridBase):
                if first_grid is None:
                    first_grid = parent
                if parent.is_in_multipage_detail():
                    parent.multipage_show_categories()
                    return True
            parent = parent.get_parent()
        if first_grid is not None:
            first_grid.focus_sidebar()
            return True
        return False


# pylint: enable=no-member


# pylint: disable-next=too-many-instance-attributes
class PreferencesGridBase(Gtk.Grid):
    """Base class for all preferences grid widgets with common UI helpers."""

    # pylint: disable=no-member
    def __init__(self, tab_label: str) -> None:
        """Initialize the preferences grid with a tab label."""

        super().__init__()
        self._tab_label = tab_label
        self._has_unsaved_changes = False
        self._focus_sidebar_callback: Callable[[], None] | None = None
        self.set_border_width(24)
        self.set_margin_start(100)
        self.set_margin_end(100)
        self.set_margin_top(40)
        self.set_margin_bottom(40)
        self.set_row_spacing(12)
        self.set_column_spacing(48)

        self._list_detail_stack_helper: ListDetailPreferencesStackHelper | None = None
        self._multipage_stack: Gtk.Stack | None = None
        self._multipage_title_callback: Callable[[str], None] | None = None
        self._multipage_main_title: str | None = None
        self._multipage_categories: list[tuple[str, str, PreferencesGridBase]] | None = None
        self._multipage_category_map: dict[str, tuple[str, PreferencesGridBase]] | None = None
        self._multipage_enable_listbox: PreferencesFocusManagedListBox | None = None
        self._multipage_enable_switch: Gtk.Switch | None = None
        self._multipage_enable_initial: bool = False
        self._multipage_categories_listbox: Gtk.ListBox | None = None
        self._multipage_last_activated_row: Gtk.ListBoxRow | None = None

    def get_title(self) -> str:
        """Return the panel title."""

        return self._tab_label

    @classmethod
    def get_documentation(cls) -> PreferencePanelDoc:
        """Return documentation metadata for this preferences grid."""

        return PreferencePanelDoc(title=cls.__name__)

    def has_changes(self) -> bool:
        """Return True if the user has made changes that haven't been written to file."""

        return self._has_unsaved_changes

    def revert_changes(self) -> None:
        """Revert any changes that were applied immediately but not saved."""

    def on_becoming_visible(self) -> None:
        """Called when this grid becomes the visible panel in the main stack."""

    def set_focus_sidebar_callback(self, callback: Callable[[], None]) -> None:
        """Set the callback to focus the sidebar navigation list."""

        self._focus_sidebar_callback = callback
        # Forward to child grids in multi-page structures
        if self._multipage_categories:
            for _label, _page_id, grid in self._multipage_categories:
                grid.set_focus_sidebar_callback(callback)

    def focus_sidebar(self) -> None:
        """Move focus to the sidebar navigation list."""

        if self._focus_sidebar_callback:
            self._focus_sidebar_callback()

    def is_in_multipage_detail(self) -> bool:
        """Returns True if this grid has a multipage stack showing a detail page."""

        if self._multipage_stack is None:
            return False
        visible_child = self._multipage_stack.get_visible_child_name()
        return visible_child is not None and visible_child != "categories"

    def _create_list_detail_preferences_stack(
        self,
        on_category_activated: Callable[[Gtk.ListBoxRow], None],
        on_detail_row_activated: Callable[[Gtk.ListBox, Gtk.ListBoxRow], None] | None = None,
    ) -> tuple[Gtk.Stack, Gtk.ListBox, Gtk.ListBox]:
        """Create a category-list stack with one shared detail list."""

        helper = ListDetailPreferencesStackHelper()
        self._list_detail_stack_helper = helper

        helper.stack = Gtk.Stack()
        helper.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT)

        helper.categories_listbox = Gtk.ListBox()
        helper.categories_listbox.set_selection_mode(Gtk.SelectionMode.NONE)
        helper.categories_listbox.connect("key-press-event", self._on_stack_categories_key_press)
        helper.categories_listbox.connect("row-activated", self._on_stack_category_activated)
        helper.on_category_activated_callback = on_category_activated
        categories_scrolled = orca_gui_helpers.create_preferences_scrolled_window(
            helper.categories_listbox
        )
        helper.stack.add_named(categories_scrolled, "categories")

        helper.detail_listbox = Gtk.ListBox()
        helper.detail_listbox.set_selection_mode(Gtk.SelectionMode.NONE)
        helper.detail_listbox.connect("key-press-event", self._on_stack_detail_key_press)
        if on_detail_row_activated:
            helper.detail_listbox.connect("row-activated", on_detail_row_activated)
        detail_scrolled = orca_gui_helpers.create_preferences_scrolled_window(helper.detail_listbox)
        helper.stack.add_named(detail_scrolled, "detail")

        helper.stack.set_hexpand(True)
        helper.stack.set_vexpand(True)
        helper.stack.set_visible_child_name("categories")
        return (helper.stack, helper.categories_listbox, helper.detail_listbox)

    def _add_stack_category_row(
        self,
        listbox: Gtk.ListBox,
        label: str,
        **custom_attrs,
    ) -> CategoryListBoxRow:
        """Add a category row to the categories listbox with chevron icon."""

        category = custom_attrs.get("category", "")
        row = CategoryListBoxRow(category)
        row.set_activatable(True)

        hbox = orca_gui_helpers.create_row_box()

        label_widget = Gtk.Label(label=label, xalign=0)
        label_widget.set_hexpand(True)
        hbox.pack_start(label_widget, True, True, 0)

        chevron = Gtk.Image.new_from_icon_name("go-next-symbolic", Gtk.IconSize.BUTTON)
        chevron_accessible = chevron.get_accessible()
        if chevron_accessible:
            chevron_accessible.set_name("")
            chevron_accessible.set_role(Atk.Role.BUTTON)
        hbox.pack_end(chevron, False, False, 0)

        row.add(hbox)

        row.show_all()
        listbox.add(row)
        return row

    def _register_stack_disable_widgets(self, *widgets: Gtk.Widget) -> None:
        """Register widgets that should be disabled when in detail view."""

        if self._list_detail_stack_helper:
            self._list_detail_stack_helper.register_disable_widgets(*widgets)

    def _show_stack_categories(self) -> None:
        """Switch to categories view and enable registered widgets."""

        if self._list_detail_stack_helper:
            self._list_detail_stack_helper.show_categories()

    def _show_stack_detail(self) -> None:
        """Switch to detail view and disable registered widgets."""

        if self._list_detail_stack_helper:
            self._list_detail_stack_helper.show_detail()

    def _on_stack_category_activated(self, _listbox: Gtk.ListBox, row: Gtk.ListBoxRow) -> None:
        """Internal handler for category activation - delegates to callback."""

        if (
            self._list_detail_stack_helper
            and self._list_detail_stack_helper.on_category_activated_callback
        ):
            self._list_detail_stack_helper.on_category_activated_callback(row)

    def _on_stack_categories_key_press(self, _widget: Gtk.Widget, event: Gdk.EventKey) -> bool:
        """Handle key press in categories page - Right arrow activates focused row."""

        if event.keyval == Gdk.KEY_Right:
            # Get the focused row (selection mode is NONE, so get_selected_row won't work)
            categories_listbox = (
                self._list_detail_stack_helper.categories_listbox
                if self._list_detail_stack_helper
                else None
            )
            if categories_listbox:
                focused_child = categories_listbox.get_focus_child()
                if focused_child and isinstance(focused_child, Gtk.ListBoxRow):
                    self._on_stack_category_activated(categories_listbox, focused_child)
                    return True
        return False

    def _on_stack_detail_key_press(self, widget: Gtk.Widget, event: Gdk.EventKey) -> bool:
        """Handle key press in detail page - Left/Escape/Alt+Left to go back."""

        # Don't intercept Left for widgets where arrows have meaning
        focused = widget.get_toplevel().get_focus()
        if event.keyval == Gdk.KEY_Left and isinstance(focused, (Gtk.Scale, Gtk.SpinButton)):
            return False

        # Left arrow or Escape to go back to categories
        if event.keyval in [Gdk.KEY_Left, Gdk.KEY_Escape]:
            self._show_stack_categories()
            return True

        # Alt+Left to go back
        if event.keyval == Gdk.KEY_Left and event.state & Gdk.ModifierType.MOD1_MASK:
            self._show_stack_categories()
            return True

        return False

    def _add_category_grids_to_stack(
        self,
        stack: Gtk.Stack,
        categories: list[tuple[str, str, PreferencesGridBase]],
    ) -> None:
        """Adds category grids to the stack with normalized margins."""

        for _label, page_id, grid in categories:
            grid.set_border_width(0)
            grid.set_margin_start(0)
            grid.set_margin_end(0)
            grid.set_margin_top(0)
            grid.set_margin_bottom(0)
            grid.set_row_spacing(6)
            for child in grid.get_children():
                if isinstance(child, Gtk.Frame):
                    child.set_margin_start(0)
                    child.set_margin_end(0)
                    child.set_margin_top(6)
                    child.set_margin_bottom(0)

            stack.add_named(grid, page_id)
            grid.connect("key-press-event", self._on_multipage_child_key_press)

    # pylint: disable-next=too-many-arguments, too-many-positional-arguments
    def _create_child_grid_preferences_stack(
        self,
        enable_label: str | None,
        enable_getter: Callable[[], bool] | None,
        enable_setter: Callable[[bool], Any] | None,
        categories: list[tuple[str, str, PreferencesGridBase]],
        title_change_callback: Callable[[str], None] | None = None,
        main_title: str | None = None,
    ) -> tuple[PreferencesFocusManagedListBox | None, Gtk.Stack, Gtk.ListBox]:
        """Create a category-list stack whose detail pages are child grids.

        This helper encapsulates the common pattern used by Speech and Braille preferences
        where there's an optional enable switch at the top, a categories page, and one
        child PreferencesGridBase page for each category.

        Args:
            enable_label: Label for the enable switch (e.g., "Enable speech"), or None to skip
            enable_getter: Function to get current enable state, or None to skip
            enable_setter: Function to set enable state, or None to skip
            categories: List of (label, page_id, child_grid) tuples for each category
            title_change_callback: Optional callback to update window title
            main_title: Title to use when on categories page (defaults to grid's label)

        Returns:
            Tuple of (enable_listbox, stack, categories_listbox) for additional customization.
            enable_listbox will be None if enable_label/getter/setter are None.

        Example:
            categories = [
                (guilabels.GENERAL, "general", self._general_grid),
                (guilabels.SETTINGS, "settings", self._settings_grid),
            ]
            enable_listbox, stack, categories_listbox = self._create_child_grid_preferences_stack(
                enable_label=guilabels.ENABLE_FEATURE,
                enable_getter=self._manager.get_enabled,
                enable_setter=self._manager.set_enabled,
                categories=categories,
                title_change_callback=self._update_title,
                main_title=guilabels.FEATURE
            )
        """

        if main_title is None:
            main_title = self._tab_label

        self._multipage_title_callback = title_change_callback
        self._multipage_main_title = main_title
        self._multipage_categories = categories
        self._multipage_category_map = {
            page_id: (label, grid) for label, page_id, grid in categories
        }

        enable_listbox: PreferencesFocusManagedListBox | None = None
        if enable_label is not None and enable_getter is not None and enable_setter is not None:
            enable_listbox = PreferencesFocusManagedListBox()
            enable_row, enable_switch, _label = orca_gui_helpers.create_switch_row(
                enable_label,
                lambda switch, _param: self._on_multipage_enable_toggled(switch, enable_setter),
                enable_getter(),
                include_top_separator=False,
            )
            enable_listbox.add_row_with_widget(enable_row, enable_switch)
            self._multipage_enable_switch = enable_switch
            self._multipage_enable_initial = enable_getter()
        self._multipage_enable_listbox = enable_listbox

        stack = Gtk.Stack()
        stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT)
        stack.set_hexpand(True)
        stack.set_vexpand(True)

        categories_listbox = Gtk.ListBox()
        categories_listbox.set_selection_mode(Gtk.SelectionMode.NONE)
        categories_listbox.connect("row-activated", self._on_multipage_category_activated)
        categories_listbox.connect("key-press-event", self._on_multipage_categories_key_press)
        orca_gui_helpers.set_listbox_separators(categories_listbox)

        for label, page_id, _grid in categories:
            self._add_stack_category_row(categories_listbox, label, category=page_id)

        categories_listbox.get_style_context().add_class("frame")
        categories_listbox.set_vexpand(False)
        categories_listbox.set_valign(Gtk.Align.START)
        stack.add_named(categories_listbox, "categories")

        self._add_category_grids_to_stack(stack, categories)

        stack.set_visible_child_name("categories")

        stack.set_margin_start(0)
        stack.set_margin_end(0)
        stack.set_margin_top(0)
        stack.set_margin_bottom(0)

        if enable_getter is not None:
            stack.set_sensitive(enable_getter())

        self._multipage_stack = stack
        self._multipage_categories_listbox = categories_listbox

        return (enable_listbox, stack, categories_listbox)

    def _on_multipage_enable_toggled(
        self,
        switch: Gtk.Switch,
        setter: Callable[[bool], Any],
    ) -> None:
        """Handle enable switch toggle in child-grid stack."""

        if self._initializing:
            return

        enabled = switch.get_active()
        if self._multipage_stack:
            self._multipage_stack.set_sensitive(enabled)

        if enabled:
            setter(True)

        self._has_unsaved_changes = enabled != self._multipage_enable_initial

    def _on_multipage_categories_key_press(self, _widget: Gtk.Widget, event: Gdk.EventKey) -> bool:
        """Handle key press in multi-page categories - Right arrow activates focused row."""

        if event.keyval == Gdk.KEY_Right:
            if self._multipage_categories_listbox:
                focused_child = self._multipage_categories_listbox.get_focus_child()
                if focused_child and isinstance(focused_child, Gtk.ListBoxRow):
                    self._on_multipage_category_activated(
                        self._multipage_categories_listbox,
                        focused_child,
                    )
                    return True
        return False

    def _on_multipage_category_activated(self, _listbox: Gtk.ListBox, row: Gtk.ListBoxRow) -> None:
        """Handle category activation in child-grid stack."""

        if not isinstance(row, CategoryListBoxRow):
            return

        category_id = row.category
        if not category_id or not self._multipage_stack:
            return

        self._multipage_last_activated_row = row

        if self._multipage_enable_listbox:
            self._multipage_enable_listbox.hide()

        # Show the child before switching to it (it may have been hidden when leaving)
        if self._multipage_category_map is not None and category_id in self._multipage_category_map:
            _, grid = self._multipage_category_map[category_id]
            if grid:
                grid.show_all()

        self._multipage_stack.set_visible_child_name(category_id)
        if self._multipage_title_callback and self._multipage_category_map is not None:
            label, _ = self._multipage_category_map.get(
                category_id,
                (self._multipage_main_title or "", None),
            )
            self._multipage_title_callback(label or "")

        # Focus first widget in grid
        if self._multipage_category_map is not None and category_id in self._multipage_category_map:
            _, grid = self._multipage_category_map[category_id]
            if grid:
                GLib.idle_add(self._multipage_focus_first_widget, grid)

    @staticmethod
    def _get_children(container: Gtk.Container) -> list[Gtk.Widget]:
        """Gets non-internal children via foreach."""

        children: list[Gtk.Widget] = []
        container.foreach(children.append)
        return children

    @staticmethod
    def _focus_first_listbox_row(listbox: Gtk.ListBox) -> bool:
        """Focuses the first sensitive, visible row in a listbox."""

        for row in listbox.get_children():
            if row.get_sensitive() and row.get_visible():
                row.grab_focus()
                return True
        return False

    def _focus_first_in_scrolled_window(self, scrolled_window: Gtk.ScrolledWindow) -> bool:
        """Tries to focus the first listbox row inside a scrolled window."""

        for sw_child in self._get_children(scrolled_window):
            viewport_children = (
                self._get_children(sw_child) if isinstance(sw_child, Gtk.Container) else []
            )
            for vp_child in viewport_children:
                if isinstance(vp_child, Gtk.ListBox) and self._focus_first_listbox_row(vp_child):
                    return True
                if isinstance(vp_child, Gtk.Container) and self._focus_first_in_container(vp_child):
                    return True
        return False

    def _focus_first_in_container(self, container: Gtk.Container) -> bool:
        """Tries to focus the first listbox row inside a container."""

        children = self._get_children(container)
        if isinstance(container, Gtk.Grid):
            children = list(reversed(children))

        for child in children:
            if isinstance(child, Gtk.ListBox) and self._focus_first_listbox_row(child):
                return True
            if isinstance(child, Gtk.ScrolledWindow) and self._focus_first_in_scrolled_window(
                child
            ):
                return True
            if isinstance(child, Gtk.Container) and self._focus_first_in_container(child):
                return True
        return False

    def _multipage_focus_first_widget(self, grid: PreferencesGridBase) -> bool:
        """Focus the first focusable widget in a multi-page child grid."""

        children = list(reversed(self._get_children(grid)))

        for child in children:
            if isinstance(child, Gtk.ScrolledWindow) and self._focus_first_in_scrolled_window(
                child
            ):
                return False
            if isinstance(child, Gtk.ListBox) and self._focus_first_listbox_row(child):
                return False
            if isinstance(child, Gtk.Container) and self._focus_first_in_container(child):
                return False

        return False

    def _on_multipage_child_key_press(self, widget: Gtk.Widget, event: Gdk.EventKey) -> bool:
        """Handle key press in multi-page child grids - Left/Escape to go back."""

        # Don't intercept Left for widgets where arrows have meaning
        focused = widget.get_toplevel().get_focus()
        if event.keyval == Gdk.KEY_Left and isinstance(focused, (Gtk.Scale, Gtk.SpinButton)):
            return False

        if event.keyval in [Gdk.KEY_Left, Gdk.KEY_Escape]:
            self.multipage_show_categories()
            return True

        if event.keyval == Gdk.KEY_Left and event.state & Gdk.ModifierType.MOD1_MASK:
            self.multipage_show_categories()
            return True

        return False

    def multipage_show_categories(self) -> None:
        """Switch back to categories view in child-grid stack."""

        if self._multipage_stack:
            # Explicitly hide the current child if it's a detail page so AT-SPI
            # updates visibility states for the categories listbox. Without this,
            # items may be reported as "not showing" and not presented. However,
            # if the current child is already the categories listbox, hiding it
            # causes GTK Stack to fall back to another child.
            current_child = self._multipage_stack.get_visible_child()
            if current_child and current_child != self._multipage_categories_listbox:
                current_child.hide()
            self._multipage_stack.set_visible_child_name("categories")

        if self._multipage_enable_listbox:
            self._multipage_enable_listbox.show()

        if self._multipage_title_callback and self._multipage_main_title is not None:
            self._multipage_title_callback(self._multipage_main_title)

        if self._multipage_categories_listbox:
            self._multipage_categories_listbox.show_all()

        if self._multipage_last_activated_row:
            self._multipage_last_activated_row.grab_focus()
        elif self._multipage_categories_listbox:
            self._multipage_categories_listbox.grab_focus()

    def multipage_on_becoming_visible(self) -> None:
        """Reset child-grid stack to categories view when grid becomes visible.

        Call this from your on_becoming_visible() override if using child-grid stack.
        """

        self._multipage_last_activated_row = None
        self.multipage_show_categories()


class AutoPreferencesGrid(PreferencesGridBase):  # pylint: disable=too-many-instance-attributes
    """Simplified preferences grid that automatically builds UI from control definitions."""

    _gsettings_schema: str = ""

    # pylint: disable=no-member
    def __init__(
        self,
        tab_label: str,
        controls: Sequence[ControlType],
        info_message: str = "",
    ) -> None:
        """Initialize the grid with a list of controls."""

        super().__init__(tab_label)
        self._controls = list(controls)
        self._widgets: list[Gtk.Widget] = []
        self._rows: list[Gtk.ListBoxRow] = []
        self._group_labels: dict[str, Gtk.Label] = {}
        self._group_listboxes: dict[str, PreferencesFocusManagedListBox] = {}
        self._info_message = info_message
        self._info_listbox: Gtk.ListBox | None = None
        self._initializing = True

        self._combo_size_group = Gtk.SizeGroup(mode=Gtk.SizeGroupMode.HORIZONTAL)
        self._widget_to_control_index: dict[Gtk.Widget, int] = {}
        self._radio_to_selection_value: dict[Gtk.RadioButton, Any] = {}

        self._build()
        self._initializing = False
        self.refresh()

    def _populate_listbox(
        self,
        listbox: PreferencesFocusManagedListBox,
        controls: list[tuple[int, ControlType]],
    ) -> None:
        """Adds control rows to a listbox and registers widgets."""

        for index, control in controls:
            widget = self._create_control_row(control, listbox)
            self._widget_to_control_index[widget] = index
            if isinstance(widget, Gtk.RadioButton):
                for radio in widget.get_group():
                    self._widget_to_control_index[radio] = index
            self._widgets.insert(index, widget)
            last_row = listbox.get_last_row()
            if last_row:
                self._rows.insert(index, last_row)

    # pylint: disable-next=too-many-statements
    def _build(self) -> None:
        """Automatically build UI from controls, grouping by member_of."""

        content_grid = Gtk.Grid()
        content_grid.set_row_spacing(12)
        content_grid.set_column_spacing(48)

        row = 0

        if self._info_message:
            self._info_listbox = orca_gui_helpers.create_info_listbox(self._info_message)
            self._info_listbox.set_margin_bottom(24)
            content_grid.attach(self._info_listbox, 0, row, 1, 1)
            row += 1

        # Group controls by member_of.
        groups: dict[str | None, list[tuple[int, ControlType]]] = {}
        for i, control in enumerate(self._controls):
            member = control.member_of
            if member not in groups:
                groups[member] = []
            groups[member].append((i, control))

        # Ungrouped controls.
        if None in groups:
            listbox = PreferencesFocusManagedListBox()
            listbox.set_hexpand(True)
            listbox.set_halign(Gtk.Align.FILL)
            listbox.get_accessible().set_name(self._tab_label)
            self._populate_listbox(listbox, groups[None])
            content_grid.attach(listbox, 0, row, 1, 1)
            row += 1

        # Then process named groups in order they appear.
        seen_groups: set[str | None] = {None}
        for control in self._controls:
            member = control.member_of
            if member is not None and member not in seen_groups:
                seen_groups.add(member)

                label = Gtk.Label(label=member, xalign=0)
                label.get_style_context().add_class("heading")
                label.set_margin_top(12 if row > 0 else 0)
                label.set_margin_bottom(6)
                content_grid.attach(label, 0, row, 1, 1)
                self._group_labels[member] = label
                row += 1

                listbox = PreferencesFocusManagedListBox()
                listbox.set_hexpand(True)
                listbox.set_halign(Gtk.Align.FILL)
                listbox.get_accessible().set_name(member)
                self._group_listboxes[member] = listbox

                self._populate_listbox(listbox, groups[member])

                content_grid.attach(listbox, 0, row, 1, 1)
                row += 1

                label_atk = label.get_accessible()
                listbox_atk = listbox.get_accessible()
                atk_relation_set = listbox_atk.ref_relation_set()
                atk_relation_set.add(Atk.Relation.new([label_atk], Atk.RelationType.LABELLED_BY))

        scrolled = Gtk.ScrolledWindow()
        scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
        scrolled.add(content_grid)
        scrolled.set_hexpand(True)
        scrolled.set_vexpand(True)
        self.attach(scrolled, 0, 0, 1, 1)

    def add_button_to_group_header(
        self,
        group_name: str,
        icon_name: str,
        callback: Callable[[Gtk.Button], None],
        accessible_name: str,
    ) -> Gtk.Button | None:
        """Add a button to a group's header. Returns the button or None if group not found."""

        label = self._group_labels.get(group_name)
        if label is None:
            return None

        parent = label.get_parent()
        if parent is None:
            return None

        button = Gtk.Button.new_from_icon_name(icon_name, Gtk.IconSize.BUTTON)
        button.get_accessible().set_name(accessible_name)
        button.connect("clicked", callback)

        label_text = label.get_label()
        margin_top = label.get_margin_top()
        margin_bottom = label.get_margin_bottom()

        header_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
        header_box.set_margin_top(margin_top)
        header_box.set_margin_bottom(margin_bottom)

        new_label = Gtk.Label(label=label_text, xalign=0)
        new_label.get_style_context().add_class("heading")
        header_box.pack_start(new_label, True, True, 0)
        header_box.pack_end(button, False, False, 0)

        if isinstance(parent, Gtk.Grid):
            top = parent.child_get_property(label, "top-attach")
            left = parent.child_get_property(label, "left-attach")
            parent.remove(label)
            parent.attach(header_box, left, top, 1, 1)
            header_box.show_all()

        self._group_labels[group_name] = new_label

        listbox = self._group_listboxes.get(group_name)
        if listbox:
            new_label_atk = new_label.get_accessible()
            listbox_atk = listbox.get_accessible()
            atk_relation_set = listbox_atk.ref_relation_set()
            atk_relation_set.add(Atk.Relation.new([new_label_atk], Atk.RelationType.LABELLED_BY))

        return button

    def _create_control_row(
        self,
        control: ControlType,
        listbox: PreferencesFocusManagedListBox,
    ) -> Gtk.Widget:
        """Create a row for any type of control."""

        if isinstance(control, BooleanPreferenceControl):
            return self._create_boolean_row(control, listbox)
        if isinstance(control, IntRangePreferenceControl):
            return self._create_int_range_row(control, listbox)
        if isinstance(control, FloatRangePreferenceControl):
            return self._create_float_range_row(control, listbox)
        if isinstance(control, EnumPreferenceControl):
            return self._create_enum_row(control, listbox)
        if isinstance(control, ColorPreferenceControl):
            return self._create_color_row(control, listbox)
        if isinstance(control, SelectionPreferenceControl):
            return self._create_selection_row(control, listbox)
        raise TypeError(f"Unknown control type: {type(control)}")

    def _create_boolean_row(
        self,
        control: BooleanPreferenceControl,
        listbox: PreferencesFocusManagedListBox,
    ) -> Gtk.Switch:
        """Create a switch row for a boolean control."""

        row, switch, _label = orca_gui_helpers.create_switch_row(
            control.label,
            self._on_value_changed,
            bool(control.getter()),
            include_top_separator=False,
        )
        listbox.add_row_with_widget(row, switch)
        return switch

    def _create_int_range_row(
        self,
        control: IntRangePreferenceControl,
        listbox: PreferencesFocusManagedListBox,
    ) -> Gtk.SpinButton:
        """Create a spin button row for an integer range control."""

        adjustment = Gtk.Adjustment(
            value=control.minimum,
            lower=control.minimum,
            upper=control.maximum,
            step_increment=1,
            page_increment=10,
        )
        row, spin, _label = orca_gui_helpers.create_spin_button_row(
            control.label,
            adjustment,
            self._on_value_changed,
            include_top_separator=False,
        )
        listbox.add_row_with_widget(row, spin)
        return spin

    def _create_float_range_row(
        self,
        control: FloatRangePreferenceControl,
        listbox: PreferencesFocusManagedListBox,
    ) -> Gtk.Scale:
        """Create a scale row for a float range control."""

        adjustment = orca_gui_helpers.create_range_adjustment(
            value=control.minimum,
            minimum=control.minimum,
            maximum=control.maximum,
        )
        row, scale, _label = orca_gui_helpers.create_slider_row(
            control.label,
            adjustment,
            self._on_value_changed,
            include_top_separator=False,
        )
        listbox.add_row_with_widget(row, scale)
        return scale

    def _create_color_row(
        self,
        control: ColorPreferenceControl,
        listbox: PreferencesFocusManagedListBox,
    ) -> Gtk.ColorButton:
        """Create a color button row for a color control."""

        row, color_button, _label = orca_gui_helpers.create_color_button_row(
            control.label,
            self._on_value_changed,
            include_top_separator=False,
        )
        orca_gui_helpers.set_color_button_hex(color_button, control.getter())
        listbox.add_row_with_widget(row, color_button)
        return color_button

    def _create_enum_row(
        self,
        control: EnumPreferenceControl,
        listbox: PreferencesFocusManagedListBox,
    ) -> Gtk.ComboBoxText:
        """Create a combo box row for an enum control."""

        row, combo, _label = orca_gui_helpers.create_combo_box_text_row(
            control.label,
            [(option, option) for option in control.options],
            include_top_separator=False,
            changed_handler=self._on_value_changed,
        )
        self._combo_size_group.add_widget(combo)
        listbox.add_row_with_widget(row, combo)

        return combo

    def _create_selection_row(
        self,
        control: SelectionPreferenceControl,
        listbox: PreferencesFocusManagedListBox,
    ) -> Gtk.Widget:
        """Create a combo box or radio button rows for a selection control."""

        if control.get_actions_for_option is None:
            return self._create_selection_combo_row(control, listbox)
        return self._create_selection_radio_rows(control, listbox)

    def _create_selection_combo_row(
        self,
        control: SelectionPreferenceControl,
        listbox: PreferencesFocusManagedListBox,
    ) -> Gtk.ComboBoxText:
        """Create a combo box row for a selection control (no per-item actions)."""

        row, combo, _label = orca_gui_helpers.create_combo_box_text_row(
            control.label,
            [(option, option) for option in control.options],
            include_top_separator=False,
            changed_handler=self._on_value_changed,
        )
        self._combo_size_group.add_widget(combo)
        listbox.add_row_with_widget(row, combo)

        return combo

    def _create_radio_button_with_actions(
        self,
        option_label: str,
        value: Any,
        group: Gtk.RadioButton | None,
    ) -> RadioButtonWithActions:
        """Create a radio button for a selection option."""

        if group is None:
            radio = RadioButtonWithActions(label=option_label)
        else:
            radio = RadioButtonWithActions(group=group, label=option_label)
        radio.set_hexpand(True)
        radio.connect("toggled", self._on_value_changed)
        radio.connect("key-press-event", self._on_radio_key_press)
        self._radio_to_selection_value[radio] = value
        return radio

    def _create_radio_action_buttons(
        self,
        actions: list[tuple[str, str, Callable[[], None]]],
    ) -> tuple[Gtk.Box | None, list[Gtk.Button]]:
        """Create action buttons for a radio-button selection row."""

        if not actions:
            return None, []

        button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
        action_buttons = []
        for action_label, icon_name, callback in actions:
            button = orca_gui_helpers.create_flat_icon_button(icon_name, action_label)
            button.connect("clicked", lambda _btn, cb=callback: cb())
            button.connect("key-press-event", self._on_action_button_key_press)
            button_box.pack_start(button, False, False, 0)
            action_buttons.append(button)

        return button_box, action_buttons

    def _create_selection_radio_rows(
        self,
        control: SelectionPreferenceControl,
        listbox: PreferencesFocusManagedListBox,
    ) -> Gtk.RadioButton:
        """Create radio button rows with three-dot menus for a selection control."""

        assert control.get_actions_for_option is not None
        values = control.values if control.values is not None else control.options
        first_radio = None
        current_value = control.getter()

        for option_label, value in zip(control.options, values, strict=True):
            row = Gtk.ListBoxRow()
            row.set_activatable(False)
            hbox = orca_gui_helpers.create_row_box()
            radio = self._create_radio_button_with_actions(option_label, value, first_radio)
            if first_radio is None:
                first_radio = radio
            hbox.pack_start(radio, True, True, 0)

            button_box, action_buttons = self._create_radio_action_buttons(
                control.get_actions_for_option(value)
            )
            if button_box is not None:
                hbox.pack_end(button_box, False, False, 0)
            radio.action_buttons = action_buttons

            row.add(hbox)
            listbox.add_row_with_widget(row, radio)

            if value == current_value:
                listbox.select_row(row)

        return first_radio

    def _handle_left_arrow_navigation(self) -> bool:
        """Exits multipage detail or navigates to sidebar on Left arrow."""

        parent: Gtk.Widget | None = self
        while parent is not None:
            if isinstance(parent, PreferencesGridBase) and parent.is_in_multipage_detail():
                parent.multipage_show_categories()
                return True
            parent = parent.get_parent()
        self.focus_sidebar()
        return True

    @staticmethod
    def _navigate_radio_group(radio: Gtk.RadioButton, keyval) -> bool:
        """Handles Up/Down navigation within a radio button group."""

        group = radio.get_group()
        if not group:
            return False

        current_index = -1
        for i, r in enumerate(group):
            if r == radio:
                current_index = i
                break

        if current_index == -1:
            return False

        offset = -1 if keyval == Gdk.KEY_Down else 1
        next_radio = group[(current_index + offset) % len(group)]

        def activate_next():
            next_radio.grab_focus()
            next_radio.set_active(True)
            return False

        GLib.idle_add(activate_next)
        return True

    def _on_radio_key_press(self, radio: Gtk.RadioButton, event) -> bool:
        """Handle navigation keys for radio button group."""

        if event.keyval == Gdk.KEY_Tab and not event.state & Gdk.ModifierType.SHIFT_MASK:
            if isinstance(radio, RadioButtonWithActions) and radio.action_buttons:
                radio.action_buttons[0].grab_focus()
                return True
            return False

        if event.keyval == Gdk.KEY_Left:
            return self._handle_left_arrow_navigation()

        if event.keyval in (Gdk.KEY_Up, Gdk.KEY_Down):
            return self._navigate_radio_group(radio, event.keyval)

        return False

    @staticmethod
    def _get_action_button_context(
        button: Gtk.Button,
    ) -> tuple[RadioButtonWithActions, list[Gtk.Button], int] | None:
        """Returns (radio, action_buttons, current_index) or None if not in a valid context."""

        button_box = button.get_parent()
        hbox = button_box.get_parent() if button_box else None
        if not hbox:
            return None

        radio = None
        for child in hbox.get_children():
            if isinstance(child, RadioButtonWithActions):
                radio = child
                break

        if not radio or not radio.action_buttons:
            return None

        try:
            current_index = radio.action_buttons.index(button)
        except ValueError:
            return None

        return radio, radio.action_buttons, current_index

    def _on_action_button_key_press(self, button: Gtk.Button, event) -> bool:
        """Handle Tab/Shift+Tab navigation for action buttons."""

        if event.keyval == Gdk.KEY_Left:
            return self._handle_left_arrow_navigation()

        context = self._get_action_button_context(button)
        if context is None:
            return False

        radio, action_buttons, current_index = context

        if event.keyval == Gdk.KEY_Tab and not event.state & Gdk.ModifierType.SHIFT_MASK:
            if current_index < len(action_buttons) - 1:
                action_buttons[current_index + 1].grab_focus()
                return True
            return False

        is_shift_tab = event.keyval == Gdk.KEY_ISO_Left_Tab or (
            event.keyval == Gdk.KEY_Tab and (event.state & Gdk.ModifierType.SHIFT_MASK)
        )
        if is_shift_tab:
            if current_index > 0:
                action_buttons[current_index - 1].grab_focus()
            else:
                radio.grab_focus()
            return True

        return False

    def _on_value_changed(self, widget: Gtk.Widget, *_args: Any) -> None:
        """Handle value changes in any widget."""

        # For radio buttons, only process the activation (not deactivation)
        if self._initializing or (isinstance(widget, Gtk.RadioButton) and not widget.get_active()):
            return

        # Check if this is an apply_immediately control
        tracks_changes = True
        if widget in self._widget_to_control_index:
            control = self._controls[self._widget_to_control_index[widget]]
            if isinstance(control, BooleanPreferenceControl) and control.apply_immediately:
                assert isinstance(widget, Gtk.Switch)
                control.setter(widget.get_active())
            elif isinstance(control, IntRangePreferenceControl) and control.apply_immediately:
                assert isinstance(widget, Gtk.SpinButton)
                control.setter(widget.get_value_as_int())
            elif isinstance(control, ColorPreferenceControl):
                assert isinstance(widget, Gtk.ColorButton)
                control.setter(orca_gui_helpers.rgba_to_hex(widget.get_rgba()))
            elif isinstance(control, SelectionPreferenceControl):
                if control.apply_immediately and control.setter is not None:
                    if isinstance(widget, Gtk.ComboBoxText):
                        active = widget.get_active()
                        if active >= 0:
                            value_list = control.values or control.options
                            control.setter(value_list[active])
                    elif isinstance(widget, Gtk.RadioButton):
                        if widget in self._radio_to_selection_value:
                            control.setter(self._radio_to_selection_value[widget])
                tracks_changes = control.tracks_changes

        if tracks_changes:
            self._has_unsaved_changes = True
        self._update_sensitivity()

    def _update_sensitivity(self) -> None:
        """Update widget and row sensitivity based on determine_sensitivity callbacks."""

        sensitivity_cache: dict[int, bool] = {}
        callback_results: dict[Callable[[], bool], bool] = {}
        for i, control in enumerate(self._controls):
            callback = control.determine_sensitivity
            if callback is not None:
                if callback not in callback_results:
                    callback_results[callback] = callback()
                sensitive = callback_results[callback]
                sensitivity_cache[i] = sensitive
                self._widgets[i].set_sensitive(sensitive)
                self._rows[i].set_sensitive(sensitive)

        for group_name, label in self._group_labels.items():
            group_indices = [i for i, c in enumerate(self._controls) if c.member_of == group_name]
            if not group_indices:
                continue

            all_insensitive = all(
                i in sensitivity_cache and not sensitivity_cache[i] for i in group_indices
            )
            label.set_sensitive(not all_insensitive)

    def has_changes(self) -> bool:
        """Return True if the user has made changes that haven't been written to file."""

        for i, control in enumerate(self._controls):
            if isinstance(control, IntRangePreferenceControl):
                widget = self._widgets[i]
                if isinstance(widget, Gtk.SpinButton):
                    widget.update()

        return self._has_unsaved_changes

    def reload(self) -> None:
        """Reload all values from their getters and refresh the UI."""

        self._has_unsaved_changes = False
        self.refresh()

    def _refresh_selection_widget(
        self,
        control: SelectionPreferenceControl,
        widget: Gtk.Widget,
    ) -> None:
        """Refreshes a selection preference widget to reflect its current value."""

        current_value = control.getter()
        if isinstance(widget, Gtk.ComboBoxText):
            value_list = control.values or control.options
            with contextlib.suppress(ValueError):
                widget.set_active(value_list.index(current_value))
        elif isinstance(widget, Gtk.RadioButton):
            for radio in widget.get_group():
                if (
                    radio in self._radio_to_selection_value
                    and self._radio_to_selection_value[radio] == current_value
                ):
                    radio.set_active(True)
                    break

    def _refresh_widget(self, control: ControlType, widget: Gtk.Widget) -> None:
        """Refreshes a single widget to reflect its control's current value."""

        if isinstance(control, BooleanPreferenceControl):
            assert isinstance(widget, Gtk.Switch)
            widget.set_active(control.getter())
        elif isinstance(control, IntRangePreferenceControl):
            assert isinstance(widget, Gtk.SpinButton)
            widget.set_value(control.getter())
        elif isinstance(control, FloatRangePreferenceControl):
            assert isinstance(widget, Gtk.Scale)
            widget.set_value(control.getter())
        elif isinstance(control, EnumPreferenceControl):
            assert isinstance(widget, Gtk.ComboBoxText)
            with contextlib.suppress(ValueError):
                widget.set_active((control.values or control.options).index(control.getter()))
        elif isinstance(control, ColorPreferenceControl):
            assert isinstance(widget, Gtk.ColorButton)
            orca_gui_helpers.set_color_button_hex(widget, control.getter())
        elif isinstance(control, SelectionPreferenceControl):
            self._refresh_selection_widget(control, widget)

    def refresh(self) -> None:
        """Update all widgets to reflect current values."""

        self._initializing = True
        for i, control in enumerate(self._controls):
            self._refresh_widget(control, self._widgets[i])
        self._initializing = False
        self._update_sensitivity()

    def _get_selection_value(
        self,
        control: SelectionPreferenceControl,
        widget: Gtk.Widget,
    ) -> Any:
        """Extracts the current selection value from a combo box or radio group."""

        if isinstance(widget, Gtk.ComboBoxText):
            active = widget.get_active()
            if active >= 0:
                return (control.values or control.options)[active]
        elif isinstance(widget, Gtk.RadioButton):
            for radio in widget.get_group():
                if radio.get_active() and radio in self._radio_to_selection_value:
                    return self._radio_to_selection_value[radio]
        return None

    def _get_widget_value(self, control: ControlType, widget: Gtk.Widget) -> Any:
        """Extracts the current value from a control's widget."""

        if isinstance(control, BooleanPreferenceControl):
            assert isinstance(widget, Gtk.Switch)
            return widget.get_active()
        if isinstance(control, IntRangePreferenceControl):
            assert isinstance(widget, Gtk.SpinButton)
            return int(widget.get_value())
        if isinstance(control, FloatRangePreferenceControl):
            assert isinstance(widget, Gtk.Scale)
            return widget.get_value()
        if isinstance(control, EnumPreferenceControl):
            assert isinstance(widget, Gtk.ComboBoxText)
            active = widget.get_active()
            return (control.values or control.options)[active] if active >= 0 else None
        if isinstance(control, ColorPreferenceControl):
            assert isinstance(widget, Gtk.ColorButton)
            return orca_gui_helpers.rgba_to_hex(widget.get_rgba())
        return (
            self._get_selection_value(control, widget)
            if isinstance(
                control,
                SelectionPreferenceControl,
            )
            else None
        )

    def _get_control_value(self, control: ControlType, default: Any = None) -> Any:
        """Return the current UI value for control, or default if it is not found."""

        for ctrl, widget in zip(self._controls, self._widgets, strict=True):
            if ctrl is control:
                return self._get_widget_value(control, widget)
        return default

    def save_settings(self, profile: str = "", app_name: str = "") -> dict[str, Any]:
        """Save all values using their setters and return a dict of changes."""

        result: dict[str, Any] = {}

        for i, control in enumerate(self._controls):
            value = self._get_widget_value(control, self._widgets[i])
            if value is not None and control.setter is not None:
                control.setter(value)
                if control.prefs_key:
                    result[control.prefs_key] = value

        self._has_unsaved_changes = False
        self._write_gsettings(result, profile, app_name)
        return result

    def _write_gsettings(self, result: dict, profile: str, app_name: str) -> None:
        """Writes this grid's settings to dconf if profile and schema are set."""

        if not profile or not self._gsettings_schema:
            return
        from . import gsettings_registry  # pylint: disable=import-outside-toplevel

        skip_defaults = not app_name and profile == "default"
        gsettings_registry.get_registry().save_schema(
            self._gsettings_schema,
            result,
            profile,
            app_name,
            skip_defaults,
        )
