mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 15:57:56 +01:00
* touching publish.yml * ColorScheme->Scheme * ColorScheme->Scheme 2 * Prototype of GetAttributeForRole * Badly broke CM * Further Badly broke CM * Refactored CM big-time. View still broken * All unit test pass again. Tons added. CM is still WIP, but Schemes is not mostly refactored and working. * Actually: All unit test pass again. Tons added. CM is still WIP, but Schemes is not mostly refactored and working. * Bug fixes. DeepMemberWiseClone cleanup * Further cleanup of Scope<T>, ConfigProperty, etc. * Made ConfigManager thread safe. * WIP: Broken * WIP: new deep clone impl * WIP: new deep clone impl is done. Now fixing CM * WIP: - config.md - Working on AOT clean up - Core CM is broken; but known. * WIP * Merged. Removed CM from Application.Init * WIP * More WIP; Less broke * All CM unit tests pass... Not sure if it actually works though * All unit tests pass... Themes are broken though in UI Cat * CM Ready for review? * Fixed failures due to TextStyles PR * Working on Scheme/Attribute * Working on Scheme/Attribute 2 * Working on Scheme/Attribute 3 * Working on Scheme/Attribute 4 * Working on Scheme/Attribute 5 * Working on Scheme/Attribute 6 * Added test to show how awful memory usage is * Improved schema. Updated config.json * Nade Scope<T> concurrentdictionary and added test to prove * Made Themes ConcrurrentDictionary. Added bunches of tests * Code cleanup * Code cleanup 2 * Code cleanup 3 * Tweaking Scheme * ClearJsonErrors * ClearJsonErrors2 * Updated Attribute API * It all (mostly) works! * Skip odd unit test * Messed with Themes * Theme tweaks * Code reorg. New .md stuff * Fixed Enabled. Added mock driver * Fixed a bunch of View.Enabled related issues * Scheme -> Get/SetScheme() * Cleanup * Cleanup2 * Broke something * Fixed everything * Made CM.Enable better * Text Style Scenario * Added comments * Fixed UI Catalog Theme Changing * Fixed more dynamic CM update stuff * Warning cleanup * New Default Theme * fixed unit test * Refactoring Scheme and Attribute to fix inheritance * more unit tests * ConfigProperty is not updating schemes correctly * All unit tests pass. Code cleanup * All unit tests pass. Code cleanup2 * Fixed unit tests * Upgraded TextField and TextView * Fixed TextView !Enabled bug * More updates to TextView. More unit tests for SchemeManager * Upgraded CharMap * API docs * Fixe HexView API * upgrade HexView * Fixed shortcut KeyView * Fixed more bugs. Added new themes * updated themes * upgraded Border * Fixed themes memory usage...mostly * Fixed themes memory usage...mostly2 * Fixed themes memory usage...2 * Fixed themes memory usage...3 * Added new colors * Fixed GetHardCodedConfig bug * Added Themes Scenario - WIP * Added Themes Scenario * Tweaked Themes Scenario * Code cleanup * Fixed json schmea * updated deepdives * updated deepdives * Tweaked Themes Scenario * Made Schemes a concurrent dict * Test cleanup * Thread safe ConfigProperty tests * trying to make things more thread safe * more trying to make things more thread safe * Fixing bugs in shadowview * Fixing bugs in shadowview 2 * Refactored GetViewsUnderMouse to GetViewsUnderLocation etc... * Fixed dupe unit tests? * Added better description of layout and coordiantes to deep dive * Added better description of layout and coordiantes to deep dive * Modified tests that call v2.AddTimeout; they were returning true which means restart the timer! This was causing mac/linux unit test failures. I think * Fixed auto scheme. Broke TextView/TextField selection * Realized Attribute.IsExplicitlySet is stupid; just use nullable * Fixed Attribute. Simplified. MOre theme testing * Updated themes again * GetViewsUnderMouse to GetViewsUnderLocation broke TransparentMouse. * Fixing mouseunder bugs * rewriting... * All working again. Shadows are now slick as snot. GetViewsUnderLocation is rewritten to actually work and be readable. Tons more low-level unit tests. Margin is now actually ViewportSettings.Transparent. * Code cleanup * Code cleanup * Code cleanup of color apis * Fixed Hover/Highlight * Update Examples/UICatalog/Scenarios/AllViewsTester.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Examples/UICatalog/Scenarios/Clipping.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fixed race condition? * reverted * Simplified Attribute API by removing events from SetAttributeForRole * Removed recursion from GetViewsAtLocation * Removed unneeded code * Code clean up. Fixed Scheme bug. * reverted temporary disable * Adjusted scheme algo * Upgraded TextValidateField * Fixed TextValidate bugs * Tweaks * Frameview rounded border by default * API doc cleanup * Readme fix * Addressed tznind feeback * Fixed more unit test issues by protecting Application statics from being set if Application.Initialized is not true * Fixed more unit test issues by protecting Application statics from being set if Application.Initialized is not true 2 * cleanup --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
243 lines
13 KiB
Markdown
243 lines
13 KiB
Markdown
# Cancellable Work Pattern in Terminal.Gui
|
||
|
||
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`.
|
||
|
||
## 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.
|
||
|
||
## Components
|
||
|
||
The *Cancellable Work Pattern* consists of the following components:
|
||
- **Workflow**: A sequence of phases, which may be multi-phase (e.g., rendering in `View.Draw`), linear (e.g., key processing in `View.Keyboard`), per-unit (e.g., command execution in `View.Command`), or event-driven (e.g., key handling in `Application.Keyboard`, property changes in `OrientationHelper`).
|
||
- **Notifications**: Events (e.g., `DrawingText`, `KeyDown`, `Activating`, `OrientationChanging`) and virtual methods (e.g., `OnDrawingText`, `OnKeyDown`, `OnActivating`, `OnOrientationChanging`) raised at each phase to notify observers.
|
||
- **Cancellation**: Mechanisms to halt a phase or workflow, such as setting `Cancel`/`Handled` properties in event arguments or returning `bool` from virtual methods.
|
||
- **Context**: Data passed to observers for informed decision-making, such as `DrawContext` (drawing), `Key` (keyboard), `ICommandContext` (commands), or `CancelEventArgs<Orientation>` (orientation).
|
||
- **Default Behavior**: A standard implementation for each phase, such as `DrawText` (drawing), `InvokeCommands` (keyboard and application-level), `RaiseActivating` (commands), or updating a property (`OrientationHelper`).
|
||
|
||
## 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.
|
||
|
||
### 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:
|
||
```csharp
|
||
private void DoDrawText(DrawContext? context = null)
|
||
{
|
||
if (OnDrawingText(context)) // Virtual method for subclasses
|
||
{
|
||
return; // Cancel if true
|
||
}
|
||
if (OnDrawingText()) // Legacy virtual method
|
||
{
|
||
return; // Cancel if true
|
||
}
|
||
var dev = new DrawEventArgs(Viewport, Rectangle.Empty, context);
|
||
DrawingText?.Invoke(this, dev); // Notify observers
|
||
if (dev.Cancel) // Check for cancellation
|
||
{
|
||
return;
|
||
}
|
||
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.
|
||
- **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.
|
||
|
||
#### Example: `ProcessKeyDown`
|
||
```csharp
|
||
public virtual bool ProcessKeyDown(Key key)
|
||
{
|
||
if (OnKeyDown(key)) // Virtual method
|
||
{
|
||
return true; // Cancel if true
|
||
}
|
||
KeyDown?.Invoke(this, key); // Notify observers
|
||
if (key.Handled) // Check for cancellation
|
||
{
|
||
return true;
|
||
}
|
||
bool handled = InvokeCommands(key, KeyBindingScope.HotKey | KeyBindingScope.Focused); // Default behavior
|
||
return handled;
|
||
}
|
||
```
|
||
- **Workflow**: Linear, processing one key event.
|
||
- **Notifications**: `OnKeyDown` (virtual), `KeyDown` (event).
|
||
- **Cancellation**: `OnKeyDown` returning `true` or `key.Handled = true`.
|
||
- **Context**: `Key` provides key details and bindings.
|
||
- **Default Behavior**: `InvokeCommands` maps keys to commands (e.g., `Command.Accept`).
|
||
- **Use Case**: Allows views to customize key handling (e.g., `TextField` capturing input) or cancel default command execution.
|
||
|
||
### 3. View.Command: Command Execution
|
||
|
||
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`:
|
||
```csharp
|
||
protected bool? RaiseActivating(ICommandContext? ctx)
|
||
{
|
||
CommandEventArgs args = new() { Context = ctx };
|
||
if (OnActivating(args) || args.Handled)
|
||
{
|
||
return true;
|
||
}
|
||
Activating?.Invoke(this, args);
|
||
return Activating is null ? null : args.Handled;
|
||
}
|
||
```
|
||
- **Workflow**: Single phase for `Command.Activate`.
|
||
- **Notifications**: `OnActivating` (virtual), `Activating` (event).
|
||
- **Cancellation**: `OnActivating` returning `true` or `args.Handled = true`.
|
||
- **Context**: `ICommandContext` provides `Command`, `Source`, and `Binding`.
|
||
- **Default Behavior**: `SetFocus` for `Command.Activate` (in `SetupCommands`).
|
||
- **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
|
||
|
||
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)
|
||
{
|
||
if (KeyDown is null)
|
||
{
|
||
return false;
|
||
}
|
||
KeyDown?.Invoke(null, key); // Notify observers
|
||
return key.Handled; // Check for cancellation
|
||
}
|
||
```
|
||
- **Workflow**: Event-driven, processing one key event.
|
||
- **Notifications**: `KeyDown` (event, no virtual method).
|
||
- **Cancellation**: `key.Handled = true`.
|
||
- **Context**: `Key` provides key details.
|
||
- **Default Behavior**: None; relies on subscribers (e.g., `Top` view processing).
|
||
- **Use Case**: Allows global key bindings (e.g., `Ctrl+Q` to quit) or cancellation of default view handling.
|
||
|
||
### 5. OrientationHelper: Property Changes
|
||
|
||
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
|
||
{
|
||
get => _orientation;
|
||
set
|
||
{
|
||
if (_orientation == value)
|
||
{
|
||
return;
|
||
}
|
||
var oldOrientation = _orientation;
|
||
var args = new CancelEventArgs<Orientation>(_orientation, ref value);
|
||
if (OnOrientationChanging(args))
|
||
{
|
||
return; // Cancel if true
|
||
}
|
||
OrientationChanging?.Invoke(this, args);
|
||
if (args.Cancel)
|
||
{
|
||
return;
|
||
}
|
||
_orientation = value;
|
||
var changedArgs = new EventArgs<Orientation>(oldOrientation, _orientation);
|
||
OnOrientationChanged(changedArgs);
|
||
OrientationChanged?.Invoke(this, changedArgs);
|
||
}
|
||
}
|
||
```
|
||
- **Workflow**: Event-driven, processing one property change.
|
||
- **Notifications**: `OnOrientationChanging` (virtual), `OrientationChanging` (event), `OnOrientationChanged`, `OrientationChanged` (post-event).
|
||
- **Cancellation**: `OnOrientationChanging` returning `true` or `args.Cancel = true`.
|
||
- **Context**: `CancelEventArgs<Orientation>` provides old and new values.
|
||
- **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. |