using System.Collections.Concurrent;
namespace Terminal.Gui.App;
///
/// INTERNAL: Implements to manage keyboard input and key bindings at the Application level.
/// This implementation is thread-safe for all public operations.
///
/// This implementation decouples keyboard handling state from the static class,
/// enabling parallelizable unit tests and better testability.
///
///
/// See for usage details.
///
///
internal class KeyboardImpl : IKeyboard, IDisposable
{
///
/// Initializes keyboard bindings and subscribes to Application configuration property events.
///
public KeyboardImpl ()
{
// DON'T access Application static properties here - they trigger ApplicationImpl.Instance
// which sets ModelUsage to LegacyStatic, breaking parallel tests.
// These will be initialized from Application static properties in Init() or when accessed.
// Initialize to reasonable defaults that match Application defaults
// These will be updated by property change events if Application properties change
_quitKey = Key.Esc;
_arrangeKey = Key.F5.WithCtrl;
_nextTabGroupKey = Key.F6;
_nextTabKey = Key.Tab;
_prevTabGroupKey = Key.F6.WithShift;
_prevTabKey = Key.Tab.WithShift;
// Subscribe to Application static property change events
// so we get updated if they change
Application.QuitKeyChanged += OnQuitKeyChanged;
Application.ArrangeKeyChanged += OnArrangeKeyChanged;
Application.NextTabGroupKeyChanged += OnNextTabGroupKeyChanged;
Application.NextTabKeyChanged += OnNextTabKeyChanged;
Application.PrevTabGroupKeyChanged += OnPrevTabGroupKeyChanged;
Application.PrevTabKeyChanged += OnPrevTabKeyChanged;
AddKeyBindings ();
}
///
/// Commands for Application. Thread-safe for concurrent access.
///
private readonly ConcurrentDictionary _commandImplementations = new ();
private Key _quitKey;
private Key _arrangeKey;
private Key _nextTabGroupKey;
private Key _nextTabKey;
private Key _prevTabGroupKey;
private Key _prevTabKey;
///
public void Dispose ()
{
// Unsubscribe from Application static property change events
Application.QuitKeyChanged -= OnQuitKeyChanged;
Application.ArrangeKeyChanged -= OnArrangeKeyChanged;
Application.NextTabGroupKeyChanged -= OnNextTabGroupKeyChanged;
Application.NextTabKeyChanged -= OnNextTabKeyChanged;
Application.PrevTabGroupKeyChanged -= OnPrevTabGroupKeyChanged;
Application.PrevTabKeyChanged -= OnPrevTabKeyChanged;
}
///
public IApplication? App { get; set; }
///
public KeyBindings KeyBindings { get; internal set; } = new (null);
///
public Key QuitKey
{
get => _quitKey;
set
{
KeyBindings.Replace (_quitKey, value);
_quitKey = value;
}
}
///
public Key ArrangeKey
{
get => _arrangeKey;
set
{
KeyBindings.Replace (_arrangeKey, value);
_arrangeKey = value;
}
}
///
public Key NextTabGroupKey
{
get => _nextTabGroupKey;
set
{
KeyBindings.Replace (_nextTabGroupKey, value);
_nextTabGroupKey = value;
}
}
///
public Key NextTabKey
{
get => _nextTabKey;
set
{
KeyBindings.Replace (_nextTabKey, value);
_nextTabKey = value;
}
}
///
public Key PrevTabGroupKey
{
get => _prevTabGroupKey;
set
{
KeyBindings.Replace (_prevTabGroupKey, value);
_prevTabGroupKey = value;
}
}
///
public Key PrevTabKey
{
get => _prevTabKey;
set
{
KeyBindings.Replace (_prevTabKey, value);
_prevTabKey = value;
}
}
///
public event EventHandler? KeyDown;
///
public event EventHandler? KeyUp;
///
public bool RaiseKeyDownEvent (Key key)
{
//ebug.Assert (App.Application.MainThreadId == Thread.CurrentThread.ManagedThreadId);
//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 (App?.Popover?.DispatchKeyDown (key) is true)
{
return true;
}
if (App?.TopRunnable is null)
{
if (App?.SessionStack is { })
{
foreach (Toplevel topLevel in App.SessionStack.ToList ())
{
if (topLevel.NewKeyDownEvent (key))
{
return true;
}
if (topLevel.Modal)
{
break;
}
}
}
}
else
{
if (App.TopRunnable.NewKeyDownEvent (key))
{
return true;
}
}
bool? commandHandled = InvokeCommandsBoundToKey (key);
if (commandHandled is true)
{
return true;
}
return false;
}
///
public bool RaiseKeyUpEvent (Key key)
{
if (App?.Initialized != true)
{
return true;
}
KeyUp?.Invoke (null, key);
if (key.Handled)
{
return true;
}
// TODO: Add Popover support
if (App?.SessionStack is { })
{
foreach (Toplevel topLevel in App.SessionStack.ToList ())
{
if (topLevel.NewKeyUpEvent (key))
{
return true;
}
if (topLevel.Modal)
{
break;
}
}
}
return false;
}
///
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 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;
}
///
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 context = new (command, null, binding); // Create the context here
return implementation (context);
}
return null;
}
internal void AddKeyBindings ()
{
_commandImplementations.Clear ();
// Things Application knows how to do
AddCommand (
Command.Quit,
() =>
{
App?.RequestStop ();
return true;
}
);
AddCommand (
Command.Suspend,
() =>
{
App?.Driver?.Suspend ();
return true;
}
);
AddCommand (
Command.NextTabStop,
() => App?.Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop));
AddCommand (
Command.PreviousTabStop,
() => App?.Navigation?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop));
AddCommand (
Command.NextTabGroup,
() => App?.Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup));
AddCommand (
Command.PreviousTabGroup,
() => App?.Navigation?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabGroup));
AddCommand (
Command.Refresh,
() =>
{
App?.LayoutAndDraw (true);
return true;
}
);
AddCommand (
Command.Arrange,
() =>
{
View? viewToArrange = App?.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;
});
// Need to clear after setting the above to ensure actually clear
// because set_QuitKey etc. may call Add
//KeyBindings.Clear ();
// Use ReplaceCommands instead of Add, because it's possible that
// during construction the Application static properties changed, and
// we added those keys already.
KeyBindings.ReplaceCommands (QuitKey, Command.Quit);
KeyBindings.ReplaceCommands (NextTabKey, Command.NextTabStop);
KeyBindings.ReplaceCommands (PrevTabKey, Command.PreviousTabStop);
KeyBindings.ReplaceCommands (NextTabGroupKey, Command.NextTabGroup);
KeyBindings.ReplaceCommands (PrevTabGroupKey, Command.PreviousTabGroup);
KeyBindings.ReplaceCommands (ArrangeKey, Command.Arrange);
// TODO: Should these be configurable?
KeyBindings.ReplaceCommands (Key.CursorRight, Command.NextTabStop);
KeyBindings.ReplaceCommands (Key.CursorDown, Command.NextTabStop);
KeyBindings.ReplaceCommands (Key.CursorLeft, Command.PreviousTabStop);
KeyBindings.ReplaceCommands (Key.CursorUp, Command.PreviousTabStop);
// TODO: Refresh Key should be configurable
KeyBindings.ReplaceCommands (Key.F5, Command.Refresh);
// TODO: Suspend Key should be configurable
if (Environment.OSVersion.Platform == PlatformID.Unix)
{
KeyBindings.ReplaceCommands (Key.Z.WithCtrl, Command.Suspend);
}
}
///
///
/// 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 void AddCommand (Command command, Func f) { _commandImplementations [command] = ctx => f (); }
private void OnArrangeKeyChanged (object? sender, ValueChangedEventArgs e) { ArrangeKey = e.NewValue; }
private void OnNextTabGroupKeyChanged (object? sender, ValueChangedEventArgs e) { NextTabGroupKey = e.NewValue; }
private void OnNextTabKeyChanged (object? sender, ValueChangedEventArgs e) { NextTabKey = e.NewValue; }
private void OnPrevTabGroupKeyChanged (object? sender, ValueChangedEventArgs e) { PrevTabGroupKey = e.NewValue; }
private void OnPrevTabKeyChanged (object? sender, ValueChangedEventArgs e) { PrevTabKey = e.NewValue; }
// Event handlers for Application static property changes
private void OnQuitKeyChanged (object? sender, ValueChangedEventArgs e) { QuitKey = e.NewValue; }
}