Sfoglia il codice sorgente

Merge pull request #3532 from tig/v2-KeyBinding-Refinements

Improves `KeyBindings`
Tig 1 anno fa
parent
commit
0ff8defb8c
54 ha cambiato i file con 3219 aggiunte e 2811 eliminazioni
  1. 0 51
      Terminal.Gui/Application.MainLoopSyncContext.cs
  2. 12 519
      Terminal.Gui/Application/Application.cs
  3. 298 0
      Terminal.Gui/Application/ApplicationKeyboard.cs
  4. 302 0
      Terminal.Gui/Application/ApplicationMouse.cs
  5. 0 0
      Terminal.Gui/Application/IterationEventArgs.cs
  6. 0 0
      Terminal.Gui/Application/MainLoop.cs
  7. 48 0
      Terminal.Gui/Application/MainLoopSyncContext.cs
  8. 0 0
      Terminal.Gui/Application/RunState.cs
  9. 0 0
      Terminal.Gui/Application/RunStateEventArgs.cs
  10. 0 0
      Terminal.Gui/Application/Timeout.cs
  11. 0 0
      Terminal.Gui/Application/TimeoutEventArgs.cs
  12. 0 22
      Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
  13. 41 0
      Terminal.Gui/Input/CommandContext.cs
  14. 14 240
      Terminal.Gui/Input/KeyBinding.cs
  15. 46 0
      Terminal.Gui/Input/KeyBindingScope.cs
  16. 258 0
      Terminal.Gui/Input/KeyBindings.cs
  17. 1 0
      Terminal.Gui/Terminal.Gui.csproj
  18. 1 1
      Terminal.Gui/View/Layout/DimAutoStyle.cs
  19. 53 0
      Terminal.Gui/View/Layout/PosAlign.cs
  20. 8 26
      Terminal.Gui/View/View.cs
  21. 4 1
      Terminal.Gui/View/ViewAdornments.cs
  22. 75 30
      Terminal.Gui/View/ViewKeyboard.cs
  23. 4 0
      Terminal.Gui/View/ViewMouse.cs
  24. 9 0
      Terminal.Gui/View/ViewText.cs
  25. 43 473
      Terminal.Gui/Views/Menu/Menu.cs
  26. 127 450
      Terminal.Gui/Views/Menu/MenuBar.cs
  27. 178 0
      Terminal.Gui/Views/Menu/MenuBarItem.cs
  28. 31 0
      Terminal.Gui/Views/Menu/MenuClosingEventArgs.cs
  29. 0 73
      Terminal.Gui/Views/Menu/MenuEventArgs.cs
  30. 273 0
      Terminal.Gui/Views/Menu/MenuItem.cs
  31. 15 0
      Terminal.Gui/Views/Menu/MenuItemCheckStyle.cs
  32. 20 0
      Terminal.Gui/Views/Menu/MenuOpenedEventArgs.cs
  33. 24 0
      Terminal.Gui/Views/Menu/MenuOpeningEventArgs.cs
  34. 189 197
      Terminal.Gui/Views/RadioGroup.cs
  35. 11 94
      Terminal.Gui/Views/StatusBar.cs
  36. 59 0
      Terminal.Gui/Views/StatusItem.cs
  37. 3 2
      Terminal.Gui/Views/Toplevel.cs
  38. 15 4
      UICatalog/Scenarios/AllViewsTester.cs
  39. 173 157
      UICatalog/Scenarios/ContextMenus.cs
  40. 33 23
      UICatalog/Scenarios/DynamicMenuBar.cs
  41. 150 188
      UICatalog/Scenarios/Editor.cs
  42. 191 0
      UICatalog/Scenarios/KeyBindings.cs
  43. 34 24
      UICatalog/Scenarios/MenuBarScenario.cs
  44. 2 2
      UICatalog/UICatalog.cs
  45. 4 0
      UnitTests/Application/ApplicationTests.cs
  46. 100 30
      UnitTests/Application/KeyboardTests.cs
  47. 48 16
      UnitTests/Input/KeyBindingTests.cs
  48. 1 3
      UnitTests/TestHelpers.cs
  49. 21 18
      UnitTests/UICatalog/ScenarioTests.cs
  50. 2 0
      UnitTests/View/MouseTests.cs
  51. 7 10
      UnitTests/Views/MenuBarTests.cs
  52. 52 19
      UnitTests/Views/RadioGroupTests.cs
  53. 1 1
      UnitTests/Views/StatusBarTests.cs
  54. 238 137
      UnitTests/Views/TextViewTests.cs

+ 0 - 51
Terminal.Gui/Application.MainLoopSyncContext.cs

@@ -1,51 +0,0 @@
-namespace Terminal.Gui;
-
-public static partial class Application
-{
-    /// <summary>
-    ///     provides the sync context set while executing code in Terminal.Gui, to let
-    ///     users use async/await on their code
-    /// </summary>
-    private sealed class MainLoopSyncContext : SynchronizationContext
-    {
-        public override SynchronizationContext CreateCopy () { return new MainLoopSyncContext (); }
-
-        public override void Post (SendOrPostCallback d, object state)
-        {
-            MainLoop?.AddIdle (
-                              () =>
-                              {
-                                  d (state);
-
-                                  return false;
-                              }
-                             );
-        }
-
-        //_mainLoop.Driver.Wakeup ();
-        public override void Send (SendOrPostCallback d, object state)
-        {
-            if (Thread.CurrentThread.ManagedThreadId == _mainThreadId)
-            {
-                d (state);
-            }
-            else
-            {
-                var wasExecuted = false;
-
-                Invoke (
-                        () =>
-                        {
-                            d (state);
-                            wasExecuted = true;
-                        }
-                       );
-
-                while (!wasExecuted)
-                {
-                    Thread.Sleep (15);
-                }
-            }
-        }
-    }
-}

+ 12 - 519
Terminal.Gui/Application.cs → Terminal.Gui/Application/Application.cs

