#nullable enable using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; namespace Terminal.Gui; public static partial class Application // Run (Begin, Run, End, Stop) { private static Key _quitKey = Key.Esc; // Resources/config.json overrides /// Gets or sets the key to quit the application. [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] public static Key QuitKey { get => _quitKey; set { if (_quitKey != value) { ReplaceKey (_quitKey, value); _quitKey = value; } } } private static Key _arrangeKey = Key.F5.WithCtrl; // Resources/config.json overrides /// Gets or sets the key to activate arranging views using the keyboard. [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] public static Key ArrangeKey { get => _arrangeKey; set { if (_arrangeKey != value) { ReplaceKey (_arrangeKey, value); _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); //#if DEBUG_IDISPOSABLE // Debug.Assert (!toplevel.WasDisposed); // if (_cachedRunStateToplevel is { } && _cachedRunStateToplevel != toplevel) // { // Debug.Assert (_cachedRunStateToplevel.WasDisposed); // } //#endif // Ensure the mouse is ungrabbed. MouseGrabView = null; var rs = new RunState (toplevel); #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); } } // 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) { 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 List ()); } 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 (); if (PositionCursor ()) { Driver?.UpdateCursor (); } NotifyNewRunState?.Invoke (toplevel, new (rs)); // Force an Idle event so that an Iteration (and Refresh) happen. Application.Invoke (() => { }); 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) { CursorVisibility 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, 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 (!Initialized) { // 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 (Initialized) { 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) ?? null; } /// 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 (); } // TODO: Rename this to LayoutAndDrawRunnables in https://github.com/gui-cs/Terminal.Gui/issues/2491 /// /// Causes any Toplevels that need layout to be laid out. Then draws any Toplevels that need dispplay. 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 LayoutAndDrawToplevels (bool forceDraw = false) { bool neededLayout = false; neededLayout = LayoutToplevels (); if (forceDraw) { Driver?.ClearContents (); } DrawToplevels (neededLayout || forceDraw); Driver?.Refresh (); } // TODO: Rename this to LayoutRunnables in https://github.com/gui-cs/Terminal.Gui/issues/2491 private static bool LayoutToplevels () { bool neededLayout = false; foreach (Toplevel tl in TopLevels.Reverse ()) { if (tl.NeedsLayout) { neededLayout = true; tl.Layout (Screen.Size); } } return neededLayout; } // TODO: Rename this to DrawRunnables in https://github.com/gui-cs/Terminal.Gui/issues/2491 private static void DrawToplevels (bool forceDraw) { foreach (Toplevel tl in TopLevels.Reverse ()) { if (forceDraw) { tl.SetNeedsDraw (); } tl.Draw (); } } /// 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; } firstIteration = RunIteration (ref state, 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, 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) { // If the driver has events pending do an iteration of the driver MainLoop if (MainLoop!.Running && MainLoop.EventsPending ()) { // Notify Toplevel it's ready if (firstIteration) { state.Toplevel.OnReady (); } MainLoop.RunIteration (); Iteration?.Invoke (null, new ()); } firstIteration = false; if (Top is null) { return firstIteration; } LayoutAndDrawToplevels (); if (PositionCursor ()) { Driver!.UpdateCursor (); } return firstIteration; } /// 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 (top is null) { top = Top; } if (!top!.Running) { return; } var ev = new ToplevelClosingEventArgs (top); top.OnClosing (ev); if (ev.Cancel) { return; } top.Running = false; OnNotifyStopRunState (top); } 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); 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 (TopLevels.Count > 0) { Top = TopLevels.Peek (); 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 (); LayoutAndDrawToplevels (); } }