Browse Source

Merge branch 'develop' into view-clear-fix

Tig 2 years ago
parent
commit
571a131490
58 changed files with 2784 additions and 1558 deletions
  1. 2 2
      .github/workflows/publish.yml
  2. 41 60
      Example/Example.cs
  3. 2 0
      Example/README.md
  4. 45 74
      README.md
  5. 38 12
      Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
  6. 11 6
      Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs
  7. 32 1
      Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
  8. 36 12
      Terminal.Gui/ConsoleDrivers/NetDriver.cs
  9. 71 8
      Terminal.Gui/ConsoleDrivers/WindowsDriver.cs
  10. 521 0
      Terminal.Gui/Core/ConsoleKeyMapping.cs
  11. 79 18
      Terminal.Gui/Core/Event.cs
  12. 40 20
      Terminal.Gui/Views/ContextMenu.cs
  13. 128 158
      Terminal.Gui/Views/Menu.cs
  14. 20 2
      Terminal.Gui/Views/ScrollView.cs
  15. 10 6
      Terminal.Gui/Views/TreeView.cs
  16. 4 0
      UICatalog/Properties/launchSettings.json
  17. 17 11
      UICatalog/Scenario.cs
  18. 1 4
      UICatalog/Scenarios/AllViewsTester.cs
  19. 0 1
      UICatalog/Scenarios/BordersComparisons.cs
  20. 1 11
      UICatalog/Scenarios/Clipping.cs
  21. 6 4
      UICatalog/Scenarios/CsvEditor.cs
  22. 2 4
      UICatalog/Scenarios/Editor.cs
  23. 1 1
      UICatalog/Scenarios/Keys.cs
  24. 12 9
      UICatalog/Scenarios/ListViewWithSelection.cs
  25. 1 5
      UICatalog/Scenarios/Notepad.cs
  26. 16 0
      UICatalog/Scenarios/TreeViewFileSystem.cs
  27. 251 0
      UICatalog/Scenarios/VkeyPacketSimulator.cs
  28. 1 4
      UICatalog/Scenarios/WindowsAndFrameViews.cs
  29. 116 249
      UICatalog/UICatalog.cs
  30. 0 25
      UnitTests/AssemblyInfo.cs
  31. 13 13
      UnitTests/ButtonTests.cs
  32. 19 19
      UnitTests/CheckboxTests.cs
  33. 10 10
      UnitTests/ComboBoxTests.cs
  34. 201 19
      UnitTests/ConsoleDriverTests.cs
  35. 44 30
      UnitTests/ContextMenuTests.cs
  36. 38 38
      UnitTests/DialogTests.cs
  37. 2 2
      UnitTests/DimTests.cs
  38. 18 204
      UnitTests/GraphViewTests.cs
  39. 11 11
      UnitTests/ListViewTests.cs
  40. 335 260
      UnitTests/MenuTests.cs
  41. 6 6
      UnitTests/MessageBoxTests.cs
  42. 2 2
      UnitTests/PanelViewTests.cs
  43. 6 6
      UnitTests/PosTests.cs
  44. 3 3
      UnitTests/RadioGroupTests.cs
  45. 12 14
      UnitTests/ScenarioTests.cs
  46. 10 10
      UnitTests/ScrollBarViewTests.cs
  47. 108 1
      UnitTests/ScrollViewTests.cs
  48. 2 2
      UnitTests/StatusBarTests.cs
  49. 31 31
      UnitTests/TabViewTests.cs
  50. 28 28
      UnitTests/TableViewTests.cs
  51. 230 0
      UnitTests/TestHelpers.cs
  52. 35 35
      UnitTests/TextFormatterTests.cs
  53. 35 35
      UnitTests/TextViewTests.cs
  54. 9 9
      UnitTests/TreeViewTests.cs
  55. 60 60
      UnitTests/ViewTests.cs
  56. 3 3
      UnitTests/WizardTests.cs
  57. BIN
      docfx/images/Example.png
  58. 8 0
      docfx/overrides/Terminal_Gui_Application.md

+ 2 - 2
.github/workflows/publish.yml

@@ -16,12 +16,12 @@ jobs:
         fetch-depth: 0 #fetch-depth is needed for GitVersion
 
     - name: Install and calculate the new version with GitVersion 
-      uses: gittools/actions/gitversion/[email protected]4
+      uses: gittools/actions/gitversion/[email protected]5
       with:
         versionSpec: 5.x
 
     - name: Determine Version
-      uses: gittools/actions/gitversion/[email protected]4
+      uses: gittools/actions/gitversion/[email protected]5
       id: gitversion # step id used as reference for output values
 
     - name: Display GitVersion outputs

+ 41 - 60
Example/Example.cs

@@ -1,76 +1,57 @@
-// A simple Terminal.Gui example in C# - using C# 9.0 Top-level statements
-// This is the same code found in the Termiminal Gui README.md file.
+// This is a simple example application.  For the full range of functionality
+// see the UICatalog project
 
-using Terminal.Gui;
-using NStack;
-
-Application.Init ();
+// A simple Terminal.Gui example in C# - using C# 9.0 Top-level statements
 
-// Creates the top-level window to show
-var win = new Window ("Example App") {
-	X = 0,
-	Y = 1, // Leave one row for the toplevel menu
+using Terminal.Gui;
 
-	// By using Dim.Fill(), this Window will automatically resize without manual intervention
-	Width = Dim.Fill (),
-	Height = Dim.Fill ()
-};
+// Initialize the console
+Application.Init();
 
-Application.Top.Add (win);
+// Creates the top-level window with border and title
+var win = new Window("Example App (Ctrl+Q to quit)");
 
-// Creates a menubar, the item "New" has a help menu.
-var menu = new MenuBar (new MenuBarItem [] {
-			new MenuBarItem ("_File", new MenuItem [] {
-				new MenuItem ("_New", "Creates a new file", null),
-				new MenuItem ("_Close", "",null),
-				new MenuItem ("_Quit", "", () => { if (Quit ()) Application.Top.Running = false; })
-			}),
-			new MenuBarItem ("_Edit", new MenuItem [] {
-				new MenuItem ("_Copy", "", null),
-				new MenuItem ("C_ut", "", null),
-				new MenuItem ("_Paste", "", null)
-			})
-		});
-Application.Top.Add (menu);
+// Create input components and labels
 
-static bool Quit ()
+var usernameLabel = new Label("Username:");
+var usernameText = new TextField("")
 {
-	var n = MessageBox.Query (50, 7, "Quit Example", "Are you sure you want to quit this example?", "Yes", "No");
-	return n == 0;
-}
+    // Position text field adjacent to label
+    X = Pos.Right(usernameLabel) + 1,
 
-var login = new Label ("Login: ") { X = 3, Y = 2 };
-var password = new Label ("Password: ") {
-	X = Pos.Left (login),
-	Y = Pos.Top (login) + 1
+    // Fill remaining horizontal space with a margin of 1
+    Width = Dim.Fill(1),
 };
-var loginText = new TextField ("") {
-	X = Pos.Right (password),
-	Y = Pos.Top (login),
-	Width = 40
+
+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),
 };
