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. /// /// /// // A simple Terminal.Gui app that creates a window with a frame and title with /// // 5 rows/columns of padding. /// Application.Init(); /// var win = new Window ($"Example App ({Application.QuitKey} to quit)") { /// X = 5, /// Y = 5, /// Width = Dim.Fill (5), /// Height = Dim.Fill (5) /// }; /// Application.Top.Add(win); /// Application.Run(); /// 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 (); } // 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; t.Dispose (); } _topLevels.Clear (); Current = null; Top?.Dispose (); Top = 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 (() => new Toplevel (), 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 ( Func topLevelFactory, 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. Load (true); 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 ()); Top = topLevelFactory (); Current = Top; // Ensure Top's layout is up to date. Current.SetRelativeLayout (Driver.Bounds); 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 () { 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) { if (Toplevel is null) { throw new ArgumentNullException (nameof (Toplevel)); } if (Toplevel.IsOverlappedContainer && OverlappedTop != Toplevel && OverlappedTop is { }) { throw new InvalidOperationException ("Only one Overlapped Container is allowed."); } // Ensure the mouse is ungrabed. MouseGrabView = null; var rs = new RunState (Toplevel); // View implements ISupportInitializeNotification which is derived from ISupportInitialize if (!Toplevel.IsInitialized) { Toplevel.BeginInit (); Toplevel.EndInit (); } lock (_topLevels) { // If Top was already initialized with Init, and Begin has never been called // Top was not added to the Toplevels Stack. It will thus never get disposed. // Clean it up here: if (Top is { } && Toplevel != Top && !_topLevels.Contains (Top)) { Top.Dispose (); Top = null; } else if (Top 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 Toplevels Id's"); } } if (Top is null || Toplevel.IsOverlappedContainer) { Top = Toplevel; } var refreshDriver = true; if (OverlappedTop == null || Toplevel.IsOverlappedContainer || (Current?.Modal == false && Toplevel.Modal) || (Current?.Modal == false && !Toplevel.Modal) || (Current?.Modal == true && Toplevel.Modal)) { if (Toplevel.Visible) { Current = Toplevel; 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); } //if (Toplevel.LayoutStyle == LayoutStyle.Computed) { Toplevel.SetRelativeLayout (Driver.Bounds); //} 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 RunStateEventArgs (rs)); return rs; } /// /// Runs the application by calling with the value of /// . /// /// See for more details. public static void Run (Func errorHandler = null) { Run (Top, errorHandler); } /// /// Runs the application by calling with a new instance of the /// specified -derived class. /// 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. /// /// /// See for more details. /// /// /// The to use. If not specified the default driver for the platform will /// be used ( , , or ). Must be /// if has already been called. /// public static void Run (Func errorHandler = null, ConsoleDriver driver = null) where T : Toplevel, new () { if (_initialized) { if (Driver is { }) { // Init() has been called and we have a driver, so just run the app. var top = new T (); Type type = top.GetType ().BaseType; while (type != typeof (Toplevel) && type != typeof (object)) { type = type.BaseType; } if (type != typeof (Toplevel)) { throw new ArgumentException ($"{top.GetType ().Name} must be derived from TopLevel"); } Run (top, errorHandler); } else { // 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 (() => new T (), driver, null, true); Run (Top, errorHandler); } } /// Runs the main loop on the given container. /// /// /// 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. /// /// /// 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) { 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 (!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; } // // provides the sync context set while executing code in Terminal.Gui, to let // users use async/await on their code // private class MainLoopSyncContext : SynchronizationContext { public override SynchronizationContext CreateCopy () { return new MainLoopSyncContext (); } public override void Post (SendOrPostCallback d, object state) { MainLoop.AddIdle ( () => { d (state); return false; } ); } //_mainLoop.Driver.Wakeup (); public override void Send (SendOrPostCallback d, object state) { if (Thread.CurrentThread.ManagedThreadId == _mainThreadId) { d (state); } else { var wasExecuted = false; Invoke ( () => { d (state); wasExecuted = true; } ); while (!wasExecuted) { Thread.Sleep (15); } } } } /// Building block API: Runs the main loop for the created . /// The state returned by the method. public static void RunLoop (RunState state) { if (state is null) { throw new ArgumentNullException (nameof (state)); } if (state.Toplevel is null) { throw new ObjectDisposedException ("state"); } var firstIteration = true; for (state.Toplevel.Running = true; state.Toplevel.Running;) { 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 IterationEventArgs ()); EnsureModalOrVisibleAlwaysOnTop (state.Toplevel); if (state.Toplevel != Current) { OverlappedTop?.OnDeactivate (state.Toplevel); state.Toplevel = Current; OverlappedTop?.OnActivate (state.Toplevel); Top.SetSubViewNeedsDisplay (); Refresh (); } } firstIteration = false; 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)) { state.Toplevel.Clear (Driver.Bounds); } 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 running the most recent 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 ToplevelClosingEventArgs (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 ToplevelEventArgs (top)); } } /// /// Building block API: completes the execution of a that was started with /// . /// /// The returned by the method. public static void End (RunState runState) { if (runState is null) { throw new ArgumentNullException (nameof (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 { Current = _topLevels.Peek (); if (_topLevels.Count == 1 && Current == OverlappedTop) { OverlappedTop.OnAllChildClosed (); } else { SetCurrentOverlappedAsTop (); runState.Toplevel.OnLeave (Current); Current.OnEnter (runState.Toplevel); } Refresh (); } runState.Toplevel?.Dispose (); 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 when /// enters and leaves to point to the current /// . /// /// 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; } // 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 != null && 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 != null && top != OverlappedTop && top != Current && Current?.Running == false && !top.Running) { 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; } /// 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 (new Rectangle (0, 0, args.Size.Width, args.Size.Height)); t.LayoutSubviews (); t.PositionToplevels (); t.OnSizeChanging (new SizeChangedEventArgs (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)) { OnUnGrabbedMouse (MouseGrabView); MouseGrabView = null; } } 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 ViewEventArgs (view)); } private static void OnUnGrabbedMouse (View view) { if (view is null) { return; } UnGrabbedMouse?.Invoke (view, new ViewEventArgs (view)); } // 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; #nullable enable /// 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; } var view = View.FindDeepestView (Current, a.MouseEvent.X, a.MouseEvent.Y, out int screenX, out int screenY); if (view is { } && view.WantContinuousButtonPressed) { WantContinuousButtonPressedView = view; } else { WantContinuousButtonPressedView = null; } if (view is { }) { a.MouseEvent.View = view; } MouseEvent?.Invoke (null, new MouseEventEventArgs (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 newxy = MouseGrabView.ScreenToFrame (a.MouseEvent.X, a.MouseEvent.Y); var nme = new MouseEvent { X = newxy.X, Y = newxy.Y, Flags = a.MouseEvent.Flags, OfX = a.MouseEvent.X - newxy.X, OfY = a.MouseEvent.Y - newxy.Y, View = view }; if (OutsideRect (new Point (nme.X, nme.Y), MouseGrabView.Bounds)) { // The mouse has moved outside the bounds of the the view that // grabbed the mouse, so we tell the view that last got // OnMouseEnter the mouse is leaving // BUGBUG: That sentence makes no sense. Either I'm missing something // or this logic is flawed. _mouseEnteredView?.OnMouseLeave (a.MouseEvent); } //System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}"); if (MouseGrabView?.OnMouseEvent (nme) == true) { return; } } if ((view is null || view == OverlappedTop) && Current is { Modal: false } && OverlappedTop != null && a.MouseEvent.Flags != MouseFlags.ReportMousePosition && a.MouseEvent.Flags != 0) { View top = FindDeepestTop (Top, a.MouseEvent.X, a.MouseEvent.Y); view = View.FindDeepestView (top, a.MouseEvent.X, a.MouseEvent.Y, out screenX, out screenY); if (view is { } && view != OverlappedTop && top != Current) { MoveCurrent ((Toplevel)top); } } bool AdornmentHandledMouseEvent (Adornment frame) { if (frame?.Thickness.Contains (frame.FrameToScreen (), a.MouseEvent.X, a.MouseEvent.Y) ?? false) { Point boundsPoint = frame.ScreenToBounds (a.MouseEvent.X, a.MouseEvent.Y); var me = new MouseEvent { X = boundsPoint.X, Y = boundsPoint.Y, Flags = a.MouseEvent.Flags, OfX = boundsPoint.X, OfY = boundsPoint.Y, View = frame }; frame.OnMouseEvent (me); return true; } return false; } if (view is { }) { // Work inside-out (Padding, Border, Margin) // TODO: Debate whether inside-out or outside-in is the right strategy if (AdornmentHandledMouseEvent (view?.Padding)) { return; } if (AdornmentHandledMouseEvent (view?.Border)) { if (view is Toplevel) { // TODO: This is a temporary hack to work around the fact that // drag handling is handled in Toplevel (See Issue #2537) var me = new MouseEvent { X = screenX, Y = screenY, Flags = a.MouseEvent.Flags, OfX = screenX, OfY = screenY, View = view }; 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; if (view.OnMouseEvent (me)) { // Should we bubble up the event, if it is not handled? //return; } BringOverlappedTopToFront (); } return; } if (AdornmentHandledMouseEvent (view?.Margin)) { return; } Rectangle bounds = view.BoundsToScreen (view.Bounds); if (bounds.Contains (a.MouseEvent.X, a.MouseEvent.Y)) { Point boundsPoint = view.ScreenToBounds (a.MouseEvent.X, a.MouseEvent.Y); var me = new MouseEvent { X = boundsPoint.X, Y = boundsPoint.Y, Flags = a.MouseEvent.Flags, OfX = boundsPoint.X, OfY = boundsPoint.Y, View = view }; 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; if (view.OnMouseEvent (me)) { // Should we bubble up the event, if it is not handled? //return; } BringOverlappedTopToFront (); } } return; static bool OutsideRect (Point p, Rectangle r) { return p.X < 0 || p.X > r.Right || p.Y < 0 || p.Y > r.Bottom; } } #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 KeyChangedEventArgs (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 KeyChangedEventArgs (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 KeyChangedEventArgs (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 } /// Event arguments for the event. public class IterationEventArgs { }