Refactor Application.Keyboard to support fully decoupled state and parallelizable unit tests (#4316)

* Initial plan

* Refactor keyboard handling to IKeyboard interface

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Add parallelizable keyboard tests and fix lazy initialization

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Fix keyboard settings preservation during Init

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Migrate all KeyboardTests to parallelizable and delete old non-parallelizable tests

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Decouple Keyboard from static Application class by adding IApplication reference

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Fix coding standards: use explicit types and target-typed new() properly

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Changes before error encountered

Co-authored-by: tig <585482+tig@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tig <585482+tig@users.noreply.github.com>
This commit is contained in:
Copilot
2025-10-24 15:24:50 -06:00
committed by GitHub
parent 4b193ecb0d
commit 4437398508
12 changed files with 1206 additions and 868 deletions

View File

@@ -144,8 +144,6 @@ public static partial class Application // Initialization (Init/Shutdown)
Debug.Assert (Popover is null); Debug.Assert (Popover is null);
Popover = new (); Popover = new ();
AddKeyBindings ();
try try
{ {
MainLoop = Driver!.Init (); MainLoop = Driver!.Init ();

View File

@@ -4,6 +4,16 @@ namespace Terminal.Gui.App;
public static partial class Application // Keyboard handling public static partial class Application // Keyboard handling
{ {
/// <summary>
/// Static reference to the current <see cref="IApplication"/> <see cref="IKeyboard"/>.
/// </summary>
public static IKeyboard Keyboard
{
get => ApplicationImpl.Instance.Keyboard;
set => ApplicationImpl.Instance.Keyboard = value ??
throw new ArgumentNullException(nameof(value));
}
/// <summary> /// <summary>
/// Called when the user presses a key (by the <see cref="IConsoleDriver"/>). Raises the cancelable /// Called when the user presses a key (by the <see cref="IConsoleDriver"/>). Raises the cancelable
/// <see cref="KeyDown"/> event, then calls <see cref="View.NewKeyDownEvent"/> on all top level views, and finally /// <see cref="KeyDown"/> event, then calls <see cref="View.NewKeyDownEvent"/> on all top level views, and finally
@@ -12,63 +22,7 @@ public static partial class Application // Keyboard handling
/// <remarks>Can be used to simulate key press events.</remarks> /// <remarks>Can be used to simulate key press events.</remarks>
/// <param name="key"></param> /// <param name="key"></param>
/// <returns><see langword="true"/> if the key was handled.</returns> /// <returns><see langword="true"/> if the key was handled.</returns>
public static bool RaiseKeyDownEvent (Key key) public static bool RaiseKeyDownEvent (Key key) => Keyboard.RaiseKeyDownEvent (key);
{
Logging.Debug ($"{key}");
// TODO: Add a way to ignore certain keys, esp for debugging.
//#if DEBUG
// if (key == Key.Empty.WithAlt || key == Key.Empty.WithCtrl)
// {
// Logging.Debug ($"Ignoring {key}");
// return false;
// }
//#endif
// TODO: This should match standard event patterns
KeyDown?.Invoke (null, key);
if (key.Handled)
{
return true;
}
if (Popover?.DispatchKeyDown (key) is true)
{
return true;
}
if (Top is null)
{
foreach (Toplevel topLevel in TopLevels.ToList ())
{
if (topLevel.NewKeyDownEvent (key))
{
return true;
}
if (topLevel.Modal)
{
break;
}
}
}
else
{
if (Top.NewKeyDownEvent (key))
{
return true;
}
}
bool? commandHandled = InvokeCommandsBoundToKey (key);
if(commandHandled is true)
{
return true;
}
return false;
}
/// <summary> /// <summary>
/// Invokes any commands bound at the Application-level to <paramref name="key"/>. /// Invokes any commands bound at the Application-level to <paramref name="key"/>.
@@ -79,38 +33,7 @@ public static partial class Application // Keyboard handling
/// <see langword="false"/> if the command was invoked and was not handled (or cancelled); input processing should continue. /// <see langword="false"/> if the command was invoked and was not handled (or cancelled); input processing should continue.
/// <see langword="true"/> if the command was invoked the command was handled (or cancelled); input processing should stop. /// <see langword="true"/> if the command was invoked the command was handled (or cancelled); input processing should stop.
/// </returns> /// </returns>
public static bool? InvokeCommandsBoundToKey (Key key) public static bool? InvokeCommandsBoundToKey (Key key) => Keyboard.InvokeCommandsBoundToKey (key);
{
bool? handled = null;
// Invoke any Application-scoped KeyBindings.
// The first view that handles the key will stop the loop.
// foreach (KeyValuePair<Key, KeyBinding> binding in KeyBindings.GetBindings (key))
if (KeyBindings.TryGet (key, out KeyBinding binding))
{
if (binding.Target is { })
{
if (!binding.Target.Enabled)
{
return null;
}
handled = binding.Target?.InvokeCommands (binding.Commands, binding);
}
else
{
bool? toReturn = null;
foreach (Command command in binding.Commands)
{
toReturn = InvokeCommand (command, key, binding);
}
handled = toReturn ?? true;
}
}
return handled;
}
/// <summary> /// <summary>
/// Invokes an Application-bound command. /// Invokes an Application-bound command.
@@ -124,24 +47,7 @@ public static partial class Application // Keyboard handling
/// <see langword="true"/> if the command was invoked the command was handled (or cancelled); input processing should stop. /// <see langword="true"/> if the command was invoked the command was handled (or cancelled); input processing should stop.
/// </returns> /// </returns>
/// <exception cref="NotSupportedException"></exception> /// <exception cref="NotSupportedException"></exception>
public static bool? InvokeCommand (Command command, Key key, KeyBinding binding) public static bool? InvokeCommand (Command command, Key key, KeyBinding binding) => Keyboard.InvokeCommand (command, key, binding);
{
if (!_commandImplementations!.ContainsKey (command))
{
throw new NotSupportedException (
@$"A KeyBinding was set up for the command {command} ({key}) but that command is not supported by Application."
);
}
if (_commandImplementations.TryGetValue (command, out View.CommandImplementation? implementation))
{
CommandContext<KeyBinding> context = new (command, null, binding); // Create the context here
return implementation (context);
}
return null;
}
/// <summary> /// <summary>
/// Raised when the user presses a key. /// Raised when the user presses a key.
@@ -155,7 +61,11 @@ public static partial class Application // Keyboard handling
/// <see cref="KeyDown"/> and <see cref="KeyUp"/> events. /// <see cref="KeyDown"/> and <see cref="KeyUp"/> events.
/// <para>Fired after <see cref="KeyDown"/> and before <see cref="KeyUp"/>.</para> /// <para>Fired after <see cref="KeyDown"/> and before <see cref="KeyUp"/>.</para>
/// </remarks> /// </remarks>
public static event EventHandler<Key>? KeyDown; public static event EventHandler<Key>? KeyDown
{
add => Keyboard.KeyDown += value;
remove => Keyboard.KeyDown -= value;
}
/// <summary> /// <summary>
/// Called when the user releases a key (by the <see cref="IConsoleDriver"/>). Raises the cancelable /// Called when the user releases a key (by the <see cref="IConsoleDriver"/>). Raises the cancelable
@@ -166,168 +76,16 @@ public static partial class Application // Keyboard handling
/// <remarks>Can be used to simulate key release events.</remarks> /// <remarks>Can be used to simulate key release events.</remarks>
/// <param name="key"></param> /// <param name="key"></param>
/// <returns><see langword="true"/> if the key was handled.</returns> /// <returns><see langword="true"/> if the key was handled.</returns>
public static bool RaiseKeyUpEvent (Key key) public static bool RaiseKeyUpEvent (Key key) => Keyboard.RaiseKeyUpEvent (key);
{
if (!Initialized)
{
return true;
}
KeyUp?.Invoke (null, key);
if (key.Handled)
{
return true;
}
// TODO: Add Popover support
foreach (Toplevel topLevel in TopLevels.ToList ())
{
if (topLevel.NewKeyUpEvent (key))
{
return true;
}
if (topLevel.Modal)
{
break;
}
}
return false;
}
#region Application-scoped KeyBindings
static Application ()
{
AddKeyBindings ();
}
/// <summary>Gets the Application-scoped key bindings.</summary> /// <summary>Gets the Application-scoped key bindings.</summary>
public static KeyBindings KeyBindings { get; internal set; } = new (null); public static KeyBindings KeyBindings => Keyboard.KeyBindings;
internal static void AddKeyBindings () internal static void AddKeyBindings ()
{ {
_commandImplementations.Clear (); if (Keyboard is Keyboard keyboard)
// Things Application knows how to do
AddCommand (
Command.Quit,
static () =>
{
RequestStop ();
return true;
}
);
AddCommand (
Command.Suspend,
static () =>
{
Driver?.Suspend ();
return true;
}
);
AddCommand (
Command.NextTabStop,
static () => Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop));
AddCommand (
Command.PreviousTabStop,
static () => Navigation?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop));
AddCommand (
Command.NextTabGroup,
static () => Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup));
AddCommand (
Command.PreviousTabGroup,
static () => Navigation?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabGroup));
AddCommand (
Command.Refresh,
static () =>
{
LayoutAndDraw (true);
return true;
}
);
AddCommand (
Command.Arrange,
static () =>
{
View? viewToArrange = Navigation?.GetFocused ();
// Go up the superview hierarchy and find the first that is not ViewArrangement.Fixed
while (viewToArrange is { SuperView: { }, Arrangement: ViewArrangement.Fixed })
{
viewToArrange = viewToArrange.SuperView;
}
if (viewToArrange is { })
{
return viewToArrange.Border?.EnterArrangeMode (ViewArrangement.Fixed);
}
return false;
});
//SetKeysToHardCodedDefaults ();
// Need to clear after setting the above to ensure actually clear
// because set_QuitKey etc.. may call Add
KeyBindings.Clear ();
KeyBindings.Add (QuitKey, Command.Quit);
KeyBindings.Add (NextTabKey, Command.NextTabStop);
KeyBindings.Add (PrevTabKey, Command.PreviousTabStop);
KeyBindings.Add (NextTabGroupKey, Command.NextTabGroup);
KeyBindings.Add (PrevTabGroupKey, Command.PreviousTabGroup);
KeyBindings.Add (ArrangeKey, Command.Arrange);
KeyBindings.Add (Key.CursorRight, Command.NextTabStop);
KeyBindings.Add (Key.CursorDown, Command.NextTabStop);
KeyBindings.Add (Key.CursorLeft, Command.PreviousTabStop);
KeyBindings.Add (Key.CursorUp, Command.PreviousTabStop);
// TODO: Refresh Key should be configurable
KeyBindings.Add (Key.F5, Command.Refresh);
// TODO: Suspend Key should be configurable
if (Environment.OSVersion.Platform == PlatformID.Unix)
{ {
KeyBindings.Add (Key.Z.WithCtrl, Command.Suspend); keyboard.AddKeyBindings ();
} }
} }
#endregion Application-scoped KeyBindings
/// <summary>
/// <para>
/// Sets the function that will be invoked for a <see cref="Command"/>.
/// </para>
/// <para>
/// If AddCommand has already been called for <paramref name="command"/> <paramref name="f"/> will
/// replace the old one.
/// </para>
/// </summary>
/// <remarks>
/// <para>
/// This version of AddCommand is for commands that do not require a <see cref="ICommandContext"/>.
/// </para>
/// </remarks>
/// <param name="command">The command.</param>
/// <param name="f">The function.</param>
private static void AddCommand (Command command, Func<bool?> f) { _commandImplementations! [command] = ctx => f (); }
/// <summary>
/// Commands for Application.
/// </summary>
private static readonly Dictionary<Command, View.CommandImplementation> _commandImplementations = new ();
} }

