using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
namespace Terminal.Gui.App;
public partial class ApplicationImpl
{
#region Begin->Run->Stop->End
// TODO: This API is not used anywhere; it can be deleted
///
public event EventHandler? SessionBegun;
// TODO: This API is not used anywhere; it can be deleted
///
public event EventHandler? SessionEnded;
///
public SessionToken Begin (Toplevel toplevel)
{
ArgumentNullException.ThrowIfNull (toplevel);
// Ensure the mouse is ungrabbed.
if (Mouse.MouseGrabView is { })
{
Mouse.UngrabMouse ();
}
var rs = new SessionToken (toplevel);
#if DEBUG_IDISPOSABLE
if (View.EnableDebugIDisposableAsserts && TopRunnable is { } && toplevel != TopRunnable && !SessionStack.Contains (TopRunnable))
{
// This assertion confirm if the TopRunnable was already disposed
Debug.Assert (TopRunnable.WasDisposed);
Debug.Assert (TopRunnable == CachedSessionTokenToplevel);
}
#endif
lock (SessionStack)
{
if (TopRunnable is { } && toplevel != TopRunnable && !SessionStack.Contains (TopRunnable))
{
// If TopRunnable was already disposed and isn't on the Toplevels Stack,
// clean it up here if is the same as _CachedSessionTokenToplevel
if (TopRunnable == CachedSessionTokenToplevel)
{
TopRunnable = null;
}
else
{
// Probably this will never hit
throw new ObjectDisposedException (TopRunnable.GetType ().FullName);
}
}
// BUGBUG: We should not depend on `Id` internally.
// BUGBUG: It is super unclear what this code does anyway.
if (string.IsNullOrEmpty (toplevel.Id))
{
var count = 1;
var id = (SessionStack.Count + count).ToString ();
while (SessionStack.Count > 0 && SessionStack.FirstOrDefault (x => x.Id == id) is { })
{
count++;
id = (SessionStack.Count + count).ToString ();
}
toplevel.Id = (SessionStack.Count + count).ToString ();
SessionStack.Push (toplevel);
}
else
{
Toplevel? dup = SessionStack.FirstOrDefault (x => x.Id == toplevel.Id);
if (dup is null)
{
SessionStack.Push (toplevel);
}
}
}
if (TopRunnable is null)
{
toplevel.App = this;
TopRunnable = toplevel;
}
if ((TopRunnable?.Modal == false && toplevel.Modal)
|| (TopRunnable?.Modal == false && !toplevel.Modal)
|| (TopRunnable?.Modal == true && toplevel.Modal))
{
if (toplevel.Visible)
{
if (TopRunnable is { HasFocus: true })
{
TopRunnable.HasFocus = false;
}
// Force leave events for any entered views in the old TopRunnable
if (Mouse.LastMousePosition is { })
{
Mouse.RaiseMouseEnterLeaveEvents (Mouse.LastMousePosition!.Value, new ());
}
TopRunnable?.OnDeactivate (toplevel);
Toplevel previousTop = TopRunnable!;
TopRunnable = toplevel;
TopRunnable.App = this;
TopRunnable.OnActivate (previousTop);
}
}
// View implements ISupportInitializeNotification which is derived from ISupportInitialize
if (!toplevel.IsInitialized)
{
toplevel.BeginInit ();
toplevel.EndInit (); // Calls Layout
}
// Try to set initial focus to any TabStop
if (!toplevel.HasFocus)
{
toplevel.SetFocus ();
}
toplevel.OnLoaded ();
LayoutAndDraw (true);
if (PositionCursor ())
{
Driver?.UpdateCursor ();
}
SessionBegun?.Invoke (this, new (rs));
return rs;
}
///
public bool StopAfterFirstIteration { get; set; }
///
public void RaiseIteration ()
{
Iteration?.Invoke (null, new (this));
}
///
public event EventHandler>? Iteration;
///
[RequiresUnreferencedCode ("AOT")]
[RequiresDynamicCode ("AOT")]
public Toplevel Run (Func? errorHandler = null, string? driverName = null) => Run (errorHandler, driverName);
///
[RequiresUnreferencedCode ("AOT")]
[RequiresDynamicCode ("AOT")]
public TView Run (Func? errorHandler = null, string? driverName = null)
where TView : Toplevel, new ()
{
if (!Initialized)
{
// Init() has NOT been called. Auto-initialize as per interface contract.
Init (driverName);
}
TView top = new ();
Run (top, errorHandler);
return top;
}
///
public void Run (Toplevel view, Func? errorHandler = null)
{
Logging.Information ($"Run '{view}'");
ArgumentNullException.ThrowIfNull (view);
if (!Initialized)
{
throw new NotInitializedException (nameof (Run));
}
if (Driver == null)
{
throw new InvalidOperationException ("Driver was inexplicably null when trying to Run view");
}
TopRunnable = view;
SessionToken rs = Begin (view);
TopRunnable.Running = true;
var firstIteration = true;
while (SessionStack.TryPeek (out Toplevel? found) && found == view && view.Running)
{
if (Coordinator is null)
{
throw new ($"{nameof (IMainLoopCoordinator)} inexplicably became null during Run");
}
Coordinator.RunIteration ();
if (StopAfterFirstIteration && firstIteration)
{
Logging.Information ("Run - Stopping after first iteration as requested");
RequestStop ((Toplevel?)view);
}
firstIteration = false;
}
Logging.Information ("Run - Calling End");
End (rs);
}
///
public void End (SessionToken sessionToken)
{
ArgumentNullException.ThrowIfNull (sessionToken);
if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover)
{
ApplicationPopover.HideWithQuitCommand (visiblePopover);
}
sessionToken.Toplevel?.OnUnloaded ();
// End the Session
// First, take it off the Toplevel Stack
if (SessionStack.TryPop (out Toplevel? topOfStack))
{
if (topOfStack != sessionToken.Toplevel)
{
// If the top of the stack is not the SessionToken.Toplevel then
// this call to End is not balanced with the call to Begin that started the Session
throw new ArgumentException ("End must be balanced with calls to Begin");
}
}
// Notify that it is closing
sessionToken.Toplevel?.OnClosed (sessionToken.Toplevel);
if (SessionStack.TryPeek (out Toplevel? newTop))
{
newTop.App = this;
TopRunnable = newTop;
TopRunnable?.SetNeedsDraw ();
}
if (sessionToken.Toplevel is { HasFocus: true })
{
sessionToken.Toplevel.HasFocus = false;
}
if (TopRunnable is { HasFocus: false })
{
TopRunnable.SetFocus ();
}
CachedSessionTokenToplevel = sessionToken.Toplevel;
sessionToken.Toplevel = null;
sessionToken.Dispose ();
// BUGBUG: Why layout and draw here? This causes the screen to be cleared!
//LayoutAndDraw (true);
// TODO: This API is not used (correctly) anywhere; it can be deleted
// TODO: Instead, callers should use the new equivalent of Toplevel.Ready
// TODO: which will be IsRunningChanged with newIsRunning == true
SessionEnded?.Invoke (this, new (CachedSessionTokenToplevel));
}
///
public void RequestStop () { RequestStop ((Toplevel?)null); }
///
public void RequestStop (Toplevel? top)
{
Logging.Trace ($"TopRunnable: '{(top is { } ? top : "null")}'");
top ??= TopRunnable;
if (top == null)
{
return;
}
ToplevelClosingEventArgs ev = new (top);
top.OnClosing (ev);
if (ev.Cancel)
{
return;
}
top.Running = false;
}
#endregion Begin->Run->Stop->End
#region Timeouts and Invoke
private readonly ITimedEvents _timedEvents = new TimedEvents ();
///
public ITimedEvents? TimedEvents => _timedEvents;
///
public object AddTimeout (TimeSpan time, Func callback) => _timedEvents.Add (time, callback);
///
public bool RemoveTimeout (object token) => _timedEvents.Remove (token);
///
public void Invoke (Action? action)
{
// If we are already on the main UI thread
if (TopRunnable is { Running: true } && MainThreadId == Thread.CurrentThread.ManagedThreadId)
{
action?.Invoke (this);
return;
}
_timedEvents.Add (
TimeSpan.Zero,
() =>
{
action?.Invoke (this);
return false;
}
);
}
///
public void Invoke (Action action)
{
// If we are already on the main UI thread
if (TopRunnable is { Running: true } && MainThreadId == Thread.CurrentThread.ManagedThreadId)
{
action?.Invoke ();
return;
}
_timedEvents.Add (
TimeSpan.Zero,
() =>
{
action?.Invoke ();
return false;
}
);
}
#endregion Timeouts and Invoke
#region IRunnable Support
///
public RunnableSessionToken Begin (IRunnable runnable)
{
ArgumentNullException.ThrowIfNull (runnable);
// Ensure the mouse is ungrabbed
if (Mouse.MouseGrabView is { })
{
Mouse.UngrabMouse ();
}
// Create session token
RunnableSessionToken token = new (runnable);
// Set the App property if the runnable is a View (needed for IsRunning/IsModal checks)
if (runnable is View runnableView)
{
runnableView.App = this;
}
// Get old IsRunning and IsModal values BEFORE any stack changes
bool oldIsRunning = runnable.IsRunning;
bool oldIsModalValue = runnable.IsModal;
// Raise IsRunningChanging (false -> true) - can be canceled
if (runnable.RaiseIsRunningChanging (oldIsRunning, true))
{
// Starting was canceled
return token;
}
// Push token onto RunnableSessionStack (IsRunning becomes true)
RunnableSessionStack?.Push (token);
// Update TopRunnable to the new top of stack
IRunnable? previousTop = null;
// In Phase 1, Toplevel doesn't implement IRunnable yet
// In Phase 2, it will, and this will work properly
if (TopRunnable is IRunnable r)
{
previousTop = r;
}
// Set TopRunnable (handles both Toplevel and IRunnable)
if (runnable is Toplevel tl)
{
TopRunnable = tl;
}
else if (runnable is View v)
{
// For now, we can't set a non-Toplevel View as TopRunnable
// This is a limitation of the current architecture
// In Phase 2, we'll make TopRunnable an IRunnable property
Logging.Warning ($"WIP on Issue #4148 - Runnable '{runnable}' is a View but not a Toplevel; cannot set as TopRunnable");
}
// Raise IsRunningChanged (now true)
runnable.RaiseIsRunningChangedEvent (true);
// If there was a previous top, it's no longer modal
if (previousTop != null)
{
// Get old IsModal value (should be true before becoming non-modal)
bool oldIsModal = previousTop.IsModal;
// Raise IsModalChanging (true -> false)
previousTop.RaiseIsModalChanging (oldIsModal, false);
// IsModal is now false (derived property)
previousTop.RaiseIsModalChangedEvent (false);
}
// New runnable becomes modal
// Raise IsModalChanging (false -> true) using the old value we captured earlier
runnable.RaiseIsModalChanging (oldIsModalValue, true);
// IsModal is now true (derived property)
runnable.RaiseIsModalChangedEvent (true);
// Initialize if needed
if (runnable is View view && !view.IsInitialized)
{
view.BeginInit ();
view.EndInit ();
// Initialized event is raised by View.EndInit()
}
// Initial Layout and draw
LayoutAndDraw (true);
// Set focus
if (runnable is View viewToFocus && !viewToFocus.HasFocus)
{
viewToFocus.SetFocus ();
}
if (PositionCursor ())
{
Driver?.UpdateCursor ();
}
return token;
}
///
public void Run (IRunnable runnable, Func? errorHandler = null)
{
ArgumentNullException.ThrowIfNull (runnable);
if (!Initialized)
{
throw new NotInitializedException (nameof (Run));
}
// Begin the session (adds to stack, raises IsRunningChanging/IsRunningChanged)
RunnableSessionToken token = Begin (runnable);
try
{
// All runnables block until RequestStop() is called
RunLoop (runnable, errorHandler);
}
finally
{
// End the session (raises IsRunningChanging/IsRunningChanged, pops from stack)
End (token);
}
}
///
public IApplication Run (Func? errorHandler = null) where TRunnable : IRunnable, new ()
{
if (!Initialized)
{
throw new NotInitializedException (nameof (Run));
}
TRunnable runnable = new ();
// Store the runnable for automatic disposal by Shutdown
FrameworkOwnedRunnable = runnable;
Run (runnable, errorHandler);
return this;
}
private void RunLoop (IRunnable runnable, Func? errorHandler)
{
// Main loop - blocks until RequestStop() is called
// Note: IsRunning is a derived property (stack.Contains), so we check it each iteration
var firstIteration = true;
while (runnable.IsRunning)
{
if (Coordinator is null)
{
throw new ($"{nameof (IMainLoopCoordinator)} inexplicably became null during Run");
}
try
{
// Process one iteration of the event loop
Coordinator.RunIteration ();
}
catch (Exception ex)
{
if (errorHandler is null || !errorHandler (ex))
{
throw;
}
}
if (StopAfterFirstIteration && firstIteration)
{
Logging.Information ("Run - Stopping after first iteration as requested");
RequestStop (runnable);
}
firstIteration = false;
}
}
///
public void End (RunnableSessionToken token)
{
ArgumentNullException.ThrowIfNull (token);
if (token.Runnable is null)
{
return; // Already ended
}
IRunnable runnable = token.Runnable;
// Get old IsRunning value (should be true before stopping)
bool oldIsRunning = runnable.IsRunning;
// Raise IsRunningChanging (true -> false) - can be canceled
// This is where Result should be extracted!
if (runnable.RaiseIsRunningChanging (oldIsRunning, false))
{
// Stopping was canceled
return;
}
// Current runnable is no longer modal
// Get old IsModal value (should be true before becoming non-modal)
bool oldIsModal = runnable.IsModal;
// Raise IsModalChanging (true -> false)
runnable.RaiseIsModalChanging (oldIsModal, false);
// IsModal is now false (will be false after pop)
runnable.RaiseIsModalChangedEvent (false);
// Pop token from RunnableSessionStack (IsRunning becomes false)
if (RunnableSessionStack?.TryPop (out RunnableSessionToken? popped) == true && popped == token)
{
// Restore previous top runnable
if (RunnableSessionStack?.TryPeek (out RunnableSessionToken? previousToken) == true && previousToken?.Runnable is { })
{
IRunnable? previousRunnable = previousToken.Runnable;
// Update TopRunnable if it's a Toplevel
if (previousRunnable is Toplevel tl)
{
TopRunnable = tl;
}
// Previous runnable becomes modal again
// Get old IsModal value (should be false before becoming modal again)
bool oldIsModalValue = previousRunnable.IsModal;
// Raise IsModalChanging (false -> true)
previousRunnable.RaiseIsModalChanging (oldIsModalValue, true);
// IsModal is now true (derived property)
previousRunnable.RaiseIsModalChangedEvent (true);
}
else
{
// No more runnables, clear TopRunnable
if (TopRunnable is IRunnable)
{
TopRunnable = null;
}
}
}
// Raise IsRunningChanged (now false)
runnable.RaiseIsRunningChangedEvent (false);
// Set focus to new TopRunnable if exists
if (TopRunnable is View viewToFocus && !viewToFocus.HasFocus)
{
viewToFocus.SetFocus ();
}
// Clear the token
token.Runnable = null;
}
///
public void RequestStop (IRunnable? runnable)
{
// Get the runnable to stop
if (runnable is null)
{
// Try to get from TopRunnable
if (TopRunnable is IRunnable r)
{
runnable = r;
}
else
{
return;
}
}
// For Toplevel, use the existing mechanism
if (runnable is Toplevel toplevel)
{
RequestStop (toplevel);
}
// Note: The End() method will be called from the finally block in Run()
// and that's where IsRunningChanging/IsRunningChanged will be raised
}
#endregion IRunnable Support
}