@@ -99,7 +99,6 @@ public static partial class Application
         // Don't dispose the Top. It's up to caller dispose it
         if (Top is { })
         {
-
             Debug.Assert (Top.WasDisposed);
 
             // If End wasn't called _cachedRunStateToplevel may be null
@@ -158,6 +157,7 @@ public static partial class Application
         KeyDown = null;
         KeyUp = null;
         SizeChanging = null;
+        ClearKeyBindings ();
 
         Colors.Reset ();
 
@@ -539,6 +539,7 @@ public static partial class Application
             toplevel.SetNeedsDisplay ();
             toplevel.Draw ();
             Driver.UpdateScreen ();
+
             if (PositionCursor (toplevel))
             {
                 Driver.UpdateCursor ();
@@ -551,13 +552,14 @@ public static partial class Application
     }
 
     /// <summary>
-    /// Calls <see cref="View.PositionCursor"/> on the most focused view in the view starting with <paramref name="view"/>.
+    ///     Calls <see cref="View.PositionCursor"/> on the most focused view in the view starting with <paramref name="view"/>.
     /// </summary>
     /// <remarks>
-    /// Does nothing if <paramref name="view"/> is <see langword="null"/> or if the most focused view is not visible or enabled.
-    /// <para>
-    /// If the most focused view is not visible within it's superview, the cursor will be hidden.
-    /// </para>
+    ///     Does nothing if <paramref name="view"/> is <see langword="null"/> or if the most focused view is not visible or
+    ///     enabled.
+    ///     <para>
+    ///         If the most focused view is not visible within it's superview, the cursor will be hidden.
+    ///     </para>
     /// </remarks>
     /// <returns><see langword="true"/> if a view positioned the cursor and the position is visible.</returns>
     internal static bool PositionCursor (View view)
@@ -581,6 +583,7 @@ public static partial class Application
         if (!mostFocused.Visible || !mostFocused.Enabled)
         {
             Driver.GetCursorVisibility (out CursorVisibility current);
+
             if (current != CursorVisibility.Invisible)
             {
                 Driver.SetCursorVisibility (CursorVisibility.Invisible);
@@ -592,6 +595,7 @@ public static partial class Application
         // If the view is not visible within it's superview, don't position the cursor
         Rectangle mostFocusedViewport = mostFocused.ViewportToScreen (mostFocused.Viewport with { Location = Point.Empty });
         Rectangle superViewViewport = mostFocused.SuperView?.ViewportToScreen (mostFocused.SuperView.Viewport with { Location = Point.Empty }) ?? Driver.Screen;
+
         if (!superViewViewport.IntersectsWith (mostFocusedViewport))
         {
             return false;
@@ -673,7 +677,7 @@ public static partial class Application
     /// </param>
     /// <returns>The created T object. The caller is responsible for disposing this object.</returns>
     public static T Run<T> (Func<Exception, bool> errorHandler = null, ConsoleDriver driver = null)
-        where T : Toplevel, new()
+        where T : Toplevel, new ()
     {
         var top = new T ();
 
@@ -960,6 +964,7 @@ public static partial class Application
         {
             state.Toplevel.Draw ();
             Driver.UpdateScreen ();
+
             //Driver.UpdateCursor ();
         }
 
@@ -1420,516 +1425,4 @@ public static partial class Application
     }
 
     #endregion Toplevel handling
-
-    #region Mouse handling
-
-    /// <summary>Disable or enable the mouse. The mouse is enabled by default.</summary>
-    [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
-    public static bool IsMouseDisabled { get; set; }
-
-    /// <summary>The current <see cref="View"/> object that wants continuous mouse button pressed events.</summary>
-    public static View WantContinuousButtonPressedView { get; private set; }
-
-    /// <summary>
-    ///     Gets the view that grabbed the mouse (e.g. for dragging). When this is set, all mouse events will be routed to
-    ///     this view until the view calls <see cref="UngrabMouse"/> or the mouse is released.
-    /// </summary>
-    public static View MouseGrabView { get; private set; }
-
-    /// <summary>Invoked when a view wants to grab the mouse; can be canceled.</summary>
-    public static event EventHandler<GrabMouseEventArgs> GrabbingMouse;
-
-    /// <summary>Invoked when a view wants un-grab the mouse; can be canceled.</summary>
-    public static event EventHandler<GrabMouseEventArgs> UnGrabbingMouse;
-
-    /// <summary>Invoked after a view has grabbed the mouse.</summary>
-    public static event EventHandler<ViewEventArgs> GrabbedMouse;
-
-    /// <summary>Invoked after a view has un-grabbed the mouse.</summary>
-    public static event EventHandler<ViewEventArgs> UnGrabbedMouse;
-
-    /// <summary>
-    ///     Grabs the mouse, forcing all mouse events to be routed to the specified view until <see cref="UngrabMouse"/>
-    ///     is called.
-    /// </summary>
-    /// <param name="view">View that will receive all mouse events until <see cref="UngrabMouse"/> is invoked.</param>
-    public static void GrabMouse (View view)
-    {
-        if (view is null)
-        {
-            return;
-        }
-
-        if (!OnGrabbingMouse (view))
-        {
-            OnGrabbedMouse (view);
-            MouseGrabView = view;
-        }
-    }
-
-    /// <summary>Releases the mouse grab, so mouse events will be routed to the view on which the mouse is.</summary>
-    public static void UngrabMouse ()
-    {
-        if (MouseGrabView is null)
-        {
-            return;
-        }
-
-        if (!OnUnGrabbingMouse (MouseGrabView))
-        {
-            View view = MouseGrabView;
-            MouseGrabView = null;
-            OnUnGrabbedMouse (view);
-        }
-    }
-
-    private static bool OnGrabbingMouse (View view)
-    {
-        if (view is null)
-        {
-            return false;
-        }
-
-        var evArgs = new GrabMouseEventArgs (view);
-        GrabbingMouse?.Invoke (view, evArgs);
-
-        return evArgs.Cancel;
-    }
-
-    private static bool OnUnGrabbingMouse (View view)
-    {
-        if (view is null)
-        {
-            return false;
-        }
-
-        var evArgs = new GrabMouseEventArgs (view);
-        UnGrabbingMouse?.Invoke (view, evArgs);
-
-        return evArgs.Cancel;
-    }
-
-    private static void OnGrabbedMouse (View view)
-    {
-        if (view is null)
-        {
-            return;
-        }
-
-        GrabbedMouse?.Invoke (view, new (view));
-    }
-
-    private static void OnUnGrabbedMouse (View view)
-    {
-        if (view is null)
-        {
-            return;
-        }
-
-        UnGrabbedMouse?.Invoke (view, new (view));
-    }
-
-#nullable enable
-
-    // Used by OnMouseEvent to track the last view that was clicked on.
-    internal static View? _mouseEnteredView;
-
-    /// <summary>Event fired when a mouse move or click occurs. Coordinates are screen relative.</summary>
-    /// <remarks>
-    ///     <para>
-    ///         Use this event to receive mouse events in screen coordinates. Use <see cref="MouseEvent"/> to
-    ///         receive mouse events relative to a <see cref="View.Viewport"/>.
-    ///     </para>
-    ///     <para>The <see cref="MouseEvent.View"/> will contain the <see cref="View"/> that contains the mouse coordinates.</para>
-    /// </remarks>
-    public static event EventHandler<MouseEvent>? MouseEvent;
-
-    /// <summary>Called when a mouse event occurs. Raises the <see cref="MouseEvent"/> event.</summary>
-    /// <remarks>This method can be used to simulate a mouse event, e.g. in unit tests.</remarks>
-    /// <param name="mouseEvent">The mouse event with coordinates relative to the screen.</param>
-    internal static void OnMouseEvent (MouseEvent mouseEvent)
-    {
-        if (IsMouseDisabled)
-        {
-            return;
-        }
-
-        var view = View.FindDeepestView (Current, mouseEvent.Position);
-
-        if (view is { })
-        {
-            mouseEvent.View = view;
-        }
-
-        MouseEvent?.Invoke (null, mouseEvent);
-
-        if (mouseEvent.Handled)
-        {
-            return;
-        }
-
-        if (MouseGrabView is { })
-        {
-            // If the mouse is grabbed, send the event to the view that grabbed it.
-            // The coordinates are relative to the Bounds of the view that grabbed the mouse.
-            Point frameLoc = MouseGrabView.ScreenToViewport (mouseEvent.Position);
-
-            var viewRelativeMouseEvent = new MouseEvent
-            {
-                Position = frameLoc,
-                Flags = mouseEvent.Flags,
-                ScreenPosition = mouseEvent.Position,
-                View = MouseGrabView
-            };
-
-            if ((MouseGrabView.Viewport with { Location = Point.Empty }).Contains (viewRelativeMouseEvent.Position) is false)
-            {
-                // The mouse has moved outside the bounds of the view that grabbed the mouse
-                _mouseEnteredView?.NewMouseLeaveEvent (mouseEvent);
-            }
-
-            //System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}");
-            if (MouseGrabView?.NewMouseEvent (viewRelativeMouseEvent) == true)
-            {
-                return;
-            }
-        }
-
-        if (view is { WantContinuousButtonPressed: true })
-        {
-            WantContinuousButtonPressedView = view;
-        }
-        else
-        {
-            WantContinuousButtonPressedView = null;
-        }
-
-
-        if (view is not Adornment)
-        {
-            if ((view is null || view == OverlappedTop)
-                && Current is { Modal: false }
-                && OverlappedTop != null
-                && mouseEvent.Flags != MouseFlags.ReportMousePosition
-                && mouseEvent.Flags != 0)
-            {
-                // This occurs when there are multiple overlapped "tops"
-                // E.g. "Mdi" - in the Background Worker Scenario
-                View? top = FindDeepestTop (Top, mouseEvent.Position);
-                view = View.FindDeepestView (top, mouseEvent.Position);
-
-                if (view is { } && view != OverlappedTop && top != Current && top is { })
-                {
-                    MoveCurrent ((Toplevel)top);
-                }
-            }
-        }
-
-        if (view is null)
-        {
-            return;
-        }
-
-        MouseEvent? me = null;
-
-        if (view is Adornment adornment)
-        {
-            Point frameLoc = adornment.ScreenToFrame (mouseEvent.Position);
-
-            me = new ()
-            {
-                Position = frameLoc,
-                Flags = mouseEvent.Flags,
-                ScreenPosition = mouseEvent.Position,
-                View = view
-            };
-        }
-        else if (view.ViewportToScreen (Rectangle.Empty with { Size = view.Viewport.Size }).Contains (mouseEvent.Position))
-        {
-            Point viewportLocation = view.ScreenToViewport (mouseEvent.Position);
-
-            me = new ()
-            {
-                Position = viewportLocation,
-                Flags = mouseEvent.Flags,
-                ScreenPosition = mouseEvent.Position,
-                View = view
-            };
-        }
-
-        if (me is null)
-        {
-            return;
-        }
-
-        if (_mouseEnteredView is null)
-        {
-            _mouseEnteredView = view;
-            view.NewMouseEnterEvent (me);
-        }
-        else if (_mouseEnteredView != view)
-        {
-            _mouseEnteredView.NewMouseLeaveEvent (me);
-            view.NewMouseEnterEvent (me);
-            _mouseEnteredView = view;
-        }
-
-        if (!view.WantMousePositionReports && mouseEvent.Flags == MouseFlags.ReportMousePosition)
-        {
-            return;
-        }
-
-        WantContinuousButtonPressedView = view.WantContinuousButtonPressed ? view : null;
-
-        //Debug.WriteLine ($"OnMouseEvent: ({a.MouseEvent.X},{a.MouseEvent.Y}) - {a.MouseEvent.Flags}");
-
-        while (view.NewMouseEvent (me) != true)
-        {
-            if (MouseGrabView is { })
-            {
-                break;
-            }
-
-            if (view is Adornment adornmentView)
-            {
-                view = adornmentView.Parent.SuperView;
-            }
-            else
-            {
-                view = view.SuperView;
-            }
-
-            if (view is null)
-            {
-                break;
-            }
-
-            Point boundsPoint = view.ScreenToViewport (mouseEvent.Position);
-
-            me = new ()
-            {
-                Position = boundsPoint,
-                Flags = mouseEvent.Flags,
-                ScreenPosition = mouseEvent.Position,
-                View = view
-            };
-        }
-
-        BringOverlappedTopToFront ();
-    }
-#nullable restore
-
-    #endregion Mouse handling
-
-    #region Keyboard handling
-
-    private static Key _alternateForwardKey = Key.Empty; // Defined in config.json
-
-    /// <summary>Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.</summary>
-    [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
-    [JsonConverter (typeof (KeyJsonConverter))]
-    public static Key AlternateForwardKey
-    {
-        get => _alternateForwardKey;
-        set
-        {
-            if (_alternateForwardKey != value)
-            {
-                Key oldKey = _alternateForwardKey;
-                _alternateForwardKey = value;
-                OnAlternateForwardKeyChanged (new (oldKey, value));
-            }
-        }
-    }
-
-    private static void OnAlternateForwardKeyChanged (KeyChangedEventArgs e)
-    {
-        foreach (Toplevel top in _topLevels.ToArray ())
-        {
-            top.OnAlternateForwardKeyChanged (e);
-        }
-    }
-
-    private static Key _alternateBackwardKey = Key.Empty; // Defined in config.json
-
-    /// <summary>Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key.</summary>
-    [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
-    [JsonConverter (typeof (KeyJsonConverter))]
-    public static Key AlternateBackwardKey
-    {
-        get => _alternateBackwardKey;
-        set
-        {
-            if (_alternateBackwardKey != value)
-            {
-                Key oldKey = _alternateBackwardKey;
-                _alternateBackwardKey = value;
-                OnAlternateBackwardKeyChanged (new (oldKey, value));
-            }
-        }
-    }
-
-    private static void OnAlternateBackwardKeyChanged (KeyChangedEventArgs oldKey)
-    {
-        foreach (Toplevel top in _topLevels.ToArray ())
-        {
-            top.OnAlternateBackwardKeyChanged (oldKey);
-        }
-    }
-
-    private static Key _quitKey = Key.Empty; // Defined in config.json
-
-    /// <summary>Gets or sets the key to quit the application.</summary>
-    [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
-    [JsonConverter (typeof (KeyJsonConverter))]
-    public static Key QuitKey
-    {
-        get => _quitKey;
-        set
-        {
-            if (_quitKey != value)
-            {
-                Key oldKey = _quitKey;
-                _quitKey = value;
-                OnQuitKeyChanged (new (oldKey, value));
-            }
-        }
-    }
-
-    private static void OnQuitKeyChanged (KeyChangedEventArgs e)
-    {
-        // Duplicate the list so if it changes during enumeration we're safe
-        foreach (Toplevel top in _topLevels.ToArray ())
-        {
-            top.OnQuitKeyChanged (e);
-        }
-    }
-
-    /// <summary>
-    ///     Event fired when the user presses a key. Fired by <see cref="OnKeyDown"/>.
-    ///     <para>
-    ///         Set <see cref="Key.Handled"/> to <see langword="true"/> to indicate the key was handled and to prevent
-    ///         additional processing.
-    ///     </para>
-    /// </summary>
-    /// <remarks>
-    ///     All drivers support firing the <see cref="KeyDown"/> event. Some drivers (Curses) do not support firing the
-    ///     <see cref="KeyDown"/> and <see cref="KeyUp"/> events.
-    ///     <para>Fired after <see cref="KeyDown"/> and before <see cref="KeyUp"/>.</para>
-    /// </remarks>
-    public static event EventHandler<Key> KeyDown;
-
-    /// <summary>
-    ///     Called by the <see cref="ConsoleDriver"/> when the user presses a key. Fires the <see cref="KeyDown"/> event
-    ///     then calls <see cref="View.NewKeyDownEvent"/> on all top level views. Called after <see cref="OnKeyDown"/> and
-    ///     before <see cref="OnKeyUp"/>.
-    /// </summary>
-    /// <remarks>Can be used to simulate key press events.</remarks>
-    /// <param name="keyEvent"></param>
-    /// <returns><see langword="true"/> if the key was handled.</returns>
-    public static bool OnKeyDown (Key keyEvent)
-    {
-        if (!_initialized)
-        {
-            return true;
-        }
-
-        KeyDown?.Invoke (null, keyEvent);
-
-        if (keyEvent.Handled)
-        {
-            return true;
-        }
-
-        foreach (Toplevel topLevel in _topLevels.ToList ())
-        {
-            if (topLevel.NewKeyDownEvent (keyEvent))
-            {
-                return true;
-            }
-
-            if (topLevel.Modal)
-            {
-                break;
-            }
-        }
-
-        // Invoke any Global KeyBindings
-        foreach (Toplevel topLevel in _topLevels.ToList ())
-        {
-            foreach (View view in topLevel.Subviews.Where (
-                                                           v => v.KeyBindings.TryGet (
-                                                                                      keyEvent,
-                                                                                      KeyBindingScope.Application,
-                                                                                      out KeyBinding _
-                                                                                     )
-                                                          ))
-            {
-                if (view.KeyBindings.TryGet (keyEvent.KeyCode, KeyBindingScope.Application, out KeyBinding _))
-                {
-                    bool? handled = view.OnInvokingKeyBindings (keyEvent);
-
-                    if (handled is { } && (bool)handled)
-                    {
-                        return true;
-                    }
-                }
-            }
-        }
-
-        return false;
-    }
-
-    /// <summary>
-    ///     Event fired when the user releases a key. Fired by <see cref="OnKeyUp"/>.
-    ///     <para>
-    ///         Set <see cref="Key.Handled"/> to <see langword="true"/> to indicate the key was handled and to prevent
-    ///         additional processing.
-    ///     </para>
-    /// </summary>
-    /// <remarks>
-    ///     All drivers support firing the <see cref="KeyDown"/> event. Some drivers (Curses) do not support firing the
-    ///     <see cref="KeyDown"/> and <see cref="KeyUp"/> events.
-    ///     <para>Fired after <see cref="KeyDown"/>.</para>
-    /// </remarks>
-    public static event EventHandler<Key> KeyUp;
-
-    /// <summary>
-    ///     Called by the <see cref="ConsoleDriver"/> when the user releases a key. Fires the <see cref="KeyUp"/> event
-    ///     then calls <see cref="View.NewKeyUpEvent"/> on all top level views. Called after <see cref="OnKeyDown"/>.
-    /// </summary>
-    /// <remarks>Can be used to simulate key press events.</remarks>
-    /// <param name="a"></param>
-    /// <returns><see langword="true"/> if the key was handled.</returns>
-    public static bool OnKeyUp (Key a)
-    {
-        if (!_initialized)
-        {
-            return true;
-        }
-
-        KeyUp?.Invoke (null, a);
-
-        if (a.Handled)
-        {
-            return true;
-        }
-
-        foreach (Toplevel topLevel in _topLevels.ToList ())
-        {
-            if (topLevel.NewKeyUpEvent (a))
-            {
-                return true;
-            }
-
-            if (topLevel.Modal)
-            {
-                break;
-            }
-        }
-
-        return false;
-    }
-
-    #endregion Keyboard handling
 }

+ 298 - 0
Terminal.Gui/Application/ApplicationKeyboard.cs

@@ -0,0 +1,298 @@
+using System.Text.Json.Serialization;
+
+namespace Terminal.Gui;
+
+partial class Application
+{
+    private static Key _alternateForwardKey = Key.Empty; // Defined in config.json
+
+    /// <summary>Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.</summary>
+    [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
+    [JsonConverter (typeof (KeyJsonConverter))]
+    public static Key AlternateForwardKey
+    {
+        get => _alternateForwardKey;
+        set
+        {
+            if (_alternateForwardKey != value)
+            {
+                Key oldKey = _alternateForwardKey;
+                _alternateForwardKey = value;
+                OnAlternateForwardKeyChanged (new (oldKey, value));
+            }
+        }
+    }
+
+    private static void OnAlternateForwardKeyChanged (KeyChangedEventArgs e)
+    {
+        foreach (Toplevel top in _topLevels.ToArray ())
+        {
+            top.OnAlternateForwardKeyChanged (e);
+        }
+    }
+
+    private static Key _alternateBackwardKey = Key.Empty; // Defined in config.json
+
+    /// <summary>Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key.</summary>
+    [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
+    [JsonConverter (typeof (KeyJsonConverter))]
+    public static Key AlternateBackwardKey
+    {
+        get => _alternateBackwardKey;
+        set
+        {
+            if (_alternateBackwardKey != value)
+            {
+                Key oldKey = _alternateBackwardKey;
+                _alternateBackwardKey = value;
+                OnAlternateBackwardKeyChanged (new (oldKey, value));
+            }
+        }
+    }
+
+    private static void OnAlternateBackwardKeyChanged (KeyChangedEventArgs oldKey)
+    {
+        foreach (Toplevel top in _topLevels.ToArray ())
+        {
+            top.OnAlternateBackwardKeyChanged (oldKey);
+        }
+    }
+
+    private static Key _quitKey = Key.Empty; // Defined in config.json
+
+    /// <summary>Gets or sets the key to quit the application.</summary>
+    [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
+    [JsonConverter (typeof (KeyJsonConverter))]
+    public static Key QuitKey
+    {
+        get => _quitKey;
+        set
+        {
+            if (_quitKey != value)
+            {
+                Key oldKey = _quitKey;
+                _quitKey = value;
+                OnQuitKeyChanged (new (oldKey, value));
+            }
+        }
+    }
+
+    private static void OnQuitKeyChanged (KeyChangedEventArgs e)
+    {
+        // Duplicate the list so if it changes during enumeration we're safe
+        foreach (Toplevel top in _topLevels.ToArray ())
+        {
+            top.OnQuitKeyChanged (e);
+        }
+    }
+
+    /// <summary>
+    ///     Event fired when the user presses a key. Fired by <see cref="OnKeyDown"/>.
+    ///     <para>
+    ///         Set <see cref="Key.Handled"/> to <see langword="true"/> to indicate the key was handled and to prevent
+    ///         additional processing.
+    ///     </para>
+    /// </summary>
+    /// <remarks>
+    ///     All drivers support firing the <see cref="KeyDown"/> event. Some drivers (Curses) do not support firing the
+    ///     <see cref="KeyDown"/> and <see cref="KeyUp"/> events.
+    ///     <para>Fired after <see cref="KeyDown"/> and before <see cref="KeyUp"/>.</para>
+    /// </remarks>
+    public static event EventHandler<Key> KeyDown;
+
+    /// <summary>
+    ///     Called by the <see cref="ConsoleDriver"/> when the user presses a key. Fires the <see cref="KeyDown"/> event
+    ///     then calls <see cref="View.NewKeyDownEvent"/> on all top level views. Called after <see cref="OnKeyDown"/> and
+    ///     before <see cref="OnKeyUp"/>.
+    /// </summary>
+    /// <remarks>Can be used to simulate key press events.</remarks>
+    /// <param name="keyEvent"></param>
+    /// <returns><see langword="true"/> if the key was handled.</returns>
+    public static bool OnKeyDown (Key keyEvent)
+    {
+        if (!_initialized)
+        {
+            return true;
+        }
+
+        KeyDown?.Invoke (null, keyEvent);
+
+        if (keyEvent.Handled)
+        {
+            return true;
+        }
+
+        foreach (Toplevel topLevel in _topLevels.ToList ())
+        {
+            if (topLevel.NewKeyDownEvent (keyEvent))
+            {
+                return true;
+            }
+
+            if (topLevel.Modal)
+            {
+                break;
+            }
+        }
+
+        // Invoke any global (Application-scoped) KeyBindings.
+        // The first view that handles the key will stop the loop.
+        foreach (KeyValuePair<Key, List<View>> binding in _keyBindings.Where (b => b.Key == keyEvent.KeyCode))
+        {
+            foreach (View view in binding.Value)
+            {
+                bool? handled = view?.OnInvokingKeyBindings (keyEvent);
+
+                if (handled != null && (bool)handled)
+                {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /// <summary>
+    ///     Event fired when the user releases a key. Fired by <see cref="OnKeyUp"/>.
+    ///     <para>
+    ///         Set <see cref="Key.Handled"/> to <see langword="true"/> to indicate the key was handled and to prevent
+    ///         additional processing.
+    ///     </para>
+    /// </summary>
+    /// <remarks>
+    ///     All drivers support firing the <see cref="KeyDown"/> event. Some drivers (Curses) do not support firing the
+    ///     <see cref="KeyDown"/> and <see cref="KeyUp"/> events.
+    ///     <para>Fired after <see cref="KeyDown"/>.</para>
+    /// </remarks>
+    public static event EventHandler<Key> KeyUp;
+
+    /// <summary>
+    ///     Called by the <see cref="ConsoleDriver"/> when the user releases a key. Fires the <see cref="KeyUp"/> event
+    ///     then calls <see cref="View.NewKeyUpEvent"/> on all top level views. Called after <see cref="OnKeyDown"/>.
+    /// </summary>
+    /// <remarks>Can be used to simulate key press events.</remarks>
+    /// <param name="a"></param>
+    /// <returns><see langword="true"/> if the key was handled.</returns>
+    public static bool OnKeyUp (Key a)
+    {
+        if (!_initialized)
+        {
+            return true;
+        }
+
+        KeyUp?.Invoke (null, a);
+
+        if (a.Handled)
+        {
+            return true;
+        }
+
+        foreach (Toplevel topLevel in _topLevels.ToList ())
+        {
+            if (topLevel.NewKeyUpEvent (a))
+            {
+                return true;
+            }
+
+            if (topLevel.Modal)
+            {
+                break;
+            }
+        }
+
+        return false;
+    }
+
+    /// <summary>
+    ///     The <see cref="KeyBindingScope.Application"/> key bindings.
+    /// </summary>
+    private static readonly Dictionary<Key, List<View>> _keyBindings = new ();
+
+    /// <summary>
+    /// Gets the list of <see cref="KeyBindingScope.Application"/> key bindings.
+    /// </summary>
+    public static Dictionary<Key, List<View>> GetKeyBindings () { return _keyBindings; }
+
+    /// <summary>
+    ///     Adds an  <see cref="KeyBindingScope.Application"/> scoped key binding.
+    /// </summary>
+    /// <remarks>
+    ///     This is an internal method used by the <see cref="View"/> class to add Application key bindings.
+    /// </remarks>
+    /// <param name="key">The key being bound.</param>
+    /// <param name="view">The view that is bound to the key.</param>
+    internal static void AddKeyBinding (Key key, View view)
+    {
+        if (!_keyBindings.ContainsKey (key))
+        {
+            _keyBindings [key] = [];
+        }
+
+        _keyBindings [key].Add (view);
+    }
+
+    /// <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<View> GetViewsWithKeyBindings () { return _keyBindings.Values.SelectMany (v => v).ToList (); }
+
+    /// <summary>
+    ///     Gets the list of Views that have <see cref="KeyBindingScope.Application"/> key bindings for the specified key.
+    /// </summary>
+    /// <remarks>
+    ///     This is an internal method used by the <see cref="View"/> class to add Application key bindings.
+    /// </remarks>
+    /// <param name="key">The key to check.</param>
+    /// <param name="views">Outputs the list of views bound to <paramref name="key"/></param>
+    /// <returns><see langword="True"/> if successful.</returns>
+    internal static bool TryGetKeyBindings (Key key, out List<View> views) { return _keyBindings.TryGetValue (key, out views); }
+
+    /// <summary>
+    ///     Removes an <see cref="KeyBindingScope.Application"/> scoped key binding.
+    /// </summary>
+    /// <remarks>
+    ///     This is an internal method used by the <see cref="View"/> class to remove Application key bindings.
+    /// </remarks>
+    /// <param name="key">The key that was bound.</param>
+    /// <param name="view">The view that is bound to the key.</param>
+    internal static void RemoveKeyBinding (Key key, View view)
+    {
+        if (_keyBindings.TryGetValue (key, out List<View> views))
+        {
+            views.Remove (view);
+
+            if (views.Count == 0)
+            {
+                _keyBindings.Remove (key);
+            }
+        }
+    }
+
+    /// <summary>
+    ///     Removes all <see cref="KeyBindingScope.Application"/> scoped key bindings for the specified view.
+    /// </summary>
+    /// <remarks>
+    ///     This is an internal method used by the <see cref="View"/> class to remove Application key bindings.
+    /// </remarks>
+    /// <param name="view">The view that is bound to the key.</param>
+    internal static void ClearKeyBindings (View view)
+    {
+        foreach (Key key in _keyBindings.Keys)
+        {
+            _keyBindings [key].Remove (view);
+        }
+    }
+
+    /// <summary>
+    ///     Removes all <see cref="KeyBindingScope.Application"/> scoped key bindings for the specified view.
+    /// </summary>
+    /// <remarks>
+    ///     This is an internal method used by the <see cref="View"/> class to remove Application key bindings.
+    /// </remarks>
+    internal static void ClearKeyBindings () { _keyBindings.Clear (); }
+}

+ 302 - 0
Terminal.Gui/Application/ApplicationMouse.cs

@@ -0,0 +1,302 @@
+namespace Terminal.Gui;
+
+partial class Application
+{
+    #region Mouse handling
+
+    /// <summary>Disable or enable the mouse. The mouse is enabled by default.</summary>
+    [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
+    public static bool IsMouseDisabled { get; set; }
+
+    /// <summary>The current <see cref="View"/> object that wants continuous mouse button pressed events.</summary>
+    public static View WantContinuousButtonPressedView { get; private set; }
+
+    /// <summary>
+    ///     Gets the view that grabbed the mouse (e.g. for dragging). When this is set, all mouse events will be routed to
+    ///     this view until the view calls <see cref="UngrabMouse"/> or the mouse is released.
+    /// </summary>
+    public static View MouseGrabView { get; private set; }
+
+    /// <summary>Invoked when a view wants to grab the mouse; can be canceled.</summary>
+    public static event EventHandler<GrabMouseEventArgs> GrabbingMouse;
+
+    /// <summary>Invoked when a view wants un-grab the mouse; can be canceled.</summary>
+    public static event EventHandler<GrabMouseEventArgs> UnGrabbingMouse;
+
+    /// <summary>Invoked after a view has grabbed the mouse.</summary>
+    public static event EventHandler<ViewEventArgs> GrabbedMouse;
+
+    /// <summary>Invoked after a view has un-grabbed the mouse.</summary>
+    public static event EventHandler<ViewEventArgs> UnGrabbedMouse;
+
+    /// <summary>
+    ///     Grabs the mouse, forcing all mouse events to be routed to the specified view until <see cref="UngrabMouse"/>
+    ///     is called.
+    /// </summary>
+    /// <param name="view">View that will receive all mouse events until <see cref="UngrabMouse"/> is invoked.</param>
+    public static void GrabMouse (View view)
+    {
+        if (view is null)
+        {
+            return;
+        }
+
+        if (!OnGrabbingMouse (view))
+        {
+            OnGrabbedMouse (view);
+            MouseGrabView = view;
+        }
+    }
+
+    /// <summary>Releases the mouse grab, so mouse events will be routed to the view on which the mouse is.</summary>
+    public static void UngrabMouse ()
+    {
+        if (MouseGrabView is null)
+        {
+            return;
+        }
+
+        if (!OnUnGrabbingMouse (MouseGrabView))
+        {
+            View view = MouseGrabView;
+            MouseGrabView = null;
+            OnUnGrabbedMouse (view);
+        }
+    }
+
+    private static bool OnGrabbingMouse (View view)
+    {
+        if (view is null)
+        {
+            return false;
+        }
+
+        var evArgs = new GrabMouseEventArgs (view);
+        GrabbingMouse?.Invoke (view, evArgs);
+
+        return evArgs.Cancel;
+    }
+
+    private static bool OnUnGrabbingMouse (View view)
+    {
+        if (view is null)
+        {
+            return false;
+        }
+
+        var evArgs = new GrabMouseEventArgs (view);
+        UnGrabbingMouse?.Invoke (view, evArgs);
+
+        return evArgs.Cancel;
+    }
+
+    private static void OnGrabbedMouse (View view)
+    {
+        if (view is null)
+        {
+            return;
+        }
+
+        GrabbedMouse?.Invoke (view, new (view));
+    }
+
+    private static void OnUnGrabbedMouse (View view)
+    {
+        if (view is null)
+        {
+            return;
+        }
+
+        UnGrabbedMouse?.Invoke (view, new (view));
+    }
+
+#nullable enable
+
+    // Used by OnMouseEvent to track the last view that was clicked on.
+    internal static View? _mouseEnteredView;
+
+    /// <summary>Event fired when a mouse move or click occurs. Coordinates are screen relative.</summary>
+    /// <remarks>
+    ///     <para>
+    ///         Use this event to receive mouse events in screen coordinates. Use <see cref="MouseEvent"/> to
+    ///         receive mouse events relative to a <see cref="View.Viewport"/>.
+    ///     </para>
+    ///     <para>The <see cref="MouseEvent.View"/> will contain the <see cref="View"/> that contains the mouse coordinates.</para>
+    /// </remarks>
+    public static event EventHandler<MouseEvent>? MouseEvent;
+
+    /// <summary>Called when a mouse event occurs. Raises the <see cref="MouseEvent"/> event.</summary>
+    /// <remarks>This method can be used to simulate a mouse event, e.g. in unit tests.</remarks>
+    /// <param name="mouseEvent">The mouse event with coordinates relative to the screen.</param>
+    internal static void OnMouseEvent (MouseEvent mouseEvent)
+    {
+        if (IsMouseDisabled)
+        {
+            return;
+        }
+
+        var view = View.FindDeepestView (Current, mouseEvent.Position);
+
+        if (view is { })
+        {
+            mouseEvent.View = view;
+        }
+
+        MouseEvent?.Invoke (null, mouseEvent);
+
+        if (mouseEvent.Handled)
+        {
+            return;
+        }
+
+        if (MouseGrabView is { })
+        {
+            // If the mouse is grabbed, send the event to the view that grabbed it.
+            // The coordinates are relative to the Bounds of the view that grabbed the mouse.
+            Point frameLoc = MouseGrabView.ScreenToViewport (mouseEvent.Position);
+
+            var viewRelativeMouseEvent = new MouseEvent
+            {
+                Position = frameLoc,
+                Flags = mouseEvent.Flags,
+                ScreenPosition = mouseEvent.Position,
+                View = MouseGrabView
+            };
+
+            if ((MouseGrabView.Viewport with { Location = Point.Empty }).Contains (viewRelativeMouseEvent.Position) is false)
+            {
+                // The mouse has moved outside the bounds of the view that grabbed the mouse
+                _mouseEnteredView?.NewMouseLeaveEvent (mouseEvent);
+            }
+
+            //System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}");
+            if (MouseGrabView?.NewMouseEvent (viewRelativeMouseEvent) == true)
+            {
+                return;
+            }
+        }
+
+        if (view is { WantContinuousButtonPressed: true })
+        {
+            WantContinuousButtonPressedView = view;
+        }
+        else
+        {
+            WantContinuousButtonPressedView = null;
+        }
+
+        if (view is not Adornment)
+        {
+            if ((view is null || view == OverlappedTop)
+                && Current is { Modal: false }
+                && OverlappedTop != null
+                && mouseEvent.Flags != MouseFlags.ReportMousePosition
+                && mouseEvent.Flags != 0)
+            {
+                // This occurs when there are multiple overlapped "tops"
+                // E.g. "Mdi" - in the Background Worker Scenario
+                View? top = FindDeepestTop (Top, mouseEvent.Position);
+                view = View.FindDeepestView (top, mouseEvent.Position);
+
+                if (view is { } && view != OverlappedTop && top != Current && top is { })
+                {
+                    MoveCurrent ((Toplevel)top);
+                }
+            }
+        }
+
+        if (view is null)
+        {
+            return;
+        }
+
+        MouseEvent? me = null;
+
+        if (view is Adornment adornment)
+        {
+            Point frameLoc = adornment.ScreenToFrame (mouseEvent.Position);
+
+            me = new ()
+            {
+                Position = frameLoc,
+                Flags = mouseEvent.Flags,
+                ScreenPosition = mouseEvent.Position,
+                View = view
+            };
+        }
+        else if (view.ViewportToScreen (Rectangle.Empty with { Size = view.Viewport.Size }).Contains (mouseEvent.Position))
+        {
+            Point viewportLocation = view.ScreenToViewport (mouseEvent.Position);
+
+            me = new ()
+            {
+                Position = viewportLocation,
+                Flags = mouseEvent.Flags,
+                ScreenPosition = mouseEvent.Position,
+                View = view
+            };
+        }
+
+        if (me is null)
+        {
+            return;
+        }
+
+        if (_mouseEnteredView is null)
+        {
+            _mouseEnteredView = view;
+            view.NewMouseEnterEvent (me);
+        }
+        else if (_mouseEnteredView != view)
+        {
+            _mouseEnteredView.NewMouseLeaveEvent (me);
+            view.NewMouseEnterEvent (me);
+            _mouseEnteredView = view;
+        }
+
+        if (!view.WantMousePositionReports && mouseEvent.Flags == MouseFlags.ReportMousePosition)
+        {
+            return;
+        }
+
+        WantContinuousButtonPressedView = view.WantContinuousButtonPressed ? view : null;
+
+        //Debug.WriteLine ($"OnMouseEvent: ({a.MouseEvent.X},{a.MouseEvent.Y}) - {a.MouseEvent.Flags}");
+
+        while (view.NewMouseEvent (me) != true)
+        {
+            if (MouseGrabView is { })
+            {
+                break;
+            }
+
+            if (view is Adornment adornmentView)
+            {
+                view = adornmentView.Parent.SuperView;
+            }
+            else
+            {
+                view = view.SuperView;
+            }
+
+            if (view is null)
+            {
+                break;
+            }
+
+            Point boundsPoint = view.ScreenToViewport (mouseEvent.Position);
+
+            me = new ()
+            {
+                Position = boundsPoint,
+                Flags = mouseEvent.Flags,
+                ScreenPosition = mouseEvent.Position,
+                View = view
+            };
+        }
+
+        BringOverlappedTopToFront ();
+    }
+
+    #endregion Mouse handling
+}

+ 0 - 0
Terminal.Gui/IterationEventArgs.cs → Terminal.Gui/Application/IterationEventArgs.cs


+ 0 - 0
Terminal.Gui/MainLoop.cs → Terminal.Gui/Application/MainLoop.cs


+ 48 - 0
Terminal.Gui/Application/MainLoopSyncContext.cs

@@ -0,0 +1,48 @@
+namespace Terminal.Gui;
+
+/// <summary>
+///     provides the sync context set while executing code in Terminal.Gui, to let
+///     users use async/await on their code
+/// </summary>
+internal sealed class MainLoopSyncContext : SynchronizationContext
+{
+    public override SynchronizationContext CreateCopy () { return new MainLoopSyncContext (); }
+
+    public override void Post (SendOrPostCallback d, object state)
+    {
+        Application.MainLoop?.AddIdle (
+                                       () =>
+                                       {
+                                           d (state);
+
+                                           return false;
+                                       }
+                                      );
+    }
+
+    //_mainLoop.Driver.Wakeup ();
+    public override void Send (SendOrPostCallback d, object state)
+    {
+        if (Thread.CurrentThread.ManagedThreadId == Application._mainThreadId)
+        {
+            d (state);
+        }
+        else
+        {
+            var wasExecuted = false;
+
+            Application.Invoke (
+                                () =>
+                                {
+                                    d (state);
+                                    wasExecuted = true;
+                                }
+                               );
+
+            while (!wasExecuted)
+            {
+                Thread.Sleep (15);
+            }
+        }
+    }
+}

+ 0 - 0
Terminal.Gui/RunState.cs → Terminal.Gui/Application/RunState.cs


+ 0 - 0
Terminal.Gui/RunStateEventArgs.cs → Terminal.Gui/Application/RunStateEventArgs.cs


+ 0 - 0
Terminal.Gui/Timeout.cs → Terminal.Gui/Application/Timeout.cs


+ 0 - 0
Terminal.Gui/TimeoutEventArgs.cs → Terminal.Gui/Application/TimeoutEventArgs.cs


+ 0 - 22
Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs

@@ -40,28 +40,6 @@ public class FakeDriver : ConsoleDriver
     public static Behaviors FakeBehaviors = new ();
     public override bool SupportsTrueColor => false;
 
-    /// <inheritdoc />
-    public override int Cols
-    {
-        get => base.Cols;
-        internal set
-        {
-            base.Cols = value;
-            FakeConsole.SetBufferSize (Cols, Rows);
-        }
-    }
-
-    /// <inheritdoc />
-    public override int Rows
-    {
-        get => base.Rows;
-        internal set
-        {
-            base.Rows = value;
-            FakeConsole.SetBufferSize (Cols, Rows);
-        }
-    }
-
     public FakeDriver ()
     {
         Cols = FakeConsole.WindowWidth = FakeConsole.BufferWidth = FakeConsole.WIDTH;

+ 41 - 0
Terminal.Gui/Input/CommandContext.cs

@@ -0,0 +1,41 @@
+#nullable enable
+namespace Terminal.Gui;
+/// <summary>
+///     Provides context for a <see cref="Command"/> that is being invoked.
+/// </summary
+/// <remarks>
+///     <para>
+///         To define a <see cref="Command"/> that is invoked with context,
+///         use <see cref="View.AddCommand(Command,Func{CommandContext,Nullable{bool}})"/>
+///     </para>
+/// </remarks>
+public record struct CommandContext
+{
+    /// <summary>
+    ///     Initializes a new instance of <see cref="CommandContext"/> with the specified <see cref="Command"/>,
+    /// </summary>
+    /// <param name="command"></param>
+    /// <param name="key"></param>
+    /// <param name="keyBinding"></param>
+    public CommandContext (Command command, Key? key, KeyBinding? keyBinding = null)
+    {
+        Command = command;
+        Key = key;
+        KeyBinding = keyBinding;
+    }
+
+    /// <summary>
+    ///     The <see cref="Command"/> that is being invoked.
+    /// </summary>
+    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; }
+}

+ 14 - 240
Terminal.Gui/Input/KeyBinding.cs

@@ -1,260 +1,34 @@
-// These classes use a key binding system based on the design implemented in Scintilla.Net which is an
+#nullable enable
+
+// These classes use a key binding system based on the design implemented in Scintilla.Net which is an
 // MIT licensed open source project https://github.com/jacobslusser/ScintillaNET/blob/master/src/ScintillaNET/Command.cs
 
 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[])"/>.
+/// Provides a collection of <see cref="Command"/> objects that are scoped to <see cref="KeyBindingScope"/>.
 /// </summary>
-/// <remarks>
-///     <para>Key bindings are scoped to the most-focused view (<see cref="Focused"/>) by default.</para>
-/// </remarks>
-[Flags]
-public enum KeyBindingScope
-{
-    /// <summary>The key binding is scoped to just the view that has focus.</summary>
-    Focused = 1,
-
-    /// <summary>
-    ///     The key binding is scoped to the View's SuperView and will be triggered even when the View does not have focus, as
-    ///     long as the SuperView does have focus. This is typically used for <see cref="View.HotKey"/>s.
-    ///     <remarks>
-    ///         <para>
-    ///             Use for Views such as MenuBar and StatusBar which provide commands (shortcuts etc...) that trigger even
-    ///             when not focused.
-    ///         </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>
-    ///     </remarks>
-    /// </summary>
-    HotKey = 2,
-
-    /// <summary>
-    ///     The key binding will be triggered regardless of which view has focus. This is typically used for global
-    ///     commands.
-    /// </summary>
-    /// <remarks>
-    ///     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 down event was not bound to a <see cref="View.HotKey"/>.
-    /// </remarks>
-    Application = 4
-}
-
-/// <summary>Provides a collection of <see cref="Command"/> objects that are scoped to <see cref="KeyBindingScope"/>.</summary>
-public class KeyBinding
+public record struct KeyBinding
 {
     /// <summary>Initializes a new instance.</summary>
-    /// <param name="commands"></param>
-    /// <param name="scope"></param>
-    public KeyBinding (Command [] commands, KeyBindingScope scope)
+    /// <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)
     {
         Commands = commands;
         Scope = scope;
+        Context = context;
     }
 
-    /// <summary>The actions which can be performed by the application or bound to keys in a <see cref="View"/> control.</summary>
+    /// <summary>The commands this key binding will invoke.</summary>
     public Command [] Commands { get; set; }
 
-    /// <summary>The scope of the <see cref="Commands"/> bound to a key.</summary>
+    /// <summary>The scope of the <see cref="Commands"/>.</summary>
     public KeyBindingScope Scope { get; set; }
-}
-
-/// <summary>A class that provides a collection of <see cref="KeyBinding"/> objects bound to a <see cref="Key"/>.</summary>
-public class KeyBindings
-{
-    // TODO: Add a dictionary comparer that ignores Scope
-    /// <summary>The collection of <see cref="KeyBinding"/> objects.</summary>
-    public Dictionary<Key, KeyBinding> Bindings { get; } = new ();
-
-    /// <summary>Adds a <see cref="KeyBinding"/> to the collection.</summary>
-    /// <param name="key"></param>
-    /// <param name="binding"></param>
-    public void Add (Key key, KeyBinding binding) { Bindings.Add (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="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 (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 _))
-        {
-            Bindings [key] = new KeyBinding (commands, scope);
-        }
-        else
-        {
-            Bindings.Add (key, new KeyBinding (commands, scope));
-        }
-    }
-
-    /// <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,KeyBindingScope,Terminal.Gui.Command[])"/> for
-    ///         <see cref="KeyBindingScope.Focused"/> scoped 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="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) { Add (key, KeyBindingScope.Focused, commands); }
-
-    /// <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.
+    ///     Arbitrary context that can be associated with this key binding.
     /// </summary>
-    /// <param name="command"></param>
-    public void Clear (params Command [] command)
-    {
-        var kvps = Bindings
-                   .Where (kvp => kvp.Value.Commands.SequenceEqual (command))
-                   .ToArray ();
-        foreach (KeyValuePair<Key, KeyBinding> kvp in kvps)
-        {
-            Bindings.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) { return TryGet (key, out KeyBinding binding) ? binding : null; }
-
-    /// <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) { return TryGet (key, scope, out KeyBinding binding) ? binding : null; }
-
-    /// <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 Key used by a set of commands.</summary>
-    /// <remarks></remarks>
-    /// <param name="commands">The set of commands to search.</param>
-    /// <returns>The <see cref="Key"/> used by a <see cref="Command"/></returns>
-    /// <exception cref="InvalidOperationException">If no matching set of commands was found.</exception>
-    public Key GetKeyFromCommands (params Command [] commands) { return Bindings.First (a => a.Value.Commands.SequenceEqual (commands)).Key; }
-
-    /// <summary>Removes a <see cref="KeyBinding"/> from the collection.</summary>
-    /// <param name="key"></param>
-    public void Remove (Key key) { Bindings.Remove (key); }
-
-    /// <summary>Replaces a key combination already bound to a set of <see cref="Command"/>s.</summary>
-    /// <remarks></remarks>
-    /// <param name="fromKey">The key to be replaced.</param>
-    /// <param name="toKey">The new key to be used.</param>
-    public void Replace (Key fromKey, Key toKey)
-    {
-        if (!TryGet (fromKey, out KeyBinding _))
-        {
-            return;
-        }
-
-        KeyBinding value = Bindings [fromKey];
-        Bindings.Remove (fromKey);
-        Bindings [toKey] = 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 (key.IsValid)
-        {
-            return Bindings.TryGetValue (key, out binding);
-        }
-
-        binding = new KeyBinding (Array.Empty<Command> (), KeyBindingScope.Focused);
-
-        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 && Bindings.TryGetValue (key, out binding))
-        {
-            if (scope.HasFlag (binding.Scope))
-            {
-                return true;
-            }
-        }
-
-        binding = new KeyBinding (Array.Empty<Command> (), KeyBindingScope.Focused);
-
-        return false;
-    }
+    public object? Context { get; set; }
 }

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

@@ -0,0 +1,46 @@
+using Terminal.Gui.Analyzers.Internal.Attributes;
+
+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>
+[Flags]
+[GenerateEnumExtensionMethods (FastHasFlags = true)]
+public enum KeyBindingScope
+{
+    /// <summary>The key binding is scoped to just the view that has focus.</summary>
+    Focused = 1,
+
+    /// <summary>
+    ///     The key binding is scoped to the View's Superview hierarchy and will be triggered even when the View does not have
+    ///     focus, as
+    ///     long as the SuperView does have focus. This is typically used for <see cref="View.HotKey"/>s.
+    ///     <remarks>
+    ///         <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>
+    ///     </remarks>
+    /// </summary>
+    HotKey = 2,
+
+    /// <summary>
+    ///     The key binding will be triggered regardless of which view has focus. This is typically used for global
+    ///     commands, which are called Shortcuts.
+    /// </summary>
+    /// <remarks>
+    ///     <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>
+    /// </remarks>
+    Application = 4
+}

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

@@ -0,0 +1,258 @@
+#nullable enable
+
+namespace Terminal.Gui;
+
+/// <summary>
+/// Provides a collection of <see cref="KeyBinding"/> objects bound to a <see cref="Key"/>.
+/// </summary>
+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"/>, such as in unit tests.
+    /// </summary>
+    public KeyBindings () { }
+
+    /// <summary>Initializes a new instance bound to <paramref name="boundView"/>.</summary>
+    public KeyBindings (View boundView) { BoundView = boundView; }
+
+    /// <summary>
+    ///     The view that the <see cref="KeyBindings"/> are bound to.
+    /// </summary>
+    public View? BoundView { get; }
+
+    // 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 ();
+
+    /// <summary>Adds a <see cref="KeyBinding"/> to the collection.</summary>
+    /// <param name="key"></param>
+    /// <param name="binding"></param>
+    public void Add (Key key, KeyBinding binding)
+    {
+        if (TryGet (key, out KeyBinding _))
+        {
+            Bindings [key] = binding;
+        }
+        else
+        {
+            Bindings.Add (key, binding);
+            if (binding.Scope.FastHasFlags (KeyBindingScope.Application))
+            {
+                Application.AddKeyBinding (key, BoundView);
+            }
+        }
+    }
+
+    /// <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 (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 _))
+        {
+            Bindings [key] = new (commands, scope);
+        }
+        else
+        {
+            Add (key, new KeyBinding (commands, scope));
+        }
+    }
+
+    /// <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,KeyBindingScope,Terminal.Gui.Command[])"/> for
+    ///         <see cref="KeyBindingScope.Focused"/> scoped 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="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)
+    {
+        Add (key, KeyBindingScope.Focused, commands);
+    }
+
+    /// <summary>Removes all <see cref="KeyBinding"/> objects from the collection.</summary>
+    public void Clear ()
+    {
+        Application.ClearKeyBindings (BoundView);
+
+        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 Key used by a set of commands.</summary>
+    /// <remarks></remarks>
+    /// <param name="commands">The set of commands to search.</param>
+    /// <returns>The <see cref="Key"/> used by a <see cref="Command"/></returns>
+    /// <exception cref="InvalidOperationException">If no matching set of commands was found.</exception>
+    public Key GetKeyFromCommands (params Command [] commands) { return Bindings.First (a => a.Value.Commands.SequenceEqual (commands)).Key; }
+
+    /// <summary>Removes a <see cref="KeyBinding"/> from the collection.</summary>
+    /// <param name="key"></param>
+    public void Remove (Key key)
+    {
+        Bindings.Remove (key);
+        Application.RemoveKeyBinding (key, BoundView);
+    }
+
+    /// <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.</param>
+    public void Replace (Key oldKey, Key newKey)
+    {
+        if (!TryGet (oldKey, out KeyBinding _))
+        {
+            return;
+        }
+
+        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 (key.IsValid)
+        {
+            return Bindings.TryGetValue (key, out binding);
+        }
+
+        binding = new (Array.Empty<Command> (), KeyBindingScope.Focused);
+
+        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 && Bindings.TryGetValue (key, out binding))
+        {
+            if (scope.HasFlag (binding.Scope))
+            {
+                return true;
+            }
+        }
+
+        binding = new (Array.Empty<Command> (), KeyBindingScope.Focused);
+
+        return false;
+    }
+}

+ 1 - 0
Terminal.Gui/Terminal.Gui.csproj

@@ -136,5 +136,6 @@
     <EmbedUntrackedSources>true</EmbedUntrackedSources>
     <EnableSourceLink>true</EnableSourceLink>
     <Authors>Miguel de Icaza, Tig Kindel (@tig), @BDisp</Authors>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 </Project>

+ 1 - 1
Terminal.Gui/View/Layout/DimAutoStyle.cs

@@ -12,7 +12,7 @@ public enum DimAutoStyle
     /// <summary>
     ///     The dimensions will be computed based on the View's <see cref="View.GetContentSize ()"/> and/or <see cref="View.Subviews"/>.
     ///     <para>
-    ///         If <see cref="View.GetContentSize ()TracksViewport"/> is <see langword="true"/>, <see cref="View.GetContentSize ()"/> will be used to determine the dimension.
+    ///         If <see cref="View.ContentSizeTracksViewport"/> is <see langword="true"/>, <see cref="View.GetContentSize ()"/> will be used to determine the dimension.
     ///     </para>
     ///     <para>
     ///         Otherwise, the Subview in <see cref="View.Subviews"/> with the largest corresponding position plus dimension

+ 53 - 0
Terminal.Gui/View/Layout/PosAlign.cs

@@ -1,6 +1,7 @@
 #nullable enable
 
 using System.ComponentModel;
+using System.Drawing;
 
 namespace Terminal.Gui;
 
@@ -166,4 +167,56 @@ public class PosAlign : Pos
 
         return 0;
     }
+
+    internal int CalculateMinDimension (int groupId, IList<View> views, Dimension dimension)
+    {
+        List<int> dimensionsList = new ();
+
+        // PERF: If this proves a perf issue, consider caching a ref to this list in each item
+        List<View> viewsInGroup = views.Where (
+                                               v =>
+                                               {
+                                                   return dimension switch
+                                                          {
+                                                              Dimension.Width when v.X is PosAlign alignX => alignX.GroupId == groupId,
+                                                              Dimension.Height when v.Y is PosAlign alignY => alignY.GroupId == groupId,
+                                                              _ => false
+                                                          };
+                                               })
+                                       .ToList ();
+
+        if (viewsInGroup.Count == 0)
+        {
+            return 0;
+        }
+
+        // PERF: We iterate over viewsInGroup multiple times here.
+
+        Aligner? firstInGroup = null;
+
+        // Update the dimensionList with the sizes of the views
+        for (var index = 0; index < viewsInGroup.Count; index++)
+        {
+            View view = viewsInGroup [index];
+
+            PosAlign? posAlign = dimension == Dimension.Width ? view.X as PosAlign : view.Y as PosAlign;
+
+            if (posAlign is { })
+            {
+                if (index == 0)
+                {
+                    firstInGroup = posAlign.Aligner;
+                }
+
+                dimensionsList.Add (dimension == Dimension.Width ? view.Frame.Width : view.Frame.Height);
+            }
+        }
+
+        // Align
+        var aligner = firstInGroup;
+        aligner.ContainerSize = dimensionsList.Sum();
+        int [] locations = aligner.Align (dimensionsList.ToArray ());
+
+        return locations.Sum ();
+    }
 }

+ 8 - 26
Terminal.Gui/View/View.cs

@@ -123,25 +123,20 @@ public partial class View : Responder, ISupportInitializeNotification
     /// </remarks>
     public View ()
     {
-        CreateAdornments ();
-
-        HotKeySpecifier = (Rune)'_';
-        TitleTextFormatter.HotKeyChanged += TitleTextFormatter_HotKeyChanged;
-
-        TextDirection = TextDirection.LeftRight_TopBottom;
-        Text = string.Empty;
+        SetupAdornments ();
+        SetupKeyboard ();
+        //SetupMouse ();
+        SetupText ();
 
         CanFocus = false;
         TabIndex = -1;
         TabStop = false;
-
-        AddCommands ();
     }
 
     /// <summary>
     ///     Event called only once when the <see cref="View"/> is being initialized for the first time. Allows
-    ///     configurations and assignments to be performed before the <see cref="View"/> being shown. This derived from
-    ///     <see cref="ISupportInitializeNotification"/> to allow notify all the views that are being initialized.
+    ///     configurations and assignments to be performed before the <see cref="View"/> being shown.
+    ///     View implements <see cref="ISupportInitializeNotification"/> to allow for more sophisticated initialization.
     /// </summary>
     public event EventHandler Initialized;
 
@@ -503,25 +498,12 @@ public partial class View : Responder, ISupportInitializeNotification
     /// <returns></returns>
     public override string ToString () { return $"{GetType ().Name}({Id}){Frame}"; }
 
-    /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
-    /// <remarks>
-    /// <para>
-    ///     Subviews added to this view via <see cref="Add(View)"/> have their lifetime owned by this view and <see cref="Dispose"/> will
-    ///     dispose them. To prevent this, and have the creator of the Subview instance handle disposal, use <see cref="Remove"/> to remove
-    ///     the subview first.
-    /// </para>
-    /// <para>
-    ///     If disposing equals true, the method has been called directly or indirectly by a user's code. Managed and
-    ///     unmanaged resources can be disposed. If disposing equals false, the method has been called by the runtime from
-    ///     inside the finalizer, and you should not reference other objects. Only unmanaged resources can be disposed.
-    /// </para>
-    /// </remarks>
-    /// <param name="disposing"></param>
+    /// <inheritdoc/>
     protected override void Dispose (bool disposing)
     {
-        // BUGBUG: We should only dispose these objects if disposing == true
         LineCanvas.Dispose ();
 
+        DisposeKeyboard ();
         DisposeAdornments ();
 
         for (int i = InternalSubviews.Count - 1; i >= 0; i--)

+ 4 - 1
Terminal.Gui/View/ViewAdornments.cs

@@ -2,7 +2,10 @@
 
 public partial class View
 {
-    private void CreateAdornments ()
+    /// <summary>
+    ///    Initializes the Adornments of the View. Called by the constructor.
+    /// </summary>
+    private void SetupAdornments ()
     {
         //// TODO: Move this to Adornment as a static factory method
         if (this is not Adornment)

+ 75 - 30
Terminal.Gui/View/ViewKeyboard.cs

@@ -4,8 +4,15 @@ namespace Terminal.Gui;
 
 public partial class View
 {
-    private void AddCommands ()
+    /// <summary>
+    ///  Helper to configure all things keyboard related for a View. Called from the View constructor.
+    /// </summary>
+    private void SetupKeyboard ()
     {
+        KeyBindings = new (this);
+        HotKeySpecifier = (Rune)'_';
+        TitleTextFormatter.HotKeyChanged += TitleTextFormatter_HotKeyChanged;
+
         // By default, the HotKey command sets the focus
         AddCommand (Command.HotKey, OnHotKey);
 
@@ -13,6 +20,15 @@ public partial class View
         AddCommand (Command.Accept, OnAccept);
     }
 
+    /// <summary>
+    ///    Helper to dispose all things keyboard related for a View. Called from the View Dispose method.
+    /// </summary>
+    private void DisposeKeyboard ()
+    {
+        TitleTextFormatter.HotKeyChanged -= TitleTextFormatter_HotKeyChanged;
+        KeyBindings.Clear ();
+    }
+
     #region HotKey Support
 
     /// <summary>
@@ -113,9 +129,10 @@ public partial class View
     /// </remarks>
     /// <param name="prevHotKey">The HotKey <paramref name="hotKey"/> is replacing. Key bindings for this key will be removed.</param>
     /// <param name="hotKey">The new HotKey. If <see cref="Key.Empty"/> <paramref name="prevHotKey"/> bindings will be removed.</param>
+    /// <param name="context">Arbitrary context that can be associated with this key binding.</param>
     /// <returns><see langword="true"/> if the HotKey bindings were added.</returns>
     /// <exception cref="ArgumentException"></exception>
-    public virtual bool AddKeyBindingsForHotKey (Key prevHotKey, Key hotKey)
+    public virtual bool AddKeyBindingsForHotKey (Key prevHotKey, Key hotKey, [CanBeNull] object context = null)
     {
         if (_hotKey == hotKey)
         {
@@ -178,15 +195,16 @@ public partial class View
         // Add the new 
         if (newKey != Key.Empty)
         {
+            KeyBinding keyBinding = new ([Command.HotKey], KeyBindingScope.HotKey, context);
             // Add the base and Alt key
-            KeyBindings.Add (newKey, KeyBindingScope.HotKey, Command.HotKey);
-            KeyBindings.Add (newKey.WithAlt, KeyBindingScope.HotKey, Command.HotKey);
+            KeyBindings.Add (newKey, keyBinding);
+            KeyBindings.Add (newKey.WithAlt, keyBinding);
 
             // If the Key is A..Z, add ShiftMask and AltMask | ShiftMask
             if (newKey.IsKeyCodeAtoZ)
             {
-                KeyBindings.Add (newKey.WithShift, KeyBindingScope.HotKey, Command.HotKey);
-                KeyBindings.Add (newKey.WithShift.WithAlt, KeyBindingScope.HotKey, Command.HotKey);
+                KeyBindings.Add (newKey.WithShift, keyBinding);
+                KeyBindings.Add (newKey.WithShift.WithAlt, keyBinding);
             }
         }
 
@@ -601,9 +619,9 @@ public partial class View
     #region Key Bindings
 
     /// <summary>Gets the key bindings for this view.</summary>
-    public KeyBindings KeyBindings { get; } = new ();
+    public KeyBindings KeyBindings { get; internal set; }
 
-    private Dictionary<Command, Func<bool?>> CommandImplementations { get; } = new ();
+    private Dictionary<Command, Func<CommandContext, bool?>> CommandImplementations { get; } = new ();
 
     /// <summary>
     ///     Low-level API called when a user presses a key; invokes any key bindings set on the view. This is called
@@ -646,17 +664,17 @@ public partial class View
             return true;
         }
 
-        if (Margin is {} && ProcessAdornmentKeyBindings (Margin, keyEvent, ref handled))
+        if (Margin is { } && ProcessAdornmentKeyBindings (Margin, keyEvent, ref handled))
         {
             return true;
         }
 
-        if (Padding is {} && ProcessAdornmentKeyBindings (Padding, keyEvent, ref handled))
+        if (Padding is { } && ProcessAdornmentKeyBindings (Padding, keyEvent, ref handled))
         {
             return true;
         }
 
-        if (Border is {} && ProcessAdornmentKeyBindings (Border, keyEvent, ref handled))
+        if (Border is { } && ProcessAdornmentKeyBindings (Border, keyEvent, ref handled))
         {
             return true;
         }
@@ -739,7 +757,7 @@ public partial class View
             }
 
             // each command has its own return value
-            bool? thisReturn = InvokeCommand (command);
+            bool? thisReturn = InvokeCommand (command, key, binding);
 
             // if we haven't got anything yet, the current command result should be used
             toReturn ??= thisReturn;
@@ -758,12 +776,13 @@ public partial class View
     ///     Invokes the specified commands.
     /// </summary>
     /// <param name="commands"></param>
+    /// <param name="key">The key that caused the commands to be invoked, if any.</param>
     /// <returns>
     ///     <see langword="null"/> if no command was found.
     ///     <see langword="true"/> if the command was invoked and it handled the command.
     ///     <see langword="false"/> if the command was invoked and it did not handle the command.
     /// </returns>
-    public bool? InvokeCommands (Command [] commands)
+    public bool? InvokeCommands (Command [] commands, [CanBeNull] Key key = null, [CanBeNull] KeyBinding? keyBinding = null)
     {
         bool? toReturn = null;
 
@@ -775,7 +794,7 @@ public partial class View
             }
 
             // each command has its own return value
-            bool? thisReturn = InvokeCommand (command);
+            bool? thisReturn = InvokeCommand (command, key, keyBinding);
 
             // if we haven't got anything yet, the current command result should be used
             toReturn ??= thisReturn;
@@ -791,42 +810,68 @@ public partial class View
     }
 
     /// <summary>Invokes the specified command.</summary>
-    /// <param name="command"></param>
+    /// <param name="command">The command to invoke.</param>
+    /// <param name="key">The key that caused the command to be invoked, if any.</param>
+    /// <param name="keyBinding"></param>
     /// <returns>
-    ///     <see langword="null"/> if no command was found. <see langword="true"/> if the command was invoked and it
-    ///     handled the command. <see langword="false"/> if the command was invoked and it did not handle the command.
+    ///     <see langword="null"/> if no command was found. <see langword="true"/> if the command was invoked, and it
+    ///     handled the command. <see langword="false"/> if the command was invoked, and it did not handle the command.
     /// </returns>
-    public bool? InvokeCommand (Command command)
+    public bool? InvokeCommand (Command command, [CanBeNull] Key key = null, [CanBeNull] KeyBinding? keyBinding = null)
     {
-        if (!CommandImplementations.ContainsKey (command))
+        if (CommandImplementations.TryGetValue (command, out Func<CommandContext, bool?> implementation))
         {
-            return null;
+            var context = new CommandContext (command, key, keyBinding); // Create the context here
+            return implementation (context);
         }
 
-        return CommandImplementations [command] ();
+        return null;
+    }
+
+    /// <summary>
+    ///     <para>
+    ///         Sets the function that will be invoked for a <see cref="Command"/>. Views should call
+    ///        AddCommand for each command they support.
+    ///     </para>
+    ///     <para>
+    ///         If AddCommand has already been called for <paramref name="command"/> <paramref name="f"/> will
+    ///         replace the old one.
+    ///     </para>
+    /// </summary>
+    /// <remarks>
+    /// <para>
+    ///     This version of AddCommand is for commands that require <see cref="CommandContext"/>. Use <see cref="AddCommand(Command,Func{System.Nullable{bool}})"/>
+    ///     in cases where the command does not require a <see cref="CommandContext"/>.
+    /// </para>
+    /// </remarks>
+    /// <param name="command">The command.</param>
+    /// <param name="f">The function.</param>
+    protected void AddCommand (Command command, Func<CommandContext, bool?> f)
+    {
+        CommandImplementations [command] = f;
     }
 
     /// <summary>
     ///     <para>
     ///         Sets the function that will be invoked for a <see cref="Command"/>. Views should call
-    ///         <see cref="AddCommand"/> for each command they support.
+    ///        AddCommand for each command they support.
     ///     </para>
     ///     <para>
-    ///         If <see cref="AddCommand"/> has already been called for <paramref name="command"/> <paramref name="f"/> will
+    ///         If AddCommand has already been called for <paramref name="command"/> <paramref name="f"/> will
     ///         replace the old one.
     ///     </para>
     /// </summary>
+    /// <remarks>
+    /// <para>
+    ///     This version of AddCommand is for commands that do not require a <see cref="CommandContext"/>.
+    ///     If the command requires context, use <see cref="AddCommand(Command,Func{CommandContext,System.Nullable{bool}})"/>
+    /// </para>
+    /// </remarks>
     /// <param name="command">The command.</param>
     /// <param name="f">The function.</param>
     protected void AddCommand (Command command, Func<bool?> f)
     {
-        // if there is already an implementation of this command
-        // replace that implementation
-        // else record how to perform the action (this should be the normal case)
-        if (CommandImplementations is { })
-        {
-            CommandImplementations [command] = f;
-        }
+        CommandImplementations [command] = ctx => f (); ;
     }
 
     /// <summary>Returns all commands that are supported by this <see cref="View"/>.</summary>

+ 4 - 0
Terminal.Gui/View/ViewMouse.cs

@@ -447,6 +447,10 @@ public partial class View
     internal bool SetHighlight (HighlightStyle style)
     {
         // TODO: Make the highlight colors configurable
+        if (!CanFocus)
+        {
+            return false;
+        }
 
         // Enable override via virtual method and/or event
         if (OnHighlight (style) == true)

+ 9 - 0
Terminal.Gui/View/ViewText.cs

@@ -4,6 +4,15 @@ namespace Terminal.Gui;
 
 public partial class View
 {
+    /// <summary>
+    ///    Initializes the Text of the View. Called by the constructor.
+    /// </summary>
+    private void SetupText ()
+    {
+        Text = string.Empty;
+        TextDirection = TextDirection.LeftRight_TopBottom;
+    }
+
     private string _text;
 
     /// <summary>

+ 43 - 473
Terminal.Gui/Views/Menu/Menu.cs

@@ -1,292 +1,5 @@
 namespace Terminal.Gui;
 
-/// <summary>Specifies how a <see cref="MenuItem"/> shows selection state.</summary>
-[Flags]
-public enum MenuItemCheckStyle
-{
-    /// <summary>The menu item will be shown normally, with no check indicator. The default.</summary>
-    NoCheck = 0b_0000_0000,
-
-    /// <summary>The menu item will indicate checked/un-checked state (see <see cref="Checked"/>).</summary>
-    Checked = 0b_0000_0001,
-
-    /// <summary>The menu item is part of a menu radio group (see <see cref="Checked"/>) and will indicate selected state.</summary>
-    Radio = 0b_0000_0010
-}
-
-/// <summary>
-///     A <see cref="MenuItem"/> has title, an associated help text, and an action to execute on activation. MenuItems
-///     can also have a checked indicator (see <see cref="Checked"/>).
-/// </summary>
-public class MenuItem
-{
-    private readonly ShortcutHelper _shortcutHelper;
-    private bool _allowNullChecked;
-    private MenuItemCheckStyle _checkType;
-
-    private string _title;
-
-    // TODO: Update to use Key instead of KeyCode
-    /// <summary>Initializes a new instance of <see cref="MenuItem"/></summary>
-    public MenuItem (KeyCode shortcut = KeyCode.Null) : this ("", "", null, null, null, shortcut) { }
-
-    // TODO: Update to use Key instead of KeyCode
-    /// <summary>Initializes a new instance of <see cref="MenuItem"/>.</summary>
-    /// <param name="title">Title for the menu item.</param>
-    /// <param name="help">Help text to display.</param>
-    /// <param name="action">Action to invoke when the menu item is activated.</param>
-    /// <param name="canExecute">Function to determine if the action can currently be executed.</param>
-    /// <param name="parent">The <see cref="Parent"/> of this menu item.</param>
-    /// <param name="shortcut">The <see cref="Shortcut"/> keystroke combination.</param>
-    public MenuItem (
-        string title,
-        string help,
-        Action action,
-        Func<bool> canExecute = null,
-        MenuItem parent = null,
-        KeyCode shortcut = KeyCode.Null
-    )
-    {
-        Title = title ?? "";
-        Help = help ?? "";
-        Action = action;
-        CanExecute = canExecute;
-        Parent = parent;
-        _shortcutHelper = new ShortcutHelper ();
-
-        if (shortcut != KeyCode.Null)
-        {
-            Shortcut = shortcut;
-        }
-    }
-
-    /// <summary>Gets or sets the action to be invoked when the menu item is triggered.</summary>
-    /// <value>Method to invoke.</value>
-    public Action Action { get; set; }
-
-    /// <summary>
-    ///     Used only if <see cref="CheckType"/> is of <see cref="MenuItemCheckStyle.Checked"/> type. If
-    ///     <see langword="true"/> allows <see cref="Checked"/> to be null, true or false. If <see langword="false"/> only
-    ///     allows <see cref="Checked"/> to be true or false.
-    /// </summary>
-    public bool AllowNullChecked
-    {
-        get => _allowNullChecked;
-        set
-        {
-            _allowNullChecked = value;
-            Checked ??= false;
-        }
-    }
-
-    /// <summary>
-    ///     Gets or sets the action to be invoked to determine if the menu can be triggered. If <see cref="CanExecute"/>
-    ///     returns <see langword="true"/> the menu item will be enabled. Otherwise, it will be disabled.
-    /// </summary>
-    /// <value>Function to determine if the action is can be executed or not.</value>
-    public Func<bool> CanExecute { get; set; }
-
-    /// <summary>
-    ///     Sets or gets whether the <see cref="MenuItem"/> shows a check indicator or not. See
-    ///     <see cref="MenuItemCheckStyle"/>.
-    /// </summary>
-    public bool? Checked { set; get; }
-
-    /// <summary>
-    ///     Sets or gets the <see cref="MenuItemCheckStyle"/> of a menu item where <see cref="Checked"/> is set to
-    ///     <see langword="true"/>.
-    /// </summary>
-    public MenuItemCheckStyle CheckType
-    {
-        get => _checkType;
-        set
-        {
-            _checkType = value;
-
-            if (_checkType == MenuItemCheckStyle.Checked && !_allowNullChecked && Checked is null)
-            {
-                Checked = false;
-            }
-        }
-    }
-
-    /// <summary>Gets or sets arbitrary data for the menu item.</summary>
-    /// <remarks>This property is not used internally.</remarks>
-    public object Data { get; set; }
-
-    /// <summary>Gets or sets the help text for the menu item. The help text is drawn to the right of the <see cref="Title"/>.</summary>
-    /// <value>The help text.</value>
-    public string Help { get; set; }
-
-    /// <summary>Gets the parent for this <see cref="MenuItem"/>.</summary>
-    /// <value>The parent.</value>
-    public MenuItem Parent { get; set; }
-
-    /// <summary>Gets or sets the title of the menu item .</summary>
-    /// <value>The title.</value>
-    public string Title
-    {
-        get => _title;
-        set
-        {
-            if (_title == value)
-            {
-                return;
-            }
-
-            _title = value;
-            GetHotKey ();
-        }
-    }
-
-    /// <summary>Gets if this <see cref="MenuItem"/> is from a sub-menu.</summary>
-    internal bool IsFromSubMenu => Parent != null;
-
-    internal int TitleLength => GetMenuBarItemLength (Title);
-
-    // 
-    // ┌─────────────────────────────┐
-    // │ Quit  Quit UI Catalog  Ctrl+Q │
-    // └─────────────────────────────┘
-    // ┌─────────────────┐
-    // │ ◌ TopLevel Alt+T │
-    // └─────────────────┘
-    // TODO: Replace the `2` literals with named constants 
-    internal int Width => 1
-                          + // space before Title
-                          TitleLength
-                          + 2
-                          + // space after Title - BUGBUG: This should be 1 
-                          (Checked == true || CheckType.HasFlag (MenuItemCheckStyle.Checked) || CheckType.HasFlag (MenuItemCheckStyle.Radio)
-                               ? 2
-                               : 0)
-                          + // check glyph + space 
-                          (Help.GetColumns () > 0 ? 2 + Help.GetColumns () : 0)
-                          + // Two spaces before Help
-                          (ShortcutTag.GetColumns () > 0
-                               ? 2 + ShortcutTag.GetColumns ()
-                               : 0); // Pad two spaces before shortcut tag (which are also aligned right)
-
-    /// <summary>Merely a debugging aid to see the interaction with main.</summary>
-    public bool GetMenuBarItem () { return IsFromSubMenu; }
-
-    /// <summary>Merely a debugging aid to see the interaction with main.</summary>
-    public MenuItem GetMenuItem () { return this; }
-
-    /// <summary>
-    ///     Returns <see langword="true"/> if the menu item is enabled. This method is a wrapper around
-    ///     <see cref="CanExecute"/>.
-    /// </summary>
-    public bool IsEnabled () { return CanExecute?.Invoke () ?? true; }
-
-    /// <summary>
-    ///     Toggle the <see cref="Checked"/> between three states if <see cref="AllowNullChecked"/> is
-    ///     <see langword="true"/> or between two states if <see cref="AllowNullChecked"/> is <see langword="false"/>.
-    /// </summary>
-    public void ToggleChecked ()
-    {
-        if (_checkType != MenuItemCheckStyle.Checked)
-        {
-            throw new InvalidOperationException ("This isn't a Checked MenuItemCheckStyle!");
-        }
-
-        bool? previousChecked = Checked;
-
-        if (AllowNullChecked)
-        {
-            Checked = previousChecked switch
-                      {
-                          null => true,
-                          true => false,
-                          false => null
-                      };
-        }
-        else
-        {
-            Checked = !Checked;
-        }
-    }
-
-    private static int GetMenuBarItemLength (string title)
-    {
-        return title.EnumerateRunes ()
-                    .Where (ch => ch != MenuBar.HotKeySpecifier)
-                    .Sum (ch => Math.Max (ch.GetColumns (), 1));
-    }
-
-    #region Keyboard Handling
-
-    // TODO: Update to use Key instead of Rune
-    /// <summary>
-    ///     The HotKey is used to activate a <see cref="MenuItem"/> with the keyboard. HotKeys are defined by prefixing the
-    ///     <see cref="Title"/> of a MenuItem with an underscore ('_').
-    ///     <para>
-    ///         Pressing Alt-Hotkey for a <see cref="MenuBarItem"/> (menu items on the menu bar) works even if the menu is
-    ///         not active). Once a menu has focus and is active, pressing just the HotKey will activate the MenuItem.
-    ///     </para>
-    ///     <para>
-    ///         For example for a MenuBar with a "_File" MenuBarItem that contains a "_New" MenuItem, Alt-F will open the
-    ///         File menu. Pressing the N key will then activate the New MenuItem.
-    ///     </para>
-    ///     <para>See also <see cref="Shortcut"/> which enable global key-bindings to menu items.</para>
-    /// </summary>
-    public Rune HotKey { get; set; }
-
-    private void GetHotKey ()
-    {
-        var nextIsHot = false;
-
-        foreach (char x in _title)
-        {
-            if (x == MenuBar.HotKeySpecifier.Value)
-            {
-                nextIsHot = true;
-            }
-            else
-            {
-                if (nextIsHot)
-                {
-                    HotKey = (Rune)char.ToUpper (x);
-
-                    break;
-                }
-
-                nextIsHot = false;
-                HotKey = default (Rune);
-            }
-        }
-    }
-
-    // TODO: Update to use Key instead of KeyCode
-    /// <summary>
-    ///     Shortcut defines a key binding to the MenuItem that will invoke the MenuItem's action globally for the
-    ///     <see cref="View"/> that is the parent of the <see cref="MenuBar"/> or <see cref="ContextMenu"/> this
-    ///     <see cref="MenuItem"/>.
-    ///     <para>
-    ///         The <see cref="KeyCode"/> will be drawn on the MenuItem to the right of the <see cref="Title"/> and
-    ///         <see cref="Help"/> text. See <see cref="ShortcutTag"/>.
-    ///     </para>
-    /// </summary>
-    public KeyCode Shortcut
-    {
-        get => _shortcutHelper.Shortcut;
-        set
-        {
-            if (_shortcutHelper.Shortcut != value && (ShortcutHelper.PostShortcutValidation (value) || value == KeyCode.Null))
-            {
-                _shortcutHelper.Shortcut = value;
-            }
-        }
-    }
-
-    /// <summary>Gets the text describing the keystroke combination defined by <see cref="Shortcut"/>.</summary>
-    public string ShortcutTag => _shortcutHelper.Shortcut == KeyCode.Null
-                                     ? string.Empty
-                                     : Key.ToString (_shortcutHelper.Shortcut, MenuBar.ShortcutDelimiter);
-
-    #endregion Keyboard Handling
-}
-
 /// <summary>
 ///     An internal class used to represent a menu pop-up menu. Created and managed by <see cref="MenuBar"/> and
 ///     <see cref="ContextMenu"/>.
@@ -408,15 +121,6 @@ internal sealed class Menu : View
                    );
 
         AddKeyBindings (_barItems);
-#if SUPPORT_ALT_TO_ACTIVATE_MENU
-        Initialized += (s, e) =>
-                       {
-                           if (SuperView is { })
-                           {
-                               SuperView.KeyUp += SuperView_KeyUp;
-                           }
-                       };
-#endif
     }
 
     public Menu ()
@@ -462,9 +166,9 @@ internal sealed class Menu : View
                         return true;
                     }
                    );
-        AddCommand (Command.Select, () => _host?.SelectItem (_menuItemToSelect));
-        AddCommand (Command.ToggleExpandCollapse, () => SelectOrRun ());
-        AddCommand (Command.HotKey, () => _host?.SelectItem (_menuItemToSelect));
+        AddCommand (Command.Select, ctx => _host?.SelectItem (ctx.KeyBinding?.Context as MenuItem));
+        AddCommand (Command.ToggleExpandCollapse, ctx => ExpandCollapse (ctx.KeyBinding?.Context as MenuItem));
+        AddCommand (Command.HotKey, ctx => _host?.SelectItem (ctx.KeyBinding?.Context as MenuItem));
 
         // Default key bindings for this view
         KeyBindings.Add (Key.CursorUp, Command.LineUp);
@@ -473,26 +177,7 @@ internal sealed class Menu : View
         KeyBindings.Add (Key.CursorRight, Command.Right);
         KeyBindings.Add (Key.Esc, Command.Cancel);
         KeyBindings.Add (Key.Enter, Command.Accept);
-        KeyBindings.Add (Key.F9, KeyBindingScope.HotKey, Command.ToggleExpandCollapse);
-
-        KeyBindings.Add (
-                         KeyCode.CtrlMask | KeyCode.Space,
-                         KeyBindingScope.HotKey,
-                         Command.ToggleExpandCollapse
-                        );
-    }
-
-#if SUPPORT_ALT_TO_ACTIVATE_MENU
-    void SuperView_KeyUp (object sender, KeyEventArgs e)
-    {
-        if (SuperView is null || SuperView.CanFocus == false || SuperView.Visible == false)
-        {
-            return;
-        }
-
-        _host.AltKeyUpHandler (e);
     }
-#endif
 
     private void AddKeyBindings (MenuBarItem menuBarItem)
     {
@@ -503,16 +188,18 @@ internal sealed class Menu : View
 
         foreach (MenuItem menuItem in menuBarItem.Children.Where (m => m is { }))
         {
-            KeyBindings.Add ((KeyCode)menuItem.HotKey.Value, Command.ToggleExpandCollapse);
+            KeyBinding keyBinding = new ([Command.ToggleExpandCollapse], KeyBindingScope.HotKey, menuItem);
 
-            KeyBindings.Add (
-                             (KeyCode)menuItem.HotKey.Value | KeyCode.AltMask,
-                             Command.ToggleExpandCollapse
-                            );
+            if ((KeyCode)menuItem.HotKey.Value != KeyCode.Null)
+            {
+                KeyBindings.Add ((KeyCode)menuItem.HotKey.Value, keyBinding);
+                KeyBindings.Add ((KeyCode)menuItem.HotKey.Value | KeyCode.AltMask, keyBinding);
+            }
 
             if (menuItem.Shortcut != KeyCode.Null)
             {
-                KeyBindings.Add (menuItem.Shortcut, KeyBindingScope.HotKey, Command.Select);
+                keyBinding = new ([Command.Select], KeyBindingScope.HotKey, menuItem);
+                KeyBindings.Add (menuItem.Shortcut, keyBinding);
             }
 
             MenuBarItem subMenu = menuBarItem.SubMenu (menuItem);
@@ -520,25 +207,29 @@ internal sealed class Menu : View
         }
     }
 
-    private int _menuBarItemToActivate = -1;
-    private MenuItem _menuItemToSelect;
-
-    /// <summary>Called when a key bound to Command.Select is pressed. This means a hot key was pressed.</summary>
+    /// <summary>Called when a key bound to Command.ToggleExpandCollapse is pressed. This means a hot key was pressed.</summary>
     /// <returns></returns>
-    private bool SelectOrRun ()
+    private bool ExpandCollapse (MenuItem menuItem)
     {
         if (!IsInitialized || !Visible)
         {
             return true;
         }
 
-        if (_menuBarItemToActivate != -1)
+
+        for (var c = 0; c < _barItems.Children.Length; c++)
         {
-            _host.Activate (1, _menuBarItemToActivate);
+            if (_barItems.Children [c] == menuItem)
+            {
+                _currentChild = c;
+
+                break;
+            }
         }
-        else if (_menuItemToSelect is { })
+
+        if (menuItem is { })
         {
-            var m = _menuItemToSelect as MenuBarItem;
+            var m = menuItem as MenuBarItem;
 
             if (m?.Children?.Length > 0)
             {
@@ -566,7 +257,7 @@ internal sealed class Menu : View
             }
             else
             {
-                _host.SelectItem (_menuItemToSelect);
+                _host.SelectItem (menuItem);
             }
         }
         else if (_host.IsMenuOpen)
@@ -578,82 +269,12 @@ internal sealed class Menu : View
             _host.OpenMenu ();
         }
 
-        //_openedByHotKey = true;
         return true;
     }
 
     /// <inheritdoc/>
     public override bool? OnInvokingKeyBindings (Key keyEvent)
     {
-        // This is a bit of a hack. We want to handle the key bindings for menu bar but
-        // InvokeKeyBindings doesn't pass any context so we can't tell which item it is for.
-        // So before we call the base class we set SelectedItem appropriately.
-
-        KeyCode key = keyEvent.KeyCode;
-
-        if (KeyBindings.TryGet (key, out _))
-        {
-            _menuBarItemToActivate = -1;
-            _menuItemToSelect = null;
-
-            MenuItem [] children = _barItems.Children;
-
-            if (children is null)
-            {
-                return base.OnInvokingKeyBindings (keyEvent);
-            }
-
-            // Search for shortcuts first. If there's a shortcut, we don't want to activate the menu item.
-            foreach (MenuItem c in children)
-            {
-                if (key == c?.Shortcut)
-                {
-                    _menuBarItemToActivate = -1;
-                    _menuItemToSelect = c;
-                    //keyEvent.Scope = KeyBindingScope.HotKey;
-
-                    return base.OnInvokingKeyBindings (keyEvent);
-                }
-
-                MenuBarItem subMenu = _barItems.SubMenu (c);
-
-                if (FindShortcutInChildMenu (key, subMenu))
-                {
-                    //keyEvent.Scope = KeyBindingScope.HotKey;
-
-                    return base.OnInvokingKeyBindings (keyEvent);
-                }
-            }
-
-            // Search for hot keys next.
-            for (var c = 0; c < children.Length; c++)
-            {
-                int hotKeyValue = children [c]?.HotKey.Value ?? default (int);
-                var hotKey = (KeyCode)hotKeyValue;
-
-                if (hotKey == KeyCode.Null)
-                {
-                    continue;
-                }
-
-                bool matches = key == hotKey || key == (hotKey | KeyCode.AltMask);
-
-                if (!_host.IsMenuOpen)
-                {
-                    // If the menu is open, only match if Alt is not pressed.
-                    matches = key == hotKey;
-                }
-
-                if (matches)
-                {
-                    _menuItemToSelect = children [c];
-                    _currentChild = c;
-
-                    return base.OnInvokingKeyBindings (keyEvent);
-                }
-            }
-        }
-
         bool? handled = base.OnInvokingKeyBindings (keyEvent);
 
         if (handled is { } && (bool)handled)
@@ -661,34 +282,11 @@ internal sealed class Menu : View
             return true;
         }
 
+        // TODO: Determine if there's a cleaner way to handle this
         // This supports the case where the menu bar is a context menu
         return _host.OnInvokingKeyBindings (keyEvent);
     }
 
-    private bool FindShortcutInChildMenu (KeyCode key, MenuBarItem menuBarItem)
-    {
-        if (menuBarItem?.Children is null)
-        {
-            return false;
-        }
-
-        foreach (MenuItem menuItem in menuBarItem.Children)
-        {
-            if (key == menuItem?.Shortcut)
-            {
-                _menuBarItemToActivate = -1;
-                _menuItemToSelect = menuItem;
-
-                return true;
-            }
-
-            MenuBarItem subMenu = menuBarItem.SubMenu (menuItem);
-            FindShortcutInChildMenu (key, subMenu);
-        }
-
-        return false;
-    }
-
     private void Current_TerminalResized (object sender, SizeChangedEventArgs e)
     {
         if (_host.IsMenuOpen)
@@ -727,6 +325,7 @@ internal sealed class Menu : View
         View view = a.View ?? this;
 
         Point boundsPoint = view.ScreenToViewport (new (a.Position.X, a.Position.Y));
+
         var me = new MouseEvent
         {
             Position = boundsPoint,
@@ -786,12 +385,12 @@ internal sealed class Menu : View
 
             Driver.SetAttribute (
                                  item is null ? GetNormalColor () :
-                                 i == _currentChild ? GetFocusColor() : GetNormalColor ()
+                                 i == _currentChild ? GetFocusColor () : GetNormalColor ()
                                 );
 
             if (item is null && BorderStyle != LineStyle.None)
             {
-                var s = ViewportToScreen (new Point (-1, i));
+                Point s = ViewportToScreen (new Point (-1, i));
                 Driver.Move (s.X, s.Y);
                 Driver.AddRune (Glyphs.LeftTee);
             }
@@ -839,7 +438,7 @@ internal sealed class Menu : View
             {
                 if (BorderStyle != LineStyle.None && SuperView?.Frame.Right - Frame.X > Frame.Width)
                 {
-                    var s = ViewportToScreen (new Point (Frame.Width - 2, i));
+                    Point s = ViewportToScreen (new Point (Frame.Width - 2, i));
                     Driver.Move (s.X, s.Y);
                     Driver.AddRune (Glyphs.RightTee);
                 }
@@ -876,7 +475,8 @@ internal sealed class Menu : View
                 textToDraw = item.Title;
             }
 
-            var screen = ViewportToScreen (new Point(0  , i));
+            Point screen = ViewportToScreen (new Point (0, i));
+
             if (screen.X < Driver.Cols)
             {
                 Driver.Move (screen.X + 1, screen.Y);
@@ -895,7 +495,7 @@ internal sealed class Menu : View
 
                     // The -3 is left/right border + one space (not sure what for)
                     tf.Draw (
-                             ViewportToScreen (new Rectangle(1, i, Frame.Width - 3, 1)),
+                             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
@@ -934,7 +534,7 @@ internal sealed class Menu : View
 
         Driver.Clip = savedClip;
 
-       // PositionCursor ();
+        // PositionCursor ();
     }
 
     private void Current_DrawContentComplete (object sender, DrawEventArgs e)
@@ -953,13 +553,10 @@ internal sealed class Menu : View
             {
                 return _host?.PositionCursor ();
             }
-            else
-            {
-                Move (2, 1 + _currentChild);
 
-                return null; // Don't show the cursor
+            Move (2, 1 + _currentChild);
 
-            }
+            return null; // Don't show the cursor
         }
 
         return _host?.PositionCursor ();
@@ -1031,11 +628,11 @@ internal sealed class Menu : View
                 _currentChild = 0;
             }
 
-            if (this != _host.openCurrentMenu && _barItems.Children [_currentChild]?.IsFromSubMenu == true && _host._selectedSub > -1)
+            if (this != _host.OpenCurrentMenu && _barItems.Children [_currentChild]?.IsFromSubMenu == true && _host._selectedSub > -1)
             {
                 _host.PreviousMenu (true);
                 _host.SelectEnabledItem (_barItems.Children, _currentChild, out _currentChild);
-                _host.openCurrentMenu = this;
+                _host.OpenCurrentMenu = this;
             }
 
             MenuItem item = _barItems.Children [_currentChild];
@@ -1096,7 +693,7 @@ internal sealed class Menu : View
 
             if (_host.UseKeysUpDownAsKeysLeftRight && !_host.UseSubMenusSingleFrame)
             {
-                if ((_currentChild == -1 || this != _host.openCurrentMenu)
+                if ((_currentChild == -1 || this != _host.OpenCurrentMenu)
                     && _barItems.Children [_currentChild + 1].IsFromSubMenu
                     && _host._selectedSub > -1)
                 {
@@ -1106,7 +703,7 @@ internal sealed class Menu : View
                     if (_currentChild > 0)
                     {
                         _currentChild--;
-                        _host.openCurrentMenu = this;
+                        _host.OpenCurrentMenu = this;
                     }
 
                     break;
@@ -1176,7 +773,7 @@ internal sealed class Menu : View
         _host?.SetNeedsDisplay ();
     }
 
-    protected internal override bool OnMouseEvent  (MouseEvent me)
+    protected internal override bool OnMouseEvent (MouseEvent me)
     {
         if (!_host._handled && !_host.HandleGrabView (me, this))
         {
@@ -1285,8 +882,8 @@ internal sealed class Menu : View
             }
 
             if (pos == -1
-                && this != _host.openCurrentMenu
-                && subMenu.Children != _host.openCurrentMenu._barItems.Children
+                && this != _host.OpenCurrentMenu
+                && subMenu.Children != _host.OpenCurrentMenu._barItems.Children
                 && !_host.CloseMenu (false, true))
             {
                 return false;
@@ -1307,33 +904,6 @@ internal sealed class Menu : View
         return true;
     }
 
-    private int GetSubMenuIndex (MenuBarItem subMenu)
-    {
-        int pos = -1;
-
-        if (Subviews.Count == 0)
-        {
-            return pos;
-        }
-
-        Menu v = null;
-
-        foreach (View menu in Subviews)
-        {
-            if (((Menu)menu)._barItems == subMenu)
-            {
-                v = (Menu)menu;
-            }
-        }
-
-        if (v is { })
-        {
-            pos = Subviews.IndexOf (v);
-        }
-
-        return pos;
-    }
-
     protected override void Dispose (bool disposing)
     {
         if (Application.Current is { })

+ 127 - 450
Terminal.Gui/Views/Menu/MenuBar.cs

@@ -1,191 +1,5 @@
 namespace Terminal.Gui;
 
-/// <summary>
-///     <see cref="MenuBarItem"/> is a menu item on  <see cref="MenuBar"/>. MenuBarItems do not support
-///     <see cref="MenuItem.Shortcut"/>.
-/// </summary>
-public class MenuBarItem : MenuItem
-{
-    /// <summary>Initializes a new <see cref="MenuBarItem"/> as a <see cref="MenuItem"/>.</summary>
-    /// <param name="title">Title for the menu item.</param>
-    /// <param name="help">Help text to display. Will be displayed next to the Title surrounded by parentheses.</param>
-    /// <param name="action">Action to invoke when the menu item is activated.</param>
-    /// <param name="canExecute">Function to determine if the action can currently be executed.</param>
-    /// <param name="parent">The parent <see cref="MenuItem"/> of this if exist, otherwise is null.</param>
-    public MenuBarItem (
-        string title,
-        string help,
-        Action action,
-        Func<bool> canExecute = null,
-        MenuItem parent = null
-    ) : base (title, help, action, canExecute, parent)
-    {
-        SetInitialProperties (title, null, null, true);
-    }
-
-    /// <summary>Initializes a new <see cref="MenuBarItem"/>.</summary>
-    /// <param name="title">Title for the menu item.</param>
-    /// <param name="children">The items in the current menu.</param>
-    /// <param name="parent">The parent <see cref="MenuItem"/> of this if exist, otherwise is null.</param>
-    public MenuBarItem (string title, MenuItem [] children, MenuItem parent = null) { SetInitialProperties (title, children, parent); }
-
-    /// <summary>Initializes a new <see cref="MenuBarItem"/> with separate list of items.</summary>
-    /// <param name="title">Title for the menu item.</param>
-    /// <param name="children">The list of items in the current menu.</param>
-    /// <param name="parent">The parent <see cref="MenuItem"/> of this if exist, otherwise is null.</param>
-    public MenuBarItem (string title, List<MenuItem []> children, MenuItem parent = null) { SetInitialProperties (title, children, parent); }
-
-    /// <summary>Initializes a new <see cref="MenuBarItem"/>.</summary>
-    /// <param name="children">The items in the current menu.</param>
-    public MenuBarItem (MenuItem [] children) : this ("", children) { }
-
-    /// <summary>Initializes a new <see cref="MenuBarItem"/>.</summary>
-    public MenuBarItem () : this (new MenuItem [] { }) { }
-
-    /// <summary>
-    ///     Gets or sets an array of <see cref="MenuItem"/> objects that are the children of this
-    ///     <see cref="MenuBarItem"/>
-    /// </summary>
-    /// <value>The children.</value>
-    public MenuItem [] Children { get; set; }
-
-    internal bool IsTopLevel => Parent is null && (Children is null || Children.Length == 0) && Action != null;
-
-    /// <summary>Get the index of a child <see cref="MenuItem"/>.</summary>
-    /// <param name="children"></param>
-    /// <returns>Returns a greater than -1 if the <see cref="MenuItem"/> is a child.</returns>
-    public int GetChildrenIndex (MenuItem children)
-    {
-        var i = 0;
-
-        if (Children is { })
-        {
-            foreach (MenuItem child in Children)
-            {
-                if (child == children)
-                {
-                    return i;
-                }
-
-                i++;
-            }
-        }
-
-        return -1;
-    }
-
-    /// <summary>Check if a <see cref="MenuItem"/> is a submenu of this MenuBar.</summary>
-    /// <param name="menuItem"></param>
-    /// <returns>Returns <c>true</c> if it is a submenu. <c>false</c> otherwise.</returns>
-    public bool IsSubMenuOf (MenuItem menuItem)
-    {
-        foreach (MenuItem child in Children)
-        {
-            if (child == menuItem && child.Parent == menuItem.Parent)
-            {
-                return true;
-            }
-        }
-
-        return false;
-    }
-
-    /// <summary>Check if a <see cref="MenuItem"/> is a <see cref="MenuBarItem"/>.</summary>
-    /// <param name="menuItem"></param>
-    /// <returns>Returns a <see cref="MenuBarItem"/> or null otherwise.</returns>
-    public MenuBarItem SubMenu (MenuItem menuItem) { return menuItem as MenuBarItem; }
-
-    internal void AddKeyBindings (MenuBar menuBar)
-    {
-        if (Children is null)
-        {
-            return;
-        }
-
-        foreach (MenuItem menuItem in Children.Where (m => m is { }))
-        {
-            if (menuItem.HotKey != default (Rune))
-            {
-                menuBar.KeyBindings.Add ((KeyCode)menuItem.HotKey.Value, Command.ToggleExpandCollapse);
-
-                menuBar.KeyBindings.Add (
-                                         (KeyCode)menuItem.HotKey.Value | KeyCode.AltMask,
-                                         KeyBindingScope.HotKey,
-                                         Command.ToggleExpandCollapse
-                                        );
-            }
-
-            if (menuItem.Shortcut != KeyCode.Null)
-            {
-                menuBar.KeyBindings.Add (menuItem.Shortcut, KeyBindingScope.HotKey, Command.Select);
-            }
-
-            SubMenu (menuItem)?.AddKeyBindings (menuBar);
-        }
-    }
-
-    private void SetInitialProperties (string title, object children, MenuItem parent = null, bool isTopLevel = false)
-    {
-        if (!isTopLevel && children is null)
-        {
-            throw new ArgumentNullException (
-                                             nameof (children),
-                                             "The parameter cannot be null. Use an empty array instead."
-                                            );
-        }
-
-        SetTitle (title ?? "");
-
-        if (parent is { })
-        {
-            Parent = parent;
-        }
-
-        if (children is List<MenuItem []> childrenList)
-        {
-            MenuItem [] newChildren = { };
-
-            foreach (MenuItem [] grandChild in childrenList)
-            {
-                foreach (MenuItem child in grandChild)
-                {
-                    SetParent (grandChild);
-                    Array.Resize (ref newChildren, newChildren.Length + 1);
-                    newChildren [newChildren.Length - 1] = child;
-                }
-            }
-
-            Children = newChildren;
-        }
-        else if (children is MenuItem [] items)
-        {
-            SetParent (items);
-            Children = items;
-        }
-        else
-        {
-            Children = null;
-        }
-    }
-
-    private void SetParent (MenuItem [] children)
-    {
-        foreach (MenuItem child in children)
-        {
-            if (child is { Parent: null })
-            {
-                child.Parent = this;
-            }
-        }
-    }
-
-    private void SetTitle (string title)
-    {
-        title ??= string.Empty;
-        Title = title;
-    }
-}
-
 /// <summary>
 ///     <para>Provides a menu bar that spans the top of a <see cref="Toplevel"/> View with drop-down and cascading menus.</para>
 ///     <para>
@@ -236,7 +50,6 @@ public class MenuBar : View
     internal bool _isMenuClosing;
     internal bool _isMenuOpening;
 
-    // BUGBUG: Hack
     internal Menu _openMenu;
     internal List<Menu> _openSubMenu;
     internal int _selected;
@@ -308,9 +121,8 @@ public class MenuBar : View
                         return true;
                     }
                    );
-
-        AddCommand (Command.ToggleExpandCollapse, () => SelectOrRun ());
-        AddCommand (Command.Select, () => Run (_menuItemToSelect?.Action));
+        AddCommand (Command.ToggleExpandCollapse, ctx => Select ((int)ctx.KeyBinding?.Context!));
+        AddCommand (Command.Select, ctx => Run ((ctx.KeyBinding?.Context as MenuItem)?.Action));
 
         // Default key bindings for this view
         KeyBindings.Add (Key.CursorLeft, Command.Left);
@@ -318,13 +130,15 @@ public class MenuBar : View
         KeyBindings.Add (Key.Esc, Command.Cancel);
         KeyBindings.Add (Key.CursorDown, Command.Accept);
         KeyBindings.Add (Key.Enter, Command.Accept);
-        KeyBindings.Add (Key, KeyBindingScope.HotKey, Command.ToggleExpandCollapse);
 
-        KeyBindings.Add (
-                         KeyCode.CtrlMask | KeyCode.Space,
-                         KeyBindingScope.HotKey,
-                         Command.ToggleExpandCollapse
-                        );
+        KeyBinding keyBinding = new ([Command.ToggleExpandCollapse], KeyBindingScope.HotKey, -1); // -1 indicates Key was used
+        KeyBindings.Add (Key, keyBinding);
+
+        // TODO: Why do we have two keybindings for opening the menu? Ctrl-Space and Key?
+        KeyBindings.Add (Key.Space.WithCtrl, keyBinding);
+
+        // TODO: Figure out how to make Alt work (on Windows)
+        //KeyBindings.Add (Key.WithAlt, keyBinding);
     }
 
     /// <summary><see langword="true"/> if the menu is open; otherwise <see langword="true"/>.</summary>
@@ -350,43 +164,27 @@ public class MenuBar : View
                 return;
             }
 
-            // TODO: Bindings (esp for hotkey) should be added across and then down. This currently does down then across. 
-            // TODO: As a result, _File._Save will have precedence over in "_File _Edit _ScrollbarView"
-            // TODO: Also: Hotkeys should not work for sub-menus if they are not visible!
-            foreach (MenuBarItem menuBarItem in Menus?.Where (m => m is { })!)
+            // TODO: Hotkeys should not work for sub-menus if they are not visible!
+            for (var i = 0; i < Menus.Length; i++)
             {
-                if (menuBarItem.HotKey != default (Rune))
+                MenuBarItem menuBarItem = Menus [i];
+
+                if (menuBarItem?.HotKey != default (Rune))
                 {
-                    KeyBindings.Add (
-                                     (KeyCode)menuBarItem.HotKey.Value,
-                                     Command.ToggleExpandCollapse
-                                    );
-
-                    KeyBindings.Add (
-                                     (KeyCode)menuBarItem.HotKey.Value | KeyCode.AltMask,
-                                     KeyBindingScope.HotKey,
-                                     Command.ToggleExpandCollapse
-                                    );
+                    KeyBinding keyBinding = new ([Command.ToggleExpandCollapse], KeyBindingScope.HotKey, i);
+                    KeyBindings.Add ((KeyCode)menuBarItem.HotKey.Value, keyBinding);
+                    KeyBindings.Add ((KeyCode)menuBarItem.HotKey.Value | KeyCode.AltMask, keyBinding);
                 }
 
-                if (menuBarItem.Shortcut != KeyCode.Null)
+                if (menuBarItem?.Shortcut != KeyCode.Null)
                 {
                     // Technically this will never run because MenuBarItems don't have shortcuts
-                    KeyBindings.Add (menuBarItem.Shortcut, KeyBindingScope.HotKey, Command.Select);
+                    KeyBinding keyBinding = new ([Command.Select], KeyBindingScope.HotKey, i);
+                    KeyBindings.Add (menuBarItem.Shortcut, keyBinding);
                 }
 
-                menuBarItem.AddKeyBindings (this);
+                menuBarItem?.AddShortcutKeyBindings (this);
             }
-#if SUPPORT_ALT_TO_ACTIVATE_MENU
-            // Enable the Alt key as a menu activator
-            Initialized += (s, e) =>
-                           {
-                               if (SuperView is { })
-                               {
-                                   SuperView.KeyUp += SuperView_KeyUp;
-                               }
-                           };
-#endif
         }
     }
 
@@ -399,7 +197,7 @@ public class MenuBar : View
     /// <summary>
     ///     Gets or sets if the sub-menus must be displayed in a single or multiple frames.
     ///     <para>
-    ///         By default any sub-sub-menus (sub-menus of the main <see cref="MenuItem"/>s) are displayed in a cascading
+    ///         By default, any sub-sub-menus (sub-menus of the main <see cref="MenuItem"/>s) are displayed in a cascading
     ///         manner, where each sub-sub-menu pops out of the sub-menu frame (either to the right or left, depending on where
     ///         the sub-menu is relative to the edge of the screen). By setting <see cref="UseSubMenusSingleFrame"/> to
     ///         <see langword="true"/>, this behavior can be changed such that all sub-sub-menus are drawn within a single
@@ -436,7 +234,7 @@ public class MenuBar : View
         }
     }
 
-    internal Menu openCurrentMenu
+    internal Menu OpenCurrentMenu
     {
         get => _ocm;
         set
@@ -486,7 +284,7 @@ public class MenuBar : View
             if (i == _selected && IsMenuOpen)
             {
                 hotColor = i == _selected ? ColorScheme.HotFocus : GetHotNormalColor ();
-                normalColor = i == _selected ? GetFocusColor() : GetNormalColor ();
+                normalColor = i == _selected ? GetFocusColor () : GetNormalColor ();
             }
             else
             {
@@ -533,17 +331,17 @@ public class MenuBar : View
         MenuItem mi = null;
         MenuBarItem parent;
 
-        if (openCurrentMenu.BarItems.Children != null
-            && openCurrentMenu.BarItems!.Children.Length > 0
-            && openCurrentMenu?._currentChild > -1)
+        if (OpenCurrentMenu.BarItems.Children != null
+            && OpenCurrentMenu.BarItems!.Children.Length > 0
+            && OpenCurrentMenu?._currentChild > -1)
         {
-            parent = openCurrentMenu.BarItems;
-            mi = parent.Children [openCurrentMenu._currentChild];
+            parent = OpenCurrentMenu.BarItems;
+            mi = parent.Children [OpenCurrentMenu._currentChild];
         }
-        else if (openCurrentMenu!.BarItems.IsTopLevel)
+        else if (OpenCurrentMenu!.BarItems.IsTopLevel)
         {
             parent = null;
-            mi = openCurrentMenu.BarItems;
+            mi = OpenCurrentMenu.BarItems;
         }
         else
         {
@@ -587,16 +385,16 @@ public class MenuBar : View
         OpenMenu (_selected);
 
         if (!SelectEnabledItem (
-                                openCurrentMenu.BarItems.Children,
-                                openCurrentMenu._currentChild,
-                                out openCurrentMenu._currentChild
+                                OpenCurrentMenu.BarItems.Children,
+                                OpenCurrentMenu._currentChild,
+                                out OpenCurrentMenu._currentChild
                                )
             && !CloseMenu (false))
         {
             return;
         }
 
-        if (!openCurrentMenu.CheckSubMenu ())
+        if (!OpenCurrentMenu.CheckSubMenu ())
         {
             return;
         }
@@ -631,6 +429,7 @@ public class MenuBar : View
                           : 0)
                    + _rightPadding;
         }
+
         return null; // Don't show the cursor
     }
 
@@ -660,7 +459,6 @@ public class MenuBar : View
         }
 
         _openedByAltKey = false;
-        _openedByHotKey = false;
         IsMenuOpen = false;
         _selected = -1;
         CanFocus = _initialCanFocus;
@@ -697,20 +495,19 @@ public class MenuBar : View
             Application.UngrabMouse ();
         }
 
-        if (openCurrentMenu is { })
+        if (OpenCurrentMenu is { })
         {
-            openCurrentMenu = null;
+            OpenCurrentMenu = null;
         }
 
         IsMenuOpen = false;
         _openedByAltKey = false;
-        _openedByHotKey = false;
         OnMenuAllClosed ();
     }
 
     internal bool CloseMenu (bool reopen = false, bool isSubMenu = false, bool ignoreUseSubMenusSingleFrame = false)
     {
-        MenuBarItem mbi = isSubMenu ? openCurrentMenu.BarItems : _openMenu?.BarItems;
+        MenuBarItem mbi = isSubMenu ? OpenCurrentMenu.BarItems : _openMenu?.BarItems;
 
         if (UseSubMenusSingleFrame && mbi is { } && !ignoreUseSubMenusSingleFrame && mbi.Parent is { })
         {
@@ -725,7 +522,7 @@ public class MenuBar : View
         {
             _isMenuClosing = false;
 
-            if (args.CurrentMenu.Parent is { })
+            if (args.CurrentMenu.Parent is { } && _openMenu is { })
             {
                 _openMenu._currentChild =
                     ((MenuBarItem)args.CurrentMenu.Parent).Children.IndexOf (args.CurrentMenu);
@@ -744,7 +541,7 @@ public class MenuBar : View
 
                 SetNeedsDisplay ();
 
-                if (_previousFocused is { } && _previousFocused is Menu && _openMenu is { } && _previousFocused.ToString () != openCurrentMenu.ToString ())
+                if (_previousFocused is Menu && _openMenu is { } && _previousFocused.ToString () != OpenCurrentMenu.ToString ())
                 {
                     _previousFocused.SetFocus ();
                 }
@@ -752,7 +549,7 @@ public class MenuBar : View
                 _openMenu?.Dispose ();
                 _openMenu = null;
 
-                if (_lastFocused is Menu || _lastFocused is MenuBar)
+                if (_lastFocused is Menu or MenuBar)
                 {
                     _lastFocused = null;
                 }
@@ -760,7 +557,7 @@ public class MenuBar : View
                 LastFocused = _lastFocused;
                 _lastFocused = null;
 
-                if (LastFocused is { } && LastFocused.CanFocus)
+                if (LastFocused is { CanFocus: true })
                 {
                     if (!reopen)
                     {
@@ -772,11 +569,11 @@ public class MenuBar : View
                         _openSubMenu = null;
                     }
 
-                    if (openCurrentMenu is { })
+                    if (OpenCurrentMenu is { })
                     {
-                        Application.Current.Remove (openCurrentMenu);
-                        openCurrentMenu.Dispose ();
-                        openCurrentMenu = null;
+                        Application.Current?.Remove (OpenCurrentMenu);
+                        OpenCurrentMenu.Dispose ();
+                        OpenCurrentMenu = null;
                     }
 
                     LastFocused.SetFocus ();
@@ -788,7 +585,6 @@ public class MenuBar : View
                 else
                 {
                     SetFocus ();
-                    //PositionCursor ();
                 }
 
                 IsMenuOpen = false;
@@ -799,7 +595,7 @@ public class MenuBar : View
                 _selectedSub = -1;
                 SetNeedsDisplay ();
                 RemoveAllOpensSubMenus ();
-                openCurrentMenu._previousSubFocused.SetFocus ();
+                OpenCurrentMenu._previousSubFocused.SetFocus ();
                 _openSubMenu = null;
                 IsMenuOpen = true;
 
@@ -823,11 +619,13 @@ public class MenuBar : View
 
         Rectangle superViewFrame = SuperView is null ? Driver.Screen : SuperView.Frame;
         View sv = SuperView is null ? Application.Current : SuperView;
+
         if (sv is null)
         {
             // Support Unit Tests
             return Point.Empty;
         }
+
         Point viewportOffset = sv?.GetViewportOffsetFromFrame () ?? Point.Empty;
 
         return new (
@@ -836,20 +634,6 @@ public class MenuBar : View
                    );
     }
 
-    /// <summary>
-    ///     Gets the <see cref="Application.Current"/> location offset relative to the <see cref="ConsoleDriver"/>
-    ///     location.
-    /// </summary>
-    /// <returns>The location offset.</returns>
-    internal Point GetScreenOffsetFromCurrent ()
-    {
-        Rectangle screen = Driver.Screen;
-        Rectangle currentFrame = Application.Current.Frame;
-        Point viewportOffset = Application.Top.GetViewportOffsetFromFrame ();
-
-        return new (screen.X - currentFrame.X - viewportOffset.X, screen.Y - currentFrame.Y - viewportOffset.Y);
-    }
-
     internal void NextMenu (bool isSubMenu = false, bool ignoreUseSubMenusSingleFrame = false)
     {
         switch (isSubMenu)
@@ -876,9 +660,9 @@ public class MenuBar : View
                 OpenMenu (_selected);
 
                 SelectEnabledItem (
-                                   openCurrentMenu.BarItems.Children,
-                                   openCurrentMenu._currentChild,
-                                   out openCurrentMenu._currentChild
+                                   OpenCurrentMenu.BarItems.Children,
+                                   OpenCurrentMenu._currentChild,
+                                   out OpenCurrentMenu._currentChild
                                   );
 
                 break;
@@ -892,9 +676,9 @@ public class MenuBar : View
                 }
                 else
                 {
-                    MenuBarItem subMenu = openCurrentMenu._currentChild > -1 && openCurrentMenu.BarItems.Children.Length > 0
-                                              ? openCurrentMenu.BarItems.SubMenu (
-                                                                                  openCurrentMenu.BarItems.Children [openCurrentMenu._currentChild]
+                    MenuBarItem subMenu = OpenCurrentMenu._currentChild > -1 && OpenCurrentMenu.BarItems.Children.Length > 0
+                                              ? OpenCurrentMenu.BarItems.SubMenu (
+                                                                                  OpenCurrentMenu.BarItems.Children [OpenCurrentMenu._currentChild]
                                                                                  )
                                               : null;
 
@@ -908,13 +692,13 @@ public class MenuBar : View
                         NextMenu (false, ignoreUseSubMenusSingleFrame);
                     }
                     else if (subMenu != null
-                             || (openCurrentMenu._currentChild > -1
-                                 && !openCurrentMenu.BarItems
-                                                    .Children [openCurrentMenu._currentChild]
+                             || (OpenCurrentMenu._currentChild > -1
+                                 && !OpenCurrentMenu.BarItems
+                                                    .Children [OpenCurrentMenu._currentChild]
                                                     .IsFromSubMenu))
                     {
                         _selectedSub++;
-                        openCurrentMenu.CheckSubMenu ();
+                        OpenCurrentMenu.CheckSubMenu ();
                     }
                     else
                     {
@@ -930,7 +714,7 @@ public class MenuBar : View
 
                     if (UseKeysUpDownAsKeysLeftRight)
                     {
-                        openCurrentMenu.CheckSubMenu ();
+                        OpenCurrentMenu.CheckSubMenu ();
                     }
                 }
 
@@ -996,7 +780,7 @@ public class MenuBar : View
                     locationOffset.Y += SuperView.Border.Thickness.Top;
                 }
 
-                _openMenu = new()
+                _openMenu = new ()
                 {
                     Host = this,
                     X = Frame.X + pos + locationOffset.X,
@@ -1004,8 +788,8 @@ public class MenuBar : View
                     BarItems = Menus [index],
                     Parent = null
                 };
-                openCurrentMenu = _openMenu;
-                openCurrentMenu._previousSubFocused = _openMenu;
+                OpenCurrentMenu = _openMenu;
+                OpenCurrentMenu._previousSubFocused = _openMenu;
 
                 if (Application.Current is { })
                 {
@@ -1013,9 +797,10 @@ public class MenuBar : View
                 }
                 else
                 {
-                    _openMenu.BeginInit();
-                    _openMenu.EndInit();
+                    _openMenu.BeginInit ();
+                    _openMenu.EndInit ();
                 }
+
                 _openMenu.SetFocus ();
 
                 break;
@@ -1038,7 +823,7 @@ public class MenuBar : View
                     {
                         locationOffset = GetLocationOffset ();
 
-                        openCurrentMenu = new()
+                        OpenCurrentMenu = new ()
                         {
                             Host = this,
                             X = last.Frame.Left + last.Frame.Width + locationOffset.X,
@@ -1053,7 +838,7 @@ public class MenuBar : View
 
                         // 2 is for the parent and the separator
                         MenuItem [] mbi = new MenuItem [2 + subMenu.Children.Length];
-                        mbi [0] = new() { Title = subMenu.Title, Parent = subMenu };
+                        mbi [0] = new () { Title = subMenu.Title, Parent = subMenu };
                         mbi [1] = null;
 
                         for (var j = 0; j < subMenu.Children.Length; j++)
@@ -1063,23 +848,23 @@ public class MenuBar : View
 
                         var newSubMenu = new MenuBarItem (mbi) { Parent = subMenu };
 
-                        openCurrentMenu = new()
+                        OpenCurrentMenu = new ()
                         {
                             Host = this, X = first.Frame.Left, Y = first.Frame.Top, BarItems = newSubMenu
                         };
                         last.Visible = false;
-                        Application.GrabMouse (openCurrentMenu);
+                        Application.GrabMouse (OpenCurrentMenu);
                     }
 
-                    openCurrentMenu._previousSubFocused = last._previousSubFocused;
-                    _openSubMenu.Add (openCurrentMenu);
-                    Application.Current?.Add (openCurrentMenu);
+                    OpenCurrentMenu._previousSubFocused = last._previousSubFocused;
+                    _openSubMenu.Add (OpenCurrentMenu);
+                    Application.Current?.Add (OpenCurrentMenu);
 
-                    if (!openCurrentMenu.IsInitialized)
+                    if (!OpenCurrentMenu.IsInitialized)
                     {
                         // Supports unit tests
-                        openCurrentMenu.BeginInit ();
-                        openCurrentMenu.EndInit ();
+                        OpenCurrentMenu.BeginInit ();
+                        OpenCurrentMenu.EndInit ();
                     }
                 }
 
@@ -1087,12 +872,12 @@ public class MenuBar : View
 
                 if (_selectedSub > -1
                     && SelectEnabledItem (
-                                          openCurrentMenu.BarItems.Children,
-                                          openCurrentMenu._currentChild,
-                                          out openCurrentMenu._currentChild
+                                          OpenCurrentMenu.BarItems.Children,
+                                          OpenCurrentMenu._currentChild,
+                                          out OpenCurrentMenu._currentChild
                                          ))
                 {
-                    openCurrentMenu.SetFocus ();
+                    OpenCurrentMenu.SetFocus ();
                 }
 
                 break;
@@ -1124,13 +909,13 @@ public class MenuBar : View
                 OpenMenu (_selected);
 
                 if (!SelectEnabledItem (
-                                        openCurrentMenu.BarItems.Children,
-                                        openCurrentMenu._currentChild,
-                                        out openCurrentMenu._currentChild,
+                                        OpenCurrentMenu.BarItems.Children,
+                                        OpenCurrentMenu._currentChild,
+                                        out OpenCurrentMenu._currentChild,
                                         false
                                        ))
                 {
-                    openCurrentMenu._currentChild = 0;
+                    OpenCurrentMenu._currentChild = 0;
                 }
 
                 break;
@@ -1182,42 +967,35 @@ public class MenuBar : View
     }
 
     internal bool SelectEnabledItem (
-        IEnumerable<MenuItem> chldren,
+        IEnumerable<MenuItem> children,
         int current,
         out int newCurrent,
         bool forward = true
     )
     {
-        if (chldren is null)
+        if (children is null)
         {
             newCurrent = -1;
 
             return true;
         }
 
-        IEnumerable<MenuItem> childrens;
-
-        if (forward)
-        {
-            childrens = chldren;
-        }
-        else
-        {
-            childrens = chldren.Reverse ();
-        }
+        IEnumerable<MenuItem> childMenuItems = forward ? children : children.Reverse ();
 
         int count;
 
+        IEnumerable<MenuItem> menuItems = childMenuItems as MenuItem [] ?? childMenuItems.ToArray ();
+
         if (forward)
         {
             count = -1;
         }
         else
         {
-            count = childrens.Count ();
+            count = menuItems.Count ();
         }
 
-        foreach (MenuItem child in childrens)
+        foreach (MenuItem child in menuItems)
         {
             if (forward)
             {
@@ -1336,7 +1114,7 @@ public class MenuBar : View
 
         if (mi.IsTopLevel)
         {
-            var screen = ViewportToScreen (new Point (0 , i));
+            Point screen = ViewportToScreen (new Point (0, i));
             var menu = new Menu { Host = this, X = screen.X, Y = screen.Y, BarItems = mi };
             menu.Run (mi.Action);
             menu.Dispose ();
@@ -1348,16 +1126,16 @@ public class MenuBar : View
             OpenMenu (i);
 
             if (!SelectEnabledItem (
-                                    openCurrentMenu.BarItems.Children,
-                                    openCurrentMenu._currentChild,
-                                    out openCurrentMenu._currentChild
+                                    OpenCurrentMenu.BarItems.Children,
+                                    OpenCurrentMenu._currentChild,
+                                    out OpenCurrentMenu._currentChild
                                    )
                 && !CloseMenu (false))
             {
                 return;
             }
 
-            if (!openCurrentMenu.CheckSubMenu ())
+            if (!OpenCurrentMenu.CheckSubMenu ())
             {
                 return;
             }
@@ -1395,8 +1173,8 @@ public class MenuBar : View
                 menu.Visible = true;
             }
 
-            openCurrentMenu = menu;
-            openCurrentMenu.SetFocus ();
+            OpenCurrentMenu = menu;
+            OpenCurrentMenu.SetFocus ();
 
             if (_openSubMenu is { })
             {
@@ -1417,7 +1195,7 @@ public class MenuBar : View
 
         if (_openSubMenu.Count > 0)
         {
-            openCurrentMenu = _openSubMenu.Last ();
+            OpenCurrentMenu = _openSubMenu.Last ();
         }
 
         _isMenuClosing = false;
@@ -1449,7 +1227,8 @@ public class MenuBar : View
             }
 
             KeyBindings.Remove (_key);
-            KeyBindings.Add (value, KeyBindingScope.HotKey, Command.ToggleExpandCollapse);
+            KeyBinding keyBinding = new ([Command.ToggleExpandCollapse], KeyBindingScope.HotKey, -1); // -1 indicates Key was used
+            KeyBindings.Add (value, keyBinding);
             _key = value;
         }
     }
@@ -1490,168 +1269,66 @@ public class MenuBar : View
     /// <summary>The specifier character for the hot keys.</summary>
     public new static Rune HotKeySpecifier => (Rune)'_';
 
-    // Set in OnInvokingKeyBindings. -1 means no menu item is selected for activation.
-    private int _menuBarItemToActivate;
-
-    // Set in OnInvokingKeyBindings. null means no sub-menu is selected for activation.
-    private MenuItem _menuItemToSelect;
+    // TODO: This doesn't actually work. Figure out why.
     private bool _openedByAltKey;
-    private bool _openedByHotKey;
 
     /// <summary>
     ///     Called when a key bound to Command.Select is pressed. Either activates the menu item or runs it, depending on
     ///     whether it has a sub-menu. If the menu is open, it will close the menu bar.
     /// </summary>
+    /// <param name="index">The index of the menu bar item to select. -1 if the selection was via <see cref="Key"/>.</param>
     /// <returns></returns>
-    private bool SelectOrRun ()
+    private bool Select (int index)
     {
         if (!IsInitialized || !Visible)
         {
             return true;
         }
 
-        _openedByHotKey = true;
-
-        if (_menuBarItemToActivate != -1)
+        // If the menubar is open and the menu that's open is 'index' then close it. Otherwise activate it.
+        if (IsMenuOpen)
         {
-            Activate (_menuBarItemToActivate);
-        }
-        else if (_menuItemToSelect is { })
-        {
-            Run (_menuItemToSelect.Action);
-        }
-        else
-        {
-            if (IsMenuOpen && _openMenu is { })
+            if (index == -1)
             {
                 CloseAllMenus ();
-            }
-            else
-            {
-                OpenMenu ();
-            }
-        }
-
-        return true;
-    }
-
-    /// <inheritdoc/>
-    public override bool? OnInvokingKeyBindings (Key key)
-    {
-        // This is a bit of a hack. We want to handle the key bindings for menu bar but
-        // InvokeKeyBindings doesn't pass any context so we can't tell which item it is for.
-        // So before we call the base class we set SelectedItem appropriately.
-        // TODO: Figure out if there's a way to have KeyBindings pass context instead. Maybe a KeyBindingContext property?
-
-        if (KeyBindings.TryGet (key, out _))
-        {
-            _menuBarItemToActivate = -1;
-            _menuItemToSelect = null;
-
-            // Search for shortcuts first. If there's a shortcut, we don't want to activate the menu item.
-            for (var i = 0; i < Menus.Length; i++)
-            {
-                // Recurse through the menu to find one with the shortcut.
-                if (FindShortcutInChildMenu (key.KeyCode, Menus [i], out _menuItemToSelect))
-                {
-                    _menuBarItemToActivate = i;
 
-                    //keyEvent.Scope = KeyBindingScope.HotKey;
-
-                    return base.OnInvokingKeyBindings (key);
-                }
-
-                // Now see if any of the menu bar items have a hot key that matches
-                // Technically this is not possible because menu bar items don't have 
-                // shortcuts or Actions. But it's here for completeness. 
-                KeyCode? shortcut = Menus [i]?.Shortcut;
-
-                if (key == shortcut)
-                {
-                    throw new InvalidOperationException ("Menu bar items cannot have shortcuts");
-                }
+                return true;
             }
 
-            // Search for hot keys next.
+            // Find the index of the open submenu and close the menu if it matches
             for (var i = 0; i < Menus.Length; i++)
             {
-                if (IsMenuOpen)
+                MenuBarItem open = Menus [i];
+
+                if (open is null)
                 {
-                    // We don't need to do anything because `Menu` will handle the key binding.
-                    //break;
+                    continue;
                 }
 
-                // No submenu item matched (or the menu is closed)
-
-                // Check if one of the menu bar item has a hot key that matches
-                var hotKey = new Key ((char)Menus [i]?.HotKey.Value);
-
-                if (hotKey != Key.Empty)
+                if (open == OpenCurrentMenu.BarItems && i == index)
                 {
-                    bool matches = key == hotKey || key == hotKey.WithAlt || key == hotKey.NoShift.WithAlt;
-
-                    if (IsMenuOpen)
-                    {
-                        // If the menu is open, only match if Alt is not pressed.
-                        matches = key == hotKey;
-                    }
-
-                    if (matches)
-                    {
-                        _menuBarItemToActivate = i;
-
-                        //keyEvent.Scope = KeyBindingScope.HotKey;
-
-                        break;
-                    }
+                    CloseAllMenus ();
+                    return true;
                 }
             }
         }
 
-        return base.OnInvokingKeyBindings (key);
-    }
-
-    // TODO: Update to use Key instead of KeyCode
-    // Recurse the child menus looking for a shortcut that matches the key
-    private bool FindShortcutInChildMenu (KeyCode key, MenuBarItem menuBarItem, out MenuItem menuItemToSelect)
-    {
-        menuItemToSelect = null;
-
-        if (key == KeyCode.Null || menuBarItem?.Children is null)
+        if (index == -1)
         {
-            return false;
+            OpenMenu ();
         }
-
-        for (var c = 0; c < menuBarItem.Children.Length; c++)
+        else
         {
-            MenuItem menuItem = menuBarItem.Children [c];
-
-            if (key == menuItem?.Shortcut)
-            {
-                menuItemToSelect = menuItem;
-
-                return true;
-            }
-
-            MenuBarItem subMenu = menuBarItem.SubMenu (menuItem);
-
-            if (subMenu is { })
-            {
-                if (FindShortcutInChildMenu (key, subMenu, out menuItemToSelect))
-                {
-                    return true;
-                }
-            }
+            Activate (index);
         }
 
-        return false;
+        return true;
     }
 
     #endregion Keyboard handling
 
     #region Mouse Handling
 
-
     /// <inheritdoc/>
     public override bool OnLeave (View view)
     {
@@ -1699,7 +1376,7 @@ public class MenuBar : View
                     {
                         if (Menus [i].IsTopLevel)
                         {
-                            var screen = ViewportToScreen (new Point(0 , i));
+                            Point screen = ViewportToScreen (new Point (0, i));
                             var menu = new Menu { Host = this, X = screen.X, Y = screen.Y, BarItems = Menus [i] };
                             menu.Run (Menus [i].Action);
                             menu.Dispose ();
@@ -1741,9 +1418,9 @@ public class MenuBar : View
                     {
                         if (!UseSubMenusSingleFrame
                             || (UseSubMenusSingleFrame
-                                && openCurrentMenu != null
-                                && openCurrentMenu.BarItems.Parent != null
-                                && openCurrentMenu.BarItems.Parent.Parent != Menus [i]))
+                                && OpenCurrentMenu != null
+                                && OpenCurrentMenu.BarItems.Parent != null
+                                && OpenCurrentMenu.BarItems.Parent.Parent != Menus [i]))
                         {
                             Activate (i);
                         }

+ 178 - 0
Terminal.Gui/Views/Menu/MenuBarItem.cs

@@ -0,0 +1,178 @@
+namespace Terminal.Gui;
+
+/// <summary>
+///     <see cref="MenuBarItem"/> is a menu item on  <see cref="MenuBar"/>. MenuBarItems do not support
+///     <see cref="MenuItem.Shortcut"/>.
+/// </summary>
+public class MenuBarItem : MenuItem
+{
+    /// <summary>Initializes a new <see cref="MenuBarItem"/> as a <see cref="MenuItem"/>.</summary>
+    /// <param name="title">Title for the menu item.</param>
+    /// <param name="help">Help text to display. Will be displayed next to the Title surrounded by parentheses.</param>
+    /// <param name="action">Action to invoke when the menu item is activated.</param>
+    /// <param name="canExecute">Function to determine if the action can currently be executed.</param>
+    /// <param name="parent">The parent <see cref="MenuItem"/> of this if any.</param>
+    public MenuBarItem (
+        string title,
+        string help,
+        Action action,
+        Func<bool> canExecute = null,
+        MenuItem parent = null
+    ) : base (title, help, action, canExecute, parent)
+    {
+        SetInitialProperties (title, null, null, true);
+    }
+
+    /// <summary>Initializes a new <see cref="MenuBarItem"/>.</summary>
+    /// <param name="title">Title for the menu item.</param>
+    /// <param name="children">The items in the current menu.</param>
+    /// <param name="parent">The parent <see cref="MenuItem"/> of this if any.</param>
+    public MenuBarItem (string title, MenuItem [] children, MenuItem parent = null) { SetInitialProperties (title, children, parent); }
+
+    /// <summary>Initializes a new <see cref="MenuBarItem"/> with separate list of items.</summary>
+    /// <param name="title">Title for the menu item.</param>
+    /// <param name="children">The list of items in the current menu.</param>
+    /// <param name="parent">The parent <see cref="MenuItem"/> of this if any.</param>
+    public MenuBarItem (string title, List<MenuItem []> children, MenuItem parent = null) { SetInitialProperties (title, children, parent); }
+
+    /// <summary>Initializes a new <see cref="MenuBarItem"/>.</summary>
+    /// <param name="children">The items in the current menu.</param>
+    public MenuBarItem (MenuItem [] children) : this ("", children) { }
+
+    /// <summary>Initializes a new <see cref="MenuBarItem"/>.</summary>
+    public MenuBarItem () : this ([]) { }
+
+    /// <summary>
+    ///     Gets or sets an array of <see cref="MenuItem"/> objects that are the children of this
+    ///     <see cref="MenuBarItem"/>
+    /// </summary>
+    /// <value>The children.</value>
+    public MenuItem [] Children { get; set; }
+
+    internal bool IsTopLevel => Parent is null && (Children is null || Children.Length == 0) && Action != null;
+
+    /// <summary>Get the index of a child <see cref="MenuItem"/>.</summary>
+    /// <param name="children"></param>
+    /// <returns>Returns a greater than -1 if the <see cref="MenuItem"/> is a child.</returns>
+    public int GetChildrenIndex (MenuItem children)
+    {
+        var i = 0;
+
+        if (Children is null)
+        {
+            return -1;
+        }
+
+        foreach (MenuItem child in Children)
+        {
+            if (child == children)
+            {
+                return i;
+            }
+
+            i++;
+        }
+
+        return -1;
+    }
+
+    /// <summary>Check if a <see cref="MenuItem"/> is a submenu of this MenuBar.</summary>
+    /// <param name="menuItem"></param>
+    /// <returns>Returns <c>true</c> if it is a submenu. <c>false</c> otherwise.</returns>
+    public bool IsSubMenuOf (MenuItem menuItem)
+    {
+        return Children.Any (child => child == menuItem && child.Parent == menuItem.Parent);
+    }
+
+    /// <summary>Check if a <see cref="MenuItem"/> is a <see cref="MenuBarItem"/>.</summary>
+    /// <param name="menuItem"></param>
+    /// <returns>Returns a <see cref="MenuBarItem"/> or null otherwise.</returns>
+    public MenuBarItem SubMenu (MenuItem menuItem) { return menuItem as MenuBarItem; }
+
+    internal void AddShortcutKeyBindings (MenuBar menuBar)
+    {
+        if (Children is null)
+        {
+            return;
+        }
+
+        foreach (MenuItem menuItem in Children.Where (m => m is { }))
+        {
+            // For MenuBar only add shortcuts for submenus
+
+            if (menuItem.Shortcut != KeyCode.Null)
+            {
+                KeyBinding keyBinding = new ([Command.Select], KeyBindingScope.HotKey, menuItem);
+                menuBar.KeyBindings.Add (menuItem.Shortcut, keyBinding);
+            }
+
+            SubMenu (menuItem)?.AddShortcutKeyBindings (menuBar);
+        }
+    }
+
+    private void SetInitialProperties (string title, object children, MenuItem parent = null, bool isTopLevel = false)
+    {
+        if (!isTopLevel && children is null)
+        {
+            throw new ArgumentNullException (
+                                             nameof (children),
+                                             @"The parameter cannot be null. Use an empty array instead."
+                                            );
+        }
+
+        SetTitle (title ?? "");
+
+        if (parent is { })
+        {
+            Parent = parent;
+        }
+
+        switch (children)
+        {
+            case List<MenuItem []> childrenList:
+            {
+                MenuItem [] newChildren = [];
+
+                foreach (MenuItem [] grandChild in childrenList)
+                {
+                    foreach (MenuItem child in grandChild)
+                    {
+                        SetParent (grandChild);
+                        Array.Resize (ref newChildren, newChildren.Length + 1);
+                        newChildren [^1] = child;
+                    }
+                }
+
+                Children = newChildren;
+
+                break;
+            }
+            case MenuItem [] items:
+                SetParent (items);
+                Children = items;
+
+                break;
+            default:
+                Children = null;
+
+                break;
+        }
+    }
+
+    private void SetParent (MenuItem [] children)
+    {
+        foreach (MenuItem child in children)
+        {
+            if (child is { Parent: null })
+            {
+                child.Parent = this;
+            }
+        }
+    }
+
+    private void SetTitle (string title)
+    {
+        title ??= string.Empty;
+        Title = title;
+    }
+}

+ 31 - 0
Terminal.Gui/Views/Menu/MenuClosingEventArgs.cs

@@ -0,0 +1,31 @@
+namespace Terminal.Gui;
+
+/// <summary>An <see cref="EventArgs"/> which allows passing a cancelable menu closing event.</summary>
+public class MenuClosingEventArgs : EventArgs
+{
+    /// <summary>Initializes a new instance of <see cref="MenuClosingEventArgs"/>.</summary>
+    /// <param name="currentMenu">The current <see cref="MenuBarItem"/> parent.</param>
+    /// <param name="reopen">Whether the current menu will reopen.</param>
+    /// <param name="isSubMenu">Indicates whether it is a sub-menu.</param>
+    public MenuClosingEventArgs (MenuBarItem currentMenu, bool reopen, bool isSubMenu)
+    {
+        CurrentMenu = currentMenu;
+        Reopen = reopen;
+        IsSubMenu = isSubMenu;
+    }
+
+    /// <summary>
+    ///     Flag that allows the cancellation of the event. If set to <see langword="true"/> in the event handler, the
+    ///     event will be canceled.
+    /// </summary>
+    public bool Cancel { get; set; }
+
+    /// <summary>The current <see cref="MenuBarItem"/> parent.</summary>
+    public MenuBarItem CurrentMenu { get; }
+
+    /// <summary>Indicates whether the current menu is a sub-menu.</summary>
+    public bool IsSubMenu { get; }
+
+    /// <summary>Indicates whether the current menu will reopen.</summary>
+    public bool Reopen { get; }
+}

+ 0 - 73
Terminal.Gui/Views/Menu/MenuEventArgs.cs

@@ -1,73 +0,0 @@
-namespace Terminal.Gui;
-
-/// <summary>
-///     An <see cref="EventArgs"/> which allows passing a cancelable menu opening event or replacing with a new
-///     <see cref="MenuBarItem"/>.
-/// </summary>
-public class MenuOpeningEventArgs : EventArgs
-{
-    /// <summary>Initializes a new instance of <see cref="MenuOpeningEventArgs"/>.</summary>
-    /// <param name="currentMenu">The current <see cref="MenuBarItem"/> parent.</param>
-    public MenuOpeningEventArgs (MenuBarItem currentMenu) { CurrentMenu = currentMenu; }
-
-    /// <summary>
-    ///     Flag that allows the cancellation of the event. If set to <see langword="true"/> in the event handler, the
-    ///     event will be canceled.
-    /// </summary>
-    public bool Cancel { get; set; }
-
-    /// <summary>The current <see cref="MenuBarItem"/> parent.</summary>
-    public MenuBarItem CurrentMenu { get; }
-
-    /// <summary>The new <see cref="MenuBarItem"/> to be replaced.</summary>
-    public MenuBarItem NewMenuBarItem { get; set; }
-}
-
-/// <summary>Defines arguments for the <see cref="MenuBar.MenuOpened"/> event</summary>
-public class MenuOpenedEventArgs : EventArgs
-{
-    /// <summary>Creates a new instance of the <see cref="MenuOpenedEventArgs"/> class</summary>
-    /// <param name="parent"></param>
-    /// <param name="menuItem"></param>
-    public MenuOpenedEventArgs (MenuBarItem parent, MenuItem menuItem)
-    {
-        Parent = parent;
-        MenuItem = menuItem;
-    }
-
-    /// <summary>Gets the <see cref="MenuItem"/> being opened.</summary>
-    public MenuItem MenuItem { get; }
-
-    /// <summary>The parent of <see cref="MenuItem"/>. Will be null if menu opening is the root.</summary>
-    public MenuBarItem Parent { get; }
-}
-
-/// <summary>An <see cref="EventArgs"/> which allows passing a cancelable menu closing event.</summary>
-public class MenuClosingEventArgs : EventArgs
-{
-    /// <summary>Initializes a new instance of <see cref="MenuClosingEventArgs"/>.</summary>
-    /// <param name="currentMenu">The current <see cref="MenuBarItem"/> parent.</param>
-    /// <param name="reopen">Whether the current menu will reopen.</param>
-    /// <param name="isSubMenu">Indicates whether it is a sub-menu.</param>
-    public MenuClosingEventArgs (MenuBarItem currentMenu, bool reopen, bool isSubMenu)
-    {
-        CurrentMenu = currentMenu;
-        Reopen = reopen;
-        IsSubMenu = isSubMenu;
-    }
-
-    /// <summary>
-    ///     Flag that allows the cancellation of the event. If set to <see langword="true"/> in the event handler, the
-    ///     event will be canceled.
-    /// </summary>
-    public bool Cancel { get; set; }
-
-    /// <summary>The current <see cref="MenuBarItem"/> parent.</summary>
-    public MenuBarItem CurrentMenu { get; }
-
-    /// <summary>Indicates whether the current menu is a sub-menu.</summary>
-    public bool IsSubMenu { get; }
-
-    /// <summary>Indicates whether the current menu will reopen.</summary>
-    public bool Reopen { get; }
-}

+ 273 - 0
Terminal.Gui/Views/Menu/MenuItem.cs

@@ -0,0 +1,273 @@
+namespace Terminal.Gui;
+
+/// <summary>
+///     A <see cref="MenuItem"/> has title, an associated help text, and an action to execute on activation. MenuItems
+///     can also have a checked indicator (see <see cref="Checked"/>).
+/// </summary>
+public class MenuItem
+{
+    private readonly ShortcutHelper _shortcutHelper;
+    private bool _allowNullChecked;
+    private MenuItemCheckStyle _checkType;
+
+    private string _title;
+
+    // TODO: Update to use Key instead of KeyCode
+    /// <summary>Initializes a new instance of <see cref="MenuItem"/></summary>
+    public MenuItem (KeyCode shortcut = KeyCode.Null) : this ("", "", null, null, null, shortcut) { }
+
+    // TODO: Update to use Key instead of KeyCode
+    /// <summary>Initializes a new instance of <see cref="MenuItem"/>.</summary>
+    /// <param name="title">Title for the menu item.</param>
+    /// <param name="help">Help text to display.</param>
+    /// <param name="action">Action to invoke when the menu item is activated.</param>
+    /// <param name="canExecute">Function to determine if the action can currently be executed.</param>
+    /// <param name="parent">The <see cref="Parent"/> of this menu item.</param>
+    /// <param name="shortcut">The <see cref="Shortcut"/> keystroke combination.</param>
+    public MenuItem (
+        string title,
+        string help,
+        Action action,
+        Func<bool> canExecute = null,
+        MenuItem parent = null,
+        KeyCode shortcut = KeyCode.Null
+    )
+    {
+        Title = title ?? "";
+        Help = help ?? "";
+        Action = action;
+        CanExecute = canExecute;
+        Parent = parent;
+        _shortcutHelper = new ();
+
+        if (shortcut != KeyCode.Null)
+        {
+            Shortcut = shortcut;
+        }
+    }
+
+    /// <summary>Gets or sets the action to be invoked when the menu item is triggered.</summary>
+    /// <value>Method to invoke.</value>
+    public Action Action { get; set; }
+
+    /// <summary>
+    ///     Used only if <see cref="CheckType"/> is of <see cref="MenuItemCheckStyle.Checked"/> type. If
+    ///     <see langword="true"/> allows <see cref="Checked"/> to be null, true or false. If <see langword="false"/> only
+    ///     allows <see cref="Checked"/> to be true or false.
+    /// </summary>
+    public bool AllowNullChecked
+    {
+        get => _allowNullChecked;
+        set
+        {
+            _allowNullChecked = value;
+            Checked ??= false;
+        }
+    }
+
+    /// <summary>
+    ///     Gets or sets the action to be invoked to determine if the menu can be triggered. If <see cref="CanExecute"/>
+    ///     returns <see langword="true"/> the menu item will be enabled. Otherwise, it will be disabled.
+    /// </summary>
+    /// <value>Function to determine if the action is can be executed or not.</value>
+    public Func<bool> CanExecute { get; set; }
+
+    /// <summary>
+    ///     Sets or gets whether the <see cref="MenuItem"/> shows a check indicator or not. See
+    ///     <see cref="MenuItemCheckStyle"/>.
+    /// </summary>
+    public bool? Checked { set; get; }
+
+    /// <summary>
+    ///     Sets or gets the <see cref="MenuItemCheckStyle"/> of a menu item where <see cref="Checked"/> is set to
+    ///     <see langword="true"/>.
+    /// </summary>
+    public MenuItemCheckStyle CheckType
+    {
+        get => _checkType;
+        set
+        {
+            _checkType = value;
+
+            if (_checkType == MenuItemCheckStyle.Checked && !_allowNullChecked && Checked is null)
+            {
+                Checked = false;
+            }
+        }
+    }
+
+    /// <summary>Gets or sets arbitrary data for the menu item.</summary>
+    /// <remarks>This property is not used internally.</remarks>
+    public object Data { get; set; }
+
+    /// <summary>Gets or sets the help text for the menu item. The help text is drawn to the right of the <see cref="Title"/>.</summary>
+    /// <value>The help text.</value>
+    public string Help { get; set; }
+
+    /// <summary>Gets the parent for this <see cref="MenuItem"/>.</summary>
+    /// <value>The parent.</value>
+    public MenuItem Parent { get; set; }
+
+    /// <summary>Gets or sets the title of the menu item .</summary>
+    /// <value>The title.</value>
+    public string Title
+    {
+        get => _title;
+        set
+        {
+            if (_title == value)
+            {
+                return;
+            }
+
+            _title = value;
+            GetHotKey ();
+        }
+    }
+
+    /// <summary>Gets if this <see cref="MenuItem"/> is from a sub-menu.</summary>
+    internal bool IsFromSubMenu => Parent != null;
+
+    internal int TitleLength => GetMenuBarItemLength (Title);
+
+    // 
+    // ┌─────────────────────────────┐
+    // │ Quit  Quit UI Catalog  Ctrl+Q │
+    // └─────────────────────────────┘
+    // ┌─────────────────┐
+    // │ ◌ TopLevel Alt+T │
+    // └─────────────────┘
+    // TODO: Replace the `2` literals with named constants 
+    internal int Width => 1
+                          + // space before Title
+                          TitleLength
+                          + 2
+                          + // space after Title - BUGBUG: This should be 1 
+                          (Checked == true || CheckType.HasFlag (MenuItemCheckStyle.Checked) || CheckType.HasFlag (MenuItemCheckStyle.Radio)
+                               ? 2
+                               : 0)
+                          + // check glyph + space 
+                          (Help.GetColumns () > 0 ? 2 + Help.GetColumns () : 0)
+                          + // Two spaces before Help
+                          (ShortcutTag.GetColumns () > 0
+                               ? 2 + ShortcutTag.GetColumns ()
+                               : 0); // Pad two spaces before shortcut tag (which are also aligned right)
+
+    /// <summary>Merely a debugging aid to see the interaction with main.</summary>
+    internal bool GetMenuBarItem () { return IsFromSubMenu; }
+
+    /// <summary>Merely a debugging aid to see the interaction with main.</summary>
+    internal MenuItem GetMenuItem () { return this; }
+
+    /// <summary>
+    ///     Returns <see langword="true"/> if the menu item is enabled. This method is a wrapper around
+    ///     <see cref="CanExecute"/>.
+    /// </summary>
+    public bool IsEnabled () { return CanExecute?.Invoke () ?? true; }
+
+    /// <summary>
+    ///     Toggle the <see cref="Checked"/> between three states if <see cref="AllowNullChecked"/> is
+    ///     <see langword="true"/> or between two states if <see cref="AllowNullChecked"/> is <see langword="false"/>.
+    /// </summary>
+    public void ToggleChecked ()
+    {
+        if (_checkType != MenuItemCheckStyle.Checked)
+        {
+            throw new InvalidOperationException ("This isn't a Checked MenuItemCheckStyle!");
+        }
+
+        bool? previousChecked = Checked;
+
+        if (AllowNullChecked)
+        {
+            Checked = previousChecked switch
+                      {
+                          null => true,
+                          true => false,
+                          false => null
+                      };
+        }
+        else
+        {
+            Checked = !Checked;
+        }
+    }
+
+    private static int GetMenuBarItemLength (string title)
+    {
+        return title.EnumerateRunes ()
+                    .Where (ch => ch != MenuBar.HotKeySpecifier)
+                    .Sum (ch => Math.Max (ch.GetColumns (), 1));
+    }
+
+    #region Keyboard Handling
+
+    // TODO: Update to use Key instead of Rune
+    /// <summary>
+    ///     The HotKey is used to activate a <see cref="MenuItem"/> with the keyboard. HotKeys are defined by prefixing the
+    ///     <see cref="Title"/> of a MenuItem with an underscore ('_').
+    ///     <para>
+    ///         Pressing Alt-Hotkey for a <see cref="MenuBarItem"/> (menu items on the menu bar) works even if the menu is
+    ///         not active). Once a menu has focus and is active, pressing just the HotKey will activate the MenuItem.
+    ///     </para>
+    ///     <para>
+    ///         For example for a MenuBar with a "_File" MenuBarItem that contains a "_New" MenuItem, Alt-F will open the
+    ///         File menu. Pressing the N key will then activate the New MenuItem.
+    ///     </para>
+    ///     <para>See also <see cref="Shortcut"/> which enable global key-bindings to menu items.</para>
+    /// </summary>
+    public Rune HotKey { get; set; }
+    private void GetHotKey ()
+    {
+        var nextIsHot = false;
+
+        foreach (char x in _title)
+        {
+            if (x == MenuBar.HotKeySpecifier.Value)
+            {
+                nextIsHot = true;
+            }
+            else
+            {
+                if (nextIsHot)
+                {
+                    HotKey = (Rune)char.ToUpper (x);
+
+                    break;
+                }
+
+                nextIsHot = false;
+                HotKey = default (Rune);
+            }
+        }
+    }
+
+    // TODO: Update to use Key instead of KeyCode
+    /// <summary>
+    ///     Shortcut defines a key binding to the MenuItem that will invoke the MenuItem's action globally for the
+    ///     <see cref="View"/> that is the parent of the <see cref="MenuBar"/> or <see cref="ContextMenu"/> this
+    ///     <see cref="MenuItem"/>.
+    ///     <para>
+    ///         The <see cref="KeyCode"/> will be drawn on the MenuItem to the right of the <see cref="Title"/> and
+    ///         <see cref="Help"/> text. See <see cref="ShortcutTag"/>.
+    ///     </para>
+    /// </summary>
+    public KeyCode Shortcut
+    {
+        get => _shortcutHelper.Shortcut;
+        set
+        {
+            if (_shortcutHelper.Shortcut != value && (ShortcutHelper.PostShortcutValidation (value) || value == KeyCode.Null))
+            {
+                _shortcutHelper.Shortcut = value;
+            }
+        }
+    }
+
+    /// <summary>Gets the text describing the keystroke combination defined by <see cref="Shortcut"/>.</summary>
+    public string ShortcutTag => _shortcutHelper.Shortcut == KeyCode.Null
+                                     ? string.Empty
+                                     : Key.ToString (_shortcutHelper.Shortcut, MenuBar.ShortcutDelimiter);
+
+    #endregion Keyboard Handling
+}

+ 15 - 0
Terminal.Gui/Views/Menu/MenuItemCheckStyle.cs

@@ -0,0 +1,15 @@
+namespace Terminal.Gui;
+
+/// <summary>Specifies how a <see cref="MenuItem"/> shows selection state.</summary>
+[Flags]
+public enum MenuItemCheckStyle
+{
+    /// <summary>The menu item will be shown normally, with no check indicator. The default.</summary>
+    NoCheck = 0b_0000_0000,
+
+    /// <summary>The menu item will indicate checked/un-checked state (see <see cref="Checked"/>).</summary>
+    Checked = 0b_0000_0001,
+
+    /// <summary>The menu item is part of a menu radio group (see <see cref="Checked"/>) and will indicate selected state.</summary>
+    Radio = 0b_0000_0010
+}

+ 20 - 0
Terminal.Gui/Views/Menu/MenuOpenedEventArgs.cs

@@ -0,0 +1,20 @@
+namespace Terminal.Gui;
+
+/// <summary>Defines arguments for the <see cref="MenuBar.MenuOpened"/> event</summary>
+public class MenuOpenedEventArgs : EventArgs
+{
+    /// <summary>Creates a new instance of the <see cref="MenuOpenedEventArgs"/> class</summary>
+    /// <param name="parent"></param>
+    /// <param name="menuItem"></param>
+    public MenuOpenedEventArgs (MenuBarItem parent, MenuItem menuItem)
+    {
+        Parent = parent;
+        MenuItem = menuItem;
+    }
+
+    /// <summary>Gets the <see cref="MenuItem"/> being opened.</summary>
+    public MenuItem MenuItem { get; }
+
+    /// <summary>The parent of <see cref="MenuItem"/>. Will be null if menu opening is the root.</summary>
+    public MenuBarItem Parent { get; }
+}

+ 24 - 0
Terminal.Gui/Views/Menu/MenuOpeningEventArgs.cs

@@ -0,0 +1,24 @@
+namespace Terminal.Gui;
+
+/// <summary>
+///     An <see cref="EventArgs"/> which allows passing a cancelable menu opening event or replacing with a new
+///     <see cref="MenuBarItem"/>.
+/// </summary>
+public class MenuOpeningEventArgs : EventArgs
+{
+    /// <summary>Initializes a new instance of <see cref="MenuOpeningEventArgs"/>.</summary>
+    /// <param name="currentMenu">The current <see cref="MenuBarItem"/> parent.</param>
+    public MenuOpeningEventArgs (MenuBarItem currentMenu) { CurrentMenu = currentMenu; }
+
+    /// <summary>
+    ///     Flag that allows the cancellation of the event. If set to <see langword="true"/> in the event handler, the
+    ///     event will be canceled.
+    /// </summary>
+    public bool Cancel { get; set; }
+
+    /// <summary>The current <see cref="MenuBarItem"/> parent.</summary>
+    public MenuBarItem CurrentMenu { get; }
+
+    /// <summary>The new <see cref="MenuBarItem"/> to be replaced.</summary>
+    public MenuBarItem NewMenuBarItem { get; set; }
+}

+ 189 - 197
Terminal.Gui/Views/RadioGroup.cs

@@ -65,16 +65,32 @@ public class RadioGroup : View
                     Command.Accept,
                     () =>
                     {
-                        SelectItem ();
+                        SelectedItem = _cursor;
+
                         return !OnAccept ();
                     }
                    );
 
+        AddCommand (
+                    Command.HotKey,
+                    ctx =>
+                    {
+                        SetFocus ();
+                        if (ctx.KeyBinding?.Context is { } && (int)ctx.KeyBinding?.Context! < _radioLabels.Count)
+                        {
+                            SelectedItem = (int)ctx.KeyBinding?.Context!;
+
+                            return !OnAccept();
+                        }
+
+                        return true;
+                    });
+
         SetupKeyBindings ();
 
         LayoutStarted += RadioGroup_LayoutStarted;
 
-        HighlightStyle = Gui.HighlightStyle.PressedOutside | Gui.HighlightStyle.Pressed;
+        HighlightStyle = HighlightStyle.PressedOutside | HighlightStyle.Pressed;
 
         MouseClick += RadioGroup_MouseClick;
     }
@@ -84,6 +100,7 @@ public class RadioGroup : View
     private void SetupKeyBindings ()
     {
         KeyBindings.Clear ();
+
         // Default keybindings for this view
         if (Orientation == Orientation.Vertical)
         {
@@ -95,6 +112,7 @@ public class RadioGroup : View
             KeyBindings.Add (Key.CursorLeft, Command.LineUp);
             KeyBindings.Add (Key.CursorRight, Command.LineDown);
         }
+
         KeyBindings.Add (Key.Home, Command.TopHome);
         KeyBindings.Add (Key.End, Command.BottomEnd);
         KeyBindings.Add (Key.Space, Command.Accept);
@@ -179,11 +197,13 @@ public class RadioGroup : View
             int prevCount = _radioLabels.Count;
             _radioLabels = value.ToList ();
 
-            foreach (string label in _radioLabels)
+            for (var index = 0; index < _radioLabels.Count; index++)
             {
+                string label = _radioLabels [index];
+
                 if (TextFormatter.FindHotKey (label, HotKeySpecifier, out _, out Key hotKey))
                 {
-                    AddKeyBindingsForHotKey (Key.Empty, hotKey);
+                    AddKeyBindingsForHotKey (Key.Empty, hotKey, index);
                 }
             }
 
@@ -192,7 +212,7 @@ public class RadioGroup : View
         }
     }
 
-    /// <inheritdoc />
+    /// <inheritdoc/>
     public override string Text
     {
         get
@@ -201,6 +221,7 @@ public class RadioGroup : View
             {
                 return string.Empty;
             }
+
             // Return labels as a CSV string
             return string.Join (",", _radioLabels);
         }
@@ -220,73 +241,51 @@ public class RadioGroup : View
     /// <summary>The currently selected item from the list of radio labels</summary>
     /// <value>The selected.</value>
     public int SelectedItem
-{
-    get => _selected;
-    set
     {
-        OnSelectedItemChanged (value, SelectedItem);
-        _cursor = Math.Max (_selected, 0);
-        SetNeedsDisplay ();
+        get => _selected;
+        set
+        {
+            OnSelectedItemChanged (value, SelectedItem);
+            _cursor = Math.Max (_selected, 0);
+            SetNeedsDisplay ();
+        }
     }
-}
 
-/// <inheritdoc/>
-public override void OnDrawContent (Rectangle viewport)
-{
-    base.OnDrawContent (viewport);
+    /// <inheritdoc/>
+    public override void OnDrawContent (Rectangle viewport)
+    {
+        base.OnDrawContent (viewport);
 
-    Driver.SetAttribute (GetNormalColor ());
+        Driver.SetAttribute (GetNormalColor ());
 
-    for (var i = 0; i < _radioLabels.Count; i++)
-    {
-        switch (Orientation)
+        for (var i = 0; i < _radioLabels.Count; i++)
         {
-            case Orientation.Vertical:
-                Move (0, i);
+            switch (Orientation)
+            {
+                case Orientation.Vertical:
+                    Move (0, i);
 
-                break;
-            case Orientation.Horizontal:
-                Move (_horizontal [i].pos, 0);
+                    break;
+                case Orientation.Horizontal:
+                    Move (_horizontal [i].pos, 0);
 
-                break;
-        }
-
-        string rl = _radioLabels [i];
-        Driver.SetAttribute (GetNormalColor ());
-        Driver.AddStr ($"{(i == _selected ? Glyphs.Selected : Glyphs.UnSelected)} ");
-        TextFormatter.FindHotKey (rl, HotKeySpecifier, out int hotPos, out Key hotKey);
+                    break;
+            }
 
-        if (hotPos != -1 && hotKey != Key.Empty)
-        {
-            Rune [] rlRunes = rl.ToRunes ();
+            string rl = _radioLabels [i];
+            Driver.SetAttribute (GetNormalColor ());
+            Driver.AddStr ($"{(i == _selected ? Glyphs.Selected : Glyphs.UnSelected)} ");
+            TextFormatter.FindHotKey (rl, HotKeySpecifier, out int hotPos, out Key hotKey);
 
-            for (var j = 0; j < rlRunes.Length; j++)
+            if (hotPos != -1 && hotKey != Key.Empty)
             {
-                Rune rune = rlRunes [j];
+                Rune [] rlRunes = rl.ToRunes ();
 
-                if (j == hotPos && i == _cursor)
-                {
-                    Application.Driver.SetAttribute (
-                                                     HasFocus
-                                                         ? ColorScheme.HotFocus
-                                                         : GetHotNormalColor ()
-                                                    );
-                }
-                else if (j == hotPos && i != _cursor)
+                for (var j = 0; j < rlRunes.Length; j++)
                 {
-                    Application.Driver.SetAttribute (GetHotNormalColor ());
-                }
-                else if (HasFocus && i == _cursor)
-                {
-                    Application.Driver.SetAttribute (ColorScheme.Focus);
-                }
+                    Rune rune = rlRunes [j];
 
-                if (rune == HotKeySpecifier && j + 1 < rlRunes.Length)
-                {
-                    j++;
-                    rune = rlRunes [j];
-
-                    if (i == _cursor)
+                    if (j == hotPos && i == _cursor)
                     {
                         Application.Driver.SetAttribute (
                                                          HasFocus
@@ -294,184 +293,177 @@ public override void OnDrawContent (Rectangle viewport)
                                                              : GetHotNormalColor ()
                                                         );
                     }
-                    else if (i != _cursor)
+                    else if (j == hotPos && i != _cursor)
                     {
                         Application.Driver.SetAttribute (GetHotNormalColor ());
                     }
-                }
+                    else if (HasFocus && i == _cursor)
+                    {
+                        Application.Driver.SetAttribute (ColorScheme.Focus);
+                    }
+
+                    if (rune == HotKeySpecifier && j + 1 < rlRunes.Length)
+                    {
+                        j++;
+                        rune = rlRunes [j];
+
+                        if (i == _cursor)
+                        {
+                            Application.Driver.SetAttribute (
+                                                             HasFocus
+                                                                 ? ColorScheme.HotFocus
+                                                                 : GetHotNormalColor ()
+                                                            );
+                        }
+                        else if (i != _cursor)
+                        {
+                            Application.Driver.SetAttribute (GetHotNormalColor ());
+                        }
+                    }
 
-                Application.Driver.AddRune (rune);
-                Driver.SetAttribute (GetNormalColor ());
+                    Application.Driver.AddRune (rune);
+                    Driver.SetAttribute (GetNormalColor ());
+                }
+            }
+            else
+            {
+                DrawHotString (rl, HasFocus && i == _cursor, ColorScheme);
             }
-        }
-        else
-        {
-            DrawHotString (rl, HasFocus && i == _cursor, ColorScheme);
         }
     }
-}
 
-/// <inheritdoc/>
-public override bool? OnInvokingKeyBindings (Key keyEvent)
-{
-    // This is a bit of a hack. We want to handle the key bindings for the radio group but
-    // InvokeKeyBindings doesn't pass any context so we can't tell if the key binding is for
-    // the radio group or for one of the radio buttons. So before we call the base class
-    // we set SelectedItem appropriately.
-
-    Key key = keyEvent;
-
-    if (KeyBindings.TryGet (key, out _))
+    /// <summary>Called when the view orientation has changed. Invokes the <see cref="OrientationChanged"/> event.</summary>
+    /// <param name="newOrientation"></param>
+    /// <returns>True of the event was cancelled.</returns>
+    public virtual bool OnOrientationChanged (Orientation newOrientation)
     {
-        // Search RadioLabels 
-        for (var i = 0; i < _radioLabels.Count; i++)
+        var args = new OrientationEventArgs (newOrientation);
+        OrientationChanged?.Invoke (this, args);
+
+        if (!args.Cancel)
         {
-            if (TextFormatter.FindHotKey (
-                                          _radioLabels [i],
-                                          HotKeySpecifier,
-                                          out _,
-                                          out Key hotKey,
-                                          true
-                                         )
-                && key.NoAlt.NoCtrl.NoShift == hotKey)
-            {
-                SelectedItem = i;
-                break;
-            }
+            _orientation = newOrientation;
+            SetupKeyBindings ();
+            SetContentSize ();
         }
-    }
-
-    return base.OnInvokingKeyBindings (keyEvent);
-}
 
-/// <summary>Called when the view orientation has changed. Invokes the <see cref="OrientationChanged"/> event.</summary>
-/// <param name="newOrientation"></param>
-/// <returns>True of the event was cancelled.</returns>
-public virtual bool OnOrientationChanged (Orientation newOrientation)
-{
-    var args = new OrientationEventArgs (newOrientation);
-    OrientationChanged?.Invoke (this, args);
+        return args.Cancel;
+    }
 
-    if (!args.Cancel)
+    // TODO: This should be cancelable
+    /// <summary>Called whenever the current selected item changes. Invokes the <see cref="SelectedItemChanged"/> event.</summary>
+    /// <param name="selectedItem"></param>
+    /// <param name="previousSelectedItem"></param>
+    public virtual void OnSelectedItemChanged (int selectedItem, int previousSelectedItem)
     {
-        _orientation = newOrientation;
-        SetupKeyBindings ();
-        SetContentSize ();
+        _selected = selectedItem;
+        SelectedItemChanged?.Invoke (this, new (selectedItem, previousSelectedItem));
     }
 
-    return args.Cancel;
-}
+    /// <summary>
+    ///     Fired when the view orientation has changed. Can be cancelled by setting
+    ///     <see cref="OrientationEventArgs.Cancel"/> to true.
+    /// </summary>
+    public event EventHandler<OrientationEventArgs> OrientationChanged;
 
-// TODO: This should be cancelable
-/// <summary>Called whenever the current selected item changes. Invokes the <see cref="SelectedItemChanged"/> event.</summary>
-/// <param name="selectedItem"></param>
-/// <param name="previousSelectedItem"></param>
-public virtual void OnSelectedItemChanged (int selectedItem, int previousSelectedItem)
-{
-    _selected = selectedItem;
-    SelectedItemChanged?.Invoke (this, new SelectedItemChangedArgs (selectedItem, previousSelectedItem));
-}
+    /// <inheritdoc/>
+    public override Point? PositionCursor ()
+    {
+        var x = 0;
+        var y = 0;
 
-/// <summary>
-///     Fired when the view orientation has changed. Can be cancelled by setting
-///     <see cref="OrientationEventArgs.Cancel"/> to true.
-/// </summary>
-public event EventHandler<OrientationEventArgs> OrientationChanged;
+        switch (Orientation)
+        {
+            case Orientation.Vertical:
+                y = _cursor;
 
-/// <inheritdoc/>
-public override Point? PositionCursor ()
-{
-    int x = 0;
-    int y = 0;
-    switch (Orientation)
-    {
-        case Orientation.Vertical:
-            y = _cursor;
+                break;
+            case Orientation.Horizontal:
+                x = _horizontal [_cursor].pos;
 
-            break;
-        case Orientation.Horizontal:
-            x = _horizontal [_cursor].pos;
+                break;
 
-            break;
+            default:
+                return null;
+        }
 
-        default:
-            return null;
-    }
+        Move (x, y);
 
-    Move (x, y);
-    return null; // Don't show the cursor
-}
+        return null; // Don't show the cursor
+    }
 
-/// <summary>Allow to invoke the <see cref="SelectedItemChanged"/> after their creation.</summary>
-public void Refresh () { OnSelectedItemChanged (_selected, -1); }
+    /// <summary>Allow to invoke the <see cref="SelectedItemChanged"/> after their creation.</summary>
+    public void Refresh () { OnSelectedItemChanged (_selected, -1); }
 
-// TODO: This should use StateEventArgs<int> and should be cancelable.
-/// <summary>Invoked when the selected radio label has changed.</summary>
-public event EventHandler<SelectedItemChangedArgs> SelectedItemChanged;
+    // TODO: This should use StateEventArgs<int> and should be cancelable.
+    /// <summary>Invoked when the selected radio label has changed.</summary>
+    public event EventHandler<SelectedItemChangedArgs> SelectedItemChanged;
 
-private void MoveDownRight ()
-{
-    if (_cursor + 1 < _radioLabels.Count)
+    private void MoveDownRight ()
     {
-        _cursor++;
-        SetNeedsDisplay ();
-    }
-    else if (_cursor > 0)
-    {
-        _cursor = 0;
-        SetNeedsDisplay ();
+        if (_cursor + 1 < _radioLabels.Count)
+        {
+            _cursor++;
+            SetNeedsDisplay ();
+        }
+        else if (_cursor > 0)
+        {
+            _cursor = 0;
+            SetNeedsDisplay ();
+        }
     }
-}
 
-private void MoveEnd () { _cursor = Math.Max (_radioLabels.Count - 1, 0); }
-private void MoveHome () { _cursor = 0; }
+    private void MoveEnd () { _cursor = Math.Max (_radioLabels.Count - 1, 0); }
+    private void MoveHome () { _cursor = 0; }
 
-private void MoveUpLeft ()
-{
-    if (_cursor > 0)
+    private void MoveUpLeft ()
     {
-        _cursor--;
-        SetNeedsDisplay ();
-    }
-    else if (_radioLabels.Count - 1 > 0)
-    {
-        _cursor = _radioLabels.Count - 1;
-        SetNeedsDisplay ();
+        if (_cursor > 0)
+        {
+            _cursor--;
+            SetNeedsDisplay ();
+        }
+        else if (_radioLabels.Count - 1 > 0)
+        {
+            _cursor = _radioLabels.Count - 1;
+            SetNeedsDisplay ();
+        }
     }
-}
 
-private void RadioGroup_LayoutStarted (object sender, EventArgs e) { SetContentSize (); }
-private void SelectItem () { SelectedItem = _cursor; }
+    private void RadioGroup_LayoutStarted (object sender, EventArgs e) { SetContentSize (); }
 
-private void SetContentSize ()
-{
-    switch (_orientation)
+    private void SetContentSize ()
     {
-        case Orientation.Vertical:
-            var width = 0;
+        switch (_orientation)
+        {
+            case Orientation.Vertical:
+                var width = 0;
 
-            foreach (string s in _radioLabels)
-            {
-                width = Math.Max (s.GetColumns () + 2, width);
-            }
+                foreach (string s in _radioLabels)
+                {
+                    width = Math.Max (s.GetColumns () + 2, width);
+                }
 
-            SetContentSize (new (width, _radioLabels.Count));
-            break;
+                SetContentSize (new (width, _radioLabels.Count));
 
-        case Orientation.Horizontal:
-            _horizontal = new List<(int pos, int length)> ();
-            var start = 0;
-            var length = 0;
+                break;
 
-            for (var i = 0; i < _radioLabels.Count; i++)
-            {
-                start += length;
+            case Orientation.Horizontal:
+                _horizontal = new ();
+                var start = 0;
+                var length = 0;
 
-                length = _radioLabels [i].GetColumns () + 2 + (i < _radioLabels.Count - 1 ? _horizontalSpace : 0);
-                _horizontal.Add ((start, length));
-            }
-            SetContentSize (new (_horizontal.Sum (item => item.length), 1));
-            break;
+                for (var i = 0; i < _radioLabels.Count; i++)
+                {
+                    start += length;
+
+                    length = _radioLabels [i].GetColumns () + 2 + (i < _radioLabels.Count - 1 ? _horizontalSpace : 0);
+                    _horizontal.Add ((start, length));
+                }
+
+                SetContentSize (new (_horizontal.Sum (item => item.length), 1));
+
+                break;
+        }
     }
 }
-}

+ 11 - 94
Terminal.Gui/Views/StatusBar.cs

@@ -1,63 +1,5 @@
 namespace Terminal.Gui;
 
-/// <summary>
-///     <see cref="StatusItem"/> objects are contained by <see cref="StatusBar"/> <see cref="View"/>s. Each
-///     <see cref="StatusItem"/> has a title, a shortcut (hotkey), and an <see cref="Command"/> that will be invoked when
-///     the <see cref="StatusItem.Shortcut"/> is pressed. The <see cref="StatusItem.Shortcut"/> will be a global hotkey for
-///     the application in the current context of the screen. The color of the <see cref="StatusItem.Title"/> will be
-///     changed after each ~. A <see cref="StatusItem.Title"/> set to `~F1~ Help` will render as *F1* using
-///     <see cref="ColorScheme.HotNormal"/> and *Help* as <see cref="ColorScheme.HotNormal"/>.
-/// </summary>
-public class StatusItem
-{
-    /// <summary>Initializes a new <see cref="StatusItem"/>.</summary>
-    /// <param name="shortcut">Shortcut to activate the <see cref="StatusItem"/>.</param>
-    /// <param name="title">Title for the <see cref="StatusItem"/>.</param>
-    /// <param name="action">Action to invoke when the <see cref="StatusItem"/> is activated.</param>
-    /// <param name="canExecute">Function to determine if the action can currently be executed.</param>
-    public StatusItem (Key shortcut, string title, Action action, Func<bool> canExecute = null)
-    {
-        Title = title ?? "";
-        Shortcut = shortcut;
-        Action = action;
-        CanExecute = canExecute;
-    }
-
-    /// <summary>Gets or sets the action to be invoked when the statusbar item is triggered</summary>
-    /// <value>Action to invoke.</value>
-    public Action Action { get; set; }
-
-    /// <summary>
-    ///     Gets or sets the action to be invoked to determine if the <see cref="StatusItem"/> can be triggered. If
-    ///     <see cref="CanExecute"/> returns <see langword="true"/> the status item will be enabled. Otherwise, it will be
-    ///     disabled.
-    /// </summary>
-    /// <value>Function to determine if the action is can be executed or not.</value>
-    public Func<bool> CanExecute { get; set; }
-
-    /// <summary>Gets or sets arbitrary data for the status item.</summary>
-    /// <remarks>This property is not used internally.</remarks>
-    public object Data { get; set; }
-
-    /// <summary>Gets the global shortcut to invoke the action on the menu.</summary>
-    public Key Shortcut { get; set; }
-
-    /// <summary>Gets or sets the title.</summary>
-    /// <value>The title.</value>
-    /// <remarks>
-    ///     The colour of the <see cref="StatusItem.Title"/> will be changed after each ~. A
-    ///     <see cref="StatusItem.Title"/> set to `~F1~ Help` will render as *F1* using <see cref="ColorScheme.HotNormal"/> and
-    ///     *Help* as <see cref="ColorScheme.HotNormal"/>.
-    /// </remarks>
-    public string Title { get; set; }
-
-    /// <summary>
-    ///     Returns <see langword="true"/> if the status item is enabled. This method is a wrapper around
-    ///     <see cref="CanExecute"/>.
-    /// </summary>
-    public bool IsEnabled () { return CanExecute?.Invoke () ?? true; }
-}
-
 /// <summary>
 ///     A status bar is a <see cref="View"/> that snaps to the bottom of a <see cref="Toplevel"/> displaying set of
 ///     <see cref="StatusItem"/>s. The <see cref="StatusBar"/> should be context sensitive. This means, if the main menu
@@ -69,8 +11,7 @@ public class StatusBar : View
 {
     private static Rune _shortcutDelimiter = (Rune)'=';
 
-    private StatusItem [] _items = { };
-    private StatusItem _itemToInvoke;
+    private StatusItem [] _items = [];
 
     /// <summary>Initializes a new instance of the <see cref="StatusBar"/> class.</summary>
     public StatusBar () : this (new StatusItem [] { }) { }
@@ -91,10 +32,11 @@ public class StatusBar : View
         CanFocus = false;
         ColorScheme = Colors.ColorSchemes ["Menu"];
         X = 0;
-        Y = Pos.AnchorEnd (1);
+        Y = Pos.AnchorEnd ();
         Width = Dim.Fill ();
         Height = 1; // BUGBUG: Views should avoid setting Height as doing so implies Frame.Size == GetContentSize ().
-        AddCommand (Command.Accept, InvokeItem);
+
+        AddCommand (Command.Accept, ctx => InvokeItem ((StatusItem)ctx.KeyBinding?.Context));
     }
 
     /// <summary>The items that compose the <see cref="StatusBar"/></summary>
@@ -110,9 +52,10 @@ public class StatusBar : View
 
             _items = value;
 
-            foreach (StatusItem item in _items)
+            foreach (StatusItem item in _items.Where (i => i.Shortcut != Key.Empty))
             {
-                KeyBindings.Add (item.Shortcut, KeyBindingScope.HotKey, Command.Accept);
+                KeyBinding keyBinding = new (new [] { Command.Accept }, KeyBindingScope.HotKey, item);
+                KeyBindings.Add (item.Shortcut, keyBinding);
             }
         }
     }
@@ -142,7 +85,7 @@ public class StatusBar : View
     }
 
     ///<inheritdoc/>
-    protected internal override bool OnMouseEvent  (MouseEvent me)
+    protected internal override bool OnMouseEvent (MouseEvent me)
     {
         if (me.Flags != MouseFlags.Button1Clicked)
         {
@@ -215,32 +158,6 @@ public class StatusBar : View
         }
     }
 
-    /// <inheritdoc/>
-    public override bool? OnInvokingKeyBindings (Key keyEvent)
-    {
-        // This is a bit of a hack. We want to handle the key bindings for status bar but
-        // InvokeKeyBindings doesn't pass any context so we can't tell which item it is for.
-        // So before we call the base class we set SelectedItem appropriately.
-        Key key = new (keyEvent);
-
-        if (KeyBindings.TryGet (key, out _))
-        {
-            // Search RadioLabels 
-            foreach (StatusItem item in Items)
-            {
-                if (item.Shortcut == key)
-                {
-                    _itemToInvoke = item;
-                    //keyEvent.Scope = KeyBindingScope.HotKey;
-
-                    break;
-                }
-            }
-        }
-
-        return base.OnInvokingKeyBindings (keyEvent);
-    }
-
     /// <summary>Removes a <see cref="StatusItem"/> at specified index of <see cref="Items"/>.</summary>
     /// <param name="index">The zero-based index of the item to remove.</param>
     /// <returns>The <see cref="StatusItem"/> removed.</returns>
@@ -287,11 +204,11 @@ public class StatusBar : View
         return len;
     }
 
-    private bool? InvokeItem ()
+    private bool? InvokeItem (StatusItem itemToInvoke)
     {
-        if (_itemToInvoke is { Action: { } })
+        if (itemToInvoke is { Action: { } })
         {
-            _itemToInvoke.Action.Invoke ();
+            itemToInvoke.Action.Invoke ();
 
             return true;
         }

+ 59 - 0
Terminal.Gui/Views/StatusItem.cs

@@ -0,0 +1,59 @@
+namespace Terminal.Gui;
+
+/// <summary>
+///     <see cref="StatusItem"/> objects are contained by <see cref="StatusBar"/> <see cref="View"/>s. Each
+///     <see cref="StatusItem"/> has a title, a shortcut (hotkey), and an <see cref="Command"/> that will be invoked when
+///     the <see cref="StatusItem.Shortcut"/> is pressed. The <see cref="StatusItem.Shortcut"/> will be a global hotkey for
+///     the application in the current context of the screen. The color of the <see cref="StatusItem.Title"/> will be
+///     changed after each ~. A <see cref="StatusItem.Title"/> set to `~F1~ Help` will render as *F1* using
+///     <see cref="ColorScheme.HotNormal"/> and *Help* as <see cref="ColorScheme.HotNormal"/>.
+/// </summary>
+public class StatusItem
+{
+    /// <summary>Initializes a new <see cref="StatusItem"/>.</summary>
+    /// <param name="shortcut">Shortcut to activate the <see cref="StatusItem"/>.</param>
+    /// <param name="title">Title for the <see cref="StatusItem"/>.</param>
+    /// <param name="action">Action to invoke when the <see cref="StatusItem"/> is activated.</param>
+    /// <param name="canExecute">Function to determine if the action can currently be executed.</param>
+    public StatusItem (Key shortcut, string title, Action action, Func<bool> canExecute = null)
+    {
+        Title = title ?? "";
+        Shortcut = shortcut;
+        Action = action;
+        CanExecute = canExecute;
+    }
+
+    /// <summary>Gets or sets the action to be invoked when the <see cref="StatusItem"/> is triggered</summary>
+    /// <value>Action to invoke.</value>
+    public Action Action { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the action to be invoked to determine if the <see cref="StatusItem"/> can be triggered. If
+    ///     <see cref="CanExecute"/> returns <see langword="true"/> the status item will be enabled. Otherwise, it will be
+    ///     disabled.
+    /// </summary>
+    /// <value>Function to determine if the action is can be executed or not.</value>
+    public Func<bool> CanExecute { get; set; }
+
+    /// <summary>Gets or sets arbitrary data for the status item.</summary>
+    /// <remarks>This property is not used internally.</remarks>
+    public object Data { get; set; }
+
+    /// <summary>Gets the global shortcut to invoke the action on the menu.</summary>
+    public Key Shortcut { get; set; }
+
+    /// <summary>Gets or sets the title.</summary>
+    /// <value>The title.</value>
+    /// <remarks>
+    ///     The colour of the <see cref="StatusItem.Title"/> will be changed after each ~. A
+    ///     <see cref="StatusItem.Title"/> set to `~F1~ Help` will render as *F1* using <see cref="ColorScheme.HotNormal"/> and
+    ///     *Help* as <see cref="ColorScheme.HotNormal"/>.
+    /// </remarks>
+    public string Title { get; set; }
+
+    /// <summary>
+    ///     Returns <see langword="true"/> if the status item is enabled. This method is a wrapper around
+    ///     <see cref="CanExecute"/>.
+    /// </summary>
+    public bool IsEnabled () { return CanExecute?.Invoke () ?? true; }
+}

+ 3 - 2
Terminal.Gui/Views/Toplevel.cs

@@ -106,7 +106,7 @@ public partial class Toplevel : View
                    );
 
         // Default keybindings for this view
-        KeyBindings.Add (Application.QuitKey, Command.QuitToplevel);
+        KeyBindings.Add (Application.QuitKey, KeyBindingScope.Application, Command.QuitToplevel);
 
         KeyBindings.Add (Key.CursorRight, Command.NextView);
         KeyBindings.Add (Key.CursorDown, Command.NextView);
@@ -118,7 +118,8 @@ public partial class Toplevel : View
         KeyBindings.Add (Key.Tab.WithCtrl, Command.NextViewOrTop);
         KeyBindings.Add (Key.Tab.WithShift.WithCtrl, Command.PreviousViewOrTop);
 
-        KeyBindings.Add (Key.F5, Command.Refresh);
+        // TODO: Refresh Key should be configurable
+        KeyBindings.Add (Key.F5, KeyBindingScope.Application, Command.Refresh);
         KeyBindings.Add (Application.AlternateForwardKey, Command.NextViewOrTop); // Needed on Unix
         KeyBindings.Add (Application.AlternateBackwardKey, Command.PreviousViewOrTop); // Needed on Unix
 

+ 15 - 4
UICatalog/Scenarios/AllViewsTester.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
+using System.Diagnostics;
 using System.Linq;
 using System.Reflection;
 using Terminal.Gui;
@@ -517,15 +518,25 @@ public class AllViewsTester : Scenario
 
         var x = view.X.ToString ();
         var y = view.Y.ToString ();
-        _xRadioGroup.SelectedItem = _posNames.IndexOf (_posNames.Where (s => x.Contains (s)).First ());
-        _yRadioGroup.SelectedItem = _posNames.IndexOf (_posNames.Where (s => y.Contains (s)).First ());
+
+        try
+        {
+            _xRadioGroup.SelectedItem = _posNames.IndexOf (_posNames.First (s => x.Contains (s)));
+            _yRadioGroup.SelectedItem = _posNames.IndexOf (_posNames.First (s => y.Contains (s)));
+        }
+        catch (InvalidOperationException e)
+        {
+            // This is a hack to work around the fact that the Pos enum doesn't have an "Align" value yet
+            Debug.WriteLine($"{e}");
+        }
+
         _xText.Text = $"{view.Frame.X}";
         _yText.Text = $"{view.Frame.Y}";
 
         var w = view.Width.ToString ();
         var h = view.Height.ToString ();
-        _wRadioGroup.SelectedItem = _dimNames.IndexOf (_dimNames.Where (s => w.Contains (s)).First ());
-        _hRadioGroup.SelectedItem = _dimNames.IndexOf (_dimNames.Where (s => h.Contains (s)).First ());
+        _wRadioGroup.SelectedItem = _dimNames.IndexOf (_dimNames.First (s => w.Contains (s)));
+        _hRadioGroup.SelectedItem = _dimNames.IndexOf (_dimNames.First (s => h.Contains (s)));
 
         if (view.Width is DimAuto)
         {

+ 173 - 157
UICatalog/Scenarios/ContextMenus.cs

@@ -17,72 +17,88 @@ public class ContextMenus : Scenario
     private TextField _tfTopLeft, _tfTopRight, _tfMiddle, _tfBottomLeft, _tfBottomRight;
     private bool _useSubMenusSingleFrame;
 
-    public override void Setup ()
+    public override void Main ()
     {
+        // Init
+        Application.Init ();
+
+        // Setup - Create a top-level application window and configure it.
+        Window appWindow = new ()
+        {
+            Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}"
+        };
+
         var text = "Context Menu";
         var width = 20;
-        KeyCode winContextMenuKey = (KeyCode)Key.Space.WithCtrl;
+        var winContextMenuKey = (KeyCode)Key.Space.WithCtrl;
 
         var label = new Label
         {
             X = Pos.Center (), Y = 1, Text = $"Press '{winContextMenuKey}' to open the Window context menu."
         };
-        Win.Add (label);
+        appWindow.Add (label);
 
-        label = new Label
+        label = new()
         {
             X = Pos.Center (),
             Y = Pos.Bottom (label),
             Text = $"Press '{ContextMenu.DefaultKey}' to open the TextField context menu."
         };
-        Win.Add (label);
+        appWindow.Add (label);
 
-        _tfTopLeft = new TextField { Width = width, Text = text };
-        Win.Add (_tfTopLeft);
+        _tfTopLeft = new() { Width = width, Text = text };
+        appWindow.Add (_tfTopLeft);
 
-        _tfTopRight = new TextField { X = Pos.AnchorEnd (width), Width = width, Text = text };
-        Win.Add (_tfTopRight);
+        _tfTopRight = new() { X = Pos.AnchorEnd (width), Width = width, Text = text };
+        appWindow.Add (_tfTopRight);
 
-        _tfMiddle = new TextField { X = Pos.Center (), Y = Pos.Center (), Width = width, Text = text };
-        Win.Add (_tfMiddle);
+        _tfMiddle = new() { X = Pos.Center (), Y = Pos.Center (), Width = width, Text = text };
+        appWindow.Add (_tfMiddle);
 
-        _tfBottomLeft = new TextField { Y = Pos.AnchorEnd (1), Width = width, Text = text };
-        Win.Add (_tfBottomLeft);
+        _tfBottomLeft = new() { Y = Pos.AnchorEnd (1), Width = width, Text = text };
+        appWindow.Add (_tfBottomLeft);
 
-        _tfBottomRight = new TextField { X = Pos.AnchorEnd (width), Y = Pos.AnchorEnd (1), Width = width, Text = text };
-        Win.Add (_tfBottomRight);
+        _tfBottomRight = new() { X = Pos.AnchorEnd (width), Y = Pos.AnchorEnd (1), Width = width, Text = text };
+        appWindow.Add (_tfBottomRight);
 
         Point mousePos = default;
 
-        Win.KeyDown += (s, e) =>
-                       {
-                           if (e.KeyCode == winContextMenuKey)
-                           {
-                               ShowContextMenu (mousePos.X, mousePos.Y);
-                               e.Handled = true;
-                           }
-                       };
-
-        Win.MouseClick += (s, e) =>
-                          {
-                              if (e.MouseEvent.Flags == _contextMenu.MouseFlags)
-                              {
-                                  ShowContextMenu (e.MouseEvent.Position.X, e.MouseEvent.Position.Y);
-                                  e.Handled = true;
-                              }
-                          };
+        appWindow.KeyDown += (s, e) =>
+                             {
+                                 if (e.KeyCode == winContextMenuKey)
+                                 {
+                                     ShowContextMenu (mousePos.X, mousePos.Y);
+                                     e.Handled = true;
+                                 }
+                             };
+
+        appWindow.MouseClick += (s, e) =>
+                                {
+                                    if (e.MouseEvent.Flags == _contextMenu.MouseFlags)
+                                    {
+                                        ShowContextMenu (e.MouseEvent.Position.X, e.MouseEvent.Position.Y);
+                                        e.Handled = true;
+                                    }
+                                };
 
         Application.MouseEvent += ApplicationMouseEvent;
 
         void ApplicationMouseEvent (object sender, MouseEvent a) { mousePos = a.Position; }
 
-        Win.WantMousePositionReports = true;
+        appWindow.WantMousePositionReports = true;
 
-        Top.Closed += (s, e) =>
-                                  {
-                                      Thread.CurrentThread.CurrentUICulture = new CultureInfo ("en-US");
-                                      Application.MouseEvent -= ApplicationMouseEvent;
-                                  };
+        appWindow.Closed += (s, e) =>
+                            {
+                                Thread.CurrentThread.CurrentUICulture = new ("en-US");
+                                Application.MouseEvent -= ApplicationMouseEvent;
+                            };
+
+        // Run - Start the application.
+        Application.Run (appWindow);
+        appWindow.Dispose ();
+
+        // Shutdown - Calling Application.Shutdown is required.
+        Application.Shutdown ();
     }
 
     private MenuItem [] GetSupportedCultures ()
@@ -102,7 +118,7 @@ public class ContextMenus : Scenario
                 CreateAction (supportedCultures, culture);
                 supportedCultures.Add (culture);
                 index++;
-                culture = new MenuItem { CheckType = MenuItemCheckStyle.Checked };
+                culture = new() { CheckType = MenuItemCheckStyle.Checked };
             }
 
             culture.Title = $"_{c.Parent.EnglishName}";
@@ -118,7 +134,7 @@ public class ContextMenus : Scenario
         {
             culture.Action += () =>
                               {
-                                  Thread.CurrentThread.CurrentUICulture = new CultureInfo (culture.Help);
+                                  Thread.CurrentThread.CurrentUICulture = new (culture.Help);
                                   culture.Checked = true;
 
                                   foreach (MenuItem item in supportedCultures)
@@ -131,126 +147,126 @@ public class ContextMenus : Scenario
 
     private void ShowContextMenu (int x, int y)
     {
-        _contextMenu = new ContextMenu
+        _contextMenu = new()
         {
-            Position = new Point (x, y),
-            MenuItems = new MenuBarItem (
-                                         new []
-                                         {
-                                             new (
-                                                  "_Configuration",
-                                                  "Show configuration",
-                                                  () => MessageBox.Query (
-                                                                          50,
-                                                                          5,
-                                                                          "Info",
-                                                                          "This would open settings dialog",
-                                                                          "Ok"
-                                                                         )
-                                                 ),
-                                             new MenuBarItem (
-                                                              "More options",
-                                                              new MenuItem []
-                                                              {
-                                                                  new (
-                                                                       "_Setup",
-                                                                       "Change settings",
-                                                                       () => MessageBox
-                                                                           .Query (
-                                                                                   50,
-                                                                                   5,
-                                                                                   "Info",
-                                                                                   "This would open setup dialog",
-                                                                                   "Ok"
-                                                                                  ),
-                                                                       shortcut: KeyCode.T
-                                                                                 | KeyCode
-                                                                                     .CtrlMask
+            Position = new (x, y),
+            MenuItems = new (
+                             new []
+                             {
+                                 new (
+                                      "_Configuration",
+                                      "Show configuration",
+                                      () => MessageBox.Query (
+                                                              50,
+                                                              5,
+                                                              "Info",
+                                                              "This would open settings dialog",
+                                                              "Ok"
+                                                             )
+                                     ),
+                                 new MenuBarItem (
+                                                  "More options",
+                                                  new MenuItem []
+                                                  {
+                                                      new (
+                                                           "_Setup",
+                                                           "Change settings",
+                                                           () => MessageBox
+                                                               .Query (
+                                                                       50,
+                                                                       5,
+                                                                       "Info",
+                                                                       "This would open setup dialog",
+                                                                       "Ok"
                                                                       ),
-                                                                  new (
-                                                                       "_Maintenance",
-                                                                       "Maintenance mode",
-                                                                       () => MessageBox
-                                                                           .Query (
-                                                                                   50,
-                                                                                   5,
-                                                                                   "Info",
-                                                                                   "This would open maintenance dialog",
-                                                                                   "Ok"
-                                                                                  )
+                                                           shortcut: KeyCode.T
+                                                                     | KeyCode
+                                                                         .CtrlMask
+                                                          ),
+                                                      new (
+                                                           "_Maintenance",
+                                                           "Maintenance mode",
+                                                           () => MessageBox
+                                                               .Query (
+                                                                       50,
+                                                                       5,
+                                                                       "Info",
+                                                                       "This would open maintenance dialog",
+                                                                       "Ok"
                                                                       )
-                                                              }
-                                                             ),
-                                             new MenuBarItem (
-                                                              "_Languages",
-                                                              GetSupportedCultures ()
-                                                             ),
-                                             _miForceMinimumPosToZero =
-                                                 new MenuItem (
-                                                               "ForceMinimumPosToZero",
-                                                               "",
-                                                               () =>
-                                                               {
-                                                                   _miForceMinimumPosToZero
-                                                                           .Checked =
-                                                                       _forceMinimumPosToZero =
-                                                                           !_forceMinimumPosToZero;
-
-                                                                   _tfTopLeft.ContextMenu
-                                                                             .ForceMinimumPosToZero =
-                                                                       _forceMinimumPosToZero;
-
-                                                                   _tfTopRight.ContextMenu
-                                                                              .ForceMinimumPosToZero =
-                                                                       _forceMinimumPosToZero;
-
-                                                                   _tfMiddle.ContextMenu
-                                                                            .ForceMinimumPosToZero =
-                                                                       _forceMinimumPosToZero;
-
-                                                                   _tfBottomLeft.ContextMenu
-                                                                                .ForceMinimumPosToZero =
-                                                                       _forceMinimumPosToZero;
-
-                                                                   _tfBottomRight
-                                                                           .ContextMenu
-                                                                           .ForceMinimumPosToZero =
-                                                                       _forceMinimumPosToZero;
-                                                               }
-                                                              )
-                                                 {
-                                                     CheckType =
-                                                         MenuItemCheckStyle
-                                                             .Checked,
-                                                     Checked =
-                                                         _forceMinimumPosToZero
-                                                 },
-                                             _miUseSubMenusSingleFrame =
-                                                 new MenuItem (
-                                                               "Use_SubMenusSingleFrame",
-                                                               "",
-                                                               () => _contextMenu
-                                                                             .UseSubMenusSingleFrame =
-                                                                         (bool)
-                                                                         (_miUseSubMenusSingleFrame
-                                                                                  .Checked =
-                                                                              _useSubMenusSingleFrame =
-                                                                                  !_useSubMenusSingleFrame)
-                                                              )
-                                                 {
-                                                     CheckType = MenuItemCheckStyle
-                                                         .Checked,
-                                                     Checked =
-                                                         _useSubMenusSingleFrame
-                                                 },
-                                             null,
-                                             new (
-                                                  "_Quit",
-                                                  "",
-                                                  () => Application.RequestStop ()
-                                                 )
-                                         }
-                                        ),
+                                                          )
+                                                  }
+                                                 ),
+                                 new MenuBarItem (
+                                                  "_Languages",
+                                                  GetSupportedCultures ()
+                                                 ),
+                                 _miForceMinimumPosToZero =
+                                     new (
+                                          "ForceMinimumPosToZero",
+                                          "",
+                                          () =>
+                                          {
+                                              _miForceMinimumPosToZero
+                                                      .Checked =
+                                                  _forceMinimumPosToZero =
+                                                      !_forceMinimumPosToZero;
+
+                                              _tfTopLeft.ContextMenu
+                                                        .ForceMinimumPosToZero =
+                                                  _forceMinimumPosToZero;
+
+                                              _tfTopRight.ContextMenu
+                                                         .ForceMinimumPosToZero =
+                                                  _forceMinimumPosToZero;
+
+                                              _tfMiddle.ContextMenu
+                                                       .ForceMinimumPosToZero =
+                                                  _forceMinimumPosToZero;
+
+                                              _tfBottomLeft.ContextMenu
+                                                           .ForceMinimumPosToZero =
+                                                  _forceMinimumPosToZero;
+
+                                              _tfBottomRight
+                                                      .ContextMenu
+                                                      .ForceMinimumPosToZero =
+                                                  _forceMinimumPosToZero;
+                                          }
+                                         )
+                                     {
+                                         CheckType =
+                                             MenuItemCheckStyle
+                                                 .Checked,
+                                         Checked =
+                                             _forceMinimumPosToZero
+                                     },
+                                 _miUseSubMenusSingleFrame =
+                                     new (
+                                          "Use_SubMenusSingleFrame",
+                                          "",
+                                          () => _contextMenu
+                                                        .UseSubMenusSingleFrame =
+                                                    (bool)
+                                                    (_miUseSubMenusSingleFrame
+                                                             .Checked =
+                                                         _useSubMenusSingleFrame =
+                                                             !_useSubMenusSingleFrame)
+                                         )
+                                     {
+                                         CheckType = MenuItemCheckStyle
+                                             .Checked,
+                                         Checked =
+                                             _useSubMenusSingleFrame
+                                     },
+                                 null,
+                                 new (
+                                      "_Quit",
+                                      "",
+                                      () => Application.RequestStop ()
+                                     )
+                             }
+                            ),
             ForceMinimumPosToZero = _forceMinimumPosToZero,
             UseSubMenusSingleFrame = _useSubMenusSingleFrame
         };

+ 33 - 23
UICatalog/Scenarios/DynamicMenuBar.cs

@@ -13,15 +13,23 @@ namespace UICatalog.Scenarios;
 [ScenarioCategory ("Menus")]
 public class DynamicMenuBar : Scenario
 {
-    public override void Init ()
+    public override void Main ()
     {
+        // Init
         Application.Init ();
 
-        Top = new ();
+        // Setup - Create a top-level application window and configure it.
+        DynamicMenuBarSample appWindow = new ()
+        {
+            Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}"
+        };
+
+        // Run - Start the application.
+        Application.Run (appWindow);
+        appWindow.Dispose ();
 
-        Top.Add (
-                 new DynamicMenuBarSample { Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" }
-                );
+        // Shutdown - Calling Application.Shutdown is required.
+        Application.Shutdown ();
     }
 
     public class Binding
@@ -106,31 +114,31 @@ public class DynamicMenuBar : Scenario
             var _lblTitle = new Label { Y = 1, Text = "Title:" };
             Add (_lblTitle);
 
-            TextTitle = new() { X = Pos.Right (_lblTitle) + 2, Y = Pos.Top (_lblTitle), Width = Dim.Fill () };
+            TextTitle = new () { X = Pos.Right (_lblTitle) + 2, Y = Pos.Top (_lblTitle), Width = Dim.Fill () };
             Add (TextTitle);
 
             var _lblHelp = new Label { X = Pos.Left (_lblTitle), Y = Pos.Bottom (_lblTitle) + 1, Text = "Help:" };
             Add (_lblHelp);
 
-            TextHelp = new() { X = Pos.Left (TextTitle), Y = Pos.Top (_lblHelp), Width = Dim.Fill () };
+            TextHelp = new () { X = Pos.Left (TextTitle), Y = Pos.Top (_lblHelp), Width = Dim.Fill () };
             Add (TextHelp);
 
             var _lblAction = new Label { X = Pos.Left (_lblTitle), Y = Pos.Bottom (_lblHelp) + 1, Text = "Action:" };
             Add (_lblAction);
 
-            TextAction = new()
+            TextAction = new ()
             {
                 X = Pos.Left (TextTitle), Y = Pos.Top (_lblAction), Width = Dim.Fill (), Height = 5
             };
             Add (TextAction);
 
-            CkbIsTopLevel = new()
+            CkbIsTopLevel = new ()
             {
                 X = Pos.Left (_lblTitle), Y = Pos.Bottom (_lblAction) + 5, Text = "IsTopLevel"
             };
             Add (CkbIsTopLevel);
 
-            CkbSubMenu = new()
+            CkbSubMenu = new ()
             {
                 X = Pos.Left (_lblTitle),
                 Y = Pos.Bottom (CkbIsTopLevel),
@@ -139,7 +147,7 @@ public class DynamicMenuBar : Scenario
             };
             Add (CkbSubMenu);
 
-            CkbNullCheck = new()
+            CkbNullCheck = new ()
             {
                 X = Pos.Left (_lblTitle), Y = Pos.Bottom (CkbSubMenu), Text = "Allow null checked"
             };
@@ -147,7 +155,7 @@ public class DynamicMenuBar : Scenario
 
             var _rChkLabels = new [] { "NoCheck", "Checked", "Radio" };
 
-            RbChkStyle = new()
+            RbChkStyle = new ()
             {
                 X = Pos.Left (_lblTitle), Y = Pos.Bottom (CkbSubMenu) + 1, RadioLabels = _rChkLabels
             };
@@ -159,7 +167,7 @@ public class DynamicMenuBar : Scenario
             };
             Add (_lblShortcut);
 
-            TextShortcut = new()
+            TextShortcut = new ()
             {
                 X = Pos.X (_lblShortcut), Y = Pos.Bottom (_lblShortcut), Width = Dim.Fill (), ReadOnly = true
             };
@@ -439,7 +447,9 @@ public class DynamicMenuBar : Scenario
                                     TextTitle.Text = string.Empty;
                                     Application.RequestStop ();
                                 };
-            var dialog = new Dialog { Title = "Enter the menu details.", Buttons = [btnOk, btnCancel], Height = Dim.Auto (DimAutoStyle.Content, 22, Driver.Rows) };
+
+            var dialog = new Dialog
+                { Title = "Enter the menu details.", Buttons = [btnOk, btnCancel], Height = Dim.Auto (DimAutoStyle.Content, 22, Driver.Rows) };
 
             Width = Dim.Fill ();
             Height = Dim.Fill () - 1;
@@ -451,7 +461,7 @@ public class DynamicMenuBar : Scenario
 
             if (valid)
             {
-                return new()
+                return new ()
                 {
                     Title = TextTitle.Text,
                     Help = TextHelp.Text,
@@ -649,7 +659,7 @@ public class DynamicMenuBar : Scenario
             };
             _frmMenu.Add (_btnPreviowsParent);
 
-            _lstMenus = new()
+            _lstMenus = new ()
             {
                 ColorScheme = Colors.ColorSchemes ["Dialog"],
                 X = Pos.Right (_btnPrevious) + 1,
@@ -746,7 +756,7 @@ public class DynamicMenuBar : Scenario
                                          DataContext.Menus [i] = DataContext.Menus [i - 1];
 
                                          DataContext.Menus [i - 1] =
-                                             new() { Title = menuItem.Title, MenuItem = menuItem };
+                                             new () { Title = menuItem.Title, MenuItem = menuItem };
                                          _lstMenus.SelectedItem = i - 1;
                                      }
                                  }
@@ -768,7 +778,7 @@ public class DynamicMenuBar : Scenario
                                            DataContext.Menus [i] = DataContext.Menus [i + 1];
 
                                            DataContext.Menus [i + 1] =
-                                               new() { Title = menuItem.Title, MenuItem = menuItem };
+                                               new () { Title = menuItem.Title, MenuItem = menuItem };
                                            _lstMenus.SelectedItem = i + 1;
                                        }
                                    }
@@ -894,7 +904,7 @@ public class DynamicMenuBar : Scenario
                                           menuBarItem.Children = childrens;
                                       }
 
-                                      DataContext.Menus.Add (new() { Title = newMenu.Title, MenuItem = newMenu });
+                                      DataContext.Menus.Add (new () { Title = newMenu.Title, MenuItem = newMenu });
                                       _lstMenus.MoveDown ();
                                   }
                               };
