using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reflection; namespace Terminal.Gui; /// A static, singleton class representing the application. This class is the entry point for the application. /// /// /// Application.Init(); /// var win = new Window ($"Example App ({Application.QuitKey} to quit)"); /// Application.Run(win); /// win.Dispose(); /// Application.Shutdown(); /// /// /// TODO: Flush this out. public static partial class Application { // For Unit testing - ignores UseSystemConsole internal static bool _forceFakeConsole; /// Gets the that has been selected. See also . public static ConsoleDriver Driver { get; internal set; } /// /// Gets or sets whether will be forced to output only the 16 colors defined in /// . The default is , meaning 24-bit (TrueColor) colors will be output /// as long as the selected supports TrueColor. /// [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] public static bool Force16Colors { get; set; } /// /// Forces the use of the specified driver (one of "fake", "ansi", "curses", "net", or "windows"). If not /// specified, the driver is selected based on the platform. /// /// /// Note, will override this configuration setting if called /// with either `driver` or `driverName` specified. /// [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] public static string ForceDriver { get; set; } = string.Empty; /// Gets all cultures supported by the application without the invariant language. public static List SupportedCultures { get; private set; } internal static List GetSupportedCultures () { CultureInfo [] culture = CultureInfo.GetCultures (CultureTypes.AllCultures); // Get the assembly var assembly = Assembly.GetExecutingAssembly (); //Find the location of the assembly string assemblyLocation = AppDomain.CurrentDomain.BaseDirectory; // Find the resource file name of the assembly var resourceFilename = $"{Path.GetFileNameWithoutExtension (AppContext.BaseDirectory)}.resources.dll"; // Return all culture for which satellite folder found with culture code. return culture.Where ( cultureInfo => Directory.Exists (Path.Combine (assemblyLocation, cultureInfo.Name)) && File.Exists (Path.Combine (assemblyLocation, cultureInfo.Name, resourceFilename)) ) .ToList (); } /// /// Gets the size of the screen. This is the size of the screen as reported by the . /// /// /// If the has not been initialized, this will return a default size of 2048x2048; useful for unit tests. /// public static Rectangle Screen => Driver?.Screen ?? new (0, 0, 2048, 2048); // 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; // IMPORTANT: Ensure all property/fields are reset here. See Init_ResetState_Resets_Properties unit test. // Encapsulate all setting of initial state for Application; Having // this in a function like this ensures we don't make mistakes in // guaranteeing that the state of this singleton is deterministic when Init // starts running and after Shutdown returns. internal static void ResetState (bool ignoreDisposed = false) { // Shutdown is the bookend for Init. As such it needs to clean up all resources // Init created. Apps that do any threading will need to code defensively for this. // e.g. see Issue #537 foreach (Toplevel t in _topLevels) { t.Running = false; } _topLevels.Clear (); Current = null; #if DEBUG_IDISPOSABLE // Don't dispose the Top. It's up to caller dispose it if (!ignoreDisposed && Top is { }) { Debug.Assert (Top.WasDisposed); // If End wasn't called _cachedRunStateToplevel may be null if (_cachedRunStateToplevel is { }) { Debug.Assert (_cachedRunStateToplevel.WasDisposed); Debug.Assert (_cachedRunStateToplevel == Top); } } #endif Top = null; _cachedRunStateToplevel = null; // MainLoop stuff MainLoop?.Dispose (); MainLoop = null; _mainThreadId = -1; Iteration = null; EndAfterFirstIteration = false; // Driver stuff if (Driver is { }) { Driver.SizeChanged -= Driver_SizeChanged; Driver.KeyDown -= Driver_KeyDown; Driver.KeyUp -= Driver_KeyUp; Driver.MouseEvent -= Driver_MouseEvent; Driver?.End (); Driver = null; } // Don't reset ForceDriver; it needs to be set before Init is called. //ForceDriver = string.Empty; //Force16Colors = false; _forceFakeConsole = false; // Run State stuff NotifyNewRunState = null; NotifyStopRunState = null; MouseGrabView = null; _initialized = false; // Mouse _mouseEnteredView = null; WantContinuousButtonPressedView = null; MouseEvent = null; GrabbedMouse = null; UnGrabbingMouse = null; GrabbedMouse = null; UnGrabbedMouse = null; // Keyboard AlternateBackwardKey = Key.Empty; AlternateForwardKey = Key.Empty; QuitKey = Key.Empty; KeyDown = null; KeyUp = null; SizeChanging = null; ClearKeyBindings (); Colors.Reset (); // Reset synchronization context to allow the user to run async/await, // as the main loop has been ended, the synchronization context from // gui.cs does no longer process any callbacks. See #1084 for more details: // (https://github.com/gui-cs/Terminal.Gui/issues/1084). SynchronizationContext.SetSynchronizationContext (null); } #region Initialization (Init/Shutdown) /// Initializes a new instance of Application. /// Call this method once per instance (or after has been called). /// /// This function loads the right for the platform, Creates a . and /// assigns it to /// /// /// must be called when the application is closing (typically after /// has returned) to ensure resources are cleaned up and /// terminal settings /// restored. /// /// /// The function combines /// and /// into a single /// call. An application cam use without explicitly calling /// . /// /// /// The to use. If neither or /// are specified the default driver for the platform will be used. /// /// /// The short name (e.g. "net", "windows", "ansi", "fake", or "curses") of the /// to use. If neither or are /// specified the default driver for the platform will be used. /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] public static void Init (ConsoleDriver driver = null, string driverName = null) { InternalInit (driver, driverName); } internal static bool _initialized; internal static int _mainThreadId = -1; // INTERNAL function for initializing an app with a Toplevel factory object, driver, and mainloop. // // Called from: // // Init() - When the user wants to use the default Toplevel. calledViaRunT will be false, causing all state to be reset. // Run() - When the user wants to use a custom Toplevel. calledViaRunT will be true, enabling Run() to be called without calling Init first. // Unit Tests - To initialize the app with a custom Toplevel, using the FakeDriver. calledViaRunT will be false, causing all state to be reset. // // calledViaRunT: If false (default) all state will be reset. If true the state will not be reset. [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] internal static void InternalInit ( ConsoleDriver driver = null, string driverName = null, bool calledViaRunT = false ) { if (_initialized && driver is null) { return; } if (_initialized) { throw new InvalidOperationException ("Init has already been called and must be bracketed by Shutdown."); } if (!calledViaRunT) { // Reset all class variables (Application is a singleton). ResetState (); } // For UnitTests if (driver is { }) { Driver = driver; } // Start the process of configuration management. // Note that we end up calling LoadConfigurationFromAllSources // multiple times. We need to do this because some settings are only // valid after a Driver is loaded. In this cases we need just // `Settings` so we can determine which driver to use. // Don't reset, so we can inherit the theme from the previous run. Load (); Apply (); // Ignore Configuration for ForceDriver if driverName is specified if (!string.IsNullOrEmpty (driverName)) { ForceDriver = driverName; } if (Driver is null) { PlatformID p = Environment.OSVersion.Platform; if (string.IsNullOrEmpty (ForceDriver)) { if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) { Driver = new WindowsDriver (); } else { Driver = new CursesDriver (); } } else { List drivers = GetDriverTypes (); Type driverType = drivers.FirstOrDefault (t => t.Name.Equals (ForceDriver, StringComparison.InvariantCultureIgnoreCase)); if (driverType is { }) { Driver = (ConsoleDriver)Activator.CreateInstance (driverType); } else { throw new ArgumentException ( $"Invalid driver name: {ForceDriver}. Valid names are {string.Join (", ", drivers.Select (t => t.Name))}" ); } } } try { MainLoop = Driver.Init (); } catch (InvalidOperationException ex) { // This is a case where the driver is unable to initialize the console. // This can happen if the console is already in use by another process or // if running in unit tests. // In this case, we want to throw a more specific exception. throw new InvalidOperationException ( "Unable to initialize the console. This can happen if the console is already in use by another process or in unit tests.", ex ); } Driver.SizeChanged += (s, args) => OnSizeChanging (args); Driver.KeyDown += (s, args) => OnKeyDown (args); Driver.KeyUp += (s, args) => OnKeyUp (args); Driver.MouseEvent += (s, args) => OnMouseEvent (args); SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext ()); SupportedCultures = GetSupportedCultures (); _mainThreadId = Thread.CurrentThread.ManagedThreadId; _initialized = true; InitializedChanged?.Invoke (null, new (in _initialized)); } private static void Driver_SizeChanged (object sender, SizeChangedEventArgs e) { OnSizeChanging (e); } private static void Driver_KeyDown (object sender, Key e) { OnKeyDown (e); } private static void Driver_KeyUp (object sender, Key e) { OnKeyUp (e); } private static void Driver_MouseEvent (object sender, MouseEvent e) { OnMouseEvent (e); } /// Gets of list of types that are available. /// [RequiresUnreferencedCode ("AOT")] public static List GetDriverTypes () { // use reflection to get the list of drivers List driverTypes = new (); foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies ()) { foreach (Type type in asm.GetTypes ()) { if (type.IsSubclassOf (typeof (ConsoleDriver)) && !type.IsAbstract) { driverTypes.Add (type); } } } return driverTypes; } /// Shutdown an application initialized with . /// /// Shutdown must be called for every call to or /// to ensure all resources are cleaned /// up (Disposed) /// and terminal settings are restored. /// public static void Shutdown () { // TODO: Throw an exception if Init hasn't been called. ResetState (); PrintJsonErrors (); InitializedChanged?.Invoke (null, new (in _initialized)); } #nullable enable /// /// This event is raised after the and methods have been called. /// /// /// Intended to support unit tests that need to know when the application has been initialized. /// public static event EventHandler>? InitializedChanged; #nullable restore #endregion Initialization (Init/Shutdown) #region Run (Begin, Run, End, Stop) /// /// 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 a 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 && OverlappedTop != toplevel && 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 (OverlappedTop is { } && toplevel != Top && _topLevels.Contains (Top)) { 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 (OverlappedTop is null || toplevel.IsOverlappedContainer || (Current?.Modal == false && toplevel.Modal) || (Current?.Modal == false && !toplevel.Modal) || (Current?.Modal == true && toplevel.Modal)) { if (toplevel.Visible) { Current?.OnDeactivate (toplevel); Toplevel previousCurrent = Current; Current = toplevel; Current.OnActivate (previousCurrent); SetCurrentOverlappedAsTop (); } else { refreshDriver = false; } } else if ((OverlappedTop != null && toplevel != OverlappedTop && Current?.Modal == true && !_topLevels.Peek ().Modal) || (OverlappedTop is { } && toplevel != OverlappedTop && Current?.Running == false)) { refreshDriver = false; MoveCurrent (toplevel); } else { refreshDriver = false; MoveCurrent (Current); } toplevel.SetRelativeLayout (Screen.Size); toplevel.LayoutSubviews (); toplevel.PositionToplevels (); toplevel.FocusFirst (); BringOverlappedTopToFront (); if (refreshDriver) { 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 }) ?? Application.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); } /// 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 (); View last = null; foreach (Toplevel v in _topLevels.Reverse ()) { if (v.Visible) { v.SetNeedsDisplay (); v.SetSubViewNeedsDisplay (); v.Draw (); } last = v; } 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); if (state.Toplevel != Current) { OverlappedTop?.OnDeactivate (state.Toplevel); state.Toplevel = Current; 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 || 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 (OverlappedTop is null || top is null || (OverlappedTop is null && top is { })) { top = Current; } if (OverlappedTop != null && top.IsOverlappedContainer && top?.Running == true && (Current?.Modal == false || (Current?.Modal == true && Current?.Running == false))) { OverlappedTop.RequestStop (); } else if (OverlappedTop != null && top != Current && Current?.Running == true && Current?.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 ((OverlappedTop != null && top != OverlappedTop && top != Current && Current?.Modal == false && Current?.Running == true && !top.Running) || (OverlappedTop != null && top != OverlappedTop && top != Current && Current?.Modal == false && Current?.Running == false && !top.Running && _topLevels.ToArray () [1].Running)) { MoveCurrent (top); } else if (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 (OverlappedTop != null && Current == top && 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 (OverlappedTop is { }) { 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 there 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 (OverlappedTop is { } && !runState.Toplevel.Modal && runState.Toplevel != OverlappedTop) { OverlappedTop.OnChildClosed (runState.Toplevel); } // Set Current and Top to the next TopLevel on the stack if (_topLevels.Count == 0) { Current = null; } else { if (_topLevels.Count > 1 && _topLevels.Peek () == OverlappedTop && OverlappedChildren.Any (t => t.Visible) is { }) { OverlappedMoveNext (); } Current = _topLevels.Peek (); if (_topLevels.Count == 1 && Current == OverlappedTop) { OverlappedTop.OnAllChildClosed (); } else { SetCurrentOverlappedAsTop (); runState.Toplevel.OnLeave (Current); Current.OnEnter (runState.Toplevel); } 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 (OverlappedTop is { } && !_topLevels.Contains (OverlappedTop)) { _cachedRunStateToplevel = OverlappedTop; } else { _cachedRunStateToplevel = runState.Toplevel; } runState.Toplevel = null; runState.Dispose (); } #endregion Run (Begin, Run, End) #region Toplevel handling /// Holds the stack of TopLevel views. // BUGBUG: Techncally, this is not the full lst of TopLevels. THere be dragons hwre. E.g. see how Toplevel.Id is used. What // about TopLevels that are just a SubView of another View? internal static readonly Stack _topLevels = new (); /// The object used for the application on startup () /// The top. public static Toplevel Top { get; private set; } /// /// The current object. This is updated in enters and leaves to /// point to the current /// . /// /// /// Only relevant in scenarios where is . /// /// The current. public static Toplevel Current { get; private set; } private static void EnsureModalOrVisibleAlwaysOnTop (Toplevel topLevel) { if (!topLevel.Running || (topLevel == Current && topLevel.Visible) || OverlappedTop == null || _topLevels.Peek ().Modal) { return; } foreach (Toplevel top in _topLevels.Reverse ()) { if (top.Modal && top != Current) { MoveCurrent (top); return; } } if (!topLevel.Visible && topLevel == Current) { OverlappedMoveNext (); } } #nullable enable private static Toplevel? FindDeepestTop (Toplevel start, in Point location) { if (!start.Frame.Contains (location)) { return null; } if (_topLevels is { Count: > 0 }) { int rx = location.X - start.Frame.X; int ry = location.Y - start.Frame.Y; foreach (Toplevel t in _topLevels) { if (t != Current) { if (t != start && t.Visible && t.Frame.Contains (rx, ry)) { start = t; break; } } } } return start; } #nullable restore private static View FindTopFromView (View view) { View top = view?.SuperView is { } && view?.SuperView != Top ? view.SuperView : view; while (top?.SuperView is { } && top?.SuperView != Top) { top = top.SuperView; } return top; } #nullable enable // Only return true if the Current has changed. private static bool MoveCurrent (Toplevel? top) { // The Current is modal and the top is not modal Toplevel then // the Current must be moved above the first not modal Toplevel. if (OverlappedTop is { } && top != OverlappedTop && top != Current && Current?.Modal == true && !_topLevels.Peek ().Modal) { lock (_topLevels) { _topLevels.MoveTo (Current, 0, new ToplevelEqualityComparer ()); } var index = 0; Toplevel [] savedToplevels = _topLevels.ToArray (); foreach (Toplevel t in savedToplevels) { if (!t.Modal && t != Current && t != top && t != savedToplevels [index]) { lock (_topLevels) { _topLevels.MoveTo (top, index, new ToplevelEqualityComparer ()); } } index++; } return false; } // The Current and the top are both not running Toplevel then // the top must be moved above the first not running Toplevel. if (OverlappedTop is { } && top != OverlappedTop && top != Current && Current?.Running == false && top?.Running == false) { lock (_topLevels) { _topLevels.MoveTo (Current, 0, new ToplevelEqualityComparer ()); } var index = 0; foreach (Toplevel t in _topLevels.ToArray ()) { if (!t.Running && t != Current && index > 0) { lock (_topLevels) { _topLevels.MoveTo (top, index - 1, new ToplevelEqualityComparer ()); } } index++; } return false; } if ((OverlappedTop is { } && top?.Modal == true && _topLevels.Peek () != top) || (OverlappedTop is { } && Current != OverlappedTop && Current?.Modal == false && top == OverlappedTop) || (OverlappedTop is { } && Current?.Modal == false && top != Current) || (OverlappedTop is { } && Current?.Modal == true && top == OverlappedTop)) { lock (_topLevels) { _topLevels.MoveTo (top, 0, new ToplevelEqualityComparer ()); Current = top; } } return true; } #nullable restore /// Invoked when the terminal's size changed. The new size of the terminal is provided. /// /// Event handlers can set to to prevent /// from changing it's size to match the new terminal size. /// public static event EventHandler SizeChanging; /// /// Called when the application's size changes. Sets the size of all s and fires the /// event. /// /// The new size. /// if the size was changed. public static bool OnSizeChanging (SizeChangedEventArgs args) { SizeChanging?.Invoke (null, args); if (args.Cancel || args.Size is null) { return false; } foreach (Toplevel t in _topLevels) { t.SetRelativeLayout (args.Size.Value); t.LayoutSubviews (); t.PositionToplevels (); t.OnSizeChanging (new (args.Size)); if (PositionCursor (t)) { Driver.UpdateCursor (); } } Refresh (); return true; } #endregion Toplevel handling /// /// Gets a string representation of the Application as rendered by . /// /// A string representation of the Application public new static string ToString () { ConsoleDriver driver = Driver; if (driver is null) { return string.Empty; } return ToString (driver); } /// /// Gets a string representation of the Application rendered by the provided . /// /// The driver to use to render the contents. /// A string representation of the Application public static string ToString (ConsoleDriver driver) { var sb = new StringBuilder (); Cell [,] contents = driver.Contents; for (var r = 0; r < driver.Rows; r++) { for (var c = 0; c < driver.Cols; c++) { Rune rune = contents [r, c].Rune; if (rune.DecodeSurrogatePair (out char [] sp)) { sb.Append (sp); } else { sb.Append ((char)rune.Value); } if (rune.GetColumns () > 1) { c++; } // See Issue #2616 //foreach (var combMark in contents [r, c].CombiningMarks) { // sb.Append ((char)combMark.Value); //} } sb.AppendLine (); } return sb.ToString (); } }