From 67514ad58d8a89b5539a62ad76b7daedfcc8d1ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 06:42:37 +0000 Subject: [PATCH] Phase 4: Make Toplevel implement IRunnable Co-authored-by: tig <585482+tig@users.noreply.github.com> --- Terminal.Gui/Views/Toplevel.cs | 277 ++++++++++++++++++++++++++++++++- 1 file changed, 274 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/Views/Toplevel.cs b/Terminal.Gui/Views/Toplevel.cs index fedc501f4..f3228e0e4 100644 --- a/Terminal.Gui/Views/Toplevel.cs +++ b/Terminal.Gui/Views/Toplevel.cs @@ -17,8 +17,12 @@ namespace Terminal.Gui.Views; /// and run (e.g. s). To run a Toplevel, create the and call /// . /// +/// +/// Toplevel implements to support the runnable session lifecycle with proper event handling +/// following the Cancellable Work Pattern (CWP). +/// /// -public partial class Toplevel : View +public partial class Toplevel : View, IRunnable { /// /// Initializes a new instance of the class, @@ -92,27 +96,60 @@ public partial class Toplevel : View /// public bool IsLoaded { get; private set; } - // TODO: IRunnable: Re-implement as an event on IRunnable; IRunnable.Activating/Activate /// Invoked when the Toplevel active. + /// + /// + /// Obsolete: Use instead. This event is maintained for backward + /// compatibility and will be raised alongside the new event. + /// + /// + [Obsolete ("Use IRunnable.Activated instead. This event is maintained for backward compatibility.")] public event EventHandler? Activate; - // TODO: IRunnable: Re-implement as an event on IRunnable; IRunnable.Deactivating/Deactivate? /// Invoked when the Toplevel ceases to be active. + /// + /// + /// Obsolete: Use instead. This event is maintained for backward + /// compatibility and will be raised alongside the new event. + /// + /// + [Obsolete ("Use IRunnable.Deactivated instead. This event is maintained for backward compatibility.")] public event EventHandler? Deactivate; /// Invoked when the Toplevel's is closed by . + /// + /// + /// Obsolete: This event is maintained for backward compatibility. The IRunnable architecture + /// combines this functionality into the event. + /// + /// + [Obsolete ("Use IRunnable.Stopped instead. This event is maintained for backward compatibility.")] public event EventHandler? Closed; /// /// Invoked when the Toplevel's is being closed by /// . /// + /// + /// + /// Obsolete: Use instead. This event is maintained for backward + /// compatibility and will be raised alongside the new event. + /// + /// + [Obsolete ("Use IRunnable.Stopping instead. This event is maintained for backward compatibility.")] public event EventHandler? Closing; /// /// Invoked when the has begun to be loaded. A Loaded event handler /// is a good place to finalize initialization before calling Run. /// + /// + /// + /// Obsolete: Use instead. The Loaded event conceptually maps to + /// the Initialized event from the ISupportInitialize pattern, which is now part of IRunnable. + /// + /// + [Obsolete ("Use View.Initialized instead. The Loaded event maps to the Initialized event from ISupportInitialize.")] public event EventHandler? Loaded; /// @@ -144,6 +181,13 @@ public partial class Toplevel : View /// on this . /// /// + /// + /// + /// Obsolete: Use instead. The Ready event is similar to Initialized + /// but was fired later in the lifecycle. The IRunnable architecture consolidates these into Initialized. + /// + /// + [Obsolete ("Use View.Initialized instead. The Ready event is consolidated into the Initialized event.")] public event EventHandler? Ready; /// @@ -159,6 +203,13 @@ public partial class Toplevel : View /// Invoked when the Toplevel has been unloaded. A Unloaded event handler is a good place /// to dispose objects after calling . /// + /// + /// + /// Obsolete: Use instead. The Unloaded event is consolidated into + /// the Stopped event in the IRunnable architecture. + /// + /// + [Obsolete ("Use IRunnable.Stopped instead. The Unloaded event is consolidated into the Stopped event.")] public event EventHandler? Unloaded; internal virtual void OnActivate (Toplevel deactivated) { Activate?.Invoke (this, new (deactivated)); } @@ -203,6 +254,226 @@ public partial class Toplevel : View #endregion + #region IRunnable Implementation + + // Note: Running property is already defined above in the Life Cycle region (line 86) + // Note: Initializing and Initialized events are inherited from View (ISupportInitialize pattern) + + /// + public event EventHandler? Stopping; + + /// + public event EventHandler? Stopped; + + /// + public event EventHandler? Activating; + + /// + public event EventHandler? Activated; + + /// + public event EventHandler? Deactivating; + + /// + public event EventHandler? Deactivated; + + /// + public virtual void RaiseStoppingEvent () + { + // CWP Phase 1: Pre-notification via virtual method (can cancel) + if (OnStopping ()) + { + return; // Stopping canceled + } + + // CWP Phase 2: Event notification (can cancel) + var args = new System.ComponentModel.CancelEventArgs (); + Stopping?.Invoke (this, args); + + if (args.Cancel) + { + return; // Stopping canceled + } + + // CWP Phase 3: Perform the work (stop the session) + Running = false; + + // CWP Phase 4: Post-notification via virtual method + OnStopped (); + + // CWP Phase 5: Post-notification event + Stopped?.Invoke (this, EventArgs.Empty); + } + + /// + public virtual bool RaiseActivatingEvent (IRunnable? deactivated) + { + // CWP Phase 1: Pre-notification via virtual method (can cancel) + if (OnActivating (deactivated)) + { + return true; // Activation canceled + } + + // CWP Phase 2: Event notification (can cancel) + var args = new RunnableActivatingEventArgs (this, deactivated); + Activating?.Invoke (this, args); + + if (args.Cancel) + { + return true; // Activation canceled + } + + // CWP Phase 3: Work is done by Application (setting Current) + // CWP Phase 4 & 5: Call post-notification methods + OnActivated (deactivated); + + return false; // Activation succeeded + } + + /// + public virtual void RaiseActivatedEvent (IRunnable? deactivated) + { + Activated?.Invoke (this, new RunnableEventArgs (this)); + } + + /// + public virtual bool RaiseDeactivatingEvent (IRunnable? activated) + { + // CWP Phase 1: Pre-notification via virtual method (can cancel) + if (OnDeactivating (activated)) + { + return true; // Deactivation canceled + } + + // CWP Phase 2: Event notification (can cancel) + var args = new RunnableDeactivatingEventArgs (this, activated); + Deactivating?.Invoke (this, args); + + if (args.Cancel) + { + return true; // Deactivation canceled + } + + // CWP Phase 3: Work is done by Application (changing Current) + // CWP Phase 4 & 5: Call post-notification methods + OnDeactivated (activated); + + return false; // Deactivation succeeded + } + + /// + public virtual void RaiseDeactivatedEvent (IRunnable? activated) + { + Deactivated?.Invoke (this, new RunnableEventArgs (this)); + } + + /// + /// Called before event. Override to cancel stopping. + /// + /// to cancel; to proceed. + /// + /// + /// This is the first phase of the Cancellable Work Pattern for stopping. + /// Default implementation calls the legacy method for backward compatibility. + /// + /// + protected virtual bool OnStopping () + { + // For backward compatibility, delegate to legacy OnClosing method + var ev = new ToplevelClosingEventArgs (this); + return OnClosing (ev); + } + + /// + /// Called after session has stopped. Override for post-stop cleanup. + /// + /// + /// Default implementation does nothing. For backward compatibility, the legacy + /// event is raised by Application.End(). + /// + protected virtual void OnStopped () + { + // Default: do nothing + // Note: Legacy Closed event is raised by Application.End() + } + + /// + /// Called before event. Override to cancel activation. + /// + /// The previously active runnable being deactivated, or null if none. + /// to cancel; to proceed. + /// + /// Default implementation returns false (allow activation). For backward compatibility, + /// the legacy method is called after activation succeeds. + /// + protected virtual bool OnActivating (IRunnable? deactivated) + { + return false; // Default: allow activation + } + + /// + /// Called after activation succeeds. Override for post-activation logic. + /// + /// The previously active runnable that was deactivated, or null if none. + /// + /// Default implementation raises the event and calls the legacy + /// method for backward compatibility. + /// + protected virtual void OnActivated (IRunnable? deactivated) + { + RaiseActivatedEvent (deactivated); + + // For backward compatibility, call legacy OnActivate if deactivated is a Toplevel + if (deactivated is Toplevel tl) + { + OnActivate (tl); + } + else + { + // If not a Toplevel, still raise the legacy Activate event with null + Activate?.Invoke (this, new ToplevelEventArgs (null)); + } + } + + /// + /// Called before event. Override to cancel deactivation. + /// + /// The newly activated runnable, or null if none. + /// to cancel; to proceed. + /// + /// Default implementation returns false (allow deactivation). + /// + protected virtual bool OnDeactivating (IRunnable? activated) + { + return false; // Default: allow deactivation + } + + /// + /// Called after deactivation succeeds. Override for post-deactivation logic. + /// + /// The newly activated runnable, or null if none. + /// + /// Default implementation raises the event and calls the legacy + /// method for backward compatibility. + /// + protected virtual void OnDeactivated (IRunnable? activated) + { + RaiseDeactivatedEvent (activated); + + // For backward compatibility, call legacy OnDeactivate if activated is a Toplevel + if (activated is Toplevel tl) + { + OnDeactivate (tl); + } + else + { + // If not a Toplevel, still raise the legacy Deactivate event with null + Deactivate?.Invoke (this, new ToplevelEventArgs (null)); + } + } + + #endregion + #region Size / Position Management // TODO: Make cancelable?