Merge branch 'v2_develop' into v2_3777-hexedit

This commit is contained in:
Tig
2024-10-15 11:07:51 -06:00
committed by GitHub
113 changed files with 2215 additions and 2393 deletions

126
docfx/docs/events.md Normal file
View File

@@ -0,0 +1,126 @@
# 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.
## 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
* *Action*
* *Event*
* *Command*
* *Invoke*
* *Raise*
* *Listen*
* *Handle/Handling/Handled* - Applies to scenarios where an event can either be handled by an event listener (or override) vs not handled. Events that originate from a user action like mouse moves and key presses are examples.
* *Cancel/Cancelling/Cancelled* - Applies to scenarios where something can be cancelled. Changing the `Orientation` of a `Slider` is cancelable.
## 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)
* [.NET Design for Extensibility - Events and Callbacks](https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/events-and-callbacks)
* [C# Event Implementation Fundamentals, Best Practices and Conventions](https://www.codeproject.com/Articles/20550/C-Event-Implementation-Fundamentals-Best-Practices)
## Naming
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).
## `EventHandler` style event best-practices
* 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.
## `Action<T>` style callback best-practices
- tbd
## `INotifyPropertyChanged` style notification best practices
- tbd
## Common Patterns
The primary pattern for events is the `event/EventHandler` idiom. We use the `Action<T>` idiom sparingly. We support `INotifyPropertyChanged` in cases where data binding is relevant.
## Cancellable Event Pattern
A cancellable event is really two events and some activity that takes place between those events. The "pre-event" happens before the activity. The activity then takes place (or not). If the activity takes place, then the "post-event" is typically raised. So, to be precise, no event is being cancelled even though we say we have a cancellable event. Rather, the activity that takes place between the two events is what is cancelled — and likely prevented from starting at all.
### **Before** - If any pre-conditions are met raise the "pre-event", typically named in the form of "xxxChanging". e.g.
- A `protected virtual` method is called. This method is named `OnxxxChanging` and the base implementation simply does `return false`.
- If the `OnxxxChanging` method returns `true` it means a derived class canceled the event. Processing should stop.
- Otherwise, the `xxxChanging` event is invoked via `xxxChanging?.Invoke(args)`. If `args.Cancel/Handled == true` it means a subscriber has cancelled the event. Processing should stop.
### **During** - Do work.
### **After** - Raise the "post-event", typically named in the form of "xxxChanged"
- A `protected virtual` method is called. This method is named `OnxxxChanged` has a return type of `void`.
- The `xxxChanged` event is invoked via `xxxChanging?.Invoke(args)`.
The `OrientationHelper` class supporting `IOrientation` and a `View` having an `Orientation` property illustrates the preferred TG pattern for cancelable events.
```cs
/// <summary>
/// Gets or sets the orientation of the View.
/// </summary>
public Orientation Orientation
{
get => _orientation;
set
{
if (_orientation == value)
{
return;
}
// Best practice is to call the virtual method first.
// This allows derived classes to handle the event and potentially cancel it.
if (_owner?.OnOrientationChanging (value, _orientation) ?? false)
{
return;
}
// If the event is not canceled by the virtual method, raise the event to notify any external subscribers.
CancelEventArgs<Orientation> args = new (in _orientation, ref value);
OrientationChanging?.Invoke (_owner, args);
if (args.Cancel)
{
return;
}
// If the event is not canceled, update the value.
Orientation old = _orientation;
if (_orientation != value)
{
_orientation = value;
if (_owner is { })
{
_owner.Orientation = value;
}
}
// Best practice is to call the virtual method first, then raise the event.
_owner?.OnOrientationChanged (_orientation);
OrientationChanged?.Invoke (_owner, new (in _orientation));
}
}
```
## `bool` or `bool?`

View File

