2
0
Эх сурвалжийг харах

Discovered serious issues with how HasFocus, OnEnter/OnLeave, etc... work in some edge cases.
This will require re-visiting the design at a deep level and fixing some long-standing but ignored issues such as how OnEnter/OnLeave don't follow proper cancelation design. Also, there's a need for keeping track of the old focus state of a tree of subviews when that tree loses focus; FocusDireciton is a hack that causes tons of confusion.

Tig 1 жил өмнө
parent
commit
4226d8172e

+ 76 - 3
Terminal.Gui/Application/Application.Navigation.cs

@@ -6,10 +6,75 @@ using System.Security.Cryptography;
 namespace Terminal.Gui;
 namespace Terminal.Gui;
 
 
 /// <summary>
 /// <summary>
-///     Helper class for <see cref="Application"/> navigation.
+///     Static helper class for <see cref="Application"/> navigation.
 /// </summary>
 /// </summary>
-internal static class ApplicationNavigation
+public static class ApplicationNavigation
 {
 {
+    private static View? _focused = null;
+
+    /// <summary>
+    ///     Gets or sets the most focused <see cref="View"/> in the application.
+    /// </summary>
+    /// <remarks>
+    ///     When set, raises <see cref="FocusedChanged"/>.
+    /// </remarks>
+    public static View? Focused
+    {
+        get => _focused;
+        set
+        {
+            if (_focused == value)
+            {
+                return;
+            }
+
+            _focused = value;
+
+            FocusedChanged?.Invoke (null, EventArgs.Empty);
+        }
+    }
+
+    /// <summary>
+    ///     Gets whether <paramref name="view"/> is in the Subview hierarchy of <paramref name="start"/>.
+    /// </summary>
+    /// <param name="start"></param>
+    /// <param name="view"></param>
+    /// <returns></returns>
+    public static bool IsInHierarchy (View start, View? view)
+    {
+        if (view is null)
+        {
+            return false;
+        }
+
+        if (view == start)
+        {
+            return true;
+        }
+
+        foreach (View subView in start.Subviews)
+        {
+            if (view == subView)
+            {
+                return true;
+            }
+
+            var found = IsInHierarchy (subView, view);
+            if (found)
+            {
+                return found;
+            }
+        }
+
+        return false;
+    }
+
+    /// <summary>
+    ///     Raised when the most focused <see cref="View"/> in the application has changed.
+    /// </summary>
+    public static event EventHandler<EventArgs>? FocusedChanged;
+
+
     /// <summary>
     /// <summary>
     ///    Gets the deepest focused subview of the specified <paramref name="view"/>.
     ///    Gets the deepest focused subview of the specified <paramref name="view"/>.
     /// </summary>
     /// </summary>
@@ -73,7 +138,7 @@ internal static class ApplicationNavigation
 
 
                 if (Application.Current.Focused is null)
                 if (Application.Current.Focused is null)
                 {
                 {
-                    Application.Current.RestoreFocus ();
+                    Application.Current.RestoreFocus (TabBehavior.TabGroup);
                 }
                 }
             }
             }
 
 
@@ -151,4 +216,12 @@ internal static class ApplicationNavigation
             ApplicationOverlapped.OverlappedMovePrevious ();
             ApplicationOverlapped.OverlappedMovePrevious ();
         }
         }
     }
     }
+
+    public static void ResetState ()
+    {
+        _focused?.Dispose ();
+        _focused = null;
+
+        FocusedChanged = null;
+    }
 }
 }

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

@@ -186,7 +186,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
 
 
         toplevel.LayoutSubviews ();
         toplevel.LayoutSubviews ();
         toplevel.PositionToplevels ();
         toplevel.PositionToplevels ();
-        toplevel.FocusFirst (null);
+        toplevel.FocusDeepest (null, NavigationDirection.Forward);
         ApplicationOverlapped.BringOverlappedTopToFront ();
         ApplicationOverlapped.BringOverlappedTopToFront ();
 
 
         if (refreshDriver)
         if (refreshDriver)
@@ -858,7 +858,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
                 if (Current is { HasFocus: false })
                 if (Current is { HasFocus: false })
                 {
                 {
                     Current.SetFocus ();
                     Current.SetFocus ();
-                    Current.RestoreFocus ();
+                    Current.RestoreFocus (null);
                 }
                 }
             }
             }
 
 

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

