فهرست منبع

Decouped Application.Navigation

Tig 3 هفته پیش
والد
کامیت
e25e04f667

+ 4 - 4
Terminal.Gui/App/ApplicationImpl.Lifecycle.cs

@@ -33,8 +33,8 @@ public partial class ApplicationImpl
             _driverName = ForceDriver;
         }
 
-        Debug.Assert (Navigation is null);
-        Navigation = new ();
+       // Debug.Assert (Navigation is null);
+       // Navigation = new ();
 
         Debug.Assert (Popover is null);
         Popover = new ();
@@ -49,7 +49,7 @@ public partial class ApplicationImpl
         Key existingPrevTabGroupKey = _keyboard?.PrevTabGroupKey ?? Key.F6.WithShift;
 
         // Reset keyboard to ensure fresh state with default bindings
-        _keyboard = new KeyboardImpl { Application = this };
+        _keyboard = new KeyboardImpl { App = this };
 
         // Restore previously set keys if they existed and were different from defaults
         if (hasExistingKeyboard)
@@ -221,7 +221,7 @@ public partial class ApplicationImpl
 
         // === 7. Clear navigation and screen state ===
         ScreenChanged = null;
-        Navigation = null;
+        //Navigation = null;
 
         // === 8. Reset initialization state ===
         Initialized = false;

+ 16 - 2
Terminal.Gui/App/ApplicationImpl.cs

@@ -75,7 +75,7 @@ public partial class ApplicationImpl : IApplication
         {
             if (_keyboard is null)
             {
-                _keyboard = new KeyboardImpl { Application = this };
+                _keyboard = new KeyboardImpl { App = this };
             }
 
             return _keyboard;
@@ -90,8 +90,22 @@ public partial class ApplicationImpl : IApplication
     /// <inheritdoc/>
     public ApplicationPopover? Popover { get; set; }
 
+    private ApplicationNavigation? _navigation;
+
     /// <inheritdoc/>
-    public ApplicationNavigation? Navigation { get; set; }
+    public ApplicationNavigation? Navigation
+    {
+        get
+        {
+            if (_navigation is null)
+            {
+                _navigation = new () { App = this };
+            }
+
+            return _navigation;
+        }
+        set => _navigation = value ?? throw new ArgumentNullException (nameof (value));
+    }
 
     /// <inheritdoc/>
     public Toplevel? Current { get; set; }

+ 7 - 2
Terminal.Gui/App/ApplicationNavigation.cs

@@ -16,6 +16,11 @@ public class ApplicationNavigation
         // TODO: Move navigation key bindings here from AddApplicationKeyBindings
     }
 
+    /// <summary>
+    ///     The <see cref="IApplication"/> instance used by this instance.
+    /// </summary>
+    public IApplication? App { get; set; }
+
     private View? _focused;
 
     /// <summary>
@@ -104,10 +109,10 @@ public class ApplicationNavigation
     /// </returns>
     public bool AdvanceFocus (NavigationDirection direction, TabBehavior? behavior)
     {
-        if (Application.Popover?.GetActivePopover () as View is { Visible: true } visiblePopover)
+        if (App?.Popover?.GetActivePopover () as View is { Visible: true } visiblePopover)
         {
             return visiblePopover.AdvanceFocus (direction, behavior);
         }
-        return Application.Current is { } && Application.Current.AdvanceFocus (direction, behavior);
+        return App?.Current is { } && App.Current.AdvanceFocus (direction, behavior);
     }
 }

+ 2 - 2
Terminal.Gui/App/Keyboard/IKeyboard.cs

@@ -3,7 +3,7 @@ namespace Terminal.Gui.App;
 /// <summary>
 ///     Defines a contract for managing keyboard input and key bindings at the Application level.
 ///     <para>
-///         This interface decouples keyboard handling state from the static <see cref="Application"/> class,
+///         This interface decouples keyboard handling state from the static <see cref="App"/> class,
 ///         enabling parallelizable unit tests and better testability.
 ///     </para>
 /// </summary>
@@ -13,7 +13,7 @@ public interface IKeyboard
     /// Sets the application instance that this keyboard handler is associated with.
     /// This provides access to application state without coupling to static Application class.
     /// </summary>