-var passText = new TextField ("") {
-	Secret = true,
-	X = Pos.Left (loginText),
-	Y = Pos.Top (password),
-	Width = Dim.Width (loginText)
+
+// Create login button
+var btnLogin = new Button("Login")
+{
+    Y = 4,
+    // center the login button horizontally
+    X = Pos.Center(),
+    IsDefault = true,
 };
 
-// Add the views to the main window, 
-win.Add (
-	// Using Computed Layout:
-	login, password, loginText, passText,
+// When login button is clicked display a message popup
+btnLogin.Clicked += () => MessageBox.Query("Logging In", "Login Successful", "Ok");
 
-	// Using Absolute Layout:
-	new CheckBox (3, 6, "Remember me"),
-	new RadioGroup (3, 8, new ustring [] { "_Personal", "_Company" }, 0),
-	new Button (3, 14, "Ok"),
-	new Button (10, 14, "Cancel"),
-	new Label (3, 18, "Press F9 or ESC plus 9 to activate the menubar")
+// Add all the views to the window
+win.Add(
+    usernameLabel, usernameText, passwordLabel, passwordText,btnLogin
 );
 
-// Run blocks until the user quits the application
-Application.Run ();
+// Show the application
+Application.Run(win);
 
-// Always bracket Application.Init with .Shutdown.
-Application.Shutdown ();
+// After the application exits, release and reset console for clean shutdown
+Application.Shutdown();

+ 2 - 0
Example/README.md

@@ -4,6 +4,8 @@ This example shows how to use the Terminal.Gui library to create a simple GUI ap
 
 This is the same code found in the Terminal.Gui README.md file.
 
+To explore the full range of functionality in Terminal.Gui, see the [UICatalog](../UICatalog) project
+
 See [README.md](https://github.com/gui-cs/Terminal.Gui) for a list of all Terminal.Gui samples.
 
 Note, the old `demo.cs` example has been deleted because it was not a very good example. It can still be found in the [git history](https://github.com/gui-cs/Terminal.Gui/tree/v1.8.2).

+ 45 - 74
README.md

@@ -61,99 +61,70 @@ See the [`Terminal.Gui/` README](https://github.com/gui-cs/Terminal.Gui/tree/mas
 
 ## Sample Usage in C#
 
+The following example shows a basic Terminal.Gui application written in C#:
+
 ```csharp
 // A simple Terminal.Gui example in C# - using C# 9.0 Top-level statements
 
 using Terminal.Gui;
-using NStack;
 
-Application.Init ();
+// Initialize the console
+Application.Init();
 
-// Creates the top-level window to show
-var win = new Window ("Example App") {
-	X = 0,
-	Y = 1, // Leave one row for the toplevel menu
+// Creates the top-level window with border and title
+var win = new Window("Example App (Ctrl+Q to quit)");
 
-	// By using Dim.Fill(), this Window will automatically resize without manual intervention
-	Width = Dim.Fill (),
-	Height = Dim.Fill ()
-};
+// Create input components and labels
 
-Application.Top.Add (win);
-
-// Creates a menubar, the item "New" has a help menu.
-var menu = new MenuBar (new MenuBarItem [] {
-			new MenuBarItem ("_File", new MenuItem [] {
-				new MenuItem ("_New", "Creates a new file", null),
-				new MenuItem ("_Close", "",null),
-				new MenuItem ("_Quit", "", () => { if (Quit ()) Application.Top.Running = false; })
-			}),
-			new MenuBarItem ("_Edit", new MenuItem [] {
-				new MenuItem ("_Copy", "", null),
-				new MenuItem ("C_ut", "", null),
-				new MenuItem ("_Paste", "", null)
-			})
-		});
-Application.Top.Add (menu);
-
-static bool Quit ()
+var usernameLabel = new Label("Username:");
+var usernameText = new TextField("")
 {
-	var n = MessageBox.Query (50, 7, "Quit Example", "Are you sure you want to quit this example?", "Yes", "No");
-	return n == 0;
-}
-
-var login = new Label ("Login: ") { X = 3, Y = 2 };
-var password = new Label ("Password: ") {
-	X = Pos.Left (login),
-	Y = Pos.Top (login) + 1
+    // Position text field adjacent to label
+    X = Pos.Right(usernameLabel) + 1,
+
+    // Fill remaining horizontal space with a margin of 1
+    Width = Dim.Fill(1),
 };
-var loginText = new TextField ("") {
-	X = Pos.Right (password),
-	Y = Pos.Top (login),
-	Width = 40
+
+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),
 };
-var passText = new TextField ("") {
-	Secret = true,
-	X = Pos.Left (loginText),
-	Y = Pos.Top (password),
-	Width = Dim.Width (loginText)
+
+// Create login button
+var btnLogin = new Button("Login")
+{
+    Y = 4,
+    // center the login button horizontally
+    X = Pos.Center(),
+    IsDefault = true,
 };
 
-// Add the views to the main window, 
-win.Add (
-	// Using Computed Layout:
-	login, password, loginText, passText,
-
-	// Using Absolute Layout:
-	new CheckBox (3, 6, "Remember me"),
-	new RadioGroup (3, 8, new ustring [] { "_Personal", "_Company" }, 0),
-	new Button (3, 14, "Ok"),
-	new Button (10, 14, "Cancel"),
-	new Label (3, 18, "Press F9 or ESC plus 9 to activate the menubar")
+// When login button is clicked display a message popup
+btnLogin.Clicked += () => MessageBox.Query("Logging In", "Login Successful", "Ok");
+
+// Add all the views to the window
+win.Add(
+    usernameLabel, usernameText, passwordLabel, passwordText,btnLogin
 );
 
-// Run blocks until the user quits the application
-Application.Run ();
+// Show the application
+Application.Run(win);
 
-// Always bracket Application.Init with .Shutdown.
-Application.Shutdown ();
+// After the application exits, release and reset console for clean shutdown
+Application.Shutdown();
 ```
 
-The example above shows adding views using both styles of layout supported by **Terminal.Gui**: **Absolute layout** and **[Computed layout](https://gui-cs.github.io/Terminal.Gui/articles/overview.html#layout)**.
-
-Alternatively, you can encapsulate the app behavior in a new `Window`-derived class, say `App.cs` containing the code above, and simplify your `Main` method to:
+When run the application looks as follows:
 
-```csharp
-using Terminal.Gui;
+![Simple Usage app](./docfx/images/Example.png)
 
-class Demo {
-	static void Main ()
-	{
-		Application.Run<App> ();
-		Application.Shutdown ();
-	}
-}
-```
+_Sample application running_
 
 ## Installing
 
@@ -184,4 +155,4 @@ Debates on architecture and design can be found in Issues tagged with [design](h
 
 ## History
 
-See [gui-cs](https://github.com/gui-cs/) for how this project came to be.
+See [gui-cs](https://github.com/gui-cs/) for how this project came to be.

+ 38 - 12
Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs

@@ -617,7 +617,7 @@ namespace Terminal.Gui {
 			return keyModifiers;
 		}
 
-		void ProcessInput (Action<KeyEvent> keyHandler, Action<KeyEvent> keyDownHandler, Action<KeyEvent> keyUpHandler, Action<MouseEvent> mouseHandler)
+		void ProcessInput ()
 		{
 			int wch;
 			var code = Curses.get_wch (out wch);
@@ -787,6 +787,8 @@ namespace Terminal.Gui {
 		}
 
 		Action<KeyEvent> keyHandler;
+		Action<KeyEvent> keyDownHandler;
+		Action<KeyEvent> keyUpHandler;
 		Action<MouseEvent> mouseHandler;
 
 		public override void PrepareToRun (MainLoop mainLoop, Action<KeyEvent> keyHandler, Action<KeyEvent> keyDownHandler, Action<KeyEvent> keyUpHandler, Action<MouseEvent> mouseHandler)
@@ -794,12 +796,14 @@ namespace Terminal.Gui {
 			// Note: Curses doesn't support keydown/up events and thus any passed keyDown/UpHandlers will never be called
 			Curses.timeout (0);
 			this.keyHandler = keyHandler;
+			this.keyDownHandler = keyDownHandler;
+			this.keyUpHandler = keyUpHandler;
 			this.mouseHandler = mouseHandler;
 
 			var mLoop = mainLoop.Driver as UnixMainLoop;
 
 			mLoop.AddWatch (0, UnixMainLoop.Condition.PollIn, x => {
-				ProcessInput (keyHandler, keyDownHandler, keyUpHandler, mouseHandler);
+				ProcessInput ();
 				return true;
 			});
 
@@ -1128,26 +1132,48 @@ namespace Terminal.Gui {
 			return false;
 		}
 
-		public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control)
+		public override void SendKeys (char keyChar, ConsoleKey consoleKey, bool shift, bool alt, bool control)
 		{
-			Key k;
+			Key key;
 
-			if ((shift || alt || control)
-				&& keyChar - (int)Key.Space >= (uint)Key.A && keyChar - (int)Key.Space <= (uint)Key.Z) {
-				k = (Key)(keyChar - (uint)Key.Space);
+			if (consoleKey == ConsoleKey.Packet) {
+				ConsoleModifiers mod = new ConsoleModifiers ();
+				if (shift) {
+					mod |= ConsoleModifiers.Shift;
+				}
+				if (alt) {
+					mod |= ConsoleModifiers.Alt;
+				}
+				if (control) {
+					mod |= ConsoleModifiers.Control;
+				}
+				var kchar = ConsoleKeyMapping.GetKeyCharFromConsoleKey (keyChar, mod, out uint ckey, out _);
+				key = ConsoleKeyMapping.MapConsoleKeyToKey ((ConsoleKey)ckey, out bool mappable);
+				if (mappable) {
+					key = (Key)kchar;
+				}
 			} else {
-				k = (Key)keyChar;
+				key = (Key)keyChar;
 			}
+
+			KeyModifiers km = new KeyModifiers ();
 			if (shift) {
-				k |= Key.ShiftMask;
+				if (keyChar == 0) {
+					key |= Key.ShiftMask;
+				}
+				km.Shift = shift;
 			}
 			if (alt) {
-				k |= Key.AltMask;
+				key |= Key.AltMask;
+				km.Alt = alt;
 			}
 			if (control) {
-				k |= Key.CtrlMask;
+				key |= Key.CtrlMask;
+				km.Ctrl = control;
 			}
-			keyHandler (new KeyEvent (k, MapKeyModifiers (k)));
+			keyDownHandler (new KeyEvent (key, km));
+			keyHandler (new KeyEvent (key, km));
+			keyUpHandler (new KeyEvent (key, km));
 		}
 
 		public override bool GetColors (int value, out Color foreground, out Color background)

+ 11 - 6
Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs

@@ -164,11 +164,13 @@ namespace Terminal.Gui {
 		//
 		//   T:System.IO.IOException:
 		//     An I/O error occurred.
+
+		static ConsoleColor _defaultBackgroundColor = ConsoleColor.Black;
+
 		/// <summary>
 		/// 
 		/// </summary>
 		public static ConsoleColor BackgroundColor { get; set; } = _defaultBackgroundColor;
-		static ConsoleColor _defaultBackgroundColor = ConsoleColor.Black;
 
 		//
 		// Summary:
@@ -187,11 +189,13 @@ namespace Terminal.Gui {
 		//
 		//   T:System.IO.IOException:
 		//     An I/O error occurred.
+
+		static ConsoleColor _defaultForegroundColor = ConsoleColor.Gray;
+
 		/// <summary>
 		/// 
 		/// </summary>
 		public static ConsoleColor ForegroundColor { get; set; } = _defaultForegroundColor;
-		static ConsoleColor _defaultForegroundColor = ConsoleColor.Gray;
 		//
 		// Summary:
 		//     Gets or sets the height of the buffer area.
@@ -541,6 +545,9 @@ namespace Terminal.Gui {
 		// Exceptions:
 		//   T:System.IO.IOException:
 		//     An I/O error occurred.
+
+		static char [,] _buffer = new char [WindowWidth, WindowHeight];
+
 		/// <summary>
 		/// 
 		/// </summary>
@@ -550,8 +557,6 @@ namespace Terminal.Gui {
 			SetCursorPosition (0, 0);
 		}
 
-		static char [,] _buffer = new char [WindowWidth, WindowHeight];
-
 		//
 		// Summary:
 		//     Copies a specified source area of the screen buffer to a specified destination
@@ -811,9 +816,9 @@ namespace Terminal.Gui {
 		public static ConsoleKeyInfo ReadKey (bool intercept)
 		{
 			if (MockKeyPresses.Count > 0) {
-				return MockKeyPresses.Pop();
+				return MockKeyPresses.Pop ();
 			} else {
-				return new ConsoleKeyInfo ('\0', (ConsoleKey)'\0', false,false,false);
+				return new ConsoleKeyInfo ('\0', (ConsoleKey)'\0', false, false, false);
 			}
 		}
 

+ 32 - 1
Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs

@@ -256,6 +256,22 @@ namespace Terminal.Gui {
 			currentAttribute = c;
 		}
 
+		public ConsoleKeyInfo FromVKPacketToKConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
+		{
+			if (consoleKeyInfo.Key != ConsoleKey.Packet) {
+				return consoleKeyInfo;
+			}
+
+			var mod = consoleKeyInfo.Modifiers;
+			var shift = (mod & ConsoleModifiers.Shift) != 0;
+			var alt = (mod & ConsoleModifiers.Alt) != 0;
+			var control = (mod & ConsoleModifiers.Control) != 0;
+
+			var keyChar = ConsoleKeyMapping.GetKeyCharFromConsoleKey (consoleKeyInfo.KeyChar, consoleKeyInfo.Modifiers, out uint virtualKey, out _);
+
+			return new ConsoleKeyInfo ((char)keyChar, (ConsoleKey)virtualKey, shift, alt, control);
+		}
+
 		Key MapKey (ConsoleKeyInfo keyInfo)
 		{
 			switch (keyInfo.Key) {
@@ -263,6 +279,8 @@ namespace Terminal.Gui {
 				return MapKeyModifiers (keyInfo, Key.Esc);
 			case ConsoleKey.Tab:
 				return keyInfo.Modifiers == ConsoleModifiers.Shift ? Key.BackTab : Key.Tab;
+			case ConsoleKey.Clear:
+				return MapKeyModifiers (keyInfo, Key.Clear);
 			case ConsoleKey.Home:
 				return MapKeyModifiers (keyInfo, Key.Home);
 			case ConsoleKey.End:
@@ -289,6 +307,8 @@ namespace Terminal.Gui {
 				return MapKeyModifiers (keyInfo, Key.DeleteChar);
 			case ConsoleKey.Insert:
 				return MapKeyModifiers (keyInfo, Key.InsertChar);
+			case ConsoleKey.PrintScreen:
+				return MapKeyModifiers (keyInfo, Key.PrintScreen);
 
 			case ConsoleKey.Oem1:
 			case ConsoleKey.Oem2:
@@ -318,6 +338,9 @@ namespace Terminal.Gui {
 				if (keyInfo.Modifiers == ConsoleModifiers.Alt) {
 					return (Key)(((uint)Key.AltMask) | ((uint)Key.A + delta));
 				}
+				if (keyInfo.Modifiers == (ConsoleModifiers.Shift | ConsoleModifiers.Alt)) {
+					return MapKeyModifiers (keyInfo, (Key)((uint)Key.A + delta));
+				}
 				if ((keyInfo.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) {
 					if (keyInfo.KeyChar == 0) {
 						return (Key)(((uint)Key.AltMask | (uint)Key.CtrlMask) | ((uint)Key.A + delta));
@@ -335,9 +358,14 @@ namespace Terminal.Gui {
 				if (keyInfo.Modifiers == ConsoleModifiers.Control) {
 					return (Key)(((uint)Key.CtrlMask) | ((uint)Key.D0 + delta));
 				}
-				if (keyInfo.KeyChar == 0 || keyInfo.KeyChar == 30) {
+				if (keyInfo.Modifiers == (ConsoleModifiers.Shift | ConsoleModifiers.Alt)) {
 					return MapKeyModifiers (keyInfo, (Key)((uint)Key.D0 + delta));
 				}
+				if ((keyInfo.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) {
+					if (keyInfo.KeyChar == 0 || keyInfo.KeyChar == 30) {
+						return MapKeyModifiers (keyInfo, (Key)((uint)Key.D0 + delta));
+					}
+				}
 				return (Key)((uint)keyInfo.KeyChar);
 			}
 			if (key >= ConsoleKey.F1 && key <= ConsoleKey.F12) {
@@ -387,6 +415,9 @@ namespace Terminal.Gui {
 
 		void ProcessInput (ConsoleKeyInfo consoleKey)
 		{
+			if (consoleKey.Key == ConsoleKey.Packet) {
+				consoleKey = FromVKPacketToKConsoleKeyInfo (consoleKey);
+			}
 			keyModifiers = new KeyModifiers ();
 			if (consoleKey.Modifiers.HasFlag (ConsoleModifiers.Shift)) {
 				keyModifiers.Shift = true;

+ 36 - 12
Terminal.Gui/ConsoleDrivers/NetDriver.cs

@@ -533,6 +533,7 @@ namespace Terminal.Gui {
 			int foundPoint = 0;
 			string value = "";
 			var kChar = GetKeyCharArray (cki);
+			//System.Diagnostics.Debug.WriteLine ($"kChar: {new string (kChar)}");
 			for (int i = 0; i < kChar.Length; i++) {
 				var c = kChar [i];
 				if (c == '<') {
@@ -560,6 +561,8 @@ namespace Terminal.Gui {
 					//	isButtonPressed = false;
 					//}
 
+					//System.Diagnostics.Debug.WriteLine ($"buttonCode: {buttonCode}");
+
 					switch (buttonCode) {
 					case 0:
 					case 8:
@@ -1610,6 +1613,22 @@ namespace Terminal.Gui {
 			currentAttribute = c;
 		}
 
+		public ConsoleKeyInfo FromVKPacketToKConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
+		{
+			if (consoleKeyInfo.Key != ConsoleKey.Packet) {
+				return consoleKeyInfo;
+			}
+
+			var mod = consoleKeyInfo.Modifiers;
+			var shift = (mod & ConsoleModifiers.Shift) != 0;
+			var alt = (mod & ConsoleModifiers.Alt) != 0;
+			var control = (mod & ConsoleModifiers.Control) != 0;
+
+			var keyChar = ConsoleKeyMapping.GetKeyCharFromConsoleKey (consoleKeyInfo.KeyChar, consoleKeyInfo.Modifiers, out uint virtualKey, out _);
+
+			return new ConsoleKeyInfo ((char)keyChar, (ConsoleKey)virtualKey, shift, alt, control);
+		}
+
 		Key MapKey (ConsoleKeyInfo keyInfo)
 		{
 			MapKeyModifiers (keyInfo, (Key)keyInfo.Key);
@@ -1687,7 +1706,7 @@ namespace Terminal.Gui {
 					return (Key)(((uint)Key.CtrlMask) | ((uint)Key.D0 + delta));
 				}
 				if ((keyInfo.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) {
-					if (keyInfo.KeyChar == 0 || keyInfo.KeyChar == 30) {
+					if (keyInfo.KeyChar == 0 || keyInfo.KeyChar == 30 || keyInfo.KeyChar == ((uint)Key.D0 + delta)) {
 						return MapKeyModifiers (keyInfo, (Key)((uint)Key.D0 + delta));
 					}
 				}
@@ -1754,14 +1773,23 @@ namespace Terminal.Gui {
 		{
 			switch (inputEvent.EventType) {
 			case NetEvents.EventType.Key:
+				ConsoleKeyInfo consoleKeyInfo = inputEvent.ConsoleKeyInfo;
+				if (consoleKeyInfo.Key == ConsoleKey.Packet) {
+					consoleKeyInfo = FromVKPacketToKConsoleKeyInfo (consoleKeyInfo);
+				}
 				keyModifiers = new KeyModifiers ();
-				var map = MapKey (inputEvent.ConsoleKeyInfo);
+				var map = MapKey (consoleKeyInfo);
 				if (map == (Key)0xffffffff) {
 					return;
 				}
-				keyDownHandler (new KeyEvent (map, keyModifiers));
-				keyHandler (new KeyEvent (map, keyModifiers));
-				keyUpHandler (new KeyEvent (map, keyModifiers));
+				if (map == Key.Null) {
+					keyDownHandler (new KeyEvent (map, keyModifiers));
+					keyUpHandler (new KeyEvent (map, keyModifiers));
+				} else {
+					keyDownHandler (new KeyEvent (map, keyModifiers));
+					keyHandler (new KeyEvent (map, keyModifiers));
+					keyUpHandler (new KeyEvent (map, keyModifiers));
+				}
 				break;
 			case NetEvents.EventType.Mouse:
 				mouseHandler (ToDriverMouse (inputEvent.MouseEvent));
@@ -1804,6 +1832,8 @@ namespace Terminal.Gui {
 
 		MouseEvent ToDriverMouse (NetEvents.MouseEvent me)
 		{
+			//System.Diagnostics.Debug.WriteLine ($"X: {me.Position.X}; Y: {me.Position.Y}; ButtonState: {me.ButtonState}");
+
 			MouseFlags mouseFlag = 0;
 
 			if ((me.ButtonState & NetEvents.MouseButtonState.Button1Pressed) != 0) {
@@ -1935,14 +1965,8 @@ namespace Terminal.Gui {
 		public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control)
 		{
 			NetEvents.InputResult input = new NetEvents.InputResult ();
-			ConsoleKey ck;
-			if (char.IsLetter (keyChar)) {
-				ck = key;
-			} else {
-				ck = (ConsoleKey)'\0';
-			}
 			input.EventType = NetEvents.EventType.Key;
-			input.ConsoleKeyInfo = new ConsoleKeyInfo (keyChar, ck, shift, alt, control);
+			input.ConsoleKeyInfo = new ConsoleKeyInfo (keyChar, key, shift, alt, control);
 
 			try {
 				ProcessInput (input);

+ 71 - 8
Terminal.Gui/ConsoleDrivers/WindowsDriver.cs

@@ -534,12 +534,14 @@ namespace Terminal.Gui {
 			public ConsoleKeyInfo consoleKeyInfo;
 			public bool CapsLock;
 			public bool NumLock;
+			public bool Scrolllock;
 
-			public ConsoleKeyInfoEx (ConsoleKeyInfo consoleKeyInfo, bool capslock, bool numlock)
+			public ConsoleKeyInfoEx (ConsoleKeyInfo consoleKeyInfo, bool capslock, bool numlock, bool scrolllock)
 			{
 				this.consoleKeyInfo = consoleKeyInfo;
 				CapsLock = capslock;
 				NumLock = numlock;
+				Scrolllock = scrolllock;
 			}
 		}
 
@@ -786,7 +788,26 @@ namespace Terminal.Gui {
 		{
 			switch (inputEvent.EventType) {
 			case WindowsConsole.EventType.Key:
+				var fromPacketKey = inputEvent.KeyEvent.wVirtualKeyCode == (uint)ConsoleKey.Packet;
+				if (fromPacketKey) {
+					inputEvent.KeyEvent = FromVKPacketToKeyEventRecord (inputEvent.KeyEvent);
+				}
 				var map = MapKey (ToConsoleKeyInfoEx (inputEvent.KeyEvent));
+				//var ke = inputEvent.KeyEvent;
+				//System.Diagnostics.Debug.WriteLine ($"fromPacketKey: {fromPacketKey}");
+				//if (ke.UnicodeChar == '\0') {
+				//	System.Diagnostics.Debug.WriteLine ("UnicodeChar: 0'\\0'");
+				//} else if (ke.UnicodeChar == 13) {
+				//	System.Diagnostics.Debug.WriteLine ("UnicodeChar: 13'\\n'");
+				//} else {
+				//	System.Diagnostics.Debug.WriteLine ($"UnicodeChar: {(uint)ke.UnicodeChar}'{ke.UnicodeChar}'");
+				//}
+				//System.Diagnostics.Debug.WriteLine ($"bKeyDown: {ke.bKeyDown}");
+				//System.Diagnostics.Debug.WriteLine ($"dwControlKeyState: {ke.dwControlKeyState}");
+				//System.Diagnostics.Debug.WriteLine ($"wRepeatCount: {ke.wRepeatCount}");
+				//System.Diagnostics.Debug.WriteLine ($"wVirtualKeyCode: {ke.wVirtualKeyCode}");
+				//System.Diagnostics.Debug.WriteLine ($"wVirtualScanCode: {ke.wVirtualScanCode}");
+
 				if (map == (Key)0xffffffff) {
 					KeyEvent key = new KeyEvent ();
 
@@ -854,6 +875,9 @@ namespace Terminal.Gui {
 						keyUpHandler (key);
 				} else {
 					if (inputEvent.KeyEvent.bKeyDown) {
+						// May occurs using SendKeys
+						if (keyModifiers == null)
+							keyModifiers = new KeyModifiers ();
 						// Key Down - Fire KeyDown Event and KeyStroke (ProcessKey) Event
 						keyDownHandler (new KeyEvent (map, keyModifiers));
 						keyHandler (new KeyEvent (map, keyModifiers));
@@ -861,7 +885,7 @@ namespace Terminal.Gui {
 						keyUpHandler (new KeyEvent (map, keyModifiers));
 					}
 				}
-				if (!inputEvent.KeyEvent.bKeyDown) {
+				if (!inputEvent.KeyEvent.bKeyDown && inputEvent.KeyEvent.dwControlKeyState == 0) {
 					keyModifiers = null;
 				}
 				break;
@@ -1242,7 +1266,38 @@ namespace Terminal.Gui {
 				keyModifiers.Scrolllock = scrolllock;
 
 			var ConsoleKeyInfo = new ConsoleKeyInfo (keyEvent.UnicodeChar, (ConsoleKey)keyEvent.wVirtualKeyCode, shift, alt, control);
-			return new WindowsConsole.ConsoleKeyInfoEx (ConsoleKeyInfo, capslock, numlock);
+
+			return new WindowsConsole.ConsoleKeyInfoEx (ConsoleKeyInfo, capslock, numlock, scrolllock);
+		}
+
+		public WindowsConsole.KeyEventRecord FromVKPacketToKeyEventRecord (WindowsConsole.KeyEventRecord keyEvent)
+		{
+			if (keyEvent.wVirtualKeyCode != (uint)ConsoleKey.Packet) {
+				return keyEvent;
+			}
+
+			var mod = new ConsoleModifiers ();
+			if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.ShiftPressed)) {
+				mod |= ConsoleModifiers.Shift;
+			}
+			if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightAltPressed) ||
+				keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftAltPressed)) {
+				mod |= ConsoleModifiers.Alt;
+			}
+			if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftControlPressed) ||
+				keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightControlPressed)) {
+				mod |= ConsoleModifiers.Control;
+			}
+			var keyChar = ConsoleKeyMapping.GetKeyCharFromConsoleKey (keyEvent.UnicodeChar, mod, out uint virtualKey, out uint scanCode);
+
+			return new WindowsConsole.KeyEventRecord {
+				UnicodeChar = (char)keyChar,
+				bKeyDown = keyEvent.bKeyDown,
+				dwControlKeyState = keyEvent.dwControlKeyState,
+				wRepeatCount = keyEvent.wRepeatCount,
+				wVirtualKeyCode = (ushort)virtualKey,
+				wVirtualScanCode = (ushort)scanCode
+			};
 		}
 
 		public Key MapKey (WindowsConsole.ConsoleKeyInfoEx keyInfoEx)
@@ -1253,6 +1308,8 @@ namespace Terminal.Gui {
 				return MapKeyModifiers (keyInfo, Key.Esc);
 			case ConsoleKey.Tab:
 				return keyInfo.Modifiers == ConsoleModifiers.Shift ? Key.BackTab : Key.Tab;
+			case ConsoleKey.Clear:
+				return MapKeyModifiers (keyInfo, Key.Clear);
 			case ConsoleKey.Home:
 				return MapKeyModifiers (keyInfo, Key.Home);
 			case ConsoleKey.End:
@@ -1279,6 +1336,8 @@ namespace Terminal.Gui {
 				return MapKeyModifiers (keyInfo, Key.DeleteChar);
 			case ConsoleKey.Insert:
 				return MapKeyModifiers (keyInfo, Key.InsertChar);
+			case ConsoleKey.PrintScreen:
+				return MapKeyModifiers (keyInfo, Key.PrintScreen);
 
 			case ConsoleKey.NumPad0:
 				return keyInfoEx.NumLock ? Key.D0 : Key.InsertChar;
@@ -1331,6 +1390,9 @@ namespace Terminal.Gui {
 				if (keyInfo.Modifiers == ConsoleModifiers.Alt) {
 					return (Key)(((uint)Key.AltMask) | ((uint)Key.A + delta));
 				}
+				if (keyInfo.Modifiers == (ConsoleModifiers.Shift | ConsoleModifiers.Alt)) {
+					return MapKeyModifiers (keyInfo, (Key)((uint)Key.A + delta));
+				}
 				if ((keyInfo.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) {
 					if (keyInfo.KeyChar == 0 || (keyInfo.KeyChar != 0 && keyInfo.KeyChar >= 1 && keyInfo.KeyChar <= 26)) {
 						return MapKeyModifiers (keyInfo, (Key)((uint)Key.A + delta));
@@ -1347,8 +1409,11 @@ namespace Terminal.Gui {
 				if (keyInfo.Modifiers == ConsoleModifiers.Control) {
 					return (Key)(((uint)Key.CtrlMask) | ((uint)Key.D0 + delta));
 				}
+				if (keyInfo.Modifiers == (ConsoleModifiers.Shift | ConsoleModifiers.Alt)) {
+					return MapKeyModifiers (keyInfo, (Key)((uint)Key.D0 + delta));
+				}
 				if ((keyInfo.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) {
-					if (keyInfo.KeyChar == 0 || keyInfo.KeyChar == 30) {
+					if (keyInfo.KeyChar == 0 || keyInfo.KeyChar == 30 || keyInfo.KeyChar == ((uint)Key.D0 + delta)) {
 						return MapKeyModifiers (keyInfo, (Key)((uint)Key.D0 + delta));
 					}
 				}
@@ -1369,7 +1434,7 @@ namespace Terminal.Gui {
 			return (Key)(0xffffffff);
 		}
 
-		Key MapKeyModifiers (ConsoleKeyInfo keyInfo, Key key)
+		private Key MapKeyModifiers (ConsoleKeyInfo keyInfo, Key key)
 		{
 			Key keyMod = new Key ();
 			if ((keyInfo.Modifiers & ConsoleModifiers.Shift) != 0)
@@ -1665,9 +1730,7 @@ namespace Terminal.Gui {
 			}
 
 			keyEvent.UnicodeChar = keyChar;
-			if ((shift || alt || control)
-				&& (key >= ConsoleKey.A && key <= ConsoleKey.Z
-				|| key >= ConsoleKey.D0 && key <= ConsoleKey.D9)) {
+			if ((uint)key < 255) {
 				keyEvent.wVirtualKeyCode = (ushort)key;
 			} else {
 				keyEvent.wVirtualKeyCode = '\0';

+ 521 - 0
Terminal.Gui/Core/ConsoleKeyMapping.cs

@@ -0,0 +1,521 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+
+namespace Terminal.Gui {
+	/// <summary>
+	/// Helper class to handle the scan code and virtual key from a <see cref="ConsoleKey"/>.
+	/// </summary>
+	public static class ConsoleKeyMapping {
+		private class ScanCodeMapping : IEquatable<ScanCodeMapping> {
+			public uint ScanCode;
+			public uint VirtualKey;
+			public ConsoleModifiers Modifiers;
+			public uint UnicodeChar;
+
+			public ScanCodeMapping (uint scanCode, uint virtualKey, ConsoleModifiers modifiers, uint unicodeChar)
+			{
+				ScanCode = scanCode;
+				VirtualKey = virtualKey;
+				Modifiers = modifiers;
+				UnicodeChar = unicodeChar;
+			}
+
+			public bool Equals (ScanCodeMapping other)
+			{
+				return (this.ScanCode.Equals (other.ScanCode) &&
+					this.VirtualKey.Equals (other.VirtualKey) &&
+					this.Modifiers.Equals (other.Modifiers) &&
+					this.UnicodeChar.Equals (other.UnicodeChar));
+			}
+		}
+
+		private static ConsoleModifiers GetModifiers (uint unicodeChar, ConsoleModifiers modifiers, bool isConsoleKey)
+		{
+			if (modifiers.HasFlag (ConsoleModifiers.Shift) &&
+				!modifiers.HasFlag (ConsoleModifiers.Alt) &&
+				!modifiers.HasFlag (ConsoleModifiers.Control)) {
+
+				return ConsoleModifiers.Shift;
+			} else if (modifiers == (ConsoleModifiers.Alt | ConsoleModifiers.Control)) {
+				return modifiers;
+			} else if ((!isConsoleKey || (isConsoleKey && (modifiers.HasFlag (ConsoleModifiers.Shift) ||
+				modifiers.HasFlag (ConsoleModifiers.Alt) || modifiers.HasFlag (ConsoleModifiers.Control)))) &&
+				unicodeChar >= 65 && unicodeChar <= 90) {
+
+				return ConsoleModifiers.Shift;
+			}
+			return 0;
+		}
+
+		private static ScanCodeMapping GetScanCode (string propName, uint keyValue, ConsoleModifiers modifiers)
+		{
+			switch (propName) {
+			case "UnicodeChar":
+				var sCode = scanCodes.FirstOrDefault ((e) => e.UnicodeChar == keyValue && e.Modifiers == modifiers);
+				if (sCode == null && modifiers == (ConsoleModifiers.Alt | ConsoleModifiers.Control)) {
+					return scanCodes.FirstOrDefault ((e) => e.UnicodeChar == keyValue && e.Modifiers == 0);
+				}
+				return sCode;
+			case "VirtualKey":
+				sCode = scanCodes.FirstOrDefault ((e) => e.VirtualKey == keyValue && e.Modifiers == modifiers);
+				if (sCode == null && modifiers == (ConsoleModifiers.Alt | ConsoleModifiers.Control)) {
+					return scanCodes.FirstOrDefault ((e) => e.VirtualKey == keyValue && e.Modifiers == 0);
+				}
+				return sCode;
+			}
+
+			return null;
+		}
+
+		/// <summary>
+		/// Get the <see cref="ConsoleKey"/> from a <see cref="Key"/>.
+		/// </summary>
+		/// <param name="keyValue">The key value.</param>
+		/// <param name="modifiers">The modifiers keys.</param>
+		/// <param name="scanCode">The resulting scan code.</param>
+		/// <param name="outputChar">The resulting output character.</param>
+		/// <returns>The <see cref="ConsoleKey"/> or the <paramref name="outputChar"/>.</returns>
+		public static uint GetConsoleKeyFromKey (uint keyValue, ConsoleModifiers modifiers, out uint scanCode, out uint outputChar)
+		{
+			scanCode = 0;
+			outputChar = keyValue;
+			if (keyValue == 0) {
+				return 0;
+			}
+
+			uint consoleKey = MapKeyToConsoleKey (keyValue, out bool mappable);
+			if (mappable) {
+				var mod = GetModifiers (keyValue, modifiers, false);
+				var scode = GetScanCode ("UnicodeChar", keyValue, mod);
+				if (scode != null) {
+					consoleKey = scode.VirtualKey;
+					scanCode = scode.ScanCode;
+					outputChar = scode.UnicodeChar;
+				} else {
+					consoleKey = consoleKey < 0xff ? (uint)(consoleKey & 0xff | 0xff << 8) : consoleKey;
+				}
+			} else {
+				var mod = GetModifiers (keyValue, modifiers, false);
+				var scode = GetScanCode ("VirtualKey", consoleKey, mod);
+				if (scode != null) {
+					consoleKey = scode.VirtualKey;
+					scanCode = scode.ScanCode;
+					outputChar = scode.UnicodeChar;
+				}
+			}
+
+			return consoleKey;
+		}
+
+		/// <summary>
+		/// Get the output character from the <see cref="ConsoleKey"/>.
+		/// </summary>
+		/// <param name="unicodeChar">The unicode character.</param>
+		/// <param name="modifiers">The modifiers keys.</param>
+		/// <param name="consoleKey">The resulting console key.</param>
+		/// <param name="scanCode">The resulting scan code.</param>
+		/// <returns>The output character or the <paramref name="consoleKey"/>.</returns>
+		public static uint GetKeyCharFromConsoleKey (uint unicodeChar, ConsoleModifiers modifiers, out uint consoleKey, out uint scanCode)
+		{
+			uint decodedChar = unicodeChar >> 8 == 0xff ? unicodeChar & 0xff : unicodeChar;
+			uint keyChar = decodedChar;
+			consoleKey = 0;
+			var mod = GetModifiers (decodedChar, modifiers, true);
+			scanCode = 0;
+			var scode = unicodeChar != 0 && unicodeChar >> 8 != 0xff ? GetScanCode ("VirtualKey", decodedChar, mod) : null;
+			if (scode != null) {
+				consoleKey = scode.VirtualKey;
+				keyChar = scode.UnicodeChar;
+				scanCode = scode.ScanCode;
+			}
+			if (scode == null) {
+				scode = unicodeChar != 0 ? GetScanCode ("UnicodeChar", decodedChar, mod) : null;
+				if (scode != null) {
+					consoleKey = scode.VirtualKey;
+					keyChar = scode.UnicodeChar;
+					scanCode = scode.ScanCode;
+				}
+			}
+			if (decodedChar != 0 && scanCode == 0 && char.IsLetter ((char)decodedChar)) {
+				string stFormD = ((char)decodedChar).ToString ().Normalize (System.Text.NormalizationForm.FormD);
+				for (int i = 0; i < stFormD.Length; i++) {
+					UnicodeCategory uc = CharUnicodeInfo.GetUnicodeCategory (stFormD [i]);
+					if (uc != UnicodeCategory.NonSpacingMark && uc != UnicodeCategory.OtherLetter) {
+						consoleKey = char.ToUpper (stFormD [i]);
+						scode = GetScanCode ("VirtualKey", char.ToUpper (stFormD [i]), 0);
+						if (scode != null) {
+							scanCode = scode.ScanCode;
+						}
+					}
+				}
+			}
+
+			return keyChar;
+		}
+
+		/// <summary>
+		/// Maps a <see cref="Key"/> to a <see cref="ConsoleKey"/>.
+		/// </summary>
+		/// <param name="keyValue">The key value.</param>
+		/// <param name="isMappable">If <see langword="true"/> is mapped to a valid character, otherwise <see langword="false"/>.</param>
+		/// <returns>The <see cref="ConsoleKey"/> or the <paramref name="keyValue"/>.</returns>
+		public static uint MapKeyToConsoleKey (uint keyValue, out bool isMappable)
+		{
+			isMappable = false;
+
+			switch ((Key)keyValue) {
+			case Key.Delete:
+				return (uint)ConsoleKey.Delete;
+			case Key.CursorUp:
+				return (uint)ConsoleKey.UpArrow;
+			case Key.CursorDown:
+				return (uint)ConsoleKey.DownArrow;
+			case Key.CursorLeft:
+				return (uint)ConsoleKey.LeftArrow;
+			case Key.CursorRight:
+				return (uint)ConsoleKey.RightArrow;
+			case Key.PageUp:
+				return (uint)ConsoleKey.PageUp;
+			case Key.PageDown:
+				return (uint)ConsoleKey.PageDown;
+			case Key.Home:
+				return (uint)ConsoleKey.Home;
+			case Key.End:
+				return (uint)ConsoleKey.End;
+			case Key.InsertChar:
+				return (uint)ConsoleKey.Insert;
+			case Key.DeleteChar:
+				return (uint)ConsoleKey.Delete;
+			case Key.F1:
+				return (uint)ConsoleKey.F1;
+			case Key.F2:
+				return (uint)ConsoleKey.F2;
+			case Key.F3:
+				return (uint)ConsoleKey.F3;
+			case Key.F4:
+				return (uint)ConsoleKey.F4;
+			case Key.F5:
+				return (uint)ConsoleKey.F5;
+			case Key.F6:
+				return (uint)ConsoleKey.F6;
+			case Key.F7:
+				return (uint)ConsoleKey.F7;
+			case Key.F8:
+				return (uint)ConsoleKey.F8;
+			case Key.F9:
+				return (uint)ConsoleKey.F9;
+			case Key.F10:
+				return (uint)ConsoleKey.F10;
+			case Key.F11:
+				return (uint)ConsoleKey.F11;
+			case Key.F12:
+				return (uint)ConsoleKey.F12;
+			case Key.F13:
+				return (uint)ConsoleKey.F13;
+			case Key.F14:
+				return (uint)ConsoleKey.F14;
+			case Key.F15:
+				return (uint)ConsoleKey.F15;
+			case Key.F16:
+				return (uint)ConsoleKey.F16;
+			case Key.F17:
+				return (uint)ConsoleKey.F17;
+			case Key.F18:
+				return (uint)ConsoleKey.F18;
+			case Key.F19:
+				return (uint)ConsoleKey.F19;
+			case Key.F20:
+				return (uint)ConsoleKey.F20;
+			case Key.F21:
+				return (uint)ConsoleKey.F21;
+			case Key.F22:
+				return (uint)ConsoleKey.F22;
+			case Key.F23:
+				return (uint)ConsoleKey.F23;
+			case Key.F24:
+				return (uint)ConsoleKey.F24;
+			case Key.BackTab:
+				return (uint)ConsoleKey.Tab;
+			case Key.Unknown:
+				isMappable = true;
+				return 0;
+			}
+			isMappable = true;
+
+			return keyValue;
+		}
+
+		/// <summary>
+		/// Maps a <see cref="ConsoleKey"/> to a <see cref="Key"/>.
+		/// </summary>
+		/// <param name="consoleKey">The console key.</param>
+		/// <param name="isMappable">If <see langword="true"/> is mapped to a valid character, otherwise <see langword="false"/>.</param>
+		/// <returns>The <see cref="Key"/> or the <paramref name="consoleKey"/>.</returns>
+		public static Key MapConsoleKeyToKey (ConsoleKey consoleKey, out bool isMappable)
+		{
+			isMappable = false;
+
+			switch (consoleKey) {
+			case ConsoleKey.Delete:
+				return Key.Delete;
+			case ConsoleKey.UpArrow:
+				return Key.CursorUp;
+			case ConsoleKey.DownArrow:
+				return Key.CursorDown;
+			case ConsoleKey.LeftArrow:
+				return Key.CursorLeft;
+			case ConsoleKey.RightArrow:
+				return Key.CursorRight;
+			case ConsoleKey.PageUp:
+				return Key.PageUp;
+			case ConsoleKey.PageDown:
+				return Key.PageDown;
+			case ConsoleKey.Home:
+				return Key.Home;
+			case ConsoleKey.End:
+				return Key.End;
+			case ConsoleKey.Insert:
+				return Key.InsertChar;
+			case ConsoleKey.F1:
+				return Key.F1;
+			case ConsoleKey.F2:
+				return Key.F2;
+			case ConsoleKey.F3:
+				return Key.F3;
+			case ConsoleKey.F4:
+				return Key.F4;
+			case ConsoleKey.F5:
+				return Key.F5;
+			case ConsoleKey.F6:
+				return Key.F6;
+			case ConsoleKey.F7:
+				return Key.F7;
+			case ConsoleKey.F8:
+				return Key.F8;
+			case ConsoleKey.F9:
+				return Key.F9;
+			case ConsoleKey.F10:
+				return Key.F10;
+			case ConsoleKey.F11:
+				return Key.F11;
+			case ConsoleKey.F12:
+				return Key.F12;
+			case ConsoleKey.F13:
+				return Key.F13;
+			case ConsoleKey.F14:
+				return Key.F14;
+			case ConsoleKey.F15:
+				return Key.F15;
+			case ConsoleKey.F16:
+				return Key.F16;
+			case ConsoleKey.F17:
+				return Key.F17;
+			case ConsoleKey.F18:
+				return Key.F18;
+			case ConsoleKey.F19:
+				return Key.F19;
+			case ConsoleKey.F20:
+				return Key.F20;
+			case ConsoleKey.F21:
+				return Key.F21;
+			case ConsoleKey.F22:
+				return Key.F22;
+			case ConsoleKey.F23:
+				return Key.F23;
+			case ConsoleKey.F24:
+				return Key.F24;
+			case ConsoleKey.Tab:
+				return Key.BackTab;
+			}
+			isMappable = true;
+
+			return (Key)consoleKey;
+		}
+
+		private static HashSet<ScanCodeMapping> scanCodes = new HashSet<ScanCodeMapping> {
+			new ScanCodeMapping (1,27,0,27),	// Escape
+			new ScanCodeMapping (1,27,ConsoleModifiers.Shift,27),
+			new ScanCodeMapping (2,49,0,49),	// D1
+			new ScanCodeMapping (2,49,ConsoleModifiers.Shift,33),
+			new ScanCodeMapping (3,50,0,50),	// D2
+			new ScanCodeMapping (3,50,ConsoleModifiers.Shift,34),
+			new ScanCodeMapping (3,50,ConsoleModifiers.Alt | ConsoleModifiers.Control,64),
+			new ScanCodeMapping (4,51,0,51),	// D3
+			new ScanCodeMapping (4,51,ConsoleModifiers.Shift,35),
+			new ScanCodeMapping (4,51,ConsoleModifiers.Alt | ConsoleModifiers.Control,163),
+			new ScanCodeMapping (5,52,0,52),	// D4
+			new ScanCodeMapping (5,52,ConsoleModifiers.Shift,36),
+			new ScanCodeMapping (5,52,ConsoleModifiers.Alt | ConsoleModifiers.Control,167),
+			new ScanCodeMapping (6,53,0,53),	// D5
+			new ScanCodeMapping (6,53,ConsoleModifiers.Shift,37),
+			new ScanCodeMapping (6,53,ConsoleModifiers.Alt | ConsoleModifiers.Control,8364),
+			new ScanCodeMapping (7,54,0,54),	// D6
+			new ScanCodeMapping (7,54,ConsoleModifiers.Shift,38),
+			new ScanCodeMapping (8,55,0,55),	// D7
+			new ScanCodeMapping (8,55,ConsoleModifiers.Shift,47),
+			new ScanCodeMapping (8,55,ConsoleModifiers.Alt | ConsoleModifiers.Control,123),
+			new ScanCodeMapping (9,56,0,56),	// D8
+			new ScanCodeMapping (9,56,ConsoleModifiers.Shift,40),
+			new ScanCodeMapping (9,56,ConsoleModifiers.Alt | ConsoleModifiers.Control,91),
+			new ScanCodeMapping (10,57,0,57),	// D9
+			new ScanCodeMapping (10,57,ConsoleModifiers.Shift,41),
+			new ScanCodeMapping (10,57,ConsoleModifiers.Alt | ConsoleModifiers.Control,93),
+			new ScanCodeMapping (11,48,0,48),	// D0
+			new ScanCodeMapping (11,48,ConsoleModifiers.Shift,61),
+			new ScanCodeMapping (11,48,ConsoleModifiers.Alt | ConsoleModifiers.Control,125),
+			new ScanCodeMapping (12,219,0,39),	// Oem4
+			new ScanCodeMapping (12,219,ConsoleModifiers.Shift,63),
+			new ScanCodeMapping (13,221,0,171),	// Oem6
+			new ScanCodeMapping (13,221,ConsoleModifiers.Shift,187),
+			new ScanCodeMapping (14,8,0,8),		// Backspace
+			new ScanCodeMapping (14,8,ConsoleModifiers.Shift,8),
+			new ScanCodeMapping (15,9,0,9),		// Tab
+			new ScanCodeMapping (15,9,ConsoleModifiers.Shift,15),
+			new ScanCodeMapping (16,81,0,113),	// Q
+			new ScanCodeMapping (16,81,ConsoleModifiers.Shift,81),
+			new ScanCodeMapping (17,87,0,119),	// W
+			new ScanCodeMapping (17,87,ConsoleModifiers.Shift,87),
+			new ScanCodeMapping (18,69,0,101),	// E
+			new ScanCodeMapping (18,69,ConsoleModifiers.Shift,69),
+			new ScanCodeMapping (19,82,0,114),	// R
+			new ScanCodeMapping (19,82,ConsoleModifiers.Shift,82),
+			new ScanCodeMapping (20,84,0,116),	// T
+			new ScanCodeMapping (20,84,ConsoleModifiers.Shift,84),
+			new ScanCodeMapping (21,89,0,121),	// Y
+			new ScanCodeMapping (21,89,ConsoleModifiers.Shift,89),
+			new ScanCodeMapping (22,85,0,117),	// U
+			new ScanCodeMapping (22,85,ConsoleModifiers.Shift,85),
+			new ScanCodeMapping (23,73,0,105),	// I
+			new ScanCodeMapping (23,73,ConsoleModifiers.Shift,73),
+			new ScanCodeMapping (24,79,0,111),	// O
+			new ScanCodeMapping (24,79,ConsoleModifiers.Shift,79),
+			new ScanCodeMapping (25,80,0,112),	// P
+			new ScanCodeMapping (25,80,ConsoleModifiers.Shift,80),
+			new ScanCodeMapping (26,187,0,43),	// OemPlus
+			new ScanCodeMapping (26,187,ConsoleModifiers.Shift,42),
+			new ScanCodeMapping (26,187,ConsoleModifiers.Alt | ConsoleModifiers.Control,168),
+			new ScanCodeMapping (27,186,0,180),	// Oem1
+			new ScanCodeMapping (27,186,ConsoleModifiers.Shift,96),
+			new ScanCodeMapping (28,13,0,13),	// Enter
+			new ScanCodeMapping (28,13,ConsoleModifiers.Shift,13),
+			new ScanCodeMapping (29,17,0,0),	// Control
+			new ScanCodeMapping (29,17,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (30,65,0,97),	// A
+			new ScanCodeMapping (30,65,ConsoleModifiers.Shift,65),
+			new ScanCodeMapping (31,83,0,115),	// S
+			new ScanCodeMapping (31,83,ConsoleModifiers.Shift,83),
+			new ScanCodeMapping (32,68,0,100),	// D
+			new ScanCodeMapping (32,68,ConsoleModifiers.Shift,68),
+			new ScanCodeMapping (33,70,0,102),	// F
+			new ScanCodeMapping (33,70,ConsoleModifiers.Shift,70),
+			new ScanCodeMapping (34,71,0,103),	// G
+			new ScanCodeMapping (34,71,ConsoleModifiers.Shift,71),
+			new ScanCodeMapping (35,72,0,104),	// H
+			new ScanCodeMapping (35,72,ConsoleModifiers.Shift,72),
+			new ScanCodeMapping (36,74,0,106),	// J
+			new ScanCodeMapping (36,74,ConsoleModifiers.Shift,74),
+			new ScanCodeMapping (37,75,0,107),	// K
+			new ScanCodeMapping (37,75,ConsoleModifiers.Shift,75),
+			new ScanCodeMapping (38,76,0,108),	// L
+			new ScanCodeMapping (38,76,ConsoleModifiers.Shift,76),
+			new ScanCodeMapping (39,192,0,231),	// Oem3
+			new ScanCodeMapping (39,192,ConsoleModifiers.Shift,199),
+			new ScanCodeMapping (40,222,0,186),	// Oem7
+			new ScanCodeMapping (40,222,ConsoleModifiers.Shift,170),
+			new ScanCodeMapping (41,220,0,92),	// Oem5
+			new ScanCodeMapping (41,220,ConsoleModifiers.Shift,124),
+			new ScanCodeMapping (42,16,0,0),	// LShift
+			new ScanCodeMapping (42,16,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (43,191,0,126),	// Oem2
+			new ScanCodeMapping (43,191,ConsoleModifiers.Shift,94),
+			new ScanCodeMapping (44,90,0,122),	// Z
+			new ScanCodeMapping (44,90,ConsoleModifiers.Shift,90),
+			new ScanCodeMapping (45,88,0,120),	// X
+			new ScanCodeMapping (45,88,ConsoleModifiers.Shift,88),
+			new ScanCodeMapping (46,67,0,99),	// C
+			new ScanCodeMapping (46,67,ConsoleModifiers.Shift,67),
+			new ScanCodeMapping (47,86,0,118),	// V
+			new ScanCodeMapping (47,86,ConsoleModifiers.Shift,86),
+			new ScanCodeMapping (48,66,0,98),	// B
+			new ScanCodeMapping (48,66,ConsoleModifiers.Shift,66),
+			new ScanCodeMapping (49,78,0,110),	// N
+			new ScanCodeMapping (49,78,ConsoleModifiers.Shift,78),
+			new ScanCodeMapping (50,77,0,109),	// M
+			new ScanCodeMapping (50,77,ConsoleModifiers.Shift,77),
+			new ScanCodeMapping (51,188,0,44),	// OemComma
+			new ScanCodeMapping (51,188,ConsoleModifiers.Shift,59),
+			new ScanCodeMapping (52,190,0,46),	// OemPeriod
+			new ScanCodeMapping (52,190,ConsoleModifiers.Shift,58),
+			new ScanCodeMapping (53,189,0,45),	// OemMinus
+			new ScanCodeMapping (53,189,ConsoleModifiers.Shift,95),
+			new ScanCodeMapping (54,16,0,0),	// RShift
+			new ScanCodeMapping (54,16,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (55,44,0,0),	// PrintScreen
+			new ScanCodeMapping (55,44,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (56,18,0,0),	// Alt
+			new ScanCodeMapping (56,18,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (57,32,0,32),	// Spacebar
+			new ScanCodeMapping (57,32,ConsoleModifiers.Shift,32),
+			new ScanCodeMapping (58,20,0,0),	// Caps
+			new ScanCodeMapping (58,20,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (59,112,0,0),	// F1
+			new ScanCodeMapping (59,112,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (60,113,0,0),	// F2
+			new ScanCodeMapping (60,113,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (61,114,0,0),	// F3
+			new ScanCodeMapping (61,114,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (62,115,0,0),	// F4
+			new ScanCodeMapping (62,115,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (63,116,0,0),	// F5
+			new ScanCodeMapping (63,116,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (64,117,0,0),	// F6
+			new ScanCodeMapping (64,117,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (65,118,0,0),	// F7
+			new ScanCodeMapping (65,118,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (66,119,0,0),	// F8
+			new ScanCodeMapping (66,119,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (67,120,0,0),	// F9
+			new ScanCodeMapping (67,120,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (68,121,0,0),	// F10
+			new ScanCodeMapping (68,121,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (69,144,0,0),	// Num
+			new ScanCodeMapping (69,144,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (70,145,0,0),	// Scroll
+			new ScanCodeMapping (70,145,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (71,36,0,0),	// Home
+			new ScanCodeMapping (71,36,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (72,38,0,0),	// UpArrow
+			new ScanCodeMapping (72,38,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (73,33,0,0),	// PageUp
+			new ScanCodeMapping (73,33,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (74,109,0,45),	// Subtract
+			new ScanCodeMapping (74,109,ConsoleModifiers.Shift,45),
+			new ScanCodeMapping (75,37,0,0),	// LeftArrow
+			new ScanCodeMapping (75,37,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (76,12,0,0),	// Center
+			new ScanCodeMapping (76,12,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (77,39,0,0),	// RightArrow
+			new ScanCodeMapping (77,39,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (78,107,0,43),	// Add
+			new ScanCodeMapping (78,107,ConsoleModifiers.Shift,43),
+			new ScanCodeMapping (79,35,0,0),	// End
+			new ScanCodeMapping (79,35,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (80,40,0,0),	// DownArrow
+			new ScanCodeMapping (80,40,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (81,34,0,0),	// PageDown
+			new ScanCodeMapping (81,34,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (82,45,0,0),	// Insert
+			new ScanCodeMapping (82,45,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (83,46,0,0),	// Delete
+			new ScanCodeMapping (83,46,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (86,226,0,60),	// OEM 102
+			new ScanCodeMapping (86,226,ConsoleModifiers.Shift,62),
+			new ScanCodeMapping (87,122,0,0),	// F11
+			new ScanCodeMapping (87,122,ConsoleModifiers.Shift,0),
+			new ScanCodeMapping (88,123,0,0),	// F12
+			new ScanCodeMapping (88,123,ConsoleModifiers.Shift,0)
+		};
+	}
+}

+ 79 - 18
Terminal.Gui/Core/Event.cs

@@ -77,11 +77,26 @@ namespace Terminal.Gui {
 		/// </summary>
 		Null = '\0',
 
+		/// <summary>
+		/// Backspace key.
+		/// </summary>
+		Backspace = 8,
+
+		/// <summary>
+		/// The key code for the user pressing the tab key (forwards tab key).
+		/// </summary>
+		Tab = 9,
+
 		/// <summary>
 		/// The key code for the user pressing the return key.
 		/// </summary>
 		Enter = '\n',
 
+		/// <summary>
+		/// The key code for the user pressing the clear key.
+		/// </summary>
+		Clear = 12,
+
 		/// <summary>
 		/// The key code for the user pressing the escape key
 		/// </summary>
@@ -363,15 +378,10 @@ namespace Terminal.Gui {
 		/// </summary>
 		CtrlMask = 0x40000000,
 
-		/// <summary>
-		/// Backspace key.
-		/// </summary>
-		Backspace = 0x100000,
-
 		/// <summary>
 		/// Cursor up key
 		/// </summary>
-		CursorUp,
+		CursorUp = 0x100000,
 		/// <summary>
 		/// Cursor down key.
 		/// </summary>
@@ -393,21 +403,33 @@ namespace Terminal.Gui {
 		/// </summary>
 		PageDown,
 		/// <summary>
-		/// Home key
+		/// Home key.
 		/// </summary>
 		Home,
 		/// <summary>
-		/// End key
+		/// End key.
 		/// </summary>
 		End,
+
+		/// <summary>
+		/// Insert character key.
+		/// </summary>
+		InsertChar,
+
 		/// <summary>
-		/// Delete character key
+		/// Delete character key.
 		/// </summary>
 		DeleteChar,
+
 		/// <summary>
-		/// Insert character key
+		/// Shift-tab key (backwards tab key).
 		/// </summary>
-		InsertChar,
+		BackTab,
+
+		/// <summary>
+		/// Print screen character key.
+		/// </summary>
+		PrintScreen,
 
 		/// <summary>
 		/// F1 key.
@@ -457,15 +479,54 @@ namespace Terminal.Gui {
 		/// F12 key.
 		/// </summary>
 		F12,
-
 		/// <summary>
-		/// The key code for the user pressing the tab key (forwards tab key).
+		/// F13 key.
 		/// </summary>
-		Tab,
+		F13,
 		/// <summary>
-		/// Shift-tab key (backwards tab key).
+		/// F14 key.
 		/// </summary>
-		BackTab,
+		F14,
+		/// <summary>
+		/// F15 key.
+		/// </summary>
+		F15,
+		/// <summary>
+		/// F16 key.
+		/// </summary>
+		F16,
+		/// <summary>
+		/// F17 key.
+		/// </summary>
+		F17,
+		/// <summary>
+		/// F18 key.
+		/// </summary>
+		F18,
+		/// <summary>
+		/// F19 key.
+		/// </summary>
+		F19,
+		/// <summary>
+		/// F20 key.
+		/// </summary>
+		F20,
+		/// <summary>
+		/// F21 key.
+		/// </summary>
+		F21,
+		/// <summary>
+		/// F22 key.
+		/// </summary>
+		F22,
+		/// <summary>
+		/// F23 key.
+		/// </summary>
+		F23,
+		/// <summary>
+		/// F24 key.
+		/// </summary>
+		F24,
 
 		/// <summary>
 		/// A key with an unknown mapping was raised.
@@ -480,7 +541,7 @@ namespace Terminal.Gui {
 		KeyModifiers keyModifiers;
 
 		/// <summary>
-		/// Symb olid definition for the key.
+		/// Symbolic definition for the key.
 		/// </summary>
 		public Key Key;
 
@@ -573,7 +634,7 @@ namespace Terminal.Gui {
 				msg += "Scrolllock-";
 			}
 
-			msg += $"{(((uint)this.KeyValue & (uint)Key.CharMask) > 27 ? $"{(char)this.KeyValue}" : $"{key}")}";
+			msg += $"{((Key)KeyValue != Key.Unknown && ((uint)this.KeyValue & (uint)Key.CharMask) > 27 ? $"{(char)this.KeyValue}" : $"{key}")}";
 
 			return msg;
 		}

+ 40 - 20
Terminal.Gui/Core/ContextMenu.cs → Terminal.Gui/Views/ContextMenu.cs

@@ -2,8 +2,24 @@
 
 namespace Terminal.Gui {
 	/// <summary>
-	/// A context menu window derived from <see cref="MenuBar"/> containing menu items
-	/// which can be opened in any position.
+	/// ContextMenu provides a pop-up menu that can be positioned anywhere within a <see cref="View"/>. 
+	/// ContextMenu is analogous to <see cref="MenuBar"/> and, once activated, works like a sub-menu 
+	/// of a <see cref="MenuBarItem"/> (but can be positioned anywhere).
+	/// <para>
+	/// By default, a ContextMenu with sub-menus is displayed in a cascading manner, where each sub-menu pops out of the ContextMenu frame
+	/// (either to the right or left, depending on where the ContextMenu is relative to the edge of the screen). By setting
+	/// <see cref="UseSubMenusSingleFrame"/> to <see langword="true"/>, this behavior can be changed such that all sub-menus are
+	/// drawn within the ContextMenu frame.
+	/// </para>
+	/// <para>
+	/// ContextMenus can be activated using the Shift-F10 key (by default; use the <see cref="Key"/> to change to another key).
+	/// </para>
+	/// <para>
+	/// Callers can cause the ContextMenu to be activated on a right-mouse click (or other interaction) by calling <see cref="Show()"/>.
+	/// </para>
+	/// <para>
+	/// ContextMenus are located using screen using screen coordinates and appear above all other Views.
+	/// </para>
 	/// </summary>
 	public sealed class ContextMenu : IDisposable {
 		private static MenuBar menuBar;
@@ -12,15 +28,15 @@ namespace Terminal.Gui {
 		private Toplevel container;
 
 		/// <summary>
-		/// Initialize a context menu with empty menu items.
+		/// Initializes a context menu with no menu items.
 		/// </summary>
 		public ContextMenu () : this (0, 0, new MenuBarItem ()) { }
 
 		/// <summary>
-		/// Initialize a context menu with menu items from a host <see cref="View"/>.
+		/// Initializes a context menu, with a <see cref="View"/> specifiying the parent/hose of the menu.
 		/// </summary>
 		/// <param name="host">The host view.</param>
-		/// <param name="menuItems">The menu items.</param>
+		/// <param name="menuItems">The menu items for the context menu.</param>
 		public ContextMenu (View host, MenuBarItem menuItems) :
 			this (host.Frame.X, host.Frame.Y, menuItems)
 		{
@@ -28,10 +44,10 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Initialize a context menu with menu items.
+		/// Initializes a context menu with menu items at a specific screen location.
 		/// </summary>
-		/// <param name="x">The left position.</param>
-		/// <param name="y">The top position.</param>
+		/// <param name="x">The left position (screen relative).</param>
+		/// <param name="y">The top position (screen relative).</param>
 		/// <param name="menuItems">The menu items.</param>
 		public ContextMenu (int x, int y, MenuBarItem menuItems)
 		{
@@ -48,7 +64,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Disposes the all the context menu objects instances.
+		/// Disposes the context menu object.
 		/// </summary>
 		public void Dispose ()
 		{
@@ -65,7 +81,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Open the <see cref="MenuItems"/> menu items.
+		/// Shows (opens) the ContextMenu, displaying the <see cref="MenuItem"/>s it contains.
 		/// </summary>
 		public void Show ()
 		{
@@ -110,13 +126,14 @@ namespace Terminal.Gui {
 			} else if (ForceMinimumPosToZero && position.Y < 0) {
 				position.Y = 0;
 			}
-
+			
 			menuBar = new MenuBar (new [] { MenuItems }) {
 				X = position.X,
 				Y = position.Y,
 				Width = 0,
 				Height = 0,
-				UseSubMenusSingleFrame = UseSubMenusSingleFrame
+				UseSubMenusSingleFrame = UseSubMenusSingleFrame,
+				Key = Key
 			};
 
 			menuBar.isContextMenuLoading = true;
@@ -138,7 +155,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Close the <see cref="MenuItems"/> menu items.
+		/// Hides (closes) the ContextMenu.
 		/// </summary>
 		public void Hide ()
 		{
@@ -157,7 +174,7 @@ namespace Terminal.Gui {
 		public event Action<MouseFlags> MouseFlagsChanged;
 
 		/// <summary>
-		/// Gets or set the menu position.
+		/// Gets or sets the menu position.
 		/// </summary>
 		public Point Position { get; set; }
 
@@ -167,7 +184,7 @@ namespace Terminal.Gui {
 		public MenuBarItem MenuItems { get; set; }
 
 		/// <summary>
-		/// The <see cref="Gui.Key"/> used to activate the context menu by keyboard.
+		/// <see cref="Gui.Key"/> specifies they keyboard key that will activate the context menu with the keyboard.
 		/// </summary>
 		public Key Key {
 			get => key;
@@ -179,7 +196,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// The <see cref="Gui.MouseFlags"/> used to activate the context menu by mouse.
+		/// <see cref="Gui.MouseFlags"/> specifies the mouse action used to activate the context menu by mouse.
 		/// </summary>
 		public MouseFlags MouseFlags {
 			get => mouseFlags;
@@ -191,7 +208,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Gets information whether menu is showing or not.
+		/// Gets whether the ContextMenu is showing or not.
 		/// </summary>
 		public static bool IsShow { get; private set; }
 
@@ -202,8 +219,9 @@ namespace Terminal.Gui {
 		public View Host { get; set; }
 
 		/// <summary>
-		/// Gets or sets whether forces the minimum position to zero
-		/// if the left or right position are negative.
+		/// Sets or gets whether the context menu be forced to the right, ensuring it is not clipped, if the x position 
+		/// is less than zero. The default is <see langword="true"/> which means the context menu will be forced to the right.
+		/// If set to <see langword="false"/>, the context menu will be clipped on the left if x is less than zero.
 		/// </summary>
 		public bool ForceMinimumPosToZero { get; set; } = true;
 
@@ -213,7 +231,9 @@ namespace Terminal.Gui {
 		public MenuBar MenuBar { get => menuBar; }
 
 		/// <summary>
-		/// Gets or sets if the sub-menus must be displayed in a single or multiple frames.
+		/// Gets or sets if sub-menus will be displayed using a "single frame" menu style. If <see langword="true"/>, the ContextMenu
+		/// and any sub-menus that would normally cascade will be displayed within a single frame. If <see langword="false"/> (the default),
+		/// sub-menus will cascade using separate frames for each level of the menu hierarchy.
 		/// </summary>
 		public bool UseSubMenusSingleFrame { get; set; }
 	}

+ 128 - 158
Terminal.Gui/Views/Menu.cs

@@ -1,13 +1,3 @@
-//
-// Menu.cs: application menus and submenus
-//
-// Authors:
-//   Miguel de Icaza ([email protected])
-//
-// TODO:
-//   Add accelerator support, but should also support chords (Shortcut in MenuItem)
-//   Allow menus inside menus
-
 using System;
 using NStack;
 using System.Linq;
@@ -21,23 +11,24 @@ namespace Terminal.Gui {
 	[Flags]
 	public enum MenuItemCheckStyle {
 		/// <summary>
-		/// The menu item will be shown normally, with no check indicator.
+		/// The menu item will be shown normally, with no check indicator. The default.
 		/// </summary>
 		NoCheck = 0b_0000_0000,
 
 		/// <summary>
-		/// The menu item will indicate checked/un-checked state (see <see cref="Checked"/>.
+		/// The menu item will indicate checked/un-checked state (see <see cref="Checked"/>).
 		/// </summary>
 		Checked = 0b_0000_0001,
 
 		/// <summary>
-		/// The menu item is part of a menu radio group (see <see cref="Checked"/> and will indicate selected state.
+		/// The menu item is part of a menu radio group (see <see cref="Checked"/>) and will indicate selected state.
 		/// </summary>
 		Radio = 0b_0000_0010,
 	};
 
 	/// <summary>
-	/// A <see cref="MenuItem"/> has a title, an associated help text, and an action to execute on activation.
+	/// A <see cref="MenuItem"/> has title, an associated help text, and an action to execute on activation. 
+	/// MenuItems can also have a checked indicator (see <see cref="Checked"/>).
 	/// </summary>
 	public class MenuItem {
 		ustring title;
@@ -78,14 +69,28 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// The HotKey is used when the menu is active, the shortcut can be triggered when the menu is not active.
-		/// For example HotKey would be "N" when the File Menu is open (assuming there is a "_New" entry
-		/// if the Shortcut is set to "Control-N", this would be a global hotkey that would trigger as well
+		/// The HotKey is used to activate a <see cref="MenuItem"/> with the keyboard. HotKeys are defined by prefixing the <see cref="Title"/>
+		/// of a MenuItem with an underscore ('_'). 
+		/// <para>
+		/// Pressing Alt-Hotkey for a <see cref="MenuBarItem"/> (menu items on the menu bar) works even if the menu is not active). 
+		/// Once a menu has focus and is active, pressing just the HotKey will activate the MenuItem.
+		/// </para>
+		/// <para>
+		/// For example for a MenuBar with a "_File" MenuBarItem that contains a "_New" MenuItem, Alt-F will open the File menu.
+		/// Pressing the N key will then activate the New MenuItem.
+		/// </para>
+		/// <para>
+		/// See also <see cref="Shortcut"/> which enable global key-bindings to menu items.
+		/// </para>
 		/// </summary>
 		public Rune HotKey;
 
 		/// <summary>
-		/// This is the global setting that can be used as a global <see cref="ShortcutHelper.Shortcut"/> to invoke the action on the menu.
+		/// Shortcut defines a key binding to the MenuItem that will invoke the MenuItem's action globally for the <see cref="View"/> that is
+		/// the parent of the <see cref="MenuBar"/> or <see cref="ContextMenu"/> this <see cref="MenuItem"/>.
+		/// <para>
+		/// The <see cref="Key"/> will be drawn on the MenuItem to the right of the <see cref="Title"/> and <see cref="Help"/> text. See <see cref="ShortcutTag"/>.
+		/// </para>
 		/// </summary>
 		public Key Shortcut {
 			get => shortcutHelper.Shortcut;
@@ -97,12 +102,12 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// The keystroke combination used in the <see cref="ShortcutHelper.ShortcutTag"/> as string.
+		/// Gets the text describing the keystroke combination defined by <see cref="Shortcut"/>.
 		/// </summary>
 		public ustring ShortcutTag => ShortcutHelper.GetShortcutTag (shortcutHelper.Shortcut);
 
 		/// <summary>
-		/// Gets or sets the title.
+		/// Gets or sets the title of the menu item .
 		/// </summary>
 		/// <value>The title.</value>
 		public ustring Title {
@@ -116,34 +121,46 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Gets or sets the help text for the menu item.
+		/// Gets or sets the help text for the menu item. The help text is drawn to the right of the <see cref="Title"/>.
 		/// </summary>
 		/// <value>The help text.</value>
 		public ustring Help { get; set; }
 
 		/// <summary>
-		/// Gets or sets the action to be invoked when the menu is triggered
+		/// Gets or sets the action to be invoked when the menu item is triggered.
 		/// </summary>
 		/// <value>Method to invoke.</value>
 		public Action Action { get; set; }
 
 		/// <summary>
-		/// Gets or sets the action to be invoked if the menu can be triggered
+		/// Gets or sets the action to be invoked to determine if the menu can be triggered. If <see cref="CanExecute"/> returns <see langword="true"/>
+		/// the menu item will be enabled. Otherwise, it will be disabled. 
 		/// </summary>
-		/// <value>Function to determine if action is ready to be executed.</value>
+		/// <value>Function to determine if the action is can be executed or not.</value>
 		public Func<bool> CanExecute { get; set; }
 
 		/// <summary>
-		/// Shortcut to check if the menu item is enabled
+		/// Returns <see langword="true"/> if the menu item is enabled. This method is a wrapper around <see cref="CanExecute"/>.
 		/// </summary>
 		public bool IsEnabled ()
 		{
 			return CanExecute == null ? true : CanExecute ();
 		}
 
-		internal int Width => 1 + TitleLength + (Help.ConsoleWidth > 0 ? Help.ConsoleWidth + 2 : 0) +
-			(Checked || CheckType.HasFlag (MenuItemCheckStyle.Checked) || CheckType.HasFlag (MenuItemCheckStyle.Radio) ? 2 : 0) +
-			(ShortcutTag.ConsoleWidth > 0 ? ShortcutTag.ConsoleWidth + 2 : 0) + 2;
+		// 
+		// ┌─────────────────────────────┐
+		// │ Quit  Quit UI Catalog  Ctrl+Q │
+		// └─────────────────────────────┘
+		// ┌─────────────────┐
+		// │ ◌ TopLevel Alt+T │
+		// └─────────────────┘
+		// TODO: Replace the `2` literals with named constants 
+		internal int Width => 1 + // space before Title
+			TitleLength +
+			2 + // space after Title - BUGBUG: This should be 1 
+			(Checked || CheckType.HasFlag (MenuItemCheckStyle.Checked) || CheckType.HasFlag (MenuItemCheckStyle.Radio) ? 2 : 0) + // check glyph + space 
+			(Help.ConsoleWidth > 0 ? 2 + Help.ConsoleWidth : 0) + // Two spaces before Help
+			(ShortcutTag.ConsoleWidth > 0 ? 2 + ShortcutTag.ConsoleWidth : 0); // Pad two spaces before shortcut tag (which are also aligned right)
 
 		/// <summary>
 		/// Sets or gets whether the <see cref="MenuItem"/> shows a check indicator or not. See <see cref="MenuItemCheckStyle"/>.
@@ -151,12 +168,12 @@ namespace Terminal.Gui {
 		public bool Checked { set; get; }
 
 		/// <summary>
-		/// Sets or gets the type selection indicator the menu item will be displayed with.
+		/// Sets or gets the <see cref="MenuItemCheckStyle"/> of a menu item where <see cref="Checked"/> is set to <see langword="true"/>.
 		/// </summary>
 		public MenuItemCheckStyle CheckType { get; set; }
 
 		/// <summary>
-		/// Gets or sets the parent for this <see cref="MenuItem"/>.
+		/// Gets the parent for this <see cref="MenuItem"/>.
 		/// </summary>
 		/// <value>The parent.</value>
 		public MenuItem Parent { get; internal set; }
@@ -167,7 +184,7 @@ namespace Terminal.Gui {
 		internal bool IsFromSubMenu { get { return Parent != null; } }
 
 		/// <summary>
-		/// Merely a debugging aid to see the interaction with main
+		/// Merely a debugging aid to see the interaction with main.
 		/// </summary>
 		public MenuItem GetMenuItem ()
 		{
@@ -175,7 +192,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Merely a debugging aid to see the interaction with main
+		/// Merely a debugging aid to see the interaction with main.
 		/// </summary>
 		public bool GetMenuBarItem ()
 		{
@@ -213,14 +230,15 @@ namespace Terminal.Gui {
 	}
 
 	/// <summary>
-	/// A <see cref="MenuBarItem"/> contains <see cref="MenuBarItem"/>s or <see cref="MenuItem"/>s.
+	/// <see cref="MenuBarItem"/> is a menu item on an app's <see cref="MenuBar"/>. 
+	/// MenuBarItems do not support <see cref="MenuItem.Shortcut"/>.
 	/// </summary>
 	public class MenuBarItem : MenuItem {
 		/// <summary>
 		/// Initializes a new <see cref="MenuBarItem"/> as a <see cref="MenuItem"/>.
 		/// </summary>
 		/// <param name="title">Title for the menu item.</param>
-		/// <param name="help">Help text to display.</param>
+		/// <param name="help">Help text to display. Will be displayed next to the Title surrounded by parentheses.</param>
 		/// <param name="action">Action to invoke when the menu item is activated.</param>
 		/// <param name="canExecute">Function to determine if the action can currently be executed.</param>
 		/// <param name="parent">The parent <see cref="MenuItem"/> of this if exist, otherwise is null.</param>
@@ -289,19 +307,6 @@ namespace Terminal.Gui {
 			}
 		}
 
-		//static int GetMaxTitleLength (MenuItem [] children)
-		//{
-		//	int maxLength = 0;
-		//	foreach (var item in children) {
-		//		int len = GetMenuBarItemLength (item.Title);
-		//		if (len > maxLength)
-		//			maxLength = len;
-		//		item.IsFromSubMenu = true;
-		//	}
-
-		//	return maxLength;
-		//}
-
 		void SetChildrensParent (MenuItem [] childrens)
 		{
 			foreach (var child in childrens) {
@@ -363,12 +368,6 @@ namespace Terminal.Gui {
 			Title = title;
 		}
 
-		///// <summary>
-		///// Gets or sets the title to display.
-		///// </summary>
-		///// <value>The title.</value>
-		//public ustring Title { get; set; }
-
 		/// <summary>
 		/// Gets or sets an array of <see cref="MenuItem"/> objects that are the children of this <see cref="MenuBarItem"/>
 		/// </summary>
@@ -391,8 +390,8 @@ namespace Terminal.Gui {
 			}
 			int minX = x;
 			int minY = y;
-			int maxW = (items.Max (z => z?.Width) ?? 0) + 2;
-			int maxH = items.Length + 2;
+			int maxW = (items.Max (z => z?.Width) ?? 0) + 2; // This 2 is frame border?
+			int maxH = items.Length + 2; // This 2 is frame border?
 			if (parent != null && x + maxW > Driver.Cols) {
 				minX = Math.Max (parent.Frame.Right - parent.Frame.Width - maxW, 0);
 			}
@@ -459,6 +458,7 @@ namespace Terminal.Gui {
 			return GetNormalColor ();
 		}
 
+		// Draws the Menu, within the Frame
 		public override void Redraw (Rect bounds)
 		{
 			Driver.SetAttribute (GetNormalColor ());
@@ -477,13 +477,14 @@ namespace Terminal.Gui {
 					Move (1, i + 1);
 
 				Driver.SetAttribute (DetermineColorSchemeFor (item, i));
-				for (int p = Bounds.X; p < Frame.Width - 2; p++) {
+				for (int p = Bounds.X; p < Frame.Width - 2; p++) { // This - 2 is for the border
 					if (p < 0)
 						continue;
 					if (item == null)
 						Driver.AddRune (Driver.HLine);
 					else if (i == 0 && p == 0 && host.UseSubMenusSingleFrame && item.Parent.Parent != null)
 						Driver.AddRune (Driver.LeftArrow);
+					// This `- 3` is left border + right border + one row in from right
 					else if (p == Frame.Width - 3 && barItems.SubMenu (barItems.Children [i]) != null)
 						Driver.AddRune (Driver.RightArrow);
 					else
@@ -527,6 +528,7 @@ namespace Terminal.Gui {
 							HotKeySpecifier = MenuBar.HotKeySpecifier,
 							Text = textToDraw
 						};
+						// The -3 is left/right border + one space (not sure what for)
 						tf.Draw (ViewToScreen (new Rect (2, i + 1, Frame.Width - 3, 1)),
 							i == current ? ColorScheme.Focus : GetNormalColor (),
 							i == current ? ColorScheme.HotFocus : ColorScheme.HotNormal,
@@ -630,7 +632,7 @@ namespace Terminal.Gui {
 					}
 				}
 			}
-			return false;
+			return host.ProcessHotKey (kb);
 		}
 
 		void RunSelected ()
@@ -832,17 +834,27 @@ namespace Terminal.Gui {
 		}
 	}
 
-
-
 	/// <summary>
-	/// Provides a menu bar with drop-down and cascading menus. 
+	///	<para>
+	/// Provides a menu bar that spans the top of a <see cref="Toplevel"/> View with drop-down and cascading menus. 
+	///	</para>
+	/// <para>
+	/// By default, any sub-sub-menus (sub-menus of the <see cref="MenuItem"/>s added to <see cref="MenuBarItem"/>s) 
+	/// are displayed in a cascading manner, where each sub-sub-menu pops out of the sub-menu frame
+	/// (either to the right or left, depending on where the sub-menu is relative to the edge of the screen). By setting
+	/// <see cref="UseSubMenusSingleFrame"/> to <see langword="true"/>, this behavior can be changed such that all sub-sub-menus are
+	/// drawn within a single frame below the MenuBar.
+	/// </para>
 	/// </summary>
 	/// <remarks>
 	///	<para>
-	///	The <see cref="MenuBar"/> appears on the first row of the terminal.
+	///	The <see cref="MenuBar"/> appears on the first row of the parent <see cref="Toplevel"/> View and uses the full width.
 	///	</para>
 	///	<para>
-	///	The <see cref="MenuBar"/> provides global hotkeys for the application.
+	///	The <see cref="MenuBar"/> provides global hotkeys for the application. See <see cref="MenuItem.HotKey"/>.
+	///	</para>
+	///	<para>
+	///	See also: <see cref="ContextMenu"/>
 	///	</para>
 	/// </remarks>
 	public class MenuBar : View {
@@ -850,7 +862,7 @@ namespace Terminal.Gui {
 		internal int selectedSub;
 
 		/// <summary>
-		/// Gets or sets the array of <see cref="MenuBarItem"/>s for the menu. Only set this when the <see cref="MenuBar"/> is visible.
+		/// Gets or sets the array of <see cref="MenuBarItem"/>s for the menu. Only set this after the <see cref="MenuBar"/> is visible.
 		/// </summary>
 		/// <value>The menu array.</value>
 		public MenuBarItem [] Menus { get; set; }
@@ -873,7 +885,7 @@ namespace Terminal.Gui {
 
 		static ustring shortcutDelimiter = "+";
 		/// <summary>
-		/// Used for change the shortcut delimiter separator.
+		/// Sets or gets the shortcut delimiter separator. The default is "+".
 		/// </summary>
 		public static ustring ShortcutDelimiter {
 			get => shortcutDelimiter;
@@ -893,6 +905,13 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Gets or sets if the sub-menus must be displayed in a single or multiple frames.
+		/// <para>
+		/// By default any sub-sub-menus (sub-menus of the main <see cref="MenuItem"/>s) are displayed in a cascading manner, 
+		/// where each sub-sub-menu pops out of the sub-menu frame
+		/// (either to the right or left, depending on where the sub-menu is relative to the edge of the screen). By setting
+		/// <see cref="UseSubMenusSingleFrame"/> to <see langword="true"/>, this behavior can be changed such that all sub-sub-menus are
+		/// drawn within a single frame below the MenuBar.
+		/// </para>		
 		/// </summary>
 		public bool UseSubMenusSingleFrame {
 			get => useSubMenusSingleFrame;
@@ -905,6 +924,11 @@ namespace Terminal.Gui {
 			}
 		}
 
+		/// <summary>
+		/// The <see cref="Gui.Key"/> used to activate the menu bar by keyboard.
+		/// </summary>
+		public Key Key { get; set; } = Key.F9;
+
 		/// <summary>
 		/// Initializes a new instance of the <see cref="MenuBar"/>.
 		/// </summary>
@@ -1024,6 +1048,14 @@ namespace Terminal.Gui {
 			isCleaning = false;
 		}
 
+		// The column where the MenuBar starts
+		static int xOrigin = 0;
+		// Spaces before the Title
+		static int leftPadding = 1;
+		// Spaces after the Title
+		static int rightPadding = 1;
+		// Spaces after the submenu Title, before Help
+		static int parensAroundHelp = 3;
 		///<inheritdoc/>
 		public override void Redraw (Rect bounds)
 		{
@@ -1033,7 +1065,7 @@ namespace Terminal.Gui {
 				Driver.AddRune (' ');
 
 			Move (1, 0);
-			int pos = 1;
+			int pos = 0;
 
 			for (int i = 0; i < Menus.Length; i++) {
 				var menu = Menus [i];
@@ -1041,17 +1073,14 @@ namespace Terminal.Gui {
 				Attribute hotColor, normalColor;
 				if (i == selected && IsMenuOpen) {
 					hotColor = i == selected ? ColorScheme.HotFocus : ColorScheme.HotNormal;
-					normalColor = i == selected ? ColorScheme.Focus :
-						GetNormalColor ();
-				} else if (openedByAltKey) {
+					normalColor = i == selected ? ColorScheme.Focus : GetNormalColor ();
+				} else { 
 					hotColor = ColorScheme.HotNormal;
 					normalColor = GetNormalColor ();
-				} else {
-					hotColor = GetNormalColor ();
-					normalColor = GetNormalColor ();
 				}
-				DrawHotString (menu.Help.IsEmpty ? $" {menu.Title}  " : $" {menu.Title}  {menu.Help}  ", hotColor, normalColor);
-				pos += 1 + menu.TitleLength + (menu.Help.ConsoleWidth > 0 ? menu.Help.ConsoleWidth + 2 : 0) + 2;
+				// Note Help on MenuBar is drawn with parens around it
+				DrawHotString (menu.Help.IsEmpty ? $" {menu.Title} " : $" {menu.Title} ({menu.Help}) ", hotColor, normalColor);
+				pos += leftPadding + menu.TitleLength + (menu.Help.ConsoleWidth > 0 ? leftPadding + menu.Help.ConsoleWidth + parensAroundHelp : 0) + rightPadding;
 			}
 			PositionCursor ();
 		}
@@ -1066,14 +1095,10 @@ namespace Terminal.Gui {
 			for (int i = 0; i < Menus.Length; i++) {
 				if (i == selected) {
 					pos++;
-					if (IsMenuOpen)
-						Move (pos + 1, 0);
-					else {
-						Move (pos + 1, 0);
-					}
+					Move (pos + 1, 0);
 					return;
 				} else {
-					pos += 1 + Menus [i].TitleLength + (Menus [i].Help.ConsoleWidth > 0 ? Menus [i].Help.ConsoleWidth + 2 : 0) + 2;
+					pos += leftPadding + Menus [i].TitleLength + (Menus [i].Help.ConsoleWidth > 0 ? Menus [i].Help.ConsoleWidth + parensAroundHelp : 0) + rightPadding;
 				}
 			}
 		}
@@ -1111,7 +1136,7 @@ namespace Terminal.Gui {
 		public event Action<MenuClosingEventArgs> MenuClosing;
 
 		/// <summary>
-		/// Raised when all the menu are closed.
+		/// Raised when all the menu is closed.
 		/// </summary>
 		public event Action MenuAllClosed;
 
@@ -1134,7 +1159,7 @@ namespace Terminal.Gui {
 		internal bool isMenuClosing;
 
 		/// <summary>
-		/// True if the menu is open; otherwise false.
+		/// <see langword="true"/> if the menu is open; otherwise <see langword="true"/>.
 		/// </summary>
 		public bool IsMenuOpen { get; protected set; }
 
@@ -1167,7 +1192,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Virtual method that will invoke the <see cref="MenuClosing"/>
+		/// Virtual method that will invoke the <see cref="MenuClosing"/>.
 		/// </summary>
 		/// <param name="currentMenu">The current menu to be closed.</param>
 		/// <param name="reopen">Whether the current menu will be reopen.</param>
@@ -1180,7 +1205,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Virtual method that will invoke the <see cref="MenuAllClosed"/>
+		/// Virtual method that will invoke the <see cref="MenuAllClosed"/>.
 		/// </summary>
 		public virtual void OnMenuAllClosed ()
 		{
@@ -1190,7 +1215,7 @@ namespace Terminal.Gui {
 		View lastFocused;
 
 		/// <summary>
-		/// Get the lasted focused view before open the menu.
+		/// Gets the view that was last focused before opening the menu.
 		/// </summary>
 		public View LastFocused { get; private set; }
 
@@ -1208,6 +1233,7 @@ namespace Terminal.Gui {
 			int pos = 0;
 			switch (subMenu) {
 			case null:
+				// Open a submenu below a MenuBar
 				lastFocused = lastFocused ?? (SuperView == null ? Application.Current.MostFocused : SuperView.MostFocused);
 				if (openSubMenu != null && !CloseMenu (false, true))
 					return;
@@ -1220,8 +1246,10 @@ namespace Terminal.Gui {
 					openMenu.Dispose ();
 				}
 
+				// This positions the submenu horizontally aligned with the first character of the
+				// menu it belongs to's text
 				for (int i = 0; i < index; i++)
-					pos += 1 + Menus [i].TitleLength + (Menus [i].Help.ConsoleWidth > 0 ? Menus [i].Help.ConsoleWidth + 2 : 0) + 2;
+					pos += Menus [i].TitleLength + (Menus [i].Help.ConsoleWidth > 0 ? Menus [i].Help.ConsoleWidth + 2 : 0) + leftPadding + rightPadding;
 				openMenu = new Menu (this, Frame.X + pos, Frame.Y + 1, Menus [index]);
 				openCurrentMenu = openMenu;
 				openCurrentMenu.previousSubFocused = openMenu;
@@ -1234,6 +1262,7 @@ namespace Terminal.Gui {
 				openMenu.SetFocus ();
 				break;
 			default:
+				// Opens a submenu next to another submenu (openSubMenu)
 				if (openSubMenu == null)
 					openSubMenu = new List<Menu> ();
 				if (sIndex > -1) {
@@ -1274,7 +1303,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Opens the current Menu programatically.
+		/// Opens the Menu programatically, as though the F9 key were pressed.
 		/// </summary>
 		public void OpenMenu ()
 		{
@@ -1356,7 +1385,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Closes the current Menu programatically, if open and not canceled.
+		/// Closes the Menu programmatically if open and not canceled (as though F9 were pressed).
 		/// </summary>
 		public bool CloseMenu (bool ignoreUseSubMenusSingleFrame = false)
 		{
@@ -1458,26 +1487,6 @@ namespace Terminal.Gui {
 			if (openSubMenu.Count > 0)
 				openCurrentMenu = openSubMenu.Last ();
 
-			//if (openMenu.Subviews.Count == 0)
-			//	return;
-			//if (index == 0) {
-			//	//SuperView.SetFocus (previousSubFocused);
-			//	FocusPrev ();
-			//	return;
-			//}
-
-			//for (int i = openMenu.Subviews.Count - 1; i > index; i--) {
-			//	isMenuClosing = true;
-			//	if (openMenu.Subviews.Count - 1 > 0)
-			//		SuperView.SetFocus (openMenu.Subviews [i - 1]);
-			//	else
-			//		SuperView.SetFocus (openMenu);
-			//	if (openMenu != null) {
-			//		Remove (openMenu.Subviews [i]);
-			//		openMenu.Remove (openMenu.Subviews [i]);
-			//	}
-			//	RemoveSubMenu (i);
-			//}
 			isMenuClosing = false;
 		}
 
@@ -1678,7 +1687,7 @@ namespace Terminal.Gui {
 		///<inheritdoc/>
 		public override bool ProcessHotKey (KeyEvent kb)
 		{
-			if (kb.Key == Key.F9) {
+			if (kb.Key == Key) {
 				if (!IsMenuOpen)
 					OpenMenu ();
 				else
@@ -1773,10 +1782,10 @@ namespace Terminal.Gui {
 			if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked || me.Flags == MouseFlags.Button1TripleClicked || me.Flags == MouseFlags.Button1Clicked ||
 				(me.Flags == MouseFlags.ReportMousePosition && selected > -1) ||
 				(me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) && selected > -1)) {
-				int pos = 1;
+				int pos = xOrigin;
 				int cx = me.X;
 				for (int i = 0; i < Menus.Length; i++) {
-					if (cx >= pos && cx < pos + 1 + Menus [i].TitleLength + Menus [i].Help.ConsoleWidth + 2) {
+					if (cx >= pos && cx < pos + leftPadding + Menus [i].TitleLength + Menus [i].Help.ConsoleWidth + rightPadding) {
 						if (me.Flags == MouseFlags.Button1Clicked) {
 							if (Menus [i].IsTopLevel) {
 								var menu = new Menu (this, i, 0, Menus [i]);
@@ -1805,7 +1814,7 @@ namespace Terminal.Gui {
 						}
 						return true;
 					}
-					pos += 1 + Menus [i].TitleLength + 2;
+					pos += leftPadding + Menus [i].TitleLength + rightPadding;
 				}
 			}
 			return false;
@@ -1878,47 +1887,6 @@ namespace Terminal.Gui {
 				handled = false;
 				return false;
 			}
-			//if (me.View != this && me.Flags != MouseFlags.Button1Pressed)
-			//	return true;
-			//else if (me.View != this && me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) {
-			//	Application.UngrabMouse ();
-			//	host.CloseAllMenus ();
-			//	return true;
-			//}
-
-
-			//if (!(me.View is MenuBar) && !(me.View is Menu) && me.Flags != MouseFlags.Button1Pressed))
-			//	return false;
-
-			//if (Application.MouseGrabView != null) {
-			//	if (me.View is MenuBar || me.View is Menu) {
-			//		me.X -= me.OfX;
-			//		me.Y -= me.OfY;
-			//		me.View.MouseEvent (me);
-			//		return true;
-			//	} else if (!(me.View is MenuBar || me.View is Menu) && me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) {
-			//		Application.UngrabMouse ();
-			//		CloseAllMenus ();
-			//	}
-			//} else if (!isMenuClosed && selected == -1 && me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) {
-			//	Application.GrabMouse (this);
-			//	return true;
-			//}
-
-			//if (Application.MouseGrabView != null) {
-			//	if (Application.MouseGrabView == me.View && me.View == current) {
-			//		me.X -= me.OfX;
-			//		me.Y -= me.OfY;
-			//	} else if (me.View != current && me.View is MenuBar && me.View is Menu) {
-			//		Application.UngrabMouse ();
-			//		Application.GrabMouse (me.View);
-			//	} else if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) {
-			//		Application.UngrabMouse ();
-			//		CloseMenu ();
-			//	}
-			//} else if ((!isMenuClosed && selected > -1)) {
-			//	Application.GrabMouse (current);
-			//}
 
 			handled = true;
 
@@ -1972,12 +1940,13 @@ namespace Terminal.Gui {
 		/// </summary>
 		public MenuBarItem NewMenuBarItem { get; set; }
 		/// <summary>
-		/// Flag that allows you to cancel the opening of the menu.
+		/// Flag that allows the cancellation of the event. If set to <see langword="true"/> in the
+		/// event handler, the event will be canceled. 
 		/// </summary>
 		public bool Cancel { get; set; }
 
 		/// <summary>
-		/// Initializes a new instance of <see cref="MenuOpeningEventArgs"/>
+		/// Initializes a new instance of <see cref="MenuOpeningEventArgs"/>.
 		/// </summary>
 		/// <param name="currentMenu">The current <see cref="MenuBarItem"/> parent.</param>
 		public MenuOpeningEventArgs (MenuBarItem currentMenu)
@@ -1996,7 +1965,7 @@ namespace Terminal.Gui {
 		public MenuBarItem CurrentMenu { get; }
 
 		/// <summary>
-		/// Indicates whether the current menu will be reopen.
+		/// Indicates whether the current menu will reopen.
 		/// </summary>
 		public bool Reopen { get; }
 
@@ -2006,15 +1975,16 @@ namespace Terminal.Gui {
 		public bool IsSubMenu { get; }
 
 		/// <summary>
-		/// Flag that allows you to cancel the opening of the menu.
+		/// Flag that allows the cancellation of the event. If set to <see langword="true"/> in the
+		/// event handler, the event will be canceled. 
 		/// </summary>
 		public bool Cancel { get; set; }
 
 		/// <summary>
-		/// Initializes a new instance of <see cref="MenuClosingEventArgs"/>
+		/// Initializes a new instance of <see cref="MenuClosingEventArgs"/>.
 		/// </summary>
 		/// <param name="currentMenu">The current <see cref="MenuBarItem"/> parent.</param>
-		/// <param name="reopen">Whether the current menu will be reopen.</param>
+		/// <param name="reopen">Whether the current menu will reopen.</param>
 		/// <param name="isSubMenu">Indicates whether it is a sub-menu.</param>
 		public MenuClosingEventArgs (MenuBarItem currentMenu, bool reopen, bool isSubMenu)
 		{

+ 20 - 2
Terminal.Gui/Views/ScrollView.cs

@@ -29,7 +29,13 @@ namespace Terminal.Gui {
 	/// </para>
 	/// </remarks>
 	public class ScrollView : View {
-		View contentView = null;
+		private class ContentView : View {
+			public ContentView (Rect frame) : base (frame)
+			{
+			}
+		}
+
+		ContentView contentView;
 		ScrollBarView vertical, horizontal;
 
 		/// <summary>
@@ -52,7 +58,7 @@ namespace Terminal.Gui {
 
 		void Initialize (Rect frame)
 		{
-			contentView = new View (frame);
+			contentView = new ContentView (frame);
 			vertical = new ScrollBarView (1, 0, isVertical: true) {
 				X = Pos.AnchorEnd (1),
 				Y = 0,
@@ -177,6 +183,12 @@ namespace Terminal.Gui {
 			set {
 				if (autoHideScrollBars != value) {
 					autoHideScrollBars = value;
+					if (Subviews.Contains (vertical)) {
+						vertical.AutoHideScrollBars = value;
+					}
+					if (Subviews.Contains (horizontal)) {
+						horizontal.AutoHideScrollBars = value;
+					}
 					SetNeedsDisplay ();
 				}
 			}
@@ -251,6 +263,8 @@ namespace Terminal.Gui {
 				SetNeedsLayout ();
 				if (value) {
 					base.Add (horizontal);
+					horizontal.ShowScrollIndicator = value;
+					horizontal.AutoHideScrollBars = autoHideScrollBars;
 					horizontal.OtherScrollBarView = vertical;
 					horizontal.OtherScrollBarView.ShowScrollIndicator = value;
 					horizontal.MouseEnter += View_MouseEnter;
@@ -290,6 +304,8 @@ namespace Terminal.Gui {
 				SetNeedsLayout ();
 				if (value) {
 					base.Add (vertical);
+					vertical.ShowScrollIndicator = value;
+					vertical.AutoHideScrollBars = autoHideScrollBars;
 					vertical.OtherScrollBarView = horizontal;
 					vertical.OtherScrollBarView.ShowScrollIndicator = value;
 					vertical.MouseEnter += View_MouseEnter;
@@ -322,10 +338,12 @@ namespace Terminal.Gui {
 				ShowHideScrollBars ();
 			} else {
 				if (ShowVerticalScrollIndicator) {
+					vertical.SetRelativeLayout (Bounds);
 					vertical.Redraw (vertical.Bounds);
 				}
 
 				if (ShowHorizontalScrollIndicator) {
+					horizontal.SetRelativeLayout (Bounds);
 					horizontal.Redraw (horizontal.Bounds);
 				}
 			}

+ 10 - 6
Terminal.Gui/Views/TreeView.cs

@@ -219,19 +219,23 @@ namespace Terminal.Gui {
 		/// <value></value>
 		public AspectGetterDelegate<T> AspectGetter { get; set; } = (o) => o.ToString () ?? "";
 
-		CursorVisibility desiredCursorVisibility = CursorVisibility.Default;
+		CursorVisibility desiredCursorVisibility = CursorVisibility.Invisible;
 
 		/// <summary>
-		/// Get / Set the wished cursor when the tree is focused
+		/// Get / Set the wished cursor when the tree is focused.
+		/// Only applies when <see cref="MultiSelect"/> is true.
+		/// Defaults to <see cref="CursorVisibility.Invisible"/>
 		/// </summary>
 		public CursorVisibility DesiredCursorVisibility {
-			get => desiredCursorVisibility;
+			get { 
+				return MultiSelect ? desiredCursorVisibility : CursorVisibility.Invisible;
+			}
 			set {
+				desiredCursorVisibility = value;
+
 				if (desiredCursorVisibility != value && HasFocus) {
-					Application.Driver.SetCursorVisibility (value);
+					Application.Driver.SetCursorVisibility (DesiredCursorVisibility);
 				}
-
-				desiredCursorVisibility = value;
 			}
 		}
 

+ 4 - 0
UICatalog/Properties/launchSettings.json

@@ -27,6 +27,10 @@
       "commandName": "Project",
       "commandLineArgs": "\"ProgressBar Styles\""
     },
+    "VkeyPacketSimulator": {
+      "commandName": "Project",
+      "commandLineArgs": "VkeyPacketSimulator"
+    },
     "WSL2": {
       "commandName": "Executable",
       "executablePath": "wsl",

+ 17 - 11
UICatalog/Scenario.cs

@@ -73,14 +73,11 @@ namespace UICatalog {
 		/// Overrides that do not call the base.<see cref="Run"/>, must call <see cref="Application.Init"/> before creating any views or calling other Terminal.Gui APIs.
 		/// </para>
 		/// </remarks>
-		public virtual void Init(Toplevel top, ColorScheme colorScheme)
+		public virtual void Init (Toplevel top, ColorScheme colorScheme)
 		{
 			Application.Init ();
 
-			Top = top;
-			if (Top == null) {
-				Top = Application.Top;
-			}
+			Top = top != null ? top : Application.Top;
 
 			Win = new Window ($"CTRL-Q to Close - Scenario: {GetName ()}") {
 				X = 0,
@@ -177,7 +174,14 @@ namespace UICatalog {
 		/// <returns>list of category names</returns>
 		public List<string> GetCategories () => ScenarioCategory.GetCategories (this.GetType ());
 
-		public override string ToString () => $"{GetName (),-30}{GetDescription ()}";
+		private static int _maxScenarioNameLen = 30;
+
+		/// <summary>
+		/// Gets the Scenario Name + Description with the Description padded
+		/// based on the longest known Scenario name.
+		/// </summary>
+		/// <returns></returns>
+		public override string ToString () => $"{GetName ().PadRight(_maxScenarioNameLen)}{GetDescription ()}";
 
 		/// <summary>
 		/// Override this to implement the <see cref="Scenario"/> setup logic (create controls, etc...). 
@@ -232,12 +236,14 @@ namespace UICatalog {
 		/// Returns an instance of each <see cref="Scenario"/> defined in the project. 
 		/// https://stackoverflow.com/questions/5411694/get-all-inherited-classes-of-an-abstract-class
 		/// </summary>
-		public static List<Type> GetDerivedClasses<T> ()
+		public static List<Scenario> GetScenarios ()
 		{
-			List<Type> objects = new List<Type> ();
-			foreach (Type type in typeof (T).Assembly.GetTypes ()
-			 .Where (myType => myType.IsClass && !myType.IsAbstract && myType.IsSubclassOf (typeof (T)))) {
-				objects.Add (type);
+			List<Scenario> objects = new List<Scenario> ();
+			foreach (Type type in typeof (Scenario).Assembly.ExportedTypes
+			 .Where (myType => myType.IsClass && !myType.IsAbstract && myType.IsSubclassOf (typeof (Scenario)))) {
+				var scenario = (Scenario)Activator.CreateInstance (type);
+				objects.Add (scenario);
+				_maxScenarioNameLen = Math.Max (_maxScenarioNameLen, scenario.GetName ().Length + 1);
 			}
 			return objects;
 		}

+ 1 - 4
UICatalog/Scenarios/AllViewsTester.cs

@@ -44,10 +44,7 @@ namespace UICatalog.Scenarios {
 		{
 			Application.Init ();
 
-			Top = top;
-			if (Top == null) {
-				Top = Application.Top;
-			}
+			Top = top != null ? top : Application.Top;
 
 			//Win = new Window ($"CTRL-Q to Close - Scenario: {GetName ()}") {
 			//	X = 0,

+ 0 - 1
UICatalog/Scenarios/BordersComparisons.cs

@@ -7,7 +7,6 @@ namespace UICatalog.Scenarios {
 	public class BordersComparisons : Scenario {
 		public override void Init (Toplevel top, ColorScheme colorScheme)
 		{
-			top.Dispose ();
 			Application.Init ();
 
 			top = Application.Top;

+ 1 - 11
UICatalog/Scenarios/Clipping.cs

@@ -11,19 +11,9 @@ namespace UICatalog.Scenarios {
 		{
 			Application.Init ();
 
-			Top = top;
-			if (Top == null) {
-				Top = Application.Top;
-			}
+			Top = top != null ? top : Application.Top;
 
 			Top.ColorScheme = Colors.Base;
-			//Win = new TopLevel($"CTRL-Q to Close - Scenario: {GetName ()}") {
-			//	X = 0,
-			//	Y = 0,
-			//	Width = Dim.Fill (),
-			//	Height = Dim.Fill ()
-			//};
-			//Top.Add (Win);
 		}
 
 		public override void Setup ()

+ 6 - 4
UICatalog/Scenarios/CsvEditor.cs

@@ -43,12 +43,14 @@ namespace UICatalog.Scenarios {
 				Height = Dim.Fill (1),
 			};
 
-			var menu = new MenuBar (new MenuBarItem [] {
-				new MenuBarItem ("_File", new MenuItem [] {
+			var fileMenu = new MenuBarItem ("_File", new MenuItem [] {
 					new MenuItem ("_Open CSV", "", () => Open()),
 					new MenuItem ("_Save", "", () => Save()),
-					new MenuItem ("_Quit", "", () => Quit()),
-				}),
+					new MenuItem ("_Quit", "Quits The App", () => Quit()),
+				});
+			//fileMenu.Help = "Help";
+			var menu = new MenuBar (new MenuBarItem [] {
+				fileMenu,
 				new MenuBarItem ("_Edit", new MenuItem [] {
 					new MenuItem ("_New Column", "", () => AddColumn()),
 					new MenuItem ("_New Row", "", () => AddRow()),

+ 2 - 4
UICatalog/Scenarios/Editor.cs

@@ -35,10 +35,7 @@ namespace UICatalog.Scenarios {
 		public override void Init (Toplevel top, ColorScheme colorScheme)
 		{
 			Application.Init ();
-			Top = top;
-			if (Top == null) {
-				Top = Application.Top;
-			}
+			Top = top != null ? top : Application.Top;
 
 			Win = new Window (_fileName ?? "Untitled") {
 				X = 0,
@@ -116,6 +113,7 @@ namespace UICatalog.Scenarios {
 					new MenuBarItem ("_Languages", GetSupportedCultures ())
 				})
 			});
+
 			Top.Add (menu);
 
 			var statusBar = new StatusBar (new StatusItem [] {

+ 1 - 1
UICatalog/Scenarios/Keys.cs

@@ -51,7 +51,7 @@ namespace UICatalog.Scenarios {
 		public override void Init (Toplevel top, ColorScheme colorScheme)
 		{
 			Application.Init ();
-			Top = top;
+			Top = top != null ? top : Application.Top;
 
 			Win = new TestWindow ($"CTRL-Q to Close - Scenario: {GetName ()}") {
 				X = 0,

+ 12 - 9
UICatalog/Scenarios/ListViewWithSelection.cs

@@ -3,6 +3,7 @@ using System;
 using System.Collections;
 using System.Collections.Generic;
 using System.Linq;
+using System.Text.Json.Nodes;
 using Terminal.Gui;
 using Attribute = Terminal.Gui.Attribute;
 
@@ -16,11 +17,13 @@ namespace UICatalog.Scenarios {
 		public CheckBox _allowMultipleCB;
 		public ListView _listView;
 
-		public List<Type> _scenarios = Scenario.GetDerivedClasses<Scenario>().OrderBy (t => Scenario.ScenarioMetadata.GetName (t)).ToList ();
+		public List<Scenario> _scenarios;
 
 		public override void Setup ()
 		{
-			_customRenderCB = new CheckBox ("Render with columns") {
+			_scenarios = Scenario.GetScenarios ().OrderBy (s => s.GetName ()).ToList ();
+
+			_customRenderCB = new CheckBox ("Use custom rendering") {
 				X = 0,
 				Y = 0,
 				Height = 1,
@@ -137,11 +140,11 @@ namespace UICatalog.Scenarios {
 		// This is basically the same implementation used by the UICatalog main window
 		internal class ScenarioListDataSource : IListDataSource {
 			int _nameColumnWidth = 30;
-			private List<Type> scenarios;
+			private List<Scenario> scenarios;
 			BitArray marks;
 			int count, len;
 
-			public List<Type> Scenarios {
+			public List<Scenario> Scenarios {
 				get => scenarios;
 				set {
 					if (value != null) {
@@ -163,14 +166,14 @@ namespace UICatalog.Scenarios {
 
 			public int Length => len;
 
-			public ScenarioListDataSource (List<Type> itemList) => Scenarios = itemList;
+			public ScenarioListDataSource (List<Scenario> itemList) => Scenarios = itemList;
 
 			public void Render (ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width, int start = 0)
 			{
 				container.Move (col, line);
 				// Equivalent to an interpolated string like $"{Scenarios[item].Name, -widtestname}"; if such a thing were possible
-				var s = String.Format (String.Format ("{{0,{0}}}", -_nameColumnWidth), Scenario.ScenarioMetadata.GetName (Scenarios [item]));
-				RenderUstr (driver, $"{s}  {Scenario.ScenarioMetadata.GetDescription (Scenarios [item])}", col, line, width, start);
+				var s = String.Format (String.Format ("{{0,{0}}}", -_nameColumnWidth), Scenarios [item].GetName ());
+				RenderUstr (driver, $"{s} ({Scenarios [item].GetDescription ()})", col, line, width, start);
 			}
 
 			public void SetMark (int item, bool value)
@@ -187,8 +190,8 @@ namespace UICatalog.Scenarios {
 
 				int maxLength = 0;
 				for (int i = 0; i < scenarios.Count; i++) {
-					var s = String.Format (String.Format ("{{0,{0}}}", -_nameColumnWidth), Scenario.ScenarioMetadata.GetName (Scenarios [i]));
-					var sc = $"{s}  {Scenario.ScenarioMetadata.GetDescription (Scenarios [i])}";
+					var s = String.Format (String.Format ("{{0,{0}}}", -_nameColumnWidth), Scenarios [i].GetName ());
+					var sc = $"{s}  {Scenarios [i].GetDescription ()}";
 					var l = sc.Length;
 					if (l > maxLength) {
 						maxLength = l;

+ 1 - 5
UICatalog/Scenarios/Notepad.cs

@@ -14,11 +14,7 @@ namespace UICatalog.Scenarios {
 		public override void Init (Toplevel top, ColorScheme colorScheme)
 		{
 			Application.Init ();
-
-			Top = top;
-			if (Top == null) {
-				Top = Application.Top;
-			}
+			Top = top != null ? top : Application.Top;
 			Top.ColorScheme = Colors.Base;
 		}
 

+ 16 - 0
UICatalog/Scenarios/TreeViewFileSystem.cs

@@ -25,6 +25,8 @@ namespace UICatalog.Scenarios {
 		private MenuItem miFullPaths;
 		private MenuItem miLeaveLastRow;
 		private MenuItem miCustomColors;
+		private MenuItem miCursor;
+		private MenuItem miMultiSelect;
 		private Terminal.Gui.Attribute green;
 		private Terminal.Gui.Attribute red;
 
@@ -54,6 +56,8 @@ namespace UICatalog.Scenarios {
 					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);
@@ -269,6 +273,18 @@ namespace UICatalog.Scenarios {
 			miLeaveLastRow.Checked = !miLeaveLastRow.Checked;
 			treeViewFiles.Style.LeaveLastRow = miLeaveLastRow.Checked;
 		}
+		private void SetCursor()
+		{
+			miCursor.Checked = !miCursor.Checked;
+			treeViewFiles.DesiredCursorVisibility = miCursor.Checked ? CursorVisibility.Default : CursorVisibility.Invisible;
+		}
+		private void SetMultiSelect()
+		{
+			miMultiSelect.Checked = !miMultiSelect.Checked;
+			treeViewFiles.MultiSelect = miMultiSelect.Checked;
+		}
+		
+
 		private void SetCustomColors()
 		{
 			var yellow = new ColorScheme

+ 251 - 0
UICatalog/Scenarios/VkeyPacketSimulator.cs

@@ -0,0 +1,251 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Terminal.Gui;
+
+namespace UICatalog.Scenarios {
+	[ScenarioMetadata (Name: "VkeyPacketSimulator", Description: "Simulates the Virtual Key Packet")]
+	[ScenarioCategory ("Keys")]
+	public class VkeyPacketSimulator : Scenario {
+		List<int> _keyboardStrokes = new List<int> ();
+		bool _outputStarted = false;
+		bool _wasUnknown = false;
+		static ManualResetEventSlim _stopOutput = new ManualResetEventSlim (false);
+
+		public override void Setup ()
+		{
+			var label = new Label ("Input") {
+				X = Pos.Center ()
+			};
+			Win.Add (label);
+
+			var btnInput = new Button ("Select Input") {
+				X = Pos.AnchorEnd (16),
+			};
+			Win.Add (btnInput);
+
+			const string ruler = "|123456789";
+
+			var inputHorizontalRuler = new Label ("") {
+				Y = Pos.Bottom (btnInput),
+				Width = Dim.Fill (),
+				ColorScheme = Colors.Error,
+				AutoSize = false
+			};
+			Win.Add (inputHorizontalRuler);
+
+			var inputVerticalRuler = new Label ("", TextDirection.TopBottom_LeftRight) {
+				Y = Pos.Bottom (btnInput),
+				Width = 1,
+				ColorScheme = Colors.Error,
+				AutoSize = false
+			};
+			Win.Add (inputVerticalRuler);
+
+			var tvInput = new TextView {
+				X = 1,
+				Y = Pos.Bottom (inputHorizontalRuler),
+				Width = Dim.Fill (),
+				Height = Dim.Percent (50) - 1
+			};
+			Win.Add (tvInput);
+
+			label = new Label ("Output") {
+				X = Pos.Center (),
+				Y = Pos.Bottom (tvInput)
+			};
+			Win.Add (label);
+
+			var btnOutput = new Button ("Select Output") {
+				X = Pos.AnchorEnd (17),
+				Y = Pos.Top (label)
+			};
+			Win.Add (btnOutput);
+
+			var outputHorizontalRuler = new Label ("") {
+				Y = Pos.Bottom (btnOutput),
+				Width = Dim.Fill (),
+				ColorScheme = Colors.Error,
+				AutoSize = false
+			};
+			Win.Add (outputHorizontalRuler);
+
+			var outputVerticalRuler = new Label ("", TextDirection.TopBottom_LeftRight) {
+				Y = Pos.Bottom (btnOutput),
+				Width = 1,
+				Height = Dim.Fill (),
+				ColorScheme = Colors.Error,
+				AutoSize = false
+			};
+			Win.Add (outputVerticalRuler);
+
+			var tvOutput = new TextView {
+				X = 1,
+				Y = Pos.Bottom (outputHorizontalRuler),
+				Width = Dim.Fill (),
+				Height = Dim.Fill (),
+				ReadOnly = true
+			};
+
+			tvOutput.KeyDown += (e) => {
+				//System.Diagnostics.Debug.WriteLine ($"Output - KeyDown: {e.KeyEvent.Key}");
+				e.Handled = true;
+				if (e.KeyEvent.Key == Key.Unknown) {
+					_wasUnknown = true;
+				}
+			};
+
+			tvOutput.KeyPress += (e) => {
+				//System.Diagnostics.Debug.WriteLine ($"Output - KeyPress - _keyboardStrokes: {_keyboardStrokes.Count}");
+				if (_outputStarted && _keyboardStrokes.Count > 0) {
+					var ev = ShortcutHelper.GetModifiersKey (e.KeyEvent);
+					//System.Diagnostics.Debug.WriteLine ($"Output - KeyPress: {ev}");
+					if (!tvOutput.ProcessKey (e.KeyEvent)) {
+						Application.MainLoop.Invoke (() => {
+							MessageBox.Query ("Keys", $"'{ShortcutHelper.GetShortcutTag (ev)}' pressed!", "Ok");
+						});
+					}
+					e.Handled = true;
+					_stopOutput.Set ();
+				}
+				//System.Diagnostics.Debug.WriteLine ($"Output - KeyPress - _keyboardStrokes: {_keyboardStrokes.Count}");
+			};
+
+			Win.Add (tvOutput);
+
+			tvInput.KeyDown += (e) => {
+				//System.Diagnostics.Debug.WriteLine ($"Input - KeyDown: {e.KeyEvent.Key}");
+				e.Handled = true;
+				if (e.KeyEvent.Key == Key.Unknown) {
+					_wasUnknown = true;
+				}
+			};
+
+			View.KeyEventEventArgs unknownChar = null;
+
+			tvInput.KeyPress += (e) => {
+				if (e.KeyEvent.Key == (Key.Q | Key.CtrlMask)) {
+					Application.RequestStop ();
+					return;
+				}
+				if (e.KeyEvent.Key == Key.Unknown) {
+					_wasUnknown = true;
+					e.Handled = true;
+					return;
+				}
+				if (_wasUnknown && _keyboardStrokes.Count == 1) {
+					_wasUnknown = false;
+				} else if (_wasUnknown && char.IsLetter ((char)e.KeyEvent.Key)) {
+					_wasUnknown = false;
+				} else if (!_wasUnknown && _keyboardStrokes.Count > 0) {
+					e.Handled = true;
+					return;
+				}
+				if (_keyboardStrokes.Count == 0) {
+					AddKeyboardStrokes (e);
+				} else {
+					_keyboardStrokes.Insert (0, 0);
+				}
+				var ev = ShortcutHelper.GetModifiersKey (e.KeyEvent);
+				//System.Diagnostics.Debug.WriteLine ($"Input - KeyPress: {ev}");
+				//System.Diagnostics.Debug.WriteLine ($"Input - KeyPress - _keyboardStrokes: {_keyboardStrokes.Count}");
+			};
+
+			tvInput.KeyUp += (e) => {
+				//System.Diagnostics.Debug.WriteLine ($"Input - KeyUp: {e.KeyEvent.Key}");
+				//var ke = e.KeyEvent;
+				var ke = ShortcutHelper.GetModifiersKey (e.KeyEvent);
+				if (_wasUnknown && (int)ke - (int)(ke & (Key.AltMask | Key.CtrlMask | Key.ShiftMask)) != 0) {
+					unknownChar = e;
+				}
+				e.Handled = true;
+				if (!_wasUnknown && _keyboardStrokes.Count > 0) {
+					_outputStarted = true;
+					tvOutput.ReadOnly = false;
+					tvOutput.SetFocus ();
+					tvOutput.SetNeedsDisplay ();
+
+					Task.Run (() => {
+						while (_outputStarted) {
+							try {
+								ConsoleModifiers mod = new ConsoleModifiers ();
+								if (ke.HasFlag (Key.ShiftMask)) {
+									mod |= ConsoleModifiers.Shift;
+								}
+								if (ke.HasFlag (Key.AltMask)) {
+									mod |= ConsoleModifiers.Alt;
+								}
+								if (ke.HasFlag (Key.CtrlMask)) {
+									mod |= ConsoleModifiers.Control;
+								}
+								for (int i = 0; i < _keyboardStrokes.Count; i++) {
+									var consoleKey = ConsoleKeyMapping.GetConsoleKeyFromKey ((uint)_keyboardStrokes [i], mod, out _, out _);
+									Application.Driver.SendKeys ((char)consoleKey, ConsoleKey.Packet, mod.HasFlag (ConsoleModifiers.Shift),
+										mod.HasFlag (ConsoleModifiers.Alt), mod.HasFlag (ConsoleModifiers.Control));
+								}
+								//}
+							} catch (Exception) {
+								Application.MainLoop.Invoke (() => {
+									MessageBox.ErrorQuery ("Error", "Couldn't send the keystrokes!", "Ok");
+									Application.RequestStop ();
+								});
+							}
+							_stopOutput.Wait ();
+							_stopOutput.Reset ();
+							_keyboardStrokes.RemoveAt (0);
+							if (_keyboardStrokes.Count == 0) {
+								_outputStarted = false;
+								Application.MainLoop.Invoke (() => {
+									tvOutput.ReadOnly = true;
+									tvInput.SetFocus ();
+								});
+							}
+						}
+						//System.Diagnostics.Debug.WriteLine ($"_outputStarted: {_outputStarted}");
+					});
+				}
+			};
+
+			btnInput.Clicked += () => {
+				if (!tvInput.HasFocus && _keyboardStrokes.Count == 0) {
+					tvInput.SetFocus ();
+				}
+			};
+
+			btnOutput.Clicked += () => {
+				if (!tvOutput.HasFocus && _keyboardStrokes.Count == 0) {
+					tvOutput.SetFocus ();
+				}
+			};
+
+			tvInput.SetFocus ();
+
+			Win.LayoutComplete += (_) => {
+				inputHorizontalRuler.Text = outputHorizontalRuler.Text = ruler.Repeat ((int)Math.Ceiling ((double)(inputHorizontalRuler.Bounds.Width) / (double)ruler.Length)) [0..(inputHorizontalRuler.Bounds.Width)];
+				inputVerticalRuler.Height = tvInput.Frame.Height + 1;
+				inputVerticalRuler.Text = ruler.Repeat ((int)Math.Ceiling ((double)(inputVerticalRuler.Bounds.Height) / (double)ruler.Length)) [0..(inputVerticalRuler.Bounds.Height)];
+				outputVerticalRuler.Text = ruler.Repeat ((int)Math.Ceiling ((double)(outputVerticalRuler.Bounds.Height) / (double)ruler.Length)) [0..(outputVerticalRuler.Bounds.Height)];
+			};
+		}
+
+		private void AddKeyboardStrokes (View.KeyEventEventArgs e)
+		{
+			var ke = e.KeyEvent;
+			var km = new KeyModifiers ();
+			if (ke.IsShift) {
+				km.Shift = true;
+			}
+			if (ke.IsAlt) {
+				km.Alt = true;
+			}
+			if (ke.IsCtrl) {
+				km.Ctrl = true;
+			}
+			var keyChar = ke.KeyValue;
+			var mK = (int)((Key)ke.KeyValue & (Key.AltMask | Key.CtrlMask | Key.ShiftMask));
+			keyChar &= ~mK;
+			_keyboardStrokes.Add (keyChar);
+		}
+	}
+}

+ 1 - 4
UICatalog/Scenarios/WindowsAndFrameViews.cs

@@ -10,10 +10,7 @@ namespace UICatalog.Scenarios {
 		{
 			Application.Init ();
 
-			Top = top;
-			if (Top == null) {
-				Top = Application.Top;
-			}
+			Top = top != null ? top : Application.Top;
 		}
 
 		public override void RequestStop ()

+ 116 - 249
UICatalog/UICatalog.cs

@@ -1,6 +1,5 @@
 using NStack;
 using System;
-using System.Collections;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Globalization;
@@ -46,56 +45,75 @@ namespace UICatalog {
 	/// UI Catalog is a comprehensive sample app and scenario library for <see cref="Terminal.Gui"/>
 	/// </summary>
 	public class UICatalogApp {
-		private static Toplevel _top;
-		private static MenuBar _menu;
 		private static int _nameColumnWidth;
 		private static FrameView _leftPane;
 		private static List<string> _categories;
 		private static ListView _categoryListView;
 		private static FrameView _rightPane;
-		private static List<Type> _scenarios;
+		private static List<Scenario> _scenarios;
 		private static ListView _scenarioListView;
 		private static StatusBar _statusBar;
 		private static StatusItem _capslock;
 		private static StatusItem _numlock;
 		private static StatusItem _scrolllock;
-		private static int _categoryListViewItem;
-		private static int _scenarioListViewItem;
 
-		private static Scenario _runningScenario = null;
+		// 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;
+
 		static void Main (string [] args)
 		{
 			Console.OutputEncoding = Encoding.Default;
 
-			if (Debugger.IsAttached)
+			if (Debugger.IsAttached) {
 				CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US");
+			}
 
-			_scenarios = Scenario.GetDerivedClasses<Scenario> ().OrderBy (t => Scenario.ScenarioMetadata.GetName (t)).ToList ();
+			_scenarios = Scenario.GetScenarios ();
 
 			if (args.Length > 0 && args.Contains ("-usc")) {
 				_useSystemConsole = true;
 				args = args.Where (val => val != "-usc").ToArray ();
 			}
 			if (args.Length > 0) {
-				var item = _scenarios.FindIndex (t => Scenario.ScenarioMetadata.GetName (t).Equals (args [0], StringComparison.OrdinalIgnoreCase));
-				_runningScenario = (Scenario)Activator.CreateInstance (_scenarios [item]);
+				var item = _scenarios.FindIndex (s => s.GetName ().Equals (args [0], StringComparison.OrdinalIgnoreCase));
+				_selectedScenario = (Scenario)Activator.CreateInstance (_scenarios [item].GetType ());
 				Application.UseSystemConsole = _useSystemConsole;
 				Application.Init ();
-				_runningScenario.Init (Application.Top, _baseColorScheme);
-				_runningScenario.Setup ();
-				_runningScenario.Run ();
-				_runningScenario = null;
+				_selectedScenario.Init (Application.Top, _colorScheme);
+				_selectedScenario.Setup ();
+				_selectedScenario.Run ();
+				_selectedScenario = null;
 				Application.Shutdown ();
 				return;
 			}
 
+			_aboutMessage = new StringBuilder ();
+			_aboutMessage.AppendLine (@"A comprehensive sample library for");
+			_aboutMessage.AppendLine (@"");
+			_aboutMessage.AppendLine (@"  _______                  _             _   _____       _  ");
+			_aboutMessage.AppendLine (@" |__   __|                (_)           | | / ____|     (_) ");
+			_aboutMessage.AppendLine (@"    | | ___ _ __ _ __ ___  _ _ __   __ _| || |  __ _   _ _  ");
+			_aboutMessage.AppendLine (@"    | |/ _ \ '__| '_ ` _ \| | '_ \ / _` | || | |_ | | | | | ");
+			_aboutMessage.AppendLine (@"    | |  __/ |  | | | | | | | | | | (_| | || |__| | |_| | | ");
+			_aboutMessage.AppendLine (@"    |_|\___|_|  |_| |_| |_|_|_| |_|\__,_|_(_)_____|\__,_|_| ");
+			_aboutMessage.AppendLine (@"");
+			_aboutMessage.AppendLine (@"https://github.com/gui-cs/Terminal.Gui");
+
 			Scenario scenario;
-			while ((scenario = GetScenarioToRun ()) != null) {
+			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
@@ -106,18 +124,12 @@ namespace UICatalog {
 				Responder.Instances.Clear ();
 #endif
 
-				scenario.Init (Application.Top, _baseColorScheme);
+				scenario.Init (Application.Top, _colorScheme);
 				scenario.Setup ();
 				scenario.Run ();
 
-				//static void LoadedHandler ()
-				//{
-				//	_rightPane.SetFocus ();
-				//	_top.Loaded -= LoadedHandler;
-				//}
-
-				//_top.Loaded += LoadedHandler;
-
+				// This call to Application.Shutdown brackets the Application.Init call
+				// made by Scenario.Init()
 				Application.Shutdown ();
 
 #if DEBUG_IDISPOSABLE
@@ -130,8 +142,6 @@ namespace UICatalog {
 #endif
 			}
 
-			Application.Shutdown ();
-
 #if DEBUG_IDISPOSABLE
 			// This proves that when the user exited the UI Catalog app
 			// it cleaned up properly.
@@ -143,33 +153,26 @@ namespace UICatalog {
 		}
 
 		/// <summary>
-		/// This shows the selection UI. Each time it is run, it calls Application.Init to reset everything.
+		/// Shows the UI Catalog selection UI. When the user selects a Scenario to run, the
+		/// UI Catalog main app UI is killed and the Scenario is run as though it were Application.Top. 
+		/// When the Scenario exits, this function exits.
 		/// </summary>
 		/// <returns></returns>
-		private static Scenario GetScenarioToRun ()
+		private static Scenario SelectScenario ()
 		{
 			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;
 
-			// Set this here because not initialized until driver is loaded
-			_baseColorScheme = Colors.Base;
-
-			StringBuilder aboutMessage = new StringBuilder ();
-			aboutMessage.AppendLine (@"A comprehensive sample library for");
-			aboutMessage.AppendLine (@"");
-			aboutMessage.AppendLine (@"  _______                  _             _   _____       _  ");
-			aboutMessage.AppendLine (@" |__   __|                (_)           | | / ____|     (_) ");
-			aboutMessage.AppendLine (@"    | | ___ _ __ _ __ ___  _ _ __   __ _| || |  __ _   _ _  ");
-			aboutMessage.AppendLine (@"    | |/ _ \ '__| '_ ` _ \| | '_ \ / _` | || | |_ | | | | | ");
-			aboutMessage.AppendLine (@"    | |  __/ |  | | | | | | | | | | (_| | || |__| | |_| | | ");
-			aboutMessage.AppendLine (@"    |_|\___|_|  |_| |_| |_|_|_| |_|\__,_|_(_)_____|\__,_|_| ");
-			aboutMessage.AppendLine (@"");
-			aboutMessage.AppendLine (@"https://github.com/gui-cs/Terminal.Gui");
-
-			_menu = new MenuBar (new MenuBarItem [] {
+			var menu = new MenuBar (new MenuBarItem [] {
 				new MenuBarItem ("_File", new MenuItem [] {
-					new MenuItem ("_Quit", "", () => Application.RequestStop(), null, null, Key.Q | Key.CtrlMask)
+					new MenuItem ("_Quit", "Quit UI Catalog", () => Application.RequestStop(), null, null, Key.Q | Key.CtrlMask)
 				}),
 				new MenuBarItem ("_Color Scheme", CreateColorSchemeMenuItems()),
 				new MenuBarItem ("Diag_nostics", CreateDiagnosticMenuItems()),
@@ -177,8 +180,8 @@ namespace UICatalog {
 					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),
-				})
+						"About UI Catalog", () =>  MessageBox.Query ("About UI Catalog", _aboutMessage.ToString(), "_Ok"), null, null, Key.CtrlMask | Key.A),
+				}),
 			});
 
 			_leftPane = new FrameView ("Categories") {
@@ -186,7 +189,7 @@ namespace UICatalog {
 				Y = 1, // for menu
 				Width = 25,
 				Height = Dim.Fill (1),
-				CanFocus = false,
+				CanFocus = true,
 				Shortcut = Key.CtrlMask | Key.C
 			};
 			_leftPane.Title = $"{_leftPane.Title} ({_leftPane.ShortcutTag})";
@@ -218,7 +221,7 @@ namespace UICatalog {
 			_rightPane.Title = $"{_rightPane.Title} ({_rightPane.ShortcutTag})";
 			_rightPane.ShortcutAction = () => _rightPane.SetFocus ();
 
-			_nameColumnWidth = Scenario.ScenarioMetadata.GetName (_scenarios.OrderByDescending (t => Scenario.ScenarioMetadata.GetName (t).Length).FirstOrDefault ()).Length;
+			_nameColumnWidth = _scenarios.OrderByDescending (s => s.GetName ().Length).FirstOrDefault ().GetName ().Length;
 
 			_scenarioListView = new ListView () {
 				X = 0,
@@ -232,9 +235,6 @@ namespace UICatalog {
 			_scenarioListView.OpenSelectedItem += _scenarioListView_OpenSelectedItem;
 			_rightPane.Add (_scenarioListView);
 
-			_categoryListView.SelectedItem = _categoryListViewItem;
-			_categoryListView.OnSelectedChanged ();
-
 			_capslock = new StatusItem (Key.CharMask, "Caps", null);
 			_numlock = new StatusItem (Key.CharMask, "Num", null);
 			_scrolllock = new StatusItem (Key.CharMask, "Scroll", null);
@@ -247,60 +247,73 @@ namespace UICatalog {
 				_numlock,
 				_scrolllock,
 				new StatusItem(Key.Q | Key.CtrlMask, "~CTRL-Q~ Quit", () => {
-					if (_runningScenario is null){
+					if (_selectedScenario is null){
 						// This causes GetScenarioToRun to return null
-						_runningScenario = null;
+						_selectedScenario = null;
 						Application.RequestStop();
 					} else {
-						_runningScenario.RequestStop();
+						_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);
-					_top.LayoutSubviews();
-					_top.SetChildNeedsDisplay();
+					Application.Top.LayoutSubviews();
+					Application.Top.SetChildNeedsDisplay();
 				}),
 				new StatusItem (Key.CharMask, Application.Driver.GetType ().Name, null),
 			};
 
-			SetColorScheme ();
-			_top = Application.Top;
-			_top.KeyDown += KeyDownHandler;
-			_top.Add (_menu);
-			_top.Add (_leftPane);
-			_top.Add (_rightPane);
-			_top.Add (_statusBar);
-
-			void TopHandler () {
-				if (_runningScenario != null) {
-					_runningScenario = 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 ();
 				}
-				_top.Loaded -= TopHandler;
+				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.Shutdown ();
+
+			return _selectedScenario;
+		}
+
+
+		/// <summary>
+		/// Launches the selected scenario, setting the global _selectedScenario
+		/// </summary>
+		/// <param name="e"></param>
+		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 ());
+
+				// Tell the main app to stop
+				Application.RequestStop ();
 			}
-			_top.Loaded += TopHandler;
-			// The following code was moved to the TopHandler event
-			//  because in the MainLoop.EventsPending (wait)
-			//  from the Application.RunLoop with the WindowsDriver
-			//  the OnReady event is triggered due the Focus event.
-			//  On CursesDriver and NetDriver the focus event won't be triggered
-			//  and if it's possible I don't know how to do it.
-			//void ReadyHandler ()
-			//{
-			//	if (!_isFirstRunning) {
-			//		_rightPane.SetFocus ();
-			//	}
-			//	_top.Ready -= ReadyHandler;
-			//}
-			//_top.Ready += ReadyHandler;
-
-			Application.Run (_top);
-			return _runningScenario;
 		}
 
 		static List<MenuItem []> CreateDiagnosticMenuItems ()
@@ -318,7 +331,7 @@ namespace UICatalog {
 		{
 			List<MenuItem> menuItems = new List<MenuItem> ();
 			var item = new MenuItem ();
-			item.Title = "_Disable/Enable Mouse";
+			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;
@@ -329,12 +342,13 @@ namespace UICatalog {
 
 			return menuItems.ToArray ();
 		}
-		private static MenuItem[] CreateKeybindings()
+		private static MenuItem [] CreateKeybindings ()
 		{
 
 			List<MenuItem> menuItems = new List<MenuItem> ();
 			var item = new MenuItem ();
-			item.Title = "Keybindings";
+			item.Title = "_Key Bindings";
+			item.Help = "Change which keys do what";
 			item.Action += () => {
 				var dlg = new KeyBindingsDialog ();
 				Application.Run (dlg);
@@ -409,7 +423,7 @@ namespace UICatalog {
 						}
 					}
 					ConsoleDriver.Diagnostics = _diagnosticFlags;
-					_top.SetNeedsDisplay ();
+					Application.Top.SetNeedsDisplay ();
 				};
 				menuItems.Add (item);
 			}
@@ -461,52 +475,9 @@ namespace UICatalog {
 					break;
 				}
 			}
-
-			//MenuItem CheckedMenuMenuItem (ustring menuItem, Action action, Func<bool> checkFunction)
-			//{
-			//	var mi = new MenuItem ();
-			//	mi.Title = menuItem;
-			//	mi.Shortcut = Key.AltMask + index.ToString () [0];
-			//	index++;
-			//	mi.CheckType |= MenuItemCheckStyle.Checked;
-			//	mi.Checked = checkFunction ();
-			//	mi.Action = () => {
-			//		action?.Invoke ();
-			//		mi.Title = menuItem;
-			//		mi.Checked = checkFunction ();
-			//	};
-			//	return mi;
-			//}
-
-			//return new MenuItem [] {
-			//	CheckedMenuMenuItem ("Use _System Console",
-			//		() => {
-			//			_useSystemConsole = !_useSystemConsole;
-			//		},
-			//		() => _useSystemConsole),
-			//	CheckedMenuMenuItem ("Diagnostics: _Frame Padding",
-			//		() => {
-			//			ConsoleDriver.Diagnostics ^= ConsoleDriver.DiagnosticFlags.FramePadding;
-			//			_top.SetNeedsDisplay ();
-			//		},
-			//		() => (ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FramePadding) == ConsoleDriver.DiagnosticFlags.FramePadding),
-			//	CheckedMenuMenuItem ("Diagnostics: Frame _Ruler",
-			//		() => {
-			//			ConsoleDriver.Diagnostics ^= ConsoleDriver.DiagnosticFlags.FrameRuler;
-			//			_top.SetNeedsDisplay ();
-			//		},
-			//		() => (ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FrameRuler) == ConsoleDriver.DiagnosticFlags.FrameRuler),
-			//};
 		}
 
-		static void SetColorScheme ()
-		{
-			_leftPane.ColorScheme = _baseColorScheme;
-			_rightPane.ColorScheme = _baseColorScheme;
-			_top?.SetNeedsDisplay ();
-		}
-
-		static ColorScheme _baseColorScheme;
+		static ColorScheme _colorScheme;
 		static MenuItem [] CreateColorSchemeMenuItems ()
 		{
 			List<MenuItem> menuItems = new List<MenuItem> ();
@@ -515,12 +486,12 @@ namespace UICatalog {
 				item.Title = $"_{sc.Key}";
 				item.Shortcut = Key.AltMask | (Key)sc.Key.Substring (0, 1) [0];
 				item.CheckType |= MenuItemCheckStyle.Radio;
-				item.Checked = sc.Value == _baseColorScheme;
+				item.Checked = sc.Value == _colorScheme;
 				item.Action += () => {
-					_baseColorScheme = sc.Value;
-					SetColorScheme ();
+					Application.Top.ColorScheme = _colorScheme = sc.Value;
+					Application.Top?.SetNeedsDisplay ();
 					foreach (var menuItem in menuItems) {
-						menuItem.Checked = menuItem.Title.Equals ($"_{sc.Key}") && sc.Value == _baseColorScheme;
+						menuItem.Checked = menuItem.Title.Equals ($"_{sc.Key}") && sc.Value == _colorScheme;
 					}
 				};
 				menuItems.Add (item);
@@ -528,106 +499,8 @@ namespace UICatalog {
 			return menuItems.ToArray ();
 		}
 
-		private static void _scenarioListView_OpenSelectedItem (EventArgs e)
-		{
-			if (_runningScenario is null) {
-				_scenarioListViewItem = _scenarioListView.SelectedItem;
-				var source = _scenarioListView.Source as ScenarioListDataSource;
-				_runningScenario = (Scenario)Activator.CreateInstance (source.Scenarios [_scenarioListView.SelectedItem]);
-				Application.RequestStop ();
-			}
-		}
-
-		internal class ScenarioListDataSource : IListDataSource {
-			private readonly int len;
-
-			public List<Type> Scenarios { get; set; }
-
-			public bool IsMarked (int item) => false;
-
-			public int Count => Scenarios.Count;
-
-			public int Length => len;
-
-			public ScenarioListDataSource (List<Type> itemList)
-			{
-				Scenarios = itemList;
-				len = GetMaxLengthItem ();
-			}
-
-			public void Render (ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width, int start = 0)
-			{
-				container.Move (col, line);
-				// Equivalent to an interpolated string like $"{Scenarios[item].Name, -widtestname}"; if such a thing were possible
-				var s = String.Format (String.Format ("{{0,{0}}}", -_nameColumnWidth), Scenario.ScenarioMetadata.GetName (Scenarios [item]));
-				RenderUstr (driver, $"{s}  {Scenario.ScenarioMetadata.GetDescription (Scenarios [item])}", col, line, width, start);
-			}
-
-			public void SetMark (int item, bool value)
-			{
-			}
-
-			int GetMaxLengthItem ()
-			{
-				if (Scenarios?.Count == 0) {
-					return 0;
-				}
-
-				int maxLength = 0;
-				for (int i = 0; i < Scenarios.Count; i++) {
-					var s = String.Format (String.Format ("{{0,{0}}}", -_nameColumnWidth), Scenario.ScenarioMetadata.GetName (Scenarios [i]));
-					var sc = $"{s}  {Scenario.ScenarioMetadata.GetDescription (Scenarios [i])}";
-					var l = sc.Length;
-					if (l > maxLength) {
-						maxLength = l;
-					}
-				}
-
-				return maxLength;
-			}
-
-			// A slightly adapted method from: https://github.com/gui-cs/Terminal.Gui/blob/fc1faba7452ccbdf49028ac49f0c9f0f42bbae91/Terminal.Gui/Views/ListView.cs#L433-L461
-			private void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width, int start = 0)
-			{
-				int used = 0;
-				int index = start;
-				while (index < ustr.Length) {
-					(var rune, var size) = Utf8.DecodeRune (ustr, index, index - ustr.Length);
-					var count = Rune.ColumnWidth (rune);
-					if (used + count >= width) break;
-					driver.AddRune (rune);
-					used += count;
-					index += size;
-				}
-
-				while (used < width) {
-					driver.AddRune (' ');
-					used++;
-				}
-			}
-
-			public IList ToList ()
-			{
-				return Scenarios;
-			}
-		}
-
-		/// <summary>
-		/// When Scenarios are running we need to override the behavior of the Menu 
-		/// and Statusbar to enable Scenarios that use those (or related key input)
-		/// to not be impacted. Same as for tabs.
-		/// </summary>
-		/// <param name="ke"></param>
 		private static void KeyDownHandler (View.KeyEventEventArgs a)
 		{
-			//if (a.KeyEvent.Key == Key.Tab || a.KeyEvent.Key == Key.BackTab) {
-			//	// BUGBUG: Work around Issue #434 by implementing our own TAB navigation
-			//	if (_top.MostFocused == _categoryListView)
-			//		_top.SetFocus (_rightPane);
-			//	else
-			//		_top.SetFocus (_leftPane);
-			//}
-
 			if (a.KeyEvent.IsCapslock) {
 				_capslock.Title = "Caps: On";
 				_statusBar.SetNeedsDisplay ();
@@ -655,22 +528,16 @@ namespace UICatalog {
 
 		private static void CategoryListView_SelectedChanged (ListViewItemEventArgs e)
 		{
-			if (_categoryListViewItem != _categoryListView.SelectedItem) {
-				_scenarioListViewItem = 0;
-			}
-			_categoryListViewItem = _categoryListView.SelectedItem;
-			var item = _categories [_categoryListViewItem];
-			List<Type> newlist;
-			if (_categoryListViewItem == 0) {
+			var item = _categories [e.Item];
+			List<Scenario> newlist;
+			if (e.Item == 0) {
 				// First category is "All"
 				newlist = _scenarios;
 
 			} else {
-				newlist = _scenarios.Where (t => Scenario.ScenarioCategory.GetCategories (t).Contains (item)).ToList ();
+				newlist = _scenarios.Where (s => s.GetCategories ().Contains (item)).ToList ();
 			}
-			_scenarioListView.Source = new ScenarioListDataSource (newlist);
-			_scenarioListView.SelectedItem = _scenarioListViewItem;
-
+			_scenarioListView.SetSource (newlist.ToList ());
 		}
 
 		private static void OpenUrl (string url)

+ 0 - 25
UnitTests/AssemblyInfo.cs

@@ -7,28 +7,3 @@ using Xunit;
 // Since Application is a singleton we can't run tests in parallel
 [assembly: CollectionBehavior (DisableTestParallelization = true)]
 
-// This class enables test functions annotated with the [AutoInitShutdown] attribute to 
-// automatically call Application.Init before called and Application.Shutdown after
-// 
-// This is necessary because a) Application is a singleton and Init/Shutdown must be called
-// as a pair, and b) all unit test functions should be atomic.
-[AttributeUsage (AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
-public class AutoInitShutdownAttribute : Xunit.Sdk.BeforeAfterTestAttribute {
-
-	static bool _init = false;
-	public override void Before (MethodInfo methodUnderTest)
-	{
-		if (_init) {
-			throw new InvalidOperationException ("After did not run.");
-		}
-
-		Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
-		_init = true;
-	}
-
-	public override void After (MethodInfo methodUnderTest)
-	{
-		Application.Shutdown ();
-		_init = false;
-	}
-}

+ 13 - 13
UnitTests/ButtonTests.cs

@@ -29,7 +29,7 @@ namespace Terminal.Gui.Views {
 			var expected = @"
 [  ]
 ";
-			GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 
 			Application.End (rs);
 			btn = new Button ("ARGS", true) { Text = "Test" };
@@ -47,7 +47,7 @@ namespace Terminal.Gui.Views {
 			expected = @"
 [◦ Test ◦]
 ";
-			GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 
 			Application.End (rs);
 			btn = new Button (3, 4, "Test", true);
@@ -65,7 +65,7 @@ namespace Terminal.Gui.Views {
 			expected = @"
    [◦ Test ◦]
 ";
-			GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 
 			Application.End (rs);
 		}
@@ -235,7 +235,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 30, 5), pos);
 		}
 
@@ -273,7 +273,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 30, 5), pos);
 		}
 
@@ -310,7 +310,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 		}
 
 		[Fact, AutoInitShutdown]
@@ -342,7 +342,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 
 			Assert.True (btn.AutoSize);
 			btn.Text = "Say Hello 你 changed";
@@ -356,7 +356,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 		}
 
 		[Fact, AutoInitShutdown]
@@ -389,7 +389,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 
 			Assert.True (btn.AutoSize);
 			btn.Text = "Say Hello 你 changed";
@@ -403,7 +403,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 		}
 
 		[Fact, AutoInitShutdown]
@@ -520,7 +520,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────────────────────────────┘
 ";
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 		}
 
 		[Fact, AutoInitShutdown]
@@ -550,7 +550,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 		}
 
 		[Fact, AutoInitShutdown]
@@ -582,7 +582,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 		}
 	}
 }

+ 19 - 19
UnitTests/CheckboxTests.cs

@@ -84,7 +84,7 @@ namespace Terminal.Gui.Views {
 √ Test
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 6, 1), pos);
 		}
 
@@ -123,7 +123,7 @@ namespace Terminal.Gui.Views {
 ";
 
 			// Positive test
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 30, 5), pos);
 
 			// Also Positive test
@@ -139,7 +139,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 30, 5), pos);
 
 			checkBox.Checked = true;
@@ -153,7 +153,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 30, 5), pos);
 
 			checkBox.AutoSize = false;
@@ -169,7 +169,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 30, 5), pos);
 
 			checkBox.Width = 19;
@@ -186,7 +186,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 30, 5), pos);
 
 			checkBox.AutoSize = true;
@@ -200,7 +200,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 30, 5), pos);
 		}
 
@@ -240,7 +240,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 30, 5), pos);
 
 			checkBox.Checked = true;
@@ -253,7 +253,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 30, 5), pos);
 		}
 
@@ -294,7 +294,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 30, 5), pos);
 
 			checkBox.Checked = true;
@@ -307,7 +307,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 30, 5), pos);
 		}
 
@@ -364,7 +364,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 30, 6), pos);
 
 			checkBox1.Checked = true;
@@ -383,7 +383,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 30, 6), pos);
 		}
 
@@ -424,7 +424,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 30, 5), pos);
 
 			checkBox.Checked = true;
@@ -437,7 +437,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 30, 5), pos);
 		}
 
@@ -470,7 +470,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 
 			Assert.True (checkBox.AutoSize);
 			checkBox.Text = "Check this out 你 changed";
@@ -484,7 +484,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 		}
 
 		[Fact, AutoInitShutdown]
@@ -516,7 +516,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 
 			Assert.True (checkBox.AutoSize);
 			checkBox.Text = "Check this out 你 changed";
@@ -530,7 +530,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 		}
 	}
 }

+ 10 - 10
UnitTests/ComboBoxTests.cs

@@ -144,7 +144,7 @@ namespace Terminal.Gui.Views {
 			Assert.Equal (0, cb.SelectedItem);
 			Assert.Equal ("One", cb.Text);
 			Application.Begin (Application.Top);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 One      ▼
 One       
 ", output);
@@ -154,7 +154,7 @@ One
 			Assert.Equal (1, cb.SelectedItem);
 			Assert.Equal ("Two", cb.Text);
 			Application.Begin (Application.Top);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 Two      ▼
 Two       
 ", output);
@@ -164,7 +164,7 @@ Two
 			Assert.Equal (2, cb.SelectedItem);
 			Assert.Equal ("Three", cb.Text);
 			Application.Begin (Application.Top);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 Three    ▼
 Three     
 ", output);
@@ -809,7 +809,7 @@ Three
 			Assert.Equal (-1, cb.SelectedItem);
 			Assert.Equal ("", cb.Text);
 			cb.Redraw (cb.Bounds);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 One   
 Two   
@@ -824,7 +824,7 @@ Three ", output);
 				cb.Subviews [1].GetNormalColor ()
 			};
 
-			GraphViewTests.AssertDriverColorsAre (@"
+			TestHelpers.AssertDriverColorsAre (@"
 000000
 00000
 22222
@@ -836,7 +836,7 @@ Three ", output);
 			Assert.Equal (-1, cb.SelectedItem);
 			Assert.Equal ("", cb.Text);
 			cb.Redraw (cb.Bounds);
-			GraphViewTests.AssertDriverColorsAre (@"
+			TestHelpers.AssertDriverColorsAre (@"
 000000
 22222
 00000
@@ -848,7 +848,7 @@ Three ", output);
 			Assert.Equal (-1, cb.SelectedItem);
 			Assert.Equal ("", cb.Text);
 			cb.Redraw (cb.Bounds);
-			GraphViewTests.AssertDriverColorsAre (@"
+			TestHelpers.AssertDriverColorsAre (@"
 000000
 22222
 22222
@@ -866,7 +866,7 @@ Three ", output);
 			Assert.Equal (2, cb.SelectedItem);
 			Assert.Equal ("Three", cb.Text);
 			cb.Redraw (cb.Bounds);
-			GraphViewTests.AssertDriverColorsAre (@"
+			TestHelpers.AssertDriverColorsAre (@"
 000000
 22222
 22222
@@ -878,7 +878,7 @@ Three ", output);
 			Assert.Equal (2, cb.SelectedItem);
 			Assert.Equal ("Three", cb.Text);
 			cb.Redraw (cb.Bounds);
-			GraphViewTests.AssertDriverColorsAre (@"
+			TestHelpers.AssertDriverColorsAre (@"
 000000
 22222
 00000
@@ -890,7 +890,7 @@ Three ", output);
 			Assert.Equal (2, cb.SelectedItem);
 			Assert.Equal ("Three", cb.Text);
 			cb.Redraw (cb.Bounds);
-			GraphViewTests.AssertDriverColorsAre (@"
+			TestHelpers.AssertDriverColorsAre (@"
 000000
 00000
 22222

+ 201 - 19
UnitTests/ConsoleDriverTests.cs

@@ -1,7 +1,7 @@
 using System;
+using System.Collections;
 using System.Collections.Generic;
 using System.Linq;
-using Terminal.Gui;
 using Terminal.Gui.Views;
 using Xunit;
 using Xunit.Abstractions;
@@ -228,7 +228,8 @@ namespace Terminal.Gui.ConsoleDrivers {
 
 			void SendKeys ()
 			{
-				var k = keyEnums [idxKey];
+				var k = shift && char.IsLetter ((char)keyEnums [idxKey]) && char.IsLower ((char)keyEnums [idxKey])
+					? (Key)char.ToUpper ((char)keyEnums [idxKey]) : keyEnums [idxKey];
 				var c = (char)k;
 				var ck = char.IsLetter (c) ? (ConsoleKey)char.ToUpper (c) : (ConsoleKey)c;
 				var mk = new KeyModifiers () {
@@ -551,7 +552,7 @@ namespace Terminal.Gui.ConsoleDrivers {
 └────────────────────────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 30, 10), pos);
 		}
 
@@ -581,7 +582,7 @@ namespace Terminal.Gui.ConsoleDrivers {
 └──────────────────┘
 ";
 
-					var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+					var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 					Assert.Equal (new Rect (0, 0, 20, 8), pos);
 
 					Assert.True (dlg.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ())));
@@ -598,7 +599,7 @@ namespace Terminal.Gui.ConsoleDrivers {
 └──────────────────┘
 ";
 
-					pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+					pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 					Assert.Equal (new Rect (0, 0, 20, 8), pos);
 
 					win.RequestStop ();
@@ -608,29 +609,210 @@ namespace Terminal.Gui.ConsoleDrivers {
 			Application.Run (win);
 			Application.Shutdown ();
 		}
-		
+
 		[Theory]
-		[InlineData(0x0000001F, 0x241F)]
-		[InlineData(0x0000007F, 0x247F)]
-		[InlineData(0x0000009F, 0x249F)]
-		[InlineData(0x0001001A, 0x241A)]
+		[InlineData (0x0000001F, 0x241F)]
+		[InlineData (0x0000007F, 0x247F)]
+		[InlineData (0x0000009F, 0x249F)]
+		[InlineData (0x0001001A, 0x241A)]
 		public void MakePrintable_Converts_Control_Chars_To_Proper_Unicode (uint code, uint expected)
 		{
-			var actual = ConsoleDriver.MakePrintable(code);
-				
+			var actual = ConsoleDriver.MakePrintable (code);
+
 			Assert.Equal (expected, actual.Value);
 		}
-		
+
 		[Theory]
-		[InlineData(0x20)]
-		[InlineData(0x7E)]
-		[InlineData(0xA0)]
-		[InlineData(0x010020)]
+		[InlineData (0x20)]
+		[InlineData (0x7E)]
+		[InlineData (0xA0)]
+		[InlineData (0x010020)]
 		public void MakePrintable_Does_Not_Convert_Ansi_Chars_To_Unicode (uint code)
 		{
-			var actual = ConsoleDriver.MakePrintable(code);
-				
+			var actual = ConsoleDriver.MakePrintable (code);
+
 			Assert.Equal (code, actual.Value);
 		}
+
+		/// <summary>
+		/// Sometimes when using remote tools EventKeyRecord sends 'virtual keystrokes'.
+		/// These are indicated with the wVirtualKeyCode of 231. When we see this code
+		/// then we need to look to the unicode character (UnicodeChar) instead of the key
+		/// when telling the rest of the framework what button was pressed. For full details
+		/// see: https://github.com/gui-cs/Terminal.Gui/issues/2008
+		/// </summary>
+		[Theory, AutoInitShutdown]
+		[ClassData (typeof (PacketTest))]
+		public void TestVKPacket (uint unicodeCharacter, bool shift, bool alt, bool control, uint initialVirtualKey, uint initialScanCode, Key expectedRemapping, uint expectedVirtualKey, uint expectedScanCode)
+		{
+			ConsoleModifiers modifiers = new ConsoleModifiers ();
+			if (shift) {
+				modifiers |= ConsoleModifiers.Shift;
+			}
+			if (alt) {
+				modifiers |= ConsoleModifiers.Alt;
+			}
+			if (control) {
+				modifiers |= ConsoleModifiers.Control;
+			}
+			var mappedConsoleKey = ConsoleKeyMapping.GetConsoleKeyFromKey (unicodeCharacter, modifiers, out uint scanCode, out uint outputChar);
+
+			if ((scanCode > 0 || mappedConsoleKey == 0) && mappedConsoleKey == initialVirtualKey) {
+				Assert.Equal (mappedConsoleKey, initialVirtualKey);
+			} else {
+				Assert.Equal (mappedConsoleKey, outputChar < 0xff ? (uint)(outputChar & 0xff | 0xff << 8) : outputChar);
+			}
+			Assert.Equal (scanCode, initialScanCode);
+
+			var keyChar = ConsoleKeyMapping.GetKeyCharFromConsoleKey (mappedConsoleKey, modifiers, out uint consoleKey, out scanCode);
+
+			//if (scanCode > 0 && consoleKey == keyChar && consoleKey > 48 && consoleKey > 57 && consoleKey < 65 && consoleKey > 91) {
+			if (scanCode > 0 && keyChar == 0 && consoleKey == mappedConsoleKey) {
+				Assert.Equal (0, (double)keyChar);
+			} else {
+				Assert.Equal (keyChar, unicodeCharacter);
+			}
+			Assert.Equal (consoleKey, expectedVirtualKey);
+			Assert.Equal (scanCode, expectedScanCode);
+
+			var top = Application.Top;
+
+			top.KeyPress += (e) => {
+				var after = ShortcutHelper.GetModifiersKey (e.KeyEvent);
+				Assert.Equal (expectedRemapping, after);
+				e.Handled = true;
+				Application.RequestStop ();
+			};
+
+			var iterations = -1;
+
+			Application.Iteration += () => {
+				iterations++;
+				if (iterations == 0) {
+					Application.Driver.SendKeys ((char)mappedConsoleKey, ConsoleKey.Packet, shift, alt, control);
+				}
+			};
+
+			Application.Run ();
+			Application.Shutdown ();
+		}
+
+		public class PacketTest : IEnumerable, IEnumerable<object []> {
+			public IEnumerator<object []> GetEnumerator ()
+			{
+				yield return new object [] { 'a', false, false, false, 'A', 30, Key.a, 'A', 30 };
+				yield return new object [] { 'A', true, false, false, 'A', 30, Key.A | Key.ShiftMask, 'A', 30 };
+				yield return new object [] { 'A', true, true, false, 'A', 30, Key.A | Key.ShiftMask | Key.AltMask, 'A', 30 };
+				yield return new object [] { 'A', true, true, true, 'A', 30, Key.A | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 'A', 30 };
+				yield return new object [] { 'z', false, false, false, 'Z', 44, Key.z, 'Z', 44 };
+				yield return new object [] { 'Z', true, false, false, 'Z', 44, Key.Z | Key.ShiftMask, 'Z', 44 };
+				yield return new object [] { 'Z', true, true, false, 'Z', 44, Key.Z | Key.ShiftMask | Key.AltMask, 'Z', 44 };
+				yield return new object [] { 'Z', true, true, true, 'Z', 44, Key.Z | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 'Z', 44 };
+				yield return new object [] { '英', false, false, false, '\0', 0, (Key)'英', '\0', 0 };
+				yield return new object [] { '英', true, false, false, '\0', 0, (Key)'英' | Key.ShiftMask, '\0', 0 };
+				yield return new object [] { '英', true, true, false, '\0', 0, (Key)'英' | Key.ShiftMask | Key.AltMask, '\0', 0 };
+				yield return new object [] { '英', true, true, true, '\0', 0, (Key)'英' | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '\0', 0 };
+				yield return new object [] { '+', false, false, false, 187, 26, (Key)'+', 187, 26 };
+				yield return new object [] { '*', true, false, false, 187, 26, (Key)'*' | Key.ShiftMask, 187, 26 };
+				yield return new object [] { '+', true, true, false, 187, 26, (Key)'+' | Key.ShiftMask | Key.AltMask, 187, 26 };
+				yield return new object [] { '+', true, true, true, 187, 26, (Key)'+' | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 187, 26 };
+				yield return new object [] { '1', false, false, false, '1', 2, Key.D1, '1', 2 };
+				yield return new object [] { '!', true, false, false, '1', 2, (Key)'!' | Key.ShiftMask, '1', 2 };
+				yield return new object [] { '1', true, true, false, '1', 2, Key.D1 | Key.ShiftMask | Key.AltMask, '1', 2 };
+				yield return new object [] { '1', true, true, true, '1', 2, Key.D1 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '1', 2 };
+				yield return new object [] { '1', false, true, true, '1', 2, Key.D1 | Key.AltMask | Key.CtrlMask, '1', 2 };
+				yield return new object [] { '1', false, true, true, '1', 2, Key.D1 | Key.AltMask | Key.CtrlMask, '1', 2 };
+				yield return new object [] { '2', false, false, false, '2', 3, Key.D2, '2', 3 };
+				yield return new object [] { '"', true, false, false, '2', 3, (Key)'"' | Key.ShiftMask, '2', 3 };
+				yield return new object [] { '2', true, true, false, '2', 3, Key.D2 | Key.ShiftMask | Key.AltMask, '2', 3 };
+				yield return new object [] { '2', true, true, true, '2', 3, Key.D2 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '2', 3 };
+				yield return new object [] { '@', false, true, true, '2', 3, (Key)'@' | Key.AltMask | Key.CtrlMask, '2', 3 };
+				yield return new object [] { '3', false, false, false, '3', 4, Key.D3, '3', 4 };
+				yield return new object [] { '#', true, false, false, '3', 4, (Key)'#' | Key.ShiftMask, '3', 4 };
+				yield return new object [] { '3', true, true, false, '3', 4, Key.D3 | Key.ShiftMask | Key.AltMask, '3', 4 };
+				yield return new object [] { '3', true, true, true, '3', 4, Key.D3 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '3', 4 };
+				yield return new object [] { '£', false, true, true, '3', 4, (Key)'£' | Key.AltMask | Key.CtrlMask, '3', 4 };
+				yield return new object [] { '4', false, false, false, '4', 5, Key.D4, '4', 5 };
+				yield return new object [] { '$', true, false, false, '4', 5, (Key)'$' | Key.ShiftMask, '4', 5 };
+				yield return new object [] { '4', true, true, false, '4', 5, Key.D4 | Key.ShiftMask | Key.AltMask, '4', 5 };
+				yield return new object [] { '4', true, true, true, '4', 5, Key.D4 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '4', 5 };
+				yield return new object [] { '§', false, true, true, '4', 5, (Key)'§' | Key.AltMask | Key.CtrlMask, '4', 5 };
+				yield return new object [] { '5', false, false, false, '5', 6, Key.D5, '5', 6 };
+				yield return new object [] { '%', true, false, false, '5', 6, (Key)'%' | Key.ShiftMask, '5', 6 };
+				yield return new object [] { '5', true, true, false, '5', 6, Key.D5 | Key.ShiftMask | Key.AltMask, '5', 6 };
+				yield return new object [] { '5', true, true, true, '5', 6, Key.D5 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '5', 6 };
+				yield return new object [] { '€', false, true, true, '5', 6, (Key)'€' | Key.AltMask | Key.CtrlMask, '5', 6 };
+				yield return new object [] { '6', false, false, false, '6', 7, Key.D6, '6', 7 };
+				yield return new object [] { '&', true, false, false, '6', 7, (Key)'&' | Key.ShiftMask, '6', 7 };
+				yield return new object [] { '6', true, true, false, '6', 7, Key.D6 | Key.ShiftMask | Key.AltMask, '6', 7 };
+				yield return new object [] { '6', true, true, true, '6', 7, Key.D6 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '6', 7 };
+				yield return new object [] { '6', false, true, true, '6', 7, Key.D6 | Key.AltMask | Key.CtrlMask, '6', 7 };
+				yield return new object [] { '7', false, false, false, '7', 8, Key.D7, '7', 8 };
+				yield return new object [] { '/', true, false, false, '7', 8, (Key)'/' | Key.ShiftMask, '7', 8 };
+				yield return new object [] { '7', true, true, false, '7', 8, Key.D7 | Key.ShiftMask | Key.AltMask, '7', 8 };
+				yield return new object [] { '7', true, true, true, '7', 8, Key.D7 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '7', 8 };
+				yield return new object [] { '{', false, true, true, '7', 8, (Key)'{' | Key.AltMask | Key.CtrlMask, '7', 8 };
+				yield return new object [] { '8', false, false, false, '8', 9, Key.D8, '8', 9 };
+				yield return new object [] { '(', true, false, false, '8', 9, (Key)'(' | Key.ShiftMask, '8', 9 };
+				yield return new object [] { '8', true, true, false, '8', 9, Key.D8 | Key.ShiftMask | Key.AltMask, '8', 9 };
+				yield return new object [] { '8', true, true, true, '8', 9, Key.D8 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '8', 9 };
+				yield return new object [] { '[', false, true, true, '8', 9, (Key)'[' | Key.AltMask | Key.CtrlMask, '8', 9 };
+				yield return new object [] { '9', false, false, false, '9', 10, Key.D9, '9', 10 };
+				yield return new object [] { ')', true, false, false, '9', 10, (Key)')' | Key.ShiftMask, '9', 10 };
+				yield return new object [] { '9', true, true, false, '9', 10, Key.D9 | Key.ShiftMask | Key.AltMask, '9', 10 };
+				yield return new object [] { '9', true, true, true, '9', 10, Key.D9 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '9', 10 };
+				yield return new object [] { ']', false, true, true, '9', 10, (Key)']' | Key.AltMask | Key.CtrlMask, '9', 10 };
+				yield return new object [] { '0', false, false, false, '0', 11, Key.D0, '0', 11 };
+				yield return new object [] { '=', true, false, false, '0', 11, (Key)'=' | Key.ShiftMask, '0', 11 };
+				yield return new object [] { '0', true, true, false, '0', 11, Key.D0 | Key.ShiftMask | Key.AltMask, '0', 11 };
+				yield return new object [] { '0', true, true, true, '0', 11, Key.D0 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '0', 11 };
+				yield return new object [] { '}', false, true, true, '0', 11, (Key)'}' | Key.AltMask | Key.CtrlMask, '0', 11 };
+				yield return new object [] { '\'', false, false, false, 219, 12, (Key)'\'', 219, 12 };
+				yield return new object [] { '?', true, false, false, 219, 12, (Key)'?' | Key.ShiftMask, 219, 12 };
+				yield return new object [] { '\'', true, true, false, 219, 12, (Key)'\'' | Key.ShiftMask | Key.AltMask, 219, 12 };
+				yield return new object [] { '\'', true, true, true, 219, 12, (Key)'\'' | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 219, 12 };
+				yield return new object [] { '«', false, false, false, 221, 13, (Key)'«', 221, 13 };
+				yield return new object [] { '»', true, false, false, 221, 13, (Key)'»' | Key.ShiftMask, 221, 13 };
+				yield return new object [] { '«', true, true, false, 221, 13, (Key)'«' | Key.ShiftMask | Key.AltMask, 221, 13 };
+				yield return new object [] { '«', true, true, true, 221, 13, (Key)'«' | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 221, 13 };
+				yield return new object [] { 'á', false, false, false, 'á', 0, (Key)'á', 'A', 30 };
+				yield return new object [] { 'Á', true, false, false, 'Á', 0, (Key)'Á' | Key.ShiftMask, 'A', 30 };
+				yield return new object [] { 'à', false, false, false, 'à', 0, (Key)'à', 'A', 30 };
+				yield return new object [] { 'À', true, false, false, 'À', 0, (Key)'À' | Key.ShiftMask, 'A', 30 };
+				yield return new object [] { 'é', false, false, false, 'é', 0, (Key)'é', 'E', 18 };
+				yield return new object [] { 'É', true, false, false, 'É', 0, (Key)'É' | Key.ShiftMask, 'E', 18 };
+				yield return new object [] { 'è', false, false, false, 'è', 0, (Key)'è', 'E', 18 };
+				yield return new object [] { 'È', true, false, false, 'È', 0, (Key)'È' | Key.ShiftMask, 'E', 18 };
+				yield return new object [] { 'í', false, false, false, 'í', 0, (Key)'í', 'I', 23 };
+				yield return new object [] { 'Í', true, false, false, 'Í', 0, (Key)'Í' | Key.ShiftMask, 'I', 23 };
+				yield return new object [] { 'ì', false, false, false, 'ì', 0, (Key)'ì', 'I', 23 };
+				yield return new object [] { 'Ì', true, false, false, 'Ì', 0, (Key)'Ì' | Key.ShiftMask, 'I', 23 };
+				yield return new object [] { 'ó', false, false, false, 'ó', 0, (Key)'ó', 'O', 24 };
+				yield return new object [] { 'Ó', true, false, false, 'Ó', 0, (Key)'Ó' | Key.ShiftMask, 'O', 24 };
+				yield return new object [] { 'ò', false, false, false, 'Ó', 0, (Key)'ò', 'O', 24 };
+				yield return new object [] { 'Ò', true, false, false, 'Ò', 0, (Key)'Ò' | Key.ShiftMask, 'O', 24 };
+				yield return new object [] { 'ú', false, false, false, 'ú', 0, (Key)'ú', 'U', 22 };
+				yield return new object [] { 'Ú', true, false, false, 'Ú', 0, (Key)'Ú' | Key.ShiftMask, 'U', 22 };
+				yield return new object [] { 'ù', false, false, false, 'ù', 0, (Key)'ù', 'U', 22 };
+				yield return new object [] { 'Ù', true, false, false, 'Ù', 0, (Key)'Ù' | Key.ShiftMask, 'U', 22 };
+				yield return new object [] { 'ö', false, false, false, 'ó', 0, (Key)'ö', 'O', 24 };
+				yield return new object [] { 'Ö', true, false, false, 'Ó', 0, (Key)'Ö' | Key.ShiftMask, 'O', 24 };
+				yield return new object [] { '<', false, false, false, 226, 86, (Key)'<', 226, 86 };
+				yield return new object [] { '>', true, false, false, 226, 86, (Key)'>' | Key.ShiftMask, 226, 86 };
+				yield return new object [] { '<', true, true, false, 226, 86, (Key)'<' | Key.ShiftMask | Key.AltMask, 226, 86 };
+				yield return new object [] { '<', true, true, true, 226, 86, (Key)'<' | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 226, 86 };
+				yield return new object [] { 'ç', false, false, false, 192, 39, (Key)'ç', 192, 39 };
+				yield return new object [] { 'Ç', true, false, false, 192, 39, (Key)'Ç' | Key.ShiftMask, 192, 39 };
+				yield return new object [] { 'ç', true, true, false, 192, 39, (Key)'ç' | Key.ShiftMask | Key.AltMask, 192, 39 };
+				yield return new object [] { 'ç', true, true, true, 192, 39, (Key)'ç' | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 192, 39 };
+				yield return new object [] { '¨', false, true, true, 187, 26, (Key)'¨' | Key.AltMask | Key.CtrlMask, 187, 26 };
+				yield return new object [] { (uint)Key.PageUp, false, false, false, 33, 73, Key.PageUp, 33, 73 };
+				yield return new object [] { (uint)Key.PageUp, true, false, false, 33, 73, Key.PageUp | Key.ShiftMask, 33, 73 };
+				yield return new object [] { (uint)Key.PageUp, true, true, false, 33, 73, Key.PageUp | Key.ShiftMask | Key.AltMask, 33, 73 };
+				yield return new object [] { (uint)Key.PageUp, true, true, true, 33, 73, Key.PageUp | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 33, 73 };
+			}
+
+			IEnumerator IEnumerable.GetEnumerator () => GetEnumerator ();
+		}
 	}
 }

+ 44 - 30
UnitTests/ContextMenuTests.cs

@@ -72,7 +72,7 @@ namespace Terminal.Gui.Core {
           └──────┘
 ";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 			cm.Hide ();
 			Assert.False (ContextMenu.IsShow);
@@ -81,7 +81,7 @@ namespace Terminal.Gui.Core {
 
 			expected = "";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 		}
 
 		[Fact]
@@ -105,7 +105,7 @@ namespace Terminal.Gui.Core {
           └──────┘
 ";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 			cm.Position = new Point (5, 10);
 
@@ -119,7 +119,7 @@ namespace Terminal.Gui.Core {
      └──────┘
 ";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 		}
 
@@ -144,7 +144,7 @@ namespace Terminal.Gui.Core {
           └──────┘
 ";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 			cm.MenuItems = new MenuBarItem (new MenuItem [] {
 				new MenuItem ("First", "", null),
@@ -164,7 +164,7 @@ namespace Terminal.Gui.Core {
           └─────────┘
 ";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 		}
 
@@ -271,7 +271,7 @@ namespace Terminal.Gui.Core {
                                                                         └──────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (72, 21, 80, 4), pos);
 
 			cm.Hide ();
@@ -312,7 +312,7 @@ namespace Terminal.Gui.Core {
                                                                       View    
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (70, 20, 78, 5), pos);
 
 			cm.Hide ();
@@ -347,7 +347,7 @@ namespace Terminal.Gui.Core {
           └──────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (10, 5, 18, 5), pos);
 
 			cm.Hide ();
@@ -370,7 +370,7 @@ namespace Terminal.Gui.Core {
      └──────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (5, 10, 13, 7), pos);
 
 			cm.Hide ();
@@ -401,7 +401,7 @@ namespace Terminal.Gui.Core {
 └────
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 1, 5, 4), pos);
 
 			cm.Hide ();
@@ -431,7 +431,7 @@ namespace Terminal.Gui.Core {
 │ Two  │
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 8, 3), pos);
 
 			cm.Hide ();
@@ -484,7 +484,7 @@ namespace Terminal.Gui.Core {
 └──────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 1, 8, 4), pos);
 
 			cm.ForceMinimumPosToZero = false;
@@ -498,7 +498,7 @@ namespace Terminal.Gui.Core {
 ──────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (1, 0, 7, 3), pos);
 		}
 
@@ -592,7 +592,7 @@ namespace Terminal.Gui.Core {
 			Assert.Equal (new Point (9, 3), tf.ContextMenu.Position);
 			Application.Top.Redraw (Application.Top.Bounds);
 			var expected = @"
-  File   Edit                   
+ File  Edit                     
                                 
                                 
   Label: TextField              
@@ -611,8 +611,8 @@ namespace Terminal.Gui.Core {
  F1 Help │ ^Q Quit              
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 32, 17), pos);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (1, 0, 32, 17), pos);
 		}
 
 		[Fact, AutoInitShutdown]
@@ -656,7 +656,7 @@ namespace Terminal.Gui.Core {
 			Assert.Equal (new Point (10, 5), tf.ContextMenu.Position);
 			Application.Top.Redraw (Application.Top.Bounds);
 			var expected = @"
-  File   Edit                               
+ File  Edit                                 
 ┌ Window ──────────────────────────────────┐
 │                                          │
 │                                          │
@@ -675,8 +675,8 @@ namespace Terminal.Gui.Core {
  F1 Help │ ^Q Quit                          
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 44, 17), pos);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (1, 0, 44, 17), pos);
 		}
 
 		[Fact, AutoInitShutdown]
@@ -707,7 +707,7 @@ namespace Terminal.Gui.Core {
 			Assert.Equal (new Point (-1, -2), cm.Position);
 			var top = Application.Top;
 			Application.Begin (top);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌────────┐
 │ One    │
 │ Two    │
@@ -726,7 +726,7 @@ namespace Terminal.Gui.Core {
 			}));
 			Application.Refresh ();
 			Assert.Equal (new Point (-1, -2), cm.Position);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌────────┐             
 │ One    │             
 │ Two    │             
@@ -747,7 +747,7 @@ namespace Terminal.Gui.Core {
 			cm.Show ();
 			Application.Refresh ();
 			Assert.Equal (new Point (41, -2), cm.Position);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
                               ┌────────┐
                               │ One    │
                               │ Two    │
@@ -766,7 +766,7 @@ namespace Terminal.Gui.Core {
 			}));
 			Application.Refresh ();
 			Assert.Equal (new Point (41, -2), cm.Position);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
                               ┌────────┐
                               │ One    │
                               │ Two    │
@@ -786,7 +786,7 @@ namespace Terminal.Gui.Core {
 			cm.Show ();
 			Application.Refresh ();
 			Assert.Equal (new Point (41, 9), cm.Position);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
                               ┌────────┐
                               │ One    │
                               │ Two    │
@@ -805,7 +805,7 @@ namespace Terminal.Gui.Core {
 			}));
 			Application.Refresh ();
 			Assert.Equal (new Point (41, 9), cm.Position);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
                               ┌────────┐
                  ┌───────────┐│ One    │
                  │ SubMenu1  ││ Two    │
@@ -822,7 +822,7 @@ namespace Terminal.Gui.Core {
 			cm.Show ();
 			Application.Refresh ();
 			Assert.Equal (new Point (41, 22), cm.Position);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
                               ┌────────┐
                               │ One    │
                               │ Two    │
@@ -841,7 +841,7 @@ namespace Terminal.Gui.Core {
 			}));
 			Application.Refresh ();
 			Assert.Equal (new Point (41, 22), cm.Position);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
                  ┌───────────┐          
                  │ SubMenu1  │┌────────┐
                  │ SubMenu2  ││ One    │
@@ -858,7 +858,7 @@ namespace Terminal.Gui.Core {
 			cm.Show ();
 			Application.Refresh ();
 			Assert.Equal (new Point (19, 10), cm.Position);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
         ┌────────┐
         │ One    │
         │ Two    │
@@ -877,7 +877,7 @@ namespace Terminal.Gui.Core {
 			}));
 			Application.Refresh ();
 			Assert.Equal (new Point (19, 10), cm.Position);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌───────────┐────┐
 │ SubMenu1  │    │
 │ SubMenu2  │    │
@@ -888,5 +888,19 @@ namespace Terminal.Gui.Core {
 │ SubMenu7  │────┘
 ", output);
 		}
+
+		[Fact, AutoInitShutdown]
+		public void Key_Open_And_Close_The_ContextMenu ()
+		{
+			var tf = new TextField ();
+			var top = Application.Top;
+			top.Add (tf);
+			Application.Begin (top);
+
+			Assert.True (tf.ProcessKey (new KeyEvent (Key.F10 | Key.ShiftMask, new KeyModifiers ())));
+			Assert.True (tf.ContextMenu.MenuBar.IsMenuOpen);
+			Assert.True (top.Subviews [1].ProcessKey (new KeyEvent (Key.F10 | Key.ShiftMask, new KeyModifiers ())));
+			Assert.Null (tf.ContextMenu.MenuBar);
+		}
 	}
 }

+ 38 - 38
UnitTests/DialogTests.cs

@@ -43,28 +43,28 @@ namespace Terminal.Gui.Views {
 			d.SetBufferSize (width, 3);
 
 			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btnText));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Justify
 			buttonRow = $"{d.VLine}      {d.LeftBracket} {btnText} {d.RightBracket}{d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
 			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, new Button (btnText));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Right
 			buttonRow = $"{d.VLine}      {d.LeftBracket} {btnText} {d.RightBracket}{d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
 			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btnText));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Left
 			buttonRow = $"{d.VLine}{d.LeftBracket} {btnText} {d.RightBracket}      {d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
 			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btnText));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 		}
 
@@ -91,28 +91,28 @@ namespace Terminal.Gui.Views {
 			d.SetBufferSize (buttonRow.Length, 3);
 
 			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btn1Text), new Button (btn2Text));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Justify
 			buttonRow = $@"{d.VLine}{btn1}   {btn2}{d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
 			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, new Button (btn1Text), new Button (btn2Text));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Right
 			buttonRow = $@"{d.VLine}  {btn1} {btn2}{d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
 			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btn1Text), new Button (btn2Text));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Left
 			buttonRow = $@"{d.VLine}{btn1} {btn2}  {d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
 			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btn1Text), new Button (btn2Text));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 		}
 
@@ -149,7 +149,7 @@ namespace Terminal.Gui.Views {
 			//button1.Visible = false;
 			//Application.RunMainLoopIteration (ref runstate, true, ref firstIteration);
 			//buttonRow = $@"{d.VLine}         {btn2} {d.VLine}";
-			//GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			//DriverAsserts.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			//Application.End (runstate);
 
 			// Justify
@@ -160,21 +160,21 @@ namespace Terminal.Gui.Views {
 			button1.Visible = false;
 			Application.RunMainLoopIteration (ref runstate, true, ref firstIteration);
 			buttonRow = $@"{d.VLine}          {btn2}{d.VLine}";
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			//// Right
 			//buttonRow = $@"{d.VLine}  {btn1} {btn2}{d.VLine}";
 			//Assert.Equal (width, buttonRow.Length);
 			//(runstate, dlg) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btn1Text), new Button (btn2Text));
-			//GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			//DriverAsserts.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			//Application.End (runstate);
 
 			//// Left
 			//buttonRow = $@"{d.VLine}{btn1} {btn2}  {d.VLine}";
 			//Assert.Equal (width, buttonRow.Length);
 			//(runstate, dlg) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btn1Text), new Button (btn2Text));
-			//GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			//DriverAsserts.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			//Application.End (runstate);
 		}
 
@@ -203,28 +203,28 @@ namespace Terminal.Gui.Views {
 			d.SetBufferSize (buttonRow.Length, 3);
 
 			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Justify
 			buttonRow = $@"{d.VLine}{btn1}  {btn2}  {btn3}{d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
 			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Right
 			buttonRow = $@"{d.VLine}  {btn1} {btn2} {btn3}{d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
 			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Left
 			buttonRow = $@"{d.VLine}{btn1} {btn2} {btn3}  {d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
 			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 		}
 
@@ -256,28 +256,28 @@ namespace Terminal.Gui.Views {
 
 			// Default - Center
 			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Justify
 			buttonRow = $"{d.VLine}{btn1} {btn2}  {btn3}  {btn4}{d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
 			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Right
 			buttonRow = $"{d.VLine}  {btn1} {btn2} {btn3} {btn4}{d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
 			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Left
 			buttonRow = $"{d.VLine}{btn1} {btn2} {btn3} {btn4}  {d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
 			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 		}
 
@@ -312,28 +312,28 @@ namespace Terminal.Gui.Views {
 
 			// Default - Center
 			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Justify
 			buttonRow = $"{d.VLine}{btn1}    {btn2}     {btn3}     {btn4}{d.VLine}";
 			Assert.Equal (width, ustring.Make (buttonRow).ConsoleWidth);
 			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Right
 			buttonRow = $"{d.VLine}           {btn1} {btn2} {btn3} {btn4}{d.VLine}";
 			Assert.Equal (width, ustring.Make (buttonRow).ConsoleWidth);
 			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Left
 			buttonRow = $"{d.VLine}{btn1} {btn2} {btn3} {btn4}           {d.VLine}";
 			Assert.Equal (width, ustring.Make (buttonRow).ConsoleWidth);
 			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 		}
 
@@ -367,28 +367,28 @@ namespace Terminal.Gui.Views {
 
 			// Default - Center
 			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Justify
 			buttonRow = $"{d.VLine}{btn1}    {btn2}     {btn3}     {btn4}{d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
 			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Right
 			buttonRow = $"{d.VLine}           {btn1} {btn2} {btn3} {btn4}{d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
 			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Left
 			buttonRow = $"{d.VLine}{btn1} {btn2} {btn3} {btn4}           {d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
 			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 		}
 
@@ -409,7 +409,7 @@ namespace Terminal.Gui.Views {
 			d.SetBufferSize (buttonRow.Length, 3);
 
 			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, null);
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 
 			Application.End (runstate);
 		}
@@ -432,7 +432,7 @@ namespace Terminal.Gui.Views {
 			d.SetBufferSize (buttonRow.Length, 3);
 
 			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btnText));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 		}
 
@@ -461,56 +461,56 @@ namespace Terminal.Gui.Views {
 			var dlg = new Dialog (title, width, 3, new Button (btn1Text)) { ButtonAlignment = Dialog.ButtonAlignments.Center };
 			runstate = Application.Begin (dlg);
 			var buttonRow = $"{d.VLine}    {btn1}     {d.VLine}";
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 
 			// Now add a second button
 			buttonRow = $"{d.VLine} {btn1} {btn2} {d.VLine}";
 			dlg.AddButton (new Button (btn2Text));
 			bool first = false;
 			Application.RunMainLoopIteration (ref runstate, true, ref first);
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Justify
 			dlg = new Dialog (title, width, 3, new Button (btn1Text)) { ButtonAlignment = Dialog.ButtonAlignments.Justify };
 			runstate = Application.Begin (dlg);
 			buttonRow = $"{d.VLine}         {btn1}{d.VLine}";
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 
 			// Now add a second button
 			buttonRow = $"{d.VLine}{btn1}   {btn2}{d.VLine}";
 			dlg.AddButton (new Button (btn2Text));
 			first = false;
 			Application.RunMainLoopIteration (ref runstate, true, ref first);
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Right
 			dlg = new Dialog (title, width, 3, new Button (btn1Text)) { ButtonAlignment = Dialog.ButtonAlignments.Right };
 			runstate = Application.Begin (dlg);
 			buttonRow = $"{d.VLine}{new String (' ', width - btn1.Length - 2)}{btn1}{d.VLine}";
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 
 			// Now add a second button
 			buttonRow = $"{d.VLine}  {btn1} {btn2}{d.VLine}";
 			dlg.AddButton (new Button (btn2Text));
 			first = false;
 			Application.RunMainLoopIteration (ref runstate, true, ref first);
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Left
 			dlg = new Dialog (title, width, 3, new Button (btn1Text)) { ButtonAlignment = Dialog.ButtonAlignments.Left };
 			runstate = Application.Begin (dlg);
 			buttonRow = $"{d.VLine}{btn1}{new String (' ', width - btn1.Length - 2)}{d.VLine}";
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 
 			// Now add a second button
 			buttonRow = $"{d.VLine}{btn1} {btn2}  {d.VLine}";
 			dlg.AddButton (new Button (btn2Text));
 			first = false;
 			Application.RunMainLoopIteration (ref runstate, true, ref first);
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 		}
 	}

+ 2 - 2
UnitTests/DimTests.cs

@@ -1002,7 +1002,7 @@ namespace Terminal.Gui.Core {
 			field.KeyDown += (k) => {
 				if (k.KeyEvent.Key == Key.Enter) {
 					((FakeDriver)Application.Driver).SetBufferSize (22, count + 4);
-					var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expecteds [count], output);
+					var pos = TestHelpers.AssertDriverContentsWithFrameAre (expecteds [count], output);
 					Assert.Equal (new Rect (0, 0, 22, count + 4), pos);
 
 					if (count < 20) {
@@ -1148,7 +1148,7 @@ namespace Terminal.Gui.Core {
 			field.KeyDown += (k) => {
 				if (k.KeyEvent.Key == Key.Enter) {
 					((FakeDriver)Application.Driver).SetBufferSize (22, count + 4);
-					var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expecteds [count], output);
+					var pos = TestHelpers.AssertDriverContentsWithFrameAre (expecteds [count], output);
 					Assert.Equal (new Rect (0, 0, 22, count + 4), pos);
 
 					if (count > 0) {

+ 18 - 204
UnitTests/GraphViewTests.cs

@@ -93,192 +93,6 @@ namespace Terminal.Gui.Views {
 			return gv;
 		}
 
-#pragma warning disable xUnit1013 // Public method should be marked as test
-		public static void AssertDriverContentsAre (string expectedLook, ITestOutputHelper output)
-		{
-#pragma warning restore xUnit1013 // Public method should be marked as test
-
-			var sb = new StringBuilder ();
-			var driver = ((FakeDriver)Application.Driver);
-
-			var contents = driver.Contents;
-
-			for (int r = 0; r < driver.Rows; r++) {
-				for (int c = 0; c < driver.Cols; c++) {
-					sb.Append ((char)contents [r, c, 0]);
-				}
-				sb.AppendLine ();
-			}
-
-			var actualLook = sb.ToString ();
-
-			if (!string.Equals (expectedLook, actualLook)) {
-
-				// ignore trailing whitespace on each line
-				var trailingWhitespace = new Regex (@"\s+$", RegexOptions.Multiline);
-
-				// get rid of trailing whitespace on each line (and leading/trailing whitespace of start/end of full string)
-				expectedLook = trailingWhitespace.Replace (expectedLook, "").Trim ();
-				actualLook = trailingWhitespace.Replace (actualLook, "").Trim ();
-
-				// standardize line endings for the comparison
-				expectedLook = expectedLook.Replace ("\r\n", "\n");
-				actualLook = actualLook.Replace ("\r\n", "\n");
-
-				output?.WriteLine ("Expected:" + Environment.NewLine + expectedLook);
-				output?.WriteLine ("But Was:" + Environment.NewLine + actualLook);
-
-				Assert.Equal (expectedLook, actualLook);
-			}
-		}
-
-		public static Rect AssertDriverContentsWithFrameAre (string expectedLook, ITestOutputHelper output)
-		{
-			var lines = new List<List<char>> ();
-			var sb = new StringBuilder ();
-			var driver = ((FakeDriver)Application.Driver);
-			var x = -1;
-			var y = -1;
-			int w = -1;
-			int h = -1;
-
-			var contents = driver.Contents;
-
-			for (int r = 0; r < driver.Rows; r++) {
-				var runes = new List<char> ();
-				for (int c = 0; c < driver.Cols; c++) {
-					var rune = (char)contents [r, c, 0];
-					if (rune != ' ') {
-						if (x == -1) {
-							x = c;
-							y = r;
-							for (int i = 0; i < c; i++) {
-								runes.InsertRange (i, new List<char> () { ' ' });
-							}
-						}
-						if (Rune.ColumnWidth (rune) > 1) {
-							c++;
-						}
-						if (c + 1 > w) {
-							w = c + 1;
-						}
-						h = r - y + 1;
-					}
-					if (x > -1) {
-						runes.Add (rune);
-					}
-				}
-				if (runes.Count > 0) {
-					lines.Add (runes);
-				}
-			}
-
-			// Remove unnecessary empty lines
-			if (lines.Count > 0) {
-				for (int r = lines.Count - 1; r > h - 1; r--) {
-					lines.RemoveAt (r);
-				}
-			}
-
-			// Remove trailing whitespace on each line
-			for (int r = 0; r < lines.Count; r++) {
-				List<char> row = lines [r];
-				for (int c = row.Count - 1; c >= 0; c--) {
-					var rune = row [c];
-					if (rune != ' ' || (row.Sum (x => Rune.ColumnWidth (x)) == w)) {
-						break;
-					}
-					row.RemoveAt (c);
-				}
-			}
-
-			// Convert char list to string
-			for (int r = 0; r < lines.Count; r++) {
-				var line = new string (lines [r].ToArray ());
-				if (r == lines.Count - 1) {
-					sb.Append (line);
-				} else {
-					sb.AppendLine (line);
-				}
-			}
-
-			var actualLook = sb.ToString ();
-
-			if (!string.Equals (expectedLook, actualLook)) {
-
-				// standardize line endings for the comparison
-				expectedLook = expectedLook.Replace ("\r\n", "\n");
-				actualLook = actualLook.Replace ("\r\n", "\n");
-
-				// Remove the first and the last line ending from the expectedLook
-				if (expectedLook.StartsWith ("\n")) {
-					expectedLook = expectedLook [1..];
-				}
-				if (expectedLook.EndsWith ("\n")) {
-					expectedLook = expectedLook [..^1];
-				}
-
-				output?.WriteLine ("Expected:" + Environment.NewLine + expectedLook);
-				output?.WriteLine ("But Was:" + Environment.NewLine + actualLook);
-
-				Assert.Equal (expectedLook, actualLook);
-			}
-			return new Rect (x > -1 ? x : 0, y > -1 ? y : 0, w > -1 ? w : 0, h > -1 ? h : 0);
-		}
-
-#pragma warning disable xUnit1013 // Public method should be marked as test
-		/// <summary>
-		/// Verifies the console was rendered using the given <paramref name="expectedColors"/> at the given locations.
-		/// Pass a bitmap of indexes into <paramref name="expectedColors"/> as <paramref name="expectedLook"/> and the
-		/// test method will verify those colors were used in the row/col of the console during rendering
-		/// </summary>
-		/// <param name="expectedLook">Numbers between 0 and 9 for each row/col of the console.  Must be valid indexes of <paramref name="expectedColors"/></param>
-		/// <param name="expectedColors"></param>
-		public static void AssertDriverColorsAre (string expectedLook, Attribute [] expectedColors)
-		{
-#pragma warning restore xUnit1013 // Public method should be marked as test
-
-			if (expectedColors.Length > 10) {
-				throw new ArgumentException ("This method only works for UIs that use at most 10 colors");
-			}
-
-			expectedLook = expectedLook.Trim ();
-			var driver = ((FakeDriver)Application.Driver);
-
-			var contents = driver.Contents;
-
-			int r = 0;
-			foreach (var line in expectedLook.Split ('\n').Select (l => l.Trim ())) {
-
-				for (int c = 0; c < line.Length; c++) {
-
-					int val = contents [r, c, 1];
-
-					var match = expectedColors.Where (e => e.Value == val).ToList ();
-					if (match.Count == 0) {
-						throw new Exception ($"Unexpected color {DescribeColor (val)} was used at row {r} and col {c} (indexes start at 0).  Color value was {val} (expected colors were {string.Join (",", expectedColors.Select (c => c.Value))})");
-					} else if (match.Count > 1) {
-						throw new ArgumentException ($"Bad value for expectedColors, {match.Count} Attributes had the same Value");
-					}
-
-					var colorUsed = Array.IndexOf (expectedColors, match [0]).ToString () [0];
-					var userExpected = line [c];
-
-					if (colorUsed != userExpected) {
-						throw new Exception ($"Colors used did not match expected at row {r} and col {c} (indexes start at 0).  Color index used was {DescribeColor (colorUsed)} but test expected {DescribeColor (userExpected)} (these are indexes into the expectedColors array)");
-					}
-				}
-
-				r++;
-			}
-		}
-
-		private static object DescribeColor (int userExpected)
-		{
-			var a = new Attribute (userExpected);
-			return $"{a.Foreground},{a.Background}";
-		}
-
 		#region Screen to Graph Tests
 
 		[Fact]
@@ -833,7 +647,7 @@ namespace Terminal.Gui.Views {
  │  MM  MM  MM
  ┼──┬M──┬M──┬M──────
    heytherebob  ";
-			GraphViewTests.AssertDriverContentsAre (looksLike, output);
+			TestHelpers.AssertDriverContentsAre (looksLike, output);
 
 			// Shutdown must be called to safely clean up Application if Init has been called
 			Application.Shutdown ();
@@ -1253,7 +1067,7 @@ namespace Terminal.Gui.Views {
 0┼┬┬┬┬┬┬┬┬
  0    5";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 			// user scrolls up one unit of graph space
 			gv.ScrollOffset = new PointF (0, 1f);
@@ -1270,7 +1084,7 @@ namespace Terminal.Gui.Views {
 1┼┬┬┬┬┬┬┬┬
  0    5";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 			// Shutdown must be called to safely clean up Application if Init has been called
 			Application.Shutdown ();
@@ -1297,7 +1111,7 @@ namespace Terminal.Gui.Views {
 0┼┬┬┬┬┬┬┬┬
  0    5";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 			// user scrolls up one unit of graph space
 			gv.ScrollOffset = new PointF (0, 1f);
@@ -1315,7 +1129,7 @@ namespace Terminal.Gui.Views {
 1┼┬┬┬┬┬┬┬┬
  0    5";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 			// Shutdown must be called to safely clean up Application if Init has been called
 			Application.Shutdown ();
@@ -1345,7 +1159,7 @@ namespace Terminal.Gui.Views {
 0┼┬┬┬┬┬┬┬┬
  0    5";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 
 			// Shutdown must be called to safely clean up Application if Init has been called
@@ -1374,7 +1188,7 @@ namespace Terminal.Gui.Views {
 0┼┬┬┬┬┬┬┬┬
  0    5";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 			// Shutdown must be called to safely clean up Application if Init has been called
 			Application.Shutdown ();
@@ -1409,7 +1223,7 @@ namespace Terminal.Gui.Views {
 0┼┬┬┬┬┬┬┬┬
  0    5";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 
 			// Shutdown must be called to safely clean up Application if Init has been called
@@ -1444,7 +1258,7 @@ namespace Terminal.Gui.Views {
 0┼┬┬┬┬┬┬┬┬
  0    5";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 
 			// Shutdown must be called to safely clean up Application if Init has been called
@@ -1473,7 +1287,7 @@ namespace Terminal.Gui.Views {
 0┼┬┬┬┬┬┬┬┬
  0    5";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 
 			// Shutdown must be called to safely clean up Application if Init has been called
@@ -1515,7 +1329,7 @@ namespace Terminal.Gui.Views {
 0┼┬┬┬┬┬┬┬┬
  0    5";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 
 			// Shutdown must be called to safely clean up Application if Init has been called
@@ -1554,7 +1368,7 @@ namespace Terminal.Gui.Views {
  0    5   
           
           ";
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 
 			// Shutdown must be called to safely clean up Application if Init has been called
@@ -1595,7 +1409,7 @@ namespace Terminal.Gui.Views {
    0    5
          
           ";
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 
 			// Shutdown must be called to safely clean up Application if Init has been called
@@ -1622,7 +1436,7 @@ namespace Terminal.Gui.Views {
          
          
           ";
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 
 			// Shutdown must be called to safely clean up Application if Init has been called
@@ -1647,7 +1461,7 @@ namespace Terminal.Gui.Views {
          
          
           ";
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 
 			// Shutdown must be called to safely clean up Application if Init has been called
@@ -1679,7 +1493,7 @@ namespace Terminal.Gui.Views {
 0┼┬┬┬┬┬┬┬┬
  0    5";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 
 			// Shutdown must be called to safely clean up Application if Init has been called
@@ -1726,14 +1540,14 @@ namespace Terminal.Gui.Views {
 				mount.Redraw (mount.Bounds);
 
 				// should have the initial text
-				GraphViewTests.AssertDriverContentsAre ("ff", null);
+				TestHelpers.AssertDriverContentsAre ("ff", null);
 
 				// change the text and redraw
 				lbl1.Text = "ff1234";
 				mount.Redraw (mount.Bounds);
 
 				// should have the new text rendered
-				GraphViewTests.AssertDriverContentsAre ("ff1234", null);
+				TestHelpers.AssertDriverContentsAre ("ff1234", null);
 
 
 			} finally {

+ 11 - 11
UnitTests/ListViewTests.cs

@@ -247,7 +247,7 @@ namespace Terminal.Gui.Views {
 			Application.Refresh ();
 
 			Assert.Equal (0, lv.SelectedItem);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌──────────┐
 │Line0     │
 │Line1     │
@@ -264,7 +264,7 @@ namespace Terminal.Gui.Views {
 			Assert.True (lv.ScrollDown (10));
 			lv.Redraw (lv.Bounds);
 			Assert.Equal (0, lv.SelectedItem);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌──────────┐
 │Line10    │
 │Line11    │
@@ -281,7 +281,7 @@ namespace Terminal.Gui.Views {
 			Assert.True (lv.MoveDown ());
 			lv.Redraw (lv.Bounds);
 			Assert.Equal (1, lv.SelectedItem);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌──────────┐
 │Line1     │
 │Line2     │
@@ -298,7 +298,7 @@ namespace Terminal.Gui.Views {
 			Assert.True (lv.MoveEnd ());
 			lv.Redraw (lv.Bounds);
 			Assert.Equal (19, lv.SelectedItem);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌──────────┐
 │Line19    │
 │          │
@@ -315,7 +315,7 @@ namespace Terminal.Gui.Views {
 			Assert.True (lv.ScrollUp (20));
 			lv.Redraw (lv.Bounds);
 			Assert.Equal (19, lv.SelectedItem);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌──────────┐
 │Line0     │
 │Line1     │
@@ -332,7 +332,7 @@ namespace Terminal.Gui.Views {
 			Assert.True (lv.MoveDown ());
 			lv.Redraw (lv.Bounds);
 			Assert.Equal (19, lv.SelectedItem);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌──────────┐
 │Line10    │
 │Line11    │
@@ -349,7 +349,7 @@ namespace Terminal.Gui.Views {
 			Assert.True (lv.ScrollUp (20));
 			lv.Redraw (lv.Bounds);
 			Assert.Equal (19, lv.SelectedItem);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌──────────┐
 │Line0     │
 │Line1     │
@@ -366,7 +366,7 @@ namespace Terminal.Gui.Views {
 			Assert.True (lv.MoveDown ());
 			lv.Redraw (lv.Bounds);
 			Assert.Equal (19, lv.SelectedItem);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌──────────┐
 │Line10    │
 │Line11    │
@@ -383,7 +383,7 @@ namespace Terminal.Gui.Views {
 			Assert.True (lv.MoveHome ());
 			lv.Redraw (lv.Bounds);
 			Assert.Equal (0, lv.SelectedItem);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌──────────┐
 │Line0     │
 │Line1     │
@@ -400,7 +400,7 @@ namespace Terminal.Gui.Views {
 			Assert.True (lv.ScrollDown (20));
 			lv.Redraw (lv.Bounds);
 			Assert.Equal (0, lv.SelectedItem);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌──────────┐
 │Line19    │
 │          │
@@ -417,7 +417,7 @@ namespace Terminal.Gui.Views {
 			Assert.True (lv.MoveUp ());
 			lv.Redraw (lv.Bounds);
 			Assert.Equal (0, lv.SelectedItem);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌──────────┐
 │Line0     │
 │Line1     │

+ 335 - 260
UnitTests/MenuTests.cs

@@ -1,7 +1,9 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using Xunit;
 using Xunit.Abstractions;
+using static Terminal.Gui.Views.MenuTests;
 
 namespace Terminal.Gui.Views {
 	public class MenuTests {
@@ -148,7 +150,7 @@ Edit
 │ Copy   Copies the selection. │
 └──────────────────────────────┘
 ";
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 			cancelClosing = true;
 			Assert.True (menu.ProcessHotKey (new KeyEvent (Key.F9, new KeyModifiers ())));
@@ -161,7 +163,7 @@ Edit
 │ Copy   Copies the selection. │
 └──────────────────────────────┘
 ";
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 			cancelClosing = false;
 			Assert.True (menu.ProcessHotKey (new KeyEvent (Key.F9, new KeyModifiers ())));
@@ -171,7 +173,7 @@ Edit
 			expected = @"
 Edit
 ";
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 			void New () => miAction = "New";
 			void Copy () => miAction = "Copy";
@@ -606,7 +608,7 @@ Edit
 └──────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 1, 8, 4), pos);
 		}
 
@@ -635,7 +637,7 @@ Edit
 ──────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 7, 4), pos);
 
 			menu.CloseAllMenus ();
@@ -649,7 +651,7 @@ Edit
 ──────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (1, 0, 7, 3), pos);
 
 			menu.CloseAllMenus ();
@@ -665,7 +667,7 @@ Edit
 └──────
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 1, 7, 4), pos);
 
 			menu.CloseAllMenus ();
@@ -680,7 +682,7 @@ Edit
 │ Two  
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 7, 3), pos);
 		}
 
@@ -705,16 +707,15 @@ Edit
 
 			Application.Top.Redraw (Application.Top.Bounds);
 			var expected = @"
-  Numbers
+ Numbers
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 9, 1), pos);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 
 			Assert.True (menu.ProcessHotKey (new KeyEvent (Key.F9, null)));
 			Application.Top.Redraw (Application.Top.Bounds);
 			expected = @"
-  Numbers 
+ Numbers  
 ┌────────┐
 │ One    │
 │ Two   ►│
@@ -722,13 +723,12 @@ Edit
 └────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 10, 6), pos);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 
 			Assert.True (Application.Top.Subviews [1].ProcessKey (new KeyEvent (Key.CursorDown, null)));
 			Application.Top.Redraw (Application.Top.Bounds);
 			expected = @"
-  Numbers                
+ Numbers                 
 ┌────────┐               
 │ One    │               
 │ Two   ►│┌─────────────┐
@@ -737,13 +737,12 @@ Edit
           └─────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 25, 7), pos);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 
 			Assert.True (Application.Top.Subviews [2].ProcessKey (new KeyEvent (Key.CursorLeft, null)));
 			Application.Top.Redraw (Application.Top.Bounds);
 			expected = @"
-  Numbers 
+ Numbers  
 ┌────────┐
 │ One    │
 │ Two   ►│
@@ -751,17 +750,15 @@ Edit
 └────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 10, 6), pos);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 
 			Assert.True (Application.Top.Subviews [1].ProcessKey (new KeyEvent (Key.Esc, null)));
 			Application.Top.Redraw (Application.Top.Bounds);
 			expected = @"
-  Numbers
+ Numbers
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 9, 1), pos);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 		}
 
 		[Fact, AutoInitShutdown]
@@ -785,11 +782,11 @@ Edit
 
 			Application.Top.Redraw (Application.Top.Bounds);
 			var expected = @"
-  Numbers
+ Numbers
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 9, 1), pos);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (1, 0, 8, 1), pos);
 
 			Assert.True (menu.MouseEvent (new MouseEvent () {
 				X = 1,
@@ -799,7 +796,7 @@ Edit
 			}));
 			Application.Top.Redraw (Application.Top.Bounds);
 			expected = @"
-  Numbers 
+ Numbers  
 ┌────────┐
 │ One    │
 │ Two   ►│
@@ -807,8 +804,8 @@ Edit
 └────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 10, 6), pos);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (1, 0, 10, 6), pos);
 
 			Assert.False (menu.MouseEvent (new MouseEvent () {
 				X = 1,
@@ -818,7 +815,7 @@ Edit
 			}));
 			Application.Top.Redraw (Application.Top.Bounds);
 			expected = @"
-  Numbers                
+ Numbers                 
 ┌────────┐               
 │ One    │               
 │ Two   ►│┌─────────────┐
@@ -827,8 +824,8 @@ Edit
           └─────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 25, 7), pos);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (1, 0, 25, 7), pos);
 
 			Assert.False (menu.MouseEvent (new MouseEvent () {
 				X = 1,
@@ -838,7 +835,7 @@ Edit
 			}));
 			Application.Top.Redraw (Application.Top.Bounds);
 			expected = @"
-  Numbers 
+ Numbers  
 ┌────────┐
 │ One    │
 │ Two   ►│
@@ -846,8 +843,8 @@ Edit
 └────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 10, 6), pos);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (1, 0, 10, 6), pos);
 
 			Assert.False (menu.MouseEvent (new MouseEvent () {
 				X = 70,
@@ -857,11 +854,11 @@ Edit
 			}));
 			Application.Top.Redraw (Application.Top.Bounds);
 			expected = @"
-  Numbers
+ Numbers
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 9, 1), pos);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (1, 0, 8, 1), pos);
 		}
 
 		[Fact, AutoInitShutdown]
@@ -887,16 +884,16 @@ Edit
 
 			Application.Top.Redraw (Application.Top.Bounds);
 			var expected = @"
-  Numbers
+ Numbers
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 9, 1), pos);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (1, 0, 8, 1), pos);
 
 			Assert.True (menu.ProcessHotKey (new KeyEvent (Key.F9, null)));
 			Application.Top.Redraw (Application.Top.Bounds);
 			expected = @"
-  Numbers 
+ Numbers  
 ┌────────┐
 │ One    │
 │ Two   ►│
@@ -904,14 +901,14 @@ Edit
 └────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 10, 6), pos);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (1, 0, 10, 6), pos);
 
 			Assert.True (Application.Top.Subviews [1].ProcessKey (new KeyEvent (Key.CursorDown, null)));
 			Assert.True (Application.Top.Subviews [1].ProcessKey (new KeyEvent (Key.Enter, null)));
 			Application.Top.Redraw (Application.Top.Bounds);
 			expected = @"
-  Numbers      
+ Numbers       
 ┌─────────────┐
 │◄    Two     │
 ├─────────────┤
@@ -920,13 +917,13 @@ Edit
 └─────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 15, 7), pos);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (1, 0, 15, 7), pos);
 
 			Assert.True (Application.Top.Subviews [2].ProcessKey (new KeyEvent (Key.Enter, null)));
 			Application.Top.Redraw (Application.Top.Bounds);
 			expected = @"
-  Numbers 
+ Numbers  
 ┌────────┐
 │ One    │
 │ Two   ►│
@@ -934,17 +931,17 @@ Edit
 └────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 10, 6), pos);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (1, 0, 10, 6), pos);
 
 			Assert.True (Application.Top.Subviews [1].ProcessKey (new KeyEvent (Key.Esc, null)));
 			Application.Top.Redraw (Application.Top.Bounds);
 			expected = @"
-  Numbers
+ Numbers
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 9, 1), pos);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (1, 0, 8, 1), pos);
 		}
 
 		[Fact, AutoInitShutdown]
@@ -970,11 +967,11 @@ Edit
 
 			Application.Top.Redraw (Application.Top.Bounds);
 			var expected = @"
-  Numbers
+ Numbers
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 9, 1), pos);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (1, 0, 8, 1), pos);
 
 			Assert.True (menu.MouseEvent (new MouseEvent () {
 				X = 1,
@@ -984,7 +981,7 @@ Edit
 			}));
 			Application.Top.Redraw (Application.Top.Bounds);
 			expected = @"
-  Numbers 
+ Numbers  
 ┌────────┐
 │ One    │
 │ Two   ►│
@@ -992,8 +989,8 @@ Edit
 └────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 10, 6), pos);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (1, 0, 10, 6), pos);
 
 			Assert.False (menu.MouseEvent (new MouseEvent () {
 				X = 1,
@@ -1003,7 +1000,7 @@ Edit
 			}));
 			Application.Top.Redraw (Application.Top.Bounds);
 			expected = @"
-  Numbers      
+ Numbers       
 ┌─────────────┐
 │◄    Two     │
 ├─────────────┤
@@ -1012,8 +1009,8 @@ Edit
 └─────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 15, 7), pos);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (1, 0, 15, 7), pos);
 
 			Assert.False (menu.MouseEvent (new MouseEvent () {
 				X = 1,
@@ -1023,7 +1020,7 @@ Edit
 			}));
 			Application.Top.Redraw (Application.Top.Bounds);
 			expected = @"
-  Numbers 
+ Numbers  
 ┌────────┐
 │ One    │
 │ Two   ►│
@@ -1031,8 +1028,8 @@ Edit
 └────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 10, 6), pos);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (1, 0, 10, 6), pos);
 
 			Assert.False (menu.MouseEvent (new MouseEvent () {
 				X = 70,
@@ -1042,11 +1039,11 @@ Edit
 			}));
 			Application.Top.Redraw (Application.Top.Bounds);
 			expected = @"
-  Numbers
+ Numbers
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 9, 1), pos);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (1, 0, 8, 1), pos);
 		}
 
 		[Fact, AutoInitShutdown]
@@ -1074,11 +1071,11 @@ Edit
 			Assert.True (menu.IsMenuOpen);
 			Application.Top.Redraw (Application.Top.Bounds);
 			var expected = @"
-  File   Edit
+ File  Edit
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 13, 1), pos);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (1, 0, 11, 1), pos);
 
 			Assert.True (menu.ProcessKey (new (Key.N, null)));
 			Application.MainLoop.MainIteration ();
@@ -1088,11 +1085,11 @@ Edit
 			Assert.True (menu.IsMenuOpen);
 			Application.Top.Redraw (Application.Top.Bounds);
 			expected = @"
-  File   Edit
+ File  Edit
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 13, 1), pos);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (1, 0, 11, 1), pos);
 
 			Assert.True (menu.ProcessKey (new (Key.CursorRight, null)));
 			Assert.True (menu.ProcessKey (new (Key.C, null)));
@@ -1100,21 +1097,150 @@ Edit
 			Assert.True (copyAction);
 		}
 
+		// Defines the expected strings for a Menu. Currently supports 
+		//   - MenuBar with any number of MenuItems 
+		//   - Each top-level MenuItem can have a SINGLE sub-menu
+		//
+		// TODO: Enable multiple sub-menus
+		// TODO: Enable checked sub-menus
+		// TODO: Enable sub-menus with sub-menus (perhaps better to put this in a separate class with focused unit tests?)
+		//
+		// E.g: 
+		//
+		// File  Edit
+		//  New    Copy
+		public class ExpectedMenuBar : MenuBar {
+			FakeDriver d = ((FakeDriver)Application.Driver);
+
+			// Each MenuBar title has a 1 space pad on each side
+			// See `static int leftPadding` and `static int rightPadding` on line 1037 of Menu.cs
+			public string MenuBarText {
+				get {
+					string txt = string.Empty;
+					foreach (var m in Menus) {
+
+						txt += " " + m.Title.ToString () + " ";
+					}
+					return txt;
+				}
+			}
+
+			// The expected strings when the menu is closed
+			public string ClosedMenuText => MenuBarText + "\n";
+
+			// Padding for the X of the sub menu Frane
+			// Menu.cs - Line 1239 in `internal void OpenMenu` is where the Menu is created
+			string padding (int i)
+			{
+				int n = 0;
+				while (i > 0){
+					n += Menus [i-1].TitleLength + 2;
+					i--;
+				}
+				return new string (' ', n);
+			}
+
+			// Define expected menu frame
+			// "┌──────┐"
+			// "│ New  │"
+			// "└──────┘"
+			// 
+			// The width of the Frame is determined in Menu.cs line 144, where `Width` is calculated
+			//   1 space before the Title and 2 spaces after the Title/Check/Help
+			public string expectedTopRow (int i) => $"{d.ULCorner}{new String (d.HLine.ToString () [0], Menus [i].Children [0].TitleLength + 3)}{d.URCorner}  \n";
+			// The 3 spaces at end are a result of Menu.cs line 1062 where `pos` is calculated (` + spacesAfterTitle`)
+			public string expectedMenuItemRow (int i) => $"{d.VLine} {Menus [i].Children [0].Title}  {d.VLine}   \n";
+			public string expectedBottomRow (int i) => $"{d.LLCorner}{new String (d.HLine.ToString () [0], Menus [i].Children [0].TitleLength + 3)}{d.LRCorner}  \n";
+
+			// The fulll expected string for an open sub menu
+			public string expectedSubMenuOpen (int i) => ClosedMenuText + 
+				(Menus [i].Children.Length > 0 ?
+					padding (i) + expectedTopRow (i) +
+					padding (i) + expectedMenuItemRow (i) +
+					padding (i) + expectedBottomRow (i) 
+				: 
+				"");
+
+			public ExpectedMenuBar (MenuBarItem [] menus) : base (menus)
+			{
+			}
+		}
+
+		[Fact, AutoInitShutdown]
+		public void MenuBar_Submenus_Alignment_Correct ()
+		{
+			// Define the expected menu
+			var expectedMenu = new ExpectedMenuBar (new MenuBarItem [] {
+				new MenuBarItem ("File", new MenuItem [] {
+					new MenuItem ("Really Long Sub Menu", "",  null)
+				}),
+				new MenuBarItem ("123", new MenuItem [] {
+					new MenuItem ("Copy", "", null)
+				}),
+				new MenuBarItem ("Format", new MenuItem [] {
+					new MenuItem ("Word Wrap", "", null)
+				}),
+				new MenuBarItem ("Help", new MenuItem [] {
+					new MenuItem ("About", "", null)
+				}),
+				new MenuBarItem ("1", new MenuItem [] {
+					new MenuItem ("2", "", null)
+				}),
+				new MenuBarItem ("3", new MenuItem [] {
+					new MenuItem ("2", "", null)
+				}),
+				new MenuBarItem ("Last one", new MenuItem [] {
+					new MenuItem ("Test", "", null)
+				})
+			});
+
+			MenuBarItem [] items = new MenuBarItem [expectedMenu.Menus.Length];
+			for (var i = 0; i < expectedMenu.Menus.Length; i++) {
+				items [i] = new MenuBarItem (expectedMenu.Menus [i].Title, new MenuItem [] {
+					new MenuItem (expectedMenu.Menus [i].Children [0].Title, "", null)
+				});
+			}
+			var menu = new MenuBar (items);
+
+			Application.Top.Add (menu);
+
+			Application.Top.Redraw (Application.Top.Bounds);
+			TestHelpers.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output);
+
+			for (var i = 0; i < expectedMenu.Menus.Length; i++) {
+				menu.OpenMenu (i);
+				Assert.True (menu.IsMenuOpen);
+				Application.Top.Redraw (Application.Top.Bounds);
+				TestHelpers.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (i), output);
+			}
+		}
+
 		[Fact, AutoInitShutdown]
 		public void HotKey_MenuBar_ProcessHotKey_Menu_ProcessKey ()
 		{
 			var newAction = false;
 			var copyAction = false;
 
-			var menu = new MenuBar (new MenuBarItem [] {
-				new MenuBarItem ("_File", new MenuItem [] {
-					new MenuItem ("_New", "", () => newAction = true)
+			// Define the expected menu
+			var expectedMenu = new ExpectedMenuBar (new MenuBarItem [] {
+				new MenuBarItem ("File", new MenuItem [] {
+					new MenuItem ("New", "",  null)
 				}),
-				new MenuBarItem ("_Edit", new MenuItem [] {
-					new MenuItem ("_Copy", "", () => copyAction = true)
+				new MenuBarItem ("Edit", new MenuItem [] {
+					new MenuItem ("Copy", "", null)
 				})
 			});
 
+			// The real menu
+			var menu = new MenuBar (new MenuBarItem [] {
+				new MenuBarItem ("_" + expectedMenu.Menus[0].Title, new MenuItem [] {
+					new MenuItem ("_" + expectedMenu.Menus[0].Children[0].Title, "",  () => newAction = true)
+				}),
+				new MenuBarItem ("_" + expectedMenu.Menus[1].Title, new MenuItem [] {
+					new MenuItem ("_" + expectedMenu.Menus[1].Children[0].Title, "",  () => copyAction = true)
+				}),
+			});
+
 			Application.Top.Add (menu);
 
 			Assert.False (newAction);
@@ -1123,15 +1249,7 @@ Edit
 			Assert.True (menu.ProcessHotKey (new (Key.AltMask | Key.F, new KeyModifiers () { Alt = true })));
 			Assert.True (menu.IsMenuOpen);
 			Application.Top.Redraw (Application.Top.Bounds);
-			var expected = @"
-  File   Edit
-┌──────┐     
-│ New  │     
-└──────┘     
-";
-
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 13, 4), pos);
+			TestHelpers.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (0), output);
 
 			Assert.True (Application.Top.Subviews [1].ProcessKey (new (Key.N, null)));
 			Application.MainLoop.MainIteration ();
@@ -1140,15 +1258,7 @@ Edit
 			Assert.True (menu.ProcessHotKey (new (Key.AltMask | Key.E, new KeyModifiers () { Alt = true })));
 			Assert.True (menu.IsMenuOpen);
 			Application.Top.Redraw (Application.Top.Bounds);
-			expected = @"
-  File   Edit   
-       ┌───────┐
-       │ Copy  │
-       └───────┘
-";
-
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 16, 4), pos);
+			TestHelpers.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (1), output);
 
 			Assert.True (Application.Top.Subviews [1].ProcessKey (new (Key.C, null)));
 			Application.MainLoop.MainIteration ();
@@ -1158,127 +1268,114 @@ Edit
 		[Fact, AutoInitShutdown]
 		public void MenuBar_Position_And_Size_With_HotKeys_Is_The_Same_As_Without_HotKeys ()
 		{
-			// With HotKeys
+			// Define the expected menu
+			var expectedMenu = new ExpectedMenuBar (new MenuBarItem [] {
+				new MenuBarItem ("File", new MenuItem [] {
+					new MenuItem ("12", "",  null)
+				}),
+				new MenuBarItem ("Edit", new MenuItem [] {
+					new MenuItem ("Copy", "", null)
+				})
+			});
+
+			// Test without HotKeys first
 			var menu = new MenuBar (new MenuBarItem [] {
-				new MenuBarItem ("_File", new MenuItem [] {
-					new MenuItem ("_New", "", null)
+				new MenuBarItem (expectedMenu.Menus[0].Title, new MenuItem [] {
+					new MenuItem (expectedMenu.Menus[0].Children[0].Title, "", null)
 				}),
-				new MenuBarItem ("_Edit", new MenuItem [] {
-					new MenuItem ("_Copy", "", null)
+				new MenuBarItem (expectedMenu.Menus[1].Title, new MenuItem [] {
+					new MenuItem (expectedMenu.Menus[1].Children[0].Title, "", null)
 				})
 			});
 
 			Application.Top.Add (menu);
 
+			// Open first
 			Assert.True (menu.ProcessHotKey (new (Key.F9, new KeyModifiers ())));
 			Assert.True (menu.IsMenuOpen);
 			Application.Top.Redraw (Application.Top.Bounds);
-			var expected = @"
-  File   Edit
-┌──────┐     
-│ New  │     
-└──────┘     
-";
-
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 13, 4), pos);
+			TestHelpers.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (0), output);
 
+			// Open second
 			Assert.True (Application.Top.Subviews [1].ProcessKey (new (Key.CursorRight, null)));
 			Assert.True (menu.IsMenuOpen);
 			Application.Top.Redraw (Application.Top.Bounds);
-			expected = @"
-  File   Edit   
-       ┌───────┐
-       │ Copy  │
-       └───────┘
-";
-
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 16, 4), pos);
+			TestHelpers.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (1), output);
 
+			// Close menu
 			Assert.True (menu.ProcessHotKey (new (Key.F9, new KeyModifiers ())));
 			Assert.False (menu.IsMenuOpen);
 			Application.Top.Redraw (Application.Top.Bounds);
-			expected = @"
-  File   Edit
-";
+			TestHelpers.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output);
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 13, 1), pos);
+			Application.Top.Remove (menu);
 
-			// Without HotKeys
+			// Now test WITH HotKeys
 			menu = new MenuBar (new MenuBarItem [] {
-				new MenuBarItem ("File", new MenuItem [] {
-					new MenuItem ("New", "", null)
+				new MenuBarItem ("_" + expectedMenu.Menus[0].Title, new MenuItem [] {
+					new MenuItem ("_" + expectedMenu.Menus[0].Children[0].Title, "",  null)
+				}),
+				new MenuBarItem ("_" + expectedMenu.Menus[1].Title, new MenuItem [] {
+					new MenuItem ("_" + expectedMenu.Menus[1].Children[0].Title, "",  null)
 				}),
-				new MenuBarItem ("Edit", new MenuItem [] {
-					new MenuItem ("Copy", "", null)
-				})
 			});
 
+			Application.Top.Add (menu);
+
+			// Open first
 			Assert.True (menu.ProcessHotKey (new (Key.F9, new KeyModifiers ())));
 			Assert.True (menu.IsMenuOpen);
 			Application.Top.Redraw (Application.Top.Bounds);
-			expected = @"
-  File   Edit
-┌──────┐     
-│ New  │     
-└──────┘     
-";
-
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 13, 4), pos);
+			TestHelpers.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (0), output);
 
+			// Open second
 			Assert.True (Application.Top.Subviews [1].ProcessKey (new (Key.CursorRight, null)));
 			Assert.True (menu.IsMenuOpen);
 			Application.Top.Redraw (Application.Top.Bounds);
-			expected = @"
-  File   Edit   
-       ┌───────┐
-       │ Copy  │
-       └───────┘
-";
+			TestHelpers.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (1), output);
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 16, 4), pos);
+			// Close menu
+			Assert.True (menu.ProcessHotKey (new (Key.F9, new KeyModifiers ())));
+			Assert.False (menu.IsMenuOpen);
+			Application.Top.Redraw (Application.Top.Bounds);
+			TestHelpers.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output);
 		}
 
 		[Fact, AutoInitShutdown]
 		public void MenuBar_ButtonPressed_Open_The_Menu_ButtonPressed_Again_Close_The_Menu ()
 		{
-			var menu = new MenuBar (new MenuBarItem [] {
+			// Define the expected menu
+			var expectedMenu = new ExpectedMenuBar (new MenuBarItem [] {
 				new MenuBarItem ("File", new MenuItem [] {
-					new MenuItem ("New", "", null)
+					new MenuItem ("Open", "",  null)
 				}),
 				new MenuBarItem ("Edit", new MenuItem [] {
 					new MenuItem ("Copy", "", null)
 				})
 			});
 
+			// Test without HotKeys first
+			var menu = new MenuBar (new MenuBarItem [] {
+				new MenuBarItem ("_" + expectedMenu.Menus[0].Title, new MenuItem [] {
+					new MenuItem ("_" + expectedMenu.Menus[0].Children[0].Title, "",  null)
+				}),
+				new MenuBarItem ("_" + expectedMenu.Menus[1].Title, new MenuItem [] {
+					new MenuItem ("_" + expectedMenu.Menus[1].Children[0].Title, "",  null)
+				}),
+			});
+
 			Application.Top.Add (menu);
 
 			Assert.True (menu.MouseEvent (new MouseEvent () { X = 1, Y = 0, Flags = MouseFlags.Button1Pressed, View = menu }));
 			Assert.True (menu.IsMenuOpen);
 			Application.Top.Redraw (Application.Top.Bounds);
-			var expected = @"
-  File   Edit
-┌──────┐     
-│ New  │     
-└──────┘     
-";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 13, 4), pos);
+			TestHelpers.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (0), output);
 
 			Assert.True (menu.MouseEvent (new MouseEvent () { X = 1, Y = 0, Flags = MouseFlags.Button1Pressed, View = menu }));
 			Assert.False (menu.IsMenuOpen);
 			Application.Top.Redraw (Application.Top.Bounds);
-			expected = @"
-  File   Edit
-";
-
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 13, 1), pos);
+			TestHelpers.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output);
 		}
 
 		[Fact]
@@ -1300,110 +1397,98 @@ Edit
 		[Fact, AutoInitShutdown]
 		public void Parent_MenuItem_Stay_Focused_If_Child_MenuItem_Is_Empty_By_Mouse ()
 		{
-			var menu = new MenuBar (new MenuBarItem [] {
+			// File  Edit  Format
+			//┌──────┐    ┌───────┐         
+			//│ New  │    │ Wrap  │         
+			//└──────┘    └───────┘         
+
+			// Define the expected menu
+			var expectedMenu = new ExpectedMenuBar (new MenuBarItem [] {
 				new MenuBarItem ("File", new MenuItem [] {
-					new MenuItem ("New", "", null)
-				}),
-				new MenuBarItem ("Edit", new MenuItem [] {
+					new MenuItem ("New", "",  null)
 				}),
+				new MenuBarItem ("Edit", new MenuItem [] {}),
 				new MenuBarItem ("Format", new MenuItem [] {
 					new MenuItem ("Wrap", "", null)
 				})
 			});
+
+			var menu = new MenuBar (new MenuBarItem [] {
+				new MenuBarItem (expectedMenu.Menus[0].Title, new MenuItem [] {
+					new MenuItem (expectedMenu.Menus[0].Children[0].Title, "", null)
+				}),
+				new MenuBarItem (expectedMenu.Menus[1].Title, new MenuItem [] {}),
+				new MenuBarItem (expectedMenu.Menus[2].Title, new MenuItem [] {
+					new MenuItem (expectedMenu.Menus[2].Children[0].Title, "", null)
+				})
+			});
+
 			var tf = new TextField () { Y = 2, Width = 10 };
 			Application.Top.Add (menu, tf);
-
 			Application.Begin (Application.Top);
+
 			Assert.True (tf.HasFocus);
 			Assert.True (menu.MouseEvent (new MouseEvent () { X = 1, Y = 0, Flags = MouseFlags.Button1Pressed, View = menu }));
 			Assert.True (menu.IsMenuOpen);
 			Assert.False (tf.HasFocus);
 			Application.Top.Redraw (Application.Top.Bounds);
-			var expected = @"
-  File   Edit   Format
-┌──────┐              
-│ New  │              
-└──────┘              
-";
-
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 22, 4), pos);
+			TestHelpers.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (0), output);
 
 			Assert.True (menu.MouseEvent (new MouseEvent () { X = 8, Y = 0, Flags = MouseFlags.ReportMousePosition, View = menu }));
 			Assert.True (menu.IsMenuOpen);
 			Assert.False (tf.HasFocus);
 			Application.Top.Redraw (Application.Top.Bounds);
-			expected = @"
-  File   Edit   Format
-";
-
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 22, 1), pos);
+			TestHelpers.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (1), output);
 
 			Assert.True (menu.MouseEvent (new MouseEvent () { X = 15, Y = 0, Flags = MouseFlags.ReportMousePosition, View = menu }));
 			Assert.True (menu.IsMenuOpen);
 			Assert.False (tf.HasFocus);
 			Application.Top.Redraw (Application.Top.Bounds);
-			expected = @"
-  File   Edit   Format 
-              ┌───────┐
-              │ Wrap  │
-              └───────┘
-";
-
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 23, 4), pos);
+			TestHelpers.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (2), output);
 
 			Assert.True (menu.MouseEvent (new MouseEvent () { X = 8, Y = 0, Flags = MouseFlags.ReportMousePosition, View = menu }));
 			Assert.True (menu.IsMenuOpen);
 			Assert.False (tf.HasFocus);
 			Application.Top.Redraw (Application.Top.Bounds);
-			expected = @"
-  File   Edit   Format
-";
-
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 22, 1), pos);
+			TestHelpers.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output);
 
 			Assert.True (menu.MouseEvent (new MouseEvent () { X = 1, Y = 0, Flags = MouseFlags.ReportMousePosition, View = menu }));
 			Assert.True (menu.IsMenuOpen);
 			Assert.False (tf.HasFocus);
 			Application.Top.Redraw (Application.Top.Bounds);
-			expected = @"
-  File   Edit   Format
-┌──────┐              
-│ New  │              
-└──────┘              
-";
+			TestHelpers.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (0), output);
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 22, 4), pos);
 
 			Assert.True (menu.MouseEvent (new MouseEvent () { X = 8, Y = 0, Flags = MouseFlags.Button1Pressed, View = menu }));
 			Assert.False (menu.IsMenuOpen);
 			Assert.True (tf.HasFocus);
 			Application.Top.Redraw (Application.Top.Bounds);
-			expected = @"
-  File   Edit   Format
-";
-
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 22, 1), pos);
+			TestHelpers.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output);
 		}
 
 		[Fact, AutoInitShutdown]
 		public void Parent_MenuItem_Stay_Focused_If_Child_MenuItem_Is_Empty_By_Keyboard ()
 		{
-			var menu = new MenuBar (new MenuBarItem [] {
+			var expectedMenu = new ExpectedMenuBar (new MenuBarItem [] {
 				new MenuBarItem ("File", new MenuItem [] {
 					new MenuItem ("New", "", null)
 				}),
-				new MenuBarItem ("Edit", new MenuItem [] {
-				}),
+				new MenuBarItem ("Edit", Array.Empty<MenuItem> ()),
 				new MenuBarItem ("Format", new MenuItem [] {
 					new MenuItem ("Wrap", "", null)
 				})
 			});
+
+			MenuBarItem [] items = new MenuBarItem [expectedMenu.Menus.Length];
+			for (var i = 0; i < expectedMenu.Menus.Length; i++) {
+				items [i] = new MenuBarItem (expectedMenu.Menus [i].Title, expectedMenu.Menus [i].Children.Length > 0 
+					? new MenuItem [] {
+						new MenuItem (expectedMenu.Menus [i].Children [0].Title, "", null),
+					} 
+					: Array.Empty<MenuItem> ());
+			}
+			var menu = new MenuBar (items);
+			
 			var tf = new TextField () { Y = 2, Width = 10 };
 			Application.Top.Add (menu, tf);
 
@@ -1413,76 +1498,66 @@ Edit
 			Assert.True (menu.IsMenuOpen);
 			Assert.False (tf.HasFocus);
 			Application.Top.Redraw (Application.Top.Bounds);
-			var expected = @"
-  File   Edit   Format
-┌──────┐              
-│ New  │              
-└──────┘              
-";
-
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 22, 4), pos);
+			TestHelpers.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen(0), output);
 
+			// Right - Edit has no sub menu; this tests that no sub menu shows
 			Assert.True (menu.openMenu.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
 			Assert.True (menu.IsMenuOpen);
 			Assert.False (tf.HasFocus);
 			Application.Top.Redraw (Application.Top.Bounds);
-			expected = @"
-  File   Edit   Format
-";
-
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 22, 1), pos);
+			TestHelpers.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (1), output);
 
+			// Right - Format
 			Assert.True (menu.openMenu.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
 			Assert.True (menu.IsMenuOpen);
 			Assert.False (tf.HasFocus);
 			Application.Top.Redraw (Application.Top.Bounds);
-			expected = @"
-  File   Edit   Format 
-              ┌───────┐
-              │ Wrap  │
-              └───────┘
-";
-
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 23, 4), pos);
+			TestHelpers.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (2), output);
 
+			// Left - Edit
 			Assert.True (menu.openMenu.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ())));
 			Assert.True (menu.IsMenuOpen);
 			Assert.False (tf.HasFocus);
 			Application.Top.Redraw (Application.Top.Bounds);
-			expected = @"
-  File   Edit   Format
-";
-
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 22, 1), pos);
+			TestHelpers.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (1), output);
 
 			Assert.True (menu.openMenu.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ())));
 			Assert.True (menu.IsMenuOpen);
 			Assert.False (tf.HasFocus);
 			Application.Top.Redraw (Application.Top.Bounds);
-			expected = @"
-  File   Edit   Format
-┌──────┐              
-│ New  │              
-└──────┘              
-";
-
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 22, 4), pos);
+			TestHelpers.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (0), output);
 
 			Assert.True (menu.ProcessHotKey (new KeyEvent (Key.F9, new KeyModifiers ())));
 			Assert.False (menu.IsMenuOpen);
 			Assert.True (tf.HasFocus);
 			Application.Top.Redraw (Application.Top.Bounds);
-			expected = @"
-  File   Edit   Format
-";
+			TestHelpers.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output);
+		}
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (2, 0, 22, 1), pos);
+		[Fact, AutoInitShutdown]
+		public void Key_Open_And_Close_The_MenuBar ()
+		{
+			var menu = new MenuBar (new MenuBarItem [] {
+				new MenuBarItem ("File", new MenuItem [] {
+					new MenuItem ("New", "", null)
+				})
+			});
+			Application.Top.Add (menu);
+			Application.Begin (Application.Top);
+
+			Assert.True (menu.ProcessHotKey (new KeyEvent (Key.F9, new KeyModifiers ())));
+			Assert.True (menu.IsMenuOpen);
+			Assert.True (menu.ProcessHotKey (new KeyEvent (Key.F9, new KeyModifiers ())));
+			Assert.False (menu.IsMenuOpen);
+
+			menu.Key = Key.F10 | Key.ShiftMask;
+			Assert.False (menu.ProcessHotKey (new KeyEvent (Key.F9, new KeyModifiers ())));
+			Assert.False (menu.IsMenuOpen);
+
+			Assert.True (menu.ProcessHotKey (new KeyEvent (Key.F10 | Key.ShiftMask, new KeyModifiers ())));
+			Assert.True (menu.IsMenuOpen);
+			Assert.True (menu.ProcessHotKey (new KeyEvent (Key.F10 | Key.ShiftMask, new KeyModifiers ())));
+			Assert.False (menu.IsMenuOpen);
 		}
 	}
 }

+ 6 - 6
UnitTests/MessageBoxTests.cs

@@ -29,7 +29,7 @@ namespace Terminal.Gui.Views {
 
 				} else if (iterations == 1) {
 					Application.Top.Redraw (Application.Top.Bounds);
-					GraphViewTests.AssertDriverContentsWithFrameAre (@"
+					TestHelpers.AssertDriverContentsWithFrameAre (@"
                ┌ Title ─────────────────────────────────────────┐
                │                    Message                     │
                │                                                │
@@ -71,7 +71,7 @@ namespace Terminal.Gui.Views {
 					Application.RequestStop ();
 				} else if (iterations == 1) {
 					Application.Top.Redraw (Application.Top.Bounds);
-					GraphViewTests.AssertDriverContentsWithFrameAre (@"
+					TestHelpers.AssertDriverContentsWithFrameAre (@"
          ┌ About UI Catalog ──────────────────────────────────────────┐
          │             A comprehensive sample library for             │
          │                                                            │
@@ -110,7 +110,7 @@ namespace Terminal.Gui.Views {
 					Application.RequestStop ();
 				} else if (iterations == 1) {
 					Application.Top.Redraw (Application.Top.Bounds);
-					GraphViewTests.AssertDriverContentsWithFrameAre (@"
+					TestHelpers.AssertDriverContentsWithFrameAre (@"
                                     ┌─────┐
                                     │Messa│
                                     │ ge  │
@@ -140,7 +140,7 @@ namespace Terminal.Gui.Views {
 					Application.RequestStop ();
 				} else if (iterations == 1) {
 					Application.Top.Redraw (Application.Top.Bounds);
-					GraphViewTests.AssertDriverContentsWithFrameAre (@"
+					TestHelpers.AssertDriverContentsWithFrameAre (@"
                                   ┌ Title ──┐
                                   │ Message │
                                   │         │
@@ -170,7 +170,7 @@ namespace Terminal.Gui.Views {
 					Application.RequestStop ();
 				} else if (iterations == 1) {
 					Application.Top.Redraw (Application.Top.Bounds);
-					GraphViewTests.AssertDriverContentsWithFrameAre (@"
+					TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌ mywindow ────────────────────────────────────────────────────────────────────┐
 │ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff│
 │ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff│
@@ -224,7 +224,7 @@ namespace Terminal.Gui.Views {
 					Application.RequestStop ();
 				} else if (iterations == 1) {
 					Application.Top.Redraw (Application.Top.Bounds);
-					GraphViewTests.AssertDriverContentsWithFrameAre (@"
+					TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌ mywindow ────────────────────────────────────────────────────────────────────┐
 │ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff │
 │ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff │

+ 2 - 2
UnitTests/PanelViewTests.cs

@@ -405,7 +405,7 @@ namespace Terminal.Gui.Views {
 └──────────────────────────────────────────────────────────────────────────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 80, 25), pos);
 		}
 
@@ -472,7 +472,7 @@ namespace Terminal.Gui.Views {
 └──────────────────────────────────────────────────────────────────────────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 80, 25), pos);
 		}
 	}

+ 6 - 6
UnitTests/PosTests.cs

@@ -164,7 +164,7 @@ namespace Terminal.Gui.Core {
 └──────────────────────────────────────┘
 ";
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 		}
 
 		[Fact]
@@ -211,7 +211,7 @@ namespace Terminal.Gui.Core {
 └──────────────────────────────────────┘
 ";
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 		}
 
 		[Fact]
@@ -247,7 +247,7 @@ namespace Terminal.Gui.Core {
 				win.Frame.Right, win.Frame.Bottom));
 			Assert.Equal (new Rect (0, 20, 78, 1), label.Frame);
 			var expected = @"
-  Menu                                                                          
+ Menu                                                                           
 ┌──────────────────────────────────────────────────────────────────────────────┐
 │                                                                              │
 │                                                                              │
@@ -274,7 +274,7 @@ namespace Terminal.Gui.Core {
  F1 Help                                                                        
 ";
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 		}
 
 		[Fact]
@@ -310,7 +310,7 @@ namespace Terminal.Gui.Core {
 				win.Frame.Right, win.Frame.Bottom));
 			Assert.Equal (new Rect (0, 20, 78, 1), label.Frame);
 			var expected = @"
-  Menu                                                                          
+ Menu                                                                           
 ┌──────────────────────────────────────────────────────────────────────────────┐
 │                                                                              │
 │                                                                              │
@@ -337,7 +337,7 @@ namespace Terminal.Gui.Core {
  F1 Help                                                                        
 ";
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 		}
 
 		[Fact]

+ 3 - 3
UnitTests/RadioGroupTests.cs

@@ -99,7 +99,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 30, 5), pos);
 
 			rg.DisplayMode = DisplayModeLayout.Horizontal;
@@ -120,7 +120,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 30, 5), pos);
 
 			rg.HorizontalSpace = 4;
@@ -140,7 +140,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 30, 5), pos);
 		}
 

+ 12 - 14
UnitTests/ScenarioTests.cs

@@ -11,7 +11,7 @@ using Xunit.Abstractions;
 // Alias Console to MockConsole so we don't accidentally use Console
 using Console = Terminal.Gui.FakeConsole;
 
-namespace Terminal.Gui {
+namespace UICatalog {
 	public class ScenarioTests {
 		readonly ITestOutputHelper output;
 
@@ -49,19 +49,18 @@ namespace Terminal.Gui {
 		[Fact]
 		public void Run_All_Scenarios ()
 		{
-			List<Type> scenarioClasses = Scenario.GetDerivedClasses<Scenario> ();
-			Assert.NotEmpty (scenarioClasses);
+			List<Scenario> scenarios = Scenario.GetScenarios ();
+			Assert.NotEmpty (scenarios);
 
-			foreach (var scenarioClass in scenarioClasses) {
+			foreach (var scenario in scenarios) {
 
-				output.WriteLine ($"Running Scenario '{scenarioClass.Name}'");
+				output.WriteLine ($"Running Scenario '{scenario}'");
 
 				Func<MainLoop, bool> closeCallback = (MainLoop loop) => {
 					Application.RequestStop ();
 					return false;
 				};
 
-				var scenario = (Scenario)Activator.CreateInstance (scenarioClass);
 				Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
 
 				// Close after a short period of time
@@ -83,11 +82,11 @@ namespace Terminal.Gui {
 		[Fact]
 		public void Run_Generic ()
 		{
-			List<Type> scenarioClasses = Scenario.GetDerivedClasses<Scenario> ();
-			Assert.NotEmpty (scenarioClasses);
+			List<Scenario> scenarios = Scenario.GetScenarios ();
+			Assert.NotEmpty (scenarios);
 
-			var item = scenarioClasses.FindIndex (t => Scenario.ScenarioMetadata.GetName (t).Equals ("Generic", StringComparison.OrdinalIgnoreCase));
-			var scenarioClass = scenarioClasses [item];
+			var item = scenarios.FindIndex (s => s.GetName ().Equals ("Generic", StringComparison.OrdinalIgnoreCase));
+			var generic = scenarios [item];
 			// Setup some fake keypresses 
 			// Passing empty string will cause just a ctrl-q to be fired
 			int stackSize = CreateInput ("");
@@ -116,13 +115,12 @@ namespace Terminal.Gui {
 				Assert.Equal (Key.CtrlMask | Key.Q, args.KeyEvent.Key);
 			};
 
-			var scenario = (Scenario)Activator.CreateInstance (scenarioClass);
-			scenario.Init (Application.Top, Colors.Base);
-			scenario.Setup ();
+			generic.Init (Application.Top, 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.
 			//var rs = Application.Begin (Application.Top);
-			scenario.Run ();
+			generic.Run ();
 
 			//Application.End (rs);
 

+ 10 - 10
UnitTests/ScrollBarViewTests.cs

@@ -737,7 +737,7 @@ namespace Terminal.Gui.Views {
 └───────────────────────────────────────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 45, 20), pos);
 
 			textView.WordWrap = true;
@@ -774,7 +774,7 @@ namespace Terminal.Gui.Views {
 └────────────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 26, 20), pos);
 
 			((FakeDriver)Application.Driver).SetBufferSize (10, 10);
@@ -800,7 +800,7 @@ namespace Terminal.Gui.Views {
 └────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 10, 10), pos);
 		}
 
@@ -824,7 +824,7 @@ namespace Terminal.Gui.Views {
 			Assert.True (sbv.OtherScrollBarView.ShowScrollIndicator);
 			Assert.True (sbv.Visible);
 			Assert.True (sbv.OtherScrollBarView.Visible);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This is a tes▲
 This is a tes┬
 This is a tes┴
@@ -842,7 +842,7 @@ This is a tes▼
 			Assert.False (sbv.Visible);
 			Assert.False (sbv.OtherScrollBarView.Visible);
 			Application.Top.Redraw (Application.Top.Bounds);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This is a test
 This is a test
 This is a test
@@ -860,7 +860,7 @@ This is a test
 			Assert.True (sbv.Visible);
 			Assert.True (sbv.OtherScrollBarView.Visible);
 			Application.Top.Redraw (Application.Top.Bounds);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This is a tes▲
 This is a tes┬
 This is a tes┴
@@ -887,7 +887,7 @@ This is a tes▼
 			Assert.Null (sbv.OtherScrollBarView);
 			Assert.True (sbv.ShowScrollIndicator);
 			Assert.True (sbv.Visible);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This is a tes▲
 This is a tes┬
 This is a tes┴
@@ -901,7 +901,7 @@ This is a tes▼
 			Assert.False (sbv.ShowScrollIndicator);
 			Assert.False (sbv.Visible);
 			Application.Top.Redraw (Application.Top.Bounds);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This is a test
 This is a test
 This is a test
@@ -930,7 +930,7 @@ This is a test
 			Assert.Null (sbv.OtherScrollBarView);
 			Assert.False (sbv.ShowScrollIndicator);
 			Assert.False (sbv.Visible);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This is a test[ Click Me! ]
 This is a test             
 This is a test             
@@ -957,7 +957,7 @@ This is a test
 			Assert.False (sbv.ShowScrollIndicator);
 			Assert.True (sbv.Visible);
 			Application.Top.Redraw (Application.Top.Bounds);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This is a test[ Click Me! ]
 This is a test             
 This is a test             

+ 108 - 1
UnitTests/ScrollViewTests.cs

@@ -4,9 +4,17 @@ using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using Xunit;
+using Xunit.Abstractions;
 
 namespace Terminal.Gui.Views {
 	public class ScrollViewTests {
+		readonly ITestOutputHelper output;
+
+		public ScrollViewTests (ITestOutputHelper output)
+		{
+			this.output = output;
+		}
+
 		[Fact]
 		public void Constructors_Defaults ()
 		{
@@ -173,5 +181,104 @@ namespace Terminal.Gui.Views {
 			Assert.False (sv.ProcessKey (new KeyEvent (Key.End | Key.CtrlMask, new KeyModifiers ())));
 			Assert.Equal (new Point (-39, -19), sv.ContentOffset);
 		}
+
+		[Fact, AutoInitShutdown]
+		public void AutoHideScrollBars_ShowHorizontalScrollIndicator_ShowVerticalScrollIndicator ()
+		{
+			var sv = new ScrollView {
+				Width = 10,
+				Height = 10
+			};
+
+			Application.Top.Add (sv);
+			Application.Begin (Application.Top);
+
+			Assert.True (sv.AutoHideScrollBars);
+			Assert.False (sv.ShowHorizontalScrollIndicator);
+			Assert.False (sv.ShowVerticalScrollIndicator);
+			TestHelpers.AssertDriverContentsWithFrameAre ("", output);
+
+			sv.AutoHideScrollBars = false;
+			sv.ShowHorizontalScrollIndicator = true;
+			sv.ShowVerticalScrollIndicator = true;
+			sv.Redraw (sv.Bounds);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+         ▲
+         ┬
+         │
+         │
+         │
+         │
+         │
+         ┴
+         ▼
+◄├─────┤► 
+", output);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void ContentSize_AutoHideScrollBars_ShowHorizontalScrollIndicator_ShowVerticalScrollIndicator ()
+		{
+			var sv = new ScrollView {
+				Width = 10,
+				Height = 10,
+				ContentSize = new Size (50, 50)
+			};
+
+			Application.Top.Add (sv);
+			Application.Begin (Application.Top);
+
+			Assert.Equal (50, sv.ContentSize.Width);
+			Assert.Equal (50, sv.ContentSize.Height);
+			Assert.True (sv.AutoHideScrollBars);
+			Assert.True (sv.ShowHorizontalScrollIndicator);
+			Assert.True (sv.ShowVerticalScrollIndicator);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+         ▲
+         ┬
+         ┴
+         ░
+         ░
+         ░
+         ░
+         ░
+         ▼
+◄├┤░░░░░► 
+", output);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void ContentOffset_ContentSize_AutoHideScrollBars_ShowHorizontalScrollIndicator_ShowVerticalScrollIndicator ()
+		{
+			var sv = new ScrollView {
+				Width = 10,
+				Height = 10,
+				ContentSize = new Size (50, 50),
+				ContentOffset = new Point (25, 25)
+			};
+
+			Application.Top.Add (sv);
+			Application.Begin (Application.Top);
+
+			Assert.Equal (-25, sv.ContentOffset.X);
+			Assert.Equal (-25, sv.ContentOffset.Y);
+			Assert.Equal (50, sv.ContentSize.Width);
+			Assert.Equal (50, sv.ContentSize.Height);
+			Assert.True (sv.AutoHideScrollBars);
+			Assert.True (sv.ShowHorizontalScrollIndicator);
+			Assert.True (sv.ShowVerticalScrollIndicator);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+         ▲
+         ░
+         ░
+         ░
+         ┬
+         │
+         ┴
+         ░
+         ▼
+◄░░░├─┤░► 
+", output);
+		}
 	}
-}
+}

+ 2 - 2
UnitTests/StatusBarTests.cs

@@ -113,7 +113,7 @@ namespace Terminal.Gui.Views {
 ^O Open {Application.Driver.VLine} ^Q Quit
 ";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 			sb = new StatusBar (new StatusItem [] {
 				new StatusItem (Key.CtrlMask | Key.Q, "~CTRL-O~ Open", null),
@@ -125,7 +125,7 @@ namespace Terminal.Gui.Views {
 CTRL-O Open {Application.Driver.VLine} CTRL-Q Quit
 ";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 		}
 
 		[Fact]

+ 31 - 31
UnitTests/TabViewTests.cs

@@ -257,7 +257,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌──┐      
 │12│13    
 │  └─────┐
@@ -268,7 +268,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
    ┌──┐   
  12│13│   
 ┌──┘  └──┐
@@ -282,7 +282,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌───────┐ 
 │1234567│ 
 │       └►
@@ -293,7 +293,7 @@ namespace Terminal.Gui.Views {
 			tv.SelectedTab = tab2;
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌──┐      
 │13│      
 ◄  └─────┐
@@ -307,7 +307,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌───────┐ 
 │abcdefg│ 
 ◄       └┐
@@ -333,7 +333,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 │12│13    
 │  └─────┐
 │hi      │
@@ -345,7 +345,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
  12│13│   
 ┌──┘  └──┐
 │hi2     │
@@ -360,7 +360,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 │1234567│ 
 │       └►
 │hi      │
@@ -371,7 +371,7 @@ namespace Terminal.Gui.Views {
 			tv.SelectedTab = tab2;
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 │13│      
 ◄  └─────┐
 │hi2     │
@@ -385,7 +385,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 │abcdefg│ 
 ◄       └┐
 │hi2     │
@@ -403,7 +403,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌─┐ 
 │T│ 
 │ └►
@@ -423,7 +423,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 │T│ 
 │ └►
 │hi│
@@ -441,7 +441,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌┐ 
 ││ 
 │└►
@@ -461,7 +461,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ││ 
 │└►
 │h│
@@ -487,7 +487,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌────────┐
 │hi      │
 │  ┌─────┘
@@ -501,7 +501,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌────────┐
 │hi      │
 │       ┌►
@@ -512,7 +512,7 @@ namespace Terminal.Gui.Views {
 			tv.SelectedTab = tab2;
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌────────┐
 │hi2     │
 ◄  ┌─────┘
@@ -526,7 +526,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌────────┐
 │hi2     │
 ◄       ┌┘
@@ -552,7 +552,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌────────┐
 │hi      │
 │        │
@@ -564,7 +564,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌────────┐
 │hi2     │
 │        │
@@ -579,7 +579,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌────────┐
 │hi      │
 │        │
@@ -590,7 +590,7 @@ namespace Terminal.Gui.Views {
 			tv.SelectedTab = tab2;
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌────────┐
 │hi2     │
 │        │
@@ -604,7 +604,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌────────┐
 │hi2     │
 │        │
@@ -624,7 +624,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌──┐
 │hi│
 │ ┌►
@@ -644,7 +644,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌──┐
 │hi│
 │  │
@@ -664,7 +664,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌─┐
 │h│
 │┌►
@@ -684,7 +684,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌─┐
 │h│
 │ │
@@ -706,7 +706,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌────┐              
 │Tab0│              
 │    └─────────────►
@@ -717,7 +717,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌──────────────┐    
 │Les Misérables│    
 ◄              └───┐
@@ -741,7 +741,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌──────────────────┐
 │hi                │
 │    ┌─────────────►
@@ -752,7 +752,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌──────────────────┐
 │hi2               │
 ◄              ┌───┘

+ 28 - 28
UnitTests/TableViewTests.cs

@@ -459,7 +459,7 @@ namespace Terminal.Gui.Views {
 ├─┼──────┤
 │1│2     │
 ";
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 			// Shutdown must be called to safely clean up Application if Init has been called
 			Application.Shutdown ();
@@ -482,7 +482,7 @@ namespace Terminal.Gui.Views {
 ├─┼─┼────┤
 │1│2│    │
 ";
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 			// Shutdown must be called to safely clean up Application if Init has been called
 			Application.Shutdown ();
@@ -506,7 +506,7 @@ namespace Terminal.Gui.Views {
 ├─┼─┤
 │1│2│
 ";
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 			// Shutdown must be called to safely clean up Application if Init has been called
 			Application.Shutdown ();
@@ -670,7 +670,7 @@ namespace Terminal.Gui.Views {
 ├─┼─┤
 │1│2│
 ";
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 
 			string expectedColors = @"
@@ -680,7 +680,7 @@ namespace Terminal.Gui.Views {
 01000
 ";
 			
-			GraphViewTests.AssertDriverColorsAre (expectedColors, new Attribute [] {
+			TestHelpers.AssertDriverColorsAre (expectedColors, new Attribute [] {
 				// 0
 				tv.ColorScheme.Normal,				
 				// 1
@@ -714,7 +714,7 @@ namespace Terminal.Gui.Views {
 ├─┼─┤
 │1│2│
 ";
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 
 			string expectedColors = @"
@@ -727,7 +727,7 @@ namespace Terminal.Gui.Views {
 			var invertHotFocus = new Attribute(tv.ColorScheme.HotFocus.Background,tv.ColorScheme.HotFocus.Foreground);
 			var invertHotNormal = new Attribute(tv.ColorScheme.HotNormal.Background,tv.ColorScheme.HotNormal.Foreground);
 
-			GraphViewTests.AssertDriverColorsAre (expectedColors, new Attribute [] {
+			TestHelpers.AssertDriverColorsAre (expectedColors, new Attribute [] {
 				// 0
 				tv.ColorScheme.Normal,				
 				// 1
@@ -771,7 +771,7 @@ namespace Terminal.Gui.Views {
 ├─┼─┤
 │1│2│
 ";
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 
 			string expectedColors = @"
@@ -781,7 +781,7 @@ namespace Terminal.Gui.Views {
 21222
 ";
 			
-			GraphViewTests.AssertDriverColorsAre (expectedColors, new Attribute [] {
+			TestHelpers.AssertDriverColorsAre (expectedColors, new Attribute [] {
 				// 0
 				tv.ColorScheme.Normal,				
 				// 1
@@ -803,7 +803,7 @@ namespace Terminal.Gui.Views {
 ├─┼─┤
 │1│5│
 ";
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 
 			expectedColors = @"
@@ -816,7 +816,7 @@ namespace Terminal.Gui.Views {
 			// now we only see 2 colors used (the selected cell color and Normal
 			// rowHighlight should no longer be used because the delegate returned null
 			// (now that the cell value is 5 - which does not match the conditional)
-			GraphViewTests.AssertDriverColorsAre (expectedColors, new Attribute [] {
+			TestHelpers.AssertDriverColorsAre (expectedColors, new Attribute [] {
 				// 0
 				tv.ColorScheme.Normal,
 				// 1
@@ -864,7 +864,7 @@ namespace Terminal.Gui.Views {
 ├─┼─┤
 │1│2│
 ";
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 
 			string expectedColors = @"
@@ -874,7 +874,7 @@ namespace Terminal.Gui.Views {
 01020
 ";
 			
-			GraphViewTests.AssertDriverColorsAre (expectedColors, new Attribute [] {
+			TestHelpers.AssertDriverColorsAre (expectedColors, new Attribute [] {
 				// 0
 				tv.ColorScheme.Normal,				
 				// 1
@@ -896,7 +896,7 @@ namespace Terminal.Gui.Views {
 ├─┼─┤
 │1│5│
 ";
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 
 			expectedColors = @"
@@ -909,7 +909,7 @@ namespace Terminal.Gui.Views {
 			// now we only see 2 colors used (the selected cell color and Normal
 			// cellHighlight should no longer be used because the delegate returned null
 			// (now that the cell value is 5 - which does not match the conditional)
-			GraphViewTests.AssertDriverColorsAre (expectedColors, new Attribute [] {
+			TestHelpers.AssertDriverColorsAre (expectedColors, new Attribute [] {
 				// 0
 				tv.ColorScheme.Normal,				
 				// 1
@@ -1005,7 +1005,7 @@ namespace Terminal.Gui.Views {
 │A│B│C│
 │1│2│3│";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 
 			// Scroll right
@@ -1026,7 +1026,7 @@ namespace Terminal.Gui.Views {
 │B│C│D│
 │2│3│4│";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 
 			// Shutdown must be called to safely clean up Application if Init has been called
@@ -1070,7 +1070,7 @@ namespace Terminal.Gui.Views {
 │A│B│C│
 │1│2│3│";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 
 			// Scroll right
@@ -1091,7 +1091,7 @@ namespace Terminal.Gui.Views {
 │D│E│F│
 │4│5│6│";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 
 			// Shutdown must be called to safely clean up Application if Init has been called
@@ -1135,7 +1135,7 @@ namespace Terminal.Gui.Views {
 │1│2                    │
 ";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 			// get a style for the long column
 			var style = tableView.Style.GetOrCreateColumnStyle(dt.Columns[2]);
@@ -1152,7 +1152,7 @@ namespace Terminal.Gui.Views {
 │1│2│aaaaaaaaaa         │
 │1│2│aaa                │
 ";
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 			// revert the style change
 			style.MaxWidth = TableView.DefaultMaxCellWidth;
@@ -1172,7 +1172,7 @@ namespace Terminal.Gui.Views {
 │1│2│aaaaaaaaaaaaa...   │
 │1│2│aaa                │
 ";
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 			// revert style change
 			style.RepresentationGetter = null;
@@ -1197,7 +1197,7 @@ namespace Terminal.Gui.Views {
 │1│2│aaaaaaaaaaaaaaaaaaa│
 │1│2│aaa                │
 ";
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 			// Now test making the width too small for the MinAcceptableWidth
 			// the Column won't fit so should not be rendered
@@ -1214,7 +1214,7 @@ namespace Terminal.Gui.Views {
 │1│2    │
 
 ";
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 			// setting width to 10 leaves just enough space for the column to
 			// meet MinAcceptableWidth of 5.  Column width includes terminator line
@@ -1228,7 +1228,7 @@ namespace Terminal.Gui.Views {
 │1│2│aaaa│
 │1│2│aaa │
 ";
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 			Application.Shutdown ();
 		}
@@ -1274,7 +1274,7 @@ namespace Terminal.Gui.Views {
 ├─┼─┼─►
 │1│2│3│";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 
 			// Scroll right
@@ -1291,7 +1291,7 @@ namespace Terminal.Gui.Views {
 ◄─┼─┼─►
 │2│3│4│";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 
 			// Scroll right twice more (to end of columns)
@@ -1306,7 +1306,7 @@ namespace Terminal.Gui.Views {
 ◄─┼─┼─┤
 │4│5│6│";
 
-			GraphViewTests.AssertDriverContentsAre (expected, output);
+			TestHelpers.AssertDriverContentsAre (expected, output);
 
 			// Shutdown must be called to safely clean up Application if Init has been called
 			Application.Shutdown ();

+ 230 - 0
UnitTests/TestHelpers.cs

@@ -0,0 +1,230 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit.Abstractions;
+using Xunit;
+using Terminal.Gui;
+using Rune = System.Rune;
+using Attribute = Terminal.Gui.Attribute;
+using System.Text.RegularExpressions;
+using System.Reflection;
+
+
+// This class enables test functions annotated with the [AutoInitShutdown] attribute to 
+// automatically call Application.Init before called and Application.Shutdown after
+// 
+// This is necessary because a) Application is a singleton and Init/Shutdown must be called
+// as a pair, and b) all unit test functions should be atomic.
+[AttributeUsage (AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+public class AutoInitShutdownAttribute : Xunit.Sdk.BeforeAfterTestAttribute {
+
+	static bool _init = false;
+	public override void Before (MethodInfo methodUnderTest)
+	{
+		if (_init) {
+			throw new InvalidOperationException ("After did not run.");
+		}
+
+		Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+		_init = true;
+	}
+
+	public override void After (MethodInfo methodUnderTest)
+	{
+		Application.Shutdown ();
+		_init = false;
+	}
+}
+
+class TestHelpers {
+
+
+#pragma warning disable xUnit1013 // Public method should be marked as test
+	public static void AssertDriverContentsAre (string expectedLook, ITestOutputHelper output)
+	{
+#pragma warning restore xUnit1013 // Public method should be marked as test
+
+		var sb = new StringBuilder ();
+		var driver = ((FakeDriver)Application.Driver);
+
+		var contents = driver.Contents;
+
+		for (int r = 0; r < driver.Rows; r++) {
+			for (int c = 0; c < driver.Cols; c++) {
+				sb.Append ((char)contents [r, c, 0]);
+			}
+			sb.AppendLine ();
+		}
+
+		var actualLook = sb.ToString ();
+
+		if (!string.Equals (expectedLook, actualLook)) {
+
+			// ignore trailing whitespace on each line
+			var trailingWhitespace = new Regex (@"\s+$", RegexOptions.Multiline);
+
+			// get rid of trailing whitespace on each line (and leading/trailing whitespace of start/end of full string)
+			expectedLook = trailingWhitespace.Replace (expectedLook, "").Trim ();
+			actualLook = trailingWhitespace.Replace (actualLook, "").Trim ();
+
+			// standardize line endings for the comparison
+			expectedLook = expectedLook.Replace ("\r\n", "\n");
+			actualLook = actualLook.Replace ("\r\n", "\n");
+
+			output?.WriteLine ("Expected:" + Environment.NewLine + expectedLook);
+			output?.WriteLine ("But Was:" + Environment.NewLine + actualLook);
+
+			Assert.Equal (expectedLook, actualLook);
+		}
+	}
+
+	public static Rect AssertDriverContentsWithFrameAre (string expectedLook, ITestOutputHelper output)
+	{
+		var lines = new List<List<char>> ();
+		var sb = new StringBuilder ();
+		var driver = ((FakeDriver)Application.Driver);
+		var x = -1;
+		var y = -1;
+		int w = -1;
+		int h = -1;
+
+		var contents = driver.Contents;
+
+		for (int r = 0; r < driver.Rows; r++) {
+			var runes = new List<char> ();
+			for (int c = 0; c < driver.Cols; c++) {
+				var rune = (char)contents [r, c, 0];
+				if (rune != ' ') {
+					if (x == -1) {
+						x = c;
+						y = r;
+						for (int i = 0; i < c; i++) {
+							runes.InsertRange (i, new List<char> () { ' ' });
+						}
+					}
+					if (Rune.ColumnWidth (rune) > 1) {
+						c++;
+					}
+					if (c + 1 > w) {
+						w = c + 1;
+					}
+					h = r - y + 1;
+				}
+				if (x > -1) {
+					runes.Add (rune);
+				}
+			}
+			if (runes.Count > 0) {
+				lines.Add (runes);
+			}
+		}
+
+		// Remove unnecessary empty lines
+		if (lines.Count > 0) {
+			for (int r = lines.Count - 1; r > h - 1; r--) {
+				lines.RemoveAt (r);
+			}
+		}
+
+		// Remove trailing whitespace on each line
+		for (int r = 0; r < lines.Count; r++) {
+			List<char> row = lines [r];
+			for (int c = row.Count - 1; c >= 0; c--) {
+				var rune = row [c];
+				if (rune != ' ' || (row.Sum (x => Rune.ColumnWidth (x)) == w)) {
+					break;
+				}
+				row.RemoveAt (c);
+			}
+		}
+
+		// Convert char list to string
+		for (int r = 0; r < lines.Count; r++) {
+			var line = new string (lines [r].ToArray ());
+			if (r == lines.Count - 1) {
+				sb.Append (line);
+			} else {
+				sb.AppendLine (line);
+			}
+		}
+
+		var actualLook = sb.ToString ();
+
+		if (!string.Equals (expectedLook, actualLook)) {
+
+			// standardize line endings for the comparison
+			expectedLook = expectedLook.Replace ("\r\n", "\n");
+			actualLook = actualLook.Replace ("\r\n", "\n");
+
+			// Remove the first and the last line ending from the expectedLook
+			if (expectedLook.StartsWith ("\n")) {
+				expectedLook = expectedLook [1..];
+			}
+			if (expectedLook.EndsWith ("\n")) {
+				expectedLook = expectedLook [..^1];
+			}
+
+			output?.WriteLine ("Expected:" + Environment.NewLine + expectedLook);
+			output?.WriteLine ("But Was:" + Environment.NewLine + actualLook);
+
+			Assert.Equal (expectedLook, actualLook);
+		}
+		return new Rect (x > -1 ? x : 0, y > -1 ? y : 0, w > -1 ? w : 0, h > -1 ? h : 0);
+	}
+
+#pragma warning disable xUnit1013 // Public method should be marked as test
+	/// <summary>
+	/// Verifies the console was rendered using the given <paramref name="expectedColors"/> at the given locations.
+	/// Pass a bitmap of indexes into <paramref name="expectedColors"/> as <paramref name="expectedLook"/> and the
+	/// test method will verify those colors were used in the row/col of the console during rendering
+	/// </summary>
+	/// <param name="expectedLook">Numbers between 0 and 9 for each row/col of the console.  Must be valid indexes of <paramref name="expectedColors"/></param>
+	/// <param name="expectedColors"></param>
+	public static void AssertDriverColorsAre (string expectedLook, Attribute [] expectedColors)
+	{
+#pragma warning restore xUnit1013 // Public method should be marked as test
+
+		if (expectedColors.Length > 10) {
+			throw new ArgumentException ("This method only works for UIs that use at most 10 colors");
+		}
+
+		expectedLook = expectedLook.Trim ();
+		var driver = ((FakeDriver)Application.Driver);
+
+		var contents = driver.Contents;
+
+		int r = 0;
+		foreach (var line in expectedLook.Split ('\n').Select (l => l.Trim ())) {
+
+			for (int c = 0; c < line.Length; c++) {
+
+				int val = contents [r, c, 1];
+
+				var match = expectedColors.Where (e => e.Value == val).ToList ();
+				if (match.Count == 0) {
+					throw new Exception ($"Unexpected color {DescribeColor (val)} was used at row {r} and col {c} (indexes start at 0).  Color value was {val} (expected colors were {string.Join (",", expectedColors.Select (c => c.Value))})");
+				} else if (match.Count > 1) {
+					throw new ArgumentException ($"Bad value for expectedColors, {match.Count} Attributes had the same Value");
+				}
+
+				var colorUsed = Array.IndexOf (expectedColors, match [0]).ToString () [0];
+				var userExpected = line [c];
+
+				if (colorUsed != userExpected) {
+					throw new Exception ($"Colors used did not match expected at row {r} and col {c} (indexes start at 0).  Color index used was {DescribeColor (colorUsed)} but test expected {DescribeColor (userExpected)} (these are indexes into the expectedColors array)");
+				}
+			}
+
+			r++;
+		}
+	}
+
+	private static object DescribeColor (int userExpected)
+	{
+		var a = new Attribute (userExpected);
+		return $"{a.Foreground},{a.Background}";
+	}
+}
+

+ 35 - 35
UnitTests/TextFormatterTests.cs

@@ -2190,7 +2190,7 @@ namespace Terminal.Gui.Core {
 └───┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, width + 2, height + 2), pos);
 		}
 
@@ -2229,7 +2229,7 @@ namespace Terminal.Gui.Core {
 └────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, width + 2, height + 2), pos);
 		}
 
@@ -2270,7 +2270,7 @@ namespace Terminal.Gui.Core {
 └──────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, width + 2, height + 2), pos);
 		}
 
@@ -2310,7 +2310,7 @@ namespace Terminal.Gui.Core {
 └────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, width + 2, height + 2), pos);
 		}
 
@@ -3059,7 +3059,7 @@ namespace Terminal.Gui.Core {
 Demo Simple Rune
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 16, 1), pos);
 		}
 
@@ -3096,7 +3096,7 @@ n
 e
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 1, 16), pos);
 		}
 
@@ -3114,7 +3114,7 @@ e
 デモエムポンズ
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 14, 1), pos);
 		}
 
@@ -3140,7 +3140,7 @@ e
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 2, 7), pos);
 		}
 
@@ -3169,7 +3169,7 @@ e
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 2, 7), pos);
 		}
 
@@ -3208,7 +3208,7 @@ e
 └────────────────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, width + 2, 6), pos);
 		}
 
@@ -3263,7 +3263,7 @@ e
 └───────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 9, height + 2), pos);
 		}
 
@@ -3302,7 +3302,7 @@ e
 └─────────────────────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, width + 2, 6), pos);
 		}
 
@@ -3361,7 +3361,7 @@ e
 └───────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 13, height + 2), pos);
 		}
 
@@ -3385,7 +3385,7 @@ e
 
 			tf2.Draw (new Rect (new Point (0, 2), tf2Size), view.GetNormalColor (), view.ColorScheme.HotNormal);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This view needs to be cleared before rewritten.                        
 This TextFormatter (tf1) without fill will not be cleared on rewritten.
 This TextFormatter (tf2) with fill will be cleared on rewritten.       
@@ -3400,7 +3400,7 @@ This TextFormatter (tf2) with fill will be cleared on rewritten.
 			tf2.Text = "This TextFormatter (tf2) is rewritten.";
 			tf2.Draw (new Rect (new Point (0, 2), tf2Size), view.GetNormalColor (), view.ColorScheme.HotNormal);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This view is rewritten.                                                
 This TextFormatter (tf1) is rewritten.will not be cleared on rewritten.
 This TextFormatter (tf2) is rewritten.                                 
@@ -3502,7 +3502,7 @@ This TextFormatter (tf2) is rewritten.
 └────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 10, 4), pos);
 
 			text = "0123456789";
@@ -3520,7 +3520,7 @@ This TextFormatter (tf2) is rewritten.
 └────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 10, 4), pos);
 		}
 
@@ -3555,7 +3555,7 @@ This TextFormatter (tf2) is rewritten.
 └────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 10, 4), pos);
 
 			text = "0123456789";
@@ -3574,7 +3574,7 @@ This TextFormatter (tf2) is rewritten.
 └────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 10, 4), pos);
 		}
 
@@ -3610,7 +3610,7 @@ This TextFormatter (tf2) is rewritten.
 └────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 10, 4), pos);
 
 			text = "0123456789";
@@ -3629,7 +3629,7 @@ This TextFormatter (tf2) is rewritten.
 └────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 10, 4), pos);
 		}
 
@@ -3666,7 +3666,7 @@ This TextFormatter (tf2) is rewritten.
 └────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 10, 4), pos);
 
 			text = "0123456789";
@@ -3685,7 +3685,7 @@ This TextFormatter (tf2) is rewritten.
 └────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 10, 4), pos);
 		}
 
@@ -3720,7 +3720,7 @@ This TextFormatter (tf2) is rewritten.
 └────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 10, 4), pos);
 
 			text = "0123456789";
@@ -3739,7 +3739,7 @@ This TextFormatter (tf2) is rewritten.
 └────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 10, 4), pos);
 		}
 
@@ -3775,7 +3775,7 @@ This TextFormatter (tf2) is rewritten.
 └────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 10, 4), pos);
 
 			text = "0123456789";
@@ -3794,7 +3794,7 @@ This TextFormatter (tf2) is rewritten.
 └────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 10, 4), pos);
 		}
 
@@ -3837,7 +3837,7 @@ This TextFormatter (tf2) is rewritten.
 └──┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 4, 10), pos);
 
 			text = "0123456789";
@@ -3862,7 +3862,7 @@ This TextFormatter (tf2) is rewritten.
 └──┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 4, 10), pos);
 		}
 
@@ -3904,7 +3904,7 @@ This TextFormatter (tf2) is rewritten.
 └──┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 4, 10), pos);
 
 			text = "0123456789";
@@ -3929,7 +3929,7 @@ This TextFormatter (tf2) is rewritten.
 └──┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 4, 10), pos);
 		}
 
@@ -3972,7 +3972,7 @@ This TextFormatter (tf2) is rewritten.
 └──┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 4, 10), pos);
 
 			text = "0123456789";
@@ -3997,7 +3997,7 @@ This TextFormatter (tf2) is rewritten.
 └──┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 4, 10), pos);
 		}
 
@@ -4039,7 +4039,7 @@ This TextFormatter (tf2) is rewritten.
 └──┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 4, 10), pos);
 
 			text = "0123456789";
@@ -4064,7 +4064,7 @@ This TextFormatter (tf2) is rewritten.
 └──┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 4, 10), pos);
 		}
 

+ 35 - 35
UnitTests/TextViewTests.cs

@@ -1973,7 +1973,7 @@ namespace Terminal.Gui.Views {
 
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This is
 the    
 first  
@@ -2000,7 +2000,7 @@ line.
 
 			Assert.Equal (new Point (0, 0), tv.CursorPosition);
 			Assert.Equal (0, tv.LeftColumn);
-			GraphViewTests.AssertDriverContentsAre (@"
+			TestHelpers.AssertDriverContentsAre (@"
 aaaa
 ", output);
 
@@ -2008,35 +2008,35 @@ aaaa
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ())));
 			Application.Refresh ();
 			Assert.Equal (0, tv.LeftColumn);
-			GraphViewTests.AssertDriverContentsAre (@"
+			TestHelpers.AssertDriverContentsAre (@"
 aaa
 ", output);
 
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ())));
 			Application.Refresh ();
 			Assert.Equal (0, tv.LeftColumn);
-			GraphViewTests.AssertDriverContentsAre (@"
+			TestHelpers.AssertDriverContentsAre (@"
 aa
 ", output);
 
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ())));
 			Application.Refresh ();
 			Assert.Equal (0, tv.LeftColumn);
-			GraphViewTests.AssertDriverContentsAre (@"
+			TestHelpers.AssertDriverContentsAre (@"
 a
 ", output);
 
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ())));
 			Application.Refresh ();
 			Assert.Equal (0, tv.LeftColumn);
-			GraphViewTests.AssertDriverContentsAre (@"
+			TestHelpers.AssertDriverContentsAre (@"
 
 ", output);
 
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ())));
 			Application.Refresh ();
 			Assert.Equal (0, tv.LeftColumn);
-			GraphViewTests.AssertDriverContentsAre (@"
+			TestHelpers.AssertDriverContentsAre (@"
 
 ", output);
 		}
@@ -2055,7 +2055,7 @@ a
 			Application.Top.Add (tv);
 
 			tv.Redraw (tv.Bounds);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This is  
 the first
 line.    
@@ -2069,7 +2069,7 @@ line.
 			tv.CursorPosition = new Point (6, 2);
 			Assert.Equal (new Point (5, 2), tv.CursorPosition);
 			tv.Redraw (tv.Bounds);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This is  
 the first
 line.    
@@ -5953,7 +5953,7 @@ line.
 			Assert.False (tv.WordWrap);
 			Assert.Equal (Point.Empty, tv.CursorPosition);
 			Assert.Equal (Point.Empty, cp);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This is the first line. 
 This is the second line.
 ", output);
@@ -5963,7 +5963,7 @@ This is the second line.
 			tv.Redraw (tv.Bounds);
 			Assert.Equal (new Point (12, 0), tv.CursorPosition);
 			Assert.Equal (new Point (12, 0), cp);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This is the first line. 
 This is the second line.
 ", output);
@@ -5972,7 +5972,7 @@ This is the second line.
 			tv.Redraw (tv.Bounds);
 			Assert.Equal (new Point (4, 2), tv.CursorPosition);
 			Assert.Equal (new Point (12, 0), cp);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This 
 is   
 the  
@@ -5991,7 +5991,7 @@ line.
 			tv.Redraw (tv.Bounds);
 			Assert.Equal (new Point (0, 3), tv.CursorPosition);
 			Assert.Equal (new Point (12, 0), cp);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This 
 is   
 the  
@@ -6010,7 +6010,7 @@ line.
 			tv.Redraw (tv.Bounds);
 			Assert.Equal (new Point (1, 3), tv.CursorPosition);
 			Assert.Equal (new Point (13, 0), cp);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This 
 is   
 the  
@@ -6029,7 +6029,7 @@ line.
 			tv.Redraw (tv.Bounds);
 			Assert.Equal (new Point (0, 3), tv.CursorPosition);
 			Assert.Equal (new Point (12, 0), cp);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This 
 is   
 the  
@@ -6061,7 +6061,7 @@ line.
 
 			Assert.False (tv.WordWrap);
 			Assert.Equal (Point.Empty, tv.CursorPosition);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This is the first line. 
 This is the second line.
 ", output);
@@ -6071,7 +6071,7 @@ This is the second line.
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ())));
 			tv.Redraw (tv.Bounds);
 			Assert.Equal (new Point (2, 0), tv.CursorPosition);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 Ths is the first line.  
 This is the second line.
 ", output);
@@ -6081,14 +6081,14 @@ This is the second line.
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ())));
 			tv.Redraw (tv.Bounds);
 			Assert.Equal (new Point (22, 0), tv.CursorPosition);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 Ths is the first line.This is the second line.
 ", output);
 
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
 			tv.Redraw (tv.Bounds);
 			Assert.Equal (new Point (0, 1), tv.CursorPosition);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 Ths is the first line.  
 This is the second line.
 ", output);
@@ -6118,7 +6118,7 @@ This is the second line.
 
 			Assert.True (tv.WordWrap);
 			Assert.Equal (Point.Empty, tv.CursorPosition);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This is the first line. 
 This is the second line.
 ", output);
@@ -6128,7 +6128,7 @@ This is the second line.
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ())));
 			tv.Redraw (tv.Bounds);
 			Assert.Equal (new Point (2, 0), tv.CursorPosition);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 Ths is the first line.  
 This is the second line.
 ", output);
@@ -6138,14 +6138,14 @@ This is the second line.
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ())));
 			tv.Redraw (tv.Bounds);
 			Assert.Equal (new Point (22, 0), tv.CursorPosition);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 Ths is the first line.This is the second line.
 ", output);
 
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
 			tv.Redraw (tv.Bounds);
 			Assert.Equal (new Point (0, 1), tv.CursorPosition);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 Ths is the first line.  
 This is the second line.
 ", output);
@@ -6174,7 +6174,7 @@ This is the second line.
 
 			Assert.False (tv.WordWrap);
 			Assert.Equal (Point.Empty, tv.CursorPosition);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This is the first line. 
 This is the second line.
 ", output);
@@ -6184,7 +6184,7 @@ This is the second line.
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.DeleteChar, new KeyModifiers ())));
 			tv.Redraw (tv.Bounds);
 			Assert.Equal (new Point (2, 0), tv.CursorPosition);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 Ths is the first line.  
 This is the second line.
 ", output);
@@ -6194,14 +6194,14 @@ This is the second line.
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.DeleteChar, new KeyModifiers ())));
 			tv.Redraw (tv.Bounds);
 			Assert.Equal (new Point (22, 0), tv.CursorPosition);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 Ths is the first line.This is the second line.
 ", output);
 
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
 			tv.Redraw (tv.Bounds);
 			Assert.Equal (new Point (0, 1), tv.CursorPosition);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 Ths is the first line.  
 This is the second line.
 ", output);
@@ -6231,7 +6231,7 @@ This is the second line.
 
 			Assert.True (tv.WordWrap);
 			Assert.Equal (Point.Empty, tv.CursorPosition);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This is the first line. 
 This is the second line.
 ", output);
@@ -6241,7 +6241,7 @@ This is the second line.
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.DeleteChar, new KeyModifiers ())));
 			tv.Redraw (tv.Bounds);
 			Assert.Equal (new Point (2, 0), tv.CursorPosition);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 Ths is the first line.  
 This is the second line.
 ", output);
@@ -6251,14 +6251,14 @@ This is the second line.
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.DeleteChar, new KeyModifiers ())));
 			tv.Redraw (tv.Bounds);
 			Assert.Equal (new Point (22, 0), tv.CursorPosition);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 Ths is the first line.This is the second line.
 ", output);
 
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
 			tv.Redraw (tv.Bounds);
 			Assert.Equal (new Point (0, 1), tv.CursorPosition);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 Ths is the first line.  
 This is the second line.
 ", output);
@@ -6295,7 +6295,7 @@ This is the second line.
 			((FakeDriver)Application.Driver).SetBufferSize (15, 15);
 			Application.Refresh ();
 			//this passes
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (
 			@"
 ┌─────────────┐
 │             │
@@ -6321,7 +6321,7 @@ This is the second line.
 			tv.InsertText ("\naaa\nbbb");
 			Application.Refresh ();
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (
+			TestHelpers.AssertDriverContentsWithFrameAre (
 			@"
 ┌─────────────┐
 │             │
@@ -6365,7 +6365,7 @@ This is the second line.
 			Application.Refresh ();
 
 			//this passes
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (
 			@"
 ┌─────────────┐
 │             │
@@ -6391,7 +6391,7 @@ This is the second line.
 			tv.InsertText ("\r\naaa\r\nbbb");
 			Application.Refresh ();
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (
+			TestHelpers.AssertDriverContentsWithFrameAre (
 			@"
 ┌─────────────┐
 │             │

+ 9 - 9
UnitTests/TreeViewTests.cs

@@ -748,7 +748,7 @@ namespace Terminal.Gui.Views {
 			tv.ColorScheme = new ColorScheme ();
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsAre (
+			TestHelpers.AssertDriverContentsAre (
 @"├-normal
 │ ├─pink
 │ └─normal
@@ -766,7 +766,7 @@ namespace Terminal.Gui.Views {
 			tv.Redraw (tv.Bounds);
 
 
-			GraphViewTests.AssertDriverContentsAre (
+			TestHelpers.AssertDriverContentsAre (
 @"├+normal
 └─pink
 ", output);
@@ -797,7 +797,7 @@ namespace Terminal.Gui.Views {
 			tv.ColorScheme = new ColorScheme ();
 			tv.Redraw (tv.Bounds);
 
-			GraphViewTests.AssertDriverContentsAre (
+			TestHelpers.AssertDriverContentsAre (
 @"├-normal
 │ ├─pink
 │ └─normal
@@ -814,7 +814,7 @@ namespace Terminal.Gui.Views {
 			tv.Redraw (tv.Bounds);
 
 
-			GraphViewTests.AssertDriverContentsAre (
+			TestHelpers.AssertDriverContentsAre (
 @"├+normal
 └─pink
 ", output);
@@ -830,7 +830,7 @@ namespace Terminal.Gui.Views {
 			tv.Redraw (tv.Bounds);
 
 
-			GraphViewTests.AssertDriverContentsAre (
+			TestHelpers.AssertDriverContentsAre (
 @"└─pink
 ", output);
 			Assert.Equal (-1, tv.GetObjectRow (n1));
@@ -861,14 +861,14 @@ namespace Terminal.Gui.Views {
 			tv.Redraw(tv.Bounds);
 
 			// Normal drawing of the tree view
-			GraphViewTests.AssertDriverContentsAre(
+			TestHelpers.AssertDriverContentsAre(
 @"├-normal
 │ ├─pink
 │ └─normal
 └─pink
 ",output);
 			// Should all be the same color
-			GraphViewTests.AssertDriverColorsAre(
+			TestHelpers.AssertDriverColorsAre(
 @"00000000
 00000000
 0000000000
@@ -892,7 +892,7 @@ namespace Terminal.Gui.Views {
 			tv.Redraw(tv.Bounds);
 	
 			// Same text
-			GraphViewTests.AssertDriverContentsAre(
+			TestHelpers.AssertDriverContentsAre(
 @"├-normal
 │ ├─pink
 │ └─normal
@@ -900,7 +900,7 @@ namespace Terminal.Gui.Views {
 ",output);
 			// but now the item (only not lines) appear
 			// in pink when they are the word "pink"
-			GraphViewTests.AssertDriverColorsAre(
+			TestHelpers.AssertDriverColorsAre(
 @"00000000
 00001111
 0000000000

+ 60 - 60
UnitTests/ViewTests.cs

@@ -1504,7 +1504,7 @@ Hello     X
 Y          
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 11, 3), pos);
 
 			label.AutoSize = false;
@@ -1519,7 +1519,7 @@ Hello     X
 Y          
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 11, 3), pos);
 		}
 
@@ -1550,7 +1550,7 @@ o
 Y  
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 3, 11), pos);
 
 			label.AutoSize = false;
@@ -1573,7 +1573,7 @@ o
 Y  
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 3, 11), pos);
 		}
 
@@ -2119,7 +2119,7 @@ Y
 └──────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 8, 4), pos);
 		}
 
@@ -2141,7 +2141,7 @@ Y
 └┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 2, 2), pos);
 		}
 
@@ -2165,7 +2165,7 @@ Y
 ──────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 7, 4), pos);
 
 			view.Frame = new Rect (-1, -1, 8, 4);
@@ -2177,7 +2177,7 @@ Y
 ──────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (6, 0, 7, 3), pos);
 
 			view.Frame = new Rect (0, 0, 8, 4);
@@ -2190,7 +2190,7 @@ Y
 └──────
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 7, 4), pos);
 
 			view.Frame = new Rect (0, 0, 8, 4);
@@ -2232,7 +2232,7 @@ Y
  └──────────────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (1, 1, 21, 14), pos);
 
 			Assert.True (scrollView.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
@@ -2255,7 +2255,7 @@ Y
  └──────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (1, 1, 21, 14), pos);
 
 			Assert.True (scrollView.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
@@ -2278,7 +2278,7 @@ Y
  └──────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (1, 1, 21, 14), pos);
 
 			Assert.True (scrollView.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
@@ -2301,7 +2301,7 @@ Y
  └──────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (1, 1, 21, 14), pos);
 
 			Assert.True (scrollView.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
@@ -2324,7 +2324,7 @@ Y
  └──────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (1, 1, 21, 14), pos);
 
 			Assert.True (scrollView.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
@@ -2347,7 +2347,7 @@ Y
  └──────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (1, 1, 21, 14), pos);
 
 			Assert.True (scrollView.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
@@ -2370,7 +2370,7 @@ Y
  └──────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (1, 1, 21, 14), pos);
 
 			Assert.True (scrollView.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
@@ -2393,7 +2393,7 @@ Y
  └──────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (1, 1, 21, 14), pos);
 
 			Assert.True (scrollView.ProcessKey (new KeyEvent (Key.CtrlMask | Key.Home, new KeyModifiers ())));
@@ -2417,7 +2417,7 @@ Y
  └──────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (1, 1, 21, 14), pos);
 
 			Assert.True (scrollView.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
@@ -2440,7 +2440,7 @@ Y
  └──────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (1, 1, 21, 14), pos);
 
 			Assert.True (scrollView.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
@@ -2463,7 +2463,7 @@ Y
  └──────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (1, 1, 21, 14), pos);
 		}
 
@@ -2503,7 +2503,7 @@ Y
 └──────────────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 20, 10), pos);
 
 			view.Clear ();
@@ -2511,7 +2511,7 @@ Y
 			expected = @"
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (Rect.Empty, pos);
 		}
 
@@ -2551,7 +2551,7 @@ Y
 └──────────────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 20, 10), pos);
 
 			view.Clear (view.Bounds);
@@ -2559,7 +2559,7 @@ Y
 			expected = @"
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (Rect.Empty, pos);
 		}
 
@@ -2626,7 +2626,7 @@ Y
 └──────────────────────────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 32, 32), pos);
 
 			verticalView.Text = $"最初の行{Environment.NewLine}二行目";
@@ -2667,7 +2667,7 @@ Y
 └──────────────────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 32, 32), pos);
 		}
 
@@ -2714,7 +2714,7 @@ Y
 └────────────────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 22, 22), pos);
 
 			view.Text = "Hello World";