@@ -53,6 +53,8 @@ public static partial class Application
     // starts running and after Shutdown returns.
     // starts running and after Shutdown returns.
     internal static void ResetState (bool ignoreDisposed = false)
     internal static void ResetState (bool ignoreDisposed = false)
     {
     {
+        ApplicationNavigation.ResetState ();
+
         // Shutdown is the bookend for Init. As such it needs to clean up all resources
         // Shutdown is the bookend for Init. As such it needs to clean up all resources
         // Init created. Apps that do any threading will need to code defensively for this.
         // Init created. Apps that do any threading will need to code defensively for this.
         // e.g. see Issue #537
         // e.g. see Issue #537

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

@@ -121,7 +121,7 @@ public partial class View // Layout APIs
         View? superView;
         View? superView;
         statusBar = null!;
         statusBar = null!;
 
 
-        if (viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top)
+        if (viewToMove is not Toplevel || viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top)
         {
         {
             maxDimension = Driver.Cols;
             maxDimension = Driver.Cols;
             superView = Application.Top;
             superView = Application.Top;

+ 61 - 93
Terminal.Gui/View/View.Navigation.cs

@@ -51,19 +51,7 @@ public partial class View // Focus and cross-view navigation management (TabStop
 
 
         if (Focused is null)
         if (Focused is null)
         {
         {
-            switch (direction)
-            {
-                case NavigationDirection.Forward:
-                    FocusFirst (behavior);
-
-                    break;
-                case NavigationDirection.Backward:
-                    FocusLast (behavior);
-
-                    break;
-                default:
-                    throw new ArgumentOutOfRangeException (nameof (direction), direction, null);
-            }
+            FocusDeepest (behavior, direction);
 
 
             return Focused is { };
             return Focused is { };
         }
         }
@@ -90,8 +78,7 @@ public partial class View // Focus and cross-view navigation management (TabStop
         }
         }
         else
         else
         {
         {
-            // focusedIndex is at end of list. If we are going backwards,...
-            if (behavior == TabStop)
+            if (behavior == TabBehavior.TabGroup && behavior == TabStop && SuperView?.TabStop == TabBehavior.TabGroup)
             {
             {
                 // Go up the hierarchy
                 // Go up the hierarchy
                 // Leave
                 // Leave
@@ -102,6 +89,11 @@ public partial class View // Focus and cross-view navigation management (TabStop
 
 
                 return false;
                 return false;
             }
             }
+
+            Focused.RestoreFocus (TabBehavior.TabStop);
+
+            return true;
+
             // Wrap around
             // Wrap around
             //if (SuperView is {})
             //if (SuperView is {})
             //{
             //{
@@ -122,7 +114,10 @@ public partial class View // Focus and cross-view navigation management (TabStop
         }
         }
 
 
         View view = index [next];
         View view = index [next];
-
+        if (view.HasFocus)
+        {
+            return true;
+        }
 
 
         // The subview does not have focus, but at least one other that can. Can this one be focused?
         // The subview does not have focus, but at least one other that can. Can this one be focused?
         if (view.CanFocus && view.Visible && view.Enabled)
         if (view.CanFocus && view.Visible && view.Enabled)
@@ -130,20 +125,7 @@ public partial class View // Focus and cross-view navigation management (TabStop
             // Make Focused Leave
             // Make Focused Leave
             Focused.SetHasFocus (false, view);
             Focused.SetHasFocus (false, view);
 
 
-            switch (direction)
-            {
-                case NavigationDirection.Forward:
-                    view.FocusFirst (TabBehavior.TabStop);
-
-                    break;
-                case NavigationDirection.Backward:
-                    view.FocusLast (TabBehavior.TabStop);
-
-                    break;
-            }
-
-            SetFocus (view);
-
+            view.FocusDeepest (TabBehavior.TabStop, direction);
             return true;
             return true;
         }
         }
 
 
