# 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`: - `GuiGraphicsHost` maintains the currently focused composition in the `focusedComposition` member variable - `GuiGraphicsHost::SetFocusInternal` is the core focus-switching method that: - Fires `lostFocus` event on the currently focused composition (if any) - Updates `focusedComposition` to the new composition - Resets caret point to (0,0) - Fires `gotFocus` event on the newly focused composition - `GuiGraphicsHost::SetFocus` validates that the composition belongs to this host before calling `SetFocusInternal` - `GuiGraphicsHost::ClearFocus` removes focus from any composition - `GuiGraphicsHost::GetFocusedComposition` returns the currently focused composition - Each composition has an event receiver that fires `gotFocus` and `lostFocus` events ### Layer 2: Control-Level Focus (`GuiControl`) The middle layer connects controls to composition-level focus: - Each `GuiControl` has a `focusableComposition` property (can be null) - `GuiControl::SetFocusableComposition` attaches `gotFocus` and `lostFocus` event handlers to the composition - When composition gains focus: `GuiControl::OnGotFocus` is called, which: - Sets `isFocused = true` - Updates control template state - Fires `FocusedChanged` event - When composition loses focus: `GuiControl::OnLostFocus` is called, which: - Sets `isFocused = false` - Updates control template state - Fires `FocusedChanged` event - `GuiControl::GetFocused` checks if the control is currently focused - `GuiControl::SetFocused` is 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)` calls `UpdateVisuallyEnabled`, which triggers `OnLostFocus` if 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::DisconnectCompositionInternal` is called recursively, clearing `focusedComposition` if 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::CollectControls` to recursively collect all controls from the control host - Groups controls by their `TabPriority` value - 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 `controlsInOrder` sorted by TAB priority - Cache is invalidated when a control's `TabPriority` changes via `GuiControl::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()` and `IsTabEnabled()` - 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 `GetNextFocusControl` with offset based on SHIFT key state - If next control found: calls `next->SetFocused()` and sets `supressTabOnce = true` to prevent TAB character insertion - Returns true to indicate key was handled Character suppression in `GuiTabActionManager::Char`: - Suppresses the TAB character (`\t`) if `supressTabOnce` is true - Resets `supressTabOnce` to 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 on - `GetActivatingAltHost()`: Returns the alt host to enter when this action is activated (null = just activate) - `OnActiveAlt()`: Called when ALT key is activated (default implementation calls `SetFocused()`) - `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 actions - `GetAltAction(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 host - `GetPreviousAltHost()`: Returns the parent host in the hierarchy - `OnActivatedAltHost(previousHost)` and `OnDeactivatedAltHost()`: Called when entering/leaving - `CollectAltActions(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 `IGuiAltAction` and `IsAltAvailable()` and `IsAltEnabled()`: adds single action - **Critical behavior**: When a control has an ALT action, executes `continue` which 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::KeyDown` checks if control host implements `IGuiAltActionHost` - Calls `EnterAltHost` to enter ALT mode `EnterAltHost` process: - Calls `ClearAltHost` to clean up previous state - Calls `host->CollectAltActions(actions)` to get all available actions - If no actions available: calls `CloseAltHost` and exits - Calls `host->OnActivatedAltHost(currentAltHost)` with previous host - Updates `currentAltHost` to the new host - Calls `CreateAltTitles` to 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 `currentActiveAltActions` dictionary mapping full key (e.g., "F", "F00", "F01") to action - For each action in `currentActiveAltActions`: - Creates a `GuiLabel` with `ThemeName::ShortcutKey` theme - Sets label text to the key string - Adds label to the action's composition - Stores label in `currentActiveAltTitles` dictionary - Calls `FilterTitles` to update visibility #### Key Input Handling (`EnterAltKey`) Processing typed characters in ALT mode: - Appends typed character to `currentAltPrefix` - Checks if `currentAltPrefix` matches any action key in `currentActiveAltActions` - If no match: calls `FilterTitles` and removes last character if no visible titles remain - If exact match found: - If action has `GetActivatingAltHost()`: calls `EnterAltHost` to enter nested ALT host - Otherwise: calls `CloseAltHost` to exit ALT mode - Calls `action->OnActiveAlt()` to activate the action (usually focuses the control) - Sets `supressAltKey` to prevent key from being processed further - Returns true #### 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 `LeaveAltHost` to exit current host and restore previous - Press BACKSPACE: calls `LeaveAltKey` to 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::Char` returns true to suppress all character input while in ALT mode or if `supressAltKey` is set - `GuiAltActionManager::KeyUp` suppresses the key-up event for the key stored in `supressAltKey` ### 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:** 1. **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()` to switch context to submenu window - Result: Pressing ALT+[key] on menu button enters submenu's ALT context 2. **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 3. **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 `currentEditor` exists and has ALT action, calls `SetAltComposition(currentEditor->GetTemplate())` and `SetAltControl(focusControl, true)`, returns `this` - Result: Can navigate within cell editor using ALT keys 4. **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: `GuiRibbonGroup` has its own ALT action (from `GuiControl` base), which blocks children via `continue` - Solution: Returns `this` when `IsAltAvailable()` 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 5. **Optional Scoped Navigation** (`GuiDatePicker`): - Problem: Date picker displays calendar with many clickable date cells, needs optional nested navigation - Solution: Returns `this` when `nestedAlt` constructor 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 `continue` prevents children from being collected, making them unreachable - **With nested hosts**: `GetActivatingAltHost` provides 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: 1. First tries `GuiAltActionManager::KeyDown`: if ALT manager is active or activates, returns true to consume event 2. Then tries `GuiTabActionManager::KeyDown`: if TAB key is handled, returns true to consume event 3. Then tries shortcut key manager (if exists) 4. Finally delivers to focused composition's event receiver if not consumed ### Character Event Processing Chain `GuiGraphicsHost::Char` processes character input in this order: 1. First tries `GuiTabActionManager::Char`: suppresses TAB character if just navigated 2. Then tries `GuiAltActionManager::Char`: suppresses all input while in ALT mode 3. 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()` and `GetEnabled()` - Used by both `IsAltEnabled()` and `IsTabEnabled()` 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+TAB - `TabNavigateWithContainer`: Tests TAB through 5 buttons including nested containers (GroupBox) - `FocusedAndDisable`: Tests that disabling focused control clears focus - `FocusedAndHide`: 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