@@ -2751,7 +2751,7 @@ Y
 └────────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 22, 22), pos);
 
 			view.AutoSize = true;
@@ -2788,7 +2788,7 @@ Y
 └────────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 22, 22), pos);
 
 			view.TextDirection = TextDirection.TopBottom_LeftRight;
@@ -2824,7 +2824,7 @@ Y
 └────────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 22, 22), pos);
 
 			view.AutoSize = false;
@@ -2861,7 +2861,7 @@ Y
 └────────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 22, 22), pos);
 
 			view.PreserveTrailingSpaces = true;
@@ -2897,7 +2897,7 @@ Y
 └────────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 22, 22), pos);
 
 			view.PreserveTrailingSpaces = false;
@@ -2937,7 +2937,7 @@ Y
 └────────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 22, 22), pos);
 
 			view.AutoSize = true;
@@ -2973,7 +2973,7 @@ Y
 └────────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 22, 22), pos);
 		}
 
@@ -3042,7 +3042,7 @@ Y
 └────────────────────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 22, 22), pos);
 
 			verticalView.Text = $"最初_の行二行目";
@@ -3080,7 +3080,7 @@ Y
 └────────────────────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (0, 0, 22, 22), pos);
 		}
 
@@ -3113,7 +3113,7 @@ Y
 └────────────────────────────┘
 ";
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 
 			Assert.True (btn.AutoSize);
 			btn.Text = "Say He_llo 你 changed";
