ApplicationImpl.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. #nullable enable
  2. using System.Collections.Concurrent;
  3. using System.Diagnostics;
  4. using System.Diagnostics.CodeAnalysis;
  5. using Microsoft.Extensions.Logging;
  6. using Terminal.Gui.Drivers;
  7. namespace Terminal.Gui.App;
  8. /// <summary>
  9. /// Implementation of core <see cref="Application"/> methods using the modern
  10. /// main loop architecture with component factories for different platforms.
  11. /// </summary>
  12. public class ApplicationImpl : IApplication
  13. {
  14. private readonly IComponentFactory? _componentFactory;
  15. private IMainLoopCoordinator? _coordinator;
  16. private string? _driverName;
  17. private readonly ITimedEvents _timedEvents = new TimedEvents ();
  18. // Private static readonly Lazy instance of Application
  19. private static Lazy<IApplication> _lazyInstance = new (() => new ApplicationImpl ());
  20. /// <summary>
  21. /// Gets the currently configured backend implementation of <see cref="Application"/> gateway methods.
  22. /// Change to your own implementation by using <see cref="ChangeInstance"/> (before init).
  23. /// </summary>
  24. public static IApplication Instance => _lazyInstance.Value;
  25. /// <inheritdoc/>
  26. public ITimedEvents? TimedEvents => _timedEvents;
  27. internal IMainLoopCoordinator? Coordinator => _coordinator;
  28. /// <summary>
  29. /// Handles which <see cref="View"/> (if any) has captured the mouse
  30. /// </summary>
  31. public IMouseGrabHandler MouseGrabHandler { get; set; } = new MouseGrabHandler ();
  32. /// <summary>
  33. /// Creates a new instance of the Application backend.
  34. /// </summary>
  35. public ApplicationImpl ()
  36. {
  37. }
  38. internal ApplicationImpl (IComponentFactory componentFactory)
  39. {
  40. _componentFactory = componentFactory;
  41. }
  42. /// <summary>
  43. /// Change the singleton implementation, should not be called except before application
  44. /// startup. This method lets you provide alternative implementations of core static gateway
  45. /// methods of <see cref="Application"/>.
  46. /// </summary>
  47. /// <param name="newApplication"></param>
  48. public static void ChangeInstance (IApplication newApplication)
  49. {
  50. _lazyInstance = new Lazy<IApplication> (newApplication);
  51. }
  52. /// <inheritdoc/>
  53. [RequiresUnreferencedCode ("AOT")]
  54. [RequiresDynamicCode ("AOT")]
  55. public void Init (IConsoleDriver? driver = null, string? driverName = null)
  56. {
  57. if (Application.Initialized)
  58. {
  59. Logging.Logger.LogError ("Init called multiple times without shutdown, aborting.");
  60. throw new InvalidOperationException ("Init called multiple times without Shutdown");
  61. }
  62. if (!string.IsNullOrWhiteSpace (driverName))
  63. {
  64. _driverName = driverName;
  65. }
  66. if (string.IsNullOrWhiteSpace (_driverName))
  67. {
  68. _driverName = Application.ForceDriver;
  69. }
  70. Debug.Assert(Application.Navigation is null);
  71. Application.Navigation = new ();
  72. Debug.Assert (Application.Popover is null);
  73. Application.Popover = new ();
  74. Application.AddKeyBindings ();
  75. CreateDriver (driverName ?? _driverName);
  76. Application.Initialized = true;
  77. Application.OnInitializedChanged (this, new (true));
  78. Application.SubscribeDriverEvents ();
  79. SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext ());
  80. Application.MainThreadId = Thread.CurrentThread.ManagedThreadId;
  81. }
  82. private void CreateDriver (string? driverName)
  83. {
  84. PlatformID p = Environment.OSVersion.Platform;
  85. // Check component factory type first - this takes precedence over driverName
  86. bool factoryIsWindows = _componentFactory is IComponentFactory<WindowsConsole.InputRecord>;
  87. bool factoryIsDotNet = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
  88. bool factoryIsUnix = _componentFactory is IComponentFactory<char>;
  89. bool factoryIsFake = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
  90. // Then check driverName
  91. bool nameIsWindows = driverName?.Contains ("win", StringComparison.OrdinalIgnoreCase) ?? false;
  92. bool nameIsDotNet = (driverName?.Contains ("dotnet", StringComparison.OrdinalIgnoreCase) ?? false);
  93. bool nameIsUnix = driverName?.Contains ("unix", StringComparison.OrdinalIgnoreCase) ?? false;
  94. bool nameIsFake = driverName?.Contains ("fake", StringComparison.OrdinalIgnoreCase) ?? false;
  95. // Decide which driver to use - component factory type takes priority
  96. if (factoryIsFake || (!factoryIsWindows && !factoryIsDotNet && !factoryIsUnix && nameIsFake))
  97. {
  98. _coordinator = CreateSubcomponents (() => new FakeComponentFactory ());
  99. }
  100. else if (factoryIsWindows || (!factoryIsDotNet && !factoryIsUnix && nameIsWindows))
  101. {
  102. _coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
  103. }
  104. else if (factoryIsDotNet || (!factoryIsWindows && !factoryIsUnix && nameIsDotNet))
  105. {
  106. _coordinator = CreateSubcomponents (() => new NetComponentFactory ());
  107. }
  108. else if (factoryIsUnix || (!factoryIsWindows && !factoryIsDotNet && nameIsUnix))
  109. {
  110. _coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
  111. }
  112. else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
  113. {
  114. _coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
  115. }
  116. else
  117. {
  118. _coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
  119. }
  120. _coordinator.StartAsync ().Wait ();
  121. if (Application.Driver == null)
  122. {
  123. throw new ("Application.Driver was null even after booting MainLoopCoordinator");
  124. }
  125. }
  126. private IMainLoopCoordinator CreateSubcomponents<T> (Func<IComponentFactory<T>> fallbackFactory)
  127. {
  128. ConcurrentQueue<T> inputBuffer = new ();
  129. ApplicationMainLoop<T> loop = new ();
  130. IComponentFactory<T> cf;
  131. if (_componentFactory is IComponentFactory<T> typedFactory)
  132. {
  133. cf = typedFactory;
  134. }
  135. else
  136. {
  137. cf = fallbackFactory ();
  138. }
  139. return new MainLoopCoordinator<T> (_timedEvents, inputBuffer, loop, cf);
  140. }
  141. /// <summary>
  142. /// Runs the application by creating a <see cref="Toplevel"/> object and calling
  143. /// <see cref="Run(Toplevel, Func{Exception, bool})"/>.
  144. /// </summary>
  145. /// <returns>The created <see cref="Toplevel"/> object. The caller is responsible for disposing this object.</returns>
  146. [RequiresUnreferencedCode ("AOT")]
  147. [RequiresDynamicCode ("AOT")]
  148. public Toplevel Run (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null) { return Run<Toplevel> (errorHandler, driver); }
  149. /// <summary>
  150. /// Runs the application by creating a <see cref="Toplevel"/>-derived object of type <c>T</c> and calling
  151. /// <see cref="Run(Toplevel, Func{Exception, bool})"/>.
  152. /// </summary>
  153. /// <param name="errorHandler"></param>
  154. /// <param name="driver">
  155. /// The <see cref="IConsoleDriver"/> to use. If not specified the default driver for the platform will
  156. /// be used. Must be <see langword="null"/> if <see cref="Init"/> has already been called.
  157. /// </param>
  158. /// <returns>The created T object. The caller is responsible for disposing this object.</returns>
  159. [RequiresUnreferencedCode ("AOT")]
  160. [RequiresDynamicCode ("AOT")]
  161. public T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null)
  162. where T : Toplevel, new()
  163. {
  164. if (!Application.Initialized)
  165. {
  166. // Init() has NOT been called. Auto-initialize as per interface contract.
  167. Init (driver, null);
  168. }
  169. var top = new T ();
  170. Run (top, errorHandler);
  171. return top;
  172. }
  173. /// <summary>Runs the Application using the provided <see cref="Toplevel"/> view.</summary>
  174. /// <param name="view">The <see cref="Toplevel"/> to run as a modal.</param>
  175. /// <param name="errorHandler">Handler for any unhandled exceptions.</param>
  176. public void Run (Toplevel view, Func<Exception, bool>? errorHandler = null)
  177. {
  178. Logging.Information ($"Run '{view}'");
  179. ArgumentNullException.ThrowIfNull (view);
  180. if (!Application.Initialized)
  181. {
  182. throw new NotInitializedException (nameof (Run));
  183. }
  184. if (Application.Driver == null)
  185. {
  186. throw new InvalidOperationException ("Driver was inexplicably null when trying to Run view");
  187. }
  188. Application.Top = view;
  189. RunState rs = Application.Begin (view);
  190. Application.Top.Running = true;
  191. while (Application.TopLevels.TryPeek (out Toplevel? found) && found == view && view.Running)
  192. {
  193. if (_coordinator is null)
  194. {
  195. throw new ($"{nameof (IMainLoopCoordinator)} inexplicably became null during Run");
  196. }
  197. _coordinator.RunIteration ();
  198. }
  199. Logging.Information ($"Run - Calling End");
  200. Application.End (rs);
  201. }
  202. /// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
  203. public void Shutdown ()
  204. {
  205. _coordinator?.Stop ();
  206. bool wasInitialized = Application.Initialized;
  207. Application.ResetState ();
  208. ConfigurationManager.PrintJsonErrors ();
  209. if (wasInitialized)
  210. {
  211. bool init = Application.Initialized;
  212. Application.OnInitializedChanged (this, new (in init));
  213. }
  214. Application.Driver = null;
  215. _lazyInstance = new (() => new ApplicationImpl ());
  216. }
  217. /// <inheritdoc />
  218. public void RequestStop (Toplevel? top)
  219. {
  220. Logging.Logger.LogInformation ($"RequestStop '{(top is {} ? top : "null")}'");
  221. top ??= Application.Top;
  222. if (top == null)
  223. {
  224. return;
  225. }
  226. var ev = new ToplevelClosingEventArgs (top);
  227. top.OnClosing (ev);
  228. if (ev.Cancel)
  229. {
  230. return;
  231. }
  232. top.Running = false;
  233. }
  234. /// <inheritdoc />
  235. public void Invoke (Action action)
  236. {
  237. // If we are already on the main UI thread
  238. if (Application.MainThreadId == Thread.CurrentThread.ManagedThreadId)
  239. {
  240. action ();
  241. return;
  242. }
  243. _timedEvents.Add (TimeSpan.Zero,
  244. () =>
  245. {
  246. action ();
  247. return false;
  248. }
  249. );
  250. }
  251. /// <inheritdoc />
  252. public bool IsLegacy => false;
  253. /// <inheritdoc />
  254. public object AddTimeout (TimeSpan time, Func<bool> callback) { return _timedEvents.Add (time, callback); }
  255. /// <inheritdoc />
  256. public bool RemoveTimeout (object token) { return _timedEvents.Remove (token); }
  257. /// <inheritdoc />
  258. public void LayoutAndDraw (bool forceDraw)
  259. {
  260. Application.Top?.SetNeedsDraw();
  261. Application.Top?.SetNeedsLayout ();
  262. }
  263. }