浏览代码

Move static members to IApplication/ApplicationImpl

- Moved InitializedChanged event to IApplication and ApplicationImpl instance
- Moved _cachedRunStateToplevel to ApplicationImpl instance field
- Moved MaximumIterationsPerSecond to IApplication property
- Moved SupportedCultures to IApplication property
- Added ResetState method to IApplication interface
- Updated Application.ResetState to delegate to ApplicationImpl.Instance.ResetState
- Added helper methods for clearing static events

Co-authored-by: tig <[email protected]>
copilot-swe-agent[bot] 1 月之前
父节点
当前提交
e03c31cd02

+ 21 - 8
Terminal.Gui/App/Application.Lifecycle.cs

@@ -156,7 +156,12 @@ public static partial class Application // Lifecycle (Init/Shutdown)
 
         MainThreadId = Thread.CurrentThread.ManagedThreadId;
         bool init = Initialized = true;
-        InitializedChanged?.Invoke (null, new (init));
+        
+        // Raise InitializedChanged event via the instance
+        if (ApplicationImpl.Instance is ApplicationImpl impl)
+        {
+            impl.RaiseInitializedChanged (init);
+        }
     }
 
     internal static void SubscribeDriverEvents ()
@@ -242,13 +247,21 @@ public static partial class Application // Lifecycle (Init/Shutdown)
     /// <remarks>
     ///     Intended to support unit tests that need to know when the application has been initialized.
     /// </remarks>
-    public static event EventHandler<EventArgs<bool>>? InitializedChanged;
-
-    /// <summary>
-    ///  Raises the <see cref="InitializedChanged"/> event.
-    /// </summary>
-    internal static void OnInitializedChanged (object sender, EventArgs<bool> e)
+    public static event EventHandler<EventArgs<bool>>? InitializedChanged
     {
-        Application.InitializedChanged?.Invoke (sender, e);
+        add
+        {
+            if (ApplicationImpl.Instance is ApplicationImpl impl)
+            {
+                impl.InitializedChanged += value;
+            }
+        }
+        remove
+        {
+            if (ApplicationImpl.Instance is ApplicationImpl impl)
+            {
+                impl.InitializedChanged -= value;
+            }
+        }
     }
 }

+ 15 - 9
Terminal.Gui/App/Application.Run.cs

@@ -22,10 +22,6 @@ public static partial class Application // Run (Begin -> Run -> Layout/Draw -> E
         set => Keyboard.ArrangeKey = value;
     }
 
-    // When `End ()` is called, it is possible `RunState.Toplevel` is a different object than `Top`.
-    // This variable is set in `End` in this case so that `Begin` correctly sets `Top`.
-    private static Toplevel? _cachedRunStateToplevel;
-
     /// <summary>
     ///     Notify that a new <see cref="RunState"/> was created (<see cref="Begin(Toplevel)"/> was called). The token is
     ///     created in <see cref="Begin(Toplevel)"/> and this event will be fired before that function exits.
@@ -45,6 +41,13 @@ public static partial class Application // Run (Begin -> Run -> Layout/Draw -> E
     /// </remarks>
     public static event EventHandler<ToplevelEventArgs>? NotifyStopRunState;
 
+    // Internal helper methods for ApplicationImpl.ResetState to clear these events
+    internal static void ClearRunStateEvents ()
+    {
+        NotifyNewRunState = null;
+        NotifyStopRunState = null;
+    }
+
     /// <summary>Building block API: Prepares the provided <see cref="Toplevel"/> for execution.</summary>
     /// <returns>
     ///     The <see cref="RunState"/> handle that needs to be passed to the <see cref="End(RunState)"/> method upon
@@ -70,21 +73,21 @@ public static partial class Application // Run (Begin -> Run -> Layout/Draw -> E
         var rs = new RunState (toplevel);
 
 #if DEBUG_IDISPOSABLE
