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/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 8ace3d396..8586a288d 100644 --- a/Terminal.Gui/Core/ConsoleDriver.cs +++ b/Terminal.Gui/Core/ConsoleDriver.cs @@ -1386,7 +1386,7 @@ namespace Terminal.Gui { Colors.Error.Normal = MakeColor (Color.Red, Color.White); Colors.Error.Focus = MakeColor (Color.Black, Color.BrightRed); Colors.Error.HotNormal = MakeColor (Color.Black, Color.White); - Colors.Error.HotFocus = MakeColor (Color.BrightRed, Color.Gray); + Colors.Error.HotFocus = MakeColor (Color.White, Color.BrightRed); Colors.Error.Disabled = MakeColor (Color.DarkGray, Color.White); } } 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/Trees/Branch.cs b/Terminal.Gui/Core/Trees/Branch.cs index a6d43cb0b..ce699af6b 100644 --- a/Terminal.Gui/Core/Trees/Branch.cs +++ b/Terminal.Gui/Core/Trees/Branch.cs @@ -5,23 +5,23 @@ using System.Linq; namespace Terminal.Gui.Trees { class Branch where T : class { /// - /// True if the branch is expanded to reveal child branches + /// True if the branch is expanded to reveal child branches. /// public bool IsExpanded { get; set; } /// - /// The users object that is being displayed by this branch of the tree + /// The users object that is being displayed by this branch of the tree. /// public T Model { get; private set; } /// - /// The depth of the current branch. Depth of 0 indicates root level branches + /// The depth of the current branch. Depth of 0 indicates root level branches. /// public int Depth { get; private set; } = 0; /// /// The children of the current branch. This is null until the first call to - /// to avoid enumerating the entire underlying hierarchy + /// to avoid enumerating the entire underlying hierarchy. /// public Dictionary> ChildBranches { get; set; } @@ -34,12 +34,12 @@ namespace Terminal.Gui.Trees { /// /// Declares a new branch of in which the users object - /// is presented + /// is presented. /// - /// The UI control in which the branch resides + /// The UI control in which the branch resides. /// Pass null for root level branches, otherwise - /// pass the parent - /// The user's object that should be displayed + /// pass the parent. + /// The user's object that should be displayed. public Branch (TreeView tree, Branch parentBranchIfAny, T model) { this.tree = tree; @@ -53,7 +53,7 @@ namespace Terminal.Gui.Trees { /// - /// Fetch the children of this branch. This method populates + /// Fetch the children of this branch. This method populates . /// public virtual void FetchChildren () { @@ -80,7 +80,7 @@ namespace Terminal.Gui.Trees { } /// - /// Renders the current on the specified line + /// Renders the current on the specified line . /// /// /// @@ -89,10 +89,10 @@ namespace Terminal.Gui.Trees { public virtual void Draw (ConsoleDriver driver, ColorScheme colorScheme, int y, int availableWidth) { // true if the current line of the tree is the selected one and control has focus - bool isSelected = tree.IsSelected (Model);// && tree.HasFocus; - Attribute lineColor = isSelected ? (tree.HasFocus ? colorScheme.HotFocus : colorScheme.HotNormal) : colorScheme.Normal ; + bool isSelected = tree.IsSelected (Model); - driver.SetAttribute (lineColor); + Attribute textColor = isSelected ? (tree.HasFocus ? colorScheme.Focus : colorScheme.HotNormal) : colorScheme.Normal; + Attribute symbolColor = tree.Style.HighlightModelTextOnly ? colorScheme.Normal : textColor; // Everything on line before the expansion run and branch text Rune [] prefix = GetLinePrefix (driver).ToArray (); @@ -104,7 +104,8 @@ namespace Terminal.Gui.Trees { // if we have scrolled to the right then bits of the prefix will have dispeared off the screen int toSkip = tree.ScrollOffsetHorizontal; - // Draw the line prefix (all paralell lanes or whitespace and an expand/collapse/leaf symbol) + driver.SetAttribute (symbolColor); + // Draw the line prefix (all parallel lanes or whitespace and an expand/collapse/leaf symbol) foreach (Rune r in prefix) { if (toSkip > 0) { @@ -117,12 +118,16 @@ namespace Terminal.Gui.Trees { // pick color for expanded symbol if (tree.Style.ColorExpandSymbol || tree.Style.InvertExpandSymbolColors) { - Attribute color; + Attribute color = symbolColor; if (tree.Style.ColorExpandSymbol) { - color = isSelected ? tree.ColorScheme.HotFocus : tree.ColorScheme.HotNormal; + if (isSelected) { + color = tree.Style.HighlightModelTextOnly ? colorScheme.HotNormal : (tree.HasFocus ? tree.ColorScheme.HotFocus : tree.ColorScheme.HotNormal); + } else { + color = tree.ColorScheme.HotNormal; + } } else { - color = lineColor; + color = symbolColor; } if (tree.Style.InvertExpandSymbolColors) { @@ -162,16 +167,14 @@ namespace Terminal.Gui.Trees { // default behaviour is for model to use the color scheme // of the tree view - var modelColor = lineColor; + var modelColor = textColor; // if custom color delegate invoke it - if(tree.ColorGetter != null) - { - var modelScheme = tree.ColorGetter(Model); + if (tree.ColorGetter != null) { + var modelScheme = tree.ColorGetter (Model); // if custom color scheme is defined for this Model - if(modelScheme != null) - { + if (modelScheme != null) { // use it modelColor = isSelected ? modelScheme.Focus : modelScheme.Normal; } @@ -179,24 +182,23 @@ namespace Terminal.Gui.Trees { driver.SetAttribute (modelColor); driver.AddStr (lineBody); - driver.SetAttribute (lineColor); if (availableWidth > 0) { + driver.SetAttribute (symbolColor); driver.AddStr (new string (' ', availableWidth)); } - driver.SetAttribute (colorScheme.Normal); } /// /// Gets all characters to render prior to the current branches line. This includes indentation - /// whitespace and any tree branches (if enabled) + /// whitespace and any tree branches (if enabled). /// /// /// private IEnumerable GetLinePrefix (ConsoleDriver driver) { - // If not showing line branches or this is a root object + // If not showing line branches or this is a root object. if (!tree.Style.ShowBranchLines) { for (int i = 0; i < Depth; i++) { yield return new Rune (' '); @@ -224,7 +226,7 @@ namespace Terminal.Gui.Trees { } /// - /// Returns all parents starting with the immediate parent and ending at the root + /// Returns all parents starting with the immediate parent and ending at the root. /// /// private IEnumerable> GetParentBranches () @@ -240,7 +242,7 @@ namespace Terminal.Gui.Trees { /// /// Returns an appropriate symbol for displaying next to the string representation of /// the object to indicate whether it or - /// not (or it is a leaf) + /// not (or it is a leaf). /// /// /// @@ -261,7 +263,7 @@ namespace Terminal.Gui.Trees { /// /// Returns true if the current branch can be expanded according to - /// the or cached children already fetched + /// the or cached children already fetched. /// /// public bool CanExpand () @@ -283,7 +285,7 @@ namespace Terminal.Gui.Trees { } /// - /// Expands the current branch if possible + /// Expands the current branch if possible. /// public void Expand () { @@ -297,7 +299,7 @@ namespace Terminal.Gui.Trees { } /// - /// Marks the branch as collapsed ( false) + /// Marks the branch as collapsed ( false). /// public void Collapse () { @@ -305,10 +307,10 @@ namespace Terminal.Gui.Trees { } /// - /// Refreshes cached knowledge in this branch e.g. what children an object has + /// Refreshes cached knowledge in this branch e.g. what children an object has. /// /// True to also refresh all - /// branches (starting with the root) + /// branches (starting with the root). public void Refresh (bool startAtTop) { // if we must go up and refresh from the top down @@ -351,7 +353,7 @@ namespace Terminal.Gui.Trees { } /// - /// Calls on the current branch and all expanded children + /// Calls on the current branch and all expanded children. /// internal void Rebuild () { @@ -375,7 +377,7 @@ namespace Terminal.Gui.Trees { /// /// Returns true if this branch has parents and it is the last node of it's parents - /// branches (or last root of the tree) + /// branches (or last root of the tree). /// /// private bool IsLast () @@ -389,7 +391,7 @@ namespace Terminal.Gui.Trees { /// /// Returns true if the given x offset on the branch line is the +/- symbol. Returns - /// false if not showing expansion symbols or leaf node etc + /// false if not showing expansion symbols or leaf node etc. /// /// /// @@ -415,10 +417,10 @@ namespace Terminal.Gui.Trees { } /// - /// Expands the current branch and all children branches + /// Expands the current branch and all children branches. /// internal void ExpandAll () - { + { Expand (); if (ChildBranches != null) { @@ -430,7 +432,7 @@ namespace Terminal.Gui.Trees { /// /// Collapses the current branch and all children branches (even though those branches are - /// no longer visible they retain collapse/expansion state) + /// no longer visible they retain collapse/expansion state). /// internal void CollapseAll () { diff --git a/Terminal.Gui/Core/Trees/TreeStyle.cs b/Terminal.Gui/Core/Trees/TreeStyle.cs index f6cc30e4c..744ed6974 100644 --- a/Terminal.Gui/Core/Trees/TreeStyle.cs +++ b/Terminal.Gui/Core/Trees/TreeStyle.cs @@ -2,46 +2,51 @@ namespace Terminal.Gui.Trees { /// - /// Defines rendering options that affect how the tree is displayed + /// Defines rendering options that affect how the tree is displayed. /// public class TreeStyle { /// - /// True to render vertical lines under expanded nodes to show which node belongs to which - /// parent. False to use only whitespace + /// to render vertical lines under expanded nodes to show which node belongs to which + /// parent. to use only whitespace. /// /// public bool ShowBranchLines { get; set; } = true; /// - /// Symbol to use for branch nodes that can be expanded to indicate this to the user. - /// Defaults to '+'. Set to null to hide + /// Symbol to use for branch nodes that can be expanded to indicate this to the user. + /// Defaults to '+'. Set to null to hide. /// public Rune? ExpandableSymbol { get; set; } = '+'; /// /// Symbol to use for branch nodes that can be collapsed (are currently expanded). - /// Defaults to '-'. Set to null to hide + /// Defaults to '-'. Set to null to hide. /// public Rune? CollapseableSymbol { get; set; } = '-'; /// - /// Set to true to highlight expand/collapse symbols in hot key color + /// Set to to highlight expand/collapse symbols in hot key color. /// public bool ColorExpandSymbol { get; set; } /// - /// Invert console colours used to render the expand symbol + /// Invert console colours used to render the expand symbol. /// public bool InvertExpandSymbolColors { get; set; } /// - /// True to leave the last row of the control free for overwritting (e.g. by a scrollbar) - /// When True scrolling will be triggered on the second last row of the control rather than + /// to leave the last row of the control free for overwritting (e.g. by a scrollbar) + /// When scrolling will be triggered on the second last row of the control rather than. /// the last. /// /// public bool LeaveLastRow { get; set; } + /// + /// Set to to cause the selected item to be rendered with only the text + /// to be highlighted. If (the default), the entire row will be highlighted. + /// + public bool HighlightModelTextOnly { get; set; } = false; } } \ No newline at end of file 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/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/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/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index edbdb8bd3..0c6bf5a24 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -1,28 +1,4 @@ -// // TextView.cs: multi-line text editing -// -// Authors: -// Miguel de Icaza (miguel@gnome.org) -// -// -// TODO: -// In ReadOnly mode backspace/space behave like pageup/pagedown -// Attributed text on spans -// Replace insertion with Insert method -// String accumulation (Control-k, control-k is not preserving the last new line, see StringToRunes -// Alt-D, Alt-Backspace -// API to set the cursor position -// API to scroll to a particular place -// keybindings to go to top/bottom -// public API to insert, remove ranges -// Add word forward/word backwards commands -// Save buffer API -// Mouse -// -// Desirable: -// Move all the text manipulation into the TextModel - - using System; using System.Collections.Generic; using System.Globalization; @@ -33,6 +9,7 @@ using System.Text; using System.Threading; using NStack; using Terminal.Gui.Resources; +using static Terminal.Gui.Graphs.PathAnnotation; using Rune = System.Rune; namespace Terminal.Gui { @@ -742,6 +719,7 @@ namespace Terminal.Gui { historyTextItems.Clear (); idxHistoryText = -1; originalText = text; + OnChangeText (null); } public bool IsDirty (ustring text) @@ -1037,120 +1015,119 @@ namespace Terminal.Gui { } /// - /// Multi-line text editing + /// Multi-line text editing . /// /// - /// - /// provides a multi-line text editor. Users interact - /// with it with the standard Emacs commands for movement or the arrow - /// keys. - /// - /// - /// - /// Shortcut - /// Action performed - /// - /// - /// Left cursor, Control-b - /// - /// Moves the editing point left. - /// - /// - /// - /// Right cursor, Control-f - /// - /// Moves the editing point right. - /// - /// - /// - /// Alt-b - /// - /// Moves one word back. - /// - /// - /// - /// Alt-f - /// - /// Moves one word forward. - /// - /// - /// - /// Up cursor, Control-p - /// - /// Moves the editing point one line up. - /// - /// - /// - /// Down cursor, Control-n - /// - /// Moves the editing point one line down - /// - /// - /// - /// Home key, Control-a - /// - /// Moves the cursor to the beginning of the line. - /// - /// - /// - /// End key, Control-e - /// - /// Moves the cursor to the end of the line. - /// - /// - /// - /// Control-Home - /// - /// Scrolls to the first line and moves the cursor there. - /// - /// - /// - /// Control-End - /// - /// Scrolls to the last line and moves the cursor there. - /// - /// - /// - /// Delete, Control-d - /// - /// Deletes the character in front of the cursor. - /// - /// - /// - /// Backspace - /// - /// Deletes the character behind the cursor. - /// - /// - /// - /// Control-k - /// - /// Deletes the text until the end of the line and replaces the kill buffer - /// with the deleted text. You can paste this text in a different place by - /// using Control-y. - /// - /// - /// - /// Control-y - /// - /// Pastes the content of the kill ring into the current position. - /// - /// - /// - /// Alt-d - /// - /// Deletes the word above the cursor and adds it to the kill ring. You - /// can paste the contents of the kill ring with Control-y. - /// - /// - /// - /// Control-q - /// - /// Quotes the next input character, to prevent the normal processing of - /// key handling to take place. - /// - /// - /// + /// + /// provides a multi-line text editor. Users interact + /// with it with the standard Windows, Mac, and Linux (Emacs) commands. + /// + /// + /// + /// Shortcut + /// Action performed + /// + /// + /// Left cursor, Control-b + /// + /// Moves the editing point left. + /// + /// + /// + /// Right cursor, Control-f + /// + /// Moves the editing point right. + /// + /// + /// + /// Alt-b + /// + /// Moves one word back. + /// + /// + /// + /// Alt-f + /// + /// Moves one word forward. + /// + /// + /// + /// Up cursor, Control-p + /// + /// Moves the editing point one line up. + /// + /// + /// + /// Down cursor, Control-n + /// + /// Moves the editing point one line down + /// + /// + /// + /// Home key, Control-a + /// + /// Moves the cursor to the beginning of the line. + /// + /// + /// + /// End key, Control-e + /// + /// Moves the cursor to the end of the line. + /// + /// + /// + /// Control-Home + /// + /// Scrolls to the first line and moves the cursor there. + /// + /// + /// + /// Control-End + /// + /// Scrolls to the last line and moves the cursor there. + /// + /// + /// + /// Delete, Control-d + /// + /// Deletes the character in front of the cursor. + /// + /// + /// + /// Backspace + /// + /// Deletes the character behind the cursor. + /// + /// + /// + /// Control-k + /// + /// Deletes the text until the end of the line and replaces the kill buffer + /// with the deleted text. You can paste this text in a different place by + /// using Control-y. + /// + /// + /// + /// Control-y + /// + /// Pastes the content of the kill ring into the current position. + /// + /// + /// + /// Alt-d + /// + /// Deletes the word above the cursor and adds it to the kill ring. You + /// can paste the contents of the kill ring with Control-y. + /// + /// + /// + /// Control-q + /// + /// Quotes the next input character, to prevent the normal processing of + /// key handling to take place. + /// + /// + /// /// public class TextView : View { TextModel model = new TextModel (); @@ -1172,10 +1149,24 @@ namespace Terminal.Gui { CultureInfo currentCulture; /// - /// Raised when the of the changes. + /// Raised when the property of the changes. /// + /// + /// The property of only changes when it is explicitly + /// set, not as the user types. To be notified as the user changes the contents of the TextView + /// see . + /// public event Action TextChanged; + /// + /// Raised when the contents of the are changed. + /// + /// + /// Unlike the event, this event is raised whenever the user types or + /// otherwise changes the contents of the . + /// + public Action ContentsChanged; + /// /// Invoked with the unwrapped . /// @@ -1183,22 +1174,12 @@ namespace Terminal.Gui { /// /// Provides autocomplete context menu based on suggestions at the current cursor - /// position. Populate to enable this feature + /// position. Populate to enable this feature /// public IAutocomplete Autocomplete { get; protected set; } = new TextViewAutocomplete (); -#if false /// - /// Changed event, raised when the text has clicked. - /// - /// - /// Client code can hook up to this event, it is - /// raised when the text in the entry changes. - /// - public Action Changed; -#endif - /// - /// Initializes a on the specified area, with absolute position and size. + /// Initializes a on the specified area, with absolute position and size. /// /// /// @@ -1208,8 +1189,8 @@ namespace Terminal.Gui { } /// - /// Initializes a on the specified area, - /// with dimensions controlled with the X, Y, Width and Height properties. + /// Initializes a on the specified area, + /// with dimensions controlled with the X, Y, Width and Height properties. /// public TextView () : base () { @@ -1404,48 +1385,56 @@ namespace Terminal.Gui { private void Model_LinesLoaded () { - historyText.Clear (Text); + // This call is not needed. Model_LinesLoaded gets invoked when + // model.LoadString (value) is called. LoadString is called from one place + // (Text.set) and historyText.Clear() is called immediately after. + // If this call happens, HistoryText_ChangeText will get called multiple times + // when Text is set, which is wrong. + //historyText.Clear (Text); } private void HistoryText_ChangeText (HistoryText.HistoryTextItem obj) { SetWrapModel (); - var startLine = obj.CursorPosition.Y; + if (obj != null) { + var startLine = obj.CursorPosition.Y; - if (obj.RemovedOnAdded != null) { - int offset; - if (obj.IsUndoing) { - offset = Math.Max (obj.RemovedOnAdded.Lines.Count - obj.Lines.Count, 1); - } else { - offset = obj.RemovedOnAdded.Lines.Count - 1; - } - for (int i = 0; i < offset; i++) { - if (Lines > obj.RemovedOnAdded.CursorPosition.Y) { - model.RemoveLine (obj.RemovedOnAdded.CursorPosition.Y); + if (obj.RemovedOnAdded != null) { + int offset; + if (obj.IsUndoing) { + offset = Math.Max (obj.RemovedOnAdded.Lines.Count - obj.Lines.Count, 1); } else { - break; + offset = obj.RemovedOnAdded.Lines.Count - 1; + } + for (int i = 0; i < offset; i++) { + if (Lines > obj.RemovedOnAdded.CursorPosition.Y) { + model.RemoveLine (obj.RemovedOnAdded.CursorPosition.Y); + } else { + break; + } } } - } - for (int i = 0; i < obj.Lines.Count; i++) { - if (i == 0) { - model.ReplaceLine (startLine, obj.Lines [i]); - } else if ((obj.IsUndoing && obj.LineStatus == HistoryText.LineStatus.Removed) - || !obj.IsUndoing && obj.LineStatus == HistoryText.LineStatus.Added) { - model.AddLine (startLine, obj.Lines [i]); - } else if (Lines > obj.CursorPosition.Y + 1) { - model.RemoveLine (obj.CursorPosition.Y + 1); + for (int i = 0; i < obj.Lines.Count; i++) { + if (i == 0) { + model.ReplaceLine (startLine, obj.Lines [i]); + } else if ((obj.IsUndoing && obj.LineStatus == HistoryText.LineStatus.Removed) + || !obj.IsUndoing && obj.LineStatus == HistoryText.LineStatus.Added) { + model.AddLine (startLine, obj.Lines [i]); + } else if (Lines > obj.CursorPosition.Y + 1) { + model.RemoveLine (obj.CursorPosition.Y + 1); + } + startLine++; } - startLine++; - } - CursorPosition = obj.FinalCursorPosition; + CursorPosition = obj.FinalCursorPosition; + } UpdateWrapModel (); - + Adjust (); + OnContentsChanged (); } void TextView_Initialized (object sender, EventArgs e) @@ -1454,6 +1443,7 @@ namespace Terminal.Gui { Application.Top.AlternateForwardKeyChanged += Top_AlternateForwardKeyChanged; Application.Top.AlternateBackwardKeyChanged += Top_AlternateBackwardKeyChanged; + OnContentsChanged (); } void Top_AlternateBackwardKeyChanged (Key obj) @@ -1480,9 +1470,11 @@ namespace Terminal.Gui { } /// - /// Sets or gets the text in the . + /// Sets or gets the text in the . /// /// + /// The event is fired whenever this property is set. Note, however, + /// that Text is not set by as the user types. /// public override ustring Text { get { @@ -1559,12 +1551,12 @@ namespace Terminal.Gui { public int Maxlength => model.GetMaxVisibleLine (topRow, topRow + Frame.Height, TabWidth); /// - /// Gets the number of lines. + /// Gets the number of lines. /// public int Lines => model.Count; /// - /// Sets or gets the current cursor position. + /// Sets or gets the current cursor position. /// public Point CursorPosition { get => new Point (currentColumn, currentRow); @@ -1828,7 +1820,7 @@ namespace Terminal.Gui { } /// - /// Loads the contents of the file into the . + /// Loads the contents of the file into the . /// /// true, if file was loaded, false otherwise. /// Path to the file to load. @@ -1845,12 +1837,13 @@ namespace Terminal.Gui { UpdateWrapModel (); SetNeedsDisplay (); Adjust (); + OnContentsChanged (); } return res; } /// - /// Loads the contents of the stream into the . + /// Loads the contents of the stream into the . /// /// true, if stream was loaded, false otherwise. /// Stream to load the contents from. @@ -1859,10 +1852,11 @@ namespace Terminal.Gui { model.LoadStream (stream); ResetPosition (); SetNeedsDisplay (); + OnContentsChanged (); } /// - /// Closes the contents of the stream into the . + /// Closes the contents of the stream into the . /// /// true, if stream was closed, false otherwise. public bool CloseFile () @@ -1874,7 +1868,7 @@ namespace Terminal.Gui { } /// - /// Gets the current cursor row. + /// Gets the current cursor row. /// public int CurrentRow => currentRow; @@ -1885,7 +1879,7 @@ namespace Terminal.Gui { public int CurrentColumn => currentColumn; /// - /// Positions the cursor on the current row and column + /// Positions the cursor on the current row and column /// public override void PositionCursor () { @@ -1936,7 +1930,7 @@ namespace Terminal.Gui { } /// - /// Sets the driver to the default color for the control where no text is being rendered. Defaults to . + /// Sets the driver to the default color for the control where no text is being rendered. Defaults to . /// protected virtual void SetNormalColor () { @@ -1945,7 +1939,7 @@ namespace Terminal.Gui { /// /// Sets the to an appropriate color for rendering the given of the - /// current . Override to provide custom coloring by calling + /// current . Override to provide custom coloring by calling /// Defaults to . /// /// @@ -1957,7 +1951,7 @@ namespace Terminal.Gui { /// /// Sets the to an appropriate color for rendering the given of the - /// current . Override to provide custom coloring by calling + /// current . Override to provide custom coloring by calling /// Defaults to . /// /// @@ -1969,7 +1963,7 @@ namespace Terminal.Gui { /// /// Sets the to an appropriate color for rendering the given of the - /// current . Override to provide custom coloring by calling + /// current . Override to provide custom coloring by calling /// Defaults to . /// /// @@ -1987,7 +1981,7 @@ namespace Terminal.Gui { /// /// Sets the to an appropriate color for rendering the given of the - /// current . Override to provide custom coloring by calling + /// current . Override to provide custom coloring by calling /// Defaults to . /// /// @@ -2000,7 +1994,7 @@ namespace Terminal.Gui { bool isReadOnly = false; /// - /// Gets or sets whether the is in read-only mode or not + /// Gets or sets whether the is in read-only mode or not /// /// Boolean value(Default false) public bool ReadOnly { @@ -2504,6 +2498,12 @@ namespace Terminal.Gui { InsertText (new KeyEvent () { Key = key }); } + + if (NeedDisplay.IsEmpty) { + PositionCursor (); + } else { + Adjust (); + } } void Insert (Rune rune) @@ -2521,6 +2521,7 @@ namespace Terminal.Gui { if (!wrapNeeded) { SetNeedsDisplay (new Rect (0, prow, Math.Max (Frame.Width, 0), Math.Max (prow + 1, 0))); } + } ustring StringFromRunes (List runes) @@ -2584,6 +2585,8 @@ namespace Terminal.Gui { UpdateWrapModel (); + OnContentsChanged (); + return; } @@ -2690,6 +2693,42 @@ namespace Terminal.Gui { OnUnwrappedCursorPosition (); } + /// + /// Event arguments for events for when the contents of the TextView change. E.g. the event. + /// + public class ContentsChangedEventArgs : EventArgs { + /// + /// Creates a new instance. + /// + /// Contains the row where the change occurred. + /// Contains the column where the change occured. + public ContentsChangedEventArgs (int currentRow, int currentColumn) + { + Row = currentRow; + Col = currentColumn; + } + + /// + /// + /// Contains the row where the change occurred. + /// + public int Row { get; private set; } + + /// + /// Contains the column where the change occurred. + /// + public int Col { get; private set; } + } + + /// + /// Called when the contents of the TextView change. E.g. when the user types text or deletes text. Raises + /// the event. + /// + public virtual void OnContentsChanged () + { + ContentsChanged?.Invoke (new ContentsChangedEventArgs (CurrentRow, CurrentColumn)); + } + (int width, int height) OffSetBackground () { int w = 0; @@ -2708,7 +2747,7 @@ namespace Terminal.Gui { /// will scroll the to display the specified column at the left if is false. /// /// Row that should be displayed at the top or Column that should be displayed at the left, - /// if the value is negative it will be reset to zero + /// if the value is negative it will be reset to zero /// If true (default) the is a row, column otherwise. public void ScrollTo (int idx, bool isRow = true) { @@ -3178,6 +3217,7 @@ namespace Terminal.Gui { UpdateWrapModel (); DoNeededAction (); + OnContentsChanged (); return true; } @@ -3674,6 +3714,7 @@ namespace Terminal.Gui { HistoryText.LineStatus.Replaced); UpdateWrapModel (); + OnContentsChanged (); return true; } @@ -3883,6 +3924,7 @@ namespace Terminal.Gui { UpdateWrapModel (); selecting = false; DoNeededAction (); + OnContentsChanged (); } /// @@ -3913,6 +3955,7 @@ namespace Terminal.Gui { historyText.Add (new List> () { new List (GetCurrentLine ()) }, CursorPosition, HistoryText.LineStatus.Replaced); + OnContentsChanged (); } else { if (selecting) { ClearRegion (); @@ -4423,6 +4466,7 @@ namespace Terminal.Gui { } } + /// /// Renders an overlay on another view at a given point that allows selecting /// from a range of 'autocomplete' options. diff --git a/Terminal.Gui/Views/TreeView.cs b/Terminal.Gui/Views/TreeView.cs index baab64642..7b8942c39 100644 --- a/Terminal.Gui/Views/TreeView.cs +++ b/Terminal.Gui/Views/TreeView.cs @@ -689,8 +689,10 @@ namespace Terminal.Gui { /// public void ScrollDown () { - ScrollOffsetVertical++; - SetNeedsDisplay (); + if (ScrollOffsetVertical <= ContentHeight - 2) { + ScrollOffsetVertical++; + SetNeedsDisplay (); + } } /// @@ -698,8 +700,10 @@ namespace Terminal.Gui { /// public void ScrollUp () { - ScrollOffsetVertical--; - SetNeedsDisplay (); + if (scrollOffsetVertical > 0) { + ScrollOffsetVertical--; + SetNeedsDisplay (); + } } /// diff --git a/Terminal.Gui/Windows/FileDialog.cs b/Terminal.Gui/Windows/FileDialog.cs index 45727532c..53e37558e 100644 --- a/Terminal.Gui/Windows/FileDialog.cs +++ b/Terminal.Gui/Windows/FileDialog.cs @@ -41,7 +41,7 @@ namespace Terminal.Gui { if (allowedFileTypes == null) return true; foreach (var ft in allowedFileTypes) - if (fsi.Name.EndsWith (ft) || ft == ".*") + if (fsi.Name.EndsWith (ft, StringComparison.InvariantCultureIgnoreCase) || ft == ".*") return true; return false; } 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 a519b2cb4..ecd1a78c7 100644 --- a/UICatalog/Properties/launchSettings.json +++ b/UICatalog/Properties/launchSettings.json @@ -48,6 +48,14 @@ "WSL": { "commandName": "WSL2", "distributionName": "" + }, + "All Views Tester": { + "commandName": "Project", + "commandLineArgs": "\"All Views Tester\"" + }, + "Windows & FrameViews": { + "commandName": "Project", + "commandLineArgs": "\"Windows & FrameViews\"" } } } \ No newline at end of file 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 667e0aafa..8042022ee 100644 --- a/UICatalog/Scenarios/CharacterMap.cs +++ b/UICatalog/Scenarios/CharacterMap.cs @@ -1,12 +1,14 @@ #define DRAW_CONTENT //#define BASE_DRAW_CONTENT +using Microsoft.VisualBasic; using NStack; using System; using System.Collections.Generic; using System.Linq; using System.Text; using Terminal.Gui; +using Terminal.Gui.Resources; using Rune = System.Rune; namespace UICatalog.Scenarios { @@ -31,51 +33,67 @@ namespace UICatalog.Scenarios { Height = Dim.Fill (), }; - var radioItems = new (ustring radioLabel, int start, int end) [] { - 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("Letter-like Symbols", 0x2100, 0x214F), - CreateRadio("Arrows", 0x2190, 0x21ff), - CreateRadio("Mathematical symbols", 0x2200, 0x22ff), - CreateRadio("Miscellaneous Technical", 0x2300, 0x23ff), - CreateRadio("Box Drawing & Geometric Shapes", 0x2500, 0x25ff), - CreateRadio("Miscellaneous Symbols", 0x2600, 0x26ff), - CreateRadio("Dingbats", 0x2700, 0x27ff), - CreateRadio("Braille", 0x2800, 0x28ff), - 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), + var jumpLabel = new Label ("Jump To Glyph:") { X = Pos.Right (_charMap) + 1, Y = Pos.Y (_charMap) }; + Win.Add (jumpLabel); + var jumpEdit = new TextField () { X = Pos.Right (jumpLabel) + 1, Y = Pos.Y (_charMap), Width = 10, }; + Win.Add (jumpEdit); + var unicodeLabel = new Label ("") { X = Pos.Right (jumpEdit) + 1, Y = Pos.Y (_charMap) }; + Win.Add (unicodeLabel); + jumpEdit.TextChanged += (s) => { + uint result = 0; + if (jumpEdit.Text.Length == 0) return; + try { + result = Convert.ToUInt32 (jumpEdit.Text.ToString (), 10); + } catch (OverflowException) { + unicodeLabel.Text = $"Invalid (overflow)"; + return; + } catch (FormatException) { + try { + result = Convert.ToUInt32 (jumpEdit.Text.ToString (), 16); + } catch (OverflowException) { + unicodeLabel.Text = $"Invalid (overflow)"; + return; + } catch (FormatException) { + unicodeLabel.Text = $"Invalid (can't parse)"; + return; + } + } + unicodeLabel.Text = $"U+{result:x4}"; + _charMap.SelectedGlyph = result; }; - (ustring radioLabel, int start, int end) CreateRadio (ustring title, int start, int end) + + var radioItems = new (ustring radioLabel, uint start, uint end) [UnicodeRange.Ranges.Count]; + + for (var i = 0; i < UnicodeRange.Ranges.Count; i++) { + var range = UnicodeRange.Ranges [i]; + radioItems [i] = CreateRadio (range.Category, range.Start, range.End); + } + (ustring radioLabel, uint start, uint end) CreateRadio (ustring title, uint start, uint 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) }; + var label = new Label ("Jump To Unicode Block:") { X = Pos.Right (_charMap) + 1, Y = Pos.Bottom (jumpLabel) + 1 }; Win.Add (label); - var jumpList = new RadioGroup (radioItems.Select (t => t.radioLabel).ToArray ()) { - X = Pos.X (label), + var jumpList = new ListView (radioItems.Select (t => t.radioLabel).ToArray ()) { + X = Pos.X (label) + 1, Y = Pos.Bottom (label), - Width = radioItems.Max (r => r.radioLabel.Length) + 3, - SelectedItem = 8 + Width = radioItems.Max (r => r.radioLabel.Length) + 2, + Height = Dim.Fill(1), + SelectedItem = 0 }; jumpList.SelectedItemChanged += (args) => { - _charMap.Start = radioItems [args.SelectedItem].start; + _charMap.StartGlyph = radioItems [jumpList.SelectedItem].start; }; Win.Add (jumpList); - jumpList.Refresh (); - jumpList.SetFocus (); + //jumpList.Refresh (); + _charMap.SetFocus (); _charMap.Width = Dim.Fill () - jumpList.Width; - } } @@ -85,23 +103,50 @@ namespace UICatalog.Scenarios { /// Specifies the starting offset for the character map. The default is 0x2500 /// which is the Box Drawing characters. /// - public int Start { + public uint StartGlyph { get => _start; set { _start = value; - ContentOffset = new Point (0, _start / 16); + _selected = value; + ContentOffset = new Point (0, (int)(_start / 16)); SetNeedsDisplay (); } } - int _start = 0x2500; + /// + /// Specifies the starting offset for the character map. The default is 0x2500 + /// which is the Box Drawing characters. + /// + public uint SelectedGlyph { + get => _selected; + set { + _selected = value; + int row = (int)_selected / 16; + int height = (Bounds.Height / ROW_HEIGHT) - 1; + if (row + ContentOffset.Y < 0) { + // Moving up. + ContentOffset = new Point (0, row); + } else if (row + ContentOffset.Y >= height) { + // Moving down. + ContentOffset = new Point (0, Math.Min (row, (row - height) + 1)); + + } else { + //ContentOffset = new Point (0, Math.Min (row, (row - height) - 1)); + } + + SetNeedsDisplay (); + } + } + + uint _start = 0; + uint _selected = 0; public const int COLUMN_WIDTH = 3; public const int ROW_HEIGHT = 1; - public static int MaxCodePointVal => 0x10FFFF; + public static uint MaxCodePointVal => 0x10FFFF; - public static int RowLabelWidth => $"U+{MaxCodePointVal:x5}".Length; + public static int RowLabelWidth => $"U+{MaxCodePointVal:x5}".Length + 1; public static int RowWidth => RowLabelWidth + (COLUMN_WIDTH * 16); public CharMap () @@ -109,7 +154,7 @@ namespace UICatalog.Scenarios { ColorScheme = Colors.Dialog; CanFocus = true; - ContentSize = new Size (CharMap.RowWidth, MaxCodePointVal / 16); + ContentSize = new Size (CharMap.RowWidth, (int)(MaxCodePointVal / 16 + 1)); ShowVerticalScrollIndicator = true; ShowHorizontalScrollIndicator = false; LayoutComplete += (args) => { @@ -124,10 +169,61 @@ namespace UICatalog.Scenarios { }; DrawContent += CharMap_DrawContent; - 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; }); + AddCommand (Command.ScrollUp, () => { + if (SelectedGlyph >= 16) { + SelectedGlyph = SelectedGlyph - 16; + } + return true; + }); + AddCommand (Command.ScrollDown, () => { + if (SelectedGlyph < MaxCodePointVal - 16) { + SelectedGlyph = SelectedGlyph + 16; + } + return true; + }); + AddCommand (Command.ScrollLeft, () => { + if (SelectedGlyph > 0) { + SelectedGlyph--; + } + return true; + }); + AddCommand (Command.ScrollRight, () => { + if (SelectedGlyph < MaxCodePointVal - 1) { + SelectedGlyph++; + } + return true; + }); + AddCommand (Command.PageUp, () => { + var page = (uint)(Bounds.Height / ROW_HEIGHT - 1) * 16; + SelectedGlyph -= Math.Min(page, SelectedGlyph); + return true; + }); + AddCommand (Command.PageDown, () => { + var page = (uint)(Bounds.Height / ROW_HEIGHT - 1) * 16; + SelectedGlyph += Math.Min(page, MaxCodePointVal -SelectedGlyph); + return true; + }); + AddCommand (Command.TopHome, () => { + SelectedGlyph = 0; + return true; + }); + AddCommand (Command.BottomEnd, () => { + SelectedGlyph = MaxCodePointVal; + return true; + }); + + MouseClick += Handle_MouseClick; + Application.Driver.SetCursorVisibility (CursorVisibility.Invisible); + } + + private void CopyValue () + { + Clipboard.Contents = $"U+{SelectedGlyph:x5}"; + } + + private void CopyGlyph () + { + Clipboard.Contents = $"{new Rune (SelectedGlyph)}"; } private void CharMap_DrawContent (Rect viewport) @@ -148,8 +244,6 @@ namespace UICatalog.Scenarios { 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) { @@ -157,10 +251,11 @@ namespace UICatalog.Scenarios { Driver.SetAttribute (GetNormalColor ()); Move (firstColumnX, y + 1); Driver.AddStr (new string (' ', 16 * COLUMN_WIDTH)); - if (val < MaxCodePointVal) { + if (val <= MaxCodePointVal) { Driver.SetAttribute (GetNormalColor ()); for (int col = 0; col < 16; col++) { - var rune = new Rune ((uint)((uint)val + col)); + uint glyph = (uint)((uint)val + col); + var rune = new Rune (glyph); //if (rune >= 0x00D800 && rune <= 0x00DFFF) { // if (col == 0) { // Driver.AddStr ("Reserved for surrogate pairs."); @@ -168,21 +263,236 @@ namespace UICatalog.Scenarios { // continue; //} Move (firstColumnX + (col * COLUMN_WIDTH) + 1, y + 1); + if (glyph == SelectedGlyph) { + Driver.SetAttribute (HasFocus ? ColorScheme.HotFocus : ColorScheme.HotNormal); + } else { + Driver.SetAttribute (GetNormalColor ()); + } Driver.AddRune (rune); } Move (0, y + 1); Driver.SetAttribute (HasFocus ? ColorScheme.HotFocus : ColorScheme.Focus); - var rowLabel = $"U+{val / 16:x4}x "; + var rowLabel = $"U+{val / 16:x5}_ "; Driver.AddStr (rowLabel); } } Driver.Clip = oldClip; } + ContextMenu _contextMenu = new ContextMenu (); + void Handle_MouseClick (MouseEventArgs args) + { + var me = args.MouseEvent; + if (me.Flags == MouseFlags.ReportMousePosition || (me.Flags != MouseFlags.Button1Clicked && + me.Flags != MouseFlags.Button1DoubleClicked && + me.Flags != _contextMenu.MouseFlags)) { + return; + } + + if (me.X < RowLabelWidth) { + return; + } + + if (me.Y < 1) { + return; + } + + var row = (me.Y - 1); + var col = (me.X - RowLabelWidth - ContentOffset.X) / COLUMN_WIDTH; + uint val = (uint)((((uint)row - (uint)ContentOffset.Y) * 16) + col); + if (val > MaxCodePointVal) { + return; + } + + if (me.Flags == MouseFlags.Button1Clicked) { + SelectedGlyph = (uint)val; + return; + } + + if (me.Flags == MouseFlags.Button1DoubleClicked) { + SelectedGlyph = (uint)val; + MessageBox.Query ("Glyph", $"{new Rune (val)} U+{SelectedGlyph:x4}", "Ok"); + return; + } + + if (me.Flags == _contextMenu.MouseFlags) { + SelectedGlyph = (uint)val; + _contextMenu = new ContextMenu (me.X + 1, me.Y + 1, + new MenuBarItem (new MenuItem [] { + new MenuItem ("_Copy Glyph", "", () => CopyGlyph (), null, null, Key.C | Key.CtrlMask), + new MenuItem ("Copy _Value", "", () => CopyValue (), null, null, Key.C | Key.ShiftMask | Key.CtrlMask), + }) { + + } + ); + _contextMenu.Show (); + } + } + protected override void Dispose (bool disposing) { DrawContent -= CharMap_DrawContent; base.Dispose (disposing); } } + + class UnicodeRange { + public uint Start; + public uint End; + public string Category; + public UnicodeRange (uint start, uint end, string category) + { + this.Start = start; + this.End = end; + this.Category = category; + } + + public static List Ranges = new List { + new UnicodeRange (0x0000, 0x001F, "ASCII Control Characters"), + new UnicodeRange (0x0080, 0x009F, "C0 Control Characters"), + new UnicodeRange(0x1100, 0x11ff,"Hangul Jamo"), // This is where wide chars tend to start + new UnicodeRange(0x20A0, 0x20CF,"Currency Symbols"), + new UnicodeRange(0x2100, 0x214F,"Letterlike Symbols"), + new UnicodeRange(0x2160, 0x218F, "Roman Numerals"), + new UnicodeRange(0x2190, 0x21ff,"Arrows" ), + new UnicodeRange(0x2200, 0x22ff,"Mathematical symbols"), + new UnicodeRange(0x2300, 0x23ff,"Miscellaneous Technical"), + new UnicodeRange(0x24B6, 0x24e9,"Circled Latin Capital Letters"), + new UnicodeRange(0x1F130, 0x1F149,"Squared Latin Capital Letters"), + new UnicodeRange(0x2500, 0x25ff,"Box Drawing & Geometric Shapes"), + new UnicodeRange(0x2600, 0x26ff,"Miscellaneous Symbols"), + new UnicodeRange(0x2700, 0x27ff,"Dingbats"), + new UnicodeRange(0x2800, 0x28ff,"Braille"), + new UnicodeRange(0x2b00, 0x2bff,"Miscellaneous Symbols and Arrows"), + new UnicodeRange(0xFB00, 0xFb4f,"Alphabetic Presentation Forms"), + new UnicodeRange(0x12400, 0x1240f,"Cuneiform Numbers and Punctuation"), + new UnicodeRange(0x1FA00, 0x1FA0f,"Chess Symbols"), + + new UnicodeRange (0x0020 ,0x007F ,"Basic Latin"), + new UnicodeRange (0x00A0 ,0x00FF ,"Latin-1 Supplement"), + new UnicodeRange (0x0100 ,0x017F ,"Latin Extended-A"), + new UnicodeRange (0x0180 ,0x024F ,"Latin Extended-B"), + new UnicodeRange (0x0250 ,0x02AF ,"IPA Extensions"), + new UnicodeRange (0x02B0 ,0x02FF ,"Spacing Modifier Letters"), + new UnicodeRange (0x0300 ,0x036F ,"Combining Diacritical Marks"), + new UnicodeRange (0x0370 ,0x03FF ,"Greek and Coptic"), + new UnicodeRange (0x0400 ,0x04FF ,"Cyrillic"), + new UnicodeRange (0x0500 ,0x052F ,"Cyrillic Supplementary"), + new UnicodeRange (0x0530 ,0x058F ,"Armenian"), + new UnicodeRange (0x0590 ,0x05FF ,"Hebrew"), + new UnicodeRange (0x0600 ,0x06FF ,"Arabic"), + new UnicodeRange (0x0700 ,0x074F ,"Syriac"), + new UnicodeRange (0x0780 ,0x07BF ,"Thaana"), + new UnicodeRange (0x0900 ,0x097F ,"Devanagari"), + new UnicodeRange (0x0980 ,0x09FF ,"Bengali"), + new UnicodeRange (0x0A00 ,0x0A7F ,"Gurmukhi"), + new UnicodeRange (0x0A80 ,0x0AFF ,"Gujarati"), + new UnicodeRange (0x0B00 ,0x0B7F ,"Oriya"), + new UnicodeRange (0x0B80 ,0x0BFF ,"Tamil"), + new UnicodeRange (0x0C00 ,0x0C7F ,"Telugu"), + new UnicodeRange (0x0C80 ,0x0CFF ,"Kannada"), + new UnicodeRange (0x0D00 ,0x0D7F ,"Malayalam"), + new UnicodeRange (0x0D80 ,0x0DFF ,"Sinhala"), + new UnicodeRange (0x0E00 ,0x0E7F ,"Thai"), + new UnicodeRange (0x0E80 ,0x0EFF ,"Lao"), + new UnicodeRange (0x0F00 ,0x0FFF ,"Tibetan"), + new UnicodeRange (0x1000 ,0x109F ,"Myanmar"), + new UnicodeRange (0x10A0 ,0x10FF ,"Georgian"), + new UnicodeRange (0x1100 ,0x11FF ,"Hangul Jamo"), + new UnicodeRange (0x1200 ,0x137F ,"Ethiopic"), + new UnicodeRange (0x13A0 ,0x13FF ,"Cherokee"), + new UnicodeRange (0x1400 ,0x167F ,"Unified Canadian Aboriginal Syllabics"), + new UnicodeRange (0x1680 ,0x169F ,"Ogham"), + new UnicodeRange (0x16A0 ,0x16FF ,"Runic"), + new UnicodeRange (0x1700 ,0x171F ,"Tagalog"), + new UnicodeRange (0x1720 ,0x173F ,"Hanunoo"), + new UnicodeRange (0x1740 ,0x175F ,"Buhid"), + new UnicodeRange (0x1760 ,0x177F ,"Tagbanwa"), + new UnicodeRange (0x1780 ,0x17FF ,"Khmer"), + new UnicodeRange (0x1800 ,0x18AF ,"Mongolian"), + new UnicodeRange (0x1900 ,0x194F ,"Limbu"), + new UnicodeRange (0x1950 ,0x197F ,"Tai Le"), + new UnicodeRange (0x19E0 ,0x19FF ,"Khmer Symbols"), + new UnicodeRange (0x1D00 ,0x1D7F ,"Phonetic Extensions"), + new UnicodeRange (0x1E00 ,0x1EFF ,"Latin Extended Additional"), + new UnicodeRange (0x1F00 ,0x1FFF ,"Greek Extended"), + new UnicodeRange (0x2000 ,0x206F ,"General Punctuation"), + new UnicodeRange (0x2070 ,0x209F ,"Superscripts and Subscripts"), + new UnicodeRange (0x20A0 ,0x20CF ,"Currency Symbols"), + new UnicodeRange (0x20D0 ,0x20FF ,"Combining Diacritical Marks for Symbols"), + new UnicodeRange (0x2100 ,0x214F ,"Letterlike Symbols"), + new UnicodeRange (0x2150 ,0x218F ,"Number Forms"), + new UnicodeRange (0x2190 ,0x21FF ,"Arrows"), + new UnicodeRange (0x2200 ,0x22FF ,"Mathematical Operators"), + new UnicodeRange (0x2300 ,0x23FF ,"Miscellaneous Technical"), + new UnicodeRange (0x2400 ,0x243F ,"Control Pictures"), + new UnicodeRange (0x2440 ,0x245F ,"Optical Character Recognition"), + new UnicodeRange (0x2460 ,0x24FF ,"Enclosed Alphanumerics"), + new UnicodeRange (0x2500 ,0x257F ,"Box Drawing"), + new UnicodeRange (0x2580 ,0x259F ,"Block Elements"), + new UnicodeRange (0x25A0 ,0x25FF ,"Geometric Shapes"), + new UnicodeRange (0x2600 ,0x26FF ,"Miscellaneous Symbols"), + new UnicodeRange (0x2700 ,0x27BF ,"Dingbats"), + new UnicodeRange (0x27C0 ,0x27EF ,"Miscellaneous Mathematical Symbols-A"), + new UnicodeRange (0x27F0 ,0x27FF ,"Supplemental Arrows-A"), + new UnicodeRange (0x2800 ,0x28FF ,"Braille Patterns"), + new UnicodeRange (0x2900 ,0x297F ,"Supplemental Arrows-B"), + new UnicodeRange (0x2980 ,0x29FF ,"Miscellaneous Mathematical Symbols-B"), + new UnicodeRange (0x2A00 ,0x2AFF ,"Supplemental Mathematical Operators"), + new UnicodeRange (0x2B00 ,0x2BFF ,"Miscellaneous Symbols and Arrows"), + new UnicodeRange (0x2E80 ,0x2EFF ,"CJK Radicals Supplement"), + new UnicodeRange (0x2F00 ,0x2FDF ,"Kangxi Radicals"), + new UnicodeRange (0x2FF0 ,0x2FFF ,"Ideographic Description Characters"), + new UnicodeRange (0x3000 ,0x303F ,"CJK Symbols and Punctuation"), + new UnicodeRange (0x3040 ,0x309F ,"Hiragana"), + new UnicodeRange (0x30A0 ,0x30FF ,"Katakana"), + new UnicodeRange (0x3100 ,0x312F ,"Bopomofo"), + new UnicodeRange (0x3130 ,0x318F ,"Hangul Compatibility Jamo"), + new UnicodeRange (0x3190 ,0x319F ,"Kanbun"), + new UnicodeRange (0x31A0 ,0x31BF ,"Bopomofo Extended"), + new UnicodeRange (0x31F0 ,0x31FF ,"Katakana Phonetic Extensions"), + new UnicodeRange (0x3200 ,0x32FF ,"Enclosed CJK Letters and Months"), + new UnicodeRange (0x3300 ,0x33FF ,"CJK Compatibility"), + new UnicodeRange (0x3400 ,0x4DBF ,"CJK Unified Ideographs Extension A"), + new UnicodeRange (0x4DC0 ,0x4DFF ,"Yijing Hexagram Symbols"), + new UnicodeRange (0x4E00 ,0x9FFF ,"CJK Unified Ideographs"), + new UnicodeRange (0xA000 ,0xA48F ,"Yi Syllables"), + new UnicodeRange (0xA490 ,0xA4CF ,"Yi Radicals"), + new UnicodeRange (0xAC00 ,0xD7AF ,"Hangul Syllables"), + new UnicodeRange (0xD800 ,0xDB7F ,"High Surrogates"), + new UnicodeRange (0xDB80 ,0xDBFF ,"High Private Use Surrogates"), + new UnicodeRange (0xDC00 ,0xDFFF ,"Low Surrogates"), + new UnicodeRange (0xE000 ,0xF8FF ,"Private Use Area"), + new UnicodeRange (0xF900 ,0xFAFF ,"CJK Compatibility Ideographs"), + new UnicodeRange (0xFB00 ,0xFB4F ,"Alphabetic Presentation Forms"), + new UnicodeRange (0xFB50 ,0xFDFF ,"Arabic Presentation Forms-A"), + new UnicodeRange (0xFE00 ,0xFE0F ,"Variation Selectors"), + new UnicodeRange (0xFE20 ,0xFE2F ,"Combining Half Marks"), + new UnicodeRange (0xFE30 ,0xFE4F ,"CJK Compatibility Forms"), + new UnicodeRange (0xFE50 ,0xFE6F ,"Small Form Variants"), + new UnicodeRange (0xFE70 ,0xFEFF ,"Arabic Presentation Forms-B"), + new UnicodeRange (0xFF00 ,0xFFEF ,"Halfwidth and Fullwidth Forms"), + new UnicodeRange (0xFFF0 ,0xFFFF ,"Specials"), + new UnicodeRange (0x10000, 0x1007F ,"Linear B Syllabary"), + new UnicodeRange (0x10080, 0x100FF ,"Linear B Ideograms"), + new UnicodeRange (0x10100, 0x1013F ,"Aegean Numbers"), + new UnicodeRange (0x10300, 0x1032F ,"Old Italic"), + new UnicodeRange (0x10330, 0x1034F ,"Gothic"), + new UnicodeRange (0x10380, 0x1039F ,"Ugaritic"), + new UnicodeRange (0x10400, 0x1044F ,"Deseret"), + new UnicodeRange (0x10450, 0x1047F ,"Shavian"), + new UnicodeRange (0x10480, 0x104AF ,"Osmanya"), + new UnicodeRange (0x10800, 0x1083F ,"Cypriot Syllabary"), + new UnicodeRange (0x1D000, 0x1D0FF ,"Byzantine Musical Symbols"), + new UnicodeRange (0x1D100, 0x1D1FF ,"Musical Symbols"), + new UnicodeRange (0x1D300, 0x1D35F ,"Tai Xuan Jing Symbols"), + new UnicodeRange (0x1D400, 0x1D7FF ,"Mathematical Alphanumeric Symbols"), + new UnicodeRange (0x1F600, 0x1F532 ,"Emojis Symbols"), + new UnicodeRange (0x20000, 0x2A6DF ,"CJK Unified Ideographs Extension B"), + new UnicodeRange (0x2F800, 0x2FA1F ,"CJK Compatibility Ideographs Supplement"), + new UnicodeRange (0xE0000, 0xE007F ,"Tags"), + new UnicodeRange((uint)(CharMap.MaxCodePointVal - 16), (uint)CharMap.MaxCodePointVal,"End"), + }; + } + } diff --git a/UICatalog/Scenarios/ClassExplorer.cs b/UICatalog/Scenarios/ClassExplorer.cs index c7b5798bd..650bd099f 100644 --- a/UICatalog/Scenarios/ClassExplorer.cs +++ b/UICatalog/Scenarios/ClassExplorer.cs @@ -53,27 +53,34 @@ namespace UICatalog.Scenarios { } } + MenuItem highlightModelTextOnly; + public override void Setup () { 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()), - }) - , + }), new MenuBarItem ("_View", new MenuItem [] { miShowPrivate = new MenuItem ("_Include Private", "", () => ShowPrivate()){ Checked = false, CheckType = MenuItemCheckStyle.Checked }, - new MenuItem ("_Expand All", "", () => treeView.ExpandAll()), - new MenuItem ("_Collapse All", "", () => treeView.CollapseAll()) }), + new MenuItem ("_Expand All", "", () => treeView.ExpandAll()), + new MenuItem ("_Collapse All", "", () => treeView.CollapseAll()) + }), + new MenuBarItem ("_Style", new MenuItem [] { + highlightModelTextOnly = new MenuItem ("_Highlight Model Text Only", "", () => OnCheckHighlightModelTextOnly()) { + CheckType = MenuItemCheckStyle.Checked + }, + }) }); - Top.Add (menu); + Application.Top.Add (menu); treeView = new TreeView () { X = 0, @@ -82,7 +89,6 @@ namespace UICatalog.Scenarios { Height = Dim.Fill (), }; - treeView.AddObjects (AppDomain.CurrentDomain.GetAssemblies ()); treeView.AspectGetter = GetRepresentation; treeView.TreeBuilder = new DelegateTreeBuilder (ChildGetter, CanExpand); @@ -100,6 +106,13 @@ namespace UICatalog.Scenarios { Win.Add (textView); } + private void OnCheckHighlightModelTextOnly () + { + treeView.Style.HighlightModelTextOnly = !treeView.Style.HighlightModelTextOnly; + highlightModelTextOnly.Checked = treeView.Style.HighlightModelTextOnly; + treeView.SetNeedsDisplay (); + } + private void ShowPrivate () { miShowPrivate.Checked = !miShowPrivate.Checked; 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 d97f6890c..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, @@ -138,9 +137,8 @@ namespace UICatalog.Scenarios { Height = Dim.Fill (), AllowsMarking = false, AllowsMultipleSelection = false, - ColorScheme = Colors.TopLevel }; - Top.Add (_listView); + Application.Top.Add (_listView); _listView.SetSource (_items); @@ -158,19 +156,19 @@ namespace UICatalog.Scenarios { TextAlignment = TextAlignment.Centered, X = Pos.Right (_listView) + 2, Y = 1, // for menu - Width = Dim.Percent (50), + Width = Dim.Percent (50), Height = 1, }; - Top.Add (label); + Application.Top.Add (label); _treeView = new TreeView () { X = Pos.Right (_listView) + 1, Y = Pos.Bottom (label), Width = Dim.Fill (), - Height = Dim.Fill (), - ColorScheme = Colors.TopLevel + Height = Dim.Fill () }; - Top.Add (_treeView); + _treeView.Style.HighlightModelTextOnly = true; + 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/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/RunTExample.cs b/UICatalog/Scenarios/RunTExample.cs new file mode 100644 index 000000000..98d421d7e --- /dev/null +++ b/UICatalog/Scenarios/RunTExample.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/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/Text.cs b/UICatalog/Scenarios/Text.cs index 7b9a25628..424c06886 100644 --- a/UICatalog/Scenarios/Text.cs +++ b/UICatalog/Scenarios/Text.cs @@ -1,5 +1,6 @@ using NStack; using System; +using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; @@ -16,12 +17,12 @@ namespace UICatalog.Scenarios { public class Text : Scenario { public override void Setup () { - var s = "TAB to jump between text fields."; - var textField = new TextField (s) { + // TextField is a simple, single-line text input control + var textField = new TextField ("TextField with test text. Unicode shouldn't 𝔹Aℝ𝔽!") { X = 1, - Y = 1, - Width = Dim.Percent (50), - //ColorScheme = Colors.Dialog + Y = 0, + Width = Dim.Percent (50) - 1, + Height = 2 }; textField.TextChanging += TextField_TextChanging; @@ -36,7 +37,7 @@ namespace UICatalog.Scenarios { var labelMirroringTextField = new Label (textField.Text) { X = Pos.Right (textField) + 1, Y = Pos.Top (textField), - Width = Dim.Fill (1) + Width = Dim.Fill (1) - 1 }; Win.Add (labelMirroringTextField); @@ -44,15 +45,17 @@ namespace UICatalog.Scenarios { labelMirroringTextField.Text = textField.Text; }; + // TextView is a rich (as in functionality, not formatting) text editing control var textView = new TextView () { X = 1, - Y = 3, - Width = Dim.Percent (50), + Y = Pos.Bottom (textField), + Width = Dim.Percent (50) - 1, Height = Dim.Percent (30), }; - textView.Text = s; + textView.Text = "TextView with some more test text. Unicode shouldn't 𝔹Aℝ𝔽!" ; textView.DrawContent += TextView_DrawContent; + // This shows how to enable autocomplete in TextView. void TextView_DrawContent (Rect e) { textView.Autocomplete.AllSuggestions = Regex.Matches (textView.Text.ToString (), "\\w+") @@ -61,40 +64,89 @@ namespace UICatalog.Scenarios { } Win.Add (textView); - var labelMirroringTextView = new Label (textView.Text) { + var labelMirroringTextView = new Label () { X = Pos.Right (textView) + 1, Y = Pos.Top (textView), - Width = Dim.Fill (1), - Height = Dim.Height (textView), + Width = Dim.Fill (1) - 1, + Height = Dim.Height (textView) - 1, }; Win.Add (labelMirroringTextView); - textView.TextChanged += () => { + // Use ContentChanged to detect if the user has typed something in a TextView. + // The TextChanged property is only fired if the TextView.Text property is + // explicitly set + textView.ContentsChanged += (a) => { + labelMirroringTextView.Enabled = !labelMirroringTextView.Enabled; labelMirroringTextView.Text = textView.Text; }; - var btnMultiline = new Button ("Toggle Multiline") { - X = Pos.Right (textView) + 1, - Y = Pos.Top (textView) + 1 + // By default TextView is a multi-line control. It can be forced to + // single-line mode. + var chxMultiline = new CheckBox ("Multiline") { + X = Pos.Left (textView), + Y = Pos.Bottom (textView), + Checked = true }; - btnMultiline.Clicked += () => textView.Multiline = !textView.Multiline; - Win.Add (btnMultiline); + chxMultiline.Toggled += (b) => textView.Multiline = b; + Win.Add (chxMultiline); - // BUGBUG: 531 - TAB doesn't go to next control from HexView - var hexView = new HexView (new System.IO.MemoryStream (Encoding.ASCII.GetBytes (s))) { - X = 1, - Y = Pos.Bottom (textView) + 1, - Width = Dim.Fill (1), - Height = Dim.Percent (30), - //ColorScheme = Colors.Dialog + var chxWordWrap = new CheckBox ("Word Wrap") { + X = Pos.Right (chxMultiline) + 2, + Y = Pos.Top (chxMultiline) }; - Win.Add (hexView); + chxWordWrap.Toggled += (b) => textView.WordWrap = b; + Win.Add (chxWordWrap); + + // TextView captures Tabs (so users can enter /t into text) by default; + // This means using Tab to navigate doesn't work by default. This shows + // how to turn tab capture off. + var chxCaptureTabs = new CheckBox ("Capture Tabs") { + X = Pos.Right (chxWordWrap) + 2, + Y = Pos.Top (chxWordWrap), + Checked = true + }; + + Key keyTab = textView.GetKeyFromCommand (Command.Tab); + Key keyBackTab = textView.GetKeyFromCommand (Command.BackTab); + chxCaptureTabs.Toggled += (b) => { + if (b) { + textView.AddKeyBinding (keyTab, Command.Tab); + textView.AddKeyBinding (keyBackTab, Command.BackTab); + } else { + textView.ClearKeybinding (keyTab); + textView.ClearKeybinding (keyBackTab); + } + textView.WordWrap = b; + }; + Win.Add (chxCaptureTabs); + + var hexEditor = new HexView (new MemoryStream (Encoding.UTF8.GetBytes ("HexEditor Unicode that shouldn't 𝔹Aℝ𝔽!"))) { + X = 1, + Y = Pos.Bottom (chxMultiline) + 1, + Width = Dim.Percent (50) - 1, + Height = Dim.Percent (30), + }; + Win.Add (hexEditor); + + var labelMirroringHexEditor = new Label () { + X = Pos.Right (hexEditor) + 1, + Y = Pos.Top (hexEditor), + Width = Dim.Fill (1) - 1, + Height = Dim.Height (hexEditor) - 1, + }; + var array = ((MemoryStream)hexEditor.Source).ToArray (); + labelMirroringHexEditor.Text = Encoding.UTF8.GetString (array, 0, array.Length); + hexEditor.Edited += (kv) => { + hexEditor.ApplyEdits (); + var array = ((MemoryStream)hexEditor.Source).ToArray (); + labelMirroringHexEditor.Text = Encoding.UTF8.GetString (array, 0, array.Length); + }; + Win.Add (labelMirroringHexEditor); var dateField = new DateField (System.DateTime.Now) { X = 1, - Y = Pos.Bottom (hexView) + 1, + Y = Pos.Bottom (hexEditor) + 1, Width = 20, - //ColorScheme = Colors.Dialog, IsShortFormat = false }; Win.Add (dateField); @@ -113,9 +165,8 @@ namespace UICatalog.Scenarios { _timeField = new TimeField (DateTime.Now.TimeOfDay) { X = Pos.Right (labelMirroringDateField) + 5, - Y = Pos.Bottom (hexView) + 1, + Y = Pos.Bottom (hexEditor) + 1, Width = 20, - //ColorScheme = Colors.Dialog, IsShortFormat = false }; Win.Add (_timeField); @@ -130,8 +181,8 @@ namespace UICatalog.Scenarios { _timeField.TimeChanged += TimeChanged; - // MaskedTextProvider - var netProviderLabel = new Label (".Net MaskedTextProvider [ 999 000 LLL >LLL| AAA aaa ]") { + // MaskedTextProvider - uses .NET MaskedTextProvider + var netProviderLabel = new Label ("NetMaskedTextProvider [ 999 000 LLL >LLL| AAA aaa ]") { X = Pos.Left (dateField), Y = Pos.Bottom (dateField) + 1 }; @@ -141,13 +192,13 @@ namespace UICatalog.Scenarios { var netProviderField = new TextValidateField (netProvider) { X = Pos.Right (netProviderLabel) + 1, - Y = Pos.Y (netProviderLabel) + Y = Pos.Y (netProviderLabel), }; Win.Add (netProviderField); - // TextRegexProvider - var regexProvider = new Label ("Gui.cs TextRegexProvider [ ^([0-9]?[0-9]?[0-9]|1000)$ ]") { + // TextRegexProvider - Regex provider implemented by Terminal.Gui + var regexProvider = new Label ("TextRegexProvider [ ^([0-9]?[0-9]?[0-9]|1000)$ ]") { X = Pos.Left (netProviderLabel), Y = Pos.Bottom (netProviderLabel) + 1 }; 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 57fa181c7..03441f2d4 100644 --- a/UICatalog/Scenarios/TreeViewFileSystem.cs +++ b/UICatalog/Scenarios/TreeViewFileSystem.cs @@ -2,11 +2,12 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection.PortableExecutable; using Terminal.Gui; using Terminal.Gui.Trees; namespace UICatalog.Scenarios { - [ScenarioMetadata (Name: "TreeViewFileSystem", Description: "Hierarchical file system explorer based on TreeView.")] + [ScenarioMetadata (Name: "File System Explorer", Description: "Hierarchical file system explorer demonstrating TreeView.")] [ScenarioCategory ("Controls"), ScenarioCategory ("TreeView"), ScenarioCategory ("Files and IO")] public class TreeViewFileSystem : Scenario { @@ -24,86 +25,97 @@ namespace UICatalog.Scenarios { private MenuItem miUnicodeSymbols; private MenuItem miFullPaths; private MenuItem miLeaveLastRow; + private MenuItem miHighlightModelTextOnly; private MenuItem miCustomColors; private MenuItem miCursor; private MenuItem miMultiSelect; - private Terminal.Gui.Attribute green; - private Terminal.Gui.Attribute red; + + private DetailsFrame detailsFrame; public override void Setup () { Win.Title = this.GetName (); Win.Y = 1; // menu - Win.Height = Dim.Fill (1); // status bar - Top.LayoutSubviews (); + Win.Height = Dim.Fill (); + Application.Top.LayoutSubviews (); var menu = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_File", new MenuItem [] { - new MenuItem ("_Quit", "", () => Quit()), + new MenuItem ("_Quit", "CTRL-Q", () => Quit()), }), new MenuBarItem ("_View", new MenuItem [] { - miShowLines = new MenuItem ("_ShowLines", "", () => ShowLines()){ + miFullPaths = new MenuItem ("_Full Paths", "", () => SetFullName()){Checked = false, CheckType = MenuItemCheckStyle.Checked}, + miMultiSelect = new MenuItem ("_Multi Select", "", () => SetMultiSelect()){Checked = true, CheckType = MenuItemCheckStyle.Checked}, + }), + new MenuBarItem ("_Style", new MenuItem [] { + miShowLines = new MenuItem ("_Show Lines", "", () => ShowLines()){ Checked = true, CheckType = MenuItemCheckStyle.Checked }, null /*separator*/, - miPlusMinus = new MenuItem ("_PlusMinusSymbols", "", () => SetExpandableSymbols('+','-')){Checked = true, CheckType = MenuItemCheckStyle.Radio}, - miArrowSymbols = new MenuItem ("_ArrowSymbols", "", () => SetExpandableSymbols('>','v')){Checked = false, CheckType = MenuItemCheckStyle.Radio}, - miNoSymbols = new MenuItem ("_NoSymbols", "", () => SetExpandableSymbols(null,null)){Checked = false, CheckType = MenuItemCheckStyle.Radio}, - miUnicodeSymbols = new MenuItem ("_Unicode", "", () => SetExpandableSymbols('ஹ','﷽')){Checked = false, CheckType = MenuItemCheckStyle.Radio}, + miPlusMinus = new MenuItem ("_Plus Minus Symbols", "+ -", () => SetExpandableSymbols('+','-')){Checked = true, CheckType = MenuItemCheckStyle.Radio}, + miArrowSymbols = new MenuItem ("_Arrow Symbols", "> v", () => SetExpandableSymbols('>','v')){Checked = false, CheckType = MenuItemCheckStyle.Radio}, + miNoSymbols = new MenuItem ("_No Symbols", "", () => SetExpandableSymbols(null,null)){Checked = false, CheckType = MenuItemCheckStyle.Radio}, + miUnicodeSymbols = new MenuItem ("_Unicode", "ஹ ﷽", () => SetExpandableSymbols('ஹ','﷽')){Checked = false, CheckType = MenuItemCheckStyle.Radio}, + null /*separator*/, + miColoredSymbols = new MenuItem ("_Colored Symbols", "", () => ShowColoredExpandableSymbols()){Checked = false, CheckType = MenuItemCheckStyle.Checked}, + miInvertSymbols = new MenuItem ("_Invert Symbols", "", () => InvertExpandableSymbols()){Checked = false, CheckType = MenuItemCheckStyle.Checked}, + null /*separator*/, + miLeaveLastRow = new MenuItem ("_Leave Last Row", "", () => SetLeaveLastRow()){Checked = true, CheckType = MenuItemCheckStyle.Checked}, + miHighlightModelTextOnly = new MenuItem ("_Highlight Model Text Only", "", () => SetCheckHighlightModelTextOnly()){Checked = false, CheckType = MenuItemCheckStyle.Checked}, + null /*separator*/, + miCustomColors = new MenuItem ("C_ustom Colors Hidden Files", "Yellow/Red", () => SetCustomColors()){Checked = false, CheckType = MenuItemCheckStyle.Checked}, null /*separator*/, - miColoredSymbols = new MenuItem ("_ColoredSymbols", "", () => ShowColoredExpandableSymbols()){Checked = false, CheckType = MenuItemCheckStyle.Checked}, - miInvertSymbols = new MenuItem ("_InvertSymbols", "", () => InvertExpandableSymbols()){Checked = false, CheckType = MenuItemCheckStyle.Checked}, - miFullPaths = new MenuItem ("_FullPaths", "", () => SetFullName()){Checked = false, CheckType = MenuItemCheckStyle.Checked}, - miLeaveLastRow = new MenuItem ("_LeaveLastRow", "", () => SetLeaveLastRow()){Checked = true, CheckType = MenuItemCheckStyle.Checked}, - miCustomColors = new MenuItem ("C_ustomColors", "", () => SetCustomColors()){Checked = false, CheckType = MenuItemCheckStyle.Checked}, miCursor = new MenuItem ("Curs_or (MultiSelect only)", "", () => SetCursor()){Checked = false, CheckType = MenuItemCheckStyle.Checked}, - miMultiSelect = new MenuItem ("_MultiSelect", "", () => SetMultiSelect()){Checked = true, CheckType = MenuItemCheckStyle.Checked}, }), }); - Top.Add (menu); - - var statusBar = new StatusBar (new StatusItem [] { - new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()), - }); - Top.Add (statusBar); - - var lblFiles = new Label ("File Tree:") { - X = 0, - Y = 1 - }; - Win.Add (lblFiles); + Application.Top.Add (menu); treeViewFiles = new TreeView () { X = 0, - Y = Pos.Bottom (lblFiles), + Y = 0, + Width = Dim.Percent (50), + Height = Dim.Fill (), + }; + + detailsFrame = new DetailsFrame () { + X = Pos.Right (treeViewFiles), + Y = 0, Width = Dim.Fill (), Height = Dim.Fill (), }; - treeViewFiles.ObjectActivated += TreeViewFiles_ObjectActivated; + Win.Add (detailsFrame); treeViewFiles.MouseClick += TreeViewFiles_MouseClick; treeViewFiles.KeyPress += TreeViewFiles_KeyPress; + treeViewFiles.SelectionChanged += TreeViewFiles_SelectionChanged; SetupFileTree (); Win.Add (treeViewFiles); + treeViewFiles.GoToFirst (); + treeViewFiles.Expand (); SetupScrollBar (); - green = Application.Driver.MakeAttribute (Color.Green, Color.Blue); - red = Application.Driver.MakeAttribute (Color.Red, Color.Blue); + treeViewFiles.SetFocus (); + + } + + private void TreeViewFiles_SelectionChanged (object sender, SelectionChangedEventArgs e) + { + ShowPropertiesOf (e.NewValue); } private void TreeViewFiles_KeyPress (View.KeyEventEventArgs obj) { - if(obj.KeyEvent.Key == (Key.R | Key.CtrlMask)) { + if (obj.KeyEvent.Key == (Key.R | Key.CtrlMask)) { var selected = treeViewFiles.SelectedObject; - + // nothing is selected if (selected == null) return; - + var location = treeViewFiles.GetObjectRow (selected); //selected object is offscreen or somehow not found @@ -120,9 +132,9 @@ namespace UICatalog.Scenarios { private void TreeViewFiles_MouseClick (View.MouseEventArgs obj) { // if user right clicks - if (obj.MouseEvent.Flags.HasFlag(MouseFlags.Button3Clicked)) { + if (obj.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)) { - var rightClicked = treeViewFiles.GetObjectOnRow ( obj.MouseEvent.Y); + var rightClicked = treeViewFiles.GetObjectOnRow (obj.MouseEvent.Y); // nothing was clicked if (rightClicked == null) @@ -141,31 +153,48 @@ namespace UICatalog.Scenarios { menu.Position = screenPoint; menu.MenuItems = new MenuBarItem (new [] { new MenuItem ("Properties", null, () => ShowPropertiesOf (forObject)) }); - - Application.MainLoop.Invoke(menu.Show); + + Application.MainLoop.Invoke (menu.Show); + } + + class DetailsFrame : FrameView { + private FileSystemInfo fileInfo; + + public DetailsFrame () + { + Title = "Details"; + Visible = true; + CanFocus = true; + } + + public FileSystemInfo FileInfo { + get => fileInfo; set { + fileInfo = value; + System.Text.StringBuilder sb = null; + if (fileInfo is FileInfo f) { + Title = $"File: {f.Name}"; + sb = new System.Text.StringBuilder (); + sb.AppendLine ($"Path:\n {f.FullName}\n"); + sb.AppendLine ($"Size:\n {f.Length:N0} bytes\n"); + sb.AppendLine ($"Modified:\n {f.LastWriteTime}\n"); + sb.AppendLine ($"Created:\n {f.CreationTime}"); + } + + if (fileInfo is DirectoryInfo dir) { + Title = $"Directory: {dir.Name}"; + sb = new System.Text.StringBuilder (); + sb.AppendLine ($"Path:\n {dir?.FullName}\n"); + sb.AppendLine ($"Modified:\n {dir.LastWriteTime}\n"); + sb.AppendLine ($"Created:\n {dir.CreationTime}\n"); + } + Text = sb.ToString (); + } + } } private void ShowPropertiesOf (FileSystemInfo fileSystemInfo) { - if (fileSystemInfo is FileInfo f) { - System.Text.StringBuilder sb = new System.Text.StringBuilder (); - sb.AppendLine ($"Path:{f.DirectoryName}"); - sb.AppendLine ($"Size:{f.Length:N0} bytes"); - sb.AppendLine ($"Modified:{ f.LastWriteTime}"); - sb.AppendLine ($"Created:{ f.CreationTime}"); - - MessageBox.Query (f.Name, sb.ToString (), "Close"); - } - - if (fileSystemInfo is DirectoryInfo dir) { - - System.Text.StringBuilder sb = new System.Text.StringBuilder (); - sb.AppendLine ($"Path:{dir.Parent?.FullName}"); - sb.AppendLine ($"Modified:{ dir.LastWriteTime}"); - sb.AppendLine ($"Created:{ dir.CreationTime}"); - - MessageBox.Query (dir.Name, sb.ToString (), "Close"); - } + detailsFrame.FileInfo = fileSystemInfo; } private void SetupScrollBar () @@ -218,11 +247,6 @@ namespace UICatalog.Scenarios { treeViewFiles.AddObjects (DriveInfo.GetDrives ().Select (d => d.RootDirectory)); } - private void TreeViewFiles_ObjectActivated (ObjectActivatedEventArgs obj) - { - ShowPropertiesOf (obj.ActivatedObject); - } - private void ShowLines () { miShowLines.Checked = !miShowLines.Checked; @@ -266,6 +290,7 @@ namespace UICatalog.Scenarios { } else { treeViewFiles.AspectGetter = (f) => f.Name; } + treeViewFiles.SetNeedsDisplay (); } private void SetLeaveLastRow () @@ -273,41 +298,45 @@ namespace UICatalog.Scenarios { miLeaveLastRow.Checked = !miLeaveLastRow.Checked; treeViewFiles.Style.LeaveLastRow = miLeaveLastRow.Checked; } - private void SetCursor() + private void SetCursor () { miCursor.Checked = !miCursor.Checked; treeViewFiles.DesiredCursorVisibility = miCursor.Checked ? CursorVisibility.Default : CursorVisibility.Invisible; } - private void SetMultiSelect() + private void SetMultiSelect () { miMultiSelect.Checked = !miMultiSelect.Checked; treeViewFiles.MultiSelect = miMultiSelect.Checked; } - - private void SetCustomColors() + + private void SetCustomColors () { - var yellow = new ColorScheme - { - Focus = new Terminal.Gui.Attribute(Color.BrightYellow,treeViewFiles.ColorScheme.Focus.Background), - Normal = new Terminal.Gui.Attribute (Color.BrightYellow,treeViewFiles.ColorScheme.Normal.Background), + var hidden = new ColorScheme { + Focus = new Terminal.Gui.Attribute (Color.BrightRed, treeViewFiles.ColorScheme.Focus.Background), + Normal = new Terminal.Gui.Attribute (Color.BrightYellow, treeViewFiles.ColorScheme.Normal.Background), }; miCustomColors.Checked = !miCustomColors.Checked; - if(miCustomColors.Checked) - { - treeViewFiles.ColorGetter = (m)=> - { - return m is DirectoryInfo ? yellow : null; + if (miCustomColors.Checked) { + treeViewFiles.ColorGetter = (m) => { + if (m is DirectoryInfo && m.Attributes.HasFlag (FileAttributes.Hidden)) return hidden; + if (m is FileInfo && m.Attributes.HasFlag (FileAttributes.Hidden)) return hidden; + return null; }; - } - else - { + } else { treeViewFiles.ColorGetter = null; } + treeViewFiles.SetNeedsDisplay (); } + private void SetCheckHighlightModelTextOnly () + { + treeViewFiles.Style.HighlightModelTextOnly = !treeViewFiles.Style.HighlightModelTextOnly; + miHighlightModelTextOnly.Checked = treeViewFiles.Style.HighlightModelTextOnly; + treeViewFiles.SetNeedsDisplay (); + } private IEnumerable GetChildren (FileSystemInfo model) { diff --git a/UICatalog/Scenarios/Unicode.cs b/UICatalog/Scenarios/Unicode.cs index a0ca89d2c..2303d1242 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..417dbe810 100644 --- a/UICatalog/Scenarios/WindowsAndFrameViews.cs +++ b/UICatalog/Scenarios/WindowsAndFrameViews.cs @@ -3,57 +3,35 @@ using System.Linq; using Terminal.Gui; namespace UICatalog.Scenarios { - [ScenarioMetadata (Name: "Windows & FrameViews", Description: "Shows Windows, sub-Windows, and FrameViews.")] + [ScenarioMetadata (Name: "Windows & FrameViews", Description: "Stress Tests 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 (); - } - - public override void Run () - { - base.Run (); - } - + public override void Setup () { static int About () { return MessageBox.Query ("About UI Catalog", "UI Catalog is a comprehensive sample library for Terminal.Gui", "Ok"); - - //var about = new Window (new Rect (0, 0, 50, 10), "About UI catalog", 0) { - // X = Pos.Center (), - // Y = Pos.Center (), - // Width = 50, - // Height = 10, - // LayoutStyle = LayoutStyle.Computed, - // ColorScheme = Colors.Error, - - //}; - - //Application.Run (about); - //return 0; - } int margin = 2; int padding = 1; int contentHeight = 7; + + // list of Windows we create var listWin = new List (); + + //Ignore the Win that UI Catalog created and create a new one + Application.Top.Remove (Win); + Win?.Dispose (); + Win = new Window ($"{listWin.Count} - Scenario: {GetName ()}", padding) { X = Pos.Center (), Y = 1, - Width = Dim.Fill (10), - Height = Dim.Percent (15) + Width = Dim.Fill (15), + Height = 10 }; + Win.ColorScheme = Colors.Dialog; var paddingButton = new Button ($"Padding of container is {padding}") { X = Pos.Center (), @@ -67,9 +45,18 @@ namespace UICatalog.Scenarios { Y = Pos.AnchorEnd (1), ColorScheme = Colors.Error }); - Top.Add (Win); + Application.Top.Add (Win); + + // add it to our list listWin.Add (Win); + // create 3 more Windows in a loop, adding them Application.Top + // Each with a + // button + // sub Window with + // TextField + // sub FrameView with + // for (var i = 0; i < 3; i++) { Window win = null; win = new Window ($"{listWin.Count} - Window Loop - padding = {i}", i) { @@ -114,11 +101,22 @@ namespace UICatalog.Scenarios { }); win.Add (frameView); - Top.Add (win); + Application.Top.Add (win); listWin.Add (win); } - + // Add a FrameView (frame) to Application.Top + // Position it at Bottom, using the list of Windows we created above. + // Fill it with + // a label + // a SubWindow containing (subWinofFV) + // a TextField + // two checkboxes + // a Sub FrameView containing (subFrameViewofFV) + // a TextField + // two CheckBoxes + // a checkbox + // a checkbox FrameView frame = null; frame = new FrameView ($"This is a FrameView") { X = margin, @@ -176,8 +174,10 @@ namespace UICatalog.Scenarios { frame.Add (subFrameViewofFV); - Top.Add (frame); + Application.Top.Add (frame); listWin.Add (frame); + + Application.Top.ColorScheme = Colors.Base; } } } \ No newline at end of file diff --git a/UICatalog/Scenarios/WizardAsView.cs b/UICatalog/Scenarios/WizardAsView.cs index ccaf3fdbf..e7be17552 100644 --- a/UICatalog/Scenarios/WizardAsView.cs +++ b/UICatalog/Scenarios/WizardAsView.cs @@ -10,10 +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) { Application.Init (); - Top = Application.Top; var menu = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_File", new MenuItem [] { @@ -22,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 () { @@ -94,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 dea8906d6..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)); 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/TextViewTests.cs b/UnitTests/TextViewTests.cs index 193ecc549..eb33c0162 100644 --- a/UnitTests/TextViewTests.cs +++ b/UnitTests/TextViewTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.Tracing; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; @@ -24,6 +25,7 @@ namespace Terminal.Gui.Views { [AttributeUsage (AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class InitShutdown : Xunit.Sdk.BeforeAfterTestAttribute { + public static string txt = "TAB to jump between text fields."; public override void Before (MethodInfo methodUnderTest) { if (_textView != null) { @@ -34,7 +36,6 @@ namespace Terminal.Gui.Views { // 1 2 3 // 01234567890123456789012345678901=32 (Length) - var txt = "TAB to jump between text fields."; var buff = new byte [txt.Length]; for (int i = 0; i < txt.Length; i++) { buff [i] = (byte)txt [i]; @@ -1395,6 +1396,22 @@ namespace Terminal.Gui.Views { Assert.Equal ("changed", _textView.Text); } + [Fact] + [InitShutdown] + public void TextChanged_Event_NoFires_OnTyping () + { + var eventcount = 0; + _textView.TextChanged += () => { + eventcount++; + }; + + _textView.Text = "ay"; + Assert.Equal (1, eventcount); + _textView.ProcessKey (new KeyEvent (Key.Y, new KeyModifiers ())); + Assert.Equal (1, eventcount); + Assert.Equal ("Yay", _textView.Text.ToString ()); + } + [Fact] [InitShutdown] public void Used_Is_True_By_Default () @@ -6409,5 +6426,351 @@ This is the second line. │ │ └─────────────┘", output); } + + [Fact, AutoInitShutdown] + public void ContentsChanged_Event_NoFires_On_CursorPosition () + { + var tv = new TextView { + Width = 50, + Height = 10, + }; + + var eventcount = 0; + Assert.Null (tv.ContentsChanged); + tv.ContentsChanged += (e) => { + eventcount++; + }; + + tv.CursorPosition = new Point (0, 0); + + Assert.Equal (0, eventcount); + } + + [Fact, AutoInitShutdown] + public void ContentsChanged_Event_Fires_On_InsertText () + { + var tv = new TextView { + Width = 50, + Height = 10, + }; + tv.CursorPosition = new Point (0, 0); + + var eventcount = 0; + + Assert.Null (tv.ContentsChanged); + tv.ContentsChanged += (e) => { + eventcount++; + }; + + + tv.InsertText ("a"); + Assert.Equal (1, eventcount); + + tv.CursorPosition = new Point (0, 0); + tv.InsertText ("bcd"); + Assert.Equal (4, eventcount); + + tv.InsertText ("e"); + Assert.Equal (5, eventcount); + + tv.InsertText ("\n"); + Assert.Equal (6, eventcount); + + tv.InsertText ("1234"); + Assert.Equal (10, eventcount); + } + + [Fact, AutoInitShutdown] + public void ContentsChanged_Event_Fires_On_Init () + { + Application.Iteration += () => { + Application.RequestStop (); + }; + + var expectedRow = 0; + var expectedCol = 0; + var eventcount = 0; + + var tv = new TextView { + Width = 50, + Height = 10, + ContentsChanged = (e) => { + eventcount++; + Assert.Equal (expectedRow, e.Row); + Assert.Equal (expectedCol, e.Col); + } + }; + + Application.Top.Add (tv); + Application.Begin (Application.Top); + Assert.Equal (1, eventcount); + } + + [Fact, AutoInitShutdown] + public void ContentsChanged_Event_Fires_On_Set_Text () + { + Application.Iteration += () => { + Application.RequestStop (); + }; + var eventcount = 0; + + var expectedRow = 0; + var expectedCol = 0; + + var tv = new TextView { + Width = 50, + Height = 10, + // you'd think col would be 3, but it's 0 because TextView sets + // row/col = 0 when you set Text + Text = "abc", + ContentsChanged = (e) => { + eventcount++; + Assert.Equal (expectedRow, e.Row); + Assert.Equal (expectedCol, e.Col); + } + }; + Assert.Equal ("abc", tv.Text); + + Application.Top.Add (tv); + var rs = Application.Begin (Application.Top); + Assert.Equal (1, eventcount); // for Initialize + + expectedCol = 0; + tv.Text = "defg"; + Assert.Equal (2, eventcount); // for set Text = "defg" + } + + [Fact, AutoInitShutdown] + public void ContentsChanged_Event_Fires_On_Typing () + { + Application.Iteration += () => { + Application.RequestStop (); + }; + var eventcount = 0; + + var expectedRow = 0; + var expectedCol = 0; + + var tv = new TextView { + Width = 50, + Height = 10, + ContentsChanged = (e) => { + eventcount++; + Assert.Equal (expectedRow, e.Row); + Assert.Equal (expectedCol, e.Col); + } + }; + + Application.Top.Add (tv); + var rs = Application.Begin (Application.Top); + Assert.Equal (1, eventcount); // for Initialize + + expectedCol = 0; + tv.Text = "ay"; + Assert.Equal (2, eventcount); + + expectedCol = 1; + tv.ProcessKey (new KeyEvent (Key.Y, new KeyModifiers ())); + Assert.Equal (3, eventcount); + Assert.Equal ("Yay", tv.Text.ToString ()); + } + + [Fact, InitShutdown] + public void ContentsChanged_Event_Fires_Using_Kill_Delete_Tests () + { + var eventcount = 0; + + _textView.ContentsChanged = (e) => { + eventcount++; + }; + + var expectedEventCount = 1; + Kill_Delete_WordForward (); + Assert.Equal (expectedEventCount, eventcount); // for Initialize + + expectedEventCount += 1; + Kill_Delete_WordBackward (); + Assert.Equal (expectedEventCount, eventcount); + + expectedEventCount += 1; + Kill_To_End_Delete_Forwards_And_Copy_To_The_Clipboard (); + Assert.Equal (expectedEventCount, eventcount); + + expectedEventCount += 1; + Kill_To_Start_Delete_Backwards_And_Copy_To_The_Clipboard (); + Assert.Equal (expectedEventCount, eventcount); + } + + + [Fact, InitShutdown] + public void ContentsChanged_Event_Fires_Using_Copy_Or_Cut_Tests () + { + var eventcount = 0; + + _textView.ContentsChanged = (e) => { + eventcount++; + }; + + var expectedEventCount = 1; + + // reset + _textView.Text = InitShutdown.txt; + Assert.Equal (expectedEventCount, eventcount); + + expectedEventCount += 3; + Copy_Or_Cut_And_Paste_With_No_Selection (); + Assert.Equal (expectedEventCount, eventcount); + + // reset + expectedEventCount += 1; + _textView.Text = InitShutdown.txt; + Assert.Equal (expectedEventCount, eventcount); + + expectedEventCount += 3; + Copy_Or_Cut_And_Paste_With_Selection (); + Assert.Equal (expectedEventCount, eventcount); + + // reset + expectedEventCount += 1; + _textView.Text = InitShutdown.txt; + Assert.Equal (expectedEventCount, eventcount); + + expectedEventCount += 1; + Copy_Or_Cut_Not_Null_If_Has_Selection (); + Assert.Equal (expectedEventCount, eventcount); + + // reset + expectedEventCount += 1; + _textView.Text = InitShutdown.txt; + Assert.Equal (expectedEventCount, eventcount); + + expectedEventCount += 1; + Copy_Or_Cut_Null_If_No_Selection (); + Assert.Equal (expectedEventCount, eventcount); + + // reset + expectedEventCount += 1; + _textView.Text = InitShutdown.txt; + Assert.Equal (expectedEventCount, eventcount); + + expectedEventCount += 4; + Copy_Without_Selection (); + Assert.Equal (expectedEventCount, eventcount); + + // reset + expectedEventCount += 1; + _textView.Text = InitShutdown.txt; + Assert.Equal (expectedEventCount, eventcount); + + expectedEventCount += 4; + Copy_Without_Selection (); + Assert.Equal (expectedEventCount, eventcount); + } + + [Fact, InitShutdown] + public void ContentsChanged_Event_Fires_On_Undo_Redo () + { + var eventcount = 0; + var expectedEventCount = 0; + + _textView.ContentsChanged = (e) => { + eventcount++; + }; + + expectedEventCount++; + _textView.Text = "This is the first line.\nThis is the second line.\nThis is the third line."; + Assert.Equal (expectedEventCount, eventcount); + + expectedEventCount++; + Assert.True (_textView.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()))); + Assert.Equal (expectedEventCount, eventcount); + + // Undo + expectedEventCount++; + Assert.True (_textView.ProcessKey (new KeyEvent (Key.Z | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (expectedEventCount, eventcount); + + // Redo + expectedEventCount++; + Assert.True (_textView.ProcessKey (new KeyEvent (Key.R | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (expectedEventCount, eventcount); + + // Undo + expectedEventCount++; + Assert.True (_textView.ProcessKey (new KeyEvent (Key.Z | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (expectedEventCount, eventcount); + + // Redo + expectedEventCount++; + Assert.True (_textView.ProcessKey (new KeyEvent (Key.R | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (expectedEventCount, eventcount); + } + + [Fact] + public void ContentsChanged_Event_Fires_ClearHistoryChanges () + { + var eventcount = 0; + + var text = "This is the first line.\nThis is the second line.\nThis is the third line."; + var tv = new TextView { + Width = 50, + Height = 10, + Text = text, + ContentsChanged = (e) => { + eventcount++; + } + }; + + Assert.True (tv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()))); + Assert.Equal ($"{Environment.NewLine}This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.", tv.Text); + Assert.Equal (4, tv.Lines); + + var expectedEventCount = 1; // for ENTER key + Assert.Equal (expectedEventCount, eventcount); + + tv.ClearHistoryChanges (); + expectedEventCount = 2; + Assert.Equal (expectedEventCount, eventcount); + } + + [Fact] + public void ContentsChanged_Event_Fires_LoadStream () + { + var eventcount = 0; + + var tv = new TextView { + Width = 50, + Height = 10, + ContentsChanged = (e) => { + eventcount++; + } + }; + + var text = "This is the first line.\r\nThis is the second line.\r\n"; + tv.LoadStream (new System.IO.MemoryStream (System.Text.Encoding.ASCII.GetBytes (text))); + Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}", tv.Text); + + Assert.Equal (1, eventcount); + } + + [Fact] + public void ContentsChanged_Event_Fires_LoadFile () + { + var eventcount = 0; + + var tv = new TextView { + Width = 50, + Height = 10, + ContentsChanged = (e) => { + eventcount++; + } + }; + var fileName = "textview.txt"; + System.IO.File.WriteAllText (fileName, "This is the first line.\r\nThis is the second line.\r\n") ; + + tv.LoadFile (fileName); + Assert.Equal (1, eventcount); + Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}", tv.Text); + } } } \ No newline at end of file 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);