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?