#nullable enable using System.Diagnostics; namespace Terminal.Gui.App; /// /// INTERNAL: Implements to manage keyboard input and key bindings at the Application level. /// /// 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 { 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 /// /// Commands for Application. /// private readonly Dictionary _commandImplementations = new (); /// public IApplication? Application { 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; /// /// Initializes keyboard bindings. /// public KeyboardImpl () { AddKeyBindings (); } /// 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 (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; } /// 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; } /// 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; } /// /// /// 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 (); } 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); } } }