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