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)