mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 07:47:54 +01:00
* Initial plan * Add instance fields to ApplicationImpl and update static Application properties to delegate to ApplicationImpl.Instance Co-authored-by: tig <585482+tig@users.noreply.github.com> * Fix code review issues: use instance fields in Run() and improve Shutdown() logic Co-authored-by: tig <585482+tig@users.noreply.github.com> * Final code review fix: use _initialized directly in Shutdown Co-authored-by: tig <585482+tig@users.noreply.github.com> * Fix Shutdown order: call ResetState before clearing instance fields Co-authored-by: tig <585482+tig@users.noreply.github.com> * Add MainThreadId as instance field in ApplicationImpl Co-authored-by: tig <585482+tig@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: tig <585482+tig@users.noreply.github.com> Co-authored-by: Tig <tig@users.noreply.github.com>
This commit is contained in:
@@ -8,7 +8,11 @@ public static partial class Application // Driver abstractions
|
||||
|
||||
// TODO: Add to IApplication
|
||||
/// <summary>Gets the <see cref="IConsoleDriver"/> that has been selected. See also <see cref="ForceDriver"/>.</summary>
|
||||
public static IConsoleDriver? Driver { get; internal set; }
|
||||
public static IConsoleDriver? Driver
|
||||
{
|
||||
get => ApplicationImpl.Instance.Driver;
|
||||
internal set => ApplicationImpl.Instance.Driver = value;
|
||||
}
|
||||
|
||||
// TODO: Add to IApplication
|
||||
// BUGBUG: Force16Colors should be nullable.
|
||||
|
||||
@@ -5,42 +5,10 @@ using System.Reflection;
|
||||
|
||||
namespace Terminal.Gui.App;
|
||||
|
||||
public static partial class Application // Lifecycle (Init/Shutdown)
|
||||
public static partial class Application // Initialization (Init/Shutdown)
|
||||
{
|
||||
// TODO: Add to IApplication
|
||||
/// <summary>Gets of list of <see cref="IConsoleDriver"/> types and type names that are available.</summary>
|
||||
/// <returns></returns>
|
||||
[RequiresUnreferencedCode ("AOT")]
|
||||
public static (List<Type?>, List<string?>) GetDriverTypes ()
|
||||
{
|
||||
// use reflection to get the list of drivers
|
||||
List<Type?> driverTypes = new ();
|
||||
|
||||
// Only inspect the IConsoleDriver assembly
|
||||
Assembly asm = typeof (IConsoleDriver).Assembly;
|
||||
|
||||
foreach (Type? type in asm.GetTypes ())
|
||||
{
|
||||
if (typeof (IConsoleDriver).IsAssignableFrom (type) && type is { IsAbstract: false, IsClass: true })
|
||||
{
|
||||
driverTypes.Add (type);
|
||||
}
|
||||
}
|
||||
|
||||
List<string?> driverTypeNames = driverTypes
|
||||
.Where (d => !typeof (IConsoleDriverFacade).IsAssignableFrom (d))
|
||||
.Select (d => d!.Name)
|
||||
.Union (["dotnet", "windows", "unix", "fake"])
|
||||
.ToList ()!;
|
||||
|
||||
return (driverTypes, driverTypeNames);
|
||||
}
|
||||
|
||||
// TODO: Add to IApplicationLifecycle
|
||||
/// <summary>
|
||||
/// Initializes a new instance of a Terminal.Gui Application. <see cref="Shutdown"/> must be called when the
|
||||
/// application is closing.
|
||||
/// </summary>
|
||||
/// <summary>Initializes a new instance of a Terminal.Gui Application. <see cref="Shutdown"/> must be called when the application is closing.</summary>
|
||||
/// <para>Call this method once per instance (or after <see cref="Shutdown"/> has been called).</para>
|
||||
/// <para>
|
||||
/// This function loads the right <see cref="IConsoleDriver"/> for the platform, Creates a <see cref="Toplevel"/>. and
|
||||
@@ -76,60 +44,31 @@ public static partial class Application // Lifecycle (Init/Shutdown)
|
||||
// that isn't supported by the modern application architecture
|
||||
if (driver is null)
|
||||
{
|
||||
string driverNameToCheck = string.IsNullOrWhiteSpace (driverName) ? ForceDriver : driverName;
|
||||
|
||||
var driverNameToCheck = string.IsNullOrWhiteSpace (driverName) ? ForceDriver : driverName;
|
||||
if (!string.IsNullOrEmpty (driverNameToCheck))
|
||||
{
|
||||
(List<Type?> drivers, List<string?> driverTypeNames) = GetDriverTypes ();
|
||||
Type? driverType = drivers.FirstOrDefault (t => t!.Name.Equals (driverNameToCheck, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
|
||||
// If it's a legacy IConsoleDriver (not a Facade), use InternalInit which supports legacy drivers
|
||||
if (driverType is { } && !typeof (IConsoleDriverFacade).IsAssignableFrom (driverType))
|
||||
{
|
||||
InternalInit (driver, driverName);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Otherwise delegate to the ApplicationImpl instance (which uses the modern architecture)
|
||||
ApplicationImpl.Instance.Init (driver, driverName ?? ForceDriver);
|
||||
}
|
||||
|
||||
// TODO: Add to IApplicationLifecycle
|
||||
/// <summary>
|
||||
/// Gets whether the application has been initialized with <see cref="Init"/> and not yet shutdown with
|
||||
/// <see cref="Shutdown"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The <see cref="InitializedChanged"/> event is raised after the <see cref="Init"/> and <see cref="Shutdown"/>
|
||||
/// methods have been called.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static bool Initialized { get; internal set; }
|
||||
internal static int MainThreadId
|
||||
{
|
||||
get => ((ApplicationImpl)ApplicationImpl.Instance).MainThreadId;
|
||||
set => ((ApplicationImpl)ApplicationImpl.Instance).MainThreadId = value;
|
||||
}
|
||||
|
||||
// TODO: Add to IApplicationLifecycle
|
||||
/// <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>
|
||||
public static event EventHandler<EventArgs<bool>>? InitializedChanged;
|
||||
|
||||
// TODO: Add to IApplicationLifecycle
|
||||
/// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
|
||||
/// <remarks>
|
||||
/// Shutdown must be called for every call to <see cref="Init"/> or
|
||||
/// <see cref="Application.Run(Toplevel, Func{Exception, bool})"/> to ensure all resources are cleaned
|
||||
/// up (Disposed)
|
||||
/// and terminal settings are restored.
|
||||
/// </remarks>
|
||||
public static void Shutdown () { ApplicationImpl.Instance.Shutdown (); }
|
||||
|
||||
// TODO: Add to IApplicationLifecycle
|
||||
// INTERNAL function for initializing an app with a Toplevel factory object, driver, and mainloop.
|
||||
//
|
||||
// Called from:
|
||||
@@ -160,7 +99,7 @@ public static partial class Application // Lifecycle (Init/Shutdown)
|
||||
if (!calledViaRunT)
|
||||
{
|
||||
// Reset all class variables (Application is a singleton).
|
||||
ResetState (true);
|
||||
ResetState (ignoreDisposed: true);
|
||||
}
|
||||
|
||||
// For UnitTests
|
||||
@@ -185,7 +124,7 @@ public static partial class Application // Lifecycle (Init/Shutdown)
|
||||
//{
|
||||
// (List<Type?> drivers, List<string?> driverTypeNames) = GetDriverTypes ();
|
||||
// Type? driverType = drivers.FirstOrDefault (t => t!.Name.Equals (ForceDriver, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
|
||||
// if (driverType is { } && !typeof (IConsoleDriverFacade).IsAssignableFrom (driverType))
|
||||
// {
|
||||
// // This is a legacy driver (not a ConsoleDriverFacade)
|
||||
@@ -193,13 +132,12 @@ public static partial class Application // Lifecycle (Init/Shutdown)
|
||||
// useLegacyDriver = true;
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
//// Use the modern application architecture
|
||||
//if (!useLegacyDriver)
|
||||
{
|
||||
ApplicationImpl.Instance.Init (driver, driverName);
|
||||
Debug.Assert (Driver is { });
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -240,14 +178,6 @@ public static partial class Application // Lifecycle (Init/Shutdown)
|
||||
InitializedChanged?.Invoke (null, new (init));
|
||||
}
|
||||
|
||||
internal static int MainThreadId { get; set; } = -1;
|
||||
|
||||
// TODO: Add to IApplicationLifecycle
|
||||
/// <summary>
|
||||
/// Raises the <see cref="InitializedChanged"/> event.
|
||||
/// </summary>
|
||||
internal static void OnInitializedChanged (object sender, EventArgs<bool> e) { InitializedChanged?.Invoke (sender, e); }
|
||||
|
||||
internal static void SubscribeDriverEvents ()
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull (Driver);
|
||||
@@ -268,9 +198,78 @@ public static partial class Application // Lifecycle (Init/Shutdown)
|
||||
Driver.MouseEvent -= Driver_MouseEvent;
|
||||
}
|
||||
|
||||
private static void Driver_SizeChanged (object? sender, SizeChangedEventArgs e) { OnSizeChanging (e); }
|
||||
private static void Driver_KeyDown (object? sender, Key e) { RaiseKeyDownEvent (e); }
|
||||
private static void Driver_KeyUp (object? sender, Key e) { RaiseKeyUpEvent (e); }
|
||||
private static void Driver_MouseEvent (object? sender, MouseEventArgs e) { RaiseMouseEvent (e); }
|
||||
|
||||
private static void Driver_SizeChanged (object? sender, SizeChangedEventArgs e) { OnSizeChanging (e); }
|
||||
/// <summary>Gets of list of <see cref="IConsoleDriver"/> types and type names that are available.</summary>
|
||||
/// <returns></returns>
|
||||
[RequiresUnreferencedCode ("AOT")]
|
||||
public static (List<Type?>, List<string?>) GetDriverTypes ()
|
||||
{
|
||||
// use reflection to get the list of drivers
|
||||
List<Type?> driverTypes = new ();
|
||||
|
||||
// Only inspect the IConsoleDriver assembly
|
||||
var asm = typeof (IConsoleDriver).Assembly;
|
||||
|
||||
foreach (Type? type in asm.GetTypes ())
|
||||
{
|
||||
if (typeof (IConsoleDriver).IsAssignableFrom (type) &&
|
||||
type is { IsAbstract: false, IsClass: true })
|
||||
{
|
||||
driverTypes.Add (type);
|
||||
}
|
||||
}
|
||||
|
||||
List<string?> driverTypeNames = driverTypes
|
||||
.Where (d => !typeof (IConsoleDriverFacade).IsAssignableFrom (d))
|
||||
.Select (d => d!.Name)
|
||||
.Union (["dotnet", "windows", "unix", "fake"])
|
||||
.ToList ()!;
|
||||
|
||||
|
||||
|
||||
return (driverTypes, driverTypeNames);
|
||||
}
|
||||
|
||||
/// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
|
||||
/// <remarks>
|
||||
/// Shutdown must be called for every call to <see cref="Init"/> or
|
||||
/// <see cref="Application.Run(Toplevel, Func{Exception, bool})"/> to ensure all resources are cleaned
|
||||
/// up (Disposed)
|
||||
/// and terminal settings are restored.
|
||||
/// </remarks>
|
||||
public static void Shutdown () => ApplicationImpl.Instance.Shutdown ();
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the application has been initialized with <see cref="Init"/> and not yet shutdown with <see cref="Shutdown"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The <see cref="InitializedChanged"/> event is raised after the <see cref="Init"/> and <see cref="Shutdown"/> methods have been called.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static bool Initialized
|
||||
{
|
||||
get => ApplicationImpl.Instance.Initialized;
|
||||
internal set => ApplicationImpl.Instance.Initialized = value;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public static event EventHandler<EventArgs<bool>>? InitializedChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="InitializedChanged"/> event.
|
||||
/// </summary>
|
||||
internal static void OnInitializedChanged (object sender, EventArgs<bool> e)
|
||||
{
|
||||
Application.InitializedChanged?.Invoke (sender, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,11 @@ public static partial class Application // Navigation stuff
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ApplicationNavigation"/> instance for the current <see cref="Application"/>.
|
||||
/// </summary>
|
||||
public static ApplicationNavigation? Navigation { get; internal set; }
|
||||
public static ApplicationNavigation? Navigation
|
||||
{
|
||||
get => ApplicationImpl.Instance.Navigation;
|
||||
internal set => ApplicationImpl.Instance.Navigation = value;
|
||||
}
|
||||
|
||||
/// <summary>Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.</summary>
|
||||
[ConfigurationProperty (Scope = typeof (SettingsScope))]
|
||||
|
||||
@@ -5,5 +5,9 @@ namespace Terminal.Gui.App;
|
||||
public static partial class Application // Popover handling
|
||||
{
|
||||
/// <summary>Gets the Application <see cref="Popover"/> manager.</summary>
|
||||
public static ApplicationPopover? Popover { get; internal set; }
|
||||
public static ApplicationPopover? Popover
|
||||
{
|
||||
get => ApplicationImpl.Instance.Popover;
|
||||
internal set => ApplicationImpl.Instance.Popover = value;
|
||||
}
|
||||
}
|
||||
@@ -7,22 +7,14 @@ public static partial class Application // Toplevel handling
|
||||
{
|
||||
// BUGBUG: Technically, this is not the full lst of TopLevels. There be dragons here, e.g. see how Toplevel.Id is used. What
|
||||
|
||||
private static readonly ConcurrentStack<Toplevel> _topLevels = new ();
|
||||
private static readonly object _topLevelsLock = new ();
|
||||
|
||||
/// <summary>Holds the stack of TopLevel views.</summary>
|
||||
internal static ConcurrentStack<Toplevel> TopLevels
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_topLevelsLock)
|
||||
{
|
||||
return _topLevels;
|
||||
}
|
||||
}
|
||||
}
|
||||
internal static ConcurrentStack<Toplevel> TopLevels => ApplicationImpl.Instance.TopLevels;
|
||||
|
||||
/// <summary>The <see cref="Toplevel"/> that is currently active.</summary>
|
||||
/// <value>The top.</value>
|
||||
public static Toplevel? Top { get; internal set; }
|
||||
public static Toplevel? Top
|
||||
{
|
||||
get => ApplicationImpl.Instance.Top;
|
||||
internal set => ApplicationImpl.Instance.Top = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,35 +3,42 @@ using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Terminal.Gui.Drivers;
|
||||
|
||||
namespace Terminal.Gui.App;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of core <see cref="Application"/> methods using the modern
|
||||
/// main loop architecture with component factories for different platforms.
|
||||
/// Implementation of core <see cref="Application"/> methods using the modern
|
||||
/// main loop architecture with component factories for different platforms.
|
||||
/// </summary>
|
||||
public class ApplicationImpl : IApplication
|
||||
{
|
||||
private readonly IComponentFactory? _componentFactory;
|
||||
private IMainLoopCoordinator? _coordinator;
|
||||
private string? _driverName;
|
||||
private readonly ITimedEvents _timedEvents = new TimedEvents ();
|
||||
private IConsoleDriver? _driver;
|
||||
private bool _initialized;
|
||||
private ApplicationPopover? _popover;
|
||||
private ApplicationNavigation? _navigation;
|
||||
private Toplevel? _top;
|
||||
private readonly ConcurrentStack<Toplevel> _topLevels = new ();
|
||||
private int _mainThreadId = -1;
|
||||
|
||||
// Private static readonly Lazy instance of Application
|
||||
private static Lazy<IApplication> _lazyInstance = new (() => new ApplicationImpl ());
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the Application backend.
|
||||
/// Gets the currently configured backend implementation of <see cref="Application"/> gateway methods.
|
||||
/// Change to your own implementation by using <see cref="ChangeInstance"/> (before init).
|
||||
/// </summary>
|
||||
public ApplicationImpl () { }
|
||||
|
||||
internal ApplicationImpl (IComponentFactory componentFactory)
|
||||
{
|
||||
_componentFactory = componentFactory;
|
||||
}
|
||||
|
||||
private readonly IComponentFactory? _componentFactory;
|
||||
private readonly ITimedEvents _timedEvents = new TimedEvents ();
|
||||
private string? _driverName;
|
||||
public static IApplication Instance => _lazyInstance.Value;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ITimedEvents? TimedEvents => _timedEvents;
|
||||
|
||||
internal IMainLoopCoordinator? Coordinator => _coordinator;
|
||||
|
||||
private IMouse? _mouse;
|
||||
|
||||
/// <summary>
|
||||
@@ -50,6 +57,11 @@ public class ApplicationImpl : IApplication
|
||||
set => _mouse = value ?? throw new ArgumentNullException (nameof (value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles which <see cref="View"/> (if any) has captured the mouse
|
||||
/// </summary>
|
||||
public IMouseGrabHandler MouseGrabHandler { get; set; } = new MouseGrabHandler ();
|
||||
|
||||
private IKeyboard? _keyboard;
|
||||
|
||||
/// <summary>
|
||||
@@ -71,74 +83,74 @@ public class ApplicationImpl : IApplication
|
||||
/// <inheritdoc/>
|
||||
public IConsoleDriver? Driver
|
||||
{
|
||||
get => Application.Driver;
|
||||
set => Application.Driver = value;
|
||||
get => _driver;
|
||||
set => _driver = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Initialized
|
||||
{
|
||||
get => Application.Initialized;
|
||||
set => Application.Initialized = value;
|
||||
get => _initialized;
|
||||
set => _initialized = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ApplicationPopover? Popover
|
||||
{
|
||||
get => Application.Popover;
|
||||
set => Application.Popover = value;
|
||||
get => _popover;
|
||||
set => _popover = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ApplicationNavigation? Navigation
|
||||
{
|
||||
get => Application.Navigation;
|
||||
set => Application.Navigation = value;
|
||||
get => _navigation;
|
||||
set => _navigation = value;
|
||||
}
|
||||
|
||||
// TODO: Create an IViewHierarchy that encapsulates Top and TopLevels and LayoutAndDraw
|
||||
/// <inheritdoc/>
|
||||
public Toplevel? Top
|
||||
{
|
||||
get => Application.Top;
|
||||
set => Application.Top = value;
|
||||
get => _top;
|
||||
set => _top = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ConcurrentStack<Toplevel> TopLevels => Application.TopLevels;
|
||||
public ConcurrentStack<Toplevel> TopLevels => _topLevels;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void LayoutAndDraw (bool forceRedraw = false)
|
||||
/// <summary>
|
||||
/// Gets or sets the main thread ID for the application.
|
||||
/// </summary>
|
||||
internal int MainThreadId
|
||||
{
|
||||
List<View> tops = [.. TopLevels];
|
||||
get => _mainThreadId;
|
||||
set => _mainThreadId = value;
|
||||
}
|
||||
|
||||
if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover)
|
||||
{
|
||||
visiblePopover.SetNeedsDraw ();
|
||||
visiblePopover.SetNeedsLayout ();
|
||||
tops.Insert (0, visiblePopover);
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public void RequestStop () => RequestStop (null);
|
||||
|
||||
// BUGBUG: Application.Screen needs to be moved to IApplication
|
||||
bool neededLayout = View.Layout (tops.ToArray ().Reverse (), Application.Screen.Size);
|
||||
/// <summary>
|
||||
/// Creates a new instance of the Application backend.
|
||||
/// </summary>
|
||||
public ApplicationImpl ()
|
||||
{
|
||||
}
|
||||
|
||||
// BUGBUG: Application.ClearScreenNextIteration needs to be moved to IApplication
|
||||
if (Application.ClearScreenNextIteration)
|
||||
{
|
||||
forceRedraw = true;
|
||||
// BUGBUG: Application.Screen needs to be moved to IApplication
|
||||
Application.ClearScreenNextIteration = false;
|
||||
}
|
||||
internal ApplicationImpl (IComponentFactory componentFactory)
|
||||
{
|
||||
_componentFactory = componentFactory;
|
||||
}
|
||||
|
||||
if (forceRedraw)
|
||||
{
|
||||
Driver?.ClearContents ();
|
||||
}
|
||||
|
||||
View.SetClipToScreen ();
|
||||
View.Draw (tops, neededLayout || forceRedraw);
|
||||
View.SetClipToScreen ();
|
||||
Driver?.Refresh ();
|
||||
/// <summary>
|
||||
/// Change the singleton implementation, should not be called except before application
|
||||
/// startup. This method lets you provide alternative implementations of core static gateway
|
||||
/// methods of <see cref="Application"/>.
|
||||
/// </summary>
|
||||
/// <param name="newApplication"></param>
|
||||
public static void ChangeInstance (IApplication newApplication)
|
||||
{
|
||||
_lazyInstance = new Lazy<IApplication> (newApplication);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -146,7 +158,7 @@ public class ApplicationImpl : IApplication
|
||||
[RequiresDynamicCode ("AOT")]
|
||||
public void Init (IConsoleDriver? driver = null, string? driverName = null)
|
||||
{
|
||||
if (Application.Initialized)
|
||||
if (_initialized)
|
||||
{
|
||||
Logging.Logger.LogError ("Init called multiple times without shutdown, aborting.");
|
||||
|
||||
@@ -163,13 +175,12 @@ public class ApplicationImpl : IApplication
|
||||
_driverName = Application.ForceDriver;
|
||||
}
|
||||
|
||||
Debug.Assert (Application.Navigation is null);
|
||||
Application.Navigation = new ();
|
||||
Debug.Assert(_navigation is null);
|
||||
_navigation = new ();
|
||||
|
||||
Debug.Assert (Application.Popover is null);
|
||||
Application.Popover = new ();
|
||||
Debug.Assert (_popover is null);
|
||||
_popover = new ();
|
||||
|
||||
// TODO: Move this into IKeyboard and Keyboard implementation
|
||||
// Preserve existing keyboard settings if they exist
|
||||
bool hasExistingKeyboard = _keyboard is not null;
|
||||
Key existingQuitKey = _keyboard?.QuitKey ?? Key.Esc;
|
||||
@@ -195,13 +206,99 @@ public class ApplicationImpl : IApplication
|
||||
|
||||
CreateDriver (driverName ?? _driverName);
|
||||
|
||||
Application.Initialized = true;
|
||||
_initialized = true;
|
||||
|
||||
Application.OnInitializedChanged (this, new (true));
|
||||
Application.SubscribeDriverEvents ();
|
||||
|
||||
SynchronizationContext.SetSynchronizationContext (new ());
|
||||
Application.MainThreadId = Thread.CurrentThread.ManagedThreadId;
|
||||
_mainThreadId = Thread.CurrentThread.ManagedThreadId;
|
||||
}
|
||||
|
||||
private void CreateDriver (string? driverName)
|
||||
{
|
||||
// When running unit tests, always use FakeDriver unless explicitly specified
|
||||
if (ConsoleDriver.RunningUnitTests &&
|
||||
string.IsNullOrEmpty (driverName) &&
|
||||
_componentFactory is null)
|
||||
{
|
||||
Logging.Logger.LogDebug ("Unit test safeguard: forcing FakeDriver (RunningUnitTests=true, driverName=null, componentFactory=null)");
|
||||
_coordinator = CreateSubcomponents (() => new FakeComponentFactory ());
|
||||
_coordinator.StartAsync ().Wait ();
|
||||
|
||||
if (_driver == null)
|
||||
{
|
||||
throw new ("Driver was null even after booting MainLoopCoordinator");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
PlatformID p = Environment.OSVersion.Platform;
|
||||
|
||||
// Check component factory type first - this takes precedence over driverName
|
||||
bool factoryIsWindows = _componentFactory is IComponentFactory<WindowsConsole.InputRecord>;
|
||||
bool factoryIsDotNet = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
|
||||
bool factoryIsUnix = _componentFactory is IComponentFactory<char>;
|
||||
bool factoryIsFake = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
|
||||
|
||||
// Then check driverName
|
||||
bool nameIsWindows = driverName?.Contains ("win", StringComparison.OrdinalIgnoreCase) ?? false;
|
||||
bool nameIsDotNet = (driverName?.Contains ("dotnet", StringComparison.OrdinalIgnoreCase) ?? false);
|
||||
bool nameIsUnix = driverName?.Contains ("unix", StringComparison.OrdinalIgnoreCase) ?? false;
|
||||
bool nameIsFake = driverName?.Contains ("fake", StringComparison.OrdinalIgnoreCase) ?? false;
|
||||
|
||||
// Decide which driver to use - component factory type takes priority
|
||||
if (factoryIsFake || (!factoryIsWindows && !factoryIsDotNet && !factoryIsUnix && nameIsFake))
|
||||
{
|
||||
_coordinator = CreateSubcomponents (() => new FakeComponentFactory ());
|
||||
}
|
||||
else if (factoryIsWindows || (!factoryIsDotNet && !factoryIsUnix && nameIsWindows))
|
||||
{
|
||||
_coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
|
||||
}
|
||||
else if (factoryIsDotNet || (!factoryIsWindows && !factoryIsUnix && nameIsDotNet))
|
||||
{
|
||||
_coordinator = CreateSubcomponents (() => new NetComponentFactory ());
|
||||
}
|
||||
else if (factoryIsUnix || (!factoryIsWindows && !factoryIsDotNet && nameIsUnix))
|
||||
{
|
||||
_coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
|
||||
}
|
||||
else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
|
||||
{
|
||||
_coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
|
||||
}
|
||||
else
|
||||
{
|
||||
_coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
|
||||
}
|
||||
|
||||
_coordinator.StartAsync ().Wait ();
|
||||
|
||||
if (_driver == null)
|
||||
{
|
||||
throw new ("Driver was null even after booting MainLoopCoordinator");
|
||||
}
|
||||
}
|
||||
|
||||
private IMainLoopCoordinator CreateSubcomponents<T> (Func<IComponentFactory<T>> fallbackFactory)
|
||||
{
|
||||
ConcurrentQueue<T> inputBuffer = new ();
|
||||
ApplicationMainLoop<T> loop = new ();
|
||||
|
||||
IComponentFactory<T> cf;
|
||||
|
||||
if (_componentFactory is IComponentFactory<T> typedFactory)
|
||||
{
|
||||
cf = typedFactory;
|
||||
}
|
||||
else
|
||||
{
|
||||
cf = fallbackFactory ();
|
||||
}
|
||||
|
||||
return new MainLoopCoordinator<T> (_timedEvents, inputBuffer, loop, cf);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -228,15 +325,14 @@ public class ApplicationImpl : IApplication
|
||||
public T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null)
|
||||
where T : Toplevel, new()
|
||||
{
|
||||
if (!Application.Initialized)
|
||||
if (!_initialized)
|
||||
{
|
||||
// Init() has NOT been called. Auto-initialize as per interface contract.
|
||||
Init (driver);
|
||||
Init (driver, null);
|
||||
}
|
||||
|
||||
T top = new ();
|
||||
Run (top, errorHandler);
|
||||
|
||||
return top;
|
||||
}
|
||||
|
||||
@@ -248,62 +344,74 @@ public class ApplicationImpl : IApplication
|
||||
Logging.Information ($"Run '{view}'");
|
||||
ArgumentNullException.ThrowIfNull (view);
|
||||
|
||||
if (!Application.Initialized)
|
||||
if (!_initialized)
|
||||
{
|
||||
throw new NotInitializedException (nameof (Run));
|
||||
}
|
||||
|
||||
if (Application.Driver == null)
|
||||
if (_driver == null)
|
||||
{
|
||||
throw new InvalidOperationException ("Driver was inexplicably null when trying to Run view");
|
||||
throw new InvalidOperationException ("Driver was inexplicably null when trying to Run view");
|
||||
}
|
||||
|
||||
Application.Top = view;
|
||||
_top = view;
|
||||
|
||||
RunState rs = Application.Begin (view);
|
||||
|
||||
Application.Top.Running = true;
|
||||
_top.Running = true;
|
||||
|
||||
while (Application.TopLevels.TryPeek (out Toplevel? found) && found == view && view.Running)
|
||||
while (_topLevels.TryPeek (out Toplevel? found) && found == view && view.Running)
|
||||
{
|
||||
if (Coordinator is null)
|
||||
if (_coordinator is null)
|
||||
{
|
||||
throw new ($"{nameof (IMainLoopCoordinator)} inexplicably became null during Run");
|
||||
}
|
||||
|
||||
Coordinator.RunIteration ();
|
||||
_coordinator.RunIteration ();
|
||||
}
|
||||
|
||||
Logging.Information ("Run - Calling End");
|
||||
Logging.Information ($"Run - Calling End");
|
||||
Application.End (rs);
|
||||
}
|
||||
|
||||
/// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
|
||||
public void Shutdown ()
|
||||
{
|
||||
Coordinator?.Stop ();
|
||||
|
||||
bool wasInitialized = Application.Initialized;
|
||||
_coordinator?.Stop ();
|
||||
|
||||
bool wasInitialized = _initialized;
|
||||
|
||||
// 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 ();
|
||||
ConfigurationManager.PrintJsonErrors ();
|
||||
|
||||
// Clear instance fields after ResetState has disposed everything
|
||||
_driver = null;
|
||||
_mouse = null;
|
||||
_keyboard = null;
|
||||
_initialized = false;
|
||||
_navigation = null;
|
||||
_popover = null;
|
||||
_top = null;
|
||||
_topLevels.Clear ();
|
||||
_mainThreadId = -1;
|
||||
|
||||
if (wasInitialized)
|
||||
{
|
||||
bool init = Application.Initialized;
|
||||
bool init = _initialized; // Will be false after clearing fields above
|
||||
Application.OnInitializedChanged (this, new (in init));
|
||||
}
|
||||
|
||||
Application.Driver = null;
|
||||
_keyboard = null;
|
||||
_lazyInstance = new (() => new ApplicationImpl ());
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <inheritdoc />
|
||||
public void RequestStop (Toplevel? top)
|
||||
{
|
||||
Logging.Logger.LogInformation ($"RequestStop '{(top is { } ? top : "null")}'");
|
||||
Logging.Logger.LogInformation ($"RequestStop '{(top is {} ? top : "null")}'");
|
||||
|
||||
top ??= Application.Top;
|
||||
top ??= _top;
|
||||
|
||||
if (top == null)
|
||||
{
|
||||
@@ -321,138 +429,65 @@ public class ApplicationImpl : IApplication
|
||||
top.Running = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RequestStop () => Application.RequestStop ();
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <inheritdoc />
|
||||
public void Invoke (Action action)
|
||||
{
|
||||
// If we are already on the main UI thread
|
||||
if (Application.MainThreadId == Thread.CurrentThread.ManagedThreadId)
|
||||
if (_mainThreadId == Thread.CurrentThread.ManagedThreadId)
|
||||
{
|
||||
action ();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_timedEvents.Add (
|
||||
TimeSpan.Zero,
|
||||
() =>
|
||||
{
|
||||
action ();
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
_timedEvents.Add (TimeSpan.Zero,
|
||||
() =>
|
||||
{
|
||||
action ();
|
||||
return false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <inheritdoc />
|
||||
public bool IsLegacy => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <inheritdoc />
|
||||
public object AddTimeout (TimeSpan time, Func<bool> callback) { return _timedEvents.Add (time, callback); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <inheritdoc />
|
||||
public bool RemoveTimeout (object token) { return _timedEvents.Remove (token); }
|
||||
|
||||
/// <summary>
|
||||
/// Change the singleton implementation, should not be called except before application
|
||||
/// startup. This method lets you provide alternative implementations of core static gateway
|
||||
/// methods of <see cref="Application"/>.
|
||||
/// </summary>
|
||||
/// <param name="newApplication"></param>
|
||||
public static void ChangeInstance (IApplication newApplication) { _lazyInstance = new (newApplication); }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the currently configured backend implementation of <see cref="Application"/> gateway methods.
|
||||
/// Change to your own implementation by using <see cref="ChangeInstance"/> (before init).
|
||||
/// </summary>
|
||||
public static IApplication Instance => _lazyInstance.Value;
|
||||
|
||||
internal IMainLoopCoordinator? Coordinator { get; private set; }
|
||||
|
||||
private void CreateDriver (string? driverName)
|
||||
/// <inheritdoc />
|
||||
public void LayoutAndDraw (bool forceRedraw = false)
|
||||
{
|
||||
// When running unit tests, always use FakeDriver unless explicitly specified
|
||||
if (ConsoleDriver.RunningUnitTests && string.IsNullOrEmpty (driverName) && _componentFactory is null)
|
||||
List<View> tops = [.. _topLevels];
|
||||
|
||||
if (_popover?.GetActivePopover () as View is { Visible: true } visiblePopover)
|
||||
{
|
||||
Logging.Logger.LogDebug ("Unit test safeguard: forcing FakeDriver (RunningUnitTests=true, driverName=null, componentFactory=null)");
|
||||
Coordinator = CreateSubcomponents (() => new FakeComponentFactory ());
|
||||
Coordinator.StartAsync ().Wait ();
|
||||
|
||||
if (Application.Driver == null)
|
||||
{
|
||||
throw new ("Application.Driver was null even after booting MainLoopCoordinator");
|
||||
}
|
||||
|
||||
return;
|
||||
visiblePopover.SetNeedsDraw ();
|
||||
visiblePopover.SetNeedsLayout ();
|
||||
tops.Insert (0, visiblePopover);
|
||||
}
|
||||
|
||||
PlatformID p = Environment.OSVersion.Platform;
|
||||
// BUGBUG: Application.Screen needs to be moved to IApplication
|
||||
bool neededLayout = View.Layout (tops.ToArray ().Reverse (), Application.Screen.Size);
|
||||
|
||||
// Check component factory type first - this takes precedence over driverName
|
||||
bool factoryIsWindows = _componentFactory is IComponentFactory<WindowsConsole.InputRecord>;
|
||||
bool factoryIsDotNet = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
|
||||
bool factoryIsUnix = _componentFactory is IComponentFactory<char>;
|
||||
bool factoryIsFake = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
|
||||
|
||||
// Then check driverName
|
||||
bool nameIsWindows = driverName?.Contains ("win", StringComparison.OrdinalIgnoreCase) ?? false;
|
||||
bool nameIsDotNet = driverName?.Contains ("dotnet", StringComparison.OrdinalIgnoreCase) ?? false;
|
||||
bool nameIsUnix = driverName?.Contains ("unix", StringComparison.OrdinalIgnoreCase) ?? false;
|
||||
bool nameIsFake = driverName?.Contains ("fake", StringComparison.OrdinalIgnoreCase) ?? false;
|
||||
|
||||
// Decide which driver to use - component factory type takes priority
|
||||
if (factoryIsFake || (!factoryIsWindows && !factoryIsDotNet && !factoryIsUnix && nameIsFake))
|
||||
// BUGBUG: Application.ClearScreenNextIteration needs to be moved to IApplication
|
||||
if (Application.ClearScreenNextIteration)
|
||||
{
|
||||
Coordinator = CreateSubcomponents (() => new FakeComponentFactory ());
|
||||
}
|
||||
else if (factoryIsWindows || (!factoryIsDotNet && !factoryIsUnix && nameIsWindows))
|
||||
{
|
||||
Coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
|
||||
}
|
||||
else if (factoryIsDotNet || (!factoryIsWindows && !factoryIsUnix && nameIsDotNet))
|
||||
{
|
||||
Coordinator = CreateSubcomponents (() => new NetComponentFactory ());
|
||||
}
|
||||
else if (factoryIsUnix || (!factoryIsWindows && !factoryIsDotNet && nameIsUnix))
|
||||
{
|
||||
Coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
|
||||
}
|
||||
else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
|
||||
{
|
||||
Coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
|
||||
}
|
||||
else
|
||||
{
|
||||
Coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
|
||||
forceRedraw = true;
|
||||
// BUGBUG: Application.Screen needs to be moved to IApplication
|
||||
Application.ClearScreenNextIteration = false;
|
||||
}
|
||||
|
||||
Coordinator.StartAsync ().Wait ();
|
||||
|
||||
if (Application.Driver == null)
|
||||
if (forceRedraw)
|
||||
{
|
||||
throw new ("Application.Driver was null even after booting MainLoopCoordinator");
|
||||
}
|
||||
}
|
||||
|
||||
private IMainLoopCoordinator CreateSubcomponents<T> (Func<IComponentFactory<T>> fallbackFactory)
|
||||
{
|
||||
ConcurrentQueue<T> inputBuffer = new ();
|
||||
ApplicationMainLoop<T> loop = new ();
|
||||
|
||||
IComponentFactory<T> cf;
|
||||
|
||||
if (_componentFactory is IComponentFactory<T> typedFactory)
|
||||
{
|
||||
cf = typedFactory;
|
||||
}
|
||||
else
|
||||
{
|
||||
cf = fallbackFactory ();
|
||||
_driver?.ClearContents ();
|
||||
}
|
||||
|
||||
return new MainLoopCoordinator<T> (_timedEvents, inputBuffer, loop, cf);
|
||||
View.SetClipToScreen ();
|
||||
View.Draw (tops, neededLayout || forceRedraw);
|
||||
View.SetClipToScreen ();
|
||||
_driver?.Refresh ();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -583,4 +583,52 @@ public class ApplicationImplTests
|
||||
|
||||
Assert.True (result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplicationImpl_UsesInstanceFields_NotStaticReferences()
|
||||
{
|
||||
// This test verifies that ApplicationImpl uses instance fields instead of static Application references
|
||||
var orig = ApplicationImpl.Instance;
|
||||
|
||||
var v2 = NewApplicationImpl();
|
||||
ApplicationImpl.ChangeInstance(v2);
|
||||
|
||||
// Before Init, all fields should be null/default
|
||||
Assert.Null(v2.Driver);
|
||||
Assert.False(v2.Initialized);
|
||||
Assert.Null(v2.Popover);
|
||||
Assert.Null(v2.Navigation);
|
||||
Assert.Null(v2.Top);
|
||||
Assert.Empty(v2.TopLevels);
|
||||
|
||||
// Init should populate instance fields
|
||||
v2.Init();
|
||||
|
||||
// After Init, Driver, Navigation, and Popover should be populated
|
||||
Assert.NotNull(v2.Driver);
|
||||
Assert.True(v2.Initialized);
|
||||
Assert.NotNull(v2.Popover);
|
||||
Assert.NotNull(v2.Navigation);
|
||||
Assert.Null(v2.Top); // Top is still null until Run
|
||||
|
||||
// Verify that static Application properties delegate to instance
|
||||
Assert.Equal(v2.Driver, Application.Driver);
|
||||
Assert.Equal(v2.Initialized, Application.Initialized);
|
||||
Assert.Equal(v2.Popover, Application.Popover);
|
||||
Assert.Equal(v2.Navigation, Application.Navigation);
|
||||
Assert.Equal(v2.Top, Application.Top);
|
||||
Assert.Same(v2.TopLevels, Application.TopLevels);
|
||||
|
||||
// Shutdown should clean up instance fields
|
||||
v2.Shutdown();
|
||||
|
||||
Assert.Null(v2.Driver);
|
||||
Assert.False(v2.Initialized);
|
||||
Assert.Null(v2.Popover);
|
||||
Assert.Null(v2.Navigation);
|
||||
Assert.Null(v2.Top);
|
||||
Assert.Empty(v2.TopLevels);
|
||||
|
||||
ApplicationImpl.ChangeInstance(orig);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user