Files
GacUI/.github/KnowledgeBase/KB_GacUI_Design_ControlFocusSwitchingAndTabAltHandling.md
2025-10-30 22:39:15 -07:00

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:

  • 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<IGuiAltActionHost>() 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