-        if (View.EnableDebugIDisposableAsserts && Top is { } && toplevel != Top && !TopLevels.Contains (Top))
+        if (View.EnableDebugIDisposableAsserts && Top is { } && toplevel != Top && !TopLevels.Contains (Top) && ApplicationImpl.Instance is ApplicationImpl appImpl)
         {
             // This assertion confirm if the Top was already disposed
             Debug.Assert (Top.WasDisposed);
-            Debug.Assert (Top == _cachedRunStateToplevel);
+            Debug.Assert (Top == appImpl._cachedRunStateToplevel);
         }
 #endif
 
         lock (TopLevels)
         {
-            if (Top is { } && toplevel != Top && !TopLevels.Contains (Top))
+            if (Top is { } && toplevel != Top && !TopLevels.Contains (Top) && ApplicationImpl.Instance is ApplicationImpl impl)
             {
                 // If Top was already disposed and isn't on the Toplevels Stack,
                 // clean it up here if is the same as _cachedRunStateToplevel
-                if (Top == _cachedRunStateToplevel)
+                if (Top == impl._cachedRunStateToplevel)
                 {
                     Top = null;
                 }
@@ -493,7 +496,10 @@ public static partial class Application // Run (Begin -> Run -> Layout/Draw -> E
             Top.SetFocus ();
         }
 
-        _cachedRunStateToplevel = runState.Toplevel;
+        if (ApplicationImpl.Instance is ApplicationImpl impl)
+        {
+            impl._cachedRunStateToplevel = runState.Toplevel;
+        }
 
         runState.Toplevel = null;
         runState.Dispose ();

+ 6 - 0
Terminal.Gui/App/Application.Screen.cs

@@ -25,6 +25,12 @@ public static partial class Application // Screen related stuff; intended to hid
     /// </remarks>
     public static event EventHandler<SizeChangedEventArgs>? SizeChanging;
 
+    // Internal helper method for ApplicationImpl.ResetState to clear this event
+    internal static void ClearSizeChangingEvent ()
+    {
+        SizeChanging = null;
+    }
+
     /// <summary>
     ///     Called when the application's size changes. Sets the size of all <see cref="Toplevel"/>s and fires the
     ///     <see cref="SizeChanging"/> event.

+ 7 - 86
Terminal.Gui/App/Application.cs

@@ -40,7 +40,7 @@ namespace Terminal.Gui.App;
 public static partial class Application
 {
     /// <summary>Gets all cultures supported by the application without the invariant language.</summary>
-    public static List<CultureInfo>? SupportedCultures { get; private set; } = GetSupportedCultures ();
+    public static List<CultureInfo>? SupportedCultures => ApplicationImpl.Instance.SupportedCultures;
 
 
     /// <summary>
@@ -58,7 +58,11 @@ public static partial class Application
     /// <remarks>Note that not every iteration draws (see <see cref="View.NeedsDraw"/>).
     /// Only affects v2 drivers.</remarks>
     /// </summary>
-    public static ushort MaximumIterationsPerSecond = DefaultMaximumIterationsPerSecond;
+    public static ushort MaximumIterationsPerSecond
+    {
+        get => ApplicationImpl.Instance.MaximumIterationsPerSecond;
+        set => ApplicationImpl.Instance.MaximumIterationsPerSecond = value;
+    }
 
     /// <summary>
     /// Default value for <see cref="MaximumIterationsPerSecond"/>
@@ -179,92 +183,9 @@ public static partial class Application
     // starts running and after Shutdown returns.
     internal static void ResetState (bool ignoreDisposed = false)
     {
-        // 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.
-        // e.g. see Issue #537
-        foreach (Toplevel? t in TopLevels)
-        {
-            t!.Running = false;
-        }
-
-        if (Popover?.GetActivePopover () is View popover)
-        {
-            // This forcefully closes the popover; invoking Command.Quit would be more graceful
-            // but since this is shutdown, doing this is ok.
-            popover.Visible = false;
-        }
-
-        Popover?.Dispose ();
-        Popover = null;
-
-        TopLevels.Clear ();
-#if DEBUG_IDISPOSABLE
-
-        // Don't dispose the Top. It's up to caller dispose it
-        if (View.EnableDebugIDisposableAsserts && !ignoreDisposed && Top is { })
-        {
-            Debug.Assert (Top.WasDisposed, $"Title = {Top.Title}, Id = {Top.Id}");
-
-            // If End wasn't called _cachedRunStateToplevel may be null
-            if (_cachedRunStateToplevel is { })
-            {
-                Debug.Assert (_cachedRunStateToplevel.WasDisposed);
-                Debug.Assert (_cachedRunStateToplevel == Top);
-            }
-        }
-#endif
-        Top = null;
-        _cachedRunStateToplevel = null;
-
-        MainThreadId = -1;
-        Iteration = null;
-        EndAfterFirstIteration = false;
-        ClearScreenNextIteration = false;
-
-        // Driver stuff
-        if (Driver is { })
-        {
-            UnsubscribeDriverEvents ();
-            Driver?.End ();
-            Driver = null;
-        }
-
-        // Reset Screen to null so it will be recalculated on next access
-        // Note: ApplicationImpl.Shutdown() also calls ResetScreen() before calling this method
-        // to avoid potential circular reference issues. Calling it twice is harmless.
         if (ApplicationImpl.Instance is ApplicationImpl impl)
         {
-            impl.ResetScreen ();
+            impl.ResetState (ignoreDisposed);
         }
-
-        // Don't reset ForceDriver; it needs to be set before Init is called.
-        //ForceDriver = string.Empty;
-        //Force16Colors = false;
-        _forceFakeConsole = false;
-
-        // Run State stuff
-        NotifyNewRunState = null;
-        NotifyStopRunState = null;
-        // Mouse and Keyboard will be lazy-initialized in ApplicationImpl on next access
-        Initialized = false;
-
-        // Mouse
-        // Do not clear _lastMousePosition; Popovers require it to stay set with
-        // last mouse pos.
-        //_lastMousePosition = null;
-        CachedViewsUnderMouse.Clear ();
-        ResetMouseState ();
-
-        // Keyboard events and bindings are now managed by the Keyboard instance
-
-        SizeChanging = null;
-
-        Navigation = null;
-
-        // Reset synchronization context to allow the user to run async/await,
-        // as the main loop has been ended, the synchronization context from
-        // gui.cs does no longer process any callbacks. See #1084 for more details:
-        // (https://github.com/gui-cs/Terminal.Gui/issues/1084).
-        SynchronizationContext.SetSynchronizationContext (null);
     }
 }

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

@@ -2,6 +2,7 @@
 using System.Collections.Concurrent;
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
 using Microsoft.Extensions.Logging;
 using Terminal.Gui.Drivers;
 
@@ -30,6 +31,12 @@ public class ApplicationImpl : IApplication
     private readonly object _lockScreen = new ();
     private Rectangle? _screen;
     private bool _clearScreenNextIteration;
+    private ushort _maximumIterationsPerSecond = Application.DefaultMaximumIterationsPerSecond;
+    private List<CultureInfo>? _supportedCultures;
+    
+    // When `End ()` is called, it is possible `RunState.Toplevel` is a different object than `Top`.
+    // This variable is set in `End` in this case so that `Begin` correctly sets `Top`.
+    internal Toplevel? _cachedRunStateToplevel;
 
     // Private static readonly Lazy instance of Application
     private static Lazy<IApplication> _lazyInstance = new (() => new ApplicationImpl ());
@@ -177,6 +184,37 @@ public class ApplicationImpl : IApplication
     /// <inheritdoc/>
     public ConcurrentStack<Toplevel> TopLevels => _topLevels;
 
+    /// <inheritdoc/>
+    public ushort MaximumIterationsPerSecond
+    {
+        get => _maximumIterationsPerSecond;
+        set => _maximumIterationsPerSecond = value;
+    }
+
+    /// <inheritdoc/>
+    public List<CultureInfo>? SupportedCultures
+    {
+        get
+        {
+            if (_supportedCultures is null)
+            {
+                _supportedCultures = Application.GetSupportedCultures ();
+            }
+            return _supportedCultures;
+        }
+    }
+
+    /// <inheritdoc/>
+    public event EventHandler<EventArgs<bool>>? InitializedChanged;
+
+    /// <summary>
+    /// Internal helper to raise InitializedChanged event. Used by legacy Init path.
+    /// </summary>
+    internal void RaiseInitializedChanged (bool initialized)
+    {
+        InitializedChanged?.Invoke (null, new (initialized));
+    }
+
     /// <summary>
     /// Gets or sets the main thread ID for the application.
     /// </summary>
@@ -267,7 +305,7 @@ public class ApplicationImpl : IApplication
 
         _initialized = true;
 
-        Application.OnInitializedChanged (this, new (true));
+        InitializedChanged?.Invoke (this, new (true));
         Application.SubscribeDriverEvents ();
 
         SynchronizationContext.SetSynchronizationContext (new ());
@@ -440,12 +478,12 @@ public class ApplicationImpl : IApplication
         
         bool wasInitialized = _initialized;
         
-        // Reset Screen before calling Application.ResetState to avoid circular reference
+        // Reset Screen before calling ResetState to avoid circular reference
         ResetScreen ();
         
         // Call ResetState FIRST so it can properly dispose Popover and other resources
         // that are accessed via Application.* static properties that now delegate to instance fields
-        Application.ResetState ();
+        ResetState ();
         ConfigurationManager.PrintJsonErrors ();
         
         // Clear instance fields after ResetState has disposed everything
@@ -466,7 +504,7 @@ public class ApplicationImpl : IApplication
         if (wasInitialized)
         {
             bool init = _initialized; // Will be false after clearing fields above
-            Application.OnInitializedChanged (this, new (in init));
+            InitializedChanged?.Invoke (this, new (in init));
         }
 
         _lazyInstance = new (() => new ApplicationImpl ());
@@ -554,6 +592,85 @@ public class ApplicationImpl : IApplication
         _driver?.Refresh ();
     }
 