View File

@@ -9,42 +9,22 @@ public static partial class Application // Navigation stuff
/// </summary> /// </summary>
public static ApplicationNavigation? Navigation { get; internal set; } public static ApplicationNavigation? Navigation { get; internal set; }
private static Key _nextTabGroupKey = Key.F6; // Resources/config.json overrides
private static Key _nextTabKey = Key.Tab; // Resources/config.json overrides
private static Key _prevTabGroupKey = Key.F6.WithShift; // Resources/config.json overrides
private static Key _prevTabKey = Key.Tab.WithShift; // Resources/config.json overrides
/// <summary>Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.</summary> /// <summary>Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.</summary>
[ConfigurationProperty (Scope = typeof (SettingsScope))] [ConfigurationProperty (Scope = typeof (SettingsScope))]
public static Key NextTabGroupKey public static Key NextTabGroupKey
{ {
get => _nextTabGroupKey; get => Keyboard.NextTabGroupKey;
set set => Keyboard.NextTabGroupKey = value;
{
//if (_nextTabGroupKey != value)
{
KeyBindings.Replace (_nextTabGroupKey, value);
_nextTabGroupKey = value;
}
}
} }
/// <summary>Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.</summary> /// <summary>Alternative key to navigate forwards through views. Tab is the primary key.</summary>
[ConfigurationProperty (Scope = typeof (SettingsScope))] [ConfigurationProperty (Scope = typeof (SettingsScope))]
public static Key NextTabKey public static Key NextTabKey
{ {
get => _nextTabKey; get => Keyboard.NextTabKey;
set set => Keyboard.NextTabKey = value;
{
//if (_nextTabKey != value)
{
KeyBindings.Replace (_nextTabKey, value);
_nextTabKey = value;
}
}
} }
/// <summary> /// <summary>
/// Raised when the user releases a key. /// Raised when the user releases a key.
/// <para> /// <para>
@@ -57,34 +37,25 @@ public static partial class Application // Navigation stuff
/// <see cref="KeyDown"/> and <see cref="KeyUp"/> events. /// <see cref="KeyDown"/> and <see cref="KeyUp"/> events.
/// <para>Fired after <see cref="KeyDown"/>.</para> /// <para>Fired after <see cref="KeyDown"/>.</para>
/// </remarks> /// </remarks>
public static event EventHandler<Key>? KeyUp; public static event EventHandler<Key>? KeyUp
/// <summary>Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key.</summary>
[ConfigurationProperty (Scope = typeof (SettingsScope))]
public static Key PrevTabGroupKey
{ {
get => _prevTabGroupKey; add => Keyboard.KeyUp += value;
set remove => Keyboard.KeyUp -= value;
{
//if (_prevTabGroupKey != value)
{
KeyBindings.Replace (_prevTabGroupKey, value);
_prevTabGroupKey = value;
}
}
} }
/// <summary>Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key.</summary> /// <summary>Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key.</summary>
[ConfigurationProperty (Scope = typeof (SettingsScope))] [ConfigurationProperty (Scope = typeof (SettingsScope))]
public static Key PrevTabGroupKey
{
get => Keyboard.PrevTabGroupKey;
set => Keyboard.PrevTabGroupKey = value;
}
/// <summary>Alternative key to navigate backwards through views. Shift+Tab is the primary key.</summary>
[ConfigurationProperty (Scope = typeof (SettingsScope))]
public static Key PrevTabKey public static Key PrevTabKey
{ {
get => _prevTabKey; get => Keyboard.PrevTabKey;
set set => Keyboard.PrevTabKey = value;
{
//if (_prevTabKey != value)
{
KeyBindings.Replace (_prevTabKey, value);
_prevTabKey = value;
}
}
} }
} }

View File

