Source code for simvx.editor.keyboard_nav

"""Keyboard Navigation — Tab/Shift+Tab panel cycling and arrow-key navigation."""

import logging

from simvx.core.ui.core import Control

log = logging.getLogger(__name__)

[docs] class KeyboardNavigator: """Manages Tab/Shift+Tab focus cycling between editor panels. Panels are registered in visual order (left-to-right, top-to-bottom). Tab advances forward, Shift+Tab moves backward, wrapping at both ends. Arrow key navigation within individual panels (scene tree, inspector) is handled by those panels' own ``_on_gui_input`` methods; this class only manages the *inter-panel* cycling. Usage:: nav = KeyboardNavigator() nav.add_panel(scene_tree_panel) nav.add_panel(viewport_panel) nav.add_panel(inspector_panel) # In the editor's shortcut handler: nav.handle_tab(shift=False) # Tab nav.handle_tab(shift=True) # Shift+Tab """ def __init__(self, panels: list[Control] | None = None): self._panels: list[Control] = list(panels) if panels else [] self._focus_index: int = 0 # ------------------------------------------------------------------ # Panel management # ------------------------------------------------------------------
[docs] def add_panel(self, panel: Control) -> None: """Register a panel for focus cycling.""" if panel not in self._panels: self._panels.append(panel)
[docs] def remove_panel(self, panel: Control) -> None: """Unregister a panel from focus cycling.""" if panel in self._panels: idx = self._panels.index(panel) self._panels.remove(panel) if self._focus_index >= len(self._panels): self._focus_index = max(0, len(self._panels) - 1) elif idx < self._focus_index: self._focus_index = max(0, self._focus_index - 1)
@property def panels(self) -> list[Control]: """Return the ordered list of registered panels.""" return list(self._panels) @property def focus_index(self) -> int: """Return the index of the currently focused panel.""" return self._focus_index @property def focused_panel(self) -> Control | None: """Return the currently focused panel, or None if no panels registered.""" if not self._panels: return None return self._panels[self._focus_index] # ------------------------------------------------------------------ # Focus cycling # ------------------------------------------------------------------
[docs] def handle_tab(self, shift: bool = False) -> bool: """Cycle focus to the next (or previous if shift) panel. Skips invisible panels. Returns True if focus was moved, False if no focusable panel was found. """ if not self._panels: return False n = len(self._panels) direction = -1 if shift else 1 # Try all panels in order, skipping invisible ones for _ in range(n): self._focus_index = (self._focus_index + direction) % n panel = self._panels[self._focus_index] if panel.visible: panel.grab_focus() log.debug("Focus cycled to panel %d: %s", self._focus_index, panel.name) return True return False
[docs] def focus_panel(self, panel: Control) -> bool: """Focus a specific panel by reference. Returns True if the panel was found and focused. """ if panel not in self._panels: return False self._focus_index = self._panels.index(panel) panel.grab_focus() return True
[docs] def focus_panel_by_name(self, name: str) -> bool: """Focus a panel by its node name. Returns True if a panel with that name was found and focused. """ for i, panel in enumerate(self._panels): if panel.name == name: self._focus_index = i panel.grab_focus() return True return False