+    /// <inheritdoc />
+    public void ResetState (bool ignoreDisposed = false)
+    {
+        // 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.
+        // e.g. see Issue #537
+        foreach (Toplevel? t in _topLevels)
+        {
+            t!.Running = false;
+        }
+
+        if (_popover?.GetActivePopover () is View popover)
+        {
+            // This forcefully closes the popover; invoking Command.Quit would be more graceful
+            // but since this is shutdown, doing this is ok.
+            popover.Visible = false;
+        }
+
+        _popover?.Dispose ();
+        _popover = null;
+
+        _topLevels.Clear ();
+#if DEBUG_IDISPOSABLE
+
+        // Don't dispose the Top. It's up to caller dispose it
+        if (View.EnableDebugIDisposableAsserts && !ignoreDisposed && _top is { })
+        {
+            Debug.Assert (_top.WasDisposed, $"Title = {_top.Title}, Id = {_top.Id}");
+
+            // If End wasn't called _cachedRunStateToplevel may be null
+            if (_cachedRunStateToplevel is { })
+            {
+                Debug.Assert (_cachedRunStateToplevel.WasDisposed);
+                Debug.Assert (_cachedRunStateToplevel == _top);
+            }
+        }
+#endif
+        _top = null;
+        _cachedRunStateToplevel = null;
+
+        _mainThreadId = -1;
+
+        // Driver stuff
+        if (_driver is { })
+        {
+            Application.UnsubscribeDriverEvents ();
+            _driver?.End ();
+            _driver = null;
+        }
+
+        // Reset Screen to null so it will be recalculated on next access
+        ResetScreen ();
+
+        // Run State stuff - these are static events on Application class
+        Application.ClearRunStateEvents ();
+
+        // Mouse and Keyboard will be lazy-initialized in ApplicationImpl on next access
+        _initialized = false;
+
+        // Mouse
+        // Do not clear _lastMousePosition; Popovers require it to stay set with
+        // last mouse pos.
+        //_lastMousePosition = null;
+        Application.CachedViewsUnderMouse.Clear ();
+        Application.ResetMouseState ();
+
+        // Keyboard events and bindings are now managed by the Keyboard instance
+
+        Application.ClearSizeChangingEvent ();
+
+        _navigation = null;
+
+        // Reset synchronization context to allow the user to run async/await,
+        // as the main loop has been ended, the synchronization context from
+        // gui.cs does no longer process any callbacks. See #1084 for more details:
+        // (https://github.com/gui-cs/Terminal.Gui/issues/1084).
+        SynchronizationContext.SetSynchronizationContext (null);
+    }
+
     /// <summary>
     /// Resets the Screen field to null so it will be recalculated on next access.
     /// </summary>

