#nullable enable using System.Diagnostics; using System.Diagnostics.CodeAnalysis; namespace Terminal.Gui.App; public static partial class Application // Run (Begin -> Run -> Layout/Draw -> End -> Stop) { /// Gets or sets the key to quit the application. [ConfigurationProperty (Scope = typeof (SettingsScope))] public static Key QuitKey { get => Keyboard.QuitKey; set => Keyboard.QuitKey = value; } /// Gets or sets the key to activate arranging views using the keyboard. [ConfigurationProperty (Scope = typeof (SettingsScope))] public static Key ArrangeKey { get => Keyboard.ArrangeKey; set => Keyboard.ArrangeKey = value; } // 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); // Ensure the mouse is ungrabbed. if (Mouse.MouseGrabView is { }) { Mouse.UngrabMouse (); } var rs = new RunState (toplevel); #if DEBUG_IDISPOSABLE if (View.EnableDebugIDisposableAsserts && 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); } } // 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 (Top is null) { Top = toplevel; } if ((Top?.Modal == false && toplevel.Modal) || (Top?.Modal == false && !toplevel.Modal) || (Top?.Modal == true && toplevel.Modal)) { if (toplevel.Visible) { if (Top is { HasFocus: true }) { Top.HasFocus = false; } // Force leave events for any entered views in the old Top if (GetLastMousePosition () is { }) { RaiseMouseEnterLeaveEvents (GetLastMousePosition ()!.Value, new ()); } Top?.OnDeactivate (toplevel); Toplevel previousTop = Top!; Top = toplevel; Top.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 (); ApplicationImpl.Instance.LayoutAndDraw (true); if (PositionCursor ()) { Driver?.UpdateCursor (); } NotifyNewRunState?.Invoke (toplevel, new (rs)); return rs; } /// /// Calls on the most focused view. /// /// /// Does nothing if there is no most focused view. /// /// 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 () { // Find the most focused view and position the cursor there. View? mostFocused = Navigation?.GetFocused (); // If the view is not visible or enabled, don't position the cursor if (mostFocused is null || !mostFocused.Visible || !mostFocused.Enabled) { var current = CursorVisibility.Invisible; Driver?.GetCursorVisibility (out 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, IConsoleDriver? driver = null) { return ApplicationImpl.Instance.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. 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, IConsoleDriver? driver = null) where T : Toplevel, new() { return ApplicationImpl.Instance.Run (errorHandler, driver); } /// 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 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) { ApplicationImpl.Instance.Run (view, errorHandler); } /// 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 ApplicationImpl.Instance.AddTimeout (time, callback); } /// Removes a previously scheduled timeout /// The token parameter is the value returned by . /// Returns /// /// if the timeout is successfully removed; otherwise, /// /// . /// This method also returns /// /// if the timeout is not found. public static bool RemoveTimeout (object token) { return ApplicationImpl.Instance.RemoveTimeout (token); } /// Runs on the thread that is processing events /// the action to be invoked on the main processing thread. public static void Invoke (Action action) { ApplicationImpl.Instance.Invoke (action); } /// /// Causes any Toplevels that need layout to be laid out. Then draws any Toplevels that need display. Only Views that /// need to be laid out (see ) will be laid out. /// Only Views that need to be drawn (see ) will be drawn. /// /// /// If the entire View hierarchy will be redrawn. The default is and /// should only be overriden for testing. /// public static void LayoutAndDraw (bool forceRedraw = false) { ApplicationImpl.Instance.LayoutAndDraw (forceRedraw); } /// This event is raised on each iteration of the main loop. /// See also public static event EventHandler? Iteration; /// /// 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;) { if (EndAfterFirstIteration && !firstIteration) { return; } firstIteration = RunIteration (ref state, firstIteration); } // Run one last iteration to consume any outstanding input events from Driver // This is important for remaining OnKeyUp events. RunIteration (ref state, firstIteration); } /// Run one application iteration. /// The state returned by . /// /// Set to if this is the first run loop iteration. /// /// if at least one iteration happened. public static bool RunIteration (ref RunState state, bool firstIteration = false) { ApplicationImpl appImpl = (ApplicationImpl)ApplicationImpl.Instance; appImpl.Coordinator?.RunIteration (); return false; } /// 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) { ApplicationImpl.Instance.RequestStop (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 (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover) { ApplicationPopover.HideWithQuitCommand (visiblePopover); } runState.Toplevel.OnUnloaded (); // End the RunState.Toplevel // First, take it off the Toplevel Stack if (TopLevels.TryPop (out Toplevel? topOfStack)) { if (topOfStack != 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"); } } // Notify that it is closing runState.Toplevel?.OnClosed (runState.Toplevel); if (TopLevels.TryPeek (out Toplevel? newTop)) { Top = newTop; Top?.SetNeedsDraw (); } if (runState.Toplevel is { HasFocus: true }) { runState.Toplevel.HasFocus = false; } if (Top is { HasFocus: false }) { Top.SetFocus (); } _cachedRunStateToplevel = runState.Toplevel; runState.Toplevel = null; runState.Dispose (); LayoutAndDraw (true); } internal static void RaiseIteration () { Iteration?.Invoke (null, new ()); } }