@@ -226,7 +208,7 @@ public partial class View // Focus and cross-view navigation management (TabStop
             if (!_canFocus && HasFocus)
             if (!_canFocus && HasFocus)
             {
             {
                 SetHasFocus (false, this);
                 SetHasFocus (false, this);
-                SuperView?.RestoreFocus ();
+                SuperView?.RestoreFocus (null);
 
 
                 // If EnsureFocus () didn't set focus to a view, focus the next focusable view in the application
                 // If EnsureFocus () didn't set focus to a view, focus the next focusable view in the application
                 if (SuperView is { Focused: null })
                 if (SuperView is { Focused: null })
@@ -300,55 +282,43 @@ public partial class View // Focus and cross-view navigation management (TabStop
     public View Focused { get; private set; }
     public View Focused { get; private set; }
 
 
     /// <summary>
     /// <summary>
-    ///     Focuses the first focusable view in <see cref="View.TabIndexes"/> if one exists. If there are no views in
+    ///     Focuses the deepest focusable view in <see cref="View.TabIndexes"/> if one exists. If there are no views in
     ///     <see cref="View.TabIndexes"/> then the focus is set to the view itself.
     ///     <see cref="View.TabIndexes"/> then the focus is set to the view itself.
     /// </summary>
     /// </summary>
     /// <param name="behavior"></param>
     /// <param name="behavior"></param>
-    public void FocusFirst (TabBehavior? behavior)
+    /// <param name="direction"></param>
+    public void FocusDeepest (TabBehavior? behavior, NavigationDirection direction)
     {
     {
         if (!CanBeVisible (this))
         if (!CanBeVisible (this))
         {
         {
             return;
             return;
         }
         }
 
 
-        if (_tabIndexes is null)
-        {
-            SuperView?.SetFocus (this);
+        View deepest = FindDeepestFocusableView (behavior, direction);
 
 
-            return;
-        }
-
-        var indicies = GetScopedTabIndexes (behavior, NavigationDirection.Forward);
-        if (indicies.Length > 0)
+        if (deepest is { })
         {
         {
-            SetFocus (indicies [0]);
+            deepest.SetFocus ();
         }
         }
+
+        SetFocus ();
     }
     }
 
 
-    /// <summary>
-    ///     Focuses the last focusable view in <see cref="View.TabIndexes"/> if one exists. If there are no views in
-    ///     <see cref="View.TabIndexes"/> then the focus is set to the view itself.
-    /// </summary>
-    /// <param name="behavior"></param>
-    public void FocusLast (TabBehavior? behavior)
+    [CanBeNull]
+    private View FindDeepestFocusableView (TabBehavior? behavior, NavigationDirection direction)
     {
     {
-        if (!CanBeVisible (this))
-        {
-            return;
-        }
+        var indicies = GetScopedTabIndexes (behavior, direction);
 
 
-        if (_tabIndexes is null)
+        foreach (View v in indicies)
         {
         {
-            SuperView?.SetFocus (this);
-
-            return;
+            if (v.TabIndexes.Count == 0)
+            {
+                return v;
+            }
+            return v.FindDeepestFocusableView (behavior, direction);
         }
         }
 
 
-        var indicies = GetScopedTabIndexes (behavior, NavigationDirection.Forward);
-        if (indicies.Length > 0)
-        {
-            SetFocus (indicies [^1]);
-        }
+        return null;
     }
     }
 
 
     /// <summary>
     /// <summary>
@@ -425,6 +395,13 @@ public partial class View // Focus and cross-view navigation management (TabStop
     /// </remarks>
     /// </remarks>
     public virtual bool OnEnter (View leavingView)
     public virtual bool OnEnter (View leavingView)
     {
     {
+        // BUGBUG: _hasFocus should ALWAYS be false when this method is called. 
+        if (_hasFocus)
+        {
+            Debug.WriteLine ($"BUGBUG: HasFocus should be false when OnEnter is called - Leaving: {leavingView} Entering: {this}");
+            // return true;
+        }
+
         var args = new FocusEventArgs (leavingView, this);
         var args = new FocusEventArgs (leavingView, this);
         Enter?.Invoke (this, args);
         Enter?.Invoke (this, args);
 
 
@@ -447,11 +424,11 @@ public partial class View // Focus and cross-view navigation management (TabStop
     /// </remarks>
     /// </remarks>
     public virtual bool OnLeave (View enteringView)
     public virtual bool OnLeave (View enteringView)
     {
     {
-        // BUGBUG: _hasFocus should ALWAYS be false when this method is called. 
-        if (_hasFocus)
+        // BUGBUG: _hasFocus should ALWAYS be true when this method is called. 
+        if (!_hasFocus)
         {
         {
-            Debug.WriteLine ($"BUGBUG: HasFocus should ALWAYS be false when OnLeave is called.");
-            return true;
+            Debug.WriteLine ($"BUGBUG: HasFocus should be true when OnLeave is called - Leaving: {this} Entering: {enteringView}");
+            //return true;
         }
         }
         var args = new FocusEventArgs (this, enteringView);
         var args = new FocusEventArgs (this, enteringView);
         Leave?.Invoke (this, args);
         Leave?.Invoke (this, args);
@@ -512,29 +489,22 @@ public partial class View // Focus and cross-view navigation management (TabStop
     }
     }
 
 
     /// <summary>
     /// <summary>
-    ///     INTERNAL helper for calling <see cref="FocusFirst"/> or <see cref="FocusLast"/> based on
+    ///     INTERNAL helper for calling <see cref="FocusDeepest"/> or <see cref="FocusLast"/> based on
     ///     <see cref="FocusDirection"/>.
     ///     <see cref="FocusDirection"/>.
     ///     FocusDirection is not public. This API is thus non-deterministic from a public API perspective.
     ///     FocusDirection is not public. This API is thus non-deterministic from a public API perspective.
     /// </summary>
     /// </summary>
-    internal void RestoreFocus ()
+    internal void RestoreFocus (TabBehavior? behavior)
     {
     {
         if (Focused is null && _subviews?.Count > 0)
         if (Focused is null && _subviews?.Count > 0)
         {
         {
-            if (FocusDirection == NavigationDirection.Forward)
-            {
-                FocusFirst (null);
-            }
-            else
-            {
-                FocusLast (null);
-            }
+            FocusDeepest (behavior, FocusDirection);
         }
         }
     }
     }
 
 
     /// <summary>
     /// <summary>
     ///     Internal API that causes <paramref name="viewToEnterFocus"/> to enter focus.
     ///     Internal API that causes <paramref name="viewToEnterFocus"/> to enter focus.
     ///     <paramref name="viewToEnterFocus"/> does not need to be a subview.
     ///     <paramref name="viewToEnterFocus"/> does not need to be a subview.
-    ///     Recursively sets focus upwards in the view hierarchy.
+    ///     Recursively sets focus DOWN in the view hierarchy.
     /// </summary>
     /// </summary>
     /// <param name="viewToEnterFocus"></param>
     /// <param name="viewToEnterFocus"></param>
     private void SetFocus (View viewToEnterFocus)
     private void SetFocus (View viewToEnterFocus)
@@ -583,19 +553,15 @@ public partial class View // Focus and cross-view navigation management (TabStop
             throw new ArgumentException (@$"The specified view {viewToEnterFocus} is not part of the hierarchy of {this}.");
             throw new ArgumentException (@$"The specified view {viewToEnterFocus} is not part of the hierarchy of {this}.");
         }
         }
 
 
-        // If a subview has focus, make it leave focus
+        // If a subview has focus, make it leave focus. This will leave focus up the hierarchy.
         Focused?.SetHasFocus (false, viewToEnterFocus);
         Focused?.SetHasFocus (false, viewToEnterFocus);
 
 
         // make viewToEnterFocus Focused and enter focus
         // make viewToEnterFocus Focused and enter focus
         View f = Focused;
         View f = Focused;
         Focused = viewToEnterFocus;
         Focused = viewToEnterFocus;
-        Focused.SetHasFocus (true, f);
+        Focused?.SetHasFocus (true, f, true);
 
 
-        // Ensure on either the first or last focusable subview of Focused
-        // BUGBUG: With Groups, this means the previous focus is lost
-        Focused.RestoreFocus ();
-
-        // Recursively set focus upwards in the view hierarchy
+        // Recursively set focus down the view hierarchy
         if (SuperView is { })
         if (SuperView is { })
         {
         {
             SuperView.SetFocus (this);
             SuperView.SetFocus (this);
@@ -626,15 +592,17 @@ public partial class View // Focus and cross-view navigation management (TabStop
     {
     {
         if (HasFocus != newHasFocus || force)
         if (HasFocus != newHasFocus || force)
         {
         {
-            _hasFocus = newHasFocus;
-
             if (newHasFocus)
             if (newHasFocus)
             {
             {
+                Debug.Assert (view is null || ApplicationNavigation.IsInHierarchy (SuperView, view));
                 OnEnter (view);
                 OnEnter (view);
+                ApplicationNavigation.Focused = this;
+                _hasFocus = true;
             }
             }
             else
             else
             {
             {
                 OnLeave (view);
                 OnLeave (view);
+                _hasFocus = false;
             }
             }
 
 
             SetNeedsDisplay ();
             SetNeedsDisplay ();
@@ -644,8 +612,8 @@ public partial class View // Focus and cross-view navigation management (TabStop
         if (!newHasFocus && Focused is { })
         if (!newHasFocus && Focused is { })
         {
         {
             View f = Focused;
             View f = Focused;
-            f.OnLeave (view);
-            f.SetHasFocus (false, view);
+            //f.OnLeave (view);
+            f.SetHasFocus (false, view, true);
             Focused = null;
             Focused = null;
         }
         }
     }
     }
@@ -654,7 +622,7 @@ public partial class View // Focus and cross-view navigation management (TabStop
 
 
 #nullable enable
 #nullable enable
 
 
-    private List<View> _tabIndexes;
+    private List<View>? _tabIndexes;
 
 
     // TODO: This should be a get-only property?
     // TODO: This should be a get-only property?
     // BUGBUG: This returns an AsReadOnly list, but isn't declared as such.
     // BUGBUG: This returns an AsReadOnly list, but isn't declared as such.
@@ -670,23 +638,23 @@ public partial class View // Focus and cross-view navigation management (TabStop
     /// <returns></returns>GetScopedTabIndexes
     /// <returns></returns>GetScopedTabIndexes
     private View [] GetScopedTabIndexes (TabBehavior? behavior, NavigationDirection direction)
     private View [] GetScopedTabIndexes (TabBehavior? behavior, NavigationDirection direction)
     {
     {
-        IEnumerable<View> indicies;
+        IEnumerable<View>? indicies;
 
 
         if (behavior.HasValue)
         if (behavior.HasValue)
         {
         {
-            indicies = _tabIndexes.Where (v => v.TabStop == behavior && v is { CanFocus: true, Visible: true, Enabled: true });
+            indicies = _tabIndexes?.Where (v => v.TabStop == behavior && v is { CanFocus: true, Visible: true, Enabled: true });
         }
         }
         else
         else
         {
         {
-            indicies = _tabIndexes.Where (v => v is { CanFocus: true, Visible: true, Enabled: true });
+            indicies = _tabIndexes?.Where (v => v is { CanFocus: true, Visible: true, Enabled: true });
         }
         }
 
 
         if (direction == NavigationDirection.Backward)
         if (direction == NavigationDirection.Backward)
         {
         {
-            indicies = indicies.Reverse ();
+            indicies = indicies?.Reverse ();
         }
         }
 
 
-        return indicies.ToArray ();
+        return indicies?.ToArray () ?? Array.Empty<View> ();
 
 
     }
     }
 
 

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

@@ -538,7 +538,7 @@ public class FileDialog : Dialog
         // to streamline user experience and allow direct typing of paths
         // to streamline user experience and allow direct typing of paths
         // with zero navigation we start with focus in the text box and any
         // with zero navigation we start with focus in the text box and any
         // default/current path fully selected and ready to be overwritten
         // default/current path fully selected and ready to be overwritten
-        _tbPath.FocusFirst (null);
+        _tbPath.FocusDeepest (null, NavigationDirection.Forward);
         _tbPath.SelectAll ();
         _tbPath.SelectAll ();
 
 
         if (string.IsNullOrEmpty (Title))
         if (string.IsNullOrEmpty (Title))
@@ -1050,7 +1050,7 @@ public class FileDialog : Dialog
     {
     {
         if (keyEvent.KeyCode == isKey)
         if (keyEvent.KeyCode == isKey)
         {
         {
-            to.FocusFirst (null);
+            to.FocusDeepest (null, NavigationDirection.Forward);
 
 
             if (to == _tbPath)
             if (to == _tbPath)
             {
             {
@@ -1439,7 +1439,7 @@ public class FileDialog : Dialog
     {
     {
         if (_treeView.HasFocus && Separators.Contains ((char)keyEvent))
         if (_treeView.HasFocus && Separators.Contains ((char)keyEvent))
         {
         {
-            _tbPath.FocusFirst (null);
+            _tbPath.FocusDeepest (null, NavigationDirection.Forward);
 
 
             // let that keystroke go through on the tbPath instead
             // let that keystroke go through on the tbPath instead
             return true;
             return true;

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

@@ -431,7 +431,7 @@ public partial class Toplevel : View
         {
         {
             if (Focused is null)
             if (Focused is null)
             {
             {
-                RestoreFocus ();
+                RestoreFocus (null);
             }
             }
 
 
             return null;
             return null;

+ 23 - 5
UICatalog/Scenarios/AdornmentsEditor.cs

@@ -34,10 +34,28 @@ public class AdornmentsEditor : View
 
 
         TabStop = TabBehavior.TabGroup;
         TabStop = TabBehavior.TabGroup;
 
 
-        Application.MouseEvent += Application_MouseEvent;
+        //Application.MouseEvent += Application_MouseEvent;
+        ApplicationNavigation.FocusedChanged += ApplicationNavigationOnFocusedChanged;
         Initialized += AdornmentsEditor_Initialized;
         Initialized += AdornmentsEditor_Initialized;
     }
     }
 
 
+    private void ApplicationNavigationOnFocusedChanged (object sender, EventArgs e)
+    {
+        if (ApplicationNavigation.IsInHierarchy (this, ApplicationNavigation.Focused))
+        {
+            return;
+        }
+
+        if (ApplicationNavigation.Focused is Adornment adornment)
+        {
+            ViewToEdit = adornment.Parent;
+        }
+        else
+        {
+            ViewToEdit = ApplicationNavigation.Focused;
+        }
+    }
+
     /// <summary>
     /// <summary>
     /// Gets or sets whether the AdornmentsEditor should automatically select the View to edit when the mouse is clicked
     /// Gets or sets whether the AdornmentsEditor should automatically select the View to edit when the mouse is clicked
     /// anywhere outside the editor.
     /// anywhere outside the editor.
@@ -170,11 +188,11 @@ public class AdornmentsEditor : View
             _viewToEdit = value;
             _viewToEdit = value;
 
 
 
 
-            _marginEditor.AdornmentToEdit = _viewToEdit.Margin ?? null;
-            _borderEditor.AdornmentToEdit = _viewToEdit.Border ?? null;
-            _paddingEditor.AdornmentToEdit = _viewToEdit.Padding ?? null;
+            _marginEditor.AdornmentToEdit = _viewToEdit?.Margin ?? null;
+            _borderEditor.AdornmentToEdit = _viewToEdit?.Border ?? null;
+            _paddingEditor.AdornmentToEdit = _viewToEdit?.Padding ?? null;
 
 
-            _lblView.Text = _viewToEdit.ToString ();
+            _lblView.Text = $"{_viewToEdit?.GetType ().Name}: {_viewToEdit?.Id}"  ?? string.Empty;
 
 
             return;
             return;
         }
         }

+ 4 - 4
UICatalog/Scenarios/Editor.cs

@@ -722,7 +722,7 @@ public class Editor : Scenario
             }
             }
             else
             else
             {
             {
-                FocusFirst (null);
+                FocusDeepest (null, NavigationDirection.Forward);
             }
             }
         }
         }
 
 
@@ -739,7 +739,7 @@ public class Editor : Scenario
         _findReplaceWindow.SuperView.BringSubviewToFront (_findReplaceWindow);
         _findReplaceWindow.SuperView.BringSubviewToFront (_findReplaceWindow);
         _tabView.SetFocus ();
         _tabView.SetFocus ();
         _tabView.SelectedTab = isFind ? _tabView.Tabs.ToArray () [0] : _tabView.Tabs.ToArray () [1];
         _tabView.SelectedTab = isFind ? _tabView.Tabs.ToArray () [0] : _tabView.Tabs.ToArray () [1];
-        _tabView.SelectedTab.View.FocusFirst (null);
+        _tabView.SelectedTab.View.FocusDeepest (null, NavigationDirection.Forward);
     }
     }
 
 
     private void CreateFindReplace ()
     private void CreateFindReplace ()
@@ -753,10 +753,10 @@ public class Editor : Scenario
 
 
         _tabView.AddTab (new () { DisplayText = "Find", View = CreateFindTab () }, true);
         _tabView.AddTab (new () { DisplayText = "Find", View = CreateFindTab () }, true);
         _tabView.AddTab (new () { DisplayText = "Replace", View = CreateReplaceTab () }, false);
         _tabView.AddTab (new () { DisplayText = "Replace", View = CreateReplaceTab () }, false);
-        _tabView.SelectedTabChanged += (s, e) => _tabView.SelectedTab.View.FocusFirst (null);
+        _tabView.SelectedTabChanged += (s, e) => _tabView.SelectedTab.View.FocusDeepest (null, NavigationDirection.Forward);
         _findReplaceWindow.Add (_tabView);
         _findReplaceWindow.Add (_tabView);
 
 
-        _tabView.SelectedTab.View.FocusLast (null); // Hack to get the first tab to be focused
+//        _tabView.SelectedTab.View.FocusLast (null); // Hack to get the first tab to be focused
         _findReplaceWindow.Visible = false;
         _findReplaceWindow.Visible = false;
         _appWindow.Add (_findReplaceWindow);
         _appWindow.Add (_findReplaceWindow);
     }
     }

+ 1 - 1
UICatalog/Scenarios/Notepad.cs

@@ -309,7 +309,7 @@ public class Notepad : Scenario
         tab.CloneTo (newTabView);
         tab.CloneTo (newTabView);
         newTile.ContentView.Add (newTabView);
         newTile.ContentView.Add (newTabView);
 
 
-        newTabView.FocusFirst (null);
+        newTabView.FocusDeepest (null, NavigationDirection.Forward);
         newTabView.AdvanceFocus (NavigationDirection.Forward, null);
         newTabView.AdvanceFocus (NavigationDirection.Forward, null);
     }
     }
 
 

+ 1 - 1
UICatalog/Scenarios/Sliders.cs

@@ -609,7 +609,7 @@ public class Sliders : Scenario
                              };
                              };
         }
         }
 
 