@@ -3127,7 +3127,7 @@ Y
 └────────────────────────────┘
 ";
 
-			GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 		}
 
 		[Fact]
@@ -3500,7 +3500,7 @@ Y
 			Assert.False (view.AutoSize);
 			Assert.Equal (new Rect (0, 0, 10, 1), view.Frame);
 			Assert.Equal ("Test", view.TextFormatter.Text);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 Test
 ", output);
 
@@ -3509,7 +3509,7 @@ Test
 			Assert.Equal (new Rect (0, 0, 10, 1), view.Frame);
 			Assert.Equal ("First line\nSecond line", view.TextFormatter.Text);
 			Application.Refresh ();
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 First line
 ", output);
 
@@ -3518,7 +3518,7 @@ First line
 			Assert.Equal (new Rect (0, 0, 11, 2), view.Frame);
 			Assert.Equal ("First line\nSecond line", view.TextFormatter.Text);
 			Application.Refresh ();
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 First line 
 Second line
 ", output);
@@ -3528,7 +3528,7 @@ Second line
 			Assert.Equal (new Rect (0, 0, 10, 1), view.Frame);
 			Assert.Equal ("First line\nSecond line", view.TextFormatter.Text);
 			Application.Refresh ();
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 First line
 ", output);
 		}
@@ -3576,7 +3576,7 @@ First line
 			Assert.False (view.AutoSize);
 			Assert.Equal (new Rect (0, 0, 1, 10), view.Frame);
 			Assert.Equal ("Test", view.TextFormatter.Text);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 T
 e
 s
