mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 15:57:56 +01:00
Merge branch 'v2_develop' into v2_3777-hexedit
This commit is contained in:
126
docfx/docs/events.md
Normal file
126
docfx/docs/events.md
Normal 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?`
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user