Source code for simvx.core.ui.navigation

"""Which keys drive UI navigation: the four ``InputMap`` actions and their defaults.

The router never tests a key name of its own. It asks this module whether the key
that arrived is ``ui_focus_next``, ``ui_focus_prev``, ``ui_accept`` or
``ui_cancel``, so navigation is bound the way every other action in the engine is
bound, and an author can move it:

.. code-block:: python

    InputMap.add_action("ui_focus_next", ["ctrl+tab"])   # rebind: Tab is now free
    InputMap.add_action("ui_focus_next")                 # no bindings: unbind entirely

Until an action is registered it falls back to :data:`DEFAULT_UI_BINDINGS`, which
is the whole of the engine-side registration: nothing is written into an
``InputMap``, so the defaults survive the ``InputMap.clear()`` that
``ProjectSettings.apply_input_actions`` performs on every settings-driven launch,
and a project that lists no ``ui_*`` action still navigates.

Matching is exact against the combo the router built from the live modifier state
(``"shift+tab"``), because navigation has to tell Tab from Shift+Tab. Only key
bindings can drive navigation; a gamepad or mouse binding on one of these actions
is ignored here, since the router routes keys.

``ui_accept`` activates the focus owner: the router calls
:meth:`~simvx.core.ui.core.Control.activate` on it, which presses a ``Button``,
toggles a ``CheckBox``, selects a ``RadioButton`` and does nothing anywhere else.
Rebinding the action moves activation with it, and a widget that wants the key
for itself claims it first, so a focused text field keeps Enter for submit.
"""

from __future__ import annotations

from typing import Any

UI_FOCUS_NEXT = "ui_focus_next"
UI_FOCUS_PREV = "ui_focus_prev"
UI_ACCEPT = "ui_accept"
UI_CANCEL = "ui_cancel"

DEFAULT_UI_BINDINGS: dict[str, tuple[str, ...]] = {
    UI_FOCUS_NEXT: ("tab",),
    UI_FOCUS_PREV: ("shift+tab",),
    UI_ACCEPT: ("enter",),
    UI_CANCEL: ("escape",),
}

__all__ = [
    "DEFAULT_UI_BINDINGS",
    "UI_ACCEPT",
    "UI_CANCEL",
    "UI_FOCUS_NEXT",
    "UI_FOCUS_PREV",
    "ui_action_keys",
    "ui_action_matches",
]


[docs] def ui_action_keys(action: str, input_map: Any = None) -> tuple[str, ...]: """Key combos currently bound to ``action``, defaults included. ``input_map`` is the map to consult (a tree's own, normally); the process-wide ``InputMap`` is used when it is None. An action the map does not know falls back to :data:`DEFAULT_UI_BINDINGS`; one it knows answers for itself, so registering it with no bindings disables the key. """ if input_map is None: from ..input.map import InputMap input_map = InputMap if input_map.has_action(action): return tuple(combo for combo in (b.key_combo for b in input_map.get_bindings(action)) if combo) return DEFAULT_UI_BINDINGS.get(action, ())
[docs] def ui_action_matches(action: str, key: str, input_map: Any = None) -> bool: """True when the combo string ``key`` is bound to ``action``.""" return bool(key) and key in ui_action_keys(action, input_map)