@@ -3588,7 +3588,7 @@ t
 			Assert.Equal (new Rect (0, 0, 1, 10), view.Frame);
 			Assert.Equal ("First line\nSecond line", view.TextFormatter.Text);
 			Application.Refresh ();
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 F
 i
 r
@@ -3606,7 +3606,7 @@ e
 			Assert.Equal (new Rect (0, 0, 2, 11), view.Frame);
 			Assert.Equal ("First line\nSecond line", view.TextFormatter.Text);
 			Application.Refresh ();
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 FS
 ie
 rc
@@ -3625,7 +3625,7 @@ en
 			Assert.Equal (new Rect (0, 0, 1, 10), view.Frame);
 			Assert.Equal ("First line\nSecond line", view.TextFormatter.Text);
 			Application.Refresh ();
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 F
 i
 r
@@ -3680,7 +3680,7 @@ e
 			Assert.False (view.AutoSize);
 			Assert.Equal (new Rect (0, 0, 10, 1), view.Frame);
 			Assert.Equal ("Test 你", view.TextFormatter.Text);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 Test 你
 ", output);
 
@@ -3689,7 +3689,7 @@ Test 你
 			Assert.Equal (new Rect (0, 0, 10, 1), view.Frame);
 			Assert.Equal ("First line 你\nSecond line 你", view.TextFormatter.Text);
 			Application.Refresh ();
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 First line
 ", output);
 
