using System.Diagnostics; using System.Globalization; using System.Reflection; using System.Text.Json.Serialization; 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 (assembly.Location)}.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 (); } // 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 () { // 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; #if DEBUG_IDISPOSABLE // Don't dispose the toplevels. It's up to caller dispose them Debug.Assert (t.WasDisposed); #endif } _topLevels.Clear (); Current = null; #if DEBUG_IDISPOSABLE // Don't dispose the Top. It's up to caller dispose it if (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; 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. /// 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. 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; } 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, MouseEventEventArgs e) { OnMouseEvent (e); } /// Gets of list of types that are available. /// 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 (); } #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 (Driver.Bounds); // BUGBUG: This call is likely not needed. toplevel.LayoutSubviews (); toplevel.PositionToplevels (); toplevel.FocusFirst (); if (refreshDriver) { OverlappedTop?.OnChildLoaded (toplevel); toplevel.OnLoaded (); toplevel.SetNeedsDisplay (); toplevel.Draw (); toplevel.PositionCursor (); Driver.Refresh (); } NotifyNewRunState?.Invoke (toplevel, new (rs)); return rs; } /// /// 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. 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. public static T Run (Func errorHandler = null, ConsoleDriver driver = null) where T : Toplevel, new() { var top = new T (); Run (top, errorHandler, driver); 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. /// /// Calling first is not needed as this function will initialize the application. /// /// 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). /// /// /// The to use. If not specified the default driver for the platform will /// be used ( , , or ). Must be /// if was called. /// public static void Run (Toplevel view, Func errorHandler = null, ConsoleDriver driver = 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. InternalInit (driver, null, true); } 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; } last?.PositionCursor (); 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.Draw (); state.Toplevel.PositionCursor (); Driver.Refresh (); } else { 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, int x, int y) { if (!start.Frame.Contains (x, y)) { return null; } if (_topLevels is { Count: > 0 }) { int rx = x - start.Frame.X; int ry = 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) { return false; } foreach (Toplevel t in _topLevels) { t.SetRelativeLayout (Rectangle.Empty with { Size = args.Size }); t.LayoutSubviews (); t.PositionToplevels (); t.OnSizeChanging (new (args.Size)); } Refresh (); return true; } #endregion Toplevel handling #region Mouse handling /// Disable or enable the mouse. The mouse is enabled by default. [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] public static bool IsMouseDisabled { get; set; } /// The current object that wants continuous mouse button pressed events. public static View WantContinuousButtonPressedView { get; private set; } /// /// Gets the view that grabbed the mouse (e.g. for dragging). When this is set, all mouse events will be routed to /// this view until the view calls or the mouse is released. /// public static View MouseGrabView { get; private set; } /// Invoked when a view wants to grab the mouse; can be canceled. public static event EventHandler GrabbingMouse; /// Invoked when a view wants un-grab the mouse; can be canceled. public static event EventHandler UnGrabbingMouse; /// Invoked after a view has grabbed the mouse. public static event EventHandler GrabbedMouse; /// Invoked after a view has un-grabbed the mouse. public static event EventHandler UnGrabbedMouse; /// /// Grabs the mouse, forcing all mouse events to be routed to the specified view until /// is called. /// /// View that will receive all mouse events until is invoked. public static void GrabMouse (View view) { if (view is null) { return; } if (!OnGrabbingMouse (view)) { OnGrabbedMouse (view); MouseGrabView = view; } } /// Releases the mouse grab, so mouse events will be routed to the view on which the mouse is. public static void UngrabMouse () { if (MouseGrabView is null) { return; } if (!OnUnGrabbingMouse (MouseGrabView)) { View view = MouseGrabView; MouseGrabView = null; OnUnGrabbedMouse (view); } } private static bool OnGrabbingMouse (View view) { if (view is null) { return false; } var evArgs = new GrabMouseEventArgs (view); GrabbingMouse?.Invoke (view, evArgs); return evArgs.Cancel; } private static bool OnUnGrabbingMouse (View view) { if (view is null) { return false; } var evArgs = new GrabMouseEventArgs (view); UnGrabbingMouse?.Invoke (view, evArgs); return evArgs.Cancel; } private static void OnGrabbedMouse (View view) { if (view is null) { return; } GrabbedMouse?.Invoke (view, new (view)); } private static void OnUnGrabbedMouse (View view) { if (view is null) { return; } UnGrabbedMouse?.Invoke (view, new (view)); } #nullable enable // Used by OnMouseEvent to track the last view that was clicked on. internal static View? _mouseEnteredView; /// Event fired when a mouse move or click occurs. Coordinates are screen relative. /// /// /// Use this event to receive mouse events in screen coordinates. Use to /// receive mouse events relative to a 's bounds. /// /// The will contain the that contains the mouse coordinates. /// public static event EventHandler MouseEvent; /// Called when a mouse event occurs. Raises the event. /// This method can be used to simulate a mouse event, e.g. in unit tests. /// The mouse event with coordinates relative to the screen. internal static void OnMouseEvent (MouseEventEventArgs a) { if (IsMouseDisabled) { return; } // TODO: In PR #3273, FindDeepestView will return adornments. Update logic below to fix adornment mouse handling var view = View.FindDeepestView (Current, a.MouseEvent.X, a.MouseEvent.Y); if (view is { }) { a.MouseEvent.View = view; } MouseEvent?.Invoke (null, new (a.MouseEvent)); if (a.MouseEvent.Handled) { return; } if (MouseGrabView is { }) { // If the mouse is grabbed, send the event to the view that grabbed it. // The coordinates are relative to the Bounds of the view that grabbed the mouse. Point boundsLoc = MouseGrabView.ScreenToBounds (a.MouseEvent.X, a.MouseEvent.Y); var viewRelativeMouseEvent = new MouseEvent { X = boundsLoc.X, Y = boundsLoc.Y, Flags = a.MouseEvent.Flags, ScreenPosition = new (a.MouseEvent.X, a.MouseEvent.Y), View = MouseGrabView }; if (MouseGrabView.Bounds.Contains (viewRelativeMouseEvent.X, viewRelativeMouseEvent.Y) is false) { // The mouse has moved outside the bounds of the view that grabbed the mouse _mouseEnteredView?.OnMouseLeave (a.MouseEvent); } //System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}"); if (MouseGrabView?.OnMouseEvent (viewRelativeMouseEvent) == true) { return; } } if (view is { WantContinuousButtonPressed: true }) { WantContinuousButtonPressedView = view; } else { WantContinuousButtonPressedView = null; } if (view is not Adornment) { if ((view is null || view == OverlappedTop) && Current is { Modal: false } && OverlappedTop != null && a.MouseEvent.Flags != MouseFlags.ReportMousePosition && a.MouseEvent.Flags != 0) { // This occurs when there are multiple overlapped "tops" // E.g. "Mdi" - in the Background Worker Scenario View? top = FindDeepestTop (Top, a.MouseEvent.X, a.MouseEvent.Y); view = View.FindDeepestView (top, a.MouseEvent.X, a.MouseEvent.Y); if (view is { } && view != OverlappedTop && top != Current) { MoveCurrent ((Toplevel)top); } } } if (view is null) { return; } MouseEvent? me = null; if (view is Adornment adornment) { Point frameLoc = adornment.ScreenToFrame (a.MouseEvent.X, a.MouseEvent.Y); me = new () { X = frameLoc.X, Y = frameLoc.Y, Flags = a.MouseEvent.Flags, ScreenPosition = new (a.MouseEvent.X, a.MouseEvent.Y), View = view }; } else if (view.BoundsToScreen (view.Bounds).Contains (a.MouseEvent.X, a.MouseEvent.Y)) { Point boundsPoint = view.ScreenToBounds (a.MouseEvent.X, a.MouseEvent.Y); me = new () { X = boundsPoint.X, Y = boundsPoint.Y, Flags = a.MouseEvent.Flags, ScreenPosition = new (a.MouseEvent.X, a.MouseEvent.Y), View = view }; } if (me is null) { return; } if (_mouseEnteredView is null) { _mouseEnteredView = view; view.OnMouseEnter (me); } else if (_mouseEnteredView != view) { _mouseEnteredView.OnMouseLeave (me); view.OnMouseEnter (me); _mouseEnteredView = view; } if (!view.WantMousePositionReports && a.MouseEvent.Flags == MouseFlags.ReportMousePosition) { return; } WantContinuousButtonPressedView = view.WantContinuousButtonPressed ? view : null; //Debug.WriteLine ($"OnMouseEvent: ({a.MouseEvent.X},{a.MouseEvent.Y}) - {a.MouseEvent.Flags}"); if (view.OnMouseEvent (me)) { // Should we bubble up the event, if it is not handled? //return; } BringOverlappedTopToFront (); } #nullable restore #endregion Mouse handling #region Keyboard handling private static Key _alternateForwardKey = Key.Empty; // Defined in config.json /// Alternative key to navigate forwards through views. Ctrl+Tab is the primary key. [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] [JsonConverter (typeof (KeyJsonConverter))] public static Key AlternateForwardKey { get => _alternateForwardKey; set { if (_alternateForwardKey != value) { Key oldKey = _alternateForwardKey; _alternateForwardKey = value; OnAlternateForwardKeyChanged (new (oldKey, value)); } } } private static void OnAlternateForwardKeyChanged (KeyChangedEventArgs e) { foreach (Toplevel top in _topLevels.ToArray ()) { top.OnAlternateForwardKeyChanged (e); } } private static Key _alternateBackwardKey = Key.Empty; // Defined in config.json /// Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key. [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] [JsonConverter (typeof (KeyJsonConverter))] public static Key AlternateBackwardKey { get => _alternateBackwardKey; set { if (_alternateBackwardKey != value) { Key oldKey = _alternateBackwardKey; _alternateBackwardKey = value; OnAlternateBackwardKeyChanged (new (oldKey, value)); } } } private static void OnAlternateBackwardKeyChanged (KeyChangedEventArgs oldKey) { foreach (Toplevel top in _topLevels.ToArray ()) { top.OnAlternateBackwardKeyChanged (oldKey); } } private static Key _quitKey = Key.Empty; // Defined in config.json /// Gets or sets the key to quit the application. [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] [JsonConverter (typeof (KeyJsonConverter))] public static Key QuitKey { get => _quitKey; set { if (_quitKey != value) { Key oldKey = _quitKey; _quitKey = value; OnQuitKeyChanged (new (oldKey, value)); } } } private static void OnQuitKeyChanged (KeyChangedEventArgs e) { // Duplicate the list so if it changes during enumeration we're safe foreach (Toplevel top in _topLevels.ToArray ()) { top.OnQuitKeyChanged (e); } } /// /// Event fired when the user presses a key. Fired by . /// /// Set to to indicate the key was handled and to prevent /// additional processing. /// /// /// /// All drivers support firing the event. Some drivers (Curses) do not support firing the /// and events. /// Fired after and before . /// public static event EventHandler KeyDown; /// /// Called by the when the user presses a key. Fires the event /// then calls on all top level views. Called after and /// before . /// /// Can be used to simulate key press events. /// /// if the key was handled. public static bool OnKeyDown (Key keyEvent) { if (!_initialized) { return true; } KeyDown?.Invoke (null, keyEvent); if (keyEvent.Handled) { return true; } foreach (Toplevel topLevel in _topLevels.ToList ()) { if (topLevel.NewKeyDownEvent (keyEvent)) { return true; } if (topLevel.Modal) { break; } } // Invoke any Global KeyBindings foreach (Toplevel topLevel in _topLevels.ToList ()) { foreach (View view in topLevel.Subviews.Where ( v => v.KeyBindings.TryGet ( keyEvent, KeyBindingScope.Application, out KeyBinding _ ) )) { if (view.KeyBindings.TryGet (keyEvent.KeyCode, KeyBindingScope.Application, out KeyBinding _)) { bool? handled = view.OnInvokingKeyBindings (keyEvent); if (handled is { } && (bool)handled) { return true; } } } } return false; } /// /// Event fired when the user releases a key. Fired by . /// /// Set to to indicate the key was handled and to prevent /// additional processing. /// /// /// /// All drivers support firing the event. Some drivers (Curses) do not support firing the /// and events. /// Fired after . /// public static event EventHandler KeyUp; /// /// Called by the when the user releases a key. Fires the event /// then calls on all top level views. Called after . /// /// Can be used to simulate key press events. /// /// if the key was handled. public static bool OnKeyUp (Key a) { if (!_initialized) { return true; } KeyUp?.Invoke (null, a); if (a.Handled) { return true; } foreach (Toplevel topLevel in _topLevels.ToList ()) { if (topLevel.NewKeyUpEvent (a)) { return true; } if (topLevel.Modal) { break; } } return false; } #endregion Keyboard handling }