-    IApplication? Application { get; set; }
+    IApplication? App { get; set; }
 
     /// <summary>
     ///     Called when the user presses a key (by the <see cref="IDriver"/>). Raises the cancelable

+ 18 - 18
Terminal.Gui/App/Keyboard/KeyboardImpl.cs

@@ -3,7 +3,7 @@ namespace Terminal.Gui.App;
 /// <summary>
 ///     INTERNAL: Implements <see cref="IKeyboard"/> to manage keyboard input and key bindings at the Application level.
 ///     <para>
-///         This implementation decouples keyboard handling state from the static <see cref="Application"/> class,
+///         This implementation decouples keyboard handling state from the static <see cref="App"/> class,
 ///         enabling parallelizable unit tests and better testability.
 ///     </para>
 ///     <para>
@@ -25,7 +25,7 @@ internal class KeyboardImpl : IKeyboard
     private readonly Dictionary<Command, View.CommandImplementation> _commandImplementations = new ();
 
     /// <inheritdoc/>
-    public IApplication? Application { get; set; }
+    public IApplication? App { get; set; }
 
     /// <inheritdoc/>
     public KeyBindings KeyBindings { get; internal set; } = new (null);
@@ -133,16 +133,16 @@ internal class KeyboardImpl : IKeyboard
             return true;
         }
 
-        if (Application?.Popover?.DispatchKeyDown (key) is true)
+        if (App?.Popover?.DispatchKeyDown (key) is true)
         {
             return true;
         }
 