@@ -937,7 +947,7 @@ public class DynamicMenuBar : Scenario
                                                                             _currentMenuBarItem.Help,
                                                                             _frmMenuDetails.CreateAction (
                                                                                                           _currentEditMenuBarItem,
-                                                                                                          new()
+                                                                                                          new ()
                                                                                                           {
                                                                                                               Title = _currentEditMenuBarItem
                                                                                                                   .Title
@@ -1108,7 +1118,7 @@ public class DynamicMenuBar : Scenario
             SetFrameDetails ();
 
             var ustringConverter = new UStringValueConverter ();
-            var listWrapperConverter = new ListWrapperConverter<DynamicMenuItemList> ();
+            ListWrapperConverter<DynamicMenuItemList> listWrapperConverter = new ListWrapperConverter<DynamicMenuItemList> ();
 
             var lblMenuBar = new Binding (this, "MenuBar", _lblMenuBar, "Text", ustringConverter);
             var lblParent = new Binding (this, "Parent", _lblParent, "Text", ustringConverter);
@@ -1297,7 +1307,7 @@ public class DynamicMenuBar : Scenario
                     if (DataContext.Menus.Count == 0)
                     {
                         DataContext.Menus.Add (
-                                               new()
+                                               new ()
                                                {
                                                    Title = _currentEditMenuBarItem.Title, MenuItem = _currentEditMenuBarItem
                                                }
@@ -1305,7 +1315,7 @@ public class DynamicMenuBar : Scenario
                     }
 
                     DataContext.Menus [index] =
-                        new()
+                        new ()
                         {
                             Title = _currentEditMenuBarItem.Title, MenuItem = _currentEditMenuBarItem
                         };

+ 150 - 188
UICatalog/Scenarios/Editor.cs

@@ -8,6 +8,7 @@ using System.Text;
 using System.Text.RegularExpressions;
 using System.Threading;
 using Terminal.Gui;
+using static UICatalog.Scenarios.DynamicMenuBar;
 
 namespace UICatalog.Scenarios;
 
@@ -18,8 +19,10 @@ namespace UICatalog.Scenarios;
 [ScenarioCategory ("Top Level Windows")]
 [ScenarioCategory ("Files and IO")]
 [ScenarioCategory ("TextView")]
+[ScenarioCategory ("Menus")]
 public class Editor : Scenario
 {
+    private Window _appWindow;
     private List<CultureInfo> _cultureInfos;
     private string _fileName = "demo.txt";
     private bool _forceMinimumPosToZero = true;
@@ -33,41 +36,36 @@ public class Editor : Scenario
     private string _textToFind;
     private string _textToReplace;
     private TextView _textView;
-    private Window _winDialog;
+    private FindReplaceWindow _findReplaceWindow;
 
-    public override void Init ()
+    public override void Main ()
     {
+        // Init
         Application.Init ();
-        _cultureInfos = Application.SupportedCultures;
-        ConfigurationManager.Themes.Theme = Theme;
-        ConfigurationManager.Apply ();
-
-        Top = new ();
 
-        Win = new()
+        // Setup - Create a top-level application window and configure it.
+        _appWindow = new ()
         {
+            //Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}",
             Title = _fileName ?? "Untitled",
-            X = 0,
-            Y = 1,
-            Width = Dim.Fill (),
-            Height = Dim.Fill (),
-            ColorScheme = Colors.ColorSchemes [TopLevelColorScheme]
+            BorderStyle = LineStyle.None
         };
-        Top.Add (Win);
 
-        _textView = new()
+        _cultureInfos = Application.SupportedCultures;
+
+        _textView = new ()
         {
             X = 0,
-            Y = 0,
+            Y = 1,
             Width = Dim.Fill (),
-            Height = Dim.Fill ()
+            Height = Dim.Fill (1),
         };
 
         CreateDemoFile (_fileName);
 
         LoadFile ();
 
-        Win.Add (_textView);
+        _appWindow.Add (_textView);
 
         var menu = new MenuBar
         {
@@ -238,7 +236,7 @@ public class Editor : Scenario
             ]
         };
 
-        Top.Add (menu);
+        _appWindow.Add (menu);
 
         var siCursorPosition = new StatusItem (KeyCode.Null, "", null);
 
@@ -267,7 +265,7 @@ public class Editor : Scenario
                                                  siCursorPosition.Title = $"Ln {e.Point.Y + 1}, Col {e.Point.X + 1}";
                                              };
 
-        Top.Add (statusBar);
+        _appWindow.Add (statusBar);
 
         _scrollBar = new (_textView, true);
 
@@ -310,49 +308,19 @@ public class Editor : Scenario
                                      _scrollBar.Refresh ();
                                  };
 
-        Win.KeyDown += (s, e) =>
-                       {
-                           if (_winDialog != null && (e.KeyCode == KeyCode.Esc || e == Application.QuitKey))
-                           {
-                               DisposeWinDialog ();
-                           }
-                           else if (e == Application.QuitKey)
-                           {
-                               Quit ();
-                               e.Handled = true;
-                           }
-                           else if (_winDialog != null && e.KeyCode == (KeyCode.Tab | KeyCode.CtrlMask))
-                           {
-                               if (_tabView.SelectedTab == _tabView.Tabs.ElementAt (_tabView.Tabs.Count - 1))
-                               {
-                                   _tabView.SelectedTab = _tabView.Tabs.ElementAt (0);
-                               }
-                               else
-                               {
-                                   _tabView.SwitchTabBy (1);
-                               }
-
-                               e.Handled = true;
-                           }
-                           else if (_winDialog != null && e.KeyCode == (KeyCode.Tab | KeyCode.CtrlMask | KeyCode.ShiftMask))
-                           {
-                               if (_tabView.SelectedTab == _tabView.Tabs.ElementAt (0))
-                               {
-                                   _tabView.SelectedTab = _tabView.Tabs.ElementAt (_tabView.Tabs.Count - 1);
-                               }
-                               else
-                               {
-                                   _tabView.SwitchTabBy (-1);
-                               }
-
-                               e.Handled = true;
-                           }
-                       };
 
-        Top.Closed += (s, e) => Thread.CurrentThread.CurrentUICulture = new ("en-US");
-    }
+        _appWindow.Closed += (s, e) => Thread.CurrentThread.CurrentUICulture = new ("en-US");
+
+        CreateFindReplace ();
 
-    public override void Setup () { }
+        // Run - Start the application.
+        Application.Run (_appWindow);
+        _appWindow.Dispose ();
+
+        // Shutdown - Calling Application.Shutdown is required.
+        Application.Shutdown ();
+
+    }
 
     private bool CanCloseFile ()
     {
@@ -366,7 +334,7 @@ public class Editor : Scenario
 
         int r = MessageBox.ErrorQuery (
                                        "Save File",
-                                       $"Do you want save changes in {Win.Title}?",
+                                       $"Do you want save changes in {_appWindow.Title}?",
                                        "Yes",
                                        "No",
                                        "Cancel"
@@ -414,7 +382,7 @@ public class Editor : Scenario
 
         if (replace
             && (string.IsNullOrEmpty (_textToFind)
-                || (_winDialog == null && string.IsNullOrEmpty (_textToReplace))))
+                || (_findReplaceWindow == null && string.IsNullOrEmpty (_textToReplace))))
         {
             Replace ();
 
@@ -689,11 +657,7 @@ public class Editor : Scenario
         for (var i = 0; i < 30; i++)
         {
             sb.Append (
-                       $"{
-                           i
-                       } - This is a test with a very long line and many lines to test the ScrollViewBar against the TextView. - {
-                           i
-                       }\n"
+                       $"{i} - This is a test with a very long line and many lines to test the ScrollViewBar against the TextView. - {i}\n"
                       );
         }
 