@@ -6,38 +6,20 @@ namespace Terminal.Gui.App;
public static partial class Application // Run (Begin, Run, End, Stop) public static partial class Application // Run (Begin, Run, End, Stop)
{ {
private static Key _quitKey = Key.Esc; // Resources/config.json overrides
/// <summary>Gets or sets the key to quit the application.</summary> /// <summary>Gets or sets the key to quit the application.</summary>
[ConfigurationProperty (Scope = typeof (SettingsScope))] [ConfigurationProperty (Scope = typeof (SettingsScope))]
public static Key QuitKey public static Key QuitKey
{ {
get => _quitKey; get => Keyboard.QuitKey;
set set => Keyboard.QuitKey = value;
{
//if (_quitKey != value)
{
KeyBindings.Replace (_quitKey, value);
_quitKey = value;
}
}
} }
private static Key _arrangeKey = Key.F5.WithCtrl; // Resources/config.json overrides
/// <summary>Gets or sets the key to activate arranging views using the keyboard.</summary> /// <summary>Gets or sets the key to activate arranging views using the keyboard.</summary>
[ConfigurationProperty (Scope = typeof (SettingsScope))] [ConfigurationProperty (Scope = typeof (SettingsScope))]
public static Key ArrangeKey public static Key ArrangeKey
{ {
get => _arrangeKey; get => Keyboard.ArrangeKey;
set set => Keyboard.ArrangeKey = value;
{
//if (_arrangeKey != value)
{
KeyBindings.Replace (_arrangeKey, value);
_arrangeKey = value;
}
}
} }
// When `End ()` is called, it is possible `RunState.Toplevel` is a different object than `Top`. // When `End ()` is called, it is possible `RunState.Toplevel` is a different object than `Top`.

View File

@@ -243,6 +243,7 @@ public static partial class Application
NotifyNewRunState = null; NotifyNewRunState = null;
NotifyStopRunState = null; NotifyStopRunState = null;
MouseGrabHandler = new MouseGrabHandler (); MouseGrabHandler = new MouseGrabHandler ();
// Keyboard will be lazy-initialized in ApplicationImpl on next access
Initialized = false; Initialized = false;
// Mouse // Mouse
@@ -252,16 +253,12 @@ public static partial class Application
CachedViewsUnderMouse.Clear (); CachedViewsUnderMouse.Clear ();
MouseEvent = null; MouseEvent = null;
// Keyboard // Keyboard events and bindings are now managed by the Keyboard instance
KeyDown = null;
KeyUp = null;
SizeChanging = null; SizeChanging = null;
Navigation = null; Navigation = null;
KeyBindings.Clear ();
AddKeyBindings ();
// Reset synchronization context to allow the user to run async/await, // Reset synchronization context to allow the user to run async/await,
// as the main loop has been ended, the synchronization context from // as the main loop has been ended, the synchronization context from
// gui.cs does no longer process any callbacks. See #1084 for more details: // gui.cs does no longer process any callbacks. See #1084 for more details:

View File

@@ -37,6 +37,65 @@ public class ApplicationImpl : IApplication
/// </summary> /// </summary>
public IMouseGrabHandler MouseGrabHandler { get; set; } = new MouseGrabHandler (); public IMouseGrabHandler MouseGrabHandler { get; set; } = new MouseGrabHandler ();
private IKeyboard? _keyboard;
/// <summary>
/// Handles keyboard input and key bindings at the Application level
/// </summary>
public IKeyboard Keyboard
{
get
{
if (_keyboard is null)
{
_keyboard = new Keyboard { Application = this };
}
return _keyboard;
}
set => _keyboard = value ?? throw new ArgumentNullException (nameof (value));
}
/// <inheritdoc/>
public IConsoleDriver? Driver
{
get => Application.Driver;
set => Application.Driver = value;
}
/// <inheritdoc/>
public bool Initialized
{
get => Application.Initialized;
set => Application.Initialized = value;
}
/// <inheritdoc/>
public ApplicationPopover? Popover
{
get => Application.Popover;
set => Application.Popover = value;
}
/// <inheritdoc/>
public ApplicationNavigation? Navigation
{
get => Application.Navigation;
set => Application.Navigation = value;
}
/// <inheritdoc/>
public Toplevel? Top
{
get => Application.Top;
set => Application.Top = value;
}
/// <inheritdoc/>
public ConcurrentStack<Toplevel> TopLevels => Application.TopLevels;
/// <inheritdoc/>
public void RequestStop () => Application.RequestStop ();
/// <summary> /// <summary>
/// Creates a new instance of the Application backend. /// Creates a new instance of the Application backend.
/// </summary> /// </summary>
@@ -88,7 +147,28 @@ public class ApplicationImpl : IApplication
Debug.Assert (Application.Popover is null); Debug.Assert (Application.Popover is null);
Application.Popover = new (); Application.Popover = new ();
Application.AddKeyBindings (); // Preserve existing keyboard settings if they exist
bool hasExistingKeyboard = _keyboard is not null;
Key existingQuitKey = _keyboard?.QuitKey ?? Key.Esc;
Key existingArrangeKey = _keyboard?.ArrangeKey ?? Key.F5.WithCtrl;
Key existingNextTabKey = _keyboard?.NextTabKey ?? Key.Tab;
Key existingPrevTabKey = _keyboard?.PrevTabKey ?? Key.Tab.WithShift;
Key existingNextTabGroupKey = _keyboard?.NextTabGroupKey ?? Key.F6;
Key existingPrevTabGroupKey = _keyboard?.PrevTabGroupKey ?? Key.F6.WithShift;
// Reset keyboard to ensure fresh state with default bindings
_keyboard = new Keyboard { Application = this };
// Restore previously set keys if they existed and were different from defaults
if (hasExistingKeyboard)
{
_keyboard.QuitKey = existingQuitKey;
_keyboard.ArrangeKey = existingArrangeKey;
_keyboard.NextTabKey = existingNextTabKey;
_keyboard.PrevTabKey = existingPrevTabKey;
_keyboard.NextTabGroupKey = existingNextTabGroupKey;
_keyboard.PrevTabGroupKey = existingPrevTabGroupKey;
}
CreateDriver (driverName ?? _driverName); CreateDriver (driverName ?? _driverName);
@@ -97,7 +177,7 @@ public class ApplicationImpl : IApplication
Application.OnInitializedChanged (this, new (true)); Application.OnInitializedChanged (this, new (true));
Application.SubscribeDriverEvents (); Application.SubscribeDriverEvents ();
SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext ()); SynchronizationContext.SetSynchronizationContext (new ());
Application.MainThreadId = Thread.CurrentThread.ManagedThreadId; Application.MainThreadId = Thread.CurrentThread.ManagedThreadId;
} }
@@ -217,7 +297,7 @@ public class ApplicationImpl : IApplication
Init (driver, null); Init (driver, null);
} }
var top = new T (); T top = new ();
Run (top, errorHandler); Run (top, errorHandler);
return top; return top;
} }
@@ -276,6 +356,7 @@ public class ApplicationImpl : IApplication
} }
Application.Driver = null; Application.Driver = null;
_keyboard = null;
_lazyInstance = new (() => new ApplicationImpl ()); _lazyInstance = new (() => new ApplicationImpl ());
} }
@@ -291,7 +372,7 @@ public class ApplicationImpl : IApplication
return; return;
} }
var ev = new ToplevelClosingEventArgs (top); ToplevelClosingEventArgs ev = new (top);
top.OnClosing (ev); top.OnClosing (ev);
if (ev.Cancel) if (ev.Cancel)

View File

@@ -20,6 +20,36 @@ public interface IApplication
/// </summary> /// </summary>
IMouseGrabHandler MouseGrabHandler { get; set; } IMouseGrabHandler MouseGrabHandler { get; set; }
/// <summary>
/// Handles keyboard input and key bindings at the Application level.
/// </summary>
IKeyboard Keyboard { get; set; }
/// <summary>Gets or sets the console driver being used.</summary>
IConsoleDriver? Driver { get; set; }
/// <summary>Gets or sets whether the application has been initialized.</summary>
bool Initialized { get; set; }
/// <summary>Gets or sets the popover manager.</summary>
ApplicationPopover? Popover { get; set; }
/// <summary>Gets or sets the navigation manager.</summary>
ApplicationNavigation? Navigation { get; set; }
/// <summary>Gets the currently active Toplevel.</summary>
Toplevel? Top { get; set; }
/// <summary>Gets the stack of all Toplevels.</summary>
System.Collections.Concurrent.ConcurrentStack<Toplevel> TopLevels { get; }
/// <summary>Requests that the application stop running.</summary>
void RequestStop ();
/// <summary>Forces all views to be laid out and drawn.</summary>
/// <param name="clearScreen">If true, clears the screen before drawing.</param>
void LayoutAndDraw (bool clearScreen = false);
/// <summary>Initializes a new instance of <see cref="Terminal.Gui"/> Application.</summary> /// <summary>Initializes a new instance of <see cref="Terminal.Gui"/> Application.</summary>
/// <para>Call this method once per instance (or after <see cref="Shutdown"/> has been called).</para> /// <para>Call this method once per instance (or after <see cref="Shutdown"/> has been called).</para>
/// <para> /// <para>

View File

@@ -0,0 +1,113 @@
#nullable enable
namespace Terminal.Gui.App;
/// <summary>
/// Defines a contract for managing keyboard input and key bindings at the Application level.
/// <para>
/// This interface decouples keyboard handling state from the static <see cref="Application"/> class,
/// enabling parallelizable unit tests and better testability.
/// </para>
/// </summary>
public interface IKeyboard
{
/// <summary>
/// Sets the application instance that this keyboard handler is associated with.
/// This provides access to application state without coupling to static Application class.
/// </summary>
IApplication? Application { get; set; }
/// <summary>
/// Called when the user presses a key (by the <see cref="IConsoleDriver"/>). Raises the cancelable
/// <see cref="KeyDown"/> event, then calls <see cref="View.NewKeyDownEvent"/> on all top level views, and finally
/// if the key was not handled, invokes any Application-scoped <see cref="KeyBindings"/>.
/// </summary>
/// <remarks>Can be used to simulate key press events.</remarks>
/// <param name="key"></param>
/// <returns><see langword="true"/> if the key was handled.</returns>
bool RaiseKeyDownEvent (Key key);
/// <summary>
/// Called when the user releases a key (by the <see cref="IConsoleDriver"/>). Raises the cancelable
/// <see cref="KeyUp"/>
/// event
/// then calls <see cref="View.NewKeyUpEvent"/> on all top level views. Called after <see cref="RaiseKeyDownEvent"/>.
/// </summary>
/// <remarks>Can be used to simulate key release events.</remarks>
/// <param name="key"></param>
/// <returns><see langword="true"/> if the key was handled.</returns>
bool RaiseKeyUpEvent (Key key);
/// <summary>
/// Invokes any commands bound at the Application-level to <paramref name="key"/>.
/// </summary>
/// <param name="key"></param>
/// <returns>
/// <see langword="null"/> if no command was found; input processing should continue.
/// <see langword="false"/> if the command was invoked and was not handled (or cancelled); input processing should continue.
/// <see langword="true"/> if the command was invoked the command was handled (or cancelled); input processing should stop.
/// </returns>
bool? InvokeCommandsBoundToKey (Key key);
/// <summary>
/// Invokes an Application-bound command.
/// </summary>
/// <param name="command">The Command to invoke</param>
/// <param name="key">The Application-bound Key that was pressed.</param>
/// <param name="binding">Describes the binding.</param>
/// <returns>
/// <see langword="null"/> if no command was found; input processing should continue.
/// <see langword="false"/> if the command was invoked and was not handled (or cancelled); input processing should continue.
/// <see langword="true"/> if the command was invoked the command was handled (or cancelled); input processing should stop.
/// </returns>
/// <exception cref="NotSupportedException"></exception>
bool? InvokeCommand (Command command, Key key, KeyBinding binding);
/// <summary>
/// Raised when the user presses a key.
/// <para>
/// Set <see cref="Key.Handled"/> to <see langword="true"/> to indicate the key was handled and to prevent
/// additional processing.
/// </para>
/// </summary>
/// <remarks>
/// All drivers support firing the <see cref="KeyDown"/> event. Some drivers (Unix) do not support firing the
/// <see cref="KeyDown"/> and <see cref="KeyUp"/> events.
/// <para>Fired after <see cref="KeyDown"/> and before <see cref="KeyUp"/>.</para>
/// </remarks>
event EventHandler<Key>? KeyDown;
/// <summary>
/// Raised when the user releases a key.
/// <para>
/// Set <see cref="Key.Handled"/> to <see langword="true"/> to indicate the key was handled and to prevent
/// additional processing.
/// </para>
/// </summary>
/// <remarks>
/// All drivers support firing the <see cref="KeyDown"/> event. Some drivers (Unix) do not support firing the
/// <see cref="KeyDown"/> and <see cref="KeyUp"/> events.
/// <para>Fired after <see cref="KeyDown"/>.</para>
/// </remarks>
event EventHandler<Key>? KeyUp;
/// <summary>Gets the Application-scoped key bindings.</summary>
KeyBindings KeyBindings { get; }
/// <summary>Gets or sets the key to quit the application.</summary>
Key QuitKey { get; set; }
/// <summary>Gets or sets the key to activate arranging views using the keyboard.</summary>
Key ArrangeKey { get; set; }
/// <summary>Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.</summary>
Key NextTabGroupKey { get; set; }
/// <summary>Alternative key to navigate forwards through views. Tab is the primary key.</summary>
Key NextTabKey { get; set; }
/// <summary>Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key.</summary>
Key PrevTabGroupKey { get; set; }
/// <summary>Alternative key to navigate backwards through views. Shift+Tab is the primary key.</summary>
Key PrevTabKey { get; set; }
}

