ApplicationImpl.Run.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665
  1. using System.Diagnostics;
  2. using System.Diagnostics.CodeAnalysis;
  3. namespace Terminal.Gui.App;
  4. public partial class ApplicationImpl
  5. {
  6. #region Begin->Run->Stop->End
  7. // TODO: This API is not used anywhere; it can be deleted
  8. /// <inheritdoc/>
  9. public event EventHandler<SessionTokenEventArgs>? SessionBegun;
  10. // TODO: This API is not used anywhere; it can be deleted
  11. /// <inheritdoc/>
  12. public event EventHandler<ToplevelEventArgs>? SessionEnded;
  13. /// <inheritdoc/>
  14. public SessionToken Begin (Toplevel toplevel)
  15. {
  16. ArgumentNullException.ThrowIfNull (toplevel);
  17. // Ensure the mouse is ungrabbed.
  18. if (Mouse.MouseGrabView is { })
  19. {
  20. Mouse.UngrabMouse ();
  21. }
  22. var rs = new SessionToken (toplevel);
  23. #if DEBUG_IDISPOSABLE
  24. if (View.EnableDebugIDisposableAsserts && TopRunnable is { } && toplevel != TopRunnable && !SessionStack.Contains (TopRunnable))
  25. {
  26. // This assertion confirm if the TopRunnable was already disposed
  27. Debug.Assert (TopRunnable.WasDisposed);
  28. Debug.Assert (TopRunnable == CachedSessionTokenToplevel);
  29. }
  30. #endif
  31. lock (SessionStack)
  32. {
  33. if (TopRunnable is { } && toplevel != TopRunnable && !SessionStack.Contains (TopRunnable))
  34. {
  35. // If TopRunnable was already disposed and isn't on the Toplevels Stack,
  36. // clean it up here if is the same as _CachedSessionTokenToplevel
  37. if (TopRunnable == CachedSessionTokenToplevel)
  38. {
  39. TopRunnable = null;
  40. }
  41. else
  42. {
  43. // Probably this will never hit
  44. throw new ObjectDisposedException (TopRunnable.GetType ().FullName);
  45. }
  46. }
  47. // BUGBUG: We should not depend on `Id` internally.
  48. // BUGBUG: It is super unclear what this code does anyway.
  49. if (string.IsNullOrEmpty (toplevel.Id))
  50. {
  51. var count = 1;
  52. var id = (SessionStack.Count + count).ToString ();
  53. while (SessionStack.Count > 0 && SessionStack.FirstOrDefault (x => x.Id == id) is { })
  54. {
  55. count++;
  56. id = (SessionStack.Count + count).ToString ();
  57. }
  58. toplevel.Id = (SessionStack.Count + count).ToString ();
  59. SessionStack.Push (toplevel);
  60. }
  61. else
  62. {
  63. Toplevel? dup = SessionStack.FirstOrDefault (x => x.Id == toplevel.Id);
  64. if (dup is null)
  65. {
  66. SessionStack.Push (toplevel);
  67. }
  68. }
  69. }
  70. if (TopRunnable is null)
  71. {
  72. toplevel.App = this;
  73. TopRunnable = toplevel;
  74. }
  75. if ((TopRunnable?.Modal == false && toplevel.Modal)
  76. || (TopRunnable?.Modal == false && !toplevel.Modal)
  77. || (TopRunnable?.Modal == true && toplevel.Modal))
  78. {
  79. if (toplevel.Visible)
  80. {
  81. if (TopRunnable is { HasFocus: true })
  82. {
  83. TopRunnable.HasFocus = false;
  84. }
  85. // Force leave events for any entered views in the old TopRunnable
  86. if (Mouse.LastMousePosition is { })
  87. {
  88. Mouse.RaiseMouseEnterLeaveEvents (Mouse.LastMousePosition!.Value, new ());
  89. }
  90. TopRunnable?.OnDeactivate (toplevel);
  91. Toplevel previousTop = TopRunnable!;
  92. TopRunnable = toplevel;
  93. TopRunnable.App = this;
  94. TopRunnable.OnActivate (previousTop);
  95. }
  96. }
  97. // View implements ISupportInitializeNotification which is derived from ISupportInitialize
  98. if (!toplevel.IsInitialized)
  99. {
  100. toplevel.BeginInit ();
  101. toplevel.EndInit (); // Calls Layout
  102. }
  103. // Try to set initial focus to any TabStop
  104. if (!toplevel.HasFocus)
  105. {
  106. toplevel.SetFocus ();
  107. }
  108. toplevel.OnLoaded ();
  109. LayoutAndDraw (true);
  110. if (PositionCursor ())
  111. {
  112. Driver?.UpdateCursor ();
  113. }
  114. SessionBegun?.Invoke (this, new (rs));
  115. return rs;
  116. }
  117. /// <inheritdoc/>
  118. public bool StopAfterFirstIteration { get; set; }
  119. /// <inheritdoc/>
  120. public void RaiseIteration ()
  121. {
  122. Iteration?.Invoke (null, new (this));
  123. }
  124. /// <inheritdoc/>
  125. public event EventHandler<EventArgs<IApplication?>>? Iteration;
  126. /// <inheritdoc/>
  127. [RequiresUnreferencedCode ("AOT")]
  128. [RequiresDynamicCode ("AOT")]
  129. public Toplevel Run (Func<Exception, bool>? errorHandler = null, string? driverName = null) => Run<Toplevel> (errorHandler, driverName);
  130. /// <inheritdoc/>
  131. [RequiresUnreferencedCode ("AOT")]
  132. [RequiresDynamicCode ("AOT")]
  133. public TView Run<TView> (Func<Exception, bool>? errorHandler = null, string? driverName = 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 (driverName);
  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. TopRunnable = view;
  159. SessionToken rs = Begin (view);
  160. TopRunnable.Running = true;
  161. var firstIteration = true;
  162. while (SessionStack.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. RequestStop ((Toplevel?)view);
  173. }
  174. firstIteration = false;
  175. }
  176. Logging.Information ("Run - Calling End");
  177. 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 (SessionStack.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 (SessionStack.TryPeek (out Toplevel? newTop))
  202. {
  203. newTop.App = this;
  204. TopRunnable = newTop;
  205. TopRunnable?.SetNeedsDraw ();
  206. }
  207. if (sessionToken.Toplevel is { HasFocus: true })
  208. {
  209. sessionToken.Toplevel.HasFocus = false;
  210. }
  211. if (TopRunnable is { HasFocus: false })
  212. {
  213. TopRunnable.SetFocus ();
  214. }
  215. CachedSessionTokenToplevel = sessionToken.Toplevel;
  216. sessionToken.Toplevel = null;
  217. sessionToken.Dispose ();
  218. // BUGBUG: Why layout and draw here? This causes the screen to be cleared!
  219. //LayoutAndDraw (true);
  220. // TODO: This API is not used (correctly) anywhere; it can be deleted
  221. // TODO: Instead, callers should use the new equivalent of Toplevel.Ready
  222. // TODO: which will be IsRunningChanged with newIsRunning == true
  223. SessionEnded?.Invoke (this, new (CachedSessionTokenToplevel));
  224. }
  225. /// <inheritdoc/>
  226. public void RequestStop () { RequestStop ((Toplevel?)null); }
  227. /// <inheritdoc/>
  228. public void RequestStop (Toplevel? top)
  229. {
  230. Logging.Trace ($"TopRunnable: '{(top is { } ? top : "null")}'");
  231. top ??= TopRunnable;
  232. if (top == null)
  233. {
  234. return;
  235. }
  236. ToplevelClosingEventArgs ev = new (top);
  237. top.OnClosing (ev);
  238. if (ev.Cancel)
  239. {
  240. return;
  241. }
  242. top.Running = false;
  243. }
  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) => _timedEvents.Add (time, callback);
  251. /// <inheritdoc/>
  252. public bool RemoveTimeout (object token) => _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 (TopRunnable 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 (TopRunnable 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. #region IRunnable Support
  291. /// <inheritdoc/>
  292. public RunnableSessionToken Begin (IRunnable runnable)
  293. {
  294. ArgumentNullException.ThrowIfNull (runnable);
  295. // Ensure the mouse is ungrabbed
  296. if (Mouse.MouseGrabView is { })
  297. {
  298. Mouse.UngrabMouse ();
  299. }
  300. // Create session token
  301. RunnableSessionToken token = new (runnable);
  302. // Set the App property if the runnable is a View (needed for IsRunning/IsModal checks)
  303. if (runnable is View runnableView)
  304. {
  305. runnableView.App = this;
  306. }
  307. // Get old IsRunning and IsModal values BEFORE any stack changes
  308. bool oldIsRunning = runnable.IsRunning;
  309. bool oldIsModalValue = runnable.IsModal;
  310. // Raise IsRunningChanging (false -> true) - can be canceled
  311. if (runnable.RaiseIsRunningChanging (oldIsRunning, true))
  312. {
  313. // Starting was canceled
  314. return token;
  315. }
  316. // Push token onto RunnableSessionStack (IsRunning becomes true)
  317. RunnableSessionStack?.Push (token);
  318. // Update TopRunnable to the new top of stack
  319. IRunnable? previousTop = null;
  320. // In Phase 1, Toplevel doesn't implement IRunnable yet
  321. // In Phase 2, it will, and this will work properly
  322. if (TopRunnable is IRunnable r)
  323. {
  324. previousTop = r;
  325. }
  326. // Set TopRunnable (handles both Toplevel and IRunnable)
  327. if (runnable is Toplevel tl)
  328. {
  329. TopRunnable = tl;
  330. }
  331. else if (runnable is View v)
  332. {
  333. // For now, we can't set a non-Toplevel View as TopRunnable
  334. // This is a limitation of the current architecture
  335. // In Phase 2, we'll make TopRunnable an IRunnable property
  336. Logging.Warning ($"WIP on Issue #4148 - Runnable '{runnable}' is a View but not a Toplevel; cannot set as TopRunnable");
  337. }
  338. // Raise IsRunningChanged (now true)
  339. runnable.RaiseIsRunningChangedEvent (true);
  340. // If there was a previous top, it's no longer modal
  341. if (previousTop != null)
  342. {
  343. // Get old IsModal value (should be true before becoming non-modal)
  344. bool oldIsModal = previousTop.IsModal;
  345. // Raise IsModalChanging (true -> false)
  346. previousTop.RaiseIsModalChanging (oldIsModal, false);
  347. // IsModal is now false (derived property)
  348. previousTop.RaiseIsModalChangedEvent (false);
  349. }
  350. // New runnable becomes modal
  351. // Raise IsModalChanging (false -> true) using the old value we captured earlier
  352. runnable.RaiseIsModalChanging (oldIsModalValue, true);
  353. // IsModal is now true (derived property)
  354. runnable.RaiseIsModalChangedEvent (true);
  355. // Initialize if needed
  356. if (runnable is View view && !view.IsInitialized)
  357. {
  358. view.BeginInit ();
  359. view.EndInit ();
  360. // Initialized event is raised by View.EndInit()
  361. }
  362. // Initial Layout and draw
  363. LayoutAndDraw (true);
  364. // Set focus
  365. if (runnable is View viewToFocus && !viewToFocus.HasFocus)
  366. {
  367. viewToFocus.SetFocus ();
  368. }
  369. if (PositionCursor ())
  370. {
  371. Driver?.UpdateCursor ();
  372. }
  373. return token;
  374. }
  375. /// <inheritdoc/>
  376. public void Run (IRunnable runnable, Func<Exception, bool>? errorHandler = null)
  377. {
  378. ArgumentNullException.ThrowIfNull (runnable);
  379. if (!Initialized)
  380. {
  381. throw new NotInitializedException (nameof (Run));
  382. }
  383. // Begin the session (adds to stack, raises IsRunningChanging/IsRunningChanged)
  384. RunnableSessionToken token = Begin (runnable);
  385. try
  386. {
  387. // All runnables block until RequestStop() is called
  388. RunLoop (runnable, errorHandler);
  389. }
  390. finally
  391. {
  392. // End the session (raises IsRunningChanging/IsRunningChanged, pops from stack)
  393. End (token);
  394. }
  395. }
  396. /// <inheritdoc/>
  397. public IApplication Run<TRunnable> (Func<Exception, bool>? errorHandler = null) where TRunnable : IRunnable, new ()
  398. {
  399. if (!Initialized)
  400. {
  401. throw new NotInitializedException (nameof (Run));
  402. }
  403. TRunnable runnable = new ();
  404. // Store the runnable for automatic disposal by Shutdown
  405. FrameworkOwnedRunnable = runnable;
  406. Run (runnable, errorHandler);
  407. return this;
  408. }
  409. private void RunLoop (IRunnable runnable, Func<Exception, bool>? errorHandler)
  410. {
  411. // Main loop - blocks until RequestStop() is called
  412. // Note: IsRunning is a derived property (stack.Contains), so we check it each iteration
  413. var firstIteration = true;
  414. while (runnable.IsRunning)
  415. {
  416. if (Coordinator is null)
  417. {
  418. throw new ($"{nameof (IMainLoopCoordinator)} inexplicably became null during Run");
  419. }
  420. try
  421. {
  422. // Process one iteration of the event loop
  423. Coordinator.RunIteration ();
  424. }
  425. catch (Exception ex)
  426. {
  427. if (errorHandler is null || !errorHandler (ex))
  428. {
  429. throw;
  430. }
  431. }
  432. if (StopAfterFirstIteration && firstIteration)
  433. {
  434. Logging.Information ("Run - Stopping after first iteration as requested");
  435. RequestStop (runnable);
  436. }
  437. firstIteration = false;
  438. }
  439. }
  440. /// <inheritdoc/>
  441. public void End (RunnableSessionToken token)
  442. {
  443. ArgumentNullException.ThrowIfNull (token);
  444. if (token.Runnable is null)
  445. {
  446. return; // Already ended
  447. }
  448. IRunnable runnable = token.Runnable;
  449. // Get old IsRunning value (should be true before stopping)
  450. bool oldIsRunning = runnable.IsRunning;
  451. // Raise IsRunningChanging (true -> false) - can be canceled
  452. // This is where Result should be extracted!
  453. if (runnable.RaiseIsRunningChanging (oldIsRunning, false))
  454. {
  455. // Stopping was canceled
  456. return;
  457. }
  458. // Current runnable is no longer modal
  459. // Get old IsModal value (should be true before becoming non-modal)
  460. bool oldIsModal = runnable.IsModal;
  461. // Raise IsModalChanging (true -> false)
  462. runnable.RaiseIsModalChanging (oldIsModal, false);
  463. // IsModal is now false (will be false after pop)
  464. runnable.RaiseIsModalChangedEvent (false);
  465. // Pop token from RunnableSessionStack (IsRunning becomes false)
  466. if (RunnableSessionStack?.TryPop (out RunnableSessionToken? popped) == true && popped == token)
  467. {
  468. // Restore previous top runnable
  469. if (RunnableSessionStack?.TryPeek (out RunnableSessionToken? previousToken) == true && previousToken?.Runnable is { })
  470. {
  471. IRunnable? previousRunnable = previousToken.Runnable;
  472. // Update TopRunnable if it's a Toplevel
  473. if (previousRunnable is Toplevel tl)
  474. {
  475. TopRunnable = tl;
  476. }
  477. // Previous runnable becomes modal again
  478. // Get old IsModal value (should be false before becoming modal again)
  479. bool oldIsModalValue = previousRunnable.IsModal;
  480. // Raise IsModalChanging (false -> true)
  481. previousRunnable.RaiseIsModalChanging (oldIsModalValue, true);
  482. // IsModal is now true (derived property)
  483. previousRunnable.RaiseIsModalChangedEvent (true);
  484. }
  485. else
  486. {
  487. // No more runnables, clear TopRunnable
  488. if (TopRunnable is IRunnable)
  489. {
  490. TopRunnable = null;
  491. }
  492. }
  493. }
  494. // Raise IsRunningChanged (now false)
  495. runnable.RaiseIsRunningChangedEvent (false);
  496. // Set focus to new TopRunnable if exists
  497. if (TopRunnable is View viewToFocus && !viewToFocus.HasFocus)
  498. {
  499. viewToFocus.SetFocus ();
  500. }
  501. // Clear the token
  502. token.Runnable = null;
  503. }
  504. /// <inheritdoc/>
  505. public void RequestStop (IRunnable? runnable)
  506. {
  507. // Get the runnable to stop
  508. if (runnable is null)
  509. {
  510. // Try to get from TopRunnable
  511. if (TopRunnable is IRunnable r)
  512. {
  513. runnable = r;
  514. }
  515. else
  516. {
  517. return;
  518. }
  519. }
  520. // For Toplevel, use the existing mechanism
  521. if (runnable is Toplevel toplevel)
  522. {
  523. RequestStop (toplevel);
  524. }
  525. // Note: The End() method will be called from the finally block in Run()
  526. // and that's where IsRunningChanging/IsRunningChanged will be raised
  527. }
  528. #endregion IRunnable Support
  529. }