@@ -3698,7 +3698,7 @@ First line
 			Assert.Equal (new Rect (0, 0, 14, 2), view.Frame);
 			Assert.Equal ("First line 你\nSecond line 你", view.TextFormatter.Text);
 			Application.Refresh ();
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 First line 你 
 Second line 你
 ", output);
@@ -3708,7 +3708,7 @@ Second line 你
 			Assert.Equal (new Rect (0, 0, 10, 1), view.Frame);
 			Assert.Equal ("First line 你\nSecond line 你", view.TextFormatter.Text);
 			Application.Refresh ();
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 First line
 ", output);
 		}
@@ -3758,7 +3758,7 @@ First line
 			// SetMinWidthHeight ensuring the minimum width for the wide char
 			Assert.Equal (new Rect (0, 0, 2, 10), view.Frame);
 			Assert.Equal ("Test 你", view.TextFormatter.Text);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 T 
 e 
 s 
@@ -3772,7 +3772,7 @@ t
 			Assert.Equal (new Rect (0, 0, 2, 10), view.Frame);
 			Assert.Equal ("First line 你\nSecond line 你", view.TextFormatter.Text);
 			Application.Refresh ();
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 F
 i
 r
@@ -3790,7 +3790,7 @@ e
 			Assert.Equal (new Rect (0, 0, 4, 13), view.Frame);
 			Assert.Equal ("First line 你\nSecond line 你", view.TextFormatter.Text);
 			Application.Refresh ();
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 F S 
 i e 
 r c 