-        app.FocusFirst (null);
+        app.FocusDeepest (null, NavigationDirection.Forward);
 
 
         Application.Run (app);
         Application.Run (app);
         app.Dispose ();
         app.Dispose ();

+ 62 - 9
UICatalog/Scenarios/ViewExperiments.cs

@@ -30,7 +30,7 @@ public class ViewExperiments : Scenario
 
 
         FrameView testFrame = new ()
         FrameView testFrame = new ()
         {
         {
-            Title = "Test Frame",
+            Title = "_1 Test Frame",
             X = Pos.Right (editor),
             X = Pos.Right (editor),
             Width = Dim.Fill (),
             Width = Dim.Fill (),
             Height = Dim.Fill (),
             Height = Dim.Fill (),
@@ -42,14 +42,27 @@ public class ViewExperiments : Scenario
         {
         {
             X = 0,
             X = 0,
             Y = 0,
             Y = 0,
-            Title = "TopButton_1",
+            Title = $"TopButton _{GetNextHotKey()}",
         };
         };
 
 
         testFrame.Add (button);
         testFrame.Add (button);
 
 
-        var overlappedView1 = CreateOverlappedView (3, 2, 2);
-        var overlappedView2 = CreateOverlappedView (4, 34, 4);
+        var tiledView1 = CreateTiledView (0, 2, 2);
+        var tiledView2 = CreateTiledView (1, Pos.Right (tiledView1), Pos.Top (tiledView1));
 
 
+        testFrame.Add (tiledView1);
+        testFrame.Add (tiledView2);
+
+        var overlappedView1 = CreateOverlappedView (2, Pos.Center(), Pos.Center());
+        var tiledSubView = CreateTiledView (4, 0, 2);
+        overlappedView1.Add (tiledSubView);
+        
+        var overlappedView2 = CreateOverlappedView (3, Pos.Center() + 5, Pos.Center() + 5);
+        tiledSubView = CreateTiledView (4, 0, 2);
+        overlappedView2.Add (tiledSubView);
+
+        tiledSubView = CreateTiledView (5, 0, Pos.Bottom(tiledSubView));
+        overlappedView2.Add (tiledSubView);
 
 
         testFrame.Add (overlappedView1);
         testFrame.Add (overlappedView1);
         testFrame.Add (overlappedView2);
         testFrame.Add (overlappedView2);
@@ -58,7 +71,7 @@ public class ViewExperiments : Scenario
         {
         {
             X = Pos.AnchorEnd (),
             X = Pos.AnchorEnd (),
             Y = Pos.AnchorEnd (),
             Y = Pos.AnchorEnd (),
-            Title = "TopButton_2",
+            Title = $"TopButton _{GetNextHotKey ()}",
         };
         };
 
 
         testFrame.Add (button);
         testFrame.Add (button);
@@ -68,7 +81,47 @@ public class ViewExperiments : Scenario
         Application.Shutdown ();
         Application.Shutdown ();
     }
     }
 
 
