|
@@ -1,321 +1,44 @@
|
|
|
-using System.Diagnostics;
|
|
|
|
|
|
|
+using System.Collections.Concurrent;
|
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
|
|
|
|
|
|
namespace Terminal.Gui.App;
|
|
namespace Terminal.Gui.App;
|
|
|
|
|
|
|
|
public partial class ApplicationImpl
|
|
public partial class ApplicationImpl
|
|
|
{
|
|
{
|
|
|
- /// <summary>
|
|
|
|
|
- /// INTERNAL: Gets or sets the managed thread ID of the application's main UI thread, which is set during
|
|
|
|
|
- /// <see cref="Init"/> and used to determine if code is executing on the main thread.
|
|
|
|
|
- /// </summary>
|
|
|
|
|
- /// <value>
|
|
|
|
|
- /// The managed thread ID of the main UI thread, or <see langword="null"/> if the application is not initialized.
|
|
|
|
|
- /// </value>
|
|
|
|
|
- internal int? MainThreadId { get; set; }
|
|
|
|
|
-
|
|
|
|
|
- #region Begin->Run->Stop->End
|
|
|
|
|
-
|
|
|
|
|
- // TODO: This API is not used anywhere; it can be deleted
|
|
|
|
|
- /// <inheritdoc/>
|
|
|
|
|
- public event EventHandler<SessionTokenEventArgs>? SessionBegun;
|
|
|
|
|
-
|
|
|
|
|
- // TODO: This API is not used anywhere; it can be deleted
|
|
|
|
|
- /// <inheritdoc/>
|
|
|
|
|
- public event EventHandler<ToplevelEventArgs>? SessionEnded;
|
|
|
|
|
-
|
|
|
|
|
- /// <inheritdoc/>
|
|
|
|
|
- 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;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // Lock object to protect session stack operations and cached state updates
|
|
|
|
|
+ private readonly object _sessionStackLock = new ();
|
|
|
|
|
|
|
|
- /// <inheritdoc/>
|
|
|
|
|
- public bool StopAfterFirstIteration { get; set; }
|
|
|
|
|
|
|
+ #region Session State - Stack and TopRunnable
|
|
|
|
|
|
|
|
/// <inheritdoc/>
|
|
/// <inheritdoc/>
|
|
|
- public void RaiseIteration ()
|
|
|
|
|
- {
|
|
|
|
|
- Iteration?.Invoke (null, new ());
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ public ConcurrentStack<SessionToken>? SessionStack { get; } = new ();
|
|
|
|
|
|
|
|
/// <inheritdoc/>
|
|
/// <inheritdoc/>
|
|
|
- public event EventHandler<IterationEventArgs>? Iteration;
|
|
|
|
|
|
|
+ public IRunnable? TopRunnable { get; private set; }
|
|
|
|
|
|
|
|
/// <inheritdoc/>
|
|
/// <inheritdoc/>
|
|
|
- [RequiresUnreferencedCode ("AOT")]
|
|
|
|
|
- [RequiresDynamicCode ("AOT")]
|
|
|
|
|
- public Toplevel Run (Func<Exception, bool>? errorHandler = null, string? driverName = null) => Run<Toplevel> (errorHandler, driverName);
|
|
|
|
|
|
|
+ public View? TopRunnableView => TopRunnable as View;
|
|
|
|
|
|
|
|
/// <inheritdoc/>
|
|
/// <inheritdoc/>
|
|
|
- [RequiresUnreferencedCode ("AOT")]
|
|
|
|
|
- [RequiresDynamicCode ("AOT")]
|
|
|
|
|
- public TView Run<TView> (Func<Exception, bool>? 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 event EventHandler<SessionTokenEventArgs>? SessionBegun;
|
|
|
|
|
|
|
|
/// <inheritdoc/>
|
|
/// <inheritdoc/>
|
|
|
- public void Run (Toplevel view, Func<Exception, bool>? 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;
|
|
|
|
|
|
|
+ public event EventHandler<SessionTokenEventArgs>? SessionEnded;
|
|
|
|
|
|
|
|
- SessionToken rs = Begin (view);
|
|
|
|
|
|
|
+ #endregion Session State - Stack and TopRunnable
|
|
|
|
|
|
|
|
- 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);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ #region Main Loop Iteration
|
|
|
|
|
|
|
|
/// <inheritdoc/>
|
|
/// <inheritdoc/>
|
|
|
- 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 bool StopAfterFirstIteration { get; set; }
|
|
|
|
|
|
|
|
/// <inheritdoc/>
|
|
/// <inheritdoc/>
|
|
|
- public void RequestStop () { RequestStop ((Toplevel?)null); }
|
|
|
|
|
|
|
+ public event EventHandler<EventArgs<IApplication?>>? Iteration;
|
|
|
|
|
|
|
|
/// <inheritdoc/>
|
|
/// <inheritdoc/>
|
|
|
- public void RequestStop (Toplevel? top)
|
|
|
|
|
- {
|
|
|
|
|
- Logging.Trace ($"TopRunnable: '{(top is { } ? top : "null")}'");
|
|
|
|
|
-
|
|
|
|
|
- top ??= TopRunnable;
|
|
|
|
|
-
|
|
|
|
|
- if (top == null)
|
|
|
|
|
- {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ public void RaiseIteration () { Iteration?.Invoke (null, new (this)); }
|
|
|
|
|
|
|
|
- ToplevelClosingEventArgs ev = new (top);
|
|
|
|
|
- top.OnClosing (ev);
|
|
|
|
|
-
|
|
|
|
|
- if (ev.Cancel)
|
|
|
|
|
- {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- top.Running = false;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #endregion Begin->Run->Stop->End
|
|
|
|
|
|
|
+ #endregion Main Loop Iteration
|
|
|
|
|
|
|
|
#region Timeouts and Invoke
|
|
#region Timeouts and Invoke
|
|
|
|
|
|
|
@@ -334,7 +57,7 @@ public partial class ApplicationImpl
|
|
|
public void Invoke (Action<IApplication>? action)
|
|
public void Invoke (Action<IApplication>? action)
|
|
|
{
|
|
{
|
|
|
// If we are already on the main UI thread
|
|
// If we are already on the main UI thread
|
|
|
- if (TopRunnable is { Running: true } && MainThreadId == Thread.CurrentThread.ManagedThreadId)
|
|
|
|
|
|
|
+ if (TopRunnableView is IRunnable { IsRunning: true } && MainThreadId == Thread.CurrentThread.ManagedThreadId)
|
|
|
{
|
|
{
|
|
|
action?.Invoke (this);
|
|
action?.Invoke (this);
|
|
|
|
|
|
|
@@ -356,7 +79,7 @@ public partial class ApplicationImpl
|
|
|
public void Invoke (Action action)
|
|
public void Invoke (Action action)
|
|
|
{
|
|
{
|
|
|
// If we are already on the main UI thread
|
|
// If we are already on the main UI thread
|
|
|
- if (TopRunnable is { Running: true } && MainThreadId == Thread.CurrentThread.ManagedThreadId)
|
|
|
|
|
|
|
+ if (TopRunnableView is IRunnable { IsRunning: true } && MainThreadId == Thread.CurrentThread.ManagedThreadId)
|
|
|
{
|
|
{
|
|
|
action?.Invoke ();
|
|
action?.Invoke ();
|
|
|
|
|
|
|
@@ -376,126 +99,148 @@ public partial class ApplicationImpl
|
|
|
|
|
|
|
|
#endregion Timeouts and Invoke
|
|
#endregion Timeouts and Invoke
|
|
|
|
|
|
|
|
- #region IRunnable Support
|
|
|
|
|
|
|
+ #region Session Lifecycle - Begin
|
|
|
|
|
|
|
|
/// <inheritdoc/>
|
|
/// <inheritdoc/>
|
|
|
- public RunnableSessionToken Begin (IRunnable runnable)
|
|
|
|
|
|
|
+ public SessionToken? Begin (IRunnable runnable)
|
|
|
{
|
|
{
|
|
|
ArgumentNullException.ThrowIfNull (runnable);
|
|
ArgumentNullException.ThrowIfNull (runnable);
|
|
|
|
|
|
|
|
- // Ensure the mouse is ungrabbed
|
|
|
|
|
- if (Mouse.MouseGrabView is { })
|
|
|
|
|
|
|
+ if (runnable.IsRunning)
|
|
|
{
|
|
{
|
|
|
- Mouse.UngrabMouse ();
|
|
|
|
|
|
|
+ throw new ArgumentException (@"The runnable is already running.", nameof (runnable));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Create session token
|
|
// 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;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ SessionToken token = new (runnable);
|
|
|
|
|
|
|
|
- // Get old IsRunning and IsModal values BEFORE any stack changes
|
|
|
|
|
|
|
+ // Get old IsRunning value BEFORE any stack changes (safe - cached value)
|
|
|
bool oldIsRunning = runnable.IsRunning;
|
|
bool oldIsRunning = runnable.IsRunning;
|
|
|
- bool oldIsModalValue = runnable.IsModal;
|
|
|
|
|
|
|
|
|
|
- // Raise IsRunningChanging (false -> true) - can be canceled
|
|
|
|
|
|
|
+ // Raise IsRunningChanging OUTSIDE lock (false -> true) - can be canceled
|
|
|
if (runnable.RaiseIsRunningChanging (oldIsRunning, true))
|
|
if (runnable.RaiseIsRunningChanging (oldIsRunning, true))
|
|
|
{
|
|
{
|
|
|
// Starting was canceled
|
|
// Starting was canceled
|
|
|
- return token;
|
|
|
|
|
|
|
+ return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Push token onto RunnableSessionStack (IsRunning becomes true)
|
|
|
|
|
- RunnableSessionStack?.Push (token);
|
|
|
|
|
|
|
+ // Set the application reference in the runnable
|
|
|
|
|
+ runnable.SetApp (this);
|
|
|
|
|
+
|
|
|
|
|
+ // Ensure the mouse is ungrabbed
|
|
|
|
|
+ Mouse.UngrabMouse ();
|
|
|
|
|
|
|
|
- // Update TopRunnable to the new top of stack
|
|
|
|
|
IRunnable? previousTop = null;
|
|
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)
|
|
|
|
|
|
|
+ // CRITICAL SECTION - Atomic stack + cached state update
|
|
|
|
|
+ lock (_sessionStackLock)
|
|
|
{
|
|
{
|
|
|
- previousTop = r;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // Get the previous top BEFORE pushing new token
|
|
|
|
|
+ if (SessionStack?.TryPeek (out SessionToken? previousToken) == true && previousToken?.Runnable is { })
|
|
|
|
|
+ {
|
|
|
|
|
+ previousTop = previousToken.Runnable;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 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");
|
|
|
|
|
|
|
+ if (previousTop == runnable)
|
|
|
|
|
+ {
|
|
|
|
|
+ throw new ArgumentOutOfRangeException (nameof (runnable), runnable, @"Attempt to Run the runnable that's already the top runnable.");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Push token onto SessionStack
|
|
|
|
|
+ SessionStack?.Push (token);
|
|
|
|
|
+
|
|
|
|
|
+ TopRunnable = runnable;
|
|
|
|
|
+
|
|
|
|
|
+ // Update cached state atomically - IsRunning and IsModal are now consistent
|
|
|
|
|
+ SessionBegun?.Invoke (this, new (token));
|
|
|
|
|
+ runnable.SetIsRunning (true);
|
|
|
|
|
+ runnable.SetIsModal (true);
|
|
|
|
|
+
|
|
|
|
|
+ // Previous top is no longer modal
|
|
|
|
|
+ if (previousTop != null)
|
|
|
|
|
+ {
|
|
|
|
|
+ previousTop.SetIsModal (false);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Raise IsRunningChanged (now true)
|
|
|
|
|
- runnable.RaiseIsRunningChangedEvent (true);
|
|
|
|
|
|
|
+ // END CRITICAL SECTION - IsRunning/IsModal now thread-safe
|
|
|
|
|
|
|
|
- // If there was a previous top, it's no longer modal
|
|
|
|
|
|
|
+ // Fire events AFTER lock released (avoid deadlocks in event handlers)
|
|
|
if (previousTop != null)
|
|
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);
|
|
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.RaiseIsRunningChangedEvent (true);
|
|
|
runnable.RaiseIsModalChangedEvent (true);
|
|
runnable.RaiseIsModalChangedEvent (true);
|
|
|
|
|
|
|
|
- // Initialize if needed
|
|
|
|
|
- if (runnable is View view && !view.IsInitialized)
|
|
|
|
|
- {
|
|
|
|
|
- view.BeginInit ();
|
|
|
|
|
- view.EndInit ();
|
|
|
|
|
|
|
+ LayoutAndDraw ();
|
|
|
|
|
|
|
|
- // Initialized event is raised by View.EndInit()
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ return token;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #endregion Session Lifecycle - Begin
|
|
|
|
|
|
|
|
- // Initial Layout and draw
|
|
|
|
|
- LayoutAndDraw (true);
|
|
|
|
|
|
|
+ #region Session Lifecycle - Run
|
|
|
|
|
+
|
|
|
|
|
+ /// <inheritdoc/>
|
|
|
|
|
+ [RequiresUnreferencedCode ("AOT")]
|
|
|
|
|
+ [RequiresDynamicCode ("AOT")]
|
|
|
|
|
+ public IApplication Run<TRunnable> (Func<Exception, bool>? errorHandler = null, string? driverName = null)
|
|
|
|
|
+ where TRunnable : IRunnable, new()
|
|
|
|
|
+ {
|
|
|
|
|
+ if (!Initialized)
|
|
|
|
|
+ {
|
|
|
|
|
+ // Init() has NOT been called. Auto-initialize as per interface contract.
|
|
|
|
|
+ Init (driverName);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // Set focus
|
|
|
|
|
- if (runnable is View viewToFocus && !viewToFocus.HasFocus)
|
|
|
|
|
|
|
+ if (Driver is null)
|
|
|
{
|
|
{
|
|
|
- viewToFocus.SetFocus ();
|
|
|
|
|
|
|
+ throw new InvalidOperationException (@"Driver is null after Init.");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (PositionCursor ())
|
|
|
|
|
|
|
+ TRunnable runnable = new ();
|
|
|
|
|
+ object? result = Run (runnable, errorHandler);
|
|
|
|
|
+
|
|
|
|
|
+ // We created the runnable, so dispose it if it's disposable
|
|
|
|
|
+ if (runnable is IDisposable disposable)
|
|
|
{
|
|
{
|
|
|
- Driver?.UpdateCursor ();
|
|
|
|
|
|
|
+ disposable.Dispose ();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return token;
|
|
|
|
|
|
|
+ return this;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc/>
|
|
/// <inheritdoc/>
|
|
|
- public void Run (IRunnable runnable, Func<Exception, bool>? errorHandler = null)
|
|
|
|
|
|
|
+ public object? Run (IRunnable runnable, Func<Exception, bool>? errorHandler = null)
|
|
|
{
|
|
{
|
|
|
ArgumentNullException.ThrowIfNull (runnable);
|
|
ArgumentNullException.ThrowIfNull (runnable);
|
|
|
|
|
|
|
|
if (!Initialized)
|
|
if (!Initialized)
|
|
|
{
|
|
{
|
|
|
- throw new NotInitializedException (nameof (Run));
|
|
|
|
|
|
|
+ throw new NotInitializedException (@"Init must be called before Run.");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Begin the session (adds to stack, raises IsRunningChanging/IsRunningChanged)
|
|
// Begin the session (adds to stack, raises IsRunningChanging/IsRunningChanged)
|
|
|
- RunnableSessionToken token = Begin (runnable);
|
|
|
|
|
|
|
+ SessionToken? token;
|
|
|
|
|
+
|
|
|
|
|
+ if (runnable.IsRunning)
|
|
|
|
|
+ {
|
|
|
|
|
+ // Find it on the stack
|
|
|
|
|
+ token = SessionStack?.FirstOrDefault (st => st.Runnable == runnable);
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ token = Begin (runnable);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (token is null)
|
|
|
|
|
+ {
|
|
|
|
|
+ Logging.Trace (@"Run - Begin session failed or was cancelled.");
|
|
|
|
|
+
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
try
|
|
try
|
|
|
{
|
|
{
|
|
@@ -507,33 +252,19 @@ public partial class ApplicationImpl
|
|
|
// End the session (raises IsRunningChanging/IsRunningChanged, pops from stack)
|
|
// End the session (raises IsRunningChanging/IsRunningChanged, pops from stack)
|
|
|
End (token);
|
|
End (token);
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- /// <inheritdoc/>
|
|
|
|
|
- public IApplication Run<TRunnable> (Func<Exception, bool>? 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;
|
|
|
|
|
|
|
+ return token.Result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private void RunLoop (IRunnable runnable, Func<Exception, bool>? errorHandler)
|
|
private void RunLoop (IRunnable runnable, Func<Exception, bool>? errorHandler)
|
|
|
{
|
|
{
|
|
|
|
|
+ runnable.StopRequested = false;
|
|
|
|
|
+
|
|
|
// Main loop - blocks until RequestStop() is called
|
|
// Main loop - blocks until RequestStop() is called
|
|
|
- // Note: IsRunning is a derived property (stack.Contains), so we check it each iteration
|
|
|
|
|
|
|
+ // Note: IsRunning is now a cached property, safe to check each iteration
|
|
|
var firstIteration = true;
|
|
var firstIteration = true;
|
|
|
|
|
|
|
|
- while (runnable.IsRunning)
|
|
|
|
|
|
|
+ while (runnable is { StopRequested: false, IsRunning: true })
|
|
|
{
|
|
{
|
|
|
if (Coordinator is null)
|
|
if (Coordinator is null)
|
|
|
{
|
|
{
|
|
@@ -563,8 +294,12 @@ public partial class ApplicationImpl
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ #endregion Session Lifecycle - Run
|
|
|
|
|
+
|
|
|
|
|
+ #region Session Lifecycle - End
|
|
|
|
|
+
|
|
|
/// <inheritdoc/>
|
|
/// <inheritdoc/>
|
|
|
- public void End (RunnableSessionToken token)
|
|
|
|
|
|
|
+ public void End (SessionToken token)
|
|
|
{
|
|
{
|
|
|
ArgumentNullException.ThrowIfNull (token);
|
|
ArgumentNullException.ThrowIfNull (token);
|
|
|
|
|
|
|
@@ -573,76 +308,84 @@ public partial class ApplicationImpl
|
|
|
return; // Already ended
|
|
return; // Already ended
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // TODO: Move Poppover to utilize IRunnable arch; Get all refs to anyting
|
|
|
|
|
+ // TODO: View-related out of ApplicationImpl.
|
|
|
|
|
+ if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover)
|
|
|
|
|
+ {
|
|
|
|
|
+ ApplicationPopover.HideWithQuitCommand (visiblePopover);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
IRunnable runnable = token.Runnable;
|
|
IRunnable runnable = token.Runnable;
|
|
|
|
|
|
|
|
- // Get old IsRunning value (should be true before stopping)
|
|
|
|
|
|
|
+ // Get old IsRunning value (safe - cached value)
|
|
|
bool oldIsRunning = runnable.IsRunning;
|
|
bool oldIsRunning = runnable.IsRunning;
|
|
|
|
|
|
|
|
- // Raise IsRunningChanging (true -> false) - can be canceled
|
|
|
|
|
|
|
+ // Raise IsRunningChanging OUTSIDE lock (true -> false) - can be canceled
|
|
|
// This is where Result should be extracted!
|
|
// This is where Result should be extracted!
|
|
|
if (runnable.RaiseIsRunningChanging (oldIsRunning, false))
|
|
if (runnable.RaiseIsRunningChanging (oldIsRunning, false))
|
|
|
{
|
|
{
|
|
|
- // Stopping was canceled
|
|
|
|
|
|
|
+ // Stopping was canceled - do not proceed with End
|
|
|
return;
|
|
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);
|
|
|
|
|
|
|
+ bool wasModal = runnable.IsModal;
|
|
|
|
|
+ IRunnable? previousRunnable = null;
|
|
|
|
|
|
|
|
- // 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)
|
|
|
|
|
|
|
+ // CRITICAL SECTION - Atomic stack + cached state update
|
|
|
|
|
+ lock (_sessionStackLock)
|
|
|
{
|
|
{
|
|
|
- // Restore previous top runnable
|
|
|
|
|
- if (RunnableSessionStack?.TryPeek (out RunnableSessionToken? previousToken) == true && previousToken?.Runnable is { })
|
|
|
|
|
|
|
+ // Pop token from SessionStack
|
|
|
|
|
+ if (wasModal && SessionStack?.TryPop (out SessionToken? popped) == true && popped == token)
|
|
|
{
|
|
{
|
|
|
- IRunnable? previousRunnable = previousToken.Runnable;
|
|
|
|
|
-
|
|
|
|
|
- // Update TopRunnable if it's a Toplevel
|
|
|
|
|
- if (previousRunnable is Toplevel tl)
|
|
|
|
|
|
|
+ // Restore previous top runnable
|
|
|
|
|
+ if (SessionStack?.TryPeek (out SessionToken? previousToken) == true && previousToken?.Runnable is { })
|
|
|
{
|
|
{
|
|
|
- TopRunnable = tl;
|
|
|
|
|
|
|
+ previousRunnable = previousToken.Runnable;
|
|
|
|
|
+
|
|
|
|
|
+ // Previous runnable becomes modal again
|
|
|
|
|
+ previousRunnable.SetIsModal (true);
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // Previous runnable becomes modal again
|
|
|
|
|
- // Get old IsModal value (should be false before becoming modal again)
|
|
|
|
|
- bool oldIsModalValue = previousRunnable.IsModal;
|
|
|
|
|
|
|
+ // Update cached state atomically - IsRunning and IsModal are now consistent
|
|
|
|
|
+ runnable.SetIsRunning (false);
|
|
|
|
|
+ runnable.SetIsModal (false);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // Raise IsModalChanging (false -> true)
|
|
|
|
|
- previousRunnable.RaiseIsModalChanging (oldIsModalValue, true);
|
|
|
|
|
|
|
+ // END CRITICAL SECTION - IsRunning/IsModal now thread-safe
|
|
|
|
|
|
|
|
- // IsModal is now true (derived property)
|
|
|
|
|
- previousRunnable.RaiseIsModalChangedEvent (true);
|
|
|
|
|
- }
|
|
|
|
|
- else
|
|
|
|
|
- {
|
|
|
|
|
- // No more runnables, clear TopRunnable
|
|
|
|
|
- if (TopRunnable is IRunnable)
|
|
|
|
|
- {
|
|
|
|
|
- TopRunnable = null;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // Fire events AFTER lock released
|
|
|
|
|
+ if (wasModal)
|
|
|
|
|
+ {
|
|
|
|
|
+ runnable.RaiseIsModalChangedEvent (false);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Raise IsRunningChanged (now false)
|
|
|
|
|
- runnable.RaiseIsRunningChangedEvent (false);
|
|
|
|
|
|
|
+ TopRunnable = null;
|
|
|
|
|
|
|
|
- // Set focus to new TopRunnable if exists
|
|
|
|
|
- if (TopRunnable is View viewToFocus && !viewToFocus.HasFocus)
|
|
|
|
|
|
|
+ if (previousRunnable != null)
|
|
|
{
|
|
{
|
|
|
- viewToFocus.SetFocus ();
|
|
|
|
|
|
|
+ TopRunnable = previousRunnable;
|
|
|
|
|
+ previousRunnable.RaiseIsModalChangedEvent (true);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Clear the token
|
|
|
|
|
|
|
+ runnable.RaiseIsRunningChangedEvent (false);
|
|
|
|
|
+
|
|
|
|
|
+ token.Result = runnable.Result;
|
|
|
|
|
+
|
|
|
|
|
+ _result = token.Result;
|
|
|
|
|
+
|
|
|
|
|
+ // Clear the Runnable from the token
|
|
|
token.Runnable = null;
|
|
token.Runnable = null;
|
|
|
|
|
+ SessionEnded?.Invoke (this, new (token));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ #endregion Session Lifecycle - End
|
|
|
|
|
+
|
|
|
|
|
+ #region Session Lifecycle - RequestStop
|
|
|
|
|
+
|
|
|
|
|
+ /// <inheritdoc/>
|
|
|
|
|
+ public void RequestStop () { RequestStop (null); }
|
|
|
|
|
+
|
|
|
/// <inheritdoc/>
|
|
/// <inheritdoc/>
|
|
|
public void RequestStop (IRunnable? runnable)
|
|
public void RequestStop (IRunnable? runnable)
|
|
|
{
|
|
{
|
|
@@ -650,7 +393,7 @@ public partial class ApplicationImpl
|
|
|
if (runnable is null)
|
|
if (runnable is null)
|
|
|
{
|
|
{
|
|
|
// Try to get from TopRunnable
|
|
// Try to get from TopRunnable
|
|
|
- if (TopRunnable is IRunnable r)
|
|
|
|
|
|
|
+ if (TopRunnableView is IRunnable r)
|
|
|
{
|
|
{
|
|
|
runnable = r;
|
|
runnable = r;
|
|
|
}
|
|
}
|
|
@@ -660,15 +403,11 @@ public partial class ApplicationImpl
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // For Toplevel, use the existing mechanism
|
|
|
|
|
- if (runnable is Toplevel toplevel)
|
|
|
|
|
- {
|
|
|
|
|
- RequestStop (toplevel);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ runnable.StopRequested = true;
|
|
|
|
|
|
|
|
// Note: The End() method will be called from the finally block in Run()
|
|
// Note: The End() method will be called from the finally block in Run()
|
|
|
// and that's where IsRunningChanging/IsRunningChanged will be raised
|
|
// and that's where IsRunningChanging/IsRunningChanged will be raised
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- #endregion IRunnable Support
|
|
|
|
|
|
|
+ #endregion Session Lifecycle - RequestStop
|
|
|
}
|
|
}
|