@@ -3811,7 +3811,7 @@ e n
 			Assert.Equal (new Rect (0, 0, 2, 10), view.Frame);
 			Assert.Equal ("First line 你\nSecond line 你", view.TextFormatter.Text);
 			Application.Refresh ();
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 F
 i
 r
@@ -3837,7 +3837,7 @@ e
 
 			Assert.True (label.Visible);
 			((FakeDriver)Application.Driver).SetBufferSize (30, 5);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌────────────────────────────┐
 │Testing visibility.         │
 │                            │
@@ -3846,7 +3846,7 @@ e
 ", output);
 
 			label.Visible = false;
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌────────────────────────────┐
 │                            │
 │                            │
@@ -3869,7 +3869,7 @@ e
 			Application.Begin (Application.Top);
 
 			Assert.True (sbv.Visible);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This is a tes▲
 This is a tes┬
 This is a tes┴
@@ -3881,7 +3881,7 @@ This is a tes▼
 			sbv.Visible = false;
 			Assert.False (sbv.Visible);
 			Application.Top.Redraw (Application.Top.Bounds);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This is a test
 This is a test
 This is a test
@@ -3893,7 +3893,7 @@ This is a test
 			sbv.Visible = true;
 			Assert.True (sbv.Visible);
 			Application.Top.Redraw (Application.Top.Bounds);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This is a tes▲
 This is a tes┬
 This is a tes┴