View File

@@ -0,0 +1,381 @@
#nullable enable
namespace Terminal.Gui.App;
/// <summary>
/// INTERNAL: Implements <see cref="IKeyboard"/> to manage keyboard input and key bindings at the Application level.
/// <para>
/// This implementation decouples keyboard handling state from the static <see cref="Application"/> class,
/// enabling parallelizable unit tests and better testability.
/// </para>
/// <para>
/// See <see cref="IKeyboard"/> for usage details.
/// </para>
/// </summary>
internal class Keyboard : IKeyboard
{
private Key _quitKey = Key.Esc; // Resources/config.json overrides
private Key _arrangeKey = Key.F5.WithCtrl; // Resources/config.json overrides
private Key _nextTabGroupKey = Key.F6; // Resources/config.json overrides
private Key _nextTabKey = Key.Tab; // Resources/config.json overrides
private Key _prevTabGroupKey = Key.F6.WithShift; // Resources/config.json overrides
private Key _prevTabKey = Key.Tab.WithShift; // Resources/config.json overrides
/// <summary>
/// Commands for Application.
/// </summary>
private readonly Dictionary<Command, View.CommandImplementation> _commandImplementations = new ();
/// <inheritdoc/>
public IApplication? Application { get; set; }
/// <inheritdoc/>
public KeyBindings KeyBindings { get; internal set; } = new (null);
/// <inheritdoc/>
public Key QuitKey
{
get => _quitKey;
set
{
KeyBindings.Replace (_quitKey, value);
_quitKey = value;
}
}
/// <inheritdoc/>
public Key ArrangeKey
{
get => _arrangeKey;
set
{
KeyBindings.Replace (_arrangeKey, value);
_arrangeKey = value;
}
}
/// <inheritdoc/>
public Key NextTabGroupKey
{
get => _nextTabGroupKey;
set
{
KeyBindings.Replace (_nextTabGroupKey, value);
_nextTabGroupKey = value;
}
}
/// <inheritdoc/>
public Key NextTabKey
{
get => _nextTabKey;
set
{
KeyBindings.Replace (_nextTabKey, value);
_nextTabKey = value;
}
}
/// <inheritdoc/>
public Key PrevTabGroupKey
{
get => _prevTabGroupKey;
set
{
KeyBindings.Replace (_prevTabGroupKey, value);
_prevTabGroupKey = value;
}
}
/// <inheritdoc/>
public Key PrevTabKey
{
get => _prevTabKey;
set
{
KeyBindings.Replace (_prevTabKey, value);
_prevTabKey = value;
}
}
/// <inheritdoc/>
public event EventHandler<Key>? KeyDown;
/// <inheritdoc/>
public event EventHandler<Key>? KeyUp;
/// <summary>
/// Initializes keyboard bindings.
/// </summary>
public Keyboard ()
{
AddKeyBindings ();
}
/// <inheritdoc/>
public bool RaiseKeyDownEvent (Key key)
{
Logging.Debug ($"{key}");
// TODO: Add a way to ignore certain keys, esp for debugging.
//#if DEBUG
// if (key == Key.Empty.WithAlt || key == Key.Empty.WithCtrl)
// {
// Logging.Debug ($"Ignoring {key}");
// return false;
// }
//#endif
// TODO: This should match standard event patterns
KeyDown?.Invoke (null, key);
if (key.Handled)
{
return true;
}
if (Application?.Popover?.DispatchKeyDown (key) is true)
{
return true;
}
if (Application?.Top is null)
{
if (Application?.TopLevels is { })
{
foreach (Toplevel topLevel in Application.TopLevels.ToList ())
{
if (topLevel.NewKeyDownEvent (key))
{
return true;
}
if (topLevel.Modal)
{
break;
}
}
}
}
else
{
if (Application.Top.NewKeyDownEvent (key))
{
return true;
}
}
bool? commandHandled = InvokeCommandsBoundToKey (key);
if(commandHandled is true)
{
return true;
}
return false;
}
/// <inheritdoc/>
public bool RaiseKeyUpEvent (Key key)
{
if (Application?.Initialized != true)
{
return true;
}
KeyUp?.Invoke (null, key);
if (key.Handled)
{
return true;
}
// TODO: Add Popover support
if (Application?.TopLevels is { })
{
foreach (Toplevel topLevel in Application.TopLevels.ToList ())
{
if (topLevel.NewKeyUpEvent (key))
{
return true;
}
if (topLevel.Modal)
{
break;
}
}
}
return false;
}
/// <inheritdoc/>
public bool? InvokeCommandsBoundToKey (Key key)
{
bool? handled = null;
// Invoke any Application-scoped KeyBindings.
// The first view that handles the key will stop the loop.
// foreach (KeyValuePair<Key, KeyBinding> binding in KeyBindings.GetBindings (key))
if (KeyBindings.TryGet (key, out KeyBinding binding))
{
if (binding.Target is { })
{
if (!binding.Target.Enabled)
{
return null;
}
handled = binding.Target?.InvokeCommands (binding.Commands, binding);
}
else
{
bool? toReturn = null;
foreach (Command command in binding.Commands)
{
toReturn = InvokeCommand (command, key, binding);
}
handled = toReturn ?? true;
}
}
return handled;
}
/// <inheritdoc/>
public bool? InvokeCommand (Command command, Key key, KeyBinding binding)
{
if (!_commandImplementations.ContainsKey (command))
{
throw new NotSupportedException (
@$"A KeyBinding was set up for the command {command} ({key}) but that command is not supported by Application."
);
}
if (_commandImplementations.TryGetValue (command, out View.CommandImplementation? implementation))
{
CommandContext<KeyBinding> context = new (command, null, binding); // Create the context here
return implementation (context);
}
return null;
}
/// <summary>
/// <para>
/// Sets the function that will be invoked for a <see cref="Command"/>.
/// </para>
/// <para>
/// If AddCommand has already been called for <paramref name="command"/> <paramref name="f"/> will
/// replace the old one.
/// </para>
/// </summary>
/// <remarks>
/// <para>
/// This version of AddCommand is for commands that do not require a <see cref="ICommandContext"/>.
/// </para>
/// </remarks>
/// <param name="command">The command.</param>
/// <param name="f">The function.</param>
private void AddCommand (Command command, Func<bool?> f) { _commandImplementations [command] = ctx => f (); }
internal void AddKeyBindings ()
{
_commandImplementations.Clear ();
// Things Application knows how to do
AddCommand (
Command.Quit,
() =>
{
Application?.RequestStop ();
return true;
}
);
AddCommand (
Command.Suspend,
() =>
{
Application?.Driver?.Suspend ();
return true;
}
);
AddCommand (
Command.NextTabStop,
() => Application?.Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop));
AddCommand (
Command.PreviousTabStop,
() => Application?.Navigation?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop));
AddCommand (
Command.NextTabGroup,
() => Application?.Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup));
AddCommand (
Command.PreviousTabGroup,
() => Application?.Navigation?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabGroup));
AddCommand (
Command.Refresh,
() =>
{
Application?.LayoutAndDraw (true);
return true;
}
);
AddCommand (
Command.Arrange,
() =>
{
View? viewToArrange = Application?.Navigation?.GetFocused ();
// Go up the superview hierarchy and find the first that is not ViewArrangement.Fixed
while (viewToArrange is { SuperView: { }, Arrangement: ViewArrangement.Fixed })
{
viewToArrange = viewToArrange.SuperView;
}
if (viewToArrange is { })
{
return viewToArrange.Border?.EnterArrangeMode (ViewArrangement.Fixed);
}
return false;
});
//SetKeysToHardCodedDefaults ();
// Need to clear after setting the above to ensure actually clear
// because set_QuitKey etc.. may call Add
KeyBindings.Clear ();
KeyBindings.Add (QuitKey, Command.Quit);
KeyBindings.Add (NextTabKey, Command.NextTabStop);
KeyBindings.Add (PrevTabKey, Command.PreviousTabStop);
KeyBindings.Add (NextTabGroupKey, Command.NextTabGroup);
KeyBindings.Add (PrevTabGroupKey, Command.PreviousTabGroup);
KeyBindings.Add (ArrangeKey, Command.Arrange);
KeyBindings.Add (Key.CursorRight, Command.NextTabStop);
KeyBindings.Add (Key.CursorDown, Command.NextTabStop);
KeyBindings.Add (Key.CursorLeft, Command.PreviousTabStop);
KeyBindings.Add (Key.CursorUp, Command.PreviousTabStop);
// TODO: Refresh Key should be configurable
KeyBindings.Add (Key.F5, Command.Refresh);
// TODO: Suspend Key should be configurable
if (Environment.OSVersion.Platform == PlatformID.Unix)
{
KeyBindings.Add (Key.Z.WithCtrl, Command.Suspend);
}
}
}

