#nullable enable
namespace Terminal.Gui;
public static partial class Application // Keyboard handling
{
///
/// Called when the user presses a key (by the ). Raises the cancelable
/// event, then calls on all top level views, and finally
/// if the key was not handled, invokes any Application-scoped .
///
/// Can be used to simulate key press events.
///
/// if the key was handled.
public static bool RaiseKeyDownEvent (Key key)
{
KeyDown?.Invoke (null, key);
if (key.Handled)
{
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;
}
}
// Invoke any Application-scoped KeyBindings.
// The first view that handles the key will stop the loop.
foreach (KeyValuePair binding in KeyBindings.Bindings.Where (b => b.Key == key.KeyCode))
{
if (binding.Value.BoundView is { })
{
bool? handled = binding.Value.BoundView?.InvokeCommands (binding.Value.Commands, binding.Key, binding.Value);
if (handled != null && (bool)handled)
{
return true;
}
}
else
{
if (!KeyBindings.TryGet (key, KeyBindingScope.Application, out KeyBinding appBinding))
{
continue;
}
bool? toReturn = null;
foreach (Command command in appBinding.Commands)
{
toReturn = InvokeCommand (command, key, appBinding);
}
return toReturn ?? true;
}
}
return false;
static bool? InvokeCommand (Command command, Key key, KeyBinding appBinding)
{
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))
{
var context = new CommandContext (command, key, appBinding); // Create the context here
return implementation (context);
}
return false;
}
}
///
/// Raised when the user presses a key.
///
/// Set to to indicate the key was handled and to prevent
/// additional processing.
///
///
///
/// All drivers support firing the event. Some drivers (Curses) do not support firing the
/// and events.
/// Fired after and before .
///
public static event EventHandler? KeyDown;
///
/// Called when the user releases a key (by the ). Raises the cancelable
/// event
/// then calls on all top level views. Called after .
///
/// Can be used to simulate key release events.
///
/// if the key was handled.
public static bool RaiseKeyUpEvent (Key key)
{
if (!Initialized)
{
return true;
}
KeyUp?.Invoke (null, key);
if (key.Handled)
{
return true;
}
foreach (Toplevel topLevel in TopLevels.ToList ())
{
if (topLevel.NewKeyUpEvent (key))
{
return true;
}
if (topLevel.Modal)
{
break;
}
}
return false;
}
#region Application-scoped KeyBindings
static Application () { AddApplicationKeyBindings (); }
/// Gets the Application-scoped key bindings.
public static KeyBindings KeyBindings { get; internal set; } = new ();
internal static void AddApplicationKeyBindings ()
{
CommandImplementations = new ();
// Things this view 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 () =>
{
LayoutAndDrawToplevels ();
return true;
}
);
AddCommand (
Command.Edit,
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;
});
KeyBindings.Clear ();
// Resources/config.json overrides
NextTabKey = Key.Tab;
PrevTabKey = Key.Tab.WithShift;
NextTabGroupKey = Key.F6;
PrevTabGroupKey = Key.F6.WithShift;
QuitKey = Key.Esc;
ArrangeKey = Key.F5.WithCtrl;
KeyBindings.Add (QuitKey, KeyBindingScope.Application, Command.Quit);
KeyBindings.Add (Key.CursorRight, KeyBindingScope.Application, Command.NextTabStop);
KeyBindings.Add (Key.CursorDown, KeyBindingScope.Application, Command.NextTabStop);
KeyBindings.Add (Key.CursorLeft, KeyBindingScope.Application, Command.PreviousTabStop);
KeyBindings.Add (Key.CursorUp, KeyBindingScope.Application, Command.PreviousTabStop);
KeyBindings.Add (NextTabKey, KeyBindingScope.Application, Command.NextTabStop);
KeyBindings.Add (PrevTabKey, KeyBindingScope.Application, Command.PreviousTabStop);
KeyBindings.Add (NextTabGroupKey, KeyBindingScope.Application, Command.NextTabGroup);
KeyBindings.Add (PrevTabGroupKey, KeyBindingScope.Application, Command.PreviousTabGroup);
KeyBindings.Add (ArrangeKey, KeyBindingScope.Application, Command.Edit);
// TODO: Refresh Key should be configurable
KeyBindings.Add (Key.F5, KeyBindingScope.Application, Command.Refresh);
// TODO: Suspend Key should be configurable
if (Environment.OSVersion.Platform == PlatformID.Unix)
{
KeyBindings.Add (Key.Z.WithCtrl, KeyBindingScope.Application, Command.Suspend);
}
}
///
/// Gets the list of Views that have key bindings.
///
///
/// This is an internal method used by the class to add Application key bindings.
///
/// The list of Views that have Application-scoped key bindings.
internal static List GetViewKeyBindings ()
{
// Get the list of views that do not have Application-scoped key bindings
return KeyBindings.Bindings
.Where (kv => kv.Value.Scope != KeyBindingScope.Application)
.Select (kv => kv.Value)
.Distinct ()
.ToList ();
}
private static void ReplaceKey (Key oldKey, Key newKey)
{
if (KeyBindings.Bindings.Count == 0)
{
return;
}
if (newKey == Key.Empty)
{
KeyBindings.Remove (oldKey);
}
else
{
KeyBindings.ReplaceKey (oldKey, newKey);
}
}
#endregion Application-scoped KeyBindings
///
///
/// Sets the function that will be invoked for a .
///
///
/// If AddCommand has already been called for will
/// replace the old one.
///
///
///
///
/// This version of AddCommand is for commands that do not require a .
///
///
/// The command.
/// The function.
private static void AddCommand (Command command, Func f) { CommandImplementations! [command] = ctx => f (); }
///
/// Commands for Application.
///
private static Dictionary? CommandImplementations { get; set; }
}