#nullable enable using System.Diagnostics; using System.Diagnostics.CodeAnalysis; namespace Terminal.Gui; public static partial class Application // Run (Begin, Run, End, Stop) { // When `End ()` is called, it is possible `RunState.Toplevel` is a different object than `Top`. // This variable is set in `End` in this case so that `Begin` correctly sets `Top`. private static Toplevel? _cachedRunStateToplevel; /// /// Notify that a new was created ( was called). The token is /// created in and this event will be fired before that function exits. /// /// /// If is callers to /// must also subscribe to and manually dispose of the token /// when the application is done. /// public static event EventHandler? NotifyNewRunState; /// Notify that an existent is stopping ( was called). /// /// If is callers to /// must also subscribe to and manually dispose of the token /// when the application is done. /// public static event EventHandler? NotifyStopRunState; /// Building block API: Prepares the provided for execution. /// /// The handle that needs to be passed to the method upon /// completion. /// /// The to prepare execution for. /// /// This method prepares the provided for running with the focus, it adds this to the list /// of s, lays out the Subviews, focuses the first element, and draws the /// in the screen. This is usually followed by executing the method, and then the /// method upon termination which will undo these changes. /// public static RunState Begin (Toplevel toplevel) { ArgumentNullException.ThrowIfNull (toplevel); #if DEBUG_IDISPOSABLE Debug.Assert (!toplevel.WasDisposed); if (_cachedRunStateToplevel is { } && _cachedRunStateToplevel != toplevel) { Debug.Assert (_cachedRunStateToplevel.WasDisposed); } #endif if (toplevel.IsOverlappedContainer && ApplicationOverlapped.OverlappedTop != toplevel && ApplicationOverlapped.OverlappedTop is { }) { throw new InvalidOperationException ("Only one Overlapped Container is allowed."); } // Ensure the mouse is ungrabbed. MouseGrabView = null; var rs = new RunState (toplevel); // View implements ISupportInitializeNotification which is derived from ISupportInitialize if (!toplevel.IsInitialized) { toplevel.BeginInit (); toplevel.EndInit (); } #if DEBUG_IDISPOSABLE if (Top is { } && toplevel != Top && !TopLevels.Contains (Top)) { // This assertion confirm if the Top was already disposed Debug.Assert (Top.WasDisposed); Debug.Assert (Top == _cachedRunStateToplevel); } #endif lock (TopLevels) { if (Top is { } && toplevel != Top && !TopLevels.Contains (Top)) { // If Top was already disposed and isn't on the Toplevels Stack, // clean it up here if is the same as _cachedRunStateToplevel if (Top == _cachedRunStateToplevel) { Top = null; } else { // Probably this will never hit throw new ObjectDisposedException (Top.GetType ().FullName); } } else if (ApplicationOverlapped.OverlappedTop is { } && toplevel != Top && TopLevels.Contains (Top!)) { // BUGBUG: Don't call OnLeave/OnEnter directly! Set HasFocus to false and let the system handle it. Top!.OnLeave (toplevel); } // 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 = (TopLevels.Count + count).ToString (); while (TopLevels.Count > 0 && TopLevels.FirstOrDefault (x => x.Id == id) is { }) { count++; id = (TopLevels.Count + count).ToString (); } toplevel.Id = (TopLevels.Count + count).ToString (); TopLevels.Push (toplevel); } else { Toplevel? dup = TopLevels.FirstOrDefault (x => x.Id == toplevel.Id); if (dup is null) { TopLevels.Push (toplevel); } } if (TopLevels.FindDuplicates (new ToplevelEqualityComparer ()).Count > 0) { throw new ArgumentException ("There are duplicates Toplevel IDs"); } } if (Top is null || toplevel.IsOverlappedContainer) { Top = toplevel; } var refreshDriver = true; if (ApplicationOverlapped.OverlappedTop is null || toplevel.IsOverlappedContainer || (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; } Current?.OnDeactivate (toplevel); Toplevel previousCurrent = Current!; Current = toplevel; Current.OnActivate (previousCurrent); ApplicationOverlapped.SetCurrentOverlappedAsTop (); } else { refreshDriver = false; } } else if ((toplevel != ApplicationOverlapped.OverlappedTop && Current?.Modal == true && !TopLevels.Peek ().Modal) || (toplevel != ApplicationOverlapped.OverlappedTop && Current?.Running == false)) { refreshDriver = false; ApplicationOverlapped.MoveCurrent (toplevel); } else { refreshDriver = false; ApplicationOverlapped.MoveCurrent (Current!); } toplevel.SetRelativeLayout (Driver!.Screen.Size); toplevel.LayoutSubviews (); toplevel.PositionToplevels (); toplevel.FocusFirst (null); ApplicationOverlapped.BringOverlappedTopToFront (); if (refreshDriver) { ApplicationOverlapped.OverlappedTop?.OnChildLoaded (toplevel); toplevel.OnLoaded (); toplevel.SetNeedsDisplay (); toplevel.Draw (); Driver.UpdateScreen (); if (PositionCursor (toplevel)) { Driver.UpdateCursor (); } } NotifyNewRunState?.Invoke (toplevel, new (rs)); return rs; } /// /// Calls on the most focused view in the view starting with . /// /// /// Does nothing if is or if the most focused view is not visible or /// enabled. /// /// If the most focused view is not visible within it's superview, the cursor will be hidden. /// /// /// if a view positioned the cursor and the position is visible. internal static bool PositionCursor (View view) { // Find the most focused view and position the cursor there. View? mostFocused = view?.MostFocused; if (mostFocused is null) { if (view is { HasFocus: true }) { mostFocused = view; } else { return false; } } // If the view is not visible or enabled, don't position the cursor if (!mostFocused.Visible || !mostFocused.Enabled) { Driver!.GetCursorVisibility (out CursorVisibility current); if (current != CursorVisibility.Invisible) { Driver.SetCursorVisibility (CursorVisibility.Invisible); } return false; } // If the view is not visible within it's superview, don't position the cursor Rectangle mostFocusedViewport = mostFocused.ViewportToScreen (mostFocused.Viewport with { Location = Point.Empty }); Rectangle superViewViewport = mostFocused.SuperView?.ViewportToScreen (mostFocused.SuperView.Viewport with { Location = Point.Empty }) ?? Driver!.Screen; if (!superViewViewport.IntersectsWith (mostFocusedViewport)) { return false; } Point? cursor = mostFocused.PositionCursor (); Driver!.GetCursorVisibility (out CursorVisibility currentCursorVisibility); if (cursor is { }) { // Convert cursor to screen coords cursor = mostFocused.ViewportToScreen (mostFocused.Viewport with { Location = cursor.Value }).Location; // If the cursor is not in a visible location in the SuperView, hide it if (!superViewViewport.Contains (cursor.Value)) { if (currentCursorVisibility != CursorVisibility.Invisible) { Driver.SetCursorVisibility (CursorVisibility.Invisible); } return false; } // Show it if (currentCursorVisibility == CursorVisibility.Invisible) { Driver.SetCursorVisibility (mostFocused.CursorVisibility); } return true; } if (currentCursorVisibility != CursorVisibility.Invisible) { Driver.SetCursorVisibility (CursorVisibility.Invisible); } return false; } /// /// Runs the application by creating a object and calling /// . /// /// /// Calling first is not needed as this function will initialize the application. /// /// must be called when the application is closing (typically after Run> has returned) to /// ensure resources are cleaned up and terminal settings restored. /// /// /// The caller is responsible for disposing the object returned by this method. /// /// /// The created object. The caller is responsible for disposing this object. [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] public static Toplevel Run (Func? errorHandler = null, ConsoleDriver? driver = null) { return Run (errorHandler, driver); } /// /// Runs the application by creating a -derived object of type T and calling /// . /// /// /// Calling first is not needed as this function will initialize the application. /// /// must be called when the application is closing (typically after Run> has returned) to /// ensure resources are cleaned up and terminal settings restored. /// /// /// The caller is responsible for disposing the object returned by this method. /// /// /// /// /// The to use. If not specified the default driver for the platform will /// be used ( , , or ). Must be /// if has already been called. /// /// The created T object. The caller is responsible for disposing this object. [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] public static T Run (Func? errorHandler = null, ConsoleDriver? driver = null) where T : Toplevel, new() { if (!IsInitialized) { // Init() has NOT been called. InternalInit (driver, null, true); } var top = new T (); Run (top, errorHandler); return top; } /// Runs the Application using the provided view. /// /// /// This method is used to start processing events for the main application, but it is also used to run other /// modal s such as boxes. /// /// /// To make a stop execution, call /// . /// /// /// Calling is equivalent to calling /// , followed by , and then calling /// . /// /// /// Alternatively, to have a program control the main loop and process events manually, call /// to set things up manually and then repeatedly call /// with the wait parameter set to false. By doing this the /// method will only process any pending events, timers, idle handlers and then /// return control immediately. /// /// When using or /// /// will be called automatically. /// /// /// RELEASE builds only: When is any exceptions will be /// rethrown. Otherwise, if will be called. If /// returns the will resume; otherwise this method will /// exit. /// /// /// The to run as a modal. /// /// RELEASE builds only: Handler for any unhandled exceptions (resumes when returns true, /// rethrows when null). /// public static void Run (Toplevel view, Func? errorHandler = null) { ArgumentNullException.ThrowIfNull (view); if (IsInitialized) { if (Driver is null) { // Disposing before throwing view.Dispose (); // This code path should be impossible because Init(null, null) will select the platform default driver throw new InvalidOperationException ( "Init() completed without a driver being set (this should be impossible); Run() cannot be called." ); } } else { // Init() has NOT been called. throw new InvalidOperationException ( "Init() has not been called. Only Run() or Run() can be used without calling Init()." ); } var resume = true; while (resume) { #if !DEBUG try { #endif resume = false; RunState runState = Begin (view); // If EndAfterFirstIteration is true then the user must dispose of the runToken // by using NotifyStopRunState event. RunLoop (runState); if (runState.Toplevel is null) { #if DEBUG_IDISPOSABLE Debug.Assert (TopLevels.Count == 0); #endif runState.Dispose (); return; } if (!EndAfterFirstIteration) { End (runState); } #if !DEBUG } catch (Exception error) { if (errorHandler is null) { throw; } resume = errorHandler (error); } #endif } } /// Adds a timeout to the application. /// /// When time specified passes, the callback will be invoked. If the callback returns true, the timeout will be /// reset, repeating the invocation. If it returns false, the timeout will stop and be removed. The returned value is a /// token that can be used to stop the timeout by calling . /// public static object AddTimeout (TimeSpan time, Func callback) { return MainLoop!.AddTimeout (time, callback); } /// Removes a previously scheduled timeout /// The token parameter is the value returned by . /// Returns /// true /// if the timeout is successfully removed; otherwise, /// false /// . /// This method also returns /// false /// if the timeout is not found. public static bool RemoveTimeout (object token) { return MainLoop?.RemoveTimeout (token) ?? false; } /// Runs on the thread that is processing events /// the action to be invoked on the main processing thread. public static void Invoke (Action action) { MainLoop?.AddIdle ( () => { action (); return false; } ); } // TODO: Determine if this is really needed. The only code that calls WakeUp I can find // is ProgressBarStyles, and it's not clear it needs to. /// Wakes up the running application that might be waiting on input. public static void Wakeup () { MainLoop?.Wakeup (); } /// Triggers a refresh of the entire display. public static void Refresh () { // TODO: Figure out how to remove this call to ClearContents. Refresh should just repaint damaged areas, not clear Driver!.ClearContents (); foreach (Toplevel v in TopLevels.Reverse ()) { if (v.Visible) { v.SetNeedsDisplay (); v.SetSubViewNeedsDisplay (); v.Draw (); } } Driver.Refresh (); } /// This event is raised on each iteration of the main loop. /// See also public static event EventHandler? Iteration; /// The driver for the application /// The main loop. internal static MainLoop? MainLoop { get; private set; } /// /// Set to true to cause to be called after the first iteration. Set to false (the default) to /// cause the application to continue running until Application.RequestStop () is called. /// public static bool EndAfterFirstIteration { get; set; } /// Building block API: Runs the main loop for the created . /// The state returned by the method. public static void RunLoop (RunState state) { ArgumentNullException.ThrowIfNull (state); ObjectDisposedException.ThrowIf (state.Toplevel is null, "state"); var firstIteration = true; for (state.Toplevel.Running = true; state.Toplevel?.Running == true;) { MainLoop!.Running = true; if (EndAfterFirstIteration && !firstIteration) { return; } RunIteration (ref state, ref firstIteration); } MainLoop!.Running = false; // Run one last iteration to consume any outstanding input events from Driver // This is important for remaining OnKeyUp events. RunIteration (ref state, ref firstIteration); } /// Run one application iteration. /// The state returned by . /// /// Set to if this is the first run loop iteration. Upon return, it /// will be set to if at least one iteration happened. /// public static void RunIteration (ref RunState state, ref bool firstIteration) { if (MainLoop!.Running && MainLoop.EventsPending ()) { // Notify Toplevel it's ready if (firstIteration) { state.Toplevel.OnReady (); } MainLoop.RunIteration (); Iteration?.Invoke (null, new ()); EnsureModalOrVisibleAlwaysOnTop (state.Toplevel); // TODO: Overlapped - Move elsewhere if (state.Toplevel != Current) { ApplicationOverlapped.OverlappedTop?.OnDeactivate (state.Toplevel); state.Toplevel = Current; ApplicationOverlapped.OverlappedTop?.OnActivate (state.Toplevel!); Top!.SetSubViewNeedsDisplay (); Refresh (); } } firstIteration = false; if (Current == null) { return; } if (state.Toplevel != Top && (Top!.NeedsDisplay || Top.SubViewNeedsDisplay || Top.LayoutNeeded)) { state.Toplevel!.SetNeedsDisplay (state.Toplevel.Frame); Top.Draw (); foreach (Toplevel top in TopLevels.Reverse ()) { if (top != Top && top != state.Toplevel) { top.SetNeedsDisplay (); top.SetSubViewNeedsDisplay (); top.Draw (); } } } if (TopLevels.Count == 1 && state.Toplevel == Top && (Driver!.Cols != state.Toplevel!.Frame.Width || Driver!.Rows != state.Toplevel.Frame.Height) && (state.Toplevel.NeedsDisplay || state.Toplevel.SubViewNeedsDisplay || state.Toplevel.LayoutNeeded)) { Driver.ClearContents (); } if (state.Toplevel!.NeedsDisplay || state.Toplevel.SubViewNeedsDisplay || state.Toplevel.LayoutNeeded || ApplicationOverlapped.OverlappedChildNeedsDisplay ()) { state.Toplevel.SetNeedsDisplay (); state.Toplevel.Draw (); Driver!.UpdateScreen (); //Driver.UpdateCursor (); } if (PositionCursor (state.Toplevel)) { Driver!.UpdateCursor (); } // else { //if (PositionCursor (state.Toplevel)) //{ // Driver.Refresh (); //} //Driver.UpdateCursor (); } if (state.Toplevel != Top && !state.Toplevel.Modal && (Top!.NeedsDisplay || Top.SubViewNeedsDisplay || Top.LayoutNeeded)) { Top.Draw (); } } /// Stops the provided , causing or the if provided. /// The to stop. /// /// This will cause to return. /// /// Calling is equivalent to setting the /// property on the currently running to false. /// /// public static void RequestStop (Toplevel? top = null) { if (ApplicationOverlapped.OverlappedTop is null || top is null) { top = Current; } if (ApplicationOverlapped.OverlappedTop != null && top!.IsOverlappedContainer && top?.Running == true && (Current?.Modal == false || Current is { Modal: true, Running: false })) { ApplicationOverlapped.OverlappedTop.RequestStop (); } else if (ApplicationOverlapped.OverlappedTop != null && top != Current && Current is { Running: true, Modal: true } && top!.Modal && top.Running) { var ev = new ToplevelClosingEventArgs (Current); Current.OnClosing (ev); if (ev.Cancel) { return; } ev = new (top); top.OnClosing (ev); if (ev.Cancel) { return; } Current.Running = false; OnNotifyStopRunState (Current); top.Running = false; OnNotifyStopRunState (top); } else if ((ApplicationOverlapped.OverlappedTop != null && top != ApplicationOverlapped.OverlappedTop && top != Current && Current is { Modal: false, Running: true } && !top!.Running) || (ApplicationOverlapped.OverlappedTop != null && top != ApplicationOverlapped.OverlappedTop && top != Current && Current is { Modal: false, Running: false } && !top!.Running && TopLevels.ToArray () [1].Running)) { ApplicationOverlapped.MoveCurrent (top); } else if (ApplicationOverlapped.OverlappedTop != null && Current != top && Current?.Running == true && !top!.Running && Current?.Modal == true && top.Modal) { // The Current and the top are both modal so needed to set the Current.Running to false too. Current.Running = false; OnNotifyStopRunState (Current); } else if (ApplicationOverlapped.OverlappedTop != null && Current == top && ApplicationOverlapped.OverlappedTop?.Running == true && Current?.Running == true && top!.Running && Current?.Modal == true && top!.Modal) { // The OverlappedTop was requested to stop inside a modal Toplevel which is the Current and top, // both are the same, so needed to set the Current.Running to false too. Current.Running = false; OnNotifyStopRunState (Current); } else { Toplevel currentTop; if (top == Current || (Current?.Modal == true && !top!.Modal)) { currentTop = Current!; } else { currentTop = top!; } if (!currentTop.Running) { return; } var ev = new ToplevelClosingEventArgs (currentTop); currentTop.OnClosing (ev); if (ev.Cancel) { return; } currentTop.Running = false; OnNotifyStopRunState (currentTop); } } private static void OnNotifyStopRunState (Toplevel top) { if (EndAfterFirstIteration) { NotifyStopRunState?.Invoke (top, new (top)); } } /// /// Building block API: completes the execution of a that was started with /// . /// /// The returned by the method. public static void End (RunState runState) { ArgumentNullException.ThrowIfNull (runState); if (ApplicationOverlapped.OverlappedTop is { }) { ApplicationOverlapped.OverlappedTop.OnChildUnloaded (runState.Toplevel); } else { runState.Toplevel.OnUnloaded (); } // End the RunState.Toplevel // First, take it off the Toplevel Stack if (TopLevels.Count > 0) { if (TopLevels.Peek () != runState.Toplevel) { // If the top of the stack is not the RunState.Toplevel then // this call to End is not balanced with the call to Begin that started the RunState throw new ArgumentException ("End must be balanced with calls to Begin"); } TopLevels.Pop (); } // Notify that it is closing runState.Toplevel?.OnClosed (runState.Toplevel); // If there is a OverlappedTop that is not the RunState.Toplevel then RunState.Toplevel // is a child of MidTop, and we should notify the OverlappedTop that it is closing if (ApplicationOverlapped.OverlappedTop is { } && !runState.Toplevel!.Modal && runState.Toplevel != ApplicationOverlapped.OverlappedTop) { ApplicationOverlapped.OverlappedTop.OnChildClosed (runState.Toplevel); } // Set Current and Top to the next TopLevel on the stack if (TopLevels.Count == 0) { if (Current is { HasFocus: true }) { Current.HasFocus = false; } Current = null; } else { if (TopLevels.Count > 1 && TopLevels.Peek () == ApplicationOverlapped.OverlappedTop && ApplicationOverlapped.OverlappedChildren?.Any (t => t.Visible) != null) { ApplicationOverlapped.OverlappedMoveNext (); } Current = TopLevels.Peek (); if (TopLevels.Count == 1 && Current == ApplicationOverlapped.OverlappedTop) { ApplicationOverlapped.OverlappedTop.OnAllChildClosed (); } else { ApplicationOverlapped.SetCurrentOverlappedAsTop (); // BUGBUG: We should not call OnEnter/OnLeave directly; they should only be called by SetHasFocus if (runState.Toplevel is { HasFocus: true }) { runState.Toplevel.HasFocus = false; } if (Current is { HasFocus: false }) { Current.SetFocus (); Current.RestoreFocus (); } } Refresh (); } // Don't dispose runState.Toplevel. It's up to caller dispose it // If it's not the same as the current in the RunIteration, // it will be fixed later in the next RunIteration. if (ApplicationOverlapped.OverlappedTop is { } && !TopLevels.Contains (ApplicationOverlapped.OverlappedTop)) { _cachedRunStateToplevel = ApplicationOverlapped.OverlappedTop; } else { _cachedRunStateToplevel = runState.Toplevel; } runState.Toplevel = null; runState.Dispose (); } }