View File

@@ -1,518 +0,0 @@
using UnitTests;
using Xunit.Abstractions;
namespace UnitTests.ApplicationTests;
/// <summary>
/// Application tests for keyboard support.
/// </summary>
public class KeyboardTests
{
public KeyboardTests (ITestOutputHelper output)
{
_output = output;
#if DEBUG_IDISPOSABLE
View.Instances.Clear ();
RunState.Instances.Clear ();
#endif
}
private readonly ITestOutputHelper _output;
private object _timeoutLock;
[Fact]
[AutoInitShutdown]
public void KeyBindings_Add_Adds ()
{
Application.KeyBindings.Add (Key.A, Command.Accept);
Application.KeyBindings.Add (Key.B, Command.Accept);
Assert.True (Application.KeyBindings.TryGet (Key.A, out KeyBinding binding));
Assert.Null (binding.Target);
Assert.True (Application.KeyBindings.TryGet (Key.B, out binding));
Assert.Null (binding.Target);
}
[Fact]
[AutoInitShutdown]
public void KeyBindings_Remove_Removes ()
{
Application.KeyBindings.Add (Key.A, Command.Accept);
Assert.True (Application.KeyBindings.TryGet (Key.A, out _));
Application.KeyBindings.Remove (Key.A);
Assert.False (Application.KeyBindings.TryGet (Key.A, out _));
}
[Fact]
public void KeyBindings_OnKeyDown ()
{
Application.Top = new ();
var view = new ScopedKeyBindingView ();
var keyWasHandled = false;
view.KeyDownNotHandled += (s, e) => keyWasHandled = true;
Application.Top.Add (view);
Application.RaiseKeyDownEvent (Key.A);
Assert.False (keyWasHandled);
Assert.True (view.ApplicationCommand);
keyWasHandled = false;
view.ApplicationCommand = false;
Application.KeyBindings.Remove (KeyCode.A);
Application.RaiseKeyDownEvent (Key.A); // old
Assert.False (keyWasHandled);
Assert.False (view.ApplicationCommand);
Application.KeyBindings.Add (Key.A.WithCtrl, view, Command.Save);
Application.RaiseKeyDownEvent (Key.A); // old
Assert.False (keyWasHandled);
Assert.False (view.ApplicationCommand);
Application.RaiseKeyDownEvent (Key.A.WithCtrl); // new
Assert.False (keyWasHandled);
Assert.True (view.ApplicationCommand);
keyWasHandled = false;
Application.RaiseKeyDownEvent (Key.H);
Assert.False (keyWasHandled);
Assert.True (view.HotKeyCommand);
keyWasHandled = false;
Assert.False (view.HasFocus);
Application.RaiseKeyDownEvent (Key.F);
Assert.False (keyWasHandled);
Assert.True (view.ApplicationCommand);
Assert.True (view.HotKeyCommand);
Assert.False (view.FocusedCommand);
Application.Top.Dispose ();
Application.ResetState (true);
}
[Fact]
[AutoInitShutdown]
public void KeyBindings_OnKeyDown_Negative ()
{
var view = new ScopedKeyBindingView ();
var keyWasHandled = false;
view.KeyDownNotHandled += (s, e) => keyWasHandled = true;
var top = new Toplevel ();
top.Add (view);
Application.Begin (top);
Application.RaiseKeyDownEvent (Key.A.WithCtrl);
Assert.False (keyWasHandled);
Assert.False (view.ApplicationCommand);
Assert.False (view.HotKeyCommand);
Assert.False (view.FocusedCommand);
keyWasHandled = false;
Assert.False (view.HasFocus);
Application.RaiseKeyDownEvent (Key.Z);
Assert.False (keyWasHandled);
Assert.False (view.ApplicationCommand);
Assert.False (view.HotKeyCommand);
Assert.False (view.FocusedCommand);
top.Dispose ();
}
[Fact]
public void NextTabGroupKey_Moves_Focus_To_TabStop_In_Next_TabGroup ()
{
// Arrange
Application.Navigation = new ();
var top = new Toplevel ();
var view1 = new View
{
Id = "view1",
CanFocus = true,
TabStop = TabBehavior.TabGroup
};
var subView1 = new View
{
Id = "subView1",
CanFocus = true,
TabStop = TabBehavior.TabStop
};
view1.Add (subView1);
var view2 = new View
{
Id = "view2",
CanFocus = true,
TabStop = TabBehavior.TabGroup
};
var subView2 = new View
{
Id = "subView2",
CanFocus = true,
TabStop = TabBehavior.TabStop
};
view2.Add (subView2);
top.Add (view1, view2);
Application.Top = top;
view1.SetFocus ();
Assert.True (view1.HasFocus);
Assert.True (subView1.HasFocus);
// Act
Application.RaiseKeyDownEvent (Application.NextTabGroupKey);
// Assert
Assert.True (view2.HasFocus);
Assert.True (subView2.HasFocus);
top.Dispose ();
Application.Navigation = null;
}
[Fact]
[AutoInitShutdown]
public void NextTabGroupKey_PrevTabGroupKey_Tests ()
{
Toplevel top = new (); // TabGroup
var w1 = new Window (); // TabGroup
var v1 = new TextField (); // TabStop
var v2 = new TextView (); // TabStop
w1.Add (v1, v2);
var w2 = new Window (); // TabGroup
var v3 = new CheckBox (); // TabStop
var v4 = new Button (); // TabStop
w2.Add (v3, v4);
top.Add (w1, w2);
Application.Iteration += (s, a) =>
{
Assert.True (v1.HasFocus);
// Across TabGroups
Application.RaiseKeyDownEvent (Key.F6);
Assert.True (v3.HasFocus);
Application.RaiseKeyDownEvent (Key.F6);
Assert.True (v1.HasFocus);
Application.RaiseKeyDownEvent (Key.F6.WithShift);
Assert.True (v3.HasFocus);
Application.RaiseKeyDownEvent (Key.F6.WithShift);
Assert.True (v1.HasFocus);
// Restore?
Application.RaiseKeyDownEvent (Key.Tab);
Assert.True (v2.HasFocus);
Application.RaiseKeyDownEvent (Key.F6);
Assert.True (v3.HasFocus);
Application.RaiseKeyDownEvent (Key.F6);
Assert.True (v2.HasFocus); // previously focused view was preserved
Application.RequestStop ();
};
Application.Run (top);
// Replacing the defaults keys to avoid errors on others unit tests that are using it.
Application.NextTabGroupKey = Key.PageDown.WithCtrl;
Application.PrevTabGroupKey = Key.PageUp.WithCtrl;
Application.QuitKey = Key.Q.WithCtrl;
Assert.Equal (KeyCode.PageDown | KeyCode.CtrlMask, Application.NextTabGroupKey.KeyCode);
Assert.Equal (KeyCode.PageUp | KeyCode.CtrlMask, Application.PrevTabGroupKey.KeyCode);
Assert.Equal (KeyCode.Q | KeyCode.CtrlMask, Application.QuitKey.KeyCode);
top.Dispose ();
// Shutdown must be called to safely clean up Application if Init has been called
Application.Shutdown ();
}
[Fact]
public void NextTabKey_Moves_Focus_To_Next_TabStop ()
{
// Arrange
Application.Navigation = new ();
var top = new Toplevel ();
var view1 = new View { Id = "view1", CanFocus = true };
var view2 = new View { Id = "view2", CanFocus = true };
top.Add (view1, view2);
Application.Top = top;
view1.SetFocus ();
// Act
Application.RaiseKeyDownEvent (Application.NextTabKey);
// Assert
Assert.True (view2.HasFocus);
top.Dispose ();
Application.Navigation = null;
}
[Fact]
public void PrevTabGroupKey_Moves_Focus_To_TabStop_In_Prev_TabGroup ()
{
// Arrange
Application.Navigation = new ();
var top = new Toplevel ();
var view1 = new View
{
Id = "view1",
CanFocus = true,
TabStop = TabBehavior.TabGroup
};
var subView1 = new View
{
Id = "subView1",
CanFocus = true,
TabStop = TabBehavior.TabStop
};
view1.Add (subView1);
var view2 = new View
{
Id = "view2",
CanFocus = true,
TabStop = TabBehavior.TabGroup
};
var subView2 = new View
{
Id = "subView2",
CanFocus = true,
TabStop = TabBehavior.TabStop
};
view2.Add (subView2);
top.Add (view1, view2);
Application.Top = top;
view1.SetFocus ();
Assert.True (view1.HasFocus);
Assert.True (subView1.HasFocus);
// Act
Application.RaiseKeyDownEvent (Application.PrevTabGroupKey);
// Assert
Assert.True (view2.HasFocus);
Assert.True (subView2.HasFocus);
top.Dispose ();
Application.Navigation = null;
}
[Fact]
public void PrevTabKey_Moves_Focus_To_Prev_TabStop ()
{
// Arrange
Application.Navigation = new ();
var top = new Toplevel ();
var view1 = new View { Id = "view1", CanFocus = true };
var view2 = new View { Id = "view2", CanFocus = true };
top.Add (view1, view2);
Application.Top = top;
view1.SetFocus ();
// Act
Application.RaiseKeyDownEvent (Application.NextTabKey);
// Assert
Assert.True (view2.HasFocus);
top.Dispose ();
Application.Navigation = null;
}
[Fact]
public void QuitKey_Default_Is_Esc ()
{
Application.ResetState (true);
// Before Init
Assert.Equal (Key.Esc, Application.QuitKey);
Application.Init (null, "fakedriver");
// After Init
Assert.Equal (Key.Esc, Application.QuitKey);
Application.Shutdown ();
}
[Fact]
[AutoInitShutdown]
public void QuitKey_Getter_Setter ()
{
Toplevel top = new ();
var isQuiting = false;
top.Closing += (s, e) =>
{
isQuiting = true;
e.Cancel = true;
};
Application.Begin (top);
top.Running = true;
Key prevKey = Application.QuitKey;
Application.RaiseKeyDownEvent (Application.QuitKey);
Assert.True (isQuiting);
isQuiting = false;
Application.RaiseKeyDownEvent (Application.QuitKey);
Assert.True (isQuiting);
isQuiting = false;
Application.QuitKey = Key.C.WithCtrl;
Application.RaiseKeyDownEvent (prevKey); // Should not quit
Assert.False (isQuiting);
Application.RaiseKeyDownEvent (Key.Q.WithCtrl); // Should not quit
Assert.False (isQuiting);
Application.RaiseKeyDownEvent (Application.QuitKey);
Assert.True (isQuiting);
// Reset the QuitKey to avoid throws errors on another tests
Application.QuitKey = prevKey;
top.Dispose ();
}
[Fact]
public void QuitKey_Quits ()
{
Assert.Null (_timeoutLock);
_timeoutLock = new ();
uint abortTime = 500;
var initialized = false;
var iteration = 0;
var shutdown = false;
object timeout = null;
Application.InitializedChanged += OnApplicationOnInitializedChanged;
Application.Init (null, "fakedriver");
Assert.True (initialized);
Assert.False (shutdown);
_output.WriteLine ("Application.Run<Toplevel> ().Dispose ()..");
Application.Run<Toplevel> ().Dispose ();
_output.WriteLine ("Back from Application.Run<Toplevel> ().Dispose ()");
Assert.True (initialized);
Assert.False (shutdown);
Assert.Equal (1, iteration);
Application.Shutdown ();
Application.InitializedChanged -= OnApplicationOnInitializedChanged;
lock (_timeoutLock)
{
if (timeout is { })
{
Application.RemoveTimeout (timeout);
timeout = null;
}
}
Assert.True (initialized);
Assert.True (shutdown);
#if DEBUG_IDISPOSABLE
Assert.Empty (View.Instances);
#endif
lock (_timeoutLock)
{
_timeoutLock = null;
}
return;
void OnApplicationOnInitializedChanged (object s, EventArgs<bool> a)
{
_output.WriteLine ("OnApplicationOnInitializedChanged: {0}", a.Value);
if (a.Value)
{
Application.Iteration += OnApplicationOnIteration;
initialized = true;
lock (_timeoutLock)
{
timeout = Application.AddTimeout (TimeSpan.FromMilliseconds (abortTime), ForceCloseCallback);
}
}
else
{
Application.Iteration -= OnApplicationOnIteration;
shutdown = true;
}
}
bool ForceCloseCallback ()
{
lock (_timeoutLock)
{
_output.WriteLine ($"ForceCloseCallback. iteration: {iteration}");
if (timeout is { })
{
timeout = null;
}
}
Application.ResetState (true);
Assert.Fail ($"Failed to Quit with {Application.QuitKey} after {abortTime}ms. Force quit.");
return false;
}
void OnApplicationOnIteration (object s, IterationEventArgs a)
{
_output.WriteLine ("Iteration: {0}", iteration);
iteration++;
Assert.True (iteration < 2, "Too many iterations, something is wrong.");
if (Application.Initialized)
{
_output.WriteLine (" Pressing QuitKey");
Application.RaiseKeyDownEvent (Application.QuitKey);
}
}
}
// Test View for testing Application key Bindings
public class ScopedKeyBindingView : View
{
public ScopedKeyBindingView ()
{
AddCommand (Command.Save, () => ApplicationCommand = true);
AddCommand (Command.HotKey, () => HotKeyCommand = true);
AddCommand (Command.Left, () => FocusedCommand = true);
Application.KeyBindings.Add (Key.A, this, Command.Save);
HotKey = KeyCode.H;
KeyBindings.Add (Key.F, Command.Left);
}
public bool ApplicationCommand { get; set; }
public bool FocusedCommand { get; set; }
public bool HotKeyCommand { get; set; }
}
}

