Runnable.cs 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. namespace Terminal.Gui.ViewBase;
  2. /// <summary>
  3. /// Base implementation of <see cref="IRunnable"/> for views that can be run as blocking sessions without returning a result.
  4. /// </summary>
  5. /// <remarks>
  6. /// <para>
  7. /// Views that don't need to return a result can derive from this class instead of <see cref="Runnable{TResult}"/>.
  8. /// </para>
  9. /// <para>
  10. /// This class provides default implementations of the <see cref="IRunnable"/> interface
  11. /// following the Terminal.Gui Cancellable Work Pattern (CWP).
  12. /// </para>
  13. /// <para>
  14. /// For views that need to return a result, use <see cref="Runnable{TResult}"/> instead.
  15. /// </para>
  16. /// </remarks>
  17. public class Runnable : View, IRunnable
  18. {
  19. // Cached state - eliminates race conditions from stack queries
  20. private bool _isRunning;
  21. private bool _isModal;
  22. /// <summary>
  23. /// Constructs a new instance of the <see cref="Runnable"/> class.
  24. /// </summary>
  25. public Runnable ()
  26. {
  27. CanFocus = true;
  28. TabStop = TabBehavior.TabGroup;
  29. Arrangement = ViewArrangement.Overlapped;
  30. Width = Dim.Fill ();
  31. Height = Dim.Fill ();
  32. SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Runnable);
  33. }
  34. /// <inheritdoc/>
  35. public object? Result { get; set; }
  36. #region IRunnable Implementation - IsRunning (from base interface)
  37. /// <inheritdoc />
  38. public void SetApp (IApplication app)
  39. {
  40. App = app;
  41. }
  42. /// <inheritdoc/>
  43. public bool IsRunning => _isRunning;
  44. /// <inheritdoc/>
  45. public void SetIsRunning (bool value) { _isRunning = value; }
  46. /// <inheritdoc />
  47. public virtual void RequestStop ()
  48. {
  49. // Use the IRunnable-specific RequestStop if the App supports it
  50. App?.RequestStop (this);
  51. }
  52. /// <inheritdoc/>
  53. public bool RaiseIsRunningChanging (bool oldIsRunning, bool newIsRunning)
  54. {
  55. // Clear previous result when starting (for non-generic Runnable)
  56. // Derived Runnable<TResult> will clear its typed Result in OnIsRunningChanging override
  57. if (newIsRunning)
  58. {
  59. Result = null;
  60. }
  61. // CWP Phase 1: Virtual method (pre-notification)
  62. if (OnIsRunningChanging (oldIsRunning, newIsRunning))
  63. {
  64. return true; // Canceled
  65. }
  66. // CWP Phase 2: Event notification
  67. bool newValue = newIsRunning;
  68. CancelEventArgs<bool> args = new (in oldIsRunning, ref newValue);
  69. IsRunningChanging?.Invoke (this, args);
  70. return args.Cancel;
  71. }
  72. /// <inheritdoc/>
  73. public event EventHandler<CancelEventArgs<bool>>? IsRunningChanging;
  74. /// <inheritdoc/>
  75. public void RaiseIsRunningChangedEvent (bool newIsRunning)
  76. {
  77. // Initialize if needed when starting
  78. if (newIsRunning && !IsInitialized)
  79. {
  80. BeginInit ();
  81. EndInit ();
  82. // Initialized event is raised by View.EndInit()
  83. }
  84. // CWP Phase 3: Post-notification (work already done by Application.Begin/End)
  85. OnIsRunningChanged (newIsRunning);
  86. EventArgs<bool> args = new (newIsRunning);
  87. IsRunningChanged?.Invoke (this, args);
  88. }
  89. /// <inheritdoc/>
  90. public event EventHandler<EventArgs<bool>>? IsRunningChanged;
  91. /// <summary>
  92. /// Called before <see cref="IsRunningChanging"/> event. Override to cancel state change or perform cleanup.
  93. /// </summary>
  94. /// <param name="oldIsRunning">The current value of <see cref="IsRunning"/>.</param>
  95. /// <param name="newIsRunning">The new value of <see cref="IsRunning"/> (true = starting, false = stopping).</param>
  96. /// <returns><see langword="true"/> to cancel; <see langword="false"/> to proceed.</returns>
  97. /// <remarks>
  98. /// <para>
  99. /// Default implementation returns <see langword="false"/> (allow change).
  100. /// </para>
  101. /// <para>
  102. /// <b>IMPORTANT</b>: When <paramref name="newIsRunning"/> is <see langword="false"/> (stopping), this is the ideal
  103. /// place to perform cleanup or validation before the runnable is removed from the stack.
  104. /// </para>
  105. /// <example>
  106. /// <code>
  107. /// protected override bool OnIsRunningChanging (bool oldIsRunning, bool newIsRunning)
  108. /// {
  109. /// if (!newIsRunning) // Stopping
  110. /// {
  111. /// // Check if user wants to save first
  112. /// if (HasUnsavedChanges ())
  113. /// {
  114. /// int result = MessageBox.Query (App, "Save?", "Save changes?", "Yes", "No", "Cancel");
  115. /// if (result == 2) return true; // Cancel stopping
  116. /// if (result == 0) Save ();
  117. /// }
  118. /// }
  119. ///
  120. /// return base.OnIsRunningChanging (oldIsRunning, newIsRunning);
  121. /// }
  122. /// </code>
  123. /// </example>
  124. /// </remarks>
  125. protected virtual bool OnIsRunningChanging (bool oldIsRunning, bool newIsRunning) => false;
  126. /// <summary>
  127. /// Called after <see cref="IsRunning"/> has changed. Override for post-state-change logic.
  128. /// </summary>
  129. /// <param name="newIsRunning">The new value of <see cref="IsRunning"/> (true = started, false = stopped).</param>
  130. /// <remarks>
  131. /// Default implementation does nothing. Overrides should call base to ensure extensibility.
  132. /// </remarks>
  133. protected virtual void OnIsRunningChanged (bool newIsRunning)
  134. {
  135. // Default: no-op
  136. }
  137. #endregion
  138. #region IRunnable Implementation - IsModal (from base interface)
  139. /// <inheritdoc/>
  140. public bool IsModal => _isModal;
  141. /// <inheritdoc/>
  142. public void SetIsModal (bool value) { _isModal = value; }
  143. /// <inheritdoc />
  144. public bool StopRequested { get; set; }
  145. /// <inheritdoc/>
  146. public void RaiseIsModalChangedEvent (bool newIsModal)
  147. {
  148. // Layout may need to change when modal state changes
  149. SetNeedsLayout ();
  150. SetNeedsDraw ();
  151. if (newIsModal)
  152. {
  153. // Set focus to self if becoming modal
  154. if (HasFocus is false)
  155. {
  156. SetFocus ();
  157. }
  158. // Position cursor and update driver
  159. if (App?.PositionCursor () == true)
  160. {
  161. App?.Driver?.UpdateCursor ();
  162. }
  163. }
  164. // CWP Phase 3: Post-notification (work already done by Application)
  165. OnIsModalChanged (newIsModal);
  166. EventArgs<bool> args = new (newIsModal);
  167. IsModalChanged?.Invoke (this, args);
  168. }
  169. /// <inheritdoc/>
  170. public event EventHandler<EventArgs<bool>>? IsModalChanged;
  171. /// <summary>
  172. /// Called after <see cref="IsModal"/> has changed. Override for post-activation logic.
  173. /// </summary>
  174. /// <param name="newIsModal">The new value of <see cref="IsModal"/> (true = became modal, false = no longer modal).</param>
  175. /// <remarks>
  176. /// <para>
  177. /// Default implementation does nothing. Overrides should call base to ensure extensibility.
  178. /// </para>
  179. /// <para>
  180. /// Common uses: setting focus when becoming modal, updating UI state.
  181. /// </para>
  182. /// </remarks>
  183. protected virtual void OnIsModalChanged (bool newIsModal)
  184. {
  185. // Default: no-op
  186. }
  187. #endregion
  188. }