ApplicationImpl.Run.cs 11 KB

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