View File

@@ -0,0 +1,477 @@
#nullable enable
using Terminal.Gui.App;
namespace UnitTests_Parallelizable.ApplicationTests;
/// <summary>
/// Parallelizable tests for keyboard handling.
/// These tests use isolated instances of <see cref="IKeyboard"/> to avoid static state dependencies.
/// </summary>
public class KeyboardTests
{
[Fact]
public void Constructor_InitializesKeyBindings ()
{
// Arrange & Act
var keyboard = new Keyboard ();
// Assert
Assert.NotNull (keyboard.KeyBindings);
// Verify that some default bindings exist
Assert.True (keyboard.KeyBindings.TryGet (keyboard.QuitKey, out _));
}
[Fact]
public void QuitKey_DefaultValue_IsEsc ()
{
// Arrange
var keyboard = new Keyboard ();
// Assert
Assert.Equal (Key.Esc, keyboard.QuitKey);
}
[Fact]
public void QuitKey_SetValue_UpdatesKeyBindings ()
{
// Arrange
var keyboard = new Keyboard ();
Key newQuitKey = Key.Q.WithCtrl;
// Act
keyboard.QuitKey = newQuitKey;
// Assert
Assert.Equal (newQuitKey, keyboard.QuitKey);
Assert.True (keyboard.KeyBindings.TryGet (newQuitKey, out KeyBinding binding));
Assert.Contains (Command.Quit, binding.Commands);
}
[Fact]
public void ArrangeKey_DefaultValue_IsCtrlF5 ()
{
// Arrange
var keyboard = new Keyboard ();
// Assert
Assert.Equal (Key.F5.WithCtrl, keyboard.ArrangeKey);
}
[Fact]
public void NextTabKey_DefaultValue_IsTab ()
{
// Arrange
var keyboard = new Keyboard ();
// Assert
Assert.Equal (Key.Tab, keyboard.NextTabKey);
}
[Fact]
public void PrevTabKey_DefaultValue_IsShiftTab ()
{
// Arrange
var keyboard = new Keyboard ();
// Assert
Assert.Equal (Key.Tab.WithShift, keyboard.PrevTabKey);
}
[Fact]
public void NextTabGroupKey_DefaultValue_IsF6 ()
{
// Arrange
var keyboard = new Keyboard ();
// Assert
Assert.Equal (Key.F6, keyboard.NextTabGroupKey);
}
[Fact]
public void PrevTabGroupKey_DefaultValue_IsShiftF6 ()
{
// Arrange
var keyboard = new Keyboard ();
// Assert
Assert.Equal (Key.F6.WithShift, keyboard.PrevTabGroupKey);
}
[Fact]
public void KeyBindings_Add_CanAddCustomBinding ()
{
// Arrange
var keyboard = new Keyboard ();
Key customKey = Key.K.WithCtrl;
// Act
keyboard.KeyBindings.Add (customKey, Command.Accept);
// Assert
Assert.True (keyboard.KeyBindings.TryGet (customKey, out KeyBinding binding));
Assert.Contains (Command.Accept, binding.Commands);
}
[Fact]
public void KeyBindings_Remove_CanRemoveBinding ()
{
// Arrange
var keyboard = new Keyboard ();
Key customKey = Key.K.WithCtrl;
keyboard.KeyBindings.Add (customKey, Command.Accept);
// Act
keyboard.KeyBindings.Remove (customKey);
// Assert
Assert.False (keyboard.KeyBindings.TryGet (customKey, out _));
}
[Fact]
public void KeyDown_Event_CanBeSubscribed ()
{
// Arrange
var keyboard = new Keyboard ();
bool eventRaised = false;
// Act
keyboard.KeyDown += (sender, key) =>
{
eventRaised = true;
};
// Assert - event subscription doesn't throw
Assert.False (eventRaised); // Event hasn't been raised yet
}
[Fact]
public void KeyUp_Event_CanBeSubscribed ()
{
// Arrange
var keyboard = new Keyboard ();
bool eventRaised = false;
// Act
keyboard.KeyUp += (sender, key) =>
{
eventRaised = true;
};
// Assert - event subscription doesn't throw
Assert.False (eventRaised); // Event hasn't been raised yet
}
[Fact]
public void InvokeCommand_WithInvalidCommand_ThrowsNotSupportedException ()
{
// Arrange
var keyboard = new Keyboard ();
// Pick a command that isn't registered
Command invalidCommand = (Command)9999;
Key testKey = Key.A;
var binding = new KeyBinding ([invalidCommand]);
// Act & Assert
Assert.Throws<NotSupportedException> (() => keyboard.InvokeCommand (invalidCommand, testKey, binding));
}
[Fact]
public void Multiple_Keyboards_CanExistIndependently ()
{
// Arrange & Act
var keyboard1 = new Keyboard ();
var keyboard2 = new Keyboard ();
keyboard1.QuitKey = Key.Q.WithCtrl;
keyboard2.QuitKey = Key.X.WithCtrl;
// Assert - each keyboard maintains independent state
Assert.Equal (Key.Q.WithCtrl, keyboard1.QuitKey);
Assert.Equal (Key.X.WithCtrl, keyboard2.QuitKey);
Assert.NotEqual (keyboard1.QuitKey, keyboard2.QuitKey);
}
[Fact]
public void KeyBindings_Replace_UpdatesExistingBinding ()
{
// Arrange
var keyboard = new Keyboard ();
Key oldKey = Key.Esc;
Key newKey = Key.Q.WithCtrl;
// Verify initial state
Assert.True (keyboard.KeyBindings.TryGet (oldKey, out KeyBinding oldBinding));
Assert.Contains (Command.Quit, oldBinding.Commands);
// Act
keyboard.KeyBindings.Replace (oldKey, newKey);
// Assert - old key should no longer have the binding
Assert.False (keyboard.KeyBindings.TryGet (oldKey, out _));
// New key should have the binding
Assert.True (keyboard.KeyBindings.TryGet (newKey, out KeyBinding newBinding));
Assert.Contains (Command.Quit, newBinding.Commands);
}
[Fact]
public void KeyBindings_Clear_RemovesAllBindings ()
{
// Arrange
var keyboard = new Keyboard ();
// Verify initial state has bindings
Assert.True (keyboard.KeyBindings.TryGet (keyboard.QuitKey, out _));
// Act
keyboard.KeyBindings.Clear ();
// Assert - previously existing binding is gone
Assert.False (keyboard.KeyBindings.TryGet (keyboard.QuitKey, out _));
}
[Fact]
public void AddKeyBindings_PopulatesDefaultBindings ()
{
// Arrange
var keyboard = new Keyboard ();
keyboard.KeyBindings.Clear ();
Assert.False (keyboard.KeyBindings.TryGet (keyboard.QuitKey, out _));
// Act
keyboard.AddKeyBindings ();
// Assert
Assert.True (keyboard.KeyBindings.TryGet (keyboard.QuitKey, out KeyBinding binding));
Assert.Contains (Command.Quit, binding.Commands);
}
// Migrated from UnitTests/Application/KeyboardTests.cs
[Fact]
public void KeyBindings_Add_Adds ()
{
// Arrange
var keyboard = new Keyboard ();
// Act
keyboard.KeyBindings.Add (Key.A, Command.Accept);
keyboard.KeyBindings.Add (Key.B, Command.Accept);
// Assert
Assert.True (keyboard.KeyBindings.TryGet (Key.A, out KeyBinding binding));
Assert.Null (binding.Target);
Assert.True (keyboard.KeyBindings.TryGet (Key.B, out binding));
Assert.Null (binding.Target);
}
[Fact]
public void KeyBindings_Remove_Removes ()
{
// Arrange
var keyboard = new Keyboard ();
keyboard.KeyBindings.Add (Key.A, Command.Accept);
Assert.True (keyboard.KeyBindings.TryGet (Key.A, out _));
// Act
keyboard.KeyBindings.Remove (Key.A);
// Assert
Assert.False (keyboard.KeyBindings.TryGet (Key.A, out _));
}
[Fact]
public void QuitKey_Default_Is_Esc ()
{
// Arrange & Act
var keyboard = new Keyboard ();
// Assert
Assert.Equal (Key.Esc, keyboard.QuitKey);
}
[Fact]
public void QuitKey_Setter_UpdatesBindings ()
{
// Arrange
var keyboard = new Keyboard ();
Key prevKey = keyboard.QuitKey;
// Act - Change QuitKey
keyboard.QuitKey = Key.C.WithCtrl;
// Assert - Old key should no longer trigger quit
Assert.False (keyboard.KeyBindings.TryGet (prevKey, out _));
// New key should trigger quit
Assert.True (keyboard.KeyBindings.TryGet (Key.C.WithCtrl, out KeyBinding binding));
Assert.Contains (Command.Quit, binding.Commands);
}
[Fact]
public void NextTabKey_Setter_UpdatesBindings ()
{
// Arrange
var keyboard = new Keyboard ();
Key prevKey = keyboard.NextTabKey;
Key newKey = Key.N.WithCtrl;
// Act
keyboard.NextTabKey = newKey;
// Assert
Assert.Equal (newKey, keyboard.NextTabKey);
Assert.True (keyboard.KeyBindings.TryGet (newKey, out KeyBinding binding));
Assert.Contains (Command.NextTabStop, binding.Commands);
}
[Fact]
public void PrevTabKey_Setter_UpdatesBindings ()
{
// Arrange
var keyboard = new Keyboard ();
Key newKey = Key.P.WithCtrl;
// Act
keyboard.PrevTabKey = newKey;
// Assert
Assert.Equal (newKey, keyboard.PrevTabKey);
Assert.True (keyboard.KeyBindings.TryGet (newKey, out KeyBinding binding));
Assert.Contains (Command.PreviousTabStop, binding.Commands);
}
[Fact]
public void NextTabGroupKey_Setter_UpdatesBindings ()
{
// Arrange
var keyboard = new Keyboard ();
Key newKey = Key.PageDown.WithCtrl;
// Act
keyboard.NextTabGroupKey = newKey;
// Assert
Assert.Equal (newKey, keyboard.NextTabGroupKey);
Assert.Equal (KeyCode.PageDown | KeyCode.CtrlMask, keyboard.NextTabGroupKey.KeyCode);
Assert.True (keyboard.KeyBindings.TryGet (newKey, out KeyBinding binding));
Assert.Contains (Command.NextTabGroup, binding.Commands);
}
[Fact]
public void PrevTabGroupKey_Setter_UpdatesBindings ()
{
// Arrange
var keyboard = new Keyboard ();
Key newKey = Key.PageUp.WithCtrl;
// Act
keyboard.PrevTabGroupKey = newKey;
// Assert
Assert.Equal (newKey, keyboard.PrevTabGroupKey);
Assert.Equal (KeyCode.PageUp | KeyCode.CtrlMask, keyboard.PrevTabGroupKey.KeyCode);
Assert.True (keyboard.KeyBindings.TryGet (newKey, out KeyBinding binding));
Assert.Contains (Command.PreviousTabGroup, binding.Commands);
}
[Fact]
public void ArrangeKey_Setter_UpdatesBindings ()
{
// Arrange
var keyboard = new Keyboard ();
Key newKey = Key.A.WithCtrl;
// Act
keyboard.ArrangeKey = newKey;
// Assert
Assert.Equal (newKey, keyboard.ArrangeKey);
Assert.True (keyboard.KeyBindings.TryGet (newKey, out KeyBinding binding));
Assert.Contains (Command.Arrange, binding.Commands);
}
[Fact]
public void KeyBindings_AddWithTarget_StoresTarget ()
{
// Arrange
var keyboard = new Keyboard ();
var view = new View ();
// Act
keyboard.KeyBindings.Add (Key.A.WithCtrl, view, Command.Accept);
// Assert
Assert.True (keyboard.KeyBindings.TryGet (Key.A.WithCtrl, out KeyBinding binding));
Assert.Equal (view, binding.Target);
Assert.Contains (Command.Accept, binding.Commands);
view.Dispose ();
}
[Fact]
public void InvokeCommandsBoundToKey_ReturnsNull_WhenNoBindingExists ()
{
// Arrange
var keyboard = new Keyboard ();
Key unboundKey = Key.Z.WithAlt.WithCtrl;
// Act
bool? result = keyboard.InvokeCommandsBoundToKey (unboundKey);
// Assert
Assert.Null (result);
}
[Fact]
public void InvokeCommandsBoundToKey_InvokesCommand_WhenBindingExists ()
{
// Arrange
var keyboard = new Keyboard ();
// QuitKey has a bound command by default
// Act
bool? result = keyboard.InvokeCommandsBoundToKey (keyboard.QuitKey);
// Assert
// Command.Quit would normally call Application.RequestStop,
// but in isolation it should return true (handled)
Assert.NotNull (result);
}
[Fact]
public void Multiple_Keyboards_Independent_KeyBindings ()
{
// Arrange
var keyboard1 = new Keyboard ();
var keyboard2 = new Keyboard ();
// Act
keyboard1.KeyBindings.Add (Key.X, Command.Accept);
keyboard2.KeyBindings.Add (Key.Y, Command.Cancel);
// Assert
Assert.True (keyboard1.KeyBindings.TryGet (Key.X, out _));
Assert.False (keyboard1.KeyBindings.TryGet (Key.Y, out _));
Assert.True (keyboard2.KeyBindings.TryGet (Key.Y, out _));
Assert.False (keyboard2.KeyBindings.TryGet (Key.X, out _));
}
[Fact]
public void KeyBindings_Replace_PreservesCommandsForNewKey ()
{
// Arrange
var keyboard = new Keyboard ();
Key oldKey = Key.Esc;
Key newKey = Key.Q.WithCtrl;
// Get the commands from the old binding
Assert.True (keyboard.KeyBindings.TryGet (oldKey, out KeyBinding oldBinding));
Command[] oldCommands = oldBinding.Commands.ToArray ();
// Act
keyboard.KeyBindings.Replace (oldKey, newKey);
// Assert - new key should have the same commands
Assert.True (keyboard.KeyBindings.TryGet (newKey, out KeyBinding newBinding));
Assert.Equal (oldCommands, newBinding.Commands);
}
}

