Runnable.cs 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. namespace Terminal.Gui.ViewBase;
  2. /// <summary>
  3. /// Base implementation of <see cref="IRunnable{TResult}"/> for views that can be run as blocking sessions.
  4. /// </summary>
  5. /// <typeparam name="TResult">The type of result data returned when the session completes.</typeparam>
  6. /// <remarks>
  7. /// <para>
  8. /// Views can derive from this class or implement <see cref="IRunnable{TResult}"/> directly.
  9. /// </para>
  10. /// <para>
  11. /// This class provides default implementations of the <see cref="IRunnable{TResult}"/> interface
  12. /// following the Terminal.Gui Cancellable Work Pattern (CWP).
  13. /// </para>
  14. /// </remarks>
  15. public class Runnable<TResult> : View, IRunnable<TResult>
  16. {
  17. /// <inheritdoc/>
  18. public TResult? Result { get; set; }
  19. #region IRunnable Implementation - IsRunning (from base interface)
  20. /// <inheritdoc/>
  21. public bool IsRunning => App?.RunnableSessionStack?.Any (token => token.Runnable == this) ?? false;
  22. /// <inheritdoc/>
  23. public bool RaiseIsRunningChanging (bool oldIsRunning, bool newIsRunning)
  24. {
  25. // Clear previous result when starting
  26. if (newIsRunning)
  27. {
  28. Result = default (TResult);
  29. }
  30. // CWP Phase 1: Virtual method (pre-notification)
  31. if (OnIsRunningChanging (oldIsRunning, newIsRunning))
  32. {
  33. return true; // Canceled
  34. }
  35. // CWP Phase 2: Event notification
  36. bool newValue = newIsRunning;
  37. CancelEventArgs<bool> args = new (in oldIsRunning, ref newValue);
  38. IsRunningChanging?.Invoke (this, args);
  39. return args.Cancel;
  40. }
  41. /// <inheritdoc/>
  42. public event EventHandler<CancelEventArgs<bool>>? IsRunningChanging;
  43. /// <inheritdoc/>
  44. public void RaiseIsRunningChangedEvent (bool newIsRunning)
  45. {
  46. // CWP Phase 3: Post-notification (work already done by Application.Begin/End)
  47. OnIsRunningChanged (newIsRunning);
  48. EventArgs<bool> args = new (newIsRunning);
  49. IsRunningChanged?.Invoke (this, args);
  50. }
  51. /// <inheritdoc/>
  52. public event EventHandler<EventArgs<bool>>? IsRunningChanged;
  53. /// <summary>
  54. /// Called before <see cref="IsRunningChanging"/> event. Override to cancel state change or extract
  55. /// <see cref="Result"/>.
  56. /// </summary>
  57. /// <param name="oldIsRunning">The current value of <see cref="IsRunning"/>.</param>
  58. /// <param name="newIsRunning">The new value of <see cref="IsRunning"/> (true = starting, false = stopping).</param>
  59. /// <returns><see langword="true"/> to cancel; <see langword="false"/> to proceed.</returns>
  60. /// <remarks>
  61. /// <para>
  62. /// Default implementation returns <see langword="false"/> (allow change).
  63. /// </para>
  64. /// <para>
  65. /// <b>IMPORTANT</b>: When <paramref name="newIsRunning"/> is <see langword="false"/> (stopping), this is the ideal
  66. /// place
  67. /// to extract <see cref="Result"/> from views before the runnable is removed from the stack.
  68. /// At this point, all views are still alive and accessible, and subscribers can inspect the result
  69. /// and optionally cancel the stop.
  70. /// </para>
  71. /// <example>
  72. /// <code>
  73. /// protected override bool OnIsRunningChanging (bool oldIsRunning, bool newIsRunning)
  74. /// {
  75. /// if (!newIsRunning) // Stopping
  76. /// {
  77. /// // Extract result before removal from stack
  78. /// Result = _textField.Text;
  79. ///
  80. /// // Or check if user wants to save first
  81. /// if (HasUnsavedChanges ())
  82. /// {
  83. /// int result = MessageBox.Query (App, "Save?", "Save changes?", "Yes", "No", "Cancel");
  84. /// if (result == 2) return true; // Cancel stopping
  85. /// if (result == 0) Save ();
  86. /// }
  87. /// }
  88. ///
  89. /// return base.OnIsRunningChanging (oldIsRunning, newIsRunning);
  90. /// }
  91. /// </code>
  92. /// </example>
  93. /// </remarks>
  94. protected virtual bool OnIsRunningChanging (bool oldIsRunning, bool newIsRunning) => false;
  95. /// <summary>
  96. /// Called after <see cref="IsRunning"/> has changed. Override for post-state-change logic.
  97. /// </summary>
  98. /// <param name="newIsRunning">The new value of <see cref="IsRunning"/> (true = started, false = stopped).</param>
  99. /// <remarks>
  100. /// Default implementation does nothing. Overrides should call base to ensure extensibility.
  101. /// </remarks>
  102. protected virtual void OnIsRunningChanged (bool newIsRunning)
  103. {
  104. // Default: no-op
  105. }
  106. #endregion
  107. #region IRunnable Implementation - IsModal (from base interface)
  108. /// <inheritdoc/>
  109. public bool IsModal
  110. {
  111. get
  112. {
  113. if (App is null)
  114. {
  115. return false;
  116. }
  117. // Check if this runnable is at the top of the RunnableSessionStack
  118. // The top of the stack is the modal runnable
  119. if (App.RunnableSessionStack is { } && App.RunnableSessionStack.TryPeek (out RunnableSessionToken? topToken))
  120. {
  121. return topToken?.Runnable == this;
  122. }
  123. // Fallback: Check if this is the TopRunnable (for Toplevel compatibility)
  124. // In Phase 1, TopRunnable is still Toplevel?, so we need to check both cases
  125. if (this is Toplevel tl && App.TopRunnable == tl)
  126. {
  127. return true;
  128. }
  129. return false;
  130. }
  131. }
  132. /// <inheritdoc/>
  133. public bool RaiseIsModalChanging (bool oldIsModal, bool newIsModal)
  134. {
  135. // CWP Phase 1: Virtual method (pre-notification)
  136. if (OnIsModalChanging (oldIsModal, newIsModal))
  137. {
  138. return true; // Canceled
  139. }
  140. // CWP Phase 2: Event notification
  141. bool newValue = newIsModal;
  142. CancelEventArgs<bool> args = new (in oldIsModal, ref newValue);
  143. IsModalChanging?.Invoke (this, args);
  144. return args.Cancel;
  145. }
  146. /// <inheritdoc/>
  147. public event EventHandler<CancelEventArgs<bool>>? IsModalChanging;
  148. /// <inheritdoc/>
  149. public void RaiseIsModalChangedEvent (bool newIsModal)
  150. {
  151. // CWP Phase 3: Post-notification (work already done by Application)
  152. OnIsModalChanged (newIsModal);
  153. EventArgs<bool> args = new (newIsModal);
  154. IsModalChanged?.Invoke (this, args);
  155. }
  156. /// <inheritdoc/>
  157. public event EventHandler<EventArgs<bool>>? IsModalChanged;
  158. /// <summary>
  159. /// Called before <see cref="IsModalChanging"/> event. Override to cancel activation/deactivation.
  160. /// </summary>
  161. /// <param name="oldIsModal">The current value of <see cref="IsModal"/>.</param>
  162. /// <param name="newIsModal">The new value of <see cref="IsModal"/> (true = becoming modal/top, false = no longer modal).</param>
  163. /// <returns><see langword="true"/> to cancel; <see langword="false"/> to proceed.</returns>
  164. /// <remarks>
  165. /// Default implementation returns <see langword="false"/> (allow change).
  166. /// </remarks>
  167. protected virtual bool OnIsModalChanging (bool oldIsModal, bool newIsModal) => false;
  168. /// <summary>
  169. /// Called after <see cref="IsModal"/> has changed. Override for post-activation logic.
  170. /// </summary>
  171. /// <param name="newIsModal">The new value of <see cref="IsModal"/> (true = became modal, false = no longer modal).</param>
  172. /// <remarks>
  173. /// <para>
  174. /// Default implementation does nothing. Overrides should call base to ensure extensibility.
  175. /// </para>
  176. /// <para>
  177. /// Common uses: setting focus when becoming modal, updating UI state.
  178. /// </para>
  179. /// </remarks>
  180. protected virtual void OnIsModalChanged (bool newIsModal)
  181. {
  182. // Default: no-op
  183. }
  184. #endregion
  185. /// <summary>
  186. /// Requests that this runnable session stop.
  187. /// </summary>
  188. public virtual void RequestStop ()
  189. {
  190. // Use the IRunnable-specific RequestStop if the App supports it
  191. App?.RequestStop (this);
  192. }
  193. }