mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 15:57:56 +01:00
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:
@@ -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 ();
|
||||||
|
|||||||
@@ -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 ();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
113
Terminal.Gui/App/Keyboard/IKeyboard.cs
Normal file
113
Terminal.Gui/App/Keyboard/IKeyboard.cs
Normal 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; }
|
||||||
|
}
|
||||||
381
Terminal.Gui/App/Keyboard/Keyboard.cs
Normal file
381
Terminal.Gui/App/Keyboard/Keyboard.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
477
Tests/UnitTestsParallelizable/Application/KeyboardTests.cs
Normal file
477
Tests/UnitTestsParallelizable/Application/KeyboardTests.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user