@@ -721,38 +685,85 @@ public class Editor : Scenario
         return item;
     }
 
-    private void CreateFindReplace (bool isFind = true)
+    private class FindReplaceWindow : Window
     {
-        if (_winDialog != null)
+        private TextView _textView;
+        public FindReplaceWindow (TextView textView)
         {
-            _winDialog.SetFocus ();
-
-            return;
+            Title = "Find and Replace";
+
+            _textView = textView;
+            X = Pos.AnchorEnd () - 1;
+            Y = 2;
+            Width = 57;
+            Height = 11;
+            Arrangement = ViewArrangement.Movable;
+
+            KeyBindings.Add (Key.Esc, KeyBindingScope.Focused, Command.Cancel);
+            AddCommand (Command.Cancel, () =>
+                                        {
+                                            Visible = false;
+
+                                            return true;
+                                        });
+            VisibleChanged += FindReplaceWindow_VisibleChanged;
+            Initialized += FindReplaceWindow_Initialized;
+
+            //var btnCancel = new Button
+            //{
+            //    X = Pos.AnchorEnd (),
+            //    Y = Pos.AnchorEnd (),
+            //    Text = "Cancel"
+            //};
+            //btnCancel.Accept += (s, e) => { Visible = false; };
+            //Add (btnCancel);
         }
 
-        _winDialog = new()
+        private void FindReplaceWindow_VisibleChanged (object sender, EventArgs e)
         {
-            Title = isFind ? "Find" : "Replace",
-            X = Win.Viewport.Width / 2 - 30,
-            Y = Win.Viewport.Height / 2 - 10,
-            ColorScheme = Colors.ColorSchemes ["TopLevel"]
-        };
+            if (Visible == false)
+            {
+                _textView.SetFocus ();
+            }
+            else
+            {
+                FocusFirst();
+            }
+        }
 
