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 f9954d0bf..65c1c6778 100644 --- a/Terminal.Gui/Core/Application.cs +++ b/Terminal.Gui/Core/Application.cs @@ -309,6 +309,9 @@ namespace Terminal.Gui { /// public static bool UseSystemConsole; + // For Unit testing - ignores UseSystemConsole + internal static bool ForceFakeConsole; + /// /// Initializes a new instance of Application. /// @@ -329,29 +332,28 @@ namespace Terminal.Gui { /// into a single call. An applciation cam use /// without explicitly calling . /// - /// The to use. If not specified the default driver for the + /// + /// The to use. If not specified the default driver for the /// platform will be used (see , , and ). - /// Specifies the to use. - public static void Init (ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null) => Init (() => Toplevel.Create (), driver, mainLoopDriver, resetState: true); + /// + /// 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; - /// - /// Internal function for initializing a Terminal.Gui application with a factory object, - /// a , and . - /// - /// This is a low-level function; most applications will use as it is simpler. - /// - /// Specifies the factory funtion./> - /// The to use. If not specified the default driver for the - /// platform will be used (see , , and ). - /// Specifies the to use. - /// If (default) all state will be reset. - /// Set to to not reset the state (for when this function is called via - /// when - /// has not already been called. f - internal static void Init (Func topLevelFactory, ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null, bool resetState = true) + // 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; @@ -359,6 +361,7 @@ namespace Terminal.Gui { 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) { @@ -367,22 +370,29 @@ namespace Terminal.Gui { // System.Diagnostics.Debugger.Break (); //#endif - // Reset all class variables (Application is a singleton). - if (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; } 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) { @@ -395,7 +405,16 @@ namespace Terminal.Gui { } MainLoop = new MainLoop (mainLoopDriver); - Driver.Init (TerminalResized); + 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 (); @@ -1250,8 +1269,7 @@ namespace Terminal.Gui { /// Runs the application by calling /// with a new instance of the specified -derived class. /// - /// If has not arleady been called, this function will - /// call . + /// Calling first is not needed as this function will initialze the application. /// /// /// must be called when the application is closing (typically after Run> has @@ -1263,27 +1281,31 @@ namespace Terminal.Gui { /// /// /// The to use. If not specified the default driver for the - /// platform will be used (see , , and ). + /// 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() will eventually cause Application.Top to be set, via Begin() and SetCurrentAsTop() - Run (top, errorHandler); } else { - if (!_initialized && driver == null) { - throw new ArgumentException ("Init has not been called; a valid driver and mainloop must be provided"); - } - // Note in this case, we don't verify the type of the Toplevel created by new T(). - Init (() => new T (), Driver == null ? driver : Driver, Driver == null ? mainLoopDriver : null, resetState: false); + // Init() has NOT been called. + InternalInit (() => new T (), driver, mainLoopDriver, calledViaRunT: true); Run (Top, errorHandler); } } diff --git a/UICatalog/Scenarios/BackgroundWorkerCollection.cs b/UICatalog/Scenarios/BackgroundWorkerCollection.cs index 13ebcc1a4..2e566ebe5 100644 --- a/UICatalog/Scenarios/BackgroundWorkerCollection.cs +++ b/UICatalog/Scenarios/BackgroundWorkerCollection.cs @@ -12,10 +12,6 @@ namespace UICatalog.Scenarios { [ScenarioCategory ("Dialogs")] [ScenarioCategory ("Controls")] public class BackgroundWorkerCollection : Scenario { - public override void Init (ColorScheme colorScheme) - { - // Do not call Init as Application.Run will do it - } public override void Run () { diff --git a/UICatalog/Scenarios/Generic - Copy.cs b/UICatalog/Scenarios/Generic - Copy.cs new file mode 100644 index 000000000..98d421d7e --- /dev/null +++ b/UICatalog/Scenarios/Generic - Copy.cs @@ -0,0 +1,76 @@ +using Terminal.Gui; + +namespace UICatalog.Scenarios { + [ScenarioMetadata (Name: "Run Example", Description: "Illustrates using Application.Run to run a custom class")] + [ScenarioCategory ("Top Level Windows")] + public class RunTExample : Scenario { + public override void Setup () + { + // No need to call Init if Application.Run is used + } + + public override void Run () + { + Application.Run (); + } + + public class ExampleWindow : Window { + public TextField usernameText; + + public ExampleWindow () + { + Title = "Example App (Ctrl+Q to quit)"; + + // Create input components and labels + var usernameLabel = new Label () { + Text = "Username:" + }; + + usernameText = new TextField ("") { + // Position text field adjacent to the label + X = Pos.Right (usernameLabel) + 1, + + // Fill remaining horizontal space + Width = Dim.Fill (), + }; + + var passwordLabel = new Label () { + Text = "Password:", + X = Pos.Left (usernameLabel), + Y = Pos.Bottom (usernameLabel) + 1 + }; + + var passwordText = new TextField ("") { + Secret = true, + // align with the text box above + X = Pos.Left (usernameText), + Y = Pos.Top (passwordLabel), + Width = Dim.Fill (), + }; + + // Create login button + var btnLogin = new Button () { + Text = "Login", + Y = Pos.Bottom (passwordLabel) + 1, + // center the login button horizontally + X = Pos.Center (), + IsDefault = true, + }; + + // When login button is clicked display a message popup + btnLogin.Clicked += () => { + if (usernameText.Text == "admin" && passwordText.Text == "password") { + MessageBox.Query ("Login Successful", $"Username: {usernameText.Text}", "Ok"); + Application.RequestStop (); + } else { + MessageBox.ErrorQuery ("Error Logging In", "Incorrect username or password (hint: admin/password)", "Ok"); + } + }; + + // Add the views to the Window + Add (usernameLabel, usernameText, passwordLabel, passwordText, btnLogin); + } + } + + } +} \ No newline at end of file diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index d4a7594d5..979b9eed7 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -117,6 +117,7 @@ namespace UICatalog { // 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.Init (); Application.Run (); Application.Shutdown (); diff --git a/UnitTests/ApplicationTests.cs b/UnitTests/ApplicationTests.cs index ef1fd01e6..5d2551fc2 100644 --- a/UnitTests/ApplicationTests.cs +++ b/UnitTests/ApplicationTests.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Linq; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -100,7 +101,7 @@ namespace Terminal.Gui.Core { Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); Toplevel topLevel = null; - Assert.Throws (() => Application.Init (() => topLevel = new TestToplevel (), new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)))); + Assert.Throws (() => Application.InternalInit (() => topLevel = new TestToplevel (), new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)))); Shutdown (); Assert.Null (Application.Top); @@ -109,7 +110,7 @@ namespace Terminal.Gui.Core { // Now try the other way topLevel = null; - Application.Init (() => topLevel = new TestToplevel (), new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); + Application.InternalInit (() => topLevel = new TestToplevel (), new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); Assert.Throws (() => Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)))); Shutdown (); @@ -118,7 +119,7 @@ namespace Terminal.Gui.Core { Assert.Null (Application.MainLoop); Assert.Null (Application.Driver); } - + class TestToplevel : Toplevel { public TestToplevel () @@ -131,7 +132,7 @@ namespace Terminal.Gui.Core { public void Init_Begin_End_Cleans_Up () { Init (); - + // Begin will cause Run() to be called, which will call Begin(). Thus will block the tests // if we don't stop Application.Iteration = () => { @@ -145,7 +146,7 @@ namespace Terminal.Gui.Core { }; Application.NotifyNewRunState += NewRunStateFn; - Toplevel topLevel = new Toplevel(); + Toplevel topLevel = new Toplevel (); var rs = Application.Begin (topLevel); Assert.NotNull (rs); Assert.NotNull (runstate); @@ -181,7 +182,7 @@ namespace Terminal.Gui.Core { // NOTE: Run, when called after Init has been called behaves differently than // when called if Init has not been called. Toplevel topLevel = null; - Application.Init (() => topLevel = new TestToplevel (), new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); + Application.InternalInit (() => topLevel = new TestToplevel (), new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); Application.RunState runstate = null; Action NewRunStateFn = (rs) => { @@ -272,9 +273,9 @@ namespace Terminal.Gui.Core { Application.Iteration = () => { Application.RequestStop (); }; - + // Init has been called and we're passing no driver to Run. This is ok. - Application.Run (errorHandler: null); + Application.Run (); Shutdown (); @@ -284,14 +285,59 @@ namespace Terminal.Gui.Core { } [Fact] - public void Run_T_NoInit_Throws () + 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 NOT been called and we're passing no driver to Run. This is an error. - Assert.Throws (() => Application.Run (errorHandler: null, driver: null, mainLoopDriver: null)); + // 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 (); @@ -379,6 +425,9 @@ namespace Terminal.Gui.Core { Application.Shutdown (); Assert.Equal (3, count); } + + // TODO: Add tests for Run that test errorHandler + #endregion #region ShutdownTests @@ -407,7 +456,7 @@ namespace Terminal.Gui.Core { Assert.Null (SynchronizationContext.Current); } #endregion - + [Fact] [AutoInitShutdown] public void SetCurrentAsTop_Run_A_Not_Modal_Toplevel_Make_It_The_Current_Application_Top ()