@@ -59,22 +59,25 @@ The Command can be invoked even if the `View` that defines them is not focused o
### **Handling Keyboard Events**
Keyboard events are retrieved from [Console Drivers](drivers.md) and passed on
to the [Application](~/api/Terminal.Gui.Application.yml) class by the [Main Loop](mainloop.md).
Keyboard events are retrieved from [Console Drivers](drivers.md) each iteration of the [Application](~/api/Terminal.Gui.Application.yml) [Main Loop](mainloop.md). The console driver raises the @Terminal.Gui.ConsoleDriver.KeyDown and @Terminal.Gui.ConsoleDriver.KeyUp events which invoke @Terminal.Gui.Application.RaiseKeyDown(Terminal.Gui.Key) and @Terminal.Gui.Application.RaiseKeyUp(Terminal.Gui.Key) respectively.
[Application](~/api/Terminal.Gui.Application.yml) then determines the current [Toplevel](~/api/Terminal.Gui.Toplevel.yml) view
(either the default created by calling @Terminal.Gui.Application.Init(Terminal.Gui.ConsoleDriver,System.String), or the one set by calling `Application.Run`). The mouse event, using [Viewport-relative coordinates](xref:Terminal.Gui.View.Viewport) is then passed to the @Terminal.Gui.View.NewKeyDownEvent(Terminal.Gui.Key) method of the current [Toplevel](~/api/Terminal.Gui.Toplevel.yml) view.
NOTE: Not all drivers/platforms support sensing distinct KeyUp events. These drivers will simulate KeyUp events by raising @Terminal.Gui.ConsoleDriver.KeyUp after @Terminal.Gui.ConsoleDriver.KeyDown.
If the view is enabled, the @Terminal.Gui.View.NewKeyDownEvent(Terminal.Gui.Key) method will do the following:
@Terminal.Gui.Application.RaiseKeyDown(Terminal.Gui.Key) raises @Terminal.Gui.Application.KeyDown and then calls @Terminal.Gui.View.NewKeyDownEvent(Terminal.Gui.Key) on all toplevel Views. If no View handles the key event, any Application-scoped key bindings will be invoked.
1) If the view has a subview that has focus, 'ProcessKeyDown' on the focused view will be called. If the focused view handles the key press, processing stops.
2) If there is no focused sub-view, or the focused sub-view does not handle the key press, @Terminal.Gui.View.OnKeyDown(Terminal.Gui.Key) will be called. If the view handles the key press, processing stops.
3) If the view does not handle the key press, @Terminal.Gui.TextField.OnInvokingKeyBindings(Terminal.Gui.Key,Terminal.Gui.KeyBindingScope) will be called. This method calls @Terminal.Gui.View.InvokeKeyBindings(Terminal.Gui.Key,Terminal.Gui.KeyBindingScope) to invoke any keys bound to commands. If the key is bound and any of it's command handlers return true, processing stops.
4) If the key is not bound, or the bound command handlers do not return true, @Terminal.Gui.View.OnProcessKeyDown(Terminal.Gui.Key) is called. If the view handles the key press, processing stops.
@Terminal.Gui.Application.RaiseKeyDown(Terminal.Gui.Key) raises @Terminal.Gui.Application.KeyDown and then calls @Terminal.Gui.View.NewKeyUpEvent(Terminal.Gui.Key) on all toplevel Views.
If a view is enabled, the @Terminal.Gui.View.NewKeyDownEvent(Terminal.Gui.Key) method will do the following:
1) If the view has a subview that has focus, 'NewKeyDown' on the focused view will be called. This is recursive. If the most-focused view handles the key press, processing stops.
2) If there is no most-focused sub-view, or a most-focused sub-view does not handle the key press, @Terminal.Gui.View.OnKeyDown(Terminal.Gui.Key) will be called. If the view handles the key press, processing stops.
3) If @Terminal.Gui.View.OnKeyDown(Terminal.Gui.Key) does not handle the event. @Terminal.Gui.View.KeyDown will be raised.
4) If the view does not handle the key down event, any bindings for the key will be invoked (see @Terminal.Gui.View.KeyBindings). If the key is bound and any of it's command handlers return true, processing stops.
5) If the key is not bound, or the bound command handlers do not return true, @Terminal.Gui.View.OnKeyDownNotHandled(Terminal.Gui.Key) is called.
## **Application Key Handling**
To define application key handling logic for an entire application in cases where the methods listed above are not suitable, use the `Application.OnKeyDown` event.
To define application key handling logic for an entire application in cases where the methods listed above are not suitable, use the @Terminal.Gui.Application.KeyDown event.
## **Key Down/Up Events**
@@ -90,17 +93,14 @@ To define application key handling logic for an entire application in cases wher
- `NewKeyDownEvent` is called on the most-focused SubView (if any) that has focus. If that call returns true, the method returns.
- Calls `OnKeyDown`.
- **During**
- Assuming `OnKeyDown` call returns false (indicating the key wasn't handled)
- `OnInvokingKeyBindings` is called to invoke any bound commands.
- `OnInvokingKeyBindings` fires the `InvokingKeyBindings` event
- Assuming `OnKeyDown` call returns false (indicating the key wasn't handled) any commands bound to the key will be invoked.
- **After**
- Assuming `OnInvokingKeyBindings` returns false (indicating the key wasn't handled)
- `OnProcessKeyDown` is called to process the key.
- `OnProcessKeyDown` fires the `ProcessKeyDown` event
- Assuming no keybinding was found or all invoked commands were not handled:
- `OnKeyDownNotHandled` is called to process the key.
- `KeyDownNotHandled` is raised.
- Subclasses of `View` can (rarely) override `OnKeyDown` to see keys before they are processed by `OnInvokingKeyBindings` and `OnProcessKeyDown
- Subclasses of `View` can (rarely) override `OnInvokingKeyBindings` to see keys before they are processed by `OnProcessKeyDown`
- Subclasses of `View` can (often) override `OnProcessKeyDown` to do normal key processing.
- Subclasses of `View` can (rarely) override `OnKeyDown` (or subscribe to `KeyDown`) to see keys before they are processed
- Subclasses of `View` can (often) override `OnKeyDownNotHandled` to do key processing for keys that were not previously handled. `TextField` and `TextView` are examples.
## ConsoleDriver

View File

@@ -196,7 +196,7 @@ In v1, the `Command` enum had duplicate entries and inconsistent naming. In v2 i
The API for mouse input is now internally consistent and easier to use.
* The @Terminal.Gui.MouseEvent class replaces `MouseEventEventArgs`.
* The @Terminal.Gui.MouseEventArgs class replaces `MouseEventEventArgs`.
* More granular APIs are provided to ease handling specific mouse actions. See [Mouse API](mouse.md).
* Views can use the @Terminal.Gui.View.Highlight event to have the view be visibly highlighted on various mouse events.
* Views can set `View.WantContinousButtonPresses = true` to have their @Terminal.Gui.Command.Accept command be invoked repeatedly as the user holds a mouse button down on the view.
@@ -213,7 +213,7 @@ The API for mouse input is now internally consistent and easier to use.
```diff
- Application.RootMouseEvent(KeyEvent arg)
+ Application.MouseEvent(object? sender, MouseEvent mouseEvent)
+ Application.MouseEvent(object? sender, MouseEventArgs mouseEvent)
```
## Navigation - `Cursor`, `Focus`, `TabStop` etc...

View File

@@ -10,9 +10,9 @@ Tenets higher in the list have precedence over tenets lower in the list.
## Mouse APIs
At the core of *Terminal.Gui*'s mouse API is the *[MouseEvent](~/api/Terminal.Gui.MouseEvent.yml)* class. The `MouseEvent` class provides a platform-independent abstraction for common mouse events. Every mouse event can be fully described in a `MouseEvent` instance, and most of the mouse-related APIs are simply helper functions for decoding a `MouseEvent`.
At the core of *Terminal.Gui*'s mouse API is the @Terminal.Gui.MouseEventArgs class. The @Terminal.Gui.MouseEventArgs class provides a platform-independent abstraction for common mouse events. Every mouse event can be fully described in a @Terminal.Gui.MouseEventArgs instance, and most of the mouse-related APIs are simply helper functions for decoding a @Terminal.Gui.MouseEventArgs.
When the user does something with the mouse, the `ConsoleDriver` maps the platform-specific mouse event into a `MouseEvent` and calls `Application.OnMouseEvent`. Then, `Application.OnMouseEvent` 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 fired with the event arguments indicating which mouse button was double-clicked.
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 Button and Movement Concepts