diff --git a/Example/Example.cs b/Example/Example.cs index c8b59dfd0..069e366d5 100644 --- a/Example/Example.cs +++ b/Example/Example.cs @@ -5,53 +5,68 @@ using Terminal.Gui; -// Initialize the console -Application.Init(); +Application.Run (); -// Creates the top-level window with border and title -var win = new Window("Example App (Ctrl+Q to quit)"); +System.Console.WriteLine ($"Username: {((ExampleWindow)Application.Top).usernameText.Text}"); -// Create input components and labels +// Before the application exits, reset Terminal.Gui for clean shutdown +Application.Shutdown (); -var usernameLabel = new Label("Username:"); -var usernameText = new TextField("") -{ - // Position text field adjacent to label - X = Pos.Right(usernameLabel) + 1, +// Defines a top-level window with border and title +public class ExampleWindow : Window { + public TextField usernameText; + + public ExampleWindow () + { + Title = "Example App (Ctrl+Q to quit)"; - // Fill remaining horizontal space with a margin of 1 - Width = Dim.Fill(1), -}; + // Create input components and labels + var usernameLabel = new Label () { + Text = "Username:" + }; -var passwordLabel = new Label(0,2,"Password:"); -var passwordText = new TextField("") -{ - Secret = true, - // align with the text box above - X = Pos.Left(usernameText), - Y = 2, - Width = Dim.Fill(1), -}; + usernameText = new TextField ("") { + // Position text field adjacent to the label + X = Pos.Right (usernameLabel) + 1, -// Create login button -var btnLogin = new Button("Login") -{ - Y = 4, - // center the login button horizontally - X = Pos.Center(), - IsDefault = true, -}; + // Fill remaining horizontal space + Width = Dim.Fill (), + }; -// When login button is clicked display a message popup -btnLogin.Clicked += () => MessageBox.Query("Logging In", "Login Successful", "Ok"); + var passwordLabel = new Label () { + Text = "Password:", + X = Pos.Left (usernameLabel), + Y = Pos.Bottom (usernameLabel) + 1 + }; -// Add all the views to the window -win.Add( - usernameLabel, usernameText, passwordLabel, passwordText,btnLogin -); + var passwordText = new TextField ("") { + Secret = true, + // align with the text box above + X = Pos.Left (usernameText), + Y = Pos.Top (passwordLabel), + Width = Dim.Fill (), + }; -// Show the application -Application.Run(win); + // Create login button + var btnLogin = new Button () { + Text = "Login", + Y = Pos.Bottom(passwordLabel) + 1, + // center the login button horizontally + X = Pos.Center (), + IsDefault = true, + }; -// After the application exits, release and reset console for clean shutdown -Application.Shutdown(); \ No newline at end of file + // When login button is clicked display a message popup + btnLogin.Clicked += () => { + if (usernameText.Text == "admin" && passwordText.Text == "password") { + MessageBox.Query ("Logging In", "Login Successful", "Ok"); + Application.RequestStop (); + } else { + MessageBox.ErrorQuery ("Logging In", "Incorrect username or password", "Ok"); + } + }; + + // Add the views to the Window + Add (usernameLabel, usernameText, passwordLabel, passwordText, btnLogin); + } +} \ No newline at end of file diff --git a/Example/Example.csproj b/Example/Example.csproj index fc1498ca1..2ebf163bc 100644 --- a/Example/Example.csproj +++ b/Example/Example.csproj @@ -10,9 +10,6 @@ 1.0 1.0 - - - diff --git a/README.md b/README.md index c99b6a634..2d3c808bb 100644 --- a/README.md +++ b/README.md @@ -68,56 +68,71 @@ The following example shows a basic Terminal.Gui application written in C#: using Terminal.Gui; -// Initialize the console -Application.Init(); +Application.Run (); -// Creates the top-level window with border and title -var win = new Window("Example App (Ctrl+Q to quit)"); +System.Console.WriteLine ($"Username: {((ExampleWindow)Application.Top).usernameText.Text}"); -// Create input components and labels +// Before the application exits, reset Terminal.Gui for clean shutdown +Application.Shutdown (); -var usernameLabel = new Label("Username:"); -var usernameText = new TextField("") -{ - // Position text field adjacent to label - X = Pos.Right(usernameLabel) + 1, +// Defines a top-level window with border and title +public class ExampleWindow : Window { + public TextField usernameText; + + public ExampleWindow () + { + Title = "Example App (Ctrl+Q to quit)"; - // Fill remaining horizontal space with a margin of 1 - Width = Dim.Fill(1), -}; + // Create input components and labels + var usernameLabel = new Label () { + Text = "Username:" + }; -var passwordLabel = new Label(0,2,"Password:"); -var passwordText = new TextField("") -{ - Secret = true, - // align with the text box above - X = Pos.Left(usernameText), - Y = 2, - Width = Dim.Fill(1), -}; + usernameText = new TextField ("") { + // Position text field adjacent to the label + X = Pos.Right (usernameLabel) + 1, -// Create login button -var btnLogin = new Button("Login") -{ - Y = 4, - // center the login button horizontally - X = Pos.Center(), - IsDefault = true, -}; + // Fill remaining horizontal space + Width = Dim.Fill (), + }; -// When login button is clicked display a message popup -btnLogin.Clicked += () => MessageBox.Query("Logging In", "Login Successful", "Ok"); + var passwordLabel = new Label () { + Text = "Password:", + X = Pos.Left (usernameLabel), + Y = Pos.Bottom (usernameLabel) + 1 + }; -// Add all the views to the window -win.Add( - usernameLabel, usernameText, passwordLabel, passwordText,btnLogin -); + var passwordText = new TextField ("") { + Secret = true, + // align with the text box above + X = Pos.Left (usernameText), + Y = Pos.Top (passwordLabel), + Width = Dim.Fill (), + }; -// Show the application -Application.Run(win); + // Create login button + var btnLogin = new Button () { + Text = "Login", + Y = Pos.Bottom(passwordLabel) + 1, + // center the login button horizontally + X = Pos.Center (), + IsDefault = true, + }; -// After the application exits, release and reset console for clean shutdown -Application.Shutdown(); + // When login button is clicked display a message popup + btnLogin.Clicked += () => { + if (usernameText.Text == "admin" && passwordText.Text == "password") { + MessageBox.Query ("Logging In", "Login Successful", "Ok"); + Application.RequestStop (); + } else { + MessageBox.ErrorQuery ("Logging In", "Incorrect username or password", "Ok"); + } + }; + + // Add the views to the Window + Add (usernameLabel, usernameText, passwordLabel, passwordText, btnLogin); + } +} ``` When run the application looks as follows: diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs index 9e77a775f..66eeed296 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -1451,16 +1451,20 @@ namespace Terminal.Gui { { TerminalResized = terminalResized; - var winSize = WinConsole.GetConsoleOutputWindow (out Point pos); - cols = winSize.Width; - rows = winSize.Height; + try { + var winSize = WinConsole.GetConsoleOutputWindow (out Point pos); + cols = winSize.Width; + rows = winSize.Height; - WindowsConsole.SmallRect.MakeEmpty (ref damageRegion); + WindowsConsole.SmallRect.MakeEmpty (ref damageRegion); - ResizeScreen (); - UpdateOffScreen (); + ResizeScreen (); + UpdateOffScreen (); - CreateColors (); + CreateColors (); + } catch (Win32Exception e) { + throw new InvalidOperationException ("The Windows Console output window is not available.", e); + } } public override void ResizeScreen () diff --git a/Terminal.Gui/Core/Application.cs b/Terminal.Gui/Core/Application.cs index ea47e6b51..65c1c6778 100644 --- a/Terminal.Gui/Core/Application.cs +++ b/Terminal.Gui/Core/Application.cs @@ -39,6 +39,7 @@ namespace Terminal.Gui { /// }; /// Application.Top.Add(win); /// Application.Run(); + /// Application.Shutdown(); /// /// /// @@ -222,19 +223,28 @@ namespace Terminal.Gui { public static bool ExitRunLoopAfterFirstIteration { get; set; } = false; /// - /// Notify that a new token was created, - /// used if is true. + /// 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 Action NotifyNewRunState; /// - /// Notify that a existent token is stopping, - /// used if is true. + /// 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 Action NotifyStopRunState; /// - /// This event is raised on each iteration of the + /// This event is raised on each iteration of the . /// /// /// See also @@ -299,36 +309,59 @@ namespace Terminal.Gui { /// public static bool UseSystemConsole; + // For Unit testing - ignores UseSystemConsole + internal static bool ForceFakeConsole; + /// /// Initializes a new instance of Application. /// - /// /// /// Call this method once per instance (or after has been called). /// /// - /// Loads the right for the platform. + /// This function loads the right for the platform, + /// Creates a . and assigns it to /// /// - /// 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. /// - /// - public static void Init (ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null) => Init (() => Toplevel.Create (), driver, mainLoopDriver); + /// + /// The function + /// combines and + /// into a single call. An applciation cam use + /// without explicitly calling . + /// + /// + /// The to use. If not specified the default driver for the + /// platform will be used (see , , and ). + /// + /// Specifies the to use. + /// Must not be if is not . + /// + public static void Init (ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null) => InternalInit (() => Toplevel.Create (), driver, mainLoopDriver); internal static bool _initialized = false; internal static int _mainThreadId = -1; - /// - /// Initializes the Terminal.Gui application - /// - static void Init (Func topLevelFactory, ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null) + // 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, IMainLoopDriver mainLoopDriver = null, bool calledViaRunT = false) { if (_initialized && driver == null) return; if (_initialized) { - throw new InvalidOperationException ("Init must be bracketed by Shutdown"); + throw new InvalidOperationException ("Init has already been called and must be bracketed by Shutdown."); } + // Note in this case, we don't verify the type of the Toplevel created by new T(). // Used only for start debugging on Unix. //#if DEBUG // while (!System.Diagnostics.Debugger.IsAttached) { @@ -337,23 +370,29 @@ namespace Terminal.Gui { // System.Diagnostics.Debugger.Break (); //#endif - // Reset all class variables (Application is a singleton). - ResetState (); + if (!calledViaRunT) { + // Reset all class variables (Application is a singleton). + ResetState (); + } - // This supports Unit Tests and the passing of a mock driver/loopdriver + // FakeDriver (for UnitTests) if (driver != null) { if (mainLoopDriver == null) { - throw new ArgumentNullException ("mainLoopDriver cannot be null if driver is provided."); + throw new ArgumentNullException ("InternalInit mainLoopDriver cannot be null if driver is provided."); + } + if (!(driver is FakeDriver)) { + throw new InvalidOperationException ("InternalInit can only be called with FakeDriver."); } Driver = driver; - Driver.Init (TerminalResized); - MainLoop = new MainLoop (mainLoopDriver); - SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext (MainLoop)); } if (Driver == null) { var p = Environment.OSVersion.Platform; - if (UseSystemConsole) { + if (ForceFakeConsole) { + // For Unit Testing only + Driver = new FakeDriver (); + mainLoopDriver = new FakeMainLoop (() => FakeConsole.ReadKey (true)); + } else if (UseSystemConsole) { Driver = new NetDriver (); mainLoopDriver = new NetMainLoop (Driver); } else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) { @@ -363,10 +402,21 @@ namespace Terminal.Gui { mainLoopDriver = new UnixMainLoop (); Driver = new CursesDriver (); } - Driver.Init (TerminalResized); - MainLoop = new MainLoop (mainLoopDriver); - SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext (MainLoop)); } + MainLoop = new MainLoop (mainLoopDriver); + + try { + Driver.Init (TerminalResized); + } 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); + } + + SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext (MainLoop)); + Top = topLevelFactory (); Current = Top; supportedCultures = GetSupportedCultures (); @@ -375,7 +425,7 @@ namespace Terminal.Gui { } /// - /// Captures the execution state for the provided view. + /// Captures the execution state for the provided view. /// public class RunState : IDisposable { /// @@ -391,31 +441,61 @@ namespace Terminal.Gui { /// public Toplevel Toplevel { get; internal set; } +#if DEBUG_IDISPOSABLE /// - /// Releases alTop = l resource used by the object. + /// For debug purposes to verify objects are being disposed properly /// - /// Call when you are finished using the . The + public bool WasDisposed = false; + /// + /// For debug purposes to verify objects are being disposed properly + /// + public int DisposedCount = 0; + /// + /// For debug purposes + /// + public static List Instances = new List (); + /// + /// For debug purposes + /// + public RunState () + { + Instances.Add (this); + } +#endif + + /// + /// Releases all resource used by the object. + /// + /// + /// Call when you are finished using the . + /// + /// /// method leaves the in an unusable state. After /// calling , you must release all references to the /// so the garbage collector can reclaim the memory that the - /// was occupying. + /// was occupying. + /// public void Dispose () { Dispose (true); GC.SuppressFinalize (this); +#if DEBUG_IDISPOSABLE + WasDisposed = true; +#endif } /// - /// Dispose the specified disposing. + /// Releases all resource used by the object. /// - /// The dispose. - /// If set to true disposing. + /// If set to we are disposing and should dispose held objects. protected virtual void Dispose (bool disposing) { if (Toplevel != null && disposing) { - End (Toplevel); - Toplevel.Dispose (); - Toplevel = null; + throw new InvalidOperationException ("You must clean up (Dispose) the Toplevel before calling Application.RunState.Dispose"); + // BUGBUG: It's insidious that we call EndFirstTopLevel here so I moved it to End. + //EndFirstTopLevel (Toplevel); + //Toplevel.Dispose (); + //Toplevel = null; } } } @@ -802,8 +882,8 @@ namespace Terminal.Gui { /// /// Building block API: Prepares the provided for execution. /// - /// The runstate handle that needs to be passed to the method upon completion. - /// Toplevel to prepare execution for. + /// The handle that needs to be passed to the method upon completion. + /// The to prepare execution for. /// /// This method prepares the provided toplevel for running with the focus, /// it adds this to the list of toplevels, sets up the mainloop to process the @@ -816,13 +896,12 @@ namespace Terminal.Gui { { if (toplevel == null) { throw new ArgumentNullException (nameof (toplevel)); - } else if (toplevel.IsMdiContainer && MdiTop != null) { + } else if (toplevel.IsMdiContainer && MdiTop != toplevel && MdiTop != null) { throw new InvalidOperationException ("Only one Mdi Container is allowed."); } var rs = new RunState (toplevel); - - Init (); + if (toplevel is ISupportInitializeNotification initializableNotification && !initializableNotification.IsInitialized) { initializableNotification.BeginInit (); @@ -833,6 +912,13 @@ namespace Terminal.Gui { } 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 != null && toplevel != Top && !toplevels.Contains(Top)) { + Top.Dispose (); + Top = null; + } if (string.IsNullOrEmpty (toplevel.Id.ToString ())) { var count = 1; var id = (toplevels.Count + count).ToString (); @@ -854,7 +940,8 @@ namespace Terminal.Gui { throw new ArgumentException ("There are duplicates toplevels Id's"); } } - if (toplevel.IsMdiContainer) { + // Fix $520 - Set Top = toplevel if Top == null + if (Top == null || toplevel.IsMdiContainer) { Top = toplevel; } @@ -893,13 +980,14 @@ namespace Terminal.Gui { Driver.Refresh (); } + NotifyNewRunState?.Invoke (rs); return rs; } /// - /// Building block API: completes the execution of a that was started with . + /// Building block API: completes the execution of a that was started with . /// - /// The runstate returned by the method. + /// The returned by the method. public static void End (RunState runState) { if (runState == null) @@ -910,12 +998,52 @@ namespace Terminal.Gui { } 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 MdiTop that is not the RunState.Toplevel then runstate.TopLevel + // is a child of MidTop and we should notify the MdiTop that it is closing + if (MdiTop != null && !(runState.Toplevel).Modal && runState.Toplevel != MdiTop) { + MdiTop.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 == MdiTop) { + MdiTop.OnAllChildClosed (); + } else { + SetCurrentAsTop (); + } + Refresh (); + } + + runState.Toplevel?.Dispose (); + runState.Toplevel = null; runState.Dispose (); } /// - /// Shutdown an application initialized with + /// 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 (); @@ -930,15 +1058,17 @@ namespace Terminal.Gui { // 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 - // TODO: Some of this state is actually related to Begin/End (not Init/Shutdown) and should be moved to `RunState` (#520) foreach (var t in toplevels) { t.Running = false; t.Dispose (); } toplevels.Clear (); Current = null; + Top?.Dispose (); Top = null; + // BUGBUG: MdiTop is not cleared here, but it should be? + MainLoop = null; Driver?.End (); Driver = null; @@ -990,40 +1120,17 @@ namespace Terminal.Gui { Driver.Refresh (); } - internal static void End (View view) - { - if (toplevels.Peek () != view) - throw new ArgumentException ("The view that you end with must be balanced"); - toplevels.Pop (); - (view as Toplevel)?.OnClosed ((Toplevel)view); - - if (MdiTop != null && !((Toplevel)view).Modal && view != MdiTop) { - MdiTop.OnChildClosed (view as Toplevel); - } - - if (toplevels.Count == 0) { - Current = null; - } else { - Current = toplevels.Peek (); - if (toplevels.Count == 1 && Current == MdiTop) { - MdiTop.OnAllChildClosed (); - } else { - SetCurrentAsTop (); - } - Refresh (); - } - } /// - /// Building block API: Runs the main loop for the created dialog + /// Building block API: Runs the for the created . /// /// - /// Use the wait parameter to control whether this is a - /// blocking or non-blocking call. + /// Use the parameter to control whether this is a blocking or non-blocking call. /// - /// The state returned by the Begin method. - /// By default this is true which will execute the runloop waiting for events, if you pass false, you can use this method to run a single iteration of the events. + /// The state returned by the method. + /// By default this is which will execute the runloop waiting for events, + /// if set to , a single iteration will execute. public static void RunLoop (RunState state, bool wait = true) { if (state == null) @@ -1033,18 +1140,21 @@ namespace Terminal.Gui { bool firstIteration = true; for (state.Toplevel.Running = true; state.Toplevel.Running;) { - if (ExitRunLoopAfterFirstIteration && !firstIteration) + if (ExitRunLoopAfterFirstIteration && !firstIteration) { return; + } RunMainLoopIteration (ref state, wait, ref firstIteration); } } /// - /// Run one iteration of the MainLoop. + /// Run one iteration of the . /// - /// The state returned by the Begin method. - /// If will execute the runloop waiting for events. - /// If it's the first run loop iteration. + /// The state returned by . + /// If will execute the runloop waiting for events. If + /// will return after a single iteration. + /// 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 RunMainLoopIteration (ref RunState state, bool wait, ref bool firstIteration) { if (MainLoop.EventsPending (wait)) { @@ -1145,30 +1255,57 @@ namespace Terminal.Gui { } /// - /// Runs the application by calling with the value of + /// 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 + /// Runs the application by calling + /// with a new instance of the specified -derived class. + /// + /// Calling first is not needed as this function will initialze 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. + /// /// - public static void Run (Func errorHandler = null) where T : Toplevel, new() + /// + /// See for more details. + /// + /// + /// The to use. If not specified the default driver for the + /// platform will be used (, , or ). + /// This parameteter must be if has already been called. + /// + /// Specifies the to use. + public static void Run (Func errorHandler = null, ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null) where T : Toplevel, new() { - if (_initialized && Driver != null) { - var top = new T (); - var type = top.GetType ().BaseType; - while (type != typeof (Toplevel) && type != typeof (object)) { - type = type.BaseType; + if (_initialized) { + if (Driver != null) { + // Init() has been called and we have a driver, so just run the app. + var top = new T (); + var 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 codepath 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."); } - if (type != typeof (Toplevel)) { - throw new ArgumentException ($"{top.GetType ().Name} must be derived from TopLevel"); - } - Run (top, errorHandler); } else { - Init (() => new T ()); + // Init() has NOT been called. + InternalInit (() => new T (), driver, mainLoopDriver, calledViaRunT: true); Run (Top, errorHandler); } } @@ -1192,16 +1329,19 @@ namespace Terminal.Gui { /// /// 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 + /// 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 is null the exception is rethrown, when it returns true the application is resumed and when false method exits gracefully. + /// RELEASE builds only: When is any exeptions will be rethrown. + /// Otheriwse, if will be called. If + /// returns the will resume; otherwise + /// this method will exit. /// /// /// The to run modally. - /// Handler for any unhandled exceptions (resumes when returns true, rethrows when null). + /// 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; @@ -1211,13 +1351,12 @@ namespace Terminal.Gui { #endif resume = false; var runToken = Begin (view); + // If ExitRunLoopAfterFirstIteration is true then the user must dispose of the runToken + // by using NotifyStopRunState event. RunLoop (runToken); - if (!ExitRunLoopAfterFirstIteration) + if (!ExitRunLoopAfterFirstIteration) { End (runToken); - else - // If ExitRunLoopAfterFirstIteration is true then the user must deal his disposing when it ends - // by using NotifyStopRunState event. - NotifyNewRunState?.Invoke (runToken); + } #if !DEBUG } catch (Exception error) @@ -1310,8 +1449,9 @@ namespace Terminal.Gui { static void OnNotifyStopRunState (Toplevel top) { - if (ExitRunLoopAfterFirstIteration) + if (ExitRunLoopAfterFirstIteration) { NotifyStopRunState?.Invoke (top); + } } /// diff --git a/Terminal.Gui/Core/ConsoleDriver.cs b/Terminal.Gui/Core/ConsoleDriver.cs index cd180af1c..8ace3d396 100644 --- a/Terminal.Gui/Core/ConsoleDriver.cs +++ b/Terminal.Gui/Core/ConsoleDriver.cs @@ -683,7 +683,7 @@ namespace Terminal.Gui { public abstract void Move (int col, int row); /// - /// Adds the specified rune to the display at the current cursor position + /// Adds the specified rune to the display at the current cursor position. /// /// Rune to add. public abstract void AddRune (Rune rune); @@ -717,10 +717,11 @@ namespace Terminal.Gui { col >= 0 && row >= 0 && col < Cols && row < Rows && clip.Contains (col, row); /// - /// Adds the specified + /// Adds the to the display at the cursor position. /// /// String. public abstract void AddStr (ustring str); + /// /// Prepare the driver and set the key and mouse events handlers. /// diff --git a/Terminal.Gui/Core/MainLoop.cs b/Terminal.Gui/Core/MainLoop.cs index e71367646..9b6492fe0 100644 --- a/Terminal.Gui/Core/MainLoop.cs +++ b/Terminal.Gui/Core/MainLoop.cs @@ -94,8 +94,8 @@ namespace Terminal.Gui { public IMainLoopDriver Driver { get; } /// - /// Invoked when a new timeout is added to be used on the case - /// if is true, + /// Invoked when a new timeout is added. To be used in the case + /// when is . /// public event Action TimeoutAdded; diff --git a/Terminal.Gui/Core/Toplevel.cs b/Terminal.Gui/Core/Toplevel.cs index a922f729d..707a3e0a5 100644 --- a/Terminal.Gui/Core/Toplevel.cs +++ b/Terminal.Gui/Core/Toplevel.cs @@ -44,7 +44,7 @@ namespace Terminal.Gui { public bool Running { get; set; } /// - /// Invoked when the Toplevel has begin loaded. + /// Invoked when the Toplevel has begun to be loaded. /// A Loaded event handler is a good place to finalize initialization before calling /// . /// @@ -77,13 +77,13 @@ namespace Terminal.Gui { /// /// Invoked when a child of the Toplevel is closed by - /// . + /// . /// public event Action ChildClosed; /// /// Invoked when the last child of the Toplevel is closed from - /// by . + /// by . /// public event Action AllChildClosed; @@ -94,7 +94,7 @@ namespace Terminal.Gui { public event Action Closing; /// - /// Invoked when the Toplevel's is closed by . + /// Invoked when the Toplevel's is closed by . /// public event Action Closed; diff --git a/Terminal.Gui/Core/View.cs b/Terminal.Gui/Core/View.cs index 417c222a3..274f40de9 100644 --- a/Terminal.Gui/Core/View.cs +++ b/Terminal.Gui/Core/View.cs @@ -1496,7 +1496,7 @@ namespace Terminal.Gui { if (Border != null) { Border.DrawContent (this); } else if (ustring.IsNullOrEmpty (TextFormatter.Text) && - (GetType ().IsPublic || GetType ().IsNestedPublic) && !IsOverridden (this, "Redraw") && + (GetType ().IsNestedPublic) && !IsOverridden (this, "Redraw") && (!NeedDisplay.IsEmpty || ChildNeedsDisplay || LayoutNeeded)) { Clear (); diff --git a/Terminal.Gui/README.md b/Terminal.Gui/README.md index 1b621260a..d459d3ac0 100644 --- a/Terminal.Gui/README.md +++ b/Terminal.Gui/README.md @@ -1,24 +1,24 @@ # Terminal.Gui Project -Contains all files required to build the **Terminal.Gui** library (and NuGet package). +All files required to build the **Terminal.Gui** library (and NuGet package). ## Project Folder Structure - `Terminal.Gui.sln` - The Visual Studio solution - `Core/` - Source files for all types that comprise the core building blocks of **Terminal-Gui** - `Application` - A `static` class that provides the base 'application driver'. Given it defines a **Terminal.Gui** application it is both logically and literally (because `static`) a singleton. It has direct dependencies on `MainLoop`, `Events.cs` `NetDriver`, `CursesDriver`, `WindowsDriver`, `Responder`, `View`, and `TopLevel` (and nothing else). - - `MainLoop` - Defines `IMainLoopDriver` and implements the and `MainLoop` class. + - `MainLoop` - Defines `IMainLoopDriver` and implements the `MainLoop` class. - `ConsoleDriver` - Definition for the Console Driver API. - - `Events.cs` - Defines keyboard and mouse related structs & classes. + - `Events.cs` - Defines keyboard and mouse-related structs & classes. - `PosDim.cs` - Implements *Computed Layout* system. These classes have deep dependencies on `View`. - `Responder` - Base class for the windowing class hierarchy. Implements support for keyboard & mouse input. - `View` - Derived from `Responder`, the base class for non-modal visual elements such as controls. - `Toplevel` - Derived from `View`, the base class for modal visual elements such as top-level windows and dialogs. Supports the concept of `MenuBar` and `StatusBar`. - - `Window` - Derived from `TopLevel`; implements top level views with a visible frame and Title. + - `Window` - Derived from `TopLevel`; implements toplevel views with a visible frame and Title. - `Types/` - A folder (not namespace) containing implementations of `Point`, `Rect`, and `Size` which are ancient versions of the modern `System.Drawing.Point`, `System.Drawing.Size`, and `System.Drawning.Rectangle`. - `ConsoleDrivers/` - Source files for the three `ConsoleDriver`-based drivers: .NET: `NetDriver`, Unix & Mac: `UnixDriver`, and Windows: `WindowsDriver`. - `Views/` - A folder (not namespace) containing the source for all built-in classes that drive from `View` (non-modals). -- `Windows/` - A folder (not namespace) containing the source all built-in classes that derive from `Window`. +- `Windows/` - A folder (not namespace) containing the source of all built-in classes that derive from `Window`. ## Version numbers @@ -55,43 +55,37 @@ The `tag` must be of the form `v..`, e.g. `v2.3.4`. `patch` can indicate pre-release or not (e.g. `pre`, `beta`, `rc`, etc...). -### 1) Generate release notes with the list of PRs since the last release +### 1) Verify the `develop` branch is ready for release -Use `gh` to get a list with just titles to make it easy to paste into release notes: +* Ensure everything is committed and pushed to the `develop` branch +* Ensure your local `develop` branch is up-to-date with `upstream/develop` -```powershell -gh pr list --limit 500 --search "is:pr is:closed is:merged closed:>=2021-05-18" -``` +### 2) Create a pull request for the release in the `develop` branch -Use the output to update `./Terminal.Gui/Terminal.Gui.csproj` with latest release notes - -### 2) Update the API documentation - -See `./docfx/README.md`. - -### 3) Create a PR for the release in the `develop` branch - -The PR title should be "Release v2.3.4" +The PR title should be of the form "Release v2.3.4" ```powershell git checkout develop -git pull -all +git pull upstream develop git checkout -b v_2_3_4 +git merge develop git add . git commit -m "Release v2.3.4" git push ``` -### 4) On github.com, verify the build action worked on your fork, then merge the PR +Go to the link printed by `git push` and fill out the Pull Request. -### 5) Pull the merged `develop` from `upstream` +### 3) On github.com, verify the build action worked on your fork, then merge the PR + +### 4) Pull the merged `develop` from `upstream` ```powershell git checkout develop git pull upstream develop ``` -### 6) Merge `develop` into `main` +### 5) Merge `develop` into `main` ```powershell git checkout main @@ -101,13 +95,13 @@ git merge develop Fix any merge errors. -### 7) Create a new annotated tag for the release +### 6) Create a new annotated tag for the release on `main` ```powershell git tag v2.3.4 -a -m "Release v2.3.4" ``` -### 8) Push the new tag to `main` on `origin` +### 7) Push the new tag to `main` on `upstream` ```powershell git push --atomic upstream main v2.3.4 @@ -115,16 +109,23 @@ git push --atomic upstream main v2.3.4 *See https://stackoverflow.com/a/3745250/297526* -### 9) Monitor Github actions to ensure the Nuget publishing worked. +### 8) Monitor Github Actions to ensure the Nuget publishing worked. -### 10) Check Nuget to see the new package version (wait a few minutes): +https://github.com/gui-cs/Terminal.Gui/actions + +### 9) Check Nuget to see the new package version (wait a few minutes) https://www.nuget.org/packages/Terminal.Gui -### 11) Add a new Release in Github: https://github.com/gui-cs/Terminal.Gui/releases +### 10) Add a new Release in Github: https://github.com/gui-cs/Terminal.Gui/releases -### 12) Tweet about it +Generate release notes with the list of PRs since the last release -### 13) Update the `develop` branch +Use `gh` to get a list with just titles to make it easy to paste into release notes: + +```powershell +gh pr list --limit 500 --search "is:pr is:closed is:merged closed:>=2021-05-18" +``` +### 11) Update the `develop` branch with the new version ```powershell git checkout develop diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj index 77c964b34..4cbb36d5c 100644 --- a/Terminal.Gui/Terminal.Gui.csproj +++ b/Terminal.Gui/Terminal.Gui.csproj @@ -17,7 +17,7 @@ - + @@ -74,93 +74,12 @@ logo.png README.md csharp, terminal, c#, f#, gui, toolkit, console, tui - Cross Platform Terminal UI toolkit for .NET + Cross platform Terminal UI toolkit for .NET Miguel de Icaza, Charlie Kindel A toolkit for building rich console apps for .NET that works on Windows, Mac, and Linux/Unix. - Terminal.Gui - Cross Platform Terminal user interface toolkit for .NET + Terminal.Gui - Cross platform Terminal User Interface (TUI) toolkit for .NET - Release v1.8.1 - * Fixes #2053. MessageBox.Query not wrapping correctly - - Release v1.8.0 - * Fixes #2043. Update to NStack v1.0.3 - * Fixes #2045. TrySetClipboardData test must be enclosed with a lock. - * Fixes #2025. API Docs are now generated via Github Action - View Source Works - * Fixes #1991. Broken link in README - * Fixes #2026. Added ClearOnVisibleFalse to flag if the view must be cleared or not. - * Fixes #2017 and #2013. MainLoopTests.InvokeLeakTest failures - * Fixes #2014. Application mouseGrabView is run twice if return true. - * Fixes #2011. Wizard no longer needs to set specific colors, because #1971 has been fixed. - * Fixes #2006. ProgressBarStyles isn't disposing the _fractionTimer on quitting if running. - * Fixes #2004. TextFormatter.Justified not adding the extra spaces. - * Fixes #2002. Added feature to fill the remaining width with spaces. - * Fixes #1999. Prevents the mouseGrabView being executed with a null view. - * Fixes #1994. BREAKING CHANGE. Ensure only a single IdleHandlers list can exist. - * Fixes #1979. MessageBox.Query not wrapping since 1.7.1 - * Fixes #1989. ListView: Ensures SelectedItem visibility on MoveDown and MoveUp. - * Fixes #1987. Textview insert text newline fix - * Fixes #1984. Setting Label.Visible to false does not hide the Label - * Fixes #820. Added HideDropdownListOnClick property. - * Fixes #1981. Added SplitNewLine method to the TextFormatter. - * Fixes #1973. Avoid positioning Submenus off screen. - * Added abstract MakeColor and CreateColors to create the colors at once. - * Fixes #1800. TextView now uses the same colors as TextField. - * Fixes #1969. ESC on CursesDriver take to long to being processed. - * Fixes #1967. New keys for DeleteAll on TextField and TextView. - * Fixes #1962 - Change KeyBindings to allow chaining commands - * Fixes #1961 Null reference in Keybindings Scenario and hotkey collision - * Fixes #1963. Only remove one character on backspace when wordwrap is on - * Fixes #1959. GoToEnd should not fail on an empty TreeView - * Fixes #1953. TextView cursor position is not updating by mouse. - * Fixes #1951. TextView with selected text doesn't scroll beyond the cursor position. - * Fixes #1948. Get unwrapped cursor position when word wrap is enabled on TextView. - * Ensures that the isButtonShift flag is disabled in all situations. - * Fixes #1943. Mouse ButtonShift is not preserving the text selected. - - Release v1.7.2 - * Fixes #1773. Base color scheme for ListView hard to read - * Fixes #1934. WindowsDriver crash when the height is less than 1 with the VS Debugger - - Release v1.7.1 - * Fixes #1930. Trailing whitespace makes MessageBox.Query buttons disappear. - * Fixes #1921. Mouse continuous button pressed is not working on ScrollView. - * Fixes #1924. Wizard: Selected help text is unreadable - - Release v1.7.0 - * Moved Terminal.Gui (and NStack) to the github.com/gui-cs organization. - * Adds multi-step Wizard View for setup experiences (#124) - * The synchronization context method Send is now blocking (#1854). - * Fixes #1917. Sometimes Clipboard.IsSupported doesn't return the correct - * Fixes #1893: Fix URLs to match gui-cs Org - * Fixes #1883. Child TopLevels now get Loaded/Ready events. - * Fixes #1867, #1866, #1796. TextView enhancements for ReadOnly and WordWrap. - * Fixes #1861. Border: Title property is preferable to Text. - * Fixes #1855. Window and Frame content view without the margin frame. - * Fixes #1848. Mouse clicks in Windows Terminal. - * Fixes #1846. TabView now clips to the draw bounds. - * Fix TableView multi selections extending to -1 indexes - * Fixes #1837. Setting Unix clipboard freezes. - * Fixes #1839. Process WindowsDriver click event if location is the same after pressed and released. - * Fixes #1830. If "libcoreclr.so" is not present then "libncursesw.so" will be used. - * Fixes #1816. MessageBox: Hides underlying dialog when visible - * Fixes #1815. Now returns false if WSL clipboard isn't supported. - * Fixes #1825. Parent MenuItem stay focused if child MenuItem is empty. - * Fixes #1812, #1797, #1791. AutoSize fixes. - * Fixes #1818. Adds Title change events to Window. - * Fixes #1810. Dialog: Closing event is not fired when ESC is pressed to close dialog. - * Fixes #1793. ScrollBarView is hiding if the host fit the available space. - * Added Pos/Dim Function feature to automate layout. - * Fixes #1786. Windows Terminal is reporting well on mouse button pressed + mouse movement. - * Fixes #1777 - Dialog button justification. Adds unit tests. - * Fixes #1739. Setting menu UseKeysUpDownAsKeysLeftRight as false by default. - * Fixes #1772. Avoids WindowsDriver flickering when resizing. - * Fixed TableView always showing selected cell(s) even when not focused - * Fixes #1769. Supports a minimum view size for non-automatic size views. - * Exposes APIs to support upcoming Web console feature - * Fixes some scrolling performance issues - * Fixes #1763. Allowing read console inputs before idle handlers. - * TableView unicode scenario usability - * Added unicode testing code to TableEditor + See: https://github.com/gui-cs/Terminal.Gui/releases \ No newline at end of file diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs index cc299e78c..c5995f838 100644 --- a/Terminal.Gui/Views/Button.cs +++ b/Terminal.Gui/Views/Button.cs @@ -248,8 +248,7 @@ namespace Terminal.Gui { /// public override bool MouseEvent (MouseEvent me) { - if (me.Flags == MouseFlags.Button1Clicked || me.Flags == MouseFlags.Button1DoubleClicked || - me.Flags == MouseFlags.Button1TripleClicked) { + if (me.Flags == MouseFlags.Button1Clicked) { if (CanFocus && Enabled) { if (!HasFocus) { SetFocus (); diff --git a/Terminal.Gui/Views/GraphView.cs b/Terminal.Gui/Views/GraphView.cs index 80cb9702e..48c62a760 100644 --- a/Terminal.Gui/Views/GraphView.cs +++ b/Terminal.Gui/Views/GraphView.cs @@ -240,7 +240,13 @@ namespace Terminal.Gui { ); } - + /// + /// Also ensures that cursor is invisible after entering the . + public override bool OnEnter (View view) + { + Driver.SetCursorVisibility (CursorVisibility.Invisible); + return base.OnEnter (view); + } /// public override bool ProcessKey (KeyEvent keyEvent) diff --git a/Terminal.Gui/Views/ScrollBarView.cs b/Terminal.Gui/Views/ScrollBarView.cs index 80c5b6ee3..ad0f469f5 100644 --- a/Terminal.Gui/Views/ScrollBarView.cs +++ b/Terminal.Gui/Views/ScrollBarView.cs @@ -462,7 +462,7 @@ namespace Terminal.Gui { return; } - Driver.SetAttribute (GetNormalColor ()); + Driver.SetAttribute (Host.HasFocus ? ColorScheme.Focus : GetNormalColor ()); if ((vertical && Bounds.Height == 0) || (!vertical && Bounds.Width == 0)) { return; @@ -613,13 +613,13 @@ namespace Terminal.Gui { int posBarOffset; /// - public override bool MouseEvent (MouseEvent me) + public override bool MouseEvent (MouseEvent mouseEvent) { - if (me.Flags != MouseFlags.Button1Pressed && me.Flags != MouseFlags.Button1DoubleClicked && - !me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) && - me.Flags != MouseFlags.Button1Released && me.Flags != MouseFlags.WheeledDown && - me.Flags != MouseFlags.WheeledUp && me.Flags != MouseFlags.WheeledRight && - me.Flags != MouseFlags.WheeledLeft && me.Flags != MouseFlags.Button1TripleClicked) { + if (mouseEvent.Flags != MouseFlags.Button1Pressed && mouseEvent.Flags != MouseFlags.Button1DoubleClicked && + !mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) && + mouseEvent.Flags != MouseFlags.Button1Released && mouseEvent.Flags != MouseFlags.WheeledDown && + mouseEvent.Flags != MouseFlags.WheeledUp && mouseEvent.Flags != MouseFlags.WheeledRight && + mouseEvent.Flags != MouseFlags.WheeledLeft && mouseEvent.Flags != MouseFlags.Button1TripleClicked) { return false; } @@ -630,24 +630,24 @@ namespace Terminal.Gui { Host.SetFocus (); } - int location = vertical ? me.Y : me.X; + int location = vertical ? mouseEvent.Y : mouseEvent.X; int barsize = vertical ? Bounds.Height : Bounds.Width; int posTopLeftTee = vertical ? posTopTee + 1 : posLeftTee + 1; int posBottomRightTee = vertical ? posBottomTee + 1 : posRightTee + 1; barsize -= 2; var pos = Position; - if (me.Flags != MouseFlags.Button1Released + if (mouseEvent.Flags != MouseFlags.Button1Released && (Application.MouseGrabView == null || Application.MouseGrabView != this)) { Application.GrabMouse (this); - } else if (me.Flags == MouseFlags.Button1Released && Application.MouseGrabView != null && Application.MouseGrabView == this) { + } else if (mouseEvent.Flags == MouseFlags.Button1Released && Application.MouseGrabView != null && Application.MouseGrabView == this) { lastLocation = -1; Application.UngrabMouse (); return true; } - if (showScrollIndicator && (me.Flags == MouseFlags.WheeledDown || me.Flags == MouseFlags.WheeledUp || - me.Flags == MouseFlags.WheeledRight || me.Flags == MouseFlags.WheeledLeft)) { - return Host.MouseEvent (me); + if (showScrollIndicator && (mouseEvent.Flags == MouseFlags.WheeledDown || mouseEvent.Flags == MouseFlags.WheeledUp || + mouseEvent.Flags == MouseFlags.WheeledRight || mouseEvent.Flags == MouseFlags.WheeledLeft)) { + return Host.MouseEvent (mouseEvent); } if (location == 0) { @@ -668,7 +668,7 @@ namespace Terminal.Gui { //} if (lastLocation > -1 || (location >= posTopLeftTee && location <= posBottomRightTee - && me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))) { + && mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))) { if (lastLocation == -1) { lastLocation = location; posBarOffset = keepContentAlwaysInViewport ? Math.Max (location - posTopLeftTee, 1) : 0; diff --git a/Terminal.Gui/Views/ScrollView.cs b/Terminal.Gui/Views/ScrollView.cs index c42308dd0..23bfedbce 100644 --- a/Terminal.Gui/Views/ScrollView.cs +++ b/Terminal.Gui/Views/ScrollView.cs @@ -32,6 +32,7 @@ namespace Terminal.Gui { private class ContentView : View { public ContentView (Rect frame) : base (frame) { + CanFocus = true; } } @@ -325,7 +326,7 @@ namespace Terminal.Gui { { Driver.SetAttribute (GetNormalColor ()); SetViewsNeedsDisplay (); - Clear (); + //Clear (); var savedClip = ClipToBounds (); OnDrawContent (new Rect (ContentOffset, @@ -507,7 +508,7 @@ namespace Terminal.Gui { { if (me.Flags != MouseFlags.WheeledDown && me.Flags != MouseFlags.WheeledUp && me.Flags != MouseFlags.WheeledRight && me.Flags != MouseFlags.WheeledLeft && - me.Flags != MouseFlags.Button1Pressed && me.Flags != MouseFlags.Button1Clicked && +// me.Flags != MouseFlags.Button1Pressed && me.Flags != MouseFlags.Button1Clicked && !me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) { return false; } diff --git a/Terminal.Gui/Views/TabView.cs b/Terminal.Gui/Views/TabView.cs index 4905912d9..1baf85993 100644 --- a/Terminal.Gui/Views/TabView.cs +++ b/Terminal.Gui/Views/TabView.cs @@ -466,31 +466,10 @@ namespace Terminal.Gui { Width = Dim.Fill (); } - /// - /// Positions the cursor at the start of the currently selected tab - /// - public override void PositionCursor () + public override bool OnEnter (View view) { - base.PositionCursor (); - - var selected = host.CalculateViewport (Bounds).FirstOrDefault (t => Equals (host.SelectedTab, t.Tab)); - - if (selected == null) { - return; - } - - int y; - - if (host.Style.TabsOnBottom) { - y = 1; - } else { - y = host.Style.ShowTopLine ? 1 : 0; - } - - Move (selected.X, y); - - - + Driver.SetCursorVisibility (CursorVisibility.Invisible); + return base.OnEnter (view); } public override void Redraw (Rect bounds) diff --git a/Terminal.Gui/Windows/Wizard.cs b/Terminal.Gui/Windows/Wizard.cs index dd0b22413..0ffc3bd6e 100644 --- a/Terminal.Gui/Windows/Wizard.cs +++ b/Terminal.Gui/Windows/Wizard.cs @@ -159,7 +159,7 @@ namespace Terminal.Gui { public event Action TitleChanged; // The contentView works like the ContentView in FrameView. - private View contentView = new View (); + private View contentView = new View () { Data = "WizardContentView" }; /// /// Sets or gets help text for the .If is empty diff --git a/UICatalog/Properties/launchSettings.json b/UICatalog/Properties/launchSettings.json index ec419ec20..b621c2df5 100644 --- a/UICatalog/Properties/launchSettings.json +++ b/UICatalog/Properties/launchSettings.json @@ -23,14 +23,18 @@ "commandName": "Project", "commandLineArgs": "WizardAsView" }, - "Issue1719Repro": { - "commandName": "Project", - "commandLineArgs": "\"ProgressBar Styles\"" - }, "VkeyPacketSimulator": { "commandName": "Project", "commandLineArgs": "VkeyPacketSimulator" }, + "CollectionNavigatorTester": { + "commandName": "Project", + "commandLineArgs": "\"Search Collection Nav\"" + }, + "Charmap": { + "commandName": "Project", + "commandLineArgs": "\"Character Map\"" + }, "WSL2": { "commandName": "Executable", "executablePath": "wsl", @@ -45,9 +49,9 @@ "commandName": "WSL2", "distributionName": "" }, - "CollectionNavigatorTester": { + "All Views Tester": { "commandName": "Project", - "commandLineArgs": "\"Search Collection Nav\"" + "commandLineArgs": "\"All Views Tester\"" } } } \ No newline at end of file diff --git a/UICatalog/README.md b/UICatalog/README.md index 91dbada2a..0a80f9769 100644 --- a/UICatalog/README.md +++ b/UICatalog/README.md @@ -2,8 +2,9 @@ UI Catalog is a comprehensive sample library for Terminal.Gui. It attempts to satisfy the following goals: -1. Be an easy to use showcase for Terminal.Gui concepts and features. -2. Provide sample code that illustrates how to properly implement said concepts & features. +1. Be an easy-to-use showcase for Terminal.Gui concepts and features. +2. Provide sample code that illustrates how to properly implement +said concepts & features. 3. Make it easy for contributors to add additional samples in a structured way. ![screenshot](screenshot.png) @@ -51,7 +52,7 @@ To add a new **Scenario** simply: 4. Implement the `Setup` override which will be called when a user selects the scenario to run. 5. Optionally, implement the `Init` and/or `Run` overrides to provide a custom implementation. -The sample below is provided in the `Scenarios` directory as a generic sample that can be copied and re-named: +The sample below is provided in the `.\UICatalog\Scenarios` directory as a generic sample that can be copied and re-named: ```csharp using Terminal.Gui; @@ -73,59 +74,23 @@ namespace UICatalog { } ``` -`Scenario` provides a `Toplevel` and `Window` the provides a canvas for the Scenario to operate. The default `Window` shows the Scenario name and supports exiting the Scenario through the `Esc` key. +`Scenario` provides `Win`, a `Window` object that provides a canvas for the Scenario to operate. + +The default `Window` shows the Scenario name and supports exiting the Scenario through the `Esc` key. ![screenshot](generic_screenshot.png) -To build a more advanced scenario, where control of the `Toplevel` and `Window` is needed (e.g. for scenarios using `MenuBar` or `StatusBar`), simply set the `Top` and `Window` properties as appropriate, as seen in the `UnicodeInMenu` scenario: +To build a more advanced scenario, where control of the `Toplevel` and `Window` is needed (e.g. for scenarios using `MenuBar` or `StatusBar`), simply use `Application.Top` per normal Terminal.Gui programming, as seen in the `Notepad` scenario. -```csharp -using Terminal.Gui; - -namespace UICatalog { - [ScenarioMetadata (Name: "Unicode In Menu", Description: "Unicode menus per PR #204")] - [ScenarioCategory ("Text")] - [ScenarioCategory ("Controls")] - class UnicodeInMenu : Scenario { - public override void Setup () - { - Top = new Toplevel (new Rect (0, 0, Application.Driver.Cols, Application.Driver.Rows)); - var menu = new MenuBar (new MenuBarItem [] { - new MenuBarItem ("_Файл", new MenuItem [] { - new MenuItem ("_Создать", "Creates new file", null), - new MenuItem ("_Открыть", "", null), - new MenuItem ("Со_хранить", "", null), - new MenuItem ("_Выход", "", () => Application.RequestStop() ) - }), - new MenuBarItem ("_Edit", new MenuItem [] { - new MenuItem ("_Copy", "", null), - new MenuItem ("C_ut", "", null), - new MenuItem ("_Paste", "", null) - }) - }); - Top.Add (menu); - - Win = new Window ($"Scenario: {GetName ()}") { - X = 0, - Y = 1, - Width = Dim.Fill (), - Height = Dim.Fill () - }; - Top.Add (Win); - } - } -} -``` - -For complete control, the `Init` and `Run` overrides can be implemented. The `base.Init` assigns `Application.Top` to `Top` and creates `Win`. The `base.Run` simply calls `Application.Run(Top)`. +For complete control, the `Init` and `Run` overrides can be implemented. The `base.Init` creates `Win`. The `base.Run` simply calls `Application.Run(Application.Top)`. ## Contribution Guidelines -- Provide a terse, descriptive name for `Scenarios`. Keep them short; the `ListView` that displays them dynamically sizes the column width and long names will make it hard for people to use. -- Provide a clear description. +- Provide a terse, descriptive `Name` for `Scenarios`. Keep them short. +- Provide a clear `Description`. - Comment `Scenario` code to describe to others why it's a useful `Scenario`. -- Annotate `Scenarios` with `[ScenarioCategory]` attributes. Try to minimize the number of new categories created. -- Use the `Bug Rero` Category for `Scnarios` that reproduce bugs. +- Annotate `Scenarios` with `[ScenarioCategory]` attributes. Minimize the number of new categories created. +- Use the `Bug Repo` Category for `Scenarios` that reproduce bugs. - Include the Github Issue # in the Description. - - Once the bug has been fixed in `master` submit another PR to remove the `Scenario` (or modify it to provide a good regression test). -- Tag bugs or suggestions for `UI Catalog` as [`Terminal.Gui` Github Issues](https://github.com/gui-cs/Terminal.Gui/issues) with "UICatalog: ". \ No newline at end of file + - Once the bug has been fixed in `develop` submit another PR to remove the `Scenario` (or modify it to provide a good regression test/sample). +- Tag bugs or suggestions for `UI Catalog` as [`Terminal.Gui` Github Issues](https://github.com/gui-cs/Terminal.Gui/issues) with "UICatalog: ". diff --git a/UICatalog/Scenario.cs b/UICatalog/Scenario.cs index 30767e190..e26ce2911 100644 --- a/UICatalog/Scenario.cs +++ b/UICatalog/Scenario.cs @@ -48,12 +48,7 @@ namespace UICatalog { private bool _disposedValue; /// - /// The Top level for the . This should be set to in most cases. - /// - public Toplevel Top { get; set; } - - /// - /// The Window for the . This should be set within the in most cases. + /// The Window for the . This should be set to in most cases. /// public Window Win { get; set; } @@ -63,22 +58,21 @@ namespace UICatalog { /// the Scenario picker UI. /// Override to provide any behavior needed. /// - /// The Toplevel created by the UI Catalog host. /// The colorscheme to use. /// /// - /// The base implementation calls , sets to the passed in , creates a for and adds it to . + /// The base implementation calls and creates a for + /// and adds it to . /// /// - /// Overrides that do not call the base., must call before creating any views or calling other Terminal.Gui APIs. + /// Overrides that do not call the base., must call + /// before creating any views or calling other Terminal.Gui APIs. /// /// - public virtual void Init (Toplevel top, ColorScheme colorScheme) + public virtual void Init (ColorScheme colorScheme) { Application.Init (); - Top = top != null ? top : Application.Top; - Win = new Window ($"CTRL-Q to Close - Scenario: {GetName ()}") { X = 0, Y = 0, @@ -86,7 +80,7 @@ namespace UICatalog { Height = Dim.Fill (), ColorScheme = colorScheme, }; - Top.Add (Win); + Application.Top.Add (Win); } /// @@ -201,7 +195,7 @@ namespace UICatalog { public virtual void Run () { // Must explicit call Application.Shutdown method to shutdown. - Application.Run (Top); + Application.Run (Application.Top); } /// diff --git a/UICatalog/Scenarios/AllViewsTester.cs b/UICatalog/Scenarios/AllViewsTester.cs index 945b25df8..7e346f02d 100644 --- a/UICatalog/Scenarios/AllViewsTester.cs +++ b/UICatalog/Scenarios/AllViewsTester.cs @@ -14,7 +14,7 @@ namespace UICatalog.Scenarios { [ScenarioCategory ("Tests")] [ScenarioCategory ("Top Level Windows")] public class AllViewsTester : Scenario { - Window _leftPane; + FrameView _leftPane; ListView _classListView; FrameView _hostPane; @@ -40,42 +40,33 @@ namespace UICatalog.Scenarios { TextField _hText; int _hVal = 0; - public override void Init (Toplevel top, ColorScheme colorScheme) + public override void Init (ColorScheme colorScheme) { Application.Init (); - - Top = top != null ? top : Application.Top; - - //Win = new Window ($"CTRL-Q to Close - Scenario: {GetName ()}") { - // X = 0, - // Y = 0, - // Width = Dim.Fill (), - // Height = Dim.Fill () - //}; - //Top.Add (Win); + // Don't create a sub-win; just use Applicatiion.Top } - + public override void Setup () { var statusBar = new StatusBar (new StatusItem [] { new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()), new StatusItem(Key.F2, "~F2~ Toggle Frame Ruler", () => { ConsoleDriver.Diagnostics ^= ConsoleDriver.DiagnosticFlags.FrameRuler; - Top.SetNeedsDisplay (); + Application.Top.SetNeedsDisplay (); }), new StatusItem(Key.F3, "~F3~ Toggle Frame Padding", () => { ConsoleDriver.Diagnostics ^= ConsoleDriver.DiagnosticFlags.FramePadding; - Top.SetNeedsDisplay (); + Application.Top.SetNeedsDisplay (); }), }); - Top.Add (statusBar); + Application.Top.Add (statusBar); _viewClasses = GetAllViewClassesCollection () .OrderBy (t => t.Name) .Select (t => new KeyValuePair (t.Name, t)) .ToDictionary (t => t.Key, t => t.Value); - _leftPane = new Window ("Classes") { + _leftPane = new FrameView ("Classes") { X = 0, Y = 0, Width = 15, @@ -241,9 +232,9 @@ namespace UICatalog.Scenarios { ColorScheme = Colors.Dialog, }; - Top.Add (_leftPane, _settingsPane, _hostPane); + Application.Top.Add (_leftPane, _settingsPane, _hostPane); - Top.LayoutSubviews (); + Application.Top.LayoutSubviews (); _curView = CreateClass (_viewClasses.First ().Value); } @@ -438,11 +429,6 @@ namespace UICatalog.Scenarios { UpdateTitle (_curView); } - public override void Run () - { - base.Run (); - } - private void Quit () { Application.RequestStop (); diff --git a/UICatalog/Scenarios/BackgroundWorkerCollection.cs b/UICatalog/Scenarios/BackgroundWorkerCollection.cs index e7bc4a059..2e566ebe5 100644 --- a/UICatalog/Scenarios/BackgroundWorkerCollection.cs +++ b/UICatalog/Scenarios/BackgroundWorkerCollection.cs @@ -12,17 +12,10 @@ namespace UICatalog.Scenarios { [ScenarioCategory ("Dialogs")] [ScenarioCategory ("Controls")] public class BackgroundWorkerCollection : Scenario { - public override void Init (Toplevel top, ColorScheme colorScheme) - { - Application.Top.Dispose (); - - Application.Run (); - - Application.Top.Dispose (); - } public override void Run () { + Application.Run (); } class MdiMain : Toplevel { diff --git a/UICatalog/Scenarios/BordersComparisons.cs b/UICatalog/Scenarios/BordersComparisons.cs index 9ea462f52..1aac4dca5 100644 --- a/UICatalog/Scenarios/BordersComparisons.cs +++ b/UICatalog/Scenarios/BordersComparisons.cs @@ -5,12 +5,10 @@ namespace UICatalog.Scenarios { [ScenarioCategory ("Layout")] [ScenarioCategory ("Borders")] public class BordersComparisons : Scenario { - public override void Init (Toplevel top, ColorScheme colorScheme) + public override void Init (ColorScheme colorScheme) { Application.Init (); - top = Application.Top; - var borderStyle = BorderStyle.Double; var drawMarginFrame = false; var borderThickness = new Thickness (1, 2, 3, 4); @@ -53,7 +51,7 @@ namespace UICatalog.Scenarios { Width = 10 }; win.Add (tf1, button, label, tv, tf2); - top.Add (win); + Application.Top.Add (win); var top2 = new Border.ToplevelContainer (new Rect (50, 5, 40, 20), new Border () { @@ -92,7 +90,7 @@ namespace UICatalog.Scenarios { Width = 10 }; top2.Add (tf3, button2, label2, tv2, tf4); - top.Add (top2); + Application.Top.Add (top2); var frm = new FrameView (new Rect (95, 5, 40, 20), "Test3", null, new Border () { @@ -128,7 +126,7 @@ namespace UICatalog.Scenarios { Width = 10 }; frm.Add (tf5, button3, label3, tv3, tf6); - top.Add (frm); + Application.Top.Add (frm); Application.Run (); } diff --git a/UICatalog/Scenarios/Buttons.cs b/UICatalog/Scenarios/Buttons.cs index f66849d9e..7269d53d3 100644 --- a/UICatalog/Scenarios/Buttons.cs +++ b/UICatalog/Scenarios/Buttons.cs @@ -56,7 +56,7 @@ namespace UICatalog.Scenarios { //View prev = colorButtonsLabel; - //With this method there is no need to call Top.Ready += () => Top.Redraw (Top.Bounds); + //With this method there is no need to call Application.TopReady += () => Application.TopRedraw (Top.Bounds); var x = Pos.Right (colorButtonsLabel) + 2; foreach (var colorScheme in Colors.ColorSchemes) { var colorButton = new Button ($"{colorScheme.Key}") { @@ -272,7 +272,7 @@ namespace UICatalog.Scenarios { } }; - Top.Ready += () => radioGroup.Refresh (); + Application.Top.Ready += () => radioGroup.Refresh (); } } } \ No newline at end of file diff --git a/UICatalog/Scenarios/CharacterMap.cs b/UICatalog/Scenarios/CharacterMap.cs index 2036e566d..667e0aafa 100644 --- a/UICatalog/Scenarios/CharacterMap.cs +++ b/UICatalog/Scenarios/CharacterMap.cs @@ -2,6 +2,7 @@ //#define BASE_DRAW_CONTENT using NStack; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -11,11 +12,12 @@ using Rune = System.Rune; namespace UICatalog.Scenarios { /// /// This Scenario demonstrates building a custom control (a class deriving from View) that: - /// - Provides a simple "Character Map" application (like Windows' charmap.exe). + /// - Provides a "Character Map" application (like Windows' charmap.exe). /// - Helps test unicode character rendering in Terminal.Gui /// - Illustrates how to use ScrollView to do infinite scrolling /// - [ScenarioMetadata (Name: "Character Map", Description: "A Unicode character set viewier built as a custom control using the ScrollView control.")] + [ScenarioMetadata (Name: "Character Map", + Description: "A Unicode character set viewier built as a custom control using the ScrollView control.")] [ScenarioCategory ("Text and Formatting")] [ScenarioCategory ("Controls")] [ScenarioCategory ("ScrollView")] @@ -26,28 +28,15 @@ namespace UICatalog.Scenarios { _charMap = new CharMap () { X = 0, Y = 0, - Width = CharMap.RowWidth + 2, Height = Dim.Fill (), - Start = 0x2500, - ColorScheme = Colors.Dialog, - CanFocus = true, }; - Win.Add (_charMap); - var label = new Label ("Jump To Unicode Block:") { X = Pos.Right (_charMap) + 1, Y = Pos.Y (_charMap) }; - Win.Add (label); - - (ustring radioLabel, int start, int end) CreateRadio (ustring title, int start, int end) - { - return ($"{title} (U+{start:x5}-{end:x5})", start, end); - } - var radioItems = new (ustring radioLabel, int start, int end) [] { - CreateRadio("ASCII Control Characterss", 0x00, 0x1F), + CreateRadio("ASCII Control Characters", 0x00, 0x1F), CreateRadio("C0 Control Characters", 0x80, 0x9f), CreateRadio("Hangul Jamo", 0x1100, 0x11ff), // This is where wide chars tend to start CreateRadio("Currency Symbols", 0x20A0, 0x20CF), - CreateRadio("Letterlike Symbols", 0x2100, 0x214F), + CreateRadio("Letter-like Symbols", 0x2100, 0x214F), CreateRadio("Arrows", 0x2190, 0x21ff), CreateRadio("Mathematical symbols", 0x2200, 0x22ff), CreateRadio("Miscellaneous Technical", 0x2300, 0x23ff), @@ -55,17 +44,25 @@ namespace UICatalog.Scenarios { CreateRadio("Miscellaneous Symbols", 0x2600, 0x26ff), CreateRadio("Dingbats", 0x2700, 0x27ff), CreateRadio("Braille", 0x2800, 0x28ff), - CreateRadio("Miscellaneous Symbols and Arrows", 0x2b00, 0x2bff), - CreateRadio("Alphabetic Presentation Forms", 0xFB00, 0xFb4f), - CreateRadio("Cuneiform Numbers and Punctuation", 0x12400, 0x1240f), + CreateRadio("Miscellaneous Symbols & Arrows", 0x2b00, 0x2bff), + CreateRadio("Alphabetic Pres. Forms", 0xFB00, 0xFb4f), + CreateRadio("Cuneiform Num. and Punct.", 0x12400, 0x1240f), CreateRadio("Chess Symbols", 0x1FA00, 0x1FA0f), CreateRadio("End", CharMap.MaxCodePointVal - 16, CharMap.MaxCodePointVal), }; + (ustring radioLabel, int start, int end) CreateRadio (ustring title, int start, int end) + { + return ($"{title} (U+{start:x5}-{end:x5})", start, end); + } + + Win.Add (_charMap); + var label = new Label ("Jump To Unicode Block:") { X = Pos.Right (_charMap) + 1, Y = Pos.Y (_charMap) }; + Win.Add (label); var jumpList = new RadioGroup (radioItems.Select (t => t.radioLabel).ToArray ()) { X = Pos.X (label), Y = Pos.Bottom (label), - Width = Dim.Fill (), + Width = radioItems.Max (r => r.radioLabel.Length) + 3, SelectedItem = 8 }; jumpList.SelectedItemChanged += (args) => { @@ -76,11 +73,9 @@ namespace UICatalog.Scenarios { jumpList.Refresh (); jumpList.SetFocus (); - } - public override void Run () - { - base.Run (); + _charMap.Width = Dim.Fill () - jumpList.Width; + } } @@ -98,97 +93,90 @@ namespace UICatalog.Scenarios { SetNeedsDisplay (); } } + int _start = 0x2500; - public const int H_SPACE = 2; - public const int V_SPACE = 2; + public const int COLUMN_WIDTH = 3; + public const int ROW_HEIGHT = 1; public static int MaxCodePointVal => 0x10FFFF; - // Row Header + space + (space + char + space) - public static int RowHeaderWidth => $"U+{MaxCodePointVal:x5}".Length; - public static int RowWidth => RowHeaderWidth + (H_SPACE * 16); + public static int RowLabelWidth => $"U+{MaxCodePointVal:x5}".Length; + public static int RowWidth => RowLabelWidth + (COLUMN_WIDTH * 16); public CharMap () { + ColorScheme = Colors.Dialog; + CanFocus = true; + ContentSize = new Size (CharMap.RowWidth, MaxCodePointVal / 16); ShowVerticalScrollIndicator = true; ShowHorizontalScrollIndicator = false; LayoutComplete += (args) => { - if (Bounds.Width <= RowWidth) { + if (Bounds.Width < RowWidth) { ShowHorizontalScrollIndicator = true; } else { ShowHorizontalScrollIndicator = false; + // Snap 1st column into view if it's been scrolled horizontally + ContentOffset = new Point (0, ContentOffset.Y); + SetNeedsDisplay (); } }; -#if DRAW_CONTENT - DrawContent += CharMap_DrawContent; -#endif + + AddCommand (Command.ScrollUp, () => { ScrollUp (1); return true; }); + AddCommand (Command.ScrollDown, () => { ScrollDown (1); return true; }); + AddCommand (Command.ScrollLeft, () => { ScrollLeft (1); return true; }); + AddCommand (Command.ScrollRight, () => { ScrollRight (1); return true; }); } private void CharMap_DrawContent (Rect viewport) { - //Rune ReplaceNonPrintables (Rune c) - //{ - // if (c < 0x20) { - // return new Rune (c + 0x2400); // U+25A1 □ WHITE SQUARE - // } else { - // return c; - // } - //} - - ContentSize = new Size (CharMap.RowWidth, MaxCodePointVal / 16 + Frame.Height - 1); - - for (int header = 0; header < 16; header++) { - Move (viewport.X + RowHeaderWidth + (header * H_SPACE), 0); - Driver.AddStr ($" {header:x} "); + var oldClip = Driver.Clip; + Driver.Clip = Frame; + // Redraw doesn't know about the scroll indicators, so if off, add one to height + if (!ShowHorizontalScrollIndicator) { + Driver.Clip = new Rect (Frame.X, Frame.Y, Frame.Width, Frame.Height + 1); } - for (int row = 0, y = 0; row < viewport.Height / 2 - 1; row++, y += V_SPACE) { - int val = (-viewport.Y + row) * 16; + Driver.SetAttribute (HasFocus ? ColorScheme.HotFocus : ColorScheme.Focus); + Move (0, 0); + Driver.AddStr (new string (' ', RowLabelWidth + 1)); + for (int hexDigit = 0; hexDigit < 16; hexDigit++) { + var x = ContentOffset.X + RowLabelWidth + (hexDigit * COLUMN_WIDTH); + if (x > RowLabelWidth - 2) { + Move (x, 0); + Driver.AddStr ($" {hexDigit:x} "); + } + } + //Move (RowWidth, 0); + //Driver.AddRune (' '); + + var firstColumnX = viewport.X + RowLabelWidth; + for (int row = -ContentOffset.Y, y = 0; row <= (-ContentOffset.Y) + (Bounds.Height / ROW_HEIGHT); row++, y += ROW_HEIGHT) { + int val = (row) * 16; + Driver.SetAttribute (GetNormalColor ()); + Move (firstColumnX, y + 1); + Driver.AddStr (new string (' ', 16 * COLUMN_WIDTH)); if (val < MaxCodePointVal) { - var rowLabel = $"U+{val / 16:x4}x"; - Move (0, y + 1); - Driver.AddStr (rowLabel); - var prevColWasWide = false; + Driver.SetAttribute (GetNormalColor ()); for (int col = 0; col < 16; col++) { - var rune = new Rune ((uint)((uint)(-viewport.Y + row) * 16 + col)); - Move (viewport.X + RowHeaderWidth + (col * H_SPACE) + (prevColWasWide ? 0 : 1), y + 1); - if (rune >= 0x00D800 && rune <= 0x00DFFF) { - if (col == 0) { - Driver.AddStr ("Reserved to surrogate pairs."); - } - continue; - } + var rune = new Rune ((uint)((uint)val + col)); + //if (rune >= 0x00D800 && rune <= 0x00DFFF) { + // if (col == 0) { + // Driver.AddStr ("Reserved for surrogate pairs."); + // } + // continue; + //} + Move (firstColumnX + (col * COLUMN_WIDTH) + 1, y + 1); Driver.AddRune (rune); - //prevColWasWide = Rune.ColumnWidth (rune) > 1; } + Move (0, y + 1); + Driver.SetAttribute (HasFocus ? ColorScheme.HotFocus : ColorScheme.Focus); + var rowLabel = $"U+{val / 16:x4}x "; + Driver.AddStr (rowLabel); } } - } -#if BASE_DRAW_CONTENT - public override void OnDrawContent (Rect viewport) - { - CharMap_DrawContent (viewport); - base.OnDrawContent (viewport); - } -#endif - - public override bool ProcessKey (KeyEvent kb) - { - if (kb.Key == Key.PageDown) { - ContentOffset = new Point (0, ContentOffset.Y - Bounds.Height / 2 + 1); - return true; - } - if (kb.Key == Key.PageUp) { - if (ContentOffset.Y + Bounds.Height / 2 - 1 < 0) { - ContentOffset = new Point (0, ContentOffset.Y + Bounds.Height / 2 - 1); - } else { - ContentOffset = Point.Empty; - } - return true; - } - return base.ProcessKey (kb); + Driver.Clip = oldClip; } protected override void Dispose (bool disposing) diff --git a/UICatalog/Scenarios/ClassExplorer.cs b/UICatalog/Scenarios/ClassExplorer.cs index adddae538..650bd099f 100644 --- a/UICatalog/Scenarios/ClassExplorer.cs +++ b/UICatalog/Scenarios/ClassExplorer.cs @@ -60,7 +60,7 @@ namespace UICatalog.Scenarios { Win.Title = this.GetName (); Win.Y = 1; // menu Win.Height = Dim.Fill (1); // status bar - Top.LayoutSubviews (); + Application.Top.LayoutSubviews (); var menu = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_File", new MenuItem [] { @@ -80,7 +80,7 @@ namespace UICatalog.Scenarios { }, }) }); - Top.Add (menu); + Application.Top.Add (menu); treeView = new TreeView () { X = 0, diff --git a/UICatalog/Scenarios/Clipping.cs b/UICatalog/Scenarios/Clipping.cs index 9c137f03e..0d68229a9 100644 --- a/UICatalog/Scenarios/Clipping.cs +++ b/UICatalog/Scenarios/Clipping.cs @@ -7,13 +7,10 @@ namespace UICatalog.Scenarios { public class Clipping : Scenario { - public override void Init (Toplevel top, ColorScheme colorScheme) + public override void Init (ColorScheme colorScheme) { Application.Init (); - - Top = top != null ? top : Application.Top; - - Top.ColorScheme = Colors.Base; + Application.Top.ColorScheme = Colors.Base; } public override void Setup () @@ -26,7 +23,7 @@ namespace UICatalog.Scenarios { X = 0, Y = 0, //ColorScheme = Colors.Dialog }; - Top.Add (label); + Application.Top.Add (label); var scrollView = new ScrollView (new Rect (3, 3, 50, 20)); scrollView.ColorScheme = Colors.Menu; @@ -69,7 +66,7 @@ namespace UICatalog.Scenarios { scrollView.Add (embedded1); - Top.Add (scrollView); + Application.Top.Add (scrollView); } } } \ No newline at end of file diff --git a/UICatalog/Scenarios/CollectionNavigatorTester.cs b/UICatalog/Scenarios/CollectionNavigatorTester.cs index 83f99664e..ba921dafa 100644 --- a/UICatalog/Scenarios/CollectionNavigatorTester.cs +++ b/UICatalog/Scenarios/CollectionNavigatorTester.cs @@ -15,11 +15,10 @@ namespace UICatalog.Scenarios { public class CollectionNavigatorTester : Scenario { // Don't create a Window, just return the top-level view - public override void Init (Toplevel top, ColorScheme colorScheme) + public override void Init (ColorScheme colorScheme) { Application.Init (); - Top = top != null ? top : Application.Top; - Top.ColorScheme = Colors.Base; + Application.Top.ColorScheme = Colors.Base; } System.Collections.Generic.List _items = new string [] { @@ -103,7 +102,7 @@ namespace UICatalog.Scenarios { new MenuBarItem("_Quit", "CTRL-Q", () => Quit()), }); - Top.Add (menu); + Application.Top.Add (menu); _items.Sort (StringComparer.OrdinalIgnoreCase); @@ -113,7 +112,7 @@ namespace UICatalog.Scenarios { Y = 1, Height = Dim.Fill () }; - Top.Add (vsep); + Application.Top.Add (vsep); CreateTreeView (); } @@ -129,7 +128,7 @@ namespace UICatalog.Scenarios { Width = Dim.Percent (50), Height = 1, }; - Top.Add (label); + Application.Top.Add (label); _listView = new ListView () { X = 0, @@ -139,7 +138,7 @@ namespace UICatalog.Scenarios { AllowsMarking = false, AllowsMultipleSelection = false, }; - Top.Add (_listView); + Application.Top.Add (_listView); _listView.SetSource (_items); @@ -160,7 +159,7 @@ namespace UICatalog.Scenarios { Width = Dim.Percent (50), Height = 1, }; - Top.Add (label); + Application.Top.Add (label); _treeView = new TreeView () { X = Pos.Right (_listView) + 1, @@ -169,7 +168,7 @@ namespace UICatalog.Scenarios { Height = Dim.Fill () }; _treeView.Style.HighlightModelTextOnly = true; - Top.Add (_treeView); + Application.Top.Add (_treeView); var root = new TreeNode ("IsLetterOrDigit examples"); root.Children = _items.Where (i => char.IsLetterOrDigit (i [0])).Select (i => new TreeNode (i)).Cast ().ToList (); diff --git a/UICatalog/Scenarios/ComputedLayout.cs b/UICatalog/Scenarios/ComputedLayout.cs index 38300482f..af7d3ba28 100644 --- a/UICatalog/Scenarios/ComputedLayout.cs +++ b/UICatalog/Scenarios/ComputedLayout.cs @@ -25,12 +25,12 @@ namespace UICatalog.Scenarios { new MenuItem ("_Quit", "", () => Quit()), }), }); - Top.Add (menu); + Application.Top.Add (menu); var statusBar = new StatusBar (new StatusItem [] { new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()), }); - Top.Add (statusBar); + Application.Top.Add (statusBar); //Top.LayoutStyle = LayoutStyle.Computed; // Demonstrate using Dim to create a horizontal ruler that always measures the parent window's width diff --git a/UICatalog/Scenarios/ContextMenus.cs b/UICatalog/Scenarios/ContextMenus.cs index a9e9223d3..897f75acc 100644 --- a/UICatalog/Scenarios/ContextMenus.cs +++ b/UICatalog/Scenarios/ContextMenus.cs @@ -81,7 +81,7 @@ namespace UICatalog.Scenarios { Win.WantMousePositionReports = true; - Top.Closed += (_) => { + Application.Top.Closed += (_) => { Thread.CurrentThread.CurrentUICulture = new CultureInfo ("en-US"); Application.RootMouseEvent -= Application_RootMouseEvent; }; diff --git a/UICatalog/Scenarios/CsvEditor.cs b/UICatalog/Scenarios/CsvEditor.cs index ab83b487d..f57d13e37 100644 --- a/UICatalog/Scenarios/CsvEditor.cs +++ b/UICatalog/Scenarios/CsvEditor.cs @@ -34,7 +34,7 @@ namespace UICatalog.Scenarios { Win.Title = this.GetName(); Win.Y = 1; // menu Win.Height = Dim.Fill (1); // status bar - Top.LayoutSubviews (); + Application.Top.LayoutSubviews (); this.tableView = new TableView () { X = 0, @@ -70,14 +70,14 @@ namespace UICatalog.Scenarios { miCentered = new MenuItem ("_Set Format Pattern", "", () => SetFormat()), }) }); - Top.Add (menu); + Application.Top.Add (menu); var statusBar = new StatusBar (new StatusItem [] { new StatusItem(Key.CtrlMask | Key.O, "~^O~ Open", () => Open()), new StatusItem(Key.CtrlMask | Key.S, "~^S~ Save", () => Save()), new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()), }); - Top.Add (statusBar); + Application.Top.Add (statusBar); Win.Add (tableView); diff --git a/UICatalog/Scenarios/Dialogs.cs b/UICatalog/Scenarios/Dialogs.cs index c3aac85a4..f8a680046 100644 --- a/UICatalog/Scenarios/Dialogs.cs +++ b/UICatalog/Scenarios/Dialogs.cs @@ -116,9 +116,9 @@ namespace UICatalog.Scenarios { { frame.Height = Dim.Height (widthEdit) + Dim.Height (heightEdit) + Dim.Height (titleEdit) + Dim.Height (numButtonsEdit) + Dim.Height (styleRadioGroup) + Dim.Height(glyphsNotWords) + 2; - Top.Loaded -= Top_Loaded; + Application.Top.Loaded -= Top_Loaded; } - Top.Loaded += Top_Loaded; + Application.Top.Loaded += Top_Loaded; label = new Label ("Button Pressed:") { X = Pos.Center (), diff --git a/UICatalog/Scenarios/DynamicMenuBar.cs b/UICatalog/Scenarios/DynamicMenuBar.cs index 82309b965..2ba8c3a07 100644 --- a/UICatalog/Scenarios/DynamicMenuBar.cs +++ b/UICatalog/Scenarios/DynamicMenuBar.cs @@ -13,11 +13,10 @@ namespace UICatalog.Scenarios { [ScenarioCategory ("Top Level Windows")] [ScenarioCategory ("Menus")] public class DynamicMenuBar : Scenario { - public override void Init (Toplevel top, ColorScheme colorScheme) + public override void Init (ColorScheme colorScheme) { Application.Init (); - Top = Application.Top; - Top.Add (new DynamicMenuBarSample ($"CTRL-Q to Close - Scenario: {GetName ()}")); + Application.Top.Add (new DynamicMenuBarSample ($"CTRL-Q to Close - Scenario: {GetName ()}")); } public class DynamicMenuItemList { diff --git a/UICatalog/Scenarios/DynamicStatusBar.cs b/UICatalog/Scenarios/DynamicStatusBar.cs index 5583afbcb..a61f366c0 100644 --- a/UICatalog/Scenarios/DynamicStatusBar.cs +++ b/UICatalog/Scenarios/DynamicStatusBar.cs @@ -12,11 +12,10 @@ namespace UICatalog.Scenarios { [ScenarioMetadata (Name: "Dynamic StatusBar", Description: "Demonstrates how to add and remove a StatusBar and change items dynamically.")] [ScenarioCategory ("Top Level Windows")] public class DynamicStatusBar : Scenario { - public override void Init (Toplevel top, ColorScheme colorScheme) + public override void Init (ColorScheme colorScheme) { Application.Init (); - Top = Application.Top; - Top.Add (new DynamicStatusBarSample ($"CTRL-Q to Close - Scenario: {GetName ()}")); + Application.Top.Add (new DynamicStatusBarSample ($"CTRL-Q to Close - Scenario: {GetName ()}")); } public class DynamicStatusItemList { diff --git a/UICatalog/Scenarios/Editor.cs b/UICatalog/Scenarios/Editor.cs index 38d62ee4c..8bc930389 100644 --- a/UICatalog/Scenarios/Editor.cs +++ b/UICatalog/Scenarios/Editor.cs @@ -30,12 +30,12 @@ namespace UICatalog.Scenarios { private TabView _tabView; private MenuItem _miForceMinimumPosToZero; private bool _forceMinimumPosToZero = true; - private readonly List _cultureInfos = Application.SupportedCultures; + private List _cultureInfos; - public override void Init (Toplevel top, ColorScheme colorScheme) + public override void Init (ColorScheme colorScheme) { Application.Init (); - Top = top != null ? top : Application.Top; + _cultureInfos = Application.SupportedCultures; Win = new Window (_fileName ?? "Untitled") { X = 0, @@ -44,7 +44,7 @@ namespace UICatalog.Scenarios { Height = Dim.Fill (), ColorScheme = colorScheme, }; - Top.Add (Win); + Application.Top.Add (Win); _textView = new TextView () { X = 0, @@ -114,7 +114,7 @@ namespace UICatalog.Scenarios { }) }); - Top.Add (menu); + Application.Top.Add (menu); var statusBar = new StatusBar (new StatusItem [] { siCursorPosition, @@ -124,7 +124,7 @@ namespace UICatalog.Scenarios { new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()), new StatusItem(Key.Null, $"OS Clipboard IsSupported : {Clipboard.IsSupported}", null) }); - Top.Add (statusBar); + Application.Top.Add (statusBar); _scrollBar = new ScrollBarView (_textView, true); @@ -196,7 +196,7 @@ namespace UICatalog.Scenarios { } }; - Top.Closed += (_) => Thread.CurrentThread.CurrentUICulture = new CultureInfo ("en-US"); + Application.Top.Closed += (_) => Thread.CurrentThread.CurrentUICulture = new CultureInfo ("en-US"); } private void DisposeWinDialog () diff --git a/UICatalog/Scenarios/Generic - Copy.cs b/UICatalog/Scenarios/Generic - Copy.cs new file mode 100644 index 000000000..98d421d7e --- /dev/null +++ b/UICatalog/Scenarios/Generic - Copy.cs @@ -0,0 +1,76 @@ +using Terminal.Gui; + +namespace UICatalog.Scenarios { + [ScenarioMetadata (Name: "Run Example", Description: "Illustrates using Application.Run to run a custom class")] + [ScenarioCategory ("Top Level Windows")] + public class RunTExample : Scenario { + public override void Setup () + { + // No need to call Init if Application.Run is used + } + + public override void Run () + { + Application.Run (); + } + + public class ExampleWindow : Window { + public TextField usernameText; + + public ExampleWindow () + { + Title = "Example App (Ctrl+Q to quit)"; + + // Create input components and labels + var usernameLabel = new Label () { + Text = "Username:" + }; + + usernameText = new TextField ("") { + // Position text field adjacent to the label + X = Pos.Right (usernameLabel) + 1, + + // Fill remaining horizontal space + Width = Dim.Fill (), + }; + + var passwordLabel = new Label () { + Text = "Password:", + X = Pos.Left (usernameLabel), + Y = Pos.Bottom (usernameLabel) + 1 + }; + + var passwordText = new TextField ("") { + Secret = true, + // align with the text box above + X = Pos.Left (usernameText), + Y = Pos.Top (passwordLabel), + Width = Dim.Fill (), + }; + + // Create login button + var btnLogin = new Button () { + Text = "Login", + Y = Pos.Bottom (passwordLabel) + 1, + // center the login button horizontally + X = Pos.Center (), + IsDefault = true, + }; + + // When login button is clicked display a message popup + btnLogin.Clicked += () => { + if (usernameText.Text == "admin" && passwordText.Text == "password") { + MessageBox.Query ("Login Successful", $"Username: {usernameText.Text}", "Ok"); + Application.RequestStop (); + } else { + MessageBox.ErrorQuery ("Error Logging In", "Incorrect username or password (hint: admin/password)", "Ok"); + } + }; + + // Add the views to the Window + Add (usernameLabel, usernameText, passwordLabel, passwordText, btnLogin); + } + } + + } +} \ No newline at end of file diff --git a/UICatalog/Scenarios/GraphViewExample.cs b/UICatalog/Scenarios/GraphViewExample.cs index 8404f044b..e8f806046 100644 --- a/UICatalog/Scenarios/GraphViewExample.cs +++ b/UICatalog/Scenarios/GraphViewExample.cs @@ -23,7 +23,7 @@ namespace UICatalog.Scenarios { Win.Title = this.GetName (); Win.Y = 1; // menu Win.Height = Dim.Fill (1); // status bar - Top.LayoutSubviews (); + Application.Top.LayoutSubviews (); graphs = new Action [] { ()=>SetupPeriodicTableScatterPlot(), //0 @@ -59,7 +59,7 @@ namespace UICatalog.Scenarios { }), }); - Top.Add (menu); + Application.Top.Add (menu); graphView = new GraphView () { X = 1, @@ -92,7 +92,7 @@ namespace UICatalog.Scenarios { new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()), new StatusItem(Key.CtrlMask | Key.G, "~^G~ Next", ()=>graphs[currentGraph++%graphs.Length]()), }); - Top.Add (statusBar); + Application.Top.Add (statusBar); } private void MultiBarGraph () diff --git a/UICatalog/Scenarios/HexEditor.cs b/UICatalog/Scenarios/HexEditor.cs index 52d02b2ef..c83408ada 100644 --- a/UICatalog/Scenarios/HexEditor.cs +++ b/UICatalog/Scenarios/HexEditor.cs @@ -52,7 +52,7 @@ namespace UICatalog.Scenarios { miAllowEdits = new MenuItem ("_AllowEdits", "", () => ToggleAllowEdits ()){Checked = _hexView.AllowEdits, CheckType = MenuItemCheckStyle.Checked} }) }); - Top.Add (menu); + Application.Top.Add (menu); statusBar = new StatusBar (new StatusItem [] { new StatusItem(Key.F2, "~F2~ Open", () => Open()), @@ -61,7 +61,7 @@ namespace UICatalog.Scenarios { siPositionChanged = new StatusItem(Key.Null, $"Position: {_hexView.Position} Line: {_hexView.CursorPosition.Y} Col: {_hexView.CursorPosition.X} Line length: {_hexView.BytesPerLine}", () => {}) }); - Top.Add (statusBar); + Application.Top.Add (statusBar); } private void _hexView_PositionChanged (HexView.HexViewEventArgs obj) diff --git a/UICatalog/Scenarios/InteractiveTree.cs b/UICatalog/Scenarios/InteractiveTree.cs index b3d63578d..f51f10238 100644 --- a/UICatalog/Scenarios/InteractiveTree.cs +++ b/UICatalog/Scenarios/InteractiveTree.cs @@ -20,14 +20,14 @@ namespace UICatalog.Scenarios { Win.Title = this.GetName (); Win.Y = 1; // menu Win.Height = Dim.Fill (1); // status bar - Top.LayoutSubviews (); + Application.Top.LayoutSubviews (); var menu = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_File", new MenuItem [] { new MenuItem ("_Quit", "", () => Quit()), }) }); - Top.Add (menu); + Application.Top.Add (menu); treeView = new TreeView () { X = 0, @@ -45,7 +45,7 @@ namespace UICatalog.Scenarios { new StatusItem(Key.CtrlMask | Key.T, "~^T~ Add Root", () => AddRootNode()), new StatusItem(Key.CtrlMask | Key.R, "~^R~ Rename Node", () => RenameNode()), }); - Top.Add (statusBar); + Application.Top.Add (statusBar); } diff --git a/UICatalog/Scenarios/Keys.cs b/UICatalog/Scenarios/Keys.cs index 35cafbc21..21b567f6d 100644 --- a/UICatalog/Scenarios/Keys.cs +++ b/UICatalog/Scenarios/Keys.cs @@ -48,10 +48,9 @@ namespace UICatalog.Scenarios { } } - public override void Init (Toplevel top, ColorScheme colorScheme) + public override void Init (ColorScheme colorScheme) { Application.Init (); - Top = top != null ? top : Application.Top; Win = new TestWindow ($"CTRL-Q to Close - Scenario: {GetName ()}") { X = 0, @@ -60,7 +59,7 @@ namespace UICatalog.Scenarios { Height = Dim.Fill (), ColorScheme = colorScheme, }; - Top.Add (Win); + Application.Top.Add (Win); } public override void Setup () @@ -107,7 +106,7 @@ namespace UICatalog.Scenarios { Shift = true }); var maxLogEntry = $"Key{"",-5}: {fakeKeyPress}".Length; - var yOffset = (Top == Application.Top ? 1 : 6); + var yOffset = (Application.Top == Application.Top ? 1 : 6); var keyStrokelist = new List (); var keyStrokeListView = new ListView (keyStrokelist) { X = 0, @@ -126,7 +125,7 @@ namespace UICatalog.Scenarios { Win.Add (processKeyLogLabel); maxLogEntry = $"{fakeKeyPress}".Length; - yOffset = (Top == Application.Top ? 1 : 6); + yOffset = (Application.Top == Application.Top ? 1 : 6); var processKeyListView = new ListView (((TestWindow)Win)._processKeyList) { X = Pos.Left (processKeyLogLabel), Y = Pos.Top (processKeyLogLabel) + yOffset, @@ -144,7 +143,7 @@ namespace UICatalog.Scenarios { }; Win.Add (processHotKeyLogLabel); - yOffset = (Top == Application.Top ? 1 : 6); + yOffset = (Application.Top == Application.Top ? 1 : 6); var processHotKeyListView = new ListView (((TestWindow)Win)._processHotKeyList) { X = Pos.Left (processHotKeyLogLabel), Y = Pos.Top (processHotKeyLogLabel) + yOffset, @@ -162,7 +161,7 @@ namespace UICatalog.Scenarios { }; Win.Add (processColdKeyLogLabel); - yOffset = (Top == Application.Top ? 1 : 6); + yOffset = (Application.Top == Application.Top ? 1 : 6); var processColdKeyListView = new ListView (((TestWindow)Win)._processColdKeyList) { X = Pos.Left (processColdKeyLogLabel), Y = Pos.Top (processColdKeyLogLabel) + yOffset, diff --git a/UICatalog/Scenarios/LabelsAsButtons.cs b/UICatalog/Scenarios/LabelsAsButtons.cs index 395a042ec..29c2b98af 100644 --- a/UICatalog/Scenarios/LabelsAsButtons.cs +++ b/UICatalog/Scenarios/LabelsAsButtons.cs @@ -59,7 +59,7 @@ namespace UICatalog.Scenarios { }; Win.Add (colorLabelsLabel); - //With this method there is no need to call Top.Ready += () => Top.Redraw (Top.Bounds); + //With this method there is no need to call Application.TopReady += () => Application.TopRedraw (Top.Bounds); var x = Pos.Right (colorLabelsLabel) + 2; foreach (var colorScheme in Colors.ColorSchemes) { var colorLabel = new Label ($"{colorScheme.Key}") { @@ -73,7 +73,7 @@ namespace UICatalog.Scenarios { Win.Add (colorLabel); x += colorLabel.Text.Length + 2; } - Top.Ready += () => Top.Redraw (Top.Bounds); + Application.Top.Ready += () => Application.Top.Redraw (Application.Top.Bounds); Label Label; Win.Add (Label = new Label ("A super long _Label that will probably expose a bug in clipping or wrapping of text. Will it?") { @@ -306,7 +306,7 @@ namespace UICatalog.Scenarios { } }; - Top.Ready += () => radioGroup.Refresh (); + Application.Top.Ready += () => radioGroup.Refresh (); } } } \ No newline at end of file diff --git a/UICatalog/Scenarios/LineViewExample.cs b/UICatalog/Scenarios/LineViewExample.cs index 4a8c4002a..cf537d952 100644 --- a/UICatalog/Scenarios/LineViewExample.cs +++ b/UICatalog/Scenarios/LineViewExample.cs @@ -17,14 +17,14 @@ namespace UICatalog.Scenarios { Win.Title = this.GetName (); Win.Y = 1; // menu Win.Height = Dim.Fill (1); // status bar - Top.LayoutSubviews (); + Application.Top.LayoutSubviews (); var menu = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_File", new MenuItem [] { new MenuItem ("_Quit", "", () => Quit()), }) }); - Top.Add (menu); + Application.Top.Add (menu); Win.Add (new Label ("Regular Line") { Y = 0 }); @@ -94,7 +94,7 @@ namespace UICatalog.Scenarios { var statusBar = new StatusBar (new StatusItem [] { new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()) }); - Top.Add (statusBar); + Application.Top.Add (statusBar); } diff --git a/UICatalog/Scenarios/MessageBoxes.cs b/UICatalog/Scenarios/MessageBoxes.cs index 0b9aaa702..b0fe23109 100644 --- a/UICatalog/Scenarios/MessageBoxes.cs +++ b/UICatalog/Scenarios/MessageBoxes.cs @@ -156,9 +156,9 @@ namespace UICatalog.Scenarios { { frame.Height = Dim.Height (widthEdit) + Dim.Height (heightEdit) + Dim.Height (titleEdit) + Dim.Height (messageEdit) + Dim.Height (numButtonsEdit) + Dim.Height (defaultButtonEdit) + Dim.Height (styleRadioGroup) + 2 + Dim.Height (ckbEffect3D); - Top.Loaded -= Top_Loaded; + Application.Top.Loaded -= Top_Loaded; } - Top.Loaded += Top_Loaded; + Application.Top.Loaded += Top_Loaded; label = new Label ("Button Pressed:") { X = Pos.Center (), diff --git a/UICatalog/Scenarios/MultiColouredTable.cs b/UICatalog/Scenarios/MultiColouredTable.cs index da772b0ec..d1d9b4059 100644 --- a/UICatalog/Scenarios/MultiColouredTable.cs +++ b/UICatalog/Scenarios/MultiColouredTable.cs @@ -16,7 +16,7 @@ namespace UICatalog.Scenarios { Win.Title = this.GetName (); Win.Y = 1; // menu Win.Height = Dim.Fill (1); // status bar - Top.LayoutSubviews (); + Application.Top.LayoutSubviews (); this.tableView = new TableViewColors () { X = 0, @@ -30,12 +30,12 @@ namespace UICatalog.Scenarios { new MenuItem ("_Quit", "", () => Quit()), }), }); - Top.Add (menu); + Application.Top.Add (menu); var statusBar = new StatusBar (new StatusItem [] { new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()), }); - Top.Add (statusBar); + Application.Top.Add (statusBar); Win.Add (tableView); diff --git a/UICatalog/Scenarios/Notepad.cs b/UICatalog/Scenarios/Notepad.cs index d9d760175..a4fe1992e 100644 --- a/UICatalog/Scenarios/Notepad.cs +++ b/UICatalog/Scenarios/Notepad.cs @@ -11,11 +11,10 @@ namespace UICatalog.Scenarios { private int numbeOfNewTabs = 1; // Don't create a Window, just return the top-level view - public override void Init (Toplevel top, ColorScheme colorScheme) + public override void Init (ColorScheme colorScheme) { Application.Init (); - Top = top != null ? top : Application.Top; - Top.ColorScheme = Colors.Base; + Application.Top.ColorScheme = Colors.Base; } public override void Setup () @@ -30,7 +29,7 @@ namespace UICatalog.Scenarios { new MenuItem ("_Quit", "", () => Quit()), }) }); - Top.Add (menu); + Application.Top.Add (menu); tabView = new TabView () { X = 0, @@ -42,7 +41,7 @@ namespace UICatalog.Scenarios { tabView.Style.ShowBorder = true; tabView.ApplyStyleChanges (); - Top.Add (tabView); + Application.Top.Add (tabView); var lenStatusItem = new StatusItem (Key.CharMask, "Len: ", null); var statusBar = new StatusBar (new StatusItem [] { @@ -59,7 +58,7 @@ namespace UICatalog.Scenarios { tabView.SelectedTabChanged += (s, e) => lenStatusItem.Title = $"Len:{(e.NewTab?.View?.Text?.Length ?? 0)}"; - Top.Add (statusBar); + Application.Top.Add (statusBar); New (); } diff --git a/UICatalog/Scenarios/ProgressBarStyles.cs b/UICatalog/Scenarios/ProgressBarStyles.cs index 0b8ee4bfe..d35949b5c 100644 --- a/UICatalog/Scenarios/ProgressBarStyles.cs +++ b/UICatalog/Scenarios/ProgressBarStyles.cs @@ -131,7 +131,7 @@ namespace UICatalog.Scenarios { Application.MainLoop.Driver.Wakeup (); }, null, 0, 300); - Top.Unloaded += Top_Unloaded; + Application.Top.Unloaded += Top_Unloaded; void Top_Unloaded () { @@ -143,7 +143,7 @@ namespace UICatalog.Scenarios { _pulseTimer.Dispose (); _pulseTimer = null; } - Top.Unloaded -= Top_Unloaded; + Application.Top.Unloaded -= Top_Unloaded; } } } diff --git a/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs b/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs index a6bd140f0..4d7ddfa44 100644 --- a/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs +++ b/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs @@ -16,7 +16,7 @@ namespace UICatalog.Scenarios { private Window _win; private string _lastRunesUsed; - public override void Init (Toplevel top, ColorScheme colorScheme) + public override void Init (ColorScheme colorScheme) { Application.Init (); diff --git a/UICatalog/Scenarios/Scrolling.cs b/UICatalog/Scenarios/Scrolling.cs index a1b82282a..de28406ae 100644 --- a/UICatalog/Scenarios/Scrolling.cs +++ b/UICatalog/Scenarios/Scrolling.cs @@ -156,9 +156,9 @@ namespace UICatalog.Scenarios { horizontalRuler.Text = rule.Repeat ((int)Math.Ceiling ((double)(horizontalRuler.Bounds.Width) / (double)rule.Length)) [0..(horizontalRuler.Bounds.Width)] + "\n" + "| ".Repeat ((int)Math.Ceiling ((double)(horizontalRuler.Bounds.Width) / (double)rule.Length)) [0..(horizontalRuler.Bounds.Width)]; verticalRuler.Text = vrule.Repeat ((int)Math.Ceiling ((double)(verticalRuler.Bounds.Height * 2) / (double)rule.Length)) [0..(verticalRuler.Bounds.Height * 2)]; - Top.Loaded -= Top_Loaded; + Application.Top.Loaded -= Top_Loaded; } - Top.Loaded += Top_Loaded; + Application.Top.Loaded += Top_Loaded; var pressMeButton = new Button ("Press me!") { X = 3, @@ -313,9 +313,9 @@ namespace UICatalog.Scenarios { void Top_Unloaded () { pulsing = false; - Top.Unloaded -= Top_Unloaded; + Application.Top.Unloaded -= Top_Unloaded; } - Top.Unloaded += Top_Unloaded; + Application.Top.Unloaded += Top_Unloaded; } } } \ No newline at end of file diff --git a/UICatalog/Scenarios/SingleBackgroundWorker.cs b/UICatalog/Scenarios/SingleBackgroundWorker.cs index a2705500c..e0268ab41 100644 --- a/UICatalog/Scenarios/SingleBackgroundWorker.cs +++ b/UICatalog/Scenarios/SingleBackgroundWorker.cs @@ -11,11 +11,11 @@ namespace UICatalog.Scenarios { public class SingleBackgroundWorker : Scenario { public override void Run () { - Top.Dispose (); + Application.Top.Dispose (); Application.Run (); - Top.Dispose (); + Application.Top.Dispose (); } public class MainApp : Toplevel { diff --git a/UICatalog/Scenarios/SyntaxHighlighting.cs b/UICatalog/Scenarios/SyntaxHighlighting.cs index cd3436341..3a696b14c 100644 --- a/UICatalog/Scenarios/SyntaxHighlighting.cs +++ b/UICatalog/Scenarios/SyntaxHighlighting.cs @@ -21,7 +21,7 @@ namespace UICatalog.Scenarios { Win.Title = this.GetName (); Win.Y = 1; // menu Win.Height = Dim.Fill (1); // status bar - Top.LayoutSubviews (); + Application.Top.LayoutSubviews (); var menu = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_File", new MenuItem [] { @@ -29,7 +29,7 @@ namespace UICatalog.Scenarios { new MenuItem ("_Quit", "", () => Quit()), }) }); - Top.Add (menu); + Application.Top.Add (menu); textView = new SqlTextView () { X = 0, @@ -49,7 +49,7 @@ namespace UICatalog.Scenarios { }); - Top.Add (statusBar); + Application.Top.Add (statusBar); } private void WordWrap () diff --git a/UICatalog/Scenarios/TabViewExample.cs b/UICatalog/Scenarios/TabViewExample.cs index 73e27ad69..2bab75e70 100644 --- a/UICatalog/Scenarios/TabViewExample.cs +++ b/UICatalog/Scenarios/TabViewExample.cs @@ -24,7 +24,7 @@ namespace UICatalog.Scenarios { Win.Title = this.GetName (); Win.Y = 1; // menu Win.Height = Dim.Fill (1); // status bar - Top.LayoutSubviews (); + Application.Top.LayoutSubviews (); var menu = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_File", new MenuItem [] { @@ -50,7 +50,7 @@ namespace UICatalog.Scenarios { }) }); - Top.Add (menu); + Application.Top.Add (menu); tabView = new TabView () { X = 0, @@ -85,7 +85,6 @@ namespace UICatalog.Scenarios { Height = Dim.Fill (), }; - frameRight.Add (new TextView () { Text = "This demos the tabs control\nSwitch between tabs using cursor keys", Width = Dim.Fill (), @@ -94,8 +93,6 @@ namespace UICatalog.Scenarios { Win.Add (frameRight); - - var frameBelow = new FrameView ("Bottom Frame") { X = 0, Y = Pos.Bottom (tabView), @@ -103,7 +100,6 @@ namespace UICatalog.Scenarios { Height = Dim.Fill (), }; - frameBelow.Add (new TextView () { Text = "This frame exists to check you can still tab here\nand that the tab control doesn't overspill it's bounds", Width = Dim.Fill (), @@ -115,7 +111,7 @@ namespace UICatalog.Scenarios { var statusBar = new StatusBar (new StatusItem [] { new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()), }); - Top.Add (statusBar); + Application.Top.Add (statusBar); } private void AddBlankTab () diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index e3da3c964..2f71e30ae 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -38,7 +38,7 @@ namespace UICatalog.Scenarios { Win.Title = this.GetName(); Win.Y = 1; // menu Win.Height = Dim.Fill (1); // status bar - Top.LayoutSubviews (); + Application.Top.LayoutSubviews (); this.tableView = new TableView () { X = 0, @@ -78,9 +78,9 @@ namespace UICatalog.Scenarios { new MenuItem ("_Set All MinAcceptableWidth=1", "",SetMinAcceptableWidthToOne), }), }); - - Top.Add (menu); + + Application.Top.Add (menu); var statusBar = new StatusBar (new StatusItem [] { new StatusItem(Key.F2, "~F2~ OpenExample", () => OpenExample(true)), @@ -88,7 +88,7 @@ namespace UICatalog.Scenarios { new StatusItem(Key.F4, "~F4~ OpenSimple", () => OpenSimple(true)), new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()), }); - Top.Add (statusBar); + Application.Top.Add (statusBar); Win.Add (tableView); diff --git a/UICatalog/Scenarios/TextFormatterDemo.cs b/UICatalog/Scenarios/TextFormatterDemo.cs index fb5187673..bf18ed506 100644 --- a/UICatalog/Scenarios/TextFormatterDemo.cs +++ b/UICatalog/Scenarios/TextFormatterDemo.cs @@ -42,7 +42,7 @@ namespace UICatalog.Scenarios { blockText.Text = ustring.Make (block.ToString ()); // .Replace(" ", "\u00A0"); // \u00A0 is 'non-breaking space Win.Add (blockText); - var unicodeCheckBox = new CheckBox ("Unicode", Top.HotKeySpecifier == (Rune)' ') { + var unicodeCheckBox = new CheckBox ("Unicode", Application.Top.HotKeySpecifier == (Rune)' ') { X = 0, Y = Pos.Bottom (blockText) + 1, }; diff --git a/UICatalog/Scenarios/TextViewAutocompletePopup.cs b/UICatalog/Scenarios/TextViewAutocompletePopup.cs index a9518586f..5907a6a25 100644 --- a/UICatalog/Scenarios/TextViewAutocompletePopup.cs +++ b/UICatalog/Scenarios/TextViewAutocompletePopup.cs @@ -33,7 +33,7 @@ namespace UICatalog.Scenarios { new MenuItem ("_Quit", "", () => Quit()) }) }); - Top.Add (menu); + Application.Top.Add (menu); textViewTopLeft = new TextView () { Width = width, @@ -89,7 +89,7 @@ namespace UICatalog.Scenarios { siMultiline = new StatusItem(Key.Null, "", null), siWrap = new StatusItem(Key.Null, "", null) }); - Top.Add (statusBar); + Application.Top.Add (statusBar); Win.LayoutStarted += Win_LayoutStarted; } diff --git a/UICatalog/Scenarios/Threading.cs b/UICatalog/Scenarios/Threading.cs index 99ff09b10..6bd159497 100644 --- a/UICatalog/Scenarios/Threading.cs +++ b/UICatalog/Scenarios/Threading.cs @@ -96,9 +96,9 @@ namespace UICatalog.Scenarios { void Top_Loaded () { _btnActionCancel.SetFocus (); - Top.Loaded -= Top_Loaded; + Application.Top.Loaded -= Top_Loaded; } - Top.Loaded += Top_Loaded; + Application.Top.Loaded += Top_Loaded; } private async void LoadData () diff --git a/UICatalog/Scenarios/TreeUseCases.cs b/UICatalog/Scenarios/TreeUseCases.cs index 4a63c25aa..aa1626c8d 100644 --- a/UICatalog/Scenarios/TreeUseCases.cs +++ b/UICatalog/Scenarios/TreeUseCases.cs @@ -17,7 +17,7 @@ namespace UICatalog.Scenarios { Win.Title = this.GetName (); Win.Y = 1; // menu Win.Height = Dim.Fill (1); // status bar - Top.LayoutSubviews (); + Application.Top.LayoutSubviews (); var menu = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_File", new MenuItem [] { @@ -31,13 +31,13 @@ namespace UICatalog.Scenarios { }), }); - Top.Add (menu); + Application.Top.Add (menu); var statusBar = new StatusBar (new StatusItem [] { new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()), }); - Top.Add (statusBar); + Application.Top.Add (statusBar); // Start with the most basic use case LoadSimpleNodes (); diff --git a/UICatalog/Scenarios/TreeViewFileSystem.cs b/UICatalog/Scenarios/TreeViewFileSystem.cs index 3f32abd5c..ba58a6cff 100644 --- a/UICatalog/Scenarios/TreeViewFileSystem.cs +++ b/UICatalog/Scenarios/TreeViewFileSystem.cs @@ -34,7 +34,7 @@ namespace UICatalog.Scenarios { Win.Title = this.GetName (); Win.Y = 1; // menu Win.Height = Dim.Fill (); - Top.LayoutSubviews (); + Application.Top.LayoutSubviews (); var menu = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_File", new MenuItem [] { @@ -65,7 +65,7 @@ namespace UICatalog.Scenarios { miCursor = new MenuItem ("Curs_or (MultiSelect only)", "", () => SetCursor()){Checked = false, CheckType = MenuItemCheckStyle.Checked}, }), }); - Top.Add (menu); + Application.Top.Add (menu); treeViewFiles = new TreeView () { X = 0, diff --git a/UICatalog/Scenarios/Unicode.cs b/UICatalog/Scenarios/Unicode.cs index 29779c4ac..c2813a0f5 100644 --- a/UICatalog/Scenarios/Unicode.cs +++ b/UICatalog/Scenarios/Unicode.cs @@ -34,14 +34,14 @@ namespace UICatalog.Scenarios { new MenuItem ("_Paste", "", null) }) }); - Top.Add (menu); + Application.Top.Add (menu); var statusBar = new StatusBar (new StatusItem [] { new StatusItem (Key.CtrlMask | Key.Q, "~^Q~ Выход", () => Application.RequestStop()), new StatusItem (Key.Unknown, "~F2~ Создать", null), new StatusItem(Key.Unknown, "~F3~ Со_хранить", null), }); - Top.Add (statusBar); + Application.Top.Add (statusBar); var label = new Label ("Label:") { X = 0, Y = 1 }; Win.Add (label); diff --git a/UICatalog/Scenarios/WindowsAndFrameViews.cs b/UICatalog/Scenarios/WindowsAndFrameViews.cs index ca3f613ef..616c20fca 100644 --- a/UICatalog/Scenarios/WindowsAndFrameViews.cs +++ b/UICatalog/Scenarios/WindowsAndFrameViews.cs @@ -6,13 +6,6 @@ namespace UICatalog.Scenarios { [ScenarioMetadata (Name: "Windows & FrameViews", Description: "Shows Windows, sub-Windows, and FrameViews.")] [ScenarioCategory ("Layout")] public class WindowsAndFrameViews : Scenario { - public override void Init (Toplevel top, ColorScheme colorScheme) - { - Application.Init (); - - Top = top != null ? top : Application.Top; - } - public override void RequestStop () { base.RequestStop (); @@ -67,7 +60,7 @@ namespace UICatalog.Scenarios { Y = Pos.AnchorEnd (1), ColorScheme = Colors.Error }); - Top.Add (Win); + Application.Top.Add (Win); listWin.Add (Win); for (var i = 0; i < 3; i++) { @@ -114,11 +107,10 @@ namespace UICatalog.Scenarios { }); win.Add (frameView); - Top.Add (win); + Application.Top.Add (win); listWin.Add (win); } - FrameView frame = null; frame = new FrameView ($"This is a FrameView") { X = margin, @@ -176,7 +168,7 @@ namespace UICatalog.Scenarios { frame.Add (subFrameViewofFV); - Top.Add (frame); + Application.Top.Add (frame); listWin.Add (frame); } } diff --git a/UICatalog/Scenarios/WizardAsView.cs b/UICatalog/Scenarios/WizardAsView.cs index a53067885..e7be17552 100644 --- a/UICatalog/Scenarios/WizardAsView.cs +++ b/UICatalog/Scenarios/WizardAsView.cs @@ -10,9 +10,9 @@ namespace UICatalog.Scenarios { [ScenarioCategory ("Wizards")] public class WizardAsView : Scenario { - public override void Init (Toplevel top, ColorScheme colorScheme) + public override void Init (ColorScheme colorScheme) { - Top = Application.Top; + Application.Init (); var menu = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_File", new MenuItem [] { @@ -21,7 +21,7 @@ namespace UICatalog.Scenarios { new MenuItem ("_Shutdown Server...", "", () => MessageBox.Query ("Wizaard", "Are you sure you want to cancel setup and shutdown?", "Ok", "Cancel")), }) }); - Top.Add (menu); + Application.Top.Add (menu); // No need for a Title because the border is disabled var wizard = new Wizard () { @@ -93,8 +93,8 @@ namespace UICatalog.Scenarios { wizard.AddStep (lastStep); lastStep.HelpText = "The wizard is complete!\n\nPress the Finish button to continue.\n\nPressing Esc will cancel."; - Top.Add (wizard); - Application.Run (Top); + Application.Top.Add (wizard); + Application.Run (Application.Top); } public override void Run () diff --git a/UICatalog/Scenarios/Wizards.cs b/UICatalog/Scenarios/Wizards.cs index e5f503414..268d9a422 100644 --- a/UICatalog/Scenarios/Wizards.cs +++ b/UICatalog/Scenarios/Wizards.cs @@ -73,9 +73,9 @@ namespace UICatalog.Scenarios { void Top_Loaded () { frame.Height = Dim.Height (widthEdit) + Dim.Height (heightEdit) + Dim.Height (titleEdit) + 2; - Top.Loaded -= Top_Loaded; + Application.Top.Loaded -= Top_Loaded; } - Top.Loaded += Top_Loaded; + Application.Top.Loaded += Top_Loaded; label = new Label ("Action:") { X = Pos.Center (), diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index 33f69f321..979b9eed7 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -44,35 +44,7 @@ namespace UICatalog { /// /// UI Catalog is a comprehensive sample app and scenario library for /// - public class UICatalogApp { - private static int _nameColumnWidth; - private static FrameView _leftPane; - private static List _categories; - private static ListView _categoryListView; - private static FrameView _rightPane; - private static List _scenarios; - private static ListView _scenarioListView; - private static StatusBar _statusBar; - private static StatusItem _capslock; - private static StatusItem _numlock; - private static StatusItem _scrolllock; - - // If set, holds the scenario the user selected - private static Scenario _selectedScenario = null; - - private static bool _useSystemConsole = false; - private static ConsoleDriver.DiagnosticFlags _diagnosticFlags; - private static bool _heightAsBuffer = false; - private static bool _isFirstRunning = true; - - // When a scenario is run, the main app is killed. These items - // are therefore cached so that when the scenario exits the - // main app UI can be restored to previous state - private static int _cachedScenarioIndex = 0; - private static int _cachedCategoryIndex = 0; - - private static StringBuilder _aboutMessage; - + class UICatalogApp { static void Main (string [] args) { Console.OutputEncoding = Encoding.Default; @@ -82,17 +54,22 @@ namespace UICatalog { } _scenarios = Scenario.GetScenarios (); + _categories = Scenario.GetAllCategories (); + _nameColumnWidth = _scenarios.OrderByDescending (s => s.GetName ().Length).FirstOrDefault ().GetName ().Length; if (args.Length > 0 && args.Contains ("-usc")) { _useSystemConsole = true; args = args.Where (val => val != "-usc").ToArray (); } + + // If a Scenario name has been provided on the commandline + // run it and exit when done. if (args.Length > 0) { var item = _scenarios.FindIndex (s => s.GetName ().Equals (args [0], StringComparison.OrdinalIgnoreCase)); _selectedScenario = (Scenario)Activator.CreateInstance (_scenarios [item].GetType ()); Application.UseSystemConsole = _useSystemConsole; Application.Init (); - _selectedScenario.Init (Application.Top, _colorScheme); + _selectedScenario.Init (_colorScheme); _selectedScenario.Setup (); _selectedScenario.Run (); _selectedScenario = null; @@ -113,43 +90,19 @@ namespace UICatalog { _aboutMessage.AppendLine (@"https://github.com/gui-cs/Terminal.Gui"); Scenario scenario; - while ((scenario = SelectScenario ()) != null) { -#if DEBUG_IDISPOSABLE - // Validate there are no outstanding Responder-based instances - // after a scenario was selected to run. This proves the main UI Catalog - // 'app' closed cleanly. - foreach (var inst in Responder.Instances) { - Debug.Assert (inst.WasDisposed); - } - Responder.Instances.Clear (); -#endif - - scenario.Init (Application.Top, _colorScheme); + while ((scenario = RunUICatalogTopLevel ()) != null) { + VerifyObjectsWereDisposed (); + scenario.Init (_colorScheme); scenario.Setup (); scenario.Run (); // This call to Application.Shutdown brackets the Application.Init call - // made by Scenario.Init() + // made by Scenario.Init() above Application.Shutdown (); -#if DEBUG_IDISPOSABLE - // After the scenario runs, validate all Responder-based instances - // were disposed. This proves the scenario 'app' closed cleanly. - foreach (var inst in Responder.Instances) { - Debug.Assert (inst.WasDisposed); - } - Responder.Instances.Clear (); -#endif + VerifyObjectsWereDisposed (); } - -#if DEBUG_IDISPOSABLE - // This proves that when the user exited the UI Catalog app - // it cleaned up properly. - foreach (var inst in Responder.Instances) { - Debug.Assert (inst.WasDisposed); - } - Responder.Instances.Clear (); -#endif + VerifyObjectsWereDisposed (); } /// @@ -158,389 +111,448 @@ namespace UICatalog { /// When the Scenario exits, this function exits. /// /// - private static Scenario SelectScenario () + static Scenario RunUICatalogTopLevel () { Application.UseSystemConsole = _useSystemConsole; - Application.Init (); - if (_colorScheme == null) { - // `Colors` is not initilized until the ConsoleDriver is loaded by - // Application.Init. Set it only the first time though so it is - // preserved between running multiple Scenarios - _colorScheme = Colors.Base; - } - Application.HeightAsBuffer = _heightAsBuffer; - - var menu = new MenuBar (new MenuBarItem [] { - new MenuBarItem ("_File", new MenuItem [] { - new MenuItem ("_Quit", "Quit UI Catalog", () => Application.RequestStop(), null, null, Key.Q | Key.CtrlMask) - }), - new MenuBarItem ("_Color Scheme", CreateColorSchemeMenuItems()), - new MenuBarItem ("Diag_nostics", CreateDiagnosticMenuItems()), - new MenuBarItem ("_Help", new MenuItem [] { - new MenuItem ("_gui.cs API Overview", "", () => OpenUrl ("https://gui-cs.github.io/Terminal.Gui/articles/overview.html"), null, null, Key.F1), - new MenuItem ("gui.cs _README", "", () => OpenUrl ("https://github.com/gui-cs/Terminal.Gui"), null, null, Key.F2), - new MenuItem ("_About...", - "About UI Catalog", () => MessageBox.Query ("About UI Catalog", _aboutMessage.ToString(), "_Ok"), null, null, Key.CtrlMask | Key.A), - }), - }); - - _leftPane = new FrameView ("Categories") { - X = 0, - Y = 1, // for menu - Width = 25, - Height = Dim.Fill (1), - CanFocus = true, - Shortcut = Key.CtrlMask | Key.C - }; - _leftPane.Title = $"{_leftPane.Title} ({_leftPane.ShortcutTag})"; - _leftPane.ShortcutAction = () => _leftPane.SetFocus (); - - _categories = Scenario.GetAllCategories (); - _categoryListView = new ListView (_categories) { - X = 0, - Y = 0, - Width = Dim.Fill (0), - Height = Dim.Fill (0), - AllowsMarking = false, - CanFocus = true, - }; - _categoryListView.OpenSelectedItem += (a) => { - _rightPane.SetFocus (); - }; - _categoryListView.SelectedItemChanged += CategoryListView_SelectedChanged; - _leftPane.Add (_categoryListView); - - _rightPane = new FrameView ("Scenarios") { - X = 25, - Y = 1, // for menu - Width = Dim.Fill (), - Height = Dim.Fill (1), - CanFocus = true, - Shortcut = Key.CtrlMask | Key.S - }; - _rightPane.Title = $"{_rightPane.Title} ({_rightPane.ShortcutTag})"; - _rightPane.ShortcutAction = () => _rightPane.SetFocus (); - - _nameColumnWidth = _scenarios.OrderByDescending (s => s.GetName ().Length).FirstOrDefault ().GetName ().Length; - - _scenarioListView = new ListView () { - X = 0, - Y = 0, - Width = Dim.Fill (0), - Height = Dim.Fill (0), - AllowsMarking = false, - CanFocus = true, - }; - - _scenarioListView.OpenSelectedItem += _scenarioListView_OpenSelectedItem; - _rightPane.Add (_scenarioListView); - - _capslock = new StatusItem (Key.CharMask, "Caps", null); - _numlock = new StatusItem (Key.CharMask, "Num", null); - _scrolllock = new StatusItem (Key.CharMask, "Scroll", null); - - _statusBar = new StatusBar () { - Visible = true, - }; - _statusBar.Items = new StatusItem [] { - _capslock, - _numlock, - _scrolllock, - new StatusItem(Key.Q | Key.CtrlMask, "~CTRL-Q~ Quit", () => { - if (_selectedScenario is null){ - // This causes GetScenarioToRun to return null - _selectedScenario = null; - Application.RequestStop(); - } else { - _selectedScenario.RequestStop(); - } - }), - new StatusItem(Key.F10, "~F10~ Hide/Show Status Bar", () => { - _statusBar.Visible = !_statusBar.Visible; - _leftPane.Height = Dim.Fill(_statusBar.Visible ? 1 : 0); - _rightPane.Height = Dim.Fill(_statusBar.Visible ? 1 : 0); - Application.Top.LayoutSubviews(); - Application.Top.SetChildNeedsDisplay(); - }), - new StatusItem (Key.CharMask, Application.Driver.GetType ().Name, null), - }; - - Application.Top.ColorScheme = _colorScheme; - Application.Top.KeyDown += KeyDownHandler; - Application.Top.Add (menu); - Application.Top.Add (_leftPane); - Application.Top.Add (_rightPane); - Application.Top.Add (_statusBar); - - void TopHandler () - { - if (_selectedScenario != null) { - _selectedScenario = null; - _isFirstRunning = false; - } - if (!_isFirstRunning) { - _rightPane.SetFocus (); - } - Application.Top.Loaded -= TopHandler; - } - Application.Top.Loaded += TopHandler; - - // Restore previous selections - _categoryListView.SelectedItem = _cachedCategoryIndex; - _scenarioListView.SelectedItem = _cachedScenarioIndex; // Run UI Catalog UI. When it exits, if _selectedScenario is != null then // a Scenario was selected. Otherwise, the user wants to exit UI Catalog. - Application.Run (Application.Top); + Application.Init (); + Application.Run (); Application.Shutdown (); return _selectedScenario; } + static List _scenarios; + static List _categories; + static int _nameColumnWidth; + // When a scenario is run, the main app is killed. These items + // are therefore cached so that when the scenario exits the + // main app UI can be restored to previous state + static int _cachedScenarioIndex = 0; + static int _cachedCategoryIndex = 0; + static StringBuilder _aboutMessage; + + // If set, holds the scenario the user selected + static Scenario _selectedScenario = null; + + static bool _useSystemConsole = false; + static ConsoleDriver.DiagnosticFlags _diagnosticFlags; + static bool _heightAsBuffer = false; + static bool _isFirstRunning = true; + static ColorScheme _colorScheme; /// - /// Launches the selected scenario, setting the global _selectedScenario + /// This is the main UI Catalog app view. It is run fresh when the app loads (if a Scenario has not been passed on + /// the command line) and each time a Scenario ends. /// - /// - private static void _scenarioListView_OpenSelectedItem (EventArgs e) - { - if (_selectedScenario is null) { - // Save selected item state - _cachedCategoryIndex = _categoryListView.SelectedItem; - _cachedScenarioIndex = _scenarioListView.SelectedItem; - // Create new instance of scenario (even though Scenarios contains instances) - _selectedScenario = (Scenario)Activator.CreateInstance (_scenarioListView.Source.ToList () [_scenarioListView.SelectedItem].GetType ()); + class UICatalogTopLevel : Toplevel { + public MenuItem miIsMouseDisabled; + public MenuItem miHeightAsBuffer; + + public FrameView LeftPane; + public ListView CategoryListView; + public FrameView RightPane; + public ListView ScenarioListView; + + public StatusItem Capslock; + public StatusItem Numlock; + public StatusItem Scrolllock; + public StatusItem DriverName; - // Tell the main app to stop - Application.RequestStop (); - } - } + public UICatalogTopLevel () + { + ColorScheme = _colorScheme; + MenuBar = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("_File", new MenuItem [] { + new MenuItem ("_Quit", "Quit UI Catalog", () => RequestStop(), null, null, Key.Q | Key.CtrlMask) + }), + new MenuBarItem ("_Color Scheme", CreateColorSchemeMenuItems()), + new MenuBarItem ("Diag_nostics", CreateDiagnosticMenuItems()), + new MenuBarItem ("_Help", new MenuItem [] { + new MenuItem ("_gui.cs API Overview", "", () => OpenUrl ("https://gui-cs.github.io/Terminal.Gui/articles/overview.html"), null, null, Key.F1), + new MenuItem ("gui.cs _README", "", () => OpenUrl ("https://github.com/gui-cs/Terminal.Gui"), null, null, Key.F2), + new MenuItem ("_About...", + "About UI Catalog", () => MessageBox.Query ("About UI Catalog", _aboutMessage.ToString(), "_Ok"), null, null, Key.CtrlMask | Key.A), + }), + }); - static List CreateDiagnosticMenuItems () - { - List menuItems = new List (); - menuItems.Add (CreateDiagnosticFlagsMenuItems ()); - menuItems.Add (new MenuItem [] { null }); - menuItems.Add (CreateSizeStyle ()); - menuItems.Add (CreateDisabledEnabledMouse ()); - menuItems.Add (CreateKeybindings ()); - return menuItems; - } + Capslock = new StatusItem (Key.CharMask, "Caps", null); + Numlock = new StatusItem (Key.CharMask, "Num", null); + Scrolllock = new StatusItem (Key.CharMask, "Scroll", null); + DriverName = new StatusItem (Key.CharMask, "Driver:", null); - private static MenuItem [] CreateDisabledEnabledMouse () - { - List menuItems = new List (); - var item = new MenuItem (); - item.Title = "_Disable Mouse"; - item.Shortcut = Key.CtrlMask | Key.AltMask | (Key)item.Title.ToString ().Substring (1, 1) [0]; - item.CheckType |= MenuItemCheckStyle.Checked; - item.Checked = Application.IsMouseDisabled; - item.Action += () => { - item.Checked = Application.IsMouseDisabled = !item.Checked; - }; - menuItems.Add (item); - - return menuItems.ToArray (); - } - private static MenuItem [] CreateKeybindings () - { - - List menuItems = new List (); - var item = new MenuItem (); - item.Title = "_Key Bindings"; - item.Help = "Change which keys do what"; - item.Action += () => { - var dlg = new KeyBindingsDialog (); - Application.Run (dlg); - }; - - menuItems.Add (null); - menuItems.Add (item); - - return menuItems.ToArray (); - } - - static MenuItem [] CreateSizeStyle () - { - List menuItems = new List (); - var item = new MenuItem (); - item.Title = "_Height As Buffer"; - item.Shortcut = Key.CtrlMask | Key.AltMask | (Key)item.Title.ToString ().Substring (1, 1) [0]; - item.CheckType |= MenuItemCheckStyle.Checked; - item.Checked = Application.HeightAsBuffer; - item.Action += () => { - item.Checked = !item.Checked; - _heightAsBuffer = item.Checked; - Application.HeightAsBuffer = _heightAsBuffer; - }; - menuItems.Add (item); - - return menuItems.ToArray (); - } - - static MenuItem [] CreateDiagnosticFlagsMenuItems () - { - const string OFF = "Diagnostics: _Off"; - const string FRAME_RULER = "Diagnostics: Frame _Ruler"; - const string FRAME_PADDING = "Diagnostics: _Frame Padding"; - var index = 0; - - List menuItems = new List (); - foreach (Enum diag in Enum.GetValues (_diagnosticFlags.GetType ())) { - var item = new MenuItem (); - item.Title = GetDiagnosticsTitle (diag); - item.Shortcut = Key.AltMask + index.ToString () [0]; - index++; - item.CheckType |= MenuItemCheckStyle.Checked; - if (GetDiagnosticsTitle (ConsoleDriver.DiagnosticFlags.Off) == item.Title) { - item.Checked = (_diagnosticFlags & (ConsoleDriver.DiagnosticFlags.FramePadding - | ConsoleDriver.DiagnosticFlags.FrameRuler)) == 0; - } else { - item.Checked = _diagnosticFlags.HasFlag (diag); - } - item.Action += () => { - var t = GetDiagnosticsTitle (ConsoleDriver.DiagnosticFlags.Off); - if (item.Title == t && !item.Checked) { - _diagnosticFlags &= ~(ConsoleDriver.DiagnosticFlags.FramePadding | ConsoleDriver.DiagnosticFlags.FrameRuler); - item.Checked = true; - } else if (item.Title == t && item.Checked) { - _diagnosticFlags |= (ConsoleDriver.DiagnosticFlags.FramePadding | ConsoleDriver.DiagnosticFlags.FrameRuler); - item.Checked = false; - } else { - var f = GetDiagnosticsEnumValue (item.Title); - if (_diagnosticFlags.HasFlag (f)) { - SetDiagnosticsFlag (f, false); + StatusBar = new StatusBar () { + Visible = true, + }; + StatusBar.Items = new StatusItem [] { + Capslock, + Numlock, + Scrolllock, + new StatusItem(Key.Q | Key.CtrlMask, "~CTRL-Q~ Quit", () => { + if (_selectedScenario is null){ + // This causes GetScenarioToRun to return null + _selectedScenario = null; + RequestStop(); } else { - SetDiagnosticsFlag (f, true); + _selectedScenario.RequestStop(); } - } - foreach (var menuItem in menuItems) { - if (menuItem.Title == t) { - menuItem.Checked = !_diagnosticFlags.HasFlag (ConsoleDriver.DiagnosticFlags.FrameRuler) - && !_diagnosticFlags.HasFlag (ConsoleDriver.DiagnosticFlags.FramePadding); - } else if (menuItem.Title != t) { - menuItem.Checked = _diagnosticFlags.HasFlag (GetDiagnosticsEnumValue (menuItem.Title)); - } - } - ConsoleDriver.Diagnostics = _diagnosticFlags; - Application.Top.SetNeedsDisplay (); + }), + new StatusItem(Key.F10, "~F10~ Hide/Show Status Bar", () => { + StatusBar.Visible = !StatusBar.Visible; + LeftPane.Height = Dim.Fill(StatusBar.Visible ? 1 : 0); + RightPane.Height = Dim.Fill(StatusBar.Visible ? 1 : 0); + LayoutSubviews(); + SetChildNeedsDisplay(); + }), + DriverName, }; - menuItems.Add (item); - } - return menuItems.ToArray (); - string GetDiagnosticsTitle (Enum diag) + LeftPane = new FrameView ("Categories") { + X = 0, + Y = 1, // for menu + Width = 25, + Height = Dim.Fill (1), + CanFocus = true, + Shortcut = Key.CtrlMask | Key.C + }; + LeftPane.Title = $"{LeftPane.Title} ({LeftPane.ShortcutTag})"; + LeftPane.ShortcutAction = () => LeftPane.SetFocus (); + + CategoryListView = new ListView (_categories) { + X = 0, + Y = 0, + Width = Dim.Fill (0), + Height = Dim.Fill (0), + AllowsMarking = false, + CanFocus = true, + }; + CategoryListView.OpenSelectedItem += (a) => { + RightPane.SetFocus (); + }; + CategoryListView.SelectedItemChanged += CategoryListView_SelectedChanged; + LeftPane.Add (CategoryListView); + + RightPane = new FrameView ("Scenarios") { + X = 25, + Y = 1, // for menu + Width = Dim.Fill (), + Height = Dim.Fill (1), + CanFocus = true, + Shortcut = Key.CtrlMask | Key.S + }; + RightPane.Title = $"{RightPane.Title} ({RightPane.ShortcutTag})"; + RightPane.ShortcutAction = () => RightPane.SetFocus (); + + ScenarioListView = new ListView () { + X = 0, + Y = 0, + Width = Dim.Fill (0), + Height = Dim.Fill (0), + AllowsMarking = false, + CanFocus = true, + }; + + ScenarioListView.OpenSelectedItem += ScenarioListView_OpenSelectedItem; + RightPane.Add (ScenarioListView); + + KeyDown += KeyDownHandler; + Add (MenuBar); + Add (LeftPane); + Add (RightPane); + Add (StatusBar); + + Loaded += LoadedHandler; + + // Restore previous selections + CategoryListView.SelectedItem = _cachedCategoryIndex; + ScenarioListView.SelectedItem = _cachedScenarioIndex; + } + + void LoadedHandler () { - switch (Enum.GetName (_diagnosticFlags.GetType (), diag)) { - case "Off": - return OFF; - case "FrameRuler": - return FRAME_RULER; - case "FramePadding": - return FRAME_PADDING; + Application.HeightAsBuffer = _heightAsBuffer; + + if (_colorScheme == null) { + ColorScheme = _colorScheme = Colors.Base; } - return ""; - } - Enum GetDiagnosticsEnumValue (ustring title) - { - switch (title.ToString ()) { - case FRAME_RULER: - return ConsoleDriver.DiagnosticFlags.FrameRuler; - case FRAME_PADDING: - return ConsoleDriver.DiagnosticFlags.FramePadding; + miIsMouseDisabled.Checked = Application.IsMouseDisabled; + miHeightAsBuffer.Checked = Application.HeightAsBuffer; + DriverName.Title = $"Driver: {Driver.GetType ().Name}"; + + if (_selectedScenario != null) { + _selectedScenario = null; + _isFirstRunning = false; } - return null; + if (!_isFirstRunning) { + RightPane.SetFocus (); + } + Loaded -= LoadedHandler; } - void SetDiagnosticsFlag (Enum diag, bool add) + /// + /// Launches the selected scenario, setting the global _selectedScenario + /// + /// + void ScenarioListView_OpenSelectedItem (EventArgs e) { - switch (diag) { - case ConsoleDriver.DiagnosticFlags.FrameRuler: - if (add) { - _diagnosticFlags |= ConsoleDriver.DiagnosticFlags.FrameRuler; - } else { - _diagnosticFlags &= ~ConsoleDriver.DiagnosticFlags.FrameRuler; - } - break; - case ConsoleDriver.DiagnosticFlags.FramePadding: - if (add) { - _diagnosticFlags |= ConsoleDriver.DiagnosticFlags.FramePadding; - } else { - _diagnosticFlags &= ~ConsoleDriver.DiagnosticFlags.FramePadding; - } - break; - default: - _diagnosticFlags = default; - break; + if (_selectedScenario is null) { + // Save selected item state + _cachedCategoryIndex = CategoryListView.SelectedItem; + _cachedScenarioIndex = ScenarioListView.SelectedItem; + // Create new instance of scenario (even though Scenarios contains instances) + _selectedScenario = (Scenario)Activator.CreateInstance (ScenarioListView.Source.ToList () [ScenarioListView.SelectedItem].GetType ()); + + // Tell the main app to stop + Application.RequestStop (); } } - } - static ColorScheme _colorScheme; - static MenuItem [] CreateColorSchemeMenuItems () - { - List menuItems = new List (); - foreach (var sc in Colors.ColorSchemes) { + List CreateDiagnosticMenuItems () + { + List menuItems = new List (); + menuItems.Add (CreateDiagnosticFlagsMenuItems ()); + menuItems.Add (new MenuItem [] { null }); + menuItems.Add (CreateHeightAsBufferMenuItems ()); + menuItems.Add (CreateDisabledEnabledMouseItems ()); + menuItems.Add (CreateKeybindingsMenuItems ()); + return menuItems; + } + + MenuItem [] CreateDisabledEnabledMouseItems () + { + List menuItems = new List (); + miIsMouseDisabled = new MenuItem (); + miIsMouseDisabled.Title = "_Disable Mouse"; + miIsMouseDisabled.Shortcut = Key.CtrlMask | Key.AltMask | (Key)miIsMouseDisabled.Title.ToString ().Substring (1, 1) [0]; + miIsMouseDisabled.CheckType |= MenuItemCheckStyle.Checked; + miIsMouseDisabled.Action += () => { + miIsMouseDisabled.Checked = Application.IsMouseDisabled = !miIsMouseDisabled.Checked; + }; + menuItems.Add (miIsMouseDisabled); + + return menuItems.ToArray (); + } + + MenuItem [] CreateKeybindingsMenuItems () + { + List menuItems = new List (); var item = new MenuItem (); - item.Title = $"_{sc.Key}"; - item.Shortcut = Key.AltMask | (Key)sc.Key.Substring (0, 1) [0]; - item.CheckType |= MenuItemCheckStyle.Radio; - item.Checked = sc.Value == _colorScheme; + item.Title = "_Key Bindings"; + item.Help = "Change which keys do what"; item.Action += () => { - Application.Top.ColorScheme = _colorScheme = sc.Value; - Application.Top?.SetNeedsDisplay (); - foreach (var menuItem in menuItems) { - menuItem.Checked = menuItem.Title.Equals ($"_{sc.Key}") && sc.Value == _colorScheme; - } + var dlg = new KeyBindingsDialog (); + Application.Run (dlg); }; + + menuItems.Add (null); menuItems.Add (item); + + return menuItems.ToArray (); + } + + MenuItem [] CreateHeightAsBufferMenuItems () + { + List menuItems = new List (); + miHeightAsBuffer = new MenuItem (); + miHeightAsBuffer.Title = "_Height As Buffer"; + miHeightAsBuffer.Shortcut = Key.CtrlMask | Key.AltMask | (Key)miHeightAsBuffer.Title.ToString ().Substring (1, 1) [0]; + miHeightAsBuffer.CheckType |= MenuItemCheckStyle.Checked; + miHeightAsBuffer.Action += () => { + miHeightAsBuffer.Checked = !miHeightAsBuffer.Checked; + Application.HeightAsBuffer = miHeightAsBuffer.Checked; + }; + menuItems.Add (miHeightAsBuffer); + + return menuItems.ToArray (); + } + + MenuItem [] CreateDiagnosticFlagsMenuItems () + { + const string OFF = "Diagnostics: _Off"; + const string FRAME_RULER = "Diagnostics: Frame _Ruler"; + const string FRAME_PADDING = "Diagnostics: _Frame Padding"; + var index = 0; + + List menuItems = new List (); + foreach (Enum diag in Enum.GetValues (_diagnosticFlags.GetType ())) { + var item = new MenuItem (); + item.Title = GetDiagnosticsTitle (diag); + item.Shortcut = Key.AltMask + index.ToString () [0]; + index++; + item.CheckType |= MenuItemCheckStyle.Checked; + if (GetDiagnosticsTitle (ConsoleDriver.DiagnosticFlags.Off) == item.Title) { + item.Checked = (_diagnosticFlags & (ConsoleDriver.DiagnosticFlags.FramePadding + | ConsoleDriver.DiagnosticFlags.FrameRuler)) == 0; + } else { + item.Checked = _diagnosticFlags.HasFlag (diag); + } + item.Action += () => { + var t = GetDiagnosticsTitle (ConsoleDriver.DiagnosticFlags.Off); + if (item.Title == t && !item.Checked) { + _diagnosticFlags &= ~(ConsoleDriver.DiagnosticFlags.FramePadding | ConsoleDriver.DiagnosticFlags.FrameRuler); + item.Checked = true; + } else if (item.Title == t && item.Checked) { + _diagnosticFlags |= (ConsoleDriver.DiagnosticFlags.FramePadding | ConsoleDriver.DiagnosticFlags.FrameRuler); + item.Checked = false; + } else { + var f = GetDiagnosticsEnumValue (item.Title); + if (_diagnosticFlags.HasFlag (f)) { + SetDiagnosticsFlag (f, false); + } else { + SetDiagnosticsFlag (f, true); + } + } + foreach (var menuItem in menuItems) { + if (menuItem.Title == t) { + menuItem.Checked = !_diagnosticFlags.HasFlag (ConsoleDriver.DiagnosticFlags.FrameRuler) + && !_diagnosticFlags.HasFlag (ConsoleDriver.DiagnosticFlags.FramePadding); + } else if (menuItem.Title != t) { + menuItem.Checked = _diagnosticFlags.HasFlag (GetDiagnosticsEnumValue (menuItem.Title)); + } + } + ConsoleDriver.Diagnostics = _diagnosticFlags; + Application.Top.SetNeedsDisplay (); + }; + menuItems.Add (item); + } + return menuItems.ToArray (); + + string GetDiagnosticsTitle (Enum diag) + { + switch (Enum.GetName (_diagnosticFlags.GetType (), diag)) { + case "Off": + return OFF; + case "FrameRuler": + return FRAME_RULER; + case "FramePadding": + return FRAME_PADDING; + } + return ""; + } + + Enum GetDiagnosticsEnumValue (ustring title) + { + switch (title.ToString ()) { + case FRAME_RULER: + return ConsoleDriver.DiagnosticFlags.FrameRuler; + case FRAME_PADDING: + return ConsoleDriver.DiagnosticFlags.FramePadding; + } + return null; + } + + void SetDiagnosticsFlag (Enum diag, bool add) + { + switch (diag) { + case ConsoleDriver.DiagnosticFlags.FrameRuler: + if (add) { + _diagnosticFlags |= ConsoleDriver.DiagnosticFlags.FrameRuler; + } else { + _diagnosticFlags &= ~ConsoleDriver.DiagnosticFlags.FrameRuler; + } + break; + case ConsoleDriver.DiagnosticFlags.FramePadding: + if (add) { + _diagnosticFlags |= ConsoleDriver.DiagnosticFlags.FramePadding; + } else { + _diagnosticFlags &= ~ConsoleDriver.DiagnosticFlags.FramePadding; + } + break; + default: + _diagnosticFlags = default; + break; + } + } + } + + MenuItem [] CreateColorSchemeMenuItems () + { + List menuItems = new List (); + foreach (var sc in Colors.ColorSchemes) { + var item = new MenuItem (); + item.Title = $"_{sc.Key}"; + item.Shortcut = Key.AltMask | (Key)sc.Key.Substring (0, 1) [0]; + item.CheckType |= MenuItemCheckStyle.Radio; + item.Checked = sc.Value == _colorScheme; + item.Action += () => { + ColorScheme = _colorScheme = sc.Value; + SetNeedsDisplay (); + foreach (var menuItem in menuItems) { + menuItem.Checked = menuItem.Title.Equals ($"_{sc.Key}") && sc.Value == _colorScheme; + } + }; + menuItems.Add (item); + } + return menuItems.ToArray (); + } + + void KeyDownHandler (View.KeyEventEventArgs a) + { + if (a.KeyEvent.IsCapslock) { + Capslock.Title = "Caps: On"; + StatusBar.SetNeedsDisplay (); + } else { + Capslock.Title = "Caps: Off"; + StatusBar.SetNeedsDisplay (); + } + + if (a.KeyEvent.IsNumlock) { + Numlock.Title = "Num: On"; + StatusBar.SetNeedsDisplay (); + } else { + Numlock.Title = "Num: Off"; + StatusBar.SetNeedsDisplay (); + } + + if (a.KeyEvent.IsScrolllock) { + Scrolllock.Title = "Scroll: On"; + StatusBar.SetNeedsDisplay (); + } else { + Scrolllock.Title = "Scroll: Off"; + StatusBar.SetNeedsDisplay (); + } + } + + void CategoryListView_SelectedChanged (ListViewItemEventArgs e) + { + var item = _categories [e.Item]; + List newlist; + if (e.Item == 0) { + // First category is "All" + newlist = _scenarios; + + } else { + newlist = _scenarios.Where (s => s.GetCategories ().Contains (item)).ToList (); + } + ScenarioListView.SetSource (newlist.ToList ()); } - return menuItems.ToArray (); } - private static void KeyDownHandler (View.KeyEventEventArgs a) + static void VerifyObjectsWereDisposed () { - if (a.KeyEvent.IsCapslock) { - _capslock.Title = "Caps: On"; - _statusBar.SetNeedsDisplay (); - } else { - _capslock.Title = "Caps: Off"; - _statusBar.SetNeedsDisplay (); +#if DEBUG_IDISPOSABLE + // Validate there are no outstanding Responder-based instances + // after a scenario was selected to run. This proves the main UI Catalog + // 'app' closed cleanly. + foreach (var inst in Responder.Instances) { + Debug.Assert (inst.WasDisposed); } + Responder.Instances.Clear (); - if (a.KeyEvent.IsNumlock) { - _numlock.Title = "Num: On"; - _statusBar.SetNeedsDisplay (); - } else { - _numlock.Title = "Num: Off"; - _statusBar.SetNeedsDisplay (); - } - - if (a.KeyEvent.IsScrolllock) { - _scrolllock.Title = "Scroll: On"; - _statusBar.SetNeedsDisplay (); - } else { - _scrolllock.Title = "Scroll: Off"; - _statusBar.SetNeedsDisplay (); + // Validate there are no outstanding Application.RunState-based instances + // after a scenario was selected to run. This proves the main UI Catalog + // 'app' closed cleanly. + foreach (var inst in Application.RunState.Instances) { + Debug.Assert (inst.WasDisposed); } + Application.RunState.Instances.Clear (); +#endif } - private static void CategoryListView_SelectedChanged (ListViewItemEventArgs e) - { - var item = _categories [e.Item]; - List newlist; - if (e.Item == 0) { - // First category is "All" - newlist = _scenarios; - - } else { - newlist = _scenarios.Where (s => s.GetCategories ().Contains (item)).ToList (); - } - _scenarioListView.SetSource (newlist.ToList ()); - } - - private static void OpenUrl (string url) + static void OpenUrl (string url) { try { if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { diff --git a/UnitTests/ApplicationTests.cs b/UnitTests/ApplicationTests.cs index e0f331e74..5d2551fc2 100644 --- a/UnitTests/ApplicationTests.cs +++ b/UnitTests/ApplicationTests.cs @@ -1,5 +1,7 @@ using System; +using System.Diagnostics; using System.Linq; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -13,31 +15,10 @@ namespace Terminal.Gui.Core { { #if DEBUG_IDISPOSABLE Responder.Instances.Clear (); + Application.RunState.Instances.Clear (); #endif } - [Fact] - public void Init_Shutdown_Cleans_Up () - { - // Verify initial state is per spec - Pre_Init_State (); - - Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); - - // Verify post-Init state is correct - Post_Init_State (); - - // MockDriver is always 80x25 - Assert.Equal (80, Application.Driver.Cols); - Assert.Equal (25, Application.Driver.Rows); - - Application.Shutdown (); - - // Verify state is back to initial - Pre_Init_State (); - - } - void Pre_Init_State () { Assert.Null (Application.Driver); @@ -62,23 +43,6 @@ namespace Terminal.Gui.Core { Assert.Null (Application.Resized); } - [Fact] - public void RunState_Dispose_Cleans_Up () - { - var rs = new Application.RunState (null); - Assert.NotNull (rs); - - // Should not throw because Toplevel was null - rs.Dispose (); - - var top = new Toplevel (); - rs = new Application.RunState (top); - Assert.NotNull (rs); - - // Should throw because there's no stack - Assert.Throws (() => rs.Dispose ()); - } - void Init () { Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); @@ -93,19 +57,106 @@ namespace Terminal.Gui.Core { } [Fact] - public void Begin_End_Cleana_Up () + public void Init_Shutdown_Cleans_Up () + { + // Verify initial state is per spec + Pre_Init_State (); + + Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); + + // Verify post-Init state is correct + Post_Init_State (); + + // MockDriver is always 80x25 + Assert.Equal (80, Application.Driver.Cols); + Assert.Equal (25, Application.Driver.Rows); + + Application.Shutdown (); + + // Verify state is back to initial + Pre_Init_State (); + + // Validate there are no outstanding Responder-based instances + // after a scenario was selected to run. This proves the main UI Catalog + // 'app' closed cleanly. + foreach (var inst in Responder.Instances) { + Assert.True (inst.WasDisposed); + } + } + + [Fact] + public void Init_Shutdown_Toplevel_Not_Disposed () + { + Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); + + Application.Shutdown (); + + Assert.Single (Responder.Instances); + Assert.True (Responder.Instances [0].WasDisposed); + } + + [Fact] + public void Init_Unbalanced_Throwss () + { + Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); + + Toplevel topLevel = null; + Assert.Throws (() => Application.InternalInit (() => topLevel = new TestToplevel (), new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)))); + Shutdown (); + + Assert.Null (Application.Top); + Assert.Null (Application.MainLoop); + Assert.Null (Application.Driver); + + // Now try the other way + topLevel = null; + Application.InternalInit (() => topLevel = new TestToplevel (), new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); + + Assert.Throws (() => Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)))); + Shutdown (); + + Assert.Null (Application.Top); + Assert.Null (Application.MainLoop); + Assert.Null (Application.Driver); + } + + + class TestToplevel : Toplevel { + public TestToplevel () + { + IsMdiContainer = false; + } + } + + [Fact] + public void Init_Begin_End_Cleans_Up () { - // Setup Mock driver Init (); - // Test null Toplevel - Assert.Throws (() => Application.Begin (null)); + // Begin will cause Run() to be called, which will call Begin(). Thus will block the tests + // if we don't stop + Application.Iteration = () => { + Application.RequestStop (); + }; - var top = new Toplevel (); - var rs = Application.Begin (top); + Application.RunState runstate = null; + Action NewRunStateFn = (rs) => { + Assert.NotNull (rs); + runstate = rs; + }; + Application.NotifyNewRunState += NewRunStateFn; + + Toplevel topLevel = new Toplevel (); + var rs = Application.Begin (topLevel); Assert.NotNull (rs); - Assert.Equal (top, Application.Current); - Application.End (rs); + Assert.NotNull (runstate); + Assert.Equal (rs, runstate); + + Assert.Equal (topLevel, Application.Top); + Assert.Equal (topLevel, Application.Current); + + Application.NotifyNewRunState -= NewRunStateFn; + Application.End (runstate); Assert.Null (Application.Current); Assert.NotNull (Application.Top); @@ -120,7 +171,200 @@ namespace Terminal.Gui.Core { } [Fact] - public void RequestStop_Stops () + public void InitWithTopLevelFactory_Begin_End_Cleans_Up () + { + // Begin will cause Run() to be called, which will call Begin(). Thus will block the tests + // if we don't stop + Application.Iteration = () => { + Application.RequestStop (); + }; + + // NOTE: Run, when called after Init has been called behaves differently than + // when called if Init has not been called. + Toplevel topLevel = null; + Application.InternalInit (() => topLevel = new TestToplevel (), new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); + + Application.RunState runstate = null; + Action NewRunStateFn = (rs) => { + Assert.NotNull (rs); + runstate = rs; + }; + Application.NotifyNewRunState += NewRunStateFn; + + var rs = Application.Begin (topLevel); + Assert.NotNull (rs); + Assert.NotNull (runstate); + Assert.Equal (rs, runstate); + + Assert.Equal (topLevel, Application.Top); + Assert.Equal (topLevel, Application.Current); + + Application.NotifyNewRunState -= NewRunStateFn; + Application.End (runstate); + + Assert.Null (Application.Current); + Assert.NotNull (Application.Top); + Assert.NotNull (Application.MainLoop); + Assert.NotNull (Application.Driver); + + Shutdown (); + + Assert.Null (Application.Top); + Assert.Null (Application.MainLoop); + Assert.Null (Application.Driver); + } + + [Fact] + public void Begin_Null_Toplevel_Throws () + { + // Setup Mock driver + Init (); + + // Test null Toplevel + Assert.Throws (() => Application.Begin (null)); + + Shutdown (); + + Assert.Null (Application.Top); + Assert.Null (Application.MainLoop); + Assert.Null (Application.Driver); + } + + #region RunTests + + [Fact] + public void Run_T_After_InitWithDriver_with_TopLevel_Throws () + { + // Setup Mock driver + Init (); + + // Run when already initialized with a Driver will throw (because Toplevel is not dervied from TopLevel) + Assert.Throws (() => Application.Run (errorHandler: null)); + + Shutdown (); + + Assert.Null (Application.Top); + Assert.Null (Application.MainLoop); + Assert.Null (Application.Driver); + } + + [Fact] + public void Run_T_After_InitWithDriver_with_TopLevel_and_Driver_Throws () + { + // Setup Mock driver + Init (); + + // Run when already initialized with a Driver will throw (because Toplevel is not dervied from TopLevel) + Assert.Throws (() => Application.Run (errorHandler: null, new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)))); + + Shutdown (); + + Assert.Null (Application.Top); + Assert.Null (Application.MainLoop); + Assert.Null (Application.Driver); + } + + [Fact] + public void Run_T_After_InitWithDriver_with_TestTopLevel_DoesNotThrow () + { + // Setup Mock driver + Init (); + + Application.Iteration = () => { + Application.RequestStop (); + }; + + // Init has been called and we're passing no driver to Run. This is ok. + Application.Run (); + + Shutdown (); + + Assert.Null (Application.Top); + Assert.Null (Application.MainLoop); + Assert.Null (Application.Driver); + } + + [Fact] + public void Run_T_After_InitNullDriver_with_TestTopLevel_Throws () + { + Application.ForceFakeConsole = true; + + Application.Init (null, null); + Assert.Equal (typeof (FakeDriver), Application.Driver.GetType ()); + + Application.Iteration = () => { + Application.RequestStop (); + }; + + // Init has been called without selecting a driver and we're passing no driver to Run. Bad + Application.Run (); + + Shutdown (); + + Assert.Null (Application.Top); + Assert.Null (Application.MainLoop); + Assert.Null (Application.Driver); + } + + [Fact] + public void Run_T_Init_Driver_Cleared_with_TestTopLevel_Throws () + { + Init (); + + Application.Driver = null; + + Application.Iteration = () => { + Application.RequestStop (); + }; + + // Init has been called, but Driver has been set to null. Bad. + Assert.Throws (() => Application.Run ()); + + Shutdown (); + + Assert.Null (Application.Top); + Assert.Null (Application.MainLoop); + Assert.Null (Application.Driver); + } + + [Fact] + public void Run_T_NoInit_DoesNotThrow () + { + Application.ForceFakeConsole = true; + + Application.Iteration = () => { + Application.RequestStop (); + }; + + Application.Run (); + Assert.Equal (typeof (FakeDriver), Application.Driver.GetType ()); + + Shutdown (); + + Assert.Null (Application.Top); + Assert.Null (Application.MainLoop); + Assert.Null (Application.Driver); + } + + [Fact] + public void Run_T_NoInit_WithDriver_DoesNotThrow () + { + Application.Iteration = () => { + Application.RequestStop (); + }; + + // Init has NOT been called and we're passing a valid driver to Run. This is ok. + Application.Run (errorHandler: null, new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); + + Shutdown (); + + Assert.Null (Application.Top); + Assert.Null (Application.MainLoop); + Assert.Null (Application.Driver); + } + + [Fact] + public void Run_RequestStop_Stops () { // Setup Mock driver Init (); @@ -144,7 +388,7 @@ namespace Terminal.Gui.Core { } [Fact] - public void RunningFalse_Stops () + public void Run_RunningFalse_Stops () { // Setup Mock driver Init (); @@ -167,7 +411,142 @@ namespace Terminal.Gui.Core { Assert.Null (Application.Driver); } + [Fact] + public void Run_Loaded_Ready_Unlodaded_Events () + { + Init (); + var top = Application.Top; + var count = 0; + top.Loaded += () => count++; + top.Ready += () => count++; + top.Unloaded += () => count++; + Application.Iteration = () => Application.RequestStop (); + Application.Run (); + Application.Shutdown (); + Assert.Equal (3, count); + } + // TODO: Add tests for Run that test errorHandler + + #endregion + + #region ShutdownTests + [Fact] + public void Shutdown_Allows_Async () + { + static async Task TaskWithAsyncContinuation () + { + await Task.Yield (); + await Task.Yield (); + } + + Init (); + Application.Shutdown (); + + var task = TaskWithAsyncContinuation (); + Thread.Sleep (20); + Assert.True (task.IsCompletedSuccessfully); + } + + [Fact] + public void Shutdown_Resets_SyncContext () + { + Init (); + Application.Shutdown (); + Assert.Null (SynchronizationContext.Current); + } + #endregion + + [Fact] + [AutoInitShutdown] + public void SetCurrentAsTop_Run_A_Not_Modal_Toplevel_Make_It_The_Current_Application_Top () + { + var t1 = new Toplevel (); + var t2 = new Toplevel (); + var t3 = new Toplevel (); + var d = new Dialog (); + var t4 = new Toplevel (); + + // t1, t2, t3, d, t4 + var iterations = 5; + + t1.Ready += () => { + Assert.Equal (t1, Application.Top); + Application.Run (t2); + }; + t2.Ready += () => { + Assert.Equal (t2, Application.Top); + Application.Run (t3); + }; + t3.Ready += () => { + Assert.Equal (t3, Application.Top); + Application.Run (d); + }; + d.Ready += () => { + Assert.Equal (t3, Application.Top); + Application.Run (t4); + }; + t4.Ready += () => { + Assert.Equal (t4, Application.Top); + t4.RequestStop (); + d.RequestStop (); + t3.RequestStop (); + t2.RequestStop (); + }; + // Now this will close the MdiContainer when all MdiChildes was closed + t2.Closed += (_) => { + t1.RequestStop (); + }; + Application.Iteration += () => { + if (iterations == 5) { + // The Current still is t4 because Current.Running is false. + Assert.Equal (t4, Application.Current); + Assert.False (Application.Current.Running); + Assert.Equal (t4, Application.Top); + } else if (iterations == 4) { + // The Current is d and Current.Running is false. + Assert.Equal (d, Application.Current); + Assert.False (Application.Current.Running); + Assert.Equal (t4, Application.Top); + } else if (iterations == 3) { + // The Current is t3 and Current.Running is false. + Assert.Equal (t3, Application.Current); + Assert.False (Application.Current.Running); + Assert.Equal (t3, Application.Top); + } else if (iterations == 2) { + // The Current is t2 and Current.Running is false. + Assert.Equal (t2, Application.Current); + Assert.False (Application.Current.Running); + Assert.Equal (t2, Application.Top); + } else { + // The Current is t1. + Assert.Equal (t1, Application.Current); + Assert.False (Application.Current.Running); + Assert.Equal (t1, Application.Top); + } + iterations--; + }; + + Application.Run (t1); + + Assert.Equal (t1, Application.Top); + } + + [Fact] + [AutoInitShutdown] + public void Internal_Properties_Correct () + { + Assert.True (Application._initialized); + Assert.NotNull (Application.Top); + var rs = Application.Begin (Application.Top); + Assert.Equal (Application.Top, rs.Toplevel); + Assert.Null (Application.MouseGrabView); // public + Assert.Null (Application.WantContinuousButtonPressedView); // public + Assert.False (Application.DebugDrawBounds); + Assert.False (Application.ShowChild (Application.Top)); + } + + #region KeyboardTests [Fact] public void KeyUp_Event () { @@ -228,46 +607,6 @@ namespace Terminal.Gui.Core { Assert.Null (Application.Driver); } - [Fact] - public void Loaded_Ready_Unlodaded_Events () - { - Init (); - var top = Application.Top; - var count = 0; - top.Loaded += () => count++; - top.Ready += () => count++; - top.Unloaded += () => count++; - Application.Iteration = () => Application.RequestStop (); - Application.Run (); - Application.Shutdown (); - Assert.Equal (3, count); - } - - [Fact] - public void Shutdown_Allows_Async () - { - static async Task TaskWithAsyncContinuation () - { - await Task.Yield (); - await Task.Yield (); - } - - Init (); - Application.Shutdown (); - - var task = TaskWithAsyncContinuation (); - Thread.Sleep (20); - Assert.True (task.IsCompletedSuccessfully); - } - - [Fact] - public void Shutdown_Resets_SyncContext () - { - Init (); - Application.Shutdown (); - Assert.Null (SynchronizationContext.Current); - } - [Fact] public void AlternateForwardKey_AlternateBackwardKey_Tests () { @@ -381,776 +720,6 @@ namespace Terminal.Gui.Core { Application.Shutdown (); } - [Fact] - public void Application_RequestStop_With_Params_On_A_Not_MdiContainer_Always_Use_The_Application_Current () - { - Init (); - - var top1 = new Toplevel (); - var top2 = new Toplevel (); - var top3 = new Window (); - var top4 = new Window (); - var d = new Dialog (); - - // top1, top2, top3, d1 = 4 - var iterations = 4; - - top1.Ready += () => { - Assert.Null (Application.MdiChildes); - Application.Run (top2); - }; - top2.Ready += () => { - Assert.Null (Application.MdiChildes); - Application.Run (top3); - }; - top3.Ready += () => { - Assert.Null (Application.MdiChildes); - Application.Run (top4); - }; - top4.Ready += () => { - Assert.Null (Application.MdiChildes); - Application.Run (d); - }; - - d.Ready += () => { - Assert.Null (Application.MdiChildes); - // This will close the d because on a not MdiContainer the Application.Current it always used. - Application.RequestStop (top1); - Assert.True (Application.Current == d); - }; - - d.Closed += (e) => Application.RequestStop (top1); - - Application.Iteration += () => { - Assert.Null (Application.MdiChildes); - if (iterations == 4) { - Assert.True (Application.Current == d); - } else if (iterations == 3) { - Assert.True (Application.Current == top4); - } else if (iterations == 2) { - Assert.True (Application.Current == top3); - } else if (iterations == 1) { - Assert.True (Application.Current == top2); - } else { - Assert.True (Application.Current == top1); - } - Application.RequestStop (top1); - iterations--; - }; - - Application.Run (top1); - - Assert.Null (Application.MdiChildes); - - Application.Shutdown (); - } - - class Mdi : Toplevel { - public Mdi () - { - IsMdiContainer = true; - } - } - - [Fact] - public void MdiContainer_With_Toplevel_RequestStop_Balanced () - { - Init (); - - var mdi = new Mdi (); - var c1 = new Toplevel (); - var c2 = new Window (); - var c3 = new Window (); - var d = new Dialog (); - - // MdiChild = c1, c2, c3 - // d1 = 1 - var iterations = 4; - - mdi.Ready += () => { - Assert.Empty (Application.MdiChildes); - Application.Run (c1); - }; - c1.Ready += () => { - Assert.Single (Application.MdiChildes); - Application.Run (c2); - }; - c2.Ready += () => { - Assert.Equal (2, Application.MdiChildes.Count); - Application.Run (c3); - }; - c3.Ready += () => { - Assert.Equal (3, Application.MdiChildes.Count); - Application.Run (d); - }; - - // More easy because the Mdi Container handles all at once - d.Ready += () => { - Assert.Equal (3, Application.MdiChildes.Count); - // This will not close the MdiContainer because d is a modal toplevel and will be closed. - mdi.RequestStop (); - }; - - // Now this will close the MdiContainer propagating through the MdiChildes. - d.Closed += (e) => { - mdi.RequestStop (); - }; - - Application.Iteration += () => { - if (iterations == 4) { - // The Dialog was not closed before and will be closed now. - Assert.True (Application.Current == d); - Assert.False (d.Running); - } else { - Assert.Equal (iterations, Application.MdiChildes.Count); - for (int i = 0; i < iterations; i++) { - Assert.Equal ((iterations - i + 1).ToString (), Application.MdiChildes [i].Id); - } - } - iterations--; - }; - - Application.Run (mdi); - - Assert.Empty (Application.MdiChildes); - - Application.Shutdown (); - } - - [Fact] - public void MdiContainer_With_Application_RequestStop_MdiTop_With_Params () - { - Init (); - - var mdi = new Mdi (); - var c1 = new Toplevel (); - var c2 = new Window (); - var c3 = new Window (); - var d = new Dialog (); - - // MdiChild = c1, c2, c3 - // d1 = 1 - var iterations = 4; - - mdi.Ready += () => { - Assert.Empty (Application.MdiChildes); - Application.Run (c1); - }; - c1.Ready += () => { - Assert.Single (Application.MdiChildes); - Application.Run (c2); - }; - c2.Ready += () => { - Assert.Equal (2, Application.MdiChildes.Count); - Application.Run (c3); - }; - c3.Ready += () => { - Assert.Equal (3, Application.MdiChildes.Count); - Application.Run (d); - }; - - // Also easy because the Mdi Container handles all at once - d.Ready += () => { - Assert.Equal (3, Application.MdiChildes.Count); - // This will not close the MdiContainer because d is a modal toplevel - Application.RequestStop (mdi); - }; - - // Now this will close the MdiContainer propagating through the MdiChildes. - d.Closed += (e) => Application.RequestStop (mdi); - - Application.Iteration += () => { - if (iterations == 4) { - // The Dialog was not closed before and will be closed now. - Assert.True (Application.Current == d); - Assert.False (d.Running); - } else { - Assert.Equal (iterations, Application.MdiChildes.Count); - for (int i = 0; i < iterations; i++) { - Assert.Equal ((iterations - i + 1).ToString (), Application.MdiChildes [i].Id); - } - } - iterations--; - }; - - Application.Run (mdi); - - Assert.Empty (Application.MdiChildes); - - Application.Shutdown (); - } - - [Fact] - public void MdiContainer_With_Application_RequestStop_MdiTop_Without_Params () - { - Init (); - - var mdi = new Mdi (); - var c1 = new Toplevel (); - var c2 = new Window (); - var c3 = new Window (); - var d = new Dialog (); - - // MdiChild = c1, c2, c3 = 3 - // d1 = 1 - var iterations = 4; - - mdi.Ready += () => { - Assert.Empty (Application.MdiChildes); - Application.Run (c1); - }; - c1.Ready += () => { - Assert.Single (Application.MdiChildes); - Application.Run (c2); - }; - c2.Ready += () => { - Assert.Equal (2, Application.MdiChildes.Count); - Application.Run (c3); - }; - c3.Ready += () => { - Assert.Equal (3, Application.MdiChildes.Count); - Application.Run (d); - }; - - //More harder because it's sequential. - d.Ready += () => { - Assert.Equal (3, Application.MdiChildes.Count); - // Close the Dialog - Application.RequestStop (); - }; - - // Now this will close the MdiContainer propagating through the MdiChildes. - d.Closed += (e) => Application.RequestStop (mdi); - - Application.Iteration += () => { - if (iterations == 4) { - // The Dialog still is the current top and we can't request stop to MdiContainer - // because we are not using parameter calls. - Assert.True (Application.Current == d); - Assert.False (d.Running); - } else { - Assert.Equal (iterations, Application.MdiChildes.Count); - for (int i = 0; i < iterations; i++) { - Assert.Equal ((iterations - i + 1).ToString (), Application.MdiChildes [i].Id); - } - } - iterations--; - }; - - Application.Run (mdi); - - Assert.Empty (Application.MdiChildes); - - Application.Shutdown (); - } - - [Fact] - public void IsMdiChild_Testing () - { - Init (); - - var mdi = new Mdi (); - var c1 = new Toplevel (); - var c2 = new Window (); - var c3 = new Window (); - var d = new Dialog (); - - Application.Iteration += () => { - Assert.False (mdi.IsMdiChild); - Assert.True (c1.IsMdiChild); - Assert.True (c2.IsMdiChild); - Assert.True (c3.IsMdiChild); - Assert.False (d.IsMdiChild); - - mdi.RequestStop (); - }; - - Application.Run (mdi); - - Application.Shutdown (); - } - - [Fact] - public void Modal_Toplevel_Can_Open_Another_Modal_Toplevel_But_RequestStop_To_The_Caller_Also_Sets_Current_Running_To_False_Too () - { - Init (); - - var mdi = new Mdi (); - var c1 = new Toplevel (); - var c2 = new Window (); - var c3 = new Window (); - var d1 = new Dialog (); - var d2 = new Dialog (); - - // MdiChild = c1, c2, c3 = 3 - // d1, d2 = 2 - var iterations = 5; - - mdi.Ready += () => { - Assert.Empty (Application.MdiChildes); - Application.Run (c1); - }; - c1.Ready += () => { - Assert.Single (Application.MdiChildes); - Application.Run (c2); - }; - c2.Ready += () => { - Assert.Equal (2, Application.MdiChildes.Count); - Application.Run (c3); - }; - c3.Ready += () => { - Assert.Equal (3, Application.MdiChildes.Count); - Application.Run (d1); - }; - d1.Ready += () => { - Assert.Equal (3, Application.MdiChildes.Count); - Application.Run (d2); - }; - - d2.Ready += () => { - Assert.Equal (3, Application.MdiChildes.Count); - Assert.True (Application.Current == d2); - Assert.True (Application.Current.Running); - // Trying to close the Dialog1 - d1.RequestStop (); - }; - - // Now this will close the MdiContainer propagating through the MdiChildes. - d1.Closed += (e) => { - Assert.True (Application.Current == d1); - Assert.False (Application.Current.Running); - mdi.RequestStop (); - }; - - Application.Iteration += () => { - if (iterations == 5) { - // The Dialog2 still is the current top and we can't request stop to MdiContainer - // because Dialog2 and Dialog1 must be closed first. - // Dialog2 will be closed in this iteration. - Assert.True (Application.Current == d2); - Assert.False (Application.Current.Running); - Assert.False (d1.Running); - } else if (iterations == 4) { - // Dialog1 will be closed in this iteration. - Assert.True (Application.Current == d1); - Assert.False (Application.Current.Running); - } else { - Assert.Equal (iterations, Application.MdiChildes.Count); - for (int i = 0; i < iterations; i++) { - Assert.Equal ((iterations - i + 1).ToString (), Application.MdiChildes [i].Id); - } - } - iterations--; - }; - - Application.Run (mdi); - - Assert.Empty (Application.MdiChildes); - - Application.Shutdown (); - } - - [Fact] - public void Modal_Toplevel_Can_Open_Another_Not_Modal_Toplevel_But_RequestStop_To_The_Caller_Also_Sets_Current_Running_To_False_Too () - { - Init (); - - var mdi = new Mdi (); - var c1 = new Toplevel (); - var c2 = new Window (); - var c3 = new Window (); - var d1 = new Dialog (); - var c4 = new Toplevel (); - - // MdiChild = c1, c2, c3, c4 = 4 - // d1 = 1 - var iterations = 5; - - mdi.Ready += () => { - Assert.Empty (Application.MdiChildes); - Application.Run (c1); - }; - c1.Ready += () => { - Assert.Single (Application.MdiChildes); - Application.Run (c2); - }; - c2.Ready += () => { - Assert.Equal (2, Application.MdiChildes.Count); - Application.Run (c3); - }; - c3.Ready += () => { - Assert.Equal (3, Application.MdiChildes.Count); - Application.Run (d1); - }; - d1.Ready += () => { - Assert.Equal (3, Application.MdiChildes.Count); - Application.Run (c4); - }; - - c4.Ready += () => { - Assert.Equal (4, Application.MdiChildes.Count); - // Trying to close the Dialog1 - d1.RequestStop (); - }; - - // Now this will close the MdiContainer propagating through the MdiChildes. - d1.Closed += (e) => { - mdi.RequestStop (); - }; - - Application.Iteration += () => { - if (iterations == 5) { - // The Dialog2 still is the current top and we can't request stop to MdiContainer - // because Dialog2 and Dialog1 must be closed first. - // Using request stop here will call the Dialog again without need - Assert.True (Application.Current == d1); - Assert.False (Application.Current.Running); - Assert.True (c4.Running); - } else { - Assert.Equal (iterations, Application.MdiChildes.Count); - for (int i = 0; i < iterations; i++) { - Assert.Equal ((iterations - i + (iterations == 4 && i == 0 ? 2 : 1)).ToString (), - Application.MdiChildes [i].Id); - } - } - iterations--; - }; - - Application.Run (mdi); - - Assert.Empty (Application.MdiChildes); - - Application.Shutdown (); - } - - [Fact] - public void MoveCurrent_Returns_False_If_The_Current_And_Top_Parameter_Are_Both_With_Running_Set_To_False () - { - Init (); - - var mdi = new Mdi (); - var c1 = new Toplevel (); - var c2 = new Window (); - var c3 = new Window (); - - // MdiChild = c1, c2, c3 - var iterations = 3; - - mdi.Ready += () => { - Assert.Empty (Application.MdiChildes); - Application.Run (c1); - }; - c1.Ready += () => { - Assert.Single (Application.MdiChildes); - Application.Run (c2); - }; - c2.Ready += () => { - Assert.Equal (2, Application.MdiChildes.Count); - Application.Run (c3); - }; - c3.Ready += () => { - Assert.Equal (3, Application.MdiChildes.Count); - c3.RequestStop (); - c1.RequestStop (); - }; - // Now this will close the MdiContainer propagating through the MdiChildes. - c1.Closed += (e) => { - mdi.RequestStop (); - }; - Application.Iteration += () => { - if (iterations == 3) { - // The Current still is c3 because Current.Running is false. - Assert.True (Application.Current == c3); - Assert.False (Application.Current.Running); - // But the childes order were reorder by Running = false - Assert.True (Application.MdiChildes [0] == c3); - Assert.True (Application.MdiChildes [1] == c1); - Assert.True (Application.MdiChildes [^1] == c2); - } else if (iterations == 2) { - // The Current is c1 and Current.Running is false. - Assert.True (Application.Current == c1); - Assert.False (Application.Current.Running); - Assert.True (Application.MdiChildes [0] == c1); - Assert.True (Application.MdiChildes [^1] == c2); - } else if (iterations == 1) { - // The Current is c2 and Current.Running is false. - Assert.True (Application.Current == c2); - Assert.False (Application.Current.Running); - Assert.True (Application.MdiChildes [^1] == c2); - } else { - // The Current is mdi. - Assert.True (Application.Current == mdi); - Assert.Empty (Application.MdiChildes); - } - iterations--; - }; - - Application.Run (mdi); - - Assert.Empty (Application.MdiChildes); - - Application.Shutdown (); - } - - [Fact] - public void MdiContainer_Throws_If_More_Than_One () - { - Init (); - - var mdi = new Mdi (); - var mdi2 = new Mdi (); - - mdi.Ready += () => { - Assert.Throws (() => Application.Run (mdi2)); - mdi.RequestStop (); - }; - - Application.Run (mdi); - - Application.Shutdown (); - } - - [Fact] - public void MdiContainer_Open_And_Close_Modal_And_Open_Not_Modal_Toplevels_Randomly () - { - Init (); - - var mdi = new Mdi (); - var logger = new Toplevel (); - - var iterations = 1; // The logger - var running = true; - var stageCompleted = true; - var allStageClosed = false; - var mdiRequestStop = false; - - mdi.Ready += () => { - Assert.Empty (Application.MdiChildes); - Application.Run (logger); - }; - - logger.Ready += () => Assert.Single (Application.MdiChildes); - - Application.Iteration += () => { - if (stageCompleted && running) { - stageCompleted = false; - var stage = new Window () { Modal = true }; - - stage.Ready += () => { - Assert.Equal (iterations, Application.MdiChildes.Count); - stage.RequestStop (); - }; - - stage.Closed += (_) => { - if (iterations == 11) { - allStageClosed = true; - } - Assert.Equal (iterations, Application.MdiChildes.Count); - if (running) { - stageCompleted = true; - - var rpt = new Window (); - - rpt.Ready += () => { - iterations++; - Assert.Equal (iterations, Application.MdiChildes.Count); - }; - - Application.Run (rpt); - } - }; - - Application.Run (stage); - - } else if (iterations == 11 && running) { - running = false; - Assert.Equal (iterations, Application.MdiChildes.Count); - - } else if (!mdiRequestStop && running && !allStageClosed) { - Assert.Equal (iterations, Application.MdiChildes.Count); - - } else if (!mdiRequestStop && !running && allStageClosed) { - Assert.Equal (iterations, Application.MdiChildes.Count); - mdiRequestStop = true; - mdi.RequestStop (); - } else { - Assert.Empty (Application.MdiChildes); - } - }; - - Application.Run (mdi); - - Assert.Empty (Application.MdiChildes); - - Application.Shutdown (); - } - - [Fact] - public void AllChildClosed_Event_Test () - { - Init (); - - var mdi = new Mdi (); - var c1 = new Toplevel (); - var c2 = new Window (); - var c3 = new Window (); - - // MdiChild = c1, c2, c3 - var iterations = 3; - - mdi.Ready += () => { - Assert.Empty (Application.MdiChildes); - Application.Run (c1); - }; - c1.Ready += () => { - Assert.Single (Application.MdiChildes); - Application.Run (c2); - }; - c2.Ready += () => { - Assert.Equal (2, Application.MdiChildes.Count); - Application.Run (c3); - }; - c3.Ready += () => { - Assert.Equal (3, Application.MdiChildes.Count); - c3.RequestStop (); - c2.RequestStop (); - c1.RequestStop (); - }; - // Now this will close the MdiContainer when all MdiChildes was closed - mdi.AllChildClosed += () => { - mdi.RequestStop (); - }; - Application.Iteration += () => { - if (iterations == 3) { - // The Current still is c3 because Current.Running is false. - Assert.True (Application.Current == c3); - Assert.False (Application.Current.Running); - // But the childes order were reorder by Running = false - Assert.True (Application.MdiChildes [0] == c3); - Assert.True (Application.MdiChildes [1] == c2); - Assert.True (Application.MdiChildes [^1] == c1); - } else if (iterations == 2) { - // The Current is c2 and Current.Running is false. - Assert.True (Application.Current == c2); - Assert.False (Application.Current.Running); - Assert.True (Application.MdiChildes [0] == c2); - Assert.True (Application.MdiChildes [^1] == c1); - } else if (iterations == 1) { - // The Current is c1 and Current.Running is false. - Assert.True (Application.Current == c1); - Assert.False (Application.Current.Running); - Assert.True (Application.MdiChildes [^1] == c1); - } else { - // The Current is mdi. - Assert.True (Application.Current == mdi); - Assert.False (Application.Current.Running); - Assert.Empty (Application.MdiChildes); - } - iterations--; - }; - - Application.Run (mdi); - - Assert.Empty (Application.MdiChildes); - - Application.Shutdown (); - } - - [Fact] - public void SetCurrentAsTop_Run_A_Not_Modal_Toplevel_Make_It_The_Current_Application_Top () - { - Init (); - - var t1 = new Toplevel (); - var t2 = new Toplevel (); - var t3 = new Toplevel (); - var d = new Dialog (); - var t4 = new Toplevel (); - - // t1, t2, t3, d, t4 - var iterations = 5; - - t1.Ready += () => { - Assert.Equal (t1, Application.Top); - Application.Run (t2); - }; - t2.Ready += () => { - Assert.Equal (t2, Application.Top); - Application.Run (t3); - }; - t3.Ready += () => { - Assert.Equal (t3, Application.Top); - Application.Run (d); - }; - d.Ready += () => { - Assert.Equal (t3, Application.Top); - Application.Run (t4); - }; - t4.Ready += () => { - Assert.Equal (t4, Application.Top); - t4.RequestStop (); - d.RequestStop (); - t3.RequestStop (); - t2.RequestStop (); - }; - // Now this will close the MdiContainer when all MdiChildes was closed - t2.Closed += (_) => { - t1.RequestStop (); - }; - Application.Iteration += () => { - if (iterations == 5) { - // The Current still is t4 because Current.Running is false. - Assert.Equal (t4, Application.Current); - Assert.False (Application.Current.Running); - Assert.Equal (t4, Application.Top); - } else if (iterations == 4) { - // The Current is d and Current.Running is false. - Assert.Equal (d, Application.Current); - Assert.False (Application.Current.Running); - Assert.Equal (t4, Application.Top); - } else if (iterations == 3) { - // The Current is t3 and Current.Running is false. - Assert.Equal (t3, Application.Current); - Assert.False (Application.Current.Running); - Assert.Equal (t3, Application.Top); - } else if (iterations == 2) { - // The Current is t2 and Current.Running is false. - Assert.Equal (t2, Application.Current); - Assert.False (Application.Current.Running); - Assert.Equal (t2, Application.Top); - } else { - // The Current is t1. - Assert.Equal (t1, Application.Current); - Assert.False (Application.Current.Running); - Assert.Equal (t1, Application.Top); - } - iterations--; - }; - - Application.Run (t1); - - Assert.Equal (t1, Application.Top); - - Application.Shutdown (); - - Assert.Null (Application.Top); - } - - [Fact] - [AutoInitShutdown] - public void Internal_Tests () - { - Assert.True (Application._initialized); - Assert.NotNull (Application.Top); - var rs = Application.Begin (Application.Top); - Assert.Equal (Application.Top, rs.Toplevel); - Assert.Null (Application.MouseGrabView); - Assert.Null (Application.WantContinuousButtonPressedView); - Assert.False (Application.DebugDrawBounds); - Assert.False (Application.ShowChild (Application.Top)); - Application.End (Application.Top); - } - [Fact] [AutoInitShutdown] public void QuitKey_Getter_Setter () @@ -1279,6 +848,8 @@ namespace Terminal.Gui.Core { Assert.Null (Toplevel.dragPosition); } + #endregion + [Fact, AutoInitShutdown] public void GetSupportedCultures_Method () { @@ -1286,129 +857,7 @@ namespace Terminal.Gui.Core { Assert.Equal (cultures.Count, Application.SupportedCultures.Count); } - [Fact, AutoInitShutdown] - public void TestAddManyTimeouts () - { - int delegatesRun = 0; - int numberOfThreads = 100; - int numberOfTimeoutsPerThread = 100; - - - lock (Application.Top) { - // start lots of threads - for (int i = 0; i < numberOfThreads; i++) { - - var myi = i; - - Task.Run (() => { - Thread.Sleep (100); - - // each thread registers lots of 1s timeouts - for (int j = 0; j < numberOfTimeoutsPerThread; j++) { - - Application.MainLoop.AddTimeout (TimeSpan.FromSeconds (1), (s) => { - - // each timeout delegate increments delegatesRun count by 1 every second - Interlocked.Increment (ref delegatesRun); - return true; - }); - } - - // if this is the first Thread created - if (myi == 0) { - - // let the timeouts run for a bit - Thread.Sleep (10000); - - // then tell the application to quit - Application.MainLoop.Invoke (() => Application.RequestStop ()); - } - }); - } - - // blocks here until the RequestStop is processed at the end of the test - Application.Run (); - - // undershoot a bit to be on the safe side. The 5000 ms wait allows the timeouts to run - // a lot but all those timeout delegates could end up going slowly on a slow machine perhaps - // so the final number of delegatesRun might vary by computer. So for this assert we say - // that it should have run at least 2 seconds worth of delegates - Assert.True (delegatesRun >= numberOfThreads * numberOfTimeoutsPerThread * 2); - } - } - - [Fact] - public void SynchronizationContext_Post () - { - Init (); - var context = SynchronizationContext.Current; - - var success = false; - Task.Run (() => { - Thread.Sleep (1_000); - - // non blocking - context.Post ( - delegate (object o) { - success = true; - - // then tell the application to quit - Application.MainLoop.Invoke (() => Application.RequestStop ()); - }, null); - Assert.False (success); - }); - - // blocks here until the RequestStop is processed at the end of the test - Application.Run (); - Assert.True (success); - - Application.Shutdown (); - } - - [Fact] - public void SynchronizationContext_Send () - { - Init (); - var context = SynchronizationContext.Current; - - var success = false; - Task.Run (() => { - Thread.Sleep (1_000); - - // blocking - context.Send ( - delegate (object o) { - success = true; - - // then tell the application to quit - Application.MainLoop.Invoke (() => Application.RequestStop ()); - }, null); - Assert.True (success); - }); - - // blocks here until the RequestStop is processed at the end of the test - Application.Run (); - Assert.True (success); - - Application.Shutdown (); - } - - [Fact] - public void SynchronizationContext_CreateCopy () - { - Init (); - - var context = SynchronizationContext.Current; - Assert.NotNull (context); - - var contextCopy = context.CreateCopy (); - Assert.NotNull (contextCopy); - - Assert.NotEqual (context, contextCopy); - - Application.Shutdown (); - } - + #region mousegrabtests [Fact, AutoInitShutdown] public void MouseGrabView_WithNullMouseEventView () { @@ -1553,5 +1002,6 @@ namespace Terminal.Gui.Core { Application.UnGrabbedMouse -= Application_UnGrabbedMouse; } } + #endregion } } diff --git a/UnitTests/MainLoopTests.cs b/UnitTests/MainLoopTests.cs index 4cf1d5b71..92bd338ae 100644 --- a/UnitTests/MainLoopTests.cs +++ b/UnitTests/MainLoopTests.cs @@ -578,9 +578,9 @@ namespace Terminal.Gui.Core { TextField tf = new (); Application.Top.Add (tf); - const int numPasses = 10; - const int numIncrements = 10000; - const int pollMs = 20000; + const int numPasses = 5; + const int numIncrements = 5000; + const int pollMs = 10000; var task = Task.Run (() => RunTest (r, tf, numPasses, numIncrements, pollMs)); @@ -591,5 +591,176 @@ namespace Terminal.Gui.Core { Assert.Equal ((numIncrements * numPasses), tbCounter); } + + private static int total; + private static Button btn; + private static string clickMe; + private static string cancel; + private static string pewPew; + private static int zero; + private static int one; + private static int two; + private static int three; + private static int four; + private static bool taskCompleted; + + [Theory, AutoInitShutdown] + [MemberData (nameof (TestAddIdle))] + public void Mainloop_Invoke_Or_AddIdle_Can_Be_Used_For_Events_Or_Actions (Action action, string pclickMe, string pcancel, string ppewPew, int pzero, int pone, int ptwo, int pthree, int pfour) + { + total = 0; + btn = null; + clickMe = pclickMe; + cancel = pcancel; + pewPew = ppewPew; + zero = pzero; + one = pone; + two = ptwo; + three = pthree; + four = pfour; + taskCompleted = false; + + var btnLaunch = new Button ("Open Window"); + + btnLaunch.Clicked += () => action (); + + Application.Top.Add (btnLaunch); + + var iterations = -1; + + Application.Iteration += () => { + iterations++; + if (iterations == 0) { + Assert.Null (btn); + Assert.Equal (zero, total); + Assert.True (btnLaunch.ProcessKey (new KeyEvent (Key.Enter, null))); + if (btn == null) { + Assert.Null (btn); + Assert.Equal (zero, total); + } else { + Assert.Equal (clickMe, btn.Text); + Assert.Equal (four, total); + } + } else if (iterations == 1) { + Assert.Equal (clickMe, btn.Text); + Assert.Equal (zero, total); + Assert.True (btn.ProcessKey (new KeyEvent (Key.Enter, null))); + Assert.Equal (cancel, btn.Text); + Assert.Equal (one, total); + } else if (taskCompleted) { + Application.RequestStop (); + } + }; + + Application.Run (); + + Assert.True (taskCompleted); + Assert.Equal (clickMe, btn.Text); + Assert.Equal (four, total); + } + + public static IEnumerable TestAddIdle { + get { + // Goes fine + Action a1 = StartWindow; + yield return new object [] { a1, "Click Me", "Cancel", "Pew Pew", 0, 1, 2, 3, 4 }; + + // Also goes fine + Action a2 = () => Application.MainLoop.Invoke (StartWindow); + yield return new object [] { a2, "Click Me", "Cancel", "Pew Pew", 0, 1, 2, 3, 4 }; + } + } + + private static void StartWindow () + { + var startWindow = new Window { + Modal = true + }; + + btn = new Button { + Text = "Click Me" + }; + + btn.Clicked += RunAsyncTest; + + var totalbtn = new Button () { + X = Pos.Right (btn), + Text = "total" + }; + + totalbtn.Clicked += () => { + MessageBox.Query ("Count", $"Count is {total}", "Ok"); + }; + + startWindow.Add (btn); + startWindow.Add (totalbtn); + + Application.Run (startWindow); + + Assert.Equal (clickMe, btn.Text); + Assert.Equal (four, total); + + Application.RequestStop (); + } + + private static async void RunAsyncTest () + { + Assert.Equal (clickMe, btn.Text); + Assert.Equal (zero, total); + + btn.Text = "Cancel"; + Interlocked.Increment (ref total); + btn.SetNeedsDisplay (); + + await Task.Run (() => { + try { + Assert.Equal (cancel, btn.Text); + Assert.Equal (one, total); + + RunSql (); + } finally { + SetReadyToRun (); + } + }).ContinueWith (async (s, e) => { + + await Task.Delay (1000); + Assert.Equal (clickMe, btn.Text); + Assert.Equal (three, total); + + Interlocked.Increment (ref total); + + Assert.Equal (clickMe, btn.Text); + Assert.Equal (four, total); + + taskCompleted = true; + + }, TaskScheduler.FromCurrentSynchronizationContext ()); + } + + private static void RunSql () + { + Thread.Sleep (100); + Assert.Equal (cancel, btn.Text); + Assert.Equal (one, total); + + Application.MainLoop.Invoke (() => { + btn.Text = "Pew Pew"; + Interlocked.Increment (ref total); + btn.SetNeedsDisplay (); + }); + } + + private static void SetReadyToRun () + { + Thread.Sleep (100); + Assert.Equal (pewPew, btn.Text); + Assert.Equal (two, total); + + Application.MainLoop.Invoke (() => { + btn.Text = "Click Me"; + Interlocked.Increment (ref total); + btn.SetNeedsDisplay (); + }); + } } } diff --git a/UnitTests/MdiTests.cs b/UnitTests/MdiTests.cs new file mode 100644 index 000000000..b509c49d6 --- /dev/null +++ b/UnitTests/MdiTests.cs @@ -0,0 +1,695 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +// Alias Console to MockConsole so we don't accidentally use Console +using Console = Terminal.Gui.FakeConsole; + +namespace Terminal.Gui.Core { + public class MdiTests { + public MdiTests () + { +#if DEBUG_IDISPOSABLE + Responder.Instances.Clear (); + Application.RunState.Instances.Clear (); +#endif + } + + + [Fact] + public void Dispose_Toplevel_IsMdiContainer_False_With_Begin_End () + { + Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); + + var top = new Toplevel (); + var rs = Application.Begin (top); + Application.End (rs); + + Application.Shutdown (); + + Assert.Equal (2, Responder.Instances.Count); + Assert.True (Responder.Instances [0].WasDisposed); + Assert.True (Responder.Instances [1].WasDisposed); + } + + [Fact] + public void Dispose_Toplevel_IsMdiContainer_True_With_Begin () + { + Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); + + var mdi = new Toplevel { IsMdiContainer = true }; + var rs = Application.Begin (mdi); + Application.End (rs); + + Application.Shutdown (); + + Assert.Equal (2, Responder.Instances.Count); + Assert.True (Responder.Instances [0].WasDisposed); + Assert.True (Responder.Instances [1].WasDisposed); + } + + [Fact, AutoInitShutdown] + public void Application_RequestStop_With_Params_On_A_Not_MdiContainer_Always_Use_Application_Current () + { + var top1 = new Toplevel (); + var top2 = new Toplevel (); + var top3 = new Window (); + var top4 = new Window (); + var d = new Dialog (); + + // top1, top2, top3, d1 = 4 + var iterations = 4; + + top1.Ready += () => { + Assert.Null (Application.MdiChildes); + Application.Run (top2); + }; + top2.Ready += () => { + Assert.Null (Application.MdiChildes); + Application.Run (top3); + }; + top3.Ready += () => { + Assert.Null (Application.MdiChildes); + Application.Run (top4); + }; + top4.Ready += () => { + Assert.Null (Application.MdiChildes); + Application.Run (d); + }; + + d.Ready += () => { + Assert.Null (Application.MdiChildes); + // This will close the d because on a not MdiContainer the Application.Current it always used. + Application.RequestStop (top1); + Assert.True (Application.Current == d); + }; + + d.Closed += (e) => Application.RequestStop (top1); + + Application.Iteration += () => { + Assert.Null (Application.MdiChildes); + if (iterations == 4) { + Assert.True (Application.Current == d); + } else if (iterations == 3) { + Assert.True (Application.Current == top4); + } else if (iterations == 2) { + Assert.True (Application.Current == top3); + } else if (iterations == 1) { + Assert.True (Application.Current == top2); + } else { + Assert.True (Application.Current == top1); + } + Application.RequestStop (top1); + iterations--; + }; + + Application.Run (top1); + + Assert.Null (Application.MdiChildes); + } + + class Mdi : Toplevel { + public Mdi () + { + IsMdiContainer = true; + } + } + + [Fact] + [AutoInitShutdown] + public void MdiContainer_With_Toplevel_RequestStop_Balanced () + { + var mdi = new Mdi (); + var c1 = new Toplevel (); + var c2 = new Window (); + var c3 = new Window (); + var d = new Dialog (); + + // MdiChild = c1, c2, c3 + // d1 = 1 + var iterations = 4; + + mdi.Ready += () => { + Assert.Empty (Application.MdiChildes); + Application.Run (c1); + }; + c1.Ready += () => { + Assert.Single (Application.MdiChildes); + Application.Run (c2); + }; + c2.Ready += () => { + Assert.Equal (2, Application.MdiChildes.Count); + Application.Run (c3); + }; + c3.Ready += () => { + Assert.Equal (3, Application.MdiChildes.Count); + Application.Run (d); + }; + + // More easy because the Mdi Container handles all at once + d.Ready += () => { + Assert.Equal (3, Application.MdiChildes.Count); + // This will not close the MdiContainer because d is a modal toplevel and will be closed. + mdi.RequestStop (); + }; + + // Now this will close the MdiContainer propagating through the MdiChildes. + d.Closed += (e) => { + mdi.RequestStop (); + }; + + Application.Iteration += () => { + if (iterations == 4) { + // The Dialog was not closed before and will be closed now. + Assert.True (Application.Current == d); + Assert.False (d.Running); + } else { + Assert.Equal (iterations, Application.MdiChildes.Count); + for (int i = 0; i < iterations; i++) { + Assert.Equal ((iterations - i + 1).ToString (), Application.MdiChildes [i].Id); + } + } + iterations--; + }; + + Application.Run (mdi); + + Assert.Empty (Application.MdiChildes); + } + + [Fact] + [AutoInitShutdown] + public void MdiContainer_With_Application_RequestStop_MdiTop_With_Params () + { + var mdi = new Mdi (); + var c1 = new Toplevel (); + var c2 = new Window (); + var c3 = new Window (); + var d = new Dialog (); + + // MdiChild = c1, c2, c3 + // d1 = 1 + var iterations = 4; + + mdi.Ready += () => { + Assert.Empty (Application.MdiChildes); + Application.Run (c1); + }; + c1.Ready += () => { + Assert.Single (Application.MdiChildes); + Application.Run (c2); + }; + c2.Ready += () => { + Assert.Equal (2, Application.MdiChildes.Count); + Application.Run (c3); + }; + c3.Ready += () => { + Assert.Equal (3, Application.MdiChildes.Count); + Application.Run (d); + }; + + // Also easy because the Mdi Container handles all at once + d.Ready += () => { + Assert.Equal (3, Application.MdiChildes.Count); + // This will not close the MdiContainer because d is a modal toplevel + Application.RequestStop (mdi); + }; + + // Now this will close the MdiContainer propagating through the MdiChildes. + d.Closed += (e) => Application.RequestStop (mdi); + + Application.Iteration += () => { + if (iterations == 4) { + // The Dialog was not closed before and will be closed now. + Assert.True (Application.Current == d); + Assert.False (d.Running); + } else { + Assert.Equal (iterations, Application.MdiChildes.Count); + for (int i = 0; i < iterations; i++) { + Assert.Equal ((iterations - i + 1).ToString (), Application.MdiChildes [i].Id); + } + } + iterations--; + }; + + Application.Run (mdi); + + Assert.Empty (Application.MdiChildes); + } + + [Fact] + [AutoInitShutdown] + public void MdiContainer_With_Application_RequestStop_MdiTop_Without_Params () + { + var mdi = new Mdi (); + var c1 = new Toplevel (); + var c2 = new Window (); + var c3 = new Window (); + var d = new Dialog (); + + // MdiChild = c1, c2, c3 = 3 + // d1 = 1 + var iterations = 4; + + mdi.Ready += () => { + Assert.Empty (Application.MdiChildes); + Application.Run (c1); + }; + c1.Ready += () => { + Assert.Single (Application.MdiChildes); + Application.Run (c2); + }; + c2.Ready += () => { + Assert.Equal (2, Application.MdiChildes.Count); + Application.Run (c3); + }; + c3.Ready += () => { + Assert.Equal (3, Application.MdiChildes.Count); + Application.Run (d); + }; + + //More harder because it's sequential. + d.Ready += () => { + Assert.Equal (3, Application.MdiChildes.Count); + // Close the Dialog + Application.RequestStop (); + }; + + // Now this will close the MdiContainer propagating through the MdiChildes. + d.Closed += (e) => Application.RequestStop (mdi); + + Application.Iteration += () => { + if (iterations == 4) { + // The Dialog still is the current top and we can't request stop to MdiContainer + // because we are not using parameter calls. + Assert.True (Application.Current == d); + Assert.False (d.Running); + } else { + Assert.Equal (iterations, Application.MdiChildes.Count); + for (int i = 0; i < iterations; i++) { + Assert.Equal ((iterations - i + 1).ToString (), Application.MdiChildes [i].Id); + } + } + iterations--; + }; + + Application.Run (mdi); + + Assert.Empty (Application.MdiChildes); + } + + [Fact] + [AutoInitShutdown] + public void IsMdiChild_Testing () + { + var mdi = new Mdi (); + var c1 = new Toplevel (); + var c2 = new Window (); + var c3 = new Window (); + var d = new Dialog (); + + Application.Iteration += () => { + Assert.False (mdi.IsMdiChild); + Assert.True (c1.IsMdiChild); + Assert.True (c2.IsMdiChild); + Assert.True (c3.IsMdiChild); + Assert.False (d.IsMdiChild); + + mdi.RequestStop (); + }; + + Application.Run (mdi); + } + + [Fact] + [AutoInitShutdown] + public void Modal_Toplevel_Can_Open_Another_Modal_Toplevel_But_RequestStop_To_The_Caller_Also_Sets_Current_Running_To_False_Too () + { + var mdi = new Mdi (); + var c1 = new Toplevel (); + var c2 = new Window (); + var c3 = new Window (); + var d1 = new Dialog (); + var d2 = new Dialog (); + + // MdiChild = c1, c2, c3 = 3 + // d1, d2 = 2 + var iterations = 5; + + mdi.Ready += () => { + Assert.Empty (Application.MdiChildes); + Application.Run (c1); + }; + c1.Ready += () => { + Assert.Single (Application.MdiChildes); + Application.Run (c2); + }; + c2.Ready += () => { + Assert.Equal (2, Application.MdiChildes.Count); + Application.Run (c3); + }; + c3.Ready += () => { + Assert.Equal (3, Application.MdiChildes.Count); + Application.Run (d1); + }; + d1.Ready += () => { + Assert.Equal (3, Application.MdiChildes.Count); + Application.Run (d2); + }; + + d2.Ready += () => { + Assert.Equal (3, Application.MdiChildes.Count); + Assert.True (Application.Current == d2); + Assert.True (Application.Current.Running); + // Trying to close the Dialog1 + d1.RequestStop (); + }; + + // Now this will close the MdiContainer propagating through the MdiChildes. + d1.Closed += (e) => { + Assert.True (Application.Current == d1); + Assert.False (Application.Current.Running); + mdi.RequestStop (); + }; + + Application.Iteration += () => { + if (iterations == 5) { + // The Dialog2 still is the current top and we can't request stop to MdiContainer + // because Dialog2 and Dialog1 must be closed first. + // Dialog2 will be closed in this iteration. + Assert.True (Application.Current == d2); + Assert.False (Application.Current.Running); + Assert.False (d1.Running); + } else if (iterations == 4) { + // Dialog1 will be closed in this iteration. + Assert.True (Application.Current == d1); + Assert.False (Application.Current.Running); + } else { + Assert.Equal (iterations, Application.MdiChildes.Count); + for (int i = 0; i < iterations; i++) { + Assert.Equal ((iterations - i + 1).ToString (), Application.MdiChildes [i].Id); + } + } + iterations--; + }; + + Application.Run (mdi); + + Assert.Empty (Application.MdiChildes); + } + + [Fact] + [AutoInitShutdown] + public void Modal_Toplevel_Can_Open_Another_Not_Modal_Toplevel_But_RequestStop_To_The_Caller_Also_Sets_Current_Running_To_False_Too () + { + var mdi = new Mdi (); + var c1 = new Toplevel (); + var c2 = new Window (); + var c3 = new Window (); + var d1 = new Dialog (); + var c4 = new Toplevel (); + + // MdiChild = c1, c2, c3, c4 = 4 + // d1 = 1 + var iterations = 5; + + mdi.Ready += () => { + Assert.Empty (Application.MdiChildes); + Application.Run (c1); + }; + c1.Ready += () => { + Assert.Single (Application.MdiChildes); + Application.Run (c2); + }; + c2.Ready += () => { + Assert.Equal (2, Application.MdiChildes.Count); + Application.Run (c3); + }; + c3.Ready += () => { + Assert.Equal (3, Application.MdiChildes.Count); + Application.Run (d1); + }; + d1.Ready += () => { + Assert.Equal (3, Application.MdiChildes.Count); + Application.Run (c4); + }; + + c4.Ready += () => { + Assert.Equal (4, Application.MdiChildes.Count); + // Trying to close the Dialog1 + d1.RequestStop (); + }; + + // Now this will close the MdiContainer propagating through the MdiChildes. + d1.Closed += (e) => { + mdi.RequestStop (); + }; + + Application.Iteration += () => { + if (iterations == 5) { + // The Dialog2 still is the current top and we can't request stop to MdiContainer + // because Dialog2 and Dialog1 must be closed first. + // Using request stop here will call the Dialog again without need + Assert.True (Application.Current == d1); + Assert.False (Application.Current.Running); + Assert.True (c4.Running); + } else { + Assert.Equal (iterations, Application.MdiChildes.Count); + for (int i = 0; i < iterations; i++) { + Assert.Equal ((iterations - i + (iterations == 4 && i == 0 ? 2 : 1)).ToString (), + Application.MdiChildes [i].Id); + } + } + iterations--; + }; + + Application.Run (mdi); + + Assert.Empty (Application.MdiChildes); + } + + [Fact] + [AutoInitShutdown] + public void MoveCurrent_Returns_False_If_The_Current_And_Top_Parameter_Are_Both_With_Running_Set_To_False () + { + var mdi = new Mdi (); + var c1 = new Toplevel (); + var c2 = new Window (); + var c3 = new Window (); + + // MdiChild = c1, c2, c3 + var iterations = 3; + + mdi.Ready += () => { + Assert.Empty (Application.MdiChildes); + Application.Run (c1); + }; + c1.Ready += () => { + Assert.Single (Application.MdiChildes); + Application.Run (c2); + }; + c2.Ready += () => { + Assert.Equal (2, Application.MdiChildes.Count); + Application.Run (c3); + }; + c3.Ready += () => { + Assert.Equal (3, Application.MdiChildes.Count); + c3.RequestStop (); + c1.RequestStop (); + }; + // Now this will close the MdiContainer propagating through the MdiChildes. + c1.Closed += (e) => { + mdi.RequestStop (); + }; + Application.Iteration += () => { + if (iterations == 3) { + // The Current still is c3 because Current.Running is false. + Assert.True (Application.Current == c3); + Assert.False (Application.Current.Running); + // But the childes order were reorder by Running = false + Assert.True (Application.MdiChildes [0] == c3); + Assert.True (Application.MdiChildes [1] == c1); + Assert.True (Application.MdiChildes [^1] == c2); + } else if (iterations == 2) { + // The Current is c1 and Current.Running is false. + Assert.True (Application.Current == c1); + Assert.False (Application.Current.Running); + Assert.True (Application.MdiChildes [0] == c1); + Assert.True (Application.MdiChildes [^1] == c2); + } else if (iterations == 1) { + // The Current is c2 and Current.Running is false. + Assert.True (Application.Current == c2); + Assert.False (Application.Current.Running); + Assert.True (Application.MdiChildes [^1] == c2); + } else { + // The Current is mdi. + Assert.True (Application.Current == mdi); + Assert.Empty (Application.MdiChildes); + } + iterations--; + }; + + Application.Run (mdi); + + Assert.Empty (Application.MdiChildes); + } + + [Fact] + [AutoInitShutdown] + public void MdiContainer_Throws_If_More_Than_One () + { + var mdi = new Mdi (); + var mdi2 = new Mdi (); + + mdi.Ready += () => { + Assert.Throws (() => Application.Run (mdi2)); + mdi.RequestStop (); + }; + + Application.Run (mdi); + } + + [Fact] + [AutoInitShutdown] + public void MdiContainer_Open_And_Close_Modal_And_Open_Not_Modal_Toplevels_Randomly () + { + var mdi = new Mdi (); + var logger = new Toplevel (); + + var iterations = 1; // The logger + var running = true; + var stageCompleted = true; + var allStageClosed = false; + var mdiRequestStop = false; + + mdi.Ready += () => { + Assert.Empty (Application.MdiChildes); + Application.Run (logger); + }; + + logger.Ready += () => Assert.Single (Application.MdiChildes); + + Application.Iteration += () => { + if (stageCompleted && running) { + stageCompleted = false; + var stage = new Window () { Modal = true }; + + stage.Ready += () => { + Assert.Equal (iterations, Application.MdiChildes.Count); + stage.RequestStop (); + }; + + stage.Closed += (_) => { + if (iterations == 11) { + allStageClosed = true; + } + Assert.Equal (iterations, Application.MdiChildes.Count); + if (running) { + stageCompleted = true; + + var rpt = new Window (); + + rpt.Ready += () => { + iterations++; + Assert.Equal (iterations, Application.MdiChildes.Count); + }; + + Application.Run (rpt); + } + }; + + Application.Run (stage); + + } else if (iterations == 11 && running) { + running = false; + Assert.Equal (iterations, Application.MdiChildes.Count); + + } else if (!mdiRequestStop && running && !allStageClosed) { + Assert.Equal (iterations, Application.MdiChildes.Count); + + } else if (!mdiRequestStop && !running && allStageClosed) { + Assert.Equal (iterations, Application.MdiChildes.Count); + mdiRequestStop = true; + mdi.RequestStop (); + } else { + Assert.Empty (Application.MdiChildes); + } + }; + + Application.Run (mdi); + + Assert.Empty (Application.MdiChildes); + } + + [Fact] + [AutoInitShutdown] + public void AllChildClosed_Event_Test () + { + var mdi = new Mdi (); + var c1 = new Toplevel (); + var c2 = new Window (); + var c3 = new Window (); + + // MdiChild = c1, c2, c3 + var iterations = 3; + + mdi.Ready += () => { + Assert.Empty (Application.MdiChildes); + Application.Run (c1); + }; + c1.Ready += () => { + Assert.Single (Application.MdiChildes); + Application.Run (c2); + }; + c2.Ready += () => { + Assert.Equal (2, Application.MdiChildes.Count); + Application.Run (c3); + }; + c3.Ready += () => { + Assert.Equal (3, Application.MdiChildes.Count); + c3.RequestStop (); + c2.RequestStop (); + c1.RequestStop (); + }; + // Now this will close the MdiContainer when all MdiChildes was closed + mdi.AllChildClosed += () => { + mdi.RequestStop (); + }; + Application.Iteration += () => { + if (iterations == 3) { + // The Current still is c3 because Current.Running is false. + Assert.True (Application.Current == c3); + Assert.False (Application.Current.Running); + // But the childes order were reorder by Running = false + Assert.True (Application.MdiChildes [0] == c3); + Assert.True (Application.MdiChildes [1] == c2); + Assert.True (Application.MdiChildes [^1] == c1); + } else if (iterations == 2) { + // The Current is c2 and Current.Running is false. + Assert.True (Application.Current == c2); + Assert.False (Application.Current.Running); + Assert.True (Application.MdiChildes [0] == c2); + Assert.True (Application.MdiChildes [^1] == c1); + } else if (iterations == 1) { + // The Current is c1 and Current.Running is false. + Assert.True (Application.Current == c1); + Assert.False (Application.Current.Running); + Assert.True (Application.MdiChildes [^1] == c1); + } else { + // The Current is mdi. + Assert.True (Application.Current == mdi); + Assert.False (Application.Current.Running); + Assert.Empty (Application.MdiChildes); + } + iterations--; + }; + + Application.Run (mdi); + + Assert.Empty (Application.MdiChildes); + } + } +} diff --git a/UnitTests/RunStateTests.cs b/UnitTests/RunStateTests.cs new file mode 100644 index 000000000..afe6886df --- /dev/null +++ b/UnitTests/RunStateTests.cs @@ -0,0 +1,105 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +// Alias Console to MockConsole so we don't accidentally use Console +using Console = Terminal.Gui.FakeConsole; + +namespace Terminal.Gui.Core { + /// + /// These tests focus on Application.RunState and the various ways it can be changed. + /// + public class RunStateTests { + public RunStateTests () + { +#if DEBUG_IDISPOSABLE + Responder.Instances.Clear (); + Application.RunState.Instances.Clear (); +#endif + } + + [Fact] + public void New_Creates_RunState () + { + var rs = new Application.RunState (null); + Assert.Null (rs.Toplevel); + + var top = new Toplevel (); + rs = new Application.RunState (top); + Assert.Equal (top, rs.Toplevel); + } + + [Fact] + public void Dispose_Cleans_Up_RunState () + { + var rs = new Application.RunState (null); + Assert.NotNull (rs); + + // Should not throw because Toplevel was null + rs.Dispose (); + Assert.True (rs.WasDisposed); + + var top = new Toplevel (); + rs = new Application.RunState (top); + Assert.NotNull (rs); + + // Should throw because Toplevel was not cleaned up + Assert.Throws (() => rs.Dispose ()); + + rs.Toplevel.Dispose (); + rs.Toplevel = null; + rs.Dispose (); + Assert.True (rs.WasDisposed); + Assert.True (top.WasDisposed); + } + + void Init () + { + Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); + Assert.NotNull (Application.Driver); + Assert.NotNull (Application.MainLoop); + Assert.NotNull (SynchronizationContext.Current); + } + + void Shutdown () + { + Application.Shutdown (); + // Validate there are no outstanding RunState-based instances left + foreach (var inst in Application.RunState.Instances) { + Assert.True (inst.WasDisposed); + } + } + + [Fact] + public void Begin_End_Cleans_Up_RunState () + { + // Setup Mock driver + Init (); + + // Test null Toplevel + Assert.Throws (() => Application.Begin (null)); + + var top = new Toplevel (); + var rs = Application.Begin (top); + Assert.NotNull (rs); + Assert.Equal (top, Application.Current); + Application.End (rs); + + Assert.Null (Application.Current); + Assert.NotNull (Application.Top); + Assert.NotNull (Application.MainLoop); + Assert.NotNull (Application.Driver); + + Shutdown (); + + Assert.True (rs.WasDisposed); + + Assert.Null (Application.Top); + Assert.Null (Application.MainLoop); + Assert.Null (Application.Driver); + } + } +} diff --git a/UnitTests/ScenarioTests.cs b/UnitTests/ScenarioTests.cs index 7a158f76c..f5f1dc57b 100644 --- a/UnitTests/ScenarioTests.cs +++ b/UnitTests/ScenarioTests.cs @@ -64,12 +64,18 @@ namespace UICatalog { Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); // Close after a short period of time - var token = Application.MainLoop.AddTimeout (TimeSpan.FromMilliseconds (200), closeCallback); + var token = Application.MainLoop.AddTimeout (TimeSpan.FromMilliseconds (100), closeCallback); - scenario.Init (Application.Top, Colors.Base); + scenario.Init (Colors.Base); scenario.Setup (); scenario.Run (); Application.Shutdown (); +#if DEBUG_IDISPOSABLE + foreach (var inst in Responder.Instances) { + Assert.True (inst.WasDisposed); + } + Responder.Instances.Clear (); +#endif } #if DEBUG_IDISPOSABLE foreach (var inst in Responder.Instances) { @@ -115,7 +121,7 @@ namespace UICatalog { Assert.Equal (Key.CtrlMask | Key.Q, args.KeyEvent.Key); }; - generic.Init (Application.Top, Colors.Base); + generic.Init (Colors.Base); generic.Setup (); // There is no need to call Application.Begin because Init already creates the Application.Top // If Application.RunState is used then the Application.RunLoop must also be used instead Application.Run. diff --git a/UnitTests/SynchronizatonContextTests.cs b/UnitTests/SynchronizatonContextTests.cs new file mode 100644 index 000000000..fe492c386 --- /dev/null +++ b/UnitTests/SynchronizatonContextTests.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Runtime.InteropServices.ComTypes; +using System.Threading; +using System.Threading.Tasks; +using Terminal.Gui; +using Xunit; +using Xunit.Sdk; + +// Alias Console to MockConsole so we don't accidentally use Console +using Console = Terminal.Gui.FakeConsole; + +namespace Terminal.Gui.Core { + public class SyncrhonizationContextTests { + + [Fact, AutoInitShutdown] + public void SynchronizationContext_Post () + { + var context = SynchronizationContext.Current; + + var success = false; + Task.Run (() => { + Thread.Sleep (1_000); + + // non blocking + context.Post ( + delegate (object o) { + success = true; + + // then tell the application to quit + Application.MainLoop.Invoke (() => Application.RequestStop ()); + }, null); + Assert.False (success); + }); + + // blocks here until the RequestStop is processed at the end of the test + Application.Run (); + Assert.True (success); + } + + [Fact, AutoInitShutdown] + public void SynchronizationContext_Send () + { + var context = SynchronizationContext.Current; + + var success = false; + Task.Run (() => { + Thread.Sleep (1_000); + + // blocking + context.Send ( + delegate (object o) { + success = true; + + // then tell the application to quit + Application.MainLoop.Invoke (() => Application.RequestStop ()); + }, null); + Assert.True (success); + }); + + // blocks here until the RequestStop is processed at the end of the test + Application.Run (); + Assert.True (success); + + } + + [Fact, AutoInitShutdown] + public void SynchronizationContext_CreateCopy () + { + var context = SynchronizationContext.Current; + Assert.NotNull (context); + + var contextCopy = context.CreateCopy (); + Assert.NotNull (contextCopy); + + Assert.NotEqual (context, contextCopy); + } + + } +} diff --git a/UnitTests/TextFormatterTests.cs b/UnitTests/TextFormatterTests.cs index 29018c4ae..b6f4e0f26 100644 --- a/UnitTests/TextFormatterTests.cs +++ b/UnitTests/TextFormatterTests.cs @@ -2841,12 +2841,12 @@ namespace Terminal.Gui.Core { c = new System.Rune (31); Assert.Equal (-1, Rune.ColumnWidth (c)); // non printable character - Assert.Equal (-1, ustring.Make (c).ConsoleWidth); + Assert.Equal (0, ustring.Make (c).ConsoleWidth);// ConsoleWidth only returns zero or greater than zero Assert.Equal (1, ustring.Make (c).Length); c = new System.Rune (127); Assert.Equal (-1, Rune.ColumnWidth (c)); // non printable character - Assert.Equal (-1, ustring.Make (c).ConsoleWidth); + Assert.Equal (0, ustring.Make (c).ConsoleWidth); Assert.Equal (1, ustring.Make (c).Length); } diff --git a/UnitTests/ToplevelTests.cs b/UnitTests/ToplevelTests.cs index 61f39abd8..7e4881a2d 100644 --- a/UnitTests/ToplevelTests.cs +++ b/UnitTests/ToplevelTests.cs @@ -431,6 +431,7 @@ namespace Terminal.Gui.Core { var top = Application.Top; Assert.Null (Application.MdiTop); top.IsMdiContainer = true; + Application.Begin (top); Assert.Equal (Application.Top, Application.MdiTop); var isRunning = true; @@ -469,6 +470,7 @@ namespace Terminal.Gui.Core { Assert.Null (top.MostFocused); Assert.Equal (win1.Subviews [0], win1.Focused); Assert.Equal (tf1W1, win1.MostFocused); + Assert.True (win1.IsMdiChild); Assert.Single (Application.MdiChildes); Application.Begin (win2); Assert.Equal (new Rect (0, 0, 40, 25), win2.Frame); diff --git a/UnitTests/ViewTests.cs b/UnitTests/ViewTests.cs index 03969c4a7..e2a20e80b 100644 --- a/UnitTests/ViewTests.cs +++ b/UnitTests/ViewTests.cs @@ -4062,29 +4062,5 @@ This is a tes Assert.False (view.IsKeyPress); Assert.True (view.IsKeyUp); } - - [Fact, AutoInitShutdown] - public void IsOverridden_False_IfNotOverriden () - { - var view = new DerivedView () { Text = "DerivedView does not override MouseEvent", Width = 10, Height = 10 }; - - Assert.False (View.IsOverridden (view, "MouseEvent")); - - var view2 = new Button () { Text = "Button does not overrides OnKeyDown", Width = 10, Height = 10 }; - - Assert.False (View.IsOverridden (view2, "OnKeyDown")); - } - - [Fact, AutoInitShutdown] - public void IsOverridden_True_IfOverriden () - { - var view = new Button () { Text = "Button overrides MouseEvent", Width = 10, Height = 10 }; - - Assert.True (View.IsOverridden (view, "MouseEvent")); - - var view2 = new DerivedView () { Text = "DerivedView overrides OnKeyDown", Width = 10, Height = 10 }; - - Assert.True (View.IsOverridden (view2, "OnKeyDown")); - } } }