Преглед на файлове

Added KeyBindings scenario

Tig преди 1 година
родител
ревизия
0610c0f20f
променени са 37 файла, в които са добавени 1738 реда и са изтрити 1102 реда
  1. 0 51
      Terminal.Gui/Application.MainLoopSyncContext.cs
  2. 12 519
      Terminal.Gui/Application/Application.cs
  3. 293 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. 34 0
      Terminal.Gui/Input/CommandContext.cs
  14. 4 240
      Terminal.Gui/Input/KeyBinding.cs
  15. 46 0
      Terminal.Gui/Input/KeyBindingScope.cs
  16. 256 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. 91 25
      Terminal.Gui/View/ViewKeyboard.cs
  23. 4 0
      Terminal.Gui/View/ViewMouse.cs
  24. 9 0
      Terminal.Gui/View/ViewText.cs
  25. 1 1
      Terminal.Gui/Views/Button.cs
  26. 2 2
      Terminal.Gui/Views/CheckBox.cs
  27. 4 4
      Terminal.Gui/Views/ColorPicker.cs
  28. 15 4
      UICatalog/Scenarios/AllViewsTester.cs
  29. 134 0
      UICatalog/Scenarios/KeyBindings.cs
  30. 2 2
      UICatalog/UICatalog.cs
  31. 4 0
      UnitTests/Application/ApplicationTests.cs
  32. 100 30
      UnitTests/Application/KeyboardTests.cs
  33. 48 16
      UnitTests/Input/KeyBindingTests.cs
  34. 1 3
      UnitTests/TestHelpers.cs
  35. 21 18
      UnitTests/UICatalog/ScenarioTests.cs
  36. 2 0
      UnitTests/View/MouseTests.cs
  37. 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
 }

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

@@ -0,0 +1,293 @@
+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>
+    ///     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;

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

@@ -0,0 +1,34 @@
+#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>
+    public CommandContext (Command command, Key? key)
+    {
+        Command = command;
+        Key = key;
+    }
+
+    /// <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; }
+}

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

@@ -1,50 +1,11 @@
-// 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[])"/>.
-/// </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>
@@ -60,201 +21,4 @@ public class KeyBinding
 
     /// <summary>The scope of the <see cref="Commands"/> bound to a key.</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.
-    /// </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;
-    }
-}
+}

+ 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
+}

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

@@ -0,0 +1,256 @@
+#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)

+ 91 - 25
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>
@@ -601,9 +617,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 +662,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;
         }
@@ -704,6 +720,30 @@ public partial class View
         return false;
     }
 
+    // Function to search the subview hierarchy for the first view that has a KeyBindingScope.Application binding for the key.
+    // Called from Application.OnKeyDown
+    // TODO: Unwind recursion
+    // QUESTION: Should this return a list of views? As is, it will only return the first view that has the binding.
+    internal static View FindViewWithApplicationKeyBinding (View start, Key keyEvent)
+    {
+        if (start.KeyBindings.TryGet (keyEvent, KeyBindingScope.Application, out KeyBinding binding))
+        {
+            return start;
+        }
+
+        foreach (View subview in start.Subviews)
+        {
+            View found = FindViewWithApplicationKeyBinding (subview, keyEvent);
+
+            if (found is { })
+            {
+                return found;
+            }
+        }
+
+        return null;
+    }
+
     /// <summary>
     ///     Invoked when a key is pressed that may be mapped to a key binding. Set <see cref="Key.Handled"/> to true to
     ///     stop the key from being processed by other views.
@@ -739,7 +779,7 @@ public partial class View
             }
 
             // each command has its own return value
-            bool? thisReturn = InvokeCommand (command);
+            bool? thisReturn = InvokeCommand (command, key);
 
             // if we haven't got anything yet, the current command result should be used
             toReturn ??= thisReturn;
@@ -758,12 +798,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)
     {
         bool? toReturn = null;
 
@@ -775,7 +816,7 @@ public partial class View
             }
 
             // each command has its own return value
