ApplicationImpl.Run.cs 11 KB

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