-        if (Application?.Current is null)
+        if (App?.Current is null)
         {
-            if (Application?.SessionStack is { })
+            if (App?.SessionStack is { })
             {
-                foreach (Toplevel topLevel in Application.SessionStack.ToList ())
+                foreach (Toplevel topLevel in App.SessionStack.ToList ())
                 {
                     if (topLevel.NewKeyDownEvent (key))
                     {
@@ -158,7 +158,7 @@ internal class KeyboardImpl : IKeyboard
         }
         else
         {
-            if (Application.Current.NewKeyDownEvent (key))
+            if (App.Current.NewKeyDownEvent (key))
             {
                 return true;
             }
@@ -176,7 +176,7 @@ internal class KeyboardImpl : IKeyboard
     /// <inheritdoc/>
     public bool RaiseKeyUpEvent (Key key)
     {
-        if (Application?.Initialized != true)
+        if (App?.Initialized != true)
         {
             return true;
         }
@@ -191,9 +191,9 @@ internal class KeyboardImpl : IKeyboard
 
         // TODO: Add Popover support
 
-        if (Application?.SessionStack is { })
+        if (App?.SessionStack is { })
         {
-            foreach (Toplevel topLevel in Application.SessionStack.ToList ())
+            foreach (Toplevel topLevel in App.SessionStack.ToList ())
             {
                 if (topLevel.NewKeyUpEvent (key))
                 {
@@ -291,7 +291,7 @@ internal class KeyboardImpl : IKeyboard
                     Command.Quit,
                     () =>
                     {
-                        Application?.RequestStop ();
+                        App?.RequestStop ();
 
                         return true;
                     }
@@ -300,32 +300,32 @@ internal class KeyboardImpl : IKeyboard
                     Command.Suspend,
                     () =>
                     {
-                        Application?.Driver?.Suspend ();
+                        App?.Driver?.Suspend ();
 
                         return true;
                     }
                    );
         AddCommand (
                     Command.NextTabStop,
-                    () => Application?.Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop));
+                    () => App?.Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop));
 
         AddCommand (
                     Command.PreviousTabStop,
-                    () => Application?.Navigation?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop));
+                    () => App?.Navigation?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop));
 
         AddCommand (
                     Command.NextTabGroup,
-                    () => Application?.Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup));
+                    () => App?.Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup));
 
         AddCommand (
                     Command.PreviousTabGroup,
-                    () => Application?.Navigation?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabGroup));
+                    () => App?.Navigation?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabGroup));
 
         AddCommand (
                     Command.Refresh,
                     () =>
                     {
-                        Application?.LayoutAndDraw (true);
+                        App?.LayoutAndDraw (true);
 
                         return true;
                     }
@@ -335,7 +335,7 @@ internal class KeyboardImpl : IKeyboard
                     Command.Arrange,
                     () =>
                     {
-                        View? viewToArrange = Application?.Navigation?.GetFocused ();
+                        View? viewToArrange = App?.Navigation?.GetFocused ();
 
                         // Go up the superview hierarchy and find the first that is not ViewArrangement.Fixed
                         while (viewToArrange is { SuperView: { }, Arrangement: ViewArrangement.Fixed })

+ 12 - 12
Terminal.Gui/ViewBase/View.Navigation.cs

@@ -395,7 +395,7 @@ public partial class View // Focus and cross-view navigation management (TabStop
     public event EventHandler<HasFocusEventArgs>? FocusedChanged;
 
     /// <summary>Returns a value indicating if this View is currently on Top (Active)</summary>
-    public bool IsCurrentTop => Application.Current == this;
+    public bool IsCurrentTop => App?.Current == this;
 
     /// <summary>
     ///     Returns the most focused SubView down the subview-hierarchy.
@@ -519,7 +519,7 @@ public partial class View // Focus and cross-view navigation management (TabStop
             if (value)
             {
                 // NOTE: If Application.Navigation is null, we pass null to FocusChanging. For unit tests.
-                (bool focusSet, bool _) = SetHasFocusTrue (Application.Navigation?.GetFocused ());
+                (bool focusSet, bool _) = SetHasFocusTrue (App?.Navigation?.GetFocused ());
 
                 if (focusSet)
                 {
@@ -556,7 +556,7 @@ public partial class View // Focus and cross-view navigation management (TabStop
     /// <returns><see langword="true"/> if the focus changed; <see langword="true"/> false otherwise.</returns>
     public bool SetFocus ()
     {
-        (bool focusSet, bool _) = SetHasFocusTrue (Application.Navigation?.GetFocused ());
+        (bool focusSet, bool _) = SetHasFocusTrue (App?.Navigation?.GetFocused ());
 
         return focusSet;
     }
@@ -721,17 +721,17 @@ public partial class View // Focus and cross-view navigation management (TabStop
             return true;
         }
 
-        View? appFocused = Application.Navigation?.GetFocused ();
+        View? appFocused = App?.Navigation?.GetFocused ();
 
         if (appFocused == currentFocused)
         {
             if (newFocused is { HasFocus: true })
             {
-                Application.Navigation?.SetFocused (newFocused);
+                App?.Navigation?.SetFocused (newFocused);
             }
             else
             {
-                Application.Navigation?.SetFocused (null);
+                App?.Navigation?.SetFocused (null);
             }
         }
 
@@ -834,7 +834,7 @@ public partial class View // Focus and cross-view navigation management (TabStop
             }
 
             // Application.Navigation.GetFocused?
-            View? applicationFocused = Application.Navigation?.GetFocused ();
+            View? applicationFocused = App?.Navigation?.GetFocused ();
 
             if (newFocusedView is null && applicationFocused != this && applicationFocused is { CanFocus: true })
             {
@@ -854,17 +854,17 @@ public partial class View // Focus and cross-view navigation management (TabStop
             }
 
             // Application.Current?
-            if (newFocusedView is null && Application.Current is { CanFocus: true, HasFocus: false })
+            if (newFocusedView is null && App?.Current is { CanFocus: true, HasFocus: false })
             {
                 // Temporarily ensure this view can't get focus
                 bool prevCanFocus = _canFocus;
                 _canFocus = false;
-                bool restoredFocus = Application.Current.RestoreFocus ();
+                bool restoredFocus = App?.Current.RestoreFocus () ?? false;
                 _canFocus = prevCanFocus;
 
-                if (Application.Current is { CanFocus: true, HasFocus: true })
+                if (App?.Current is { CanFocus: true, HasFocus: true })
                 {
-                    newFocusedView = Application.Current;
+                    newFocusedView = App?.Current;
                 }
                 else if (restoredFocus)
                 {
@@ -951,7 +951,7 @@ public partial class View // Focus and cross-view navigation management (TabStop
         // If we are the most focused view, we need to set the focused view in Application.Navigation
         if (newHasFocus && focusedView?.Focused is null)
         {
-            Application.Navigation?.SetFocused (focusedView);
+            App?.Navigation?.SetFocused (focusedView);
         }
 
         // Call the virtual method

+ 24 - 30
Tests/UnitTests/Application/Application.NavigationTests.cs

@@ -74,8 +74,6 @@ public class ApplicationNavigationTests (ITestOutputHelper output)
 
         Application.Navigation.FocusedChanged -= ApplicationNavigationOnFocusedChanged;
 
-        Application.Navigation = null;
-
         return;
 
         void ApplicationNavigationOnFocusedChanged (object sender, EventArgs e) { raised = true; }
@@ -84,12 +82,13 @@ public class ApplicationNavigationTests (ITestOutputHelper output)
     [Fact]
     public void GetFocused_Returns_Focused_View ()
     {
-        Application.Navigation = new ();
+        IApplication app = new ApplicationImpl ();
 
-        Application.Current = new ()
+        app.Current = new ()
         {
             Id = "top",
-            CanFocus = true
+            CanFocus = true,
+            App = app
         };
 
         var subView1 = new View
@@ -103,30 +102,28 @@ public class ApplicationNavigationTests (ITestOutputHelper output)
             Id = "subView2",
             CanFocus = true
         };
-        Application.Current.Add (subView1, subView2);
-        Assert.False (Application.Current.HasFocus);
 
-        Application.Current.SetFocus ();
-        Assert.True (subView1.HasFocus);
-        Assert.Equal (subView1, Application.Navigation.GetFocused ());
+        app.Current?.Add (subView1, subView2);
+        Assert.False (app.Current?.HasFocus);
 
-        Application.Navigation.AdvanceFocus (NavigationDirection.Forward, null);
-        Assert.Equal (subView2, Application.Navigation.GetFocused ());
+        app.Current?.SetFocus ();
+        Assert.True (subView1.HasFocus);
+        Assert.Equal (subView1, app.Navigation?.GetFocused ());
 
-        Application.Current.Dispose ();
-        Application.Current = null;
-        Application.Navigation = null;
+        app.Navigation?.AdvanceFocus (NavigationDirection.Forward, null);
+        Assert.Equal (subView2, app.Navigation?.GetFocused ());
     }
 
     [Fact]
     public void GetFocused_Returns_Null_If_No_Focused_View ()
     {
-        Application.Navigation = new ();
+        IApplication app = new ApplicationImpl ();
 
-        Application.Current = new ()
+        app.Current = new ()
         {
             Id = "top",
-            CanFocus = true
+            CanFocus = true,
+            App = app
         };
 
         var subView1 = new View
@@ -135,24 +132,21 @@ public class ApplicationNavigationTests (ITestOutputHelper output)
             CanFocus = true
         };
 
-        Application.Current.Add (subView1);
-        Assert.False (Application.Current.HasFocus);
+        app!.Current.Add (subView1);
+        Assert.False (app.Current.HasFocus);
 
-        Application.Current.SetFocus ();
+        app.Current.SetFocus ();
         Assert.True (subView1.HasFocus);
-        Assert.Equal (subView1, Application.Navigation.GetFocused ());
+        Assert.Equal (subView1, app.Navigation!.GetFocused ());
 
         subView1.HasFocus = false;
         Assert.False (subView1.HasFocus);
-        Assert.True (Application.Current.HasFocus);
-        Assert.Equal (Application.Current, Application.Navigation.GetFocused ());
+        Assert.True (app.Current.HasFocus);
+        Assert.Equal (app.Current, app.Navigation.GetFocused ());
 
-        Application.Current.HasFocus = false;
-        Assert.False (Application.Current.HasFocus);
-        Assert.Null (Application.Navigation.GetFocused ());
+        app.Current.HasFocus = false;
+        Assert.False (app.Current.HasFocus);
+        Assert.Null (app.Navigation.GetFocused ());
 
-        Application.Current.Dispose ();
-        Application.Current = null;
-        Application.Navigation = null;
     }
 }

+ 2 - 2
Tests/UnitTests/Application/ApplicationImplTests.cs

@@ -626,7 +626,7 @@ public class ApplicationImplTests
         Assert.Null (v2.Driver);
         Assert.False (v2.Initialized);
         Assert.Null (v2.Popover);
-        Assert.Null (v2.Navigation);
+        //Assert.Null (v2.Navigation);
         Assert.Null (v2.Current);
         Assert.Empty (v2.SessionStack);
 
@@ -654,7 +654,7 @@ public class ApplicationImplTests
         Assert.Null (v2.Driver);
         Assert.False (v2.Initialized);
         Assert.Null (v2.Popover);
-        Assert.Null (v2.Navigation);
+        //Assert.Null (v2.Navigation);
         Assert.Null (v2.Current);
         Assert.Empty (v2.SessionStack);
 

+ 1 - 1
Tests/UnitTests/Application/ApplicationTests.cs

@@ -358,7 +358,7 @@ public class ApplicationTests
             //Assert.Null (Application._lastMousePosition);
 
             // Navigation
-            Assert.Null (Application.Navigation);
+           // Assert.Null (Application.Navigation);
 
             // Popover
             Assert.Null (Application.Popover);

+ 1 - 1
Tests/UnitTests/Application/CursorTests.cs

@@ -121,12 +121,12 @@ public class CursorTests
     [AutoInitShutdown]
     public void PositionCursor_Focused_With_Position_Returns_True ()
     {
-        Application.Navigation = new ();
         TestView view = new ()
         {
             CanFocus = false,
             Width = 1,
             Height = 1,
+            App = ApplicationImpl.Instance
         };
         view.CanFocus = true;
         view.SetFocus ();

+ 11 - 10
Tests/UnitTests/View/Navigation/CanFocusTests.cs

@@ -88,20 +88,21 @@ public class CanFocusTests
     [Fact]
     public void CanFocus_Set_True_Get_AdvanceFocus_Works ()
     {
+        IApplication app = new ApplicationImpl ();
+        app.Current = new () { App = app };
+
         Label label = new () { Text = "label" };
         View view = new () { Text = "view", CanFocus = true };
-        Application.Navigation = new ();
-        Application.Current = new ();
-        Application.Current.Add (label, view);
+        app.Current.Add (label, view);
 
-        Application.Current.SetFocus ();
-        Assert.Equal (view, Application.Navigation.GetFocused ());
+        app.Current.SetFocus ();
+        Assert.Equal (view, app.Navigation!.GetFocused ());
         Assert.False (label.CanFocus);
         Assert.False (label.HasFocus);
         Assert.True (view.CanFocus);
         Assert.True (view.HasFocus);
 
-        Assert.False (Application.Navigation.AdvanceFocus (NavigationDirection.Forward, null));
+        Assert.False (app.Navigation.AdvanceFocus (NavigationDirection.Forward, null));
         Assert.False (label.HasFocus);
         Assert.True (view.HasFocus);
 
@@ -111,7 +112,7 @@ public class CanFocusTests
         Assert.True (view.HasFocus);
 
         // label can now be focused, so AdvanceFocus should move to it.
-        Assert.True (Application.Navigation.AdvanceFocus (NavigationDirection.Forward, null));
+        Assert.True (app.Navigation.AdvanceFocus (NavigationDirection.Forward, null));
         Assert.True (label.HasFocus);
         Assert.False (view.HasFocus);
 
@@ -120,11 +121,11 @@ public class CanFocusTests
         Assert.False (label.HasFocus);
         Assert.True (view.HasFocus);
 
-        Assert.True (Application.RaiseKeyDownEvent (Key.Tab));
+        Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.Tab));
         Assert.True (label.HasFocus);
         Assert.False (view.HasFocus);
 
-        Application.Current.Dispose ();
-        Application.ResetState ();
+        app.Current.Dispose ();
+        app.ResetState ();
     }
 }

+ 1 - 1
Tests/UnitTestsParallelizable/TestSetup.cs

@@ -67,7 +67,7 @@ public class GlobalTestSetup : IDisposable
         //Assert.Null (Application._lastMousePosition);
 
         // Navigation
-        Assert.Null (Application.Navigation);
+        // Assert.Null (Application.Navigation);
 
         // Popover
         Assert.Null (Application.Popover);