+ 27 - 0
Terminal.Gui/App/IApplication.cs

@@ -1,5 +1,6 @@
 #nullable enable
 using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
 
 namespace Terminal.Gui.App;
 
@@ -257,4 +258,30 @@ public interface IApplication
     ///     safe updates to <see cref="View"/> instances.
     /// </summary>
     ITimedEvents? TimedEvents { get; }
+
+    /// <summary>
+    /// Maximum number of iterations of the main loop (and hence draws)
+    /// to allow to occur per second. Defaults to <see cref="Application.DefaultMaximumIterationsPerSecond"/> which is a 40ms sleep
+    /// after iteration (factoring in how long iteration took to run).
+    /// <remarks>Note that not every iteration draws (see <see cref="View.NeedsDraw"/>).
+    /// Only affects v2 drivers.</remarks>
+    /// </summary>
+    ushort MaximumIterationsPerSecond { get; set; }
+
+    /// <summary>Gets all cultures supported by the application without the invariant language.</summary>
+    List<CultureInfo>? SupportedCultures { get; }
+
+    /// <summary>
+    ///     This event is raised after the <see cref="Init"/> and <see cref="Shutdown"/> methods have been called.
+    /// </summary>
+    /// <remarks>
+    ///     Intended to support unit tests that need to know when the application has been initialized.
+    /// </remarks>
+    event EventHandler<EventArgs<bool>>? InitializedChanged;
+
+    /// <summary>
+    ///     Resets the application state to defaults. This is called by <see cref="Shutdown"/>.
+    /// </summary>
+    /// <param name="ignoreDisposed">If true, will not assert that views are disposed.</param>
+    void ResetState (bool ignoreDisposed = false);
 }