View File

@@ -64,7 +64,7 @@ For **Application-scoped Key Bindings** there are two categories of Application-
1) **Application Command Key Bindings** - Bindings for `Command`s supported by @Terminal.Gui.App.Application. For example, @Terminal.Gui.App.Application.QuitKey, which is bound to `Command.Quit` and results in @Terminal.Gui.App.Application.RequestStop(Terminal.Gui.Views.Toplevel) being called. 1) **Application Command Key Bindings** - Bindings for `Command`s supported by @Terminal.Gui.App.Application. For example, @Terminal.Gui.App.Application.QuitKey, which is bound to `Command.Quit` and results in @Terminal.Gui.App.Application.RequestStop(Terminal.Gui.Views.Toplevel) being called.
2) **Application Key Bindings** - Bindings for `Command`s supported on arbitrary `Views` that are meant to be invoked regardless of which part of the application is visible/active. 2) **Application Key Bindings** - Bindings for `Command`s supported on arbitrary `Views` that are meant to be invoked regardless of which part of the application is visible/active.
Use @Terminal.Gui.App.Application.KeyBindings to add or modify Application-scoped Key Bindings. Use @Terminal.Gui.App.Application.Keyboard.KeyBindings to add or modify Application-scoped Key Bindings. For backward compatibility, @Terminal.Gui.App.Application.KeyBindings also provides access to the same key bindings.
**View-scoped Key Bindings** also have two categories: **View-scoped Key Bindings** also have two categories:
@@ -97,7 +97,7 @@ Keyboard events are retrieved from [Console Drivers](drivers.md) each iteration
> Not all drivers/platforms support sensing distinct KeyUp events. These drivers will simulate KeyUp events by raising KeyUp after KeyDown. > Not all drivers/platforms support sensing distinct KeyUp events. These drivers will simulate KeyUp events by raising KeyUp after KeyDown.
@Terminal.Gui.App.Application.RaiseKeyDownEvent* raises @Terminal.Gui.App.Application.KeyDown and then calls @Terminal.Gui.ViewBase.View.NewKeyDownEvent* on all toplevel Views. If no View handles the key event, any Application-scoped key bindings will be invoked. @Terminal.Gui.App.Application.RaiseKeyDownEvent* raises @Terminal.Gui.App.Application.KeyDown and then calls @Terminal.Gui.ViewBase.View.NewKeyDownEvent* on all toplevel Views. If no View handles the key event, any Application-scoped key bindings will be invoked. Application-scoped key bindings are managed through @Terminal.Gui.App.Application.Keyboard.KeyBindings.
If a view is enabled, the @Terminal.Gui.ViewBase.View.NewKeyDownEvent* method will do the following: If a view is enabled, the @Terminal.Gui.ViewBase.View.NewKeyDownEvent* method will do the following:
@@ -143,11 +143,79 @@ To define application key handling logic for an entire application in cases wher
## Application ## Application
* Implements support for `KeyBindingScope.Application`. * Implements support for `KeyBindingScope.Application`.
* Exposes @Terminal.Gui.App.Application.KeyBindings. * Keyboard functionality is now encapsulated in the @Terminal.Gui.App.IKeyboard interface, accessed via @Terminal.Gui.App.Application.Keyboard.
* Exposes cancelable `KeyDown/Up` events (via `Handled = true`). The `OnKey/Down/Up/` methods are public and can be used to simulate keyboard input. * @Terminal.Gui.App.Application.Keyboard provides access to @Terminal.Gui.Input.KeyBindings, key binding configuration (QuitKey, ArrangeKey, navigation keys), and keyboard event handling.
* For backward compatibility, @Terminal.Gui.App.Application still exposes static properties/methods that delegate to @Terminal.Gui.App.Application.Keyboard (e.g., `Application.KeyBindings`, `Application.RaiseKeyDownEvent`, `Application.QuitKey`).
* Exposes cancelable `KeyDown/Up` events (via `Handled = true`). The `RaiseKeyDownEvent` and `RaiseKeyUpEvent` methods are public and can be used to simulate keyboard input.
* The @Terminal.Gui.App.IKeyboard interface enables testability with isolated keyboard instances that don't depend on static Application state.
## View ## View
* Implements support for `KeyBindings` and `HotKeyBindings`. * Implements support for `KeyBindings` and `HotKeyBindings`.
* Exposes cancelable non-virtual methods for a new key event: `NewKeyDownEvent` and `NewKeyUpEvent`. These methods are called by `Application` can be called to simulate keyboard input. * Exposes cancelable non-virtual methods for a new key event: `NewKeyDownEvent` and `NewKeyUpEvent`. These methods are called by `Application` can be called to simulate keyboard input.
* Exposes cancelable virtual methods for a new key event: `OnKeyDown` and `OnKeyUp`. These methods are called by `NewKeyDownEvent` and `NewKeyUpEvent` and can be overridden to handle keyboard input. * Exposes cancelable virtual methods for a new key event: `OnKeyDown` and `OnKeyUp`. These methods are called by `NewKeyDownEvent` and `NewKeyUpEvent` and can be overridden to handle keyboard input.
## IKeyboard Architecture
The @Terminal.Gui.App.IKeyboard interface provides a decoupled, testable architecture for keyboard handling in Terminal.Gui. This design allows for:
### Key Features
1. **Decoupled State** - All keyboard-related state (key bindings, navigation keys, events) is encapsulated in @Terminal.Gui.App.IKeyboard, separate from the static @Terminal.Gui.App.Application class.
2. **Dependency Injection** - The @Terminal.Gui.App.Keyboard implementation receives an @Terminal.Gui.App.IApplication reference, enabling it to interact with application state without static dependencies.
3. **Testability** - Unit tests can create isolated @Terminal.Gui.App.IKeyboard instances with mock @Terminal.Gui.App.IApplication references, enabling parallel test execution without interference.
4. **Backward Compatibility** - All existing @Terminal.Gui.App.Application keyboard APIs (e.g., `Application.KeyBindings`, `Application.RaiseKeyDownEvent`, `Application.QuitKey`) remain available and delegate to `Application.Keyboard`.
### Usage Examples
**Accessing keyboard functionality:**
```csharp
// Modern approach - using IKeyboard
Application.Keyboard.KeyBindings.Add(Key.F1, Command.HotKey);
Application.Keyboard.RaiseKeyDownEvent(Key.Enter);
Application.Keyboard.QuitKey = Key.Q.WithCtrl;
// Legacy approach - still works (delegates to Application.Keyboard)
Application.KeyBindings.Add(Key.F1, Command.HotKey);
Application.RaiseKeyDownEvent(Key.Enter);
Application.QuitKey = Key.Q.WithCtrl;
```
**Testing with isolated keyboard instances:**
```csharp
// Create independent keyboard instances for parallel tests
var keyboard1 = new Keyboard();
keyboard1.QuitKey = Key.Q.WithCtrl;
keyboard1.KeyBindings.Add(Key.F1, Command.HotKey);
var keyboard2 = new Keyboard();
keyboard2.QuitKey = Key.X.WithCtrl;
keyboard2.KeyBindings.Add(Key.F2, Command.Accept);
// keyboard1 and keyboard2 maintain completely separate state
Assert.Equal(Key.Q.WithCtrl, keyboard1.QuitKey);
Assert.Equal(Key.X.WithCtrl, keyboard2.QuitKey);
```
### Architecture Benefits
- **Parallel Testing**: Multiple test methods can create and use separate @Terminal.Gui.App.IKeyboard instances simultaneously without state interference.
- **Dependency Inversion**: @Terminal.Gui.App.Keyboard depends on @Terminal.Gui.App.IApplication interface rather than static @Terminal.Gui.App.Application class.
- **Cleaner Code**: Keyboard functionality is organized in a dedicated interface rather than scattered across @Terminal.Gui.App.Application partial classes.
- **Mockability**: Tests can provide mock @Terminal.Gui.App.IApplication implementations to test keyboard behavior in isolation.
### Implementation Details
The @Terminal.Gui.App.Keyboard class implements @Terminal.Gui.App.IKeyboard and maintains:
- **KeyBindings**: Application-scoped key binding dictionary
- **Navigation Keys**: QuitKey, ArrangeKey, NextTabKey, PrevTabKey, NextTabGroupKey, PrevTabGroupKey
- **Events**: KeyDown, KeyUp events for application-level keyboard monitoring
- **Command Implementations**: Handlers for Application-scoped commands (Quit, Suspend, Navigation, Refresh, Arrange)
The @Terminal.Gui.App.ApplicationImpl class creates and manages the @Terminal.Gui.App.IKeyboard instance, setting its `Application` property to `this` to provide the necessary @Terminal.Gui.App.IApplication reference.