15 KiB
Control Focus Switching and TAB/ALT Handling
Architecture Overview
Control focus management in GacUI operates across three distinct architectural layers, each with specific responsibilities:
Layer 1: Composition-Level Focus (GuiGraphicsComposition)
The lowest layer handles focus at the composition level through GuiGraphicsHost:
GuiGraphicsHostmaintains the currently focused composition in thefocusedCompositionmember variableGuiGraphicsHost::SetFocusInternalis the core focus-switching method that:- Fires
lostFocusevent on the currently focused composition (if any) - Updates
focusedCompositionto the new composition - Resets caret point to (0,0)
- Fires
gotFocusevent on the newly focused composition
- Fires
GuiGraphicsHost::SetFocusvalidates that the composition belongs to this host before callingSetFocusInternalGuiGraphicsHost::ClearFocusremoves focus from any compositionGuiGraphicsHost::GetFocusedCompositionreturns the currently focused composition- Each composition has an event receiver that fires
gotFocusandlostFocusevents
Layer 2: Control-Level Focus (GuiControl)
The middle layer connects controls to composition-level focus:
- Each
GuiControlhas afocusableCompositionproperty (can be null) GuiControl::SetFocusableCompositionattachesgotFocusandlostFocusevent handlers to the composition- When composition gains focus:
GuiControl::OnGotFocusis called, which:- Sets
isFocused = true - Updates control template state
- Fires
FocusedChangedevent
- Sets
- When composition loses focus:
GuiControl::OnLostFocusis called, which:- Sets
isFocused = false - Updates control template state
- Fires
FocusedChangedevent
- Sets
GuiControl::GetFocusedchecks if the control is currently focusedGuiControl::SetFocusedis the public API to focus a control:- Returns early if no focusable composition exists
- Returns early if control is not visually enabled
- Returns early if focusable composition is not eventually visible
- Gets the graphics host from the focusable composition
- Calls
host->SetFocus(focusableComposition)
Layer 3: Focus Loss on State Changes
Focus is automatically cleared when controls undergo certain state changes:
- When disabled:
GuiControl::SetEnabled(false)callsUpdateVisuallyEnabled, which triggersOnLostFocusif the control was focused - When hidden:
GuiControl::SetVisible(false)does NOT automatically clear focus (based on test case comments indicating unclear behavior) - When deleted:
GuiGraphicsHost::DisconnectCompositionInternalis called recursively, clearingfocusedCompositionif it matches the disconnected composition
TAB Key Navigation
Architecture Components
TAB navigation is managed by GuiTabActionManager, which is created and owned by GuiGraphicsHost.
IGuiTabAction Interface
Controls implement IGuiTabAction service to participate in TAB navigation:
GetAcceptTabInput(): Returns whether control accepts TAB character as input (if true, TAB key is passed to control instead of used for navigation)GetTabPriority(): Returns priority for TAB order (-1 = lowest priority, 0, 1, 2... = higher priorities)IsTabEnabled(): Returns whether control can be tabbed to (checks if control is visible and enabled)IsTabAvailable(): Returns whether control is available for TAB navigation (checks if focusable composition exists)
Control List Building (BuildControlList)
The TAB manager builds a prioritized list of focusable controls:
- Uses
tab_focus::CollectControlsto recursively collect all controls from the control host - Groups controls by their
TabPriorityvalue - Inserts controls in priority order: 0, 1, 2, ..., then -1 (default/unprioritized)
- For each control, recursively collects its children and inserts them after the parent
- Result is a flattened list
controlsInOrdersorted by TAB priority - Cache is invalidated when a control's
TabPrioritychanges viaGuiControl::SetTabPriority
Focus Navigation (GetNextFocusControl)
Finding the next focusable control:
- Rebuilds control list if cache is invalidated (
available == false) - Takes current focused control and offset (+1 for forward TAB, -1 for backward SHIFT+TAB)
- Finds start index in the control list
- Wraps around using modulo arithmetic:
(index + offset + count) % count - Loops through controls until finding one that is both
IsTabAvailable()andIsTabEnabled() - Returns the next focusable control or null if none found
Key Event Handling
TAB key processing in GuiTabActionManager::KeyDown:
- Only processes TAB key when CTRL and ALT are NOT pressed
- If focused composition's control has
GetAcceptTabInput() == true, TAB is NOT intercepted (returns false to let control handle it) - Otherwise, calls
GetNextFocusControlwith offset based on SHIFT key state - If next control found: calls
next->SetFocused()and setssupressTabOnce = trueto prevent TAB character insertion - Returns true to indicate key was handled
Character suppression in GuiTabActionManager::Char:
- Suppresses the TAB character (
\t) ifsupressTabOnceis true - Resets
supressTabOnceto false after checking
ALT Key Navigation
Architecture Components
ALT navigation is managed by GuiAltActionManager, which is created and owned by GuiGraphicsHost.
IGuiAltAction Interface
Controls implement IGuiAltAction service to have ALT shortcuts:
GetAlt(): Returns the ALT key string (e.g., "F" for ALT+F)IsAltEnabled(): Returns whether ALT action is enabled (checks visible and enabled state)IsAltAvailable(): Returns whether ALT action is available (checks if focusable composition and alt string exist)GetAltComposition(): Returns the composition to show the ALT key label onGetActivatingAltHost(): Returns the alt host to enter when this action is activated (null = just activate)OnActiveAlt(): Called when ALT key is activated (default implementation callsSetFocused())IGuiAltAction::IsLegalAlt: Validates that ALT string contains only uppercase letters (A-Z) and digits (0-9)
IGuiAltActionContainer Interface
Controls can implement this to provide multiple ALT actions:
GetAltActionCount(): Returns the number of ALT actionsGetAltAction(index): Returns the ALT action at the specified index
IGuiAltActionHost Interface
Hosts create a hierarchy of ALT contexts that can be entered/exited:
GetAltComposition(): Returns the composition for this hostGetPreviousAltHost(): Returns the parent host in the hierarchyOnActivatedAltHost(previousHost)andOnDeactivatedAltHost(): Called when entering/leavingCollectAltActions(actions): Collects all ALT actions for this host
ALT action collection from controls (IGuiAltActionHost::CollectAltActionsFromControl):
- Recursively traverses control tree starting from the specified control
- If control has
IGuiAltActionContainer: collects all actions from it - Else if control has
IGuiAltActionandIsAltAvailable()andIsAltEnabled(): adds single action - Critical behavior: When a control has an ALT action, executes
continuewhich prevents its children from being added to the traversal queue - This creates a "barrier" effect where children are hidden unless you enter a nested ALT context
- Recursively processes all children only if the control doesn't have its own ALT action
ALT Mode Lifecycle
Activation
Pressing ALT (KEY_MENU) without CTRL or SHIFT:
GuiAltActionManager::KeyDownchecks if control host implementsIGuiAltActionHost- Calls
EnterAltHostto enter ALT mode
EnterAltHost process:
- Calls
ClearAltHostto clean up previous state - Calls
host->CollectAltActions(actions)to get all available actions - If no actions available: calls
CloseAltHostand exits - Calls
host->OnActivatedAltHost(currentAltHost)with previous host - Updates
currentAltHostto the new host - Calls
CreateAltTitlesto create visual labels
Visual Label Creation (CreateAltTitles)
Creating ALT key labels:
- For each ALT action key (grouped by alt string):
- If single action and non-empty key: uses key as-is
- If multiple actions with same key: appends numeric suffix (0-999) with zero-padding
- Creates
currentActiveAltActionsdictionary mapping full key (e.g., "F", "F00", "F01") to action
- For each action in
currentActiveAltActions:- Creates a
GuiLabelwithThemeName::ShortcutKeytheme - Sets label text to the key string
- Adds label to the action's composition
- Stores label in
currentActiveAltTitlesdictionary
- Creates a
- Calls
FilterTitlesto update visibility
Key Input Handling (EnterAltKey)
Processing typed characters in ALT mode:
- Appends typed character to
currentAltPrefix - Checks if
currentAltPrefixmatches any action key incurrentActiveAltActions - If no match: calls
FilterTitlesand removes last character if no visible titles remain - If exact match found:
- If action has
GetActivatingAltHost(): callsEnterAltHostto enter nested ALT host - Otherwise: calls
CloseAltHostto exit ALT mode - Calls
action->OnActiveAlt()to activate the action (usually focuses the control) - Sets
supressAltKeyto prevent key from being processed further - Returns true
- If action has
Label Filtering (FilterTitles)
Updating label visibility based on current prefix:
- Iterates through all labels in
currentActiveAltTitles - If label's key starts with
currentAltPrefix: shows label and formats text with brackets around next character (e.g., "F[0]0") - Otherwise: hides label
- Returns count of visible labels
Exiting ALT Mode
Ways to exit ALT mode:
- Press ESCAPE: calls
LeaveAltHostto exit current host and restore previous - Press BACKSPACE: calls
LeaveAltKeyto remove last character from prefix - Clicking or any other key: depends on whether key matches an action
CloseAltHost: clears all state, deletes all labels, and exits all hosts in the hierarchy
Character and Key Suppression
Input suppression while in ALT mode:
GuiAltActionManager::Charreturns true to suppress all character input while in ALT mode or ifsupressAltKeyis setGuiAltActionManager::KeyUpsuppresses the key-up event for the key stored insupressAltKey
Nested ALT Hosts
Why Nested ALT Hosts Are Needed
The continue statement in CollectAltActionsFromControl creates a "barrier" when a control has its own ALT action. This prevents the control's children from being collected at the parent level. Nested ALT hosts provide a mechanism to enter the control's context and collect children's ALT actions at a nested level.
Design Rationales for Custom GetActivatingAltHost Implementations:
-
Non-Child Control Relationships (
GuiMenuButton):- Problem: A menu button's submenu is a separate popup window, NOT a child control
- Default collection only traverses child controls, cannot reach submenu items
- Solution: Returns
subMenu->QueryTypedService<IGuiAltActionHost>()to switch context to submenu window - Result: Pressing ALT+[key] on menu button enters submenu's ALT context
-
Intentional Blocking (
GuiComboBoxBase):- Problem: UX design requires ALT key to just open dropdown, NOT enter nested navigation
- Default would collect ALT actions from dropdown's controls
- Solution: Explicitly returns
GuiSelectableButton::GetActivatingAltHost()(usually null) - Result: ALT key only opens dropdown without entering nested mode
-
Dynamic/Temporary Content (
GuiVirtualDataGrid):- Problem: Cell editor is created on-demand, not a permanent child
- Default collection only sees permanent children, misses temporary editor
- Solution: When
currentEditorexists and has ALT action, callsSetAltComposition(currentEditor->GetTemplate())andSetAltControl(focusControl, true), returnsthis - Result: Can navigate within cell editor using ALT keys
-
Scoped Navigation for Dense Control Groups (
GuiRibbonGroup):- Problem: Ribbon groups contain many buttons, exposing all at window level creates too many conflicts
- Default would collect all buttons at same level as the group (if group didn't have its own ALT action)
- But:
GuiRibbonGrouphas its own ALT action (fromGuiControlbase), which blocks children viacontinue - Solution: Returns
thiswhenIsAltAvailable()is true, creating two-level navigation - Result: First press ALT+[group-key] to enter group, then press ALT+[button-key] to select button
- Benefit: Reduces conflicts and creates logical grouping
-
Optional Scoped Navigation (
GuiDatePicker):- Problem: Date picker displays calendar with many clickable date cells, needs optional nested navigation
- Solution: Returns
thiswhennestedAltconstructor parameter is true - Result: When enabled, pressing ALT+[key] enters calendar's ALT context for date selection
The Critical Role of the continue Barrier
The continue statement in CollectAltActionsFromControl serves as a crucial design element:
- Without nested hosts: When a control has an ALT action, the
continueprevents children from being collected, making them unreachable - With nested hosts:
GetActivatingAltHostprovides a way to "un-hide" children by entering a nested context that re-collects them - For containers like
GuiRibbonGroup: This creates hierarchical navigation instead of flat navigation, reducing ALT key conflicts and improving scalability for interfaces with many controls
Event Flow Integration
Key Event Processing Chain
GuiGraphicsHost::KeyDown processes native window key events in this order:
- First tries
GuiAltActionManager::KeyDown: if ALT manager is active or activates, returns true to consume event - Then tries
GuiTabActionManager::KeyDown: if TAB key is handled, returns true to consume event - Then tries shortcut key manager (if exists)
- Finally delivers to focused composition's event receiver if not consumed
Character Event Processing Chain
GuiGraphicsHost::Char processes character input in this order:
- First tries
GuiTabActionManager::Char: suppresses TAB character if just navigated - Then tries
GuiAltActionManager::Char: suppresses all input while in ALT mode - Finally delivers to focused composition's event receiver if not suppressed
Control Visibility and Enable State
GuiControl::IsControlVisibleAndEnabled determines if a control can receive focus:
- Traverses up parent chain checking both
GetVisible()andGetEnabled() - Used by both
IsAltEnabled()andIsTabEnabled()to determine if control can receive focus via ALT or TAB - Ensures that if a parent is disabled or invisible, all children are excluded from ALT/TAB navigation
Test Coverage
TAB Navigation Tests
TestApplication_Tab.cpp contains comprehensive TAB navigation tests:
TabNavigate: Tests forward TAB through 3 buttons and backward SHIFT+TABTabNavigateWithContainer: Tests TAB through 5 buttons including nested containers (GroupBox)FocusedAndDisable: Tests that disabling focused control clears focusFocusedAndHide: Tests hiding focused control (focus behavior noted as unclear in comment)FocusedAndDelete: Tests deleting focused control clears focus
ALT Focus Test
TestControls_CoreApplication_GuiControl.cpp contains AltFocus test:
- Tests ALT key activation in nested control scenarios