-            bool? thisReturn = InvokeCommand (command);
+            bool? thisReturn = InvokeCommand (command, key);
 
             // if we haven't got anything yet, the current command result should be used
             toReturn ??= thisReturn;
@@ -791,42 +832,67 @@ 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>
     /// <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)
     {
-        if (!CommandImplementations.ContainsKey (command))
+        if (CommandImplementations.TryGetValue (command, out Func<CommandContext, bool?> implementation))
         {
-            return null;
+            var context = new CommandContext (command, key); // 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>

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

@@ -53,7 +53,7 @@ public class Button : View
         HighlightStyle |= HighlightStyle.Hover;
 #endif
         // Override default behavior of View
-        AddCommand (Command.HotKey, () =>
+        AddCommand (Command.HotKey, ctx =>
         {
             SetFocus ();
             return !OnAccept ();

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

@@ -25,8 +25,8 @@ public class CheckBox : View
         CanFocus = true;
 
         // Things this view knows how to do
-        AddCommand (Command.Accept, OnToggled);
-        AddCommand (Command.HotKey, OnToggled);
+        AddCommand (Command.Accept, ctx => OnToggled ());
+        AddCommand (Command.HotKey, ctx => OnToggled ());
 
         // Default keybindings for this view
         KeyBindings.Add (Key.Space, Command.Accept);

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

@@ -195,10 +195,10 @@ public class ColorPicker : View
     /// <summary>Add the commands.</summary>
     private void AddCommands ()
     {
-        AddCommand (Command.Left, () => MoveLeft ());
-        AddCommand (Command.Right, () => MoveRight ());
-        AddCommand (Command.LineUp, () => MoveUp ());
-        AddCommand (Command.LineDown, () => MoveDown ());
+        AddCommand (Command.Left, (ctx) => MoveLeft ());
+        AddCommand (Command.Right, (ctx) => MoveRight ());
+        AddCommand (Command.LineUp, (ctx) => MoveUp ());
+        AddCommand (Command.LineDown, (ctx) => MoveDown ());
     }
 
     /// <summary>Add the KeyBindinds.</summary>

+ 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)
         {

+ 134 - 0
UICatalog/Scenarios/KeyBindings.cs

@@ -0,0 +1,134 @@
+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
+{
+    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 ()}",
+        };
+
+
+        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> bindingList = new ();
+        ListView keyBindingsListView = new ()
+        {
+            X = 0,
+            Y = Pos.Bottom (keyBindingsDemo) + 1,
+            Width = 60,
+            Height = Dim.Fill (1),
+            CanFocus = true,
+            Source = new ListWrapper<string> (bindingList),
+        };
+        appWindow.Add (keyBindingsListView);
+
+        foreach (var binding in appWindow.KeyBindings.Bindings)
+        {
+            bindingList.Add ($"{appWindow.GetType ().Name} - {binding.Key} - {binding.Value.Scope}: {binding.Value.Commands [0]}");
+        }
+
+        foreach (var subview in appWindow.Subviews)
+        {
+            foreach (var binding in subview.KeyBindings.Bindings)
+            {
+                bindingList.Add ($"{subview.GetType ().Name} - {binding.Key} - {binding.Value.Scope}: {binding.Value.Commands [0]}");
+            }
+        }
+
+        keyBindingsListView.SelectedItem = 0;
+        //keyBindingsListView.MoveEnd ();
+
+        //appWindow.Initialized += (s, e) =>
+        //{
+        //    keyBindingsListView.EnsureSelectedItemVisible ();
+        //};
+        // Run - Start the application.
+        Application.Run (appWindow);
+        appWindow.Dispose ();
+
+        // Shutdown - Calling Application.Shutdown is required.
+        Application.Shutdown ();
+    }
+}
+
+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);
+    }
+}

+ 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

Файловите разлики са ограничени, защото са твърде много
+ 238 - 137
UnitTests/Views/TextViewTests.cs


Някои файлове не бяха показани, защото твърде много файлове са промени