@@ -3905,7 +3905,7 @@ This is a tes▼
 			sbv.ClearOnVisibleFalse = true;
 			sbv.Visible = false;
 			Assert.False (sbv.Visible);
-			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This is a tes
 This is a tes
 This is a tes

+ 3 - 3
UnitTests/WizardTests.cs

@@ -127,7 +127,7 @@ namespace Terminal.Gui.Views {
 
 			var wizard = new Wizard (title) { Width = width, Height = height };
 			Application.End (Application.Begin (wizard));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{row2}\n{row3}\n{separatorRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{row2}\n{row3}\n{separatorRow}\n{buttonRow}\n{bottomRow}", output);
 		}
 
 		[Fact, AutoInitShutdown]
@@ -164,7 +164,7 @@ namespace Terminal.Gui.Views {
 			var runstate = Application.Begin (wizard);
 			Application.RunMainLoopIteration (ref runstate, true, ref firstIteration);
 
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{row2}\n{row3}\n{row4}\n{separatorRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{row2}\n{row3}\n{row4}\n{separatorRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 		}
 
@@ -231,7 +231,7 @@ namespace Terminal.Gui.Views {
 			wizard.AddStep (new Wizard.WizardStep ("ABCD"));
 
 			Application.End (Application.Begin (wizard));
-			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{separatorRow}\n{buttonRow}\n{bottomRow}", output);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{separatorRow}\n{buttonRow}\n{bottomRow}", output);
 		}
 
 		[Fact, AutoInitShutdown]

BIN
docfx/images/Example.png


+ 8 - 0
docfx/overrides/Terminal_Gui_Application.md

@@ -0,0 +1,8 @@
+---
+uid: Terminal.Gui.Application
+summary: '*You can override summary for the API here using *MARKDOWN* syntax'
+---
+
+*Please type below more information about this API:*
+
+![Sample](images/sample.png)