mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 15:57:56 +01:00
This commit is contained in:
@@ -2,39 +2,100 @@
|
||||
|
||||
The *Cancellable Work Pattern* is a core design pattern in Terminal.Gui, used to structure workflows that can be executed in a default manner, modified by external code or subclasses, or cancelled entirely. This pattern is prevalent across various components of Terminal.Gui, including the `View` class for rendering, keyboard input, and command execution, as well as application-level input handling and property changes. Unlike traditional inheritance-based approaches that rely on overriding virtual methods (which often require subclasses to understand base class implementation details), the *Cancellable Work Pattern* prioritizes events for loose coupling, supplemented by optional virtual methods for flexibility.
|
||||
|
||||
This deep dive defines the *Cancellable Work Pattern*, outlines its components and goals, and illustrates its implementation through examples in `View.Draw`, `View.Keyboard`, `View.Command`, `Application.Keyboard`, and `OrientationHelper`.
|
||||
This document is a conceptual definition of *Cancellable Work Pattern* and outlines its components and goals, and illustrates its implementation through examples in `View.Draw`, `View.Keyboard`, `View.Command`, `Application.Keyboard`, and `OrientationHelper`.
|
||||
|
||||
See the [Events Deep Dive](events.md) for a concrete deep dive and tutorial.
|
||||
|
||||
> [!NOTE]
|
||||
> Some terms in this document are based on a yet-to-be addressed Issue: https://github.com/gui-cs/Terminal.Gui/issues/4050
|
||||
|
||||
## Definition
|
||||
|
||||
The *Cancellable Work Pattern* is a design pattern for executing a structured workflow with one or more phases, where each phase can:
|
||||
|
||||
- Proceed in a default manner.
|
||||
- Be modified by external code or subclasses.
|
||||
- Be cancelled to halt further processing.
|
||||
|
||||
The pattern uses events as the primary mechanism for notification and customization, supplemented by virtual methods for subclassing when needed. It is a specialization of the **Observer Pattern**, extended with structured workflows, explicit cancellation mechanisms, and context-aware notifications. It also incorporates elements of the **Template Method Pattern** (via virtual methods) and **Pipeline Pattern** (via sequential phases).
|
||||
|
||||
## Goals
|
||||
|
||||
The *Cancellable Work Pattern* is designed to achieve the following:
|
||||
1. **Default Execution**: Provide a standard process that executes unless interrupted, ensuring predictable behavior out of the box.
|
||||
2. **Modification**: Allow external code or subclasses to customize specific phases without requiring deep knowledge of the implementation.
|
||||
3. **Cancellation**: Enable halting of a phase or the entire workflow, giving consumers control over the process.
|
||||
4. **Decoupling**: Use events to reduce reliance on inheritance, minimizing the need for subclasses to understand base class details.
|
||||
|
||||
## Lexicon and Taxonomy
|
||||
|
||||
[!INCLUDE [Events Lexicon](~/includes/events-lexicon.md)]
|
||||
|
||||
## Core Concept
|
||||
|
||||
At its core, CWP defines a workflow as a sequence of one or more distinct phases, each representing a unit of work within a larger operation. For each phase, the pattern provides mechanisms to:
|
||||
|
||||
- **Execute Default Behavior**: A predefined implementation that executes if no external intervention occurs, ensuring the system remains functional out of the box.
|
||||
- **Allow Customization**: Through event subscriptions or method overrides, external code or subclasses can inject custom logic to alter the phase's behavior without needing to replace the entire workflow.
|
||||
- **Support Cancellation**: A explicit mechanism to halt the execution of a phase or the entire workflow, preventing further processing when certain conditions are met (e.g., user intervention, error states, or logical constraints).
|
||||
|
||||
This triadic structure—default execution, customization, and cancellation—distinguishes CWP from simpler event-driven or inheritance-based approaches. It ensures that workflows are both robust (via defaults) and flexible (via customization and cancellation), making it ideal for complex systems like terminal user interfaces where multiple stakeholders (e.g., framework developers, application developers, and end-users) interact with the same processes.
|
||||
|
||||
### Structural Components
|
||||
|
||||
The Cancellable Work Pattern typically comprises the following components:
|
||||
|
||||
1. **Workflow Container**: The entity (often a class or object) that encapsulates the overall workflow, defining the sequence of phases and orchestrating their execution. In Terminal.Gui, this might be a `View` object managing rendering or input handling.
|
||||
|
||||
2. **Phases**: Individual steps within the workflow, each representing a discrete unit of work. Each phase has a default implementation and points for intervention. For example, rendering text in a view could be a single phase within a broader drawing workflow.
|
||||
|
||||
3. **Notification Mechanisms**: Events or callbacks that notify external observers of a phase's impending execution, allowing them to intervene. These are typically implemented as delegate-based events (e.g., `DrawingText` event in Terminal.Gui) or virtual methods (e.g., `OnDrawingText`).
|
||||
|
||||
4. **Cancellation Flags**: Boolean indicators or properties within event arguments that signal whether a phase or workflow should be halted. In Terminal.Gui, this is often seen as `Handled` or `Cancel` properties in event args objects.
|
||||
|
||||
5. **Context Objects**: Data structures passed to notification handlers, providing relevant state or parameters about the phase (e.g., `DrawContext` or `Key` objects in Terminal.Gui), enabling informed decision-making by custom logic.
|
||||
|
||||
### Operational Flow
|
||||
|
||||
The operational flow of CWP follows a consistent pattern for each phase within a workflow:
|
||||
|
||||
1. **Pre-Phase Notification**: Before executing a phase's default behavior, the workflow container raises an event or calls a virtual method to notify potential observers or subclasses. This step allows for preemptive customization or cancellation.
|
||||
|
||||
2. **Cancellation Check**: If the notification mechanism indicates cancellation (e.g., a return value of `true` from a virtual method or a `Cancel` flag set in event args), the phase is aborted, and control may return or move to the next phase, depending on the workflow design.
|
||||
|
||||
3. **Default Execution**: If no cancellation occurs, the phase's default behavior is executed. This ensures the workflow progresses as intended in the absence of external intervention.
|
||||
|
||||
4. **Post-Phase Notification (Optional)**: In some implementations, a secondary notification occurs after the phase completes, informing observers of the outcome or updated state (e.g., `OrientationChanged` event after a property update in Terminal.Gui).
|
||||
|
||||
This flow repeats for each phase, allowing granular control over complex operations. Importantly, CWP decouples the workflow's structure from its customization, as external code can subscribe to events without needing to subclass or understand the container's internal logic.
|
||||
|
||||
## Advantages
|
||||
|
||||
- **Flexibility**: Developers can modify specific phases without altering the entire workflow, supporting a wide range of use cases from minor tweaks to complete overrides.
|
||||
|
||||
- **Decoupling**: By prioritizing events over inheritance, CWP reduces tight coupling between base and derived classes, adhering to principles of loose coupling in software design.
|
||||
|
||||
- **Robustness**: Default behaviors ensure the system remains operational even if no customization is provided, reducing the risk of incomplete implementations.
|
||||
|
||||
- **Control**: Cancellation mechanisms provide precise control over workflow execution, critical in interactive systems where user input or state changes may necessitate halting operations.
|
||||
|
||||
## Limitations
|
||||
|
||||
- **Complexity**: Multi-phase workflows can become intricate, especially when numerous events and cancellation points are involved, potentially leading to debugging challenges.
|
||||
|
||||
- **Performance Overhead**: Raising events and checking cancellation flags for each phase introduces minor performance costs, which may accumulate in high-frequency operations like rendering.
|
||||
|
||||
- **Learning Curve**: Understanding the pattern's structure and knowing when to use events versus overrides requires familiarity, which may pose a barrier to novice developers.
|
||||
|
||||
## Applicability
|
||||
|
||||
CWP is particularly suited to domains where workflows must balance standardization with adaptability, such as user interface frameworks (e.g., Terminal.Gui), game engines, or workflow automation systems. It excels in scenarios where operations are inherently interruptible—such as responding to user input, rendering dynamic content, or managing state transitions—and where multiple components or developers need to collaborate on the same process without tight dependencies.
|
||||
|
||||
In the context of Terminal.Gui, CWP underpins critical functionalities like view rendering, keyboard input processing, command execution, and property change handling, ensuring that these operations are both predictable by default and customizable as needed by application developers.
|
||||
|
||||
## Implementation in Terminal.Gui
|
||||
|
||||
The *Cancellable Work Pattern* is implemented consistently across several key areas of Terminal.Gui’s `v2_develop` branch. Below are five primary examples, each illustrating the pattern in a different domain: rendering, keyboard input at the view level, command execution, application-level keyboard input, and property changes.
|
||||
The *Cancellable Work Pattern* is implemented consistently across several key areas of Terminal.Gui's `v2_develop` branch. Below are five primary examples, each illustrating the pattern in a different domain: rendering, keyboard input at the view level, command execution, application-level keyboard input, and property changes.
|
||||
|
||||
### 1. View.Draw: Rendering Workflow
|
||||
|
||||
The `View.Draw` method orchestrates the rendering of a view, including its adornments (margin, border, padding), viewport, text, content, subviews, and line canvas. It is a multi-phase workflow where each phase can be customized or cancelled.
|
||||
|
||||
#### Example: `DoDrawText`
|
||||
The `DoDrawText` method, responsible for drawing a view’s text, exemplifies the pattern:
|
||||
|
||||
The `DoDrawText` method, responsible for drawing a view's text, exemplifies the pattern:
|
||||
```csharp
|
||||
private void DoDrawText(DrawContext? context = null)
|
||||
{
|
||||
@@ -55,20 +116,22 @@ private void DoDrawText(DrawContext? context = null)
|
||||
DrawText(context); // Default behavior
|
||||
}
|
||||
```
|
||||
|
||||
- **Workflow**: Single phase for text drawing within the broader `Draw` workflow.
|
||||
- **Notifications**: `OnDrawingText` (virtual), `DrawingText` (event).
|
||||
- **Cancellation**: `OnDrawingText` returning `true` or `dev.Cancel = true`.
|
||||
- **Context**: `DrawContext` and `DrawEventArgs` provide rendering details.
|
||||
- **Default Behavior**: `DrawText` renders the view’s text.
|
||||
- **Default Behavior**: `DrawText` renders the view's text.
|
||||
- **Use Case**: Allows customization of text rendering (e.g., custom formatting) or cancellation (e.g., skipping text for performance).
|
||||
|
||||
### 2. View.Keyboard: View-Level Keyboard Input
|
||||
|
||||
The `View.ProcessKeyDown` method processes keyboard input for a view, mapping keys to commands or handling them directly. It is a linear workflow with a single phase per key event.
|
||||
The `View.NewKeyDownEvent` method processes keyboard input for a view, mapping keys to commands or handling them directly. It is a linear workflow with a single phase per key event.
|
||||
|
||||
#### Example: `NewKeyDownEvent`
|
||||
|
||||
#### Example: `ProcessKeyDown`
|
||||
```csharp
|
||||
public virtual bool ProcessKeyDown(Key key)
|
||||
public bool NewKeyDownEvent(Key key)
|
||||
{
|
||||
if (OnKeyDown(key)) // Virtual method
|
||||
{
|
||||
@@ -94,28 +157,30 @@ public virtual bool ProcessKeyDown(Key key)
|
||||
|
||||
The `View.Command` APIs execute commands like `Command.Activate` and `Command.Accept`, used for state changes (e.g., `CheckBox` toggling) and action confirmation (e.g., dialog submission). It is a per-unit workflow, with one phase per command.
|
||||
|
||||
#### Example: `RaiseActivating`
|
||||
The `RaiseActivating` method handles `Command.Activate`:
|
||||
#### Example: `RaiseAccepting`
|
||||
|
||||
```csharp
|
||||
protected bool? RaiseActivating(ICommandContext? ctx)
|
||||
protected bool? RaiseAccepting(ICommandContext? ctx)
|
||||
{
|
||||
CommandEventArgs args = new() { Context = ctx };
|
||||
if (OnActivating(args) || args.Handled)
|
||||
if (OnAccepting(args) || args.Handled)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
Activating?.Invoke(this, args);
|
||||
return Activating is null ? null : args.Handled;
|
||||
Accepting?.Invoke(this, args);
|
||||
return Accepting is null ? null : args.Handled;
|
||||
}
|
||||
```
|
||||
- **Workflow**: Single phase for `Command.Activate`.
|
||||
- **Notifications**: `OnActivating` (virtual), `Activating` (event).
|
||||
- **Cancellation**: `OnActivating` returning `true` or `args.Handled = true`.
|
||||
|
||||
- **Workflow**: Single phase for `Command.Accept`.
|
||||
- **Notifications**: `OnAccepting` (virtual), `Accepting` (event).
|
||||
- **Cancellation**: `OnAccepting` returning `true` or `args.Handled = true`.
|
||||
- **Context**: `ICommandContext` provides `Command`, `Source`, and `Binding`.
|
||||
- **Default Behavior**: `SetFocus` for `Command.Activate` (in `SetupCommands`).
|
||||
- **Default Behavior**: Propagates to `SuperView` or default button if not handled.
|
||||
- **Use Case**: Allows customization of state changes (e.g., `CheckBox` toggling) or cancellation (e.g., preventing focus in `MenuItemv2`).
|
||||
|
||||
#### Propagation Challenge
|
||||
|
||||
- `Command.Activate` is local, limiting hierarchical coordination (e.g., `MenuBarv2` popovers). A proposed `PropagatedCommands` property addresses this, as detailed in the appendix.
|
||||
|
||||
### 4. Application.Keyboard: Application-Level Keyboard Input
|
||||
@@ -123,6 +188,7 @@ protected bool? RaiseActivating(ICommandContext? ctx)
|
||||
The `Application.OnKeyDown` method processes application-wide keyboard input, raising events for global key handling. It is an event-driven workflow, with a single phase per key event.
|
||||
|
||||
#### Example: `OnKeyDown`
|
||||
|
||||
```csharp
|
||||
public static bool OnKeyDown(Key key)
|
||||
{
|
||||
@@ -134,6 +200,7 @@ public static bool OnKeyDown(Key key)
|
||||
return key.Handled; // Check for cancellation
|
||||
}
|
||||
```
|
||||
|
||||
- **Workflow**: Event-driven, processing one key event.
|
||||
- **Notifications**: `KeyDown` (event, no virtual method).
|
||||
- **Cancellation**: `key.Handled = true`.
|
||||
@@ -146,6 +213,7 @@ public static bool OnKeyDown(Key key)
|
||||
The `OrientationHelper` class manages orientation changes (e.g., in `StackPanel`), raising events for property updates. It is an event-driven workflow, with a single phase per change.
|
||||
|
||||
#### Example: `Orientation` Setter
|
||||
|
||||
```csharp
|
||||
public Orientation Orientation
|
||||
{
|
||||
@@ -174,6 +242,7 @@ public Orientation Orientation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Workflow**: Event-driven, processing one property change.
|
||||
- **Notifications**: `OnOrientationChanging` (virtual), `OrientationChanging` (event), `OnOrientationChanged`, `OrientationChanged` (post-event).
|
||||
- **Cancellation**: `OnOrientationChanging` returning `true` or `args.Cancel = true`.
|
||||
@@ -181,58 +250,3 @@ public Orientation Orientation
|
||||
- **Default Behavior**: Updates `_orientation` and notifies via `OrientationChanged`.
|
||||
- **Use Case**: Allows customization of orientation changes (e.g., adjusting layout) or cancellation (e.g., rejecting invalid orientations).
|
||||
|
||||
## Proposed Enhancement: Command Propagation
|
||||
|
||||
The *Cancellable Work Pattern* in `View.Command` currently supports local `Command.Activate` and propagating `Command.Accept`. To address hierarchical coordination needs (e.g., `MenuBarv2` popovers, `Dialog` closing), a `PropagatedCommands` property is proposed (Issue #4050):
|
||||
|
||||
- **Change**: Add `IReadOnlyList<Command> PropagatedCommands` to `View`, defaulting to `[Command.Accept]`. `Raise*` methods propagate if the command is in `SuperView?.PropagatedCommands` and `args.Handled` is `false`.
|
||||
- **Example**:
|
||||
```csharp
|
||||
public IReadOnlyList<Command> PropagatedCommands { get; set; } = new List<Command> { Command.Accept };
|
||||
protected bool? RaiseActivating(ICommandContext? ctx)
|
||||
{
|
||||
CommandEventArgs args = new() { Context = ctx };
|
||||
if (OnActivating(args) || args.Handled)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
Activating?.Invoke(this, args);
|
||||
if (!args.Handled && SuperView?.PropagatedCommands.Contains(Command.Activate) == true)
|
||||
{
|
||||
return SuperView.InvokeCommand(Command.Activate, ctx);
|
||||
}
|
||||
return Activating is null ? null : args.Handled;
|
||||
}
|
||||
```
|
||||
- **Impact**: Enables `Command.Activate` propagation for `MenuBarv2` while preserving `Command.Accept` propagation, maintaining decoupling and avoiding noise from irrelevant commands.
|
||||
|
||||
## Challenges and Recommendations
|
||||
|
||||
1. **Conflation in FlagSelector**:
|
||||
- **Issue**: `CheckBox.Activating` triggers `Accepting`, conflating state change and confirmation.
|
||||
- **Recommendation**: Refactor to separate `Activating` and `Accepting`:
|
||||
```csharp
|
||||
checkbox.Activating += (sender, args) =>
|
||||
{
|
||||
if (RaiseActivating(args.Context) is true)
|
||||
{
|
||||
args.Handled = true;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
2. **Propagation Limitations**:
|
||||
- **Issue**: Local `Command.Activate` restricts `MenuBarv2` coordination; `Command.Accept` uses hacks (#3925).
|
||||
- **Recommendation**: Adopt `PropagatedCommands` to enable targeted propagation, as proposed.
|
||||
|
||||
3. **Documentation Gaps**:
|
||||
- **Issue**: The pattern’s phases and `Handled` semantics are not fully documented.
|
||||
- **Recommendation**: Document the pattern’s structure, phases, and examples across `View.Draw`, `View.Keyboard`, `View.Command`, `Application.Keyboard`, and `OrientationHelper`.
|
||||
|
||||
4. **Complexity in Multi-Phase Workflows**:
|
||||
- **Issue**: `View.Draw`’s multi-phase workflow can be complex for developers to customize.
|
||||
- **Recommendation**: Provide clearer phase-specific documentation and examples.
|
||||
|
||||
## Conclusion
|
||||
|
||||
The *Cancellable Work Pattern* is a foundational design in Terminal.Gui, enabling extensible, cancellable, and decoupled workflows across rendering, input handling, command execution, and property changes. Its implementation in `View.Draw`, `View.Keyboard`, `View.Command`, `Application.Keyboard`, and `OrientationHelper` supports diverse use cases, from `Menuv2`’s hierarchical menus to `CheckBox`’s state toggling. The proposed `PropagatedCommands` property enhances the pattern’s applicability in `View.Command`, addressing propagation needs while maintaining its core principles. By refining implementation flaws (e.g., `FlagSelector`) and improving documentation, Terminal.Gui can further leverage this pattern for robust, flexible UI interactions.
|
||||
@@ -7,26 +7,25 @@
|
||||
|
||||
## Overview
|
||||
|
||||
The `Dim.Auto` type is a type of `Dim` that automatically sizes the view based on its content. This is useful when you want to size a view based on the content it contains. That content can either be the `Text`, the `SubViews`, or something else defined by the view.
|
||||
The `Dim.Auto` type is a specialized `Dim` class in Terminal.Gui v2 that enables automatic sizing of a `View` based on its content. This is particularly useful for dynamically sizing views to accommodate varying content such as text, subviews, or explicitly set content areas. Unlike other `Dim` types like `Dim.Absolute` or `Dim.Fill`, `Dim.Auto` calculates dimensions at runtime based on specified criteria, making it ideal for responsive UI design in terminal applications.
|
||||
|
||||
Like all `Dim` types, `Dim.Auto` is used to set the `Width` or `Height` of a view and can be combined with other `Dim` types using addition or subtraction (see. `DimCombine`).
|
||||
Like all `Dim` types, `Dim.Auto` is used to set the `Width` or `Height` of a view and can be combined with other `Dim` types using addition or subtraction (see `DimCombine`).
|
||||
|
||||
The `DimAutoStyle` enum defines the different ways that `Dim.Auto` can be used to size a view. The `DimAutoStyle` enum has the following values:
|
||||
The `DimAutoStyle` enum defines the different strategies that `Dim.Auto` can employ to size a view. The `DimAutoStyle` enum has the following values:
|
||||
|
||||
* `Text` - The view is sized based on the `Text` property and `TextFormatter` settings.
|
||||
* `Content` - The view is sized based on either the value returned by `View.SetContentSize()` or the `Subviews` property. If the content size is not explicitly set (via `View.SetContentSize()`), the view is sized based on the Subview with the largest relvant dimension plus location. If the content size is explicitly set, the view is sized based on the value returned by `View.SetContentSize()`.
|
||||
* `Auto` - The view is sized based on both the text and content, whichever is larger.
|
||||
- **Text**: The view is sized based on the `Text` property and `TextFormatter` settings. This considers the formatted text dimensions, constrained by any specified maximum dimensions.
|
||||
- **Content**: The view is sized based on either the value returned by `View.GetContentSize()` or the `Subviews` property. If the content size is explicitly set (via `View.SetContentSize()`), the view is sized based on that value. Otherwise, it considers the subview with the largest relevant dimension plus its position.
|
||||
- **Auto**: The view is sized based on both the text and content, whichever results in the larger dimension.
|
||||
|
||||
## Using Dim.Auto
|
||||
|
||||
`Dim.Auto` is defined as:
|
||||
|
||||
```cs
|
||||
public static Dim Auto (DimAutoStyle style = DimAutoStyle.Auto, Dim minimumContentDim = null, Dim max = null)
|
||||
public static Dim Auto (DimAutoStyle style = DimAutoStyle.Auto, Dim minimumContentDim = null, Dim maximumContentDim = null)
|
||||
```
|
||||
|
||||
To use `Dim.Auto`, set the `Width` or `Height` property of a view to `Dim.Auto (DimAutoStyle.Text)` or `Dim.Auto (DimAutoStyle.Content)`.
|
||||
|
||||
To use `Dim.Auto`, set the `Width` or `Height` property of a view to `Dim.Auto(DimAutoStyle.Text)`, `Dim.Auto(DimAutoStyle.Content)`, or `Dim.Auto(DimAutoStyle.Auto)`.
|
||||
|
||||
For example, to create a `View` that is sized based on the `Text` property, you can do this:
|
||||
|
||||
@@ -34,8 +33,8 @@ For example, to create a `View` that is sized based on the `Text` property, you
|
||||
View view = new ()
|
||||
{
|
||||
Text = "Hello, World!",
|
||||
Width = Dim.Auto (DimAutoStyle.Text),
|
||||
Height = Dim.Auto (DimAutoStyle.Text),
|
||||
Width = Dim.Auto(DimAutoStyle.Text),
|
||||
Height = Dim.Auto(DimAutoStyle.Text),
|
||||
};
|
||||
```
|
||||
|
||||
@@ -46,15 +45,15 @@ To create a `View` that is sized based on its `Subviews`, you can do this:
|
||||
```cs
|
||||
View view = new ()
|
||||
{
|
||||
Width = Dim.Auto (DimAutoStyle.Content),
|
||||
Height = Dim.Auto (DimAutoStyle.Content),
|
||||
Width = Dim.Auto(DimAutoStyle.Content),
|
||||
Height = Dim.Auto(DimAutoStyle.Content),
|
||||
};
|
||||
view.Add (new Label () { Text = "Hello, World!" });
|
||||
view.Add(new Label() { Text = "Hello, World!" });
|
||||
```
|
||||
|
||||
In this example, the `View` will be sized based on the size of the `Label` that is added to it.
|
||||
|
||||
### Specifying a miniumum size
|
||||
### Specifying a Minimum Size
|
||||
|
||||
You can specify a minimum size by passing a `Dim` object to the `minimumContentDim` parameter. For example, to create a `View` that is sized based on the `Text` property, but has a minimum width of 10 columns, you can do this:
|
||||
|
||||
@@ -62,8 +61,8 @@ You can specify a minimum size by passing a `Dim` object to the `minimumContentD
|
||||
View view = new ()
|
||||
{
|
||||
Text = "Hello, World!",
|
||||
Width = Dim.Auto (DimAutoStyle.Text, minimumContentDim: Dim.Absolute (10)), // Same as `minimumContentDim: 10`
|
||||
Height = Dim.Auto (DimAutoStyle.Text),
|
||||
Width = Dim.Auto(DimAutoStyle.Text, minimumContentDim: Dim.Absolute(10)), // Same as `minimumContentDim: 10`
|
||||
Height = Dim.Auto(DimAutoStyle.Text),
|
||||
};
|
||||
```
|
||||
|
||||
@@ -72,71 +71,120 @@ Sometimes it's useful to have the minimum size be dynamic. Use `Dim.Func` as fol
|
||||
```cs
|
||||
View view = new ()
|
||||
{
|
||||
Width = Dim.Auto (DimAutoStyle.Content, minimumContentDim: Dim.Func (GetDynamicMinSize)),
|
||||
Height = Dim.Auto (DimAutoStyle.Text),
|
||||
Width = Dim.Auto(DimAutoStyle.Content, minimumContentDim: Dim.Func(GetDynamicMinSize)),
|
||||
Height = Dim.Auto(DimAutoStyle.Text),
|
||||
};
|
||||
|
||||
int GetDynamicMinSize ()
|
||||
int GetDynamicMinSize()
|
||||
{
|
||||
return someDynamicInt;
|
||||
}
|
||||
```
|
||||
|
||||
### Specifying a maximum size
|
||||
### Specifying a Maximum Size
|
||||
|
||||
It is common to want to constrain how large a View can be sized. The `maximumContentDim` parameter to the `Dim.Auto ()` method enables this. Like `minimumContentDim` it is of type `Dim` and thus can represent a dynamic value. For example, by default `Dialog` specifies `maximumContentDim` as `Dim.Percent (90)` to ensure a Dialog box is never larger than 90% of the screen.
|
||||
It is common to want to constrain how large a View can be sized. The `maximumContentDim` parameter to the `Dim.Auto()` method enables this. Like `minimumContentDim`, it is of type `Dim` and thus can represent a dynamic value. For example, by default, `Dialog` specifies `maximumContentDim` as `Dim.Percent(90)` to ensure a dialog box is never larger than 90% of the screen.
|
||||
|
||||
```cs
|
||||
View dialog = new ()
|
||||
{
|
||||
Width = Dim.Auto(DimAutoStyle.Content, maximumContentDim: Dim.Percent(90)),
|
||||
Height = Dim.Auto(DimAutoStyle.Content, maximumContentDim: Dim.Percent(90)),
|
||||
};
|
||||
```
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Calculation Logic
|
||||
|
||||
The `Dim.Auto` class calculates dimensions dynamically during the layout process. Here's how it works under the hood, based on the codebase analysis:
|
||||
|
||||
- **Text-Based Sizing (`DimAutoStyle.Text`)**: When using `Text` style, the dimension is determined by the formatted text size as computed by `TextFormatter`. For width, it uses `ConstrainToWidth`, and for height, it uses `ConstrainToHeight`. These values are set based on the formatted text size, constrained by any maximum dimensions provided.
|
||||
- **Content-Based Sizing (`DimAutoStyle.Content`)**: For `Content` style, if `ContentSizeTracksViewport` is `false` and there are no subviews, it uses the explicitly set content size from `GetContentSize()`. Otherwise, it iterates through subviews to calculate the maximum dimension needed based on their positions and sizes.
|
||||
- **Auto Sizing (`DimAutoStyle.Auto`)**: This combines both `Text` and `Content` strategies, taking the larger of the two calculated dimensions.
|
||||
|
||||
The calculation in `DimAuto.Calculate` method also respects `minimumContentDim` and `maximumContentDim`:
|
||||
- The final size is at least the minimum specified (if any), and at most the maximum specified (if any).
|
||||
- Adornments (like margins, borders, and padding) are added to the calculated content size to ensure the view's frame includes these visual elements.
|
||||
|
||||
### Handling Subviews
|
||||
|
||||
When sizing based on subviews, `Dim.Auto` employs a sophisticated approach to handle dependencies:
|
||||
- It categorizes subviews based on their `Pos` and `Dim` types to manage layout dependencies. For instance, it processes subviews with absolute positions and dimensions first, then handles more complex cases like `PosAnchorEnd` or `DimView`.
|
||||
- This ensures that views dependent on other views' sizes or positions are calculated correctly, avoiding circular dependencies and ensuring accurate sizing.
|
||||
|
||||
### Adornments Consideration
|
||||
|
||||
The size calculation includes the thickness of adornments (margin, border, padding) to ensure the view's total frame size accounts for these elements. This is evident in the code where `adornmentThickness` is added to the computed content size.
|
||||
|
||||
## Limitations
|
||||
|
||||
`Dim.Auto` is not always the best choice for sizing a view. For example, if you want a view to fill the entire width of the Superview, you should use `Dim.Fill ()` instead of `Dim.Auto (DimAutoStyle.Content)`.
|
||||
`Dim.Auto` is not always the best choice for sizing a view. Consider the following limitations:
|
||||
|
||||
`Dim.Auto` is also not always the most efficient way to size a view. If you know the size of the content ahead of time, you can set the `Width` and `Height` properties to `Dim.Absolute (n)` instead of using `Dim.Auto`.
|
||||
- **Performance Overhead**: `Dim.Auto` can introduce performance overhead due to the dynamic calculation of sizes, especially with many subviews or complex text formatting. If the size is known and static, `Dim.Absolute(n)` might be more efficient.
|
||||
- **Not Suitable for Full-Screen Layouts**: If you want a view to fill the entire width or height of the superview, `Dim.Fill()` is more appropriate than `Dim.Auto(DimAutoStyle.Content)` as it directly uses the superview's dimensions without content-based calculations.
|
||||
- **Dependency Complexity**: When subviews themselves use `Dim.Auto` or other dependent `Dim` types, the layout process can become complex and may require multiple iterations to stabilize, potentially leading to unexpected results if not carefully managed.
|
||||
|
||||
## Behavior of other Pos/Dim Types when used within a Dim.Auto-sized View
|
||||
## Behavior of Other Pos/Dim Types When Used Within a Dim.Auto-Sized View
|
||||
|
||||
The table below descibes the behavior of the various Pos/Dim types when used by subviews of a View that uses `Dim.Auto` for it's `Width` or `Height`:
|
||||
The table below describes the behavior of various `Pos` and `Dim` types when used by subviews of a view that uses `Dim.Auto` for its `Width` or `Height`. This reflects how these types influence the automatic sizing:
|
||||
|
||||
| Type | Impacts Dimension | Notes |
|
||||
|-------------|-------------------|---------------------------------------------------------------------------------------------------------|
|
||||
| PosAlign | Yes | The subviews with the same `GroupId` will be aligned at the maximimum dimension to enable them to not be clipped. This dimension plus the group's position will determine the minimum `Dim.Auto` dimension. |
|
||||
| PosView | Yes | The position plus the dimension of `subview.Target` will determine the minimum `Dim.Auto` dimension. |
|
||||
| PosCombine | Yes | <needs clarification> |
|
||||
| PosAnchorEnd| Yes | The `Dim.Auto` dimension will be increased by the dimension of the subview. |
|
||||
| PosCenter | No | |
|
||||
| PosPercent | No | |
|
||||
| PosAbsolute | Yes | |
|
||||
| PosFunc | Yes | |
|
||||
| DimView | Yes | The position plus the dimension of `subview.Target` will determine the minimum `Dim.Auto` dimension. |
|
||||
| DimCombine | Yes | <needs clarification> |
|
||||
| DimFill | No | |
|
||||
| DimPercent | No | |
|
||||
| DimAuto | Yes | |
|
||||
| DimAbsolute | Yes | |
|
||||
| DimFunc | Yes | <needs clarification> |
|
||||
| Type | Impacts Dimension | Notes |
|
||||
|---------------|-------------------|---------------------------------------------------------------------------------------------------|
|
||||
| **PosAlign** | Yes | The subviews with the same `GroupId` will be aligned at the maximum dimension to enable them to not be clipped. This dimension plus the group's position will determine the minimum `Dim.Auto` dimension. |
|
||||
| **PosView** | Yes | The position plus the dimension of `subview.Target` will determine the minimum `Dim.Auto` dimension. |
|
||||
| **PosCombine**| Yes | Impacts dimension if it includes a `Pos` type that affects dimension (like `PosView` or `PosAnchorEnd`). |
|
||||
| **PosAnchorEnd**| Yes | The `Dim.Auto` dimension will be increased by the dimension of the subview to accommodate its anchored position. |
|
||||
| **PosCenter** | No | Does not impact the dimension as it centers based on superview size, not content. |
|
||||
| **PosPercent**| No | Does not impact dimension unless combined with other impacting types; based on superview size. |
|
||||
| **PosAbsolute**| Yes | Impacts dimension if the absolute position plus subview dimension exceeds current content size. |
|
||||
| **PosFunc** | Yes | Impacts dimension if the function returns a value that, combined with subview dimension, exceeds content size. |
|
||||
| **DimView** | Yes | The dimension of `subview.Target` will contribute to the minimum `Dim.Auto` dimension. |
|
||||
| **DimCombine**| Yes | Impacts dimension if it includes a `Dim` type that affects dimension (like `DimView` or `DimAuto`). |
|
||||
| **DimFill** | No | Does not impact dimension as it fills remaining space, not contributing to content-based sizing. |
|
||||
| **DimPercent**| No | Does not impact dimension as it is based on superview size, not content. |
|
||||
| **DimAuto** | Yes | Contributes to dimension based on its own content or text sizing, potentially increasing the superview's size. |
|
||||
| **DimAbsolute**| Yes | Impacts dimension if the absolute size plus position exceeds current content size. |
|
||||
| **DimFunc** | Yes | Impacts dimension if the function returns a size that, combined with position, exceeds content size. |
|
||||
|
||||
## Building Dim.Auto Friendly Views
|
||||
|
||||
## Building Dim.Auto friendly View
|
||||
It is common to build view classes that have a natural size based on their content. For example, the `Label` class is sized based on the `Text` property.
|
||||
|
||||
It is common to build View classes that have a natrual size based on their content. For example, the `Label` class is a view that is sized based on the `Text` property.
|
||||
`Slider` is a good example of a sophisticated `Dim.Auto`-friendly view. Developers using these views shouldn't need to know the details of how the view is sized; they should just be able to use the view and have it size itself correctly.
|
||||
|
||||
`Slider` is a good example of sophsticated Dim.Auto friendly view.
|
||||
|
||||
Developers using these views shouldn't need to know the details of how the view is sized, they should just be able to use the view and have it size itself correctly.
|
||||
|
||||
For example, a vertical `Slider` with 3 options may be created like this: which is size based on the number of options it has, it's orientation, etc...
|
||||
For example, a vertical `Slider` with 3 options may be created like this, sized based on the number of options, its orientation, etc.:
|
||||
|
||||
```cs
|
||||
List<object> options = new () { "Option 1", "Option 2", "Option 3" };
|
||||
Slider slider = new (options)
|
||||
List<object> options = new() { "Option 1", "Option 2", "Option 3" };
|
||||
Slider slider = new(options)
|
||||
{
|
||||
Orientation = Orientation.Vertical,
|
||||
Type = SliderType.Multiple,
|
||||
};
|
||||
view.Add (slider);
|
||||
view.Add(slider);
|
||||
```
|
||||
|
||||
Note the developer does not need to specify the size of the `Slider`, it will size itself based on the number of options and the orientation.
|
||||
Note the developer does not need to specify the size of the `Slider`; it will size itself based on the number of options and the orientation.
|
||||
|
||||
Views like `Slider` do this by setting `Width` and `Height` to `Dim.Auto (DimAutoStyle.Content)` in the constructor and calling `SetContentSize()` whenever the desired content size changes. The View will then be sized to be big enough to fit the content.
|
||||
Views like `Slider` achieve this by setting `Width` and `Height` to `Dim.Auto(DimAutoStyle.Content)` in the constructor and calling `SetContentSize()` whenever the desired content size changes. The view will then be sized to be big enough to fit the content.
|
||||
|
||||
Views that use `Text` for their content can just set `Width` and `Height` to `Dim.Auto (DimAutoStyle.Text)`. It is recommended to use `Height = Dim.Auto (DimAutoStyle.Text, minimumContentDim: 1)` to ensure the View can show at least one line of text.
|
||||
Views that use `Text` for their content can set `Width` and `Height` to `Dim.Auto(DimAutoStyle.Text)`. It is recommended to use `Height = Dim.Auto(DimAutoStyle.Text, minimumContentDim: 1)` to ensure the view can show at least one line of text.
|
||||
|
||||
### Best Practices for Custom Views
|
||||
|
||||
- **Set Appropriate DimAutoStyle**: Choose `Text`, `Content`, or `Auto` based on what drives the view's size. Use `Text` for text-driven views like labels, `Content` for container-like views with subviews or explicit content sizes, and `Auto` for mixed content.
|
||||
- **Update Content Size Dynamically**: If your view's content changes (e.g., text updates or subviews are added/removed), call `SetContentSize()` or ensure properties like `Text` are updated to trigger re-layout.
|
||||
- **Consider Minimum and Maximum Constraints**: Use `minimumContentDim` to prevent views from becoming too small to be usable, and `maximumContentDim` to prevent them from growing excessively large, especially in constrained terminal environments.
|
||||
- **Handle Adornments**: Be aware that `Dim.Auto` accounts for adornments in its sizing. If your view has custom adornments, ensure they are properly factored into the layout by the base `View` class.
|
||||
|
||||
## Debugging Dim.Auto Issues
|
||||
|
||||
If you encounter unexpected sizing with `Dim.Auto`, consider the following debugging steps based on the codebase's diagnostic capabilities:
|
||||
|
||||
- **Enable Validation**: Set `ValidatePosDim` to `true` on the view to enable runtime validation of `Pos` and `Dim` settings. This will throw exceptions if invalid configurations are detected, helping identify issues like circular dependencies or negative sizes.
|
||||
- **Check Content Size**: Verify if `ContentSizeTracksViewport` is behaving as expected. If set to `false`, ensure `SetContentSize()` is called with the correct dimensions. Use logging to track `GetContentSize()` outputs.
|
||||
- **Review Subview Dependencies**: Look for subviews with `Pos` or `Dim` types that impact dimension (like `PosAnchorEnd` or `DimView`). Ensure their target views are laid out before the current view to avoid incorrect sizing.
|
||||
- **Inspect Text Formatting**: For `Text` style, check `TextFormatter` settings and constraints (`ConstrainToWidth`, `ConstrainToHeight`). Ensure text is formatted correctly before sizing calculations.
|
||||
|
||||
By understanding the intricacies of `Dim.Auto` as implemented in Terminal.Gui v2, developers can create responsive and adaptive terminal UIs that automatically adjust to content changes, enhancing user experience and maintainability.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Terminal.Gui Event Deep Dive
|
||||
|
||||
Terminal.Gui exposes and uses events in many places. This deep dive covers the patterns used, where they are used, and notes any exceptions.
|
||||
This document provides a comprehensive overview of how events work in Terminal.Gui. For the conceptual overview of the Cancellable Work Pattern, see [Cancellable Work Pattern](cancellable-work-pattern.md).
|
||||
|
||||
## See Also
|
||||
|
||||
@@ -8,16 +8,334 @@ Terminal.Gui exposes and uses events in many places. This deep dive covers the p
|
||||
* [Command Deep Dive](command.md)
|
||||
* [Lexicon & Taxonomy](lexicon.md)
|
||||
|
||||
## Tenets for Terminal.Gui Events (Unless you know better ones...)
|
||||
|
||||
Tenets higher in the list have precedence over tenets lower in the list.
|
||||
|
||||
* **UI Interaction and Live Data Are Different Beasts** - TG distinguishes between events used for human interaction and events for live data. We don't believe in a one-size-fits-all eventing model. For UI interactions we use `EventHandler`. For data binding we think `INotifyPropertyChanged` is groovy. For some callbacks we use `Action<T>`.
|
||||
|
||||
## Lexicon and Taxonomy
|
||||
|
||||
[!INCLUDE [Events Lexicon](~/includes/events-lexicon.md)]
|
||||
|
||||
## Event Categories
|
||||
|
||||
Terminal.Gui uses several types of events:
|
||||
|
||||
1. **UI Interaction Events**: Events triggered by user input (keyboard, mouse)
|
||||
2. **View Lifecycle Events**: Events related to view creation, activation, and disposal
|
||||
3. **Property Change Events**: Events for property value changes
|
||||
4. **Drawing Events**: Events related to view rendering
|
||||
5. **Command Events**: Events for command execution and workflow control
|
||||
|
||||
## Event Patterns
|
||||
|
||||
### 1. Cancellable Work Pattern (CWP)
|
||||
|
||||
The [Cancellable Work Pattern (CWP)](cancellable-work-pattern.md) is a core pattern in Terminal.Gui that provides a consistent way to handle cancellable operations. An "event" has two components:
|
||||
|
||||
1. **Virtual Method**: `protected virtual OnMethod()` that can be overridden in a subclass so the subclass can participate
|
||||
2. **Event**: `public event EventHandler<>` that allows external subscribers to participate
|
||||
|
||||
The virtual method is called first, letting subclasses have priority. Then the event is invoked.
|
||||
|
||||
Optional CWP Helper Classes are provided to provide consistency.
|
||||
|
||||
#### Manual CWP Implementation
|
||||
|
||||
The basic CWP pattern combines a protected virtual method with a public event:
|
||||
|
||||
```csharp
|
||||
public class MyView : View
|
||||
{
|
||||
// Public event
|
||||
public event EventHandler<MouseEventArgs>? MouseEvent;
|
||||
|
||||
// Protected virtual method
|
||||
protected virtual bool OnMouseEvent(MouseEventArgs args)
|
||||
{
|
||||
// Return true to handle the event and stop propagation
|
||||
return false;
|
||||
}
|
||||
|
||||
// Internal method to raise the event
|
||||
internal bool RaiseMouseEvent(MouseEventArgs args)
|
||||
{
|
||||
// Call virtual method first
|
||||
if (OnMouseEvent(args) || args.Handled)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Then raise the event
|
||||
MouseEvent?.Invoke(this, args);
|
||||
return args.Handled;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### CWP with Helper Classes
|
||||
|
||||
Terminal.Gui provides static helper classes to implement CWP:
|
||||
|
||||
#### Property Changes
|
||||
|
||||
For property changes, use `CWPPropertyHelper.ChangeProperty`:
|
||||
|
||||
```csharp
|
||||
public class MyView : View
|
||||
{
|
||||
private string _text = string.Empty;
|
||||
public event EventHandler<ValueChangingEventArgs<string>>? TextChanging;
|
||||
public event EventHandler<ValueChangedEventArgs<string>>? TextChanged;
|
||||
|
||||
public string Text
|
||||
{
|
||||
get => _text;
|
||||
set
|
||||
{
|
||||
if (CWPPropertyHelper.ChangeProperty(
|
||||
currentValue: _text,
|
||||
newValue: value,
|
||||
onChanging: args => OnTextChanging(args),
|
||||
changingEvent: TextChanging,
|
||||
onChanged: args => OnTextChanged(args),
|
||||
changedEvent: TextChanged,
|
||||
out string finalValue))
|
||||
{
|
||||
_text = finalValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Virtual method called before the change
|
||||
protected virtual bool OnTextChanging(ValueChangingEventArgs<string> args)
|
||||
{
|
||||
// Return true to cancel the change
|
||||
return false;
|
||||
}
|
||||
|
||||
// Virtual method called after the change
|
||||
protected virtual void OnTextChanged(ValueChangedEventArgs<string> args)
|
||||
{
|
||||
// React to the change
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Workflows
|
||||
|
||||
For general workflows, use `CWPWorkflowHelper`:
|
||||
|
||||
```csharp
|
||||
public class MyView : View
|
||||
{
|
||||
public bool? ExecuteWorkflow()
|
||||
{
|
||||
ResultEventArgs<bool> args = new();
|
||||
return CWPWorkflowHelper.Execute(
|
||||
onMethod: args => OnExecuting(args),
|
||||
eventHandler: Executing,
|
||||
args: args,
|
||||
defaultAction: () =>
|
||||
{
|
||||
// Main execution logic
|
||||
DoWork();
|
||||
args.Result = true;
|
||||
});
|
||||
}
|
||||
|
||||
// Virtual method called before execution
|
||||
protected virtual bool OnExecuting(ResultEventArgs<bool> args)
|
||||
{
|
||||
// Return true to cancel execution
|
||||
return false;
|
||||
}
|
||||
|
||||
public event EventHandler<ResultEventArgs<bool>>? Executing;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Action Callbacks
|
||||
|
||||
For simple callbacks without cancellation, use `Action`. For example, in `Shortcut`:
|
||||
|
||||
```csharp
|
||||
public class Shortcut : View
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the action to be invoked when the shortcut key is pressed or the shortcut is clicked on with the
|
||||
/// mouse.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note, the <see cref="View.Accepting"/> event is fired first, and if cancelled, the event will not be invoked.
|
||||
/// </remarks>
|
||||
public Action? Action { get; set; }
|
||||
|
||||
internal virtual bool? DispatchCommand(ICommandContext? commandContext)
|
||||
{
|
||||
bool cancel = base.DispatchCommand(commandContext) == true;
|
||||
|
||||
if (cancel)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Action is { })
|
||||
{
|
||||
Logging.Debug($"{Title} ({commandContext?.Source?.Title}) - Invoke Action...");
|
||||
Action.Invoke();
|
||||
|
||||
// Assume if there's a subscriber to Action, it's handled.
|
||||
cancel = true;
|
||||
}
|
||||
|
||||
return cancel;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Property Change Notifications
|
||||
|
||||
For property change notifications, implement `INotifyPropertyChanged`. For example, in `Aligner`:
|
||||
|
||||
```csharp
|
||||
public class Aligner : INotifyPropertyChanged
|
||||
{
|
||||
private Alignment _alignment;
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public Alignment Alignment
|
||||
{
|
||||
get => _alignment;
|
||||
set
|
||||
{
|
||||
_alignment = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Alignment)));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Event Propagation
|
||||
|
||||
Events in Terminal.Gui often propagate through the view hierarchy. For example, in `Button`, the `Selecting` and `Accepting` events are raised as part of the command handling process:
|
||||
|
||||
```csharp
|
||||
private bool? HandleHotKeyCommand (ICommandContext commandContext)
|
||||
{
|
||||
bool cachedIsDefault = IsDefault; // Supports "Swap Default" in Buttons scenario where IsDefault changes
|
||||
|
||||
if (RaiseSelecting (commandContext) is true)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool? handled = RaiseAccepting (commandContext);
|
||||
|
||||
if (handled == true)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
SetFocus ();
|
||||
|
||||
// If Accept was not handled...
|
||||
if (cachedIsDefault && SuperView is { })
|
||||
{
|
||||
return SuperView.InvokeCommand (Command.Accept);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
This example shows how `Button` first raises the `Selecting` event, and if not canceled, proceeds to raise the `Accepting` event. If `Accepting` is not handled and the button is the default, it invokes the `Accept` command on the `SuperView`, demonstrating event propagation up the view hierarchy.
|
||||
|
||||
## Event Context
|
||||
|
||||
### Event Arguments
|
||||
|
||||
Terminal.Gui provides rich context through event arguments. For example, `CommandEventArgs`:
|
||||
|
||||
```csharp
|
||||
public class CommandEventArgs : EventArgs
|
||||
{
|
||||
public ICommandContext? Context { get; set; }
|
||||
public bool Handled { get; set; }
|
||||
public bool Cancel { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### Command Context
|
||||
|
||||
Command execution includes context through `ICommandContext`:
|
||||
|
||||
```csharp
|
||||
public interface ICommandContext
|
||||
{
|
||||
View Source { get; }
|
||||
object? Parameter { get; }
|
||||
IDictionary<string, object> State { get; }
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Event Naming**:
|
||||
- Use past tense for completed events (e.g., `Clicked`, `Changed`)
|
||||
- Use present tense for ongoing events (e.g., `Clicking`, `Changing`)
|
||||
- Use "ing" suffix for cancellable events
|
||||
|
||||
2. **Event Handler Implementation**:
|
||||
- Keep handlers short and focused
|
||||
- Use async/await for long-running tasks
|
||||
- Unsubscribe from events in Dispose
|
||||
- Use weak event patterns for long-lived subscriptions
|
||||
|
||||
3. **Event Context**:
|
||||
- Provide rich context in event args
|
||||
- Include source view and binding details
|
||||
- Add view-specific state when needed
|
||||
|
||||
4. **Event Propagation**:
|
||||
- Use appropriate propagation mechanisms
|
||||
- Avoid unnecessary event bubbling
|
||||
- Consider using `PropagatedCommands` for hierarchical views
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Memory Leaks**:
|
||||
```csharp
|
||||
// BAD: Potential memory leak
|
||||
view.Activating += OnActivating;
|
||||
|
||||
// GOOD: Unsubscribe in Dispose
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
view.Activating -= OnActivating;
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
```
|
||||
|
||||
2. **Incorrect Event Cancellation**:
|
||||
```csharp
|
||||
// BAD: Using Cancel for event handling
|
||||
args.Cancel = true; // Wrong for MouseEventArgs
|
||||
|
||||
// GOOD: Using Handled for event handling
|
||||
args.Handled = true; // Correct for MouseEventArgs
|
||||
|
||||
// GOOD: Using Cancel for operation cancellation
|
||||
args.Cancel = true; // Correct for CancelEventArgs
|
||||
```
|
||||
|
||||
3. **Missing Context**:
|
||||
```csharp
|
||||
// BAD: Missing context
|
||||
Activating?.Invoke(this, new CommandEventArgs());
|
||||
|
||||
// GOOD: Including context
|
||||
Activating?.Invoke(this, new CommandEventArgs { Context = ctx });
|
||||
```
|
||||
|
||||
## Useful External Documentation
|
||||
|
||||
* [.NET Naming Guidelines - Names of Events](https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/names-of-type-members?redirectedfrom=MSDN#names-of-events)
|
||||
@@ -28,28 +346,52 @@ Tenets higher in the list have precedence over tenets lower in the list.
|
||||
|
||||
TG follows the *naming* advice provided in [.NET Naming Guidelines - Names of Events](https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/names-of-type-members?redirectedfrom=MSDN#names-of-events).
|
||||
|
||||
## Common Event Patterns
|
||||
## Known Issues
|
||||
|
||||
### Cancellable Work Pattern
|
||||
### Proposed Enhancement: Command Propagation
|
||||
|
||||
The [Cancellable Work Pattern](cancellable-work-pattern.md) is a pattern that allows for the cancellation of work.
|
||||
The *Cancellable Work Pattern* in `View.Command` currently supports local `Command.Activate` and propagating `Command.Accept`. To address hierarchical coordination needs (e.g., `MenuBarv2` popovers, `Dialog` closing), a `PropagatedCommands` property is proposed (Issue #4050):
|
||||
|
||||
### OnEvent/Event
|
||||
- **Change**: Add `IReadOnlyList<Command> PropagatedCommands` to `View`, defaulting to `[Command.Accept]`. `Raise*` methods propagate if the command is in `SuperView?.PropagatedCommands` and `args.Handled` is `false`.
|
||||
- **Example**:
|
||||
|
||||
The primary pattern for events is the `OnEvent/Event` idiom.
|
||||
```csharp
|
||||
public IReadOnlyList<Command> PropagatedCommands { get; set; } = new List<Command> { Command.Accept };
|
||||
protected bool? RaiseAccepting(ICommandContext? ctx)
|
||||
{
|
||||
CommandEventArgs args = new() { Context = ctx };
|
||||
if (OnAccepting(args) || args.Handled)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
Accepting?.Invoke(this, args);
|
||||
if (!args.Handled && SuperView?.PropagatedCommands.Contains(Command.Accept) == true)
|
||||
{
|
||||
return SuperView.InvokeCommand(Command.Accept, ctx);
|
||||
}
|
||||
return Accepting is null ? null : args.Handled;
|
||||
}
|
||||
```
|
||||
|
||||
* Implement a helper method for raising the event: `RaisexxxEvent`.
|
||||
* If the event is cancelable, the return type should be either `bool` or `bool?`.
|
||||
* Can be `private`, `internal`, or `public` depending on the situation. `internal` should only be used to enable unit tests.
|
||||
* Raising an event involves FIRST calling the `protected virtual` method, THEN invoking
|
||||
the `EventHandler`.
|
||||
- **Impact**: Enables `Command.Activate` propagation for `MenuBarv2` while preserving `Command.Accept` propagation, maintaining decoupling and avoiding noise from irrelevant commands.
|
||||
|
||||
### Action
|
||||
|
||||
We use the `Action<T>` idiom sparingly.
|
||||
|
||||
### INotifyPropertyChanged
|
||||
|
||||
We support `INotifyPropertyChanged` in cases where data binding is relevant.
|
||||
### **Conflation in FlagSelector**:
|
||||
- **Issue**: `CheckBox.Activating` triggers `Accepting`, conflating state change and confirmation.
|
||||
- **Recommendation**: Refactor to separate `Activating` and `Accepting`:
|
||||
```csharp
|
||||
checkbox.Activating += (sender, args) =>
|
||||
{
|
||||
if (RaiseAccepting(args.Context) is true)
|
||||
{
|
||||
args.Handled = true;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### **Propagation Limitations**:
|
||||
- **Issue**: Local `Command.Activate` restricts `MenuBarv2` coordination; `Command.Accept` uses hacks (#3925).
|
||||
- **Recommendation**: Adopt `PropagatedCommands` to enable targeted propagation, as proposed.
|
||||
|
||||
### **Complexity in Multi-Phase Workflows**:
|
||||
- **Issue**: `View.Draw`'s multi-phase workflow can be complex for developers to customize.
|
||||
- **Recommendation**: Provide clearer phase-specific documentation and examples.
|
||||
|
||||
@@ -13,31 +13,54 @@ Tenets higher in the list have precedence over tenets lower in the list.
|
||||
|
||||
* **Keyboard Required; Mouse Optional** - Terminal users expect full functionality without having to pick up the mouse. At the same time they love being able to use the mouse when it makes sense to do so. We strive to ensure anything that can be done with the keyboard is also possible with the mouse. We avoid features that are only useable with the mouse.
|
||||
|
||||
* **Be Consistent With the User's Platform** - Users get to choose the platform they run *Terminal.Gui* apps on and those apps should respond to mouse input in a way that is consistent with the platform. For example, on Windows ???
|
||||
* **Be Consistent With the User's Platform** - Users get to choose the platform they run *Terminal.Gui* apps on and those apps should respond to mouse input in a way that is consistent with the platform. For example, on Windows, right-click typically shows context menus, double-click activates items, and the mouse wheel scrolls content. On other platforms, Terminal.Gui respects the platform's conventions for mouse interactions.
|
||||
|
||||
## Mouse APIs
|
||||
|
||||
*Terminal.Gui* provides the following APIs for handling mouse input:
|
||||
|
||||
* **MouseEventArgs** - @Terminal.Gui.Input.MouseEventArgs provides a platform-independent abstraction for common mouse operations. It is used for processing mouse input and raising mouse events.
|
||||
* **Mouse Bindings** - Mouse Bindings provide a declarative method for handling mouse input in View implementations. The View calls Terminal.Gui.ViewBase.View.AddCommand* to declare it supports a particular command and then uses @Terminal.Gui.Input.MouseBindings to indicate which mouse events will invoke the command.
|
||||
* **Mouse Events** - The Mouse Bindings API is rich enough to support the majority of use-cases. However, in some cases subscribing directly to key events is needed (e.g. drag & drop). Use @Terminal.Gui.ViewBase.View.MouseEvent and related events in these cases.
|
||||
|
||||
* **Mouse Bindings** - Mouse Bindings provide a declarative method for handling mouse input in View implementations. The View calls @Terminal.Gui.ViewBase.View.AddCommand to declare it supports a particular command and then uses @Terminal.Gui.Input.MouseBindings to indicate which mouse events will invoke the command.
|
||||
|
||||
* **Mouse Events** - The Mouse Bindings API is rich enough to support the majority of use-cases. However, in some cases subscribing directly to mouse events is needed (e.g. drag & drop). Use @Terminal.Gui.ViewBase.View.MouseEvent and related events in these cases.
|
||||
|
||||
* **Mouse State** - @Terminal.Gui.ViewBase.View.MouseState provides an abstraction for the current state of the mouse, enabling views to do interesting things like change their appearance based on the mouse state.
|
||||
|
||||
Each of these APIs are described more fully below.
|
||||
|
||||
## Mouse Bindings
|
||||
|
||||
Mouse Bindings is the preferred way of handling mouse input in View implementations. The View calls Terminal.Gui.ViewBase.View.AddCommand* to declare it supports a particular command and then uses @Terminal.Gui.Input.MouseBindings to indicate which mouse events will invoke the command. For example, if a View wants to respond to the user using the mouse wheel to scroll up, it would do this:
|
||||
Mouse Bindings is the preferred way of handling mouse input in View implementations. The View calls @Terminal.Gui.ViewBase.View.AddCommand to declare it supports a particular command and then uses @Terminal.Gui.Input.MouseBindings to indicate which mouse events will invoke the command. For example, if a View wants to respond to the user using the mouse wheel to scroll up, it would do this:
|
||||
|
||||
```cs
|
||||
public MyView : View
|
||||
public class MyView : View
|
||||
{
|
||||
AddCommand (Command.ScrollUp, () => ScrollVertical (-1));
|
||||
MouseBindings.Add (MouseFlags.Button1DoubleClick, Command.ScrollUp);
|
||||
public MyView()
|
||||
{
|
||||
AddCommand (Command.ScrollUp, () => ScrollVertical (-1));
|
||||
MouseBindings.Add (MouseFlags.WheelUp, Command.ScrollUp);
|
||||
|
||||
AddCommand (Command.ScrollDown, () => ScrollVertical (1));
|
||||
MouseBindings.Add (MouseFlags.WheelDown, Command.ScrollDown);
|
||||
|
||||
AddCommand (Command.Select, () => SelectItem());
|
||||
MouseBindings.Add (MouseFlags.Button1Clicked, Command.Select);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The [Command](~/api/Terminal.Gui.Input.Command.yml) enum lists generic operations that are implemented by views.
|
||||
The @Terminal.Gui.Input.Command enum lists generic operations that are implemented by views.
|
||||
|
||||
### Common Mouse Bindings
|
||||
|
||||
Here are some common mouse binding patterns used throughout Terminal.Gui:
|
||||
|
||||
* **Click Events**: `MouseFlags.Button1Clicked` for primary selection/activation
|
||||
* **Double-Click Events**: `MouseFlags.Button1DoubleClicked` for default actions (like opening/accepting)
|
||||
* **Right-Click Events**: `MouseFlags.Button3Clicked` for context menus
|
||||
* **Scroll Events**: `MouseFlags.WheelUp` and `MouseFlags.WheelDown` for scrolling content
|
||||
* **Drag Events**: `MouseFlags.Button1Pressed` combined with mouse move tracking for drag operations
|
||||
|
||||
## Mouse Events
|
||||
|
||||
@@ -45,19 +68,153 @@ At the core of *Terminal.Gui*'s mouse API is the @Terminal.Gui.Input.MouseEventA
|
||||
|
||||
When the user does something with the mouse, the `ConsoleDriver` maps the platform-specific mouse event into a `MouseEventArgs` and calls `Application.RaiseMouseEvent`. Then, `Application.RaiseMouseEvent` determines which `View` the event should go to. The `View.OnMouseEvent` method can be overridden or the `View.MouseEvent` event can be subscribed to, to handle the low-level mouse event. If the low-level event is not handled by a view, `Application` will then call the appropriate high-level helper APIs. For example, if the user double-clicks the mouse, `View.OnMouseClick` will be called/`View.MouseClick` will be raised with the event arguments indicating which mouse button was double-clicked.
|
||||
|
||||
### Mouse Event Processing Flow
|
||||
|
||||
Mouse events are processed through the following workflow using the [Cancellable Work Pattern](cancellable-work-pattern.md):
|
||||
|
||||
1. **Driver Level**: The ConsoleDriver captures platform-specific mouse events and converts them to `MouseEventArgs`
|
||||
2. **Application Level**: `Application.RaiseMouseEvent` determines the target view and routes the event
|
||||
3. **View Level**: The target view processes the event through:
|
||||
- `OnMouseEvent` (virtual method that can be overridden)
|
||||
- `MouseEvent` event (for event subscribers)
|
||||
- Mouse bindings (if the event wasn't handled)
|
||||
- High-level events like `OnMouseClick`, `MouseEnter`, `MouseLeave`
|
||||
|
||||
### Handling Mouse Events Directly
|
||||
|
||||
For scenarios requiring direct mouse event handling (such as custom drag-and-drop operations), subscribe to the `MouseEvent` or override `OnMouseEvent`:
|
||||
|
||||
```cs
|
||||
public class CustomView : View
|
||||
{
|
||||
public CustomView()
|
||||
{
|
||||
MouseEvent += OnMouseEventHandler;
|
||||
}
|
||||
|
||||
private void OnMouseEventHandler(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (e.Flags.HasFlag(MouseFlags.Button1Pressed))
|
||||
{
|
||||
// Handle drag start
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Alternative: Override the virtual method
|
||||
protected override bool OnMouseEvent(MouseEventArgs mouseEvent)
|
||||
{
|
||||
if (mouseEvent.Flags.HasFlag(MouseFlags.Button1Pressed))
|
||||
{
|
||||
// Handle drag start
|
||||
return true; // Event was handled
|
||||
}
|
||||
return base.OnMouseEvent(mouseEvent);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Mouse State
|
||||
|
||||
The @Terminal.Gui.ViewBase.View.MouseState property provides an abstraction for the current state of the mouse, enabling views to do interesting things like change their appearance based on the mouse state.
|
||||
|
||||
Mouse states include:
|
||||
* **Normal** - Default state when mouse is not interacting with the view
|
||||
* **Over** - Mouse is positioned over the view
|
||||
* **Pressed** - Mouse button is pressed down while over the view
|
||||
* **Clicked** - Mouse was clicked on the view
|
||||
|
||||
It works in conjunction with the @Terminal.Gui.ViewBase.View.HighlightStates which is a list of mouse states that will cause a view to become highlighted.
|
||||
|
||||
Subscribe to the @Terminal.Gui.ViewBase.View.MouseStateChanged event to be notified when the mouse state changes:
|
||||
|
||||
```cs
|
||||
view.MouseStateChanged += (sender, e) =>
|
||||
{
|
||||
switch (e.NewState)
|
||||
{
|
||||
case MouseState.Over:
|
||||
// Change appearance when mouse hovers
|
||||
break;
|
||||
case MouseState.Pressed:
|
||||
// Change appearance when pressed
|
||||
break;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Mouse Button and Movement Concepts
|
||||
|
||||
* **Down** - Indicates the user pushed a mouse button down.
|
||||
* **Pressed** - Indicates the mouse button is down; for example if the mouse was pressed down and remains down for a period of time.
|
||||
* **Released** - Indicates the user released a mouse button.
|
||||
* **Clicked** - Indicates the user pressed then released the mouse button while over a particular View.
|
||||
* **Double-Clicked** - Indicates the user clicked twice in rapid succession.
|
||||
* **Moved** - Indicates the mouse moved to a new location since the last mouse event.
|
||||
* **Wheel** - Indicates the mouse wheel was scrolled up or down.
|
||||
|
||||
## **Global Mouse Handling**
|
||||
## Global Mouse Handling
|
||||
|
||||
The @Terminal.Gui.App.Application.MouseEvent event can be used if an application wishes to receive all mouse events.
|
||||
The @Terminal.Gui.App.Application.MouseEvent event can be used if an application wishes to receive all mouse events before they are processed by individual views:
|
||||
|
||||
```cs
|
||||
Application.MouseEvent += (sender, e) =>
|
||||
{
|
||||
// Handle application-wide mouse events
|
||||
if (e.Flags.HasFlag(MouseFlags.Button3Clicked))
|
||||
{
|
||||
ShowGlobalContextMenu(e.Position);
|
||||
e.Handled = true;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Mouse Enter/Leave Events
|
||||
|
||||
The @Terminal.Gui.ViewBase.View.MouseEnter and @Terminal.Gui.ViewBase.View.MouseLeave events enable a View to take action when the mouse is over the view. Internally, this is used to enable @Terminal.Gui.ViewBase.View.Highlight.
|
||||
The @Terminal.Gui.ViewBase.View.MouseEnter and @Terminal.Gui.ViewBase.View.MouseLeave events enable a View to take action when the mouse enters or exits the view boundary. Internally, this is used to enable @Terminal.Gui.ViewBase.View.Highlight functionality:
|
||||
|
||||
```cs
|
||||
view.MouseEnter += (sender, e) =>
|
||||
{
|
||||
// Mouse entered the view
|
||||
UpdateTooltip("Hovering over button");
|
||||
};
|
||||
|
||||
view.MouseLeave += (sender, e) =>
|
||||
{
|
||||
// Mouse left the view
|
||||
HideTooltip();
|
||||
};
|
||||
```
|
||||
|
||||
## Mouse Coordinate Systems
|
||||
|
||||
Mouse coordinates in Terminal.Gui are provided in multiple coordinate systems:
|
||||
|
||||
* **Screen Coordinates** - Relative to the entire terminal screen (0,0 is top-left of terminal)
|
||||
* **View Coordinates** - Relative to the view's content area (0,0 is top-left of view's viewport)
|
||||
|
||||
The `MouseEventArgs` provides both coordinate systems:
|
||||
* `MouseEventArgs.Position` - Screen coordinates
|
||||
* `MouseEventArgs.ViewPosition` - View-relative coordinates (when available)
|
||||
|
||||
## Best Practices
|
||||
|
||||
* **Use Mouse Bindings** when possible for simple mouse interactions - they integrate well with the Command system
|
||||
* **Handle Mouse Events directly** for complex interactions like drag-and-drop or custom gestures
|
||||
* **Respect platform conventions** - use right-click for context menus, double-click for default actions
|
||||
* **Provide keyboard alternatives** - ensure all mouse functionality has keyboard equivalents
|
||||
* **Test with different terminals** - mouse support varies between terminal applications
|
||||
* **Use Mouse State** to provide visual feedback when users hover or interact with views
|
||||
|
||||
## Limitations and Considerations
|
||||
|
||||
* Not all terminal applications support mouse input - always provide keyboard alternatives
|
||||
* Mouse wheel support may vary between platforms and terminals
|
||||
* Some terminals may not support all mouse buttons or modifier keys
|
||||
* Mouse coordinates are limited to character cell boundaries - sub-character precision is not available
|
||||
* Performance can be impacted by excessive mouse move event handling - use mouse enter/leave events when appropriate rather than tracking all mouse moves
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -1,88 +1,175 @@
|
||||
# Terminal.Gui v2
|
||||
|
||||
This document provides an overview of the new features and improvements in Terminal.Gui v2.
|
||||
This document provides an in-depth overview of the new features, improvements, and architectural changes in Terminal.Gui v2 compared to v1.
|
||||
|
||||
For information on how to port code from v1 to v2, see the [v1 To v2 Migration Guide](migratingfromv1.md).
|
||||
|
||||
## Modern Look & Feel
|
||||
## Architectural Overhaul and Design Philosophy
|
||||
|
||||
Apps built with Terminal.Gui now feel modern thanks to these improvements:
|
||||
Terminal.Gui v2 represents a fundamental rethinking of the library's architecture, driven by the need for better maintainability, performance, and developer experience. The primary design goals in v2 include:
|
||||
|
||||
* *TrueColor support* - 24-bit color support for Windows, Mac, and Linux. Legacy 16-color systems are still supported, automatically. See [TrueColor](https://gui-cs.github.io/Terminal.GuiV2Docs/docs/overview.html#truecolor) for details.
|
||||
* *Enhanced Borders and Padding* - Terminal.Gui now supports a `Border`, `Margin`, and `Padding` property on all views. This simplifies View development and enables a sophisticated look and feel. See [Adornments](https://gui-cs.github.io/Terminal.GuiV2Docs/docs/overview.html#adornments) for details.
|
||||
* *User Configurable Color Themes* - See [Color Themes](https://gui-cs.github.io/Terminal.GuiV2Docs/docs/overview.html#color-themes) for details.
|
||||
* *Enhanced Unicode/Wide Character support* - Terminal.Gui now supports the full range of Unicode/wide characters. See [Unicode](https://gui-cs.github.io/Terminal.GuiV2Docs/docs/overview.html#unicode) for details.
|
||||
* [LineCanvas](~/api/Terminal.Gui.Drawing.LineCanvas.yml) - Terminal.Gui now supports a line canvas enabling high-performance drawing of lines and shapes using box-drawing glyphs. `LineCanvas` provides *auto join*, a smart TUI drawing system that automatically selects the correct line/box drawing glyphs for intersections making drawing complex shapes easy. See [Line Canvas](https://gui-cs.github.io/Terminal.GuiV2Docs/docs/overview.html#line-canvas) for details.
|
||||
- **Decoupling of Concepts**: In v1, many concepts like focus management, layout, and input handling were tightly coupled, leading to fragile and hard-to-predict behavior. v2 explicitly separates these concerns, resulting in a more modular and testable codebase.
|
||||
- **Performance Optimization**: v2 reduces overhead in rendering, event handling, and view management by streamlining internal data structures and algorithms.
|
||||
- **Modern .NET Practices**: The API has been updated to align with contemporary .NET conventions, such as using events with `EventHandler<T>` and leveraging modern C# features like target-typed `new` and file-scoped namespaces.
|
||||
- **Accessibility and Usability**: v2 places a stronger emphasis on ensuring that terminal applications are accessible, with improved keyboard navigation and visual feedback.
|
||||
|
||||
## Simplified API
|
||||
This architectural shift has resulted in the removal of thousands of lines of redundant or overly complex code from v1, replaced with cleaner, more focused implementations.
|
||||
|
||||
The entire library has been reviewed and simplified. As a result, the API is more consistent and uses modern .NET API standards (e.g. for events). This refactoring resulted in the removal of thousands of lines of code, better unit tests, and higher performance than v1.
|
||||
## Modern Look & Feel - Technical Details
|
||||
|
||||
## [View](~/api/Terminal.Gui.ViewBase.View.yml) Improvements
|
||||
* *Improved!* View Lifetime Management is Now Deterministic - In v1 the rules for lifetime management of `View` objects was unclear and led to non-dterministic behavior and hard to diagnose bugs. This was particularly acute in the behavior of `Application.Run`. In v2, the rules are clear and the code and unit test infrastructure tries to enforce them. See [Migrating From v1 To v2](migratingfromv1.md) for more details.
|
||||
* *New!* Adornments - Adornments are a special form of View that appear outside the `Viewport`: @Terminal.Gui.ViewBase.View.Margin, @Terminal.Gui.ViewBase.View.Border, and @Terminal.Gui.ViewBase.View.Padding.
|
||||
* *New!* Built-in Scrolling/Virtual Content Area - In v1, to have a view a user could scroll required either a bespoke scrolling implementation, inheriting from `ScrollView`, or managing the complexity of `ScrollBarView` directly. In v2, the base-View class supports scrolling inherently. The area of a view visible to the user at a given moment was previously a rectangle called `Bounds`. `Bounds.Location` was always `Point.Empty`. In v2 the visible area is a rectangle called `Viewport` which is a portal into the Views content, which can be bigger (or smaller) than the area visible to the user. Causing a view to scroll is as simple as changing `View.Viewport.Location`. The View's content described by `View.GetContentSize()`. See [Layout](layout.md) for details.
|
||||
* *Improved!* @Terminal.Gui.Views.ScrollBar replaces `ScrollBarView` with a much cleaner implementation of a scrollbar. In addition, @Terminal.Gui.ViewBase.View.VerticalScrollBar and @Terminal.Gui.ViewBase.View.HorizontalScrollBar provide a simple way to enable scroll bars in any View with almost no code. See See [Scrolling Deep Dive](scrolling.md) for more.
|
||||
* *New!* @Terminal.Gui.ViewBase.DimAuto - Automatically sizes the view to fit the view's Text, SubViews, or ContentArea.
|
||||
* *Improved!* @Terminal.Gui.ViewBase.PosAnchorEnd - New to v2 is `Pos.AnchorEnd ()` (with no parameters) which allows a view to be anchored to the right or bottom of the SuperView.
|
||||
* *New!* @Terminal.Gui.ViewBase.PosAlign - Aligns a set of views horizontally or vertically (left, right, center, etc...).
|
||||
* *New!* @Terminal.Gui.ViewBase.View.Arrangement enables tiled and overlapped view arrangement and moving/resizing Views with the keyboard and mouse. See [Arrangement](arrangement.md).
|
||||
* *Improved!* Keyboard [Navigation](navigation.md) has been revamped to be more reliability and ensure TUI apps built with Terminal.Gui are accessible.
|
||||
* *New!* Sizable/Movable views - Any view can now be set to have resizeable borders and/or be dragged around.
|
||||
* *Improved!* Consistent tabbing behavior - Tab navigation now behaves as expected, cleanly and consistently.
|
||||
### TrueColor Support
|
||||
- **Implementation**: v2 introduces 24-bit color support by extending the `Attribute` class to handle RGB values, with fallback to 16-color mode for older terminals. This is evident in the `ConsoleDriver` implementations, which now map colors to the appropriate terminal escape sequences.
|
||||
- **Impact**: Developers can now use a full spectrum of colors without manual palette management, as seen in v1. The `Color` struct in v2 supports direct RGB input, and drivers handle the translation to terminal capabilities.
|
||||
- **Usage**: See the `ColorPicker` view for an example of how TrueColor is leveraged to provide a rich color selection UI.
|
||||
|
||||
## New and Improved Built-in Views
|
||||
### Enhanced Borders and Padding (Adornments)
|
||||
- **Implementation**: v2 introduces a new `Adornment` class hierarchy, with `Margin`, `Border`, and `Padding` as distinct view-like entities that wrap content. This is a significant departure from v1, where borders were often hardcoded or required custom drawing.
|
||||
- **Code Change**: In v1, `View` had rudimentary border support via properties like `BorderStyle`. In v2, `View` has a `Border` property of type `Border`, which is itself a configurable entity with properties like `Thickness` and `Effect3D`.
|
||||
- **Impact**: This allows for consistent border rendering across all views and simplifies custom view development by providing a reusable adornment framework.
|
||||
|
||||
* *[DatePicker](~/api/Terminal.Gui.Views.DatePicker.yml)* - NEW!
|
||||
* *ScrollView* - Replaced by built-in scrolling.
|
||||
* *@"Terminal.Gui.Views.ScrollBar"* - Replaces *ScrollBarView* with a much simpler view.
|
||||
* *[Slider](~/api/Terminal.Gui.Views.Slider.yml)* - NEW!
|
||||
* *[Shortcut](~/api/Terminal.Gui.Views.Shortcut.yml)* - NEW! An opinionated (visually & API) View for displaying a command, helptext, key.
|
||||
* *[Bar](~/api/Terminal.Gui.Views.Bar.yml)* - NEW! Building-block View for containing Shortcuts. Opinionated relative to Orientation but minimially so. The basis for the new StatusBar, MenuBar, and Menu views.
|
||||
* *[StatusBar](~/api/Terminal.Gui.Views.StatusBar.yml)* - New implementation based on `Bar`
|
||||
* *[MenuBar](~/api/Terminal.Gui.Views.MenuBar.yml)* - COMING SOON! New implementation based on `Bar`
|
||||
* *[PopoverMenu](~/api/Terminal.Gui.Views.PopoverMenu.yml)* - COMING SOON! New implementation based on `Bar`
|
||||
* *[FileDialog](~/api/Terminal.Gui.Views.FileDialog.yml)* - The new, modern file dialog includes icons (in TUI!) for files/folders, search, and a `TreeView`.
|
||||
* *[TableView](~/api/Terminal.Gui.Views.TableView.yml)* - No longer just DataTable, now supports any collections, checkboxes and even expandable trees
|
||||
* *@"Terminal.Gui.Views.ColorPicker"* - Fully supports TrueColor with the ability to choose a color using HSV, RGB, or HSL as well as W3C standard color names.
|
||||
### User Configurable Color Themes
|
||||
- **Implementation**: v2 adds a `ConfigurationManager` that supports loading and saving color schemes from configuration files. Themes are applied via `ColorScheme` objects, which can be customized per view or globally.
|
||||
- **Impact**: Unlike v1, where color schemes were static or required manual override, v2 enables end-users to personalize the UI without code changes, enhancing accessibility and user preference support.
|
||||
|
||||
## Beauty
|
||||
### Enhanced Unicode/Wide Character Support
|
||||
- **Implementation**: v2 improves Unicode handling by correctly managing wide characters in text rendering and input processing. The `TextFormatter` class now accounts for Unicode width in layout calculations.
|
||||
- **Impact**: This fixes v1 issues where wide characters (e.g., CJK scripts) could break layout or input handling, making Terminal.Gui v2 suitable for international applications.
|
||||
|
||||
Terminal.Gui has never been prettier
|
||||
### LineCanvas
|
||||
- **Implementation**: A new `LineCanvas` class provides a drawing API for creating lines and shapes using box-drawing characters. It includes logic for auto-joining lines at intersections, selecting appropriate glyphs dynamically.
|
||||
- **Code Example**: In v2, `LineCanvas` is used internally by views like `Border` to draw clean, connected lines, a feature absent in v1.
|
||||
- **Impact**: Developers can create complex diagrams or UI elements with minimal effort, improving the visual fidelity of terminal applications.
|
||||
|
||||
* *ShowBorders* - Get that 3D 'pop' for your buttons
|
||||
* *Gradient* - Render beautiful true color borders, titles etc with the new Gradient API
|
||||
## Simplified API - Under the Hood
|
||||
|
||||
### API Consistency and Reduction
|
||||
- **Change**: v2 revisits every public API, consolidating redundant methods and properties. For example, v1 had multiple focus-related methods scattered across `View` and `Application`; v2 centralizes these in `ApplicationNavigation`.
|
||||
- **Impact**: This reduces the learning curve for new developers and minimizes the risk of using deprecated or inconsistent APIs.
|
||||
- **Example**: The v1 `View.MostFocused` property is replaced by `Application.Navigation.GetFocused()`, reducing traversal overhead and clarifying intent.
|
||||
|
||||
## Configuration Manager
|
||||
### Modern .NET Standards
|
||||
- **Change**: Events in v2 use `EventHandler<T>` instead of v1's custom delegate types. Methods follow consistent naming (e.g., `OnHasFocusChanged` vs. v1's varied naming).
|
||||
- **Impact**: Developers familiar with .NET conventions will find v2 more intuitive, and tools like IntelliSense provide better support due to standardized signatures.
|
||||
|
||||
Terminal.Gui now supports a configuration manager enabling library and app settings to be persisted and loaded from the file system. See [Configuration Manager](https://gui-cs.github.io/Terminal.GuiV2Docs/docs/overview.html#configuration-manager) for details.
|
||||
### Performance Gains
|
||||
- **Change**: v2 optimizes rendering by minimizing unnecessary redraws through a smarter `NeedsDisplay` system and reducing object allocations in hot paths like event handling.
|
||||
- **Impact**: Applications built with v2 will feel snappier, especially in complex UIs with many views or frequent updates, addressing v1 performance bottlenecks.
|
||||
|
||||
## Logging & Metrics
|
||||
## View Improvements - Deep Dive
|
||||
|
||||
Terminal.Gui now features multi level logging of engine internals and system performance metrics (redraws, invoke durations etc). Never again wonder why your frame rate is low, or a given terminal/distro does not behave as expected.
|
||||
See [Logging](logging.md) for details.
|
||||
### Deterministic View Lifetime Management
|
||||
- **v1 Issue**: Lifetime rules for `View` objects were unclear, leading to memory leaks or premature disposal, especially with `Application.Run`.
|
||||
- **v2 Solution**: v2 defines explicit rules for view disposal and ownership, enforced by unit tests. `Application.Run` now clearly manages the lifecycle of `Toplevel` views, ensuring deterministic cleanup.
|
||||
- **Impact**: Developers can predict when resources are released, reducing bugs related to dangling references or uninitialized states.
|
||||
|
||||
## Sixel Image Support
|
||||
### Adornments Framework
|
||||
- **Technical Detail**: Adornments are implemented as nested views that surround the content area, each with its own drawing and layout logic. For instance, `Border` can draw 3D effects or custom glyphs.
|
||||
- **Code Change**: In v2, `View` has properties like `Margin`, `Border`, and `Padding`, each configurable independently, unlike v1's limited border support.
|
||||
- **Impact**: This modular approach allows for reusable UI elements and simplifies creating visually consistent applications.
|
||||
|
||||
Recently added to Windows Terminal and long supported in mainstream linux terminals, this graphics protcol allows images and even animations to be rendered directly into the console.
|
||||
### Built-in Scrolling/Virtual Content Area
|
||||
- **v1 Issue**: Scrolling required using `ScrollView` or manual offset management, which was error-prone.
|
||||
- **v2 Solution**: Every `View` in v2 has a `Viewport` rectangle representing the visible portion of a potentially larger content area defined by `GetContentSize()`. Changing `Viewport.Location` scrolls the content.
|
||||
- **Code Example**: In v2, `TextView` uses this to handle large text buffers without additional wrapper views.
|
||||
- **Impact**: Simplifies implementing scrollable content and reduces the need for specialized container views.
|
||||
|
||||
## Updated Keyboard API
|
||||
### Improved ScrollBar
|
||||
- **Change**: v2 replaces `ScrollBarView` with `ScrollBar`, a cleaner implementation integrated with the built-in scrolling system. `VerticalScrollBar` and `HorizontalScrollBar` properties on `View` enable scroll bars with minimal code.
|
||||
- **Impact**: Developers can add scroll bars to any view without managing separate view hierarchies, a significant usability improvement over v1.
|
||||
|
||||
The API for handling keyboard input is significantly improved. See [Keyboard API](keyboard.md).
|
||||
### DimAuto, PosAnchorEnd, and PosAlign
|
||||
- **DimAuto**: Automatically sizes views based on content or subviews, reducing manual layout calculations.
|
||||
- **PosAnchorEnd**: Allows anchoring to the right or bottom of a superview, enabling flexible layouts not easily achievable in v1.
|
||||
- **PosAlign**: Provides alignment options (left, center, right) for multiple views, streamlining UI design.
|
||||
- **Impact**: These features reduce boilerplate layout code and support responsive designs in terminal constraints.
|
||||
|
||||
* The `Key` class replaces the `KeyEvent` struct and provides a platform-independent abstraction for common keyboard operations. It is used for processing keyboard input and raising keyboard events. This class provides a high-level abstraction with helper methods and properties for common keyboard operations. Use this class instead of the low-level `KeyCode` enum when possible. See [Key](~/api/Terminal.Gui.Input.Key.yml) for more details.
|
||||
* The preferred way to handle single keystrokes is to use **Key Bindings**. Key Bindings map a key press to a [Command](~/api/Terminal.Gui.Input.Command.yml). A view can declare which commands it supports, and provide a lambda that implements the functionality of the command, using `View.AddCommand()`. Use the `View.Keybindings` to configure the key bindings.
|
||||
* For better consistency and user experience, the default key for closing an app or `Toplevel` is now `Esc` (it was previously `Ctrl+Q`).
|
||||
### View Arrangement
|
||||
- **Technical Detail**: The `Arrangement` property on `View` supports flags like `Movable`, `Resizable`, and `Overlapped`, enabling dynamic UI interactions via keyboard and mouse.
|
||||
- **Code Example**: `Window` in v2 uses `Arrangement` to allow dragging and resizing, a feature requiring custom logic in v1.
|
||||
- **Impact**: Developers can create desktop-like experiences in the terminal with minimal effort.
|
||||
|
||||
## Updated Mouse API
|
||||
### Keyboard Navigation Overhaul
|
||||
- **v1 Issue**: Navigation was inconsistent, with coupled concepts like `CanFocus` and `TabStop` leading to unpredictable focus behavior.
|
||||
- **v2 Solution**: v2 decouples these concepts, introduces `TabBehavior` enum for clearer intent (`TabStop`, `TabGroup`, `NoStop`), and centralizes navigation logic in `ApplicationNavigation`.
|
||||
- **Impact**: Ensures accessibility by guaranteeing keyboard access to all focusable elements, with unit tests enforcing navigation keys on built-in views.
|
||||
|
||||
The API for mouse input is now internally consistent and easiser to use.
|
||||
### Sizable/Movable Views
|
||||
- **Implementation**: Any view can be made resizable or movable by setting `Arrangement` flags, with built-in mouse and keyboard handlers for interaction.
|
||||
- **Impact**: Enhances user experience by allowing runtime UI customization, a feature limited to specific views like `Window` in v1.
|
||||
|
||||
* The `MouseEvent` class replaces `MouseEventEventArgs`.
|
||||
* More granular APIs are provided to ease handling specific mouse actions. See [Mouse API](mouse.md).
|
||||
* Views can use the `View.Highlight` event to have the view be visibly highlighted on various mouse events.
|
||||
* Views can set `View.WantContinousButtonPresses = true` to have their `Command.Accept` command be invoked repeatedly as the user holds a mouse button down on the view.
|
||||
## New and Improved Built-in Views - Detailed Analysis
|
||||
|
||||
## AOT support
|
||||
*AOT/single file app support* now works out of the box.
|
||||
### New Views
|
||||
- **DatePicker**: Provides a calendar-based date selection UI, leveraging v2's improved drawing and navigation systems.
|
||||
- **Slider**: A new control for range selection, using `LineCanvas` for smooth rendering and supporting TrueColor for visual feedback.
|
||||
- **Shortcut**: An opinionated view for command display with key bindings, simplifying status bar or toolbar creation.
|
||||
- **Bar**: A foundational view for horizontal or vertical layouts of `Shortcut` or other items, used in `StatusBar`, `MenuBar`, and `PopoverMenu`.
|
||||
- **FileDialog**: Modernized with a `TreeView` for navigation, icons using Unicode glyphs, and search functionality, far surpassing v1's basic dialog.
|
||||
- **ColorPicker**: Leverages TrueColor for a comprehensive color selection experience, supporting multiple color models (HSV, RGB, HSL).
|
||||
|
||||
### Improved Views
|
||||
- **ScrollView**: Deprecated in favor of built-in scrolling on `View`, reducing complexity and view hierarchy depth.
|
||||
- **TableView**: Now supports generic collections, checkboxes, and tree structures, moving beyond v1's `DataTable` limitation, with improved rendering performance.
|
||||
- **StatusBar**: Rebuilt on `Bar`, providing a more flexible and visually appealing status display.
|
||||
|
||||
## Beauty - Visual Enhancements
|
||||
|
||||
### Borders
|
||||
- **Implementation**: Uses the `Border` adornment to render 3D effects or custom styles, configurable per view.
|
||||
- **Impact**: Adds visual depth to UI elements, making applications feel more polished compared to v1's flat borders.
|
||||
|
||||
### Gradient
|
||||
- **Implementation**: A new `Gradient` API allows rendering color transitions across view elements, using TrueColor for smooth effects.
|
||||
- **Impact**: Enables modern-looking UI elements like gradient borders or backgrounds, not possible in v1 without custom drawing.
|
||||
|
||||
## Configuration Manager - Persistence and Customization
|
||||
- **Technical Detail**: `ConfigurationManager` in v2 uses JSON or other formats to persist settings like themes, key bindings, and view properties to disk.
|
||||
- **Code Change**: Unlike v1, where settings were ephemeral or hardcoded, v2 provides a centralized system for loading/saving configurations.
|
||||
- **Impact**: Allows for user-specific customizations and library-wide settings without recompilation, enhancing flexibility.
|
||||
|
||||
## Logging & Metrics - Debugging and Performance
|
||||
- **Implementation**: v2 introduces a multi-level logging system for internal operations (e.g., rendering, input handling) and metrics for performance tracking (e.g., frame rate, redraw times).
|
||||
- **Impact**: Developers can diagnose issues like slow redraws or terminal compatibility problems, a capability absent in v1, reducing guesswork in debugging.
|
||||
|
||||
## Sixel Image Support - Graphics in Terminal
|
||||
- **Technical Detail**: v2 supports the Sixel protocol for rendering images and animations directly in compatible terminals (e.g., Windows Terminal, xterm).
|
||||
- **Code Change**: New rendering logic in console drivers detects terminal support and handles Sixel data transmission.
|
||||
- **Impact**: Brings graphical capabilities to terminal applications, far beyond v1's text-only rendering, opening up new use cases like image previews.
|
||||
|
||||
## Updated Keyboard API - Comprehensive Input Handling
|
||||
|
||||
### Key Class
|
||||
- **Change**: Replaces v1's `KeyEvent` struct with a `Key` class, providing a high-level abstraction over raw key codes with properties for modifiers and key type.
|
||||
- **Impact**: Simplifies keyboard handling by abstracting platform differences, making code more portable and readable.
|
||||
|
||||
### Key Bindings
|
||||
- **Implementation**: v2 introduces a binding system mapping keys to `Command` enums via `View.KeyBindings`, with scopes (`Application`, `Focused`, `HotKey`) for priority.
|
||||
- **Impact**: Replaces v1's ad-hoc key handling with a structured approach, allowing views to declare supported commands and customize responses easily.
|
||||
- **Example**: `TextField` in v2 binds `Key.Tab` to text insertion rather than focus change, customizable by developers.
|
||||
|
||||
### Default Close Key
|
||||
- **Change**: Changed from `Ctrl+Q` in v1 to `Esc` in v2 for closing apps or `Toplevel` views.
|
||||
- **Impact**: Aligns with common user expectations, improving UX consistency across terminal applications.
|
||||
|
||||
## Updated Mouse API - Enhanced Interaction
|
||||
|
||||
### MouseEvent Class
|
||||
- **Change**: Replaces `MouseEventEventArgs` with `MouseEvent`, providing a cleaner structure for mouse data (position, flags).
|
||||
- **Impact**: Simplifies event handling with a more intuitive API, reducing errors in mouse interaction logic.
|
||||
|
||||
### Granular Mouse Handling
|
||||
- **Implementation**: v2 offers specific events for clicks, double-clicks, and movement, with flags for button states.
|
||||
- **Impact**: Developers can handle complex mouse interactions (e.g., drag-and-drop) more easily than in v1.
|
||||
|
||||
### Highlight Event and Continuous Button Presses
|
||||
- **Highlight**: Views can visually respond to mouse hover or click via the `Highlight` event.
|
||||
- **Continuous Presses**: Setting `WantContinuousButtonPresses = true` repeats `Command.Accept` during button hold, useful for sliders or buttons.
|
||||
- **Impact**: Enhances interactive feedback, making terminal UIs feel more responsive.
|
||||
|
||||
## AOT Support - Deployment and Performance
|
||||
- **Implementation**: v2 ensures compatibility with Ahead-of-Time compilation and single-file applications by avoiding reflection patterns problematic for AOT.
|
||||
- **Impact**: Simplifies deployment for environments requiring AOT (e.g., .NET Native), a feature not explicitly supported in v1, reducing runtime overhead.
|
||||
|
||||
## Conclusion
|
||||
|
||||
Terminal.Gui v2 is a transformative update, addressing core limitations of v1 through architectural redesign, performance optimizations, and feature enhancements. From TrueColor and adornments for visual richness to decoupled navigation and modern input APIs for usability, v2 provides a robust foundation for building sophisticated terminal applications. The detailed changes in view management, configuration, and debugging tools empower developers to create more maintainable and user-friendly applications.
|
||||
37
docfx/docs/showcase.md
Normal file
37
docfx/docs/showcase.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Showcase
|
||||
|
||||
## Applications Built with Terminal.Gui
|
||||
|
||||
- **[UI Catalog](https://github.com/gui-cs/Terminal.Gui/tree/master/UICatalog)** - The UI Catalog project provides an easy to use and extend sample illustrating the capabilities of **Terminal.Gui**. Run `dotnet run --project UICatalog` to run the UI Catalog.
|
||||

|
||||
⠀
|
||||
- **[PowerShell's `Out-ConsoleGridView`](https://github.com/PowerShell/GraphicalTools)** - `OCGV` sends the output from a command to an interactive table.
|
||||

|
||||
⠀
|
||||
- **[F7History](https://github.com/gui-cs/F7History)** - Graphical Command History for PowerShell (built on PowerShell's `Out-ConsoleGridView`).
|
||||

|
||||
⠀
|
||||
- **[PoshRedisViewer](https://github.com/En3Tho/PoshRedisViewer)** - A compact Redis viewer module for PowerShell written in F#.
|
||||

|
||||
⠀
|
||||
- **[PoshDotnetDumpAnalyzeViewer](https://github.com/En3Tho/PoshDotnetDumpAnalyzeViewer)** - dotnet-dump UI module for PowerShell.
|
||||

|
||||
⠀
|
||||
- **[TerminalGuiDesigner](https://github.com/tznind/TerminalGuiDesigner)** - Cross platform view designer for building Terminal.Gui applications.
|
||||

|
||||
- **[Capital and Cargo](https://github.com/dhorions/Capital-and-Cargo)** - A retro console game where you buy, sell, produce and transport goods built with Terminal.Gui
|
||||

|
||||
- **[Falcon](https://github.com/MaciekWin3/Falcon)** - Terminal chat application that uses SignalR and Terminal.Gui.
|
||||

|
||||
- **[Muse](https://github.com/MaciekWin3/Muse)** - Muse is terminal music player built with Terminal.Gui and NAudio on .NET platform.
|
||||

|
||||
- **[Whale](https://github.com/MaciekWin3/Whale)** - Lightweight terminal user interface application that helps software engineers manage Docker containers.
|
||||

|
||||
- **[TermKeyVault](https://github.com/MaciekWin3/TermKeyVault)** - Terminal based password manager built with F# and Terminal.Gui.
|
||||

|
||||
|
||||
## Examples
|
||||
|
||||
- **[C# Example](https://github.com/gui-cs/Terminal.Gui/tree/master/Example)** - Run `dotnet run` in the `Example` directory to run the C# Example.
|
||||
- **[F# Example](https://github.com/gui-cs/Terminal.Gui/tree/master/FSharpExample)** - An example showing how to build a Terminal.Gui app using F#.
|
||||
- **[Reactive Example](https://github.com/gui-cs/Terminal.Gui/tree/master/ReactiveExample)** - A sample app that shows how to use `System.Reactive` and `ReactiveUI` with `Terminal.Gui`. The app uses the MVVM architecture that may seem familiar to folks coming from WPF, Xamarin Forms, UWP, Avalonia, or Windows Forms. In this app, we implement the data bindings using ReactiveUI `WhenAnyValue` syntax and [Pharmacist](https://github.com/reactiveui/pharmacist) — a tool that converts all events in a NuGet package into observable wrappers.
|
||||
Reference in New Issue
Block a user