mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 15:57:56 +01:00
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
# Navigation Deep Dive
|
||||
|
||||
This document covers Terminal.Gui's navigation system, which determines:
|
||||
|
||||
- What are the visual cues that help the user know which element of an application is receiving keyboard and mouse input (which one has focus)?
|
||||
- How does the user change which element of an application has focus?
|
||||
- How does the user change which element of an application has focus?
|
||||
- What are the visual cues that help the user know what keystrokes will change the focus?
|
||||
- What are the visual cues that help the user know what keystrokes will cause action in elements of the application that don't currently have focus?
|
||||
- What is the order in which UI elements are traversed when using keyboard navigation?
|
||||
@@ -24,14 +25,49 @@ See the [Keyboard Tenets](keyboard.md) as they apply as well.
|
||||
|
||||
Tenets higher in the list have precedence over tenets lower in the list.
|
||||
|
||||
* **One Focus Per App** - It should not be possible to have two views be the "most focused" view in an application.
|
||||
* **One Focus Per App** - It should not be possible to have two views be the "most focused" view in an application. There is always exactly one view that is the target of keyboard input.
|
||||
|
||||
* **There's Always a Way With The Keyboard** - The framework strives to ensure users' wanting to use the keyboard can't get into a situation where some element of the application is not accessible via the keyboard. For example, we have unit tests that ensure built-in Views will all have at least one navigation key that advances focus. Another example: As long as a View with a HotKey is visible and enabled, regardless of view-hierarchy, if the user presses that hotkey, the action defined by the hotkey will happen (and, by default the View that defines it will be focused).
|
||||
* **There's Always a Way With The Keyboard** - The framework strives to ensure users wanting to use the keyboard can't get into a situation where some element of the application is not accessible via the keyboard. For example, we have unit tests that ensure built-in Views will all have at least one navigation key that advances focus. Another example: As long as a View with a HotKey is visible and enabled, regardless of view-hierarchy, if the user presses that hotkey, the action defined by the hotkey will happen (and, by default the View that defines it will be focused).
|
||||
|
||||
* **Flexible Overrides** - The framework makes it easy for navigation changes to be made from code and enables changing of behavior to be done in flexible ways. For example a view can be prevented from getting focus by setting `CanFocus` to `false` or overriding `OnHasFocusChanging` and returning `true` to cancel.
|
||||
|
||||
* **Decouple Concepts** - In v1 `CanFocus` is tightly coupled with `HasFocus`, `TabIndex`, `TabIndexes`, and `TabStop` and vice-versa. There was a bunch of "magic" logic that automatically attempted to keep these concepts aligned. This resulted in a poorly specified, hard-to-test, and fragile API. In v2 we strive to keep the related navigation concepts decoupled. For example, `CanFocus` and `TabStop` are decoupled. A view with `CanFocus == true` can have `TabStop == NoStop` and still be focusable with the mouse.
|
||||
|
||||
## Answering the Key Navigation Questions
|
||||
|
||||
### Visual Cues for Focus
|
||||
|
||||
**Current Focus Indicator:**
|
||||
- Views with focus are rendered using their `ColorScheme.Focus` attribute
|
||||
- The focused view may display a cursor (for text input views)
|
||||
- Views in the focus chain (SuperViews of the focused view) also use focused styling
|
||||
|
||||
**Navigation Cues:**
|
||||
- HotKeys are indicated by underlined characters in Labels, Buttons, and MenuItems
|
||||
- Tab order is generally left-to-right, top-to-bottom within containers
|
||||
- Focus indicators (such as highlight rectangles) show which view will receive input
|
||||
|
||||
### Changing Focus
|
||||
|
||||
**Keyboard Methods:**
|
||||
- `Tab` / `Shift+Tab` - Navigate between TabStop views
|
||||
- `F6` / `Shift+F6` - Navigate between TabGroup containers
|
||||
- Arrow keys - Navigate within containers or between adjacent views
|
||||
- HotKeys - Direct navigation to specific views (Alt+letter combinations)
|
||||
- `Enter` / `Space` - Activate the focused view
|
||||
|
||||
**Mouse Methods:**
|
||||
- Click on any focusable view to give it focus
|
||||
- Focus behavior depends on whether the view was previously focused (RestoreFocus vs AdvanceFocus)
|
||||
|
||||
### Navigation Order
|
||||
|
||||
Views are traversed based on their `TabStop` behavior and position in the view hierarchy:
|
||||
|
||||
1. **TabStop Views** - Navigated with Tab/Shift+Tab in layout order
|
||||
2. **TabGroup Views** - Containers navigated with F6/Shift+F6
|
||||
3. **NoStop Views** - Skipped during keyboard navigation but can receive mouse focus
|
||||
|
||||
## Keyboard Navigation
|
||||
|
||||
The majority of the Terminal.Gui Navigation system is dedicated to enabling the keyboard to be used to navigate Views.
|
||||
@@ -47,9 +83,29 @@ Terminal.Gui defines these keys for keyboard navigation:
|
||||
- `Application.NextTabGroupKey` (`Key.F6`) - Navigates to the next view in the view-hierarchy that is a `TabGroup` (see below). If there is no next, the first view that is a `TabGroup` will gain focus.
|
||||
- `Application.PrevTabGroupKey` (`Key.F6.WithShift`) - Opposite of `Application.NextTabGroupKey`.
|
||||
|
||||
`F6` was chosen to match [Windows](https://learn.microsoft.com/en-us/windows/apps/design/input/keyboard-accelerators#common-keyboard-accelerators)
|
||||
`F6` was chosen to match [Windows](https://learn.microsoft.com/en-us/windows/apps/design/input/keyboard-accelerators#common-keyboard-accelerators) conventions.
|
||||
|
||||
These keys are all registered as `KeyBindingScope.Application` key bindings by `Application`. Because application-scoped key bindings have the lowest priority, Views can override the behaviors of these keys (e.g. `TextView` overrides `Key.Tab` by default, enabling the user to enter `\t` into text). The `AllViews_AtLeastOneNavKey_Leaves` unit test ensures all built-in Views have at least one of the above keys that can advance.
|
||||
These keys are all registered as `KeyBindingScope.Application` key bindings by `Application`. Because application-scoped key bindings have the lowest priority, Views can override the behaviors of these keys (e.g. `TextView` overrides `Key.Tab` by default, enabling the user to enter `\t` into text). The `AllViews_AtLeastOneNavKey_Leaves` unit test ensures all built-in Views have at least one of the above keys that can advance focus.
|
||||
|
||||
### Navigation Examples
|
||||
|
||||
```csharp
|
||||
// Basic focus management
|
||||
var button = new Button() { Text = "Click Me", CanFocus = true, TabStop = TabBehavior.TabStop };
|
||||
var textField = new TextField() { Text = "", CanFocus = true, TabStop = TabBehavior.TabStop };
|
||||
|
||||
// Container with group navigation
|
||||
var frameView = new FrameView()
|
||||
{
|
||||
Title = "Options",
|
||||
CanFocus = true,
|
||||
TabStop = TabBehavior.TabGroup
|
||||
};
|
||||
|
||||
// Programmatic focus control
|
||||
button.SetFocus(); // Give focus to specific view
|
||||
Application.Navigation.AdvanceFocus(NavigationDirection.Forward, TabBehavior.TabStop);
|
||||
```
|
||||
|
||||
### HotKeys
|
||||
|
||||
@@ -57,7 +113,16 @@ See also [Keyboard](keyboard.md) where HotKey is covered more deeply...
|
||||
|
||||
`HotKeys` can be used to navigate across the entire application view-hierarchy. They work independently of `Focus`. This enables a user to navigate across a complex UI of nested subviews if needed (even in overlapped scenarios). An example use case is the `AllViewsTester` Scenario.
|
||||
|
||||
Additionally, multiple Views in an application (even within the same SuperView) can have the same HotKey. Each press of the HotKey will invoke the next HotKey across the View hierarchy (NOT IMPLEMENTED YET see https://github.com/gui-cs/Terminal.Gui/issues/3554).
|
||||
HotKeys are defined using the `HotKey` property and are activated using `Alt+` the specified key:
|
||||
|
||||
```csharp
|
||||
var saveButton = new Button() { Text = "_Save", HotKey = Key.S };
|
||||
var exitButton = new Button() { Text = "E_xit", HotKey = Key.X };
|
||||
|
||||
// Alt+S will activate save, Alt+X will activate exit, regardless of current focus
|
||||
```
|
||||
|
||||
Additionally, multiple Views in an application (even within the same SuperView) can have the same HotKey.
|
||||
|
||||
## Mouse Navigation
|
||||
|
||||
@@ -71,19 +136,42 @@ The answer to both questions is:
|
||||
|
||||
If the View was previously focused, the system keeps a record of the SubView that was previously most-focused and restores focus to that SubView (`RestoreFocus()`).
|
||||
|
||||
If the View was not previously focused, `AdvanceFocus()` is called.
|
||||
If the View was not previously focused, `AdvanceFocus()` is called to find the next appropriate focus target.
|
||||
|
||||
For this to work properly, there must be logic that removes the focus-cache used by `RestoreFocus()` if something changes that makes the previously-focusable view not focusable (e.g. if Visible has changed).
|
||||
|
||||
### Mouse Focus Examples
|
||||
|
||||
```csharp
|
||||
// Mouse click behavior
|
||||
view.MouseEvent += (sender, e) =>
|
||||
{
|
||||
if (e.Flags.HasFlag(MouseFlags.Button1Clicked) && view.CanFocus)
|
||||
{
|
||||
view.SetFocus();
|
||||
e.Handled = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Focus on mouse enter (optional behavior)
|
||||
view.MouseEnter += (sender, e) =>
|
||||
{
|
||||
if (view.CanFocus && focusOnHover)
|
||||
{
|
||||
view.SetFocus();
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Application Level Navigation
|
||||
|
||||
At the application level, navigation is encapsulated within the @Terminal.Gui.ApplicationNavigation helper class which is publicly exposed via the @Terminal.Gui.App.Application.Navigation property.
|
||||
|
||||
@Terminal.Gui.App.ApplicationNavigation.GetFocused gets the most-focused View in the application. Will return `null` if there is no view with focus (an extremely rare situation). This replaces `View.MostFocused` in v1.
|
||||
|
||||
The @Terminal.Gui.App.ApplicationNavigation.FocusedChanged and @Terminal.Gui.App.ApplicationNavigation.FocusedChanging events are raised when the most-focused View in the application is changing or has changed. `FocusedChanged` is useful for apps that want to do something with the most-focused view (e.g. see `AdornmentsEditor`). `FocusChanging` is useful apps that want to override what view can be focused across an entire app.
|
||||
The @Terminal.Gui.App.ApplicationNavigation.FocusedChanged and @Terminal.Gui.App.ApplicationNavigation.FocusedChanging events are raised when the most-focused View in the application is changing or has changed. `FocusedChanged` is useful for apps that want to do something with the most-focused view (e.g. see `AdornmentsEditor`). `FocusChanging` is useful for apps that want to override what view can be focused across an entire app.
|
||||
|
||||
The @Terminal.Gui.App.ApplicationNavigation.AdvanceFocus* method causes the focus to advance (forward or backwards) to the next View in the application view-hierarchy, using `behavior` as a filter.
|
||||
The @Terminal.Gui.App.ApplicationNavigation.AdvanceFocus method causes the focus to advance (forward or backwards) to the next View in the application view-hierarchy, using `behavior` as a filter.
|
||||
|
||||
The implementation is simple:
|
||||
|
||||
@@ -95,27 +183,104 @@ This method is called from the `Command` handlers bound to the application-scope
|
||||
|
||||
This method replaces about a dozen functions in v1 (scattered across `Application` and `Toplevel`).
|
||||
|
||||
### Application Navigation Examples
|
||||
|
||||
```csharp
|
||||
// Listen for global focus changes
|
||||
Application.Navigation.FocusedChanged += (sender, e) =>
|
||||
{
|
||||
var focused = Application.Navigation.GetFocused();
|
||||
StatusBar.Text = $"Focused: {focused?.GetType().Name ?? "None"}";
|
||||
};
|
||||
|
||||
// Prevent certain views from getting focus
|
||||
Application.Navigation.FocusedChanging += (sender, e) =>
|
||||
{
|
||||
if (e.NewView is SomeRestrictedView)
|
||||
{
|
||||
e.Cancel = true; // Prevent focus change
|
||||
}
|
||||
};
|
||||
|
||||
// Programmatic navigation
|
||||
Application.Navigation.AdvanceFocus(NavigationDirection.Forward, TabBehavior.TabStop);
|
||||
Application.Navigation.AdvanceFocus(NavigationDirection.Backward, TabBehavior.TabGroup);
|
||||
```
|
||||
|
||||
## View Level Navigation
|
||||
|
||||
@Terminal.Gui.ViewBase.View.AdvanceFocus* is the primary method for developers to cause a view to gain or lose focus.
|
||||
@Terminal.Gui.ViewBase.View.AdvanceFocus is the primary method for developers to cause a view to gain or lose focus.
|
||||
|
||||
Various events are raised when a View's focus is changing. For example, @Terminal.Gui.ViewBase.View.HasFocusChanging and @Terminal.Gui.ViewBase.View.HasFocusChanged.
|
||||
|
||||
### View Focus Management
|
||||
|
||||
```csharp
|
||||
// Basic focus control
|
||||
public class CustomView : View
|
||||
{
|
||||
protected override void OnHasFocusChanging(CancelEventArgs<bool> e)
|
||||
{
|
||||
if (SomeCondition)
|
||||
{
|
||||
e.Cancel = true; // Prevent focus change
|
||||
return;
|
||||
}
|
||||
base.OnHasFocusChanging(e);
|
||||
}
|
||||
|
||||
protected override void OnHasFocusChanged(EventArgs<bool> e)
|
||||
{
|
||||
if (e.CurrentValue)
|
||||
{
|
||||
// View gained focus
|
||||
UpdateAppearance();
|
||||
}
|
||||
base.OnHasFocusChanged(e);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## What makes a View focusable?
|
||||
|
||||
First, only Views that are visible and enabled can gain focus. Both `Visible` and `Enabled` must be `true` for a view to be focusable.
|
||||
|
||||
For visible and enabled Views, the `CanFocus` property is then used to determine whether the `View` is focusable. `CanFocus` must be `true` for a View to gain focus. However, even if `CanFocus` is `true`, other factors can prevent the view from gaining focus...
|
||||
|
||||
A visible, enabled, and `CanFocus == true` view can be focused if the user uses the mouse to clicks on it or if code explicitly calls `View.SetFocus()`. Of course, the view itself or some other code can cancel the focus (e.g. by overriding `OnEnter`).
|
||||
A visible, enabled, and `CanFocus == true` view can be focused if the user uses the mouse to clicks on it or if code explicitly calls `View.SetFocus()`. Of course, the view itself or some other code can cancel the focus (e.g. by overriding `OnHasFocusChanging`).
|
||||
|
||||
For keyboard navigation, the `TabStop` property is a filter for which views are focusable from the current most-focused. `TabStop` has no impact on mouse navigation. `TabStop` is of type `TabBehavior`.
|
||||
|
||||
* `null` - This View is still being initialized; acts as a signal to `set_CanFocus` to set `TabStop` to `TabBehavior.TabStop` as convince for the most common use-case. Equivalent to `TabBehavior.NoStop` when determining if a view is focusable by the keyboard or not.
|
||||
* `TabBehavior.NoStop` - Prevents the user from using keyboard navigation to cause view (and by definition it's subviews) to gain focus. Note: The view can still be focused using code or the mouse.
|
||||
### TabBehavior Values
|
||||
|
||||
* `null` - This View is still being initialized; acts as a signal to `set_CanFocus` to set `TabStop` to `TabBehavior.TabStop` as convenience for the most common use-case. Equivalent to `TabBehavior.NoStop` when determining if a view is focusable by the keyboard or not.
|
||||
|
||||
* `TabBehavior.NoStop` - Prevents the user from using keyboard navigation to cause view (and by definition its subviews) to gain focus. Note: The view can still be focused using code or the mouse.
|
||||
|
||||
* `TabBehavior.TabStop` - Indicates a View is a focusable view with no focusable subviews. `Application.Next/PrevTabStopKey` will advance ONLY through the peer-Views (`SuperView.SubViews`).
|
||||
|
||||
* `TabBehavior.GroupStop` - Indicates a View is a focusable container for other focusable views and enables keyboard navigation across these containers. This applies to both tiled and overlapped views. For example, `FrameView` is a simple view designed to be a visible container of other views tiled scenarios. It has `TabStop` set to `TabBehavior.GroupStop` (and `Arrangement` set to `ViewArrangement.Fixed`). Likewise, `Window` is a simple view designed to be a visible container of other views in overlapped scenarios. It has `TabStop` set to `TabBehavior.GroupStop` (and `Arrangement` set to `ViewArrangement.Movable | ViewArrangement.Resizable | ViewArrangement.Overlapped`). `Application.Next/PrevGroupStopKey` will advance across all `GroupStop` views in the application (unless blocked by a `NoStop` SuperView).
|
||||
* `TabBehavior.TabGroup` - Indicates a View is a focusable container for other focusable views and enables keyboard navigation across these containers. This applies to both tiled and overlapped views. For example, `FrameView` is a simple view designed to be a visible container of other views in tiled scenarios. It has `TabStop` set to `TabBehavior.TabGroup` (and `Arrangement` set to `ViewArrangement.Fixed`). Likewise, `Window` is a simple view designed to be a visible container of other views in overlapped scenarios. It has `TabStop` set to `TabBehavior.TabGroup` (and `Arrangement` set to `ViewArrangement.Movable | ViewArrangement.Resizable | ViewArrangement.Overlapped`). `Application.Next/PrevGroupStopKey` will advance across all `TabGroup` views in the application (unless blocked by a `NoStop` SuperView).
|
||||
|
||||
### Focus Requirements Summary
|
||||
|
||||
For a view to be focusable:
|
||||
|
||||
1. **Visible** = `true`
|
||||
2. **Enabled** = `true`
|
||||
3. **CanFocus** = `true`
|
||||
4. **TabStop** != `TabBehavior.NoStop` (for keyboard navigation only)
|
||||
|
||||
```csharp
|
||||
// Example: Make a view focusable
|
||||
var view = new Label()
|
||||
{
|
||||
Text = "Focusable Label",
|
||||
Visible = true, // Must be visible
|
||||
Enabled = true, // Must be enabled
|
||||
CanFocus = true, // Must be able to focus
|
||||
TabStop = TabBehavior.TabStop // Keyboard navigable
|
||||
};
|
||||
```
|
||||
|
||||
## How To Tell if a View has focus? And which view is the most-focused?
|
||||
|
||||
@@ -123,21 +288,44 @@ For keyboard navigation, the `TabStop` property is a filter for which views are
|
||||
|
||||
Setting this property to `true` has the same effect as calling `View.SetFocus ()`, which also means the focus may not change as a result.
|
||||
|
||||
If `v.HasFocus == true` then
|
||||
If `v.HasFocus == true` then:
|
||||
|
||||
- All views up `v`'s superview-hierarchy must be focusable.
|
||||
- All views up `v`'s superview-hierarchy will also have `HasFocus == true`.
|
||||
- The deepest-subview of `v` that is focusable will also have `HasFocus == true`
|
||||
|
||||
In other words, `v.HasFocus == true` does not necessarily mean `v` is the most-focused view, receiving input. If it has focusable sub-views, one of those (or a further subview) will be the most-focused (`Application.Navigation.Focused`).
|
||||
In other words, `v.HasFocus == true` does not necessarily mean `v` is the most-focused view, receiving input. If it has focusable sub-views, one of those (or a further subview) will be the most-focused (`Application.Navigation.GetFocused()`).
|
||||
|
||||
The `private bool _hasFocus` field backs `HasFocus` and is the ultimate source of truth whether a View has focus or not.
|
||||
|
||||
### Focus Chain Example
|
||||
|
||||
```csharp
|
||||
// In a hierarchy: Window -> Dialog -> Button
|
||||
// If Button has focus, then:
|
||||
window.HasFocus == true // Part of focus chain
|
||||
dialog.HasFocus == true // Part of focus chain
|
||||
button.HasFocus == true // Actually focused
|
||||
|
||||
// Application.Navigation.GetFocused() returns button
|
||||
var mostFocused = Application.Navigation.GetFocused(); // Returns button
|
||||
```
|
||||
|
||||
### How does a user tell?
|
||||
|
||||
In short: `ColorScheme.Focused`.
|
||||
In short: `ColorScheme.Focus` - Views in the focus chain render with focused colors.
|
||||
|
||||
(More needed for HasFocus SuperViews. The current `ColorScheme` design is such that this is awkward. See [Issue #2381](https://github.com/gui-cs/Terminal.Gui/issues/2381#issuecomment-1890814959))
|
||||
Views use their `ColorScheme.Focus` attribute when they are part of the focus chain. This provides visual feedback about which part of the application is active.
|
||||
|
||||
```csharp
|
||||
// Custom focus styling
|
||||
protected override void OnDrawContent(Rectangle viewport)
|
||||
{
|
||||
var attribute = HasFocus ? GetFocusColor() : GetNormalColor();
|
||||
Driver.SetAttribute(attribute);
|
||||
// ... draw content
|
||||
}
|
||||
```
|
||||
|
||||
## How to make a View become focused?
|
||||
|
||||
@@ -145,13 +333,49 @@ The primary `public` method for developers to cause a view to get focus is `View
|
||||
|
||||
Unlike v1, in v2, this method can return `false` if the focus change doesn't happen (e.g. because the view wasn't focusable, or the focus change was cancelled).
|
||||
|
||||
```csharp
|
||||
// Programmatic focus control
|
||||
if (myButton.SetFocus())
|
||||
{
|
||||
Console.WriteLine("Button now has focus");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Could not focus button");
|
||||
}
|
||||
|
||||
// Alternative: Set HasFocus property (same effect)
|
||||
myButton.HasFocus = true;
|
||||
```
|
||||
|
||||
## How to make a View become NOT focused?
|
||||
|
||||
The typical method to make a view lose focus is to have another View gain focus.
|
||||
|
||||
```csharp
|
||||
// Focus another view to remove focus from current
|
||||
otherView.SetFocus();
|
||||
|
||||
// Or advance focus programmatically
|
||||
Application.Navigation.AdvanceFocus(NavigationDirection.Forward, TabBehavior.TabStop);
|
||||
|
||||
// Focus can also be lost when views become non-focusable
|
||||
myView.CanFocus = false; // Will lose focus if it had it
|
||||
myView.Visible = false; // Will lose focus if it had it
|
||||
myView.Enabled = false; // Will lose focus if it had it
|
||||
```
|
||||
|
||||
## Determining the Most Focused SubView
|
||||
|
||||
In v1 `View` had `MostFocused` property that traversed up the view-hierarchy returning the last view found with `HasFocus == true`. In v2, `Application.Focused` provides the same functionality with less overhead.
|
||||
In v1 `View` had `MostFocused` property that traversed up the view-hierarchy returning the last view found with `HasFocus == true`. In v2, `Application.Navigation.GetFocused()` provides the same functionality with less overhead.
|
||||
|
||||
```csharp
|
||||
// v2 way to get the most focused view
|
||||
var focused = Application.Navigation.GetFocused();
|
||||
|
||||
// This replaces the v1 pattern:
|
||||
// var focused = Application.Top.MostFocused;
|
||||
```
|
||||
|
||||
## How Does `View.Add/Remove` Work?
|
||||
|
||||
@@ -161,28 +385,181 @@ Also, in v1, if `view.CanFocus == true`, `Add` would automatically set `TabStop`
|
||||
|
||||
In v2, developers need to explicitly set `CanFocus` for any view in the view-hierarchy where focus is desired. This simplifies the implementation significantly and removes confusing behavior.
|
||||
|
||||
In v2, the automatic setting of `TabStop` in `Add` is retained because it is not overly complex to do so and is a nice convenience for developers to not have to set both `Tabstop` and `CanFocus`. Note we do NOT automatically change `CanFocus` if `TabStop` is changed.
|
||||
In v2, the automatic setting of `TabStop` in `Add` is retained because it is not overly complex to do so and is a nice convenience for developers to not have to set both `TabStop` and `CanFocus`. Note we do NOT automatically change `CanFocus` if `TabStop` is changed.
|
||||
|
||||
```csharp
|
||||
// v2 explicit focus setup
|
||||
var container = new FrameView()
|
||||
{
|
||||
Title = "Container",
|
||||
CanFocus = true, // Must be explicitly set
|
||||
TabStop = TabBehavior.TabGroup
|
||||
};
|
||||
|
||||
var button = new Button()
|
||||
{
|
||||
Text = "Click Me",
|
||||
CanFocus = true, // Must be explicitly set
|
||||
TabStop = TabBehavior.TabStop // Set automatically by Add(), but can override
|
||||
};
|
||||
|
||||
container.Add(button); // Does not automatically set CanFocus on container
|
||||
```
|
||||
|
||||
## Knowing When a View's Focus is Changing
|
||||
|
||||
@Terminal.Gui.ViewBase.View.HasFocusChanging and @Terminal.Gui.ViewBase.View.HasFocusChanged are raised when a View's focus is changing.
|
||||
|
||||
```csharp
|
||||
// Monitor focus changes
|
||||
view.HasFocusChanging += (sender, e) =>
|
||||
{
|
||||
if (e.NewValue && !ValidateCanFocus())
|
||||
{
|
||||
e.Cancel = true; // Prevent gaining focus
|
||||
}
|
||||
};
|
||||
|
||||
view.HasFocusChanged += (sender, e) =>
|
||||
{
|
||||
if (e.CurrentValue)
|
||||
{
|
||||
OnViewGainedFocus();
|
||||
}
|
||||
else
|
||||
{
|
||||
OnViewLostFocus();
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Built-In Views Interactivity
|
||||
|
||||
| | | | | **Keyboard** | | | | **Mouse** | | | | |
|
||||
|----------------|-------------------------|------------|---------------|--------------|-----------------------|------------------------------|---------------------------|------------------------------|------------------------------|------------------------------|----------------|---------------|
|
||||
| | **Number<br>of States** | **Static** | **IsDefault** | **Hotkeys** | **Select<br>Command** | **Accept<br>Command** | **Hotkey<br>Command** | **CanFocus<br>Click** | **CanFocus<br>DblCLick** | **!CanFocus<br>Click** | **RightClick** | **GrabMouse** |
|
||||
| **View** | 1 | Yes | No | 1 | OnSelect | OnAccept | Focus | Focus | | | | No |
|
||||
| **Label** | 1 | Yes | No | 1 | OnSelect | OnAccept | FocusNext | Focus | | FocusNext | | No |
|
||||
| **Button** | 1 | No | Yes | 1 | OnSelect | Focus<br>OnAccept | Focus<br>OnAccept | HotKey | | Select | | No |
|
||||
| **Checkbox** | 3 | No | No | 1 | OnSelect<br>Advance | OnAccept | OnAccept | Select | | Select | | No |
|
||||
| **RadioGroup** | > 1 | No | No | 2+ | Advance | Set SelectedItem<br>OnAccept | Focus<br>Set SelectedItem | SetFocus<br>Set _cursor | | SetFocus<br>Set _cursor | | No |
|
||||
| **Slider** | > 1 | No | No | 1 | SetFocusedOption | SetFocusedOption<br>OnAccept | Focus | SetFocus<br>SetFocusedOption | | SetFocus<br>SetFocusedOption | | Yes |
|
||||
| **ListView** | > 1 | No | No | 1 | MarkUnMarkRow | OpenSelectedItem<br>OnAccept | OnAccept | SetMark<br>OnSelectedChanged | OpenSelectedItem<br>OnAccept | | | No |
|
||||
The following table summarizes how built-in views respond to various input methods:
|
||||
|
||||
## Accessibility Tenets
|
||||
| View | States | Static | Default | HotKeys | Select Cmd | Accept Cmd | HotKey Cmd | Click Focus | DblClick | RightClick | GrabMouse |
|
||||
|------|--------|--------|---------|---------|------------|------------|------------|-------------|----------|------------|-----------|
|
||||
| **View** | 1 | Yes | No | 1 | OnSelect | OnAccept | Focus | Focus | - | - | No |
|
||||
| **Label** | 1 | Yes | No | 1 | OnSelect | OnAccept | FocusNext | Focus | - | FocusNext | No |
|
||||
| **Button** | 1 | No | Yes | 1 | OnSelect | Focus+OnAccept | Focus+OnAccept | HotKey | - | Select | No |
|
||||
| **CheckBox** | 3 | No | No | 1 | OnSelect+Advance | OnAccept | OnAccept | Select | - | Select | No |
|
||||
| **RadioGroup** | >1 | No | No | 2+ | Advance | SetSelected+OnAccept | Focus+SetSelected | SetFocus+SetCursor | - | SetFocus+SetCursor | No |
|
||||
| **Slider** | >1 | No | No | 1 | SetFocusedOption | SetFocusedOption+OnAccept | Focus | SetFocus+SetOption | - | SetFocus+SetOption | Yes |
|
||||
| **ListView** | >1 | No | No | 1 | MarkUnMarkRow | OpenSelected+OnAccept | OnAccept | SetMark+OnSelectedChanged | OpenSelected+OnAccept | - | No |
|
||||
| **TextField** | 1 | No | No | 1 | - | OnAccept | Focus | Focus | SelectAll | ContextMenu | No |
|
||||
| **TextView** | 1 | No | No | 1 | - | OnAccept | Focus | Focus | - | ContextMenu | Yes |
|
||||
|
||||
See https://devblogs.microsoft.com/dotnet/the-journey-to-accessible-apps-keyboard-accessible/
|
||||
### Table Legend
|
||||
|
||||
https://github.com/dotnet/maui/issues/1646
|
||||
- **States**: Number of visual/functional states the view can have
|
||||
- **Static**: Whether the view is primarily for display (non-interactive)
|
||||
- **Default**: Whether the view can be a default button (activated by Enter)
|
||||
- **HotKeys**: Number of hotkeys the view typically supports
|
||||
- **Select Cmd**: What happens when Command.Select is invoked
|
||||
- **Accept Cmd**: What happens when Command.Accept is invoked
|
||||
- **HotKey Cmd**: What happens when the view's hotkey is pressed
|
||||
- **Click Focus**: Behavior when clicked (if CanFocus=true)
|
||||
- **DblClick**: Behavior on double-click
|
||||
- **RightClick**: Behavior on right-click
|
||||
- **GrabMouse**: Whether the view captures mouse for drag operations
|
||||
|
||||
## Common Navigation Patterns
|
||||
|
||||
### Dialog Navigation
|
||||
|
||||
```csharp
|
||||
var dialog = new Dialog()
|
||||
{
|
||||
Title = "Settings",
|
||||
CanFocus = true,
|
||||
TabStop = TabBehavior.TabGroup
|
||||
};
|
||||
|
||||
var okButton = new Button() { Text = "OK", IsDefault = true };
|
||||
var cancelButton = new Button() { Text = "Cancel" };
|
||||
|
||||
// Tab navigates between buttons, Enter activates default
|
||||
dialog.Add(okButton, cancelButton);
|
||||
```
|
||||
|
||||
### Container Navigation
|
||||
|
||||
```csharp
|
||||
var leftPanel = new FrameView()
|
||||
{
|
||||
Title = "Options",
|
||||
TabStop = TabBehavior.TabGroup,
|
||||
X = 0,
|
||||
Width = Dim.Percent(50)
|
||||
};
|
||||
|
||||
var rightPanel = new FrameView()
|
||||
{
|
||||
Title = "Preview",
|
||||
TabStop = TabBehavior.TabGroup,
|
||||
X = Pos.Right(leftPanel),
|
||||
Width = Dim.Fill()
|
||||
};
|
||||
|
||||
// F6 navigates between panels, Tab navigates within panels
|
||||
```
|
||||
|
||||
### List Navigation
|
||||
|
||||
```csharp
|
||||
var listView = new ListView()
|
||||
{
|
||||
CanFocus = true,
|
||||
TabStop = TabBehavior.TabStop
|
||||
};
|
||||
|
||||
// Arrow keys navigate items, Enter selects, Space toggles
|
||||
listView.KeyBindings.Add(Key.CursorUp, Command.Up);
|
||||
listView.KeyBindings.Add(Key.CursorDown, Command.Down);
|
||||
listView.KeyBindings.Add(Key.Enter, Command.Accept);
|
||||
```
|
||||
|
||||
## Accessibility Considerations
|
||||
|
||||
Terminal.Gui's navigation system is designed with accessibility in mind:
|
||||
|
||||
### Keyboard Accessibility
|
||||
- All functionality must be accessible via keyboard
|
||||
- Tab order should be logical and predictable
|
||||
- HotKeys provide direct access to important functions
|
||||
- Arrow keys provide fine-grained navigation within controls
|
||||
|
||||
### Visual Accessibility
|
||||
- Focus indicators must be clearly visible
|
||||
- Color is not the only indicator of focus state
|
||||
- Text and background contrast meets accessibility standards
|
||||
- HotKeys are visually indicated (underlined characters)
|
||||
|
||||
### Screen Reader Support
|
||||
- Focus changes are announced through system events
|
||||
- View titles and labels provide context
|
||||
- Status information is available programmatically
|
||||
|
||||
### Best Practices for Accessible Navigation
|
||||
|
||||
```csharp
|
||||
// Provide meaningful labels
|
||||
var button = new Button() { Text = "_Save Document", HotKey = Key.S };
|
||||
|
||||
// Set logical tab order
|
||||
container.TabStop = TabBehavior.TabGroup;
|
||||
foreach (var view in container.Subviews)
|
||||
{
|
||||
view.TabStop = TabBehavior.TabStop;
|
||||
}
|
||||
|
||||
// Provide keyboard alternatives to mouse actions
|
||||
view.KeyBindings.Add(Key.F10, Command.Context); // Right-click equivalent
|
||||
view.KeyBindings.Add(Key.Space, Command.Select); // Click equivalent
|
||||
```
|
||||
|
||||
For more information on accessibility standards, see:
|
||||
- [Web Content Accessibility Guidelines (WCAG)](https://www.w3.org/WAI/WCAG21/quickref/)
|
||||
- [Microsoft Accessibility Guidelines](https://learn.microsoft.com/en-us/windows/apps/design/accessibility/)
|
||||
- [.NET Accessibility Documentation](https://learn.microsoft.com/en-us/dotnet/desktop/winforms/advanced/walkthrough-creating-an-accessible-windows-based-application)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user