using System.Diagnostics; using System.Diagnostics.CodeAnalysis; namespace Terminal.Gui.App; public partial class ApplicationImpl { /// /// INTERNAL: Gets or sets the managed thread ID of the application's main UI thread, which is set during /// and used to determine if code is executing on the main thread. /// /// /// The managed thread ID of the main UI thread, or if the application is not initialized. /// internal int? MainThreadId { get; set; } #region Begin->Run->Stop->End /// public event EventHandler? SessionBegun; /// 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 && Current is { } && toplevel != Current && !SessionStack.Contains (Current)) { // This assertion confirm if the Current was already disposed Debug.Assert (Current.WasDisposed); Debug.Assert (Current == CachedSessionTokenToplevel); } #endif lock (SessionStack) { if (Current is { } && toplevel != Current && !SessionStack.Contains (Current)) { // If Current was already disposed and isn't on the Toplevels Stack, // clean it up here if is the same as _CachedSessionTokenToplevel if (Current == CachedSessionTokenToplevel) { Current = null; } else { // Probably this will never hit throw new ObjectDisposedException (Current.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 (Current is null) { toplevel.App = this; Current = toplevel; } if ((Current?.Modal == false && toplevel.Modal) || (Current?.Modal == false && !toplevel.Modal) || (Current?.Modal == true && toplevel.Modal)) { if (toplevel.Visible) { if (Current is { HasFocus: true }) { Current.HasFocus = false; } // Force leave events for any entered views in the old Current if (Mouse.LastMousePosition is { }) { Mouse.RaiseMouseEnterLeaveEvents (Mouse.LastMousePosition!.Value, new ()); } Current?.OnDeactivate (toplevel); Toplevel previousTop = Current!; Current = toplevel; Current.App = this; Current.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 event EventHandler? Iteration; /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] public Toplevel Run (Func? errorHandler = null, string? driverName = null) { return 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"); } Current = view; SessionToken rs = Begin (view); Current.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"); view.RequestStop (); } 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; Current = newTop; Current?.SetNeedsDraw (); } if (sessionToken.Toplevel is { HasFocus: true }) { sessionToken.Toplevel.HasFocus = false; } if (Current is { HasFocus: false }) { Current.SetFocus (); } CachedSessionTokenToplevel = sessionToken.Toplevel; sessionToken.Toplevel = null; sessionToken.Dispose (); // BUGBUG: Why layout and draw here? This causes the screen to be cleared! //LayoutAndDraw (true); SessionEnded?.Invoke (this, new (CachedSessionTokenToplevel)); } /// public void RequestStop () { RequestStop (null); } /// public void RequestStop (Toplevel? top) { Logging.Trace ($"Current: '{(top is { } ? top : "null")}'"); top ??= Current; if (top == null) { return; } ToplevelClosingEventArgs ev = new (top); top.OnClosing (ev); if (ev.Cancel) { return; } top.Running = false; } /// public void RaiseIteration () { Iteration?.Invoke (null, new ()); } #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) { return _timedEvents.Add (time, callback); } /// public bool RemoveTimeout (object token) { return _timedEvents.Remove (token); } /// public void Invoke (Action? action) { // If we are already on the main UI thread if (Current 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 (Current is { Running: true } && MainThreadId == Thread.CurrentThread.ManagedThreadId) { action?.Invoke (); return; } _timedEvents.Add ( TimeSpan.Zero, () => { action?.Invoke (); return false; } ); } #endregion Timeouts and Invoke }