-    private View CreateOverlappedView (int id, int x, int y)
+    private int _hotkeyCount;
+
+    private char GetNextHotKey ()
+    {
+        return (char)((int)'A' + _hotkeyCount++);
+    }
+
+    private View CreateTiledView (int id, Pos x, Pos y)
+    {
+        View overlapped = new View
+        {
+            X = x,
+            Y = y,
+            Height = Dim.Auto (),
+            Width = Dim.Auto (),
+            Title = $"Tiled{id} _{GetNextHotKey ()}",
+            Id = $"Tiled{id}",
+            BorderStyle = LineStyle.Single,
+            CanFocus = true, // Can't drag without this? BUGBUG
+            TabStop = TabBehavior.TabGroup,
+            Arrangement = ViewArrangement.Fixed
+        };
+
+        Button button = new ()
+        {
+            Title = $"Tiled Button{id} _{GetNextHotKey ()}"
+        };
+        overlapped.Add (button);
+
+        button = new ()
+        {
+            Y = Pos.Bottom (button),
+            Title = $"Tiled Button{id} _{GetNextHotKey ()}"
+        };
+        overlapped.Add (button);
+
+        return overlapped;
+    }
+
+
+    private View CreateOverlappedView (int id, Pos x, Pos y)
     {
     {
         View overlapped = new View
         View overlapped = new View
         {
         {
@@ -76,7 +129,7 @@ public class ViewExperiments : Scenario
             Y = y,
             Y = y,
             Height = Dim.Auto (),
             Height = Dim.Auto (),
             Width = Dim.Auto (),
             Width = Dim.Auto (),
-            Title = $"Overlapped_{id}",
+            Title = $"Overlapped{id} _{GetNextHotKey ()}",
             ColorScheme = Colors.ColorSchemes ["Toplevel"],
             ColorScheme = Colors.ColorSchemes ["Toplevel"],
             Id = $"Overlapped{id}",
             Id = $"Overlapped{id}",
             ShadowStyle = ShadowStyle.Transparent,
             ShadowStyle = ShadowStyle.Transparent,
@@ -88,14 +141,14 @@ public class ViewExperiments : Scenario
 
 
         Button button = new ()
         Button button = new ()
         {
         {
-            Title = $"Button{id} _{id * 2}"
+            Title = $"Button{id} _{GetNextHotKey ()}"
         };
         };
         overlapped.Add (button);
         overlapped.Add (button);
 
 
         button = new ()
         button = new ()
         {
         {
             Y = Pos.Bottom (button),
             Y = Pos.Bottom (button),
-            Title = $"Button{id} _{id * 2 + 1}"
+            Title = $"Button{id} _{GetNextHotKey ()}"
         };
         };
         overlapped.Add (button);
         overlapped.Add (button);
 
 

+ 26 - 1
UnitTests/Application/Application.NavigationTests.cs

@@ -1,12 +1,37 @@
 using Moq;
 using Moq;
 using Xunit.Abstractions;
 using Xunit.Abstractions;
+using Terminal.Gui;
 
 
-namespace Terminal.Gui.ApplicationTests;
+namespace Terminal.Gui.ApplicationTests.NavigationTests;
 
 
 public class ApplicationNavigationTests (ITestOutputHelper output)
 public class ApplicationNavigationTests (ITestOutputHelper output)
 {
 {
     private readonly ITestOutputHelper _output = output;
     private readonly ITestOutputHelper _output = output;
 
 
+    [Fact]
+    public void Focused_Change_Raises_FocusedChanged ()
+    {
+        bool raised = false;
+
+        ApplicationNavigation.FocusedChanged += ApplicationNavigationOnFocusedChanged;
+
+        ApplicationNavigation.Focused = new View();
+
+        Assert.True (raised);
+
+        ApplicationNavigation.Focused.Dispose ();
+        ApplicationNavigation.Focused = null;
+
+        ApplicationNavigation.FocusedChanged -= ApplicationNavigationOnFocusedChanged;
+
+        return;
+
+        void ApplicationNavigationOnFocusedChanged (object sender, EventArgs e)
+        {
+            raised = true;
+        }
+    }
+
     [Fact]
     [Fact]
     public void GetDeepestFocusedSubview_ShouldReturnNull_WhenViewIsNull ()
     public void GetDeepestFocusedSubview_ShouldReturnNull_WhenViewIsNull ()
     {
     {

+ 2 - 0
UnitTests/Application/ApplicationTests.cs

@@ -201,6 +201,8 @@ public class ApplicationTests
             // Keyboard
             // Keyboard
             Assert.Empty (Application.GetViewKeyBindings ());
             Assert.Empty (Application.GetViewKeyBindings ());
 
 
+            Assert.Null (ApplicationNavigation.Focused);
+
             // Events - Can't check
             // Events - Can't check
             //Assert.Null (Application.NotifyNewRunState);
             //Assert.Null (Application.NotifyNewRunState);
             //Assert.Null (Application.NotifyNewRunState);
             //Assert.Null (Application.NotifyNewRunState);

+ 1 - 1
UnitTests/View/NavigationTests.cs

@@ -510,7 +510,7 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews
                                      Assert.False (win.HasFocus);
                                      Assert.False (win.HasFocus);
 
 
                                      win.Enabled = true;
                                      win.Enabled = true;
-                                     win.FocusFirst (null);
+                                     win.FocusDeepest (null, NavigationDirection.Forward);
                                      Assert.True (button.HasFocus);
                                      Assert.True (button.HasFocus);
                                      Assert.True (win.HasFocus);
                                      Assert.True (win.HasFocus);
 
 

+ 1 - 1
UnitTests/View/ViewTests.cs

@@ -1091,7 +1091,7 @@ At 0,0
                                      Assert.True (RunesCount () == 0);
                                      Assert.True (RunesCount () == 0);
 
 
                                      win.Visible = true;
                                      win.Visible = true;
-                                     win.FocusFirst (null);
+                                     win.FocusDeepest (null, NavigationDirection.Forward);
                                      Assert.True (button.HasFocus);
                                      Assert.True (button.HasFocus);
                                      Assert.True (win.HasFocus);
                                      Assert.True (win.HasFocus);
                                      top.Draw ();
                                      top.Draw ();

+ 3 - 3
UnitTests/Views/ComboBoxTests.cs

@@ -852,7 +852,7 @@ Three ",
         Assert.True (Application.OnKeyDown (Key.CursorDown)); // losing focus
         Assert.True (Application.OnKeyDown (Key.CursorDown)); // losing focus
         Assert.False (cb.IsShow);
         Assert.False (cb.IsShow);
         Assert.False (cb.HasFocus);
         Assert.False (cb.HasFocus);
-        top.FocusFirst (null); // Gets focus again
+        top.FocusDeepest (null, NavigationDirection.Forward); // Gets focus again
         Assert.False (cb.IsShow);
         Assert.False (cb.IsShow);
         Assert.True (cb.HasFocus);
         Assert.True (cb.HasFocus);
         cb.Expand ();
         cb.Expand ();
@@ -960,7 +960,7 @@ Three
         Assert.False (cb.IsShow);
         Assert.False (cb.IsShow);
         Assert.Equal (-1, cb.SelectedItem);
         Assert.Equal (-1, cb.SelectedItem);
         Assert.Equal ("One", cb.Text);
         Assert.Equal ("One", cb.Text);
-        top.FocusFirst (null); // Gets focus again
+        top.FocusDeepest (null, NavigationDirection.Forward); // Gets focus again
         Assert.True (cb.HasFocus);
         Assert.True (cb.HasFocus);
         Assert.False (cb.IsShow);
         Assert.False (cb.IsShow);
         Assert.Equal (-1, cb.SelectedItem);
         Assert.Equal (-1, cb.SelectedItem);
@@ -980,7 +980,7 @@ Three
         var cb = new ComboBox ();
         var cb = new ComboBox ();
         var top = new Toplevel ();
         var top = new Toplevel ();
         top.Add (cb);
         top.Add (cb);
-        top.FocusFirst (null);
+        top.FocusDeepest (null, NavigationDirection.Forward);
         Assert.Null (cb.Source);
         Assert.Null (cb.Source);
         Assert.Equal (-1, cb.SelectedItem);
         Assert.Equal (-1, cb.SelectedItem);
         ObservableCollection<string> source = [];
         ObservableCollection<string> source = [];

+ 2 - 2
UnitTests/Views/TableViewTests.cs

@@ -616,7 +616,7 @@ public class TableViewTests (ITestOutputHelper output)
         top.Add (tableView);
         top.Add (tableView);
         Application.Begin (top);
         Application.Begin (top);
 
 
-        top.FocusFirst (null);
+        top.FocusDeepest (null, NavigationDirection.Forward);
         Assert.True (tableView.HasFocus);
         Assert.True (tableView.HasFocus);
 
 
         Assert.Equal (0, tableView.RowOffset);
         Assert.Equal (0, tableView.RowOffset);
@@ -1606,7 +1606,7 @@ public class TableViewTests (ITestOutputHelper output)
         top.Add (tv);
         top.Add (tv);
         Application.Begin (top);
         Application.Begin (top);
 
 
-        top.FocusFirst (null);
+        top.FocusDeepest (null, NavigationDirection.Forward);
         Assert.True (tv.HasFocus);
         Assert.True (tv.HasFocus);
 
 
         // already on fish
         // already on fish

+ 3 - 3
UnitTests/Views/TextFieldTests.cs

@@ -78,7 +78,7 @@ public class TextFieldTests (ITestOutputHelper output)
     public void Cancel_TextChanging_ThenBackspace ()
     public void Cancel_TextChanging_ThenBackspace ()
     {
     {
         var tf = new TextField ();
         var tf = new TextField ();
-        tf.RestoreFocus ();
+        tf.RestoreFocus (null);
         tf.NewKeyDownEvent (Key.A.WithShift);
         tf.NewKeyDownEvent (Key.A.WithShift);
         Assert.Equal ("A", tf.Text);
         Assert.Equal ("A", tf.Text);
 
 
@@ -929,7 +929,7 @@ public class TextFieldTests (ITestOutputHelper output)
     public void Backspace_From_End ()
     public void Backspace_From_End ()
     {
     {
         var tf = new TextField { Text = "ABC" };
         var tf = new TextField { Text = "ABC" };
-        tf.RestoreFocus ();
+        tf.RestoreFocus (null);
         Assert.Equal ("ABC", tf.Text);
         Assert.Equal ("ABC", tf.Text);
         tf.BeginInit ();
         tf.BeginInit ();
         tf.EndInit ();
         tf.EndInit ();
@@ -956,7 +956,7 @@ public class TextFieldTests (ITestOutputHelper output)
     public void Backspace_From_Middle ()
     public void Backspace_From_Middle ()
     {
     {
         var tf = new TextField { Text = "ABC" };
         var tf = new TextField { Text = "ABC" };
-        tf.RestoreFocus ();
+        tf.RestoreFocus (null);
         tf.CursorPosition = 2;
         tf.CursorPosition = 2;
         Assert.Equal ("ABC", tf.Text);
         Assert.Equal ("ABC", tf.Text);
 
 

+ 1 - 1
UnitTests/Views/TreeTableSourceTests.cs

@@ -289,7 +289,7 @@ public class TreeTableSourceTests : IDisposable
 
 
         var top = new Toplevel ();
         var top = new Toplevel ();
         top.Add (tableView);
         top.Add (tableView);
-        top.RestoreFocus ();
+        top.RestoreFocus (null);
         Assert.Equal (tableView, top.MostFocused);
         Assert.Equal (tableView, top.MostFocused);
 
 
         return tableView;
         return tableView;