-        _tabView = new() { X = 0, Y = 0, Width = Dim.Fill (), Height = Dim.Fill () };
+        private void FindReplaceWindow_Initialized (object sender, EventArgs e)
+        {
+            Border.LineStyle = LineStyle.Dashed;
+            Border.Thickness = new (0, 1, 0, 0);
+        }
+    }
 
-        _tabView.AddTab (new() { DisplayText = "Find", View = FindTab () }, isFind);
-        View replace = ReplaceTab ();
-        _tabView.AddTab (new() { DisplayText = "Replace", View = replace }, !isFind);
-        _tabView.SelectedTabChanged += (s, e) => _tabView.SelectedTab.View.FocusFirst ();
-        _winDialog.Add (_tabView);
+    private void ShowFindReplace (bool isFind = true)
+    {
+        _findReplaceWindow.Visible = true;
+        _findReplaceWindow.SuperView.BringSubviewToFront (_findReplaceWindow);
+        _tabView.SetFocus();
+        _tabView.SelectedTab = isFind ? _tabView.Tabs.ToArray () [0] : _tabView.Tabs.ToArray () [1];
+        _tabView.SelectedTab.View.FocusFirst ();
+    }
 
-        Win.Add (_winDialog);
+    private void CreateFindReplace ()
+    {
+        _findReplaceWindow = new (_textView);
+        _tabView = new ()
+        {
+            X = 0, Y = 0,
+            Width = Dim.Fill (), Height = Dim.Fill (0)
+        };
 
-        _winDialog.Width = replace.Width + 4;
-        _winDialog.Height = replace.Height + 4;
+        _tabView.AddTab (new () { DisplayText = "Find", View = CreateFindTab () }, true);
+        _tabView.AddTab (new () { DisplayText = "Replace", View = CreateReplaceTab () }, false);
+        _tabView.SelectedTabChanged += (s, e) => _tabView.SelectedTab.View.FocusFirst ();
+        _findReplaceWindow.Add (_tabView);
 
-        _winDialog.SuperView.BringSubviewToFront (_winDialog);
-        _winDialog.SetFocus ();
+        _tabView.SelectedTab.View.FocusLast (); // Hack to get the first tab to be focused
+        _findReplaceWindow.Visible = false;
+        _appWindow.Add (_findReplaceWindow);
     }
 
     private MenuItem [] CreateKeepChecked ()
