Source code for frontend.keyboard_nav

"""Keyboard navigation: arrow keys to move focus, Enter to activate."""

from tkinter import Event, TclError
from typing import Any, Callable, Optional, Sequence


[docs] def bind_enter_to_accept( widgets: Sequence[Any], accept_callback: Callable[[], None], ) -> None: """ Bind Enter key events to trigger accept callback on widgets. Binds both <Return> and <KP_Enter> events on each widget to the accept_callback function, so that pressing Enter from input widgets (Spinbox, Entry, Combobox, etc.) triggers the accept action. Args: widgets: Sequence of Tkinter widgets to bind events to. accept_callback: Callback function with signature ``() -> None`` to call when Enter is pressed. """ def _on_enter(_event: Event) -> str: accept_callback() return "break" for w in widgets: w.bind("<Return>", _on_enter) w.bind("<KP_Enter>", _on_enter)
[docs] def setup_arrow_enter_navigation( widgets_grid: Sequence[Sequence[Any]], on_enter: Optional[Callable[[Any, Event], bool]] = None, ) -> None: """ Set up keyboard navigation for a grid of widgets. Binds arrow keys to move focus between widgets in the grid and Return/Enter keys to activate the focused widget. The grid is a 2D list of focusable widgets (e.g. ttk.Button); use None for empty cells. Args: widgets_grid: 2D sequence of widgets arranged in a grid layout. Use ``None`` for empty cells in the grid. on_enter: Optional callback function called when Enter is pressed. Signature: ``on_enter(widget, event) -> bool``. If it returns ``True``, the default behavior (invoke button) is skipped. Use this to handle Enter on non-button widgets (e.g. radiobutton -> confirm dialog). """ grid: dict[tuple[int, int], Any] = {} for r, row in enumerate(widgets_grid): for c, w in enumerate(row): if w is not None: grid[(r, c)] = w def focus_at(nr: int, nc: int) -> None: w = grid.get((nr, nc)) if w is not None: w.focus_set() def move(event: Event, dr: int, dc: int) -> str: current = event.widget for (r, c), w in grid.items(): if w == current: focus_at(r + dr, c + dc) return "break" return "break" def invoke_focused(event: Event) -> str: w = event.widget if on_enter is not None and on_enter(w, event): return "break" invoke = getattr(w, "invoke", None) if callable(invoke): try: invoke() except TclError: pass # widget may be in invalid state (e.g. destroying) return "break" for (r, c), w in grid.items(): w.bind("<Return>", invoke_focused) w.bind("<KP_Enter>", invoke_focused) w.bind("<Left>", lambda e, dr=0, dc=-1: move(e, dr, dc)) w.bind("<Right>", lambda e, dr=0, dc=1: move(e, dr, dc)) w.bind("<Up>", lambda e, dr=-1, dc=0: move(e, dr, dc)) w.bind("<Down>", lambda e, dr=1, dc=0: move(e, dr, dc))