Browse Source

Merge pull request #3880 from tig/v2_3778-Command-Decoupling

Fixes #3778: Decouples `Command` from `KeyBindings`
Tig 7 months ago
parent
commit
7676f89e39
99 changed files with 2880 additions and 2657 deletions
  1. 1 1
      Terminal.Gui/Application/Application.Initialization.cs
  2. 39 80
      Terminal.Gui/Application/Application.Keyboard.cs
  3. 1 1
      Terminal.Gui/Application/Application.Mouse.cs
  4. 5 5
      Terminal.Gui/Application/Application.Navigation.cs
  5. 2 2
      Terminal.Gui/Application/Application.Run.cs
  6. 2 1
      Terminal.Gui/Application/Application.cs
  7. 1 1
      Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs
  8. 1 1
      Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs
  9. 0 93
      Terminal.Gui/EnumExtensions/KeyBindingScopeExtensions.cs
  10. 7 2
      Terminal.Gui/Input/Command.cs
  11. 11 35
      Terminal.Gui/Input/CommandContext.cs
  12. 5 2
      Terminal.Gui/Input/CommandEventArgs.cs
  13. 18 0
      Terminal.Gui/Input/ICommandContext.cs
  14. 13 0
      Terminal.Gui/Input/IInputBinding.cs
  15. 233 0
      Terminal.Gui/Input/InputBindings.cs
  16. 0 58
      Terminal.Gui/Input/KeyBindingScope.cs
  17. 0 447
      Terminal.Gui/Input/KeyBindings.cs
  18. 7 6
      Terminal.Gui/Input/Keyboard/Key.cs
  19. 15 17
      Terminal.Gui/Input/Keyboard/KeyBinding.cs
  20. 56 0
      Terminal.Gui/Input/Keyboard/KeyBindings.cs
  21. 0 0
      Terminal.Gui/Input/Keyboard/KeyChangedEventArgs.cs
  22. 0 0
      Terminal.Gui/Input/Keyboard/KeyEqualityComparer.cs
  23. 0 0
      Terminal.Gui/Input/Keyboard/KeystrokeNavigatorEventArgs.cs
  24. 0 0
      Terminal.Gui/Input/Mouse/GrabMouseEventArgs.cs
  25. 32 0
      Terminal.Gui/Input/Mouse/MouseBinding.cs
  26. 21 0
      Terminal.Gui/Input/Mouse/MouseBindings.cs
  27. 0 0
      Terminal.Gui/Input/Mouse/MouseEventArgs.cs
  28. 0 0
      Terminal.Gui/Input/Mouse/MouseFlags.cs
  29. 1 0
      Terminal.Gui/Input/Mouse/MouseFlagsChangedEventArgs.cs
  30. 0 12
      Terminal.Gui/Input/PointEventArgs.cs
  31. 54 0
      Terminal.Gui/Resources/Strings.Designer.cs
  32. 18 0
      Terminal.Gui/Resources/Strings.resx
  33. 10 10
      Terminal.Gui/View/Adornment/Border.cs
  34. 61 54
      Terminal.Gui/View/View.Command.cs
  35. 83 125
      Terminal.Gui/View/View.Keyboard.cs
  36. 205 91
      Terminal.Gui/View/View.Mouse.cs
  37. 2 1
      Terminal.Gui/View/View.cs
  38. 4 4
      Terminal.Gui/Views/Button.cs
  39. 156 189
      Terminal.Gui/Views/CharMap/CharMap.cs
  40. 10 14
      Terminal.Gui/Views/CharMap/UcdApiClient.cs
  41. 1 3
      Terminal.Gui/Views/CharMap/UnicodeRange.cs
  42. 13 4
      Terminal.Gui/Views/CheckBox.cs
  43. 34 37
      Terminal.Gui/Views/ColorPicker.16.cs
  44. 4 2
      Terminal.Gui/Views/ColorPicker.Prompt.cs
  45. 7 3
      Terminal.Gui/Views/ComboBox.cs
  46. 1 1
      Terminal.Gui/Views/FrameView.cs
  47. 22 37
      Terminal.Gui/Views/HexView.cs
  48. 4 3
      Terminal.Gui/Views/Label.cs
  49. 13 5
      Terminal.Gui/Views/ListView.cs
  50. 2 1
      Terminal.Gui/Views/Menu/ContextMenu.cs
  51. 544 509
      Terminal.Gui/Views/Menu/Menu.cs
  52. 27 19
      Terminal.Gui/Views/Menu/MenuBar.cs
  53. 3 3
      Terminal.Gui/Views/Menu/MenuBarItem.cs
  54. 16 16
      Terminal.Gui/Views/Menu/MenuItem.cs
  55. 9 4
      Terminal.Gui/Views/MessageBox.cs
  56. 151 196
      Terminal.Gui/Views/RadioGroup.cs
  57. 1 1
      Terminal.Gui/Views/ScrollBar/ScrollSlider.cs
  58. 36 34
      Terminal.Gui/Views/Shortcut.cs
  59. 3 3
      Terminal.Gui/Views/Slider.cs
  60. 2 2
      Terminal.Gui/Views/TableView/TableView.cs
  61. 9 9
      Terminal.Gui/Views/TextField.cs
  62. 27 27
      Terminal.Gui/Views/TextView.cs
  63. 3 3
      Terminal.Gui/Views/TreeView/TreeView.cs
  64. 1 1
      UICatalog/Scenario.cs
  65. 1 1
      UICatalog/Scenarios/Arrangement.cs
  66. 1 1
      UICatalog/Scenarios/Bars.cs
  67. 2 2
      UICatalog/Scenarios/Editor.cs
  68. 23 32
      UICatalog/Scenarios/Editors/EventLog.cs
  69. 14 10
      UICatalog/Scenarios/KeyBindings.cs
  70. 3 1
      UICatalog/Scenarios/MessageBoxes.cs
  71. 1 1
      UICatalog/Scenarios/Navigation.cs
  72. 2 8
      UICatalog/Scenarios/Shortcuts.cs
  73. 2 2
      UICatalog/Scenarios/TableEditor.cs
  74. 2 2
      UICatalog/Scenarios/Text.cs
  75. 1 1
      UICatalog/UICatalog.cs
  76. 2 5
      UnitTests/Application/ApplicationTests.cs
  77. 11 40
      UnitTests/Application/KeyboardTests.cs
  78. 0 2
      UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs
  79. 10 0
      UnitTests/Input/Keyboard/KeyBindingTests.cs
  80. 121 155
      UnitTests/Input/Keyboard/KeyBindingsTests.cs
  81. 0 0
      UnitTests/Input/Keyboard/KeyTests.cs
  82. 6 0
      UnitTests/Input/Mouse/MouseBindingTests.cs
  83. 344 0
      UnitTests/Input/Mouse/MouseBindingsTests.cs
  84. 18 0
      UnitTests/Input/Mouse/MouseEventArgsTest.cs
  85. 57 26
      UnitTests/View/Keyboard/HotKeyTests.cs
  86. 7 6
      UnitTests/View/Keyboard/KeyBindingsTests.cs
  87. 2 2
      UnitTests/View/Keyboard/KeyboardEventTests.cs
  88. 21 27
      UnitTests/View/ViewCommandTests.cs
  89. 3 7
      UnitTests/Views/AllViewsTests.cs
  90. 13 4
      UnitTests/Views/CheckBoxTests.cs
  91. 79 79
      UnitTests/Views/ContextMenuTests.cs
  92. 9 8
      UnitTests/Views/MenuBarTests.cs
  93. 29 24
      UnitTests/Views/RadioGroupTests.cs
  94. 19 19
      UnitTests/Views/ShortcutTests.cs
  95. 1 1
      UnitTests/Views/TextViewTests.cs
  96. 2 2
      UnitTests/Views/TimeFieldTests.cs
  97. 4 0
      docfx/docs/View.md
  98. 39 14
      docfx/docs/keyboard.md
  99. 24 0
      docfx/docs/mouse.md

+ 1 - 1
Terminal.Gui/Application/Application.Initialization.cs

@@ -92,7 +92,7 @@ public static partial class Application // Initialization (Init/Shutdown)
             }
         }
 
-        AddApplicationKeyBindings ();
+        AddKeyBindings ();
 
         // Start the process of configuration management.
         // Note that we end up calling LoadConfigurationFromAllSources

+ 39 - 80
Terminal.Gui/Application/Application.Keyboard.cs

@@ -45,16 +45,17 @@ public static partial class Application // Keyboard handling
 
         // Invoke any Application-scoped KeyBindings.
         // The first view that handles the key will stop the loop.
-        foreach (KeyValuePair<Key, KeyBinding> binding in KeyBindings.Bindings.Where (b => b.Key == key.KeyCode))
+        // foreach (KeyValuePair<Key, KeyBinding> binding in KeyBindings.GetBindings (key))
+        if (KeyBindings.TryGet (key, out KeyBinding binding))
         {
-            if (binding.Value.BoundView is { })
+            if (binding.Target is { })
             {
-                if (!binding.Value.BoundView.Enabled)
+                if (!binding.Target.Enabled)
                 {
                     return false;
                 }
 
-                bool? handled = binding.Value.BoundView?.InvokeCommands (binding.Value.Commands, binding.Key, binding.Value);
+                bool? handled = binding.Target?.InvokeCommands (binding.Commands, binding);
 
                 if (handled != null && (bool)handled)
                 {
@@ -63,16 +64,17 @@ public static partial class Application // Keyboard handling
             }
             else
             {
-                if (!KeyBindings.TryGet (key, KeyBindingScope.Application, out KeyBinding appBinding))
+                // BUGBUG: this seems unneeded.
+                if (!KeyBindings.TryGet (key, out KeyBinding keybinding))
                 {
-                    continue;
+                    return false;
                 }
 
                 bool? toReturn = null;
 
-                foreach (Command command in appBinding.Commands)
+                foreach (Command command in keybinding.Commands)
                 {
-                    toReturn = InvokeCommand (command, key, appBinding);
+                    toReturn = InvokeCommand (command, key, keybinding);
                 }
 
                 return toReturn ?? true;
@@ -81,18 +83,18 @@ public static partial class Application // Keyboard handling
 
         return false;
 
-        static bool? InvokeCommand (Command command, Key key, KeyBinding appBinding)
+        static bool? InvokeCommand (Command command, Key key, KeyBinding binding)
         {
-            if (!CommandImplementations!.ContainsKey (command))
+            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))
+            if (_commandImplementations.TryGetValue (command, out View.CommandImplementation? implementation))
             {
-                var context = new CommandContext (command, key, appBinding); // Create the context here
+                CommandContext<KeyBinding> context = new (command, binding); // Create the context here
 
                 return implementation (context);
             }
@@ -116,7 +118,8 @@ public static partial class Application // Keyboard handling
     public static event EventHandler<Key>? KeyDown;
 
     /// <summary>
-    ///     Called when the user releases a key (by the <see cref="IConsoleDriver"/>). Raises the cancelable <see cref="KeyUp"/>
+    ///     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>
@@ -155,14 +158,14 @@ public static partial class Application // Keyboard handling
 
     #region Application-scoped KeyBindings
 
-    static Application () { AddApplicationKeyBindings (); }
+    static Application () { AddKeyBindings (); }
 
     /// <summary>Gets the Application-scoped key bindings.</summary>
-    public static KeyBindings KeyBindings { get; internal set; } = new ();
+    public static KeyBindings KeyBindings { get; internal set; } = new (null);
 
-    internal static void AddApplicationKeyBindings ()
+    internal static void AddKeyBindings ()
     {
-        CommandImplementations = new ();
+        _commandImplementations.Clear ();
 
         // Things this view knows how to do
         AddCommand (
@@ -231,83 +234,40 @@ public static partial class Application // Keyboard handling
                         return false;
                     });
 
-        KeyBindings.Clear ();
-
         // Resources/config.json overrides
+        QuitKey = Key.Esc;
         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);
+        // Need to clear after setting the above to ensure actually clear
+        // because set_QuitKey etc.. may call Add
+        KeyBindings.Clear ();
 
-        KeyBindings.Add (NextTabGroupKey, KeyBindingScope.Application, Command.NextTabGroup);
-        KeyBindings.Add (PrevTabGroupKey, KeyBindingScope.Application, Command.PreviousTabGroup);
+        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.Edit);
 
-        KeyBindings.Add (ArrangeKey, KeyBindingScope.Application, Command.Edit);
+        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, KeyBindingScope.Application, Command.Refresh);
+        KeyBindings.Add (Key.F5, Command.Refresh);
 
         // TODO: Suspend Key should be configurable
         if (Environment.OSVersion.Platform == PlatformID.Unix)
         {
-            KeyBindings.Add (Key.Z.WithCtrl, KeyBindingScope.Application, Command.Suspend);
-        }
-    }
-
-    /// <summary>
-    ///     Gets the list of Views that have <see cref="KeyBindingScope.Application"/> key bindings.
-    /// </summary>
-    /// <remarks>
-    ///     This is an internal method used by the <see cref="View"/> class to add Application key bindings.
-    /// </remarks>
-    /// <returns>The list of Views that have Application-scoped key bindings.</returns>
-    internal static List<KeyBinding> 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
-        {
-            if (KeyBindings.TryGet(oldKey, out KeyBinding binding))
-            {
-                KeyBindings.Remove (oldKey);
-                KeyBindings.Add (newKey, binding);
-            }
-            else
-            {
-                KeyBindings.Add (newKey, binding);
-            }
+            KeyBindings.Add (Key.Z.WithCtrl, Command.Suspend);
         }
     }
 
-
     #endregion Application-scoped KeyBindings
 
     /// <summary>
@@ -321,16 +281,15 @@ public static partial class Application // Keyboard handling
     /// </summary>
     /// <remarks>
     ///     <para>
-    ///         This version of AddCommand is for commands that do not require a <see cref="CommandContext"/>.
+    ///         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 (); }
+    private static void AddCommand (Command command, Func<bool?> f) { _commandImplementations! [command] = ctx => f (); }
 
     /// <summary>
     ///     Commands for Application.
     /// </summary>
-    private static Dictionary<Command, View.CommandImplementation>? CommandImplementations { get; set; }
-
+    private static readonly Dictionary<Command, View.CommandImplementation> _commandImplementations = new ();
 }

+ 1 - 1
Terminal.Gui/Application/Application.Mouse.cs

@@ -184,7 +184,7 @@ public static partial class Application // Mouse handling
         }
 
         // Create a view-relative mouse event to send to the view that is under the mouse.
-        MouseEventArgs? viewMouseEvent;
+        MouseEventArgs viewMouseEvent;
 
         if (deepestViewUnderMouse is Adornment adornment)
         {

+ 5 - 5
Terminal.Gui/Application/Application.Navigation.cs

@@ -22,7 +22,7 @@ public static partial class Application // Navigation stuff
         {
             if (_nextTabGroupKey != value)
             {
-                ReplaceKey (_nextTabGroupKey, value);
+                KeyBindings.Replace (_nextTabGroupKey, value);
                 _nextTabGroupKey = value;
             }
         }
@@ -37,7 +37,7 @@ public static partial class Application // Navigation stuff
         {
             if (_nextTabKey != value)
             {
-                ReplaceKey (_nextTabKey, value);
+                KeyBindings.Replace (_nextTabKey, value);
                 _nextTabKey = value;
             }
         }
@@ -66,7 +66,7 @@ public static partial class Application // Navigation stuff
         {
             if (_prevTabGroupKey != value)
             {
-                ReplaceKey (_prevTabGroupKey, value);
+                KeyBindings.Replace (_prevTabGroupKey, value);
                 _prevTabGroupKey = value;
             }
         }
@@ -78,10 +78,10 @@ public static partial class Application // Navigation stuff
     {
         get => _prevTabKey;
         set
-        {
+        {   
             if (_prevTabKey != value)
             {
-                ReplaceKey (_prevTabKey, value);
+                KeyBindings.Replace (_prevTabKey, value);
                 _prevTabKey = value;
             }
         }

+ 2 - 2
Terminal.Gui/Application/Application.Run.cs

@@ -19,7 +19,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
         {
             if (_quitKey != value)
             {
-                ReplaceKey (_quitKey, value);
+                KeyBindings.Replace (_quitKey, value);
                 _quitKey = value;
             }
         }
@@ -37,7 +37,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
         {
             if (_arrangeKey != value)
             {
-                ReplaceKey (_arrangeKey, value);
+                KeyBindings.Replace (_arrangeKey, value);
                 _arrangeKey = value;
             }
         }

+ 2 - 1
Terminal.Gui/Application/Application.cs

@@ -214,7 +214,8 @@ public static partial class Application
 
         ClearScreenNextIteration = false;
 
-        AddApplicationKeyBindings ();
+        KeyBindings.Clear ();
+        AddKeyBindings ();
 
         // Reset synchronization context to allow the user to run async/await,
         // as the main loop has been ended, the synchronization context from

+ 1 - 1
Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs

@@ -21,7 +21,7 @@ public class AnsiEscapeSequenceRequest : AnsiEscapeSequence
 
 
     /// <summary>
-    ///     Sends the <see cref="Request"/> to the raw output stream of the current <see cref="ConsoleDriver"/>.
+    ///     Sends the <see cref="AnsiEscapeSequence.Request"/> to the raw output stream of the current <see cref="ConsoleDriver"/>.
     ///     Only call this method from the main UI thread. You should use <see cref="AnsiRequestScheduler"/> if
     ///     sending many requests.
     /// </summary>

+ 1 - 1
Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs

@@ -509,7 +509,7 @@ internal class WindowsDriver : ConsoleDriver
             case WindowsConsole.EventType.Mouse:
                 MouseEventArgs me = ToDriverMouse (inputEvent.MouseEvent);
 
-                if (me is null || me.Flags == MouseFlags.None)
+                if (/*me is null ||*/ me.Flags == MouseFlags.None)
                 {
                     break;
                 }

+ 0 - 93
Terminal.Gui/EnumExtensions/KeyBindingScopeExtensions.cs

@@ -1,93 +0,0 @@
-#nullable enable
-
-using System.CodeDom.Compiler;
-using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
-using System.Runtime.CompilerServices;
-
-namespace Terminal.Gui.EnumExtensions;
-
-/// <summary>Extension methods for the <see cref="Terminal.Gui.KeyBindingScope"/> <see langword="enum"/> type.</summary>
-[GeneratedCode ("Terminal.Gui.Analyzers.Internal", "1.0")]
-[CompilerGenerated]
-[DebuggerNonUserCode]
-[ExcludeFromCodeCoverage (Justification = "Generated code is already tested.")]
-[PublicAPI]
-public static class KeyBindingScopeExtensions
-{
-    /// <summary>
-    ///     Directly converts this <see cref="Terminal.Gui.KeyBindingScope"/> value to an <see langword="int"/> value with the
-    ///     same binary representation.
-    /// </summary>
-    /// <remarks>NO VALIDATION IS PERFORMED!</remarks>
-    [MethodImpl (MethodImplOptions.AggressiveInlining)]
-    public static int AsInt32 (this KeyBindingScope e) => Unsafe.As<KeyBindingScope, int> (ref e);
-
-    /// <summary>
-    ///     Directly converts this <see cref="Terminal.Gui.KeyBindingScope"/> value to a <see langword="uint"/> value with the
-    ///     same binary representation.
-    /// </summary>
-    /// <remarks>NO VALIDATION IS PERFORMED!</remarks>
-    [MethodImpl (MethodImplOptions.AggressiveInlining)]
-    public static uint AsUInt32 (this KeyBindingScope e) => Unsafe.As<KeyBindingScope, uint> (ref e);
-
-    /// <summary>
-    ///     Determines if the specified flags are set in the current value of this
-    ///     <see cref="Terminal.Gui.KeyBindingScope"/>.
-    /// </summary>
-    /// <remarks>NO VALIDATION IS PERFORMED!</remarks>
-    /// <returns>
-    ///     True, if all flags present in <paramref name="checkFlags"/> are also present in the current value of the
-    ///     <see cref="Terminal.Gui.KeyBindingScope"/>.<br/>Otherwise false.
-    /// </returns>
-    [MethodImpl (MethodImplOptions.AggressiveInlining)]
-    public static bool FastHasFlags (this KeyBindingScope e, KeyBindingScope checkFlags)
-    {
-        ref uint enumCurrentValueRef = ref Unsafe.As<KeyBindingScope, uint> (ref e);
-        ref uint checkFlagsValueRef = ref Unsafe.As<KeyBindingScope, uint> (ref checkFlags);
-
-        return (enumCurrentValueRef & checkFlagsValueRef) == checkFlagsValueRef;
-    }
-
-    /// <summary>
-    ///     Determines if the specified mask bits are set in the current value of this
-    ///     <see cref="Terminal.Gui.KeyBindingScope"/>.
-    /// </summary>
-    /// <param name="e">
-    ///     The <see cref="Terminal.Gui.KeyBindingScope"/> value to check against the <paramref name="mask"/>
-    ///     value.
-    /// </param>
-    /// <param name="mask">A mask to apply to the current value.</param>
-    /// <returns>
-    ///     True, if all bits set to 1 in the mask are also set to 1 in the current value of the
-    ///     <see cref="Terminal.Gui.KeyBindingScope"/>.<br/>Otherwise false.
-    /// </returns>
-    /// <remarks>NO VALIDATION IS PERFORMED!</remarks>
-    [MethodImpl (MethodImplOptions.AggressiveInlining)]
-    public static bool FastHasFlags (this KeyBindingScope e, int mask)
-    {
-        ref int enumCurrentValueRef = ref Unsafe.As<KeyBindingScope, int> (ref e);
-
-        return (enumCurrentValueRef & mask) == mask;
-    }
-
-    /// <summary>
-    ///     Determines if the specified <see langword="int"/> value is explicitly defined as a named value of the
-    ///     <see cref="Terminal.Gui.KeyBindingScope"/> <see langword="enum"/> type.
-    /// </summary>
-    /// <remarks>
-    ///     Only explicitly named values return true, as with IsDefined. Combined valid flag values of flags enums which are
-    ///     not explicitly named will return false.
-    /// </remarks>
-    public static bool FastIsDefined (this KeyBindingScope _, int value)
-    {
-        return value switch
-               {
-                   0 => true,
-                   1 => true,
-                   2 => true,
-                   4 => true,
-                   _ => false
-               };
-    }
-}

+ 7 - 2
Terminal.Gui/Input/Command.cs

@@ -4,9 +4,14 @@
 namespace Terminal.Gui;
 
 /// <summary>
-///     Actions which can be performed by a <see cref="View"/>. Commands are typically invoked via
-///     <see cref="View.KeyBindings"/> and mouse events.
+///     Actions which can be performed by a <see cref="View"/>.
 /// </summary>
+/// <seealso cref="View.KeyBindings"/>
+/// <seealso cref="View.MouseBindings"/>
+/// <seealso cref="Application.KeyBindings"/>
+/// <remarks>
+///     <see cref="Application"/> supports a subset of these commands by default, which can be overriden via <see cref="Application.KeyBindings"/>.
+/// </remarks>
 public enum Command
 {
     #region Base View Commands

+ 11 - 35
Terminal.Gui/Input/CommandContext.cs

@@ -3,52 +3,28 @@ namespace Terminal.Gui;
 
 #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved
 /// <summary>
-///     Provides context for a <see cref="Command"/> that is being invoked.
+///     Provides context for a <see cref="Command"/> invocation.
 /// </summary>
-/// <remarks>
-///     <para>
-///         To define a <see cref="Command"/> that is invoked with context,
-///         use <see cref="View.AddCommand(Command,Func{CommandContext,System.Nullable{bool}})"/>.
-///     </para>
-/// </remarks>
-/// <seealso cref="Application.KeyBindings"/>
-/// <seealso cref="View.KeyBindings"/>
-/// <seealso cref="Command"/>
+/// <seealso cref="View.Invoke(Command)"/>.
 #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved
-public record struct CommandContext
+public record struct CommandContext<TBinding> : ICommandContext
 {
     /// <summary>
-    ///     Initializes a new instance of <see cref="CommandContext"/> with the specified <see cref="Command"/>,
+    ///     Initializes a new instance with the specified <see cref="Command"/>,
     /// </summary>
     /// <param name="command"></param>
-    /// <param name="key"></param>
-    /// <param name="keyBinding"></param>
-    /// <param name="data"></param>
-    public CommandContext (Command command, Key? key, KeyBinding? keyBinding = null, object? data = null)
+    /// <param name="binding"></param>
+    public CommandContext (Command command, TBinding? binding)
     {
         Command = command;
-        Key = key;
-        KeyBinding = keyBinding;
-        Data = data;
+        Binding = binding;
     }
 
-    /// <summary>
-    ///     The <see cref="Command"/> that is being invoked.
-    /// </summary>
+    /// <inheritdoc />
     public Command Command { get; set; }
 
     /// <summary>
-    ///     The <see cref="Key"/> that is being invoked. This is the key that was pressed to invoke the <see cref="Command"/>.
-    /// </summary>
-    public Key? Key { get; set; }
-
-    /// <summary>
-    /// The KeyBinding that was used to invoke the <see cref="Command"/>, if any.
-    /// </summary>
-    public KeyBinding? KeyBinding { get; set; }
-
-    /// <summary>
-    ///     Arbitrary data.
+    /// The keyboard or mouse minding that was used to invoke the <see cref="Command"/>, if any.
     /// </summary>
-    public object? Data { get; set; }
-}
+    public TBinding? Binding { get; set; }
+}

+ 5 - 2
Terminal.Gui/Input/CommandEventArgs.cs

@@ -9,7 +9,10 @@ namespace Terminal.Gui;
 public class CommandEventArgs : CancelEventArgs
 {
     /// <summary>
-    ///     The context for the command.
+    ///     The context for the command, if any.
     /// </summary>
-    public CommandContext Context { get; init; }
+    /// <remarks>
+    ///     If <see langword="null"/> the command was invoked without context.
+    /// </remarks>
+    public required ICommandContext? Context { get; init; }
 }

+ 18 - 0
Terminal.Gui/Input/ICommandContext.cs

@@ -0,0 +1,18 @@
+#nullable enable
+namespace Terminal.Gui;
+
+#pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved
+/// <summary>
+///     Describes the context in which a <see cref="Command"/> is being invoked. <see cref="CommandContext{TBindingType}"/> inherits from this interface.
+///     When a <see cref="Command"/> is invoked,
+///     a context object is passed to Command handlers as an <see cref="ICommandContext"/> reference.
+/// </summary>
+/// <seealso cref="View.AddCommand(Command, View.CommandImplementation)"/>.
+#pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved
+public interface ICommandContext
+{
+    /// <summary>
+    ///     The <see cref="Command"/> that is being invoked.
+    /// </summary>
+    public Command Command { get; set; }
+}

+ 13 - 0
Terminal.Gui/Input/IInputBinding.cs

@@ -0,0 +1,13 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>
+///     Describes an input binding. Used to bind a set of <see cref="Command"/> objects to a specific input event.
+/// </summary>
+public interface IInputBinding
+{
+    /// <summary>
+    ///     Gets or sets the commands this input binding will invoke.
+    /// </summary>
+    Command [] Commands { get; set; }
+}

+ 233 - 0
Terminal.Gui/Input/InputBindings.cs

@@ -0,0 +1,233 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>
+///     Abstract class for <see cref="KeyBindings"/> and <see cref="MouseBindings"/>.
+/// </summary>
+/// <typeparam name="TEvent">The type of the event (e.g. <see cref="Key"/> or <see cref="MouseFlags"/>).</typeparam>
+/// <typeparam name="TBinding">The binding type (e.g. <see cref="KeyBinding"/>).</typeparam>
+public abstract class InputBindings<TEvent, TBinding> where TBinding : IInputBinding, new () where TEvent : notnull
+{
+    /// <summary>
+    ///     The bindings.
+    /// </summary>
+    private readonly Dictionary<TEvent, TBinding> _bindings;
+
+    private readonly Func<Command [], TEvent, TBinding> _constructBinding;
+
+    /// <summary>
+    ///     Initializes a new instance.
+    /// </summary>
+    /// <param name="constructBinding"></param>
+    /// <param name="equalityComparer"></param>
+    protected InputBindings (Func<Command [], TEvent, TBinding> constructBinding, IEqualityComparer<TEvent> equalityComparer)
+    {
+        _constructBinding = constructBinding;
+        _bindings = new (equalityComparer);
+    }
+
+    /// <summary>
+    ///     Tests whether <paramref name="eventArgs"/> is valid or not.
+    /// </summary>
+    /// <param name="eventArgs"></param>
+    /// <returns></returns>
+    public abstract bool IsValid (TEvent eventArgs);
+
+    /// <summary>Adds a <typeparamref name="TEvent"/> bound to <typeparamref name="TBinding"/> to the collection.</summary>
+    /// <param name="eventArgs"></param>
+    /// <param name="binding"></param>
+    public void Add (TEvent eventArgs, TBinding binding)
+    {
+        if (!IsValid (eventArgs))
+        {
+            throw new ArgumentException (@"Invalid newEventArgs", nameof (eventArgs));
+        }
+
+        if (TryGet (eventArgs, out TBinding _))
+        {
+            throw new InvalidOperationException (@$"A binding for {eventArgs} exists ({binding}).");
+        }
+
+        // IMPORTANT: Add a COPY of the eventArgs. This is needed because ConfigurationManager.Apply uses DeepMemberWiseCopy 
+        // IMPORTANT: update the memory referenced by the key, and Dictionary uses caching for performance, and thus 
+        // IMPORTANT: Apply will update the Dictionary with the new eventArgs, but the old eventArgs will still be in the dictionary.
+        // IMPORTANT: See the ConfigurationManager.Illustrate_DeepMemberWiseCopy_Breaks_Dictionary test for details.
+        _bindings.Add (eventArgs, binding);
+    }
+
+    /// <summary>
+    ///     <para>Adds a new <typeparamref name="TEvent"/> that will trigger the commands in <paramref name="commands"/>.</para>
+    ///     <para>
+    ///         If the <typeparamref name="TEvent"/> is already bound to a different set of <see cref="Command"/>s it will be rebound
+    ///         <paramref name="commands"/>.
+    ///     </para>
+    /// </summary>
+    /// <param name="eventArgs">The event to check.</param>
+    /// <param name="commands">
+    ///     The command to invoked on the <see cref="View"/> when <paramref name="eventArgs"/> is received. When
+    ///     multiple commands are provided,they will be applied in sequence. The bound <paramref name="eventArgs"/> event
+    ///     will be
+    ///     consumed if any took effect.
+    /// </param>
+    public void Add (TEvent eventArgs, params Command [] commands)
+    {
+        if (commands.Length == 0)
+        {
+            throw new ArgumentException (@"At least one command must be specified", nameof (commands));
+        }
+
+        if (TryGet (eventArgs, out TBinding? binding))
+        {
+            throw new InvalidOperationException (@$"A binding for {eventArgs} exists ({binding}).");
+        }
+
+        Add (eventArgs, _constructBinding (commands, eventArgs));
+    }
+
+    /// <summary>
+    ///     Gets the bindings.
+    /// </summary>
+    /// <returns></returns>
+    public IEnumerable<KeyValuePair<TEvent, TBinding>> GetBindings () { return _bindings; }
+
+    /// <summary>Removes all <typeparamref name="TEvent"/> objects from the collection.</summary>
+    public void Clear () { _bindings.Clear (); }
+
+    /// <summary>
+    ///     Removes all bindings that trigger the given command set. Views can have multiple different <typeparamref name="TEvent"/>
+    ///     bound to
+    ///     the same command sets and this method will clear all of them.
+    /// </summary>
+    /// <param name="command"></param>
+    public void Clear (params Command [] command)
+    {
+        KeyValuePair<TEvent, TBinding> [] kvps = _bindings
+                                                 .Where (kvp => kvp.Value.Commands.SequenceEqual (command))
+                                                 .ToArray ();
+
+        foreach (KeyValuePair<TEvent, TBinding> kvp in kvps)
+        {
+            Remove (kvp.Key);
+        }
+    }
+
+    /// <summary>Gets the <typeparamref name="TBinding"/> for the specified <typeparamref name="TEvent"/>.</summary>
+    /// <param name="eventArgs"></param>
+    /// <returns></returns>
+    public TBinding? Get (TEvent eventArgs)
+    {
+        if (TryGet (eventArgs, out TBinding? binding))
+        {
+            return binding;
+        }
+
+        throw new InvalidOperationException ($"{eventArgs} is not bound.");
+    }
+
+    /// <summary>Gets the commands bound with the specified <typeparamref name="TEvent"/>.</summary>
+    /// <remarks></remarks>
+    /// <param name="eventArgs">The <typeparamref name="TEvent"/> to check.</param>
+    /// <param name="binding">
+    ///     When this method returns, contains the commands bound with the <typeparamref name="TEvent"/>, if the <typeparamref name="TEvent"/> is
+    ///     not
+    ///     found; otherwise, null. This parameter is passed uninitialized.
+    /// </param>
+    /// <returns><see langword="true"/> if the <typeparamref name="TEvent"/> is bound; otherwise <see langword="false"/>.</returns>
+    public bool TryGet (TEvent eventArgs, out TBinding? binding) { return _bindings.TryGetValue (eventArgs, out binding); }
+
+    /// <summary>Gets the array of <see cref="Command"/>s bound to <paramref name="eventArgs"/> if it exists.</summary>
+    /// <param name="eventArgs">The <typeparamref name="TEvent"/> to check.</param>
+    /// <returns>
+    ///     The array of <see cref="Command"/>s if <paramref name="eventArgs"/> is bound. An empty <see cref="Command"/> array
+    ///     if not.
+    /// </returns>
+    public Command [] GetCommands (TEvent eventArgs)
+    {
+        if (TryGet (eventArgs, out TBinding? bindings))
+        {
+            return bindings!.Commands;
+        }
+
+        return [];
+    }
+
+    /// <summary>
+    ///     Gets the first matching <typeparamref name="TEvent"/> bound to the set of commands specified by
+    ///     <paramref name="commands"/>.
+    /// </summary>
+    /// <param name="commands">The set of commands to search.</param>
+    /// <returns>
+    ///     The first matching <typeparamref name="TEvent"/> bound to the set of commands specified by
+    ///     <paramref name="commands"/>. <see langword="null"/> if the set of caommands was not found.
+    /// </returns>
+    public TEvent GetFirstFromCommands (params Command [] commands) { return _bindings.FirstOrDefault (a => a.Value.Commands.SequenceEqual (commands)).Key; }
+
+    /// <summary>Gets all <typeparamref name="TEvent"/> bound to the set of commands specified by <paramref name="commands"/>.</summary>
+    /// <param name="commands">The set of commands to search.</param>
+    /// <returns>
+    ///     The <typeparamref name="TEvent"/>s bound to the set of commands specified by <paramref name="commands"/>. An empty list if
+    ///     the
+    ///     set of caommands was not found.
+    /// </returns>
+    public IEnumerable<TEvent> GetAllFromCommands (params Command [] commands)
+    {
+        return _bindings.Where (a => a.Value.Commands.SequenceEqual (commands)).Select (a => a.Key);
+    }
+
+    /// <summary>Replaces a <typeparamref name="TEvent"/> combination already bound to a set of <see cref="Command"/>s.</summary>
+    /// <remarks></remarks>
+    /// <param name="oldEventArgs">The <typeparamref name="TEvent"/> to be replaced.</param>
+    /// <param name="newEventArgs">
+    ///     The new <typeparamref name="TEvent"/> to be used.
+    /// </param>
+    public void Replace (TEvent oldEventArgs, TEvent newEventArgs)
+    {
+        if (!IsValid (newEventArgs))
+        {
+            throw new ArgumentException (@"Invalid newEventArgs", nameof (newEventArgs));
+        }
+
+        if (TryGet (oldEventArgs, out TBinding? binding))
+        {
+            Remove (oldEventArgs);
+            Add (newEventArgs, binding!);
+        }
+        else
+        {
+            Add (newEventArgs, binding!);
+        }
+    }
+
+    /// <summary>Replaces the commands already bound to a combination of <typeparamref name="TEvent"/>.</summary>
+    /// <remarks>
+    ///     <para>
+    ///         If the of <typeparamref name="TEvent"/> is not already bound, it will be added.
+    ///     </para>
+    /// </remarks>
+    /// <param name="eventArgs">The combination of <typeparamref name="TEvent"/> bound to the command to be replaced.</param>
+    /// <param name="newCommands">The set of commands to replace the old ones with.</param>
+    public void ReplaceCommands (TEvent eventArgs, params Command [] newCommands)
+    {
+        if (TryGet (eventArgs, out TBinding _))
+        {
+            Remove (eventArgs);
+            Add (eventArgs, newCommands);
+        }
+        else
+        {
+            Add (eventArgs, newCommands);
+        }
+    }
+
+    /// <summary>Removes a <typeparamref name="TEvent"/> from the collection.</summary>
+    /// <param name="eventArgs"></param>
+    public void Remove (TEvent eventArgs)
+    {
+        if (!TryGet (eventArgs, out _))
+        {
+            return;
+        }
+
+        _bindings.Remove (eventArgs);
+    }
+}

+ 0 - 58
Terminal.Gui/Input/KeyBindingScope.cs

@@ -1,58 +0,0 @@
-namespace Terminal.Gui;
-
-/// <summary>
-///     Defines the scope of a <see cref="Command"/> that has been bound to a key with
-///     <see cref="KeyBindings.Add(Key, Terminal.Gui.Command[])"/>.
-/// </summary>
-/// <remarks>
-///     <para>Key bindings are scoped to the most-focused view (<see cref="Focused"/>) by default.</para>
-/// </remarks>
-/// <seealso cref="Application.KeyBindings"/>
-/// <seealso cref="View.KeyBindings"/>
-/// <seealso cref="Command"/>
-[Flags]
-public enum KeyBindingScope
-{
-    /// <summary>The key binding is disabled.</summary>
-    Disabled = 0,
-
-    /// <summary>
-    ///     The key binding is scoped to just the view that has focus.
-    ///     <para>
-    ///     </para>
-    /// </summary>
-    /// <seealso cref="View.KeyBindings"/>
-    Focused = 1,
-
-    /// <summary>
-    ///     The key binding is scoped to the View's Superview hierarchy and the bound <see cref="Command"/>s will be invoked
-    ///     even when the View does not have
-    ///     focus, as
-    ///     long as some View up the SuperView hierachy does have focus. This is typically used for <see cref="View.HotKey"/>s.
-    ///     <para>
-    ///         The View must be visible.
-    ///     </para>
-    ///     <para>
-    ///         HotKey-scoped key bindings are only invoked if the key down event was not handled by the focused view or
-    ///         any of its subviews.
-    ///     </para>
-    /// </summary>
-    /// <seealso cref="View.KeyBindings"/>
-    /// <seeals cref="View.HotKey"/>
-    HotKey = 2,
-
-    /// <summary>
-    ///     The and the bound <see cref="Command"/>s will be invoked regardless of which View has focus. This is typically used
-    ///     for global
-    ///     commands, which are called Shortcuts.
-    ///     <para>
-    ///         The View does not need to be visible.
-    ///     </para>
-    ///     <para>
-    ///         Application-scoped key bindings are only invoked if the key down event was not handled by the focused view or
-    ///         any of its subviews, and if the key was not bound to a <see cref="View.HotKey"/>.
-    ///     </para>
-    /// </summary>
-    /// <seealso cref="Application.KeyBindings"/>
-    Application = 4
-}

+ 0 - 447
Terminal.Gui/Input/KeyBindings.cs

@@ -1,447 +0,0 @@
-#nullable enable
-
-using static System.Formats.Asn1.AsnWriter;
-
-namespace Terminal.Gui;
-
-/// <summary>
-///     Provides a collection of <see cref="KeyBinding"/> objects bound to a <see cref="Key"/>.
-/// </summary>
-/// <seealso cref="Application.KeyBindings"/>
-/// <seealso cref="View.KeyBindings"/>
-/// <seealso cref="Command"/>
-public class KeyBindings
-{
-    /// <summary>
-    ///     Initializes a new instance. This constructor is used when the <see cref="KeyBindings"/> are not bound to a
-    ///     <see cref="View"/>. This is used for Application.KeyBindings and unit tests.
-    /// </summary>
-    public KeyBindings () { }
-
-    /// <summary>Initializes a new instance bound to <paramref name="boundView"/>.</summary>
-    public KeyBindings (View? boundView) { BoundView = boundView; }
-
-    /// <summary>Adds a <see cref="KeyBinding"/> to the collection.</summary>
-    /// <param name="key"></param>
-    /// <param name="binding"></param>
-    /// <param name="boundViewForAppScope">Optional View for <see cref="KeyBindingScope.Application"/> bindings.</param>
-    public void Add (Key key, KeyBinding binding, View? boundViewForAppScope = null)
-    {
-        if (BoundView is { } && binding.Scope.FastHasFlags (KeyBindingScope.Application))
-        {
-            throw new InvalidOperationException ("Application scoped KeyBindings must be added via Application.KeyBindings.Add");
-        }
-
-        if (BoundView is { } && boundViewForAppScope is null)
-        {
-            boundViewForAppScope = BoundView;
-        }
-
-        if (TryGet (key, out KeyBinding _))
-        {
-            throw new InvalidOperationException (@$"A key binding for {key} exists ({binding}).");
-
-            //Bindings [key] = binding;
-        }
-
-        if (BoundView is { })
-        {
-            binding.BoundView = BoundView;
-        }
-        else
-        {
-            binding.BoundView = boundViewForAppScope;
-        }
-
-        // IMPORTANT: Add a COPY of the key. This is needed because ConfigurationManager.Apply uses DeepMemberWiseCopy 
-        // IMPORTANT: update the memory referenced by the key, and Dictionary uses caching for performance, and thus 
-        // IMPORTANT: Apply will update the Dictionary with the new key, but the old key will still be in the dictionary.
-        // IMPORTANT: See the ConfigurationManager.Illustrate_DeepMemberWiseCopy_Breaks_Dictionary test for details.
-        Bindings.Add (new (key), binding);
-    }
-
-    /// <summary>
-    ///     <para>Adds a new key combination that will trigger the commands in <paramref name="commands"/>.</para>
-    ///     <para>
-    ///         If the key is already bound to a different array of <see cref="Command"/>s it will be rebound
-    ///         <paramref name="commands"/>.
-    ///     </para>
-    /// </summary>
-    /// <remarks>
-    ///     Commands are only ever applied to the current <see cref="View"/> (i.e. this feature cannot be used to switch
-    ///     focus to another view and perform multiple commands there).
-    /// </remarks>
-    /// <param name="key">The key to check.</param>
-    /// <param name="scope">The scope for the command.</param>
-    /// <param name="boundViewForAppScope">Optional View for <see cref="KeyBindingScope.Application"/> bindings.</param>
-    /// <param name="commands">
-    ///     The command to invoked on the <see cref="View"/> when <paramref name="key"/> is pressed. When
-    ///     multiple commands are provided,they will be applied in sequence. The bound <paramref name="key"/> strike will be
-    ///     consumed if any took effect.
-    /// </param>
-    public void Add (Key key, KeyBindingScope scope, View? boundViewForAppScope = null, params Command [] commands)
-    {
-        if (BoundView is { } && scope.FastHasFlags (KeyBindingScope.Application))
-        {
-            throw new ArgumentException ("Application scoped KeyBindings must be added via Application.KeyBindings.Add");
-        }
-        else
-        {
-            // boundViewForAppScope = BoundView;
-        }
-
-        if (key is null || !key.IsValid)
-        {
-            //throw new ArgumentException ("Invalid Key", nameof (commands));
-            return;
-        }
-
-        if (commands.Length == 0)
-        {
-            throw new ArgumentException (@"At least one command must be specified", nameof (commands));
-        }
-
-        if (TryGet (key, out KeyBinding binding))
-        {
-            throw new InvalidOperationException (@$"A key binding for {key} exists ({binding}).");
-        }
-
-        Add (key, new KeyBinding (commands, scope, boundViewForAppScope), boundViewForAppScope);
-    }
-
-    /// <summary>
-    ///     <para>Adds a new key combination that will trigger the commands in <paramref name="commands"/>.</para>
-    ///     <para>
-    ///         If the key is already bound to a different array of <see cref="Command"/>s it will be rebound
-    ///         <paramref name="commands"/>.
-    ///     </para>
-    /// </summary>
-    /// <remarks>
-    ///     Commands are only ever applied to the current <see cref="View"/> (i.e. this feature cannot be used to switch
-    ///     focus to another view and perform multiple commands there).
-    /// </remarks>
-    /// <param name="key">The key to check.</param>
-    /// <param name="scope">The scope for the command.</param>
-    /// <param name="commands">
-    ///     The command to invoked on the <see cref="View"/> when <paramref name="key"/> is pressed. When
-    ///     multiple commands are provided,they will be applied in sequence. The bound <paramref name="key"/> strike will be
-    ///     consumed if any took effect.
-    /// </param>
-    public void Add (Key key, KeyBindingScope scope, params Command [] commands)
-    {
-        if (BoundView is null && !scope.FastHasFlags (KeyBindingScope.Application))
-        {
-            throw new InvalidOperationException ("BoundView cannot be null.");
-        }
-
-        if (BoundView is { } && scope.FastHasFlags (KeyBindingScope.Application))
-        {
-            throw new InvalidOperationException ("Application scoped KeyBindings must be added via Application.KeyBindings.Add");
-        }
-
-        if (key == Key.Empty || !key.IsValid)
-        {
-            throw new ArgumentException (@"Invalid Key", nameof (commands));
-        }
-
-        if (commands.Length == 0)
-        {
-            throw new ArgumentException (@"At least one command must be specified", nameof (commands));
-        }
-
-        if (TryGet (key, out KeyBinding binding))
-        {
-            throw new InvalidOperationException (@$"A key binding for {key} exists ({binding}).");
-        }
-
-        Add (key, new KeyBinding (commands, scope, BoundView), BoundView);
-    }
-
-    /// <summary>
-    ///     <para>
-    ///         Adds a new key combination that will trigger the commands in <paramref name="commands"/> (if supported by the
-    ///         View - see <see cref="View.GetSupportedCommands"/>).
-    ///     </para>
-    ///     <para>
-    ///         This is a helper function for <see cref="Add(Key,KeyBinding,View?)"/>. If used for a View (
-    ///         <see cref="BoundView"/> is set), the scope will be set to <see cref="KeyBindingScope.Focused"/>.
-    ///         Otherwise, it will be set to <see cref="KeyBindingScope.Application"/>.
-    ///     </para>
-    ///     <para>
-    ///         If the key is already bound to a different array of <see cref="Command"/>s it will be rebound
-    ///         <paramref name="commands"/>.
-    ///     </para>
-    /// </summary>
-    /// <remarks>
-    ///     Commands are only ever applied to the current <see cref="View"/> (i.e. this feature cannot be used to switch
-    ///     focus to another view and perform multiple commands there).
-    /// </remarks>
-    /// <param name="key">The key to check.</param>
-    /// <param name="boundViewForAppScope">Optional View for <see cref="KeyBindingScope.Application"/> bindings.</param>
-    /// <param name="commands">
-    ///     The command to invoked on the <see cref="View"/> when <paramref name="key"/> is pressed. When
-    ///     multiple commands are provided,they will be applied in sequence. The bound <paramref name="key"/> strike will be
-    ///     consumed if any took effect.
-    /// </param>
-    public void Add (Key key, View? boundViewForAppScope = null, params Command [] commands)
-    {
-        if (BoundView is null && boundViewForAppScope is null)
-        {
-            throw new ArgumentException (@"Application scoped KeyBindings must provide a bound view to Add.", nameof (boundViewForAppScope));
-        }
-
-        Add (key, BoundView is { } ? KeyBindingScope.Focused : KeyBindingScope.Application, boundViewForAppScope, commands);
-    }
-
-    /// <summary>
-    ///     <para>
-    ///         Adds a new key combination that will trigger the commands in <paramref name="commands"/> (if supported by the
-    ///         View - see <see cref="View.GetSupportedCommands"/>).
-    ///     </para>
-    ///     <para>
-    ///         This is a helper function for <see cref="Add(Key,KeyBinding,View?)"/>. If used for a View (
-    ///         <see cref="BoundView"/> is set), the scope will be set to <see cref="KeyBindingScope.Focused"/>.
-    ///         Otherwise, it will be set to <see cref="KeyBindingScope.Application"/>.
-    ///     </para>
-    ///     <para>
-    ///         If the key is already bound to a different array of <see cref="Command"/>s it will be rebound
-    ///         <paramref name="commands"/>.
-    ///     </para>
-    /// </summary>
-    /// <remarks>
-    ///     Commands are only ever applied to the current <see cref="View"/> (i.e. this feature cannot be used to switch
-    ///     focus to another view and perform multiple commands there).
-    /// </remarks>
-    /// <param name="key">The key to check.</param>
-    /// <param name="commands">
-    ///     The command to invoked on the <see cref="View"/> when <paramref name="key"/> is pressed. When
-    ///     multiple commands are provided,they will be applied in sequence. The bound <paramref name="key"/> strike will be
-    ///     consumed if any took effect.
-    /// </param>
-    public void Add (Key key, params Command [] commands)
-    {
-        if (BoundView is null)
-        {
-            throw new ArgumentException (@"Application scoped KeyBindings must provide a boundViewForAppScope to Add.");
-        }
-
-        Add (key, BoundView is { } ? KeyBindingScope.Focused : KeyBindingScope.Application, null, commands);
-    }
-
-    // TODO: Add a dictionary comparer that ignores Scope
-    // TODO: This should not be public!
-    /// <summary>The collection of <see cref="KeyBinding"/> objects.</summary>
-    public Dictionary<Key, KeyBinding> Bindings { get; } = new (new KeyEqualityComparer ());
-
-    /// <summary>
-    ///     Gets the keys that are bound.
-    /// </summary>
-    /// <returns></returns>
-    public IEnumerable<Key> GetBoundKeys ()
-    {
-        return Bindings.Keys;
-    }
-
-    /// <summary>
-    ///     The view that the <see cref="KeyBindings"/> are bound to.
-    /// </summary>
-    /// <remarks>
-    ///     If <see langword="null"/> the KeyBindings object is being used for Application.KeyBindings.
-    /// </remarks>
-    internal View? BoundView { get; }
-
-    /// <summary>Removes all <see cref="KeyBinding"/> objects from the collection.</summary>
-    public void Clear () { Bindings.Clear (); }
-
-    /// <summary>
-    ///     Removes all key bindings that trigger the given command set. Views can have multiple different keys bound to
-    ///     the same command sets and this method will clear all of them.
-    /// </summary>
-    /// <param name="command"></param>
-    public void Clear (params Command [] command)
-    {
-        KeyValuePair<Key, KeyBinding> [] kvps = Bindings
-                                                .Where (kvp => kvp.Value.Commands.SequenceEqual (command))
-                                                .ToArray ();
-
-        foreach (KeyValuePair<Key, KeyBinding> kvp in kvps)
-        {
-            Remove (kvp.Key);
-        }
-    }
-
-    /// <summary>Gets the <see cref="KeyBinding"/> for the specified <see cref="Key"/>.</summary>
-    /// <param name="key"></param>
-    /// <returns></returns>
-    public KeyBinding Get (Key key)
-    {
-        if (TryGet (key, out KeyBinding binding))
-        {
-            return binding;
-        }
-
-        throw new InvalidOperationException ($"Key {key} is not bound.");
-    }
-
-    /// <summary>Gets the <see cref="KeyBinding"/> for the specified <see cref="Key"/>.</summary>
-    /// <param name="key"></param>
-    /// <param name="scope"></param>
-    /// <returns></returns>
-    public KeyBinding Get (Key key, KeyBindingScope scope)
-    {
-        if (TryGet (key, scope, out KeyBinding binding))
-        {
-            return binding;
-        }
-
-        throw new InvalidOperationException ($"Key {key}/{scope} is not bound.");
-    }
-
-    /// <summary>Gets the array of <see cref="Command"/>s bound to <paramref name="key"/> if it exists.</summary>
-    /// <param name="key">The key to check.</param>
-    /// <returns>
-    ///     The array of <see cref="Command"/>s if <paramref name="key"/> is bound. An empty <see cref="Command"/> array
-    ///     if not.
-    /// </returns>
-    public Command [] GetCommands (Key key)
-    {
-        if (TryGet (key, out KeyBinding bindings))
-        {
-            return bindings.Commands;
-        }
-
-        return Array.Empty<Command> ();
-    }
-
-    /// <summary>Gets the first Key bound to the set of commands specified by <paramref name="commands"/>.</summary>
-    /// <param name="commands">The set of commands to search.</param>
-    /// <returns>The first <see cref="Key"/> bound to the set of commands specified by <paramref name="commands"/>. <see langword="null"/> if the set of caommands was not found.</returns>
-    public Key? GetKeyFromCommands (params Command [] commands)
-    {
-        return Bindings.FirstOrDefault (a => a.Value.Commands.SequenceEqual (commands)).Key;
-    }
-
-    /// <summary>Gets Keys bound to the set of commands specified by <paramref name="commands"/>.</summary>
-    /// <param name="commands">The set of commands to search.</param>
-    /// <returns>The <see cref="Key"/>s bound to the set of commands specified by <paramref name="commands"/>. An empty list if the set of caommands was not found.</returns>
-    public IEnumerable<Key> GetKeysFromCommands (params Command [] commands)
-    {
-        return Bindings.Where (a => a.Value.Commands.SequenceEqual (commands)).Select (a => a.Key);
-    }
-
-    /// <summary>Removes a <see cref="KeyBinding"/> from the collection.</summary>
-    /// <param name="key"></param>
-    /// <param name="boundViewForAppScope">Optional View for <see cref="KeyBindingScope.Application"/> bindings.</param>
-    public void Remove (Key key, View? boundViewForAppScope = null)
-    {
-        if (!TryGet (key, out KeyBinding _))
-        {
-            return;
-        }
-
-        Bindings.Remove (key);
-    }
-
-    /// <summary>Replaces the commands already bound to a key.</summary>
-    /// <remarks>
-    ///     <para>
-    ///         If the key is not already bound, it will be added.
-    ///     </para>
-    /// </remarks>
-    /// <param name="key">The key bound to the command to be replaced.</param>
-    /// <param name="commands">The set of commands to replace the old ones with.</param>
-    public void ReplaceCommands (Key key, params Command [] commands)
-    {
-        if (TryGet (key, out KeyBinding binding))
-        {
-            binding.Commands = commands;
-        }
-        else
-        {
-            Add (key, commands);
-        }
-    }
-
-    /// <summary>Replaces a key combination already bound to a set of <see cref="Command"/>s.</summary>
-    /// <remarks></remarks>
-    /// <param name="oldKey">The key to be replaced.</param>
-    /// <param name="newKey">The new key to be used. If <see cref="Key.Empty"/> no action will be taken.</param>
-    public void ReplaceKey (Key oldKey, Key newKey)
-    {
-        if (!TryGet (oldKey, out KeyBinding _))
-        {
-            throw new InvalidOperationException ($"Key {oldKey} is not bound.");
-        }
-
-        if (!newKey.IsValid)
-        {
-            throw new InvalidOperationException ($"Key {newKey} is is not valid.");
-        }
-
-        KeyBinding value = Bindings [oldKey];
-        Remove (oldKey);
-        Add (newKey, value);
-    }
-
-    /// <summary>Gets the commands bound with the specified Key.</summary>
-    /// <remarks></remarks>
-    /// <param name="key">The key to check.</param>
-    /// <param name="binding">
-    ///     When this method returns, contains the commands bound with the specified Key, if the Key is
-    ///     found; otherwise, null. This parameter is passed uninitialized.
-    /// </param>
-    /// <returns><see langword="true"/> if the Key is bound; otherwise <see langword="false"/>.</returns>
-    public bool TryGet (Key key, out KeyBinding binding)
-    {
-        //if (BoundView is null)
-        //{
-        //    throw new InvalidOperationException ("KeyBindings must be bound to a View to use this method.");
-        //}
-
-        binding = new (Array.Empty<Command> (), KeyBindingScope.Disabled, null);
-
-        if (key.IsValid)
-        {
-            return Bindings.TryGetValue (key, out binding);
-        }
-
-        return false;
-    }
-
-    /// <summary>Gets the commands bound with the specified Key that are scoped to a particular scope.</summary>
-    /// <remarks></remarks>
-    /// <param name="key">The key to check.</param>
-    /// <param name="scope">the scope to filter on</param>
-    /// <param name="binding">
-    ///     When this method returns, contains the commands bound with the specified Key, if the Key is
-    ///     found; otherwise, null. This parameter is passed uninitialized.
-    /// </param>
-    /// <returns><see langword="true"/> if the Key is bound; otherwise <see langword="false"/>.</returns>
-    public bool TryGet (Key key, KeyBindingScope scope, out KeyBinding binding)
-    {
-        if (!key.IsValid)
-        {
-        //if (BoundView is null)
-        //{
-        //    throw new InvalidOperationException ("KeyBindings must be bound to a View to use this method.");
-        //}
-
-            binding = new (Array.Empty<Command> (), KeyBindingScope.Disabled, null);
-            return false;
-        }
-
-        if (Bindings.TryGetValue (key, out binding))
-        {
-            if (scope.HasFlag (binding.Scope))
-            {
-                return true;
-            }
-        }
-        else
-        {
-            binding = new (Array.Empty<Command> (), KeyBindingScope.Disabled, null);
-        }
-
-        return false;
-    }
-}

+ 7 - 6
Terminal.Gui/Input/Key.cs → Terminal.Gui/Input/Keyboard/Key.cs

@@ -1,3 +1,4 @@
+#nullable enable
 using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 
@@ -211,7 +212,7 @@ public class Key : EventArgs, IEquatable<Key>
 #if DEBUG
             if (GetIsKeyCodeAtoZ (value) && (value & KeyCode.Space) != 0)
             {
-                throw new ArgumentException ($"Invalid KeyCode: {value} is invalid.", nameof (value));
+                throw new ArgumentException (@$"Invalid KeyCode: {value} is invalid.", nameof (value));
             }
 
 #endif
@@ -393,7 +394,7 @@ public class Key : EventArgs, IEquatable<Key>
     public static implicit operator string (Key key) { return key.ToString (); }
 
     /// <inheritdoc/>
-    public override bool Equals (object obj)
+    public override bool Equals (object? obj)
     {
         if (obj is Key other)
         {
@@ -402,7 +403,7 @@ public class Key : EventArgs, IEquatable<Key>
         return false;
     }
 
-    bool IEquatable<Key>.Equals (Key other) { return Equals (other); }
+    bool IEquatable<Key>.Equals (Key? other) { return Equals (other); }
 
     /// <inheritdoc/>
     public override int GetHashCode () { return _keyCode.GetHashCode (); }
@@ -471,7 +472,7 @@ public class Key : EventArgs, IEquatable<Key>
             return ((Rune)(uint)key).ToString ();
         }
 
-        string keyName = Enum.GetName (typeof (KeyCode), key);
+        string? keyName = Enum.GetName (typeof (KeyCode), key);
 
         return !string.IsNullOrEmpty (keyName) ? keyName : ((Rune)(uint)key).ToString ();
     }
@@ -574,7 +575,7 @@ public class Key : EventArgs, IEquatable<Key>
     /// <param name="key">The parsed value.</param>
     /// <returns>A boolean value indicating whether parsing was successful.</returns>
     /// <remarks></remarks>
-    public static bool TryParse (string text, [NotNullWhen (true)] out Key key)
+    public static bool TryParse (string text, out Key key)
     {
         if (string.IsNullOrEmpty (text))
         {
@@ -599,7 +600,7 @@ public class Key : EventArgs, IEquatable<Key>
                 return true;
         }
 
-        key = null;
+        key = null!;
 
         Rune separator = Separator;
 

+ 15 - 17
Terminal.Gui/Input/KeyBinding.cs → Terminal.Gui/Input/Keyboard/KeyBinding.cs

@@ -6,48 +6,46 @@
 namespace Terminal.Gui;
 
 /// <summary>
-/// Provides a collection of <see cref="Command"/> objects that are scoped to <see cref="KeyBindingScope"/>.
+/// Provides a collection of <see cref="Command"/> objects stored in <see cref="KeyBindings"/>.
 /// </summary>
 /// <seealso cref="Application.KeyBindings"/>
 /// <seealso cref="View.KeyBindings"/>
 /// <seealso cref="Command"/>
-public record struct KeyBinding
+public record struct KeyBinding : IInputBinding
 {
     /// <summary>Initializes a new instance.</summary>
     /// <param name="commands">The commands this key binding will invoke.</param>
-    /// <param name="scope">The scope of the <see cref="Commands"/>.</param>
     /// <param name="context">Arbitrary context that can be associated with this key binding.</param>
-    public KeyBinding (Command [] commands, KeyBindingScope scope, object? context = null)
+    public KeyBinding (Command [] commands, object? context = null)
     {
         Commands = commands;
-        Scope = scope;
-        Context = context;
+        Data = context;
     }
 
     /// <summary>Initializes a new instance.</summary>
     /// <param name="commands">The commands this key binding will invoke.</param>
-    /// <param name="scope">The scope of the <see cref="Commands"/>.</param>
-    /// <param name="boundView">The view the key binding is bound to.</param>
-    /// <param name="context">Arbitrary context that can be associated with this key binding.</param>
-    public KeyBinding (Command [] commands, KeyBindingScope scope, View? boundView, object? context = null)
+    /// <param name="target">The view the key binding is bound to.</param>
+    /// <param name="data">Arbitrary data that can be associated with this key binding.</param>
+    public KeyBinding (Command [] commands, View? target, object? data = null)
     {
         Commands = commands;
-        Scope = scope;
-        BoundView = boundView;
-        Context = context;
+        Target = target;
+        Data = data;
     }
 
     /// <summary>The commands this key binding will invoke.</summary>
     public Command [] Commands { get; set; }
 
-    /// <summary>The scope of the <see cref="Commands"/>.</summary>
-    public KeyBindingScope Scope { get; set; }
+    /// <summary>
+    ///     The Key that is bound to the <see cref="Commands"/>.
+    /// </summary>
+    public Key? Key { get; set; }
 
     /// <summary>The view the key binding is bound to.</summary>
-    public View? BoundView { get; set; }
+    public View? Target { get; set; }
 
     /// <summary>
     ///     Arbitrary context that can be associated with this key binding.
     /// </summary>
-    public object? Context { get; set; }
+    public object? Data { get; set; }
 }

+ 56 - 0
Terminal.Gui/Input/Keyboard/KeyBindings.cs

@@ -0,0 +1,56 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>
+///     Provides a collection of <see cref="Key"/>s bound to <see cref="Command"/>s.
+/// </summary>
+/// <seealso cref="Application.KeyBindings"/>
+/// <seealso cref="View.KeyBindings"/>
+/// <seealso cref="Command"/>
+public class KeyBindings : InputBindings<Key, KeyBinding>
+{
+    /// <summary>Initializes a new instance bound to <paramref name="target"/>.</summary>
+    public KeyBindings (View? target) : base ((commands, key) => new (commands), new KeyEqualityComparer ())
+    {
+        Target = target;
+    }
+
+    /// <inheritdoc />
+    public override bool IsValid (Key eventArgs) { return eventArgs.IsValid; }
+
+    /// <summary>
+    ///     <para>
+    ///         Adds a new key combination that will trigger the commands in <paramref name="commands"/> on the View
+    ///         specified by <paramref name="target"/>.
+    ///     </para>
+    ///     <para>
+    ///         If the key is already bound to a different array of <see cref="Command"/>s it will be rebound
+    ///         <paramref name="commands"/>.
+    ///     </para>
+    /// </summary>
+    /// <remarks>
+    /// </remarks>
+    /// <param name="key">The key to check.</param>
+    /// <param name="target">
+    ///     The View the commands will be invoked on. If <see langword="null"/>, the key will be bound to
+    ///     <see cref="Application"/>.
+    /// </param>
+    /// <param name="commands">
+    ///     The command to invoked on the <see paramref="target"/> when <paramref name="key"/> is pressed. When
+    ///     multiple commands are provided,they will be applied in sequence. The bound <paramref name="key"/> strike will be
+    ///     consumed if any took effect.
+    /// </param>
+    public void Add (Key key, View? target, params Command [] commands)
+    {
+        KeyBinding binding = new (commands, target);
+        Add (key, binding);
+    }
+
+    /// <summary>
+    ///     The view that the <see cref="KeyBindings"/> are bound to.
+    /// </summary>
+    /// <remarks>
+    ///     If <see langword="null"/> the KeyBindings object is being used for Application.KeyBindings.
+    /// </remarks>
+    public View? Target { get; init; }
+}

+ 0 - 0
Terminal.Gui/Input/KeyChangedEventArgs.cs → Terminal.Gui/Input/Keyboard/KeyChangedEventArgs.cs


+ 0 - 0
Terminal.Gui/Input/KeyEqualityComparer.cs → Terminal.Gui/Input/Keyboard/KeyEqualityComparer.cs


+ 0 - 0
Terminal.Gui/Input/KeystrokeNavigatorEventArgs.cs → Terminal.Gui/Input/Keyboard/KeystrokeNavigatorEventArgs.cs


+ 0 - 0
Terminal.Gui/Input/GrabMouseEventArgs.cs → Terminal.Gui/Input/Mouse/GrabMouseEventArgs.cs


+ 32 - 0
Terminal.Gui/Input/Mouse/MouseBinding.cs

@@ -0,0 +1,32 @@
+#nullable enable
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Provides a collection of <see cref="MouseFlags"/> bound to <see cref="Command"/>s.
+/// </summary>
+/// <seealso cref="MouseBindings"/>
+/// <seealso cref="Command"/>
+public record struct MouseBinding : IInputBinding
+{
+    /// <summary>Initializes a new instance.</summary>
+    /// <param name="commands">The commands this mouse binding will invoke.</param>
+    /// <param name="mouseFlags">The mouse flags that trigger this binding.</param>
+    public MouseBinding (Command [] commands, MouseFlags mouseFlags)
+    {
+        Commands = commands;
+
+        MouseEventArgs = new MouseEventArgs()
+        {
+            Flags = mouseFlags
+        };
+    }
+
+    /// <summary>The commands this binding will invoke.</summary>
+    public Command [] Commands { get; set; }
+
+    /// <summary>
+    ///     The mouse event arguments.
+    /// </summary>
+    public MouseEventArgs? MouseEventArgs { get; set; }
+}

+ 21 - 0
Terminal.Gui/Input/Mouse/MouseBindings.cs

@@ -0,0 +1,21 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>
+///     Provides a collection of <see cref="MouseBinding"/> objects bound to a combination of <see cref="MouseFlags"/>.
+/// </summary>
+/// <seealso cref="View.MouseBindings"/>
+/// <seealso cref="Command"/>
+public class MouseBindings : InputBindings<MouseFlags, MouseBinding>
+{
+    /// <summary>
+    ///     Initializes a new instance.
+    /// </summary>
+    public MouseBindings () : base (
+                                    (commands, flags) => new (commands, flags),
+                                    EqualityComparer<MouseFlags>.Default)
+    { }
+
+    /// <inheritdoc />
+    public override bool IsValid (MouseFlags eventArgs) { return eventArgs != MouseFlags.None; }
+}

+ 0 - 0
Terminal.Gui/Input/MouseEventArgs.cs → Terminal.Gui/Input/Mouse/MouseEventArgs.cs


+ 0 - 0
Terminal.Gui/Input/MouseFlags.cs → Terminal.Gui/Input/Mouse/MouseFlags.cs


+ 1 - 0
Terminal.Gui/Input/MouseFlagsChangedEventArgs.cs → Terminal.Gui/Input/Mouse/MouseFlagsChangedEventArgs.cs

@@ -1,5 +1,6 @@
 namespace Terminal.Gui;
 
+// TODO: This class is unnecessary. Replace it with CancelEventArgs<T> from Terminal.Gui.View\CancelEventArgs.cs
 /// <summary>Args for events that describe a change in <see cref="MouseFlags"/></summary>
 public class MouseFlagsChangedEventArgs : EventArgs
 {

+ 0 - 12
Terminal.Gui/Input/PointEventArgs.cs

@@ -1,12 +0,0 @@
-namespace Terminal.Gui;
-
-/// <summary>Event args for events which relate to a single <see cref="Point"/></summary>
-public class PointEventArgs : EventArgs
-{
-    /// <summary>Creates a new instance of the <see cref="PointEventArgs"/> class</summary>
-    /// <param name="p"></param>
-    public PointEventArgs (Point p) { Point = p; }
-
-    /// <summary>The point the event happened at</summary>
-    public Point Point { get; }
-}

+ 54 - 0
Terminal.Gui/Resources/Strings.Designer.cs

@@ -1437,6 +1437,42 @@ namespace Terminal.Gui.Resources {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized string similar to Copy Code_point.
+        /// </summary>
+        internal static string charMapCopyCP {
+            get {
+                return ResourceManager.GetString("charMapCopyCP", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Copy _Glyph.
+        /// </summary>
+        internal static string charMapCopyGlyph {
+            get {
+                return ResourceManager.GetString("charMapCopyGlyph", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Getting Codepoint Information.
+        /// </summary>
+        internal static string charMapCPInfoDlgTitle {
+            get {
+                return ResourceManager.GetString("charMapCPInfoDlgTitle", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Codepoint Information from.
+        /// </summary>
+        internal static string charMapInfoDlgInfoLabel {
+            get {
+                return ResourceManager.GetString("charMapInfoDlgInfoLabel", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized string similar to Co_lors.
         /// </summary>
@@ -1518,6 +1554,24 @@ namespace Terminal.Gui.Resources {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized string similar to Error.
+        /// </summary>
+        internal static string error {
+            get {
+                return ResourceManager.GetString("error", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to failed getting.
+        /// </summary>
+        internal static string failedGetting {
+            get {
+                return ResourceManager.GetString("failedGetting", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized string similar to Any Files.
         /// </summary>

+ 18 - 0
Terminal.Gui/Resources/Strings.resx

@@ -721,4 +721,22 @@
   <data name="ctxColors" xml:space="preserve">
     <value>Co_lors</value>
   </data>
+  <data name="charMapCPInfoDlgTitle" xml:space="preserve">
+    <value>Getting Codepoint Information</value>
+  </data>
+  <data name="charMapCopyGlyph" xml:space="preserve">
+    <value>Copy _Glyph</value>
+  </data>
+  <data name="charMapCopyCP" xml:space="preserve">
+    <value>Copy Code_point</value>
+  </data>
+  <data name="charMapInfoDlgInfoLabel" xml:space="preserve">
+    <value>Codepoint Information from</value>
+  </data>
+  <data name="error" xml:space="preserve">
+    <value>Error</value>
+  </data>
+  <data name="failedGetting" xml:space="preserve">
+    <value>failed getting</value>
+  </data>
 </root>

+ 10 - 10
Terminal.Gui/View/Adornment/Border.cs

@@ -1390,15 +1390,15 @@ public class Border : Adornment
                         return true; // Always eat
                     });
 
-        KeyBindings.Add (Key.Esc, KeyBindingScope.HotKey, Command.Quit);
-        KeyBindings.Add (Application.ArrangeKey, KeyBindingScope.HotKey, Command.Quit);
-        KeyBindings.Add (Key.CursorUp, KeyBindingScope.HotKey, Command.Up);
-        KeyBindings.Add (Key.CursorDown, KeyBindingScope.HotKey, Command.Down);
-        KeyBindings.Add (Key.CursorLeft, KeyBindingScope.HotKey, Command.Left);
-        KeyBindings.Add (Key.CursorRight, KeyBindingScope.HotKey, Command.Right);
-
-        KeyBindings.Add (Key.Tab, KeyBindingScope.HotKey, Command.Tab);
-        KeyBindings.Add (Key.Tab.WithShift, KeyBindingScope.HotKey, Command.BackTab);
+        HotKeyBindings.Add (Key.Esc, Command.Quit);
+        HotKeyBindings.Add (Application.ArrangeKey, Command.Quit);
+        HotKeyBindings.Add (Key.CursorUp,  Command.Up);
+        HotKeyBindings.Add (Key.CursorDown, Command.Down);
+        HotKeyBindings.Add (Key.CursorLeft, Command.Left);
+        HotKeyBindings.Add (Key.CursorRight, Command.Right);
+
+        HotKeyBindings.Add (Key.Tab, Command.Tab);
+        HotKeyBindings.Add (Key.Tab.WithShift,  Command.BackTab);
     }
 
     private void ApplicationOnMouseEvent (object? sender, MouseEventArgs e)
@@ -1472,7 +1472,7 @@ public class Border : Adornment
             _bottomSizeButton = null;
         }
 
-        KeyBindings.Clear ();
+        HotKeyBindings.Clear ();
 
         if (CanFocus)
         {

+ 61 - 54
Terminal.Gui/View/View.Command.cs

@@ -5,6 +5,8 @@ namespace Terminal.Gui;
 
 public partial class View // Command APIs
 {
+    private readonly Dictionary<Command, CommandImplementation> _commandImplementations = new ();
+
     #region Default Implementation
 
     /// <summary>
@@ -30,7 +32,7 @@ public partial class View // Command APIs
                     });
 
         // Space or single-click - Raise Selecting
-        AddCommand (Command.Select, (ctx) =>
+        AddCommand (Command.Select, ctx =>
                                     {
                                         if (RaiseSelecting (ctx) is true)
                                         {
@@ -54,7 +56,7 @@ public partial class View // Command APIs
     /// </summary>
     /// <remarks>
     /// <para>
-    ///     The <see cref="Accepting"/> event should raised after the state of the View has changed (after <see cref="Selecting"/> is raised).
+    ///     The <see cref="Accepting"/> event should be raised after the state of the View has changed (after <see cref="Selecting"/> is raised).
     /// </para>
     /// <para>
     ///    If the Accepting event is not handled, <see cref="Command.Accept"/> will be invoked on the SuperView, enabling default Accept behavior.
@@ -65,11 +67,11 @@ public partial class View // Command APIs
     /// </para>
     /// </remarks>
     /// <returns>
-    ///     <see langword="null"/> if no event was raised; input proessing should continue.
-    ///     <see langword="false"/> if the event was raised and was not handled (or cancelled); input proessing should continue.
-    ///     <see langword="true"/> if the event was raised and handled (or cancelled); input proessing should stop.
+    ///     <see langword="null"/> if no event was raised; input processing should continue.
+    ///     <see langword="false"/> if the event was raised and was not handled (or cancelled); input processing should continue.
+    ///     <see langword="true"/> if the event was raised and handled (or cancelled); input processing should stop.
     /// </returns>
-    protected bool? RaiseAccepting (CommandContext ctx)
+    protected bool? RaiseAccepting (ICommandContext? ctx)
     {
         CommandEventArgs args = new () { Context = ctx };
 
@@ -93,14 +95,17 @@ public partial class View // Command APIs
 
             if (isDefaultView != this && isDefaultView is Button { IsDefault: true } button)
             {
-                bool? handled = isDefaultView.InvokeCommand (Command.Accept, ctx: new (Command.Accept, null, null, this));
+                bool? handled = isDefaultView.InvokeCommand<KeyBinding> (Command.Accept, new ([Command.Accept], null, this));
                 if (handled == true)
                 {
                     return true;
                 }
             }
 
-            return SuperView?.InvokeCommand (Command.Accept, ctx: new (Command.Accept, null, null, this)) == true;
+            if (SuperView is { })
+            {
+                return SuperView?.InvokeCommand<KeyBinding> (Command.Accept, new ([Command.Accept], null, this)) is true;
+            }
         }
 
         return Accepting is null ? null : args.Cancel;
@@ -135,14 +140,14 @@ public partial class View // Command APIs
     ///     event. The default <see cref="Command.Select"/> handler calls this method.
     /// </summary>
     /// <remarks>
-    ///     The <see cref="Selecting"/> event should raised after the state of the View has been changed and before see <see cref="Accepting"/>.
+    ///     The <see cref="Selecting"/> event should be raised after the state of the View has been changed and before see <see cref="Accepting"/>.
     /// </remarks>
     /// <returns>
-    ///     <see langword="null"/> if no event was raised; input proessing should continue.
-    ///     <see langword="false"/> if the event was raised and was not handled (or cancelled); input proessing should continue.
-    ///     <see langword="true"/> if the event was raised and handled (or cancelled); input proessing should stop.
+    ///     <see langword="null"/> if no event was raised; input processing should continue.
+    ///     <see langword="false"/> if the event was raised and was not handled (or cancelled); input processing should continue.
+    ///     <see langword="true"/> if the event was raised and handled (or cancelled); input processing should stop.
     /// </returns>
-    protected bool? RaiseSelecting (CommandContext ctx)
+    protected bool? RaiseSelecting (ICommandContext? ctx)
     {
         CommandEventArgs args = new () { Context = ctx };
 
@@ -179,13 +184,13 @@ public partial class View // Command APIs
     ///     event. The default <see cref="Command.HotKey"/> handler calls this method.
     /// </summary>
     /// <returns>
-    ///     <see langword="null"/> if no event was raised; input proessing should continue.
-    ///     <see langword="false"/> if the event was raised and was not handled (or cancelled); input proessing should continue.
-    ///     <see langword="true"/> if the event was raised and handled (or cancelled); input proessing should stop.
+    ///     <see langword="null"/> if no event was raised; input processing should continue.
+    ///     <see langword="false"/> if the event was raised and was not handled (or cancelled); input processing should continue.
+    ///     <see langword="true"/> if the event was raised and handled (or cancelled); input processing should stop.
     /// </returns>
     protected bool? RaiseHandlingHotKey ()
     {
-        CommandEventArgs args = new ();
+        CommandEventArgs args = new () { Context = new CommandContext<KeyBinding> () { Command = Command.HotKey } };
 
         // Best practice is to invoke the virtual method first.
         // This allows derived classes to handle the event and potentially cancel it.
@@ -219,13 +224,13 @@ public partial class View // Command APIs
     /// <summary>
     /// Function signature commands.
     /// </summary>
-    /// <param name="ctx">Provides information about the circumstances of invoking the command (e.g. <see cref="CommandContext.Key"/>)</param>
+    /// <param name="ctx">Provides context about the circumstances of invoking the command.</param>
     /// <returns>
-    ///     <see langword="null"/> if no command was found; input proessing should continue.
-    ///     <see langword="false"/> if the command was invoked and was not handled (or cancelled); input proessing should continue.
-    ///     <see langword="true"/> if the command was invoked the command was handled (or cancelled); input proessing should stop.
+    ///     <see langword="null"/> if no event was raised; input processing should continue.
+    ///     <see langword="false"/> if the event was raised and was not handled (or cancelled); input processing should continue.
+    ///     <see langword="true"/> if the event was raised and handled (or cancelled); input processing should stop.
     /// </returns>
-    public delegate bool? CommandImplementation (CommandContext ctx);
+    public delegate bool? CommandImplementation (ICommandContext? ctx);
 
     /// <summary>
     ///     <para>
@@ -239,12 +244,12 @@ public partial class View // Command APIs
     /// </summary>
     /// <remarks>
     ///     <para>
-    ///         This version of AddCommand is for commands that require <see cref="CommandContext"/>.
+    ///         This version of AddCommand is for commands that require <see cref="ICommandContext"/>.
     ///     </para>
     /// </remarks>
     /// <param name="command">The command.</param>
     /// <param name="impl">The delegate.</param>
-    protected void AddCommand (Command command, CommandImplementation impl) { CommandImplementations [command] = impl; }
+    protected void AddCommand (Command command, CommandImplementation impl) { _commandImplementations [command] = impl; }
 
     /// <summary>
     ///     <para>
@@ -258,45 +263,44 @@ public partial class View // Command APIs
     /// </summary>
     /// <remarks>
     ///     <para>
-    ///         This version of AddCommand is for commands that do not require a <see cref="CommandContext"/>.
+    ///         This version of AddCommand is for commands that do not require context.
     ///         If the command requires context, use
     ///         <see cref="AddCommand(Command,CommandImplementation)"/>
     ///     </para>
     /// </remarks>
     /// <param name="command">The command.</param>
     /// <param name="impl">The delegate.</param>
-    protected void AddCommand (Command command, Func<bool?> impl) { CommandImplementations [command] = ctx => impl (); }
+    protected void AddCommand (Command command, Func<bool?> impl) { _commandImplementations [command] = ctx => impl (); }
 
     /// <summary>Returns all commands that are supported by this <see cref="View"/>.</summary>
     /// <returns></returns>
-    public IEnumerable<Command> GetSupportedCommands () { return CommandImplementations.Keys; }
+    public IEnumerable<Command> GetSupportedCommands () { return _commandImplementations.Keys; }
 
     /// <summary>
     ///     Invokes the specified commands.
     /// </summary>
     /// <param name="commands">The set of commands to invoke.</param>
-    /// <param name="key">The key that caused the command to be invoked, if any. This will be passed as context with the command.</param>
-    /// <param name="keyBinding">The key binding that was bound to the key and caused the invocation, if any. This will be passed as context with the command.</param>
+    /// <param name="binding">The binding that caused the invocation, if any. This will be passed as context with the command.</param>
     /// <returns>
-    ///     <see langword="null"/> if no command was found; input proessing should continue.
-    ///     <see langword="false"/> if at least one command was invoked and was not handled (or cancelled); input proessing should continue.
-    ///     <see langword="true"/> if at least one command was invoked the command was handled (or cancelled); input proessing should stop.
+    ///     <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>
-    public bool? InvokeCommands (Command [] commands, Key? key = null, KeyBinding? keyBinding = null)
+    public bool? InvokeCommands<TBindingType> (Command [] commands, TBindingType binding)
     {
         bool? toReturn = null;
 
         foreach (Command command in commands)
         {
-            if (!CommandImplementations.ContainsKey (command))
+            if (!_commandImplementations.ContainsKey (command))
             {
                 throw new NotSupportedException (
-                                                 @$"A KeyBinding was set up for the command {command} ({key}) but that command is not supported by this View ({GetType ().Name})"
+                                                 @$"A Binding was set up for the command {command} ({binding}) but that command is not supported by this View ({GetType ().Name})"
                                                 );
             }
 
             // each command has its own return value
-            bool? thisReturn = InvokeCommand (command, key, keyBinding);
+            bool? thisReturn = InvokeCommand<TBindingType> (command, binding);
 
             // if we haven't got anything yet, the current command result should be used
             toReturn ??= thisReturn;
@@ -311,41 +315,44 @@ public partial class View // Command APIs
         return toReturn;
     }
 
-    /// <summary>Invokes the specified command.</summary>
+    /// <summary>
+    /// Invokes the specified command.
+    /// </summary>
     /// <param name="command">The command to invoke.</param>
-    /// <param name="key">The key that caused the command to be invoked, if any. This will be passed as context with the command.</param>
-    /// <param name="keyBinding">The key binding that was bound to the key and caused the invocation, if any. This will be passed as context with the command.</param>
+    /// <param name="binding">The binding that caused the invocation, if any. This will be passed as context with the command.</param>
     /// <returns>
-    ///     <see langword="null"/> if no command was found; input proessing should continue.
-    ///     <see langword="false"/> if the command was invoked and was not handled (or cancelled); input proessing should continue.
-    ///     <see langword="true"/> if the command was invoked the command was handled (or cancelled); input proessing should stop.
+    ///     <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>
-    public bool? InvokeCommand (Command command, Key? key = null, KeyBinding? keyBinding = null)
+    public bool? InvokeCommand<TBindingType> (Command command, TBindingType binding)
     {
-        if (CommandImplementations.TryGetValue (command, out CommandImplementation? implementation))
+        if (_commandImplementations.TryGetValue (command, out CommandImplementation? implementation))
         {
-            var context = new CommandContext (command, key, keyBinding); // Create the context here
-            return implementation (context);
+            return implementation (new CommandContext<TBindingType> ()
+            {
+                Command = command,
+                Binding = binding,
+            });
         }
 
         return null;
     }
 
     /// <summary>
-    /// Invokes the specified command.
+    /// Invokes the specified command without context.
     /// </summary>
     /// <param name="command">The command to invoke.</param>
-    /// <param name="ctx">Context to pass with the invocation.</param>
     /// <returns>
-    ///     <see langword="null"/> if no command was found; input proessing should continue.
-    ///     <see langword="false"/> if the command was invoked and was not handled (or cancelled); input proessing should continue.
-    ///     <see langword="true"/> if the command was invoked the command was handled (or cancelled); input proessing should stop.
+    ///     <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>
-    public bool? InvokeCommand (Command command, CommandContext ctx)
+    public bool? InvokeCommand (Command command)
     {
-        if (CommandImplementations.TryGetValue (command, out CommandImplementation? implementation))
+        if (_commandImplementations.TryGetValue (command, out CommandImplementation? implementation))
         {
-            return implementation (ctx);
+            return implementation (null);
         }
 
         return null;

+ 83 - 125
Terminal.Gui/View/View.Keyboard.cs

@@ -1,6 +1,4 @@
 #nullable enable
-using System.Diagnostics;
-
 namespace Terminal.Gui;
 
 public partial class View // Keyboard APIs
@@ -14,6 +12,8 @@ public partial class View // Keyboard APIs
         KeyBindings.Add (Key.Space, Command.Select);
         KeyBindings.Add (Key.Enter, Command.Accept);
 
+        HotKeyBindings = new (this);
+
         // Note, setting HotKey will bind HotKey to Command.HotKey
         HotKeySpecifier = (Rune)'_';
         TitleTextFormatter.HotKeyChanged += TitleTextFormatter_HotKeyChanged;
@@ -152,50 +152,55 @@ public partial class View // Keyboard APIs
         }
 
         // Remove base version
-        if (KeyBindings.TryGet (prevHotKey, out _))
+        if (HotKeyBindings.TryGet (prevHotKey, out _))
         {
-            KeyBindings.Remove (prevHotKey);
+            HotKeyBindings.Remove (prevHotKey);
         }
 
         // Remove the Alt version
-        if (KeyBindings.TryGet (prevHotKey.WithAlt, out _))
+        if (HotKeyBindings.TryGet (prevHotKey.WithAlt, out _))
         {
-            KeyBindings.Remove (prevHotKey.WithAlt);
+            HotKeyBindings.Remove (prevHotKey.WithAlt);
         }
 
         if (_hotKey.IsKeyCodeAtoZ)
         {
             // Remove the shift version
-            if (KeyBindings.TryGet (prevHotKey.WithShift, out _))
+            if (HotKeyBindings.TryGet (prevHotKey.WithShift, out _))
             {
-                KeyBindings.Remove (prevHotKey.WithShift);
+                HotKeyBindings.Remove (prevHotKey.WithShift);
             }
 
             // Remove alt | shift version
-            if (KeyBindings.TryGet (prevHotKey.WithShift.WithAlt, out _))
+            if (HotKeyBindings.TryGet (prevHotKey.WithShift.WithAlt, out _))
             {
-                KeyBindings.Remove (prevHotKey.WithShift.WithAlt);
+                HotKeyBindings.Remove (prevHotKey.WithShift.WithAlt);
             }
         }
 
         // Add the new 
         if (newKey != Key.Empty)
         {
-            KeyBinding keyBinding = new ([Command.HotKey], KeyBindingScope.HotKey, context);
+            KeyBinding keyBinding = new ()
+            {
+                Commands = [Command.HotKey],
+                Key = newKey,
+                Data = context
+            };
 
             // Add the base and Alt key
-            KeyBindings.Remove (newKey);
-            KeyBindings.Add (newKey, keyBinding);
-            KeyBindings.Remove (newKey.WithAlt);
-            KeyBindings.Add (newKey.WithAlt, keyBinding);
+            HotKeyBindings.Remove (newKey);
+            HotKeyBindings.Add (newKey, keyBinding);
+            HotKeyBindings.Remove (newKey.WithAlt);
+            HotKeyBindings.Add (newKey.WithAlt, keyBinding);
 
             // If the Key is A..Z, add ShiftMask and AltMask | ShiftMask
             if (newKey.IsKeyCodeAtoZ)
             {
-                KeyBindings.Remove (newKey.WithShift);
-                KeyBindings.Add (newKey.WithShift, keyBinding);
-                KeyBindings.Remove (newKey.WithShift.WithAlt);
-                KeyBindings.Add (newKey.WithShift.WithAlt, keyBinding);
+                HotKeyBindings.Remove (newKey.WithShift);
+                HotKeyBindings.Add (newKey.WithShift, keyBinding);
+                HotKeyBindings.Remove (newKey.WithShift.WithAlt);
+                HotKeyBindings.Add (newKey.WithShift.WithAlt, keyBinding);
             }
         }
 
@@ -255,7 +260,8 @@ public partial class View // Keyboard APIs
     ///     <para>
     ///         If a more focused subview does not handle the key press, this method raises <see cref="OnKeyDown"/>/
     ///         <see cref="KeyDown"/> to allow the
-    ///         view to pre-process the key press. If <see cref="OnKeyDown"/>/<see cref="KeyDown"/> is not handled any commands bound to the key will be invoked.
+    ///         view to pre-process the key press. If <see cref="OnKeyDown"/>/<see cref="KeyDown"/> is not handled any commands
+    ///         bound to the key will be invoked.
     ///         Then, only if no key bindings are
     ///         handled, <see cref="OnKeyDownNotHandled"/>/<see cref="KeyDownNotHandled"/> will be raised allowing the view to
     ///         process the key press.
@@ -291,7 +297,14 @@ public partial class View // Keyboard APIs
         // During (this is what can be cancelled)
 
         // TODO: NewKeyDownEvent returns bool. It should be bool? so state of InvokeCommands can be reflected up stack
-        if (InvokeCommandsBoundToKey (key) is true || key.Handled)
+        if (InvokeCommands (key) is true || key.Handled)
+        {
+            return true;
+        }
+
+        bool? handled = false;
+
+        if (InvokeCommandsBoundToHotKey (key, ref handled))
         {
             return true;
         }
@@ -344,10 +357,6 @@ public partial class View // Keyboard APIs
     ///     and processing should stop.
     /// </returns>
     /// <remarks>
-    ///     <para>
-    ///         For processing <see cref="HotKey"/>s and commands, use <see cref="Command"/> and
-    ///         <see cref="KeyBindings.Add(Key, Command[])"/>instead.
-    ///     </para>
     ///     <para>Fires the <see cref="KeyDown"/> event.</para>
     /// </remarks>
     protected virtual bool OnKeyDown (Key key) { return false; }
@@ -373,10 +382,6 @@ public partial class View // Keyboard APIs
     /// </summary>
     /// <remarks>
     ///     <para>
-    ///         For processing <see cref="HotKey"/>s and commands, use <see cref="Command"/> and
-    ///         <see cref="KeyBindings.Add(Key, Command[])"/>instead.
-    ///     </para>
-    ///     <para>
     ///         Not all terminals support distinct key up notifications; applications should avoid depending on distinct
     ///         KeyUp events.
     ///     </para>
@@ -393,10 +398,6 @@ public partial class View // Keyboard APIs
     /// </summary>
     /// <remarks>
     ///     <para>
-    ///         For processing <see cref="HotKey"/>s and commands, use <see cref="Command"/> and
-    ///         <see cref="KeyBindings.Add(Key, Command[])"/>instead.
-    ///     </para>
-    ///     <para>
     ///         SubViews can use the <see cref="KeyDownNotHandled"/> of their super view override the default behavior of when
     ///         key bindings are invoked.
     ///     </para>
@@ -499,10 +500,11 @@ public partial class View // Keyboard APIs
 
     #region Key Bindings
 
-    /// <summary>Gets the key bindings for this view.</summary>
+    /// <summary>Gets the bindings for this view that will be invoked only if this view has focus.</summary>
     public KeyBindings KeyBindings { get; internal set; } = null!;
 
-    private Dictionary<Command, CommandImplementation> CommandImplementations { get; } = new ();
+    /// <summary>Gets the bindings for this view that will be invoked regardless of whether this view has focus or not.</summary>
+    public KeyBindings HotKeyBindings { get; internal set; } = null!;
 
     /// <summary>
     ///     INTERNAL API: Invokes any commands bound to <paramref name="key"/> on this view, adornments, and subviews.
@@ -516,17 +518,15 @@ public partial class View // Keyboard APIs
     ///     <see langword="true"/> if at least one command was invoked and handled (or
     ///     cancelled); input processing should stop.
     /// </returns>
-    internal bool? InvokeCommandsBoundToKey (Key key)
+    internal bool? InvokeCommands (Key key)
     {
-        KeyBindingScope scope = KeyBindingScope.Focused | KeyBindingScope.HotKey;
-
         // * If no key binding was found, `InvokeKeyBindings` returns `null`.
         //   Continue passing the event (return `false` from `OnInvokeKeyBindings`).
         // * If key bindings were found, but none handled the key (all `Command`s returned `false`),
         //   `InvokeKeyBindings` returns `false`. Continue passing the event (return `false` from `OnInvokeKeyBindings`)..
         // * If key bindings were found, and any handled the key (at least one `Command` returned `true`),
         //   `InvokeKeyBindings` returns `true`. Continue passing the event (return `false` from `OnInvokeKeyBindings`).
-        bool?  handled = InvokeCommands (key, scope);
+        bool? handled = DoInvokeCommands (key);
 
         if (handled is true)
         {
@@ -535,22 +535,17 @@ public partial class View // Keyboard APIs
             return handled;
         }
 
-        if (Margin is { } && InvokeCommandsBoundToKeyOnAdornment (Margin, key, scope, ref handled))
-        {
-            return true;
-        }
-
-        if (Padding is { } && InvokeCommandsBoundToKeyOnAdornment (Padding, key, scope, ref handled))
+        if (Margin is { } && InvokeCommandsBoundToKeyOnAdornment (Margin, key, ref handled))
         {
             return true;
         }
 
-        if (Border is { } && InvokeCommandsBoundToKeyOnAdornment (Border, key, scope, ref handled))
+        if (Padding is { } && InvokeCommandsBoundToKeyOnAdornment (Padding, key, ref handled))
         {
             return true;
         }
 
-        if (InvokeCommandsBoundToKeyOnSubviews (key, scope, ref handled))
+        if (Border is { } && InvokeCommandsBoundToKeyOnAdornment (Border, key, ref handled))
         {
             return true;
         }
@@ -558,9 +553,9 @@ public partial class View // Keyboard APIs
         return handled;
     }
 
-    private static bool InvokeCommandsBoundToKeyOnAdornment (Adornment adornment, Key key, KeyBindingScope scope, ref bool? handled)
+    private static bool InvokeCommandsBoundToKeyOnAdornment (Adornment adornment, Key key, ref bool? handled)
     {
-        bool? adornmentHandled = adornment.InvokeCommandsBoundToKey (key);
+        bool? adornmentHandled = adornment.InvokeCommands (key);
 
         if (adornmentHandled is true)
         {
@@ -574,7 +569,7 @@ public partial class View // Keyboard APIs
 
         foreach (View subview in adornment.Subviews)
         {
-            bool? subViewHandled = subview.InvokeCommandsBoundToKey (key);
+            bool? subViewHandled = subview.InvokeCommands (key);
 
             if (subViewHandled is { })
             {
@@ -590,76 +585,35 @@ public partial class View // Keyboard APIs
         return false;
     }
 
-    private bool InvokeCommandsBoundToKeyOnSubviews (Key key, KeyBindingScope scope, ref bool? handled, bool invoke = true)
+    // BUGBUG: This will miss any hotkeys in subviews of Adornments.
+    /// <summary>
+    ///     Invokes any commands bound to <paramref name="hotKey"/> on this view and subviews.
+    /// </summary>
+    /// <param name="hotKey"></param>
+    /// <param name="handled"></param>
+    /// <returns></returns>
+    internal bool InvokeCommandsBoundToHotKey (Key hotKey, ref bool? handled)
     {
-        // Now, process any key bindings in the subviews that are tagged to KeyBindingScope.HotKey.
-        foreach (View subview in Subviews)
+        // Process this View
+        if (HotKeyBindings.TryGet (hotKey, out KeyBinding binding))
         {
-            if (subview == Focused)
-            {
-                continue;
-            }
-
-            if (subview.KeyBindings.TryGet (key, scope, out KeyBinding binding))
-            {
-                if (binding.Scope == KeyBindingScope.Focused && !subview.HasFocus)
-                {
-                    continue;
-                }
-
-                if (!invoke)
-                {
-                    return true;
-                }
-
-                bool? subViewHandled = subview.InvokeCommandsBoundToKey (key);
-
-                if (subViewHandled is { })
-                {
-                    handled = subViewHandled;
-
-                    if ((bool)subViewHandled)
-                    {
-                        return true;
-                    }
-                }
-            }
-
-            bool recurse = subview.InvokeCommandsBoundToKeyOnSubviews (key, scope, ref handled, invoke);
-
-            if (recurse || (handled is { } && (bool)handled))
+            if (InvokeCommands (binding.Commands, binding) is true)
             {
                 return true;
             }
         }
 
-        return false;
-    }
-
-    // TODO: This is a "prototype" debug check. It may be too annoying vs. useful.
-    // TODO: A better approach would be to have Application hold a list of bound Hotkeys, similar to
-    // TODO: how Application holds a list of Application Scoped key bindings and then check that list.
-    /// <summary>
-    ///     Returns true if Key is bound in this view hierarchy. For debugging
-    /// </summary>
-    /// <param name="key">The key to test.</param>
-    /// <param name="boundView">Returns the view the key is bound to.</param>
-    /// <returns></returns>
-    public bool IsHotKeyBound (Key key, out View? boundView)
-    {
-        // recurse through the subviews to find the views that has the key bound
-        boundView = null;
-
+        // Now, process any HotKey bindings in the subviews
         foreach (View subview in Subviews)
         {
-            if (subview.KeyBindings.TryGet (key, KeyBindingScope.HotKey, out _))
+            if (subview == Focused)
             {
-                boundView = subview;
-
-                return true;
+                continue;
             }
 
-            if (subview.IsHotKeyBound (key, out boundView))
+            bool recurse = subview.InvokeCommandsBoundToHotKey (hotKey, ref handled);
+
+            if (recurse || (handled is { } && (bool)handled))
             {
                 return true;
             }
@@ -673,7 +627,6 @@ public partial class View // Keyboard APIs
     ///     <para>See <see href="../docs/keyboard.md">for an overview of Terminal.Gui keyboard APIs.</see></para>
     /// </summary>
     /// <param name="key">The key event passed.</param>
-    /// <param name="scope">The scope.</param>
     /// <returns>
     ///     <see langword="null"/> if no command was invoked; input processing should continue.
     ///     <see langword="false"/> if at least one command was invoked and was not handled (or cancelled); input processing
@@ -681,31 +634,36 @@ public partial class View // Keyboard APIs
     ///     <see langword="true"/> if at least one command was invoked and handled (or cancelled); input processing should
     ///     stop.
     /// </returns>
-    protected bool? InvokeCommands (Key key, KeyBindingScope scope)
+    protected bool? DoInvokeCommands (Key key)
     {
-        if (!KeyBindings.TryGet (key, scope, out KeyBinding binding))
+        if (!KeyBindings.TryGet (key, out KeyBinding binding))
         {
             return null;
         }
 
-#if DEBUG
-
-        //if (Application.KeyBindings.TryGet (key, out KeyBinding b))
-        //{
-        //    Debug.WriteLine (
-        //                     $"WARNING: InvokeKeyBindings ({key}) - An Application scope binding exists for this key. The registered view will not invoke Command.");
-        //}
+        return InvokeCommands (binding.Commands, binding);
+    }
 
-        // TODO: This is a "prototype" debug check. It may be too annoying vs. useful.
-        // Scour the bindings up our View hierarchy
-        // to ensure that the key is not already bound to a different set of commands.
-        if (SuperView?.IsHotKeyBound (key, out View? previouslyBoundView) ?? false)
+    /// <summary>
+    ///     Invokes the Commands bound to <paramref name="hotKey"/>.
+    ///     <para>See <see href="../docs/keyboard.md">for an overview of Terminal.Gui keyboard APIs.</see></para>
+    /// </summary>
+    /// <param name="hotKey">The hot key event passed.</param>
+    /// <returns>
+    ///     <see langword="null"/> if no command was invoked; input processing should continue.
+    ///     <see langword="false"/> if at least one command was invoked and was not handled (or cancelled); input processing
+    ///     should continue.
+    ///     <see langword="true"/> if at least one command was invoked and handled (or cancelled); input processing should
+    ///     stop.
+    /// </returns>
+    protected bool? InvokeCommandsBoundToHotKey (Key hotKey)
+    {
+        if (!HotKeyBindings.TryGet (hotKey, out KeyBinding binding))
         {
-            Debug.WriteLine ($"WARNING: InvokeKeyBindings ({key}) - A subview or peer has bound this Key and will not see it: {previouslyBoundView}.");
+            return null;
         }
 
-#endif
-        return InvokeCommands (binding.Commands, key, binding);
+        return InvokeCommands (binding.Commands, binding);
     }
 
     #endregion Key Bindings

+ 205 - 91
Terminal.Gui/View/View.Mouse.cs

@@ -5,13 +5,53 @@ namespace Terminal.Gui;
 
 public partial class View // Mouse APIs
 {
+    /// <summary>Gets the mouse bindings for this view.</summary>
+    public MouseBindings MouseBindings { get; internal set; } = null!;
+
+    private void SetupMouse ()
+    {
+        MouseBindings = new ();
+
+        // TODO: Should the default really work with any button or just button1?
+        MouseBindings.Add (MouseFlags.Button1Clicked, Command.Select);
+        MouseBindings.Add (MouseFlags.Button2Clicked, Command.Select);
+        MouseBindings.Add (MouseFlags.Button3Clicked, Command.Select);
+        MouseBindings.Add (MouseFlags.Button4Clicked, Command.Select);
+        MouseBindings.Add (MouseFlags.Button1Clicked | MouseFlags.ButtonCtrl, Command.Select);
+    }
+
+    /// <summary>
+    ///     Invokes the Commands bound to the MouseFlags specified by <paramref name="mouseEventArgs"/>.
+    ///     <para>See <see href="../docs/mouse.md">for an overview of Terminal.Gui mouse APIs.</see></para>
+    /// </summary>
+    /// <param name="mouseEventArgs">The mouse event passed.</param>
+    /// <returns>
+    ///     <see langword="null"/> if no command was invoked; input processing should continue.
+    ///     <see langword="false"/> if at least one command was invoked and was not handled (or cancelled); input processing
+    ///     should continue.
+    ///     <see langword="true"/> if at least one command was invoked and handled (or cancelled); input processing should
+    ///     stop.
+    /// </returns>
+    protected bool? InvokeCommandsBoundToMouse (MouseEventArgs mouseEventArgs)
+    {
+        if (!MouseBindings.TryGet (mouseEventArgs.Flags, out MouseBinding binding))
+        {
+            return null;
+        }
+
+        binding.MouseEventArgs = mouseEventArgs;
+
+        return InvokeCommands (binding.Commands, binding);
+    }
+
     #region MouseEnterLeave
 
     private bool _hovering;
     private ColorScheme? _savedNonHoverColorScheme;
 
     /// <summary>
-    ///     INTERNAL Called by <see cref="Application.RaiseMouseEvent"/> when the mouse moves over the View's <see cref="Frame"/>.
+    ///     INTERNAL Called by <see cref="Application.RaiseMouseEvent"/> when the mouse moves over the View's
+    ///     <see cref="Frame"/>.
     ///     <see cref="MouseLeave"/> will
     ///     be raised when the mouse is no longer over the <see cref="Frame"/>. If another View occludes this View, the
     ///     that View will also receive MouseEnter/Leave events.
@@ -126,7 +166,8 @@ public partial class View // Mouse APIs
     public event EventHandler<CancelEventArgs>? MouseEnter;
 
     /// <summary>
-    ///     INTERNAL Called by <see cref="Application.RaiseMouseEvent"/> when the mouse leaves <see cref="Frame"/>, or is occluded
+    ///     INTERNAL Called by <see cref="Application.RaiseMouseEvent"/> when the mouse leaves <see cref="Frame"/>, or is
+    ///     occluded
     ///     by another non-SubView.
     /// </summary>
     /// <remarks>
@@ -204,7 +245,8 @@ public partial class View // Mouse APIs
     public bool WantMousePositionReports { get; set; }
 
     /// <summary>
-    ///     Processes a new <see cref="MouseEvent"/>. This method is called by <see cref="Application.RaiseMouseEvent"/> when a mouse
+    ///     Processes a new <see cref="MouseEvent"/>. This method is called by <see cref="Application.RaiseMouseEvent"/> when a
+    ///     mouse
     ///     event occurs.
     /// </summary>
     /// <remarks>
@@ -219,8 +261,10 @@ public partial class View // Mouse APIs
     ///         See <see cref="SetPressedHighlight"/> for more information.
     ///     </para>
     ///     <para>
-    ///         If <see cref="WantContinuousButtonPressed"/> is <see langword="true"/>, the <see cref="RaiseMouseEvent"/>/<see cref="MouseEvent"/> event
-    ///         will be raised on any new mouse event where <see cref="Terminal.Gui.MouseEventArgs.Flags"/> indicates a button is pressed.
+    ///         If <see cref="WantContinuousButtonPressed"/> is <see langword="true"/>, the <see cref="RaiseMouseEvent"/>/
+    ///         <see cref="MouseEvent"/> event
+    ///         will be raised on any new mouse event where <see cref="Terminal.Gui.MouseEventArgs.Flags"/> indicates a button
+    ///         is pressed.
     ///     </para>
     /// </remarks>
     /// <param name="mouseEvent"></param>
@@ -269,14 +313,18 @@ public partial class View // Mouse APIs
             }
         }
 
+        // We get here if the view did not handle the mouse event via OnMouseEvent/MouseEvent and
+        // it did not handle the press/release/clicked events via HandlePress/HandleRelease/HandleClicked
         if (mouseEvent.IsSingleDoubleOrTripleClicked)
         {
-            // If it's a click, and we didn't handle it, then we need to generate a click event
-            // We get here if the view did not handle the mouse event via OnMouseEvent/MouseEvent and
-            // it did not handle the press/release/clicked events via HandlePress/HandleRelease/HandleClicked
             return RaiseMouseClickEvent (mouseEvent);
         }
 
+        if (mouseEvent.IsWheel)
+        {
+            return RaiseMouseWheelEvent (mouseEvent);
+        }
+
         return false;
     }
 
@@ -287,7 +335,7 @@ public partial class View // Mouse APIs
     /// <returns><see langword="true"/>, if the event was handled, <see langword="false"/> otherwise.</returns>
     public bool RaiseMouseEvent (MouseEventArgs mouseEvent)
     {
-        if (OnMouseEvent (mouseEvent) || mouseEvent.Handled == true)
+        if (OnMouseEvent (mouseEvent) || mouseEvent.Handled)
         {
             return true;
         }
@@ -305,10 +353,7 @@ public partial class View // Mouse APIs
     /// </remarks>
     /// <param name="mouseEvent"></param>
     /// <returns><see langword="true"/>, if the event was handled, <see langword="false"/> otherwise.</returns>
-    protected virtual bool OnMouseEvent (MouseEventArgs mouseEvent)
-    {
-        return false;
-    }
+    protected virtual bool OnMouseEvent (MouseEventArgs mouseEvent) { return false; }
 
     /// <summary>Raised when a mouse event occurs.</summary>
     /// <remarks>
@@ -320,6 +365,98 @@ public partial class View // Mouse APIs
 
     #endregion Low Level Mouse Events
 
+    #region Mouse Pressed Events
+
+    /// <summary>
+    ///     INTERNAL For cases where the view is grabbed and the mouse is clicked, this method handles the released event
+    ///     (typically
+    ///     when <see cref="WantContinuousButtonPressed"/> or <see cref="HighlightStyle"/> are set).
+    /// </summary>
+    /// <remarks>
+    ///     Marked internal just to support unit tests
+    /// </remarks>
+    /// <param name="mouseEvent"></param>
+    /// <returns><see langword="true"/>, if the event was handled, <see langword="false"/> otherwise.</returns>
+    internal bool WhenGrabbedHandleReleased (MouseEventArgs mouseEvent)
+    {
+        mouseEvent.Handled = false;
+
+        if (mouseEvent.IsReleased)
+        {
+            if (Application.MouseGrabView == this)
+            {
+                SetPressedHighlight (HighlightStyle.None);
+            }
+
+            return mouseEvent.Handled = true;
+        }
+
+        return false;
+    }
+
+    /// <summary>
+    ///     INTERNAL For cases where the view is grabbed and the mouse is clicked, this method handles the released event
+    ///     (typically
+    ///     when <see cref="WantContinuousButtonPressed"/> or <see cref="HighlightStyle"/> are set).
+    /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         Marked internal just to support unit tests
+    ///     </para>
+    /// </remarks>
+    /// <param name="mouseEvent"></param>
+    /// <returns><see langword="true"/>, if the event was handled, <see langword="false"/> otherwise.</returns>
+    private bool WhenGrabbedHandlePressed (MouseEventArgs mouseEvent)
+    {
+        mouseEvent.Handled = false;
+
+        if (mouseEvent.IsPressed)
+        {
+            // The first time we get pressed event, grab the mouse and set focus
+            if (Application.MouseGrabView != this)
+            {
+                Application.GrabMouse (this);
+
+                if (!HasFocus && CanFocus)
+                {
+                    // Set the focus, but don't invoke Accept
+                    SetFocus ();
+                }
+
+                mouseEvent.Handled = true;
+            }
+
+            if (Viewport.Contains (mouseEvent.Position))
+            {
+                if (this is not Adornment
+                    && SetPressedHighlight (HighlightStyle.HasFlag (HighlightStyle.Pressed) ? HighlightStyle.Pressed : HighlightStyle.None))
+                {
+                    return true;
+                }
+            }
+            else
+            {
+                if (this is not Adornment
+                    && SetPressedHighlight (HighlightStyle.HasFlag (HighlightStyle.PressedOutside) ? HighlightStyle.PressedOutside : HighlightStyle.None))
+
+                {
+                    return true;
+                }
+            }
+
+            if (WantContinuousButtonPressed && Application.MouseGrabView == this)
+            {
+                return RaiseMouseClickEvent (mouseEvent);
+            }
+
+            return mouseEvent.Handled = true;
+        }
+
+        return false;
+    }
+
+    #endregion Mouse Pressed Events
+
     #region Mouse Click Events
 
     /// <summary>Raises the <see cref="OnMouseClick"/>/<see cref="MouseClick"/> event.</summary>
@@ -328,7 +465,8 @@ public partial class View // Mouse APIs
     ///         Called when the mouse is either clicked or double-clicked.
     ///     </para>
     ///     <para>
-    ///         If <see cref="WantContinuousButtonPressed"/> is <see langword="true"/>, will be invoked on every mouse event where
+    ///         If <see cref="WantContinuousButtonPressed"/> is <see langword="true"/>, will be invoked on every mouse event
+    ///         where
     ///         the mouse button is pressed.
     ///     </para>
     /// </remarks>
@@ -358,9 +496,8 @@ public partial class View // Mouse APIs
 
         // Post-conditions
 
-        // Always invoke Select command on MouseClick
         // By default, this will raise Selecting/OnSelecting - Subclasses can override this via AddCommand (Command.Select ...).
-        args.Handled = InvokeCommand (Command.Select, ctx: new (Command.Select, key: null, data: args)) == true;
+        args.Handled = InvokeCommandsBoundToMouse (args) == true;
 
         return args.Handled;
     }
@@ -373,7 +510,8 @@ public partial class View // Mouse APIs
     ///         Called when the mouse is either clicked or double-clicked.
     ///     </para>
     ///     <para>
-    ///         If <see cref="WantContinuousButtonPressed"/> is <see langword="true"/>, will be called on every mouse event where
+    ///         If <see cref="WantContinuousButtonPressed"/> is <see langword="true"/>, will be called on every mouse event
+    ///         where
     ///         the mouse button is pressed.
     ///     </para>
     /// </remarks>
@@ -387,14 +525,16 @@ public partial class View // Mouse APIs
     ///         Raised when the mouse is either clicked or double-clicked.
     ///     </para>
     ///     <para>
-    ///         If <see cref="WantContinuousButtonPressed"/> is <see langword="true"/>, will be raised on every mouse event where
+    ///         If <see cref="WantContinuousButtonPressed"/> is <see langword="true"/>, will be raised on every mouse event
+    ///         where
     ///         the mouse button is pressed.
     ///     </para>
     /// </remarks>
     public event EventHandler<MouseEventArgs>? MouseClick;
 
     /// <summary>
-    ///     INTERNAL For cases where the view is grabbed and the mouse is clicked, this method handles the click event (typically
+    ///     INTERNAL For cases where the view is grabbed and the mouse is clicked, this method handles the click event
+    ///     (typically
     ///     when <see cref="WantContinuousButtonPressed"/> or <see cref="HighlightStyle"/> are set).
     /// </summary>
     /// <remarks>
@@ -428,93 +568,58 @@ public partial class View // Mouse APIs
         return false;
     }
 
-    /// <summary>
-    ///     INTERNAL For cases where the view is grabbed and the mouse is clicked, this method handles the released event (typically
-    ///     when <see cref="WantContinuousButtonPressed"/> or <see cref="HighlightStyle"/> are set).
-    /// </summary>
+    #endregion Mouse Clicked Events
+
+    #region Mouse Wheel Events
+
+    /// <summary>Raises the <see cref="OnMouseWheel"/>/<see cref="MouseWheel"/> event.</summary>
     /// <remarks>
-    ///     Marked internal just to support unit tests
     /// </remarks>
-    /// <param name="mouseEvent"></param>
     /// <returns><see langword="true"/>, if the event was handled, <see langword="false"/> otherwise.</returns>
-    internal bool WhenGrabbedHandleReleased (MouseEventArgs mouseEvent)
+    protected bool RaiseMouseWheelEvent (MouseEventArgs args)
     {
-        mouseEvent.Handled = false;
+        // Pre-conditions
+        if (!Enabled)
+        {
+            // QUESTION: Is this right? Should a disabled view eat mouse?
+            return args.Handled = false;
+        }
 
-        if (mouseEvent.IsReleased)
+        // Cancellable event
+
+        if (OnMouseWheel (args) || args.Handled)
         {
-            if (Application.MouseGrabView == this)
-            {
-                SetPressedHighlight (HighlightStyle.None);
-            }
+            return args.Handled;
+        }
 
-            return mouseEvent.Handled = true;
+        MouseWheel?.Invoke (this, args);
+
+        if (args.Handled)
+        {
+            return true;
         }
 
-        return false;
+        args.Handled = InvokeCommandsBoundToMouse (args) == true;
+
+        return args.Handled;
     }
 
     /// <summary>
-    ///     INTERNAL For cases where the view is grabbed and the mouse is clicked, this method handles the released event (typically
-    ///     when <see cref="WantContinuousButtonPressed"/> or <see cref="HighlightStyle"/> are set).
+    ///     Called when a mouse wheel event occurs. Check <see cref="MouseEventArgs.Flags"/> to see which wheel was moved was
+    ///     clicked.
     /// </summary>
     /// <remarks>
-    ///     <para>
-    ///         Marked internal just to support unit tests
-    ///     </para>
     /// </remarks>
-    /// <param name="mouseEvent"></param>
+    /// <param name="args"></param>
     /// <returns><see langword="true"/>, if the event was handled, <see langword="false"/> otherwise.</returns>
-    private bool WhenGrabbedHandlePressed (MouseEventArgs mouseEvent)
-    {
-        mouseEvent.Handled = false;
+    protected virtual bool OnMouseWheel (MouseEventArgs args) { return false; }
 
-        if (mouseEvent.IsPressed)
-        {
-            // The first time we get pressed event, grab the mouse and set focus
-            if (Application.MouseGrabView != this)
-            {
-                Application.GrabMouse (this);
-
-                if (!HasFocus && CanFocus)
-                {
-                    // Set the focus, but don't invoke Accept
-                    SetFocus ();
-                }
-
-                mouseEvent.Handled = true;
-            }
-
-            if (Viewport.Contains (mouseEvent.Position))
-            {
-                if (this is not Adornment
-                    && SetPressedHighlight (HighlightStyle.HasFlag (HighlightStyle.Pressed) ? HighlightStyle.Pressed : HighlightStyle.None))
-                {
-                    return true;
-                }
-            }
-            else
-            {
-                if (this is not Adornment
-                    && SetPressedHighlight (HighlightStyle.HasFlag (HighlightStyle.PressedOutside) ? HighlightStyle.PressedOutside : HighlightStyle.None))
-
-                {
-                    return true;
-                }
-            }
-
-            if (WantContinuousButtonPressed && Application.MouseGrabView == this)
-            {
-                return RaiseMouseClickEvent (mouseEvent);
-            }
-
-            return mouseEvent.Handled = true;
-        }
-
-        return false;
-    }
+    /// <summary>Raised when a mouse wheel event occurs.</summary>
+    /// <remarks>
+    /// </remarks>
+    public event EventHandler<MouseEventArgs>? MouseWheel;
 
-    #endregion Mouse Click Events
+    #endregion Mouse Wheel Events
 
     #region Highlight Handling
 
@@ -541,6 +646,13 @@ public partial class View // Mouse APIs
 
         Highlight?.Invoke (this, args);
 
+        //if (args.Cancel)
+        //{
+        //    return true;
+        //}
+
+        //args.Cancel = InvokeCommandsBoundToMouse (args) == true;
+
         return args.Cancel;
     }
 
@@ -666,15 +778,15 @@ public partial class View // Mouse APIs
 
             if (start is not Adornment)
             {
-                if (start.Margin is {} && start.Margin.Contains (currentLocation))
+                if (start.Margin is { } && start.Margin.Contains (currentLocation))
                 {
                     found = start.Margin;
                 }
-                else if (start.Border is {} && start.Border.Contains (currentLocation))
+                else if (start.Border is { } && start.Border.Contains (currentLocation))
                 {
                     found = start.Border;
                 }
-                else if (start.Padding is { } && start.Padding.Contains(currentLocation))
+                else if (start.Padding is { } && start.Padding.Contains (currentLocation))
                 {
                     found = start.Padding;
                 }
@@ -720,4 +832,6 @@ public partial class View // Mouse APIs
 
         return viewsUnderMouse;
     }
+
+    private void DisposeMouse () { }
 }

+ 2 - 1
Terminal.Gui/View/View.cs

@@ -147,7 +147,7 @@ public partial class View : IDisposable, ISupportInitializeNotification
 
         SetupKeyboard ();
 
-        //SetupMouse ();
+        SetupMouse ();
 
         SetupText ();
 
@@ -550,6 +550,7 @@ public partial class View : IDisposable, ISupportInitializeNotification
     {
         LineCanvas.Dispose ();
 
+        DisposeMouse ();
         DisposeKeyboard ();
         DisposeAdornments ();
         DisposeScrollBars ();

+ 4 - 4
Terminal.Gui/Views/Button.cs

@@ -70,16 +70,16 @@ public class Button : View, IDesignable
         HighlightStyle = DefaultHighlightStyle;
     }
 
-    private bool? HandleHotKeyCommand (CommandContext ctx)
+    private bool? HandleHotKeyCommand (ICommandContext commandContext)
     {
         bool cachedIsDefault = IsDefault; // Supports "Swap Default" in Buttons scenario where IsDefault changes
 
-        if (RaiseSelecting (ctx) is true)
+        if (RaiseSelecting (commandContext) is true)
         {
             return true;
         }
 
-        bool? handled = RaiseAccepting (ctx);
+        bool? handled = RaiseAccepting (commandContext);
 
         if (handled == true)
         {
@@ -133,7 +133,7 @@ public class Button : View, IDesignable
         }
 
         // TODO: With https://github.com/gui-cs/Terminal.Gui/issues/3778 we won't have to pass data:
-        e.Handled = InvokeCommand (Command.HotKey, new (Command.HotKey, null, data: this)) == true;
+        e.Handled = InvokeCommand<KeyBinding> (Command.HotKey, new KeyBinding ([Command.HotKey], this, data: null)) == true;
     }
 
     private void Button_TitleChanged (object sender, EventArgs<string> e)

+ 156 - 189
UICatalog/Scenarios/CharacterMap/CharMap.cs → Terminal.Gui/Views/CharMap/CharMap.cs

@@ -1,13 +1,10 @@
 #nullable enable
-using System;
+using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
-using System.Linq;
-using System.Net.Http;
-using System.Text;
 using System.Text.Json;
-using Terminal.Gui;
+using Terminal.Gui.Resources;
 
-namespace UICatalog.Scenarios;
+namespace Terminal.Gui;
 
 /// <summary>
 ///     A scrollable map of the Unicode codepoints.
@@ -23,106 +20,36 @@ public class CharMap : View, IDesignable
 
     private ContextMenu _contextMenu = new ();
 
+
     /// <summary>
     ///     Initializes a new instance.
     /// </summary>
+    [RequiresUnreferencedCode ("AOT")]
+    [RequiresDynamicCode ("AOT")]
     public CharMap ()
     {
         base.ColorScheme = Colors.ColorSchemes ["Dialog"];
         CanFocus = true;
         CursorVisibility = CursorVisibility.Default;
 
-        AddCommand (
-                    Command.Up,
-                    () =>
-                    {
-                        SelectedCodePoint -= 16;
+        AddCommand (Command.Up, commandContext => Move (commandContext, -16));
+        AddCommand (Command.Down, commandContext => Move (commandContext, 16));
+        AddCommand (Command.Left, commandContext => Move (commandContext, -1));
+        AddCommand (Command.Right, commandContext => Move (commandContext, 1));
 
-                        return true;
-                    }
-                   );
+        AddCommand (Command.PageUp, commandContext => Move (commandContext, -(Viewport.Height - HEADER_HEIGHT / _rowHeight) * 16));
+        AddCommand (Command.PageDown, commandContext => Move (commandContext, (Viewport.Height - HEADER_HEIGHT / _rowHeight) * 16));
+        AddCommand (Command.Start, commandContext => Move (commandContext, -SelectedCodePoint));
+        AddCommand (Command.End, commandContext => Move (commandContext, MAX_CODE_POINT - SelectedCodePoint));
 
-        AddCommand (
-                    Command.Down,
-                    () =>
-                    {
-                        SelectedCodePoint += 16;
+        AddCommand (Command.ScrollDown, () => ScrollVertical (1));
+        AddCommand (Command.ScrollUp, () => ScrollVertical (-1));
+        AddCommand (Command.ScrollRight, () => ScrollHorizontal (1));
+        AddCommand (Command.ScrollLeft, () => ScrollHorizontal (-1));
 
-                        return true;
-                    }
-                   );
-
-        AddCommand (
-                    Command.Left,
-                    () =>
-                    {
-                        SelectedCodePoint--;
-
-                        return true;
-                    }
-                   );
-
-        AddCommand (
-                    Command.Right,
-                    () =>
-                    {
-                        SelectedCodePoint++;
-
-                        return true;
-                    }
-                   );
-
-        AddCommand (
-                    Command.PageUp,
-                    () =>
-                    {
-                        int page = (Viewport.Height - HEADER_HEIGHT / _rowHeight) * 16;
-                        SelectedCodePoint -= page;
-
-                        return true;
-                    }
-                   );
-
-        AddCommand (
-                    Command.PageDown,
-                    () =>
-                    {
-                        int page = (Viewport.Height - HEADER_HEIGHT / _rowHeight) * 16;
-                        SelectedCodePoint += page;
-
-                        return true;
-                    }
-                   );
-
-        AddCommand (
-                    Command.Start,
-                    () =>
-                    {
-                        SelectedCodePoint = 0;
-
-                        return true;
-                    }
-                   );
-
-        AddCommand (
-                    Command.End,
-                    () =>
-                    {
-                        SelectedCodePoint = MAX_CODE_POINT;
-
-                        return true;
-                    }
-                   );
-
-        AddCommand (
-                    Command.Accept,
-                    () =>
-                    {
-                        ShowDetails ();
-
-                        return true;
-                    }
-                   );
+        AddCommand (Command.Accept, HandleAcceptCommand);
+        AddCommand (Command.Select, HandleSelectCommand);
+        AddCommand (Command.Context, HandleContextCommand);
 
         KeyBindings.Add (Key.CursorUp, Command.Up);
         KeyBindings.Add (Key.CursorDown, Command.Down);
@@ -132,8 +59,15 @@ public class CharMap : View, IDesignable
         KeyBindings.Add (Key.PageDown, Command.PageDown);
         KeyBindings.Add (Key.Home, Command.Start);
         KeyBindings.Add (Key.End, Command.End);
+        KeyBindings.Add (ContextMenu.DefaultKey, Command.Context);
 
-        MouseClick += Handle_MouseClick;
+        MouseBindings.Add (MouseFlags.Button1DoubleClicked, Command.Accept);
+        MouseBindings.ReplaceCommands(MouseFlags.Button3Clicked, Command.Context);
+        MouseBindings.ReplaceCommands (MouseFlags.Button1Clicked | MouseFlags.ButtonCtrl, Command.Context);
+        MouseBindings.Add (MouseFlags.WheeledDown, Command.ScrollDown);
+        MouseBindings.Add (MouseFlags.WheeledUp, Command.ScrollUp);
+        MouseBindings.Add (MouseFlags.WheeledLeft, Command.ScrollLeft);
+        MouseBindings.Add (MouseFlags.WheeledRight, Command.ScrollRight);
 
         SetContentSize (new (COLUMN_WIDTH * 16 + RowLabelWidth, MAX_CODE_POINT / 16 * _rowHeight + HEADER_HEIGHT));
 
@@ -169,6 +103,18 @@ public class CharMap : View, IDesignable
         VerticalScrollBar.Y = HEADER_HEIGHT; // Header
     }
 
+    private bool? Move (ICommandContext? commandContext, int cpOffset)
+    {
+        if (RaiseSelecting (commandContext) is true)
+        {
+            return true;
+        }
+
+        SelectedCodePoint += cpOffset;
+
+        return true;
+    }
+
     private void ScrollToMakeCursorVisible (Point offsetToNewCursor)
     {
         // Adjust vertical scrolling
@@ -203,6 +149,7 @@ public class CharMap : View, IDesignable
         return new (x, y);
     }
 
+    /// <inheritdoc/>
     public override Point? PositionCursor ()
     {
         Point cursor = GetCursor (SelectedCodePoint);
@@ -297,6 +244,7 @@ public class CharMap : View, IDesignable
 
     private static int RowLabelWidth => $"U+{MAX_CODE_POINT:x5}".Length + 1;
 
+    /// <inheritdoc/>
     protected override bool OnDrawingContent ()
     {
         if (Viewport.Height == 0 || Viewport.Width == 0)
@@ -465,79 +413,91 @@ public class CharMap : View, IDesignable
     #region Mouse Handling
 
     // TODO: Use this to demonstrate using a popover to show glyph info on hover
-    public event EventHandler<ListViewItemEventArgs>? Hover;
+    // public event EventHandler<ListViewItemEventArgs>? Hover;
 
-    /// <inheritdoc />
-    protected override bool OnMouseEvent (MouseEventArgs mouseEvent)
+    private bool? HandleSelectCommand (ICommandContext? commandContext)
     {
-        if (mouseEvent.Flags == MouseFlags.WheeledDown)
+        Point position = GetCursor (SelectedCodePoint);
+
+        if (commandContext is CommandContext<MouseBinding> { Binding.MouseEventArgs: { } } mouseCommandContext)
         {
-            if (Viewport.Y + Viewport.Height - HEADER_HEIGHT < GetContentSize ().Height)
+            // If the mouse is clicked on the headers, map it to the first glyph of the row/col
+            position = mouseCommandContext.Binding.MouseEventArgs.Position;
+
+            if (position.Y == 0)
+            {
+                position = position with { Y = GetCursor (SelectedCodePoint).Y };
+            }
+
+            if (position.X < RowLabelWidth || position.X > RowLabelWidth + 16 * COLUMN_WIDTH - 1)
             {
-                ScrollVertical (1);
+                position = position with { X = GetCursor (SelectedCodePoint).X };
             }
-            return mouseEvent.Handled = true;
         }
 
-        if (mouseEvent.Flags == MouseFlags.WheeledUp)
+        if (RaiseSelecting (commandContext) is true)
         {
-            ScrollVertical (-1);
-            return mouseEvent.Handled = true;
+            return true;
         }
 
-        if (mouseEvent.Flags == MouseFlags.WheeledRight)
+        if (!TryGetCodePointFromPosition (position, out int cp))
         {
-            if (Viewport.X + Viewport.Width < GetContentSize ().Width)
-            {
-                ScrollHorizontal (1);
-            }
-            return mouseEvent.Handled = true;
+            return false;
         }
 
-        if (mouseEvent.Flags == MouseFlags.WheeledLeft)
+        if (cp != SelectedCodePoint)
         {
-            ScrollHorizontal (-1);
-            return mouseEvent.Handled = true;
+            if (!HasFocus && CanFocus)
+            {
+                SetFocus ();
+            }
+
+            SelectedCodePoint = cp;
         }
 
-        return false;
+        return true;
     }
 
-    private void Handle_MouseClick (object? sender, MouseEventArgs me)
-    {
-        if (me.Flags != MouseFlags.ReportMousePosition && me.Flags != MouseFlags.Button1Clicked && me.Flags != MouseFlags.Button1DoubleClicked)
-        {
-            return;
-        }
+    [RequiresUnreferencedCode ("AOT")]
+    [RequiresDynamicCode ("AOT")]
 
-        if (me.Position.Y == 0)
+    private bool? HandleAcceptCommand (ICommandContext? commandContext)
+    {
+        if (RaiseAccepting (commandContext) is true)
         {
-            me.Position = me.Position with { Y = GetCursor (SelectedCodePoint).Y };
+            return true;
         }
 
-        if (me.Position.X < RowLabelWidth || me.Position.X > RowLabelWidth + 16 * COLUMN_WIDTH - 1)
+        if (commandContext is CommandContext<MouseBinding> { Binding.MouseEventArgs: { } } mouseCommandContext)
         {
-            me.Position = me.Position with { X = GetCursor (SelectedCodePoint).X };
-        }
+            if (!HasFocus && CanFocus)
+            {
+                SetFocus ();
+            }
 
-        int row = (me.Position.Y - 1 - -Viewport.Y) / _rowHeight; // -1 for header
-        int col = (me.Position.X - RowLabelWidth - -Viewport.X) / COLUMN_WIDTH;
+            if (!TryGetCodePointFromPosition (mouseCommandContext.Binding.MouseEventArgs.Position, out int cp))
+            {
+                return false;
+            }
 
-        if (col > 15)
-        {
-            col = 15;
+            SelectedCodePoint = cp;
         }
 
-        int val = row * 16 + col;
+        ShowDetails ();
 
-        if (val > MAX_CODE_POINT)
-        {
-            return;
-        }
+        return true;
+    }
 
-        if (me.Flags == MouseFlags.ReportMousePosition)
+    private bool? HandleContextCommand (ICommandContext? commandContext)
+    {
+        int newCodePoint = SelectedCodePoint;
+
+        if (commandContext is CommandContext<MouseBinding> { Binding.MouseEventArgs: { } } mouseCommandContext)
         {
-            Hover?.Invoke (this, new (val, null));
+            if (!TryGetCodePointFromPosition (mouseCommandContext.Binding.MouseEventArgs.Position, out newCodePoint))
+            {
+                return false;
+            }
         }
 
         if (!HasFocus && CanFocus)
@@ -545,76 +505,83 @@ public class CharMap : View, IDesignable
             SetFocus ();
         }
 
-        me.Handled = true;
+        SelectedCodePoint = newCodePoint;
 
-        if (me.Flags == MouseFlags.Button1Clicked)
+        _contextMenu = new ()
         {
-            SelectedCodePoint = val;
+            Position = ViewportToScreen (GetCursor (SelectedCodePoint))
+        };
 
-            return;
-        }
+        MenuBarItem menuItems = new (
+                                     [
+                                         new (
+                                              Strings.charMapCopyGlyph,
+                                              "",
+                                              CopyGlyph,
+                                              null,
+                                              null,
+                                              (KeyCode)Key.G.WithCtrl
+                                             ),
+                                         new (
+                                              Strings.charMapCopyCP,
+                                              "",
+                                              CopyCodePoint,
+                                              null,
+                                              null,
+                                              (KeyCode)Key.P.WithCtrl
+                                             )
+                                     ]
+                                    );
+        _contextMenu.Show (menuItems);
 
-        if (me.Flags == MouseFlags.Button1DoubleClicked)
-        {
-            SelectedCodePoint = val;
-            ShowDetails ();
+        return true;
+    }
 
-            return;
-        }
+    private bool TryGetCodePointFromPosition (Point position, out int codePoint)
+    {
+        int row = (position.Y - 1 - -Viewport.Y) / _rowHeight; // -1 for header
+        int col = (position.X - RowLabelWidth - -Viewport.X) / COLUMN_WIDTH;
 
-        if (me.Flags == _contextMenu.MouseFlags)
+        if (col > 15)
         {
-            SelectedCodePoint = val;
+            col = 15;
+        }
 
-            _contextMenu = new ()
-            {
-                Position = new (me.Position.X + 1, me.Position.Y + 1)
-            };
+        codePoint = row * 16 + col;
 
-            MenuBarItem menuItems = new (
-                                         new MenuItem []
-                                         {
-                                             new (
-                                                  "_Copy Glyph",
-                                                  "",
-                                                  CopyGlyph,
-                                                  null,
-                                                  null,
-                                                  (KeyCode)Key.C.WithCtrl
-                                                 ),
-                                             new (
-                                                  "Copy Code _Point",
-                                                  "",
-                                                  CopyCodePoint,
-                                                  null,
-                                                  null,
-                                                  (KeyCode)Key.C.WithCtrl
-                                                              .WithShift
-                                                 )
-                                         }
-                                        );
-            _contextMenu.Show (menuItems);
+        if (codePoint > MAX_CODE_POINT)
+        {
+            return false;
         }
+
+        return true;
     }
 
     #endregion Mouse Handling
 
     #region Details Dialog
 
+    [RequiresUnreferencedCode ("AOT")]
+    [RequiresDynamicCode ("AOT")]
     private void ShowDetails ()
     {
+        if (!Application.Initialized)
+        {
+            // Some unit tests invoke Accept without Init
+            return;
+        }
         UcdApiClient? client = new ();
         var decResponse = string.Empty;
         var getCodePointError = string.Empty;
 
         Dialog? waitIndicator = new ()
         {
-            Title = "Getting Code Point Information",
+            Title = Strings.charMapCPInfoDlgTitle,
             X = Pos.Center (),
             Y = Pos.Center (),
             Width = 40,
             Height = 10,
-            Buttons = [new () { Text = "_Cancel" }]
+            Buttons = [new () { Text = Strings.btnCancel }]
         };
 
         var errorLabel = new Label
@@ -641,7 +608,7 @@ public class CharMap : View, IDesignable
                                {
                                    try
                                    {
-                                       decResponse = await client.GetCodepointDec (SelectedCodePoint);
+                                       decResponse = await client.GetCodepointDec (SelectedCodePoint).ConfigureAwait (false);
                                        Application.Invoke (() => waitIndicator.RequestStop ());
                                    }
                                    catch (HttpRequestException e)
@@ -676,15 +643,15 @@ public class CharMap : View, IDesignable
                                                         document.RootElement,
                                                         new
                                                             JsonSerializerOptions
-                                                        { WriteIndented = true }
+                                                            { WriteIndented = true }
                                                        );
             }
 
             var title = $"{ToCamelCase (name!)} - {new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}";
 
-            Button? copyGlyph = new () { Text = "Copy _Glyph" };
-            Button? copyCodepoint = new () { Text = "Copy Code _Point" };
-            Button? cancel = new () { Text = "Cancel" };
+            Button? copyGlyph = new () { Text = Strings.charMapCopyGlyph };
+            Button? copyCodepoint = new () { Text = Strings.charMapCopyCP };
+            Button? cancel = new () { Text = Strings.btnCancel };
 
             var dlg = new Dialog { Title = title, Buttons = [copyGlyph, copyCodepoint, cancel] };
 
@@ -747,7 +714,7 @@ public class CharMap : View, IDesignable
             label = new ()
             {
                 Text =
-                    $"Code Point Information from {UcdApiClient.BaseUrl}codepoint/dec/{SelectedCodePoint}:",
+                    $"{Strings.charMapInfoDlgInfoLabel} {UcdApiClient.BaseUrl}codepoint/dec/{SelectedCodePoint}:",
                 X = 0,
                 Y = Pos.Bottom (label)
             };
@@ -771,9 +738,9 @@ public class CharMap : View, IDesignable
         else
         {
             MessageBox.ErrorQuery (
-                                   "Code Point API",
-                                   $"{UcdApiClient.BaseUrl}codepoint/dec/{SelectedCodePoint} did not return a result for\r\n {new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}.",
-                                   "_Ok"
+                                   Strings.error,
+                                   $"{UcdApiClient.BaseUrl}codepoint/dec/{SelectedCodePoint} {Strings.failedGetting}{Environment.NewLine}{new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}.",
+                                   Strings.btnOk
                                   );
         }
 

+ 10 - 14
UICatalog/Scenarios/CharacterMap/UcdApiClient.cs → Terminal.Gui/Views/CharMap/UcdApiClient.cs

@@ -1,48 +1,44 @@
 #nullable enable
-using System;
-using System.Net.Http;
-using System.Threading.Tasks;
-
-namespace UICatalog.Scenarios;
+namespace Terminal.Gui;
 
 /// <summary>
 ///     A helper class for accessing the ucdapi.org API.
 /// </summary>
-public class UcdApiClient
+internal class UcdApiClient
 {
     public const string BaseUrl = "https://ucdapi.org/unicode/latest/";
     private static readonly HttpClient _httpClient = new ();
 
     public async Task<string> GetChars (string chars)
     {
-        HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}");
+        HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}").ConfigureAwait (false);
         response.EnsureSuccessStatusCode ();
 
-        return await response.Content.ReadAsStringAsync ();
+        return await response.Content.ReadAsStringAsync ().ConfigureAwait (false);
     }
 
     public async Task<string> GetCharsName (string chars)
     {
         HttpResponseMessage response =
-            await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}/name");
+            await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}/name").ConfigureAwait (false);
         response.EnsureSuccessStatusCode ();
 
-        return await response.Content.ReadAsStringAsync ();
+        return await response.Content.ReadAsStringAsync ().ConfigureAwait (false);
     }
 
     public async Task<string> GetCodepointDec (int dec)
     {
-        HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/dec/{dec}");
+        HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/dec/{dec}").ConfigureAwait (false);
         response.EnsureSuccessStatusCode ();
 
-        return await response.Content.ReadAsStringAsync ();
+        return await response.Content.ReadAsStringAsync ().ConfigureAwait (false);
     }
 
     public async Task<string> GetCodepointHex (string hex)
     {
-        HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/hex/{hex}");
+        HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/hex/{hex}").ConfigureAwait (false);
         response.EnsureSuccessStatusCode ();
 
-        return await response.Content.ReadAsStringAsync ();
+        return await response.Content.ReadAsStringAsync ().ConfigureAwait (false);
     }
 }

+ 1 - 3
UICatalog/Scenarios/CharacterMap/UnicodeRange.cs → Terminal.Gui/Views/CharMap/UnicodeRange.cs

@@ -1,10 +1,8 @@
 #nullable enable
-using System.Collections.Generic;
-using System.Linq;
 using System.Reflection;
 using System.Text.Unicode;
 
-namespace UICatalog.Scenarios;
+namespace Terminal.Gui;
 
 /// <summary>
 ///     Represents all of the Uniicode ranges.from System.Text.Unicode.UnicodeRange plus

+ 13 - 4
Terminal.Gui/Views/CheckBox.cs

@@ -24,17 +24,26 @@ public class CheckBox : View
         AddCommand (Command.Select, AdvanceAndSelect);
 
         // Hotkey - Advance state and raise Select event - DO NOT raise Accept
-        AddCommand (Command.HotKey, AdvanceAndSelect);
+        AddCommand (Command.HotKey, ctx =>
+                                    {
+                                        if (RaiseHandlingHotKey () is true)
+                                        {
+                                            return true;
+                                        }
+                                        return AdvanceAndSelect (ctx);
+                                    });
 
         // Accept (Enter key) - Raise Accept event - DO NOT advance state
         AddCommand (Command.Accept, RaiseAccepting);
 
+        MouseBindings.Add (MouseFlags.Button1DoubleClicked, Command.Accept);
+
         TitleChanged += Checkbox_TitleChanged;
 
         HighlightStyle = DefaultHighlightStyle;
     }
 
-    private bool? AdvanceAndSelect (CommandContext ctx)
+    private bool? AdvanceAndSelect (ICommandContext? commandContext)
     {
         bool? cancelled = AdvanceCheckState ();
 
@@ -43,12 +52,12 @@ public class CheckBox : View
             return true;
         }
 
-        if (RaiseSelecting (ctx) is true)
+        if (RaiseSelecting (commandContext) is true)
         {
             return true;
         }
 
-        return ctx.Command == Command.HotKey ? cancelled : cancelled is false;
+        return commandContext?.Command == Command.HotKey ? cancelled : cancelled is false;
     }
 
     private void Checkbox_TitleChanged (object? sender, EventArgs<string> e)

+ 34 - 37
Terminal.Gui/Views/ColorPicker.16.cs

@@ -1,4 +1,5 @@
-namespace Terminal.Gui;
+#nullable enable
+namespace Terminal.Gui;
 
 /// <summary>The <see cref="ColorPicker16"/> <see cref="View"/> Color picker.</summary>
 public class ColorPicker16 : View
@@ -7,10 +8,10 @@ public class ColorPicker16 : View
     public ColorPicker16 () { SetInitialProperties (); }
 
     /// <summary>Columns of color boxes</summary>
-    private readonly int _cols = 8;
+    private const int COLS = 8;
 
     /// <summary>Rows of color boxes</summary>
-    private readonly int _rows = 2;
+    private const int ROWS = 2;
 
     private int _boxHeight = 2;
     private int _boxWidth = 4;
@@ -25,9 +26,9 @@ public class ColorPicker16 : View
             if (_boxHeight != value)
             {
                 _boxHeight = value;
-                Width = Dim.Auto (minimumContentDim: _boxWidth * _cols);
-                Height = Dim.Auto (minimumContentDim: _boxHeight * _rows);
-                SetContentSize (new (_boxWidth * _cols, _boxHeight * _rows));
+                Width = Dim.Auto (minimumContentDim: _boxWidth * COLS);
+                Height = Dim.Auto (minimumContentDim: _boxHeight * ROWS);
+                SetContentSize (new (_boxWidth * COLS, _boxHeight * ROWS));
                 SetNeedsLayout ();
             }
         }
@@ -42,40 +43,39 @@ public class ColorPicker16 : View
             if (_boxWidth != value)
             {
                 _boxWidth = value;
-                Width = Dim.Auto (minimumContentDim: _boxWidth * _cols);
-                Height = Dim.Auto (minimumContentDim: _boxHeight * _rows);
-                SetContentSize (new (_boxWidth * _cols, _boxHeight * _rows));
+                Width = Dim.Auto (minimumContentDim: _boxWidth * COLS);
+                Height = Dim.Auto (minimumContentDim: _boxHeight * ROWS);
+                SetContentSize (new (_boxWidth * COLS, _boxHeight * ROWS));
                 SetNeedsLayout ();
             }
         }
     }
 
     /// <summary>Fired when a color is picked.</summary>
-    [CanBeNull]
-    public event EventHandler<ColorEventArgs> ColorChanged;
+    public event EventHandler<ColorEventArgs>? ColorChanged;
 
     /// <summary>Cursor for the selected color.</summary>
     public Point Cursor
     {
-        get => new (_selectColorIndex % _cols, _selectColorIndex / _cols);
+        get => new (_selectColorIndex % COLS, _selectColorIndex / COLS);
         set
         {
-            int colorIndex = value.Y * _cols + value.X;
+            int colorIndex = value.Y * COLS + value.X;
             SelectedColor = (ColorName16)colorIndex;
         }
     }
 
     /// <summary>Moves the selected item index to the next row.</summary>
     /// <returns></returns>
-    private bool MoveDown (CommandContext ctx)
+    private bool MoveDown (ICommandContext? commandContext)
     {
-        if (RaiseSelecting (ctx) == true)
+        if (RaiseSelecting (commandContext) == true)
         {
             return true;
         }
-        if (Cursor.Y < _rows - 1)
+        if (Cursor.Y < ROWS - 1)
         {
-            SelectedColor += _cols;
+            SelectedColor += COLS;
         }
 
         return true;
@@ -83,9 +83,9 @@ public class ColorPicker16 : View
 
     /// <summary>Moves the selected item index to the previous column.</summary>
     /// <returns></returns>
-    private bool MoveLeft (CommandContext ctx)
+    private bool MoveLeft (ICommandContext? commandContext)
     {
-        if (RaiseSelecting (ctx) == true)
+        if (RaiseSelecting (commandContext) == true)
         {
             return true;
         }
@@ -100,13 +100,13 @@ public class ColorPicker16 : View
 
     /// <summary>Moves the selected item index to the next column.</summary>
     /// <returns></returns>
-    private bool MoveRight (CommandContext ctx)
+    private bool MoveRight (ICommandContext? commandContext)
     {
-        if (RaiseSelecting (ctx) == true)
+        if (RaiseSelecting (commandContext) == true)
         {
             return true;
         }
-        if (Cursor.X < _cols - 1)
+        if (Cursor.X < COLS - 1)
         {
             SelectedColor++;
         }
@@ -116,15 +116,15 @@ public class ColorPicker16 : View
 
     /// <summary>Moves the selected item index to the previous row.</summary>
     /// <returns></returns>
-    private bool MoveUp (CommandContext ctx)
+    private bool MoveUp (ICommandContext? commandContext)
     {
-        if (RaiseSelecting (ctx) == true)
+        if (RaiseSelecting (commandContext) == true)
         {
             return true;
         }
         if (Cursor.Y > 0)
         {
-            SelectedColor -= _cols;
+            SelectedColor -= COLS;
         }
 
         return true;
@@ -133,14 +133,14 @@ public class ColorPicker16 : View
     ///<inheritdoc/>
     protected override bool OnDrawingContent ()
     {
-        SetAttribute (HasFocus ? ColorScheme.Focus : GetNormalColor ());
+        SetAttribute (HasFocus ? GetFocusColor () : GetNormalColor ());
         var colorIndex = 0;
 
         for (var y = 0; y < Math.Max (2, Viewport.Height / BoxHeight); y++)
         {
             for (var x = 0; x < Math.Max (8, Viewport.Width / BoxWidth); x++)
             {
-                int foregroundColorIndex = y == 0 ? colorIndex + _cols : colorIndex - _cols;
+                int foregroundColorIndex = y == 0 ? colorIndex + COLS : colorIndex - COLS;
 
                 if (foregroundColorIndex > 15 || colorIndex > 15)
                 {
@@ -188,10 +188,11 @@ public class ColorPicker16 : View
 
         AddCommand (Command.Select, (ctx) =>
                                     {
-                                        bool set = false;
-                                        if (ctx.Data is MouseEventArgs me)
+                                        var set = false;
+
+                                        if (ctx is CommandContext<MouseBinding> { Binding.MouseEventArgs: { } } mouseCommandContext)
                                         {
-                                            Cursor = new (me.Position.X / _boxWidth, me.Position.Y / _boxHeight);
+                                            Cursor = new (mouseCommandContext.Binding.MouseEventArgs.Position.X / _boxWidth, mouseCommandContext.Binding.MouseEventArgs.Position.Y / _boxHeight);
                                             set = true;
                                         }
                                         return RaiseAccepting (ctx) == true || set;
@@ -209,21 +210,17 @@ public class ColorPicker16 : View
 
     // TODO: Decouple Cursor from SelectedColor so that mouse press-and-hold can show the color under the cursor.
 
-
     /// <summary>Draw a box for one color.</summary>
     /// <param name="x">X location.</param>
     /// <param name="y">Y location</param>
     /// <param name="selected"></param>
     private void DrawColorBox (int x, int y, bool selected)
     {
-        var index = 0;
-
         for (var zoomedY = 0; zoomedY < BoxHeight; zoomedY++)
         {
             for (var zoomedX = 0; zoomedX < BoxWidth; zoomedX++)
             {
                 AddRune (x * BoxWidth + zoomedX, y * BoxHeight + zoomedY, (Rune)' ');
-                index++;
             }
         }
 
@@ -280,8 +277,8 @@ public class ColorPicker16 : View
         AddCommands ();
         AddKeyBindings ();
 
-        Width = Dim.Auto (minimumContentDim: _boxWidth * _cols);
-        Height = Dim.Auto (minimumContentDim: _boxHeight * _rows);
-        SetContentSize (new (_boxWidth * _cols, _boxHeight * _rows));
+        Width = Dim.Auto (minimumContentDim: _boxWidth * COLS);
+        Height = Dim.Auto (minimumContentDim: _boxHeight * ROWS);
+        SetContentSize (new (_boxWidth * COLS, _boxHeight * ROWS));
     }
 }

+ 4 - 2
Terminal.Gui/Views/ColorPicker.Prompt.cs

@@ -1,4 +1,6 @@
-namespace Terminal.Gui;
+using Terminal.Gui.Resources;
+
+namespace Terminal.Gui;
 
 public partial class ColorPicker
 {
@@ -42,7 +44,7 @@ public partial class ColorPicker
         {
             X = Pos.Center () + 5,
             Y = 4,
-            Text = "Cancel",
+            Text = Strings.btnCancel,
             Width = Dim.Auto ()
         };
 

+ 7 - 3
Terminal.Gui/Views/ComboBox.cs

@@ -80,7 +80,11 @@ public class ComboBox : View, IDesignable
         // Things this view knows how to do
         AddCommand (Command.Accept, (ctx) =>
                                     {
-                                        if (ctx.Data == _search)
+                                        if (ctx is not CommandContext<KeyBinding> keyCommandContext)
+                                        {
+                                            return false;
+                                        }
+                                        if (keyCommandContext.Binding.Data == _search)
                                         {
                                             return null;
                                         }
@@ -395,7 +399,7 @@ public class ComboBox : View, IDesignable
         }
     }
 
-    private bool ActivateSelected (CommandContext ctx)
+    private bool ActivateSelected (ICommandContext commandContext)
     {
         if (HasItems ())
         {
@@ -404,7 +408,7 @@ public class ComboBox : View, IDesignable
                 return false;
             }
 
-            return RaiseAccepting (ctx) == true;
+            return RaiseAccepting (commandContext) == true;
         }
 
         return false;

+ 1 - 1
Terminal.Gui/Views/FrameView.cs

@@ -25,7 +25,7 @@ public class FrameView : View
     private void FrameView_MouseClick (object sender, MouseEventArgs e)
     {
         // base sets focus on HotKey
-        e.Handled = InvokeCommand (Command.HotKey, ctx: new (Command.HotKey, key: null, data: this)) == true;
+        e.Handled = InvokeCommand<KeyBinding> (Command.HotKey, new ([Command.HotKey], this, this)) == true;
     }
 
 

+ 22 - 37
Terminal.Gui/Views/HexView.cs

@@ -58,9 +58,7 @@ public class HexView : View, IDesignable
         _leftSideHasFocus = true;
         _firstNibble = true;
 
-        // PERF: Closure capture of 'this' creates a lot of overhead.
-        // BUG: Closure capture of 'this' may have unexpected results depending on how this is called.
-        // The above two comments apply to all the lambdas passed to all calls to AddCommand below.
+        AddCommand (Command.Select, HandleMouseClick);
         AddCommand (Command.Left, () => MoveLeft ());
         AddCommand (Command.Right, () => MoveRight ());
         AddCommand (Command.Down, () => MoveDown (BytesPerLine));
@@ -76,6 +74,8 @@ public class HexView : View, IDesignable
                     Command.EndOfPage,
                     () => MoveDown (BytesPerLine * (Viewport.Height - 1 - (int)(Address - Viewport.Y) / BytesPerLine))
                    );
+        AddCommand (Command.ScrollDown, () => ScrollVertical (1));
+        AddCommand (Command.ScrollUp, () => ScrollVertical (-1));
         AddCommand (Command.DeleteCharLeft, () => true);
         AddCommand (Command.DeleteCharRight, () => true);
         AddCommand (Command.Insert, () => true);
@@ -84,11 +84,8 @@ public class HexView : View, IDesignable
         KeyBindings.Add (Key.CursorRight, Command.Right);
         KeyBindings.Add (Key.CursorDown, Command.Down);
         KeyBindings.Add (Key.CursorUp, Command.Up);
-
         KeyBindings.Add (Key.PageUp, Command.PageUp);
-
         KeyBindings.Add (Key.PageDown, Command.PageDown);
-
         KeyBindings.Add (Key.Home, Command.Start);
         KeyBindings.Add (Key.End, Command.End);
         KeyBindings.Add (Key.CursorLeft.WithCtrl, Command.LeftStart);
@@ -103,6 +100,12 @@ public class HexView : View, IDesignable
         KeyBindings.Remove (Key.Space);
         KeyBindings.Remove (Key.Enter);
 
+        // The Select handler deals with both single and double clicks
+        MouseBindings.ReplaceCommands (MouseFlags.Button1Clicked, Command.Select);
+        MouseBindings.Add (MouseFlags.Button1DoubleClicked, Command.Select);
+        MouseBindings.Add (MouseFlags.WheeledUp, Command.ScrollUp);
+        MouseBindings.Add (MouseFlags.WheeledDown, Command.ScrollDown);
+
         SubviewsLaidOut += HexViewSubviewsLaidOut;
     }
 
@@ -173,7 +176,7 @@ public class HexView : View, IDesignable
 
         if (offsetToNewCursor.X < 1)
         {
-            ScrollHorizontal(offsetToNewCursor.X);
+            ScrollHorizontal (offsetToNewCursor.X);
         }
         else if (offsetToNewCursor.X >= Viewport.Width)
         {
@@ -347,20 +350,16 @@ public class HexView : View, IDesignable
 
     private int GetLeftSideStartColumn () { return AddressWidth == 0 ? 0 : AddressWidth + 1; }
 
-    /// <inheritdoc/>
-    protected override bool OnMouseEvent (MouseEventArgs me)
+    private bool? HandleMouseClick (ICommandContext? commandContext)
     {
-        if (_source is null)
+        if (commandContext is not CommandContext<MouseBinding> { Binding.MouseEventArgs: { } } mouseCommandContext)
         {
             return false;
         }
 
-        if (!me.Flags.HasFlag (MouseFlags.Button1Clicked)
-            && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked)
-            && !me.Flags.HasFlag (MouseFlags.WheeledDown)
-            && !me.Flags.HasFlag (MouseFlags.WheeledUp))
+        if (RaiseSelecting (commandContext) is true)
         {
-            return false;
+            return true;
         }
 
         if (!HasFocus)
@@ -368,21 +367,7 @@ public class HexView : View, IDesignable
             SetFocus ();
         }
 
-        if (me.Flags == MouseFlags.WheeledDown)
-        {
-            ScrollVertical (1);
-
-            return true;
-        }
-
-        if (me.Flags == MouseFlags.WheeledUp)
-        {
-            ScrollVertical (-1);
-
-            return true;
-        }
-
-        if (me.Position.X < GetLeftSideStartColumn ())
+        if (mouseCommandContext.Binding.MouseEventArgs.Position.X < GetLeftSideStartColumn ())
         {
             return true;
         }
@@ -391,14 +376,14 @@ public class HexView : View, IDesignable
         int blocksSize = blocks * HEX_COLUMN_WIDTH;
         int blocksRightOffset = GetLeftSideStartColumn () + blocksSize - 1;
 
-        if (me.Position.X > blocksRightOffset + BytesPerLine - 1)
+        if (mouseCommandContext.Binding.MouseEventArgs.Position.X > blocksRightOffset + BytesPerLine - 1)
         {
             return true;
         }
 
-        bool clickIsOnLeftSide = me.Position.X >= blocksRightOffset;
-        long lineStart = me.Position.Y * BytesPerLine + Viewport.Y * BytesPerLine;
-        int x = me.Position.X - GetLeftSideStartColumn () + 1;
+        bool clickIsOnLeftSide = mouseCommandContext.Binding.MouseEventArgs.Position.X >= blocksRightOffset;
+        long lineStart = mouseCommandContext.Binding.MouseEventArgs.Position.Y * BytesPerLine + Viewport.Y * BytesPerLine;
+        int x = mouseCommandContext.Binding.MouseEventArgs.Position.X - GetLeftSideStartColumn () + 1;
         int block = x / HEX_COLUMN_WIDTH;
         x -= block * 2;
         int empty = x % 3;
@@ -413,14 +398,14 @@ public class HexView : View, IDesignable
 
         if (clickIsOnLeftSide)
         {
-            Address = Math.Min (lineStart + me.Position.X - blocksRightOffset, GetEditedSize ());
+            Address = Math.Min (lineStart + mouseCommandContext.Binding.MouseEventArgs.Position.X - blocksRightOffset, GetEditedSize ());
         }
         else
         {
             Address = Math.Min (lineStart + item, GetEditedSize ());
         }
 
-        if (me.Flags == MouseFlags.Button1DoubleClicked)
+        if (mouseCommandContext.Binding.MouseEventArgs.Flags == MouseFlags.Button1DoubleClicked)
         {
             _leftSideHasFocus = !clickIsOnLeftSide;
 
@@ -435,7 +420,7 @@ public class HexView : View, IDesignable
             SetNeedsDraw ();
         }
 
-        return true;
+        return false;
     }
 
     ///<inheritdoc/>

+ 4 - 3
Terminal.Gui/Views/Label.cs

@@ -36,7 +36,7 @@ public class Label : View, IDesignable
     {
         if (!CanFocus)
         {
-            e.Handled = InvokeCommand (Command.HotKey, ctx: new (Command.HotKey, key: null, data: this)) == true;
+            e.Handled = InvokeCommand<KeyBinding> (Command.HotKey, new ([Command.HotKey], this, data: this)) == true;
         }
     }
 
@@ -60,7 +60,7 @@ public class Label : View, IDesignable
         set => TextFormatter.HotKeySpecifier = base.HotKeySpecifier = value;
     }
 
-    private bool? InvokeHotKeyOnNext (CommandContext context)
+    private bool? InvokeHotKeyOnNext (ICommandContext commandContext)
     {
         if (RaiseHandlingHotKey () == true)
         {
@@ -78,7 +78,8 @@ public class Label : View, IDesignable
 
         if (me != -1 && me < SuperView?.Subviews.Count - 1)
         {
-            return SuperView?.Subviews [me + 1].InvokeCommand (Command.HotKey, context.Key, context.KeyBinding) == true;
+
+            return SuperView?.Subviews [me + 1].InvokeCommand (Command.HotKey) == true;
         }
 
         return false;

+ 13 - 5
Terminal.Gui/Views/ListView.cs

@@ -141,7 +141,15 @@ public class ListView : View, IDesignable
                                         return !SetFocus ();
                                     });
 
-        AddCommand (Command.SelectAll, (ctx) => MarkAll ((bool)ctx.KeyBinding?.Context!));
+        AddCommand (Command.SelectAll, (ctx) =>
+                                       {
+                                           if (ctx is not CommandContext<KeyBinding> keyCommandContext)
+                                           {
+                                               return false;
+                                           }
+
+                                           return keyCommandContext.Binding.Data is { } && MarkAll ((bool)keyCommandContext.Binding.Data);
+                                       });
 
         // Default keybindings for all ListViews
         KeyBindings.Add (Key.CursorUp, Command.Up);
@@ -163,8 +171,8 @@ public class ListView : View, IDesignable
         KeyBindings.Add (Key.Space.WithShift, [Command.Select, Command.Down]);
 
         // Use the form of Add that lets us pass context to the handler
-        KeyBindings.Add (Key.A.WithCtrl, new KeyBinding ([Command.SelectAll], KeyBindingScope.Focused, true));
-        KeyBindings.Add (Key.U.WithCtrl, new KeyBinding ([Command.SelectAll], KeyBindingScope.Focused, false));
+        KeyBindings.Add (Key.A.WithCtrl, new KeyBinding ([Command.SelectAll], true));
+        KeyBindings.Add (Key.U.WithCtrl, new KeyBinding ([Command.SelectAll], false));
     }
 
     /// <inheritdoc />
@@ -805,14 +813,14 @@ public class ListView : View, IDesignable
         // at it
         if (AllowsMarking)
         {
-            var keys = KeyBindings.GetKeysFromCommands (Command.Select);
+            var keys = KeyBindings.GetAllFromCommands (Command.Select);
 
             if (keys.Contains (a))
             {
                 return false;
             }
 
-            keys = KeyBindings.GetKeysFromCommands ([Command.Select, Command.Down]);
+            keys = KeyBindings.GetAllFromCommands ([Command.Select, Command.Down]);
 
             if (keys.Contains (a))
             {

+ 2 - 1
Terminal.Gui/Views/Menu/ContextMenu.cs

@@ -150,7 +150,7 @@ public sealed class ContextMenu : IDisposable
                 if (menuItem.ShortcutKey != Key.Empty)
                 {
                     // Remove an existent ShortcutKey
-                    _menuBar?.KeyBindings.Remove (menuItem.ShortcutKey!);
+                    _menuBar?.HotKeyBindings.Remove (menuItem.ShortcutKey!);
                 }
             }
         }
@@ -250,6 +250,7 @@ public sealed class ContextMenu : IDisposable
 
         _menuBar._isContextMenuLoading = true;
         _menuBar.MenuAllClosed += MenuBar_MenuAllClosed;
+
         _menuBar.BeginInit ();
         _menuBar.EndInit ();
         IsShow = true;

+ 544 - 509
Terminal.Gui/Views/Menu/Menu.cs

@@ -1,7 +1,5 @@
 #nullable enable
 
-using static System.Formats.Asn1.AsnWriter;
-
 namespace Terminal.Gui;
 
 /// <summary>
@@ -10,76 +8,115 @@ namespace Terminal.Gui;
 /// </summary>
 internal sealed class Menu : View
 {
-    private readonly MenuBarItem? _barItems;
-    private readonly MenuBar _host;
-    internal int _currentChild;
-    internal View? _previousSubFocused;
-
-    internal static Rectangle MakeFrame (int x, int y, MenuItem? []? items, Menu? parent = null)
+    public Menu ()
     {
-        if (items is null || items.Length == 0)
+        if (Application.Top is { })
         {
-            return Rectangle.Empty;
+            Application.Top.DrawComplete += Top_DrawComplete;
+            Application.Top.SizeChanging += Current_TerminalResized;
         }
 
-        int minX = x;
-        int minY = y;
-        const int borderOffset = 2; // This 2 is for the space around
-        int maxW = (items.Max (z => z?.Width) ?? 0) + borderOffset;
-        int maxH = items.Length + borderOffset;
+        Application.MouseEvent += Application_RootMouseEvent;
 
-        if (parent is { } && x + maxW > Driver.Cols)
-        {
-            minX = Math.Max (parent.Frame.Right - parent.Frame.Width - maxW, 0);
-        }
+        // Things this view knows how to do
+        AddCommand (Command.Up, () => MoveUp ());
+        AddCommand (Command.Down, () => MoveDown ());
 
-        if (y + maxH > Driver.Rows)
-        {
-            minY = Math.Max (Driver.Rows - maxH, 0);
-        }
+        AddCommand (
+                    Command.Left,
+                    () =>
+                    {
+                        _host!.PreviousMenu (true);
 
-        return new (minX, minY, maxW, maxH);
-    }
+                        return true;
+                    }
+                   );
 
-    internal required MenuBar Host
-    {
-        get => _host;
-        init
-        {
-            ArgumentNullException.ThrowIfNull (value);
-            _host = value;
-        }
-    }
+        AddCommand (
+                    Command.Cancel,
+                    () =>
+                    {
+                        CloseAllMenus ();
 
-    internal required MenuBarItem? BarItems
-    {
-        get => _barItems!;
-        init
-        {
-            ArgumentNullException.ThrowIfNull (value);
-            _barItems = value;
+                        return true;
+                    }
+                   );
 
-            // Debugging aid so ToString() is helpful
-            Text = _barItems.Title;
-        }
+        AddCommand (
+                    Command.Accept,
+                    () =>
+                    {
+                        RunSelected ();
+
+                        return true;
+                    }
+                   );
+
+        AddCommand (
+                    Command.Select,
+                    ctx =>
+                    {
+                        if (ctx is not CommandContext<KeyBinding> keyCommandContext)
+                        {
+                            return false;
+                        }
+
+                        return _host?.SelectItem ((keyCommandContext.Binding.Data as MenuItem)!);
+                    });
+
+        AddCommand (
+                    Command.Toggle,
+                    ctx =>
+                    {
+                        if (ctx is not CommandContext<KeyBinding> keyCommandContext)
+                        {
+                            return false;
+                        }
+
+                        return ExpandCollapse ((keyCommandContext.Binding.Data as MenuItem)!);
+                    });
+
+        AddCommand (
+                    Command.HotKey,
+                    ctx =>
+                    {
+                        if (ctx is not CommandContext<KeyBinding> keyCommandContext)
+                        {
+                            return false;
+                        }
+
+                        return _host?.SelectItem ((keyCommandContext.Binding.Data as MenuItem)!);
+                    });
+
+        // Default key bindings for this view
+        KeyBindings.Add (Key.CursorUp, Command.Up);
+        KeyBindings.Add (Key.CursorDown, Command.Down);
+        KeyBindings.Add (Key.CursorLeft, Command.Left);
+        KeyBindings.Add (Key.CursorRight, Command.Right);
+        KeyBindings.Add (Key.Esc, Command.Cancel);
     }
 
-    internal Menu? Parent { get; init; }
+    internal int _currentChild;
+    internal View? _previousSubFocused;
+    private readonly MenuBarItem? _barItems;
+    private readonly MenuBar _host;
 
     public override void BeginInit ()
     {
         base.BeginInit ();
 
-        var frame = MakeFrame (Frame.X, Frame.Y, _barItems!.Children!, Parent);
+        Rectangle frame = MakeFrame (Frame.X, Frame.Y, _barItems!.Children!, Parent);
 
         if (Frame.X != frame.X)
         {
             X = frame.X;
         }
+
         if (Frame.Y != frame.Y)
         {
             Y = frame.Y;
         }
+
         Width = frame.Width;
         Height = frame.Height;
 
@@ -93,10 +130,11 @@ internal sealed class Menu : View
 
                     if (menuItem.ShortcutKey != Key.Empty)
                     {
-                        KeyBinding keyBinding = new ([Command.Select], KeyBindingScope.HotKey, menuItem);
+                        KeyBinding keyBinding = new ([Command.Select], this, data: menuItem);
+
                         // Remove an existent ShortcutKey
-                        menuItem._menuBar.KeyBindings.Remove (menuItem.ShortcutKey!);
-                        menuItem._menuBar.KeyBindings.Add (menuItem.ShortcutKey!, keyBinding);
+                        menuItem._menuBar.HotKeyBindings.Remove (menuItem.ShortcutKey!);
+                        menuItem._menuBar.HotKeyBindings.Add (menuItem.ShortcutKey!, keyBinding);
                     }
                 }
             }
@@ -155,196 +193,302 @@ internal sealed class Menu : View
         AddKeyBindingsHotKey (_barItems);
     }
 
-    public Menu ()
+    public override Point? PositionCursor ()
     {
-        if (Application.Top is { })
+        if (_host.IsMenuOpen)
         {
-            Application.Top.DrawComplete += Top_DrawComplete;
-            Application.Top.SizeChanging += Current_TerminalResized;
-        }
-
-        Application.MouseEvent += Application_RootMouseEvent;
-
-        // Things this view knows how to do
-        AddCommand (Command.Up, () => MoveUp ());
-        AddCommand (Command.Down, () => MoveDown ());
-
-        AddCommand (
-                    Command.Left,
-                    () =>
-                    {
-                        _host!.PreviousMenu (true);
-
-                        return true;
-                    }
-                   );
-
-        AddCommand (
-                    Command.Cancel,
-                    () =>
-                    {
-                        CloseAllMenus ();
-
-                        return true;
-                    }
-                   );
+            if (_barItems!.IsTopLevel)
+            {
+                return _host.PositionCursor ();
+            }
 
-        AddCommand (
-                    Command.Accept,
-                    () =>
-                    {
-                        RunSelected ();
+            Move (2, 1 + _currentChild);
 
-                        return true;
-                    }
-                   );
-        AddCommand (Command.Select, ctx => _host?.SelectItem ((ctx.KeyBinding?.Context as MenuItem)!));
-        AddCommand (Command.Toggle, ctx => ExpandCollapse ((ctx.KeyBinding?.Context as MenuItem)!));
-        AddCommand (Command.HotKey, ctx => _host?.SelectItem ((ctx.KeyBinding?.Context as MenuItem)!));
+            return null; // Don't show the cursor
+        }
 
-        // Default key bindings for this view
-        KeyBindings.Add (Key.CursorUp, Command.Up);
-        KeyBindings.Add (Key.CursorDown, Command.Down);
-        KeyBindings.Add (Key.CursorLeft, Command.Left);
-        KeyBindings.Add (Key.CursorRight, Command.Right);
-        KeyBindings.Add (Key.Esc, Command.Cancel);
+        return _host.PositionCursor ();
     }
 
-    private void AddKeyBindingsHotKey (MenuBarItem? menuBarItem)
+    public void Run (Action? action)
     {
-        if (menuBarItem is null || menuBarItem.Children is null)
+        if (action is null)
         {
             return;
         }
 
-        IEnumerable<MenuItem> menuItems = menuBarItem.Children.Where (m => m is { })!;
-
-        foreach (MenuItem menuItem in menuItems)
-        {
-            KeyBinding keyBinding = new ([Command.Toggle], KeyBindingScope.HotKey, menuItem);
+        Application.UngrabMouse ();
+        _host.CloseAllMenus ();
+        Application.LayoutAndDraw (true);
 
-            if (menuItem.HotKey != Key.Empty)
-            {
-                KeyBindings.Remove (menuItem.HotKey!);
-                KeyBindings.Add (menuItem.HotKey!, keyBinding);
-                KeyBindings.Remove (menuItem.HotKey!.WithAlt);
-                KeyBindings.Add (menuItem.HotKey.WithAlt, keyBinding);
-            }
-        }
+        _host.Run (action);
     }
 
-    private void RemoveKeyBindingsHotKey (MenuBarItem? menuBarItem)
+    protected override void Dispose (bool disposing)
     {
-        if (menuBarItem is null || menuBarItem.Children is null)
+        RemoveKeyBindingsHotKey (_barItems);
+
+        if (Application.Top is { })
         {
-            return;
+            Application.Top.DrawComplete -= Top_DrawComplete;
+            Application.Top.SizeChanging -= Current_TerminalResized;
         }
 
-        IEnumerable<MenuItem> menuItems = menuBarItem.Children.Where (m => m is { })!;
+        Application.MouseEvent -= Application_RootMouseEvent;
+        base.Dispose (disposing);
+    }
 
-        foreach (MenuItem menuItem in menuItems)
+    protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? view)
+    {
+        if (!newHasFocus)
         {
-            if (menuItem.HotKey != Key.Empty)
-            {
-                KeyBindings.Remove (menuItem.HotKey!);
-                KeyBindings.Remove (menuItem.HotKey!.WithAlt);
-            }
+            _host.LostFocus (previousFocusedView!);
         }
     }
 
-    /// <summary>Called when a key bound to Command.ToggleExpandCollapse is pressed. This means a hot key was pressed.</summary>
-    /// <returns></returns>
-    private bool ExpandCollapse (MenuItem? menuItem)
+    /// <inheritdoc/>
+    protected override bool OnKeyDownNotHandled (Key keyEvent)
     {
-        if (!IsInitialized || !Visible)
+        // We didn't handle the key, pass it on to host
+        bool? handled = null;
+        return _host.InvokeCommandsBoundToHotKey (keyEvent, ref handled) == true;
+    }
+
+    protected override bool OnMouseEvent (MouseEventArgs me)
+    {
+        if (!_host._handled && !_host.HandleGrabView (me, this))
         {
-            return true;
+            return false;
         }
 
+        _host._handled = false;
+        bool disabled;
 
-        for (var c = 0; c < _barItems!.Children!.Length; c++)
+        if (me.Flags == MouseFlags.Button1Clicked)
         {
-            if (_barItems.Children [c] == menuItem)
+            disabled = false;
+
+            if (me.Position.Y < 0)
             {
-                _currentChild = c;
+                return me.Handled = true;
+            }
 
-                break;
+            if (me.Position.Y >= _barItems!.Children!.Length)
+            {
+                return me.Handled = true;
             }
-        }
 
-        if (menuItem is { })
-        {
-            var m = menuItem as MenuBarItem;
+            MenuItem item = _barItems.Children [me.Position.Y]!;
 
-            if (m?.Children?.Length > 0)
+            // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
+            if (item is null || !item.IsEnabled ())
             {
-                MenuItem? item = _barItems.Children [_currentChild];
-
-                if (item is null)
-                {
-                    return true;
-                }
+                disabled = true;
+            }
 
-                // ReSharper disable once ConditionIsAlwaysTrueOrFalse
-                bool disabled = item is null || !item.IsEnabled ();
+            if (disabled)
+            {
+                return me.Handled = true;
+            }
 
-                if (!disabled && (_host.UseSubMenusSingleFrame || !CheckSubMenu ()))
-                {
-                    SetNeedsDraw ();
-                    SetParentSetNeedsDisplay ();
+            _currentChild = me.Position.Y;
+            RunSelected ();
 
-                    return true;
-                }
+            return me.Handled = true;
+        }
 
-                if (!disabled)
-                {
-                    _host.OnMenuOpened ();
-                }
+        if (me.Flags != MouseFlags.Button1Pressed
+            && me.Flags != MouseFlags.Button1DoubleClicked
+            && me.Flags != MouseFlags.Button1TripleClicked
+            && me.Flags != MouseFlags.ReportMousePosition
+            && !me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))
+        {
+            return false;
+        }
+
+        {
+            disabled = false;
+
+            if (me.Position.Y < 0 || me.Position.Y >= _barItems!.Children!.Length)
+            {
+                return me.Handled = true;
             }
-            else
+
+            MenuItem item = _barItems.Children [me.Position.Y]!;
+
+            // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
+            if (item is null)
             {
-                _host.SelectItem (menuItem);
+                return me.Handled = true;
+            }
+
+            if (item.IsEnabled () != true)
+            {
+                disabled = true;
+            }
+
+            if (!disabled)
+            {
+                _currentChild = me.Position.Y;
+            }
+
+            if (_host.UseSubMenusSingleFrame || !CheckSubMenu ())
+            {
+                SetNeedsDraw ();
+                SetParentSetNeedsDisplay ();
+
+                return me.Handled = true;
             }
+
+            _host.OnMenuOpened ();
+
+            return me.Handled = true;
         }
-        else if (_host.IsMenuOpen)
+    }
+
+    /// <inheritdoc/>
+    protected override void OnVisibleChanged ()
+    {
+        base.OnVisibleChanged ();
+
+        if (Visible)
         {
-            _host.CloseAllMenus ();
+            Application.MouseEvent += Application_RootMouseEvent;
         }
         else
         {
-            _host.OpenMenu ();
+            Application.MouseEvent -= Application_RootMouseEvent;
+        }
+    }
+
+    internal required MenuBarItem? BarItems
+    {
+        get => _barItems!;
+        init
+        {
+            ArgumentNullException.ThrowIfNull (value);
+            _barItems = value;
+
+            // Debugging aid so ToString() is helpful
+            Text = _barItems.Title;
+        }
+    }
+
+    internal bool CheckSubMenu ()
+    {
+        if (_currentChild == -1 || _barItems?.Children? [_currentChild] is null)
+        {
+            return true;
+        }
+
+        MenuBarItem? subMenu = _barItems.SubMenu (_barItems.Children [_currentChild]!);
+
+        // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
+        if (subMenu is { })
+        {
+            int pos = -1;
+
+            if (_host._openSubMenu is { })
+            {
+                pos = _host._openSubMenu.FindIndex (o => o._barItems == subMenu);
+            }
+
+            if (pos == -1
+                && this != _host.OpenCurrentMenu
+                && subMenu.Children != _host.OpenCurrentMenu!._barItems!.Children
+                && !_host.CloseMenu (false, true))
+            {
+                return false;
+            }
+
+            _host.Activate (_host._selected, pos, subMenu);
+        }
+        else if (_host._openSubMenu?.Count == 0 || _host._openSubMenu?.Last ()._barItems!.IsSubMenuOf (_barItems.Children [_currentChild]!) == false)
+        {
+            return _host.CloseMenu (false, true);
+        }
+        else
+        {
+            SetNeedsDraw ();
+            SetParentSetNeedsDisplay ();
         }
 
         return true;
     }
 
-    /// <inheritdoc />
-    protected override bool OnKeyDownNotHandled (Key keyEvent)
+    internal Attribute DetermineColorSchemeFor (MenuItem? item, int index)
     {
-        // We didn't handle the key, pass it on to host
-        return _host.InvokeCommandsBoundToKey (keyEvent) == true;
+        if (item is null)
+        {
+            return GetNormalColor ();
+        }
+
+        if (index == _currentChild)
+        {
+            return GetFocusColor ();
+        }
+
+        return !item.IsEnabled () ? ColorScheme!.Disabled : GetNormalColor ();
     }
 
-    private void Current_TerminalResized (object? sender, SizeChangedEventArgs e)
+    internal required MenuBar Host
     {
-        if (_host.IsMenuOpen)
+        get => _host;
+        init
         {
-            _host.CloseAllMenus ();
+            ArgumentNullException.ThrowIfNull (value);
+            _host = value;
         }
     }
 
-    /// <inheritdoc/>
-    protected override void OnVisibleChanged ()
+    internal static Rectangle MakeFrame (int x, int y, MenuItem? []? items, Menu? parent = null)
     {
-        base.OnVisibleChanged ();
+        if (items is null || items.Length == 0)
+        {
+            return Rectangle.Empty;
+        }
 
-        if (Visible)
+        int minX = x;
+        int minY = y;
+        const int borderOffset = 2; // This 2 is for the space around
+        int maxW = (items.Max (z => z?.Width) ?? 0) + borderOffset;
+        int maxH = items.Length + borderOffset;
+
+        if (parent is { } && x + maxW > Driver.Cols)
         {
-            Application.MouseEvent += Application_RootMouseEvent;
+            minX = Math.Max (parent.Frame.Right - parent.Frame.Width - maxW, 0);
         }
-        else
+
+        if (y + maxH > Driver.Rows)
         {
-            Application.MouseEvent -= Application_RootMouseEvent;
+            minY = Math.Max (Driver.Rows - maxH, 0);
+        }
+
+        return new (minX, minY, maxW, maxH);
+    }
+
+    internal Menu? Parent { get; init; }
+
+    private void AddKeyBindingsHotKey (MenuBarItem? menuBarItem)
+    {
+        if (menuBarItem is null || menuBarItem.Children is null)
+        {
+            return;
+        }
+
+        IEnumerable<MenuItem> menuItems = menuBarItem.Children.Where (m => m is { })!;
+
+        foreach (MenuItem menuItem in menuItems)
+        {
+            KeyBinding keyBinding = new ([Command.Toggle], this, data: menuItem);
+
+            if (menuItem.HotKey != Key.Empty)
+            {
+                HotKeyBindings.Remove (menuItem.HotKey!);
+                HotKeyBindings.Add (menuItem.HotKey!, keyBinding);
+                HotKeyBindings.Remove (menuItem.HotKey!.WithAlt);
+                HotKeyBindings.Add (menuItem.HotKey.WithAlt, keyBinding);
+            }
         }
     }
 
@@ -378,279 +522,83 @@ internal sealed class Menu : View
         }
     }
 
-    internal Attribute DetermineColorSchemeFor (MenuItem? item, int index)
+    private void CloseAllMenus ()
     {
-        if (item is null)
-        {
-            return GetNormalColor ();
-        }
-
-        if (index == _currentChild)
-        {
-            return GetFocusColor ();
-        }
-
-        return !item.IsEnabled () ? ColorScheme!.Disabled : GetNormalColor ();
+        Application.UngrabMouse ();
+        _host.CloseAllMenus ();
     }
 
-    // By doing this we draw last, over everything else.
-    private void Top_DrawComplete (object? sender, DrawEventArgs e)
+    private void Current_TerminalResized (object? sender, SizeChangedEventArgs e)
     {
-        if (!Visible)
+        if (_host.IsMenuOpen)
         {
-            return;
+            _host.CloseAllMenus ();
         }
+    }
 
-        if (_barItems!.Children is null)
+    /// <summary>Called when a key bound to Command.ToggleExpandCollapse is pressed. This means a hot key was pressed.</summary>
+    /// <returns></returns>
+    private bool ExpandCollapse (MenuItem? menuItem)
+    {
+        if (!IsInitialized || !Visible)
         {
-            return;
+            return true;
         }
 
-        DrawBorderAndPadding ();
-        RenderLineCanvas ();
-
-        // BUGBUG: Views should not change the clip. Doing so is an indcation of poor design or a bug in the framework.
-        Region? savedClip = View.SetClipToScreen ();
-
-        SetAttribute (GetNormalColor ());
-
-        for (int i = Viewport.Y; i < _barItems!.Children.Length; i++)
+        for (var c = 0; c < _barItems!.Children!.Length; c++)
         {
-            if (i < 0)
+            if (_barItems.Children [c] == menuItem)
             {
-                continue;
-            }
+                _currentChild = c;
 
-            if (ViewportToScreen (Viewport).Y + i >= Driver.Rows)
-            {
                 break;
             }
+        }
 
-            MenuItem? item = _barItems.Children [i];
-
-            SetAttribute (
-                                 // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
-                                 item is null ? GetNormalColor () :
-                                 i == _currentChild ? GetFocusColor () : GetNormalColor ()
-                                );
+        if (menuItem is { })
+        {
+            var m = menuItem as MenuBarItem;
 
-            if (item is null && BorderStyle != LineStyle.None)
-            {
-                Point s = ViewportToScreen (new Point (-1, i));
-                Driver.Move (s.X, s.Y);
-                Driver.AddRune (Glyphs.LeftTee);
-            }
-            else if (Frame.X < Driver.Cols)
+            if (m?.Children?.Length > 0)
             {
-                Move (0, i);
-            }
+                MenuItem? item = _barItems.Children [_currentChild];
 
-            SetAttribute (DetermineColorSchemeFor (item, i));
+                if (item is null)
+                {
+                    return true;
+                }
 
-            for (int p = Viewport.X; p < Frame.Width - 2; p++)
-            {
-                // This - 2 is for the border
-                if (p < 0)
-                {
-                    continue;
-                }
-
-                if (ViewportToScreen (Viewport).X + p >= Driver.Cols)
-                {
-                    break;
-                }
+                // ReSharper disable once ConditionIsAlwaysTrueOrFalse
+                bool disabled = item is null || !item.IsEnabled ();
 
-                if (item is null)
-                {
-                    Driver.AddRune (Glyphs.HLine);
-                }
-                else if (i == 0 && p == 0 && _host.UseSubMenusSingleFrame && item.Parent!.Parent is { })
+                if (!disabled && (_host.UseSubMenusSingleFrame || !CheckSubMenu ()))
                 {
-                    Driver.AddRune (Glyphs.LeftArrow);
-                }
+                    SetNeedsDraw ();
+                    SetParentSetNeedsDisplay ();
 
-                // This `- 3` is left border + right border + one row in from right
-                else if (p == Frame.Width - 3 && _barItems?.SubMenu (_barItems.Children [i]!) is { })
-                {
-                    Driver.AddRune (Glyphs.RightArrow);
-                }
-                else
-                {
-                    Driver.AddRune ((Rune)' ');
+                    return true;
                 }
-            }
 
-            if (item is null)
-            {
-                if (BorderStyle != LineStyle.None && SuperView?.Frame.Right - Frame.X > Frame.Width)
+                if (!disabled)
                 {
-                    Point s = ViewportToScreen (new Point (Frame.Width - 2, i));
-                    Driver.Move (s.X, s.Y);
-                    Driver.AddRune (Glyphs.RightTee);
+                    _host.OnMenuOpened ();
                 }
-
-                continue;
-            }
-
-            string? textToDraw;
-            Rune nullCheckedChar = Glyphs.CheckStateNone;
-            Rune checkChar = Glyphs.Selected;
-            Rune uncheckedChar = Glyphs.UnSelected;
-
-            if (item.CheckType.HasFlag (MenuItemCheckStyle.Checked))
-            {
-                checkChar = Glyphs.CheckStateChecked;
-                uncheckedChar = Glyphs.CheckStateUnChecked;
-            }
-
-            // Support Checked even though CheckType wasn't set
-            if (item is { CheckType: MenuItemCheckStyle.Checked, Checked: null })
-            {
-                textToDraw = $"{nullCheckedChar} {item.Title}";
-            }
-            else if (item.Checked == true)
-            {
-                textToDraw = $"{checkChar} {item.Title}";
-            }
-            else if (item.CheckType.HasFlag (MenuItemCheckStyle.Checked) || item.CheckType.HasFlag (MenuItemCheckStyle.Radio))
-            {
-                textToDraw = $"{uncheckedChar} {item.Title}";
             }
             else
             {
-                textToDraw = item.Title;
-            }
-
-            Point screen = ViewportToScreen (new Point (0, i));
-
-            if (screen.X < Driver.Cols)
-            {
-                Driver.Move (screen.X + 1, screen.Y);
-
-                if (!item.IsEnabled ())
-                {
-                    DrawHotString (textToDraw, ColorScheme!.Disabled, ColorScheme.Disabled);
-                }
-                else if (i == 0 && _host.UseSubMenusSingleFrame && item.Parent!.Parent is { })
-                {
-                    var tf = new TextFormatter
-                    {
-                        ConstrainToWidth = Frame.Width - 3,
-                        ConstrainToHeight = 1,
-                        Alignment = Alignment.Center, HotKeySpecifier = MenuBar.HotKeySpecifier, Text = textToDraw
-                    };
-
-                    // The -3 is left/right border + one space (not sure what for)
-                    tf.Draw (
-                             ViewportToScreen (new Rectangle (1, i, Frame.Width - 3, 1)),
-                             i == _currentChild ? GetFocusColor () : GetNormalColor (),
-                             i == _currentChild ? ColorScheme!.HotFocus : ColorScheme!.HotNormal,
-                             SuperView?.ViewportToScreen (SuperView.Viewport) ?? Rectangle.Empty
-                            );
-                }
-                else
-                {
-                    DrawHotString (
-                                   textToDraw,
-                                   i == _currentChild ? ColorScheme!.HotFocus : ColorScheme!.HotNormal,
-                                   i == _currentChild ? GetFocusColor () : GetNormalColor ()
-                                  );
-                }
-
-                // The help string
-                int l = item.ShortcutTag.GetColumns () == 0
-                            ? item.Help.GetColumns ()
-                            : item.Help.GetColumns () + item.ShortcutTag.GetColumns () + 2;
-                int col = Frame.Width - l - 3;
-                screen = ViewportToScreen (new Point (col, i));
-
-                if (screen.X < Driver.Cols)
-                {
-                    Driver.Move (screen.X, screen.Y);
-                    Driver.AddStr (item.Help);
-
-                    // The shortcut tag string
-                    if (!string.IsNullOrEmpty (item.ShortcutTag))
-                    {
-                        Driver.Move (screen.X + l - item.ShortcutTag.GetColumns (), screen.Y);
-                        Driver.AddStr (item.ShortcutTag);
-                    }
-                }
-            }
-        }
-
-        View.SetClip (savedClip);
-    }
-
-    public override Point? PositionCursor ()
-    {
-        if (_host.IsMenuOpen)
-        {
-            if (_barItems!.IsTopLevel)
-            {
-                return _host.PositionCursor ();
+                _host.SelectItem (menuItem);
             }
-
-            Move (2, 1 + _currentChild);
-
-            return null; // Don't show the cursor
-        }
-
-        return _host.PositionCursor ();
-    }
-
-    public void Run (Action? action)
-    {
-        if (action is null)
-        {
-            return;
-        }
-
-        Application.UngrabMouse ();
-        _host.CloseAllMenus ();
-        Application.LayoutAndDraw (true);
-
-        _host.Run (action);
-    }
-
-    protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? view)
-    {
-        if (!newHasFocus)
-        {
-            _host.LostFocus (previousFocusedView!);
         }
-    }
-
-    private void RunSelected ()
-    {
-        if (_barItems!.IsTopLevel)
+        else if (_host.IsMenuOpen)
         {
-            Run (_barItems.Action);
+            _host.CloseAllMenus ();
         }
         else
         {
-            switch (_currentChild)
-            {
-                case > -1 when _barItems.Children! [_currentChild]!.Action != null!:
-                    Run (_barItems.Children [_currentChild]?.Action);
-
-                    break;
-                case 0 when _host.UseSubMenusSingleFrame && _barItems.Children [_currentChild]?.Parent!.Parent != null:
-                    _host.PreviousMenu (_barItems.Children [_currentChild]!.Parent!.IsFromSubMenu, true);
-
-                    break;
-                case > -1 when _barItems.SubMenu (_barItems.Children [_currentChild]) != null!:
-                    CheckSubMenu ();
-
-                    break;
-            }
+            _host.OpenMenu ();
         }
-    }
 
-    private void CloseAllMenus ()
-    {
-        Application.UngrabMouse ();
-        _host.CloseAllMenus ();
+        return true;
     }
 
     private bool MoveDown ()
@@ -801,6 +749,51 @@ internal sealed class Menu : View
         return true;
     }
 
+    private void RemoveKeyBindingsHotKey (MenuBarItem? menuBarItem)
+    {
+        if (menuBarItem is null || menuBarItem.Children is null)
+        {
+            return;
+        }
+
+        IEnumerable<MenuItem> menuItems = menuBarItem.Children.Where (m => m is { })!;
+
+        foreach (MenuItem menuItem in menuItems)
+        {
+            if (menuItem.HotKey != Key.Empty)
+            {
+                KeyBindings.Remove (menuItem.HotKey!);
+                KeyBindings.Remove (menuItem.HotKey!.WithAlt);
+            }
+        }
+    }
+
+    private void RunSelected ()
+    {
+        if (_barItems!.IsTopLevel)
+        {
+            Run (_barItems.Action);
+        }
+        else
+        {
+            switch (_currentChild)
+            {
+                case > -1 when _barItems.Children! [_currentChild]!.Action != null!:
+                    Run (_barItems.Children [_currentChild]?.Action);
+
+                    break;
+                case 0 when _host.UseSubMenusSingleFrame && _barItems.Children [_currentChild]?.Parent!.Parent != null:
+                    _host.PreviousMenu (_barItems.Children [_currentChild]!.Parent!.IsFromSubMenu, true);
+
+                    break;
+                case > -1 when _barItems.SubMenu (_barItems.Children [_currentChild]) != null!:
+                    CheckSubMenu ();
+
+                    break;
+            }
+        }
+    }
+
     private void SetParentSetNeedsDisplay ()
     {
         if (_host._openSubMenu is { })
@@ -815,151 +808,193 @@ internal sealed class Menu : View
         _host.SetNeedsDraw ();
     }
 
-    protected override bool OnMouseEvent (MouseEventArgs me)
+    // By doing this we draw last, over everything else.
+    private void Top_DrawComplete (object? sender, DrawEventArgs e)
     {
-        if (!_host._handled && !_host.HandleGrabView (me, this))
+        if (!Visible)
         {
-            return false;
+            return;
         }
 
-        _host._handled = false;
-        bool disabled;
-
-        if (me.Flags == MouseFlags.Button1Clicked)
+        if (_barItems!.Children is null)
         {
-            disabled = false;
+            return;
+        }
 
-            if (me.Position.Y < 0)
+        DrawBorderAndPadding ();
+        RenderLineCanvas ();
+
+        // BUGBUG: Views should not change the clip. Doing so is an indcation of poor design or a bug in the framework.
+        Region? savedClip = SetClipToScreen ();
+
+        SetAttribute (GetNormalColor ());
+
+        for (int i = Viewport.Y; i < _barItems!.Children.Length; i++)
+        {
+            if (i < 0)
             {
-                return me.Handled = true;
+                continue;
             }
 
-            if (me.Position.Y >= _barItems!.Children!.Length)
+            if (ViewportToScreen (Viewport).Y + i >= Driver.Rows)
             {
-                return me.Handled = true;
+                break;
             }
 
-            MenuItem item = _barItems.Children [me.Position.Y]!;
+            MenuItem? item = _barItems.Children [i];
 
-            // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
-            if (item is null || !item.IsEnabled ())
+            SetAttribute (
+
+                          // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
+                          item is null ? GetNormalColor () :
+                          i == _currentChild ? GetFocusColor () : GetNormalColor ()
+                         );
+
+            if (item is null && BorderStyle != LineStyle.None)
             {
-                disabled = true;
+                Point s = ViewportToScreen (new Point (-1, i));
+                Driver.Move (s.X, s.Y);
+                Driver.AddRune (Glyphs.LeftTee);
             }
-
-            if (disabled)
+            else if (Frame.X < Driver.Cols)
             {
-                return me.Handled = true;
+                Move (0, i);
             }
 
-            _currentChild = me.Position.Y;
-            RunSelected ();
+            SetAttribute (DetermineColorSchemeFor (item, i));
 
-            return me.Handled = true;
-        }
+            for (int p = Viewport.X; p < Frame.Width - 2; p++)
+            {
+                // This - 2 is for the border
+                if (p < 0)
+                {
+                    continue;
+                }
 
-        if (me.Flags != MouseFlags.Button1Pressed
-            && me.Flags != MouseFlags.Button1DoubleClicked
-            && me.Flags != MouseFlags.Button1TripleClicked
-            && me.Flags != MouseFlags.ReportMousePosition
-            && !me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))
-        {
-            return false;
-        }
+                if (ViewportToScreen (Viewport).X + p >= Driver.Cols)
+                {
+                    break;
+                }
 
-        {
-            disabled = false;
+                if (item is null)
+                {
+                    Driver.AddRune (Glyphs.HLine);
+                }
+                else if (i == 0 && p == 0 && _host.UseSubMenusSingleFrame && item.Parent!.Parent is { })
+                {
+                    Driver.AddRune (Glyphs.LeftArrow);
+                }
 
-            if (me.Position.Y < 0 || me.Position.Y >= _barItems!.Children!.Length)
-            {
-                return me.Handled = true;
+                // This `- 3` is left border + right border + one row in from right
+                else if (p == Frame.Width - 3 && _barItems?.SubMenu (_barItems.Children [i]!) is { })
+                {
+                    Driver.AddRune (Glyphs.RightArrow);
+                }
+                else
+                {
+                    Driver.AddRune ((Rune)' ');
+                }
             }
 
-            MenuItem item = _barItems.Children [me.Position.Y]!;
-
-            // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
             if (item is null)
             {
-                return me.Handled = true;
+                if (BorderStyle != LineStyle.None && SuperView?.Frame.Right - Frame.X > Frame.Width)
+                {
+                    Point s = ViewportToScreen (new Point (Frame.Width - 2, i));
+                    Driver.Move (s.X, s.Y);
+                    Driver.AddRune (Glyphs.RightTee);
+                }
+
+                continue;
             }
 
-            if (item.IsEnabled () != true)
+            string? textToDraw;
+            Rune nullCheckedChar = Glyphs.CheckStateNone;
+            Rune checkChar = Glyphs.Selected;
+            Rune uncheckedChar = Glyphs.UnSelected;
+
+            if (item.CheckType.HasFlag (MenuItemCheckStyle.Checked))
             {
-                disabled = true;
+                checkChar = Glyphs.CheckStateChecked;
+                uncheckedChar = Glyphs.CheckStateUnChecked;
             }
 
-            if (!disabled)
+            // Support Checked even though CheckType wasn't set
+            if (item is { CheckType: MenuItemCheckStyle.Checked, Checked: null })
             {
-                _currentChild = me.Position.Y;
+                textToDraw = $"{nullCheckedChar} {item.Title}";
             }
-
-            if (_host.UseSubMenusSingleFrame || !CheckSubMenu ())
+            else if (item.Checked == true)
             {
-                SetNeedsDraw ();
-                SetParentSetNeedsDisplay ();
-
-                return me.Handled = true;
+                textToDraw = $"{checkChar} {item.Title}";
             }
-
-            _host.OnMenuOpened ();
-
-            return me.Handled = true;
-        }
-    }
-
-    internal bool CheckSubMenu ()
-    {
-        if (_currentChild == -1 || _barItems?.Children? [_currentChild] is null)
-        {
-            return true;
-        }
-
-        MenuBarItem? subMenu = _barItems.SubMenu (_barItems.Children [_currentChild]!);
-
-        // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
-        if (subMenu is { })
-        {
-            int pos = -1;
-
-            if (_host._openSubMenu is { })
+            else if (item.CheckType.HasFlag (MenuItemCheckStyle.Checked) || item.CheckType.HasFlag (MenuItemCheckStyle.Radio))
             {
-                pos = _host._openSubMenu.FindIndex (o => o._barItems == subMenu);
+                textToDraw = $"{uncheckedChar} {item.Title}";
             }
-
-            if (pos == -1
-                && this != _host.OpenCurrentMenu
-                && subMenu.Children != _host.OpenCurrentMenu!._barItems!.Children
-                && !_host.CloseMenu (false, true))
+            else
             {
-                return false;
+                textToDraw = item.Title;
             }
 
-            _host.Activate (_host._selected, pos, subMenu);
-        }
-        else if (_host._openSubMenu?.Count == 0 || _host._openSubMenu?.Last ()._barItems!.IsSubMenuOf (_barItems.Children [_currentChild]!) == false)
-        {
-            return _host.CloseMenu (false, true);
-        }
-        else
-        {
-            SetNeedsDraw ();
-            SetParentSetNeedsDisplay ();
-        }
+            Point screen = ViewportToScreen (new Point (0, i));
 
-        return true;
-    }
+            if (screen.X < Driver.Cols)
+            {
+                Driver.Move (screen.X + 1, screen.Y);
 
-    protected override void Dispose (bool disposing)
-    {
-        RemoveKeyBindingsHotKey (_barItems);
+                if (!item.IsEnabled ())
+                {
+                    DrawHotString (textToDraw, ColorScheme!.Disabled, ColorScheme.Disabled);
+                }
+                else if (i == 0 && _host.UseSubMenusSingleFrame && item.Parent!.Parent is { })
+                {
+                    var tf = new TextFormatter
+                    {
+                        ConstrainToWidth = Frame.Width - 3,
+                        ConstrainToHeight = 1,
+                        Alignment = Alignment.Center, HotKeySpecifier = MenuBar.HotKeySpecifier, Text = textToDraw
+                    };
 
-        if (Application.Top is { })
-        {
-            Application.Top.DrawComplete -= Top_DrawComplete;
-            Application.Top.SizeChanging -= Current_TerminalResized;
+                    // The -3 is left/right border + one space (not sure what for)
+                    tf.Draw (
+                             ViewportToScreen (new Rectangle (1, i, Frame.Width - 3, 1)),
+                             i == _currentChild ? GetFocusColor () : GetNormalColor (),
+                             i == _currentChild ? ColorScheme!.HotFocus : ColorScheme!.HotNormal,
+                             SuperView?.ViewportToScreen (SuperView.Viewport) ?? Rectangle.Empty
+                            );
+                }
+                else
+                {
+                    DrawHotString (
+                                   textToDraw,
+                                   i == _currentChild ? ColorScheme!.HotFocus : ColorScheme!.HotNormal,
+                                   i == _currentChild ? GetFocusColor () : GetNormalColor ()
+                                  );
+                }
+
+                // The help string
+                int l = item.ShortcutTag.GetColumns () == 0
+                            ? item.Help.GetColumns ()
+                            : item.Help.GetColumns () + item.ShortcutTag.GetColumns () + 2;
+                int col = Frame.Width - l - 3;
+                screen = ViewportToScreen (new Point (col, i));
+
+                if (screen.X < Driver.Cols)
+                {
+                    Driver.Move (screen.X, screen.Y);
+                    Driver.AddStr (item.Help);
+
+                    // The shortcut tag string
+                    if (!string.IsNullOrEmpty (item.ShortcutTag))
+                    {
+                        Driver.Move (screen.X + l - item.ShortcutTag.GetColumns (), screen.Y);
+                        Driver.AddStr (item.ShortcutTag);
+                    }
+                }
+            }
         }
 
-        Application.MouseEvent -= Application_RootMouseEvent;
-        base.Dispose (disposing);
+        SetClip (savedClip);
     }
 }

+ 27 - 19
Terminal.Gui/Views/Menu/MenuBar.cs

@@ -133,16 +133,24 @@ public class MenuBar : View, IDesignable
                                                   {
                                                       CloseOtherOpenedMenuBar ();
 
-                                                      return Select (Menus.IndexOf (ctx.KeyBinding?.Context));
+                                                      if (ctx is not CommandContext<KeyBinding> keyCommandContext)
+                                                      {
+                                                          return false;
+                                                      }
+                                                      return Select (Menus.IndexOf (keyCommandContext.Binding.Data));
                                                   });
         AddCommand (Command.Select, ctx =>
                                     {
-                                        if (ctx.Data is MouseEventArgs)
+                                        if (ctx is not CommandContext<KeyBinding> keyCommandContext)
+                                        {
+                                            return false ;
+                                        }
+                                        if (keyCommandContext.Binding.Data is MouseEventArgs)
                                         {
                                             // HACK: Work around the fact that View.MouseClick always invokes Select
                                             return false;
                                         }
-                                        var res = Run ((ctx.KeyBinding?.Context as MenuItem)?.Action!);
+                                        var res = Run ((keyCommandContext.Binding.Data as MenuItem)?.Action!);
                                         CloseAllMenus ();
 
                                         return res;
@@ -154,13 +162,13 @@ public class MenuBar : View, IDesignable
         KeyBindings.Add (Key.Esc, Command.Cancel);
         KeyBindings.Add (Key.CursorDown, Command.Accept);
 
-        KeyBinding keyBinding = new ([Command.Toggle], KeyBindingScope.HotKey, -1); // -1 indicates Key was used
-        KeyBindings.Add (Key, keyBinding);
+        KeyBinding keyBinding = new ([Command.Toggle], this, data: -1); // -1 indicates Key was used
+        HotKeyBindings.Add (Key, keyBinding);
 
         // TODO: Why do we have two keybindings for opening the menu? Ctrl-Space and Key?
-        KeyBindings.Add (Key.Space.WithCtrl, keyBinding);
+        HotKeyBindings.Add (Key.Space.WithCtrl, keyBinding);
         // This is needed for macOS because Key.Space.WithCtrl doesn't work
-        KeyBindings.Add (Key.Space.WithAlt, keyBinding);
+        HotKeyBindings.Add (Key.Space.WithAlt, keyBinding);
 
         // TODO: Figure out how to make Alt work (on Windows)
         //KeyBindings.Add (Key.WithAlt, keyBinding);
@@ -196,21 +204,21 @@ public class MenuBar : View, IDesignable
 
                 if (menuBarItem.HotKey != Key.Empty)
                 {
-                    KeyBindings.Remove (menuBarItem.HotKey!);
-                    KeyBinding keyBinding = new ([Command.Toggle], KeyBindingScope.Focused, menuBarItem);
-                    KeyBindings.Add (menuBarItem.HotKey!, keyBinding);
-                    KeyBindings.Remove (menuBarItem.HotKey!.WithAlt);
-                    keyBinding = new ([Command.Toggle], KeyBindingScope.HotKey, menuBarItem);
-                    KeyBindings.Add (menuBarItem.HotKey.WithAlt, keyBinding);
+                    HotKeyBindings.Remove (menuBarItem.HotKey!);
+                    KeyBinding keyBinding = new ([Command.Toggle], this, menuBarItem);
+                    HotKeyBindings.Add (menuBarItem.HotKey!, keyBinding);
+                    HotKeyBindings.Remove (menuBarItem.HotKey!.WithAlt);
+                    keyBinding = new ([Command.Toggle], this, data: menuBarItem);
+                    HotKeyBindings.Add (menuBarItem.HotKey.WithAlt, keyBinding);
                 }
 
                 if (menuBarItem.ShortcutKey != Key.Empty)
                 {
                     // Technically this will never run because MenuBarItems don't have shortcuts
                     // unless the IsTopLevel is true
-                    KeyBindings.Remove (menuBarItem.ShortcutKey!);
-                    KeyBinding keyBinding = new ([Command.Select], KeyBindingScope.HotKey, menuBarItem);
-                    KeyBindings.Add (menuBarItem.ShortcutKey!, keyBinding);
+                    HotKeyBindings.Remove (menuBarItem.ShortcutKey!);
+                    KeyBinding keyBinding = new ([Command.Select], this, data: menuBarItem);
+                    HotKeyBindings.Add (menuBarItem.ShortcutKey!, keyBinding);
                 }
 
                 menuBarItem.AddShortcutKeyBindings (this);
@@ -1302,9 +1310,9 @@ public class MenuBar : View, IDesignable
                 return;
             }
 
-            KeyBindings.Remove (_key);
-            KeyBinding keyBinding = new ([Command.Toggle], KeyBindingScope.HotKey, -1); // -1 indicates Key was used
-            KeyBindings.Add (value, keyBinding);
+            HotKeyBindings.Remove (_key);
+            KeyBinding keyBinding = new ([Command.Toggle], this, data: -1); // -1 indicates Key was used
+            HotKeyBindings.Add (value, keyBinding);
             _key = value;
         }
     }

+ 3 - 3
Terminal.Gui/Views/Menu/MenuBarItem.cs

@@ -222,7 +222,7 @@ public class MenuBarItem : MenuItem
                 if (menuItem?.ShortcutKey != Key.Empty)
                 {
                     // Remove an existent ShortcutKey
-                    _menuBar?.KeyBindings.Remove (menuItem?.ShortcutKey!);
+                    _menuBar?.HotKeyBindings.Remove (menuItem?.ShortcutKey!);
                 }
             }
         }
@@ -230,7 +230,7 @@ public class MenuBarItem : MenuItem
         if (ShortcutKey != Key.Empty)
         {
             // Remove an existent ShortcutKey
-            _menuBar?.KeyBindings.Remove (ShortcutKey!);
+            _menuBar?.HotKeyBindings.Remove (ShortcutKey!);
         }
 
         var index = _menuBar!.Menus.IndexOf (this);
@@ -239,7 +239,7 @@ public class MenuBarItem : MenuItem
             if (_menuBar.Menus [index].HotKey != Key.Empty)
             {
                 // Remove an existent HotKey
-                _menuBar.KeyBindings.Remove (HotKey!.WithAlt);
+                _menuBar.HotKeyBindings.Remove (HotKey!.WithAlt);
             }
 
             _menuBar.Menus [index] = null!;

+ 16 - 16
Terminal.Gui/Views/Menu/MenuItem.cs

@@ -149,11 +149,11 @@ public class MenuItem
         if (AllowNullChecked)
         {
             Checked = previousChecked switch
-                      {
-                          null => true,
-                          true => false,
-                          false => null
-                      };
+            {
+                null => true,
+                true => false,
+                false => null
+            };
         }
         else
         {
@@ -242,9 +242,9 @@ public class MenuItem
             }
             else if (nextIsHot)
             {
-                    HotKey = char.ToLower (x);
+                HotKey = char.ToLower (x);
 
-                    return;
+                return;
             }
         }
 
@@ -289,15 +289,15 @@ public class MenuItem
     {
         if (key != Key.Empty)
         {
-            _menuBar.KeyBindings.Remove (key);
+            _menuBar.HotKeyBindings.Remove (key);
         }
 
         if (ShortcutKey != Key.Empty)
         {
-            KeyBinding keyBinding = new ([Command.Select], KeyBindingScope.HotKey, this);
+            KeyBinding keyBinding = new ([Command.Select], null, data: this);
             // Remove an existent ShortcutKey
-            _menuBar.KeyBindings.Remove (ShortcutKey!);
-            _menuBar.KeyBindings.Add (ShortcutKey!, keyBinding);
+            _menuBar.HotKeyBindings.Remove (ShortcutKey!);
+            _menuBar.HotKeyBindings.Add (ShortcutKey!, keyBinding);
         }
     }
 
@@ -314,7 +314,7 @@ public class MenuItem
 
             if (index > -1)
             {
-                _menuBar.KeyBindings.Remove (oldKey.WithAlt);
+                _menuBar.HotKeyBindings.Remove (oldKey.WithAlt);
             }
         }
 
@@ -324,9 +324,9 @@ public class MenuItem
 
             if (index > -1)
             {
-                _menuBar.KeyBindings.Remove (HotKey!.WithAlt);
-                KeyBinding keyBinding = new ([Command.Toggle], KeyBindingScope.HotKey, this);
-                _menuBar.KeyBindings.Add (HotKey.WithAlt, keyBinding);
+                _menuBar.HotKeyBindings.Remove (HotKey!.WithAlt);
+                KeyBinding keyBinding = new ([Command.Toggle], null, data: this);
+                _menuBar.HotKeyBindings.Add (HotKey.WithAlt, keyBinding);
             }
         }
     }
@@ -378,7 +378,7 @@ public class MenuItem
         if (ShortcutKey != Key.Empty)
         {
             // Remove an existent ShortcutKey
-            _menuBar.KeyBindings.Remove (ShortcutKey!);
+            _menuBar.HotKeyBindings.Remove (ShortcutKey!);
         }
     }
 }

+ 9 - 4
Terminal.Gui/Views/MessageBox.cs

@@ -358,14 +358,19 @@ public static class MessageBox
                 if (count == defaultButton)
                 {
                     b.IsDefault = true;
-                    b.Accepting += (s, e) =>
+                    b.Accepting += (_, e) =>
                                    {
+                                       if (e.Context is not CommandContext<KeyBinding> keyCommandContext)
+                                       {
+                                           return;
+                                       }
+
                                        // TODO: With https://github.com/gui-cs/Terminal.Gui/issues/3778 we can simplify this
-                                       if (e.Context.Data is Button button)
+                                       if (keyCommandContext.Binding.Data is Button button)
                                        {
                                            Clicked = (int)button.Data!;
-                                       } 
-                                       else if (e.Context.KeyBinding?.BoundView is Button btn)
+                                       }
+                                       else if (keyCommandContext.Binding.Target is Button btn)
                                        {
                                            Clicked = (int)btn.Data!;
                                        }

+ 151 - 196
Terminal.Gui/Views/RadioGroup.cs

@@ -16,170 +16,170 @@ public class RadioGroup : View, IDesignable, IOrientation
         Width = Dim.Auto (DimAutoStyle.Content);
         Height = Dim.Auto (DimAutoStyle.Content);
 
-
         // Select (Space key or mouse click) - The default implementation sets focus. RadioGroup does not.
-        AddCommand (
-                    Command.Select,
-                    (ctx) =>
-                    {
-                        bool cursorChanged = false;
-                        if (SelectedItem == Cursor)
-                        {
-                            cursorChanged = MoveDownRight ();
-                            if (!cursorChanged)
-                            {
-                                cursorChanged = MoveHome ();
-                            }
-                        }
-
-                        bool selectedItemChanged = false;
-                        if (SelectedItem != Cursor)
-                        {
-                            selectedItemChanged = ChangeSelectedItem (Cursor);
-                        }
-
-                        if (cursorChanged || selectedItemChanged)
-                        {
-                            if (RaiseSelecting (ctx) == true)
-                            {
-                                return true;
-                            }
-                        }
+        AddCommand (Command.Select, HandleSelectCommand);
 
-                        return cursorChanged || selectedItemChanged;
-                    });
-
-        // Accept (Enter key) - Raise Accept event - DO NOT advance state
-        AddCommand (Command.Accept, RaiseAccepting);
+        // Accept (Enter key or Doubleclick) - Raise Accept event - DO NOT advance state
+        AddCommand (Command.Accept, HandleAcceptCommand);
 
         // Hotkey - ctx may indicate a radio item hotkey was pressed. Behavior depends on HasFocus
         //          If HasFocus and it's this.HotKey invoke Select command - DO NOT raise Accept
         //          If it's a radio item HotKey select that item and raise Selected event - DO NOT raise Accept
         //          If nothing is selected, select first and raise Selected event - DO NOT raise Accept
-        AddCommand (Command.HotKey,
-                    ctx =>
-                            {
-                                var item = ctx.KeyBinding?.Context as int?;
-
-                                if (HasFocus)
-                                {
-                                    if (ctx is { KeyBinding: { } } && (ctx.KeyBinding.Value.BoundView != this || HotKey == ctx.Key?.NoAlt.NoCtrl.NoShift))
-                                    {
-                                        // It's this.HotKey OR Another View (Label?) forwarded the hotkey command to us - Act just like `Space` (Select)
-                                        return InvokeCommand (Command.Select, ctx.Key, ctx.KeyBinding);
-                                    }
-                                }
-
-                                if (item is { } && item < _radioLabels.Count)
-                                {
-                                    if (item.Value == SelectedItem)
-                                    {
-                                        return true;
-                                    }
-
-                                    // If a RadioItem.HotKey is pressed we always set the selected item - never SetFocus
-                                    bool selectedItemChanged = ChangeSelectedItem (item.Value);
-
-                                    if (selectedItemChanged)
-                                    {
-                                        // Doesn't matter if it's handled
-                                        RaiseSelecting (ctx);
-                                        return true;
-                                    }
-
-
-                                    return false;
-                                }
-
-                                if (SelectedItem == -1 && ChangeSelectedItem (0))
-                                {
-                                    if (RaiseSelecting (ctx) == true)
-                                    {
-                                        return true;
-                                    }
-                                    return false;
-                                }
-
-                                if (RaiseHandlingHotKey () == true)
-                                {
-                                    return true;
-                                };
-
-                                // Default Command.Hotkey sets focus
-                                SetFocus ();
-
-                                return true;
-                            });
-
-        AddCommand (
-                    Command.Up,
-                    () =>
-                    {
-                        if (!HasFocus)
-                        {
-                            return false;
-                        }
+        AddCommand (Command.HotKey, HandleHotKeyCommand);
 
-                        return MoveUpLeft ();
-                    }
-                   );
+        AddCommand (Command.Up, () => HasFocus && MoveUpLeft ());
+        AddCommand (Command.Down, () => HasFocus && MoveDownRight ());
+        AddCommand (Command.Start, () => HasFocus && MoveHome ());
+        AddCommand (Command.End, () => HasFocus && MoveEnd ());
 
-        AddCommand (
-                    Command.Down,
-                    () =>
-                    {
-                        if (!HasFocus)
-                        {
-                            return false;
-                        }
+        // ReSharper disable once UseObjectOrCollectionInitializer
+        _orientationHelper = new (this);
+        _orientationHelper.Orientation = Orientation.Vertical;
 
-                        return MoveDownRight ();
-                    }
-                   );
+        SetupKeyBindings ();
 
-        AddCommand (
-                    Command.Start,
-                    () =>
-                    {
-                        if (!HasFocus)
-                        {
-                            return false;
-                        }
+        // By default, single click is already bound to Command.Select
+        MouseBindings.Add (MouseFlags.Button1DoubleClicked, Command.Accept);
 
-                        MoveHome ();
+        SubviewLayout += RadioGroup_LayoutStarted;
 
-                        return true;
-                    }
-                   );
+        HighlightStyle = HighlightStyle.PressedOutside | HighlightStyle.Pressed;
+    }
 
-        AddCommand (
-                    Command.End,
-                    () =>
-                    {
-                        if (!HasFocus)
-                        {
-                            return false;
-                        }
+    private bool? HandleHotKeyCommand (ICommandContext? ctx)
+    {
+        // If the command did not come from a keyboard event, ignore it
+        if (ctx is not CommandContext<KeyBinding> keyCommandContext)
+        {
+            return false;
+        }
 
-                        MoveEnd ();
+        var item = keyCommandContext.Binding.Data as int?;
 
-                        return true;
-                    }
-                   );
+        if (HasFocus)
+        {
+            if ((item is null || HotKey == keyCommandContext.Binding.Key?.NoAlt.NoCtrl.NoShift!))
+            {
+                // It's this.HotKey OR Another View (Label?) forwarded the hotkey command to us - Act just like `Space` (Select)
+                return InvokeCommand (Command.Select);
+            }
+        }
 
-        // ReSharper disable once UseObjectOrCollectionInitializer
-        _orientationHelper = new (this);
-        _orientationHelper.Orientation = Orientation.Vertical;
-        _orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e);
-        _orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e);
+        if (item is { } && item < _radioLabels.Count)
+        {
+            if (item.Value == SelectedItem)
+            {
+                return true;
+            }
 
-        SetupKeyBindings ();
+            // If a RadioItem.HotKey is pressed we always set the selected item - never SetFocus
+            bool selectedItemChanged = ChangeSelectedItem (item.Value);
 
-        SubviewLayout += RadioGroup_LayoutStarted;
+            if (selectedItemChanged)
+            {
+                // Doesn't matter if it's handled
+                RaiseSelecting (ctx);
 
-        HighlightStyle = HighlightStyle.PressedOutside | HighlightStyle.Pressed;
+                return true;
+            }
+
+            return false;
+        }
+
+        if (SelectedItem == -1 && ChangeSelectedItem (0))
+        {
+            if (RaiseSelecting (ctx) == true)
+            {
+                return true;
+            }
+
+            return false;
+        }
+
+        if (RaiseHandlingHotKey () == true)
+        {
+            return true;
+        }
+
+        ;
+
+        // Default Command.Hotkey sets focus
+        SetFocus ();
+
+        return true;
+    }
+
+    private bool? HandleAcceptCommand (ICommandContext? ctx)
+    {
+        if (!DoubleClickAccepts
+            && ctx is CommandContext<MouseBinding> mouseCommandContext
+            && mouseCommandContext.Binding.MouseEventArgs!.Flags.HasFlag (MouseFlags.Button1DoubleClicked))
+        {
+            return false;
+        }
+
+        return RaiseAccepting (ctx);
+    }
+
+    private bool? HandleSelectCommand (ICommandContext? ctx)
+    {
+        if (ctx is CommandContext<MouseBinding> mouseCommandContext
+            && mouseCommandContext.Binding.MouseEventArgs!.Flags.HasFlag (MouseFlags.Button1Clicked))
+        {
+            int viewportX = mouseCommandContext.Binding.MouseEventArgs.Position.X;
+            int viewportY = mouseCommandContext.Binding.MouseEventArgs.Position.Y;
+
+            int pos = Orientation == Orientation.Horizontal ? viewportX : viewportY;
+
+            int rCount = Orientation == Orientation.Horizontal
+                             ? _horizontal!.Last ().pos + _horizontal!.Last ().length
+                             : _radioLabels.Count;
+
+            if (pos < rCount)
+            {
+                int c = Orientation == Orientation.Horizontal
+                            ? _horizontal!.FindIndex (x => x.pos <= viewportX && x.pos + x.length - 2 >= viewportX)
+                            : viewportY;
+
+                if (c > -1)
+                {
+                    // Just like the user pressing the items' hotkey
+                    return InvokeCommand<KeyBinding> (Command.HotKey, new KeyBinding ([Command.HotKey], target: this, data: c)) == true;
+                }
+            }
+
+            return false;
+        }
+
+        bool cursorChanged = false;
+
+        if (SelectedItem == Cursor)
+        {
+            cursorChanged = MoveDownRight ();
+
+            if (!cursorChanged)
+            {
+                cursorChanged = MoveHome ();
+            }
+        }
+
+        bool selectedItemChanged = false;
+
+        if (SelectedItem != Cursor)
+        {
+            selectedItemChanged = ChangeSelectedItem (Cursor);
+        }
 
-        MouseClick += RadioGroup_MouseClick;
+        if (cursorChanged || selectedItemChanged)
+        {
+            if (RaiseSelecting (ctx) == true)
+            {
+                return true;
+            }
+        }
+
+        return cursorChanged || selectedItemChanged;
     }
 
     // TODO: Fix InvertColorsOnPress - only highlight the selected item
@@ -220,48 +220,6 @@ public class RadioGroup : View, IDesignable, IOrientation
     /// </remarks>
     public bool DoubleClickAccepts { get; set; } = true;
 
-    private void RadioGroup_MouseClick (object? sender, MouseEventArgs e)
-    {
-        if (e.Flags.HasFlag (MouseFlags.Button1Clicked))
-        {
-            int viewportX = e.Position.X;
-            int viewportY = e.Position.Y;
-
-            int pos = Orientation == Orientation.Horizontal ? viewportX : viewportY;
-
-            int rCount = Orientation == Orientation.Horizontal
-                             ? _horizontal!.Last ().pos + _horizontal!.Last ().length
-                             : _radioLabels.Count;
-
-            if (pos < rCount)
-            {
-                int c = Orientation == Orientation.Horizontal
-                            ? _horizontal!.FindIndex (x => x.pos <= viewportX && x.pos + x.length - 2 >= viewportX)
-                            : viewportY;
-
-                if (c > -1)
-                {
-                    // Just like the user pressing the items' hotkey
-                    e.Handled = InvokeCommand (Command.HotKey, null, new KeyBinding ([Command.HotKey], KeyBindingScope.HotKey, boundView: this, context: c)) == true;
-                }
-            }
-
-            return;
-        }
-
-        if (DoubleClickAccepts && e.Flags.HasFlag (MouseFlags.Button1DoubleClicked))
-        {
-            // NOTE: Drivers ALWAYS generate a Button1Clicked event before Button1DoubleClicked
-            // NOTE: So, we've already selected an item.
-
-            // Just like the user pressing `Enter`
-            InvokeCommand (Command.Accept);
-        }
-
-        // HACK: Always eat so Select is not invoked by base
-        e.Handled = true;
-    }
-
     private List<(int pos, int length)>? _horizontal;
     private int _horizontalSpace = 2;
 
@@ -393,11 +351,7 @@ public class RadioGroup : View, IDesignable, IOrientation
 
                     if (j == hotPos && i == Cursor)
                     {
-                        SetAttribute (
-                                                          HasFocus
-                                                              ? ColorScheme!.HotFocus
-                                                              : GetHotNormalColor ()
-                                                         );
+                        SetAttribute (HasFocus ? GetHotFocusColor() : GetHotNormalColor ());
                     }
                     else if (j == hotPos && i != Cursor)
                     {
@@ -415,11 +369,7 @@ public class RadioGroup : View, IDesignable, IOrientation
 
                         if (i == Cursor)
                         {
-                            SetAttribute (
-                                                              HasFocus
-                                                                  ? ColorScheme!.HotFocus
-                                                                  : GetHotNormalColor ()
-                                                             );
+                            SetAttribute (HasFocus ? GetHotFocusColor() : GetHotNormalColor ());
                         }
                         else if (i != Cursor)
                         {
@@ -532,7 +482,12 @@ public class RadioGroup : View, IDesignable, IOrientation
         return false;
     }
 
-    private void MoveEnd () { Cursor = Math.Max (_radioLabels.Count - 1, 0); }
+    private bool MoveEnd ()
+    {
+        Cursor = Math.Max (_radioLabels.Count - 1, 0);
+
+        return true;
+    }
 
     private bool MoveHome ()
     {

+ 1 - 1
Terminal.Gui/Views/ScrollBar/ScrollSlider.cs

@@ -241,7 +241,7 @@ public class ScrollSlider : View, IOrientation, IDesignable
         OnScrolled (distance);
         Scrolled?.Invoke (this, new (in distance));
 
-        RaiseSelecting (new (Command.Select, null, null, distance));
+        RaiseSelecting (new CommandContext<KeyBinding> (Command.Select, new KeyBinding ([Command.Select], null, distance)));
     }
 
     /// <summary>

+ 36 - 34
Terminal.Gui/Views/Shortcut.cs

@@ -18,7 +18,7 @@ namespace Terminal.Gui;
 ///         - Pressing the HotKey specified by <see cref="CommandView"/>.
 ///     </para>
 ///     <para>
-///         If <see cref="KeyBindingScope"/> is <see cref="KeyBindingScope.Application"/>, <see cref="Key"/> will invoke
+///         If <see cref="BindKeyToApplication"/> is <see langword="true"/>, <see cref="Key"/> will invoke
 ///         <see cref="Command.Accept"/>
 ///         regardless of what View has focus, enabling an application-wide keyboard shortcut.
 ///     </para>
@@ -69,7 +69,7 @@ public class Shortcut : View, IOrientation, IDesignable
     /// <param name="helpText">The help text to display.</param>
     public Shortcut (View targetView, Command command, string commandText, string? helpText = null)
         : this (
-                targetView?.KeyBindings.GetKeyFromCommands (command)!,
+                targetView?.HotKeyBindings.GetFirstFromCommands (command)!,
                 commandText,
                 null,
                 helpText)
@@ -117,7 +117,7 @@ public class Shortcut : View, IOrientation, IDesignable
         {
             Id = "CommandView",
             Width = Dim.Auto (),
-            Height = Dim.Fill()
+            Height = Dim.Fill ()
         };
         Title = commandText ?? string.Empty;
 
@@ -144,7 +144,7 @@ public class Shortcut : View, IOrientation, IDesignable
                          DimAutoStyle.Content,
                          minimumContentDim: Dim.Func (() => _minimumNaturalWidth ?? 0),
                          maximumContentDim: Dim.Func (() => _minimumNaturalWidth ?? 0))!;
-}
+    }
 
     private AlignmentModes _alignmentModes = AlignmentModes.StartToEnd | AlignmentModes.IgnoreFirstOrLast;
 
@@ -224,7 +224,7 @@ public class Shortcut : View, IOrientation, IDesignable
         _minimumNaturalWidth = PosAlign.CalculateMinDimension (0, Subviews, Dimension.Width);
 
         // Reset our relative layout
-        SetRelativeLayout (SuperView?.GetContentSize() ?? Application.Screen.Size);
+        SetRelativeLayout (SuperView?.GetContentSize () ?? Application.Screen.Size);
     }
 
     // TODO: Enable setting of the margin thickness
@@ -300,17 +300,19 @@ public class Shortcut : View, IOrientation, IDesignable
         AddCommand (Command.Select, DispatchCommand);
     }
 
-    private bool? DispatchCommand (CommandContext ctx)
+    private bool? DispatchCommand (ICommandContext? commandContext)
     {
-        if (ctx.Data != this)
+        CommandContext<KeyBinding>? keyCommandContext = commandContext is CommandContext<KeyBinding> ? (CommandContext<KeyBinding>)commandContext : default;
+
+        if (keyCommandContext?.Binding.Data != this)
         {
             // Invoke Select on the command view to cause it to change state if it wants to
             // If this causes CommandView to raise Accept, we eat it
-            ctx.Data = this;
-            CommandView.InvokeCommand (Command.Select, ctx);
+            keyCommandContext = keyCommandContext!.Value with { Binding = keyCommandContext.Value.Binding with { Data = this } };
+            CommandView.InvokeCommand (Command.Select, keyCommandContext);
         }
 
-        if (RaiseSelecting (ctx) is true)
+        if (RaiseSelecting (keyCommandContext) is true)
         {
             return true;
         }
@@ -320,16 +322,16 @@ public class Shortcut : View, IOrientation, IDesignable
 
         var cancel = false;
 
-        cancel = RaiseAccepting (ctx) is true;
+        cancel = RaiseAccepting (commandContext) is true;
 
         if (cancel)
         {
             return true;
         }
 
-        if (ctx.Command != Command.Accept)
+        if (commandContext?.Command != Command.Accept)
         {
-           // return false;
+            // return false;
         }
 
         if (Action is { })
@@ -342,7 +344,7 @@ public class Shortcut : View, IOrientation, IDesignable
 
         if (_targetView is { })
         {
-            _targetView.InvokeCommand (Command);
+            _targetView.InvokeCommand (Command, commandContext);
         }
 
         return cancel;
@@ -493,10 +495,11 @@ public class Shortcut : View, IOrientation, IDesignable
 
             void CommandViewOnSelecting (object? sender, CommandEventArgs e)
             {
-                if (e.Context.Data != this)
+                if ((e.Context is CommandContext<KeyBinding> keyCommandContext && keyCommandContext.Binding.Data != this) ||
+                    e.Context is CommandContext<MouseBinding>)
                 {
                     // Forward command to ourselves
-                    InvokeCommand (Command.Select, new (Command.Select, null, null, this));
+                    InvokeCommand<KeyBinding> (Command.Select, new ([Command.Select], null, this));
                 }
 
                 // BUGBUG: This prevents NumericUpDown on statusbar in HexEditor from working
@@ -612,32 +615,31 @@ public class Shortcut : View, IOrientation, IDesignable
         }
     }
 
-    private KeyBindingScope _keyBindingScope = KeyBindingScope.HotKey;
+    private bool _bindKeyToApplication = false;
 
     /// <summary>
-    ///     Gets or sets the scope for the key binding for how <see cref="Key"/> is bound to <see cref="Command"/>.
+    ///     Gets or sets whether <see cref="Key"/> is bound to <see cref="Command"/> via <see cref="View.HotKeyBindings"/> or <see cref="Application.KeyBindings"/>.
     /// </summary>
-    public KeyBindingScope KeyBindingScope
+    public bool BindKeyToApplication
     {
-        get => _keyBindingScope;
+        get => _bindKeyToApplication;
         set
         {
-            if (value == _keyBindingScope)
+            if (value == _bindKeyToApplication)
             {
                 return;
             }
 
-            if (_keyBindingScope == KeyBindingScope.Application)
+            if (_bindKeyToApplication)
             {
-                Application.KeyBindings.Remove (Key, this);
+                Application.KeyBindings.Remove (Key);
             }
-
-            if (_keyBindingScope is KeyBindingScope.HotKey or KeyBindingScope.Focused)
+            else
             {
-                KeyBindings.Remove (Key);
+                HotKeyBindings.Remove (Key);
             }
 
-            _keyBindingScope = value;
+            _bindKeyToApplication = value;
 
             UpdateKeyBindings (Key.Empty);
         }
@@ -700,25 +702,25 @@ public class Shortcut : View, IOrientation, IDesignable
     {
         if (Key.IsValid)
         {
-            if (KeyBindingScope.FastHasFlags (KeyBindingScope.Application))
+            if (BindKeyToApplication)
             {
                 if (oldKey != Key.Empty)
                 {
-                    Application.KeyBindings.Remove (oldKey, this);
+                    Application.KeyBindings.Remove (oldKey);
                 }
 
-                Application.KeyBindings.Remove (Key, this);
+                Application.KeyBindings.Remove (Key);
                 Application.KeyBindings.Add (Key, this, Command.HotKey);
             }
             else
             {
                 if (oldKey != Key.Empty)
                 {
-                    KeyBindings.Remove (oldKey);
+                    HotKeyBindings.Remove (oldKey);
                 }
 
-                KeyBindings.Remove (Key);
-                KeyBindings.Add (Key, KeyBindingScope | KeyBindingScope.HotKey, Command.HotKey);
+                HotKeyBindings.Remove (Key);
+                HotKeyBindings.Add (Key,  Command.HotKey);
             }
         }
     }
@@ -766,7 +768,7 @@ public class Shortcut : View, IOrientation, IDesignable
             if (_nonFocusColorScheme is { })
             {
                 base.ColorScheme = _nonFocusColorScheme;
-               //_nonFocusColorScheme = null;
+                //_nonFocusColorScheme = null;
             }
             else
             {

+ 3 - 3
Terminal.Gui/Views/Slider.cs

@@ -850,7 +850,7 @@ public class Slider<T> : View, IOrientation
 
         if (IsInitialized)
         {
-            normalAttr = GetNormalColor();
+            normalAttr = GetNormalColor ();
             setAttr = Style.SetChar.Attribute ?? GetHotNormalColor ();
         }
 
@@ -1785,11 +1785,11 @@ public class Slider<T> : View, IOrientation
         return SetFocusedOption ();
     }
 
-    internal bool Accept (CommandContext ctx)
+    internal bool Accept (ICommandContext commandContext)
     {
         SetFocusedOption ();
 
-        return RaiseAccepting (ctx) == true;
+        return RaiseAccepting (commandContext) == true;
     }
 
     internal bool MovePlus ()

+ 2 - 2
Terminal.Gui/Views/TableView/TableView.cs

@@ -293,7 +293,7 @@ public class TableView : View, IDesignable
             {
                 if (KeyBindings.TryGet (cellActivationKey, out _))
                 {
-                    KeyBindings.ReplaceKey (cellActivationKey, value);
+                    KeyBindings.Replace (cellActivationKey, value);
                 }
                 else
                 {
@@ -1013,7 +1013,7 @@ public class TableView : View, IDesignable
         if (CollectionNavigator != null
             && HasFocus
             && Table.Rows != 0
-            && key != KeyBindings.GetKeyFromCommands (Command.Accept)
+            && key != KeyBindings.GetFirstFromCommands (Command.Accept)
             && key != CellActivationKey
             && CollectionNavigatorBase.IsCompatibleKey (key)
             && !key.KeyCode.HasFlag (KeyCode.CtrlMask)

+ 9 - 9
Terminal.Gui/Views/TextField.cs

@@ -402,7 +402,7 @@ public class TextField : View
         ContextMenu = new () { Host = this };
         ContextMenu.KeyChanged += ContextMenu_KeyChanged;
 
-        KeyBindings.Add (ContextMenu.Key, KeyBindingScope.HotKey, Command.Context);
+        KeyBindings.Add (ContextMenu.Key, Command.Context);
 
         KeyBindings.Remove (Key.Space);
     }
@@ -1236,7 +1236,7 @@ public class TextField : View
                              () => SelectAll (),
                              null,
                              null,
-                             (KeyCode)KeyBindings.GetKeyFromCommands (Command.SelectAll)
+                             (KeyCode)KeyBindings.GetFirstFromCommands (Command.SelectAll)
                             ),
                         new (
                              Strings.ctxDeleteAll,
@@ -1244,7 +1244,7 @@ public class TextField : View
                              () => DeleteAll (),
                              null,
                              null,
-                             (KeyCode)KeyBindings.GetKeyFromCommands (Command.DeleteAll)
+                             (KeyCode)KeyBindings.GetFirstFromCommands (Command.DeleteAll)
                             ),
                         new (
                              Strings.ctxCopy,
@@ -1252,7 +1252,7 @@ public class TextField : View
                              () => Copy (),
                              null,
                              null,
-                             (KeyCode)KeyBindings.GetKeyFromCommands (Command.Copy)
+                             (KeyCode)KeyBindings.GetFirstFromCommands (Command.Copy)
                             ),
                         new (
                              Strings.ctxCut,
@@ -1260,7 +1260,7 @@ public class TextField : View
                              () => Cut (),
                              null,
                              null,
-                             (KeyCode)KeyBindings.GetKeyFromCommands (Command.Cut)
+                             (KeyCode)KeyBindings.GetFirstFromCommands (Command.Cut)
                             ),
                         new (
                              Strings.ctxPaste,
@@ -1268,7 +1268,7 @@ public class TextField : View
                              () => Paste (),
                              null,
                              null,
-                             (KeyCode)KeyBindings.GetKeyFromCommands (Command.Paste)
+                             (KeyCode)KeyBindings.GetFirstFromCommands (Command.Paste)
                             ),
                         new (
                              Strings.ctxUndo,
@@ -1276,7 +1276,7 @@ public class TextField : View
                              () => Undo (),
                              null,
                              null,
-                             (KeyCode)KeyBindings.GetKeyFromCommands (Command.Undo)
+                             (KeyCode)KeyBindings.GetFirstFromCommands (Command.Undo)
                             ),
                         new (
                              Strings.ctxRedo,
@@ -1284,13 +1284,13 @@ public class TextField : View
                              () => Redo (),
                              null,
                              null,
-                             (KeyCode)KeyBindings.GetKeyFromCommands (Command.Redo)
+                             (KeyCode)KeyBindings.GetFirstFromCommands (Command.Redo)
                             )
                     }
                    );
     }
 
-    private void ContextMenu_KeyChanged (object sender, KeyChangedEventArgs e) { KeyBindings.ReplaceKey (e.OldKey.KeyCode, e.NewKey.KeyCode); }
+    private void ContextMenu_KeyChanged (object sender, KeyChangedEventArgs e) { KeyBindings.Replace (e.OldKey.KeyCode, e.NewKey.KeyCode); }
 
     private List<Rune> DeleteSelectedText ()
     {

+ 27 - 27
Terminal.Gui/Views/TextView.cs

@@ -2413,7 +2413,7 @@ public class TextView : View
         ContextMenu = new ();
         ContextMenu.KeyChanged += ContextMenu_KeyChanged!;
 
-        KeyBindings.Add ((KeyCode)ContextMenu.Key, KeyBindingScope.HotKey, Command.Context);
+        KeyBindings.Add (ContextMenu.Key, Command.Context);
     }
 
     private void TextView_Added1 (object? sender, SuperViewChangedEventArgs e) { throw new NotImplementedException (); }
@@ -3710,8 +3710,8 @@ public class TextView : View
     /// <summary>Invoke the <see cref="UnwrappedCursorPosition"/> event with the unwrapped <see cref="CursorPosition"/>.</summary>
     public virtual void OnUnwrappedCursorPosition (int? cRow = null, int? cCol = null)
     {
-        int? row = cRow is null ? CurrentRow : cRow;
-        int? col = cCol is null ? CurrentColumn : cCol;
+        int? row = cRow ?? CurrentRow;
+        int? col = cCol ?? CurrentColumn;
 
         if (cRow is null && cCol is null && _wordWrap)
         {
@@ -3719,7 +3719,7 @@ public class TextView : View
             col = _wrapManager.GetModelColFromWrappedLines (CurrentRow, CurrentColumn);
         }
 
-        UnwrappedCursorPosition?.Invoke (this, new (new ((int)col, (int)row)));
+        UnwrappedCursorPosition?.Invoke (this, new Point (col.Value, row.Value));
     }
 
     /// <summary>Paste the clipboard contents into the current selected position.</summary>
@@ -3956,7 +3956,7 @@ public class TextView : View
     }
 
     /// <summary>Invoked with the unwrapped <see cref="CursorPosition"/>.</summary>
-    public event EventHandler<PointEventArgs>? UnwrappedCursorPosition;
+    public event EventHandler<Point>? UnwrappedCursorPosition;
 
     /// <summary>
     ///     Sets the <see cref="View.Driver"/> to an appropriate color for rendering the given <paramref name="idxCol"/>
@@ -4163,7 +4163,7 @@ public class TextView : View
                              SelectAll,
                              null,
                              null,
-                             (KeyCode)KeyBindings.GetKeyFromCommands (Command.SelectAll)
+                             (KeyCode)KeyBindings.GetFirstFromCommands (Command.SelectAll)
                             ),
                         new (
                              Strings.ctxDeleteAll,
@@ -4171,7 +4171,7 @@ public class TextView : View
                              DeleteAll,
                              null,
                              null,
-                             (KeyCode)KeyBindings.GetKeyFromCommands (Command.DeleteAll)
+                             (KeyCode)KeyBindings.GetFirstFromCommands (Command.DeleteAll)
                             ),
                         new (
                              Strings.ctxCopy,
@@ -4179,7 +4179,7 @@ public class TextView : View
                              Copy,
                              null,
                              null,
-                             (KeyCode)KeyBindings.GetKeyFromCommands (Command.Copy)
+                             (KeyCode)KeyBindings.GetFirstFromCommands (Command.Copy)
                             ),
                         new (
                              Strings.ctxCut,
@@ -4187,7 +4187,7 @@ public class TextView : View
                              Cut,
                              null,
                              null,
-                             (KeyCode)KeyBindings.GetKeyFromCommands (Command.Cut)
+                             (KeyCode)KeyBindings.GetFirstFromCommands (Command.Cut)
                             ),
                         new (
                              Strings.ctxPaste,
@@ -4195,7 +4195,7 @@ public class TextView : View
                              Paste,
                              null,
                              null,
-                             (KeyCode)KeyBindings.GetKeyFromCommands (Command.Paste)
+                             (KeyCode)KeyBindings.GetFirstFromCommands (Command.Paste)
                             ),
                         new (
                              Strings.ctxUndo,
@@ -4203,7 +4203,7 @@ public class TextView : View
                              Undo,
                              null,
                              null,
-                             (KeyCode)KeyBindings.GetKeyFromCommands (Command.Undo)
+                             (KeyCode)KeyBindings.GetFirstFromCommands (Command.Undo)
                             ),
                         new (
                              Strings.ctxRedo,
@@ -4211,7 +4211,7 @@ public class TextView : View
                              Redo,
                              null,
                              null,
-                             (KeyCode)KeyBindings.GetKeyFromCommands (Command.Redo)
+                             (KeyCode)KeyBindings.GetFirstFromCommands (Command.Redo)
                             ),
                         new (
                              Strings.ctxColors,
@@ -4219,7 +4219,7 @@ public class TextView : View
                              () => PromptForColors (),
                              null,
                              null,
-                             (KeyCode)KeyBindings.GetKeyFromCommands (Command.Open)
+                             (KeyCode)KeyBindings.GetFirstFromCommands (Command.Open)
                             )
                     }
                    );
@@ -4333,7 +4333,7 @@ public class TextView : View
         DoNeededAction ();
     }
 
-    private void ContextMenu_KeyChanged (object sender, KeyChangedEventArgs e) { KeyBindings.ReplaceKey (e.OldKey, e.NewKey); }
+    private void ContextMenu_KeyChanged (object sender, KeyChangedEventArgs e) { KeyBindings.Replace (e.OldKey, e.NewKey); }
 
     private bool DeleteTextBackwards ()
     {
@@ -4465,12 +4465,12 @@ public class TextView : View
         }
         else
         {
-            _historyText.Add ([[.. currentLine]], CursorPosition);
+            _historyText.Add ([ [.. currentLine]], CursorPosition);
 
             currentLine.RemoveAt (CurrentColumn);
 
             _historyText.Add (
-                              [[.. currentLine]],
+                              [ [.. currentLine]],
                               CursorPosition,
                               HistoryText.LineStatus.Replaced
                              );
@@ -5057,7 +5057,7 @@ public class TextView : View
         }
 
         _historyText.Add (
-                          [[.. GetCurrentLine ()]],
+                          [ [.. GetCurrentLine ()]],
                           CursorPosition,
                           HistoryText.LineStatus.Replaced
                          );
@@ -5097,7 +5097,7 @@ public class TextView : View
             return;
         }
 
-        _historyText.Add ([[.. currentLine]], CursorPosition);
+        _historyText.Add ([ [.. currentLine]], CursorPosition);
 
         if (currentLine.Count == 0)
         {
@@ -5164,7 +5164,7 @@ public class TextView : View
         }
 
         _historyText.Add (
-                          [[.. GetCurrentLine ()]],
+                          [ [.. GetCurrentLine ()]],
                           CursorPosition,
                           HistoryText.LineStatus.Replaced
                          );
@@ -5188,14 +5188,14 @@ public class TextView : View
 
         List<Cell> currentLine = GetCurrentLine ();
 
-        _historyText.Add ([[.. GetCurrentLine ()]], CursorPosition);
+        _historyText.Add ([ [.. GetCurrentLine ()]], CursorPosition);
 
         if (CurrentColumn == 0)
         {
             DeleteTextBackwards ();
 
             _historyText.ReplaceLast (
-                                      [[.. GetCurrentLine ()]],
+                                      [ [.. GetCurrentLine ()]],
                                       CursorPosition,
                                       HistoryText.LineStatus.Replaced
                                      );
@@ -5234,7 +5234,7 @@ public class TextView : View
         }
 
         _historyText.Add (
-                          [[.. GetCurrentLine ()]],
+                          [ [.. GetCurrentLine ()]],
                           CursorPosition,
                           HistoryText.LineStatus.Replaced
                          );
@@ -5256,14 +5256,14 @@ public class TextView : View
 
         List<Cell> currentLine = GetCurrentLine ();
 
-        _historyText.Add ([[.. GetCurrentLine ()]], CursorPosition);
+        _historyText.Add ([ [.. GetCurrentLine ()]], CursorPosition);
 
         if (currentLine.Count == 0 || CurrentColumn == currentLine.Count)
         {
             DeleteTextForwards ();
 
             _historyText.ReplaceLast (
-                                      [[.. GetCurrentLine ()]],
+                                      [ [.. GetCurrentLine ()]],
                                       CursorPosition,
                                       HistoryText.LineStatus.Replaced
                                      );
@@ -5293,7 +5293,7 @@ public class TextView : View
         }
 
         _historyText.Add (
-                          [[.. GetCurrentLine ()]],
+                          [ [.. GetCurrentLine ()]],
                           CursorPosition,
                           HistoryText.LineStatus.Replaced
                          );
@@ -6143,7 +6143,7 @@ public class TextView : View
         Paste ();
     }
 
-    private bool ProcessEnterKey (CommandContext ctx)
+    private bool ProcessEnterKey (ICommandContext? commandContext)
     {
         ResetColumnTrack ();
 
@@ -6156,7 +6156,7 @@ public class TextView : View
         {
             // By Default pressing ENTER should be ignored (OnAccept will return false or null). Only cancel if the
             // event was fired and set Cancel = true.
-            return RaiseAccepting (ctx) is null or false;
+            return RaiseAccepting (commandContext) is null or false;
         }
 
         SetWrapModel ();

+ 3 - 3
Terminal.Gui/Views/TreeView/TreeView.cs

@@ -375,7 +375,7 @@ public class TreeView<T> : View, ITreeView where T : class
         {
             if (objectActivationKey != value)
             {
-                KeyBindings.ReplaceKey (ObjectActivationKey, value);
+                KeyBindings.Replace (ObjectActivationKey, value);
                 objectActivationKey = value;
                 SetNeedsDraw ();
             }
@@ -462,10 +462,10 @@ public class TreeView<T> : View, ITreeView where T : class
     ///     <para>This method also ensures that the selected object is visible.</para>
     /// </summary>
     /// <returns><see langword="true"/> if <see cref="ObjectActivated"/> was fired.</returns>
-    public bool? ActivateSelectedObjectIfAny (CommandContext ctx)
+    public bool? ActivateSelectedObjectIfAny (ICommandContext commandContext)
     {
         // By default, Command.Accept calls OnAccept, so we need to call it here to ensure that the event is fired.
-        if (RaiseAccepting (ctx) == true)
+        if (RaiseAccepting (commandContext) == true)
         {
             return true;
         }

+ 1 - 1
UICatalog/Scenario.cs

@@ -219,7 +219,7 @@ public class Scenario : IDisposable
     private void OnApplicationOnIteration (object? s, IterationEventArgs a)
     {
         BenchmarkResults.IterationCount++;
-        if (BenchmarkResults.IterationCount > BENCHMARK_MAX_NATURAL_ITERATIONS + (_demoKeys.Count * BENCHMARK_KEY_PACING))
+        if (BenchmarkResults.IterationCount > BENCHMARK_MAX_NATURAL_ITERATIONS + (_demoKeys!.Count * BENCHMARK_KEY_PACING))
         {
             Application.RequestStop ();
         }

+ 1 - 1
UICatalog/Scenarios/Arrangement.cs

@@ -134,7 +134,7 @@ public class Arrangement : Scenario
                        {
                            Title = "Toggle Hide",
                            Text = "App",
-                           KeyBindingScope = KeyBindingScope.Application,
+                           BindKeyToApplication = true,
                            Key = Key.F4.WithCtrl,
                            Action = () =>
                                     {

+ 1 - 1
UICatalog/Scenarios/Bars.cs

@@ -164,7 +164,7 @@ public class Bars : Scenario
         {
             Title = "Toggle Hide",
             Text = "App",
-            KeyBindingScope = KeyBindingScope.Application,
+            BindKeyToApplication = true,
             Key = Key.F4.WithCtrl,
         };
         popOverMenu.Add (toggleShortcut);

+ 2 - 2
UICatalog/Scenarios/Editor.cs

@@ -265,7 +265,7 @@ public class Editor : Scenario
         _textView.VerticalScrollBar.AutoShow = false;
         _textView.UnwrappedCursorPosition += (s, e) =>
                                              {
-                                                 siCursorPosition.Title = $"Ln {e.Point.Y + 1}, Col {e.Point.X + 1}";
+                                                 siCursorPosition.Title = $"Ln {e.Y + 1}, Col {e.X + 1}";
                                              };
 
         _appWindow.Add (statusBar);
@@ -699,7 +699,7 @@ public class Editor : Scenario
             Height = 11;
             Arrangement = ViewArrangement.Movable;
 
-            KeyBindings.Add (Key.Esc, KeyBindingScope.Focused, Command.Cancel);
+            KeyBindings.Add (Key.Esc, Command.Cancel);
             AddCommand (Command.Cancel, () =>
                                         {
                                             Visible = false;

+ 23 - 32
UICatalog/Scenarios/Editors/EventLog.cs

@@ -1,8 +1,6 @@
 #nullable enable
 using System;
 using System.Collections.ObjectModel;
-using System.Diagnostics.Tracing;
-using System.Text;
 using Terminal.Gui;
 
 namespace UICatalog.Scenarios;
@@ -22,12 +20,15 @@ public class EventLog : ListView
 
         X = Pos.AnchorEnd ();
         Y = 0;
-        Width = Dim.Func (() =>
+
+        Width = Dim.Func (
+                          () =>
                           {
                               if (!IsInitialized)
                               {
                                   return 0;
                               }
+
                               return Math.Min (SuperView!.Viewport.Width / 3, MaxLength + GetAdornmentsThickness ().Horizontal);
                           });
         Height = Dim.Fill ();
@@ -42,17 +43,18 @@ public class EventLog : ListView
         HorizontalScrollBar.AutoShow = true;
         VerticalScrollBar.AutoShow = true;
 
-        AddCommand (Command.DeleteAll,
-                   () =>
-                   {
-                       _eventSource.Clear ();
+        AddCommand (
+                    Command.DeleteAll,
+                    () =>
+                    {
+                        _eventSource.Clear ();
 
-                       return true;
-                   });
+                        return true;
+                    });
 
         KeyBindings.Add (Key.Delete, Command.DeleteAll);
-
     }
+
     public ExpanderButton? ExpandButton { get; }
 
     private readonly ObservableCollection<string> _eventSource = [];
@@ -74,28 +76,16 @@ public class EventLog : ListView
             if (_viewToLog is { })
             {
                 _viewToLog.Initialized += (s, args) =>
-                                             {
-                                                 View? sender = s as View;
-                                                 Log ($"Initialized: {GetIdentifyingString (sender)}");
-                                             };
-
-                _viewToLog.MouseClick += (s, args) =>
-                {
-                    Log ($"MouseClick: {args}");
-                };
-
-                _viewToLog.HandlingHotKey += (s, args) =>
-                                        {
-                                            Log ($"HandlingHotKey: {args.Context.Command} {args.Context.Data}");
-                                        };
-                _viewToLog.Selecting += (s, args) =>
-                                        {
-                                            Log ($"Selecting: {args.Context.Command} {args.Context.Data}");
-                                        };
-                _viewToLog.Accepting += (s, args) =>
-                                        {
-                                            Log ($"Accepting: {args.Context.Command} {args.Context.Data}");
-                                        };
+                                          {
+                                              var sender = s as View;
+                                              Log ($"Initialized: {GetIdentifyingString (sender)}");
+                                          };
+
+                _viewToLog.MouseClick += (s, args) => { Log ($"MouseClick: {args}"); };
+                _viewToLog.MouseWheel += (s, args) => { Log ($"MouseWheel: {args}"); };
+                _viewToLog.HandlingHotKey += (s, args) => { Log ($"HandlingHotKey: {args.Context}"); };
+                _viewToLog.Selecting += (s, args) => { Log ($"Selecting: {args.Context}"); };
+                _viewToLog.Accepting += (s, args) => { Log ($"Accepting: {args.Context}"); };
             }
         }
     }
@@ -111,6 +101,7 @@ public class EventLog : ListView
         Border?.Add (ExpandButton!);
         Source = new ListWrapper<string> (_eventSource);
     }
+
     private string GetIdentifyingString (View? view)
     {
         if (view is null)

+ 14 - 10
UICatalog/Scenarios/KeyBindings.cs

@@ -80,10 +80,10 @@ public sealed class KeyBindings : Scenario
         };
         appWindow.Add (appBindingsListView);
 
-        foreach (var key in Application.KeyBindings.GetBoundKeys())
+        foreach (Key key in Application.KeyBindings.GetBindings().ToDictionary().Keys)
         {
             var binding = Application.KeyBindings.Get (key);
-            appBindings.Add ($"{key} -> {binding.BoundView?.GetType ().Name} - {binding.Commands [0]}");
+            appBindings.Add ($"{key} -> {binding.Target?.GetType ().Name} - {binding.Commands [0]}");
         }
 
         ObservableCollection<string> hotkeyBindings = new ();
@@ -104,7 +104,7 @@ public sealed class KeyBindings : Scenario
 
         foreach (var subview in appWindow.Subviews)
         {
-            foreach (var binding in subview.KeyBindings.Bindings.Where (b => b.Value.Scope == KeyBindingScope.HotKey))
+            foreach (KeyValuePair<Key, KeyBinding> binding in subview.HotKeyBindings.GetBindings ())
             {
                 hotkeyBindings.Add ($"{binding.Key} -> {subview.GetType ().Name} - {binding.Value.Commands [0]}");
             }
@@ -148,8 +148,8 @@ public sealed class KeyBindings : Scenario
 
         _focusedBindingsListView.Title = $"_Focused ({focused?.GetType ().Name}) Bindings";
 
-        _focusedBindings.Clear();
-        foreach (var binding in focused?.KeyBindings!.Bindings.Where (b => b.Value.Scope == KeyBindingScope.Focused)!)
+        _focusedBindings.Clear ();
+        foreach (var binding in focused?.KeyBindings!.GetBindings ())
         {
             _focusedBindings.Add ($"{binding.Key} -> {binding.Value.Commands [0]}");
         }
@@ -165,28 +165,32 @@ public class KeyBindingsDemo : View
 
         AddCommand (Command.Save, ctx =>
                                  {
-                                     MessageBox.Query ($"{ctx.KeyBinding?.Scope}", $"Key: {ctx.Key}\nCommand: {ctx.Command}", buttons: "Ok");
+                                     MessageBox.Query ($"{ctx.Command}", $"Ctx: {ctx}", buttons: "Ok");
                                      return true;
                                  });
         AddCommand (Command.New, ctx =>
                                 {
-                                    MessageBox.Query ($"{ctx.KeyBinding?.Scope}", $"Key: {ctx.Key}\nCommand: {ctx.Command}", buttons: "Ok");
+                                    MessageBox.Query ($"{ctx.Command}", $"Ctx: {ctx}", buttons: "Ok");
                                     return true;
                                 });
         AddCommand (Command.HotKey, ctx =>
         {
-            MessageBox.Query ($"{ctx.KeyBinding?.Scope}", $"Key: {ctx.Key}\nCommand: {ctx.Command}", buttons: "Ok");
+            MessageBox.Query ($"{ctx.Command}", $"Ctx: {ctx}\nCommand: {ctx.Command}", buttons: "Ok");
             SetFocus ();
             return true;
         });
 
-        KeyBindings.Add (Key.F2, KeyBindingScope.Focused, Command.Save);
+        KeyBindings.Add (Key.F2, Command.Save);
         KeyBindings.Add (Key.F3, Command.New); // same as specifying KeyBindingScope.Focused
         Application.KeyBindings.Add (Key.F4, this, Command.New);
 
         AddCommand (Command.Quit, ctx =>
                                          {
-                                             MessageBox.Query ($"{ctx.KeyBinding?.Scope}", $"Key: {ctx.Key}\nCommand: {ctx.Command}", buttons: "Ok");
+                                             if (ctx is not CommandContext<KeyBinding> keyCommandContext)
+                                             {
+                                                 return false;
+                                             }
+                                             MessageBox.Query ($"{keyCommandContext.Binding}", $"Key: {keyCommandContext.Binding.Key}\nCommand: {ctx.Command}", buttons: "Ok");
                                              Application.RequestStop ();
                                              return true;
                                          });

+ 3 - 1
UICatalog/Scenarios/MessageBoxes.cs

@@ -185,7 +185,9 @@ public class MessageBoxes : Scenario
 
         var styleRadioGroup = new RadioGroup
         {
-            X = Pos.Right (label) + 1, Y = Pos.Top (label), RadioLabels = new [] { "_Query", "_Error" }
+            X = Pos.Right (label) + 1, 
+            Y = Pos.Top (label), 
+            RadioLabels = ["_Query", "_Error"],
         };
         frame.Add (styleRadioGroup);
 

+ 1 - 1
UICatalog/Scenarios/Navigation.cs

@@ -150,7 +150,7 @@ public class Navigation : Scenario
                        {
                            Title = "Toggle Hide",
                            Text = "App",
-                           KeyBindingScope = KeyBindingScope.Application,
+                           BindKeyToApplication = true,
                            Key = Key.F4.WithCtrl,
                            Action = () =>
                                     {

+ 2 - 8
UICatalog/Scenarios/Shortcuts.cs

@@ -63,7 +63,6 @@ public class Shortcuts : Scenario
                 HighlightStyle = HighlightStyle.None,
             },
             Key = Key.F5.WithCtrl.WithAlt.WithShift,
-            KeyBindingScope = KeyBindingScope.HotKey,
         };
 
         // ((CheckBox)vShortcut3.CommandView).CheckedStateChanging += (_, args) =>
@@ -111,7 +110,6 @@ public class Shortcuts : Scenario
                 HighlightStyle = HighlightStyle.None,
             },
             Key = Key.F.WithCtrl,
-            KeyBindingScope = KeyBindingScope.HotKey,
         };
         ((CheckBox)commandFirstShortcut.CommandView).CheckedState =
             commandFirstShortcut.AlignmentModes.HasFlag (AlignmentModes.EndToStart) ? CheckState.UnChecked : CheckState.Checked;
@@ -151,7 +149,6 @@ public class Shortcuts : Scenario
             Width = Dim.Fill ()! - Dim.Width (eventLog),
             Key = Key.F4,
             HelpText = "Changes all Command.CanFocus",
-            KeyBindingScope = KeyBindingScope.HotKey,
             CommandView = new CheckBox { Text = "_CanFocus" },
         };
 
@@ -183,7 +180,7 @@ public class Shortcuts : Scenario
             Title = "A_pp Shortcut",
             Key = Key.F1,
             Text = "Width is DimFill",
-            KeyBindingScope = KeyBindingScope.Application,
+            BindKeyToApplication = true,
         };
 
         Application.Top.Add (appShortcut);
@@ -203,7 +200,6 @@ public class Shortcuts : Scenario
                 HighlightStyle = HighlightStyle.None
             },
             Key = Key.K,
-            KeyBindingScope = KeyBindingScope.HotKey,
         };
         var button = (Button)buttonShortcut.CommandView;
         buttonShortcut.Accepting += Button_Clicked;
@@ -218,7 +214,6 @@ public class Shortcuts : Scenario
             Y = Pos.Bottom (buttonShortcut),
             Key = Key.F2,
             Width = Dim.Fill ()! - Dim.Width (eventLog),
-            KeyBindingScope = KeyBindingScope.HotKey,
             CommandView = new RadioGroup
             {
                 Orientation = Orientation.Vertical,
@@ -244,7 +239,6 @@ public class Shortcuts : Scenario
             X = 0,
             Y = Pos.Bottom (radioGroupShortcut),
             Width = Dim.Fill ()! - Dim.Width (eventLog),
-            KeyBindingScope = KeyBindingScope.HotKey,
             HelpText = "Sliders work!",
             CommandView = new Slider<string>
             {
@@ -457,7 +451,7 @@ public class Shortcuts : Scenario
             X = Pos.Align (Alignment.Start, AlignmentModes.IgnoreFirstOrLast, 1),
             Y = Pos.AnchorEnd () - 1,
             Key = Key.Esc,
-            KeyBindingScope = KeyBindingScope.Application,
+            BindKeyToApplication = true,
             Title = "Quit",
             HelpText = "App Scope",
         };

+ 2 - 2
UICatalog/Scenarios/TableEditor.cs

@@ -65,8 +65,8 @@ public class TableEditor : Scenario
              "Cuneiform Numbers and Punctuation"
             ),
         new (
-             (uint)(UICatalog.Scenarios.UnicodeRange.Ranges.Max (r => r.End) - 16),
-             (uint)UICatalog.Scenarios.UnicodeRange.Ranges.Max (r => r.End),
+             (uint)(Terminal.Gui.UnicodeRange.Ranges.Max (r => r.End) - 16),
+             (uint)Terminal.Gui.UnicodeRange.Ranges.Max (r => r.End),
              "End"
             ),
         new (0x0020, 0x007F, "Basic Latin"),

+ 2 - 2
UICatalog/Scenarios/Text.cs

@@ -148,8 +148,8 @@ public class Text : Scenario
                                     }
                                 };
 
-        Key keyTab = textView.KeyBindings.GetKeyFromCommands (Command.Tab);
-        Key keyBackTab = textView.KeyBindings.GetKeyFromCommands (Command.BackTab);
+        Key keyTab = textView.KeyBindings.GetFirstFromCommands (Command.Tab);
+        Key keyBackTab = textView.KeyBindings.GetFirstFromCommands (Command.BackTab);
 
         chxCaptureTabs.CheckedStateChanging += (s, e) =>
                                   {

+ 1 - 1
UICatalog/UICatalog.cs

@@ -761,7 +761,7 @@ public class UICatalogApp
                     CanFocus = false
                 },
                 HelpText = "",
-                KeyBindingScope = KeyBindingScope.Application,
+                BindKeyToApplication = true,
                 Key = Key.F7
             };
 

+ 2 - 5
UnitTests/Application/ApplicationTests.cs

@@ -317,9 +317,6 @@ public class ApplicationTests
             Assert.Empty (Application.TopLevels);
             Assert.Empty (Application._cachedViewsUnderMouse);
 
-            // Keyboard
-            Assert.Empty (Application.GetViewKeyBindings ());
-
             // Mouse
             Assert.Null (Application._lastMousePosition);
 
@@ -358,7 +355,7 @@ public class ApplicationTests
         Application.PrevTabGroupKey = Key.A;
         Application.NextTabGroupKey = Key.B;
         Application.QuitKey = Key.C;
-        Application.KeyBindings.Add (Key.D, KeyBindingScope.Application, Command.Cancel);
+        Application.KeyBindings.Add (Key.D, Command.Cancel);
 
         Application._cachedViewsUnderMouse.Clear ();
 
@@ -555,7 +552,7 @@ public class ApplicationTests
 
         Assert.Equal (Key.Q.WithCtrl, Application.QuitKey);
 
-        Assert.Contains (Key.Q.WithCtrl, Application.KeyBindings.Bindings);
+        Assert.True (Application.KeyBindings.TryGet (Key.Q.WithCtrl, out _));
 
         Application.Shutdown ();
         Locations = ConfigLocations.Default;

+ 11 - 40
UnitTests/Application/KeyboardTests.cs

@@ -135,22 +135,22 @@ public class KeyboardTests
 
     [Fact]
     [AutoInitShutdown]
-    public void KeyBinding_Application_KeyBindings_Add_Adds ()
+    public void KeyBindings_Add_Adds ()
     {
-        Application.KeyBindings.Add (Key.A, KeyBindingScope.Application, Command.Accept);
-        Application.KeyBindings.Add (Key.B, KeyBindingScope.Application, Command.Accept);
+        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.BoundView);
+        Assert.Null (binding.Target);
         Assert.True (Application.KeyBindings.TryGet (Key.B, out binding));
-        Assert.Null (binding.BoundView);
+        Assert.Null (binding.Target);
     }
-
+            
     [Fact]
     [AutoInitShutdown]
-    public void KeyBinding_Application_RemoveKeyBinding_Removes ()
+    public void KeyBindings_Remove_Removes ()
     {
-        Application.KeyBindings.Add (Key.A, KeyBindingScope.Application, Command.Accept);
+        Application.KeyBindings.Add (Key.A, Command.Accept);
 
         Assert.True (Application.KeyBindings.TryGet (Key.A, out _));
 
@@ -159,7 +159,7 @@ public class KeyboardTests
     }
 
     [Fact]
-    public void KeyBinding_OnKeyDown ()
+    public void KeyBindings_OnKeyDown ()
     {
         Application.Top = new Toplevel ();
         var view = new ScopedKeyBindingView ();
@@ -189,6 +189,7 @@ public class KeyboardTests
         keyWasHandled = false;
         Application.RaiseKeyDownEvent (Key.H);
         Assert.False (keyWasHandled);
+        Assert.True (view.HotKeyCommand);
 
         keyWasHandled = false;
         Assert.False (view.HasFocus);
@@ -204,7 +205,7 @@ public class KeyboardTests
 
     [Fact]
     [AutoInitShutdown]
-    public void KeyBinding_OnKeyDown_Negative ()
+    public void KeyBindings_OnKeyDown_Negative ()
     {
         var view = new ScopedKeyBindingView ();
         var keyWasHandled = false;
@@ -230,36 +231,6 @@ public class KeyboardTests
         top.Dispose ();
     }
 
-    [Fact]
-    [AutoInitShutdown]
-    public void KeyBinding_View_KeyBindings_Add_Adds ()
-    {
-        View view1 = new ();
-        Application.KeyBindings.Add (Key.A, view1, Command.Accept);
-
-        View view2 = new ();
-        Application.KeyBindings.Add (Key.B, view2, Command.Accept);
-
-        Assert.True (Application.KeyBindings.TryGet (Key.A, out KeyBinding binding));
-        Assert.Equal (view1, binding.BoundView);
-        Assert.True (Application.KeyBindings.TryGet (Key.B, out binding));
-        Assert.Equal (view2, binding.BoundView);
-    }
-
-    [Fact]
-    [AutoInitShutdown]
-    public void KeyBinding_View_KeyBindings_RemoveKeyBinding_Removes ()
-    {
-        View view1 = new ();
-        Application.KeyBindings.Add (Key.A, view1, Command.Accept);
-
-        View view2 = new ();
-        Application.KeyBindings.Add (Key.B, view1, Command.Accept);
-
-        Application.KeyBindings.Remove (Key.A, view1);
-        Assert.False (Application.KeyBindings.TryGet (Key.A, out _));
-    }
-
     [Fact]
     public void KeyUp_Event ()
     {

+ 0 - 2
UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs

@@ -438,8 +438,6 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
     [Fact]
     public void UnknownResponses_ParameterShouldMatch ()
     {
-        int i = 0;
-
         // Track unknown responses passed to the UnexpectedResponseHandler
         var unknownResponses = new List<string> ();
 

+ 10 - 0
UnitTests/Input/Keyboard/KeyBindingTests.cs

@@ -0,0 +1,10 @@
+using Terminal.Gui.EnumExtensions;
+using Xunit.Abstractions;
+
+namespace Terminal.Gui.InputTests;
+
+public class KeyBindingTests ()
+{
+    // TODO: Add tests for KeyBinding
+
+}

+ 121 - 155
UnitTests/Input/KeyBindingTests.cs → UnitTests/Input/Keyboard/KeyBindingsTests.cs

@@ -1,41 +1,52 @@
 using Terminal.Gui.EnumExtensions;
 using Xunit.Abstractions;
+using static Unix.Terminal.Delegates;
 
 namespace Terminal.Gui.InputTests;
 
-public class KeyBindingTests
+public class KeyBindingsTests ()
 {
-    public KeyBindingTests (ITestOutputHelper output) { _output = output; }
-    private readonly ITestOutputHelper _output;
-
     [Fact]
-    public void Add_Invalid_Key_Throws ()
+    public void Add_Adds ()
     {
-        var keyBindings = new KeyBindings (new View ());
-        List<Command> commands = new ();
-        Assert.Throws<ArgumentException> (() => keyBindings.Add (Key.Empty, KeyBindingScope.HotKey, Command.Accept));
+        var keyBindings = new KeyBindings (new ());
+        Command [] commands = { Command.Right, Command.Left };
+
+        var key = new Key (Key.A);
+        keyBindings.Add (Key.A, commands);
+        KeyBinding binding = keyBindings.Get (key);
+        Assert.Contains (Command.Right, binding.Commands);
+        Assert.Contains (Command.Left, binding.Commands);
+
+        binding = keyBindings.Get (key);
+        Assert.Contains (Command.Right, binding.Commands);
+        Assert.Contains (Command.Left, binding.Commands);
+
+        Command [] resultCommands = keyBindings.GetCommands (key);
+        Assert.Contains (Command.Right, resultCommands);
+        Assert.Contains (Command.Left, resultCommands);
     }
 
     [Fact]
-    public void Add_BoundView_Null_Non_AppScope_Throws ()
+    public void Add_Invalid_Key_Throws ()
     {
-        var keyBindings = new KeyBindings ();
+        var keyBindings = new KeyBindings (new View ());
         List<Command> commands = new ();
-        Assert.Throws<InvalidOperationException> (() => keyBindings.Add (Key.Empty, KeyBindingScope.HotKey, Command.Accept));
+        Assert.Throws<ArgumentException> (() => keyBindings.Add (Key.Empty, Command.Accept));
     }
 
     [Fact]
-    public void Add_Multiple_Adds ()
+    public void Add_Multiple_Commands_Adds ()
     {
-        var keyBindings = new KeyBindings ();
-        Command [] commands = { Command.Right, Command.Left };
+        var keyBindings = new KeyBindings (new ());
+        Command [] commands = [Command.Right, Command.Left];
 
-        keyBindings.Add (Key.A, KeyBindingScope.Application, commands);
+        keyBindings.Add (Key.A, commands);
         Command [] resultCommands = keyBindings.GetCommands (Key.A);
         Assert.Contains (Command.Right, resultCommands);
         Assert.Contains (Command.Left, resultCommands);
 
-        keyBindings.Add (Key.B, KeyBindingScope.Application, commands);
+        keyBindings.Add (Key.B, commands);
         resultCommands = keyBindings.GetCommands (Key.B);
         Assert.Contains (Command.Right, resultCommands);
         Assert.Contains (Command.Left, resultCommands);
@@ -44,20 +55,20 @@ public class KeyBindingTests
     [Fact]
     public void Add_No_Commands_Throws ()
     {
-        var keyBindings = new KeyBindings ();
+        var keyBindings = new KeyBindings (new ());
         List<Command> commands = new ();
         Assert.Throws<ArgumentException> (() => keyBindings.Add (Key.A, commands.ToArray ()));
     }
 
     [Fact]
-    public void Add_Single_Adds ()
+    public void Add_Single_Command_Adds ()
     {
-        var keyBindings = new KeyBindings ();
-        keyBindings.Add (Key.A, KeyBindingScope.Application, Command.HotKey);
+        var keyBindings = new KeyBindings (new ());
+        keyBindings.Add (Key.A, Command.HotKey);
         Command [] resultCommands = keyBindings.GetCommands (Key.A);
         Assert.Contains (Command.HotKey, resultCommands);
 
-        keyBindings.Add (Key.B, KeyBindingScope.Application, Command.HotKey);
+        keyBindings.Add (Key.B, Command.HotKey);
         resultCommands = keyBindings.GetCommands (Key.B);
         Assert.Contains (Command.HotKey, resultCommands);
     }
@@ -65,40 +76,39 @@ public class KeyBindingTests
 
     // Add should not allow duplicates
     [Fact]
-    public void Add_With_Bound_View_Throws_If_App_Scope ()
+    public void Add_Throws_If_Exists ()
     {
         var keyBindings = new KeyBindings (new View ());
-        Assert.Throws<InvalidOperationException> (() => keyBindings.Add (Key.A, KeyBindingScope.Application, Command.Accept));
-    }
-
-    // Add should not allow duplicates
-        [Fact]
-    public void Add_With_Throws_If_Exists ()
-    {
-        var keyBindings = new KeyBindings (new View ());
-        keyBindings.Add (Key.A, KeyBindingScope.HotKey, Command.HotKey);
-        Assert.Throws<InvalidOperationException> (() => keyBindings.Add (Key.A, KeyBindingScope.HotKey, Command.Accept));
+        keyBindings.Add (Key.A, Command.HotKey);
+        Assert.Throws<InvalidOperationException> (() => keyBindings.Add (Key.A, Command.Accept));
 
         Command [] resultCommands = keyBindings.GetCommands (Key.A);
         Assert.Contains (Command.HotKey, resultCommands);
 
         keyBindings = new (new View ());
-        keyBindings.Add (Key.A, KeyBindingScope.Focused, Command.HotKey);
-        Assert.Throws<InvalidOperationException> (() => keyBindings.Add (Key.A, KeyBindingScope.Focused, Command.Accept));
+        keyBindings.Add (Key.A, Command.HotKey);
+        Assert.Throws<InvalidOperationException> (() => keyBindings.Add (Key.A, Command.Accept));
 
         resultCommands = keyBindings.GetCommands (Key.A);
         Assert.Contains (Command.HotKey, resultCommands);
 
         keyBindings = new (new View ());
-        keyBindings.Add (Key.A, KeyBindingScope.HotKey, Command.HotKey);
-        Assert.Throws<InvalidOperationException> (() => keyBindings.Add (Key.A, KeyBindingScope.HotKey, Command.Accept));
+        keyBindings.Add (Key.A, Command.HotKey);
+        Assert.Throws<InvalidOperationException> (() => keyBindings.Add (Key.A, Command.Accept));
 
         resultCommands = keyBindings.GetCommands (Key.A);
         Assert.Contains (Command.HotKey, resultCommands);
 
         keyBindings = new (new View ());
-        keyBindings.Add (Key.A, new KeyBinding (new [] { Command.HotKey }, KeyBindingScope.HotKey));
-        Assert.Throws<InvalidOperationException> (() => keyBindings.Add (Key.A, new KeyBinding (new [] { Command.Accept }, KeyBindingScope.HotKey)));
+        keyBindings.Add (Key.A, Command.Accept);
+        Assert.Throws<InvalidOperationException> (() => keyBindings.Add (Key.A, Command.ScrollDown));
+
+        resultCommands = keyBindings.GetCommands (Key.A);
+        Assert.Contains (Command.Accept, resultCommands);
+
+        keyBindings = new (new View ());
+        keyBindings.Add (Key.A, new KeyBinding ([Command.HotKey]));
+        Assert.Throws<InvalidOperationException> (() => keyBindings.Add (Key.A, new KeyBinding (new [] { Command.Accept })));
 
         resultCommands = keyBindings.GetCommands (Key.A);
         Assert.Contains (Command.HotKey, resultCommands);
@@ -108,8 +118,8 @@ public class KeyBindingTests
     [Fact]
     public void Clear_Clears ()
     {
-        var keyBindings = new KeyBindings ();
-        keyBindings.Add (Key.B, KeyBindingScope.Application, Command.HotKey);
+        var keyBindings = new KeyBindings (new ());
+        keyBindings.Add (Key.B, Command.HotKey);
         keyBindings.Clear ();
         Command [] resultCommands = keyBindings.GetCommands (Key.A);
         Assert.Empty (resultCommands);
@@ -120,25 +130,25 @@ public class KeyBindingTests
     [Fact]
     public void Defaults ()
     {
-        var keyBindings = new KeyBindings ();
-        Assert.Empty (keyBindings.Bindings);
-        Assert.Null (keyBindings.GetKeyFromCommands (Command.Accept));
-        Assert.Null (keyBindings.BoundView);
+        var keyBindings = new KeyBindings (new ());
+        Assert.Empty (keyBindings.GetBindings ());
+        Assert.Null (keyBindings.GetFirstFromCommands (Command.Accept));
+        Assert.NotNull (keyBindings.Target);
     }
 
     [Fact]
     public void Get_Binding_Not_Found_Throws ()
     {
-        var keyBindings = new KeyBindings ();
+        var keyBindings = new KeyBindings (new ());
         Assert.Throws<InvalidOperationException> (() => keyBindings.Get (Key.A));
-        Assert.Throws<InvalidOperationException> (() => keyBindings.Get (Key.B, KeyBindingScope.Application));
+        Assert.Throws<InvalidOperationException> (() => keyBindings.Get (Key.B));
     }
 
     // GetCommands
     [Fact]
     public void GetCommands_Unknown_ReturnsEmpty ()
     {
-        var keyBindings = new KeyBindings ();
+        var keyBindings = new KeyBindings (new ());
         Command [] resultCommands = keyBindings.GetCommands (Key.A);
         Assert.Empty (resultCommands);
     }
@@ -146,8 +156,8 @@ public class KeyBindingTests
     [Fact]
     public void GetCommands_WithCommands_ReturnsCommands ()
     {
-        var keyBindings = new KeyBindings ();
-        keyBindings.Add (Key.A, KeyBindingScope.Application, Command.HotKey);
+        var keyBindings = new KeyBindings (new ());
+        keyBindings.Add (Key.A, Command.HotKey);
         Command [] resultCommands = keyBindings.GetCommands (Key.A);
         Assert.Contains (Command.HotKey, resultCommands);
     }
@@ -155,10 +165,10 @@ public class KeyBindingTests
     [Fact]
     public void GetCommands_WithMultipleBindings_ReturnsCommands ()
     {
-        var keyBindings = new KeyBindings ();
+        var keyBindings = new KeyBindings (new ());
         Command [] commands = { Command.Right, Command.Left };
-        keyBindings.Add (Key.A, KeyBindingScope.Application, commands);
-        keyBindings.Add (Key.B, KeyBindingScope.Application, commands);
+        keyBindings.Add (Key.A, commands);
+        keyBindings.Add (Key.B, commands);
         Command [] resultCommands = keyBindings.GetCommands (Key.A);
         Assert.Contains (Command.Right, resultCommands);
         Assert.Contains (Command.Left, resultCommands);
@@ -170,9 +180,9 @@ public class KeyBindingTests
     [Fact]
     public void GetCommands_WithMultipleCommands_ReturnsCommands ()
     {
-        var keyBindings = new KeyBindings ();
+        var keyBindings = new KeyBindings (new ());
         Command [] commands = { Command.Right, Command.Left };
-        keyBindings.Add (Key.A, KeyBindingScope.Application, commands);
+        keyBindings.Add (Key.A, commands);
         Command [] resultCommands = keyBindings.GetCommands (Key.A);
         Assert.Contains (Command.Right, resultCommands);
         Assert.Contains (Command.Left, resultCommands);
@@ -181,27 +191,27 @@ public class KeyBindingTests
     [Fact]
     public void GetKeyFromCommands_MultipleCommands ()
     {
-        var keyBindings = new KeyBindings ();
+        var keyBindings = new KeyBindings (new ());
         Command [] commands1 = { Command.Right, Command.Left };
-        keyBindings.Add (Key.A, KeyBindingScope.Application, commands1);
+        keyBindings.Add (Key.A, commands1);
 
         Command [] commands2 = { Command.Up, Command.Down };
-        keyBindings.Add (Key.B, KeyBindingScope.Application, commands2);
+        keyBindings.Add (Key.B, commands2);
 
-        Key key = keyBindings.GetKeyFromCommands (commands1);
+        Key key = keyBindings.GetFirstFromCommands (commands1);
         Assert.Equal (Key.A, key);
 
-        key = keyBindings.GetKeyFromCommands (commands2);
+        key = keyBindings.GetFirstFromCommands (commands2);
         Assert.Equal (Key.B, key);
     }
 
     [Fact]
     public void GetKeyFromCommands_OneCommand ()
     {
-        var keyBindings = new KeyBindings ();
-        keyBindings.Add (Key.A, KeyBindingScope.Application, Command.Right);
+        var keyBindings = new KeyBindings (new ());
+        keyBindings.Add (Key.A, Command.Right);
 
-        Key key = keyBindings.GetKeyFromCommands (Command.Right);
+        Key key = keyBindings.GetFirstFromCommands (Command.Right);
         Assert.Equal (Key.A, key);
     }
 
@@ -209,16 +219,16 @@ public class KeyBindingTests
     [Fact]
     public void GetKeyFromCommands_Unknown_Returns_Key_Empty ()
     {
-        var keyBindings = new KeyBindings ();
-        Assert.Null (keyBindings.GetKeyFromCommands (Command.Accept));
+        var keyBindings = new KeyBindings (new ());
+        Assert.Null (keyBindings.GetFirstFromCommands (Command.Accept));
     }
 
     [Fact]
     public void GetKeyFromCommands_WithCommands_ReturnsKey ()
     {
-        var keyBindings = new KeyBindings ();
-        keyBindings.Add (Key.A, KeyBindingScope.Application, Command.HotKey);
-        Key resultKey = keyBindings.GetKeyFromCommands (Command.HotKey);
+        var keyBindings = new KeyBindings (new ());
+        keyBindings.Add (Key.A, Command.HotKey);
+        Key resultKey = keyBindings.GetFirstFromCommands (Command.HotKey);
         Assert.Equal (Key.A, resultKey);
     }
 
@@ -226,24 +236,24 @@ public class KeyBindingTests
     public void ReplaceKey_Replaces ()
     {
         var keyBindings = new KeyBindings (new ());
-        keyBindings.Add (Key.A, KeyBindingScope.Focused, Command.HotKey);
-        keyBindings.Add (Key.B, KeyBindingScope.Focused, Command.HotKey);
-        keyBindings.Add (Key.C, KeyBindingScope.Focused, Command.HotKey);
-        keyBindings.Add (Key.D, KeyBindingScope.Focused, Command.HotKey);
+        keyBindings.Add (Key.A, Command.HotKey);
+        keyBindings.Add (Key.B, Command.HotKey);
+        keyBindings.Add (Key.C, Command.HotKey);
+        keyBindings.Add (Key.D, Command.HotKey);
 
-        keyBindings.ReplaceKey (Key.A, Key.E);
+        keyBindings.Replace (Key.A, Key.E);
         Assert.Empty (keyBindings.GetCommands (Key.A));
         Assert.Contains (Command.HotKey, keyBindings.GetCommands (Key.E));
 
-        keyBindings.ReplaceKey (Key.B, Key.F);
+        keyBindings.Replace (Key.B, Key.F);
         Assert.Empty (keyBindings.GetCommands (Key.B));
         Assert.Contains (Command.HotKey, keyBindings.GetCommands (Key.F));
 
-        keyBindings.ReplaceKey (Key.C, Key.G);
+        keyBindings.Replace (Key.C, Key.G);
         Assert.Empty (keyBindings.GetCommands (Key.C));
         Assert.Contains (Command.HotKey, keyBindings.GetCommands (Key.G));
 
-        keyBindings.ReplaceKey (Key.D, Key.H);
+        keyBindings.Replace (Key.D, Key.H);
         Assert.Empty (keyBindings.GetCommands (Key.D));
         Assert.Contains (Command.HotKey, keyBindings.GetCommands (Key.H));
     }
@@ -252,120 +262,62 @@ public class KeyBindingTests
     public void ReplaceKey_Replaces_Leaves_Old_Binding ()
     {
         var keyBindings = new KeyBindings (new ());
-        keyBindings.Add (Key.A, KeyBindingScope.Focused, Command.Accept);
-        keyBindings.Add (Key.B, KeyBindingScope.Focused, Command.HotKey);
+        keyBindings.Add (Key.A, Command.Accept);
+        keyBindings.Add (Key.B, Command.HotKey);
 
-        keyBindings.ReplaceKey (keyBindings.GetKeyFromCommands (Command.Accept), Key.C);
+        keyBindings.Replace (keyBindings.GetFirstFromCommands (Command.Accept), Key.C);
         Assert.Empty (keyBindings.GetCommands (Key.A));
         Assert.Contains (Command.Accept, keyBindings.GetCommands (Key.C));
     }
 
     [Fact]
-    public void ReplaceKey_Throws_If_DoesNotContain_Old ()
+    public void ReplaceKey_Adds_If_DoesNotContain_Old ()
     {
-        var keyBindings = new KeyBindings ();
-        Assert.Throws<InvalidOperationException> (() => keyBindings.ReplaceKey (Key.A, Key.B));
+        var keyBindings = new KeyBindings (new ());
+        keyBindings.Replace (Key.A, Key.B);
+        Assert.True (keyBindings.TryGet (Key.B, out _));
     }
 
     [Fact]
     public void ReplaceKey_Throws_If_New_Is_Empty ()
     {
-        var keyBindings = new KeyBindings ();
-        keyBindings.Add (Key.A, KeyBindingScope.Application, Command.HotKey);
-        Assert.Throws<InvalidOperationException> (() => keyBindings.ReplaceKey (Key.A, Key.Empty));
-    }
-
-    // Add with scope does the right things
-    [Theory]
-    [InlineData (KeyBindingScope.Focused)]
-    [InlineData (KeyBindingScope.HotKey)]
-    [InlineData (KeyBindingScope.Application)]
-    public void Scope_Add_Adds (KeyBindingScope scope)
-    {
-        var keyBindings = new KeyBindings (scope.FastHasFlags(KeyBindingScope.Application) ? null : new ());
-        Command [] commands = { Command.Right, Command.Left };
-
-        var key = new Key (Key.A);
-        keyBindings.Add (Key.A, scope, commands);
-        KeyBinding binding = keyBindings.Get (key);
-        Assert.Contains (Command.Right, binding.Commands);
-        Assert.Contains (Command.Left, binding.Commands);
-
-        binding = keyBindings.Get (key, scope);
-        Assert.Contains (Command.Right, binding.Commands);
-        Assert.Contains (Command.Left, binding.Commands);
-
-        Command [] resultCommands = keyBindings.GetCommands (key);
-        Assert.Contains (Command.Right, resultCommands);
-        Assert.Contains (Command.Left, resultCommands);
+        var keyBindings = new KeyBindings (new ());
+        keyBindings.Add (Key.A, Command.HotKey);
+        Assert.Throws<ArgumentException> (() => keyBindings.Replace (Key.A, Key.Empty));
     }
 
-    [Theory]
-    [InlineData (KeyBindingScope.Focused)]
-    [InlineData (KeyBindingScope.HotKey)]
-    [InlineData (KeyBindingScope.Application)]
-    public void Scope_Get_Filters (KeyBindingScope scope)
+    [Fact]
+    public void Get_Gets ()
     {
-        var keyBindings = new KeyBindings (scope.FastHasFlags (KeyBindingScope.Application) ? null : new ());
-        Command [] commands = { Command.Right, Command.Left };
+        var keyBindings = new KeyBindings (new ());
+        Command [] commands = [Command.Right, Command.Left];
 
         var key = new Key (Key.A);
-        keyBindings.Add (key, scope, commands);
+        keyBindings.Add (key, commands);
         KeyBinding binding = keyBindings.Get (key);
         Assert.Contains (Command.Right, binding.Commands);
         Assert.Contains (Command.Left, binding.Commands);
 
-        binding = keyBindings.Get (key, scope);
-        Assert.Contains (Command.Right, binding.Commands);
-        Assert.Contains (Command.Left, binding.Commands);
-    }
-
-    [Theory]
-    [InlineData (KeyBindingScope.Focused)]
-    [InlineData (KeyBindingScope.HotKey)]
-    [InlineData (KeyBindingScope.Application)]
-    public void Scope_TryGet_Filters (KeyBindingScope scope)
-    {
-        var keyBindings = new KeyBindings (scope.FastHasFlags (KeyBindingScope.Application) ? null : new ());
-        Command [] commands = { Command.Right, Command.Left };
-
-        var key = new Key (Key.A);
-        keyBindings.Add (key, scope, commands);
-        bool success = keyBindings.TryGet (key, out KeyBinding binding);
-        Assert.Contains (Command.Right, binding.Commands);
-        Assert.Contains (Command.Left, binding.Commands);
-
-        success = keyBindings.TryGet (key, scope, out binding);
+        binding = keyBindings.Get (key);
         Assert.Contains (Command.Right, binding.Commands);
         Assert.Contains (Command.Left, binding.Commands);
-
-        // negative test
-        success = keyBindings.TryGet (key, 0, out binding);
-        Assert.False (success);
-
-        Command [] resultCommands = keyBindings.GetCommands (key);
-        Assert.Contains (Command.Right, resultCommands);
-        Assert.Contains (Command.Left, resultCommands);
     }
 
     // TryGet
     [Fact]
     public void TryGet_Succeeds ()
     {
-        var keyBindings = new KeyBindings ();
-        keyBindings.Add (Key.Q.WithCtrl, KeyBindingScope.Application, Command.HotKey);
+        var keyBindings = new KeyBindings (new ());
+        keyBindings.Add (Key.Q.WithCtrl, Command.HotKey);
         var key = new Key (Key.Q.WithCtrl);
         bool result = keyBindings.TryGet (key, out KeyBinding _);
-        Assert.True (result);;
-
-        result = keyBindings.Bindings.TryGetValue (key, out KeyBinding _);
-        Assert.True (result);
+        Assert.True (result); ;
     }
 
     [Fact]
     public void TryGet_Unknown_ReturnsFalse ()
     {
-        var keyBindings = new KeyBindings ();
+        var keyBindings = new KeyBindings (new ());
         bool result = keyBindings.TryGet (Key.A, out KeyBinding _);
         Assert.False (result);
     }
@@ -373,10 +325,24 @@ public class KeyBindingTests
     [Fact]
     public void TryGet_WithCommands_ReturnsTrue ()
     {
-        var keyBindings = new KeyBindings ();
-        keyBindings.Add (Key.A, KeyBindingScope.Application, Command.HotKey);
+        var keyBindings = new KeyBindings (new ());
+        keyBindings.Add (Key.A, Command.HotKey);
         bool result = keyBindings.TryGet (Key.A, out KeyBinding bindings);
         Assert.True (result);
         Assert.Contains (Command.HotKey, bindings.Commands);
     }
+
+    [Fact]
+    public void ReplaceCommands_Replaces ()
+    {
+        var keyBindings = new KeyBindings (new ());
+        keyBindings.Add (Key.A, Command.Accept);
+
+        keyBindings.ReplaceCommands (Key.A, Command.Refresh);
+
+        bool result = keyBindings.TryGet (Key.A, out KeyBinding bindings);
+        Assert.True (result);
+        Assert.Contains (Command.Refresh, bindings.Commands);
+
+    }
 }

+ 0 - 0
UnitTests/Input/KeyTests.cs → UnitTests/Input/Keyboard/KeyTests.cs


+ 6 - 0
UnitTests/Input/Mouse/MouseBindingTests.cs

@@ -0,0 +1,6 @@
+namespace Terminal.Gui.InputTests;
+
+public class MouseBindingTests
+{
+    // TODO: Add tests for MouseBinding
+}

+ 344 - 0
UnitTests/Input/Mouse/MouseBindingsTests.cs

@@ -0,0 +1,344 @@
+namespace Terminal.Gui.InputTests;
+
+public class MouseBindingsTests
+{
+    [Fact]
+    public void Add_Adds ()
+    {
+        var mouseBindings = new MouseBindings ();
+        Command [] commands = [Command.Right, Command.Left];
+
+        var flags = MouseFlags.AllEvents;
+        mouseBindings.Add (flags, commands);
+        MouseBinding binding = mouseBindings.Get (flags);
+        Assert.Contains (Command.Right, binding.Commands);
+        Assert.Contains (Command.Left, binding.Commands);
+
+        binding = mouseBindings.Get (flags);
+        Assert.Contains (Command.Right, binding.Commands);
+        Assert.Contains (Command.Left, binding.Commands);
+
+        Command [] resultCommands = mouseBindings.GetCommands (flags);
+        Assert.Contains (Command.Right, resultCommands);
+        Assert.Contains (Command.Left, resultCommands);
+    }
+
+    [Fact]
+    public void Add_Invalid_Flag_Throws ()
+    {
+        var mouseBindings = new MouseBindings ();
+        List<Command> commands = new ();
+        Assert.Throws<ArgumentException> (() => mouseBindings.Add (MouseFlags.None, Command.Accept));
+    }
+
+    [Fact]
+    public void Add_Multiple_Commands_Adds ()
+    {
+        var mouseBindings = new MouseBindings ();
+        Command [] commands = [Command.Right, Command.Left];
+
+        mouseBindings.Add (MouseFlags.Button1Clicked, commands);
+        Command [] resultCommands = mouseBindings.GetCommands (MouseFlags.Button1Clicked);
+        Assert.Contains (Command.Right, resultCommands);
+        Assert.Contains (Command.Left, resultCommands);
+
+        mouseBindings.Add (MouseFlags.Button2Clicked, commands);
+        resultCommands = mouseBindings.GetCommands (MouseFlags.Button2Clicked);
+        Assert.Contains (Command.Right, resultCommands);
+        Assert.Contains (Command.Left, resultCommands);
+    }
+
+    [Fact]
+    public void Add_No_Commands_Throws ()
+    {
+        var mouseBindings = new MouseBindings ();
+        List<Command> commands = new ();
+        Assert.Throws<ArgumentException> (() => mouseBindings.Add (MouseFlags.Button1Clicked, commands.ToArray ()));
+    }
+
+    [Fact]
+    public void Add_Single_Command_Adds ()
+    {
+        var mouseBindings = new MouseBindings ();
+        mouseBindings.Add (MouseFlags.Button1Clicked, Command.HotKey);
+        Command [] resultCommands = mouseBindings.GetCommands (MouseFlags.Button1Clicked);
+        Assert.Contains (Command.HotKey, resultCommands);
+
+        mouseBindings.Add (MouseFlags.Button2Clicked, Command.HotKey);
+        resultCommands = mouseBindings.GetCommands (MouseFlags.Button2Clicked);
+        Assert.Contains (Command.HotKey, resultCommands);
+    }
+
+    // Add should not allow duplicates
+    [Fact]
+    public void Add_Throws_If_Exists ()
+    {
+        var mouseBindings = new MouseBindings ();
+        mouseBindings.Add (MouseFlags.Button1Clicked, Command.HotKey);
+        Assert.Throws<InvalidOperationException> (() => mouseBindings.Add (MouseFlags.Button1Clicked, Command.Accept));
+
+        Command [] resultCommands = mouseBindings.GetCommands (MouseFlags.Button1Clicked);
+        Assert.Contains (Command.HotKey, resultCommands);
+
+        mouseBindings = new ();
+        mouseBindings.Add (MouseFlags.Button1Clicked, Command.HotKey);
+        Assert.Throws<InvalidOperationException> (() => mouseBindings.Add (MouseFlags.Button1Clicked, Command.Accept));
+
+        resultCommands = mouseBindings.GetCommands (MouseFlags.Button1Clicked);
+        Assert.Contains (Command.HotKey, resultCommands);
+
+        mouseBindings = new ();
+        mouseBindings.Add (MouseFlags.Button1Clicked, Command.HotKey);
+        Assert.Throws<InvalidOperationException> (() => mouseBindings.Add (MouseFlags.Button1Clicked, Command.Accept));
+
+        resultCommands = mouseBindings.GetCommands (MouseFlags.Button1Clicked);
+        Assert.Contains (Command.HotKey, resultCommands);
+
+        mouseBindings = new ();
+        mouseBindings.Add (MouseFlags.Button1Clicked, Command.Accept);
+        Assert.Throws<InvalidOperationException> (() => mouseBindings.Add (MouseFlags.Button1Clicked, Command.ScrollDown));
+
+        resultCommands = mouseBindings.GetCommands (MouseFlags.Button1Clicked);
+        Assert.Contains (Command.Accept, resultCommands);
+
+        mouseBindings = new ();
+        mouseBindings.Add (MouseFlags.Button1Clicked, new MouseBinding ([Command.HotKey], MouseFlags.Button1Clicked));
+
+        Assert.Throws<InvalidOperationException> (
+                                                  () => mouseBindings.Add (
+                                                                           MouseFlags.Button1Clicked,
+                                                                           new MouseBinding ([Command.Accept], MouseFlags.Button1Clicked)));
+
+        resultCommands = mouseBindings.GetCommands (MouseFlags.Button1Clicked);
+        Assert.Contains (Command.HotKey, resultCommands);
+    }
+
+    // Clear
+    [Fact]
+    public void Clear_Clears ()
+    {
+        var mouseBindings = new MouseBindings ();
+        mouseBindings.Add (MouseFlags.Button1Clicked, Command.HotKey);
+        mouseBindings.Clear ();
+        Command [] resultCommands = mouseBindings.GetCommands (MouseFlags.Button1Clicked);
+        Assert.Empty (resultCommands);
+        resultCommands = mouseBindings.GetCommands (MouseFlags.Button1Clicked);
+        Assert.Empty (resultCommands);
+    }
+
+    [Fact]
+    public void Defaults ()
+    {
+        var mouseBindings = new MouseBindings ();
+        Assert.Empty (mouseBindings.GetBindings ());
+        Assert.Equal (MouseFlags.None, mouseBindings.GetFirstFromCommands (Command.Accept));
+    }
+
+    [Fact]
+    public void Get_Binding_Not_Found_Throws ()
+    {
+        var mouseBindings = new MouseBindings ();
+        Assert.Throws<InvalidOperationException> (() => mouseBindings.Get (MouseFlags.Button1Clicked));
+        Assert.Throws<InvalidOperationException> (() => mouseBindings.Get (MouseFlags.AllEvents));
+    }
+
+    // GetCommands
+    [Fact]
+    public void GetCommands_Unknown_ReturnsEmpty ()
+    {
+        var mouseBindings = new MouseBindings ();
+        Command [] resultCommands = mouseBindings.GetCommands (MouseFlags.Button1Clicked);
+        Assert.Empty (resultCommands);
+    }
+
+    [Fact]
+    public void GetCommands_WithCommands_ReturnsCommands ()
+    {
+        var mouseBindings = new MouseBindings ();
+        mouseBindings.Add (MouseFlags.Button1Clicked, Command.HotKey);
+        Command [] resultCommands = mouseBindings.GetCommands (MouseFlags.Button1Clicked);
+        Assert.Contains (Command.HotKey, resultCommands);
+    }
+
+    [Fact]
+    public void GetCommands_WithMultipleBindings_ReturnsCommands ()
+    {
+        var mouseBindings = new MouseBindings ();
+        Command [] commands = [Command.Right, Command.Left];
+        mouseBindings.Add (MouseFlags.Button1Clicked, commands);
+        mouseBindings.Add (MouseFlags.Button2Clicked, commands);
+        Command [] resultCommands = mouseBindings.GetCommands (MouseFlags.Button1Clicked);
+        Assert.Contains (Command.Right, resultCommands);
+        Assert.Contains (Command.Left, resultCommands);
+        resultCommands = mouseBindings.GetCommands (MouseFlags.Button2Clicked);
+        Assert.Contains (Command.Right, resultCommands);
+        Assert.Contains (Command.Left, resultCommands);
+    }
+
+    [Fact]
+    public void GetCommands_WithMultipleCommands_ReturnsCommands ()
+    {
+        var mouseBindings = new MouseBindings ();
+        Command [] commands = [Command.Right, Command.Left];
+        mouseBindings.Add (MouseFlags.Button1Clicked, commands);
+        Command [] resultCommands = mouseBindings.GetCommands (MouseFlags.Button1Clicked);
+        Assert.Contains (Command.Right, resultCommands);
+        Assert.Contains (Command.Left, resultCommands);
+    }
+
+    [Fact]
+    public void GetMouseFlagsFromCommands_MultipleCommands ()
+    {
+        var mouseBindings = new MouseBindings ();
+        Command [] commands1 = [Command.Right, Command.Left];
+        mouseBindings.Add (MouseFlags.Button1Clicked, commands1);
+
+        Command [] commands2 = { Command.Up, Command.Down };
+        mouseBindings.Add (MouseFlags.Button2Clicked, commands2);
+
+        MouseFlags mouseFlags = mouseBindings.GetFirstFromCommands (commands1);
+        Assert.Equal (MouseFlags.Button1Clicked, mouseFlags);
+
+        mouseFlags = mouseBindings.GetFirstFromCommands (commands2);
+        Assert.Equal (MouseFlags.Button2Clicked, mouseFlags);
+    }
+
+    [Fact]
+    public void GetMouseFlagsFromCommands_OneCommand ()
+    {
+        var mouseBindings = new MouseBindings ();
+        mouseBindings.Add (MouseFlags.Button1Clicked, Command.Right);
+
+        MouseFlags mouseFlags = mouseBindings.GetFirstFromCommands (Command.Right);
+        Assert.Equal (MouseFlags.Button1Clicked, mouseFlags);
+    }
+
+    // GetMouseFlagsFromCommands
+    [Fact]
+    public void GetMouseFlagsFromCommands_Unknown_Returns_Key_Empty ()
+    {
+        var mouseBindings = new MouseBindings ();
+        Assert.Equal (MouseFlags.None, mouseBindings.GetFirstFromCommands (Command.Accept));
+    }
+
+    [Fact]
+    public void GetMouseFlagsFromCommands_WithCommands_ReturnsKey ()
+    {
+        var mouseBindings = new MouseBindings ();
+        mouseBindings.Add (MouseFlags.Button1Clicked, Command.HotKey);
+        MouseFlags mouseFlags = mouseBindings.GetFirstFromCommands (Command.HotKey);
+        Assert.Equal (MouseFlags.Button1Clicked, mouseFlags);
+    }
+
+    [Fact]
+    public void ReplaceMouseFlags_Replaces ()
+    {
+        var mouseBindings = new MouseBindings ();
+        mouseBindings.Add (MouseFlags.Button1Clicked, Command.HotKey);
+        mouseBindings.Add (MouseFlags.Button2Clicked, Command.HotKey);
+        mouseBindings.Add (MouseFlags.Button3Clicked, Command.HotKey);
+        mouseBindings.Add (MouseFlags.Button4Clicked, Command.HotKey);
+
+        mouseBindings.Replace (MouseFlags.Button1Clicked, MouseFlags.Button1DoubleClicked);
+        Assert.Empty (mouseBindings.GetCommands (MouseFlags.Button1Clicked));
+        Assert.Contains (Command.HotKey, mouseBindings.GetCommands (MouseFlags.Button1DoubleClicked));
+
+        mouseBindings.Replace (MouseFlags.Button2Clicked, MouseFlags.Button2DoubleClicked);
+        Assert.Empty (mouseBindings.GetCommands (MouseFlags.Button2Clicked));
+        Assert.Contains (Command.HotKey, mouseBindings.GetCommands (MouseFlags.Button2DoubleClicked));
+
+        mouseBindings.Replace (MouseFlags.Button3Clicked, MouseFlags.Button3DoubleClicked);
+        Assert.Empty (mouseBindings.GetCommands (MouseFlags.Button3Clicked));
+        Assert.Contains (Command.HotKey, mouseBindings.GetCommands (MouseFlags.Button3DoubleClicked));
+
+        mouseBindings.Replace (MouseFlags.Button4Clicked, MouseFlags.Button4DoubleClicked);
+        Assert.Empty (mouseBindings.GetCommands (MouseFlags.Button4Clicked));
+        Assert.Contains (Command.HotKey, mouseBindings.GetCommands (MouseFlags.Button4DoubleClicked));
+    }
+
+    [Fact]
+    public void ReplaceMouseFlags_Replaces_Leaves_Old_Binding ()
+    {
+        var mouseBindings = new MouseBindings ();
+        mouseBindings.Add (MouseFlags.Button1Clicked, Command.Accept);
+        mouseBindings.Add (MouseFlags.Button2Clicked, Command.HotKey);
+
+        mouseBindings.Replace (mouseBindings.GetFirstFromCommands (Command.Accept), MouseFlags.Button3Clicked);
+        Assert.Empty (mouseBindings.GetCommands (MouseFlags.Button1Clicked));
+        Assert.Contains (Command.Accept, mouseBindings.GetCommands (MouseFlags.Button3Clicked));
+    }
+
+    [Fact]
+    public void ReplaceMouseFlags_Adds_If_DoesNotContain_Old ()
+    {
+        var mouseBindings = new MouseBindings ();
+        mouseBindings.Replace (MouseFlags.Button1Clicked, MouseFlags.Button2Clicked);
+        Assert.True (mouseBindings.TryGet (MouseFlags.Button2Clicked, out _));
+    }
+
+    [Fact]
+    public void ReplaceMouseFlags_Throws_If_New_Is_None ()
+    {
+        var mouseBindings = new MouseBindings ();
+        mouseBindings.Add (MouseFlags.Button1Clicked, Command.HotKey);
+        Assert.Throws<ArgumentException> (() => mouseBindings.Replace (MouseFlags.Button1Clicked, MouseFlags.None));
+    }
+
+    [Fact]
+    public void Get_Gets ()
+    {
+        var mouseBindings = new MouseBindings ();
+        Command [] commands = [Command.Right, Command.Left];
+
+        mouseBindings.Add (MouseFlags.Button1Clicked, commands);
+        MouseBinding binding = mouseBindings.Get (MouseFlags.Button1Clicked);
+        Assert.Contains (Command.Right, binding.Commands);
+        Assert.Contains (Command.Left, binding.Commands);
+
+        binding = mouseBindings.Get (MouseFlags.Button1Clicked);
+        Assert.Contains (Command.Right, binding.Commands);
+        Assert.Contains (Command.Left, binding.Commands);
+    }
+
+    // TryGet
+    [Fact]
+    public void TryGet_Succeeds ()
+    {
+        var mouseBindings = new MouseBindings ();
+        mouseBindings.Add (MouseFlags.Button1Clicked, Command.HotKey);
+        bool result = mouseBindings.TryGet (MouseFlags.Button1Clicked, out MouseBinding _);
+        Assert.True (result);
+        ;
+    }
+
+    [Fact]
+    public void TryGet_Unknown_ReturnsFalse ()
+    {
+        var mouseBindings = new MouseBindings ();
+        bool result = mouseBindings.TryGet (MouseFlags.Button1Clicked, out MouseBinding _);
+        Assert.False (result);
+    }
+
+    [Fact]
+    public void TryGet_WithCommands_ReturnsTrue ()
+    {
+        var mouseBindings = new MouseBindings ();
+        mouseBindings.Add (MouseFlags.Button1Clicked, Command.HotKey);
+        bool result = mouseBindings.TryGet (MouseFlags.Button1Clicked, out MouseBinding bindings);
+        Assert.True (result);
+        Assert.Contains (Command.HotKey, bindings.Commands);
+    }
+
+    [Fact]
+    public void ReplaceCommands_Replaces ()
+    {
+        var mouseBindings = new MouseBindings ();
+        mouseBindings.Add (MouseFlags.Button1Clicked, Command.Accept);
+
+        mouseBindings.ReplaceCommands (MouseFlags.Button1Clicked, Command.Refresh);
+
+        bool result = mouseBindings.TryGet (MouseFlags.Button1Clicked, out MouseBinding bindings);
+        Assert.True (result);
+        Assert.Contains (Command.Refresh, bindings.Commands);
+    }
+}

+ 18 - 0
UnitTests/Input/Mouse/MouseEventArgsTest.cs

@@ -0,0 +1,18 @@
+namespace Terminal.Gui.InputTests;
+
+public class MouseEventArgsTests
+{
+    [Fact]
+    public void Constructor_Default_ShouldSetFlagsToNone ()
+    {
+        var eventArgs = new MouseEventArgs ();
+        Assert.Equal (MouseFlags.None, eventArgs.Flags);
+    }
+
+    [Fact]
+    public void HandledProperty_ShouldBeFalseByDefault ()
+    {
+        var eventArgs = new MouseEventArgs ();
+        Assert.False (eventArgs.Handled);
+    }
+}

+ 57 - 26
UnitTests/View/Keyboard/HotKeyTests.cs

@@ -1,4 +1,5 @@
 using System.Text;
+using UICatalog.Scenarios;
 using Xunit.Abstractions;
 
 namespace Terminal.Gui.ViewTests;
@@ -27,9 +28,9 @@ public class HotKeyTests
         // Verify key bindings were set
 
         // As passed
-        Command [] commands = view.KeyBindings.GetCommands (key);
+        Command [] commands = view.HotKeyBindings.GetCommands (key);
         Assert.Contains (Command.HotKey, commands);
-        commands = view.KeyBindings.GetCommands (key | KeyCode.AltMask);
+        commands = view.HotKeyBindings.GetCommands (key | KeyCode.AltMask);
         Assert.Contains (Command.HotKey, commands);
 
         KeyCode baseKey = key & ~KeyCode.ShiftMask;
@@ -37,13 +38,13 @@ public class HotKeyTests
         // If A...Z, with and without shift
         if (baseKey is >= KeyCode.A and <= KeyCode.Z)
         {
-            commands = view.KeyBindings.GetCommands (key | KeyCode.ShiftMask);
+            commands = view.HotKeyBindings.GetCommands (key | KeyCode.ShiftMask);
             Assert.Contains (Command.HotKey, commands);
-            commands = view.KeyBindings.GetCommands (key & ~KeyCode.ShiftMask);
+            commands = view.HotKeyBindings.GetCommands (key & ~KeyCode.ShiftMask);
             Assert.Contains (Command.HotKey, commands);
-            commands = view.KeyBindings.GetCommands (key | KeyCode.AltMask);
+            commands = view.HotKeyBindings.GetCommands (key | KeyCode.AltMask);
             Assert.Contains (Command.HotKey, commands);
-            commands = view.KeyBindings.GetCommands ((key & ~KeyCode.ShiftMask) | KeyCode.AltMask);
+            commands = view.HotKeyBindings.GetCommands ((key & ~KeyCode.ShiftMask) | KeyCode.AltMask);
             Assert.Contains (Command.HotKey, commands);
         }
         else
@@ -51,17 +52,42 @@ public class HotKeyTests
             // Non A..Z keys should not have shift bindings
             if (key.HasFlag (KeyCode.ShiftMask))
             {
-                commands = view.KeyBindings.GetCommands (key & ~KeyCode.ShiftMask);
+                commands = view.HotKeyBindings.GetCommands (key & ~KeyCode.ShiftMask);
                 Assert.Empty (commands);
             }
             else
             {
-                commands = view.KeyBindings.GetCommands (key | KeyCode.ShiftMask);
+                commands = view.HotKeyBindings.GetCommands (key | KeyCode.ShiftMask);
                 Assert.Empty (commands);
             }
         }
     }
 
+    [Fact]
+    public void AddKeyBindingsForHotKey_SetsBinding_Key ()
+    {
+        var view = new View ();
+        view.HotKey = KeyCode.Z;
+        Assert.Equal (string.Empty, view.Title);
+        Assert.Equal (KeyCode.Z, view.HotKey);
+
+        view.AddKeyBindingsForHotKey (view.HotKey, Key.A);
+        view.HotKeyBindings.TryGet (Key.A, out var binding);
+        Assert.Equal (Key.A, binding.Key);
+    }
+
+    [Fact]
+    public void AddKeyBindingsForHotKey_SetsBinding_Data ()
+    {
+        var view = new View ();
+        view.HotKey = KeyCode.Z;
+        Assert.Equal (KeyCode.Z, view.HotKey);
+
+        view.AddKeyBindingsForHotKey (view.HotKey, Key.A, "data");
+        view.HotKeyBindings.TryGet (Key.A, out var binding);
+        Assert.Equal ("data", binding.Data);
+    }
+
     [Fact]
     public void Defaults ()
     {
@@ -72,6 +98,11 @@ public class HotKeyTests
         // Verify key bindings were set
         Command [] commands = view.KeyBindings.GetCommands (KeyCode.Null);
         Assert.Empty (commands);
+
+        commands = view.HotKeyBindings.GetCommands (KeyCode.Null);
+        Assert.Empty (commands);
+
+        Assert.Empty (view.HotKeyBindings.GetBindings ());
     }
 
     [Theory]
@@ -94,7 +125,7 @@ public class HotKeyTests
     public void NewKeyDownEvent_Ignores_Focus_KeyBindings_SuperView ()
     {
         var view = new View ();
-        view.KeyBindings.Add (Key.A, Command.HotKey); // implies KeyBindingScope.Focused - so this should not be invoked
+        view.HotKeyBindings.Add (Key.A, Command.HotKey);
         view.KeyDownNotHandled += (s, e) => { Assert.Fail (); };
 
         var superView = new View ();
@@ -108,7 +139,7 @@ public class HotKeyTests
     public void NewKeyDownEvent_Honors_HotKey_KeyBindings_SuperView ()
     {
         var view = new View ();
-        view.KeyBindings.Add (Key.A, KeyBindingScope.HotKey, Command.HotKey);
+        view.HotKeyBindings.Add (Key.A, Command.HotKey);
         bool hotKeyInvoked = false;
         view.HandlingHotKey += (s, e) => { hotKeyInvoked = true; };
 
@@ -167,16 +198,16 @@ public class HotKeyTests
         Assert.Equal (KeyCode.A, view.HotKey);
 
         // Verify key bindings were set
-        Command [] commands = view.KeyBindings.GetCommands (KeyCode.A);
+        Command [] commands = view.HotKeyBindings.GetCommands (KeyCode.A);
         Assert.Contains (Command.HotKey, commands);
 
-        commands = view.KeyBindings.GetCommands (KeyCode.A | KeyCode.ShiftMask);
+        commands = view.HotKeyBindings.GetCommands (KeyCode.A | KeyCode.ShiftMask);
         Assert.Contains (Command.HotKey, commands);
 
-        commands = view.KeyBindings.GetCommands (KeyCode.A | KeyCode.AltMask);
+        commands = view.HotKeyBindings.GetCommands (KeyCode.A | KeyCode.AltMask);
         Assert.Contains (Command.HotKey, commands);
 
-        commands = view.KeyBindings.GetCommands (KeyCode.A | KeyCode.ShiftMask | KeyCode.AltMask);
+        commands = view.HotKeyBindings.GetCommands (KeyCode.A | KeyCode.ShiftMask | KeyCode.AltMask);
         Assert.Contains (Command.HotKey, commands);
 
         // Now set again
@@ -184,16 +215,16 @@ public class HotKeyTests
         Assert.Equal (string.Empty, view.Title);
         Assert.Equal (KeyCode.B, view.HotKey);
 
-        commands = view.KeyBindings.GetCommands (KeyCode.A);
+        commands = view.HotKeyBindings.GetCommands (KeyCode.A);
         Assert.DoesNotContain (Command.HotKey, commands);
 
-        commands = view.KeyBindings.GetCommands (KeyCode.A | KeyCode.ShiftMask);
+        commands = view.HotKeyBindings.GetCommands (KeyCode.A | KeyCode.ShiftMask);
         Assert.DoesNotContain (Command.HotKey, commands);
 
-        commands = view.KeyBindings.GetCommands (KeyCode.A | KeyCode.AltMask);
+        commands = view.HotKeyBindings.GetCommands (KeyCode.A | KeyCode.AltMask);
         Assert.DoesNotContain (Command.HotKey, commands);
 
-        commands = view.KeyBindings.GetCommands (KeyCode.A | KeyCode.ShiftMask | KeyCode.AltMask);
+        commands = view.HotKeyBindings.GetCommands (KeyCode.A | KeyCode.ShiftMask | KeyCode.AltMask);
         Assert.DoesNotContain (Command.HotKey, commands);
     }
 
@@ -232,7 +263,7 @@ public class HotKeyTests
         // Verify key bindings were set
 
         // As passed
-        Command [] commands = view.KeyBindings.GetCommands (view.HotKey);
+        Command [] commands = view.HotKeyBindings.GetCommands (view.HotKey);
         Assert.Contains (Command.HotKey, commands);
 
         Key baseKey = view.HotKey.NoShift;
@@ -240,13 +271,13 @@ public class HotKeyTests
         // If A...Z, with and without shift
         if (baseKey.IsKeyCodeAtoZ)
         {
-            commands = view.KeyBindings.GetCommands (view.HotKey.WithShift);
+            commands = view.HotKeyBindings.GetCommands (view.HotKey.WithShift);
             Assert.Contains (Command.HotKey, commands);
-            commands = view.KeyBindings.GetCommands (view.HotKey.NoShift);
+            commands = view.HotKeyBindings.GetCommands (view.HotKey.NoShift);
             Assert.Contains (Command.HotKey, commands);
-            commands = view.KeyBindings.GetCommands (view.HotKey.WithAlt);
+            commands = view.HotKeyBindings.GetCommands (view.HotKey.WithAlt);
             Assert.Contains (Command.HotKey, commands);
-            commands = view.KeyBindings.GetCommands (view.HotKey.NoShift.WithAlt);
+            commands = view.HotKeyBindings.GetCommands (view.HotKey.NoShift.WithAlt);
             Assert.Contains (Command.HotKey, commands);
         }
         else
@@ -254,12 +285,12 @@ public class HotKeyTests
             // Non A..Z keys should not have shift bindings
             if (view.HotKey.IsShift)
             {
-                commands = view.KeyBindings.GetCommands (view.HotKey.NoShift);
+                commands = view.HotKeyBindings.GetCommands (view.HotKey.NoShift);
                 Assert.Empty (commands);
             }
             else
             {
-                commands = view.KeyBindings.GetCommands (view.HotKey.WithShift);
+                commands = view.HotKeyBindings.GetCommands (view.HotKey.WithShift);
                 Assert.Empty (commands);
             }
         }
@@ -366,7 +397,7 @@ public class HotKeyTests
         view.Selecting += (s, e) => selectRaised = true;
 
         Assert.Equal (KeyCode.T, view.HotKey);
-        Assert.True (Application.RaiseKeyDownEvent (Key.T)); 
+        Assert.True (Application.RaiseKeyDownEvent (Key.T));
         Assert.True (hotKeyRaised);
         Assert.False (acceptRaised);
         Assert.False (selectRaised);

+ 7 - 6
UnitTests/View/Keyboard/ViewKeyBindingTests.cs → UnitTests/View/Keyboard/KeyBindingsTests.cs

@@ -2,13 +2,14 @@
 
 namespace Terminal.Gui.ViewTests;
 
-public class ViewKeyBindingTests (ITestOutputHelper output)
+/// <summary>
+///     Tests for View.KeyBindings
+/// </summary>
+public class KeyBindingsTests ()
 {
-    private readonly ITestOutputHelper _output = output;
-
     [Fact]
     [AutoInitShutdown]
-    public void Focus_KeyBinding ()
+    public void Focused_HotKey_Application_All_Work ()
     {
         var view = new ScopedKeyBindingView ();
         var keyWasHandled = false;
@@ -48,7 +49,7 @@ public class ViewKeyBindingTests (ITestOutputHelper output)
 
     [Fact]
     [AutoInitShutdown]
-    public void Focus_KeyBinding_Negative ()
+    public void KeyBinding_Negative ()
     {
         var view = new ScopedKeyBindingView ();
         var keyWasHandled = false;
@@ -139,7 +140,7 @@ public class ViewKeyBindingTests (ITestOutputHelper output)
 
             Application.KeyBindings.Add (Key.A, this, Command.Save);
             HotKey = KeyCode.H;
-            KeyBindings.Add (Key.F, KeyBindingScope.Focused, Command.Left);
+            KeyBindings.Add (Key.F, Command.Left);
         }
 
         public bool ApplicationCommand { get; set; }

+ 2 - 2
UnitTests/View/Keyboard/KeyboardEventTests.cs

@@ -273,12 +273,12 @@ public class KeyboardEventTests (ITestOutputHelper output) : TestsAllViews
     [InlineData (null, null)]
     [InlineData (true, true)]
     [InlineData (false, false)]
-    public void InvokeCommandsBoundToKey_Returns_Nullable_Properly (bool? toReturn, bool? expected)
+    public void InvokeCommands_Returns_Nullable_Properly (bool? toReturn, bool? expected)
     {
         var view = new KeyBindingsTestView ();
         view.CommandReturns = toReturn;
 
-        bool? result = view.InvokeCommandsBoundToKey (Key.A);
+        bool? result = view.InvokeCommands (Key.A);
         Assert.Equal (expected, result);
     }
 

+ 21 - 27
UnitTests/View/ViewCommandTests.cs

@@ -1,12 +1,9 @@
-using System.ComponentModel;
-using System.Text;
-using Xunit.Abstractions;
-
 namespace Terminal.Gui.ViewTests;
 
-public class ViewCommandTests (ITestOutputHelper output)
+public class ViewCommandTests
 {
     #region OnAccept/Accept tests
+
     [Fact]
     public void Accept_Command_Raises_NoFocus ()
     {
@@ -77,8 +74,8 @@ public class ViewCommandTests (ITestOutputHelper output)
     [Fact]
     public void Accept_Command_Bubbles_Up_To_SuperView ()
     {
-        var view = new ViewEventTester () { Id = "view" };
-        var subview = new ViewEventTester () { Id = "subview" };
+        var view = new ViewEventTester { Id = "view" };
+        var subview = new ViewEventTester { Id = "subview" };
         view.Add (subview);
 
         subview.InvokeCommand (Command.Accept);
@@ -97,7 +94,7 @@ public class ViewCommandTests (ITestOutputHelper output)
         Assert.Equal (1, view.OnAcceptedCount);
 
         // Add a super view to test deeper hierarchy
-        var superView = new ViewEventTester () { Id = "superView" };
+        var superView = new ViewEventTester { Id = "superView" };
         superView.Add (view);
 
         subview.InvokeCommand (Command.Accept);
@@ -135,7 +132,7 @@ public class ViewCommandTests (ITestOutputHelper output)
     [CombinatorialData]
     public void Select_Command_Raises_SetsFocus (bool canFocus)
     {
-        var view = new ViewEventTester ()
+        var view = new ViewEventTester
         {
             CanFocus = canFocus
         };
@@ -236,30 +233,29 @@ public class ViewCommandTests (ITestOutputHelper output)
             CanFocus = true;
 
             Accepting += (s, a) =>
-                      {
-                          a.Cancel = HandleAccepted;
-                          AcceptedCount++;
-                      };
+                         {
+                             a.Cancel = HandleAccepted;
+                             AcceptedCount++;
+                         };
 
             HandlingHotKey += (s, a) =>
-                             {
-                                 a.Cancel = HandleHandlingHotKey;
-                                 HandlingHotKeyCount++;
-                             };
-
+                              {
+                                  a.Cancel = HandleHandlingHotKey;
+                                  HandlingHotKeyCount++;
+                              };
 
             Selecting += (s, a) =>
-                             {
-                                 a.Cancel = HandleSelecting;
-                                 SelectingCount++;
-                             };
+                         {
+                             a.Cancel = HandleSelecting;
+                             SelectingCount++;
+                         };
         }
 
         public int OnAcceptedCount { get; set; }
         public int AcceptedCount { get; set; }
         public bool HandleOnAccepted { get; set; }
 
-        /// <inheritdoc />
+        /// <inheritdoc/>
         protected override bool OnAccepting (CommandEventArgs args)
         {
             OnAcceptedCount++;
@@ -273,7 +269,7 @@ public class ViewCommandTests (ITestOutputHelper output)
         public int HandlingHotKeyCount { get; set; }
         public bool HandleOnHandlingHotKey { get; set; }
 
-        /// <inheritdoc />
+        /// <inheritdoc/>
         protected override bool OnHandlingHotKey (CommandEventArgs args)
         {
             OnHandlingHotKeyCount++;
@@ -283,12 +279,11 @@ public class ViewCommandTests (ITestOutputHelper output)
 
         public bool HandleHandlingHotKey { get; set; }
 
-
         public int OnSelectingCount { get; set; }
         public int SelectingCount { get; set; }
         public bool HandleOnSelecting { get; set; }
 
-        /// <inheritdoc />
+        /// <inheritdoc/>
         protected override bool OnSelecting (CommandEventArgs args)
         {
             OnSelectingCount++;
@@ -297,6 +292,5 @@ public class ViewCommandTests (ITestOutputHelper output)
         }
 
         public bool HandleSelecting { get; set; }
-
     }
 }

+ 3 - 7
UnitTests/Views/AllViewsTests.cs

@@ -188,25 +188,21 @@ public class AllViewsTests (ITestOutputHelper output) : TestsAllViews
             view.HotKey = Key.T;
         }
 
-        var selectingCount = 0;
-        view.Selecting += (s, e) => selectingCount++;
-
         var acceptedCount = 0;
         view.Accepting += (s, e) =>
                          {
                              acceptedCount++;
                          };
 
-        var hotkeyHandledCount = 0;
+        var handlingHotKeyCount = 0;
         view.HandlingHotKey += (s, e) =>
                          {
-                             hotkeyHandledCount++;
+                             handlingHotKeyCount++;
                          };
 
         if (view.InvokeCommand (Command.HotKey) == true)
         {
-            Assert.Equal (1, hotkeyHandledCount);
-            Assert.Equal (0, selectingCount);
+            Assert.Equal (1, handlingHotKeyCount);
             Assert.Equal (0, acceptedCount);
         }
     }

+ 13 - 4
UnitTests/Views/CheckBoxTests.cs

@@ -252,7 +252,7 @@ public class CheckBoxTests (ITestOutputHelper output)
 
     [Fact]
     [SetupFakeDriver]
-    public void Mouse_Click ()
+    public void Mouse_Click_Selects ()
     {
         var checkBox = new CheckBox { Text = "_Checkbox" };
         Assert.True (checkBox.CanFocus);
@@ -296,7 +296,7 @@ public class CheckBoxTests (ITestOutputHelper output)
 
     [Fact]
     [SetupFakeDriver]
-    public void Mouse_DoubleClick ()
+    public void Mouse_DoubleClick_Accepts ()
     {
         var checkBox = new CheckBox { Text = "_Checkbox" };
         Assert.True (checkBox.CanFocus);
@@ -308,7 +308,11 @@ public class CheckBoxTests (ITestOutputHelper output)
         checkBox.Selecting += (s, e) => selectCount++;
 
         int acceptCount = 0;
-        checkBox.Accepting += (s, e) => acceptCount++;
+        checkBox.Accepting += (s, e) =>
+                              {
+                                  acceptCount++;
+                                  e.Cancel = true;
+                              };
 
         checkBox.HasFocus = true;
         Assert.True (checkBox.HasFocus);
@@ -319,9 +323,14 @@ public class CheckBoxTests (ITestOutputHelper output)
 
         Assert.True (checkBox.NewMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1DoubleClicked }));
 
+        Assert.Equal (CheckState.UnChecked, checkBox.CheckedState);
+        Assert.Equal (0, checkedStateChangingCount);
+        Assert.Equal (0, selectCount);
+        Assert.Equal (1, acceptCount);
+
     }
 
-#endregion Mouse Tests
+    #endregion Mouse Tests
 
     [Fact]
     [AutoInitShutdown]

+ 79 - 79
UnitTests/Views/ContextMenuTests.cs

@@ -1483,9 +1483,9 @@ public class ContextMenuTests (ITestOutputHelper output)
         Assert.False (deleteFile);
 
         cm.Show (menuItems);
-        Assert.True (cm.MenuBar!.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl));
-        Assert.True (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithCtrl));
-        Assert.True (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.D.WithCtrl));
+        Assert.True (cm.MenuBar!.HotKeyBindings.TryGet (Key.N.WithCtrl, out _));
+        Assert.True (cm.MenuBar.HotKeyBindings.TryGet (Key.R.WithCtrl, out _));
+        Assert.True (cm.MenuBar.HotKeyBindings.TryGet (Key.D.WithCtrl, out _));
 
         Assert.True (Application.RaiseKeyDownEvent (Key.N.WithCtrl));
         Application.MainLoop!.RunIteration ();
@@ -1502,9 +1502,9 @@ public class ContextMenuTests (ITestOutputHelper output)
         Assert.True (deleteFile);
         Assert.False (cm.MenuBar.IsMenuOpen);
 
-        Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl));
-        Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithCtrl));
-        Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.D.WithCtrl));
+        Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _));
+        Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.WithCtrl, out _));
+        Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.D.WithCtrl, out _));
 
         newFile = false;
         renameFile = false;
@@ -1555,8 +1555,8 @@ public class ContextMenuTests (ITestOutputHelper output)
         top.Add (menuBar);
         Application.Begin (top);
 
-        Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl));
-        Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithCtrl));
+        Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _));
+        Assert.False (menuBar.HotKeyBindings.TryGet (Key.R.WithCtrl, out _));
         Assert.Null (cm.MenuBar);
 
         Assert.True (Application.RaiseKeyDownEvent (Key.N.WithCtrl));
@@ -1568,10 +1568,10 @@ public class ContextMenuTests (ITestOutputHelper output)
         newFile = false;
 
         cm.Show (menuItems);
-        Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl));
-        Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithCtrl));
-        Assert.False (cm.MenuBar!.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl));
-        Assert.True (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithCtrl));
+        Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _));
+        Assert.False (menuBar.HotKeyBindings.TryGet (Key.R.WithCtrl, out _));
+        Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.N.WithCtrl, out _));
+        Assert.True (cm.MenuBar.HotKeyBindings.TryGet (Key.R.WithCtrl, out _));
 
         Assert.True (cm.MenuBar.IsMenuOpen);
         Assert.True (Application.RaiseKeyDownEvent (Key.N.WithCtrl));
@@ -1584,10 +1584,10 @@ public class ContextMenuTests (ITestOutputHelper output)
         Assert.True (renameFile);
         Assert.False (cm.MenuBar.IsMenuOpen);
 
-        Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl));
-        Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithCtrl));
-        Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl));
-        Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithCtrl));
+        Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _));
+        Assert.False (menuBar.HotKeyBindings.TryGet (Key.R.WithCtrl, out _));
+        Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _));
+        Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.WithCtrl, out _));
 
         newFile = false;
         renameFile = false;
@@ -1634,7 +1634,7 @@ public class ContextMenuTests (ITestOutputHelper output)
         top.Add (menuBar);
         Application.Begin (top);
 
-        Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl));
+        Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _));
         Assert.Null (cm.MenuBar);
 
         Assert.True (Application.RaiseKeyDownEvent (Key.N.WithCtrl));
@@ -1645,8 +1645,8 @@ public class ContextMenuTests (ITestOutputHelper output)
         newMenuBar = false;
 
         cm.Show (menuItems);
-        Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl));
-        Assert.True (cm.MenuBar!.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl));
+        Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _));
+        Assert.True (cm.MenuBar!.HotKeyBindings.TryGet (Key.N.WithCtrl, out _));
 
         Assert.True (cm.MenuBar.IsMenuOpen);
         Assert.True (Application.RaiseKeyDownEvent (Key.N.WithCtrl));
@@ -1657,8 +1657,8 @@ public class ContextMenuTests (ITestOutputHelper output)
         Assert.True (newContextMenu);
         Assert.False (cm.MenuBar!.IsMenuOpen);
 
-        Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl));
-        Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl));
+        Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _));
+        Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _));
 
         newMenuBar = false;
         newContextMenu = false;
@@ -1704,20 +1704,20 @@ public class ContextMenuTests (ITestOutputHelper output)
 
         cm.Show (menuItems);
         Assert.True (cm.MenuBar!.IsMenuOpen);
-        Assert.False (cm.MenuBar!.KeyBindings.Bindings.ContainsKey (Key.N.WithAlt));
-        Assert.False (cm.MenuBar!.KeyBindings.Bindings.ContainsKey (Key.N.NoShift));
-        Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithAlt));
-        Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.NoShift));
-        Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.D.WithAlt));
-        Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.D.NoShift));
+        Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.N.WithAlt, out _));
+        Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.N.NoShift, out _));
+        Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.WithAlt, out _));
+        Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.NoShift, out _));
+        Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.D.WithAlt, out _));
+        Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.D.NoShift, out _));
         Assert.Single (Application.Top!.Subviews);
         View [] menus = Application.Top!.Subviews.Where (v => v is Menu m && m.Host == cm.MenuBar).ToArray ();
-        Assert.True (menus [0].KeyBindings.Bindings.ContainsKey (Key.N.WithAlt));
-        Assert.True (menus [0].KeyBindings.Bindings.ContainsKey (Key.N.NoShift));
-        Assert.True (menus [0].KeyBindings.Bindings.ContainsKey (Key.R.WithAlt));
-        Assert.True (menus [0].KeyBindings.Bindings.ContainsKey (Key.R.NoShift));
-        Assert.True (menus [0].KeyBindings.Bindings.ContainsKey (Key.D.WithAlt));
-        Assert.True (menus [0].KeyBindings.Bindings.ContainsKey (Key.D.NoShift));
+        Assert.True (menus [0].HotKeyBindings.TryGet (Key.N.WithAlt, out _));
+        Assert.True (menus [0].HotKeyBindings.TryGet (Key.N.NoShift, out _));
+        Assert.True (menus [0].HotKeyBindings.TryGet (Key.R.WithAlt, out _));
+        Assert.True (menus [0].HotKeyBindings.TryGet (Key.R.NoShift, out _));
+        Assert.True (menus [0].HotKeyBindings.TryGet (Key.D.WithAlt, out _));
+        Assert.True (menus [0].HotKeyBindings.TryGet (Key.D.NoShift, out _));
 
         Assert.True (Application.RaiseKeyDownEvent (Key.N.WithAlt));
         Assert.False (cm.MenuBar!.IsMenuOpen);
@@ -1734,12 +1734,12 @@ public class ContextMenuTests (ITestOutputHelper output)
         Application.MainLoop!.RunIteration ();
         Assert.True (deleteFile);
 
-        Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithAlt));
-        Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.N.NoShift));
-        Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithAlt));
-        Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.NoShift));
-        Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.D.WithAlt));
-        Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.D.NoShift));
+        Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _));
+        Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.N.NoShift, out _));
+        Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.WithAlt, out _));
+        Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.NoShift, out _));
+        Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.D.WithAlt, out _));
+        Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.D.NoShift, out _));
 
         newFile = false;
         renameFile = false;
@@ -1796,9 +1796,9 @@ public class ContextMenuTests (ITestOutputHelper output)
         top.Add (menuBar);
         Application.Begin (top);
 
-        Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.F.WithAlt));
-        Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithAlt));
-        Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithAlt));
+        Assert.True (menuBar.HotKeyBindings.TryGet (Key.F.WithAlt, out _));
+        Assert.False (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _));
+        Assert.False (menuBar.HotKeyBindings.TryGet (Key.R.WithAlt, out _));
         View [] menus = Application.Top!.Subviews.Where (v => v is Menu m && m.Host == menuBar).ToArray ();
         Assert.Empty (menus);
         Assert.Null (cm.MenuBar);
@@ -1807,7 +1807,7 @@ public class ContextMenuTests (ITestOutputHelper output)
         Assert.True (menuBar.IsMenuOpen);
         Assert.Equal (2, Application.Top!.Subviews.Count);
         menus = Application.Top!.Subviews.Where (v => v is Menu m && m.Host == menuBar).ToArray ();
-        Assert.True (menus [0].KeyBindings.Bindings.ContainsKey (Key.N.WithAlt));
+        Assert.True (menus [0].HotKeyBindings.TryGet (Key.N.WithAlt, out _));
         Assert.True (Application.RaiseKeyDownEvent (Key.N.WithAlt));
         Assert.False (menuBar.IsMenuOpen);
         Assert.False (Application.RaiseKeyDownEvent (Key.R.WithAlt));
@@ -1818,29 +1818,29 @@ public class ContextMenuTests (ITestOutputHelper output)
         newFile = false;
 
         cm.Show (menuItems);
-        Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.F.WithAlt));
-        Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.F.NoShift));
-        Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithAlt));
-        Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.NoShift));
-        Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.E.WithAlt));
-        Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.E.NoShift));
-        Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithAlt));
-        Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.R.NoShift));
+        Assert.True (menuBar.HotKeyBindings.TryGet (Key.F.WithAlt, out _));
+        Assert.True (menuBar.HotKeyBindings.TryGet (Key.F.NoShift, out _));
+        Assert.False (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _));
+        Assert.False (menuBar.HotKeyBindings.TryGet (Key.N.NoShift, out _));
+        Assert.False (menuBar.HotKeyBindings.TryGet (Key.E.WithAlt, out _));
+        Assert.False (menuBar.HotKeyBindings.TryGet (Key.E.NoShift, out _));
+        Assert.False (menuBar.HotKeyBindings.TryGet (Key.R.WithAlt, out _));
+        Assert.False (menuBar.HotKeyBindings.TryGet (Key.R.NoShift, out _));
         Assert.True (cm.MenuBar!.IsMenuOpen);
-        Assert.False (cm.MenuBar!.KeyBindings.Bindings.ContainsKey (Key.F.WithAlt));
-        Assert.False (cm.MenuBar!.KeyBindings.Bindings.ContainsKey (Key.F.NoShift));
-        Assert.False (cm.MenuBar!.KeyBindings.Bindings.ContainsKey (Key.N.WithAlt));
-        Assert.False (cm.MenuBar!.KeyBindings.Bindings.ContainsKey (Key.N.NoShift));
-        Assert.False (cm.MenuBar!.KeyBindings.Bindings.ContainsKey (Key.E.WithAlt));
-        Assert.False (cm.MenuBar!.KeyBindings.Bindings.ContainsKey (Key.E.NoShift));
-        Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithAlt));
-        Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.NoShift));
+        Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.F.WithAlt, out _));
+        Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.F.NoShift, out _));
+        Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.N.WithAlt, out _));
+        Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.N.NoShift, out _));
+        Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.E.WithAlt, out _));
+        Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.E.NoShift, out _));
+        Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.WithAlt, out _));
+        Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.NoShift, out _));
         Assert.Equal (3, Application.Top!.Subviews.Count);
         menus = Application.Top!.Subviews.Where (v => v is Menu m && m.Host == cm.MenuBar).ToArray ();
-        Assert.True (menus [0].KeyBindings.Bindings.ContainsKey (Key.E.WithAlt));
-        Assert.True (menus [0].KeyBindings.Bindings.ContainsKey (Key.E.NoShift));
-        Assert.True (menus [1].KeyBindings.Bindings.ContainsKey (Key.R.WithAlt));
-        Assert.True (menus [1].KeyBindings.Bindings.ContainsKey (Key.R.NoShift));
+        Assert.True (menus [0].HotKeyBindings.TryGet (Key.E.WithAlt, out _));
+        Assert.True (menus [0].HotKeyBindings.TryGet (Key.E.NoShift, out _));
+        Assert.True (menus [1].HotKeyBindings.TryGet (Key.R.WithAlt, out _));
+        Assert.True (menus [1].HotKeyBindings.TryGet (Key.R.NoShift, out _));
         Assert.True (cm.MenuBar.IsMenuOpen);
         Assert.True (Application.RaiseKeyDownEvent (Key.F.WithAlt));
         Assert.False (cm.MenuBar.IsMenuOpen);
@@ -1852,14 +1852,14 @@ public class ContextMenuTests (ITestOutputHelper output)
         Assert.True (cm.MenuBar.IsMenuOpen);
         Assert.Equal (3, Application.Top!.Subviews.Count);
         menus = Application.Top!.Subviews.Where (v => v is Menu m && m.Host == cm.MenuBar).ToArray ();
-        Assert.True (menus [0].KeyBindings.Bindings.ContainsKey (Key.E.WithAlt));
-        Assert.True (menus [0].KeyBindings.Bindings.ContainsKey (Key.E.NoShift));
-        Assert.False (menus [0].KeyBindings.Bindings.ContainsKey (Key.R.WithAlt));
-        Assert.False (menus [0].KeyBindings.Bindings.ContainsKey (Key.R.NoShift));
-        Assert.False (menus [1].KeyBindings.Bindings.ContainsKey (Key.E.WithAlt));
-        Assert.False (menus [1].KeyBindings.Bindings.ContainsKey (Key.E.NoShift));
-        Assert.True (menus [1].KeyBindings.Bindings.ContainsKey (Key.R.WithAlt));
-        Assert.True (menus [1].KeyBindings.Bindings.ContainsKey (Key.R.NoShift));
+        Assert.True (menus [0].HotKeyBindings.TryGet (Key.E.WithAlt, out _));
+        Assert.True (menus [0].HotKeyBindings.TryGet (Key.E.NoShift, out _));
+        Assert.False (menus [0].HotKeyBindings.TryGet (Key.R.WithAlt, out _));
+        Assert.False (menus [0].HotKeyBindings.TryGet (Key.R.NoShift, out _));
+        Assert.False (menus [1].HotKeyBindings.TryGet (Key.E.WithAlt, out _));
+        Assert.False (menus [1].HotKeyBindings.TryGet (Key.E.NoShift, out _));
+        Assert.True (menus [1].HotKeyBindings.TryGet (Key.R.WithAlt, out _));
+        Assert.True (menus [1].HotKeyBindings.TryGet (Key.R.NoShift, out _));
         Assert.True (Application.RaiseKeyDownEvent (Key.E.NoShift));
         Assert.True (Application.RaiseKeyDownEvent (Key.R.WithAlt));
         Assert.False (cm.MenuBar.IsMenuOpen);
@@ -1867,14 +1867,14 @@ public class ContextMenuTests (ITestOutputHelper output)
         Assert.True (renameFile);
 
         Assert.Single (Application.Top!.Subviews);
-        Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.F.WithAlt));
-        Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.F.NoShift));
-        Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithAlt));
-        Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.NoShift));
-        Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.E.WithAlt));
-        Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.E.NoShift));
-        Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithAlt));
-        Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.NoShift));
+        Assert.True (menuBar.HotKeyBindings.TryGet (Key.F.WithAlt, out _));
+        Assert.True (menuBar.HotKeyBindings.TryGet (Key.F.NoShift, out _));
+        Assert.False (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _));
+        Assert.False (menuBar.HotKeyBindings.TryGet (Key.N.NoShift, out _));
+        Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.E.WithAlt, out _));
+        Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.E.NoShift, out _));
+        Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.WithAlt, out _));
+        Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.NoShift, out _));
 
         newFile = false;
         renameFile = false;

+ 9 - 8
UnitTests/Views/MenuBarTests.cs

@@ -19,8 +19,9 @@ public class MenuBarTests (ITestOutputHelper output)
         menuBar.Menus = [menuBarItem];
         Assert.Single (menuBar.Menus);
         Assert.Single (menuBar.Menus [0].Children!);
-        Assert.Contains (Key.N.WithAlt, menuBar.KeyBindings.Bindings);
-        Assert.DoesNotContain (Key.I, menuBar.KeyBindings.Bindings);
+
+        Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _));
+        Assert.False (menuBar.HotKeyBindings.TryGet (Key.I, out _));
 
         var top = new Toplevel ();
         top.Add (menuBar);
@@ -39,12 +40,12 @@ public class MenuBarTests (ITestOutputHelper output)
         menuItem.RemoveMenuItem ();
         Assert.Single (menuBar.Menus);
         Assert.Null (menuBar.Menus [0].Children);
-        Assert.Contains (Key.N.WithAlt, menuBar.KeyBindings.Bindings);
-        Assert.DoesNotContain (Key.I, menuBar.KeyBindings.Bindings);
+        Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _));
+        Assert.False (menuBar.HotKeyBindings.TryGet (Key.I, out _));
 
         menuBarItem.RemoveMenuItem ();
         Assert.Empty (menuBar.Menus);
-        Assert.DoesNotContain (Key.N.WithAlt, menuBar.KeyBindings.Bindings);
+        Assert.False (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _));
 
         top.Dispose ();
     }
@@ -2998,12 +2999,12 @@ Edit
             ]
         };
 
-        Assert.Contains (Key.A.WithCtrl, menuBar.KeyBindings.Bindings);
+        Assert.True (menuBar.HotKeyBindings.TryGet (Key.A.WithCtrl, out _));
 
         menuBar.Menus [0].Children! [0].ShortcutKey = Key.B.WithCtrl;
 
-        Assert.DoesNotContain (Key.A.WithCtrl, menuBar.KeyBindings.Bindings);
-        Assert.Contains (Key.B.WithCtrl, menuBar.KeyBindings.Bindings);
+        Assert.False (menuBar.HotKeyBindings.TryGet (Key.A.WithCtrl, out _));
+        Assert.True (menuBar.HotKeyBindings.TryGet (Key.B.WithCtrl, out _));
     }
 
     [Fact]

+ 29 - 24
UnitTests/Views/RadioGroupTests.cs

@@ -64,24 +64,28 @@ public class RadioGroupTests (ITestOutputHelper output)
     }
 
     [Fact]
-    public void KeyBindings_Are_Added_Correctly ()
+    public void HotKeyBindings_Are_Added_Correctly ()
     {
         var rg = new RadioGroup { RadioLabels = new [] { "_Left", "_Right" } };
-        Assert.NotEmpty (rg.KeyBindings.GetCommands (Key.L));
-        Assert.NotEmpty (rg.KeyBindings.GetCommands (Key.R));
+        Assert.NotEmpty (rg.HotKeyBindings.GetCommands (Key.L));
+        Assert.NotEmpty (rg.HotKeyBindings.GetCommands (Key.R));
 
-        Assert.NotEmpty (rg.KeyBindings.GetCommands (Key.L.WithShift));
-        Assert.NotEmpty (rg.KeyBindings.GetCommands (Key.L.WithAlt));
+        Assert.NotEmpty (rg.HotKeyBindings.GetCommands (Key.L.WithShift));
+        Assert.NotEmpty (rg.HotKeyBindings.GetCommands (Key.L.WithAlt));
 
-        Assert.NotEmpty (rg.KeyBindings.GetCommands (Key.R.WithShift));
-        Assert.NotEmpty (rg.KeyBindings.GetCommands (Key.R.WithAlt));
+        Assert.NotEmpty (rg.HotKeyBindings.GetCommands (Key.R.WithShift));
+        Assert.NotEmpty (rg.HotKeyBindings.GetCommands (Key.R.WithAlt));
     }
 
     [Fact]
     public void Commands_HasFocus ()
     {
         Application.Navigation = new ();
-        var rg = new RadioGroup { RadioLabels = new [] { "Test", "New Test" } };
+        var rg = new RadioGroup
+        {
+            Id = "rg",
+            RadioLabels = ["Test", "New Test"]
+        };
         Application.Top = new ();
         Application.Top.Add (rg);
         rg.SetFocus ();
@@ -201,7 +205,7 @@ public class RadioGroupTests (ITestOutputHelper output)
     public void HotKey_HasFocus_False ()
     {
         Application.Navigation = new ();
-        var rg = new RadioGroup { RadioLabels = new [] { "Test", "New Test" } };
+        var rg = new RadioGroup { RadioLabels = ["Test", "New Test"] };
         Application.Top = new ();
 
         // With !HasFocus
@@ -278,7 +282,7 @@ public class RadioGroupTests (ITestOutputHelper output)
     public void HotKeys_HasFocus_False_Does_Not_SetFocus_Selects ()
     {
         Application.Navigation = new ();
-        var rg = new RadioGroup { RadioLabels = new [] { "Item _A", "Item _B" } };
+        var rg = new RadioGroup { RadioLabels = ["Item _A", "Item _B"] };
         Application.Top = new ();
 
         // With !HasFocus
@@ -363,14 +367,14 @@ public class RadioGroupTests (ITestOutputHelper output)
     [Fact]
     public void HotKeys_HasFocus_True_Selects ()
     {
-        var rg = new RadioGroup { RadioLabels = new [] { "_Left", "_Right", "Cen_tered", "_Justified" } };
+        var rg = new RadioGroup { RadioLabels = ["_Left", "_Right", "Cen_tered", "_Justified"] };
         Application.Top = new ();
         Application.Top.Add (rg);
         rg.SetFocus ();
 
-        Assert.NotEmpty (rg.KeyBindings.GetCommands (KeyCode.L));
-        Assert.NotEmpty (rg.KeyBindings.GetCommands (KeyCode.L | KeyCode.ShiftMask));
-        Assert.NotEmpty (rg.KeyBindings.GetCommands (KeyCode.L | KeyCode.AltMask));
+        Assert.NotEmpty (rg.HotKeyBindings.GetCommands (KeyCode.L));
+        Assert.NotEmpty (rg.HotKeyBindings.GetCommands (KeyCode.L | KeyCode.ShiftMask));
+        Assert.NotEmpty (rg.HotKeyBindings.GetCommands (KeyCode.L | KeyCode.AltMask));
 
         Assert.True (Application.RaiseKeyDownEvent (Key.T));
         Assert.Equal (2, rg.SelectedItem);
@@ -425,7 +429,7 @@ public class RadioGroupTests (ITestOutputHelper output)
         var group = new RadioGroup
         {
             Title = "Radio_Group",
-            RadioLabels = new [] { "_Left", "_Right", "Cen_tered", "_Justified" }
+            RadioLabels = ["_Left", "_Right", "Cen_tered", "_Justified"]
         };
         superView.Add (group);
 
@@ -450,7 +454,7 @@ public class RadioGroupTests (ITestOutputHelper output)
         var group = new RadioGroup
         {
             Title = "Radio_Group",
-            RadioLabels = new [] { "_Left", "_Right", "Cen_tered", "_Justified" }
+            RadioLabels = ["_Left", "_Right", "Cen_tered", "_Justified"]
         };
         group.SelectedItem = -1;
 
@@ -488,7 +492,7 @@ public class RadioGroupTests (ITestOutputHelper output)
     [Fact]
     public void HotKey_Command_Does_Not_Accept ()
     {
-        var group = new RadioGroup { RadioLabels = new [] { "_Left", "_Right", "Cen_tered", "_Justified" } };
+        var group = new RadioGroup { RadioLabels = ["_Left", "_Right", "Cen_tered", "_Justified"] };
         var accepted = false;
 
         group.Accepting += OnAccept;
@@ -504,7 +508,7 @@ public class RadioGroupTests (ITestOutputHelper output)
     [Fact]
     public void Accept_Command_Fires_Accept ()
     {
-        var group = new RadioGroup { RadioLabels = new [] { "_Left", "_Right", "Cen_tered", "_Justified" } };
+        var group = new RadioGroup { RadioLabels = ["_Left", "_Right", "Cen_tered", "_Justified"] };
         var accepted = false;
 
         group.Accepting += OnAccept;
@@ -521,7 +525,7 @@ public class RadioGroupTests (ITestOutputHelper output)
     [AutoInitShutdown]
     public void Orientation_Width_Height_Vertical_Horizontal_Space ()
     {
-        var rg = new RadioGroup { RadioLabels = new [] { "Test", "New Test 你" } };
+        var rg = new RadioGroup { RadioLabels = ["Test", "New Test 你"] };
         var win = new Window { Width = Dim.Fill (), Height = Dim.Fill () };
         win.Add (rg);
         var top = new Toplevel ();
@@ -597,7 +601,7 @@ public class RadioGroupTests (ITestOutputHelper output)
     {
         int previousSelectedItem = -1;
         int selectedItem = -1;
-        var rg = new RadioGroup { RadioLabels = new [] { "Test", "New Test" } };
+        var rg = new RadioGroup { RadioLabels = ["Test", "New Test"] };
 
         rg.SelectedItemChanged += (s, e) =>
                                   {
@@ -664,7 +668,7 @@ public class RadioGroupTests (ITestOutputHelper output)
 
     [Fact]
     [SetupFakeDriver]
-    public void Mouse_DoubleClick ()
+    public void Mouse_DoubleClick_Accepts ()
     {
         var radioGroup = new RadioGroup
         {
@@ -701,12 +705,13 @@ public class RadioGroupTests (ITestOutputHelper output)
         // NOTE: We need to do the same
 
         Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked }));
-        Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1DoubleClicked }));
+        Assert.False (radioGroup.NewMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1DoubleClicked }));
         Assert.Equal (0, radioGroup.SelectedItem);
         Assert.Equal (0, selectedItemChanged);
         Assert.Equal (0, selectingCount);
         Assert.Equal (1, acceptedCount);
 
+        // single click twice
         Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1Clicked }));
         Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1Clicked }));
         Assert.Equal (1, radioGroup.SelectedItem);
@@ -715,7 +720,7 @@ public class RadioGroupTests (ITestOutputHelper output)
         Assert.Equal (1, acceptedCount);
 
         Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1Clicked }));
-        Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1DoubleClicked }));
+        Assert.False (radioGroup.NewMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1DoubleClicked }));
         Assert.Equal (1, radioGroup.SelectedItem);
         Assert.Equal (1, selectedItemChanged);
         Assert.Equal (1, selectingCount);
@@ -762,7 +767,7 @@ public class RadioGroupTests (ITestOutputHelper output)
 
         radioGroup.DoubleClickAccepts = false;
         Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1Clicked }));
-        Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1DoubleClicked }));
+        Assert.False (radioGroup.NewMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1DoubleClicked }));
     }
 
     #endregion Mouse Tests

+ 19 - 19
UnitTests/Views/ShortcutTests.cs

@@ -100,7 +100,7 @@ public class ShortcutTests
             Title = command,
         };
 
-        shortcut.Layout();
+        shortcut.Layout ();
 
         // |0123456789
         // | C  H  K |
@@ -281,47 +281,47 @@ public class ShortcutTests
         var shortcut = new Shortcut ();
 
         shortcut.Key = Key.A;
-        Assert.Contains (Key.A, shortcut.KeyBindings.Bindings.Keys);
+        Assert.True (shortcut.HotKeyBindings.TryGet (Key.A, out _));
 
         shortcut.Key = Key.B;
-        Assert.DoesNotContain (Key.A, shortcut.KeyBindings.Bindings.Keys);
-        Assert.Contains (Key.B, shortcut.KeyBindings.Bindings.Keys);
+        Assert.False (shortcut.HotKeyBindings.TryGet (Key.A, out _));
+        Assert.True (shortcut.HotKeyBindings.TryGet (Key.B, out _));
     }
 
     // Test Key gets bound correctly
     [Fact]
-    public void KeyBindingScope_Defaults_To_HotKey ()
+    public void BindKeyToApplication_Defaults_To_HotKey ()
     {
         var shortcut = new Shortcut ();
 
-        Assert.Equal (KeyBindingScope.HotKey, shortcut.KeyBindingScope);
+        Assert.False (shortcut.BindKeyToApplication);
     }
 
     [Fact]
-    public void KeyBindingScope_Can_Be_Set ()
+    public void BindKeyToApplication_Can_Be_Set ()
     {
         var shortcut = new Shortcut ();
 
-        shortcut.KeyBindingScope = KeyBindingScope.Application;
+        shortcut.BindKeyToApplication = true;
 
-        Assert.Equal (KeyBindingScope.Application, shortcut.KeyBindingScope);
+        Assert.True (shortcut.BindKeyToApplication);
     }
 
     [Fact]
-    public void KeyBindingScope_Changing_Adjusts_KeyBindings ()
+    public void BindKeyToApplication_Changing_Adjusts_KeyBindings ()
     {
         var shortcut = new Shortcut ();
 
         shortcut.Key = Key.A;
-        Assert.Contains (Key.A, shortcut.KeyBindings.Bindings.Keys);
+        Assert.True (shortcut.HotKeyBindings.TryGet (Key.A, out _));
 
-        shortcut.KeyBindingScope = KeyBindingScope.Application;
-        Assert.DoesNotContain (Key.A, shortcut.KeyBindings.Bindings.Keys);
-        Assert.Contains (Key.A, Application.KeyBindings.Bindings.Keys);
+        shortcut.BindKeyToApplication = true;
+        Assert.False (shortcut.HotKeyBindings.TryGet (Key.A, out _));
+        Assert.True (Application.KeyBindings.TryGet (Key.A, out _));
 
-        shortcut.KeyBindingScope = KeyBindingScope.HotKey;
-        Assert.Contains (Key.A, shortcut.KeyBindings.Bindings.Keys);
-        Assert.DoesNotContain (Key.A, Application.KeyBindings.Bindings.Keys);
+        shortcut.BindKeyToApplication = false;
+        Assert.True (shortcut.HotKeyBindings.TryGet (Key.A, out _));
+        Assert.False (Application.KeyBindings.TryGet (Key.A, out _));
     }
 
     [Theory]
@@ -789,7 +789,7 @@ public class ShortcutTests
         var shortcut = new Shortcut
         {
             Key = Key.A,
-            KeyBindingScope = KeyBindingScope.Application,
+            BindKeyToApplication = true,
             Text = "0",
             Title = "_C"
         };
@@ -867,7 +867,7 @@ public class ShortcutTests
         var shortcut = new Shortcut
         {
             Key = Key.A,
-            KeyBindingScope = KeyBindingScope.Application,
+            BindKeyToApplication = true,
             Text = "0",
             Title = "_C",
             CanFocus = canFocus

+ 1 - 1
UnitTests/Views/TextViewTests.cs

@@ -6999,7 +6999,7 @@ TAB to jump between text field",
         {
             Width = Dim.Fill (), Height = Dim.Fill (), Text = "This is the first line.\nThis is the second line.\n"
         };
-        tv.UnwrappedCursorPosition += (s, e) => { cp = e.Point; };
+        tv.UnwrappedCursorPosition += (s, e) => { cp = e; };
         var top = new Toplevel ();
         top.Add (tv);
         Application.Begin (top);

+ 2 - 2
UnitTests/Views/TimeFieldTests.cs

@@ -147,8 +147,8 @@ public class TimeFieldTests
         Assert.True (tf.NewKeyDownEvent (Key.End));
         Assert.Equal (8, tf.CursorPosition);
         Assert.True (tf.NewKeyDownEvent (Key.A.WithCtrl));
-        Assert.Equal (9, tf.CursorPosition);
-        Assert.Equal (tf.SelectedLength, tf.Text.Length);
+        Assert.Equal (1, tf.CursorPosition);
+        Assert.Equal (9, tf.Text.Length);
         Assert.True (tf.NewKeyDownEvent (Key.E.WithCtrl));
         Assert.Equal (8, tf.CursorPosition);
         Assert.True (tf.NewKeyDownEvent (Key.CursorLeft));

+ 4 - 0
docfx/docs/View.md

@@ -14,6 +14,10 @@
   
   * *Parent View* - A view that holds a reference to another view in a parent/child relationship, but is NOT a SuperView of the child. Terminal.Gui uses the terms "Child" and "Parent" sparingly. Generally Subview/SuperView is preferred.
   
+### Input
+
+See the [Keyboard Deep Dive](keyboard.md) and [Mouse Deep Dive](mouse.md).
+
 ### Layout and Arrangement
 
 See the [Layout Deep Dive](layout.md) and the [Arrangement Deep Dive](arrangement.md).

+ 39 - 14
docfx/docs/keyboard.md

@@ -1,4 +1,4 @@
-# Keyboard Events
+# Keyboard Deep Dive
 
 ## Tenets for Terminal.Gui Keyboard Handling (Unless you know better ones...)
 
@@ -14,19 +14,34 @@ Tenets higher in the list have precedence over tenets lower in the list.
 
 * **If It's Hot, It Works** - If a View with a @Terminal.Gui.View.HotKey is visible, and the HotKey is visible, the user should be able to press that HotKey and whatever behavior is defined for it should work. For example, in v1, when a Modal view was active, the HotKeys on MenuBar continued to show "hot". In v2 we strive to ensure this doesn't happen.
 
-
 ## Keyboard APIs
 
 *Terminal.Gui* provides the following APIs for handling keyboard input:
 
-### **[Key](~/api/Terminal.Gui.Key.yml)**
-
-The `Key` class provides a platform-independent abstraction for common keyboard operations. It is used for processing keyboard input and raising keyboard events. This class provides a high-level abstraction with helper methods and properties for common keyboard operations. Use this class instead of the low-level `KeyCode` enum when possible.
+* **Key** - @Terminal.Gui.Key provides a platform-independent abstraction for common keyboard operations. It is used for processing keyboard input and raising keyboard events. This class provides a high-level abstraction with helper methods and properties for common keyboard operations. Use this class instead of the low-level `KeyCode` enum when possible.
+* **Key Bindings** - Key Bindings provide a declarative method for handling keyboard input in View implementations. The View calls @Terminal.Gui.AddCommand to declare it supports a particular command and then uses @Terminal.Gui.KeyBindings to indicate which key presses will invoke the command. 
+* **Key Events** - The Key Bindings API is rich enough to support the vast majority of use-cases. However, in some cases subscribing directly to key events is needed (e.g. when capturing arbitrary typing by a user). Use @Terminal.Gui.View.KeyDown and related events in these cases.
 
-See [Key](~/api/Terminal.Gui.Key.yml) for more details.
+Each of these APIs are described more fully below.
 
 ### **[Key Bindings](~/api/Terminal.Gui.KeyBindings.yml)**
 
+Key Bindings is the preferred way of handling keyboard input in View implementations. The View calls @Terminal.Gui.AddCommand to declare it supports a particular command and then uses @Terminal.Gui.KeyBindings to indicate which key presses will invoke the command. For example, if a View wants to respond to the user pressing the up arrow key to scroll up it would do this
+
+```cs
+public MyView : View
+{
+  AddCommand (Command.ScrollUp, () => ScrollVertical (-1));
+  KeyBindings.Add (Key.CursorUp, Command.ScrollUp);
+}
+```
+
+The `Character Map` Scenario includes a View called `CharMap` that is a good example of the Key Bindings API. 
+
+The [Command](~/api/Terminal.Gui.Command.yml) enum lists generic operations that are implemented by views. For example `Command.Accept` in a `Button` results in the `Accepting` event 
+firing while in `TableView` it is bound to `CellActivated`. Not all commands
+are implemented by all views (e.g. you cannot scroll in a `Button`). Use the @Terminal.Gui.View.GetSupportedCommands method to determine which commands are implemented by a `View`. 
+
 The default key for activating a button is `Space`. You can change this using 
 `KeyBindings.ReplaceKey()`:
 
@@ -35,19 +50,29 @@ var btn = new Button () { Title = "Press me" };
 btn.KeyBindings.ReplaceKey (btn.KeyBindings.GetKeyFromCommands (Command.Accept));
 ```
 
-The [Command](~/api/Terminal.Gui.Command.yml) enum lists generic operations that are implemented by views. For example `Command.Accept` in a `Button` results in the `Clicked` event 
-firing while in `TableView` it is bound to `CellActivated`. Not all commands
-are implemented by all views (e.g. you cannot scroll in a `Button`). Use the @Terminal.Gui.View.GetSupportedCommands() method to determine which commands are implemented by a `View`. 
+Key Bindings can be added at the `Application` or `View` level. 
+
+For **Application-scoped Key Bindings** there are two categories of Application-scoped Key Bindings:
+
+1) **Application Command Key Bindings** - Bindings for `Command`s supported by @Terminal.Gui.Application. For example, @Terminal.Gui.Application.QuitKey, which is bound to `Command.Quit` and results in @Terminal.Gui.Application.RequestStop 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. 
+
+Use @Terminal.Gui.Application.KeyBindings to add or modify Application-scoped Key Bindings.
+
+**View-scoped Key Bindings** also have two categories:
+
+1) **HotKey Bindings** - These bind to `Command`s that will be invoked regardless of whether the View has focus or not. The most common use-case for `HotKey` bindings is @Terminal.Gui.View.HotKey. For example, a `Button` with a `Title` of `_OK`, the user can press `Alt-O` and the button will be accepted regardless of whether it has focus or not. Add and modify HotKey bindings with @Terminal.Gui.View.HotKeyBindings.
+2) **Focused Bindings** - These bind to `Command`s that will be invoked only when the View has focus. Focused Key Bindings are the easiest way to enable a View to support responding to key events. Add and modify Focused bindings with @Terminal.Gui.View.KeyBindings.
 
-Key Bindings can be added at the `Application` or `View` level. For Application-scoped Key Bindings see @Terminal.Gui.Application.Navigation. For View-scoped Key Bindings see @Terminal.Gui.View.KeyBindings.
+**Application-Scoped** Key Bindings 
 
-### **@"Terminal.Gui.View.HotKey"** 
+### HotKey
 
 A **HotKey** is a key press that selects a visible UI item. For selecting items across `View`s (e.g. a `Button` in a `Dialog`) the key press must have the `Alt` modifier. For selecting items within a `View` that are not `View`s themselves, the key press can be key without the `Alt` modifier.  For example, in a `Dialog`, a `Button` with the text of "_Text" can be selected with `Alt+T`. Or, in a `Menu` with "_File _Edit", `Alt+F` will select (show) the "_File" menu. If the "_File" menu has a sub-menu of "_New" `Alt+N` or `N` will ONLY select the "_New" sub-menu if the "_File" menu is already opened.
 
 By default, the `Text` of a `View` is used to determine the `HotKey` by looking for the first occurrence of the @Terminal.Gui.View.HotKeySpecifier (which is underscore (`_`) by default). The character following the underscore is the `HotKey`. If the `HotKeySpecifier` is not found in `Text`, the first character of `Text` is used as the `HotKey`. The `Text` of a `View` can be changed at runtime, and the `HotKey` will be updated accordingly. @"Terminal.Gui.View.HotKey" is `virtual` enabling this behavior to be customized.
 
-### **[Shortcut](~/api/Terminal.Gui.Shortcut.yml)**
+### **Shortcut**
 
 A **Shortcut** is an opinionated (visually & API) View for displaying a command, help text, key key press that invokes a [Command](~/api/Terminal.Gui.Command.yml).
 
@@ -57,7 +82,7 @@ The Command can be invoked even if the `View` that defines them is not focused o
 
 [MenuBar](~/api/Terminal.Gui.MenuBar.yml), [ContextMenu](~/api/Terminal.Gui.ContextMenu.yml), and [StatusBar](~/api/Terminal.Gui.StatusBar.yml) support `Shortcut`s. 
 
-### **Handling Keyboard Events**
+### **Key Events**
 
 Keyboard events are retrieved from [Console Drivers](drivers.md) each iteration of the [Application](~/api/Terminal.Gui.Application.yml) [Main Loop](mainloop.md). The console driver raises the @Terminal.Gui.ConsoleDriver.KeyDown and @Terminal.Gui.ConsoleDriver.KeyUp events which invoke @Terminal.Gui.Application.RaiseKeyDown(Terminal.Gui.Key) and @Terminal.Gui.Application.RaiseKeyUp(Terminal.Gui.Key) respectively.
 
@@ -116,7 +141,7 @@ To define application key handling logic for an entire application in cases wher
 
 ## View
 
-* Implements support for `KeyBindingScope.View` and `KeyBindingScope.HotKey`.
+* 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 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.
 

+ 24 - 0
docfx/docs/mouse.md

@@ -10,6 +10,30 @@ Tenets higher in the list have precedence over tenets lower in the list.
 
 ## Mouse APIs
 
+*Terminal.Gui* provides the following APIs for handling mouse input:
+
+* **MouseEventArgs** - @Terminal.Gui.MouseEventArgs provides a platform-independent abstraction for common mouse operations. It is used for processing mouse input and raising mouse events.
+* **Mouse Bindings** - Mouse Bindings provide a declarative method for handling mouse input in View implementations. The View calls @Terminal.Gui.AddCommand to declare it supports a particular command and then uses @Terminal.Gui.MouseBindings to indicate which mouse events will invoke the command. 
+* **Mouse Events** - The Mouse Bindings API is rich enough to support the  majority of use-cases. However, in some cases subscribing directly to key events is needed (e.g. drag & drop). Use @Terminal.Gui.View.MouseEvent and related events in these cases.
+
+Each of these APIs are described more fully below.
+
+## Mouse Bindings
+
+Mouse Bindings is the preferred way of handling mouse input in View implementations. The View calls @Terminal.Gui.AddCommand to declare it supports a particular command and then uses @Terminal.Gui.MouseBindings to indicate which mouse events will invoke the command. For example, if a View wants to respond to the user using the mouse wheel to scroll up, it would do this:
+
+```cs
+public MyView : View
+{
+  AddCommand (Command.ScrollUp, () => ScrollVertical (-1));
+  MouseBindings.Add (MouseFlags.Button1DoubleClick, Command.ScrollUp);
+}
+```
+
+The [Command](~/api/Terminal.Gui.Command.yml) enum lists generic operations that are implemented by views. 
+
+## Mouse Events
+
 At the core of *Terminal.Gui*'s mouse API is the @Terminal.Gui.MouseEventArgs class. The @Terminal.Gui.MouseEventArgs class provides a platform-independent abstraction for common mouse events. Every mouse event can be fully described in a @Terminal.Gui.MouseEventArgs instance, and most of the mouse-related APIs are simply helper functions for decoding a @Terminal.Gui.MouseEventArgs.
 
 When the user does something with the mouse, the `ConsoleDriver` maps the platform-specific mouse event into a `MouseEventArgs` and calls `Application.RaiseMouseEvent`. Then, `Application.RaiseMouseEvent` determines which `View` the event should go to. The `View.OnMouseEvent` method can be overridden or the `View.MouseEvent` event can be subscribed to, to handle the low-level mouse event. If the low-level event is not handled by a view, `Application` will then call the appropriate high-level helper APIs. For example, if the user double-clicks the mouse, `View.OnMouseClick` will be called/`View.MouseClick` will be raised with the event arguments indicating which mouse button was double-clicked.