ApplicationImpl.Run.cs 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. #nullable enable
  2. using System.Diagnostics;
  3. using System.Diagnostics.CodeAnalysis;
  4. namespace Terminal.Gui.App;
  5. public partial class ApplicationImpl
  6. {
  7. /// <summary>
  8. /// INTERNAL: Gets or sets the managed thread ID of the application's main UI thread, which is set during
  9. /// <see cref="Init"/> and used to determine if code is executing on the main thread.
  10. /// </summary>
  11. /// <value>
  12. /// The managed thread ID of the main UI thread, or <see langword="null"/> if the application is not initialized.
  13. /// </value>
  14. internal int? MainThreadId { get; set; }
  15. #region Begin->Run->Stop->End
  16. /// <inheritdoc/>
  17. public event EventHandler<SessionTokenEventArgs>? SessionBegun;
  18. /// <inheritdoc/>
  19. public event EventHandler<ToplevelEventArgs>? SessionEnded;
  20. /// <inheritdoc/>
  21. public SessionToken Begin (Toplevel toplevel)
  22. {
  23. ArgumentNullException.ThrowIfNull (toplevel);
  24. // Ensure the mouse is ungrabbed.
  25. if (Mouse.MouseGrabView is { })
  26. {
  27. Mouse.UngrabMouse ();
  28. }
  29. var rs = new SessionToken (toplevel);
  30. #if DEBUG_IDISPOSABLE
  31. if (View.EnableDebugIDisposableAsserts && Top is { } && toplevel != Top && !TopLevels.Contains (Top))
  32. {
  33. // This assertion confirm if the Top was already disposed
  34. Debug.Assert (Top.WasDisposed);
  35. Debug.Assert (Top == CachedSessionTokenToplevel);
  36. }
  37. #endif
  38. lock (TopLevels)
  39. {
  40. if (Top is { } && toplevel != Top && !TopLevels.Contains (Top))
  41. {
  42. // If Top was already disposed and isn't on the Toplevels Stack,
  43. // clean it up here if is the same as _CachedSessionTokenToplevel
  44. if (Top == CachedSessionTokenToplevel)
  45. {
  46. Top = null;
  47. }
  48. else
  49. {
  50. // Probably this will never hit
  51. throw new ObjectDisposedException (Top.GetType ().FullName);
  52. }
  53. }
  54. // BUGBUG: We should not depend on `Id` internally.
  55. // BUGBUG: It is super unclear what this code does anyway.
  56. if (string.IsNullOrEmpty (toplevel.Id))
  57. {
  58. var count = 1;
  59. var id = (TopLevels.Count + count).ToString ();
  60. while (TopLevels.Count > 0 && TopLevels.FirstOrDefault (x => x.Id == id) is { })
  61. {
  62. count++;
  63. id = (TopLevels.Count + count).ToString ();
  64. }
  65. toplevel.Id = (TopLevels.Count + count).ToString ();
  66. TopLevels.Push (toplevel);
  67. }
  68. else
  69. {
  70. Toplevel? dup = TopLevels.FirstOrDefault (x => x.Id == toplevel.Id);
  71. if (dup is null)
  72. {
  73. TopLevels.Push (toplevel);
  74. }
  75. }
  76. }
  77. if (Top is null)
  78. {
  79. Top = toplevel;
  80. }
  81. if ((Top?.Modal == false && toplevel.Modal)
  82. || (Top?.Modal == false && !toplevel.Modal)
  83. || (Top?.Modal == true && toplevel.Modal))
  84. {
  85. if (toplevel.Visible)
  86. {
  87. if (Top is { HasFocus: true })
  88. {
  89. Top.HasFocus = false;
  90. }
  91. // Force leave events for any entered views in the old Top
  92. if (Mouse.GetLastMousePosition () is { })
  93. {
  94. Mouse.RaiseMouseEnterLeaveEvents (Mouse.GetLastMousePosition ()!.Value, new ());
  95. }
  96. Top?.OnDeactivate (toplevel);
  97. Toplevel previousTop = Top!;
  98. Top = toplevel;
  99. Top.OnActivate (previousTop);
  100. }
  101. }
  102. // View implements ISupportInitializeNotification which is derived from ISupportInitialize
  103. if (!toplevel.IsInitialized)
  104. {
  105. toplevel.BeginInit ();
  106. toplevel.EndInit (); // Calls Layout
  107. }
  108. // Try to set initial focus to any TabStop
  109. if (!toplevel.HasFocus)
  110. {
  111. toplevel.SetFocus ();
  112. }
  113. toplevel.OnLoaded ();
  114. Instance.LayoutAndDraw (true);
  115. if (PositionCursor ())
  116. {
  117. Driver?.UpdateCursor ();
  118. }
  119. SessionBegun?.Invoke (this, new (rs));
  120. return rs;
  121. }
  122. /// <inheritdoc/>
  123. public bool StopAfterFirstIteration { get; set; }
  124. /// <inheritdoc/>
  125. public event EventHandler<IterationEventArgs>? Iteration;
  126. /// <inheritdoc/>
  127. [RequiresUnreferencedCode ("AOT")]
  128. [RequiresDynamicCode ("AOT")]
  129. public Toplevel Run (Func<Exception, bool>? errorHandler = null, string? driver = null) { return Run<Toplevel> (errorHandler, driver); }
  130. /// <inheritdoc/>
  131. [RequiresUnreferencedCode ("AOT")]
  132. [RequiresDynamicCode ("AOT")]
  133. public TView Run<TView> (Func<Exception, bool>? errorHandler = null, string? driver = null)
  134. where TView : Toplevel, new ()
  135. {
  136. if (!Initialized)
  137. {
  138. // Init() has NOT been called. Auto-initialize as per interface contract.
  139. Init (null, driver);
  140. }
  141. TView top = new ();
  142. Run (top, errorHandler);
  143. return top;
  144. }
  145. /// <inheritdoc/>
  146. public void Run (Toplevel view, Func<Exception, bool>? errorHandler = null)
  147. {
  148. Logging.Information ($"Run '{view}'");
  149. ArgumentNullException.ThrowIfNull (view);
  150. if (!Initialized)
  151. {
  152. throw new NotInitializedException (nameof (Run));
  153. }
  154. if (Driver == null)
  155. {
  156. throw new InvalidOperationException ("Driver was inexplicably null when trying to Run view");
  157. }
  158. Top = view;
  159. SessionToken rs = Application.Begin (view);
  160. Top.Running = true;
  161. var firstIteration = true;
  162. while (TopLevels.TryPeek (out Toplevel? found) && found == view && view.Running)
  163. {
  164. if (Coordinator is null)
  165. {
  166. throw new ($"{nameof (IMainLoopCoordinator)} inexplicably became null during Run");
  167. }
  168. Coordinator.RunIteration ();
  169. if (StopAfterFirstIteration && firstIteration)
  170. {
  171. Logging.Information ("Run - Stopping after first iteration as requested");
  172. view.RequestStop ();
  173. }
  174. firstIteration = false;
  175. }
  176. Logging.Information ("Run - Calling End");
  177. Application.End (rs);
  178. }
  179. /// <inheritdoc/>
  180. public void End (SessionToken sessionToken)
  181. {
  182. ArgumentNullException.ThrowIfNull (sessionToken);
  183. if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover)
  184. {
  185. ApplicationPopover.HideWithQuitCommand (visiblePopover);
  186. }
  187. sessionToken.Toplevel.OnUnloaded ();
  188. // End the Session
  189. // First, take it off the Toplevel Stack
  190. if (TopLevels.TryPop (out Toplevel? topOfStack))
  191. {
  192. if (topOfStack != sessionToken.Toplevel)
  193. {
  194. // If the top of the stack is not the SessionToken.Toplevel then
  195. // this call to End is not balanced with the call to Begin that started the Session
  196. throw new ArgumentException ("End must be balanced with calls to Begin");
  197. }
  198. }
  199. // Notify that it is closing
  200. sessionToken.Toplevel?.OnClosed (sessionToken.Toplevel);
  201. if (TopLevels.TryPeek (out Toplevel? newTop))
  202. {
  203. Top = newTop;
  204. Top?.SetNeedsDraw ();
  205. }
  206. if (sessionToken.Toplevel is { HasFocus: true })
  207. {
  208. sessionToken.Toplevel.HasFocus = false;
  209. }
  210. if (Top is { HasFocus: false })
  211. {
  212. Top.SetFocus ();
  213. }
  214. CachedSessionTokenToplevel = sessionToken.Toplevel;
  215. sessionToken.Toplevel = null;
  216. sessionToken.Dispose ();
  217. // BUGBUG: Why layout and draw here? This causes the screen to be cleared!
  218. //LayoutAndDraw (true);
  219. SessionEnded?.Invoke (this, new (CachedSessionTokenToplevel));
  220. }
  221. /// <inheritdoc/>
  222. public void RequestStop () { RequestStop (null); }
  223. /// <inheritdoc/>
  224. public void RequestStop (Toplevel? top)
  225. {
  226. Logging.Trace ($"Top: '{(top is { } ? top : "null")}'");
  227. top ??= Top;
  228. if (top == null)
  229. {
  230. return;
  231. }
  232. ToplevelClosingEventArgs ev = new (top);
  233. top.OnClosing (ev);
  234. if (ev.Cancel)
  235. {
  236. return;
  237. }
  238. top.Running = false;
  239. }
  240. /// <inheritdoc/>
  241. public void RaiseIteration () { Iteration?.Invoke (null, new ()); }
  242. #endregion Begin->Run->Stop->End
  243. #region Timeouts and Invoke
  244. private readonly ITimedEvents _timedEvents = new TimedEvents ();
  245. /// <inheritdoc/>
  246. public ITimedEvents? TimedEvents => _timedEvents;
  247. /// <inheritdoc/>
  248. public object AddTimeout (TimeSpan time, Func<bool> callback) { return _timedEvents.Add (time, callback); }
  249. /// <inheritdoc/>
  250. public bool RemoveTimeout (object token) { return _timedEvents.Remove (token); }
  251. /// <inheritdoc/>
  252. public void Invoke (Action action)
  253. {
  254. // If we are already on the main UI thread
  255. if (Top is { Running: true } && MainThreadId == Thread.CurrentThread.ManagedThreadId)
  256. {
  257. action ();
  258. return;
  259. }
  260. _timedEvents.Add (
  261. TimeSpan.Zero,
  262. () =>
  263. {
  264. action ();
  265. return false;
  266. }
  267. );
  268. }
  269. #endregion Timeouts and Invoke
  270. }