@@ -822,34 +833,22 @@ public class Editor : Scenario
         }
     }
 
-    private void DisposeWinDialog ()
-    {
-        _winDialog.Dispose ();
-        Win.Remove (_winDialog);
-        _winDialog = null;
-    }
-
-    private void Find () { CreateFindReplace (); }
+    private void Find () { ShowFindReplace(true); }
     private void FindNext () { ContinueFind (); }
     private void FindPrevious () { ContinueFind (false); }
 
-    private View FindTab ()
+    private View CreateFindTab ()
     {
-        var d = new View ();
-
-        d.DrawContent += (s, e) =>
-                         {
-                             foreach (View v in d.Subviews)
-                             {
-                                 v.SetNeedsDisplay ();
-                             }
-                         };
+        var d = new View ()
+        {
+            Width = Dim.Fill (),
+            Height = Dim.Fill ()
+        };
 
         int lblWidth = "Replace:".Length;
 
         var label = new Label
         {
-            Y = 1,
             Width = lblWidth,
             TextAlignment = Alignment.End,
 
@@ -861,18 +860,19 @@ public class Editor : Scenario
 
         var txtToFind = new TextField
         {
-            X = Pos.Right (label) + 1, Y = Pos.Top (label), Width = 20, Text = _textToFind
+            X = Pos.Right (label) + 1,
+            Y = Pos.Top (label),
+            Width = Dim.Fill (1),
+            Text = _textToFind
         };
         txtToFind.Enter += (s, e) => txtToFind.Text = _textToFind;
         d.Add (txtToFind);
 
         var btnFindNext = new Button
         {
-            X = Pos.Right (txtToFind) + 1,
-            Y = Pos.Top (label),
-            Width = 20,
+            X = Pos.Align (Alignment.Center),
+            Y = Pos.AnchorEnd (),
             Enabled = !string.IsNullOrEmpty (txtToFind.Text),
-            TextAlignment = Alignment.Center,
             IsDefault = true,
 
             Text = "Find _Next"
@@ -882,12 +882,9 @@ public class Editor : Scenario
 
         var btnFindPrevious = new Button
         {
-            X = Pos.Right (txtToFind) + 1,
-            Y = Pos.Top (btnFindNext) + 1,
-            Width = 20,
+            X = Pos.Align (Alignment.Center),
+            Y = Pos.AnchorEnd (),
             Enabled = !string.IsNullOrEmpty (txtToFind.Text),
-            TextAlignment = Alignment.Center,
-
             Text = "Find _Previous"
         };
         btnFindPrevious.Accept += (s, e) => FindPrevious ();
@@ -901,18 +898,6 @@ public class Editor : Scenario
                                      btnFindPrevious.Enabled = !string.IsNullOrEmpty (txtToFind.Text);
                                  };
 
-        var btnCancel = new Button
-        {
-            X = Pos.Right (txtToFind) + 1,
-            Y = Pos.Top (btnFindPrevious) + 2,
-            Width = 20,
-            TextAlignment = Alignment.Center,
-
-            Text = "Cancel"
-        };
-        btnCancel.Accept += (s, e) => { DisposeWinDialog (); };
-        d.Add (btnCancel);
-
         var ckbMatchCase = new CheckBox
         {
             X = 0, Y = Pos.Top (txtToFind) + 2, Checked = _matchCase, Text = "Match c_ase"
@@ -926,10 +911,6 @@ public class Editor : Scenario
         };
         ckbMatchWholeWord.Toggled += (s, e) => _matchWholeWord = (bool)ckbMatchWholeWord.Checked;
         d.Add (ckbMatchWholeWord);
-
-        d.Width = label.Width + txtToFind.Width + btnFindNext.Width + 2;
-        d.Height = btnFindNext.Height + btnFindPrevious.Height + btnCancel.Height + 4;
-
         return d;
     }
 
@@ -950,7 +931,7 @@ public class Editor : Scenario
                 CreateAction (supportedCultures, culture);
                 supportedCultures.Add (culture);
                 index++;
-                culture = new() { CheckType = MenuItemCheckStyle.Checked };
+                culture = new () { CheckType = MenuItemCheckStyle.Checked };
             }
 
             culture.Title = $"_{c.Parent.EnglishName}";
@@ -986,7 +967,7 @@ public class Editor : Scenario
 
             //_textView.Text = System.IO.File.ReadAllText (_fileName);
             _originalText = Encoding.Unicode.GetBytes (_textView.Text);
-            Win.Title = _fileName;
+            _appWindow.Title = _fileName;
             _saved = true;
         }
     }
@@ -998,7 +979,7 @@ public class Editor : Scenario
             return;
         }
 
-        Win.Title = "Untitled.txt";
+        _appWindow.Title = "Untitled.txt";
         _fileName = null;
         _originalText = new MemoryStream ().ToArray ();
         _textView.Text = Encoding.Unicode.GetString (_originalText);
@@ -1053,11 +1034,11 @@ public class Editor : Scenario
         Application.RequestStop ();
     }
 
-    private void Replace () { CreateFindReplace (false); }
+    private void Replace () { ShowFindReplace (false); }
 
     private void ReplaceAll ()
     {
-        if (string.IsNullOrEmpty (_textToFind) || (string.IsNullOrEmpty (_textToReplace) && _winDialog == null))
+        if (string.IsNullOrEmpty (_textToFind) || (string.IsNullOrEmpty (_textToReplace) && _findReplaceWindow == null))
         {
             Replace ();
 
@@ -1085,26 +1066,20 @@ public class Editor : Scenario
     private void ReplaceNext () { ContinueFind (true, true); }
     private void ReplacePrevious () { ContinueFind (false, true); }
 
-    private View ReplaceTab ()
+    private View CreateReplaceTab ()
     {
-        var d = new View ();
-
-        d.DrawContent += (s, e) =>
-                         {
-                             foreach (View v in d.Subviews)
-                             {
-                                 v.SetNeedsDisplay ();
-                             }
-                         };
+        var d = new View ()
+        {
+            Width = Dim.Fill (),
+            Height = Dim.Fill ()
+        };
 
         int lblWidth = "Replace:".Length;
 
         var label = new Label
         {
-            Y = 1,
             Width = lblWidth,
             TextAlignment = Alignment.End,
-
             Text = "Find:"
         };
         d.Add (label);
@@ -1113,45 +1088,50 @@ public class Editor : Scenario
 
         var txtToFind = new TextField
         {
-            X = Pos.Right (label) + 1, Y = Pos.Top (label), Width = 20, Text = _textToFind
+            X = Pos.Right (label) + 1,
+            Y = Pos.Top (label),
+            Width = Dim.Fill (1),
+            Text = _textToFind
         };
         txtToFind.Enter += (s, e) => txtToFind.Text = _textToFind;
         d.Add (txtToFind);
 
         var btnFindNext = new Button
         {
-            X = Pos.Right (txtToFind) + 1,
-            Y = Pos.Top (label),
-            Width = 20,
+            X = Pos.Align (Alignment.Center),
+            Y = Pos.AnchorEnd (),
             Enabled = !string.IsNullOrEmpty (txtToFind.Text),
-            TextAlignment = Alignment.Center,
             IsDefault = true,
-
             Text = "Replace _Next"
         };
         btnFindNext.Accept += (s, e) => ReplaceNext ();
         d.Add (btnFindNext);
 
-        label = new() { X = Pos.Left (label), Y = Pos.Top (label) + 1, Text = "Replace:" };
+        label = new ()
+        {
+            X = Pos.Left (label),
+            Y = Pos.Top (label) + 1,
+            Text = "Replace:"
+        };
         d.Add (label);
 
         SetFindText ();
 
         var txtToReplace = new TextField
         {
-            X = Pos.Right (label) + 1, Y = Pos.Top (label), Width = 20, Text = _textToReplace
+            X = Pos.Right (label) + 1,
+            Y = Pos.Top (label),
+            Width = Dim.Fill (1),
+            Text = _textToReplace
         };
         txtToReplace.TextChanged += (s, e) => _textToReplace = txtToReplace.Text;
         d.Add (txtToReplace);
 
         var btnFindPrevious = new Button
         {
-            X = Pos.Right (txtToFind) + 1,
-            Y = Pos.Top (btnFindNext) + 1,
-            Width = 20,
+            X = Pos.Align (Alignment.Center),
+            Y = Pos.AnchorEnd (),
             Enabled = !string.IsNullOrEmpty (txtToFind.Text),
-            TextAlignment = Alignment.Center,
-
             Text = "Replace _Previous"
         };
         btnFindPrevious.Accept += (s, e) => ReplacePrevious ();
@@ -1159,12 +1139,9 @@ public class Editor : Scenario
 
         var btnReplaceAll = new Button
         {
-            X = Pos.Right (txtToFind) + 1,
-            Y = Pos.Top (btnFindPrevious) + 1,
-            Width = 20,
+            X = Pos.Align (Alignment.Center),
+            Y = Pos.AnchorEnd (),
             Enabled = !string.IsNullOrEmpty (txtToFind.Text),
-            TextAlignment = Alignment.Center,
-
             Text = "Replace _All"
         };
         btnReplaceAll.Accept += (s, e) => ReplaceAll ();
@@ -1179,18 +1156,6 @@ public class Editor : Scenario
                                      btnReplaceAll.Enabled = !string.IsNullOrEmpty (txtToFind.Text);
                                  };
 
-        var btnCancel = new Button
-        {
-            X = Pos.Right (txtToFind) + 1,
-            Y = Pos.Top (btnReplaceAll) + 1,
-            Width = 20,
-            TextAlignment = Alignment.Center,
-
-            Text = "Cancel"
-        };
-        btnCancel.Accept += (s, e) => { DisposeWinDialog (); };
-        d.Add (btnCancel);
-
         var ckbMatchCase = new CheckBox
         {
             X = 0, Y = Pos.Top (txtToFind) + 2, Checked = _matchCase, Text = "Match c_ase"
@@ -1205,9 +1170,6 @@ public class Editor : Scenario
         ckbMatchWholeWord.Toggled += (s, e) => _matchWholeWord = (bool)ckbMatchWholeWord.Checked;
         d.Add (ckbMatchWholeWord);
 
-        d.Width = lblWidth + txtToFind.Width + btnFindNext.Width + 2;
-        d.Height = btnFindNext.Height + btnFindPrevious.Height + btnCancel.Height + 4;
-
         return d;
     }
 
@@ -1217,7 +1179,7 @@ public class Editor : Scenario
         {
             // FIXED: BUGBUG: #279 TextView does not know how to deal with \r\n, only \r 
             // As a result files saved on Windows and then read back will show invalid chars.
-            return SaveFile (Win.Title, _fileName);
+            return SaveFile (_appWindow.Title, _fileName);
         }
 
         return SaveAs ();
@@ -1231,7 +1193,7 @@ public class Editor : Scenario
         };
         var sd = new SaveDialog { Title = "Save file", AllowedTypes = aTypes };
 
-        sd.Path = Win.Title;
+        sd.Path = _appWindow.Title;
         Application.Run (sd);
         bool canceled = sd.Canceled;
         string path = sd.Path;
@@ -1270,7 +1232,7 @@ public class Editor : Scenario
     {
         try
         {
-            Win.Title = title;
+            _appWindow.Title = title;
             _fileName = file;
             File.WriteAllText (_fileName, _textView.Text);
             _originalText = Encoding.Unicode.GetBytes (_textView.Text);

+ 191 - 0
UICatalog/Scenarios/KeyBindings.cs

@@ -0,0 +1,191 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Text;
+using Terminal.Gui;
+
+namespace UICatalog.Scenarios;
+
+[ScenarioMetadata ("KeyBindings", "Illustrates the KeyBindings API.")]
+[ScenarioCategory ("Mouse and Keyboard")]
+public sealed class KeyBindings : Scenario
+{
+    private readonly ObservableCollection<string> _focusedBindings = [];
+    private ListView _focusedBindingsListView;
+
+    public override void Main ()
+    {
+        // Init
+        Application.Init ();
+
+        // Setup - Create a top-level application window and configure it.
+        Window appWindow = new ()
+        {
+            Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}",
+            SuperViewRendersLineCanvas = true,
+        };
+
+        Label label = new ()
+        {
+            Title = "_Label:",
+        };
+        TextField textField = new ()
+        {
+            X = Pos.Right (label),
+            Y = Pos.Top (label),
+            Width = 20,
+        };
+
+        appWindow.Add (label, textField);
+
+        Button button = new ()
+        {
+            X = Pos.Right (textField) + 1,
+            Y = Pos.Top (label),
+            Text = "_Button",
+        };
+        appWindow.Add (button);
+
+        KeyBindingsDemo keyBindingsDemo = new ()
+        {
+            X = Pos.Right (button) + 1,
+            Width = Dim.Auto (DimAutoStyle.Text),
+            Height = Dim.Auto (DimAutoStyle.Text),
+            HotKeySpecifier = (Rune)'_',
+            Title = "_KeyBindingsDemo",
+            Text = @"These keys will cause this view to show a message box:
+- Hotkey: k, K, Alt-K, Alt-Shift-K
+- Focused: F3
+- Application: F4
+Pressing Ctrl-Q will cause it to quit the app.",
+            BorderStyle = LineStyle.Dashed
+        };
+        appWindow.Add (keyBindingsDemo);
+
+        ObservableCollection<string> appBindings = new ();
+        ListView appBindingsListView = new ()
+        {
+            Title = "_Application Bindings",
+            BorderStyle = LineStyle.Single,
+            X = -1,
+            Y = Pos.Bottom (keyBindingsDemo) + 1,
+            Width = Dim.Auto (),
+            Height = Dim.Fill () + 1,
+            CanFocus = true,
+            Source = new ListWrapper<string> (appBindings),
+            SuperViewRendersLineCanvas = true
+        };
+        appWindow.Add (appBindingsListView);
+
+        foreach (var appBinding in Application.GetKeyBindings ())
+        {
+            foreach (var view in appBinding.Value)
+            {
+                var commands = view.KeyBindings.GetCommands (appBinding.Key);
+                appBindings.Add ($"{appBinding.Key} -> {view.GetType ().Name} - {commands [0]}");
+            }
+        }
+
+        ObservableCollection<string> hotkeyBindings = new ();
+        ListView hotkeyBindingsListView = new ()
+        {
+            Title = "_Hotkey Bindings",
+            BorderStyle = LineStyle.Single,
+            X = Pos.Right (appBindingsListView) - 1,
+            Y = Pos.Bottom (keyBindingsDemo) + 1,
+            Width = Dim.Auto (),
+            Height = Dim.Fill () + 1,
+            CanFocus = true,
+            Source = new ListWrapper<string> (hotkeyBindings),
+            SuperViewRendersLineCanvas = true
+
+        };
+        appWindow.Add (hotkeyBindingsListView);
+
+        foreach (var subview in appWindow.Subviews)
+        {
+            foreach (var binding in subview.KeyBindings.Bindings.Where (b => b.Value.Scope == KeyBindingScope.HotKey))
+            {
+                hotkeyBindings.Add ($"{binding.Key} -> {subview.GetType ().Name} - {binding.Value.Commands [0]}");
+            }
+        }
+
+        _focusedBindingsListView = new ()
+        {
+            Title = "_Focused Bindings",
+            BorderStyle = LineStyle.Single,
+            X = Pos.Right (hotkeyBindingsListView) - 1,
+            Y = Pos.Bottom (keyBindingsDemo) + 1,
+            Width = Dim.Auto (),
+            Height = Dim.Fill () + 1,
+            CanFocus = true,
+            Source = new ListWrapper<string> (_focusedBindings),
+            SuperViewRendersLineCanvas = true
+
+        };
+        appWindow.Add (_focusedBindingsListView);
+
+        appWindow.Leave += AppWindow_Leave;
+        appWindow.Enter += AppWindow_Leave;
+        appWindow.DrawContent += AppWindow_DrawContent;
+
+        // Run - Start the application.
+        Application.Run (appWindow);
+        appWindow.Dispose ();
+
+        // Shutdown - Calling Application.Shutdown is required.
+        Application.Shutdown ();
+    }
+
+    private void AppWindow_DrawContent (object sender, DrawEventArgs e)
+    {
+        _focusedBindingsListView.Title = $"_Focused ({Application.Top.MostFocused.GetType ().Name}) Bindings";
+
+        _focusedBindings.Clear ();
+        foreach (var binding in Application.Top.MostFocused.KeyBindings.Bindings.Where (b => b.Value.Scope == KeyBindingScope.Focused))
+        {
+            _focusedBindings.Add ($"{binding.Key} -> {binding.Value.Commands [0]}");
+        }
+    }
+
+    private void AppWindow_Leave (object sender, FocusEventArgs e)
+    {
+        //foreach (var binding in Application.Top.MostFocused.KeyBindings.Bindings.Where (b => b.Value.Scope == KeyBindingScope.Focused))
+        //{
+        //    _focusedBindings.Add ($"{binding.Key} -> {binding.Value.Commands [0]}");
+        //}
+    }
+}
+
+public class KeyBindingsDemo : View
+{
+    public KeyBindingsDemo ()
+    {
+        CanFocus = true;
+
+        AddCommand (Command.New, ctx =>
+                                {
+                                    MessageBox.Query ("Hi", $"Key: {ctx.Key}\nCommand: {ctx.Command}", buttons: "Ok");
+
+                                    return true;
+                                });
+        AddCommand (Command.HotKey, ctx =>
+        {
+            MessageBox.Query ("Hi", $"Key: {ctx.Key}\nCommand: {ctx.Command}", buttons: "Ok");
+            SetFocus ();
+            return true;
+        });
+
+        KeyBindings.Add (Key.F3, KeyBindingScope.Focused, Command.New);
+        KeyBindings.Add (Key.F4, KeyBindingScope.Application, Command.New);
+
+
+        AddCommand (Command.QuitToplevel, ctx =>
+                                         {
+                                             Application.RequestStop ();
+                                             return true;
+                                         });
+        KeyBindings.Add (Key.Q.WithCtrl, KeyBindingScope.Application, Command.QuitToplevel);
+    }
+}

+ 34 - 24
UICatalog/Scenarios/MenuBarScenario.cs

@@ -5,7 +5,7 @@ namespace UICatalog.Scenarios;
 
 [ScenarioMetadata ("MenuBar", "Demonstrates the MenuBar using the same menu used in unit tests.")]
 [ScenarioCategory ("Controls")]
-[ScenarioCategory ("Menu")]
+[ScenarioCategory ("Menus")]
 public class MenuBarScenario : Scenario
 {
     private Label _currentMenuBarItem;
@@ -15,13 +15,14 @@ public class MenuBarScenario : Scenario
     private Label _lastKey;
 
     /// <summary>
-    ///     This method creates at test menu bar. It is called by the MenuBar unit tests so it's possible to do both unit
+    ///     This method creates at test menu bar. It is called by the MenuBar unit tests, so it's possible to do both unit
     ///     testing and user-experience testing with the same setup.
     /// </summary>
     /// <param name="actionFn"></param>
     /// <returns></returns>
     public static MenuBar CreateTestMenu (Func<string, bool> actionFn)
     {
+        // TODO: add a disabled menu item to this
         var mb = new MenuBar
         {
             Menus =
@@ -195,48 +196,50 @@ public class MenuBarScenario : Scenario
         return mb;
     }
 
-    // Don't create a Window, just return the top-level view
-    public override void Init ()
+    public override void Main ()
     {
+        // Init
         Application.Init ();
-        Top = new ();
-        Top.ColorScheme = Colors.ColorSchemes ["Base"];
-    }
 
-    public override void Setup ()
-    {
+        // Setup - Create a top-level application window and configure it.
+        Window appWindow = new ()
+        {
+            Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}",
+            BorderStyle = LineStyle.None
+        };
+
         MenuItem mbiCurrent = null;
         MenuItem miCurrent = null;
 
         var label = new Label { X = 0, Y = 10, Text = "Last Key: " };
-        Top.Add (label);
+        appWindow.Add (label);
 
         _lastKey = new Label { X = Pos.Right (label), Y = Pos.Top (label), Text = "" };
 
-        Top.Add (_lastKey);
+        appWindow.Add (_lastKey);
         label = new Label { X = 0, Y = Pos.Bottom (label), Text = "Current MenuBarItem: " };
-        Top.Add (label);
+        appWindow.Add (label);
 
         _currentMenuBarItem = new Label { X = Pos.Right (label), Y = Pos.Top (label), Text = "" };
-        Top.Add (_currentMenuBarItem);
+        appWindow.Add (_currentMenuBarItem);
 
         label = new Label { X = 0, Y = Pos.Bottom (label), Text = "Current MenuItem: " };
-        Top.Add (label);
+        appWindow.Add (label);
 
         _currentMenuItem = new Label { X = Pos.Right (label), Y = Pos.Top (label), Text = "" };
-        Top.Add (_currentMenuItem);
+        appWindow.Add (_currentMenuItem);
 
         label = new Label { X = 0, Y = Pos.Bottom (label), Text = "Last Action: " };
-        Top.Add (label);
+        appWindow.Add (label);
 
         _lastAction = new Label { X = Pos.Right (label), Y = Pos.Top (label), Text = "" };
-        Top.Add (_lastAction);
+        appWindow.Add (_lastAction);
 
         label = new Label { X = 0, Y = Pos.Bottom (label), Text = "Focused View: " };
-        Top.Add (label);
+        appWindow.Add (label);
 
         _focusedView = new Label { X = Pos.Right (label), Y = Pos.Top (label), Text = "" };
-        Top.Add (_focusedView);
+        appWindow.Add (_focusedView);
 
         MenuBar menuBar = CreateTestMenu (
                                           s =>
@@ -277,21 +280,28 @@ public class MenuBarScenario : Scenario
                                };
 
         // There's no focus change event, so this is a bit of a hack.
-        menuBar.LayoutComplete += (s, e) => { _focusedView.Text = Top.MostFocused?.ToString () ?? "None"; };
+        menuBar.LayoutComplete += (s, e) => { _focusedView.Text = appWindow.MostFocused?.ToString () ?? "None"; };
 
         var openBtn = new Button { X = Pos.Center (), Y = 4, Text = "_Open Menu", IsDefault = true };
         openBtn.Accept += (s, e) => { menuBar.OpenMenu (); };
-        Top.Add (openBtn);
+        appWindow.Add (openBtn);
 
         var hideBtn = new Button { X = Pos.Center (), Y = Pos.Bottom (openBtn), Text = "Toggle Menu._Visible" };
         hideBtn.Accept += (s, e) => { menuBar.Visible = !menuBar.Visible; };
-        Top.Add (hideBtn);
+        appWindow.Add (hideBtn);
 
         var enableBtn = new Button { X = Pos.Center (), Y = Pos.Bottom (hideBtn), Text = "_Toggle Menu.Enable" };
         enableBtn.Accept += (s, e) => { menuBar.Enabled = !menuBar.Enabled; };
-        Top.Add (enableBtn);
+        appWindow.Add (enableBtn);
+
+        appWindow.Add (menuBar);
+
+        // Run - Start the application.
+        Application.Run (appWindow);
+        appWindow.Dispose ();
 
-        Top.Add (menuBar);
+        // Shutdown - Calling Application.Shutdown is required.
+        Application.Shutdown ();
     }
 
     private void SetCurrentMenuBarItem (MenuItem mbi) { _currentMenuBarItem.Text = mbi != null ? mbi.Title : "Closed"; }

+ 2 - 2
UICatalog/UICatalog.cs

@@ -129,7 +129,7 @@ internal class UICatalogApp
                                 {
                                     var options = new Options
                                     {
-                                        Driver = context.ParseResult.GetValueForOption (driverOption),
+                                        Driver = context.ParseResult.GetValueForOption (driverOption) ?? string.Empty,
                                         Scenario = context.ParseResult.GetValueForArgument (scenarioArgument)
                                         /* etc. */
                                     };
@@ -662,7 +662,7 @@ internal class UICatalogApp
 
             List<MenuItem> schemeMenuItems = new ();
 
-            foreach (KeyValuePair<string, ColorScheme> sc in Colors.ColorSchemes)
+            foreach (KeyValuePair<string, ColorScheme?> sc in Colors.ColorSchemes)
             {
                 var item = new MenuItem { Title = $"_{sc.Key}", Data = sc.Key };
                 item.CheckType |= MenuItemCheckStyle.Radio;

+ 4 - 0
UnitTests/Application/ApplicationTests.cs

@@ -193,6 +193,9 @@ public class ApplicationTests
             Assert.Empty (Application._topLevels);
             Assert.Null (Application._mouseEnteredView);
 
+            // Keyboard
+            Assert.Empty (Application.GetViewsWithKeyBindings ());
+
             // Events - Can't check
             //Assert.Null (Application.NotifyNewRunState);
             //Assert.Null (Application.NotifyNewRunState);
@@ -225,6 +228,7 @@ public class ApplicationTests
         Application.AlternateBackwardKey = Key.A;
         Application.AlternateForwardKey = Key.B;
         Application.QuitKey = Key.C;
+        Application.AddKeyBinding(Key.A, new View ());
 
         //Application.OverlappedChildren = new List<View> ();
         //Application.OverlappedTop = 

+ 100 - 30
UnitTests/Application/KeyboardTests.cs

@@ -2,6 +2,9 @@
 
 namespace Terminal.Gui.ApplicationTests;
 
+/// <summary>
+/// Application tests for keyboard support.
+/// </summary>
 public class KeyboardTests
 {
     private readonly ITestOutputHelper _output;
@@ -15,6 +18,46 @@ public class KeyboardTests
 #endif
     }
 
+
+    [Fact]
+    [AutoInitShutdown]
+    public void QuitKey_Getter_Setter ()
+    {
+        Toplevel top = new ();
+        var isQuiting = false;
+
+        top.Closing += (s, e) =>
+                       {
+                           isQuiting = true;
+                           e.Cancel = true;
+                       };
+
+        Application.Begin (top);
+        top.Running = true;
+
+        Assert.Equal (KeyCode.Q | KeyCode.CtrlMask, Application.QuitKey.KeyCode);
+        Application.Driver.SendKeys ('Q', ConsoleKey.Q, false, false, true);
+        Assert.True (isQuiting);
+
+        isQuiting = false;
+        Application.OnKeyDown (Key.Q.WithCtrl);
+        Assert.True (isQuiting);
+
+        isQuiting = false;
+        Application.QuitKey = Key.C.WithCtrl;
+        Application.Driver.SendKeys ('Q', ConsoleKey.Q, false, false, true);
+        Assert.False (isQuiting);
+        Application.OnKeyDown (Key.Q.WithCtrl);
+        Assert.False (isQuiting);
+
+        Application.OnKeyDown (Application.QuitKey);
+        Assert.True (isQuiting);
+
+        // Reset the QuitKey to avoid throws errors on another tests
+        Application.QuitKey = Key.Q.WithCtrl;
+        top.Dispose ();
+    }
+
     [Fact]
     public void AlternateForwardKey_AlternateBackwardKey_Tests ()
     {
@@ -320,7 +363,7 @@ public class KeyboardTests
 
     [Fact]
     [AutoInitShutdown]
-    public void OnKeyDown_Application_KeyBinding ()
+    public void KeyBinding_OnKeyDown ()
     {
         var view = new ScopedKeyBindingView ();
         var invoked = false;
@@ -365,7 +408,7 @@ public class KeyboardTests
 
     [Fact]
     [AutoInitShutdown]
-    public void OnKeyDown_Application_KeyBinding_Negative ()
+    public void KeyBinding_OnKeyDown_Negative ()
     {
         var view = new ScopedKeyBindingView ();
         var invoked = false;
@@ -391,46 +434,73 @@ public class KeyboardTests
         top.Dispose ();
     }
 
+
     [Fact]
     [AutoInitShutdown]
-    public void QuitKey_Getter_Setter ()
+    public void KeyBinding_AddKeyBinding_Adds ()
     {
-        Toplevel top = new ();
-        var isQuiting = false;
+        View view1 = new ();
+        Application.AddKeyBinding (Key.A, view1);
 
-        top.Closing += (s, e) =>
-                       {
-                           isQuiting = true;
-                           e.Cancel = true;
-                       };
+        View view2 = new ();
+        Application.AddKeyBinding (Key.A, view2);
 
-        Application.Begin (top);
-        top.Running = true;
+        Assert.True (Application.TryGetKeyBindings (Key.A, out List<View> views));
+        Assert.Contains (view1, views);
+        Assert.Contains (view2, views);
 
-        Assert.Equal (KeyCode.Q | KeyCode.CtrlMask, Application.QuitKey.KeyCode);
-        Application.Driver.SendKeys ('Q', ConsoleKey.Q, false, false, true);
-        Assert.True (isQuiting);
+        Assert.False (Application.TryGetKeyBindings (Key.B, out List<View> _));
+    }
 
-        isQuiting = false;
-        Application.OnKeyDown (Key.Q.WithCtrl);
-        Assert.True (isQuiting);
+    [Fact]
+    [AutoInitShutdown]
+    public void KeyBinding_ViewKeyBindings_Add_Adds ()
+    {
+        View view1 = new ();
+        view1.KeyBindings.Add (Key.A, KeyBindingScope.Application, Command.Save);
+        view1.KeyBindings.Add (Key.B, KeyBindingScope.HotKey, Command.Left);
+        Assert.Single (Application.GetViewsWithKeyBindings ());
 
-        isQuiting = false;
-        Application.QuitKey = Key.C.WithCtrl;
-        Application.Driver.SendKeys ('Q', ConsoleKey.Q, false, false, true);
-        Assert.False (isQuiting);
-        Application.OnKeyDown (Key.Q.WithCtrl);
-        Assert.False (isQuiting);
+        View view2 = new ();
+        view2.KeyBindings.Add (Key.A, KeyBindingScope.Application, Command.Save);
+        view2.KeyBindings.Add (Key.B, KeyBindingScope.HotKey, Command.Left);
 
-        Application.OnKeyDown (Application.QuitKey);
-        Assert.True (isQuiting);
+        Assert.True (Application.TryGetKeyBindings (Key.A, out List<View> views));
+        Assert.Contains (view1, views);
+        Assert.Contains (view2, views);
 
-        // Reset the QuitKey to avoid throws errors on another tests
-        Application.QuitKey = Key.Q.WithCtrl;
-        top.Dispose ();
+        Assert.False (Application.TryGetKeyBindings (Key.B, out List<View> _));
+    }
+
+    [Fact]
+    [AutoInitShutdown]
+    public void KeyBinding_RemoveKeyBinding_Removes ()
+    {
+        View view1 = new ();
+        Application.AddKeyBinding (Key.A, view1);
+
+        Assert.True (Application.TryGetKeyBindings (Key.A, out List<View> views));
+        Assert.Contains (view1, views);
+
+        Application.RemoveKeyBinding (Key.A, view1);
+        Assert.False (Application.TryGetKeyBindings (Key.A, out List<View> _));
+    }
+
+    [Fact]
+    [AutoInitShutdown]
+    public void KeyBinding_ViewKeyBindings_RemoveKeyBinding_Removes ()
+    {
+        View view1 = new ();
+        view1.KeyBindings.Add (Key.A, KeyBindingScope.Application, Command.Save);
+
+        Assert.True (Application.TryGetKeyBindings (Key.A, out List<View> views));
+        Assert.Contains (view1, views);
+
+        view1.KeyBindings.Remove (Key.A);
+        Assert.False (Application.TryGetKeyBindings (Key.A, out List<View> _));
     }
 
-    // test Application key Bindings
+    // Test View for testing Application key Bindings
     public class ScopedKeyBindingView : View
     {
         public ScopedKeyBindingView ()

+ 48 - 16
UnitTests/Input/KeyBindingTests.cs

@@ -1,5 +1,4 @@
-using UICatalog.Scenarios;
-using Xunit.Abstractions;
+using Xunit.Abstractions;
 
 namespace Terminal.Gui.InputTests;
 
@@ -160,6 +159,39 @@ public class KeyBindingTests
         Assert.Equal (Key.A, resultKey);
     }
 
+    // Add should not allow duplicates
+    [Fact]
+    public void Add_Replaces_If_Exists ()
+    {
+        var keyBindings = new KeyBindings ();
+        keyBindings.Add (Key.A, Command.HotKey);
+        keyBindings.Add (Key.A, Command.Accept);
+
+        Command [] resultCommands = keyBindings.GetCommands (Key.A);
+        Assert.DoesNotContain (Command.HotKey, resultCommands);
+
+        keyBindings = new ();
+        keyBindings.Add (Key.A, KeyBindingScope.Focused, Command.HotKey);
+        keyBindings.Add (Key.A, KeyBindingScope.Focused, Command.Accept);
+
+        resultCommands = keyBindings.GetCommands (Key.A);
+        Assert.DoesNotContain (Command.HotKey, resultCommands);
+
+        keyBindings = new ();
+        keyBindings.Add (Key.A, KeyBindingScope.HotKey, Command.HotKey);
+        keyBindings.Add (Key.A, KeyBindingScope.Focused, Command.Accept);
+
+        resultCommands = keyBindings.GetCommands (Key.A);
+        Assert.DoesNotContain (Command.HotKey, resultCommands);
+
+        keyBindings = new ();
+        keyBindings.Add (Key.A, new KeyBinding (new [] { Command.HotKey }, KeyBindingScope.HotKey));
+        keyBindings.Add (Key.A, new KeyBinding (new [] { Command.Accept }, KeyBindingScope.HotKey));
+
+        resultCommands = keyBindings.GetCommands (Key.A);
+        Assert.DoesNotContain (Command.HotKey, resultCommands);
+    }
+
     [Fact]
     public void Replace_Key ()
     {
@@ -173,17 +205,17 @@ public class KeyBindingTests
         Assert.Empty (keyBindings.GetCommands (Key.A));
         Assert.Contains (Command.HotKey, keyBindings.GetCommands (Key.E));
 
-        keyBindings.Replace (Key.B, Key.E);
+        keyBindings.Replace (Key.B, Key.F);
         Assert.Empty (keyBindings.GetCommands (Key.B));
-        Assert.Contains (Command.HotKey, keyBindings.GetCommands (Key.E));
+        Assert.Contains (Command.HotKey, keyBindings.GetCommands (Key.F));
 
-        keyBindings.Replace (Key.C, Key.E);
+        keyBindings.Replace (Key.C, Key.G);
         Assert.Empty (keyBindings.GetCommands (Key.C));
-        Assert.Contains (Command.HotKey, keyBindings.GetCommands (Key.E));
+        Assert.Contains (Command.HotKey, keyBindings.GetCommands (Key.G));
 
-        keyBindings.Replace (Key.D, Key.E);
+        keyBindings.Replace (Key.D, Key.H);
         Assert.Empty (keyBindings.GetCommands (Key.D));
-        Assert.Contains (Command.HotKey, keyBindings.GetCommands (Key.E));
+        Assert.Contains (Command.HotKey, keyBindings.GetCommands (Key.H));
     }
 
     // Add with scope does the right things
@@ -229,14 +261,14 @@ public class KeyBindingTests
         binding = keyBindings.Get (key, scope);
         Assert.Contains (Command.Right, binding.Commands);
         Assert.Contains (Command.Left, binding.Commands);
+    }
 
-        // negative test
-        binding = keyBindings.Get (key, (KeyBindingScope)0);
-        Assert.Null (binding);
-
-        Command [] resultCommands = keyBindings.GetCommands (key);
-        Assert.Contains (Command.Right, resultCommands);
-        Assert.Contains (Command.Left, resultCommands);
+    [Fact]
+    public void Get_Binding_Not_Found_Throws ()
+    {
+        var keyBindings = new KeyBindings ();
+        Assert.Throws<InvalidOperationException> (() => keyBindings.Get (Key.A));
+        Assert.Throws<InvalidOperationException> (() => keyBindings.Get (Key.B, KeyBindingScope.Application));
     }
 
     [Theory]
@@ -259,7 +291,7 @@ public class KeyBindingTests
         Assert.Contains (Command.Left, binding.Commands);
 
         // negative test
-        success = keyBindings.TryGet (key, (KeyBindingScope)0, out binding);
+        success = keyBindings.TryGet (key, 0, out binding);
         Assert.False (success);
 
         Command [] resultCommands = keyBindings.GetCommands (key);

+ 1 - 3
UnitTests/TestHelpers.cs

@@ -77,7 +77,7 @@ public class AutoInitShutdownAttribute : BeforeAfterTestAttribute
         if (AutoInit)
         {
             // TODO: This Dispose call is here until all unit tests that don't correctly dispose Toplevel's they create are fixed.
-            //Application.Top?.Dispose ();
+            Application.Top?.Dispose ();
             Application.Shutdown ();
 #if DEBUG_IDISPOSABLE
             if (Responder.Instances.Count == 0)
@@ -171,8 +171,6 @@ public class SetupFakeDriverAttribute : BeforeAfterTestAttribute
         Debug.WriteLine ($"Before: {methodUnderTest.Name}");
         Assert.Null (Application.Driver);
         Application.Driver = new FakeDriver { Rows = 25, Cols = 25 };
-        Assert.Equal (FakeConsole.BufferWidth, Application.Driver.Cols);
-        Assert.Equal (FakeConsole.BufferHeight, Application.Driver.Rows);
         base.Before (methodUnderTest);
     }
 }

+ 21 - 18
UnitTests/UICatalog/ScenarioTests.cs

@@ -1,4 +1,5 @@
 using System.Collections.ObjectModel;
+using System.Diagnostics;
 using System.Reflection;
 using Xunit.Abstractions;
 
@@ -40,22 +41,8 @@ public class ScenarioTests : TestsAllViews
         // Press QuitKey 
         Assert.Empty (FakeConsole.MockKeyPresses);
 
-        // BUGBUG: (#2474) For some reason ReadKey is not returning the QuitKey for some Scenarios
-        // by adding this Space it seems to work.
-        //FakeConsole.PushMockKeyPress (Key.Space);
         FakeConsole.PushMockKeyPress ((KeyCode)Application.QuitKey);
 
-        // The only key we care about is the QuitKey
-        Application.KeyDown += (sender, args) =>
-                               {
-                                   _output.WriteLine ($"  Keypress: {args.KeyCode}");
-
-                                   // BUGBUG: (#2474) For some reason ReadKey is not returning the QuitKey for some Scenarios
-                                   // by adding this Space it seems to work.
-                                   // See #2474 for why this is commented out
-                                   Assert.Equal (Application.QuitKey.KeyCode, args.KeyCode);
-                               };
-
         uint abortTime = 500;
 
         // If the scenario doesn't close within 500ms, this will force it to quit
@@ -78,6 +65,10 @@ public class ScenarioTests : TestsAllViews
 
         Application.Iteration += (s, a) =>
                                  {
+                                     // Press QuitKey 
+                                     Assert.Empty (FakeConsole.MockKeyPresses);
+                                     FakeConsole.PushMockKeyPress ((KeyCode)Application.QuitKey);
+
                                      //output.WriteLine ($"  iteration {++iterations}");
                                      if (Application.Top.Running && FakeConsole.MockKeyPresses.Count == 0)
                                      {
@@ -426,15 +417,27 @@ public class ScenarioTests : TestsAllViews
         {
             var x = view.X.ToString ();
             var y = view.Y.ToString ();
-            _xRadioGroup.SelectedItem = posNames.IndexOf (posNames.Where (s => x.Contains (s)).First ());
-            _yRadioGroup.SelectedItem = posNames.IndexOf (posNames.Where (s => y.Contains (s)).First ());
+
+            try
+            {
+                _xRadioGroup.SelectedItem = posNames.IndexOf (posNames.First (s => x.Contains (s)));
+                _yRadioGroup.SelectedItem = posNames.IndexOf (posNames.First (s => y.Contains (s)));
+            }
+            catch (InvalidOperationException e)
+            {
+                // This is a hack to work around the fact that the Pos enum doesn't have an "Align" value yet
+                Debug.WriteLine ($"{e}");
+            }
+
             _xText.Text = $"{view.Frame.X}";
             _yText.Text = $"{view.Frame.Y}";
 
             var w = view.Width.ToString ();
             var h = view.Height.ToString ();
-            _wRadioGroup.SelectedItem = dimNames.IndexOf (dimNames.Where (s => w.Contains (s)).First ());
-            _hRadioGroup.SelectedItem = dimNames.IndexOf (dimNames.Where (s => h.Contains (s)).First ());
+
+            _wRadioGroup.SelectedItem = dimNames.IndexOf (dimNames.First (s => w.Contains (s)));
+            _hRadioGroup.SelectedItem = dimNames.IndexOf (dimNames.First (s => h.Contains (s)));
+
             _wText.Text = $"{view.Frame.Width}";
             _hText.Text = $"{view.Frame.Height}";
         }

+ 2 - 0
UnitTests/View/MouseTests.cs

@@ -534,6 +534,7 @@ public class MouseTests (ITestOutputHelper output) : TestsAllViews
     {
         var view = new View ()
         {
+            CanFocus = true,
             HighlightStyle = highlightOnPress,
             Height = 1,
             Width = 1
@@ -588,6 +589,7 @@ public class MouseTests (ITestOutputHelper output) : TestsAllViews
     {
         var view = new View ()
         {
+            CanFocus = true,
             HighlightStyle = HighlightStyle.Pressed | HighlightStyle.PressedOutside,
             Height = 1,
             Width = 1

+ 7 - 10
UnitTests/Views/MenuBarTests.cs

@@ -1235,11 +1235,11 @@ wo
     [InlineData ("_Edit", "F_ind", "", KeyCode.AltMask | KeyCode.E, KeyCode.F)]
     [InlineData ("_Edit", "F_ind", "", KeyCode.AltMask | KeyCode.E, KeyCode.AltMask | KeyCode.F)]
     [InlineData ("Closed", "None", "Replace", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.R)]
-    [InlineData ("Closed", "None", "", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.C)]
+    [InlineData ("Closed", "None", "Copy", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.C)]
     [InlineData ("_Edit", "_1st", "", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3)]
     [InlineData ("Closed", "None", "1", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3, KeyCode.D1)]
     [InlineData ("Closed", "None", "1", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3, KeyCode.Enter)]
-    [InlineData ("Closed", "None", "", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3, KeyCode.D4)]
+    [InlineData ("_Edit", "_3rd Level", "", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3, KeyCode.D4)]
     [InlineData ("Closed", "None", "5", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D4, KeyCode.D5)]
     [InlineData ("_About", "_About", "", KeyCode.AltMask | KeyCode.A)]
     public void KeyBindings_Navigation_Commands (
@@ -2246,7 +2246,6 @@ wo
         Assert.True (menu.NewKeyDownEvent (menu.Key));
         Assert.True (menu.IsMenuOpen);
 
-        // BUGBUG: This is wrong -> The _New doc isn't enabled because it can't execute and so can't be selected
         // The _New doc is enabled but the sub-menu isn't enabled. Is show but can't be selected and executed
         Assert.Equal ("_New", miCurrent.Parent.Title);
         Assert.Equal ("_New doc", miCurrent.Title);
@@ -2399,7 +2398,7 @@ Edit
         menu.MenuOpened += (s, e) =>
                            {
                                miCurrent = e.MenuItem;
-                               mCurrent = menu.openCurrentMenu;
+                               mCurrent = menu.OpenCurrentMenu;
                            };
         var top = new Toplevel ();
         top.Add (menu);
@@ -2770,7 +2769,7 @@ Edit
     }
 
     [Fact]
-    public void Separators_Does_Not_Throws_Pressing_Menu_Shortcut ()
+    public void Separator_Does_Not_Throws_Pressing_Menu_Hotkey ()
     {
         var menu = new MenuBar
         {
@@ -2782,9 +2781,7 @@ Edit
                     )
             ]
         };
-
-        Exception exception = Record.Exception (() => Assert.True (menu.NewKeyDownEvent (Key.Q.WithAlt)));
-        Assert.Null (exception);
+        Assert.False (menu.NewKeyDownEvent (Key.Q.WithAlt));
     }
 
     [Fact]
@@ -3126,7 +3123,7 @@ Edit
         Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorDown));
         menu.Draw ();
         menu._openMenu.Draw ();
-        menu.openCurrentMenu.Draw ();
+        menu.OpenCurrentMenu.Draw ();
 
         expected = @"
  Numbers           
@@ -3419,7 +3416,7 @@ Edit
         Assert.True (menu._openMenu.NewKeyDownEvent (Key.Enter));
         menu.Draw ();
         menu._openMenu.Draw ();
-        menu.openCurrentMenu.Draw ();
+        menu.OpenCurrentMenu.Draw ();
 
         expected = @"
  Numbers     

+ 52 - 19
UnitTests/Views/RadioGroupTests.cs

@@ -14,12 +14,12 @@ public class RadioGroupTests (ITestOutputHelper output)
         Assert.Equal (Rectangle.Empty, rg.Frame);
         Assert.Equal (0, rg.SelectedItem);
 
-        rg = new() { RadioLabels = new [] { "Test" } };
+        rg = new () { RadioLabels = new [] { "Test" } };
         Assert.True (rg.CanFocus);
         Assert.Single (rg.RadioLabels);
         Assert.Equal (0, rg.SelectedItem);
 
-        rg = new()
+        rg = new ()
         {
             X = 1,
             Y = 2,
@@ -32,7 +32,7 @@ public class RadioGroupTests (ITestOutputHelper output)
         Assert.Equal (new (1, 2, 20, 5), rg.Frame);
         Assert.Equal (0, rg.SelectedItem);
 
-        rg = new() { X = 1, Y = 2, RadioLabels = new [] { "Test" } };
+        rg = new () { X = 1, Y = 2, RadioLabels = new [] { "Test" } };
 
         var view = new View { Width = 30, Height = 40 };
         view.Add (rg);
@@ -130,6 +130,51 @@ public class RadioGroupTests (ITestOutputHelper output)
         Assert.Equal (1, rg.SelectedItem);
     }
 
+    [Fact]
+    public void HotKey_For_View_SetsFocus ()
+    {
+        var superView = new View
+        {
+            CanFocus = true
+        };
+        superView.Add (new View { CanFocus = true });
+
+        var group = new RadioGroup
+        {
+            Title = "Radio_Group",
+            RadioLabels = new [] { "_Left", "_Right", "Cen_tered", "_Justified" }
+        };
+        superView.Add (group);
+
+        Assert.False (group.HasFocus);
+        Assert.Equal (0, group.SelectedItem);
+
+        group.NewKeyDownEvent (Key.G.WithAlt);
+
+        Assert.Equal (0, group.SelectedItem);
+        Assert.True (group.HasFocus);
+    }
+
+    [Fact]
+    public void HotKey_For_Item_SetsFocus ()
+    {
+        var superView = new View
+        {
+            CanFocus = true
+        };
+        superView.Add (new View { CanFocus = true });
+        var group = new RadioGroup { RadioLabels = new [] { "_Left", "_Right", "Cen_tered", "_Justified" } };
+        superView.Add (group);
+
+        Assert.False (group.HasFocus);
+        Assert.Equal (0, group.SelectedItem);
+
+        group.NewKeyDownEvent (Key.R);
+
+        Assert.Equal (1, group.SelectedItem);
+        Assert.True (group.HasFocus);
+    }
+
     [Fact]
     public void HotKey_Command_Does_Not_Accept ()
     {
@@ -184,12 +229,8 @@ public class RadioGroupTests (ITestOutputHelper output)
 
         var expected = @$"
 ┌────────────────────────────┐
-│{
-    CM.Glyphs.Selected
-} Test                      │
-│{
-    CM.Glyphs.UnSelected
-} New Test 你               │
+│{CM.Glyphs.Selected} Test                      │
+│{CM.Glyphs.UnSelected} New Test 你               │
 │                            │
 └────────────────────────────┘
 ";
@@ -209,11 +250,7 @@ public class RadioGroupTests (ITestOutputHelper output)
 
         expected = @$"
 ┌────────────────────────────┐
-│{
-    CM.Glyphs.Selected
-} Test  {
-    CM.Glyphs.UnSelected
-} New Test 你       │
+│{CM.Glyphs.Selected} Test  {CM.Glyphs.UnSelected} New Test 你       │
 │                            │
 │                            │
 └────────────────────────────┘
@@ -234,11 +271,7 @@ public class RadioGroupTests (ITestOutputHelper output)
 
         expected = @$"
 ┌────────────────────────────┐
-│{
-    CM.Glyphs.Selected
-} Test    {
-    CM.Glyphs.UnSelected
-} New Test 你     │
+│{CM.Glyphs.Selected} Test    {CM.Glyphs.UnSelected} New Test 你     │
 │                            │
 │                            │
 └────────────────────────────┘

+ 1 - 1
UnitTests/Views/StatusBarTests.cs

@@ -187,7 +187,7 @@ CTRL-O Open {
         Assert.False (sb.CanFocus);
         Assert.Equal (Colors.ColorSchemes ["Menu"], sb.ColorScheme);
         Assert.Equal (0, sb.X);
-        Assert.Equal ("AnchorEnd(1)", sb.Y.ToString ());
+        Assert.Equal ("AnchorEnd()", sb.Y.ToString ());
         Assert.Equal (Dim.Fill (), sb.Width);
         Assert.Equal (1, sb.Height);
     }

File diff suppressed because it is too large
+ 238 - 137
UnitTests/Views/TextViewTests.cs


Some files were not shown because too many files changed in this diff