瀏覽代碼

merge develop to v2

Tig Kindel 2 年之前
父節點
當前提交
c566a66bc0
共有 100 個文件被更改,包括 5160 次插入2700 次删除
  1. 2 2
      .github/workflows/api-docs.yml
  2. 3 2
      .github/workflows/dotnet-core.yml
  3. 3 3
      .github/workflows/publish.yml
  4. 59 63
      Example/Example.cs
  5. 0 3
      Example/Example.csproj
  6. 2 0
      Example/README.md
  7. 85 84
      README.md
  8. 2 2
      ReactiveExample/ReactiveExample.csproj
  9. 20 16
      Terminal.Gui UnitTests/ScenarioTests.cs
  10. 57 0
      Terminal.Gui UnitTests/UnitTests.csproj
  11. 134 177
      Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
  12. 8 4
      Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs
  13. 15 7
      Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs
  14. 111 12
      Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
  15. 5 11
      Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs
  16. 43 13
      Terminal.Gui/ConsoleDrivers/NetDriver.cs
  17. 93 21
      Terminal.Gui/ConsoleDrivers/WindowsDriver.cs
  18. 267 107
      Terminal.Gui/Core/Application.cs
  19. 38 11
      Terminal.Gui/Core/Clipboard/Clipboard.cs
  20. 26 20
      Terminal.Gui/Core/Clipboard/ClipboardBase.cs
  21. 244 0
      Terminal.Gui/Core/CollectionNavigator.cs
  22. 41 41
      Terminal.Gui/Core/Command.cs
  23. 87 145
      Terminal.Gui/Core/ConsoleDriver.cs
  24. 521 0
      Terminal.Gui/Core/ConsoleKeyMapping.cs
  25. 97 26
      Terminal.Gui/Core/Event.cs
  26. 9 5
      Terminal.Gui/Core/MainLoop.cs
  27. 1 1
      Terminal.Gui/Core/PosDim.cs
  28. 20 0
      Terminal.Gui/Core/Responder.cs
  29. 30 30
      Terminal.Gui/Core/TextFormatter.cs
  30. 19 27
      Terminal.Gui/Core/Toplevel.cs
  31. 41 39
      Terminal.Gui/Core/Trees/Branch.cs
  32. 15 10
      Terminal.Gui/Core/Trees/TreeStyle.cs
  33. 180 173
      Terminal.Gui/Core/View.cs
  34. 3 10
      Terminal.Gui/Core/Window.cs
  35. 30 30
      Terminal.Gui/README.md
  36. 6 86
      Terminal.Gui/Terminal.Gui.csproj
  37. 1 2
      Terminal.Gui/Views/Button.cs
  38. 7 2
      Terminal.Gui/Views/ComboBox.cs
  39. 43 20
      Terminal.Gui/Views/ContextMenu.cs
  40. 7 1
      Terminal.Gui/Views/GraphView.cs
  41. 124 118
      Terminal.Gui/Views/ListView.cs
  42. 149 160
      Terminal.Gui/Views/Menu.cs
  43. 34 5
      Terminal.Gui/Views/RadioGroup.cs
  44. 14 14
      Terminal.Gui/Views/ScrollBarView.cs
  45. 25 15
      Terminal.Gui/Views/ScrollView.cs
  46. 7 50
      Terminal.Gui/Views/StatusBar.cs
  47. 75 31
      Terminal.Gui/Views/TabView.cs
  48. 288 33
      Terminal.Gui/Views/TableView.cs
  49. 5 1
      Terminal.Gui/Views/TextField.cs
  50. 236 192
      Terminal.Gui/Views/TextView.cs
  51. 168 159
      Terminal.Gui/Views/TreeView.cs
  52. 7 3
      Terminal.Gui/Windows/Dialog.cs
  53. 43 7
      Terminal.Gui/Windows/FileDialog.cs
  54. 5 2
      Terminal.Gui/Windows/MessageBox.cs
  55. 1 1
      Terminal.Gui/Windows/Wizard.cs
  56. 1 0
      Terminal.sln
  57. 128 0
      Terminal.sln.DotSettings
  58. 31 3
      UICatalog/Properties/launchSettings.json
  59. 15 50
      UICatalog/README.md
  60. 25 25
      UICatalog/Scenario.cs
  61. 10 27
      UICatalog/Scenarios/AllViewsTester.cs
  62. 1 8
      UICatalog/Scenarios/BackgroundWorkerCollection.cs
  63. 4 7
      UICatalog/Scenarios/BordersComparisons.cs
  64. 2 2
      UICatalog/Scenarios/Buttons.cs
  65. 400 102
      UICatalog/Scenarios/CharacterMap.cs
  66. 20 7
      UICatalog/Scenarios/ClassExplorer.cs
  67. 4 17
      UICatalog/Scenarios/Clipping.cs
  68. 192 0
      UICatalog/Scenarios/CollectionNavigatorTester.cs
  69. 2 2
      UICatalog/Scenarios/ComputedLayout.cs
  70. 1 1
      UICatalog/Scenarios/ContextMenus.cs
  71. 211 196
      UICatalog/Scenarios/CsvEditor.cs
  72. 2 2
      UICatalog/Scenarios/Dialogs.cs
  73. 3 4
      UICatalog/Scenarios/DynamicMenuBar.cs
  74. 2 3
      UICatalog/Scenarios/DynamicStatusBar.cs
  75. 10 12
      UICatalog/Scenarios/Editor.cs
  76. 3 3
      UICatalog/Scenarios/GraphViewExample.cs
  77. 2 2
      UICatalog/Scenarios/HexEditor.cs
  78. 3 3
      UICatalog/Scenarios/InteractiveTree.cs
  79. 7 8
      UICatalog/Scenarios/Keys.cs
  80. 3 3
      UICatalog/Scenarios/LabelsAsButtons.cs
  81. 3 3
      UICatalog/Scenarios/LineViewExample.cs
  82. 12 9
      UICatalog/Scenarios/ListViewWithSelection.cs
  83. 2 2
      UICatalog/Scenarios/MessageBoxes.cs
  84. 3 3
      UICatalog/Scenarios/MultiColouredTable.cs
  85. 57 39
      UICatalog/Scenarios/Notepad.cs
  86. 2 2
      UICatalog/Scenarios/ProgressBarStyles.cs
  87. 76 0
      UICatalog/Scenarios/RunTExample.cs
  88. 4 4
      UICatalog/Scenarios/RuneWidthGreaterThanOne.cs
  89. 33 20
      UICatalog/Scenarios/Scrolling.cs
  90. 2 2
      UICatalog/Scenarios/SingleBackgroundWorker.cs
  91. 3 3
      UICatalog/Scenarios/SyntaxHighlighting.cs
  92. 3 7
      UICatalog/Scenarios/TabViewExample.cs
  93. 108 4
      UICatalog/Scenarios/TableEditor.cs
  94. 84 33
      UICatalog/Scenarios/Text.cs
  95. 1 1
      UICatalog/Scenarios/TextFormatterDemo.cs
  96. 2 2
      UICatalog/Scenarios/TextViewAutocompletePopup.cs
  97. 2 2
      UICatalog/Scenarios/Threading.cs
  98. 3 3
      UICatalog/Scenarios/TreeUseCases.cs
  99. 119 74
      UICatalog/Scenarios/TreeViewFileSystem.cs
  100. 3 3
      UICatalog/Scenarios/Unicode.cs

+ 2 - 2
.github/workflows/api-docs.yml

@@ -10,10 +10,10 @@ jobs:
 
     steps:
     - name: Checkout
-      uses: actions/checkout@v2
+      uses: actions/checkout@v3
 
     - name: Setup .NET Core
-      uses: actions/[email protected].1
+      uses: actions/[email protected].3
       with:
         dotnet-version: 6.0.100
     

+ 3 - 2
.github/workflows/dotnet-core.yml

@@ -15,12 +15,13 @@ jobs:
     - uses: actions/checkout@v3
 
     - name: Setup .NET Core
-      uses: actions/[email protected].1
+      uses: actions/[email protected].3
       with:
         dotnet-version: 6.0.100
 
     - name: Install dependencies
-      run: dotnet restore
+      run: |
+        dotnet restore
 
     - name: Build Debug
       run: dotnet build --configuration Debug --no-restore

+ 3 - 3
.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]3
+      uses: gittools/actions/gitversion/[email protected]5
       with:
         versionSpec: 5.x
 
     - name: Determine Version
-      uses: gittools/actions/gitversion/[email protected]3
+      uses: gittools/actions/gitversion/[email protected]5
       id: gitversion # step id used as reference for output values
 
     - name: Display GitVersion outputs
@@ -30,7 +30,7 @@ jobs:
         echo "CommitsSinceVersionSource: ${{ steps.gitversion.outputs.CommitsSinceVersionSource }}"
 
     - name: Setup dotnet
-      uses: actions/[email protected].1
+      uses: actions/[email protected].3
       with:
         dotnet-version: 6.0.100
 

+ 59 - 63
Example/Example.cs

@@ -1,76 +1,72 @@
-// 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
+
+// A simple Terminal.Gui example in C# - using C# 9.0 Top-level statements
 
 using Terminal.Gui;
-using NStack;
 
-Application.Init ();
+Application.Run<ExampleWindow> ();
+
+System.Console.WriteLine ($"Username: {((ExampleWindow)Application.Top).usernameText.Text}");
 
-// Creates the top-level window to show
-var win = new Window ("Example App") {
-	X = 0,
-	Y = 1, // Leave one row for the toplevel menu
+// Before the application exits, reset Terminal.Gui for clean shutdown
+Application.Shutdown ();
 
-	// By using Dim.Fill(), this Window will automatically resize without manual intervention
-	Width = Dim.Fill (),
-	Height = Dim.Fill ()
-};
+// Defines a top-level window with border and title
+public class ExampleWindow : Window {
+	public TextField usernameText;
+	
+	public ExampleWindow ()
+	{
+		Title = "Example App (Ctrl+Q to quit)";
 
-Application.Top.Add (win);
+		// Create input components and labels
+		var usernameLabel = new Label () { 
+			Text = "Username:" 
+		};
 
-// 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);
+		usernameText = new TextField ("") {
+			// Position text field adjacent to the label
+			X = Pos.Right (usernameLabel) + 1,
 
-static bool Quit ()
-{
-	var n = MessageBox.Query (50, 7, "Quit Example", "Are you sure you want to quit this example?", "Yes", "No");
-	return n == 0;
-}
+			// Fill remaining horizontal space
+			Width = Dim.Fill (),
+		};
 
-var login = new Label ("Login: ") { X = 3, Y = 2 };
-var password = new Label ("Password: ") {
-	X = Pos.Left (login),
-	Y = Pos.Top (login) + 1
-};
-var loginText = new TextField ("") {
-	X = Pos.Right (password),
-	Y = Pos.Top (login),
-	Width = 40
-};
-var passText = new TextField ("") {
-	Secret = true,
-	X = Pos.Left (loginText),
-	Y = Pos.Top (password),
-	Width = Dim.Width (loginText)
-};
+		var passwordLabel = new Label () {
+			Text = "Password:",
+			X = Pos.Left (usernameLabel),
+			Y = Pos.Bottom (usernameLabel) + 1
+		};
 
-// Add the views to the main window, 
-win.Add (
-	// Using Computed Layout:
-	login, password, loginText, passText,
+		var passwordText = new TextField ("") {
+			Secret = true,
+			// align with the text box above
+			X = Pos.Left (usernameText),
+			Y = Pos.Top (passwordLabel),
+			Width = Dim.Fill (),
+		};
 
-	// 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")
-);
+		// Create login button
+		var btnLogin = new Button () {
+			Text = "Login",
+			Y = Pos.Bottom(passwordLabel) + 1,
+			// center the login button horizontally
+			X = Pos.Center (),
+			IsDefault = true,
+		};
 
-// Run blocks until the user quits the application
-Application.Run ();
+		// When login button is clicked display a message popup
+		btnLogin.Clicked += () => {
+			if (usernameText.Text == "admin" && passwordText.Text == "password") {
+				MessageBox.Query ("Logging In", "Login Successful", "Ok");
+				Application.RequestStop ();
+			} else {
+				MessageBox.ErrorQuery ("Logging In", "Incorrect username or password", "Ok");
+			}
+		};
 
-// Always bracket Application.Init with .Shutdown.
-Application.Shutdown ();
+		// Add the views to the Window
+		Add (usernameLabel, usernameText, passwordLabel, passwordText, btnLogin);
+	}
+}

+ 0 - 3
Example/Example.csproj

@@ -10,9 +10,6 @@
     <Version>2.0</Version>
     <InformationalVersion>2.0</InformationalVersion>
   </PropertyGroup>
-  <ItemGroup>
-    <PackageReference Include="NStack.Core" Version="1.0.3" />
-  </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\Terminal.Gui\Terminal.Gui.csproj" />
   </ItemGroup>

+ 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).

+ 85 - 84
README.md

@@ -16,6 +16,19 @@ A toolkit for building rich console apps for .NET, .NET Core, and Mono that work
 
 ![Sample app](docfx/images/sample.gif)
 
+## Quick Start
+
+Paste these commands into your favorite terminal on Windows, Mac, or Linux. This will install the [Terminal.Gui.Templates](https://github.com/gui-cs/Terminal.Gui.templates), create a new "Hello World" TUI app, and run it.
+
+(Press `CTRL-Q` to exit the app)
+
+```powershell
+dotnet new --install Terminal.Gui.templates
+dotnet new tui -n myproj
+cd myproj
+dotnet run
+```
+
 ## Documentation 
 
 * [Documentation Home](https://gui-cs.github.io/Terminal.Gui/index.html)
@@ -51,100 +64,86 @@ 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 ();
-
-// Creates the top-level window to show
-var win = new Window ("Example App") {
-	X = 0,
-	Y = 1, // Leave one row for the toplevel menu
-
-	// By using Dim.Fill(), this Window will automatically resize without manual intervention
-	Width = Dim.Fill (),
-	Height = Dim.Fill ()
-};
-
-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 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
-};
-var loginText = new TextField ("") {
-	X = Pos.Right (password),
-	Y = Pos.Top (login),
-	Width = 40
-};
-var passText = new TextField ("") {
-	Secret = true,
-	X = Pos.Left (loginText),
-	Y = Pos.Top (password),
-	Width = Dim.Width (loginText)
-};
-
-// 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")
-);
-
-// Run blocks until the user quits the application
-Application.Run ();
-
-// Always bracket Application.Init with .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)**.
+Application.Run<ExampleWindow> ();
 
-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:
+System.Console.WriteLine ($"Username: {((ExampleWindow)Application.Top).usernameText.Text}");
 
-```csharp
-using Terminal.Gui;
+// Before the application exits, reset Terminal.Gui for clean shutdown
+Application.Shutdown ();
 
-class Demo {
-	static void Main ()
+// Defines a top-level window with border and title
+public class ExampleWindow : Window {
+	public TextField usernameText;
+	
+	public ExampleWindow ()
 	{
-		Application.Run<App> ();
-		Application.Shutdown ();
+		Title = "Example App (Ctrl+Q to quit)";
+
+		// Create input components and labels
+		var usernameLabel = new Label () { 
+			Text = "Username:" 
+		};
+
+		usernameText = new TextField ("") {
+			// Position text field adjacent to the label
+			X = Pos.Right (usernameLabel) + 1,
+
+			// Fill remaining horizontal space
+			Width = Dim.Fill (),
+		};
+
+		var passwordLabel = new Label () {
+			Text = "Password:",
+			X = Pos.Left (usernameLabel),
+			Y = Pos.Bottom (usernameLabel) + 1
+		};
+
+		var passwordText = new TextField ("") {
+			Secret = true,
+			// align with the text box above
+			X = Pos.Left (usernameText),
+			Y = Pos.Top (passwordLabel),
+			Width = Dim.Fill (),
+		};
+
+		// Create login button
+		var btnLogin = new Button () {
+			Text = "Login",
+			Y = Pos.Bottom(passwordLabel) + 1,
+			// center the login button horizontally
+			X = Pos.Center (),
+			IsDefault = true,
+		};
+
+		// When login button is clicked display a message popup
+		btnLogin.Clicked += () => {
+			if (usernameText.Text == "admin" && passwordText.Text == "password") {
+				MessageBox.Query ("Logging In", "Login Successful", "Ok");
+				Application.RequestStop ();
+			} else {
+				MessageBox.ErrorQuery ("Logging In", "Incorrect username or password", "Ok");
+			}
+		};
+
+		// Add the views to the Window
+		Add (usernameLabel, usernameText, passwordLabel, passwordText, btnLogin);
 	}
 }
 ```
 
+When run the application looks as follows:
+
+![Simple Usage app](./docfx/images/Example.png)
+
+_Sample application running_
+
 ## Installing
 
 Use NuGet to install the `Terminal.Gui` NuGet package: https://www.nuget.org/packages/Terminal.Gui
@@ -157,13 +156,15 @@ To install Terminal.Gui into a .NET Core project, use the `dotnet` CLI tool with
 dotnet add package Terminal.Gui
 ```
 
-See [CONTRIBUTING.md](CONTRIBUTING.md) for instructions for downloading and forking the source.
+Or, you can use the [Terminal.Gui.Templates](https://github.com/gui-cs/Terminal.Gui.templates).
 
-## Running and Building
+## Building the Library and Running the Examples
 
 * Windows, Mac, and Linux - Build and run using the .NET SDK command line tools (`dotnet build` in the root directory). Run `UICatalog` with `dotnet run --project UICatalog`.
 * Windows - Open `Terminal.sln` with Visual Studio 2022.
 
+See [CONTRIBUTING.md](CONTRIBUTING.md) for instructions for downloading and forking the source.
+
 ## Contributing
 
 See [CONTRIBUTING.md](https://github.com/gui-cs/Terminal.Gui/blob/master/CONTRIBUTING.md).
@@ -172,4 +173,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.

+ 2 - 2
ReactiveExample/ReactiveExample.csproj

@@ -11,8 +11,8 @@
     <InformationalVersion>2.0</InformationalVersion>
   </PropertyGroup>
   <ItemGroup>
-    <PackageReference Include="ReactiveUI.Fody" Version="18.3.1" />
-    <PackageReference Include="ReactiveUI" Version="18.3.1" />
+    <PackageReference Include="ReactiveUI.Fody" Version="18.4.1" />
+    <PackageReference Include="ReactiveUI" Version="18.4.1" />
     <PackageReference Include="ReactiveMarbles.ObservableEvents.SourceGenerator" Version="1.2.3" PrivateAssets="all" />
   </ItemGroup>
   <ItemGroup>

+ 20 - 16
UnitTests/ScenarioTests.cs → Terminal.Gui 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,28 +49,33 @@ 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
-				var token = Application.MainLoop.AddTimeout (TimeSpan.FromMilliseconds (200), closeCallback);
+				var token = Application.MainLoop.AddTimeout (TimeSpan.FromMilliseconds (100), closeCallback);
 
-				scenario.Init (Application.Top, Colors.Base);
+				scenario.Init (Colors.Base);
 				scenario.Setup ();
 				scenario.Run ();
 				Application.Shutdown ();
+#if DEBUG_IDISPOSABLE
+				foreach (var inst in Responder.Instances) {
+					Assert.True (inst.WasDisposed);
+				}
+				Responder.Instances.Clear ();
+#endif
 			}
 #if DEBUG_IDISPOSABLE
 			foreach (var inst in Responder.Instances) {
@@ -83,11 +88,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 +121,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 (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);
 

+ 57 - 0
Terminal.Gui UnitTests/UnitTests.csproj

@@ -0,0 +1,57 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <TargetFramework>net6.0</TargetFramework>
+    <IsPackable>false</IsPackable>
+    <UseDataCollector />
+    <!-- Version numbers are automatically updated by gitversion when a release is released -->
+    <!-- In the source tree the version will always be 1.0 for all projects. -->
+    <!-- Do not modify these. -->
+    <AssemblyVersion>1.0</AssemblyVersion>
+    <FileVersion>1.0</FileVersion>
+    <Version>1.0</Version>
+    <InformationalVersion>1.0</InformationalVersion>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+    <DefineConstants>TRACE</DefineConstants>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+    <DefineConstants>TRACE;DEBUG_IDISPOSABLE</DefineConstants>
+  </PropertyGroup>
+  <ItemGroup>
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
+    <PackageReference Include="ReportGenerator" Version="5.1.10" />
+    <PackageReference Include="System.Collections" Version="4.3.0" />
+    <PackageReference Include="xunit" Version="2.4.2" />
+    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
+      <PrivateAssets>all</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+    </PackageReference>
+    <PackageReference Include="coverlet.collector" Version="3.2.0">
+      <PrivateAssets>all</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+    </PackageReference>
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\Terminal.Gui\Terminal.Gui.csproj" />
+    <ProjectReference Include="..\UICatalog\UICatalog.csproj" />
+  </ItemGroup>
+  <PropertyGroup Label="FineCodeCoverage">
+    <Enabled>
+      True
+    </Enabled>
+    <Exclude>
+      [UICatalog]*
+    </Exclude>
+    <Include></Include>
+    <ExcludeByFile>
+      <!--**/Migrations/*
+      **/Hacks/*.cs-->
+    </ExcludeByFile>
+    <ExcludeByAttribute>
+      <!--MyCustomExcludeFromCodeCoverage-->
+    </ExcludeByAttribute>
+    <IncludeTestAssembly>
+      False
+    </IncludeTestAssembly>
+  </PropertyGroup>
+</Project>

+ 134 - 177
Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs

@@ -6,6 +6,7 @@
 //
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Linq;
 using System.Runtime.InteropServices;
 using System.Threading.Tasks;
@@ -617,7 +618,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 +788,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 +797,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;
 			});
 
@@ -950,11 +955,13 @@ namespace Terminal.Gui {
 
 		public static bool Is_WSL_Platform ()
 		{
-			if (new CursesClipboard ().IsSupported) {
-				return false;
-			}
-			var result = BashRunner.Run ("uname -a", runCurses: false);
-			if (result.Contains ("microsoft") && result.Contains ("WSL")) {
+			// xclip does not work on WSL, so we need to use the Windows clipboard vis Powershell
+			//if (new CursesClipboard ().IsSupported) {
+			//	// If xclip is installed on Linux under WSL, this will return true.
+			//	return false;
+			//}
+			var (exitCode, result) = ClipboardProcessRunner.Bash ("uname -a", waitForOutput: true);
+			if (exitCode == 0 && result.Contains ("microsoft") && result.Contains ("WSL")) {
 				return true;
 			}
 			return false;
@@ -1128,26 +1135,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)
@@ -1233,134 +1262,79 @@ namespace Terminal.Gui {
 		}
 	}
 
+	/// <summary>
+	///  A clipboard implementation for Linux.
+	///  This implementation uses the xclip command to access the clipboard.
+	/// </summary>	
+	/// <remarks>
+	/// If xclip is not installed, this implementation will not work.
+	/// </remarks>
 	class CursesClipboard : ClipboardBase {
 		public CursesClipboard ()
 		{
 			IsSupported = CheckSupport ();
 		}
 
+		string xclipPath = string.Empty;
 		public override bool IsSupported { get; }
 
 		bool CheckSupport ()
 		{
 			try {
-				var result = BashRunner.Run ("which xclip", runCurses: false);
-				return result.FileExists ();
+				var (exitCode, result) = ClipboardProcessRunner.Bash ("which xclip", waitForOutput: true);
+				if (exitCode == 0 && result.FileExists ()) {
+					xclipPath = result;
+					return true;
+				}
 			} catch (Exception) {
 				// Permissions issue.
-				return false;
 			}
+			return false;
 		}
 
 		protected override string GetClipboardDataImpl ()
 		{
 			var tempFileName = System.IO.Path.GetTempFileName ();
-			try {
-				// BashRunner.Run ($"xsel -o --clipboard > {tempFileName}");
-				BashRunner.Run ($"xclip -selection clipboard -o > {tempFileName}");
-				return System.IO.File.ReadAllText (tempFileName);
-			} finally {
-				System.IO.File.Delete (tempFileName);
-			}
-		}
+			var xclipargs = "-selection clipboard -o";
 
-		protected override void SetClipboardDataImpl (string text)
-		{
-			// var tempFileName = System.IO.Path.GetTempFileName ();
-			// System.IO.File.WriteAllText (tempFileName, text);
-			// try {
-			// 	// BashRunner.Run ($"cat {tempFileName} | xsel -i --clipboard");
-			// 	BashRunner.Run ($"cat {tempFileName} | xclip -selection clipboard");
-			// } finally {
-			// 	System.IO.File.Delete (tempFileName);
-			// }
-
-			BashRunner.Run ("xclip -selection clipboard -i", false, text);
-		}
-	}
-
-	static class BashRunner {
-		public static string Run (string commandLine, bool output = true, string inputText = "", bool runCurses = true)
-		{
-			var arguments = $"-c \"{commandLine}\"";
-
-			if (output) {
-				var errorBuilder = new System.Text.StringBuilder ();
-				var outputBuilder = new System.Text.StringBuilder ();
-
-				using (var process = new System.Diagnostics.Process {
-					StartInfo = new System.Diagnostics.ProcessStartInfo {
-						FileName = "bash",
-						Arguments = arguments,
-						RedirectStandardOutput = true,
-						RedirectStandardError = true,
-						UseShellExecute = false,
-						CreateNoWindow = false,
-					}
-				}) {
-					process.Start ();
-					process.OutputDataReceived += (sender, args) => { outputBuilder.AppendLine (args.Data); };
-					process.BeginOutputReadLine ();
-					process.ErrorDataReceived += (sender, args) => { errorBuilder.AppendLine (args.Data); };
-					process.BeginErrorReadLine ();
-					if (!process.DoubleWaitForExit ()) {
-						var timeoutError = $@"Process timed out. Command line: bash {arguments}.
-							Output: {outputBuilder}
-							Error: {errorBuilder}";
-						throw new Exception (timeoutError);
-					}
-					if (process.ExitCode == 0) {
-						if (runCurses && Application.Driver is CursesDriver) {
-							Curses.raw ();
-							Curses.noecho ();
-						}
-						return outputBuilder.ToString ();
-					}
-
-					var error = $@"Could not execute process. Command line: bash {arguments}.
-						Output: {outputBuilder}
-						Error: {errorBuilder}";
-					throw new Exception (error);
-				}
-			} else {
-				using (var process = new System.Diagnostics.Process {
-					StartInfo = new System.Diagnostics.ProcessStartInfo {
-						FileName = "bash",
-						Arguments = arguments,
-						RedirectStandardInput = true,
-						RedirectStandardError = true,
-						UseShellExecute = false,
-						CreateNoWindow = false
-					}
-				}) {
-					process.Start ();
-					process.StandardInput.Write (inputText);
-					process.StandardInput.Close ();
-					process.WaitForExit ();
-					if (runCurses && Application.Driver is CursesDriver) {
+			try {
+				var (exitCode, result) = ClipboardProcessRunner.Bash ($"{xclipPath} {xclipargs} > {tempFileName}", waitForOutput: false);
+				if (exitCode == 0) {
+					if (Application.Driver is CursesDriver) {
 						Curses.raw ();
 						Curses.noecho ();
 					}
-					return inputText;
+					return System.IO.File.ReadAllText (tempFileName);
 				}
+			} catch (Exception e) {
+				throw new NotSupportedException ($"\"{xclipPath} {xclipargs}\" failed.", e);
+			} finally {
+				System.IO.File.Delete (tempFileName);
 			}
+			return string.Empty;
 		}
 
-		public static bool DoubleWaitForExit (this System.Diagnostics.Process process)
+		protected override void SetClipboardDataImpl (string text)
 		{
-			var result = process.WaitForExit (500);
-			if (result) {
-				process.WaitForExit ();
+			var xclipargs = "-selection clipboard -i";
+			try {
+				var (exitCode, _) = ClipboardProcessRunner.Bash ($"{xclipPath} {xclipargs}", text, waitForOutput: false);
+				if (exitCode == 0 && Application.Driver is CursesDriver) {
+					Curses.raw ();
+					Curses.noecho ();
+				}
+			} catch (Exception e) {
+				throw new NotSupportedException ($"\"{xclipPath} {xclipargs} < {text}\" failed", e);
 			}
-			return result;
-		}
-
-		public static bool FileExists (this string value)
-		{
-			return !string.IsNullOrEmpty (value) && !value.Contains ("not found");
 		}
 	}
 
+	/// <summary>
+	///  A clipboard implementation for MacOSX. 
+	///  This implementation uses the Mac clipboard API (via P/Invoke) to copy/paste.
+	///  The existance of the Mac pbcopy and pbpaste commands 
+	///  is used to determine if copy/paste is supported.
+	/// </summary>	
 	class MacOSXClipboard : ClipboardBase {
 		IntPtr nsString = objc_getClass ("NSString");
 		IntPtr nsPasteboard = objc_getClass ("NSPasteboard");
@@ -1387,12 +1361,12 @@ namespace Terminal.Gui {
 
 		bool CheckSupport ()
 		{
-			var result = BashRunner.Run ("which pbcopy");
-			if (!result.FileExists ()) {
+			var (exitCode, result) = ClipboardProcessRunner.Bash ("which pbcopy", waitForOutput: true);
+			if (exitCode != 0 || !result.FileExists ()) {
 				return false;
 			}
-			result = BashRunner.Run ("which pbpaste");
-			return result.FileExists ();
+			(exitCode, result) = ClipboardProcessRunner.Bash ("which pbpaste", waitForOutput: true);
+			return exitCode == 0 && result.FileExists ();
 		}
 
 		protected override string GetClipboardDataImpl ()
@@ -1435,94 +1409,77 @@ namespace Terminal.Gui {
 		static extern IntPtr sel_registerName (string selectorName);
 	}
 
+	/// <summary>
+	///  A clipboard implementation for Linux, when running under WSL. 
+	///  This implementation uses the Windows clipboard to store the data, and uses Windows'
+	///  powershell.exe (launched via WSL interop services) to set/get the Windows
+	///  clipboard. 
+	/// </summary>
 	class WSLClipboard : ClipboardBase {
+		bool isSupported = false;
 		public WSLClipboard ()
 		{
-			IsSupported = CheckSupport ();
+			isSupported = CheckSupport ();
 		}
 
-		public override bool IsSupported { get; }
+		public override bool IsSupported {
+			get {
+				return isSupported = CheckSupport ();
+			}
+		}
+
+		private static string powershellPath = string.Empty;
 
 		bool CheckSupport ()
 		{
-			try {
-				var result = BashRunner.Run ("which powershell.exe");
-				return result.FileExists ();
-			} catch (System.Exception) {
-				return false;
-			}
+			if (string.IsNullOrEmpty (powershellPath)) {
+				// Specify pwsh.exe (not pwsh) to ensure we get the Windows version (invoked via WSL)
+				var (exitCode, result) = ClipboardProcessRunner.Bash ("which pwsh.exe", waitForOutput: true);
+				if (exitCode > 0) {
+					(exitCode, result) = ClipboardProcessRunner.Bash ("which powershell.exe", waitForOutput: true);
+				}
 
-			//var result = BashRunner.Run ("which powershell.exe");
-			//if (!result.FileExists ()) {
-			//	return false;
-			//}
-			//result = BashRunner.Run ("which clip.exe");
-			//return result.FileExists ();
+				if (exitCode == 0) {
+					powershellPath = result;
+				}
+			}
+			return !string.IsNullOrEmpty (powershellPath);
 		}
 
 		protected override string GetClipboardDataImpl ()
 		{
-			using (var powershell = new System.Diagnostics.Process {
-				StartInfo = new System.Diagnostics.ProcessStartInfo {
-					RedirectStandardOutput = true,
-					FileName = "powershell.exe",
-					Arguments = "-noprofile -command \"Get-Clipboard\"",
-					UseShellExecute = false,
-					CreateNoWindow = true
-				}
-			}) {
-				powershell.Start ();
-				var result = powershell.StandardOutput.ReadToEnd ();
-				powershell.StandardOutput.Close ();
-				if (!powershell.DoubleWaitForExit ()) {
-					var timeoutError = $@"Process timed out. Command line: bash {powershell.StartInfo.Arguments}.
-							Output: {powershell.StandardOutput.ReadToEnd ()}
-							Error: {powershell.StandardError.ReadToEnd ()}";
-					throw new Exception (timeoutError);
-				}
+			if (!IsSupported) {
+				return string.Empty;
+			}
+
+			var (exitCode, output) = ClipboardProcessRunner.Process (powershellPath, "-noprofile -command \"Get-Clipboard\"");
+			if (exitCode == 0) {
 				if (Application.Driver is CursesDriver) {
 					Curses.raw ();
 					Curses.noecho ();
 				}
-				if (result.EndsWith ("\r\n")) {
-					result = result.Substring (0, result.Length - 2);
+
+				if (output.EndsWith ("\r\n")) {
+					output = output.Substring (0, output.Length - 2);
 				}
-				return result;
+				return output;
 			}
+			return string.Empty;
 		}
 
 		protected override void SetClipboardDataImpl (string text)
 		{
-			using (var powershell = new System.Diagnostics.Process {
-				StartInfo = new System.Diagnostics.ProcessStartInfo {
-					FileName = "powershell.exe",
-					Arguments = $"-noprofile -command \"Set-Clipboard -Value \\\"{text}\\\"\""
-				}
-			}) {
-				powershell.Start ();
-				if (!powershell.DoubleWaitForExit ()) {
-					var timeoutError = $@"Process timed out. Command line: bash {powershell.StartInfo.Arguments}.
-							Output: {powershell.StandardOutput.ReadToEnd ()}
-							Error: {powershell.StandardError.ReadToEnd ()}";
-					throw new Exception (timeoutError);
-				}
+			if (!IsSupported) {
+				return;
+			}
+
+			var (exitCode, output) = ClipboardProcessRunner.Process (powershellPath, $"-noprofile -command \"Set-Clipboard -Value \\\"{text}\\\"\"");
+			if (exitCode == 0) {
 				if (Application.Driver is CursesDriver) {
 					Curses.raw ();
 					Curses.noecho ();
 				}
 			}
-
-			//using (var clipExe = new System.Diagnostics.Process {
-			//	StartInfo = new System.Diagnostics.ProcessStartInfo {
-			//		FileName = "clip.exe",
-			//		RedirectStandardInput = true
-			//	}
-			//}) {
-			//	clipExe.Start ();
-			//	clipExe.StandardInput.Write (text);
-			//	clipExe.StandardInput.Close ();
-			//	clipExe.WaitForExit ();
-			//}
 		}
 	}
 }

+ 8 - 4
Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs

@@ -38,6 +38,11 @@ namespace Terminal.Gui {
 	/// can watch file descriptors using the AddWatch methods.
 	/// </remarks>
 	internal class UnixMainLoop : IMainLoopDriver {
+		public UnixMainLoop (ConsoleDriver consoleDriver = null)
+		{
+			// UnixDriver doesn't use the consoleDriver parameter, but the WindowsDriver does.
+		}
+
 		public const int KEY_RESIZE = unchecked((int)0xffffffffffffffff);
 
 		[StructLayout (LayoutKind.Sequential)]
@@ -176,16 +181,15 @@ namespace Terminal.Gui {
 		{
 			UpdatePollMap ();
 
-			if (CheckTimers (wait, out var pollTimeout)) {
-				return true;
-			}
+			bool checkTimersResult = CheckTimers (wait, out var pollTimeout);
 
 			var n = poll (pollmap, (uint)pollmap.Length, pollTimeout);
 
 			if (n == KEY_RESIZE) {
 				winChanged = true;
 			}
-			return n >= KEY_RESIZE || CheckTimers (wait, out pollTimeout);
+
+			return checkTimersResult || n >= KEY_RESIZE;
 		}
 
 		bool CheckTimers (bool wait, out int pollTimeout)

+ 15 - 7
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);
 			}
 		}
 
@@ -1396,7 +1401,10 @@ namespace Terminal.Gui {
 		/// <param name="buffer"></param>
 		public static void Write (char [] buffer)
 		{
-			throw new NotImplementedException ();
+			_buffer [CursorLeft, CursorTop] = (char)0;
+			foreach (var ch in buffer) {
+				_buffer [CursorLeft, CursorTop] += ch;
+			}
 		}
 
 		//

+ 111 - 12
Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs

@@ -6,10 +6,12 @@
 //
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Linq;
 using System.Runtime.InteropServices;
 using System.Threading;
 using NStack;
+
 // Alias Console to MockConsole so we don't accidentally use Console
 using Console = Terminal.Gui.FakeConsole;
 
@@ -19,6 +21,27 @@ namespace Terminal.Gui {
 	/// </summary>
 	public class FakeDriver : ConsoleDriver {
 #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+
+		public class Behaviors {
+
+			public bool UseFakeClipboard { get; internal set; }
+			public bool FakeClipboardAlwaysThrowsNotSupportedException { get; internal set; }
+			public bool FakeClipboardIsSupportedAlwaysFalse { get; internal set; }
+
+			public Behaviors (bool useFakeClipboard = false, bool fakeClipboardAlwaysThrowsNotSupportedException = false, bool fakeClipboardIsSupportedAlwaysTrue = false)
+			{
+				UseFakeClipboard = useFakeClipboard;
+				FakeClipboardAlwaysThrowsNotSupportedException = fakeClipboardAlwaysThrowsNotSupportedException;
+				FakeClipboardIsSupportedAlwaysFalse = fakeClipboardIsSupportedAlwaysTrue;
+				
+				// double check usage is correct
+				Debug.Assert (useFakeClipboard == false && fakeClipboardAlwaysThrowsNotSupportedException == false);
+				Debug.Assert (useFakeClipboard == false && fakeClipboardIsSupportedAlwaysTrue == false);
+			}
+		}
+
+		public static FakeDriver.Behaviors FakeBehaviors = new Behaviors ();
+
 		int cols, rows, left, top;
 		public override int Cols => cols;
 		public override int Rows => rows;
@@ -26,7 +49,8 @@ namespace Terminal.Gui {
 		public override int Left => 0;
 		public override int Top => 0;
 		public override bool HeightAsBuffer { get; set; }
-		public override IClipboard Clipboard { get; }
+		private IClipboard clipboard = null;
+		public override IClipboard Clipboard => clipboard;
 
 		// The format is rows, columns and 3 values on the last column: Rune, Attribute and Dirty Flag
 		int [,,] contents;
@@ -59,15 +83,19 @@ namespace Terminal.Gui {
 
 		public FakeDriver ()
 		{
-			if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) {
-				Clipboard = new WindowsClipboard ();
-			} else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) {
-				Clipboard = new MacOSXClipboard ();
+			if (FakeBehaviors.UseFakeClipboard) {
+				clipboard = new FakeClipboard (FakeBehaviors.FakeClipboardAlwaysThrowsNotSupportedException, FakeBehaviors.FakeClipboardIsSupportedAlwaysFalse);
 			} else {
-				if (CursesDriver.Is_WSL_Platform ()) {
-					Clipboard = new WSLClipboard ();
+				if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) {
+					clipboard = new WindowsClipboard ();
+				} else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) {
+					clipboard = new MacOSXClipboard ();
 				} else {
-					Clipboard = new CursesClipboard ();
+					if (CursesDriver.Is_WSL_Platform ()) {
+						clipboard = new WSLClipboard ();
+					} else {
+						clipboard = new CursesClipboard ();
+					}
 				}
 			}
 		}
@@ -104,12 +132,12 @@ namespace Terminal.Gui {
 					needMove = false;
 				}
 				if (runeWidth < 2 && ccol > 0
-					&& Rune.ColumnWidth ((char)contents [crow, ccol - 1, 0]) > 1) {
+					&& Rune.ColumnWidth ((Rune)contents [crow, ccol - 1, 0]) > 1) {
 
 					contents [crow, ccol - 1, 0] = (int)(uint)' ';
 
 				} else if (runeWidth < 2 && ccol <= Clip.Right - 1
-					&& Rune.ColumnWidth ((char)contents [crow, ccol, 0]) > 1) {
+					&& Rune.ColumnWidth ((Rune)contents [crow, ccol, 0]) > 1) {
 
 					contents [crow, ccol + 1, 0] = (int)(uint)' ';
 					contents [crow, ccol + 1, 2] = 1;
@@ -234,7 +262,12 @@ namespace Terminal.Gui {
 						if (color != redrawColor)
 							SetColor (color);
 
-						FakeConsole.Write ((char)contents [row, col, 0]);
+						Rune rune = contents [row, col, 0];
+						if (Rune.DecodeSurrogatePair (rune, out char [] spair)) {
+							FakeConsole.Write (spair);
+						} else {
+							FakeConsole.Write ((char)rune);
+						}
 						contents [row, col, 2] = 0;
 					}
 				}
@@ -256,6 +289,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 +312,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 +340,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 +371,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 +391,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 +448,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;
@@ -610,6 +674,41 @@ namespace Terminal.Gui {
 		}
 
 		#endregion
+
+		public class FakeClipboard : ClipboardBase {
+			public Exception FakeException = null;
+
+			string contents = string.Empty;
+
+			bool isSupportedAlwaysFalse = false;
+
+			public override bool IsSupported => !isSupportedAlwaysFalse;
+
+			public FakeClipboard (bool fakeClipboardThrowsNotSupportedException = false, bool isSupportedAlwaysFalse = false)
+			{
+				this.isSupportedAlwaysFalse = isSupportedAlwaysFalse;
+				if (fakeClipboardThrowsNotSupportedException) {
+					FakeException = new NotSupportedException ("Fake clipboard exception");
+				}
+			}
+
+			protected override string GetClipboardDataImpl ()
+			{
+				if (FakeException != null) {
+					throw FakeException;
+				}
+				return contents;
+			}
+
+			protected override void SetClipboardDataImpl (string text)
+			{
+				if (FakeException != null) {
+					throw FakeException;
+				}
+				contents = text;
+			}
+		}
+
 #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
 	}
 }

+ 5 - 11
Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs

@@ -15,7 +15,7 @@ namespace Terminal.Gui {
 		AutoResetEvent waitForProbe = new AutoResetEvent (false);
 		ConsoleKeyInfo? keyResult = null;
 		MainLoop mainLoop;
-		Func<ConsoleKeyInfo> consoleKeyReaderFn = null;
+		Func<ConsoleKeyInfo> consoleKeyReaderFn = () => FakeConsole.ReadKey (true);
 
 		/// <summary>
 		/// Invoked when a Key is pressed.
@@ -23,18 +23,12 @@ namespace Terminal.Gui {
 		public Action<ConsoleKeyInfo> KeyPressed;
 
 		/// <summary>
-		/// Initializes the class.
+		/// Creates an instance of the FakeMainLoop. <paramref name="consoleDriver"/> is not used.
 		/// </summary>
-		/// <remarks>
-		///   Passing a consoleKeyReaderfn is provided to support unit test scenarios.
-		/// </remarks>
-		/// <param name="consoleKeyReaderFn">The method to be called to get a key from the console.</param>
-		public FakeMainLoop (Func<ConsoleKeyInfo> consoleKeyReaderFn = null)
+		/// <param name="consoleDriver"></param>
+		public FakeMainLoop (ConsoleDriver consoleDriver = null)
 		{
-			if (consoleKeyReaderFn == null) {
-				throw new ArgumentNullException ("key reader function must be provided.");
-			}
-			this.consoleKeyReaderFn = consoleKeyReaderFn;
+			// consoleDriver is not needed/used in FakeConsole
 		}
 
 		void WindowsKeyReader ()

+ 43 - 13
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:
@@ -1480,7 +1483,13 @@ namespace Terminal.Gui {
 							output.Append (WriteAttributes (attr));
 						}
 						outputWidth++;
-						output.Append ((char)contents [row, col, 0]);
+						var rune = contents [row, col, 0];
+						char [] spair;
+						if (Rune.DecodeSurrogatePair((uint) rune, out spair)) {
+							output.Append (spair);
+						} else {
+							output.Append ((char)rune);
+						}
 						contents [row, col, 2] = 0;
 					}
 				}
@@ -1610,6 +1619,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 +1712,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 +1779,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 +1838,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 +1971,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);

+ 93 - 21
Terminal.Gui/ConsoleDrivers/WindowsDriver.cs

@@ -251,7 +251,7 @@ namespace Terminal.Gui {
 				throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ());
 			}
 			var winRect = new SmallRect (0, 0, (short)(newCols - 1), (short)Math.Max (newRows - 1, 0));
-			if (!SetConsoleWindowInfo (ScreenBuffer, true, ref winRect)) {
+			if (!SetConsoleWindowInfo (OutputHandle, true, ref winRect)) {
 				//throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ());
 				return new Size (cols, rows);
 			}
@@ -261,7 +261,7 @@ namespace Terminal.Gui {
 
 		void SetConsoleOutputWindow (CONSOLE_SCREEN_BUFFER_INFOEX csbi)
 		{
-			if (ScreenBuffer != IntPtr.Zero && !SetConsoleScreenBufferInfoEx (OutputHandle, ref csbi)) {
+			if (ScreenBuffer != IntPtr.Zero && !SetConsoleScreenBufferInfoEx (ScreenBuffer, ref csbi)) {
 				throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ());
 			}
 		}
@@ -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;
 			}
 		}
 
@@ -771,7 +773,7 @@ namespace Terminal.Gui {
 					w += 3;
 				}
 				var newSize = WinConsole.SetConsoleWindow (
-					(short)Math.Max (w, 16), (short)Math.Max (e.Height, 1));
+					(short)Math.Max (w, 16), (short)Math.Max (e.Height, 0));
 				left = 0;
 				top = 0;
 				cols = newSize.Width;
@@ -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)
@@ -1386,16 +1451,20 @@ namespace Terminal.Gui {
 		{
 			TerminalResized = terminalResized;
 
-			var winSize = WinConsole.GetConsoleOutputWindow (out Point pos);
-			cols = winSize.Width;
-			rows = winSize.Height;
+			try {
+				var winSize = WinConsole.GetConsoleOutputWindow (out Point pos);
+				cols = winSize.Width;
+				rows = winSize.Height;
 
-			WindowsConsole.SmallRect.MakeEmpty (ref damageRegion);
+				WindowsConsole.SmallRect.MakeEmpty (ref damageRegion);
 
-			ResizeScreen ();
-			UpdateOffScreen ();
+				ResizeScreen ();
+				UpdateOffScreen ();
 
-			CreateColors ();
+				CreateColors ();
+			} catch (Win32Exception e) {
+				throw new InvalidOperationException ("The Windows Console output window is not available.", e);
+			}
 		}
 
 		public override void ResizeScreen ()
@@ -1435,11 +1504,16 @@ namespace Terminal.Gui {
 			crow = row;
 		}
 
+		int GetOutputBufferPosition ()
+		{
+			return crow * Cols + ccol;
+		}
+
 		public override void AddRune (Rune rune)
 		{
 			rune = MakePrintable (rune);
 			var runeWidth = Rune.ColumnWidth (rune);
-			var position = crow * Cols + ccol;
+			var position = GetOutputBufferPosition ();
 			var validClip = IsValidContent (ccol, crow, Clip);
 
 			if (validClip) {
@@ -1453,7 +1527,7 @@ namespace Terminal.Gui {
 				} else if (runeWidth < 2 && ccol <= Clip.Right - 1
 					&& Rune.ColumnWidth ((char)contents [crow, ccol, 0]) > 1) {
 
-					var prevPosition = crow * Cols + ccol + 1;
+					var prevPosition = GetOutputBufferPosition () + 1;
 					OutputBuffer [prevPosition].Char.UnicodeChar = (char)' ';
 					contents [crow, ccol + 1, 0] = (int)(uint)' ';
 
@@ -1474,7 +1548,7 @@ namespace Terminal.Gui {
 			ccol++;
 			if (runeWidth > 1) {
 				if (validClip && ccol < Clip.Right) {
-					position = crow * Cols + ccol;
+					position = GetOutputBufferPosition ();
 					OutputBuffer [position].Attributes = (ushort)currentAttribute;
 					OutputBuffer [position].Char.UnicodeChar = (char)0x00;
 					contents [crow, ccol, 0] = (int)(uint)0x00;
@@ -1660,9 +1734,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';

+ 267 - 107
Terminal.Gui/Core/Application.cs

@@ -39,6 +39,7 @@ namespace Terminal.Gui {
 	/// };
 	/// Application.Top.Add(win);
 	/// Application.Run();
+	/// Application.Shutdown();
 	/// </code>
 	/// </example>
 	/// <remarks>
@@ -222,19 +223,28 @@ namespace Terminal.Gui {
 		public static bool ExitRunLoopAfterFirstIteration { get; set; } = false;
 
 		/// <summary>
-		/// Notify that a new <see cref="RunState"/> token was created,
-		/// used if <see cref="ExitRunLoopAfterFirstIteration"/> is true.
+		/// Notify that a new <see cref="RunState"/> was created (<see cref="Begin(Toplevel)"/> was called). The token is created in 
+		/// <see cref="Begin(Toplevel)"/> and this event will be fired before that function exits.
 		/// </summary>
+		/// <remarks>
+		///	If <see cref="ExitRunLoopAfterFirstIteration"/> is <see langword="true"/> callers to
+		///	<see cref="Begin(Toplevel)"/> must also subscribe to <see cref="NotifyStopRunState"/>
+		///	and manually dispose of the <see cref="RunState"/> token when the application is done.
+		/// </remarks>
 		public static event Action<RunState> NotifyNewRunState;
 
 		/// <summary>
-		/// Notify that a existent <see cref="RunState"/> token is stopping,
-		/// used if <see cref="ExitRunLoopAfterFirstIteration"/> is true.
+		/// Notify that a existent <see cref="RunState"/> is stopping (<see cref="End(RunState)"/> was called).
 		/// </summary>
+		/// <remarks>
+		///	If <see cref="ExitRunLoopAfterFirstIteration"/> is <see langword="true"/> callers to
+		///	<see cref="Begin(Toplevel)"/> must also subscribe to <see cref="NotifyStopRunState"/>
+		///	and manually dispose of the <see cref="RunState"/> token when the application is done.
+		/// </remarks>
 		public static event Action<Toplevel> NotifyStopRunState;
 
 		/// <summary>
-		///   This event is raised on each iteration of the <see cref="MainLoop"/> 
+		///   This event is raised on each iteration of the <see cref="MainLoop"/>. 
 		/// </summary>
 		/// <remarks>
 		///   See also <see cref="Timeout"/>
@@ -299,36 +309,59 @@ namespace Terminal.Gui {
 		/// </summary>
 		public static bool UseSystemConsole;
 
+		// For Unit testing - ignores UseSystemConsole
+		internal static bool ForceFakeConsole;
+
 		/// <summary>
 		/// Initializes a new instance of <see cref="Terminal.Gui"/> Application. 
 		/// </summary>
-		/// <remarks>
 		/// <para>
 		/// Call this method once per instance (or after <see cref="Shutdown"/> has been called).
 		/// </para>
 		/// <para>
-		/// Loads the right <see cref="ConsoleDriver"/> for the platform.
+		/// This function loads the right <see cref="ConsoleDriver"/> for the platform, 
+		/// Creates a <see cref="Toplevel"/>. and assigns it to <see cref="Top"/>
 		/// </para>
 		/// <para>
-		/// Creates a <see cref="Toplevel"/> and assigns it to <see cref="Top"/>
+		/// <see cref="Shutdown"/> must be called when the application is closing (typically after <see cref="Run(Func{Exception, bool})"/> has 
+		/// returned) to ensure resources are cleaned up and terminal settings restored.
 		/// </para>
-		/// </remarks>
-		public static void Init (ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null) => Init (() => Toplevel.Create (), driver, mainLoopDriver);
+		/// <para>
+		/// The <see cref="Run{T}(Func{Exception, bool}, ConsoleDriver, IMainLoopDriver)"/> function 
+		/// combines <see cref="Init(ConsoleDriver, IMainLoopDriver)"/> and <see cref="Run(Toplevel, Func{Exception, bool})"/>
+		/// into a single call. An applciation cam use <see cref="Run{T}(Func{Exception, bool}, ConsoleDriver, IMainLoopDriver)"/> 
+		/// without explicitly calling <see cref="Init(ConsoleDriver, IMainLoopDriver)"/>.
+		/// </para>
+		/// <param name="driver">
+		/// The <see cref="ConsoleDriver"/> to use. If not specified the default driver for the
+		/// platform will be used (see <see cref="WindowsDriver"/>, <see cref="CursesDriver"/>, and <see cref="NetDriver"/>).</param>
+		/// <param name="mainLoopDriver">
+		/// Specifies the <see cref="MainLoop"/> to use. 
+		/// Must not be <see langword="null"/> if <paramref name="driver"/> is not <see langword="null"/>.
+		/// </param>
+		public static void Init (ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null) => InternalInit (() => Toplevel.Create (), driver, mainLoopDriver);
 
 		internal static bool _initialized = false;
 		internal static int _mainThreadId = -1;
 
-		/// <summary>
-		/// Initializes the Terminal.Gui application
-		/// </summary>
-		static void Init (Func<Toplevel> topLevelFactory, ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null)
+		// INTERNAL function for initializing an app with a Toplevel factory object, driver, and mainloop.
+		//
+		// Called from:
+		// 
+		// Init() - When the user wants to use the default Toplevel. calledViaRunT will be false, causing all state to be reset.
+		// Run<T>() - When the user wants to use a custom Toplevel. calledViaRunT will be true, enabling Run<T>() to be called without calling Init first.
+		// Unit Tests - To initialize the app with a custom Toplevel, using the FakeDriver. calledViaRunT will be false, causing all state to be reset.
+		// 
+		// calledViaRunT: If false (default) all state will be reset. If true the state will not be reset.
+		internal static void InternalInit (Func<Toplevel> topLevelFactory, ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null, bool calledViaRunT = false)
 		{
 			if (_initialized && driver == null) return;
 
 			if (_initialized) {
-				throw new InvalidOperationException ("Init must be bracketed by Shutdown");
+				throw new InvalidOperationException ("Init has already been called and must be bracketed by Shutdown.");
 			}
 
+			// Note in this case, we don't verify the type of the Toplevel created by new T(). 
 			// Used only for start debugging on Unix.
 			//#if DEBUG
 			//			while (!System.Diagnostics.Debugger.IsAttached) {
@@ -337,36 +370,69 @@ namespace Terminal.Gui {
 			//			System.Diagnostics.Debugger.Break ();
 			//#endif
 
-			// Reset all class variables (Application is a singleton).
-			ResetState ();
+			if (!calledViaRunT) {
+				// Reset all class variables (Application is a singleton).
+				ResetState ();
+			}
 
-			// This supports Unit Tests and the passing of a mock driver/loopdriver
+			// For UnitTests
 			if (driver != null) {
-				if (mainLoopDriver == null) {
-					throw new ArgumentNullException ("mainLoopDriver cannot be null if driver is provided.");
-				}
+				//if (mainLoopDriver == null) {
+				//	throw new ArgumentNullException ("InternalInit mainLoopDriver cannot be null if driver is provided.");
+				//}
+				//if (!(driver is FakeDriver)) {
+				//	throw new InvalidOperationException ("InternalInit can only be called with FakeDriver.");
+				//}
 				Driver = driver;
-				Driver.Init (TerminalResized);
-				MainLoop = new MainLoop (mainLoopDriver);
-				SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext (MainLoop));
 			}
 
 			if (Driver == null) {
 				var p = Environment.OSVersion.Platform;
-				if (UseSystemConsole) {
+				if (ForceFakeConsole) {
+					// For Unit Testing only
+					Driver = new FakeDriver ();
+				} else if (UseSystemConsole) {
 					Driver = new NetDriver ();
-					mainLoopDriver = new NetMainLoop (Driver);
 				} else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) {
 					Driver = new WindowsDriver ();
-					mainLoopDriver = new WindowsMainLoop (Driver);
 				} else {
-					mainLoopDriver = new UnixMainLoop ();
 					Driver = new CursesDriver ();
 				}
+				if (Driver == null) {
+					throw new InvalidOperationException ("Init could not determine the ConsoleDriver to use.");
+				}
+			}
+
+			if (mainLoopDriver == null) {
+				// TODO: Move this logic into ConsoleDriver
+				if (Driver is FakeDriver) {
+					mainLoopDriver = new FakeMainLoop (Driver);
+				} else if (Driver is NetDriver) {
+					mainLoopDriver = new NetMainLoop (Driver);
+				} else if (Driver is WindowsDriver) {
+					mainLoopDriver = new WindowsMainLoop (Driver);
+				} else if (Driver is CursesDriver) {
+					mainLoopDriver = new UnixMainLoop (Driver);
+				}
+				if (mainLoopDriver == null) {
+					throw new InvalidOperationException ("Init could not determine the MainLoopDriver to use.");
+				}
+			}
+
+			MainLoop = new MainLoop (mainLoopDriver);
+
+			try {
 				Driver.Init (TerminalResized);
-				MainLoop = new MainLoop (mainLoopDriver);
-				SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext (MainLoop));
+			} catch (InvalidOperationException ex) {
+				// This is a case where the driver is unable to initialize the console.
+				// This can happen if the console is already in use by another process or
+				// if running in unit tests.
+				// In this case, we want to throw a more specific exception.
+				throw new InvalidOperationException ("Unable to initialize the console. This can happen if the console is already in use by another process or in unit tests.", ex);
 			}
+
+			SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext (MainLoop));
+
 			Top = topLevelFactory ();
 			Current = Top;
 			supportedCultures = GetSupportedCultures ();
@@ -375,7 +441,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Captures the execution state for the provided <see cref="Toplevel"/>  view.
+		/// Captures the execution state for the provided <see cref="Toplevel"/> view.
 		/// </summary>
 		public class RunState : IDisposable {
 			/// <summary>
@@ -391,31 +457,61 @@ namespace Terminal.Gui {
 			/// </summary>
 			public Toplevel Toplevel { get; internal set; }
 
+#if DEBUG_IDISPOSABLE
+			/// <summary>
+			/// For debug purposes to verify objects are being disposed properly
+			/// </summary>
+			public bool WasDisposed = false;
+			/// <summary>
+			/// For debug purposes to verify objects are being disposed properly
+			/// </summary>
+			public int DisposedCount = 0;
 			/// <summary>
-			/// Releases alTop = l resource used by the <see cref="Application.RunState"/> object.
+			/// For debug purposes
 			/// </summary>
-			/// <remarks>Call <see cref="Dispose()"/> when you are finished using the <see cref="Application.RunState"/>. The
+			public static List<RunState> Instances = new List<RunState> ();
+			/// <summary>
+			/// For debug purposes
+			/// </summary>
+			public RunState ()
+			{
+				Instances.Add (this);
+			}
+#endif
+
+			/// <summary>
+			/// Releases all resource used by the <see cref="Application.RunState"/> object.
+			/// </summary>
+			/// <remarks>
+			/// Call <see cref="Dispose()"/> when you are finished using the <see cref="Application.RunState"/>. 
+			/// </remarks>
+			/// <remarks>
 			/// <see cref="Dispose()"/> method leaves the <see cref="Application.RunState"/> in an unusable state. After
 			/// calling <see cref="Dispose()"/>, you must release all references to the
 			/// <see cref="Application.RunState"/> so the garbage collector can reclaim the memory that the
-			/// <see cref="Application.RunState"/> was occupying.</remarks>
+			/// <see cref="Application.RunState"/> was occupying.
+			/// </remarks>
 			public void Dispose ()
 			{
 				Dispose (true);
 				GC.SuppressFinalize (this);
+#if DEBUG_IDISPOSABLE
+				WasDisposed = true;
+#endif
 			}
 
 			/// <summary>
-			/// Dispose the specified disposing.
+			/// Releases all resource used by the <see cref="Application.RunState"/> object.
 			/// </summary>
-			/// <returns>The dispose.</returns>
-			/// <param name="disposing">If set to <c>true</c> disposing.</param>
+			/// <param name="disposing">If set to <see langword="true"/> we are disposing and should dispose held objects.</param>
 			protected virtual void Dispose (bool disposing)
 			{
 				if (Toplevel != null && disposing) {
-					End (Toplevel);
-					Toplevel.Dispose ();
-					Toplevel = null;
+					throw new InvalidOperationException ("You must clean up (Dispose) the Toplevel before calling Application.RunState.Dispose");
+					// BUGBUG: It's insidious that we call EndFirstTopLevel here so I moved it to End.
+					//EndFirstTopLevel (Toplevel);
+					//Toplevel.Dispose ();
+					//Toplevel = null;
 				}
 			}
 		}
@@ -670,10 +766,14 @@ namespace Terminal.Gui {
 				me.View = view;
 			}
 			RootMouseEvent?.Invoke (me);
+
+			if (me.Handled) {
+				return;
+			}
+
 			if (mouseGrabView != null) {
 				if (view == null) {
-					UngrabMouse ();
-					return;
+					view = mouseGrabView;
 				}
 
 				var newxy = mouseGrabView.ScreenToView (me.X, me.Y);
@@ -688,7 +788,7 @@ namespace Terminal.Gui {
 				if (OutsideFrame (new Point (nme.X, nme.Y), mouseGrabView.Frame)) {
 					lastMouseOwnerView?.OnMouseLeave (me);
 				}
-				// System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}");
+				//System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}");
 				if (mouseGrabView?.OnMouseEvent (nme) == true) {
 					return;
 				}
@@ -797,8 +897,8 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Building block API: Prepares the provided <see cref="Toplevel"/>  for execution.
 		/// </summary>
-		/// <returns>The runstate handle that needs to be passed to the <see cref="End(RunState)"/> method upon completion.</returns>
-		/// <param name="toplevel">Toplevel to prepare execution for.</param>
+		/// <returns>The <see cref="RunState"/> handle that needs to be passed to the <see cref="End(RunState)"/> method upon completion.</returns>
+		/// <param name="toplevel">The <see cref="Toplevel"/> to prepare execution for.</param>
 		/// <remarks>
 		///  This method prepares the provided toplevel for running with the focus,
 		///  it adds this to the list of toplevels, sets up the mainloop to process the
@@ -811,13 +911,12 @@ namespace Terminal.Gui {
 		{
 			if (toplevel == null) {
 				throw new ArgumentNullException (nameof (toplevel));
-			} else if (toplevel.IsMdiContainer && MdiTop != null) {
+			} else if (toplevel.IsMdiContainer && MdiTop != toplevel && MdiTop != null) {
 				throw new InvalidOperationException ("Only one Mdi Container is allowed.");
 			}
 
 			var rs = new RunState (toplevel);
 
-			Init ();
 			if (toplevel is ISupportInitializeNotification initializableNotification &&
 			    !initializableNotification.IsInitialized) {
 				initializableNotification.BeginInit ();
@@ -828,6 +927,13 @@ namespace Terminal.Gui {
 			}
 
 			lock (toplevels) {
+				// If Top was already initialized with Init, and Begin has never been called
+				// Top was not added to the toplevels Stack. It will thus never get disposed.
+				// Clean it up here:
+				if (Top != null && toplevel != Top && !toplevels.Contains (Top)) {
+					Top.Dispose ();
+					Top = null;
+				}
 				if (string.IsNullOrEmpty (toplevel.Id.ToString ())) {
 					var count = 1;
 					var id = (toplevels.Count + count).ToString ();
@@ -849,7 +955,8 @@ namespace Terminal.Gui {
 					throw new ArgumentException ("There are duplicates toplevels Id's");
 				}
 			}
-			if (toplevel.IsMdiContainer) {
+			// Fix $520 - Set Top = toplevel if Top == null
+			if (Top == null || toplevel.IsMdiContainer) {
 				Top = toplevel;
 			}
 
@@ -888,13 +995,14 @@ namespace Terminal.Gui {
 				Driver.Refresh ();
 			}
 
+			NotifyNewRunState?.Invoke (rs);
 			return rs;
 		}
 
 		/// <summary>
-		/// Building block API: completes the execution of a <see cref="Toplevel"/>  that was started with <see cref="Begin(Toplevel)"/> .
+		/// Building block API: completes the execution of a <see cref="Toplevel"/> that was started with <see cref="Begin(Toplevel)"/> .
 		/// </summary>
-		/// <param name="runState">The runstate returned by the <see cref="Begin(Toplevel)"/> method.</param>
+		/// <param name="runState">The <see cref="RunState"/> returned by the <see cref="Begin(Toplevel)"/> method.</param>
 		public static void End (RunState runState)
 		{
 			if (runState == null)
@@ -905,12 +1013,52 @@ namespace Terminal.Gui {
 			} else {
 				runState.Toplevel.OnUnloaded ();
 			}
+
+			// End the RunState.Toplevel 
+			// First, take it off the toplevel Stack
+			if (toplevels.Count > 0) {
+				if (toplevels.Peek () != runState.Toplevel) {
+					// If there the top of the stack is not the RunState.Toplevel then
+					// this call to End is not balanced with the call to Begin that started the RunState
+					throw new ArgumentException ("End must be balanced with calls to Begin");
+				}
+				toplevels.Pop ();
+			}
+
+			// Notify that it is closing
+			runState.Toplevel?.OnClosed (runState.Toplevel);
+
+			// If there is a MdiTop that is not the RunState.Toplevel then runstate.TopLevel 
+			// is a child of MidTop and we should notify the MdiTop that it is closing
+			if (MdiTop != null && !(runState.Toplevel).Modal && runState.Toplevel != MdiTop) {
+				MdiTop.OnChildClosed (runState.Toplevel);
+			}
+
+			// Set Current and Top to the next TopLevel on the stack
+			if (toplevels.Count == 0) {
+				Current = null;
+			} else {
+				Current = toplevels.Peek ();
+				if (toplevels.Count == 1 && Current == MdiTop) {
+					MdiTop.OnAllChildClosed ();
+				} else {
+					SetCurrentAsTop ();
+				}
+				Refresh ();
+			}
+
+			runState.Toplevel?.Dispose ();
+			runState.Toplevel = null;
 			runState.Dispose ();
 		}
 
 		/// <summary>
-		/// Shutdown an application initialized with <see cref="Init(ConsoleDriver, IMainLoopDriver)"/>
+		/// Shutdown an application initialized with <see cref="Init(ConsoleDriver, IMainLoopDriver)"/>.
 		/// </summary>
+		/// <remarks>
+		/// Shutdown must be called for every call to <see cref="Init(ConsoleDriver, IMainLoopDriver)"/> or <see cref="Application.Run(Toplevel, Func{Exception, bool})"/>
+		/// to ensure all resources are cleaned up (Disposed) and terminal settings are restored.
+		/// </remarks>
 		public static void Shutdown ()
 		{
 			ResetState ();
@@ -925,15 +1073,17 @@ namespace Terminal.Gui {
 			// Shutdown is the bookend for Init. As such it needs to clean up all resources
 			// Init created. Apps that do any threading will need to code defensively for this.
 			// e.g. see Issue #537
-			// TODO: Some of this state is actually related to Begin/End (not Init/Shutdown) and should be moved to `RunState` (#520)
 			foreach (var t in toplevels) {
 				t.Running = false;
 				t.Dispose ();
 			}
 			toplevels.Clear ();
 			Current = null;
+			Top?.Dispose ();
 			Top = null;
 
+			// BUGBUG: MdiTop is not cleared here, but it should be?
+
 			MainLoop = null;
 			Driver?.End ();
 			Driver = null;
@@ -985,40 +1135,17 @@ namespace Terminal.Gui {
 			Driver.Refresh ();
 		}
 
-		internal static void End (View view)
-		{
-			if (toplevels.Peek () != view)
-				throw new ArgumentException ("The view that you end with must be balanced");
-			toplevels.Pop ();
 
-			(view as Toplevel)?.OnClosed ((Toplevel)view);
-
-			if (MdiTop != null && !((Toplevel)view).Modal && view != MdiTop) {
-				MdiTop.OnChildClosed (view as Toplevel);
-			}
-
-			if (toplevels.Count == 0) {
-				Current = null;
-			} else {
-				Current = toplevels.Peek ();
-				if (toplevels.Count == 1 && Current == MdiTop) {
-					MdiTop.OnAllChildClosed ();
-				} else {
-					SetCurrentAsTop ();
-				}
-				Refresh ();
-			}
-		}
 
 		/// <summary>
-		///   Building block API: Runs the main loop for the created dialog
+		///   Building block API: Runs the <see cref="MainLoop"/> for the created <see cref="Toplevel"/>.
 		/// </summary>
 		/// <remarks>
-		///   Use the wait parameter to control whether this is a
-		///   blocking or non-blocking call.
+		///   Use the <paramref name="wait"/> parameter to control whether this is a blocking or non-blocking call.
 		/// </remarks>
-		/// <param name="state">The state returned by the Begin method.</param>
-		/// <param name="wait">By default this is true which will execute the runloop waiting for events, if you pass false, you can use this method to run a single iteration of the events.</param>
+		/// <param name="state">The state returned by the <see cref="Begin(Toplevel)"/> method.</param>
+		/// <param name="wait">By default this is <see langword="true"/> which will execute the runloop waiting for events, 
+		/// if set to <see langword="false"/>, a single iteration will execute.</param>
 		public static void RunLoop (RunState state, bool wait = true)
 		{
 			if (state == null)
@@ -1028,18 +1155,21 @@ namespace Terminal.Gui {
 
 			bool firstIteration = true;
 			for (state.Toplevel.Running = true; state.Toplevel.Running;) {
-				if (ExitRunLoopAfterFirstIteration && !firstIteration)
+				if (ExitRunLoopAfterFirstIteration && !firstIteration) {
 					return;
+				}
 				RunMainLoopIteration (ref state, wait, ref firstIteration);
 			}
 		}
 
 		/// <summary>
-		/// Run one iteration of the MainLoop.
+		/// Run one iteration of the <see cref="MainLoop"/>.
 		/// </summary>
-		/// <param name="state">The state returned by the Begin method.</param>
-		/// <param name="wait">If will execute the runloop waiting for events.</param>
-		/// <param name="firstIteration">If it's the first run loop iteration.</param>
+		/// <param name="state">The state returned by <see cref="Begin(Toplevel)"/>.</param>
+		/// <param name="wait">If <see langword="true"/> will execute the runloop waiting for events. If <see langword="true"/>
+		/// will return after a single iteration.</param>
+		/// <param name="firstIteration">Set to <see langword="true"/> if this is the first run loop iteration. Upon return,
+		/// it will be set to <see langword="false"/> if at least one iteration happened.</param>
 		public static void RunMainLoopIteration (ref RunState state, bool wait, ref bool firstIteration)
 		{
 			if (MainLoop.EventsPending (wait)) {
@@ -1140,30 +1270,57 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Runs the application by calling <see cref="Run(Toplevel, Func{Exception, bool})"/> with the value of <see cref="Top"/>
+		/// Runs the application by calling <see cref="Run(Toplevel, Func{Exception, bool})"/> with the value of <see cref="Top"/>.
 		/// </summary>
+		/// <remarks>
+		/// See <see cref="Run(Toplevel, Func{Exception, bool})"/> for more details.
+		/// </remarks>
 		public static void Run (Func<Exception, bool> errorHandler = null)
 		{
 			Run (Top, errorHandler);
 		}
 
 		/// <summary>
-		/// Runs the application by calling <see cref="Run(Toplevel, Func{Exception, bool})"/> with a new instance of the specified <see cref="Toplevel"/>-derived class
+		/// Runs the application by calling <see cref="Run(Toplevel, Func{Exception, bool})"/> 
+		/// with a new instance of the specified <see cref="Toplevel"/>-derived class.
+		/// <para>
+		/// Calling <see cref="Init(ConsoleDriver, IMainLoopDriver)"/> first is not needed as this function will initialze the application.
+		/// </para>
+		/// <para>
+		/// <see cref="Shutdown"/> must be called when the application is closing (typically after Run> has 
+		/// returned) to ensure resources are cleaned up and terminal settings restored.
+		/// </para>
 		/// </summary>
-		public static void Run<T> (Func<Exception, bool> errorHandler = null) where T : Toplevel, new()
+		/// <remarks>
+		/// See <see cref="Run(Toplevel, Func{Exception, bool})"/> for more details.
+		/// </remarks>
+		/// <param name="errorHandler"></param>
+		/// <param name="driver">The <see cref="ConsoleDriver"/> to use. If not specified the default driver for the
+		/// platform will be used (<see cref="WindowsDriver"/>, <see cref="CursesDriver"/>, or <see cref="NetDriver"/>).
+		/// This parameteter must be <see langword="null"/> if <see cref="Init(ConsoleDriver, IMainLoopDriver)"/> has already been called. 
+		/// </param>
+		/// <param name="mainLoopDriver">Specifies the <see cref="MainLoop"/> to use.</param>
+		public static void Run<T> (Func<Exception, bool> errorHandler = null, ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null) where T : Toplevel, new()
 		{
-			if (_initialized && Driver != null) {
-				var top = new T ();
-				var type = top.GetType ().BaseType;
-				while (type != typeof (Toplevel) && type != typeof (object)) {
-					type = type.BaseType;
-				}
-				if (type != typeof (Toplevel)) {
-					throw new ArgumentException ($"{top.GetType ().Name} must be derived from TopLevel");
+			if (_initialized) {
+				if (Driver != null) {
+					// Init() has been called and we have a driver, so just run the app.
+					var top = new T ();
+					var type = top.GetType ().BaseType;
+					while (type != typeof (Toplevel) && type != typeof (object)) {
+						type = type.BaseType;
+					}
+					if (type != typeof (Toplevel)) {
+						throw new ArgumentException ($"{top.GetType ().Name} must be derived from TopLevel");
+					}
+					Run (top, errorHandler);
+				} else {
+					// This codepath should be impossible because Init(null, null) will select the platform default driver
+					throw new InvalidOperationException ("Init() completed without a driver being set (this should be impossible); Run<T>() cannot be called.");
 				}
-				Run (top, errorHandler);
 			} else {
-				Init (() => new T ());
+				// Init() has NOT been called.
+				InternalInit (() => new T (), driver, mainLoopDriver, calledViaRunT: true);
 				Run (Top, errorHandler);
 			}
 		}
@@ -1187,16 +1344,19 @@ namespace Terminal.Gui {
 		///   <para>
 		///     Alternatively, to have a program control the main loop and 
 		///     process events manually, call <see cref="Begin(Toplevel)"/> to set things up manually and then
-		///     repeatedly call <see cref="RunLoop(RunState, bool)"/> with the wait parameter set to false.   By doing this
+		///     repeatedly call <see cref="RunLoop(RunState, bool)"/> with the wait parameter set to false. By doing this
 		///     the <see cref="RunLoop(RunState, bool)"/> method will only process any pending events, timers, idle handlers and
 		///     then return control immediately.
 		///   </para>
 		///   <para>
-		///     When <paramref name="errorHandler"/> is null the exception is rethrown, when it returns true the application is resumed and when false method exits gracefully.
+		///     RELEASE builds only: When <paramref name="errorHandler"/> is <see langword="null"/> any exeptions will be rethrown.  
+		///     Otheriwse, if <paramref name="errorHandler"/> will be called. If <paramref name="errorHandler"/> 
+		///     returns <see langword="true"/> the <see cref="RunLoop(RunState, bool)"/> will resume; otherwise 
+		///     this method will exit.
 		///   </para>
 		/// </remarks>
 		/// <param name="view">The <see cref="Toplevel"/> to run modally.</param>
-		/// <param name="errorHandler">Handler for any unhandled exceptions (resumes when returns true, rethrows when null).</param>
+		/// <param name="errorHandler">RELEASE builds only: Handler for any unhandled exceptions (resumes when returns true, rethrows when null).</param>
 		public static void Run (Toplevel view, Func<Exception, bool> errorHandler = null)
 		{
 			var resume = true;
@@ -1206,13 +1366,12 @@ namespace Terminal.Gui {
 #endif
 				resume = false;
 				var runToken = Begin (view);
+				// If ExitRunLoopAfterFirstIteration is true then the user must dispose of the runToken
+				// by using NotifyStopRunState event.
 				RunLoop (runToken);
-				if (!ExitRunLoopAfterFirstIteration)
+				if (!ExitRunLoopAfterFirstIteration) {
 					End (runToken);
-				else
-					// If ExitRunLoopAfterFirstIteration is true then the user must deal his disposing when it ends
-					// by using NotifyStopRunState event.
-					NotifyNewRunState?.Invoke (runToken);
+				}
 #if !DEBUG
 				}
 				catch (Exception error)
@@ -1305,8 +1464,9 @@ namespace Terminal.Gui {
 
 		static void OnNotifyStopRunState (Toplevel top)
 		{
-			if (ExitRunLoopAfterFirstIteration)
+			if (ExitRunLoopAfterFirstIteration) {
 				NotifyStopRunState?.Invoke (top);
+			}
 		}
 
 		/// <summary>

+ 38 - 11
Terminal.Gui/Core/Clipboard/Clipboard.cs

@@ -3,13 +3,32 @@ using System;
 
 namespace Terminal.Gui {
 	/// <summary>
-	/// Provides cut, copy, and paste support for the clipboard with OS interaction.
+	/// Provides cut, copy, and paste support for the OS clipboard.
 	/// </summary>
+	/// <remarks>
+	/// <para>
+	/// On Windows, the <see cref="Clipboard"/> class uses the Windows Clipboard APIs via P/Invoke.
+	/// </para>
+	/// <para>
+	/// On Linux, when not running under Windows Subsystem for Linux (WSL),
+	/// the <see cref="Clipboard"/> class uses the xclip command line tool. If xclip is not installed,
+	/// the clipboard will not work.
+	/// </para>
+	/// <para>
+	/// On Linux, when running under Windows Subsystem for Linux (WSL),
+	/// the <see cref="Clipboard"/> class launches Windows' powershell.exe via WSL interop and uses the
+	/// "Set-Clipboard" and "Get-Clipboard" Powershell CmdLets. 
+	/// </para>
+	/// <para>
+	/// On the Mac, the <see cref="Clipboard"/> class uses the MacO OS X pbcopy and pbpaste command line tools
+	/// and the Mac clipboard APIs vai P/Invoke.
+	/// </para>
+	/// </remarks>
 	public static class Clipboard {
 		static ustring contents;
 
 		/// <summary>
-		/// Get or sets the operation system clipboard, otherwise the contents field.
+		/// Gets (copies from) or sets (pastes to) the contents of the OS clipboard.
 		/// </summary>
 		public static ustring Contents {
 			get {
@@ -25,10 +44,15 @@ namespace Terminal.Gui {
 			}
 			set {
 				try {
-					if (IsSupported && value != null) {
+					if (IsSupported) {
+						if (value == null) {
+							value = string.Empty;
+						}
 						Application.Driver.Clipboard.SetClipboardData (value.ToString ());
 					}
 					contents = value;
+				} catch (NotSupportedException e) {
+					throw e;
 				} catch (Exception) {
 					contents = value;
 				}
@@ -38,32 +62,35 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Returns true if the environmental dependencies are in place to interact with the OS clipboard.
 		/// </summary>
+		/// <remarks>
+		/// </remarks>
 		public static bool IsSupported { get => Application.Driver.Clipboard.IsSupported; }
 
 		/// <summary>
-		/// Gets the operation system clipboard if possible.
+		/// Copies the contents of the OS clipboard to <paramref name="result"/> if possible.
 		/// </summary>
-		/// <param name="result">Clipboard contents read</param>
-		/// <returns>true if it was possible to read the OS clipboard.</returns>
+		/// <param name="result">The contents of the OS clipboard if successful, <see cref="string.Empty"/> if not.</param>
+		/// <returns><see langword="true"/> the OS clipboard was retrieved, <see langword="false"/> otherwise.</returns>
 		public static bool TryGetClipboardData (out string result)
 		{
-			if (Application.Driver.Clipboard.TryGetClipboardData (out result)) {
+			if (IsSupported && Application.Driver.Clipboard.TryGetClipboardData (out result)) {
 				if (contents != result) {
 					contents = result;
 				}
 				return true;
 			}
+			result = string.Empty;
 			return false;
 		}
 
 		/// <summary>
-		/// Sets the operation system clipboard if possible.
+		/// Pastes the <paramref name="text"/> to the OS clipboard if possible.
 		/// </summary>
-		/// <param name="text"></param>
-		/// <returns>True if the clipboard content was set successfully.</returns>
+		/// <param name="text">The text to paste to the OS clipboard.</param>
+		/// <returns><see langword="true"/> the OS clipboard was set, <see langword="false"/> otherwise.</returns>
 		public static bool TrySetClipboardData (string text)
 		{
-			if (Application.Driver.Clipboard.TrySetClipboardData (text)) {
+			if (IsSupported && Application.Driver.Clipboard.TrySetClipboardData (text)) {
 				contents = text;
 				return true;
 			}

+ 26 - 20
Terminal.Gui/Core/Clipboard/ClipboardBase.cs

@@ -15,48 +15,52 @@ namespace Terminal.Gui {
 		public abstract bool IsSupported { get; }
 
 		/// <summary>
-		/// Get the operation system clipboard.
+		/// Returns the contents of the OS clipboard if possible.
 		/// </summary>
-		/// <exception cref="NotSupportedException">Thrown if it was not possible to read the clipboard contents</exception>
+		/// <returns>The contents of the OS clipboard if successful.</returns>
+		/// <exception cref="NotSupportedException">Thrown if it was not possible to copy from the OS clipboard.</exception>
 		public string GetClipboardData ()
 		{
 			try {
 				return GetClipboardDataImpl ();
-			} catch (Exception ex) {
-				throw new NotSupportedException ("Failed to read clipboard.", ex);
+			} catch (NotSupportedException ex) {
+				throw new NotSupportedException ("Failed to copy from the OS clipboard.", ex);
 			}
 		}
 
 		/// <summary>
-		/// Get the operation system clipboard.
+		/// Returns the contents of the OS clipboard if possible. Implemented by <see cref="ConsoleDriver"/>-specific subclasses.
 		/// </summary>
+		/// <returns>The contents of the OS clipboard if successful.</returns>
+		/// <exception cref="NotSupportedException">Thrown if it was not possible to copy from the OS clipboard.</exception>
 		protected abstract string GetClipboardDataImpl ();
 
 		/// <summary>
-		/// Sets the operation system clipboard.
+		/// Pastes the <paramref name="text"/> to the OS clipboard if possible.
 		/// </summary>
-		/// <param name="text"></param>
-		/// <exception cref="NotSupportedException">Thrown if it was not possible to set the clipboard contents</exception>
+		/// <param name="text">The text to paste to the OS clipboard.</param>
+		/// <exception cref="NotSupportedException">Thrown if it was not possible to paste to the OS clipboard.</exception>
 		public void SetClipboardData (string text)
 		{
 			try {
 				SetClipboardDataImpl (text);
-			} catch (Exception ex) {
-				throw new NotSupportedException ("Failed to write to clipboard.", ex);
+			} catch (NotSupportedException ex) {
+				throw new NotSupportedException ("Failed to paste to the OS clipboard.", ex);
 			}
 		}
 
 		/// <summary>
-		/// Sets the operation system clipboard.
+		/// Pastes the <paramref name="text"/> to the OS clipboard if possible. Implemented by <see cref="ConsoleDriver"/>-specific subclasses.
 		/// </summary>
-		/// <param name="text"></param>
+		/// <param name="text">The text to paste to the OS clipboard.</param>
+		/// <exception cref="NotSupportedException">Thrown if it was not possible to paste to the OS clipboard.</exception>
 		protected abstract void SetClipboardDataImpl (string text);
 
 		/// <summary>
-		/// Gets the operation system clipboard if possible.
+		/// Copies the contents of the OS clipboard to <paramref name="result"/> if possible.
 		/// </summary>
-		/// <param name="result">Clipboard contents read</param>
-		/// <returns>true if it was possible to read the OS clipboard.</returns>
+		/// <param name="result">The contents of the OS clipboard if successful, <see cref="string.Empty"/> if not.</param>
+		/// <returns><see langword="true"/> the OS clipboard was retrieved, <see langword="false"/> otherwise.</returns>
 		public bool TryGetClipboardData (out string result)
 		{
 			// Don't even try to read because environment is not set up.
@@ -71,17 +75,18 @@ namespace Terminal.Gui {
 					result = GetClipboardDataImpl ();
 				}
 				return true;
-			} catch (Exception) {
+			} catch (NotSupportedException ex) {
+				System.Diagnostics.Debug.WriteLine ($"TryGetClipboardData: {ex.Message}");
 				result = null;
 				return false;
 			}
 		}
 
 		/// <summary>
-		/// Sets the operation system clipboard if possible.
+		/// Pastes the <paramref name="text"/> to the OS clipboard if possible.
 		/// </summary>
-		/// <param name="text"></param>
-		/// <returns>True if the clipboard content was set successfully</returns>
+		/// <param name="text">The text to paste to the OS clipboard.</param>
+		/// <returns><see langword="true"/> the OS clipboard was set, <see langword="false"/> otherwise.</returns>
 		public bool TrySetClipboardData (string text)
 		{
 			// Don't even try to set because environment is not set up
@@ -92,7 +97,8 @@ namespace Terminal.Gui {
 			try {
 				SetClipboardDataImpl (text);
 				return true;
-			} catch (Exception) {
+			} catch (NotSupportedException ex) {
+				System.Diagnostics.Debug.WriteLine ($"TrySetClipboardData: {ex.Message}");
 				return false;
 			}
 		}

+ 244 - 0
Terminal.Gui/Core/CollectionNavigator.cs

@@ -0,0 +1,244 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Terminal.Gui {
+	/// <summary>
+	/// Navigates a collection of items using keystrokes. The keystrokes are used to build a search string. 
+	/// The <see cref="SearchString"/> is used to find the next item in the collection that matches the search string
+	/// when <see cref="GetNextMatchingItem(int, char)"/> is called.
+	/// <para>
+	/// If the user types keystrokes that can't be found in the collection, 
+	/// the search string is cleared and the next item is found that starts with the last keystroke.
+	/// </para>
+	/// <para>
+	/// If the user pauses keystrokes for a short time (see <see cref="TypingDelay"/>), the search string is cleared.
+	/// </para>
+	/// </summary>
+	public class CollectionNavigator {
+		/// <summary>
+		/// Constructs a new CollectionNavigator.
+		/// </summary>
+		public CollectionNavigator () { }
+
+		/// <summary>
+		/// Constructs a new CollectionNavigator for the given collection.
+		/// </summary>
+		/// <param name="collection"></param>
+		public CollectionNavigator (IEnumerable<object> collection) => Collection = collection;
+
+		DateTime lastKeystroke = DateTime.Now;
+		/// <summary>
+		/// Gets or sets the number of milliseconds to delay before clearing the search string. The delay is
+		/// reset on each call to <see cref="GetNextMatchingItem(int, char)"/>. The default is 500ms.
+		/// </summary>
+		public int TypingDelay { get; set; } = 500;
+
+		/// <summary>
+		/// The compararer function to use when searching the collection.
+		/// </summary>
+		public StringComparer Comparer { get; set; } = StringComparer.InvariantCultureIgnoreCase;
+
+		/// <summary>
+		/// The collection of objects to search. <see cref="object.ToString()"/> is used to search the collection.
+		/// </summary>
+		public IEnumerable<object> Collection { get; set; }
+
+		/// <summary>
+		/// Event arguments for the <see cref="CollectionNavigator.SearchStringChanged"/> event.
+		/// </summary>
+		public class KeystrokeNavigatorEventArgs {
+			/// <summary>
+			/// he current <see cref="SearchString"/>.
+			/// </summary>
+			public string SearchString { get; }
+
+			/// <summary>
+			/// Initializes a new instance of <see cref="KeystrokeNavigatorEventArgs"/>
+			/// </summary>
+			/// <param name="searchString">The current <see cref="SearchString"/>.</param>
+			public KeystrokeNavigatorEventArgs (string searchString)
+			{
+				SearchString = searchString;
+			}
+		}
+
+		/// <summary>
+		/// This event is invoked when <see cref="SearchString"/>  changes. Useful for debugging.
+		/// </summary>
+		public event Action<KeystrokeNavigatorEventArgs> SearchStringChanged;
+
+		private string _searchString = "";
+		/// <summary>
+		/// Gets the current search string. This includes the set of keystrokes that have been pressed
+		/// since the last unsuccessful match or after <see cref="TypingDelay"/>) milliseconds. Useful for debugging.
+		/// </summary>
+		public string SearchString {
+			get => _searchString;
+			private set {
+				_searchString = value;
+				OnSearchStringChanged (new KeystrokeNavigatorEventArgs (value));
+			}
+		}
+
+		/// <summary>
+		/// Invoked when the <see cref="SearchString"/> changes. Useful for debugging. Invokes the <see cref="SearchStringChanged"/> event.
+		/// </summary>
+		/// <param name="e"></param>
+		public virtual void OnSearchStringChanged (KeystrokeNavigatorEventArgs e)
+		{
+			SearchStringChanged?.Invoke (e);
+		}
+
+		/// <summary>
+		/// Gets the index of the next item in the collection that matches the current <see cref="SearchString"/> plus the provided character (typically
+		/// from a key press).
+		/// </summary>
+		/// <param name="currentIndex">The index in the collection to start the search from.</param>
+		/// <param name="keyStruck">The character of the key the user pressed.</param>
+		/// <returns>The index of the item that matches what the user has typed. 
+		/// Returns <see langword="-1"/> if no item in the collection matched.</returns>
+		public int GetNextMatchingItem (int currentIndex, char keyStruck)
+		{
+			AssertCollectionIsNotNull ();
+			if (!char.IsControl (keyStruck)) {
+
+				// maybe user pressed 'd' and now presses 'd' again.
+				// a candidate search is things that begin with "dd"
+				// but if we find none then we must fallback on cycling
+				// d instead and discard the candidate state
+				string candidateState = "";
+
+				// is it a second or third (etc) keystroke within a short time
+				if (SearchString.Length > 0 && DateTime.Now - lastKeystroke < TimeSpan.FromMilliseconds (TypingDelay)) {
+					// "dd" is a candidate
+					candidateState = SearchString + keyStruck;
+				} else {
+					// its a fresh keystroke after some time
+					// or its first ever key press
+					SearchString = new string (keyStruck, 1);
+				}
+
+				var idxCandidate = GetNextMatchingItem (currentIndex, candidateState,
+					// prefer not to move if there are multiple characters e.g. "ca" + 'r' should stay on "car" and not jump to "cart"
+					candidateState.Length > 1);
+
+				if (idxCandidate != -1) {
+					// found "dd" so candidate searchstring is accepted
+					lastKeystroke = DateTime.Now;
+					SearchString = candidateState;
+					return idxCandidate;
+				}
+
+				//// nothing matches "dd" so discard it as a candidate
+				//// and just cycle "d" instead
+				lastKeystroke = DateTime.Now;
+				idxCandidate = GetNextMatchingItem (currentIndex, candidateState);
+
+				// if a match wasn't found, the user typed a 'wrong' key in their search ("can" + 'z'
+				// instead of "can" + 'd').
+				if (SearchString.Length > 1 && idxCandidate == -1) {
+					// ignore it since we're still within the typing delay
+					// don't add it to SearchString either
+					return currentIndex;
+				}
+
+				// if no changes to current state manifested
+				if (idxCandidate == currentIndex || idxCandidate == -1) {
+					// clear history and treat as a fresh letter
+					ClearSearchString ();
+					
+					// match on the fresh letter alone
+					SearchString = new string (keyStruck, 1);
+					idxCandidate = GetNextMatchingItem (currentIndex, SearchString);
+					return idxCandidate == -1 ? currentIndex : idxCandidate;
+				}
+
+				// Found another "d" or just leave index as it was
+				return idxCandidate;
+
+			} else {
+				// clear state because keypress was a control char
+				ClearSearchString ();
+
+				// control char indicates no selection
+				return -1;
+			}
+		}
+
+		/// <summary>
+		/// Gets the index of the next item in the collection that matches <paramref name="search"/>. 
+		/// </summary>
+		/// <param name="currentIndex">The index in the collection to start the search from.</param>
+		/// <param name="search">The search string to use.</param>
+		/// <param name="minimizeMovement">Set to <see langword="true"/> to stop the search on the first match
+		/// if there are multiple matches for <paramref name="search"/>.
+		/// e.g. "ca" + 'r' should stay on "car" and not jump to "cart". If <see langword="false"/> (the default), 
+		/// the next matching item will be returned, even if it is above in the collection.
+		/// </param>
+		/// <returns>The index of the next matching item or <see langword="-1"/> if no match was found.</returns>
+		internal int GetNextMatchingItem (int currentIndex, string search, bool minimizeMovement = false)
+		{
+			if (string.IsNullOrEmpty (search)) {
+				return -1;
+			}
+			AssertCollectionIsNotNull ();
+
+			// find indexes of items that start with the search text
+			int [] matchingIndexes = Collection.Select ((item, idx) => (item, idx))
+				  .Where (k => k.item?.ToString ().StartsWith (search, StringComparison.InvariantCultureIgnoreCase) ?? false)
+				  .Select (k => k.idx)
+				  .ToArray ();
+
+			// if there are items beginning with search
+			if (matchingIndexes.Length > 0) {
+				// is one of them currently selected?
+				var currentlySelected = Array.IndexOf (matchingIndexes, currentIndex);
+
+				if (currentlySelected == -1) {
+					// we are not currently selecting any item beginning with the search
+					// so jump to first item in list that begins with the letter
+					return matchingIndexes [0];
+				} else {
+
+					// the current index is part of the matching collection
+					if (minimizeMovement) {
+						// if we would rather not jump around (e.g. user is typing lots of text to get this match)
+						return matchingIndexes [currentlySelected];
+					}
+
+					// cycle to next (circular)
+					return matchingIndexes [(currentlySelected + 1) % matchingIndexes.Length];
+				}
+			}
+
+			// nothing starts with the search
+			return -1;
+		}
+
+		private void AssertCollectionIsNotNull ()
+		{
+			if (Collection == null) {
+				throw new InvalidOperationException ("Collection is null");
+			}
+		}
+
+		private void ClearSearchString ()
+		{
+			SearchString = "";
+			lastKeystroke = DateTime.Now;
+		}
+
+		/// <summary>
+		/// Returns true if <paramref name="kb"/> is a searchable key
+		/// (e.g. letters, numbers etc) that is valid to pass to to this
+		/// class for search filtering.
+		/// </summary>
+		/// <param name="kb"></param>
+		/// <returns></returns>
+		public static bool IsCompatibleKey (KeyEvent kb)
+		{
+			return !kb.IsAlt && !kb.IsCapslock && !kb.IsCtrl && !kb.IsScrolllock && !kb.IsNumlock;
+		}
+	}
+}

+ 41 - 41
Terminal.Gui/Core/Command.cs

@@ -10,54 +10,54 @@ namespace Terminal.Gui {
 	public enum Command {
 
 		/// <summary>
-		/// Moves the caret down one line.
+		/// Moves down one item (cell, line, etc...).
 		/// </summary>
 		LineDown,
 
 		/// <summary>
-		/// Extends the selection down one line.
+		/// Extends the selection down one (cell, line, etc...).
 		/// </summary>
 		LineDownExtend,
 
 		/// <summary>
-		/// Moves the caret down to the last child node of the branch that holds the current selection
+		/// Moves down to the last child node of the branch that holds the current selection.
 		/// </summary>
 		LineDownToLastBranch,
 
 		/// <summary>
-		/// Scrolls down one line (without changing the selection).
+		/// Scrolls down one (cell, line, etc...) (without changing the selection).
 		/// </summary>
 		ScrollDown,
 
 		// --------------------------------------------------------------------
 
 		/// <summary>
-		/// Moves the caret up one line.
+		/// Moves up one (cell, line, etc...).
 		/// </summary>
 		LineUp,
 
 		/// <summary>
-		/// Extends the selection up one line.
+		/// Extends the selection up one item (cell, line, etc...).
 		/// </summary>
 		LineUpExtend,
 
 		/// <summary>
-		/// Moves the caret up to the first child node of the branch that holds the current selection
+		/// Moves up to the first child node of the branch that holds the current selection.
 		/// </summary>
 		LineUpToFirstBranch,
 
 		/// <summary>
-		/// Scrolls up one line (without changing the selection).
+		/// Scrolls up one item (cell, line, etc...) (without changing the selection).
 		/// </summary>
 		ScrollUp,
 
 		/// <summary>
-		/// Moves the selection left one by the minimum increment supported by the view e.g. single character, cell, item etc.
+		/// Moves the selection left one by the minimum increment supported by the <see cref="View"/> e.g. single character, cell, item etc.
 		/// </summary>
 		Left,
 
 		/// <summary>
-		/// Scrolls one character to the left
+		/// Scrolls one item (cell, character, etc...) to the left
 		/// </summary>
 		ScrollLeft,
 
@@ -72,7 +72,7 @@ namespace Terminal.Gui {
 		Right,
 
 		/// <summary>
-		/// Scrolls one character to the right.
+		/// Scrolls one item (cell, character, etc...) to the right.
 		/// </summary>
 		ScrollRight,
 
@@ -102,12 +102,12 @@ namespace Terminal.Gui {
 		WordRightExtend,
 
 		/// <summary>
-		/// Deletes and copies to the clipboard the characters from the current position to the end of the line.
+		/// Cuts to the clipboard the characters from the current position to the end of the line.
 		/// </summary>
 		CutToEndLine,
 
 		/// <summary>
-		/// Deletes and copies to the clipboard the characters from the current position to the start of the line.
+		/// Cuts to the clipboard the characters from the current position to the start of the line.
 		/// </summary>
 		CutToStartLine,
 
@@ -140,47 +140,47 @@ namespace Terminal.Gui {
 		DisableOverwrite,
 
 		/// <summary>
-		/// Move the page down.
+		/// Move one page down.
 		/// </summary>
 		PageDown,
 
 		/// <summary>
-		/// Move the page down increase selection area to cover revealed objects/characters.
+		/// Move one page page extending the selection to cover revealed objects/characters.
 		/// </summary>
 		PageDownExtend,
 
 		/// <summary>
-		/// Move the page up.
+		/// Move one page up.
 		/// </summary>
 		PageUp,
 
 		/// <summary>
-		/// Move the page up increase selection area to cover revealed objects/characters.
+		/// Move one page up extending the selection to cover revealed objects/characters.
 		/// </summary>
 		PageUpExtend,
 
 		/// <summary>
-		/// Moves to top begin.
+		/// Moves to the top/home.
 		/// </summary>
 		TopHome,
 
 		/// <summary>
-		/// Extends the selection to the top begin.
+		/// Extends the selection to the top/home.
 		/// </summary>
 		TopHomeExtend,
 
 		/// <summary>
-		/// Moves to bottom end.
+		/// Moves to the bottom/end.
 		/// </summary>
 		BottomEnd,
 
 		/// <summary>
-		/// Extends the selection to the bottom end.
+		/// Extends the selection to the bottom/end.
 		/// </summary>
 		BottomEndExtend,
 
 		/// <summary>
-		/// Open selected item.
+		/// Open the selected item.
 		/// </summary>
 		OpenSelectedItem,
 
@@ -190,43 +190,43 @@ namespace Terminal.Gui {
 		ToggleChecked,
 
 		/// <summary>
-		/// Accepts the current state (e.g. selection, button press etc)
+		/// Accepts the current state (e.g. selection, button press etc).
 		/// </summary>
 		Accept,
 
 		/// <summary>
-		/// Toggles the Expanded or collapsed state of a a list or item (with subitems)
+		/// Toggles the Expanded or collapsed state of a a list or item (with subitems).
 		/// </summary>
 		ToggleExpandCollapse,
 
 		/// <summary>
-		/// Expands a list or item (with subitems)
+		/// Expands a list or item (with subitems).
 		/// </summary>
 		Expand,
 
 		/// <summary>
-		/// Recursively Expands all child items and their child items (if any)
+		/// Recursively Expands all child items and their child items (if any).
 		/// </summary>
 		ExpandAll,
 
 		/// <summary>
-		/// Collapses a list or item (with subitems)
+		/// Collapses a list or item (with subitems).
 		/// </summary>
 		Collapse,
 
 		/// <summary>
-		/// Recursively collapses a list items of their children (if any)
+		/// Recursively collapses a list items of their children (if any).
 		/// </summary>
 		CollapseAll,
 
 		/// <summary>
-		/// Cancels any current temporary states on the control e.g. expanding
-		/// a combo list
+		/// Cancels an action or any temporary states on the control e.g. expanding
+		/// a combo list.
 		/// </summary>
 		Cancel,
 
 		/// <summary>
-		/// Unix emulation
+		/// Unix emulation.
 		/// </summary>
 		UnixEmulation,
 
@@ -241,12 +241,12 @@ namespace Terminal.Gui {
 		DeleteCharLeft,
 
 		/// <summary>
-		/// Selects all objects in the control.
+		/// Selects all objects.
 		/// </summary>
 		SelectAll,
 
 		/// <summary>
-		/// Deletes all objects in the control.
+		/// Deletes all objects.
 		/// </summary>
 		DeleteAll,
 
@@ -336,7 +336,7 @@ namespace Terminal.Gui {
 		Paste,
 
 		/// <summary>
-		/// Quit a toplevel.
+		/// Quit a <see cref="Toplevel"/>.
 		/// </summary>
 		QuitToplevel,
 
@@ -356,37 +356,37 @@ namespace Terminal.Gui {
 		PreviousView,
 
 		/// <summary>
-		/// Moves focus to the next view or toplevel (case of Mdi).
+		/// Moves focus to the next view or toplevel (case of MDI).
 		/// </summary>
 		NextViewOrTop,
 
 		/// <summary>
-		/// Moves focus to the next previous or toplevel (case of Mdi).
+		/// Moves focus to the next previous or toplevel (case of MDI).
 		/// </summary>
 		PreviousViewOrTop,
 
 		/// <summary>
-		/// Refresh the application.
+		/// Refresh.
 		/// </summary>
 		Refresh,
 
 		/// <summary>
-		/// Toggles the extended selection.
+		/// Toggles the selection.
 		/// </summary>
 		ToggleExtend,
 
 		/// <summary>
-		/// Inserts a new line.
+		/// Inserts a new item.
 		/// </summary>
 		NewLine,
 
 		/// <summary>
-		/// Inserts a tab.
+		/// Tabs to the next item.
 		/// </summary>
 		Tab,
 
 		/// <summary>
-		/// Inserts a shift tab.
+		/// Tabs back to the previous item.
 		/// </summary>
 		BackTab
 	}

+ 87 - 145
Terminal.Gui/Core/ConsoleDriver.cs

@@ -8,8 +8,11 @@
 using NStack;
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Linq;
 using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using Unix.Terminal;
 
 namespace Terminal.Gui {
 	/// <summary>
@@ -209,153 +212,27 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// The default color for text, when the view is not focused.
 		/// </summary>
-		public Attribute Normal { get { return _normal; } set { _normal = SetAttribute (value); } }
+		public Attribute Normal { get { return _normal; } set { _normal = value; } }
 
 		/// <summary>
 		/// The color for text when the view has the focus.
 		/// </summary>
-		public Attribute Focus { get { return _focus; } set { _focus = SetAttribute (value); } }
+		public Attribute Focus { get { return _focus; } set { _focus = value; } }
 
 		/// <summary>
 		/// The color for the hotkey when a view is not focused
 		/// </summary>
-		public Attribute HotNormal { get { return _hotNormal; } set { _hotNormal = SetAttribute (value); } }
+		public Attribute HotNormal { get { return _hotNormal; } set { _hotNormal = value; } }
 
 		/// <summary>
 		/// The color for the hotkey when the view is focused.
 		/// </summary>
-		public Attribute HotFocus { get { return _hotFocus; } set { _hotFocus = SetAttribute (value); } }
+		public Attribute HotFocus { get { return _hotFocus; } set { _hotFocus = value; } }
 
 		/// <summary>
 		/// The default color for text, when the view is disabled.
 		/// </summary>
-		public Attribute Disabled { get { return _disabled; } set { _disabled = SetAttribute (value); } }
-
-		bool preparingScheme = false;
-
-		Attribute SetAttribute (Attribute attribute, [CallerMemberName] string callerMemberName = null)
-		{
-			if (!Application._initialized && !preparingScheme)
-				return attribute;
-
-			if (preparingScheme)
-				return attribute;
-
-			preparingScheme = true;
-			switch (caller) {
-			case "TopLevel":
-				switch (callerMemberName) {
-				case "Normal":
-					HotNormal = Application.Driver.MakeAttribute (HotNormal.Foreground, attribute.Background);
-					break;
-				case "Focus":
-					HotFocus = Application.Driver.MakeAttribute (HotFocus.Foreground, attribute.Background);
-					break;
-				case "HotNormal":
-					HotFocus = Application.Driver.MakeAttribute (attribute.Foreground, HotFocus.Background);
-					break;
-				case "HotFocus":
-					HotNormal = Application.Driver.MakeAttribute (attribute.Foreground, HotNormal.Background);
-					if (Focus.Foreground != attribute.Background)
-						Focus = Application.Driver.MakeAttribute (Focus.Foreground, attribute.Background);
-					break;
-				}
-				break;
-
-			case "Base":
-				switch (callerMemberName) {
-				case "Normal":
-					HotNormal = Application.Driver.MakeAttribute (HotNormal.Foreground, attribute.Background);
-					break;
-				case "Focus":
-					HotFocus = Application.Driver.MakeAttribute (HotFocus.Foreground, attribute.Background);
-					break;
-				case "HotNormal":
-					HotFocus = Application.Driver.MakeAttribute (attribute.Foreground, HotFocus.Background);
-					Normal = Application.Driver.MakeAttribute (Normal.Foreground, attribute.Background);
-					break;
-				case "HotFocus":
-					HotNormal = Application.Driver.MakeAttribute (attribute.Foreground, HotNormal.Background);
-					if (Focus.Foreground != attribute.Background)
-						Focus = Application.Driver.MakeAttribute (Focus.Foreground, attribute.Background);
-					break;
-				}
-				break;
-
-			case "Menu":
-				switch (callerMemberName) {
-				case "Normal":
-					if (Focus.Background != attribute.Background)
-						Focus = Application.Driver.MakeAttribute (attribute.Foreground, Focus.Background);
-					HotNormal = Application.Driver.MakeAttribute (HotNormal.Foreground, attribute.Background);
-					Disabled = Application.Driver.MakeAttribute (Disabled.Foreground, attribute.Background);
-					break;
-				case "Focus":
-					Normal = Application.Driver.MakeAttribute (attribute.Foreground, Normal.Background);
-					HotFocus = Application.Driver.MakeAttribute (HotFocus.Foreground, attribute.Background);
-					break;
-				case "HotNormal":
-					if (Focus.Background != attribute.Background)
-						HotFocus = Application.Driver.MakeAttribute (attribute.Foreground, HotFocus.Background);
-					Normal = Application.Driver.MakeAttribute (Normal.Foreground, attribute.Background);
-					Disabled = Application.Driver.MakeAttribute (Disabled.Foreground, attribute.Background);
-					break;
-				case "HotFocus":
-					HotNormal = Application.Driver.MakeAttribute (attribute.Foreground, HotNormal.Background);
-					if (Focus.Foreground != attribute.Background)
-						Focus = Application.Driver.MakeAttribute (Focus.Foreground, attribute.Background);
-					break;
-				case "Disabled":
-					if (Focus.Background != attribute.Background)
-						HotFocus = Application.Driver.MakeAttribute (attribute.Foreground, HotFocus.Background);
-					Normal = Application.Driver.MakeAttribute (Normal.Foreground, attribute.Background);
-					HotNormal = Application.Driver.MakeAttribute (HotNormal.Foreground, attribute.Background);
-					break;
-				}
-				break;
-
-			case "Dialog":
-				switch (callerMemberName) {
-				case "Normal":
-					if (Focus.Background != attribute.Background)
-						Focus = Application.Driver.MakeAttribute (attribute.Foreground, Focus.Background);
-					HotNormal = Application.Driver.MakeAttribute (HotNormal.Foreground, attribute.Background);
-					break;
-				case "Focus":
-					Normal = Application.Driver.MakeAttribute (attribute.Foreground, Normal.Background);
-					HotFocus = Application.Driver.MakeAttribute (HotFocus.Foreground, attribute.Background);
-					break;
-				case "HotNormal":
-					if (Focus.Background != attribute.Background)
-						HotFocus = Application.Driver.MakeAttribute (attribute.Foreground, HotFocus.Background);
-					if (Normal.Foreground != attribute.Background)
-						Normal = Application.Driver.MakeAttribute (Normal.Foreground, attribute.Background);
-					break;
-				case "HotFocus":
-					HotNormal = Application.Driver.MakeAttribute (attribute.Foreground, HotNormal.Background);
-					if (Focus.Foreground != attribute.Background)
-						Focus = Application.Driver.MakeAttribute (Focus.Foreground, attribute.Background);
-					break;
-				}
-				break;
-
-			case "Error":
-				switch (callerMemberName) {
-				case "Normal":
-					HotNormal = Application.Driver.MakeAttribute (HotNormal.Foreground, attribute.Background);
-					HotFocus = Application.Driver.MakeAttribute (HotFocus.Foreground, attribute.Background);
-					break;
-				case "HotNormal":
-				case "HotFocus":
-					HotFocus = Application.Driver.MakeAttribute (attribute.Foreground, attribute.Background);
-					Normal = Application.Driver.MakeAttribute (Normal.Foreground, attribute.Background);
-					break;
-				}
-				break;
-			}
-			preparingScheme = false;
-			return attribute;
-		}
+		public Attribute Disabled { get { return _disabled; } set { _disabled = value; } }
 
 		/// <summary>
 		/// Compares two <see cref="ColorScheme"/> objects for equality.
@@ -681,11 +558,13 @@ namespace Terminal.Gui {
 		/// <param name="col">Column to move the cursor to.</param>
 		/// <param name="row">Row to move the cursor to.</param>
 		public abstract void Move (int col, int row);
+
 		/// <summary>
-		/// Adds the specified rune to the display at the current cursor position
+		/// Adds the specified rune to the display at the current cursor position.
 		/// </summary>
 		/// <param name="rune">Rune to add.</param>
 		public abstract void AddRune (Rune rune);
+
 		/// <summary>
 		/// Ensures a Rune is not a control character and can be displayed by translating characters below 0x20
 		/// to equivalent, printable, Unicode chars.
@@ -694,21 +573,13 @@ namespace Terminal.Gui {
 		/// <returns></returns>
 		public static Rune MakePrintable (Rune c)
 		{
-			var controlChars = gethexaformat (c, 4);
-			if (controlChars <= 0x1F || (controlChars >= 0X7F && controlChars <= 0x9F)) {
+			if (c <= 0x1F || (c >= 0X7F && c <= 0x9F)) {
 				// ASCII (C0) control characters.
 				// C1 control characters (https://www.aivosto.com/articles/control-characters.html#c1)
-				return new Rune (controlChars + 0x2400);
-			} else {
-				return c;
+				return new Rune (c + 0x2400);
 			}
-		}
 
-		static uint gethexaformat (uint rune, int length)
-		{
-			var hex = rune.ToString ($"x{length}");
-			var hexstr = hex.Substring (hex.Length - length, length);
-			return (uint)int.Parse (hexstr, System.Globalization.NumberStyles.HexNumber);
+			return c;
 		}
 
 		/// <summary>
@@ -722,10 +593,11 @@ namespace Terminal.Gui {
 			col >= 0 && row >= 0 && col < Cols && row < Rows && clip.Contains (col, row);
 
 		/// <summary>
-		/// Adds the specified
+		/// Adds the <paramref name="str"/> to the display at the cursor position.
 		/// </summary>
 		/// <param name="str">String.</param>
 		public abstract void AddStr (ustring str);
+
 		/// <summary>
 		/// Prepare the driver and set the key and mouse events handlers.
 		/// </summary>
@@ -1390,8 +1262,78 @@ namespace Terminal.Gui {
 			Colors.Error.Normal = MakeColor (Color.Red, Color.White);
 			Colors.Error.Focus = MakeColor (Color.Black, Color.BrightRed);
 			Colors.Error.HotNormal = MakeColor (Color.Black, Color.White);
-			Colors.Error.HotFocus = MakeColor (Color.BrightRed, Color.Gray);
+			Colors.Error.HotFocus = MakeColor (Color.White, Color.BrightRed);
 			Colors.Error.Disabled = MakeColor (Color.DarkGray, Color.White);
 		}
 	}
+
+	/// <summary>
+	/// Helper class for console drivers to invoke shell commands to interact with the clipboard.
+	/// Used primarily by CursesDriver, but also used in Unit tests which is why it is in
+	/// ConsoleDriver.cs.
+	/// </summary>
+	internal static class ClipboardProcessRunner {
+		public static (int exitCode, string result) Bash (string commandLine, string inputText = "", bool waitForOutput = false)
+		{
+			var arguments = $"-c \"{commandLine}\"";
+			var (exitCode, result) = Process ("bash", arguments, inputText, waitForOutput);
+
+			return (exitCode, result.TrimEnd ());
+		}
+
+		public static (int exitCode, string result) Process (string cmd, string arguments, string input = null, bool waitForOutput = true)
+		{
+			var output = string.Empty;
+
+			using (Process process = new Process {
+				StartInfo = new ProcessStartInfo {
+					FileName = cmd,
+					Arguments = arguments,
+					RedirectStandardOutput = true,
+					RedirectStandardError = true,
+					RedirectStandardInput = true,
+					UseShellExecute = false,
+					CreateNoWindow = true,
+				}
+			}) {
+				var eventHandled = new TaskCompletionSource<bool> ();
+				process.Start ();
+				if (!string.IsNullOrEmpty (input)) {
+					process.StandardInput.Write (input);
+					process.StandardInput.Close ();
+				}
+
+				if (!process.WaitForExit (5000)) {
+					var timeoutError = $@"Process timed out. Command line: {process.StartInfo.FileName} {process.StartInfo.Arguments}.";
+					throw new TimeoutException (timeoutError);
+				}
+
+				if (waitForOutput && process.StandardOutput.Peek () != -1) {
+					output = process.StandardOutput.ReadToEnd ();
+				}
+
+				if (process.ExitCode > 0) {
+					output = $@"Process failed to run. Command line: {cmd} {arguments}.
+										Output: {output}
+										Error: {process.StandardError.ReadToEnd ()}";
+				}
+
+				return (process.ExitCode, output);
+			}
+		}
+
+		public static bool DoubleWaitForExit (this System.Diagnostics.Process process)
+		{
+			var result = process.WaitForExit (500);
+			if (result) {
+				process.WaitForExit ();
+			}
+			return result;
+		}
+
+		public static bool FileExists (this string value)
+		{
+			return !string.IsNullOrEmpty (value) && !value.Contains ("not found");
+		}
+	}
 }

+ 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)
+		};
+	}
+}

+ 97 - 26
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>
-		/// Delete character key
+		/// Insert character key.
+		/// </summary>
+		InsertChar,
+
+		/// <summary>
+		/// 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;
 		}
@@ -706,38 +767,48 @@ namespace Terminal.Gui {
 	}
 
 	/// <summary>
-	/// Describes a mouse event
+	/// Low-level construct that conveys the details of mouse events, such
+	/// as coordinates and button state, from ConsoleDrivers up to <see cref="Application"/> and
+	/// Views.
 	/// </summary>
-	public struct MouseEvent {
+	/// <remarks>The <see cref="Application"/> class includes the <see cref="Application.RootMouseEvent"/>
+	/// Action which takes a MouseEvent argument.</remarks>
+	public class MouseEvent {
 		/// <summary>
 		/// The X (column) location for the mouse event.
 		/// </summary>
-		public int X;
+		public int X { get; set; }
 
 		/// <summary>
 		/// The Y (column) location for the mouse event.
 		/// </summary>
-		public int Y;
+		public int Y { get; set; }
 
 		/// <summary>
 		/// Flags indicating the kind of mouse event that is being posted.
 		/// </summary>
-		public MouseFlags Flags;
+		public MouseFlags Flags { get; set; }
 
 		/// <summary>
 		/// The offset X (column) location for the mouse event.
 		/// </summary>
-		public int OfX;
+		public int OfX { get; set; }
 
 		/// <summary>
 		/// The offset Y (column) location for the mouse event.
 		/// </summary>
-		public int OfY;
+		public int OfY { get; set; }
 
 		/// <summary>
 		/// The current view at the location for the mouse event.
 		/// </summary>
-		public View View;
+		public View View { get; set; }
+
+		/// <summary>
+		/// Indicates if the current mouse event has already been processed and the driver should stop notifying any other event subscriber.
+		/// Its important to set this value to true specially when updating any View's layout from inside the subscriber method.
+		/// </summary>
+		public bool Handled { get; set; }
 
 		/// <summary>
 		/// Returns a <see cref="T:System.String"/> that represents the current <see cref="MouseEvent"/>.

+ 9 - 5
Terminal.Gui/Core/MainLoop.cs

@@ -94,15 +94,16 @@ namespace Terminal.Gui {
 		public IMainLoopDriver Driver { get; }
 
 		/// <summary>
-		/// Invoked when a new timeout is added to be used on the case
-		/// if <see cref="Application.ExitRunLoopAfterFirstIteration"/> is true,
+		/// Invoked when a new timeout is added. To be used in the case
+		/// when <see cref="Application.ExitRunLoopAfterFirstIteration"/> is <see langword="true"/>.
 		/// </summary>
 		public event Action<long> TimeoutAdded;
 
 		/// <summary>
 		///  Creates a new Mainloop. 
 		/// </summary>
-		/// <param name="driver">Should match the <see cref="ConsoleDriver"/> (one of the implementations UnixMainLoop, NetMainLoop or WindowsMainLoop).</param>
+		/// <param name="driver">Should match the <see cref="ConsoleDriver"/> 
+		/// (one of the implementations FakeMainLoop, UnixMainLoop, NetMainLoop or WindowsMainLoop).</param>
 		public MainLoop (IMainLoopDriver driver)
 		{
 			Driver = driver;
@@ -306,9 +307,12 @@ namespace Terminal.Gui {
 
 			Driver.MainIteration ();
 
+			bool runIdle = false;
 			lock (idleHandlersLock) {
-				if (idleHandlers.Count > 0)
-					RunIdle ();
+				runIdle = idleHandlers.Count > 0;
+			}
+			if (runIdle) {
+				RunIdle ();
 			}
 		}
 

+ 1 - 1
Terminal.Gui/Core/PosDim.cs

@@ -593,7 +593,7 @@ namespace Terminal.Gui {
 
 		internal class DimCombine : Dim {
 			internal Dim left, right;
-			bool add;
+			internal bool add;
 			public DimCombine (bool add, Dim left, Dim right)
 			{
 				this.left = left;

+ 20 - 0
Terminal.Gui/Core/Responder.cs

@@ -16,6 +16,7 @@
 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
+using System.Reflection;
 
 namespace Terminal.Gui {
 	/// <summary>
@@ -236,6 +237,25 @@ namespace Terminal.Gui {
 		/// </summary>
 		public virtual void OnVisibleChanged () { }
 
+		/// <summary>
+		/// Utilty function to determine <paramref name="method"/> is overridden in the <paramref name="subclass"/>.
+		/// </summary>
+		/// <param name="subclass">The view.</param>
+		/// <param name="method">The method name.</param>
+		/// <returns><see langword="true"/> if it's overridden, <see langword="false"/> otherwise.</returns>
+		internal static bool IsOverridden (Responder subclass, string method)
+		{
+			MethodInfo m = subclass.GetType ().GetMethod (method,
+				BindingFlags.Instance
+				| BindingFlags.Public
+				| BindingFlags.NonPublic
+				| BindingFlags.DeclaredOnly);
+			if (m == null) {
+				return false;
+			}
+			return m.GetBaseDefinition ().DeclaringType != m.DeclaringType;
+		}
+		
 		/// <summary>
 		/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
 		/// </summary>

+ 30 - 30
Terminal.Gui/Core/TextFormatter.cs

@@ -293,12 +293,6 @@ namespace Terminal.Gui {
 			}
 		}
 
-		/// <summary>
-		/// Specifies the mask to apply to the hotkey to tag it as the hotkey. The default value of <c>0x100000</c> causes
-		/// the underlying Rune to be identified as a "private use" Unicode character.
-		/// </summary>HotKeyTagMask
-		public uint HotKeyTagMask { get; set; } = 0x100000;
-
 		/// <summary>
 		/// Gets the cursor position from <see cref="HotKey"/>. If the <see cref="HotKey"/> is defined, the cursor will be positioned over it.
 		/// </summary>
@@ -317,8 +311,9 @@ namespace Terminal.Gui {
 			get {
 				// With this check, we protect against subclasses with overrides of Text
 				if (ustring.IsNullOrEmpty (Text) || Size.IsEmpty) {
-					lines = new List<ustring> ();
-					lines.Add (ustring.Empty);
+					lines = new List<ustring> {
+						ustring.Empty
+					};
 					NeedsFormat = false;
 					return lines;
 				}
@@ -716,7 +711,7 @@ namespace Terminal.Gui {
 		}
 
 		static char [] whitespace = new char [] { ' ', '\t' };
-		private int hotKeyPos;
+		private int hotKeyPos = -1;
 
 		/// <summary>
 		/// Reformats text into lines, applying text alignment and optionally wrapping text to new lines on word boundaries.
@@ -1113,14 +1108,13 @@ namespace Terminal.Gui {
 		/// <returns>The text with the hotkey tagged.</returns>
 		/// <remarks>
 		/// The returned string will not render correctly without first un-doing the tag. To undo the tag, search for 
-		/// Runes with a bitmask of <c>otKeyTagMask</c> and remove that bitmask.
 		/// </remarks>
 		public ustring ReplaceHotKeyWithTag (ustring text, int hotPos)
 		{
 			// Set the high bit
 			var runes = text.ToRuneList ();
 			if (Rune.IsLetterOrNumber (runes [hotPos])) {
-				runes [hotPos] = new Rune ((uint)runes [hotPos] | HotKeyTagMask);
+				runes [hotPos] = new Rune ((uint)runes [hotPos]);
 			}
 			return ustring.Make (runes);
 		}
@@ -1182,10 +1176,22 @@ namespace Terminal.Gui {
 			}
 
 			var isVertical = IsVerticalDirection (textDirection);
+			var savedClip = Application.Driver?.Clip;
+			var maxBounds = bounds;
+			if (Application.Driver != null) {
+				Application.Driver.Clip = maxBounds = containerBounds == default
+					? bounds
+					: new Rect (Math.Max (containerBounds.X, bounds.X),
+					Math.Max (containerBounds.Y, bounds.Y),
+					Math.Max (Math.Min (containerBounds.Width, containerBounds.Right - bounds.Left), 0),
+					Math.Max (Math.Min (containerBounds.Height, containerBounds.Bottom - bounds.Top), 0));
+			}
 
 			for (int line = 0; line < linesFormated.Count; line++) {
 				if ((isVertical && line > bounds.Width) || (!isVertical && line > bounds.Height))
 					continue;
+				if ((isVertical && line > maxBounds.Left + maxBounds.Width - bounds.X) || (!isVertical && line > maxBounds.Top + maxBounds.Height - bounds.Y))
+					break;
 
 				var runes = lines [line].ToRunes ();
 
@@ -1206,11 +1212,11 @@ namespace Terminal.Gui {
 					if (isVertical) {
 						var runesWidth = GetSumMaxCharWidth (Lines, line);
 						x = bounds.Right - runesWidth;
-						CursorPosition = bounds.Width - runesWidth + hotKeyPos;
+						CursorPosition = bounds.Width - runesWidth + (hotKeyPos > -1 ? hotKeyPos : 0);
 					} else {
 						var runesWidth = GetTextWidth (ustring.Make (runes));
 						x = bounds.Right - runesWidth;
-						CursorPosition = bounds.Width - runesWidth + hotKeyPos;
+						CursorPosition = bounds.Width - runesWidth + (hotKeyPos > -1 ? hotKeyPos : 0);
 					}
 				} else if (textAlignment == TextAlignment.Left || textAlignment == TextAlignment.Justified) {
 					if (isVertical) {
@@ -1219,16 +1225,16 @@ namespace Terminal.Gui {
 					} else {
 						x = bounds.Left;
 					}
-					CursorPosition = hotKeyPos;
+					CursorPosition = hotKeyPos > -1 ? hotKeyPos : 0;
 				} else if (textAlignment == TextAlignment.Centered) {
 					if (isVertical) {
 						var runesWidth = GetSumMaxCharWidth (Lines, line);
 						x = bounds.Left + line + ((bounds.Width - runesWidth) / 2);
-						CursorPosition = (bounds.Width - runesWidth) / 2 + hotKeyPos;
+						CursorPosition = (bounds.Width - runesWidth) / 2 + (hotKeyPos > -1 ? hotKeyPos : 0);
 					} else {
 						var runesWidth = GetTextWidth (ustring.Make (runes));
 						x = bounds.Left + (bounds.Width - runesWidth) / 2;
-						CursorPosition = (bounds.Width - runesWidth) / 2 + hotKeyPos;
+						CursorPosition = (bounds.Width - runesWidth) / 2 + (hotKeyPos > -1 ? hotKeyPos : 0);
 					}
 				} else {
 					throw new ArgumentOutOfRangeException ();
@@ -1262,15 +1268,6 @@ namespace Terminal.Gui {
 				var start = isVertical ? bounds.Top : bounds.Left;
 				var size = isVertical ? bounds.Height : bounds.Width;
 				var current = start;
-				var savedClip = Application.Driver?.Clip;
-				if (Application.Driver != null) {
-					Application.Driver.Clip = containerBounds == default
-						? bounds
-						: new Rect (Math.Max (containerBounds.X, bounds.X),
-						Math.Max (containerBounds.Y, bounds.Y),
-						Math.Max (Math.Min (containerBounds.Width, containerBounds.Right - bounds.Left), 0),
-						Math.Max (Math.Min (containerBounds.Height, containerBounds.Bottom - bounds.Top), 0));
-				}
 
 				for (var idx = (isVertical ? start - y : start - x); current < start + size; idx++) {
 					if (!fillRemaining && idx < 0) {
@@ -1279,6 +1276,9 @@ namespace Terminal.Gui {
 					} else if (!fillRemaining && idx > runes.Length - 1) {
 						break;
 					}
+					if ((!isVertical && idx > maxBounds.Left + maxBounds.Width - bounds.X) || (isVertical && idx > maxBounds.Top + maxBounds.Height - bounds.Y))
+						break;
+
 					var rune = (Rune)' ';
 					if (isVertical) {
 						Application.Driver?.Move (x, current);
@@ -1291,13 +1291,13 @@ namespace Terminal.Gui {
 							rune = runes [idx];
 						}
 					}
-					if ((rune & HotKeyTagMask) == HotKeyTagMask) {
+					if (HotKeyPos > -1 && idx == HotKeyPos) {
 						if ((isVertical && textVerticalAlignment == VerticalTextAlignment.Justified) ||
-						    (!isVertical && textAlignment == TextAlignment.Justified)) {
+						(!isVertical && textAlignment == TextAlignment.Justified)) {
 							CursorPosition = idx - start;
 						}
 						Application.Driver?.SetAttribute (hotColor);
-						Application.Driver?.AddRune ((Rune)((uint)rune & ~HotKeyTagMask));
+						Application.Driver?.AddRune (rune);
 						Application.Driver?.SetAttribute (normalColor);
 					} else {
 						Application.Driver?.AddRune (rune);
@@ -1313,9 +1313,9 @@ namespace Terminal.Gui {
 						break;
 					}
 				}
-				if (Application.Driver != null)
-					Application.Driver.Clip = (Rect)savedClip;
 			}
+			if (Application.Driver != null)
+				Application.Driver.Clip = (Rect)savedClip;
 		}
 	}
 }

+ 19 - 27
Terminal.Gui/Core/Toplevel.cs

@@ -44,7 +44,7 @@ namespace Terminal.Gui {
 		public bool Running { get; set; }
 
 		/// <summary>
-		/// Invoked when the Toplevel <see cref="Application.RunState"/> has begin loaded.
+		/// Invoked when the Toplevel <see cref="Application.RunState"/> has begun to be loaded.
 		/// A Loaded event handler is a good place to finalize initialization before calling 
 		/// <see cref="Application.RunLoop(Application.RunState, bool)"/>.
 		/// </summary>
@@ -77,13 +77,13 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Invoked when a child of the Toplevel <see cref="Application.RunState"/> is closed by  
-		/// <see cref="Application.End(View)"/>.
+		/// <see cref="Application.End(Application.RunState)"/>.
 		/// </summary>
 		public event Action<Toplevel> ChildClosed;
 
 		/// <summary>
 		/// Invoked when the last child of the Toplevel <see cref="Application.RunState"/> is closed from 
-		/// by <see cref="Application.End(View)"/>.
+		/// by <see cref="Application.End(Application.RunState)"/>.
 		/// </summary>
 		public event Action AllChildClosed;
 
@@ -94,7 +94,7 @@ namespace Terminal.Gui {
 		public event Action<ToplevelClosingEventArgs> Closing;
 
 		/// <summary>
-		/// Invoked when the Toplevel's <see cref="Application.RunState"/> is closed by <see cref="Application.End(View)"/>.
+		/// Invoked when the Toplevel's <see cref="Application.RunState"/> is closed by <see cref="Application.End(Application.RunState)"/>.
 		/// </summary>
 		public event Action<Toplevel> Closed;
 
@@ -613,11 +613,9 @@ namespace Terminal.Gui {
 			}
 			nx = Math.Max (x, 0);
 			nx = nx + top.Frame.Width > l ? Math.Max (l - top.Frame.Width, 0) : nx;
-			var canChange = SetWidth (top.Frame.Width, out int rWidth);
-			if (canChange && rWidth < 0 && nx >= top.Frame.X) {
-				nx = Math.Max (top.Frame.Right - 2, 0);
-			} else if (rWidth < 0 && nx >= top.Frame.X) {
-				nx = Math.Min (nx + 1, top.Frame.Right - 2);
+			var mfLength = top.Border?.DrawMarginFrame == true ? 2 : 1;
+			if (nx + mfLength > top.Frame.X + top.Frame.Width) {
+				nx = Math.Max (top.Frame.Right - mfLength, 0);
 			}
 			//System.Diagnostics.Debug.WriteLine ($"nx:{nx}, rWidth:{rWidth}");
 			bool m, s;
@@ -656,11 +654,8 @@ namespace Terminal.Gui {
 			}
 			ny = Math.Min (ny, l);
 			ny = ny + top.Frame.Height >= l ? Math.Max (l - top.Frame.Height, m ? 1 : 0) : ny;
-			canChange = SetHeight (top.Frame.Height, out int rHeight);
-			if (canChange && rHeight < 0 && ny >= top.Frame.Y) {
-				ny = Math.Max (top.Frame.Bottom - 2, 0);
-			} else if (rHeight < 0 && ny >= top.Frame.Y) {
-				ny = Math.Min (ny + 1, top.Frame.Bottom - 2);
+			if (ny + mfLength > top.Frame.Y + top.Frame.Height) {
+				ny = Math.Max (top.Frame.Bottom - mfLength, 0);
 			}
 			//System.Diagnostics.Debug.WriteLine ($"ny:{ny}, rHeight:{rHeight}");
 
@@ -701,7 +696,7 @@ namespace Terminal.Gui {
 			}
 
 			if (sb != null && ny + top.Frame.Height != superView.Frame.Height - (sb.Visible ? 1 : 0)
-					&& top.Height is Dim.DimFill) {
+				&& top.Height is Dim.DimFill && -top.Height.Anchor (0) < 1) {
 
 				top.Height = Dim.Fill (sb.Visible ? 1 : 0);
 				layoutSubviews = true;
@@ -775,6 +770,8 @@ namespace Terminal.Gui {
 				return true;
 			}
 
+			//System.Diagnostics.Debug.WriteLine ($"dragPosition before: {dragPosition.HasValue}");
+
 			int nx, ny;
 			if (!dragPosition.HasValue && (mouseEvent.Flags == MouseFlags.Button1Pressed
 				|| mouseEvent.Flags == MouseFlags.Button2Pressed
@@ -809,32 +806,27 @@ namespace Terminal.Gui {
 						SuperView.SetNeedsDisplay ();
 					}
 					EnsureVisibleBounds (this, mouseEvent.X + (SuperView == null ? mouseEvent.OfX - start.X : Frame.X - start.X),
-						mouseEvent.Y + (SuperView == null ? mouseEvent.OfY : Frame.Y),
+						mouseEvent.Y + (SuperView == null ? mouseEvent.OfY - start.Y : Frame.Y - start.Y),
 						out nx, out ny, out _, out _);
 
 					dragPosition = new Point (nx, ny);
-					LayoutSubviews ();
-					Frame = new Rect (nx, ny, Frame.Width, Frame.Height);
-					if (X == null || X is Pos.PosAbsolute) {
-						X = nx;
-					}
-					if (Y == null || Y is Pos.PosAbsolute) {
-						Y = ny;
-					}
-					//System.Diagnostics.Debug.WriteLine ($"nx:{nx},ny:{ny}");
+					X = nx;
+					Y = ny;
+					//System.Diagnostics.Debug.WriteLine ($"Drag: nx:{nx},ny:{ny}");
 
 					SetNeedsDisplay ();
 					return true;
 				}
 			}
 
-			if (mouseEvent.Flags == MouseFlags.Button1Released && dragPosition.HasValue) {
+			if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Released) && dragPosition.HasValue) {
 				Application.UngrabMouse ();
 				Driver.UncookMouse ();
 				dragPosition = null;
 			}
 
-			//System.Diagnostics.Debug.WriteLine (mouseEvent.ToString ());
+			//System.Diagnostics.Debug.WriteLine ($"dragPosition after: {dragPosition.HasValue}");
+			//System.Diagnostics.Debug.WriteLine ($"Toplevel: {mouseEvent}");
 			return false;
 		}
 

+ 41 - 39
Terminal.Gui/Core/Trees/Branch.cs

@@ -5,23 +5,23 @@ using System.Linq;
 namespace Terminal.Gui.Trees {
 	class Branch<T> where T : class {
 		/// <summary>
-		/// True if the branch is expanded to reveal child branches
+		/// True if the branch is expanded to reveal child branches.
 		/// </summary>
 		public bool IsExpanded { get; set; }
 
 		/// <summary>
-		/// The users object that is being displayed by this branch of the tree
+		/// The users object that is being displayed by this branch of the tree.
 		/// </summary>
 		public T Model { get; private set; }
 
 		/// <summary>
-		/// The depth of the current branch.  Depth of 0 indicates root level branches
+		/// The depth of the current branch.  Depth of 0 indicates root level branches.
 		/// </summary>
 		public int Depth { get; private set; } = 0;
 
 		/// <summary>
 		/// The children of the current branch.  This is null until the first call to 
-		/// <see cref="FetchChildren"/> to avoid enumerating the entire underlying hierarchy
+		/// <see cref="FetchChildren"/> to avoid enumerating the entire underlying hierarchy.
 		/// </summary>
 		public Dictionary<T, Branch<T>> ChildBranches { get; set; }
 
@@ -34,12 +34,12 @@ namespace Terminal.Gui.Trees {
 
 		/// <summary>
 		/// Declares a new branch of <paramref name="tree"/> in which the users object 
-		/// <paramref name="model"/> is presented
+		/// <paramref name="model"/> is presented.
 		/// </summary>
-		/// <param name="tree">The UI control in which the branch resides</param>
+		/// <param name="tree">The UI control in which the branch resides.</param>
 		/// <param name="parentBranchIfAny">Pass null for root level branches, otherwise
-		/// pass the parent</param>
-		/// <param name="model">The user's object that should be displayed</param>
+		/// pass the parent.</param>
+		/// <param name="model">The user's object that should be displayed.</param>
 		public Branch (TreeView<T> tree, Branch<T> parentBranchIfAny, T model)
 		{
 			this.tree = tree;
@@ -53,7 +53,7 @@ namespace Terminal.Gui.Trees {
 
 
 		/// <summary>
-		/// Fetch the children of this branch. This method populates <see cref="ChildBranches"/>
+		/// Fetch the children of this branch. This method populates <see cref="ChildBranches"/>.
 		/// </summary>
 		public virtual void FetchChildren ()
 		{
@@ -80,7 +80,7 @@ namespace Terminal.Gui.Trees {
 		}
 
 		/// <summary>
-		/// Renders the current <see cref="Model"/> on the specified line <paramref name="y"/>
+		/// Renders the current <see cref="Model"/> on the specified line <paramref name="y"/>.
 		/// </summary>
 		/// <param name="driver"></param>
 		/// <param name="colorScheme"></param>
@@ -89,10 +89,10 @@ namespace Terminal.Gui.Trees {
 		public virtual void Draw (ConsoleDriver driver, ColorScheme colorScheme, int y, int availableWidth)
 		{
 			// true if the current line of the tree is the selected one and control has focus
-			bool isSelected = tree.IsSelected (Model) && tree.HasFocus;
-			Attribute lineColor = isSelected ? colorScheme.Focus : colorScheme.Normal;
+			bool isSelected = tree.IsSelected (Model);
 
-			driver.SetAttribute (lineColor);
+			Attribute textColor = isSelected ? (tree.HasFocus ? colorScheme.Focus : colorScheme.HotNormal) : colorScheme.Normal;
+			Attribute symbolColor = tree.Style.HighlightModelTextOnly ? colorScheme.Normal : textColor;
 
 			// Everything on line before the expansion run and branch text
 			Rune [] prefix = GetLinePrefix (driver).ToArray ();
@@ -104,7 +104,8 @@ namespace Terminal.Gui.Trees {
 			// if we have scrolled to the right then bits of the prefix will have dispeared off the screen
 			int toSkip = tree.ScrollOffsetHorizontal;
 
-			// Draw the line prefix (all paralell lanes or whitespace and an expand/collapse/leaf symbol)
+			driver.SetAttribute (symbolColor);
+			// Draw the line prefix (all parallel lanes or whitespace and an expand/collapse/leaf symbol)
 			foreach (Rune r in prefix) {
 
 				if (toSkip > 0) {
@@ -117,12 +118,16 @@ namespace Terminal.Gui.Trees {
 
 			// pick color for expanded symbol
 			if (tree.Style.ColorExpandSymbol || tree.Style.InvertExpandSymbolColors) {
-				Attribute color;
+				Attribute color = symbolColor;
 
 				if (tree.Style.ColorExpandSymbol) {
-					color = isSelected ? tree.ColorScheme.HotFocus : tree.ColorScheme.HotNormal;
+					if (isSelected) {
+						color = tree.Style.HighlightModelTextOnly ? colorScheme.HotNormal : (tree.HasFocus ? tree.ColorScheme.HotFocus : tree.ColorScheme.HotNormal);
+					} else {
+						color = tree.ColorScheme.HotNormal;
+					}
 				} else {
-					color = lineColor;
+					color = symbolColor;
 				}
 
 				if (tree.Style.InvertExpandSymbolColors) {
@@ -162,16 +167,14 @@ namespace Terminal.Gui.Trees {
 
 			// default behaviour is for model to use the color scheme
 			// of the tree view
-			var modelColor = lineColor;
+			var modelColor = textColor;
 
 			// if custom color delegate invoke it
-			if(tree.ColorGetter != null)
-			{
-				var modelScheme = tree.ColorGetter(Model);
+			if (tree.ColorGetter != null) {
+				var modelScheme = tree.ColorGetter (Model);
 
 				// if custom color scheme is defined for this Model
-				if(modelScheme != null)
-				{
+				if (modelScheme != null) {
 					// use it
 					modelColor = isSelected ? modelScheme.Focus : modelScheme.Normal;
 				}
@@ -179,24 +182,23 @@ namespace Terminal.Gui.Trees {
 
 			driver.SetAttribute (modelColor);
 			driver.AddStr (lineBody);
-			driver.SetAttribute (lineColor);
 
 			if (availableWidth > 0) {
+				driver.SetAttribute (symbolColor);
 				driver.AddStr (new string (' ', availableWidth));
 			}
-
 			driver.SetAttribute (colorScheme.Normal);
 		}
 
 		/// <summary>
 		/// Gets all characters to render prior to the current branches line.  This includes indentation
-		/// whitespace and any tree branches (if enabled)
+		/// whitespace and any tree branches (if enabled).
 		/// </summary>
 		/// <param name="driver"></param>
 		/// <returns></returns>
 		private IEnumerable<Rune> GetLinePrefix (ConsoleDriver driver)
 		{
-			// If not showing line branches or this is a root object
+			// If not showing line branches or this is a root object.
 			if (!tree.Style.ShowBranchLines) {
 				for (int i = 0; i < Depth; i++) {
 					yield return new Rune (' ');
@@ -224,7 +226,7 @@ namespace Terminal.Gui.Trees {
 		}
 
 		/// <summary>
-		/// Returns all parents starting with the immediate parent and ending at the root
+		/// Returns all parents starting with the immediate parent and ending at the root.
 		/// </summary>
 		/// <returns></returns>
 		private IEnumerable<Branch<T>> GetParentBranches ()
@@ -240,7 +242,7 @@ namespace Terminal.Gui.Trees {
 		/// <summary>
 		/// Returns an appropriate symbol for displaying next to the string representation of 
 		/// the <see cref="Model"/> object to indicate whether it <see cref="IsExpanded"/> or
-		/// not (or it is a leaf)
+		/// not (or it is a leaf).
 		/// </summary>
 		/// <param name="driver"></param>
 		/// <returns></returns>
@@ -261,7 +263,7 @@ namespace Terminal.Gui.Trees {
 
 		/// <summary>
 		/// Returns true if the current branch can be expanded according to 
-		/// the <see cref="TreeBuilder{T}"/> or cached children already fetched
+		/// the <see cref="TreeBuilder{T}"/> or cached children already fetched.
 		/// </summary>
 		/// <returns></returns>
 		public bool CanExpand ()
@@ -283,7 +285,7 @@ namespace Terminal.Gui.Trees {
 		}
 
 		/// <summary>
-		/// Expands the current branch if possible
+		/// Expands the current branch if possible.
 		/// </summary>
 		public void Expand ()
 		{
@@ -297,7 +299,7 @@ namespace Terminal.Gui.Trees {
 		}
 
 		/// <summary>
-		/// Marks the branch as collapsed (<see cref="IsExpanded"/> false)
+		/// Marks the branch as collapsed (<see cref="IsExpanded"/> false).
 		/// </summary>
 		public void Collapse ()
 		{
@@ -305,10 +307,10 @@ namespace Terminal.Gui.Trees {
 		}
 
 		/// <summary>
-		/// Refreshes cached knowledge in this branch e.g. what children an object has
+		/// Refreshes cached knowledge in this branch e.g. what children an object has.
 		/// </summary>
 		/// <param name="startAtTop">True to also refresh all <see cref="Parent"/> 
-		/// branches (starting with the root)</param>
+		/// branches (starting with the root).</param>
 		public void Refresh (bool startAtTop)
 		{
 			// if we must go up and refresh from the top down
@@ -351,7 +353,7 @@ namespace Terminal.Gui.Trees {
 		}
 
 		/// <summary>
-		/// Calls <see cref="Refresh(bool)"/> on the current branch and all expanded children
+		/// Calls <see cref="Refresh(bool)"/> on the current branch and all expanded children.
 		/// </summary>
 		internal void Rebuild ()
 		{
@@ -375,7 +377,7 @@ namespace Terminal.Gui.Trees {
 
 		/// <summary>
 		/// Returns true if this branch has parents and it is the last node of it's parents 
-		/// branches (or last root of the tree)
+		/// branches (or last root of the tree).
 		/// </summary>
 		/// <returns></returns>
 		private bool IsLast ()
@@ -389,7 +391,7 @@ namespace Terminal.Gui.Trees {
 
 		/// <summary>
 		/// Returns true if the given x offset on the branch line is the +/- symbol.  Returns 
-		/// false if not showing expansion symbols or leaf node etc
+		/// false if not showing expansion symbols or leaf node etc.
 		/// </summary>
 		/// <param name="driver"></param>
 		/// <param name="x"></param>
@@ -415,7 +417,7 @@ namespace Terminal.Gui.Trees {
 		}
 
 		/// <summary>
-		/// Expands the current branch and all children branches
+		/// Expands the current branch and all children branches.
 		/// </summary>
 		internal void ExpandAll ()
 		{
@@ -430,7 +432,7 @@ namespace Terminal.Gui.Trees {
 
 		/// <summary>
 		/// Collapses the current branch and all children branches (even though those branches are 
-		/// no longer visible they retain collapse/expansion state)
+		/// no longer visible they retain collapse/expansion state).
 		/// </summary>
 		internal void CollapseAll ()
 		{

+ 15 - 10
Terminal.Gui/Core/Trees/TreeStyle.cs

@@ -2,46 +2,51 @@
 
 namespace Terminal.Gui.Trees {
 	/// <summary>
-	/// Defines rendering options that affect how the tree is displayed
+	/// Defines rendering options that affect how the tree is displayed.
 	/// </summary>
 	public class TreeStyle {
 
 		/// <summary>
-		/// True to render vertical lines under expanded nodes to show which node belongs to which 
-		/// parent.  False to use only whitespace
+		/// <see langword="true"/> to render vertical lines under expanded nodes to show which node belongs to which 
+		/// parent. <see langword="false"/> to use only whitespace.
 		/// </summary>
 		/// <value></value>
 		public bool ShowBranchLines { get; set; } = true;
 
 		/// <summary>
-		/// Symbol to use for branch nodes that can be expanded to indicate this to the user.  
-		/// Defaults to '+'. Set to null to hide
+		/// Symbol to use for branch nodes that can be expanded to indicate this to the user. 
+		/// Defaults to '+'. Set to null to hide.
 		/// </summary>
 		public Rune? ExpandableSymbol { get; set; } = '+';
 
 		/// <summary>
 		/// Symbol to use for branch nodes that can be collapsed (are currently expanded).
-		/// Defaults to '-'.  Set to null to hide
+		/// Defaults to '-'. Set to null to hide.
 		/// </summary>
 		public Rune? CollapseableSymbol { get; set; } = '-';
 
 		/// <summary>
-		/// Set to true to highlight expand/collapse symbols in hot key color
+		/// Set to <see langword="true"/> to highlight expand/collapse symbols in hot key color.
 		/// </summary>
 		public bool ColorExpandSymbol { get; set; }
 
 		/// <summary>
-		/// Invert console colours used to render the expand symbol
+		/// Invert console colours used to render the expand symbol.
 		/// </summary>
 		public bool InvertExpandSymbolColors { get; set; }
 
 		/// <summary>
-		/// True to leave the last row of the control free for overwritting (e.g. by a scrollbar)
-		/// When True scrolling will be triggered on the second last row of the control rather than
+		/// <see langword="true"/> to leave the last row of the control free for overwritting (e.g. by a scrollbar)
+		/// When <see langword="true"/> scrolling will be triggered on the second last row of the control rather than.
 		/// the last.
 		/// </summary>
 		/// <value></value>
 		public bool LeaveLastRow { get; set; }
 
+		/// <summary>
+		/// Set to <see langword="true"/> to cause the selected item to be rendered with only the <see cref="Branch{T}.Model"/> text
+		/// to be highlighted. If <see langword="false"/> (the default), the entire row will be highlighted.
+		/// </summary>
+		public bool HighlightModelTextOnly { get; set; } = false;
 	}
 }

文件差異過大導致無法顯示
+ 180 - 173
Terminal.Gui/Core/View.cs


+ 3 - 10
Terminal.Gui/Core/Window.cs

@@ -277,18 +277,17 @@ namespace Terminal.Gui {
 		{
 			var padding = Border.GetSumThickness ();
 			var scrRect = ViewToScreen (new Rect (0, 0, Frame.Width, Frame.Height));
-			//var borderLength = Border.DrawMarginFrame ? 1 : 0;
 
-			// FIXED: Why do we draw the frame twice? This call is here to clear the content area, I think. Why not just clear that area?
-			if (!NeedDisplay.IsEmpty) {
+			if (!NeedDisplay.IsEmpty || ChildNeedsDisplay || LayoutNeeded) {
 				Driver.SetAttribute (GetNormalColor ());
 				Clear ();
+				contentView.SetNeedsDisplay ();
 			}
 			var savedClip = contentView.ClipToBounds ();
 
 			// Redraw our contentView
 			// DONE: smartly constrict contentView.Bounds to just be what intersects with the 'bounds' we were passed
-			contentView.Redraw (!NeedDisplay.IsEmpty ? contentView.Bounds : bounds);
+			contentView.Redraw (!NeedDisplay.IsEmpty || ChildNeedsDisplay || LayoutNeeded ? contentView.Bounds : bounds);
 			Driver.Clip = savedClip;
 
 			ClearLayoutNeeded ();
@@ -303,12 +302,6 @@ namespace Terminal.Gui {
 			if (Border.DrawMarginFrame)
 				Driver.DrawWindowTitle (scrRect, Title, padding.Left, padding.Top, padding.Right, padding.Bottom);
 			Driver.SetAttribute (GetNormalColor ());
-
-			// Checks if there are any SuperView view which intersect with this window.
-			if (SuperView != null) {
-				SuperView.SetNeedsLayout ();
-				SuperView.SetNeedsDisplay ();
-			}
 		}
 
 		/// <inheritdoc/>

+ 30 - 30
Terminal.Gui/README.md

@@ -1,24 +1,24 @@
 # Terminal.Gui Project
 
-Contains all files required to build the **Terminal.Gui** library (and NuGet package).
+All files required to build the **Terminal.Gui** library (and NuGet package).
 
 ## Project Folder Structure
 
 - `Terminal.Gui.sln` - The Visual Studio solution
 - `Core/` - Source files for all types that comprise the core building blocks of **Terminal-Gui** 
     - `Application` - A `static` class that provides the base 'application driver'. Given it defines a **Terminal.Gui** application it is both logically and literally (because `static`) a singleton. It has direct dependencies on `MainLoop`, `Events.cs` `NetDriver`, `CursesDriver`, `WindowsDriver`, `Responder`, `View`, and `TopLevel` (and nothing else).
-    - `MainLoop` - Defines `IMainLoopDriver` and implements the and `MainLoop` class.
+    - `MainLoop` - Defines `IMainLoopDriver` and implements the `MainLoop` class.
     - `ConsoleDriver` - Definition for the Console Driver API.
-    - `Events.cs` - Defines keyboard and mouse related structs & classes. 
+    - `Events.cs` - Defines keyboard and mouse-related structs & classes. 
     - `PosDim.cs` - Implements *Computed Layout* system. These classes have deep dependencies on `View`.
     - `Responder` - Base class for the windowing class hierarchy. Implements support for keyboard & mouse input.
     - `View` - Derived from `Responder`, the base class for non-modal visual elements such as controls.
     - `Toplevel` - Derived from `View`, the base class for modal visual elements such as top-level windows and dialogs. Supports the concept of `MenuBar` and `StatusBar`.
-    - `Window` - Derived from `TopLevel`; implements top level views with a visible frame and Title.
+    - `Window` - Derived from `TopLevel`; implements toplevel views with a visible frame and Title.
 - `Types/` - A folder (not namespace) containing implementations of `Point`, `Rect`, and `Size` which are ancient versions of the modern `System.Drawing.Point`, `System.Drawing.Size`, and `System.Drawning.Rectangle`.
 - `ConsoleDrivers/` - Source files for the three `ConsoleDriver`-based drivers: .NET: `NetDriver`, Unix & Mac: `UnixDriver`, and Windows: `WindowsDriver`.
 - `Views/` - A folder (not namespace) containing the source for all built-in classes that drive from `View` (non-modals). 
-- `Windows/` - A folder (not namespace) containing the source all built-in classes that derive from `Window`.
+- `Windows/` - A folder (not namespace) containing the source of all built-in classes that derive from `Window`.
 
 ## Version numbers
 
@@ -55,43 +55,36 @@ The `tag` must be of the form `v<major>.<minor>.<patch>`, e.g. `v2.3.4`.
 
 `patch` can indicate pre-release or not (e.g. `pre`, `beta`, `rc`, etc...). 
 
-### 1) Generate release notes with the list of PRs since the last release 
+### 1) Verify the `develop` branch is ready for release
 
-Use `gh` to get a list with just titles to make it easy to paste into release notes: 
-
-```powershell
-gh pr list --limit 500 --search "is:pr is:closed is:merged closed:>=2021-05-18"
-```
-
-Use the output to update `./Terminal.Gui/Terminal.Gui.csproj` with latest release notes
-
-### 2) Update the API documentation
-
-See `./docfx/README.md`.
+* Ensure everything is committed and pushed to the `develop` branch
+* Ensure your local `develop` branch is up-to-date with `upstream/develop`
 
-### 3) Create a PR for the release in the `develop` branch
+### 2) Create a pull request for the release in the `develop` branch
 
-The PR title should be "Release v2.3.4"
+The PR title should be of the form "Release v2.3.4"
 
 ```powershell
 git checkout develop
-git pull -all
+git pull upstream develop
 git checkout -b v_2_3_4
 git add .
 git commit -m "Release v2.3.4"
 git push
 ```
 
-### 4) On github.com, verify the build action worked on your fork, then merge the PR
+Go to the link printed by `git push` and fill out the Pull Request.
+
+### 3) On github.com, verify the build action worked on your fork, then merge the PR
 
-### 5) Pull the merged `develop` from `upstream`
+### 4) Pull the merged `develop` from `upstream`
 
 ```powershell
 git checkout develop
 git pull upstream develop
 ```
 
-### 6) Merge `develop` into `main`
+### 5) Merge `develop` into `main`
 
 ```powershell
 git checkout main
@@ -101,13 +94,13 @@ git merge develop
 
 Fix any merge errors.
 
-### 7) Create a new annotated tag for the release
+### 6) Create a new annotated tag for the release on `main`
 
 ```powershell
 git tag v2.3.4 -a -m "Release v2.3.4"
 ```       
 
-### 8) Push the new tag to `main` on `origin`
+### 7) Push the new tag to `main` on `upstream`
 
 ```powershell
 git push --atomic upstream main v2.3.4
@@ -115,16 +108,23 @@ git push --atomic upstream main v2.3.4
 
 *See https://stackoverflow.com/a/3745250/297526*
 
-### 9) Monitor Github actions to ensure the Nuget publishing worked.
+### 8) Monitor Github Actions to ensure the Nuget publishing worked.
 
-### 10) Check Nuget to see the new package version (wait a few minutes): 
+https://github.com/gui-cs/Terminal.Gui/actions
+
+### 9) Check Nuget to see the new package version (wait a few minutes) 
 https://www.nuget.org/packages/Terminal.Gui
 
-### 11) Add a new Release in Github: https://github.com/gui-cs/Terminal.Gui/releases
+### 10) Add a new Release in Github: https://github.com/gui-cs/Terminal.Gui/releases
+
+Generate release notes with the list of PRs since the last release 
 
-### 12) Tweet about it
+Use `gh` to get a list with just titles to make it easy to paste into release notes: 
 
-### 13) Update the `develop` branch
+```powershell
+gh pr list --limit 500 --search "is:pr is:closed is:merged closed:>=2021-05-18"
+```
+### 11) Update the `develop` branch with the new version
 
 ```powershell
 git checkout develop

+ 6 - 86
Terminal.Gui/Terminal.Gui.csproj

@@ -16,14 +16,15 @@
     <InformationalVersion>2.0</InformationalVersion>
   </PropertyGroup>
   <ItemGroup>
+    <PackageReference Include="Microsoft.DotNet.PlatformAbstractions" Version="3.1.6" />
     <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
-    <PackageReference Include="NStack.Core" Version="1.0.3" />
+    <PackageReference Include="NStack.Core" Version="1.0.7" />
     <InternalsVisibleTo Include="UnitTests" />
   </ItemGroup>
   <!-- Uncomment the RestoreSources element to have dotnet restore pull NStack from a local dir for testing -->
   <PropertyGroup>
     <!-- See https://stackoverflow.com/a/44463578/297526 -->
-    <!--<RestoreSources>$(RestoreSources);../../local-packages;https://api.nuget.org/v3/index.json</RestoreSources>-->
+    <!--<RestoreSources>$(RestoreSources);..\..\NStack\NStack\bin\Debug;https://api.nuget.org/v3/index.json</RestoreSources>-->
   </PropertyGroup>
   <!-- API Documentation -->
   <ItemGroup>
@@ -74,93 +75,12 @@
     <PackageIcon>logo.png</PackageIcon>
     <PackageReadmeFile>README.md</PackageReadmeFile>
     <PackageTags>csharp, terminal, c#, f#, gui, toolkit, console, tui</PackageTags>
-    <Description>Cross Platform Terminal UI toolkit for .NET</Description>
+    <Description>Cross platform Terminal UI toolkit for .NET</Description>
     <Owners>Miguel de Icaza, Charlie Kindel</Owners>
     <Summary>A toolkit for building rich console apps for .NET that works on Windows, Mac, and Linux/Unix.</Summary>
-    <Title>Terminal.Gui - Cross Platform Terminal user interface toolkit for .NET</Title>
+    <Title>Terminal.Gui - Cross platform Terminal User Interface (TUI) toolkit for .NET</Title>
     <PackageReleaseNotes>
-      Release v1.8.1
-      * Fixes #2053. MessageBox.Query not wrapping correctly 
-      
-      Release v1.8.0
-      * Fixes #2043. Update to NStack v1.0.3
-      * Fixes #2045. TrySetClipboardData test must be enclosed with a lock.
-      * Fixes #2025. API Docs are now generated via Github Action - View Source Works
-      * Fixes #1991. Broken link in README
-      * Fixes #2026. Added ClearOnVisibleFalse to flag if the view must be cleared or not.
-      * Fixes #2017 and #2013. MainLoopTests.InvokeLeakTest failures
-      * Fixes #2014. Application mouseGrabView is run twice if return true.
-      * Fixes #2011. Wizard no longer needs to set specific colors, because #1971 has been fixed.
-      * Fixes #2006. ProgressBarStyles isn't disposing the _fractionTimer on quitting if running.
-      * Fixes #2004. TextFormatter.Justified not adding the extra spaces.
-      * Fixes #2002. Added feature to fill the remaining width with spaces.
-      * Fixes #1999. Prevents the mouseGrabView being executed with a null view.
-      * Fixes #1994. BREAKING CHANGE. Ensure only a single IdleHandlers list can exist.
-      * Fixes #1979. MessageBox.Query not wrapping since 1.7.1
-      * Fixes #1989. ListView: Ensures SelectedItem visibility on MoveDown and MoveUp.
-      * Fixes #1987. Textview insert text newline fix
-      * Fixes #1984. Setting Label.Visible to false does not hide the Label
-      * Fixes #820. Added HideDropdownListOnClick property.
-      * Fixes #1981. Added SplitNewLine method to the TextFormatter.
-      * Fixes #1973. Avoid positioning Submenus off screen.
-      * Added abstract MakeColor and CreateColors to create the colors at once.
-      * Fixes #1800. TextView now uses the same colors as TextField.
-      * Fixes #1969. ESC on CursesDriver take to long to being processed.
-      * Fixes #1967. New keys for DeleteAll on TextField and TextView.
-      * Fixes #1962 - Change KeyBindings to allow chaining commands
-      * Fixes #1961 Null reference in Keybindings Scenario and hotkey collision
-      * Fixes #1963. Only remove one character on backspace when wordwrap is on
-      * Fixes #1959. GoToEnd should not fail on an empty TreeView
-      * Fixes #1953. TextView cursor position is not updating by mouse.
-      * Fixes #1951. TextView with selected text doesn't scroll beyond the cursor position.
-      * Fixes #1948. Get unwrapped cursor position when word wrap is enabled on TextView.
-      * Ensures that the isButtonShift flag is disabled in all situations.
-      * Fixes #1943. Mouse ButtonShift is not preserving the text selected.
-
-      Release v1.7.2
-      * Fixes #1773. Base color scheme for ListView hard to read
-      * Fixes #1934. WindowsDriver crash when the height is less than 1 with the VS Debugger
-
-      Release v1.7.1
-      * Fixes #1930. Trailing whitespace makes MessageBox.Query buttons disappear.
-      * Fixes #1921. Mouse continuous button pressed is not working on ScrollView.
-      * Fixes #1924. Wizard: Selected help text is unreadable
-
-      Release v1.7.0
-      * Moved Terminal.Gui (and NStack) to the github.com/gui-cs organization.
-      * Adds multi-step Wizard View for setup experiences (#124)
-      * The synchronization context method Send is now blocking (#1854).
-      * Fixes #1917. Sometimes Clipboard.IsSupported doesn't return the correct
-      * Fixes #1893: Fix URLs to match gui-cs Org
-      * Fixes #1883. Child TopLevels now get Loaded/Ready events.
-      * Fixes #1867, #1866, #1796. TextView enhancements for ReadOnly and WordWrap.
-      * Fixes #1861. Border: Title property is preferable to Text.
-      * Fixes #1855. Window and Frame content view without the margin frame.
-      * Fixes #1848. Mouse clicks in Windows Terminal.
-      * Fixes #1846. TabView now clips to the draw bounds.
-      * Fix TableView multi selections extending to -1 indexes
-      * Fixes #1837. Setting Unix clipboard freezes.
-      * Fixes #1839. Process WindowsDriver click event if location is the same after pressed and released.
-      * Fixes #1830. If "libcoreclr.so" is not present then "libncursesw.so" will be used.
-      * Fixes #1816. MessageBox: Hides underlying dialog when visible
-      * Fixes #1815. Now returns false if WSL clipboard isn't supported.
-      * Fixes #1825. Parent MenuItem stay focused if child MenuItem is empty.
-      * Fixes #1812, #1797, #1791. AutoSize fixes.
-      * Fixes #1818. Adds Title change events to Window.
-      * Fixes #1810. Dialog: Closing event is not fired when ESC is pressed to close dialog.
-      * Fixes #1793. ScrollBarView is hiding if the host fit the available space.
-      * Added Pos/Dim Function feature to automate layout.
-      * Fixes #1786. Windows Terminal is reporting well on mouse button pressed + mouse movement.
-      * Fixes #1777 - Dialog button justification. Adds unit tests.
-      * Fixes #1739. Setting menu UseKeysUpDownAsKeysLeftRight as false by default.
-      * Fixes #1772. Avoids WindowsDriver flickering when resizing.
-      * Fixed TableView always showing selected cell(s) even when not focused
-      * Fixes #1769. Supports a minimum view size for non-automatic size views.
-      * Exposes APIs to support upcoming Web console feature
-      * Fixes some scrolling performance issues
-      * Fixes #1763. Allowing read console inputs before idle handlers.
-      * TableView unicode scenario usability
-      * Added unicode testing code to TableEditor
+      See: https://github.com/gui-cs/Terminal.Gui/releases
     </PackageReleaseNotes>
   </PropertyGroup>
 </Project>

+ 1 - 2
Terminal.Gui/Views/Button.cs

@@ -248,8 +248,7 @@ namespace Terminal.Gui {
 		///<inheritdoc/>
 		public override bool MouseEvent (MouseEvent me)
 		{
-			if (me.Flags == MouseFlags.Button1Clicked || me.Flags == MouseFlags.Button1DoubleClicked ||
-				me.Flags == MouseFlags.Button1TripleClicked) {
+			if (me.Flags == MouseFlags.Button1Clicked) {
 				if (CanFocus && Enabled) {
 					if (!HasFocus) {
 						SetFocus ();

+ 7 - 2
Terminal.Gui/Views/ComboBox.cs

@@ -749,7 +749,7 @@ namespace Terminal.Gui {
 			}
 
 			SetValue (searchset [listview.SelectedItem]);
-			search.CursorPosition = search.Text.RuneCount;
+			search.CursorPosition = search.Text.ConsoleWidth;
 			Search_Changed (search.Text);
 			OnOpenSelectedItem ();
 			Reset (keepSearchText: true);
@@ -825,7 +825,12 @@ namespace Terminal.Gui {
 				}
 			}
 
-			ShowList ();
+			if (HasFocus) {
+				ShowList ();
+			} else if (autoHide) {
+				isShow = false;
+				HideList ();
+			}
 		}
 
 		/// <summary>

+ 43 - 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,15 +44,18 @@ 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)
 		{
 			if (IsShow) {
-				Hide ();
+				if (menuBar.SuperView != null) {
+					Hide ();
+				}
+				IsShow = false;
 			}
 			MenuItems = menuItems;
 			Position = new Point (x, y);
@@ -48,7 +67,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Disposes the all the context menu objects instances.
+		/// Disposes the context menu object.
 		/// </summary>
 		public void Dispose ()
 		{
@@ -65,7 +84,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 ()
 		{
@@ -116,7 +135,8 @@ namespace Terminal.Gui {
 				Y = position.Y,
 				Width = 0,
 				Height = 0,
-				UseSubMenusSingleFrame = UseSubMenusSingleFrame
+				UseSubMenusSingleFrame = UseSubMenusSingleFrame,
+				Key = Key
 			};
 
 			menuBar.isContextMenuLoading = true;
@@ -138,7 +158,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Close the <see cref="MenuItems"/> menu items.
+		/// Hides (closes) the ContextMenu.
 		/// </summary>
 		public void Hide ()
 		{
@@ -157,7 +177,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 +187,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 +199,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 +211,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 +222,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 +234,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; }
 	}

+ 7 - 1
Terminal.Gui/Views/GraphView.cs

@@ -240,7 +240,13 @@ namespace Terminal.Gui {
 				);
 		}
 
-
+		/// <inheritdoc/>
+		/// <remarks>Also ensures that cursor is invisible after entering the <see cref="GraphView"/>.</remarks>
+		public override bool OnEnter (View view)
+		{
+			Driver.SetCursorVisibility (CursorVisibility.Invisible);
+			return base.OnEnter (view);
+		}
 
 		/// <inheritdoc/>
 		public override bool ProcessKey (KeyEvent keyEvent)

+ 124 - 118
Terminal.Gui/Views/ListView.cs

@@ -1,25 +1,7 @@
-//
-// ListView.cs: ListView control
-//
-// Authors:
-//   Miguel de Icaza ([email protected])
-//
-//
-// TODO:
-//   - Should we support multiple columns, if so, how should that be done?
-//   - Show mark for items that have been marked.
-//   - Mouse support
-//   - Scrollbars?
-//
-// Column considerations:
-//   - Would need a way to specify widths
-//   - Should it automatically extract data out of structs/classes based on public fields/properties?
-//   - It seems that this would be useful just for the "simple" API, not the IListDAtaSource, as that one has full support for it.
-//   - Should a function be specified that retrieves the individual elements?
-//
 using System;
 using System.Collections;
 using System.Collections.Generic;
+using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using NStack;
@@ -59,7 +41,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Should return whether the specified item is currently marked.
 		/// </summary>
-		/// <returns><c>true</c>, if marked, <c>false</c> otherwise.</returns>
+		/// <returns><see langword="true"/>, if marked, <see langword="false"/> otherwise.</returns>
 		/// <param name="item">Item index.</param>
 		bool IsMarked (int item);
 
@@ -67,7 +49,7 @@ namespace Terminal.Gui {
 		/// Flags the item as marked.
 		/// </summary>
 		/// <param name="item">Item index.</param>
-		/// <param name="value">If set to <c>true</c> value.</param>
+		/// <param name="value">If set to <see langword="true"/> value.</param>
 		void SetMark (int item, bool value);
 
 		/// <summary>
@@ -89,8 +71,8 @@ namespace Terminal.Gui {
 	/// <para>
 	///   By default <see cref="ListView"/> uses <see cref="object.ToString"/> to render the items of any
 	///   <see cref="IList"/> object (e.g. arrays, <see cref="List{T}"/>,
-	///   and other collections). Alternatively, an object that implements the <see cref="IListDataSource"/>
-	///   interface can be provided giving full control of what is rendered.
+	///   and other collections). Alternatively, an object that implements <see cref="IListDataSource"/>
+	///   can be provided giving full control of what is rendered.
 	/// </para>
 	/// <para>
 	///   <see cref="ListView"/> can display any object that implements the <see cref="IList"/> interface.
@@ -107,6 +89,10 @@ namespace Terminal.Gui {
 	///   [x] or [ ] and bind the SPACE key to toggle the selection. To implement a different
 	///   marking style set <see cref="AllowsMarking"/> to false and implement custom rendering.
 	/// </para>
+	/// <para>
+	///   Searching the ListView with the keyboard is supported. Users type the
+	///   first characters of an item, and the first item that starts with what the user types will be selected.
+	/// </para>
 	/// </remarks>
 	public class ListView : View {
 		int top, left;
@@ -124,6 +110,7 @@ namespace Terminal.Gui {
 			get => source;
 			set {
 				source = value;
+				KeystrokeNavigator.Collection = source?.ToList ()?.Cast<object> ();
 				top = 0;
 				selected = 0;
 				lastSelectedItem = -1;
@@ -169,22 +156,28 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Gets or sets whether this <see cref="ListView"/> allows items to be marked.
 		/// </summary>
-		/// <value><c>true</c> if allows marking elements of the list; otherwise, <c>false</c>.
-		/// </value>
+		/// <value>Set to <see langword="true"/> to allow marking elements of the list.</value>
 		/// <remarks>
-		/// If set to true, <see cref="ListView"/> will render items marked items with "[x]", and unmarked items with "[ ]"
-		/// spaces. SPACE key will toggle marking.
+		/// If set to <see langword="true"/>, <see cref="ListView"/> will render items marked items with "[x]", and unmarked items with "[ ]"
+		/// spaces. SPACE key will toggle marking. The default is <see langword="false"/>.
 		/// </remarks>
 		public bool AllowsMarking {
 			get => allowsMarking;
 			set {
 				allowsMarking = value;
+				if (allowsMarking) {
+					AddKeyBinding (Key.Space, Command.ToggleChecked);
+				} else {
+					ClearKeybinding (Key.Space);
+				}
+
 				SetNeedsDisplay ();
 			}
 		}
 
 		/// <summary>
-		/// If set to true allows more than one item to be selected. If false only allow one item selected.
+		/// If set to <see langword="true"/> more than one item can be selected. If <see langword="false"/> selecting
+		/// an item will cause all others to be un-selected. The default is <see langword="false"/>.
 		/// </summary>
 		public bool AllowsMultipleSelection {
 			get => allowsMultipleSelection;
@@ -198,6 +191,7 @@ namespace Terminal.Gui {
 						}
 					}
 				}
+				SetNeedsDisplay ();
 			}
 		}
 
@@ -219,7 +213,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Gets or sets the left column where the item start to be displayed at on the <see cref="ListView"/>.
+		/// Gets or sets the leftmost column that is currently visible (when scrolling horizontally).
 		/// </summary>
 		/// <value>The left position.</value>
 		public int LeftItem {
@@ -236,7 +230,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Gets the widest item.
+		/// Gets the widest item in the list.
 		/// </summary>
 		public int Maxlength => (source?.Length) ?? 0;
 
@@ -264,10 +258,12 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Initializes a new instance of <see cref="ListView"/> that will display the contents of the object implementing the <see cref="IList"/> interface, 
+		/// Initializes a new instance of <see cref="ListView"/> that will display the 
+		/// contents of the object implementing the <see cref="IList"/> interface, 
 		/// with relative positioning.
 		/// </summary>
-		/// <param name="source">An <see cref="IList"/> data source, if the elements are strings or ustrings, the string is rendered, otherwise the ToString() method is invoked on the result.</param>
+		/// <param name="source">An <see cref="IList"/> data source, if the elements are strings or ustrings, 
+		/// the string is rendered, otherwise the ToString() method is invoked on the result.</param>
 		public ListView (IList source) : this (MakeWrapper (source))
 		{
 		}
@@ -296,7 +292,8 @@ namespace Terminal.Gui {
 		/// Initializes a new instance of <see cref="ListView"/> that will display the contents of the object implementing the <see cref="IList"/> interface with an absolute position.
 		/// </summary>
 		/// <param name="rect">Frame for the listview.</param>
-		/// <param name="source">An IList data source, if the elements of the IList are strings or ustrings, the string is rendered, otherwise the ToString() method is invoked on the result.</param>
+		/// <param name="source">An IList data source, if the elements of the IList are strings or ustrings, 
+		/// the string is rendered, otherwise the ToString() method is invoked on the result.</param>
 		public ListView (Rect rect, IList source) : this (rect, MakeWrapper (source))
 		{
 			Initialize ();
@@ -306,7 +303,9 @@ namespace Terminal.Gui {
 		/// Initializes a new instance of <see cref="ListView"/> with the provided data source and an absolute position
 		/// </summary>
 		/// <param name="rect">Frame for the listview.</param>
-		/// <param name="source">IListDataSource object that provides a mechanism to render the data. The number of elements on the collection should not change, if you must change, set the "Source" property to reset the internal settings of the ListView.</param>
+		/// <param name="source">IListDataSource object that provides a mechanism to render the data. 
+		/// The number of elements on the collection should not change, if you must change, 
+		/// set the "Source" property to reset the internal settings of the ListView.</param>
 		public ListView (Rect rect, IListDataSource source) : base (rect)
 		{
 			this.source = source;
@@ -331,13 +330,13 @@ namespace Terminal.Gui {
 			AddCommand (Command.ToggleChecked, () => MarkUnmarkRow ());
 
 			// Default keybindings for all ListViews
-			AddKeyBinding (Key.CursorUp,Command.LineUp);
+			AddKeyBinding (Key.CursorUp, Command.LineUp);
 			AddKeyBinding (Key.P | Key.CtrlMask, Command.LineUp);
 
 			AddKeyBinding (Key.CursorDown, Command.LineDown);
 			AddKeyBinding (Key.N | Key.CtrlMask, Command.LineDown);
 
-			AddKeyBinding(Key.PageUp,Command.PageUp);
+			AddKeyBinding (Key.PageUp, Command.PageUp);
 
 			AddKeyBinding (Key.PageDown, Command.PageDown);
 			AddKeyBinding (Key.V | Key.CtrlMask, Command.PageDown);
@@ -347,8 +346,6 @@ namespace Terminal.Gui {
 			AddKeyBinding (Key.End, Command.BottomEnd);
 
 			AddKeyBinding (Key.Enter, Command.OpenSelectedItem);
-
-			AddKeyBinding (Key.Space, Command.ToggleChecked);
 		}
 
 		///<inheritdoc/>
@@ -386,7 +383,8 @@ namespace Terminal.Gui {
 						Driver.SetAttribute (current);
 					}
 					if (allowsMarking) {
-						Driver.AddRune (source.IsMarked (item) ? (AllowsMultipleSelection ? Driver.Checked : Driver.Selected) : (AllowsMultipleSelection ? Driver.UnChecked : Driver.UnSelected));
+						Driver.AddRune (source.IsMarked (item) ? (AllowsMultipleSelection ? Driver.Checked : Driver.Selected) :
+							(AllowsMultipleSelection ? Driver.UnChecked : Driver.UnSelected));
 						Driver.AddRune (' ');
 					}
 					Source.Render (this, Driver, isSelected, item, col, row, f.Width - col, start);
@@ -409,23 +407,43 @@ namespace Terminal.Gui {
 		/// </summary>
 		public event Action<ListViewRowEventArgs> RowRender;
 
+		/// <summary>
+		/// Gets the <see cref="CollectionNavigator"/> that searches the <see cref="ListView.Source"/> collection as
+		/// the user types.
+		/// </summary>
+		public CollectionNavigator KeystrokeNavigator { get; private set; } = new CollectionNavigator ();
+
 		///<inheritdoc/>
 		public override bool ProcessKey (KeyEvent kb)
 		{
-			if (source == null)
+			if (source == null) {
 				return base.ProcessKey (kb);
+			}
 
 			var result = InvokeKeybindings (kb);
-			if (result != null)
+			if (result != null) {
 				return (bool)result;
+			}
+
+			// Enable user to find & select an item by typing text
+			if (CollectionNavigator.IsCompatibleKey (kb)) {
+				var newItem = KeystrokeNavigator?.GetNextMatchingItem (SelectedItem, (char)kb.KeyValue);
+				if (newItem is int && newItem != -1) {
+					SelectedItem = (int)newItem;
+					EnsureSelectedItemVisible ();
+					SetNeedsDisplay ();
+					return true;
+				}
+			}
 
 			return false;
 		}
 
 		/// <summary>
-		/// Prevents marking if it's not allowed mark and if it's not allows multiple selection.
+		/// If <see cref="AllowsMarking"/> and <see cref="AllowsMultipleSelection"/> are both <see langword="true"/>,
+		/// unmarks all marked items other than the currently selected. 
 		/// </summary>
-		/// <returns></returns>
+		/// <returns><see langword="true"/> if unmarking was successful.</returns>
 		public virtual bool AllowsAll ()
 		{
 			if (!allowsMarking)
@@ -442,9 +460,9 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Marks an unmarked row.
+		/// Marks the <see cref="SelectedItem"/> if it is not already marked.
 		/// </summary>
-		/// <returns></returns>
+		/// <returns><see langword="true"/> if the <see cref="SelectedItem"/> was marked.</returns>
 		public virtual bool MarkUnmarkRow ()
 		{
 			if (AllowsAll ()) {
@@ -457,7 +475,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Moves the selected item index to the next page.
+		/// Changes the <see cref="SelectedItem"/> to the item at the top of the visible list.
 		/// </summary>
 		/// <returns></returns>
 		public virtual bool MovePageUp ()
@@ -476,7 +494,8 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Moves the selected item index to the previous page.
+		/// Changes the <see cref="SelectedItem"/> to the item just below the bottom 
+		/// of the visible list, scrolling if needed.
 		/// </summary>
 		/// <returns></returns>
 		public virtual bool MovePageDown ()
@@ -498,7 +517,8 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Moves the selected item index to the next row.
+		/// Changes the <see cref="SelectedItem"/> to the next item in the list, 
+		/// scrolling the list if needed.
 		/// </summary>
 		/// <returns></returns>
 		public virtual bool MoveDown ()
@@ -538,7 +558,8 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Moves the selected item index to the previous row.
+		/// Changes the <see cref="SelectedItem"/> to the previous item in the list, 
+		/// scrolling the list if needed.
 		/// </summary>
 		/// <returns></returns>
 		public virtual bool MoveUp ()
@@ -574,7 +595,8 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Moves the selected item index to the last row.
+		/// Changes the <see cref="SelectedItem"/> to last item in the list, 
+		/// scrolling the list if needed.
 		/// </summary>
 		/// <returns></returns>
 		public virtual bool MoveEnd ()
@@ -592,7 +614,8 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Moves the selected item index to the first row.
+		/// Changes the <see cref="SelectedItem"/> to the first item in the list, 
+		/// scrolling the list if needed.
 		/// </summary>
 		/// <returns></returns>
 		public virtual bool MoveHome ()
@@ -608,23 +631,23 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Scrolls the view down.
+		/// Scrolls the view down by <paramref name="items"/> items.
 		/// </summary>
-		/// <param name="lines">Number of lines to scroll down.</param>
-		public virtual bool ScrollDown (int lines)
+		/// <param name="items">Number of items to scroll down.</param>
+		public virtual bool ScrollDown (int items)
 		{
-			top = Math.Max (Math.Min (top + lines, source.Count - 1), 0);
+			top = Math.Max (Math.Min (top + items, source.Count - 1), 0);
 			SetNeedsDisplay ();
 			return true;
 		}
 
 		/// <summary>
-		/// Scrolls the view up.
+		/// Scrolls the view up by <paramref name="items"/> items.
 		/// </summary>
-		/// <param name="lines">Number of lines to scroll up.</param>
-		public virtual bool ScrollUp (int lines)
+		/// <param name="items">Number of items to scroll up.</param>
+		public virtual bool ScrollUp (int items)
 		{
-			top = Math.Max (top - lines, 0);
+			top = Math.Max (top - items, 0);
 			SetNeedsDisplay ();
 			return true;
 		}
@@ -655,7 +678,7 @@ namespace Terminal.Gui {
 		private bool allowsMultipleSelection = true;
 
 		/// <summary>
-		/// Invokes the SelectedChanged event if it is defined.
+		/// Invokes the <see cref="SelectedItemChanged"/> event if it is defined.
 		/// </summary>
 		/// <returns></returns>
 		public virtual bool OnSelectedChanged ()
@@ -673,7 +696,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Invokes the OnOpenSelectedItem event if it is defined.
+		/// Invokes the <see cref="OpenSelectedItem"/> event if it is defined.
 		/// </summary>
 		/// <returns></returns>
 		public virtual bool OnOpenSelectedItem ()
@@ -704,8 +727,7 @@ namespace Terminal.Gui {
 			Application.Driver.SetCursorVisibility (CursorVisibility.Invisible);
 
 			if (lastSelectedItem == -1) {
-				EnsuresVisibilitySelectedItem ();
-				OnSelectedChanged ();
+				EnsureSelectedItemVisible ();
 			}
 
 			return base.OnEnter (view);
@@ -721,7 +743,10 @@ namespace Terminal.Gui {
 			return base.OnLeave (view);
 		}
 
-		void EnsuresVisibilitySelectedItem ()
+		/// <summary>
+		/// Ensures the selected item is always visible on the screen.
+		/// </summary>
+		public void EnsureSelectedItemVisible ()
 		{
 			SuperView?.LayoutSubviews ();
 			if (selected < top) {
@@ -788,23 +813,15 @@ namespace Terminal.Gui {
 
 			return true;
 		}
-
-
 	}
 
-	/// <summary>
-	/// Implements an <see cref="IListDataSource"/> that renders arbitrary <see cref="IList"/> instances for <see cref="ListView"/>.
-	/// </summary>
-	/// <remarks>Implements support for rendering marked items.</remarks>
+	/// <inheritdoc/>
 	public class ListWrapper : IListDataSource {
 		IList src;
 		BitArray marks;
 		int count, len;
 
-		/// <summary>
-		/// Initializes a new instance of <see cref="ListWrapper"/> given an <see cref="IList"/>
-		/// </summary>
-		/// <param name="source"></param>
+		/// <inheritdoc/>
 		public ListWrapper (IList source)
 		{
 			if (source != null) {
@@ -815,14 +832,10 @@ namespace Terminal.Gui {
 			}
 		}
 
-		/// <summary>
-		/// Gets the number of items in the <see cref="IList"/>.
-		/// </summary>
+		/// <inheritdoc/>
 		public int Count => src != null ? src.Count : 0;
 
-		/// <summary>
-		/// Gets the maximum item length in the <see cref="IList"/>.
-		/// </summary>
+		/// <inheritdoc/>
 		public int Length => len;
 
 		int GetMaxLengthItem ()
@@ -836,7 +849,7 @@ namespace Terminal.Gui {
 				var t = src [i];
 				int l;
 				if (t is ustring u) {
-					l = u.RuneCount;
+					l = TextFormatter.GetTextWidth (u);
 				} else if (t is string s) {
 					l = s.Length;
 				} else {
@@ -853,33 +866,15 @@ namespace Terminal.Gui {
 
 		void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width, int start = 0)
 		{
-			int byteLen = ustr.Length;
-			int used = 0;
-			for (int i = start; i < byteLen;) {
-				(var rune, var size) = Utf8.DecodeRune (ustr, i, i - byteLen);
-				var count = Rune.ColumnWidth (rune);
-				if (used + count > width)
-					break;
-				driver.AddRune (rune);
-				used += count;
-				i += size;
-			}
-			for (; used < width; used++) {
+			var u = TextFormatter.ClipAndJustify (ustr, width, TextAlignment.Left);
+			driver.AddStr (u);
+			width -= TextFormatter.GetTextWidth (u);
+			while (width-- > 0) {
 				driver.AddRune (' ');
 			}
 		}
 
-		/// <summary>
-		/// Renders a <see cref="ListView"/> item to the appropriate type.
-		/// </summary>
-		/// <param name="container">The ListView.</param>
-		/// <param name="driver">The driver used by the caller.</param>
-		/// <param name="marked">Informs if it's marked or not.</param>
-		/// <param name="item">The item.</param>
-		/// <param name="col">The col where to move.</param>
-		/// <param name="line">The line where to move.</param>
-		/// <param name="width">The item width.</param>
-		/// <param name="start">The index of the string to be displayed.</param>
+		/// <inheritdoc/>
 		public void Render (ListView container, ConsoleDriver driver, bool marked, int item, int col, int line, int width, int start = 0)
 		{
 			container.Move (col, line);
@@ -897,11 +892,7 @@ namespace Terminal.Gui {
 			}
 		}
 
-		/// <summary>
-		/// Returns true if the item is marked, false otherwise.
-		/// </summary>
-		/// <param name="item">The item.</param>
-		/// <returns><c>true</c>If is marked.<c>false</c>otherwise.</returns>
+		/// <inheritdoc/>
 		public bool IsMarked (int item)
 		{
 			if (item >= 0 && item < count)
@@ -909,25 +900,40 @@ namespace Terminal.Gui {
 			return false;
 		}
 
-		/// <summary>
-		/// Sets the item as marked or unmarked based on the value is true or false, respectively.
-		/// </summary>
-		/// <param name="item">The item</param>
-		/// <param name="value"><true>Marks the item.</true><false>Unmarked the item.</false>The value.</param>
+		/// <inheritdoc/>
 		public void SetMark (int item, bool value)
 		{
 			if (item >= 0 && item < count)
 				marks [item] = value;
 		}
 
-		/// <summary>
-		/// Returns the source as IList.
-		/// </summary>
-		/// <returns></returns>
+		/// <inheritdoc/>
 		public IList ToList ()
 		{
 			return src;
 		}
+
+		/// <inheritdoc/>
+		public int StartsWith (string search)
+		{
+			if (src == null || src?.Count == 0) {
+				return -1;
+			}
+
+			for (int i = 0; i < src.Count; i++) {
+				var t = src [i];
+				if (t is ustring u) {
+					if (u.ToUpper ().StartsWith (search.ToUpperInvariant ())) {
+						return i;
+					}
+				} else if (t is string s) {
+					if (s.StartsWith (search, StringComparison.InvariantCultureIgnoreCase)) {
+						return i;
+					}
+				}
+			}
+			return -1;
+		}
 	}
 
 	/// <summary>

+ 149 - 160
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 ()
@@ -688,6 +690,7 @@ namespace Terminal.Gui {
 				}
 			} while (barItems.Children [current] == null || disabled);
 			SetNeedsDisplay ();
+			SetParentSetNeedsDisplay ();
 			if (!host.UseSubMenusSingleFrame)
 				host.OnMenuOpened ();
 			return true;
@@ -735,11 +738,24 @@ namespace Terminal.Gui {
 				}
 			} while (barItems.Children [current] == null || disabled);
 			SetNeedsDisplay ();
+			SetParentSetNeedsDisplay ();
 			if (!host.UseSubMenusSingleFrame)
 				host.OnMenuOpened ();
 			return true;
 		}
 
+		private void SetParentSetNeedsDisplay ()
+		{
+			if (host.openSubMenu != null) {
+				foreach (var menu in host.openSubMenu) {
+					menu.SetNeedsDisplay ();
+				}
+			}
+
+			host?.openMenu.SetNeedsDisplay ();
+			host.SetNeedsDisplay ();
+		}
+
 		public override bool MouseEvent (MouseEvent me)
 		{
 			if (!host.handled && !host.HandleGrabView (me, this)) {
@@ -756,6 +772,7 @@ namespace Terminal.Gui {
 					return true;
 				var item = barItems.Children [meY];
 				if (item == null || !item.IsEnabled ()) disabled = true;
+				if (disabled) return true;
 				current = meY;
 				if (item != null && !disabled)
 					RunSelected ();
@@ -775,6 +792,7 @@ namespace Terminal.Gui {
 					current = me.Y - 1;
 				if (host.UseSubMenusSingleFrame || !CheckSubMenu ()) {
 					SetNeedsDisplay ();
+					SetParentSetNeedsDisplay ();
 					return true;
 				}
 				host.OnMenuOpened ();
@@ -803,6 +821,7 @@ namespace Terminal.Gui {
 				return host.CloseMenu (false, true);
 			} else {
 				SetNeedsDisplay ();
+				SetParentSetNeedsDisplay ();
 			}
 			return true;
 		}
@@ -832,17 +851,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. See <see cref="MenuItem.HotKey"/>.
 	///	</para>
 	///	<para>
-	///	The <see cref="MenuBar"/> provides global hotkeys for the application.
+	///	See also: <see cref="ContextMenu"/>
 	///	</para>
 	/// </remarks>
 	public class MenuBar : View {
@@ -850,7 +879,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 +902,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 +922,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 +941,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 +1065,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 +1082,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 +1090,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) {
-					hotColor = ColorScheme.HotNormal;
-					normalColor = GetNormalColor ();
+					normalColor = i == selected ? ColorScheme.Focus : GetNormalColor ();
 				} else {
-					hotColor = GetNormalColor ();
+					hotColor = ColorScheme.HotNormal;
 					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 +1112,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 +1153,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 +1176,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; }
 
@@ -1156,7 +1198,9 @@ namespace Terminal.Gui {
 		public virtual void OnMenuOpened ()
 		{
 			MenuItem mi = null;
-			if (openCurrentMenu.barItems.Children != null && openCurrentMenu?.current > -1) {
+			if (openCurrentMenu.barItems.Children != null && openCurrentMenu.barItems.Children.Length > 0
+				&& openCurrentMenu?.current > -1) {
+
 				mi = openCurrentMenu.barItems.Children [openCurrentMenu.current];
 			} else if (openCurrentMenu.barItems.IsTopLevel) {
 				mi = openCurrentMenu.barItems;
@@ -1167,7 +1211,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 +1224,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 +1234,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 +1252,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 +1265,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 +1281,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 +1322,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 +1404,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 +1506,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;
 		}
 
@@ -1577,7 +1605,7 @@ namespace Terminal.Gui {
 					var subMenu = openCurrentMenu.current > -1 && openCurrentMenu.barItems.Children.Length > 0
 						? openCurrentMenu.barItems.SubMenu (openCurrentMenu.barItems.Children [openCurrentMenu.current])
 						: null;
-					if ((selectedSub == -1 || openSubMenu == null || openSubMenu?.Count == selectedSub) && subMenu == null) {
+					if ((selectedSub == -1 || openSubMenu == null || openSubMenu?.Count - 1 == selectedSub) && subMenu == null) {
 						if (openSubMenu != null && !CloseMenu (false, true))
 							return;
 						NextMenu (false, ignoreUseSubMenusSingleFrame);
@@ -1678,7 +1706,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 +1801,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 +1833,7 @@ namespace Terminal.Gui {
 						}
 						return true;
 					}
-					pos += 1 + Menus [i].TitleLength + 2;
+					pos += leftPadding + Menus [i].TitleLength + rightPadding;
 				}
 			}
 			return false;
@@ -1878,47 +1906,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 +1959,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 +1984,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 +1994,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)
 		{

+ 34 - 5
Terminal.Gui/Views/RadioGroup.cs

@@ -67,6 +67,7 @@ namespace Terminal.Gui {
 				Frame = rect;
 			}
 			CanFocus = true;
+			HotKeySpecifier = new Rune ('_');
 
 			// Things this view knows how to do
 			AddCommand (Command.LineUp, () => { MoveUp (); return true; });
@@ -215,9 +216,36 @@ namespace Terminal.Gui {
 					Move (horizontal [i].pos, 0);
 					break;
 				}
+				var rl = radioLabels [i];
 				Driver.SetAttribute (GetNormalColor ());
 				Driver.AddStr (ustring.Make (new Rune [] { i == selected ? Driver.Selected : Driver.UnSelected, ' ' }));
-				DrawHotString (radioLabels [i], HasFocus && i == cursor, ColorScheme);
+				TextFormatter.FindHotKey (rl, HotKeySpecifier, true, out int hotPos, out Key hotKey);
+				if (hotPos != -1 && (hotKey != Key.Null || hotKey != Key.Unknown)) {
+					var rlRunes = rl.ToRunes ();
+					for (int j = 0; j < rlRunes.Length; j++) {
+						Rune rune = rlRunes [j];
+						if (j == hotPos && i == cursor) {
+							Application.Driver.SetAttribute (HasFocus ? ColorScheme.HotFocus : GetHotNormalColor ());
+						} else if (j == hotPos && i != cursor) {
+							Application.Driver.SetAttribute (GetHotNormalColor ());
+						} else if (HasFocus && i == cursor) {
+							Application.Driver.SetAttribute (ColorScheme.Focus);
+						}
+						if (rune == HotKeySpecifier && j + 1 < rlRunes.Length) {
+							j++;
+							rune = rlRunes [j];
+							if (i == cursor) {
+								Application.Driver.SetAttribute (HasFocus ? ColorScheme.HotFocus : GetHotNormalColor ());
+							} else if (i != cursor) {
+								Application.Driver.SetAttribute (GetHotNormalColor ());
+							}
+						}
+						Application.Driver.AddRune (rune);
+						Driver.SetAttribute (GetNormalColor ());
+					}
+				} else {
+					DrawHotString (rl, HasFocus && i == cursor, ColorScheme);
+				}
 			}
 		}
 
@@ -280,11 +308,12 @@ namespace Terminal.Gui {
 				key = Char.ToUpper ((char)key);
 				foreach (var l in radioLabels) {
 					bool nextIsHot = false;
-					foreach (var c in l) {
-						if (c == '_')
+					TextFormatter.FindHotKey (l, HotKeySpecifier, true, out _, out Key hotKey);
+					foreach (Rune c in l) {
+						if (c == HotKeySpecifier) {
 							nextIsHot = true;
-						else {
-							if (nextIsHot && c == key) {
+						} else {
+							if ((nextIsHot && Rune.ToUpper (c) == key) || (key == (uint)hotKey)) {
 								SelectedItem = i;
 								cursor = i;
 								if (!HasFocus)

+ 14 - 14
Terminal.Gui/Views/ScrollBarView.cs

@@ -462,7 +462,7 @@ namespace Terminal.Gui {
 				return;
 			}
 
-			Driver.SetAttribute (GetNormalColor ());
+			Driver.SetAttribute (Host.HasFocus ? ColorScheme.Focus : GetNormalColor ());
 
 			if ((vertical && Bounds.Height == 0) || (!vertical && Bounds.Width == 0)) {
 				return;
@@ -613,13 +613,13 @@ namespace Terminal.Gui {
 		int posBarOffset;
 
 		///<inheritdoc/>
-		public override bool MouseEvent (MouseEvent me)
+		public override bool MouseEvent (MouseEvent mouseEvent)
 		{
-			if (me.Flags != MouseFlags.Button1Pressed && me.Flags != MouseFlags.Button1DoubleClicked &&
-				!me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) &&
-				me.Flags != MouseFlags.Button1Released && me.Flags != MouseFlags.WheeledDown &&
-				me.Flags != MouseFlags.WheeledUp && me.Flags != MouseFlags.WheeledRight &&
-				me.Flags != MouseFlags.WheeledLeft && me.Flags != MouseFlags.Button1TripleClicked) {
+			if (mouseEvent.Flags != MouseFlags.Button1Pressed && mouseEvent.Flags != MouseFlags.Button1DoubleClicked &&
+				!mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) &&
+				mouseEvent.Flags != MouseFlags.Button1Released && mouseEvent.Flags != MouseFlags.WheeledDown &&
+				mouseEvent.Flags != MouseFlags.WheeledUp && mouseEvent.Flags != MouseFlags.WheeledRight &&
+				mouseEvent.Flags != MouseFlags.WheeledLeft && mouseEvent.Flags != MouseFlags.Button1TripleClicked) {
 				return false;
 			}
 
@@ -630,24 +630,24 @@ namespace Terminal.Gui {
 				Host.SetFocus ();
 			}
 
-			int location = vertical ? me.Y : me.X;
+			int location = vertical ? mouseEvent.Y : mouseEvent.X;
 			int barsize = vertical ? Bounds.Height : Bounds.Width;
 			int posTopLeftTee = vertical ? posTopTee + 1 : posLeftTee + 1;
 			int posBottomRightTee = vertical ? posBottomTee + 1 : posRightTee + 1;
 			barsize -= 2;
 			var pos = Position;
 
-			if (me.Flags != MouseFlags.Button1Released
+			if (mouseEvent.Flags != MouseFlags.Button1Released
 				&& (Application.MouseGrabView == null || Application.MouseGrabView != this)) {
 				Application.GrabMouse (this);
-			} else if (me.Flags == MouseFlags.Button1Released && Application.MouseGrabView != null && Application.MouseGrabView == this) {
+			} else if (mouseEvent.Flags == MouseFlags.Button1Released && Application.MouseGrabView != null && Application.MouseGrabView == this) {
 				lastLocation = -1;
 				Application.UngrabMouse ();
 				return true;
 			}
-			if (showScrollIndicator && (me.Flags == MouseFlags.WheeledDown || me.Flags == MouseFlags.WheeledUp ||
-				me.Flags == MouseFlags.WheeledRight || me.Flags == MouseFlags.WheeledLeft)) {
-				return Host.MouseEvent (me);
+			if (showScrollIndicator && (mouseEvent.Flags == MouseFlags.WheeledDown || mouseEvent.Flags == MouseFlags.WheeledUp ||
+				mouseEvent.Flags == MouseFlags.WheeledRight || mouseEvent.Flags == MouseFlags.WheeledLeft)) {
+				return Host.MouseEvent (mouseEvent);
 			}
 
 			if (location == 0) {
@@ -668,7 +668,7 @@ namespace Terminal.Gui {
 				//}
 
 				if (lastLocation > -1 || (location >= posTopLeftTee && location <= posBottomRightTee
-				&& me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))) {
+				&& mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))) {
 					if (lastLocation == -1) {
 						lastLocation = location;
 						posBarOffset = keepContentAlwaysInViewport ? Math.Max (location - posTopLeftTee, 1) : 0;

+ 25 - 15
Terminal.Gui/Views/ScrollView.cs

@@ -13,7 +13,6 @@
 
 using System;
 using System.Linq;
-using System.Reflection;
 
 namespace Terminal.Gui {
 	/// <summary>
@@ -30,7 +29,14 @@ namespace Terminal.Gui {
 	/// </para>
 	/// </remarks>
 	public class ScrollView : View {
-		View contentView = null;
+		private class ContentView : View {
+			public ContentView (Rect frame) : base (frame)
+			{
+				CanFocus = true;
+			}
+		}
+
+		ContentView contentView;
 		ScrollBarView vertical, horizontal;
 
 		/// <summary>
@@ -53,7 +59,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,
@@ -178,6 +184,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 ();
 				}
 			}
@@ -217,7 +229,7 @@ namespace Terminal.Gui {
 		/// <param name="view">The view to add to the scrollview.</param>
 		public override void Add (View view)
 		{
-			if (!IsOverridden (view)) {
+			if (!IsOverridden (view, "MouseEvent")) {
 				view.MouseEnter += View_MouseEnter;
 				view.MouseLeave += View_MouseLeave;
 			}
@@ -237,14 +249,6 @@ namespace Terminal.Gui {
 			Application.GrabMouse (this);
 		}
 
-		bool IsOverridden (View view)
-		{
-			Type t = view.GetType ();
-			MethodInfo m = t.GetMethod ("MouseEvent");
-
-			return (m.DeclaringType == t || m.ReflectedType == t) && m.GetBaseDefinition ().DeclaringType == typeof (Responder);
-		}
-
 		/// <summary>
 		/// Gets or sets the visibility for the horizontal scroll indicator.
 		/// </summary>
@@ -260,6 +264,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;
@@ -299,6 +305,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;
@@ -318,7 +326,7 @@ namespace Terminal.Gui {
 		{
 			Driver.SetAttribute (GetNormalColor ());
 			SetViewsNeedsDisplay ();
-			Clear ();
+			//Clear ();
 
 			var savedClip = ClipToBounds ();
 			OnDrawContent (new Rect (ContentOffset,
@@ -331,10 +339,12 @@ namespace Terminal.Gui {
 				ShowHideScrollBars ();
 			} else {
 				if (ShowVerticalScrollIndicator) {
+					vertical.SetRelativeLayout (Bounds);
 					vertical.Redraw (vertical.Bounds);
 				}
 
 				if (ShowHorizontalScrollIndicator) {
+					horizontal.SetRelativeLayout (Bounds);
 					horizontal.Redraw (horizontal.Bounds);
 				}
 			}
@@ -498,7 +508,7 @@ namespace Terminal.Gui {
 		{
 			if (me.Flags != MouseFlags.WheeledDown && me.Flags != MouseFlags.WheeledUp &&
 				me.Flags != MouseFlags.WheeledRight && me.Flags != MouseFlags.WheeledLeft &&
-				me.Flags != MouseFlags.Button1Pressed && me.Flags != MouseFlags.Button1Clicked &&
+//				me.Flags != MouseFlags.Button1Pressed && me.Flags != MouseFlags.Button1Clicked &&
 				!me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) {
 				return false;
 			}
@@ -515,7 +525,7 @@ namespace Terminal.Gui {
 				vertical.MouseEvent (me);
 			} else if (me.Y == horizontal.Frame.Y && ShowHorizontalScrollIndicator) {
 				horizontal.MouseEvent (me);
-			} else if (IsOverridden (me.View)) {
+			} else if (IsOverridden (me.View, "MouseEvent")) {
 				Application.UngrabMouse ();
 			}
 			return true;

+ 7 - 50
Terminal.Gui/Views/StatusBar.cs

@@ -55,6 +55,12 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// <value>Action to invoke.</value>
 		public Action Action { get; }
+
+		/// <summary>
+		/// Gets or sets arbitrary data for the status item.
+		/// </summary>
+		/// <remarks>This property is not used internally.</remarks>
+		public object Data { get; set; }
 	};
 
 	/// <summary>
@@ -64,8 +70,6 @@ namespace Terminal.Gui {
 	/// So for each context must be a new instance of a statusbar.
 	/// </summary>
 	public class StatusBar : View {
-		bool disposedValue;
-
 		/// <summary>
 		/// The items that compose the <see cref="StatusBar"/>
 		/// </summary>
@@ -87,39 +91,9 @@ namespace Terminal.Gui {
 			CanFocus = false;
 			ColorScheme = Colors.Menu;
 			X = 0;
+			Y = Pos.AnchorEnd (1);
 			Width = Dim.Fill ();
 			Height = 1;
-
-			Initialized += StatusBar_Initialized;
-			Application.Resized += Application_Resized ();
-		}
-
-		private void StatusBar_Initialized (object sender, EventArgs e)
-		{
-			if (SuperView.Frame == Rect.Empty) {
-				((Toplevel)SuperView).Loaded += StatusBar_Loaded;
-			} else {
-				Y = Math.Max (SuperView.Frame.Height - (Visible ? 1 : 0), 0);
-			}
-		}
-
-		private void StatusBar_Loaded ()
-		{
-			Y = Math.Max (SuperView.Frame.Height - (Visible ? 1 : 0), 0);
-			((Toplevel)SuperView).Loaded -= StatusBar_Loaded;
-		}
-
-		private Action<Application.ResizedEventArgs> Application_Resized ()
-		{
-			return delegate {
-				X = 0;
-				Height = 1;
-				if (SuperView != null || SuperView is Toplevel) {
-					if (Frame.Y != SuperView.Frame.Height - (Visible ? 1 : 0)) {
-						Y = SuperView.Frame.Height - (Visible ? 1 : 0);
-					}
-				}
-			};
 		}
 
 		static ustring shortcutDelimiter = "-";
@@ -145,12 +119,6 @@ namespace Terminal.Gui {
 		///<inheritdoc/>
 		public override void Redraw (Rect bounds)
 		{
-			//if (Frame.Y != Driver.Rows - 1) {
-			//	Frame = new Rect (Frame.X, Driver.Rows - 1, Frame.Width, Frame.Height);
-			//	Y = Driver.Rows - 1;
-			//	SetNeedsDisplay ();
-			//}
-
 			Move (0, 0);
 			Driver.SetAttribute (GetNormalColor ());
 			for (int i = 0; i < Frame.Width; i++)
@@ -228,17 +196,6 @@ namespace Terminal.Gui {
 			});
 		}
 
-		/// <inheritdoc/>
-		protected override void Dispose (bool disposing)
-		{
-			if (!disposedValue) {
-				if (disposing) {
-					Application.Resized -= Application_Resized ();
-				}
-				disposedValue = true;
-			}
-		}
-
 		///<inheritdoc/>
 		public override bool OnEnter (View view)
 		{

+ 75 - 31
Terminal.Gui/Views/TabView.cs

@@ -53,6 +53,14 @@ namespace Terminal.Gui {
 		/// </summary>
 		public event EventHandler<TabChangedEventArgs> SelectedTabChanged;
 
+
+		/// <summary>
+		/// Event fired when a <see cref="TabView.Tab"/> is clicked.  Can be used to cancel navigation,
+		/// show context menu (e.g. on right click) etc.
+		/// </summary>
+		public event EventHandler<TabMouseEventArgs> TabClicked;
+
+
 		/// <summary>
 		/// The currently selected member of <see cref="Tabs"/> chosen by the user
 		/// </summary>
@@ -98,7 +106,7 @@ namespace Terminal.Gui {
 
 
 		/// <summary>
-		/// Initialzies a <see cref="TabView"/> class using <see cref="LayoutStyle.Computed"/> layout.
+		/// Initializes a <see cref="TabView"/> class using <see cref="LayoutStyle.Computed"/> layout.
 		/// </summary>
 		public TabView () : base ()
 		{
@@ -182,7 +190,7 @@ namespace Terminal.Gui {
 
 			if (Style.ShowBorder) {
 
-				// How muc space do we need to leave at the bottom to show the tabs
+				// How much space do we need to leave at the bottom to show the tabs
 				int spaceAtBottom = Math.Max (0, GetTabHeight (false) - 1);
 				int startAtY = Math.Max (0, GetTabHeight (true) - 1);
 
@@ -347,8 +355,10 @@ namespace Terminal.Gui {
 				var maxWidth = Math.Max (0, Math.Min (bounds.Width - 3, MaxTabTextWidth));
 
 				// if tab view is width <= 3 don't render any tabs
-				if (maxWidth == 0)
-					yield break;
+				if (maxWidth == 0) {
+					yield return new TabToRender (i, tab, string.Empty, Equals (SelectedTab, tab), 0);
+					break;
+				}
 
 				if (tabTextWidth > maxWidth) {
 					text = tab.Text.ToString ().Substring (0, (int)maxWidth);
@@ -412,7 +422,7 @@ namespace Terminal.Gui {
 
 			// if the currently selected tab is no longer a member of Tabs
 			if (SelectedTab == null || !Tabs.Contains (SelectedTab)) {
-				// select the tab closest to the one that disapeared
+				// select the tab closest to the one that disappeared
 				var toSelect = Math.Max (idx - 1, 0);
 
 				if (toSelect < Tabs.Count) {
@@ -464,31 +474,10 @@ namespace Terminal.Gui {
 				Width = Dim.Fill ();
 			}
 
-			/// <summary>
-			/// Positions the cursor at the start of the currently selected tab
-			/// </summary>
-			public override void PositionCursor ()
+			public override bool OnEnter (View view)
 			{
-				base.PositionCursor ();
-
-				var selected = host.CalculateViewport (Bounds).FirstOrDefault (t => Equals (host.SelectedTab, t.Tab));
-
-				if (selected == null) {
-					return;
-				}
-
-				int y;
-
-				if (host.Style.TabsOnBottom) {
-					y = 1;
-				} else {
-					y = host.Style.ShowTopLine ? 1 : 0;
-				}
-
-				Move (selected.X, y);
-
-
-
+				Driver.SetCursorVisibility (CursorVisibility.Invisible);
+				return base.OnEnter (view);
 			}
 
 			public override void Redraw (Rect bounds)
@@ -657,7 +646,7 @@ namespace Terminal.Gui {
 					Driver.AddRune (Driver.LeftArrow);
 				}
 
-				// if there are mmore tabs to the right not visible
+				// if there are more tabs to the right not visible
 				if (ShouldDrawRightScrollIndicator (tabLocations)) {
 					Move (width - 1, y);
 
@@ -684,6 +673,22 @@ namespace Terminal.Gui {
 
 			public override bool MouseEvent (MouseEvent me)
 			{
+				var hit = ScreenToTab (me.X, me.Y);
+
+				bool isClick = me.Flags.HasFlag (MouseFlags.Button1Clicked) ||
+					me.Flags.HasFlag (MouseFlags.Button2Clicked) ||
+					me.Flags.HasFlag (MouseFlags.Button3Clicked);
+
+				if (isClick) {
+					host.OnTabClicked (new TabMouseEventArgs (hit, me));
+
+					// user canceled click
+					if (me.Handled) {
+						return true;
+					}
+				}
+
+
 				if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) &&
 				!me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) &&
 				!me.Flags.HasFlag (MouseFlags.Button1TripleClicked))
@@ -708,7 +713,7 @@ namespace Terminal.Gui {
 						return true;
 					}
 
-					var hit = ScreenToTab (me.X, me.Y);
+
 					if (hit != null) {
 						host.SelectedTab = hit;
 						SetNeedsDisplay ();
@@ -757,6 +762,45 @@ namespace Terminal.Gui {
 			}
 		}
 
+		/// <summary>
+		/// Raises the <see cref="TabClicked"/> event.
+		/// </summary>
+		/// <param name="tabMouseEventArgs"></param>
+		protected virtual private void OnTabClicked (TabMouseEventArgs tabMouseEventArgs)
+		{
+			TabClicked?.Invoke (this, tabMouseEventArgs);
+		}
+
+		/// <summary>
+		/// Describes a mouse event over a specific <see cref="TabView.Tab"/> in a <see cref="TabView"/>.
+		/// </summary>
+		public class TabMouseEventArgs : EventArgs {
+
+			/// <summary>
+			/// Gets the <see cref="TabView.Tab"/> (if any) that the mouse
+			/// was over when the <see cref="MouseEvent"/> occurred.
+			/// </summary>
+			/// <remarks>This will be null if the click is after last tab
+			/// or before first.</remarks>
+			public Tab Tab { get; }
+
+			/// <summary>
+			/// Gets the actual mouse event.  Use <see cref="MouseEvent.Handled"/> to cancel this event
+			/// and perform custom behavior (e.g. show a context menu).
+			/// </summary>
+			public MouseEvent MouseEvent { get; }
+
+			/// <summary>
+			/// Creates a new instance of the <see cref="TabMouseEventArgs"/> class.
+			/// </summary>
+			/// <param name="tab"><see cref="TabView.Tab"/> that the mouse was over when the event occurred.</param>
+			/// <param name="mouseEvent">The mouse activity being reported</param>
+			public TabMouseEventArgs (Tab tab, MouseEvent mouseEvent)
+			{
+				Tab = tab;
+				MouseEvent = mouseEvent;
+			}
+		}
 
 		/// <summary>
 		/// A single tab in a <see cref="TabView"/>

+ 288 - 33
Terminal.Gui/Views/TableView.cs

@@ -109,7 +109,7 @@ namespace Terminal.Gui {
 			get => columnOffset;
 
 			//try to prevent this being set to an out of bounds column
-			set => columnOffset = Table == null ? 0 : Math.Max (0, Math.Min (Table.Columns.Count - 1, value));
+			set => columnOffset = TableIsNullOrInvisible() ? 0 : Math.Max (0, Math.Min (Table.Columns.Count - 1, value));
 		}
 
 		/// <summary>
@@ -117,7 +117,7 @@ namespace Terminal.Gui {
 		/// </summary>
 		public int RowOffset {
 			get => rowOffset;
-			set => rowOffset = Table == null ? 0 : Math.Max (0, Math.Min (Table.Rows.Count - 1, value));
+			set => rowOffset = TableIsNullOrInvisible () ? 0 : Math.Max (0, Math.Min (Table.Rows.Count - 1, value));
 		}
 
 		/// <summary>
@@ -130,7 +130,7 @@ namespace Terminal.Gui {
 				var oldValue = selectedColumn;
 
 				//try to prevent this being set to an out of bounds column
-				selectedColumn = Table == null ? 0 : Math.Min (Table.Columns.Count - 1, Math.Max (0, value));
+				selectedColumn = TableIsNullOrInvisible () ? 0 : Math.Min (Table.Columns.Count - 1, Math.Max (0, value));
 
 				if (oldValue != selectedColumn)
 					OnSelectedCellChanged (new SelectedCellChangedEventArgs (Table, oldValue, SelectedColumn, SelectedRow, SelectedRow));
@@ -146,7 +146,7 @@ namespace Terminal.Gui {
 
 				var oldValue = selectedRow;
 
-				selectedRow = Table == null ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value));
+				selectedRow = TableIsNullOrInvisible () ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value));
 
 				if (oldValue != selectedRow)
 					OnSelectedCellChanged (new SelectedCellChangedEventArgs (Table, SelectedColumn, SelectedColumn, oldValue, selectedRow));
@@ -315,7 +315,7 @@ namespace Terminal.Gui {
 				var rowToRender = RowOffset + (line - headerLinesConsumed);
 
 				//if we have run off the end of the table
-				if (Table == null || rowToRender >= Table.Rows.Count || rowToRender < 0)
+				if (TableIsNullOrInvisible () || rowToRender >= Table.Rows.Count || rowToRender < 0)
 					continue;
 
 				RenderRow (line, rowToRender, columnsToRender);
@@ -427,6 +427,36 @@ namespace Terminal.Gui {
 
 		private void RenderHeaderUnderline (int row, int availableWidth, ColumnToRender [] columnsToRender)
 		{
+			/*
+			 *  First lets work out if we should be rendering scroll indicators
+			 */
+
+			// are there are visible columns to the left that have been pushed
+			// off the screen due to horizontal scrolling?
+			bool moreColumnsToLeft = ColumnOffset > 0;
+
+			// if we moved left would we find a new column (or are they all invisible?)
+			if(!TryGetNearestVisibleColumn (ColumnOffset-1, false, false, out _)) {
+				moreColumnsToLeft = false;
+			}
+
+			// are there visible columns to the right that have not yet been reached?
+			// lets find out, what is the column index of the last column we are rendering
+			int lastColumnIdxRendered = ColumnOffset + columnsToRender.Length - 1;
+			
+			// are there more valid indexes?
+			bool moreColumnsToRight = lastColumnIdxRendered < Table.Columns.Count;
+
+			// if we went right from the last column would we find a new visible column?
+			if(!TryGetNearestVisibleColumn (lastColumnIdxRendered + 1, true, false, out _)) {
+				// no we would not
+				moreColumnsToRight = false;
+			}
+
+			/*
+			 *  Now lets draw the line itself
+			 */
+
 			// Renders a line below the table headers (when visible) like:
 			// ├──────────┼───────────┼───────────────────┼──────────┼────────┼─────────────┤
 
@@ -436,7 +466,7 @@ namespace Terminal.Gui {
 				// whole way but update to instead draw a header indicator
 				// or scroll arrow etc
 				var rune = Driver.HLine;
-
+				
 				if (Style.ShowVerticalHeaderLines) {
 					if (c == 0) {
 						// for first character render line
@@ -445,7 +475,7 @@ namespace Terminal.Gui {
 						// unless we have horizontally scrolled along
 						// in which case render an arrow, to indicate user
 						// can scroll left
-						if(Style.ShowHorizontalScrollIndicators && ColumnOffset > 0)
+						if(Style.ShowHorizontalScrollIndicators && moreColumnsToLeft)
 						{
 							rune = Driver.LeftArrow;
 							scrollLeftPoint = new Point(c,row);
@@ -465,8 +495,7 @@ namespace Terminal.Gui {
 						// unless there is more of the table we could horizontally
 						// scroll along to see. In which case render an arrow,
 						// to indicate user can scroll right
-						if(Style.ShowHorizontalScrollIndicators &&
-							ColumnOffset + columnsToRender.Length < Table.Columns.Count)
+						if(Style.ShowHorizontalScrollIndicators && moreColumnsToRight)
 						{
 							rune = Driver.RightArrow;
 							scrollRightPoint = new Point(c,row);
@@ -683,7 +712,7 @@ namespace Terminal.Gui {
 		/// <inheritdoc/>
 		public override bool ProcessKey (KeyEvent keyEvent)
 		{
-			if (Table == null || Table.Columns.Count <= 0) {
+			if (TableIsNullOrInvisible ()) {
 				PositionCursor ();
 				return false;
 			}
@@ -705,6 +734,12 @@ namespace Terminal.Gui {
 		/// <param name="extendExistingSelection">True to create a multi cell selection or adjust an existing one</param>
 		public void SetSelection (int col, int row, bool extendExistingSelection)
 		{
+			// if we are trying to increase the column index then
+			// we are moving right otherwise we are moving left
+			bool lookRight = col > selectedColumn;
+
+			col = GetNearestVisibleColumn (col, lookRight, true);
+
 			if (!MultiSelect || !extendExistingSelection)
 				MultiSelectedRegions.Clear ();
 
@@ -726,6 +761,41 @@ namespace Terminal.Gui {
 			SelectedRow = row;
 		}
 
+		/// <summary>
+		/// Unions the current selected cell (and/or regions) with the provided cell and makes
+		/// it the active one.
+		/// </summary>
+		/// <param name="col"></param>
+		/// <param name="row"></param>
+		private void UnionSelection (int col, int row)
+		{
+			if (!MultiSelect || TableIsNullOrInvisible()) {
+				return;
+			}
+			
+			EnsureValidSelection ();
+
+			var oldColumn = SelectedColumn;
+			var oldRow = SelectedRow;
+
+			// move us to the new cell
+			SelectedColumn = col;
+			SelectedRow = row;
+			MultiSelectedRegions.Push (
+				CreateTableSelection (col, row)
+				);
+
+			// if the old cell was not part of a rectangular select
+			// or otherwise selected we need to retain it in the selection
+
+			if (!IsSelected (oldColumn, oldRow)) {
+				MultiSelectedRegions.Push (
+					CreateTableSelection (oldColumn, oldRow)
+					);
+			}
+		}
+
+
 		/// <summary>
 		/// Moves the <see cref="SelectedRow"/> and <see cref="SelectedColumn"/> by the provided offsets. Optionally starting a box selection (see <see cref="MultiSelect"/>)
 		/// </summary>
@@ -759,22 +829,28 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Moves or extends the selection to the first cell in the table (0,0)
+		/// Moves or extends the selection to the first cell in the table (0,0).
+		/// If <see cref="FullRowSelect"/> is enabled then selection instead moves
+		/// to (<see cref="SelectedColumn"/>,0) i.e. no horizontal scrolling.
 		/// </summary>
 		/// <param name="extend">true to extend the current selection (if any) instead of replacing</param>
 		public void ChangeSelectionToStartOfTable (bool extend)
 		{
-			SetSelection (0, 0, extend);
+			SetSelection (FullRowSelect ? SelectedColumn : 0, 0, extend);
 			Update ();
 		}
 
 		/// <summary>
-		/// Moves or extends the selection to the final cell in the table
+		/// Moves or extends the selection to the final cell in the table (nX,nY).
+		/// If <see cref="FullRowSelect"/> is enabled then selection instead moves
+		/// to (<see cref="SelectedColumn"/>,nY) i.e. no horizontal scrolling.
 		/// </summary>
 		/// <param name="extend">true to extend the current selection (if any) instead of replacing</param>
 		public void ChangeSelectionToEndOfTable(bool extend)
 		{
-			SetSelection (Table.Columns.Count - 1, Table.Rows.Count - 1, extend);
+			var finalColumn = Table.Columns.Count - 1;
+
+			SetSelection (FullRowSelect ? SelectedColumn : finalColumn, Table.Rows.Count - 1, extend);
 			Update ();
 		}
 
@@ -804,7 +880,7 @@ namespace Terminal.Gui {
 		/// </summary>
 		public void SelectAll ()
 		{
-			if (Table == null || !MultiSelect || Table.Rows.Count == 0)
+			if (TableIsNullOrInvisible() || !MultiSelect || Table.Rows.Count == 0)
 				return;
 
 			MultiSelectedRegions.Clear ();
@@ -817,10 +893,12 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Returns all cells in any <see cref="MultiSelectedRegions"/> (if <see cref="MultiSelect"/> is enabled) and the selected cell
 		/// </summary>
+		/// <remarks>Return value is not affected by <see cref="FullRowSelect"/> (i.e. returned <see cref="Point"/>s are not expanded to 
+		/// include all points on row).</remarks>
 		/// <returns></returns>
 		public IEnumerable<Point> GetAllSelectedCells ()
 		{
-			if (Table == null || Table.Rows.Count == 0)
+			if (TableIsNullOrInvisible () || Table.Rows.Count == 0)
 				yield break;
 
 			EnsureValidSelection ();
@@ -880,13 +958,30 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Returns true if the given cell is selected either because it is the active cell or part of a multi cell selection (e.g. <see cref="FullRowSelect"/>)
+		/// Returns a single point as a <see cref="TableSelection"/>
+		/// </summary>
+		/// <param name="x"></param>
+		/// <param name="y"></param>
+		/// <returns></returns>
+		private TableSelection CreateTableSelection (int x, int y)
+		{
+			return CreateTableSelection (x, y, x, y);
+		}
+		/// <summary>
+		/// <para>
+		/// Returns true if the given cell is selected either because it is the active cell or part of a multi cell selection (e.g. <see cref="FullRowSelect"/>).
+		/// </para>
+		/// <remarks>Returns <see langword="false"/> if <see cref="ColumnStyle.Visible"/> is <see langword="false"/>.</remarks>
 		/// </summary>
 		/// <param name="col"></param>
 		/// <param name="row"></param>
 		/// <returns></returns>
 		public bool IsSelected (int col, int row)
 		{
+			if(!IsColumnVisible(col)) {
+				return false;
+			}	
+
 			// Cell is also selected if in any multi selection region
 			if (MultiSelect && MultiSelectedRegions.Any (r => r.Rect.Contains (col, row)))
 				return true;
@@ -899,12 +994,28 @@ namespace Terminal.Gui {
 					(col == SelectedColumn || FullRowSelect);
 		}
 
+		/// <summary>
+		/// Returns true if the given <paramref name="columnIndex"/> indexes a visible
+		/// column otherwise false.  Returns false for indexes that are out of bounds.
+		/// </summary>
+		/// <param name="columnIndex"></param>
+		/// <returns></returns>
+		private bool IsColumnVisible (int columnIndex)
+		{
+			// if the column index provided is out of bounds
+			if (columnIndex < 0 || columnIndex >= table.Columns.Count) {
+				return false;
+			}
+
+			return this.Style.GetColumnStyleIfAny (Table.Columns [columnIndex])?.Visible ?? true;
+		}
+
 		/// <summary>
 		/// Positions the cursor in the area of the screen in which the start of the active cell is rendered.  Calls base implementation if active cell is not visible due to scrolling or table is loaded etc
 		/// </summary>
 		public override void PositionCursor ()
 		{
-			if (Table == null) {
+			if (TableIsNullOrInvisible ()) {
 				base.PositionCursor ();
 				return;
 			}
@@ -927,7 +1038,7 @@ namespace Terminal.Gui {
 				SetFocus ();
 			}
 
-			if (Table == null || Table.Columns.Count <= 0) {
+			if (TableIsNullOrInvisible ()) {
 				return false;
 			}
 
@@ -981,7 +1092,12 @@ namespace Terminal.Gui {
 				var hit = ScreenToCell (me.X, me.Y);
 				if (hit != null) {
 
-					SetSelection (hit.Value.X, hit.Value.Y, me.Flags.HasFlag (MouseFlags.ButtonShift));
+					if(MultiSelect && HasControlOrAlt(me)) {
+						UnionSelection(hit.Value.X, hit.Value.Y);
+					} else {
+						SetSelection (hit.Value.X, hit.Value.Y, me.Flags.HasFlag (MouseFlags.ButtonShift));
+					}
+
 					Update ();
 				}
 			}
@@ -997,15 +1113,33 @@ namespace Terminal.Gui {
 			return false;
 		}
 
-		/// <summary>
-		/// Returns the column and row of <see cref="Table"/> that corresponds to a given point on the screen (relative to the control client area).  Returns null if the point is in the header, no table is loaded or outside the control bounds
+		private bool HasControlOrAlt (MouseEvent me)
+		{
+			return me.Flags.HasFlag (MouseFlags.ButtonAlt) || me.Flags.HasFlag (MouseFlags.ButtonCtrl);
+		}
+
+		/// <summary>.
+		/// Returns the column and row of <see cref="Table"/> that corresponds to a given point 
+		/// on the screen (relative to the control client area).  Returns null if the point is
+		/// in the header, no table is loaded or outside the control bounds.
 		/// </summary>
-		/// <param name="clientX">X offset from the top left of the control</param>
-		/// <param name="clientY">Y offset from the top left of the control</param>
-		/// <returns></returns>
+		/// <param name="clientX">X offset from the top left of the control.</param>
+		/// <param name="clientY">Y offset from the top left of the control.</param>
+		/// <returns>Cell clicked or null.</returns>
 		public Point? ScreenToCell (int clientX, int clientY)
 		{
-			if (Table == null || Table.Columns.Count <= 0)
+			return ScreenToCell(clientX, clientY, out _);
+		}
+
+		/// <inheritdoc cref="ScreenToCell(int, int)"/>
+		/// <param name="clientX">X offset from the top left of the control.</param>
+		/// <param name="clientY">Y offset from the top left of the control.</param>
+		/// <param name="headerIfAny">If the click is in a header this is the column clicked.</param>
+		public Point? ScreenToCell (int clientX, int clientY, out DataColumn headerIfAny)
+		{
+			headerIfAny = null;
+
+			if (TableIsNullOrInvisible ())
 				return null;
 
 			var viewPort = CalculateViewport (Bounds);
@@ -1015,11 +1149,20 @@ namespace Terminal.Gui {
 			var col = viewPort.LastOrDefault (c => c.X <= clientX);
 
 			// Click is on the header section of rendered UI
-			if (clientY < headerHeight)
+			if (clientY < headerHeight) {
+				headerIfAny = col?.Column;
 				return null;
+			}
+				
 
 			var rowIdx = RowOffset - headerHeight + clientY;
 
+			// if click is off bottom of the rows don't give an
+			// invalid index back to user!
+			if (rowIdx >= Table.Rows.Count) {
+				return null;
+			}	
+
 			if (col != null && rowIdx >= 0) {
 
 				return new Point (col.Column.Ordinal, rowIdx);
@@ -1036,7 +1179,7 @@ namespace Terminal.Gui {
 		/// <returns></returns>
 		public Point? CellToScreen (int tableColumn, int tableRow)
 		{
-			if (Table == null || Table.Columns.Count <= 0)
+			if (TableIsNullOrInvisible ())
 				return null;
 
 			var viewPort = CalculateViewport (Bounds);
@@ -1065,7 +1208,7 @@ namespace Terminal.Gui {
 		/// <remarks>This always calls <see cref="View.SetNeedsDisplay()"/></remarks>
 		public void Update ()
 		{
-			if (Table == null) {
+			if (TableIsNullOrInvisible ()) {
 				SetNeedsDisplay ();
 				return;
 			}
@@ -1084,7 +1227,7 @@ namespace Terminal.Gui {
 		/// <remarks>Changes will not be immediately visible in the display until you call <see cref="View.SetNeedsDisplay()"/></remarks>
 		public void EnsureValidScrollOffsets ()
 		{
-			if (Table == null) {
+			if (TableIsNullOrInvisible ()) {
 				return;
 			}
 
@@ -1099,7 +1242,7 @@ namespace Terminal.Gui {
 		/// <remarks>Changes will not be immediately visible in the display until you call <see cref="View.SetNeedsDisplay()"/></remarks>
 		public void EnsureValidSelection ()
 		{
-			if (Table == null) {
+			if (TableIsNullOrInvisible()) {
 
 				// Table doesn't exist, we should probably clear those selections
 				MultiSelectedRegions.Clear ();
@@ -1109,6 +1252,9 @@ namespace Terminal.Gui {
 			SelectedColumn = Math.Max (Math.Min (SelectedColumn, Table.Columns.Count - 1), 0);
 			SelectedRow = Math.Max (Math.Min (SelectedRow, Table.Rows.Count - 1), 0);
 
+			// If SelectedColumn is invisible move it to a visible one
+			SelectedColumn = GetNearestVisibleColumn (SelectedColumn, lookRight: true, true);
+
 			var oldRegions = MultiSelectedRegions.ToArray ().Reverse ();
 
 			MultiSelectedRegions.Clear ();
@@ -1137,7 +1283,100 @@ namespace Terminal.Gui {
 
 				MultiSelectedRegions.Push (region);
 			}
+		}
+
+		/// <summary>
+		/// Returns true if the <see cref="Table"/> is not set or all the
+		/// <see cref="DataColumn"/> in the <see cref="Table"/> have an explicit
+		/// <see cref="ColumnStyle"/> that marks them <see cref="ColumnStyle.visible"/>
+		/// <see langword="false"/>.
+		/// </summary>
+		/// <returns></returns>
+		private bool TableIsNullOrInvisible ()
+		{
+			return Table == null ||
+				Table.Columns.Count <= 0 ||
+				Table.Columns.Cast<DataColumn> ().All (
+				c => (Style.GetColumnStyleIfAny (c)?.Visible ?? true) == false);
+		}
+
+		/// <summary>
+		/// Returns <paramref name="columnIndex"/> unless the <see cref="ColumnStyle.Visible"/> is false for
+		/// the indexed <see cref="DataColumn"/>.  If so then the index returned is nudged to the nearest visible
+		/// column.
+		/// </summary>
+		/// <remarks>Returns <paramref name="columnIndex"/> unchanged if it is invalid (e.g. out of bounds).</remarks>
+		/// <param name="columnIndex">The input column index.</param>
+		/// <param name="lookRight">When nudging invisible selections look right first.
+		/// <see langword="true"/> to look right, <see langword="false"/> to look left.</param>
+		/// <param name="allowBumpingInOppositeDirection">If we cannot find anything visible when
+		/// looking in direction of <paramref name="lookRight"/> then should we look in the opposite
+		/// direction instead? Use true if you want to push a selection to a valid index no matter what.
+		/// Use false if you are primarily interested in learning about directional column visibility.</param>
+		private int GetNearestVisibleColumn (int columnIndex, bool lookRight, bool allowBumpingInOppositeDirection)
+		{
+			if(TryGetNearestVisibleColumn(columnIndex,lookRight,allowBumpingInOppositeDirection, out var answer))
+			{
+				return answer;
+			}
+
+			return columnIndex;
+		}
+
+		private bool TryGetNearestVisibleColumn (int columnIndex, bool lookRight, bool allowBumpingInOppositeDirection, out int idx)
+		{
+			// if the column index provided is out of bounds
+			if (columnIndex < 0 || columnIndex >= table.Columns.Count) {
+
+				idx = columnIndex;
+				return false;
+			}
+
+			// get the column visibility by index (if no style visible is true)
+			bool [] columnVisibility = Table.Columns.Cast<DataColumn> ()
+				.Select (c => this.Style.GetColumnStyleIfAny (c)?.Visible ?? true)
+				.ToArray();
+
+			// column is visible
+			if (columnVisibility [columnIndex]) {
+				idx = columnIndex;
+				return true;
+			}
+
+			int increment = lookRight ? 1 : -1;
+
+			// move in that direction
+			for (int i = columnIndex; i >=0 && i < columnVisibility.Length; i += increment) {
+				// if we find a visible column
+				if(columnVisibility [i]) 
+				{
+					idx = i;
+					return true;
+				}
+			}
+
+			// Caller only wants to look in one direction and we did not find any
+			// visible columns in that direction
+			if(!allowBumpingInOppositeDirection) {
+				idx = columnIndex;
+				return false;
+			}
 
+			// Caller will let us look in the other direction so
+			// now look other way
+			increment = -increment;
+
+			for (int i = columnIndex; i >= 0 && i < columnVisibility.Length; i += increment) {
+				// if we find a visible column
+				if (columnVisibility [i]) {
+					idx = i;
+					return true;
+				}
+			}
+
+			// nothing seems to be visible so just return input index
+			idx = columnIndex;
+			return false;
 		}
 
 		/// <summary>
@@ -1217,7 +1456,7 @@ namespace Terminal.Gui {
 		/// <returns></returns>
 		private IEnumerable<ColumnToRender> CalculateViewport (Rect bounds, int padding = 1)
 		{
-			if (Table == null || Table.Columns.Count <= 0)
+			if (TableIsNullOrInvisible ())
 				yield break;
 
 			int usedSpace = 0;
@@ -1242,6 +1481,12 @@ namespace Terminal.Gui {
 				var colStyle = Style.GetColumnStyleIfAny (col);
 				int colWidth;
 
+				// if column is not being rendered
+				if(colStyle?.Visible == false) {
+					// do not add it to the returned columns
+					continue;
+				}
+
 				// is there enough space for this column (and it's data)?
 				colWidth = CalculateMaxCellWidth (col, rowsToRender, colStyle) + padding;
 
@@ -1289,7 +1534,7 @@ namespace Terminal.Gui {
 
 		private bool ShouldRenderHeaders ()
 		{
-			if (Table == null || Table.Columns.Count == 0)
+			if (TableIsNullOrInvisible ())
 				return false;
 
 			return Style.AlwaysShowHeaders || rowOffset == 0;
@@ -1397,6 +1642,7 @@ namespace Terminal.Gui {
 			/// Return null for the default
 			/// </summary>
 			public CellColorGetterDelegate ColorGetter;
+			private bool visible = true;
 
 			/// <summary>
 			/// Defines the format for values e.g. "yyyy-MM-dd" for dates
@@ -1427,6 +1673,15 @@ namespace Terminal.Gui {
 			/// </summary>
 			public int MinAcceptableWidth { get; set; } = DefaultMinAcceptableWidth;
 
+			/// <summary>
+			/// Gets or Sets a value indicating whether the column should be visible to the user.
+			/// This affects both whether it is rendered and whether it can be selected. Defaults to
+			/// true.
+			/// </summary>
+			/// <remarks>If <see cref="MaxWidth"/> is 0 then <see cref="Visible"/> will always return false.</remarks>
+			public bool Visible { get => MaxWidth >= 0 && visible; set => visible = value; }
+
+
 			/// <summary>
 			/// Returns the alignment for the cell based on <paramref name="cellValue"/> and <see cref="AlignmentGetter"/>/<see cref="Alignment"/>
 			/// </summary>

+ 5 - 1
Terminal.Gui/Views/TextField.cs

@@ -235,7 +235,10 @@ namespace Terminal.Gui {
 
 		private void HistoryText_ChangeText (HistoryText.HistoryTextItem obj)
 		{
-			Text = ustring.Make (obj.Lines [obj.CursorPosition.Y]);
+			if (obj == null)
+				return;
+
+			Text = ustring.Make (obj?.Lines [obj.CursorPosition.Y]);
 			CursorPosition = obj.CursorPosition.X;
 			Adjust ();
 		}
@@ -295,6 +298,7 @@ namespace Terminal.Gui {
 					}
 					return;
 				}
+				ClearAllSelection ();
 				text = TextModel.ToRunes (newText.NewText);
 
 				if (!Secret && !historyText.IsFromHistory) {

+ 236 - 192
Terminal.Gui/Views/TextView.cs

@@ -1,28 +1,4 @@
-//
 // TextView.cs: multi-line text editing
-//
-// Authors:
-//   Miguel de Icaza ([email protected])
-//
-// 
-// TODO:
-// In ReadOnly mode backspace/space behave like pageup/pagedown
-// Attributed text on spans
-// Replace insertion with Insert method
-// String accumulation (Control-k, control-k is not preserving the last new line, see StringToRunes
-// Alt-D, Alt-Backspace
-// API to set the cursor position
-// API to scroll to a particular place
-// keybindings to go to top/bottom
-// public API to insert, remove ranges
-// Add word forward/word backwards commands
-// Save buffer API
-// Mouse
-//
-// Desirable:
-//   Move all the text manipulation into the TextModel
-
-
 using System;
 using System.Collections.Generic;
 using System.Globalization;
@@ -33,6 +9,7 @@ using System.Text;
 using System.Threading;
 using NStack;
 using Terminal.Gui.Resources;
+using static Terminal.Gui.Graphs.PathAnnotation;
 using Rune = System.Rune;
 
 namespace Terminal.Gui {
@@ -742,6 +719,7 @@ namespace Terminal.Gui {
 			historyTextItems.Clear ();
 			idxHistoryText = -1;
 			originalText = text;
+			OnChangeText (null);
 		}
 
 		public bool IsDirty (ustring text)
@@ -873,7 +851,7 @@ namespace Terminal.Gui {
 			var firstLine = wrappedModelLines.IndexOf (r => r.ModelLine == modelLine);
 			int modelCol = 0;
 
-			for (int i = firstLine; i <= line; i++) {
+			for (int i = firstLine; i <= Math.Min (line, wrappedModelLines.Count - 1); i++) {
 				var wLine = wrappedModelLines [i];
 
 				if (i < line) {
@@ -1037,120 +1015,119 @@ namespace Terminal.Gui {
 	}
 
 	/// <summary>
-	///   Multi-line text editing <see cref="View"/>
+	///  Multi-line text editing <see cref="View"/>.
 	/// </summary>
 	/// <remarks>
-	///   <para>
-	///     <see cref="TextView"/> provides a multi-line text editor. Users interact
-	///     with it with the standard Emacs commands for movement or the arrow
-	///     keys. 
-	///   </para> 
-	///   <list type="table"> 
-	///     <listheader>
-	///       <term>Shortcut</term>
-	///       <description>Action performed</description>
-	///     </listheader>
-	///     <item>
-	///        <term>Left cursor, Control-b</term>
-	///        <description>
-	///          Moves the editing point left.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Right cursor, Control-f</term>
-	///        <description>
-	///          Moves the editing point right.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Alt-b</term>
-	///        <description>
-	///          Moves one word back.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Alt-f</term>
-	///        <description>
-	///          Moves one word forward.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Up cursor, Control-p</term>
-	///        <description>
-	///          Moves the editing point one line up.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Down cursor, Control-n</term>
-	///        <description>
-	///          Moves the editing point one line down
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Home key, Control-a</term>
-	///        <description>
-	///          Moves the cursor to the beginning of the line.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>End key, Control-e</term>
-	///        <description>
-	///          Moves the cursor to the end of the line.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Control-Home</term>
-	///        <description>
-	///          Scrolls to the first line and moves the cursor there.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Control-End</term>
-	///        <description>
-	///          Scrolls to the last line and moves the cursor there.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Delete, Control-d</term>
-	///        <description>
-	///          Deletes the character in front of the cursor.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Backspace</term>
-	///        <description>
-	///          Deletes the character behind the cursor.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Control-k</term>
-	///        <description>
-	///          Deletes the text until the end of the line and replaces the kill buffer
-	///          with the deleted text.   You can paste this text in a different place by
-	///          using Control-y.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Control-y</term>
-	///        <description>
-	///           Pastes the content of the kill ring into the current position.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Alt-d</term>
-	///        <description>
-	///           Deletes the word above the cursor and adds it to the kill ring.  You 
-	///           can paste the contents of the kill ring with Control-y.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Control-q</term>
-	///        <description>
-	///          Quotes the next input character, to prevent the normal processing of
-	///          key handling to take place.
-	///        </description>
-	///     </item>
-	///   </list>
+	///  <para>
+	///   <see cref="TextView"/> provides a multi-line text editor. Users interact
+	///   with it with the standard Windows, Mac, and Linux (Emacs) commands. 
+	///  </para> 
+	///  <list type="table"> 
+	///   <listheader>
+	///    <term>Shortcut</term>
+	///    <description>Action performed</description>
+	///   </listheader>
+	///   <item>
+	///    <term>Left cursor, Control-b</term>
+	///    <description>
+	///     Moves the editing point left.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Right cursor, Control-f</term>
+	///    <description>
+	///     Moves the editing point right.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Alt-b</term>
+	///    <description>
+	///     Moves one word back.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Alt-f</term>
+	///    <description>
+	///     Moves one word forward.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Up cursor, Control-p</term>
+	///    <description>
+	///     Moves the editing point one line up.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Down cursor, Control-n</term>
+	///    <description>
+	///     Moves the editing point one line down
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Home key, Control-a</term>
+	///    <description>
+	///     Moves the cursor to the beginning of the line.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>End key, Control-e</term>
+	///    <description>
+	///     Moves the cursor to the end of the line.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Control-Home</term>
+	///    <description>
+	///     Scrolls to the first line and moves the cursor there.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Control-End</term>
+	///    <description>
+	///     Scrolls to the last line and moves the cursor there.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Delete, Control-d</term>
+	///    <description>
+	///     Deletes the character in front of the cursor.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Backspace</term>
+	///    <description>
+	///     Deletes the character behind the cursor.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Control-k</term>
+	///    <description>
+	///     Deletes the text until the end of the line and replaces the kill buffer
+	///     with the deleted text. You can paste this text in a different place by
+	///     using Control-y.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Control-y</term>
+	///    <description>
+	///      Pastes the content of the kill ring into the current position.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Alt-d</term>
+	///    <description>
+	///      Deletes the word above the cursor and adds it to the kill ring. You 
+	///      can paste the contents of the kill ring with Control-y.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Control-q</term>
+	///    <description>
+	///     Quotes the next input character, to prevent the normal processing of
+	///     key handling to take place.
+	///    </description>
+	///   </item>
+	///  </list>
 	/// </remarks>
 	public class TextView : View {
 		TextModel model = new TextModel ();
@@ -1172,10 +1149,24 @@ namespace Terminal.Gui {
 		CultureInfo currentCulture;
 
 		/// <summary>
-		/// Raised when the <see cref="Text"/> of the <see cref="TextView"/> changes.
+		/// Raised when the <see cref="Text"/> property of the <see cref="TextView"/> changes.
 		/// </summary>
+		/// <remarks>
+		/// The <see cref="Text"/> property of <see cref="TextView"/> only changes when it is explicitly
+		/// set, not as the user types. To be notified as the user changes the contents of the TextView
+		/// see <see cref="IsDirty"/>.
+		/// </remarks>
 		public event Action TextChanged;
 
+		/// <summary>
+		///  Raised when the contents of the <see cref="TextView"/> are changed. 
+		/// </summary>
+		/// <remarks>
+		/// Unlike the <see cref="TextChanged"/> event, this event is raised whenever the user types or
+		/// otherwise changes the contents of the <see cref="TextView"/>.
+		/// </remarks>
+		public event Action<ContentsChangedEventArgs> ContentsChanged;
+
 		/// <summary>
 		/// Invoked with the unwrapped <see cref="CursorPosition"/>.
 		/// </summary>
@@ -1183,22 +1174,12 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Provides autocomplete context menu based on suggestions at the current cursor
-		/// position.  Populate <see cref="Autocomplete.AllSuggestions"/> to enable this feature
+		/// position. Populate <see cref="Autocomplete.AllSuggestions"/> to enable this feature
 		/// </summary>
 		public IAutocomplete Autocomplete { get; protected set; } = new TextViewAutocomplete ();
 
-#if false
-		/// <summary>
-		///   Changed event, raised when the text has clicked.
-		/// </summary>
-		/// <remarks>
-		///   Client code can hook up to this event, it is
-		///   raised when the text in the entry changes.
-		/// </remarks>
-		public Action Changed;
-#endif
 		/// <summary>
-		///   Initializes a <see cref="TextView"/> on the specified area, with absolute position and size.
+		///  Initializes a <see cref="TextView"/> on the specified area, with absolute position and size.
 		/// </summary>
 		/// <remarks>
 		/// </remarks>
@@ -1208,8 +1189,8 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		///   Initializes a <see cref="TextView"/> on the specified area, 
-		///   with dimensions controlled with the X, Y, Width and Height properties.
+		///  Initializes a <see cref="TextView"/> on the specified area, 
+		///  with dimensions controlled with the X, Y, Width and Height properties.
 		/// </summary>
 		public TextView () : base ()
 		{
@@ -1404,48 +1385,56 @@ namespace Terminal.Gui {
 
 		private void Model_LinesLoaded ()
 		{
-			historyText.Clear (Text);
+			// This call is not needed. Model_LinesLoaded gets invoked when
+			// model.LoadString (value) is called. LoadString is called from one place
+			// (Text.set) and historyText.Clear() is called immediately after.
+			// If this call happens, HistoryText_ChangeText will get called multiple times
+			// when Text is set, which is wrong.
+			//historyText.Clear (Text);
 		}
 
 		private void HistoryText_ChangeText (HistoryText.HistoryTextItem obj)
 		{
 			SetWrapModel ();
 
-			var startLine = obj.CursorPosition.Y;
+			if (obj != null) {
+				var startLine = obj.CursorPosition.Y;
 
-			if (obj.RemovedOnAdded != null) {
-				int offset;
-				if (obj.IsUndoing) {
-					offset = Math.Max (obj.RemovedOnAdded.Lines.Count - obj.Lines.Count, 1);
-				} else {
-					offset = obj.RemovedOnAdded.Lines.Count - 1;
-				}
-				for (int i = 0; i < offset; i++) {
-					if (Lines > obj.RemovedOnAdded.CursorPosition.Y) {
-						model.RemoveLine (obj.RemovedOnAdded.CursorPosition.Y);
+				if (obj.RemovedOnAdded != null) {
+					int offset;
+					if (obj.IsUndoing) {
+						offset = Math.Max (obj.RemovedOnAdded.Lines.Count - obj.Lines.Count, 1);
 					} else {
-						break;
+						offset = obj.RemovedOnAdded.Lines.Count - 1;
+					}
+					for (int i = 0; i < offset; i++) {
+						if (Lines > obj.RemovedOnAdded.CursorPosition.Y) {
+							model.RemoveLine (obj.RemovedOnAdded.CursorPosition.Y);
+						} else {
+							break;
+						}
 					}
 				}
-			}
 
-			for (int i = 0; i < obj.Lines.Count; i++) {
-				if (i == 0) {
-					model.ReplaceLine (startLine, obj.Lines [i]);
-				} else if ((obj.IsUndoing && obj.LineStatus == HistoryText.LineStatus.Removed)
-						|| !obj.IsUndoing && obj.LineStatus == HistoryText.LineStatus.Added) {
-					model.AddLine (startLine, obj.Lines [i]);
-				} else if (Lines > obj.CursorPosition.Y + 1) {
-					model.RemoveLine (obj.CursorPosition.Y + 1);
+				for (int i = 0; i < obj.Lines.Count; i++) {
+					if (i == 0) {
+						model.ReplaceLine (startLine, obj.Lines [i]);
+					} else if ((obj.IsUndoing && obj.LineStatus == HistoryText.LineStatus.Removed)
+							|| !obj.IsUndoing && obj.LineStatus == HistoryText.LineStatus.Added) {
+						model.AddLine (startLine, obj.Lines [i]);
+					} else if (Lines > obj.CursorPosition.Y + 1) {
+						model.RemoveLine (obj.CursorPosition.Y + 1);
+					}
+					startLine++;
 				}
-				startLine++;
-			}
 
-			CursorPosition = obj.FinalCursorPosition;
+				CursorPosition = obj.FinalCursorPosition;
+			}
 
 			UpdateWrapModel ();
 
 			Adjust ();
+			OnContentsChanged ();
 		}
 
 		void TextView_Initialized (object sender, EventArgs e)
@@ -1454,6 +1443,7 @@ namespace Terminal.Gui {
 
 			Application.Top.AlternateForwardKeyChanged += Top_AlternateForwardKeyChanged;
 			Application.Top.AlternateBackwardKeyChanged += Top_AlternateBackwardKeyChanged;
+			OnContentsChanged ();
 		}
 
 		void Top_AlternateBackwardKeyChanged (Key obj)
@@ -1480,9 +1470,11 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		///   Sets or gets the text in the <see cref="TextView"/>.
+		///  Sets or gets the text in the <see cref="TextView"/>.
 		/// </summary>
 		/// <remarks>
+		/// The <see cref="TextChanged"/> event is fired whenever this property is set. Note, however,
+		/// that Text is not set by <see cref="TextView"/> as the user types.
 		/// </remarks>
 		public override ustring Text {
 			get {
@@ -1559,12 +1551,12 @@ namespace Terminal.Gui {
 		public int Maxlength => model.GetMaxVisibleLine (topRow, topRow + Frame.Height, TabWidth);
 
 		/// <summary>
-		/// Gets the  number of lines.
+		/// Gets the number of lines.
 		/// </summary>
 		public int Lines => model.Count;
 
 		/// <summary>
-		///    Sets or gets the current cursor position.
+		///  Sets or gets the current cursor position.
 		/// </summary>
 		public Point CursorPosition {
 			get => new Point (currentColumn, currentRow);
@@ -1828,7 +1820,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Loads the contents of the file into the  <see cref="TextView"/>.
+		/// Loads the contents of the file into the <see cref="TextView"/>.
 		/// </summary>
 		/// <returns><c>true</c>, if file was loaded, <c>false</c> otherwise.</returns>
 		/// <param name="path">Path to the file to load.</param>
@@ -1838,6 +1830,7 @@ namespace Terminal.Gui {
 			try {
 				SetWrapModel ();
 				res = model.LoadFile (path);
+				historyText.Clear (Text);
 				ResetPosition ();
 			} catch (Exception) {
 				throw;
@@ -1850,19 +1843,20 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Loads the contents of the stream into the  <see cref="TextView"/>.
+		/// Loads the contents of the stream into the <see cref="TextView"/>.
 		/// </summary>
 		/// <returns><c>true</c>, if stream was loaded, <c>false</c> otherwise.</returns>
 		/// <param name="stream">Stream to load the contents from.</param>
 		public void LoadStream (Stream stream)
 		{
 			model.LoadStream (stream);
+			historyText.Clear (Text);
 			ResetPosition ();
 			SetNeedsDisplay ();
 		}
 
 		/// <summary>
-		/// Closes the contents of the stream into the  <see cref="TextView"/>.
+		/// Closes the contents of the stream into the <see cref="TextView"/>.
 		/// </summary>
 		/// <returns><c>true</c>, if stream was closed, <c>false</c> otherwise.</returns>
 		public bool CloseFile ()
@@ -1874,7 +1868,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		///    Gets the current cursor row.
+		///  Gets the current cursor row.
 		/// </summary>
 		public int CurrentRow => currentRow;
 
@@ -1885,7 +1879,7 @@ namespace Terminal.Gui {
 		public int CurrentColumn => currentColumn;
 
 		/// <summary>
-		///   Positions the cursor on the current row and column
+		///  Positions the cursor on the current row and column
 		/// </summary>
 		public override void PositionCursor ()
 		{
@@ -1936,7 +1930,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Sets the driver to the default color for the control where no text is being rendered.  Defaults to <see cref="ColorScheme.Normal"/>.
+		/// Sets the driver to the default color for the control where no text is being rendered. Defaults to <see cref="ColorScheme.Normal"/>.
 		/// </summary>
 		protected virtual void SetNormalColor ()
 		{
@@ -1945,7 +1939,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Sets the <see cref="View.Driver"/> to an appropriate color for rendering the given <paramref name="idx"/> of the
-		/// current <paramref name="line"/>.  Override to provide custom coloring by calling <see cref="ConsoleDriver.SetAttribute(Attribute)"/>
+		/// current <paramref name="line"/>. Override to provide custom coloring by calling <see cref="ConsoleDriver.SetAttribute(Attribute)"/>
 		/// Defaults to <see cref="ColorScheme.Normal"/>.
 		/// </summary>
 		/// <param name="line"></param>
@@ -1957,7 +1951,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Sets the <see cref="View.Driver"/> to an appropriate color for rendering the given <paramref name="idx"/> of the
-		/// current <paramref name="line"/>.  Override to provide custom coloring by calling <see cref="ConsoleDriver.SetAttribute(Attribute)"/>
+		/// current <paramref name="line"/>. Override to provide custom coloring by calling <see cref="ConsoleDriver.SetAttribute(Attribute)"/>
 		/// Defaults to <see cref="ColorScheme.Focus"/>.
 		/// </summary>
 		/// <param name="line"></param>
@@ -1969,7 +1963,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Sets the <see cref="View.Driver"/> to an appropriate color for rendering the given <paramref name="idx"/> of the
-		/// current <paramref name="line"/>.  Override to provide custom coloring by calling <see cref="ConsoleDriver.SetAttribute(Attribute)"/>
+		/// current <paramref name="line"/>. Override to provide custom coloring by calling <see cref="ConsoleDriver.SetAttribute(Attribute)"/>
 		/// Defaults to <see cref="ColorScheme.Focus"/>.
 		/// </summary>
 		/// <param name="line"></param>
@@ -1987,7 +1981,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Sets the <see cref="View.Driver"/> to an appropriate color for rendering the given <paramref name="idx"/> of the
-		/// current <paramref name="line"/>.  Override to provide custom coloring by calling <see cref="ConsoleDriver.SetAttribute(Attribute)"/>
+		/// current <paramref name="line"/>. Override to provide custom coloring by calling <see cref="ConsoleDriver.SetAttribute(Attribute)"/>
 		/// Defaults to <see cref="ColorScheme.HotFocus"/>.
 		/// </summary>
 		/// <param name="line"></param>
@@ -2000,7 +1994,7 @@ namespace Terminal.Gui {
 		bool isReadOnly = false;
 
 		/// <summary>
-		/// Gets or sets whether the  <see cref="TextView"/> is in read-only mode or not
+		/// Gets or sets whether the <see cref="TextView"/> is in read-only mode or not
 		/// </summary>
 		/// <value>Boolean value(Default false)</value>
 		public bool ReadOnly {
@@ -2504,6 +2498,12 @@ namespace Terminal.Gui {
 
 				InsertText (new KeyEvent () { Key = key });
 			}
+
+			if (NeedDisplay.IsEmpty) {
+				PositionCursor ();
+			} else {
+				Adjust ();
+			}
 		}
 
 		void Insert (Rune rune)
@@ -2521,6 +2521,7 @@ namespace Terminal.Gui {
 			if (!wrapNeeded) {
 				SetNeedsDisplay (new Rect (0, prow, Math.Max (Frame.Width, 0), Math.Max (prow + 1, 0)));
 			}
+
 		}
 
 		ustring StringFromRunes (List<Rune> runes)
@@ -2584,6 +2585,8 @@ namespace Terminal.Gui {
 
 				UpdateWrapModel ();
 
+				OnContentsChanged ();
+
 				return;
 			}
 
@@ -2690,6 +2693,42 @@ namespace Terminal.Gui {
 			OnUnwrappedCursorPosition ();
 		}
 
+		/// <summary>
+		/// Event arguments for events for when the contents of the TextView change. E.g. the <see cref="ContentsChanged"/> event.
+		/// </summary>
+		public class ContentsChangedEventArgs : EventArgs {
+			/// <summary>
+			/// Creates a new <see cref="ContentsChanged"/> instance.
+			/// </summary>
+			/// <param name="currentRow">Contains the row where the change occurred.</param>
+			/// <param name="currentColumn">Contains the column where the change occured.</param>
+			public ContentsChangedEventArgs (int currentRow, int currentColumn)
+			{
+				Row = currentRow;
+				Col = currentColumn;
+			}
+
+			/// <summary>
+			/// 
+			/// Contains the row where the change occurred.
+			/// </summary>
+			public int Row { get; private set; }
+
+			/// <summary>
+			/// Contains the column where the change occurred.
+			/// </summary>
+			public int Col { get; private set; }
+		}
+
+		/// <summary>
+		/// Called when the contents of the TextView change. E.g. when the user types text or deletes text. Raises
+		/// the <see cref="ContentsChanged"/> event.
+		/// </summary>
+		public virtual void OnContentsChanged ()
+		{
+			ContentsChanged?.Invoke (new ContentsChangedEventArgs (CurrentRow, CurrentColumn));
+		}
+
 		(int width, int height) OffSetBackground ()
 		{
 			int w = 0;
@@ -2708,7 +2747,7 @@ namespace Terminal.Gui {
 		/// will scroll the <see cref="TextView"/> to display the specified column at the left if <paramref name="isRow"/> is false.
 		/// </summary>
 		/// <param name="idx">Row that should be displayed at the top or Column that should be displayed at the left,
-		///  if the value is negative it will be reset to zero</param>
+		/// if the value is negative it will be reset to zero</param>
 		/// <param name="isRow">If true (default) the <paramref name="idx"/> is a row, column otherwise.</param>
 		public void ScrollTo (int idx, bool isRow = true)
 		{
@@ -3178,6 +3217,7 @@ namespace Terminal.Gui {
 			UpdateWrapModel ();
 
 			DoNeededAction ();
+			OnContentsChanged ();
 			return true;
 		}
 
@@ -3674,6 +3714,7 @@ namespace Terminal.Gui {
 				HistoryText.LineStatus.Replaced);
 
 			UpdateWrapModel ();
+			OnContentsChanged ();
 
 			return true;
 		}
@@ -3883,6 +3924,7 @@ namespace Terminal.Gui {
 			UpdateWrapModel ();
 			selecting = false;
 			DoNeededAction ();
+			OnContentsChanged ();
 		}
 
 		/// <summary>
@@ -3913,6 +3955,7 @@ namespace Terminal.Gui {
 
 				historyText.Add (new List<List<Rune>> () { new List<Rune> (GetCurrentLine ()) }, CursorPosition,
 					HistoryText.LineStatus.Replaced);
+				OnContentsChanged ();
 			} else {
 				if (selecting) {
 					ClearRegion ();
@@ -4423,6 +4466,7 @@ namespace Terminal.Gui {
 		}
 	}
 
+
 	/// <summary>
 	/// Renders an overlay on another view at a given point that allows selecting
 	/// from a range of 'autocomplete' options.

+ 168 - 159
Terminal.Gui/Views/TreeView.cs

@@ -1,5 +1,5 @@
 // This code is based on http://objectlistview.sourceforge.net (GPLv3 tree/list controls 
-// by [email protected]).  Phillip has explicitly granted permission for his design
+// by [email protected]). Phillip has explicitly granted permission for his design
 // and code to be used in this library under the MIT license.
 
 using NStack;
@@ -12,18 +12,18 @@ using Terminal.Gui.Trees;
 namespace Terminal.Gui {
 
 	/// <summary>
-	/// Interface for all non generic members of <see cref="TreeView{T}"/>
+	/// Interface for all non generic members of <see cref="TreeView{T}"/>.
 	/// 
 	/// <a href="https://gui-cs.github.io/Terminal.Gui/articles/treeview.html">See TreeView Deep Dive for more information</a>.
 	/// </summary>
 	public interface ITreeView {
 		/// <summary>
-		/// Contains options for changing how the tree is rendered
+		/// Contains options for changing how the tree is rendered.
 		/// </summary>
 		TreeStyle Style { get; set; }
 
 		/// <summary>
-		/// Removes all objects from the tree and clears selection
+		/// Removes all objects from the tree and clears selection.
 		/// </summary>
 		void ClearObjects ();
 
@@ -43,7 +43,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Creates a new instance of the tree control with absolute positioning and initialises
-		/// <see cref="TreeBuilder{T}"/> with default <see cref="ITreeNode"/> based builder
+		/// <see cref="TreeBuilder{T}"/> with default <see cref="ITreeNode"/> based builder.
 		/// </summary>
 		public TreeView ()
 		{
@@ -53,8 +53,8 @@ namespace Terminal.Gui {
 	}
 
 	/// <summary>
-	/// Hierarchical tree view with expandable branches.  Branch objects are dynamically determined
-	/// when expanded using a user defined <see cref="ITreeBuilder{T}"/>
+	/// Hierarchical tree view with expandable branches. Branch objects are dynamically determined
+	/// when expanded using a user defined <see cref="ITreeBuilder{T}"/>.
 	/// 
 	/// <a href="https://gui-cs.github.io/Terminal.Gui/articles/treeview.html">See TreeView Deep Dive for more information</a>.
 	/// </summary>
@@ -64,7 +64,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Determines how sub branches of the tree are dynamically built at runtime as the user
-		/// expands root nodes
+		/// expands root nodes.
 		/// </summary>
 		/// <value></value>
 		public ITreeBuilder<T> TreeBuilder { get; set; }
@@ -74,30 +74,27 @@ namespace Terminal.Gui {
 		/// </summary>
 		T selectedObject;
 
-
 		/// <summary>
-		/// Contains options for changing how the tree is rendered
+		/// Contains options for changing how the tree is rendered.
 		/// </summary>
 		public TreeStyle Style { get; set; } = new TreeStyle ();
 
-
 		/// <summary>
-		/// True to allow multiple objects to be selected at once
+		/// True to allow multiple objects to be selected at once.
 		/// </summary>
 		/// <value></value>
 		public bool MultiSelect { get; set; } = true;
 
-
 		/// <summary>
 		/// True makes a letter key press navigate to the next visible branch that begins with
-		/// that letter/digit
+		/// that letter/digit.
 		/// </summary>
 		/// <value></value>
 		public bool AllowLetterBasedNavigation { get; set; } = true;
 
 		/// <summary>
-		/// The currently selected object in the tree.  When <see cref="MultiSelect"/> is true this
-		/// is the object at which the cursor is at
+		/// The currently selected object in the tree. When <see cref="MultiSelect"/> is true this
+		/// is the object at which the cursor is at.
 		/// </summary>
 		public T SelectedObject {
 			get => selectedObject;
@@ -111,16 +108,15 @@ namespace Terminal.Gui {
 			}
 		}
 
-
 		/// <summary>
 		/// This event is raised when an object is activated e.g. by double clicking or 
-		/// pressing <see cref="ObjectActivationKey"/>
+		/// pressing <see cref="ObjectActivationKey"/>.
 		/// </summary>
 		public event Action<ObjectActivatedEventArgs<T>> ObjectActivated;
 
 		/// <summary>
 		/// Key which when pressed triggers <see cref="TreeView{T}.ObjectActivated"/>.
-		/// Defaults to Enter
+		/// Defaults to Enter.
 		/// </summary>
 		public Key ObjectActivationKey {
 			get => objectActivationKey;
@@ -140,15 +136,14 @@ namespace Terminal.Gui {
 		/// <value></value>
 		public MouseFlags? ObjectActivationButton { get; set; } = MouseFlags.Button1DoubleClicked;
 
-		
 		/// <summary>
-		/// Delegate for multi colored tree views.  Return the <see cref="ColorScheme"/> to use
+		/// Delegate for multi colored tree views. Return the <see cref="ColorScheme"/> to use
 		/// for each passed object or null to use the default.
 		/// </summary>
-		public Func<T,ColorScheme> ColorGetter {get;set;}
+		public Func<T, ColorScheme> ColorGetter { get; set; }
 
 		/// <summary>
-		/// Secondary selected regions of tree when <see cref="MultiSelect"/> is true
+		/// Secondary selected regions of tree when <see cref="MultiSelect"/> is true.
 		/// </summary>
 		private Stack<TreeSelection<T>> multiSelectedRegions = new Stack<TreeSelection<T>> ();
 
@@ -157,36 +152,35 @@ namespace Terminal.Gui {
 		/// </summary>
 		private IReadOnlyCollection<Branch<T>> cachedLineMap;
 
-
 		/// <summary>
 		/// Error message to display when the control is not properly initialized at draw time 
-		/// (nodes added but no tree builder set)
+		/// (nodes added but no tree builder set).
 		/// </summary>
 		public static ustring NoBuilderError = "ERROR: TreeBuilder Not Set";
 		private Key objectActivationKey = Key.Enter;
 
 		/// <summary>
-		/// Called when the <see cref="SelectedObject"/> changes
+		/// Called when the <see cref="SelectedObject"/> changes.
 		/// </summary>
 		public event EventHandler<SelectionChangedEventArgs<T>> SelectionChanged;
 
 		/// <summary>
-		/// The root objects in the tree, note that this collection is of root objects only
+		/// The root objects in the tree, note that this collection is of root objects only.
 		/// </summary>
 		public IEnumerable<T> Objects { get => roots.Keys; }
 
 		/// <summary>
-		/// Map of root objects to the branches under them.  All objects have 
-		/// a <see cref="Branch{T}"/> even if that branch has no children
+		/// Map of root objects to the branches under them. All objects have 
+		/// a <see cref="Branch{T}"/> even if that branch has no children.
 		/// </summary>
 		internal Dictionary<T, Branch<T>> roots { get; set; } = new Dictionary<T, Branch<T>> ();
 
 		/// <summary>
 		/// The amount of tree view that has been scrolled off the top of the screen (by the user 
-		/// scrolling down)
+		/// scrolling down).
 		/// </summary>
-		/// <remarks>Setting a value of less than 0 will result in a offset of 0.  To see changes 
-		/// in the UI call <see cref="View.SetNeedsDisplay()"/></remarks>
+		/// <remarks>Setting a value of less than 0 will result in a offset of 0. To see changes 
+		/// in the UI call <see cref="View.SetNeedsDisplay()"/>.</remarks>
 		public int ScrollOffsetVertical {
 			get => scrollOffsetVertical;
 			set {
@@ -194,12 +188,11 @@ namespace Terminal.Gui {
 			}
 		}
 
-
 		/// <summary>
-		/// The amount of tree view that has been scrolled to the right (horizontally)
+		/// The amount of tree view that has been scrolled to the right (horizontally).
 		/// </summary>
-		/// <remarks>Setting a value of less than 0 will result in a offset of 0.  To see changes 
-		/// in the UI call <see cref="View.SetNeedsDisplay()"/></remarks>
+		/// <remarks>Setting a value of less than 0 will result in a offset of 0. To see changes 
+		/// in the UI call <see cref="View.SetNeedsDisplay()"/>.</remarks>
 		public int ScrollOffsetHorizontal {
 			get => scrollOffsetHorizontal;
 			set {
@@ -208,37 +201,42 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// The current number of rows in the tree (ignoring the controls bounds)
+		/// The current number of rows in the tree (ignoring the controls bounds).
 		/// </summary>
 		public int ContentHeight => BuildLineMap ().Count ();
 
 		/// <summary>
-		/// Returns the string representation of model objects hosted in the tree.  Default 
-		/// implementation is to call <see cref="object.ToString"/>
+		/// Returns the string representation of model objects hosted in the tree. Default 
+		/// implementation is to call <see cref="object.ToString"/>.
 		/// </summary>
 		/// <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 {
-				if (desiredCursorVisibility != value && HasFocus) {
-					Application.Driver.SetCursorVisibility (value);
+				if (desiredCursorVisibility != value) {
+					desiredCursorVisibility = value;
+					if (HasFocus) {
+						Application.Driver.SetCursorVisibility (DesiredCursorVisibility);
+					}
 				}
-
-				desiredCursorVisibility = value;
 			}
 		}
 
 		/// <summary>
-		/// Creates a new tree view with absolute positioning.  
+		/// Creates a new tree view with absolute positioning. 
 		/// Use <see cref="AddObjects(IEnumerable{T})"/> to set set root objects for the tree.
-		/// Children will not be rendered until you set <see cref="TreeBuilder"/>
+		/// Children will not be rendered until you set <see cref="TreeBuilder"/>.
 		/// </summary>
 		public TreeView () : base ()
 		{
@@ -295,7 +293,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Initialises <see cref="TreeBuilder"/>.Creates a new tree view with absolute 
-		/// positioning.  Use <see cref="AddObjects(IEnumerable{T})"/> to set set root 
+		/// positioning. Use <see cref="AddObjects(IEnumerable{T})"/> to set set root 
 		/// objects for the tree.
 		/// </summary>
 		public TreeView (ITreeBuilder<T> builder) : this ()
@@ -312,7 +310,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Adds a new root level object unless it is already a root of the tree
+		/// Adds a new root level object unless it is already a root of the tree.
 		/// </summary>
 		/// <param name="o"></param>
 		public void AddObject (T o)
@@ -324,9 +322,8 @@ namespace Terminal.Gui {
 			}
 		}
 
-
 		/// <summary>
-		/// Removes all objects from the tree and clears <see cref="SelectedObject"/>
+		/// Removes all objects from the tree and clears <see cref="SelectedObject"/>.
 		/// </summary>
 		public void ClearObjects ()
 		{
@@ -341,7 +338,7 @@ namespace Terminal.Gui {
 		/// Removes the given root object from the tree
 		/// </summary>
 		/// <remarks>If <paramref name="o"/> is the currently <see cref="SelectedObject"/> then the
-		/// selection is cleared</remarks>
+		/// selection is cleared</remarks>.
 		/// <param name="o"></param>
 		public void Remove (T o)
 		{
@@ -357,9 +354,9 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Adds many new root level objects.  Objects that are already root objects are ignored
+		/// Adds many new root level objects. Objects that are already root objects are ignored.
 		/// </summary>
-		/// <param name="collection">Objects to add as new root level objects</param>
+		/// <param name="collection">Objects to add as new root level objects.</param>.\
 		public void AddObjects (IEnumerable<T> collection)
 		{
 			bool objectsAdded = false;
@@ -378,13 +375,13 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Refreshes the state of the object <paramref name="o"/> in the tree.  This will 
-		/// recompute children, string representation etc
+		/// Refreshes the state of the object <paramref name="o"/> in the tree. This will 
+		/// recompute children, string representation etc.
 		/// </summary>
 		/// <remarks>This has no effect if the object is not exposed in the tree.</remarks>
 		/// <param name="o"></param>
 		/// <param name="startAtTop">True to also refresh all ancestors of the objects branch 
-		/// (starting with the root).  False to refresh only the passed node</param>
+		/// (starting with the root). False to refresh only the passed node.</param>
 		public void RefreshObject (T o, bool startAtTop = false)
 		{
 			var branch = ObjectToBranch (o);
@@ -399,7 +396,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Rebuilds the tree structure for all exposed objects starting with the root objects.
 		/// Call this method when you know there are changes to the tree but don't know which 
-		/// objects have changed (otherwise use <see cref="RefreshObject(T, bool)"/>)
+		/// objects have changed (otherwise use <see cref="RefreshObject(T, bool)"/>).
 		/// </summary>
 		public void RebuildTree ()
 		{
@@ -412,10 +409,10 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Returns the currently expanded children of the passed object.  Returns an empty
-		/// collection if the branch is not exposed or not expanded
+		/// Returns the currently expanded children of the passed object. Returns an empty
+		/// collection if the branch is not exposed or not expanded.
 		/// </summary>
-		/// <param name="o">An object in the tree</param>
+		/// <param name="o">An object in the tree.</param>
 		/// <returns></returns>
 		public IEnumerable<T> GetChildren (T o)
 		{
@@ -428,10 +425,10 @@ namespace Terminal.Gui {
 			return branch.ChildBranches?.Values?.Select (b => b.Model)?.ToArray () ?? new T [0];
 		}
 		/// <summary>
-		/// Returns the parent object of <paramref name="o"/> in the tree.  Returns null if 
-		/// the object is not exposed in the tree
+		/// Returns the parent object of <paramref name="o"/> in the tree. Returns null if 
+		/// the object is not exposed in the tree.
 		/// </summary>
-		/// <param name="o">An object in the tree</param>
+		/// <param name="o">An object in the tree.</param>
 		/// <returns></returns>
 		public T GetParent (T o)
 		{
@@ -468,20 +465,19 @@ namespace Terminal.Gui {
 					Driver.SetAttribute (GetNormalColor ());
 					Driver.AddStr (new string (' ', bounds.Width));
 				}
-
 			}
 		}
 
 		/// <summary>
 		/// Returns the index of the object <paramref name="o"/> if it is currently exposed (it's 
-		/// parent(s) have been expanded).  This can be used with <see cref="ScrollOffsetVertical"/>
-		/// and <see cref="View.SetNeedsDisplay()"/> to scroll to a specific object
+		/// parent(s) have been expanded). This can be used with <see cref="ScrollOffsetVertical"/>
+		/// and <see cref="View.SetNeedsDisplay()"/> to scroll to a specific object.
 		/// </summary>
 		/// <remarks>Uses the Equals method and returns the first index at which the object is found
-		///  or -1 if it is not found</remarks>
-		/// <param name="o">An object that appears in your tree and is currently exposed</param>
+		/// or -1 if it is not found.</remarks>
+		/// <param name="o">An object that appears in your tree and is currently exposed.</param>
 		/// <returns>The index the object was found at or -1 if it is not currently revealed or
-		/// not in the tree at all</returns>
+		/// not in the tree at all.</returns>
 		public int GetScrollOffsetOf (T o)
 		{
 			var map = BuildLineMap ();
@@ -496,11 +492,11 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Returns the maximum width line in the tree including prefix and expansion symbols
+		/// Returns the maximum width line in the tree including prefix and expansion symbols.
 		/// </summary>
 		/// <param name="visible">True to consider only rows currently visible (based on window
-		///  bounds and <see cref="ScrollOffsetVertical"/>.  False to calculate the width of 
-		/// every exposed branch in the tree</param>
+		/// bounds and <see cref="ScrollOffsetVertical"/>. False to calculate the width of 
+		/// every exposed branch in the tree.</param>
 		/// <returns></returns>
 		public int GetContentWidth (bool visible)
 		{
@@ -531,7 +527,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Calculates all currently visible/expanded branches (including leafs) and outputs them 
-		/// by index from the top of the screen
+		/// by index from the top of the screen.
 		/// </summary>
 		/// <remarks>Index 0 of the returned array is the first item that should be visible in the
 		/// top of the control, index 1 is the next etc.</remarks>
@@ -548,7 +544,11 @@ namespace Terminal.Gui {
 				toReturn.AddRange (AddToLineMap (root));
 			}
 
-			return cachedLineMap = new ReadOnlyCollection<Branch<T>> (toReturn);
+			cachedLineMap = new ReadOnlyCollection<Branch<T>> (toReturn);
+			
+			// Update the collection used for search-typing
+			KeystrokeNavigator.Collection = cachedLineMap.Select (b => AspectGetter (b.Model)).ToArray ();
+			return cachedLineMap;
 		}
 
 		private IEnumerable<Branch<T>> AddToLineMap (Branch<T> currentBranch)
@@ -556,7 +556,6 @@ namespace Terminal.Gui {
 			yield return currentBranch;
 
 			if (currentBranch.IsExpanded) {
-
 				foreach (var subBranch in currentBranch.ChildBranches.Values) {
 					foreach (var sub in AddToLineMap (subBranch)) {
 						yield return sub;
@@ -565,6 +564,12 @@ namespace Terminal.Gui {
 			}
 		}
 
+		/// <summary>
+		/// Gets the <see cref="CollectionNavigator"/> that searches the <see cref="Objects"/> collection as
+		/// the user types.
+		/// </summary>
+		public CollectionNavigator KeystrokeNavigator { get; private set; } = new CollectionNavigator ();
+
 		/// <inheritdoc/>
 		public override bool ProcessKey (KeyEvent keyEvent)
 		{
@@ -572,21 +577,33 @@ namespace Terminal.Gui {
 				return false;
 			}
 
-			// if it is a single character pressed without any control keys
-			if (keyEvent.KeyValue > 0 && keyEvent.KeyValue < 0xFFFF) {
-
-				if (char.IsLetterOrDigit ((char)keyEvent.KeyValue) && AllowLetterBasedNavigation && !keyEvent.IsShift && !keyEvent.IsAlt && !keyEvent.IsCtrl) {
-					AdjustSelectionToNextItemBeginningWith ((char)keyEvent.KeyValue);
-					return true;
-				}
-			}
-
 			try {
+				// First of all deal with any registered keybindings
 				var result = InvokeKeybindings (keyEvent);
-				if (result != null)
+				if (result != null) {
 					return (bool)result;
-			} finally {
+				}
+
+				// If not a keybinding, is the key a searchable key press?
+				if (CollectionNavigator.IsCompatibleKey (keyEvent) && AllowLetterBasedNavigation) {
+					IReadOnlyCollection<Branch<T>> map;
+
+					// If there has been a call to InvalidateMap since the last time
+					// we need a new one to reflect the new exposed tree state
+					map = BuildLineMap ();
 
+					// Find the current selected object within the tree
+					var current = map.IndexOf (b => b.Model == SelectedObject);
+					var newIndex = KeystrokeNavigator?.GetNextMatchingItem (current, (char)keyEvent.KeyValue);
+
+					if (newIndex is int && newIndex != -1) {
+						SelectedObject = map.ElementAt ((int)newIndex).Model;
+						EnsureVisible (selectedObject);
+						SetNeedsDisplay ();
+						return true;
+					}
+				}
+			} finally {
 				PositionCursor ();
 			}
 
@@ -597,7 +614,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// <para>Triggers the <see cref="ObjectActivated"/> event with the <see cref="SelectedObject"/>.</para>
 		/// 
-		/// <para>This method also ensures that the selected object is visible</para>
+		/// <para>This method also ensures that the selected object is visible.</para>
 		/// </summary>
 		public void ActivateSelectedObjectIfAny ()
 		{
@@ -622,7 +639,7 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// <param name="toFind"></param>
 		/// <returns></returns>
-		public int? GetObjectRow(T toFind)
+		public int? GetObjectRow (T toFind)
 		{
 			var idx = BuildLineMap ().IndexOf (o => o.Model.Equals (toFind));
 
@@ -633,11 +650,11 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// <para>Moves the <see cref="SelectedObject"/> to the next item that begins with <paramref name="character"/></para>
-		/// <para>This method will loop back to the start of the tree if reaching the end without finding a match</para>
+		/// <para>Moves the <see cref="SelectedObject"/> to the next item that begins with <paramref name="character"/>.</para>
+		/// <para>This method will loop back to the start of the tree if reaching the end without finding a match.</para>
 		/// </summary>
-		/// <param name="character">The first character of the next item you want selected</param>
-		/// <param name="caseSensitivity">Case sensitivity of the search</param>
+		/// <param name="character">The first character of the next item you want selected.</param>
+		/// <param name="caseSensitivity">Case sensitivity of the search.</param>
 		public void AdjustSelectionToNextItemBeginningWith (char character, StringComparison caseSensitivity = StringComparison.CurrentCultureIgnoreCase)
 		{
 			// search for next branch that begins with that letter
@@ -650,7 +667,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Moves the selection up by the height of the control (1 page).
 		/// </summary>
-		/// <param name="expandSelection">True if the navigation should add the covered nodes to the selected current selection</param>
+		/// <param name="expandSelection">True if the navigation should add the covered nodes to the selected current selection.</param>
 		/// <exception cref="NotImplementedException"></exception>
 		public void MovePageUp (bool expandSelection = false)
 		{
@@ -660,7 +677,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Moves the selection down by the height of the control (1 page).
 		/// </summary>
-		/// <param name="expandSelection">True if the navigation should add the covered nodes to the selected current selection</param>
+		/// <param name="expandSelection">True if the navigation should add the covered nodes to the selected current selection.</param>
 		/// <exception cref="NotImplementedException"></exception>
 		public void MovePageDown (bool expandSelection = false)
 		{
@@ -668,25 +685,29 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Scrolls the view area down a single line without changing the current selection
+		/// Scrolls the view area down a single line without changing the current selection.
 		/// </summary>
 		public void ScrollDown ()
 		{
-			ScrollOffsetVertical++;
-			SetNeedsDisplay ();
+			if (ScrollOffsetVertical <= ContentHeight - 2) {
+				ScrollOffsetVertical++;
+				SetNeedsDisplay ();
+			}
 		}
 
 		/// <summary>
-		/// Scrolls the view area up a single line without changing the current selection
+		/// Scrolls the view area up a single line without changing the current selection.
 		/// </summary>
 		public void ScrollUp ()
 		{
-			ScrollOffsetVertical--;
-			SetNeedsDisplay ();
+			if (scrollOffsetVertical > 0) {
+				ScrollOffsetVertical--;
+				SetNeedsDisplay ();
+			}
 		}
 
 		/// <summary>
-		/// Raises the <see cref="ObjectActivated"/> event
+		/// Raises the <see cref="ObjectActivated"/> event.
 		/// </summary>
 		/// <param name="e"></param>
 		protected virtual void OnObjectActivated (ObjectActivatedEventArgs<T> e)
@@ -695,15 +716,15 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Returns the object in the tree list that is currently visible
-		/// at the provided row.  Returns null if no object is at that location.
+		/// Returns the object in the tree list that is currently visible.
+		/// at the provided row. Returns null if no object is at that location.
 		/// <remarks>
 		/// </remarks>
 		/// If you have screen coordinates then use <see cref="View.ScreenToView(int, int)"/>
 		/// to translate these into the client area of the <see cref="TreeView{T}"/>.
 		/// </summary>
-		/// <param name="row">The row of the <see cref="View.Bounds"/> of the <see cref="TreeView{T}"/></param>
-		/// <returns>The object currently displayed on this row or null</returns>
+		/// <param name="row">The row of the <see cref="View.Bounds"/> of the <see cref="TreeView{T}"/>.</param>
+		/// <returns>The object currently displayed on this row or null.</returns>
 		public T GetObjectOnRow (int row)
 		{
 			return HitTest (row)?.Model;
@@ -728,7 +749,6 @@ namespace Terminal.Gui {
 				SetFocus ();
 			}
 
-
 			if (me.Flags == MouseFlags.WheeledDown) {
 
 				ScrollDown ();
@@ -784,7 +804,6 @@ namespace Terminal.Gui {
 						multiSelectedRegions.Clear ();
 					}
 				} else {
-
 					// It is a first click somewhere in the current line that doesn't look like an expansion/collapse attempt
 					SelectedObject = clickedBranch.Model;
 					multiSelectedRegions.Clear ();
@@ -814,16 +833,15 @@ namespace Terminal.Gui {
 				// mouse event is handled.
 				return true;
 			}
-
 			return false;
 		}
 
 		/// <summary>
 		/// Returns the branch at the given <paramref name="y"/> client
-		/// coordinate e.g. following a click event
+		/// coordinate e.g. following a click event.
 		/// </summary>
-		/// <param name="y">Client Y position in the controls bounds</param>
-		/// <returns>The clicked branch or null if outside of tree region</returns>
+		/// <param name="y">Client Y position in the controls bounds.</param>
+		/// <returns>The clicked branch or null if outside of tree region.</returns>
 		private Branch<T> HitTest (int y)
 		{
 			var map = BuildLineMap ();
@@ -840,7 +858,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Positions the cursor at the start of the selected objects line (if visible)
+		/// Positions the cursor at the start of the selected objects line (if visible).
 		/// </summary>
 		public override void PositionCursor ()
 		{
@@ -861,11 +879,10 @@ namespace Terminal.Gui {
 			}
 		}
 
-
 		/// <summary>
-		/// Determines systems behaviour when the left arrow key is pressed.  Default behaviour is
+		/// Determines systems behaviour when the left arrow key is pressed. Default behaviour is
 		/// to collapse the current tree node if possible otherwise changes selection to current 
-		/// branches parent
+		/// branches parent.
 		/// </summary>
 		protected virtual void CursorLeft (bool ctrl)
 		{
@@ -889,7 +906,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Changes the <see cref="SelectedObject"/> to the first root object and resets 
-		/// the <see cref="ScrollOffsetVertical"/> to 0
+		/// the <see cref="ScrollOffsetVertical"/> to 0.
 		/// </summary>
 		public void GoToFirst ()
 		{
@@ -901,7 +918,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Changes the <see cref="SelectedObject"/> to the last object in the tree and scrolls so
-		/// that it is visible
+		/// that it is visible.
 		/// </summary>
 		public void GoToEnd ()
 		{
@@ -914,8 +931,8 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Changes the <see cref="SelectedObject"/> to <paramref name="toSelect"/> and scrolls to ensure
-		/// it is visible.  Has no effect if <paramref name="toSelect"/> is not exposed in the tree (e.g. 
-		/// its parents are collapsed)
+		/// it is visible. Has no effect if <paramref name="toSelect"/> is not exposed in the tree (e.g. 
+		/// its parents are collapsed).
 		/// </summary>
 		/// <param name="toSelect"></param>
 		public void GoTo (T toSelect)
@@ -930,14 +947,14 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// The number of screen lines to move the currently selected object by.  Supports negative 
-		/// <paramref name="offset"/>.  Each branch occupies 1 line on screen
+		/// The number of screen lines to move the currently selected object by. Supports negative values.
+		/// <paramref name="offset"/>. Each branch occupies 1 line on screen.
 		/// </summary>
 		/// <remarks>If nothing is currently selected or the selected object is no longer in the tree
-		/// then the first object in the tree is selected instead</remarks>
+		/// then the first object in the tree is selected instead.</remarks>
 		/// <param name="offset">Positive to move the selection down the screen, negative to move it up</param>
 		/// <param name="expandSelection">True to expand the selection (assuming 
-		/// <see cref="MultiSelect"/> is enabled).  False to replace</param>
+		/// <see cref="MultiSelect"/> is enabled). False to replace.</param>
 		public void AdjustSelection (int offset, bool expandSelection = false)
 		{
 			// if it is not a shift click or we don't allow multi select
@@ -953,7 +970,6 @@ namespace Terminal.Gui {
 				var idx = map.IndexOf (b => b.Model.Equals (SelectedObject));
 
 				if (idx == -1) {
-
 					// The current selection has disapeared!
 					SelectedObject = roots.Keys.FirstOrDefault ();
 				} else {
@@ -977,14 +993,12 @@ namespace Terminal.Gui {
 
 					EnsureVisible (SelectedObject);
 				}
-
 			}
-
 			SetNeedsDisplay ();
 		}
 
 		/// <summary>
-		/// Moves the selection to the first child in the currently selected level
+		/// Moves the selection to the first child in the currently selected level.
 		/// </summary>
 		public void AdjustSelectionToBranchStart ()
 		{
@@ -1024,7 +1038,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Moves the selection to the last child in the currently selected level
+		/// Moves the selection to the last child in the currently selected level.
 		/// </summary>
 		public void AdjustSelectionToBranchEnd ()
 		{
@@ -1058,13 +1072,12 @@ namespace Terminal.Gui {
 				currentBranch = next;
 				next = map.ElementAt (currentIdx);
 			}
-
 			GoToEnd ();
 		}
 
 
 		/// <summary>
-		/// Sets the selection to the next branch that matches the <paramref name="predicate"/>
+		/// Sets the selection to the next branch that matches the <paramref name="predicate"/>.
 		/// </summary>
 		/// <param name="predicate"></param>
 		private void AdjustSelectionToNext (Func<Branch<T>, bool> predicate)
@@ -1102,7 +1115,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Adjusts the <see cref="ScrollOffsetVertical"/> to ensure the given
-		/// <paramref name="model"/> is visible.  Has no effect if already visible
+		/// <paramref name="model"/> is visible. Has no effect if already visible.
 		/// </summary>
 		public void EnsureVisible (T model)
 		{
@@ -1129,7 +1142,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Expands the currently <see cref="SelectedObject"/>
+		/// Expands the currently <see cref="SelectedObject"/>.
 		/// </summary>
 		public void Expand ()
 		{
@@ -1138,9 +1151,9 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Expands the supplied object if it is contained in the tree (either as a root object or 
-		/// as an exposed branch object)
+		/// as an exposed branch object).
 		/// </summary>
-		/// <param name="toExpand">The object to expand</param>
+		/// <param name="toExpand">The object to expand.</param>
 		public void Expand (T toExpand)
 		{
 			if (toExpand == null) {
@@ -1153,9 +1166,9 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Expands the supplied object and all child objects
+		/// Expands the supplied object and all child objects.
 		/// </summary>
-		/// <param name="toExpand">The object to expand</param>
+		/// <param name="toExpand">The object to expand.</param>
 		public void ExpandAll (T toExpand)
 		{
 			if (toExpand == null) {
@@ -1168,7 +1181,7 @@ namespace Terminal.Gui {
 		}
 		/// <summary>
 		/// Fully expands all nodes in the tree, if the tree is very big and built dynamically this
-		/// may take a while (e.g. for file system)
+		/// may take a while (e.g. for file system).
 		/// </summary>
 		public void ExpandAll ()
 		{
@@ -1181,7 +1194,7 @@ namespace Terminal.Gui {
 		}
 		/// <summary>
 		/// Returns true if the given object <paramref name="o"/> is exposed in the tree and can be
-		/// expanded otherwise false
+		/// expanded otherwise false.
 		/// </summary>
 		/// <param name="o"></param>
 		/// <returns></returns>
@@ -1192,7 +1205,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Returns true if the given object <paramref name="o"/> is exposed in the tree and 
-		/// expanded otherwise false
+		/// expanded otherwise false.
 		/// </summary>
 		/// <param name="o"></param>
 		/// <returns></returns>
@@ -1210,26 +1223,26 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Collapses the supplied object if it is currently expanded 
+		/// Collapses the supplied object if it is currently expanded .
 		/// </summary>
-		/// <param name="toCollapse">The object to collapse</param>
+		/// <param name="toCollapse">The object to collapse.</param>
 		public void Collapse (T toCollapse)
 		{
 			CollapseImpl (toCollapse, false);
 		}
 
 		/// <summary>
-		/// Collapses the supplied object if it is currently expanded.  Also collapses all children
-		/// branches (this will only become apparent when/if the user expands it again)
+		/// Collapses the supplied object if it is currently expanded. Also collapses all children
+		/// branches (this will only become apparent when/if the user expands it again).
 		/// </summary>
-		/// <param name="toCollapse">The object to collapse</param>
+		/// <param name="toCollapse">The object to collapse.</param>
 		public void CollapseAll (T toCollapse)
 		{
 			CollapseImpl (toCollapse, true);
 		}
 
 		/// <summary>
-		/// Collapses all root nodes in the tree
+		/// Collapses all root nodes in the tree.
 		/// </summary>
 		public void CollapseAll ()
 		{
@@ -1242,19 +1255,17 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Implementation of <see cref="Collapse(T)"/> and <see cref="CollapseAll(T)"/>.  Performs
-		/// operation and updates selection if disapeared
+		/// Implementation of <see cref="Collapse(T)"/> and <see cref="CollapseAll(T)"/>. Performs
+		/// operation and updates selection if disapeared.
 		/// </summary>
 		/// <param name="toCollapse"></param>
 		/// <param name="all"></param>
 		protected void CollapseImpl (T toCollapse, bool all)
 		{
-
 			if (toCollapse == null) {
 				return;
 			}
 
-
 			var branch = ObjectToBranch (toCollapse);
 
 			// Nothing to collapse
@@ -1287,12 +1298,12 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Returns the corresponding <see cref="Branch{T}"/> in the tree for
-		/// <paramref name="toFind"/>.  This will not work for objects hidden
-		/// by their parent being collapsed
+		/// <paramref name="toFind"/>. This will not work for objects hidden
+		/// by their parent being collapsed.
 		/// </summary>
 		/// <param name="toFind"></param>
 		/// <returns>The branch for <paramref name="toFind"/> or null if it is not currently 
-		/// exposed in the tree</returns>
+		/// exposed in the tree.</returns>
 		private Branch<T> ObjectToBranch (T toFind)
 		{
 			return BuildLineMap ().FirstOrDefault (o => o.Model.Equals (toFind));
@@ -1300,7 +1311,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Returns true if the <paramref name="model"/> is either the 
-		/// <see cref="SelectedObject"/> or part of a <see cref="MultiSelect"/>
+		/// <see cref="SelectedObject"/> or part of a <see cref="MultiSelect"/>.
 		/// </summary>
 		/// <param name="model"></param>
 		/// <returns></returns>
@@ -1335,7 +1346,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Selects all objects in the tree when <see cref="MultiSelect"/> is enabled otherwise 
-		/// does nothing
+		/// does nothing.
 		/// </summary>
 		public void SelectAll ()
 		{
@@ -1357,9 +1368,8 @@ namespace Terminal.Gui {
 			OnSelectionChanged (new SelectionChangedEventArgs<T> (this, SelectedObject, SelectedObject));
 		}
 
-
 		/// <summary>
-		/// Raises the SelectionChanged event
+		/// Raises the SelectionChanged event.
 		/// </summary>
 		/// <param name="e"></param>
 		protected virtual void OnSelectionChanged (SelectionChangedEventArgs<T> e)
@@ -1401,5 +1411,4 @@ namespace Terminal.Gui {
 			return included.Contains (model);
 		}
 	}
-
 }

+ 7 - 3
Terminal.Gui/Windows/Dialog.cs

@@ -20,7 +20,7 @@ namespace Terminal.Gui {
 	///  or buttons added to the dialog calls <see cref="Application.RequestStop"/>.
 	/// </remarks>
 	public class Dialog : Window {
-		List<Button> buttons = new List<Button> ();
+		internal List<Button> buttons = new List<Button> ();
 		const int padding = 0;
 
 		/// <summary>
@@ -164,7 +164,11 @@ namespace Terminal.Gui {
 				for (int i = buttons.Count - 1; i >= 0; i--) {
 					Button button = buttons [i];
 					shiftLeft += button.Frame.Width + (i == buttons.Count - 1 ? 0 : 1);
-					button.X = Pos.AnchorEnd (shiftLeft);
+					if (shiftLeft > -1) {
+						button.X = Pos.AnchorEnd (shiftLeft);
+					} else {
+						button.X = Frame.Width - shiftLeft;
+					}
 					button.Y = Pos.AnchorEnd (1);
 				}
 				break;
@@ -173,7 +177,7 @@ namespace Terminal.Gui {
 				// Justify Buttons
 				// leftmost and rightmost buttons are hard against edges. The rest are evenly spaced.
 
-				var spacing = (int)Math.Ceiling ((double)(Bounds.Width - buttonsWidth - 2) / (buttons.Count - 1));
+				var spacing = (int)Math.Ceiling ((double)(Bounds.Width - buttonsWidth - (Border.DrawMarginFrame ? 2 : 0)) / (buttons.Count - 1));
 				for (int i = buttons.Count - 1; i >= 0; i--) {
 					Button button = buttons [i];
 					if (i == buttons.Count - 1) {

+ 43 - 7
Terminal.Gui/Windows/FileDialog.cs

@@ -41,7 +41,7 @@ namespace Terminal.Gui {
 			if (allowedFileTypes == null)
 				return true;
 			foreach (var ft in allowedFileTypes)
-				if (fsi.Name.EndsWith (ft) || ft == ".*")
+				if (fsi.Name.EndsWith (ft, StringComparison.InvariantCultureIgnoreCase) || ft == ".*")
 					return true;
 			return false;
 		}
@@ -83,6 +83,7 @@ namespace Terminal.Gui {
 				case DirectoryNotFoundException _:
 				case ArgumentException _:
 					dirInfo = null;
+					watcher?.Dispose ();
 					watcher = null;
 					infos.Clear ();
 					valid = true;
@@ -104,7 +105,15 @@ namespace Terminal.Gui {
 		{
 			if (!_disposedValue) {
 				if (disposing) {
+					if (watcher != null) {
+						watcher.Changed -= Watcher_Changed;
+						watcher.Created -= Watcher_Changed;
+						watcher.Deleted -= Watcher_Changed;
+						watcher.Renamed -= Watcher_Changed;
+						watcher.Error -= Watcher_Error;
+					}
 					watcher?.Dispose ();
+					watcher = null;
 				}
 
 				_disposedValue = true;
@@ -116,11 +125,17 @@ namespace Terminal.Gui {
 
 		void Watcher_Error (object sender, ErrorEventArgs e)
 		{
+			if (Application.MainLoop == null)
+				return;
+
 			Application.MainLoop.Invoke (() => Reload ());
 		}
 
 		void Watcher_Changed (object sender, FileSystemEventArgs e)
 		{
+			if (Application.MainLoop == null)
+				return;
+
 			Application.MainLoop.Invoke (() => Reload ());
 		}
 
@@ -656,12 +671,17 @@ namespace Terminal.Gui {
 				X = Pos.Right (nameEntry) + 2,
 				Y = Pos.Top (nameEntry),
 				Width = Dim.Fill (1),
-				Height = allowedTypes != null ? allowedTypes.Count + 1 : 1,
+				Height = SetComboBoxHeight (allowedTypes),
 				Text = allowedTypes?.Count > 0 ? allowedTypes [0] : string.Empty,
-				ReadOnly = true
+				SelectedItem = allowedTypes?.Count > 0 ? 0 : -1,
+				ReadOnly = true,
+				HideDropdownListOnClick = true
 			};
 			cmbAllowedTypes.SetSource (allowedTypes ?? new List<string> ());
-			cmbAllowedTypes.OpenSelectedItem += (e) => AllowedFileTypes = cmbAllowedTypes.Text.ToString ().Split (';');
+			cmbAllowedTypes.OpenSelectedItem += (e) => {
+				dirListView.AllowedFileTypes = cmbAllowedTypes.Text.ToString ().Split (';');
+				dirListView.Reload ();
+			};
 			Add (cmbAllowedTypes);
 
 			dirListView = new DirListView (this) {
@@ -673,7 +693,7 @@ namespace Terminal.Gui {
 			DirectoryPath = Path.GetFullPath (Environment.CurrentDirectory);
 			Add (dirListView);
 
-			AllowedFileTypes = cmbAllowedTypes.Text.ToString ().Split (';');
+			AllowedFileTypes = allowedTypes?.Count > 0 ? allowedTypes?.ToArray () : null;
 			dirListView.DirectoryChanged = (dir) => { nameEntry.Text = ustring.Empty; dirEntry.Text = dir; };
 			dirListView.FileChanged = (file) => nameEntry.Text = file == ".." ? "" : file;
 			dirListView.SelectedChanged = (file) => nameEntry.Text = file.Item1 == ".." ? "" : file.Item1;
@@ -738,6 +758,11 @@ namespace Terminal.Gui {
 			}
 		}
 
+		private static int SetComboBoxHeight (List<string> allowedTypes)
+		{
+			return allowedTypes != null ? Math.Min (allowedTypes.Count + 1, 8) : 8;
+		}
+
 		internal bool canceled;
 
 		///<inheritdoc/>
@@ -821,13 +846,24 @@ namespace Terminal.Gui {
 			}
 		}
 
+		private string [] allowedFileTypes;
+
 		/// <summary>
 		/// The array of filename extensions allowed, or null if all file extensions are allowed.
 		/// </summary>
 		/// <value>The allowed file types.</value>
 		public string [] AllowedFileTypes {
-			get => dirListView.AllowedFileTypes;
-			set => dirListView.AllowedFileTypes = value;
+			get => allowedFileTypes;
+			set {
+				allowedFileTypes = value;
+				var selected = cmbAllowedTypes.SelectedItem;
+				cmbAllowedTypes.SetSource (value);
+				cmbAllowedTypes.SelectedItem = selected > -1 ? selected : 0;
+				SetComboBoxHeight (value?.ToList ());
+				dirListView.AllowedFileTypes = value != null
+					? value [cmbAllowedTypes.SelectedItem].Split (';')
+					: null;
+			}
 		}
 
 		/// <summary>

+ 5 - 2
Terminal.Gui/Windows/MessageBox.cs

@@ -238,7 +238,10 @@ namespace Terminal.Gui {
 		static int QueryFull (bool useErrorColors, int width, int height, ustring title, ustring message,
 			int defaultButton = 0, Border border = null, params ustring [] buttons)
 		{
-			const int defaultWidth = 50;
+			int defaultWidth = 50;
+			if (defaultWidth > Application.Driver.Cols / 2) {
+				defaultWidth = (int)(Application.Driver.Cols * 0.60f);
+			}
 			int maxWidthLine = TextFormatter.MaxWidthLine (message);
 			if (maxWidthLine > Application.Driver.Cols) {
 				maxWidthLine = Application.Driver.Cols;
@@ -300,7 +303,7 @@ namespace Terminal.Gui {
 
 			if (width == 0 & height == 0) {
 				// Dynamically size Width
-				d.Width = Math.Min (Math.Max (maxWidthLine, Math.Max (title.ConsoleWidth, Math.Max (textWidth + 2, d.GetButtonsWidth ()))), Application.Driver.Cols); // textWidth + (left + padding + padding + right)
+				d.Width = Math.Min (Math.Max (maxWidthLine, Math.Max (title.ConsoleWidth, Math.Max (textWidth + 2, d.GetButtonsWidth () + d.buttons.Count + 2))), Application.Driver.Cols); // textWidth + (left + padding + padding + right)
 			}
 
 			// Setup actions

+ 1 - 1
Terminal.Gui/Windows/Wizard.cs

@@ -159,7 +159,7 @@ namespace Terminal.Gui {
 			public event Action<TitleEventArgs> TitleChanged;
 
 			// The contentView works like the ContentView in FrameView.
-			private View contentView = new View ();
+			private View contentView = new View () { Data = "WizardContentView" };
 
 			/// <summary>
 			/// Sets or gets help text for the <see cref="WizardStep"/>.If <see cref="WizardStep.HelpText"/> is empty

+ 1 - 0
Terminal.sln

@@ -20,6 +20,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
 		.github\workflows\dotnet-core.yml = .github\workflows\dotnet-core.yml
 		.github\workflows\publish.yml = .github\workflows\publish.yml
 		README.md = README.md
+		testenvironments.json = testenvironments.json
 	EndProjectSection
 EndProject
 Global

+ 128 - 0
Terminal.sln.DotSettings

@@ -0,0 +1,128 @@
+<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
+	<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeAccessorOwnerBody/@EntryIndexedValue">WARNING</s:String>
+	<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CheckNamespace/@EntryIndexedValue">DO_NOT_SHOW</s:String>
+	<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MemberCanBePrivate_002EGlobal/@EntryIndexedValue">HINT</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/ACCESSOR_OWNER_BODY/@EntryValue">ExpressionBody</s:String>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/ARGUMENTS_SKIP_SINGLE/@EntryValue">True</s:Boolean>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_FOR/@EntryValue">NotRequired</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_FOREACH/@EntryValue">NotRequired</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_IFELSE/@EntryValue">NotRequired</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_LOCK/@EntryValue">RequiredForMultiline</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_WHILE/@EntryValue">RequiredForMultiline</s:String>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_REDUNDANT/@EntryValue">False</s:Boolean>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/CONSTRUCTOR_OR_DESTRUCTOR_BODY/@EntryValue">BlockBody</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/DEFAULT_INTERNAL_MODIFIER/@EntryValue">Explicit</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/DEFAULT_PRIVATE_MODIFIER/@EntryValue">Implicit</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/FORCE_ATTRIBUTE_STYLE/@EntryValue">Separate</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/LOCAL_FUNCTION_BODY/@EntryValue">BlockBody</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/METHOD_OR_OPERATOR_BODY/@EntryValue">BlockBody</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/MODIFIERS_ORDER/@EntryValue">internal volatile public private new static async protected extern sealed override virtual unsafe abstract readonly</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/NAMESPACE_BODY/@EntryValue">FileScoped</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/OBJECT_CREATION_WHEN_TYPE_EVIDENT/@EntryValue">ExplicitlyTyped</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/OBJECT_CREATION_WHEN_TYPE_NOT_EVIDENT/@EntryValue">ExplicitlyTyped</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/PARENTHESES_GROUP_NON_OBVIOUS_OPERATIONS/@EntryValue">Conditional</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/PARENTHESES_NON_OBVIOUS_OPERATIONS/@EntryValue">Shift, Bitwise, Conditional</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/PARENTHESES_REDUNDANCY_STYLE/@EntryValue">Remove</s:String>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/PARENTHESES_SAME_TYPE_OPERATIONS/@EntryValue">True</s:Boolean>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/ThisQualifier/INSTANCE_MEMBERS_QUALIFY_DECLARED_IN/@EntryValue">0</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ACCESSOR_DECLARATION_BRACES/@EntryValue">END_OF_LINE</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ACCESSOR_OWNER_DECLARATION_BRACES/@EntryValue">END_OF_LINE</s:String>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_CALLS_CHAIN/@EntryValue">False</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_STATEMENT_CONDITIONS/@EntryValue">False</s:Boolean>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGNMENT_TAB_FILL_STYLE/@EntryValue">USE_TABS_ONLY</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ANONYMOUS_METHOD_DECLARATION_BRACES/@EntryValue">END_OF_LINE</s:String>
+	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AFTER_BLOCK_STATEMENTS/@EntryValue">0</s:Int64>
+	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AFTER_MULTILINE_STATEMENTS/@EntryValue">0</s:Int64>
+	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AFTER_START_COMMENT/@EntryValue">0</s:Int64>
+	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AFTER_USING_LIST/@EntryValue">0</s:Int64>
+	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AROUND_AUTO_PROPERTY/@EntryValue">0</s:Int64>
+	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AROUND_FIELD/@EntryValue">0</s:Int64>
+	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AROUND_INVOCABLE/@EntryValue">0</s:Int64>
+	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AROUND_LOCAL_METHOD/@EntryValue">0</s:Int64>
+	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AROUND_NAMESPACE/@EntryValue">0</s:Int64>
+	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AROUND_PROPERTY/@EntryValue">0</s:Int64>
+	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AROUND_REGION/@EntryValue">0</s:Int64>
+	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AROUND_SINGLE_LINE_TYPE/@EntryValue">0</s:Int64>
+	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AROUND_TYPE/@EntryValue">0</s:Int64>
+	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_BEFORE_SINGLE_LINE_COMMENT/@EntryValue">0</s:Int64>
+	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_INSIDE_REGION/@EntryValue">0</s:Int64>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_CASE_FROM_SWITCH/@EntryValue">False</s:Boolean>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_PARS/@EntryValue">OUTSIDE</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_PREPROCESSOR_REGION/@EntryValue">USUAL_INDENT</s:String>
+	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_SIZE/@EntryValue">8</s:Int64>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_STATEMENT_PARS/@EntryValue">OUTSIDE</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_STYLE/@EntryValue">Tab</s:String>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_TYPE_CONSTRAINTS/@EntryValue">False</s:Boolean>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INITIALIZER_BRACES/@EntryValue">END_OF_LINE</s:String>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INT_ALIGN_COMMENTS/@EntryValue">False</s:Boolean>
+	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_BLANK_LINES_IN_CODE/@EntryValue">100</s:Int64>
+	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_BLANK_LINES_IN_DECLARATIONS/@EntryValue">100</s:Int64>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_EXISTING_ATTRIBUTE_ARRANGEMENT/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_EXISTING_DECLARATION_BLOCK_ARRANGEMENT/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_EXISTING_EMBEDDED_ARRANGEMENT/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_EXISTING_EMBEDDED_BLOCK_ARRANGEMENT/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_EXISTING_ENUM_ARRANGEMENT/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_EXISTING_EXPR_MEMBER_ARRANGEMENT/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_EXISTING_INITIALIZER_ARRANGEMENT/@EntryValue">True</s:Boolean>
+	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/MAX_INITIALIZER_ELEMENTS_ON_LINE/@EntryValue">2</s:Int64>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/NESTED_TERNARY_STYLE/@EntryValue">SIMPLE_WRAP</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/OTHER_BRACES/@EntryValue">END_OF_LINE</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_ACCESSORHOLDER_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">NEVER</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_EXPR_ACCESSOR_ON_SINGLE_LINE/@EntryValue">ALWAYS</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_EXPR_METHOD_ON_SINGLE_LINE/@EntryValue">ALWAYS</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_EXPR_PROPERTY_ON_SINGLE_LINE/@EntryValue">ALWAYS</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_FIELD_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">NEVER</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_METHOD_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">NEVER</s:String>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_ACCESSOR_ON_SINGLE_LINE/@EntryValue">False</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_ANONYMOUSMETHOD_ON_SINGLE_LINE/@EntryValue">False</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_ENUM_ON_SINGLE_LINE/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_INITIALIZER_ON_SINGLE_LINE/@EntryValue">False</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_METHOD_ON_SINGLE_LINE/@EntryValue">False</s:Boolean>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_TYPE_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">NEVER</s:String>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/REMOVE_BLANK_LINES_NEAR_BRACES_IN_CODE/@EntryValue">False</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/REMOVE_BLANK_LINES_NEAR_BRACES_IN_DECLARATIONS/@EntryValue">False</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_AFTER_TYPECAST_PARENTHESES/@EntryValue">False</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_ARRAY_ACCESS_BRACKETS/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_ARRAY_RANK_BRACKETS/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_DEFAULT_PARENTHESES/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_EMPTY_METHOD_CALL_PARENTHESES/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_EMPTY_METHOD_PARENTHESES/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_EXTENDS_COLON/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_METHOD_CALL_PARENTHESES/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_METHOD_PARENTHESES/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_NAMEOF_PARENTHESES/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_NEW_PARENTHESES/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_TYPE_PARAMETER_CONSTRAINT_COLON/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_TYPEOF_PARENTHESES/@EntryValue">True</s:Boolean>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/TYPE_DECLARATION_BRACES/@EntryValue">END_OF_LINE</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_ARRAY_INITIALIZER_STYLE/@EntryValue">CHOP_ALWAYS</s:String>
+	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_BEFORE_BINARY_OPSIGN/@EntryValue">True</s:Boolean>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_CHAINED_BINARY_EXPRESSIONS/@EntryValue">WRAP_IF_LONG</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_CHAINED_METHOD_CALLS/@EntryValue">WRAP_IF_LONG</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_FOR_STMT_HEADER_STYLE/@EntryValue">WRAP_IF_LONG</s:String>
+	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_LIMIT/@EntryValue">527</s:Int64>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_LINQ_EXPRESSIONS/@EntryValue">CHOP_ALWAYS</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_MULTIPLE_DECLARATION_STYLE/@EntryValue">WRAP_IF_LONG</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_OBJECT_AND_COLLECTION_INITIALIZER_STYLE/@EntryValue">CHOP_ALWAYS</s:String>
+	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_TERNARY_EXPR_STYLE/@EntryValue">WRAP_IF_LONG</s:String>
+	<s:Boolean x:Key="/Default/CodeStyle/CSharpUsing/AddImportsToDeepestScope/@EntryValue">False</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/PreferExplicitDiscardDeclaration/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/PreferSeparateDeconstructedVariablesDeclaration/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/CodeStyle/Naming/CSharpNaming/ApplyAutoDetectedRules/@EntryValue">False</s:Boolean>
+	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=Constants/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
+	<s:Boolean x:Key="/Default/Environment/InjectedLayers/FileInjectedLayer/=CAF4ECB3AC41AE43BD233D613AC1562C/@KeyIndexDefined">True</s:Boolean>
+	<s:String x:Key="/Default/Environment/InjectedLayers/FileInjectedLayer/=CAF4ECB3AC41AE43BD233D613AC1562C/AbsolutePath/@EntryValue">Terminal.sln.DotSettings</s:String>
+	<s:String x:Key="/Default/Environment/InjectedLayers/FileInjectedLayer/=CAF4ECB3AC41AE43BD233D613AC1562C/RelativePath/@EntryValue"></s:String>
+	<s:Boolean x:Key="/Default/Environment/InjectedLayers/FileInjectedLayer/=F728A143A60F2142985180A92B1C45E8/@KeyIndexDefined">True</s:Boolean>
+	<s:String x:Key="/Default/Environment/InjectedLayers/FileInjectedLayer/=F728A143A60F2142985180A92B1C45E8/AbsolutePath/@EntryValue">/Users/alex/Development/Terminal.Gui/Terminal.sln.DotSettings</s:String>
+	<s:String x:Key="/Default/Environment/InjectedLayers/FileInjectedLayer/=F728A143A60F2142985180A92B1C45E8/RelativePath/@EntryValue"></s:String>
+	<s:Boolean x:Key="/Default/Environment/InjectedLayers/InjectedLayerCustomization/=FileCAF4ECB3AC41AE43BD233D613AC1562C/@KeyIndexDefined">True</s:Boolean>
+	<s:Double x:Key="/Default/Environment/InjectedLayers/InjectedLayerCustomization/=FileCAF4ECB3AC41AE43BD233D613AC1562C/RelativePriority/@EntryValue">1</s:Double>
+	<s:Boolean x:Key="/Default/Environment/InjectedLayers/InjectedLayerCustomization/=FileF728A143A60F2142985180A92B1C45E8/@KeyIndexDefined">True</s:Boolean>
+	<s:Double x:Key="/Default/Environment/InjectedLayers/InjectedLayerCustomization/=FileF728A143A60F2142985180A92B1C45E8/RelativePriority/@EntryValue">2</s:Double>
+	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

+ 31 - 3
UICatalog/Properties/launchSettings.json

@@ -3,10 +3,16 @@
     "UICatalog": {
       "commandName": "Project"
     },
-    "UICatalog : -usc": {
+    "UICatalog -usc": {
       "commandName": "Project",
       "commandLineArgs": "-usc"
     },
+    "WSL: UICatalog -usc": {
+      "commandName": "Executable",
+      "executablePath": "wsl",
+      "commandLineArgs": "dotnet UICatalog.dll -usc",
+      "distributionName": ""
+    },
     "Wizards": {
       "commandName": "Project",
       "commandLineArgs": "Wizards"
@@ -23,9 +29,31 @@
       "commandName": "Project",
       "commandLineArgs": "WizardAsView"
     },
-    "Issue1719Repro": {
+    "VkeyPacketSimulator": {
+      "commandName": "Project",
+      "commandLineArgs": "VkeyPacketSimulator"
+    },
+    "CollectionNavigatorTester": {
+      "commandName": "Project",
+      "commandLineArgs": "\"Search Collection Nav\""
+    },
+    "Charmap": {
       "commandName": "Project",
-      "commandLineArgs": "\"ProgressBar Styles\""
+      "commandLineArgs": "\"Character Map\""
+    },
+    "All Views Tester": {
+      "commandName": "Project",
+      "commandLineArgs": "\"All Views Tester\""
+    },
+    "Windows & FrameViews": {
+      "commandName": "Project",
+      "commandLineArgs": "\"Windows & FrameViews\""
+    },
+    "WSL : UICatalog": {
+      "commandName": "Executable",
+      "executablePath": "wsl",
+      "commandLineArgs": "dotnet UICatalog.dll",
+      "distributionName": ""
     }
   }
 }

+ 15 - 50
UICatalog/README.md

@@ -2,8 +2,9 @@
 
 UI Catalog is a comprehensive sample library for Terminal.Gui. It attempts to satisfy the following goals:
 
-1. Be an easy to use showcase for Terminal.Gui concepts and features.
-2. Provide sample code that illustrates how to properly implement said concepts & features.
+1. Be an easy-to-use showcase for Terminal.Gui concepts and features.
+2. Provide sample code that illustrates how to properly implement 
+said concepts & features.
 3. Make it easy for contributors to add additional samples in a structured way.
 
 ![screenshot](screenshot.png)
@@ -51,7 +52,7 @@ To add a new **Scenario** simply:
 4. Implement the `Setup` override which will be called when a user selects the scenario to run.
 5. Optionally, implement the `Init` and/or `Run` overrides to provide a custom implementation.
 
-The sample below is provided in the `Scenarios` directory as a generic sample that can be copied and re-named:
+The sample below is provided in the `.\UICatalog\Scenarios` directory as a generic sample that can be copied and re-named:
 
 ```csharp
 using Terminal.Gui;
@@ -73,59 +74,23 @@ namespace UICatalog {
 }
 ```
 
-`Scenario` provides a `Toplevel` and `Window` the provides a canvas for the Scenario to operate. The default `Window` shows the Scenario name and supports exiting the Scenario through the `Esc` key. 
+`Scenario` provides `Win`, a `Window` object that provides a canvas for the Scenario to operate. 
 
-![screenshot](generic_screenshot.png)
-
-To build a more advanced scenario, where control of the `Toplevel` and `Window` is needed (e.g. for scenarios using `MenuBar` or `StatusBar`), simply set the `Top` and `Window` properties as appropriate, as seen in the `UnicodeInMenu` scenario:
+The default `Window` shows the Scenario name and supports exiting the Scenario through the `Esc` key. 
 
-```csharp
-using Terminal.Gui;
+![screenshot](generic_screenshot.png)
 
-namespace UICatalog {
-	[ScenarioMetadata (Name: "Unicode In Menu", Description: "Unicode menus per PR #204")]
-	[ScenarioCategory ("Text")]
-	[ScenarioCategory ("Controls")]
-	class UnicodeInMenu : Scenario {
-		public override void Setup ()
-		{
-			Top = new Toplevel (new Rect (0, 0, Application.Driver.Cols, Application.Driver.Rows));
-			var menu = new MenuBar (new MenuBarItem [] {
-				new MenuBarItem ("_Файл", new MenuItem [] {
-					new MenuItem ("_Создать", "Creates new file", null),
-					new MenuItem ("_Открыть", "", null),
-					new MenuItem ("Со_хранить", "", null),
-					new MenuItem ("_Выход", "", () => Application.RequestStop() )
-				}),
-				new MenuBarItem ("_Edit", new MenuItem [] {
-					new MenuItem ("_Copy", "", null),
-					new MenuItem ("C_ut", "", null),
-					new MenuItem ("_Paste", "", null)
-				})
-			});
-			Top.Add (menu);
-
-			Win = new Window ($"Scenario: {GetName ()}") {
-				X = 0,
-				Y = 1,
-				Width = Dim.Fill (),
-				Height = Dim.Fill ()
-			};
-			Top.Add (Win);
-		}
-	}
-}
-```
+To build a more advanced scenario, where control of the `Toplevel` and `Window` is needed (e.g. for scenarios using `MenuBar` or `StatusBar`), simply use `Application.Top` per normal Terminal.Gui programming, as seen in the `Notepad` scenario.
 
-For complete control, the `Init` and `Run` overrides can be implemented. The `base.Init` assigns `Application.Top` to `Top` and creates `Win`. The `base.Run` simply calls `Application.Run(Top)`.
+For complete control, the `Init` and `Run` overrides can be implemented. The `base.Init` creates `Win`. The `base.Run` simply calls `Application.Run(Application.Top)`.
 
 ## Contribution Guidelines
 
-- Provide a terse, descriptive name for `Scenarios`. Keep them short; the `ListView` that displays them dynamically sizes the column width and long names will make it hard for people to use.
-- Provide a clear description.
+- Provide a terse, descriptive `Name` for `Scenarios`. Keep them short.
+- Provide a clear `Description`.
 - Comment `Scenario` code to describe to others why it's a useful `Scenario`.
-- Annotate `Scenarios` with `[ScenarioCategory]` attributes. Try to minimize the number of new categories created.
-- Use the `Bug Rero` Category for `Scnarios` that reproduce bugs. 
+- Annotate `Scenarios` with `[ScenarioCategory]` attributes. Minimize the number of new categories created.
+- Use the `Bug Repo` Category for `Scenarios` that reproduce bugs. 
 	- Include the Github Issue # in the Description.
-	- Once the bug has been fixed in `master` submit another PR to remove the `Scenario` (or modify it to provide a good regression test).
-- Tag bugs or suggestions for `UI Catalog` as [`Terminal.Gui` Github Issues](https://github.com/gui-cs/Terminal.Gui/issues) with "UICatalog: ".
+	- Once the bug has been fixed in `develop` submit another PR to remove the `Scenario` (or modify it to provide a good regression test/sample).
+- Tag bugs or suggestions for `UI Catalog` as [`Terminal.Gui` Github Issues](https://github.com/gui-cs/Terminal.Gui/issues) with "UICatalog: ".

+ 25 - 25
UICatalog/Scenario.cs

@@ -48,12 +48,7 @@ namespace UICatalog {
 		private bool _disposedValue;
 
 		/// <summary>
-		/// The Top level for the <see cref="Scenario"/>. This should be set to <see cref="Terminal.Gui.Application.Top"/> in most cases.
-		/// </summary>
-		public Toplevel Top { get; set; }
-
-		/// <summary>
-		/// The Window for the <see cref="Scenario"/>. This should be set within the <see cref="Terminal.Gui.Application.Top"/> in most cases.
+		/// The Window for the <see cref="Scenario"/>. This should be set to <see cref="Terminal.Gui.Application.Top"/> in most cases.
 		/// </summary>
 		public Window Win { get; set; }
 
@@ -63,25 +58,21 @@ namespace UICatalog {
 		/// the Scenario picker UI.
 		/// Override <see cref="Init"/> to provide any <see cref="Terminal.Gui.Toplevel"/> behavior needed.
 		/// </summary>
-		/// <param name="top">The Toplevel created by the UI Catalog host.</param>
 		/// <param name="colorScheme">The colorscheme to use.</param>
 		/// <remarks>
 		/// <para>
-		/// The base implementation calls <see cref="Application.Init"/>, sets <see cref="Top"/> to the passed in <see cref="Toplevel"/>, creates a <see cref="Window"/> for <see cref="Win"/> and adds it to <see cref="Top"/>.
+		/// The base implementation calls <see cref="Application.Init"/> and creates a <see cref="Window"/> for <see cref="Win"/> 
+		/// and adds it to <see cref="Application.Top"/>.
 		/// </para>
 		/// <para>
-		/// 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.
+		/// 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 (ColorScheme colorScheme)
 		{
 			Application.Init ();
 
-			Top = top;
-			if (Top == null) {
-				Top = Application.Top;
-			}
-
 			Win = new Window ($"CTRL-Q to Close - Scenario: {GetName ()}") {
 				X = 0,
 				Y = 0,
@@ -89,7 +80,7 @@ namespace UICatalog {
 				Height = Dim.Fill (),
 				ColorScheme = colorScheme,
 			};
-			Top.Add (Win);
+			Application.Top.Add (Win);
 		}
 
 		/// <summary>
@@ -177,7 +168,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...). 
@@ -197,7 +195,7 @@ namespace UICatalog {
 		public virtual void Run ()
 		{
 			// Must explicit call Application.Shutdown method to shutdown.
-			Application.Run (Top);
+			Application.Run (Application.Top);
 		}
 
 		/// <summary>
@@ -229,17 +227,19 @@ namespace UICatalog {
 		}
 
 		/// <summary>
-		/// Returns an instance of each <see cref="Scenario"/> defined in the project. 
+		/// Returns a list of all <see cref="Scenario"/> instanaces defined in the project, sorted by <see cref="ScenarioMetadata.Name"/>.
 		/// 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;
+			return objects.OrderBy (s => s.GetName ()).ToList ();
 		}
 
 		protected virtual void Dispose (bool disposing)

+ 10 - 27
UICatalog/Scenarios/AllViewsTester.cs

@@ -14,7 +14,7 @@ namespace UICatalog.Scenarios {
 	[ScenarioCategory ("Tests")]
 	[ScenarioCategory ("Top Level Windows")]
 	public class AllViewsTester : Scenario {
-		Window _leftPane;
+		FrameView _leftPane;
 		ListView _classListView;
 		FrameView _hostPane;
 
@@ -40,45 +40,33 @@ namespace UICatalog.Scenarios {
 		TextField _hText;
 		int _hVal = 0;
 
-		public override void Init (Toplevel top, ColorScheme colorScheme)
+		public override void Init (ColorScheme colorScheme)
 		{
 			Application.Init ();
-
-			Top = top;
-			if (Top == null) {
-				Top = Application.Top;
-			}
-
-			//Win = new Window ($"CTRL-Q to Close - Scenario: {GetName ()}") {
-			//	X = 0,
-			//	Y = 0,
-			//	Width = Dim.Fill (),
-			//	Height = Dim.Fill ()
-			//};
-			//Top.Add (Win);
+			// Don't create a sub-win; just use Applicatiion.Top
 		}
-
+		
 		public override void Setup ()
 		{
 			var statusBar = new StatusBar (new StatusItem [] {
 				new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()),
 				new StatusItem(Key.F2, "~F2~ Toggle Frame Ruler", () => {
 					ConsoleDriver.Diagnostics ^= ConsoleDriver.DiagnosticFlags.FrameRuler;
-					Top.SetNeedsDisplay ();
+					Application.Top.SetNeedsDisplay ();
 				}),
 				new StatusItem(Key.F3, "~F3~ Toggle Frame Padding", () => {
 					ConsoleDriver.Diagnostics ^= ConsoleDriver.DiagnosticFlags.FramePadding;
-					Top.SetNeedsDisplay ();
+					Application.Top.SetNeedsDisplay ();
 				}),
 			});
-			Top.Add (statusBar);
+			Application.Top.Add (statusBar);
 
 			_viewClasses = GetAllViewClassesCollection ()
 				.OrderBy (t => t.Name)
 				.Select (t => new KeyValuePair<string, Type> (t.Name, t))
 				.ToDictionary (t => t.Key, t => t.Value);
 
-			_leftPane = new Window ("Classes") {
+			_leftPane = new FrameView ("Classes") {
 				X = 0,
 				Y = 0,
 				Width = 15,
@@ -244,9 +232,9 @@ namespace UICatalog.Scenarios {
 				ColorScheme = Colors.Dialog,
 			};
 
-			Top.Add (_leftPane, _settingsPane, _hostPane);
+			Application.Top.Add (_leftPane, _settingsPane, _hostPane);
 
-			Top.LayoutSubviews ();
+			Application.Top.LayoutSubviews ();
 
 			_curView = CreateClass (_viewClasses.First ().Value);
 		}
@@ -441,11 +429,6 @@ namespace UICatalog.Scenarios {
 			UpdateTitle (_curView);
 		}
 
-		public override void Run ()
-		{
-			base.Run ();
-		}
-
 		private void Quit ()
 		{
 			Application.RequestStop ();

+ 1 - 8
UICatalog/Scenarios/BackgroundWorkerCollection.cs

@@ -12,17 +12,10 @@ namespace UICatalog.Scenarios {
 	[ScenarioCategory ("Dialogs")]
 	[ScenarioCategory ("Controls")]
 	public class BackgroundWorkerCollection : Scenario {
-		public override void Init (Toplevel top, ColorScheme colorScheme)
-		{
-			Application.Top.Dispose ();
-
-			Application.Run<MdiMain> ();
-
-			Application.Top.Dispose ();
-		}
 
 		public override void Run ()
 		{
+			Application.Run<MdiMain> ();
 		}
 
 		class MdiMain : Toplevel {

+ 4 - 7
UICatalog/Scenarios/BordersComparisons.cs

@@ -5,13 +5,10 @@ namespace UICatalog.Scenarios {
 	[ScenarioCategory ("Layout")]
 	[ScenarioCategory ("Borders")]
 	public class BordersComparisons : Scenario {
-		public override void Init (Toplevel top, ColorScheme colorScheme)
+		public override void Init (ColorScheme colorScheme)
 		{
-			top.Dispose ();
 			Application.Init ();
 
-			top = Application.Top;
-
 			var borderStyle = BorderStyle.Double;
 			var drawMarginFrame = false;
 			var borderThickness = new Thickness (1, 2, 3, 4);
@@ -54,7 +51,7 @@ namespace UICatalog.Scenarios {
 				Width = 10
 			};
 			win.Add (tf1, button, label, tv, tf2);
-			top.Add (win);
+			Application.Top.Add (win);
 
 			var top2 = new Border.ToplevelContainer (new Rect (50, 5, 40, 20),
 				new Border () {
@@ -93,7 +90,7 @@ namespace UICatalog.Scenarios {
 				Width = 10
 			};
 			top2.Add (tf3, button2, label2, tv2, tf4);
-			top.Add (top2);
+			Application.Top.Add (top2);
 
 			var frm = new FrameView (new Rect (95, 5, 40, 20), "Test3", null,
 				new Border () {
@@ -129,7 +126,7 @@ namespace UICatalog.Scenarios {
 				Width = 10
 			};
 			frm.Add (tf5, button3, label3, tv3, tf6);
-			top.Add (frm);
+			Application.Top.Add (frm);
 
 			Application.Run ();
 		}

+ 2 - 2
UICatalog/Scenarios/Buttons.cs

@@ -56,7 +56,7 @@ namespace UICatalog.Scenarios {
 
 			//View prev = colorButtonsLabel;
 
-			//With this method there is no need to call Top.Ready += () => Top.Redraw (Top.Bounds);
+			//With this method there is no need to call Application.TopReady += () => Application.TopRedraw (Top.Bounds);
 			var x = Pos.Right (colorButtonsLabel) + 2;
 			foreach (var colorScheme in Colors.ColorSchemes) {
 				var colorButton = new Button ($"{colorScheme.Key}") {
@@ -272,7 +272,7 @@ namespace UICatalog.Scenarios {
 				}
 			};
 
-			Top.Ready += () => radioGroup.Refresh ();
+			Application.Top.Ready += () => radioGroup.Refresh ();
 		}
 	}
 }

+ 400 - 102
UICatalog/Scenarios/CharacterMap.cs

@@ -1,21 +1,25 @@
 #define DRAW_CONTENT
 //#define BASE_DRAW_CONTENT
 
+using Microsoft.VisualBasic;
 using NStack;
+using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using Terminal.Gui;
+using Terminal.Gui.Resources;
 using Rune = System.Rune;
 
 namespace UICatalog.Scenarios {
 	/// <summary>
 	/// This Scenario demonstrates building a custom control (a class deriving from View) that:
-	///   - Provides a simple "Character Map" application (like Windows' charmap.exe).
+	///   - Provides a "Character Map" application (like Windows' charmap.exe).
 	///   - Helps test unicode character rendering in Terminal.Gui
 	///   - Illustrates how to use ScrollView to do infinite scrolling
 	/// </summary>
-	[ScenarioMetadata (Name: "Character Map", Description: "A Unicode character set viewier built as a custom control using the ScrollView control.")]
+	[ScenarioMetadata (Name: "Character Map",
+		Description: "A Unicode character set viewier built as a custom control using the ScrollView control.")]
 	[ScenarioCategory ("Text and Formatting")]
 	[ScenarioCategory ("Controls")]
 	[ScenarioCategory ("ScrollView")]
@@ -26,61 +30,70 @@ namespace UICatalog.Scenarios {
 			_charMap = new CharMap () {
 				X = 0,
 				Y = 0,
-				Width = CharMap.RowWidth + 2,
 				Height = Dim.Fill (),
-				Start = 0x2500,
-				ColorScheme = Colors.Dialog,
-				CanFocus = true,
 			};
 
-			Win.Add (_charMap);
-			var label = new Label ("Jump To Unicode Block:") { X = Pos.Right (_charMap) + 1, Y = Pos.Y (_charMap) };
-			Win.Add (label);
+			var jumpLabel = new Label ("Jump To Glyph:") { X = Pos.Right (_charMap) + 1, Y = Pos.Y (_charMap) };
+			Win.Add (jumpLabel);
+			var jumpEdit = new TextField () { X = Pos.Right (jumpLabel) + 1, Y = Pos.Y (_charMap), Width = 10, };
+			Win.Add (jumpEdit);
+			var unicodeLabel = new Label ("") { X = Pos.Right (jumpEdit) + 1, Y = Pos.Y (_charMap) };
+			Win.Add (unicodeLabel);
+			jumpEdit.TextChanged += (s) => {
+				uint result = 0;
+				if (jumpEdit.Text.Length == 0) return;
+				try {
+					result = Convert.ToUInt32 (jumpEdit.Text.ToString (), 10);
+				} catch (OverflowException) {
+					unicodeLabel.Text = $"Invalid (overflow)";
+					return;
+				} catch (FormatException) {
+					try {
+						result = Convert.ToUInt32 (jumpEdit.Text.ToString (), 16);
+					} catch (OverflowException) {
+						unicodeLabel.Text = $"Invalid (overflow)";
+						return;
+					} catch (FormatException) {
+						unicodeLabel.Text = $"Invalid (can't parse)";
+						return;
+					}
+				}
+				unicodeLabel.Text = $"U+{result:x4}";
+				_charMap.SelectedGlyph = result;
+			};
 
-			(ustring radioLabel, int start, int end) CreateRadio (ustring title, int start, int end)
+			var radioItems = new (ustring radioLabel, uint start, uint end) [UnicodeRange.Ranges.Count];
+
+			for (var i = 0; i < UnicodeRange.Ranges.Count; i++) {
+				var range = UnicodeRange.Ranges [i];
+				radioItems [i] = CreateRadio (range.Category, range.Start, range.End);
+			}
+			(ustring radioLabel, uint start, uint end) CreateRadio (ustring title, uint start, uint end)
 			{
 				return ($"{title} (U+{start:x5}-{end:x5})", start, end);
 			}
 
-			var radioItems = new (ustring radioLabel, int start, int end) [] {
-				CreateRadio("ASCII Control Characterss", 0x00, 0x1F),
-				CreateRadio("C0 Control Characters", 0x80, 0x9f),
-				CreateRadio("Hangul Jamo", 0x1100, 0x11ff),	// This is where wide chars tend to start
-				CreateRadio("Currency Symbols", 0x20A0, 0x20CF),
-				CreateRadio("Letterlike Symbols", 0x2100, 0x214F),
-				CreateRadio("Arrows", 0x2190, 0x21ff),
-				CreateRadio("Mathematical symbols", 0x2200, 0x22ff),
-				CreateRadio("Miscellaneous Technical", 0x2300, 0x23ff),
-				CreateRadio("Box Drawing & Geometric Shapes", 0x2500, 0x25ff),
-				CreateRadio("Miscellaneous Symbols", 0x2600, 0x26ff),
-				CreateRadio("Dingbats", 0x2700, 0x27ff),
-				CreateRadio("Braille", 0x2800, 0x28ff),
-				CreateRadio("Miscellaneous Symbols and Arrows", 0x2b00, 0x2bff),
-				CreateRadio("Alphabetic Presentation Forms", 0xFB00, 0xFb4f),
-				CreateRadio("Cuneiform Numbers and Punctuation", 0x12400, 0x1240f),
-				CreateRadio("Chess Symbols", 0x1FA00, 0x1FA0f),
-				CreateRadio("End", CharMap.MaxCodePointVal - 16, CharMap.MaxCodePointVal),
-			};
+			Win.Add (_charMap);
+			var label = new Label ("Jump To Unicode Block:") { X = Pos.Right (_charMap) + 1, Y = Pos.Bottom (jumpLabel) + 1 };
+			Win.Add (label);
 
-			var jumpList = new RadioGroup (radioItems.Select (t => t.radioLabel).ToArray ()) {
-				X = Pos.X (label),
+			var jumpList = new ListView (radioItems.Select (t => t.radioLabel).ToArray ()) {
+				X = Pos.X (label) + 1,
 				Y = Pos.Bottom (label),
-				Width = Dim.Fill (),
-				SelectedItem = 8
+				Width = radioItems.Max (r => r.radioLabel.Length) + 2,
+				Height = Dim.Fill(1),
+				SelectedItem = 0
 			};
 			jumpList.SelectedItemChanged += (args) => {
-				_charMap.Start = radioItems [args.SelectedItem].start;
+				_charMap.StartGlyph = radioItems [jumpList.SelectedItem].start;
 			};
 
 			Win.Add (jumpList);
 
-			jumpList.Refresh ();
-			jumpList.SetFocus ();
-		}
+			//jumpList.Refresh ();
+			_charMap.SetFocus ();
 
-		public override void Run ()
-		{
-			base.Run ();
+			_charMap.Width = Dim.Fill () - jumpList.Width;
 		}
 	}
 
@@ -90,105 +103,230 @@ namespace UICatalog.Scenarios {
 		/// Specifies the starting offset for the character map. The default is 0x2500 
 		/// which is the Box Drawing characters.
 		/// </summary>
-		public int Start {
+		public uint StartGlyph {
 			get => _start;
 			set {
 				_start = value;
-				ContentOffset = new Point (0, _start / 16);
+				_selected = value;
+				ContentOffset = new Point (0, (int)(_start / 16));
+				SetNeedsDisplay ();
+			}
+		}
+
+		/// <summary>
+		/// Specifies the starting offset for the character map. The default is 0x2500 
+		/// which is the Box Drawing characters.
+		/// </summary>
+		public uint SelectedGlyph {
+			get => _selected;
+			set {
+				_selected = value;
+				int row = (int)_selected / 16;
+				int height = (Bounds.Height / ROW_HEIGHT) - 1;
+				if (row + ContentOffset.Y < 0) {
+					// Moving up.
+					ContentOffset = new Point (0, row);
+				} else if (row + ContentOffset.Y >= height) {
+					// Moving down.
+					ContentOffset = new Point (0, Math.Min (row, (row - height) + 1));
+
+				} else {
+					//ContentOffset = new Point (0, Math.Min (row, (row - height) - 1));
+				}
+
 				SetNeedsDisplay ();
 			}
 		}
-		int _start = 0x2500;
 
-		public const int H_SPACE = 2;
-		public const int V_SPACE = 2;
+		uint _start = 0;
+		uint _selected = 0;
 
-		public static int MaxCodePointVal => 0x10FFFF;
+		public const int COLUMN_WIDTH = 3;
+		public const int ROW_HEIGHT = 1;
 
-		// Row Header + space + (space + char + space)
-		public static int RowHeaderWidth => $"U+{MaxCodePointVal:x5}".Length;
-		public static int RowWidth => RowHeaderWidth + (H_SPACE * 16);
+		public static uint MaxCodePointVal => 0x10FFFF;
+
+		public static int RowLabelWidth => $"U+{MaxCodePointVal:x5}".Length + 1; 
+		public static int RowWidth => RowLabelWidth + (COLUMN_WIDTH * 16);
 
 		public CharMap ()
 		{
-			ContentSize = new Size (CharMap.RowWidth, MaxCodePointVal / 16);
+			ColorScheme = Colors.Dialog;
+			CanFocus = true;
+
+			ContentSize = new Size (CharMap.RowWidth, (int)(MaxCodePointVal / 16 + 1));
 			ShowVerticalScrollIndicator = true;
 			ShowHorizontalScrollIndicator = false;
 			LayoutComplete += (args) => {
-				if (Bounds.Width <= RowWidth) {
+				if (Bounds.Width < RowWidth) {
 					ShowHorizontalScrollIndicator = true;
 				} else {
 					ShowHorizontalScrollIndicator = false;
+					// Snap 1st column into view if it's been scrolled horizontally 
+					ContentOffset = new Point (0, ContentOffset.Y);
+					SetNeedsDisplay ();
 				}
 			};
-#if DRAW_CONTENT
-
 			DrawContent += CharMap_DrawContent;
-#endif
+
+			AddCommand (Command.ScrollUp, () => {
+				if (SelectedGlyph >= 16) {
+					SelectedGlyph = SelectedGlyph - 16;
+				}
+				return true;
+			});
+			AddCommand (Command.ScrollDown, () => {
+				if (SelectedGlyph < MaxCodePointVal - 16) {
+					SelectedGlyph = SelectedGlyph + 16;
+				}
+				return true;
+			});
+			AddCommand (Command.ScrollLeft, () => {
+				if (SelectedGlyph > 0) {
+					SelectedGlyph--;
+				}
+				return true;
+			});
+			AddCommand (Command.ScrollRight, () => {
+				if (SelectedGlyph < MaxCodePointVal - 1) {
+					SelectedGlyph++;
+				}
+				return true;
+			});
+			AddCommand (Command.PageUp, () => {
+				var page = (uint)(Bounds.Height / ROW_HEIGHT - 1) * 16;
+				SelectedGlyph -= Math.Min(page, SelectedGlyph);
+				return true;
+			});
+			AddCommand (Command.PageDown, () => {
+				var page = (uint)(Bounds.Height / ROW_HEIGHT - 1) * 16;
+				SelectedGlyph += Math.Min(page, MaxCodePointVal -SelectedGlyph);
+				return true;
+			});
+			AddCommand (Command.TopHome, () => {
+				SelectedGlyph = 0;
+				return true;
+			});
+			AddCommand (Command.BottomEnd, () => {
+				SelectedGlyph = MaxCodePointVal;
+				return true;
+			});
+
+			MouseClick += Handle_MouseClick;
+			Application.Driver.SetCursorVisibility (CursorVisibility.Invisible);
+		}
+
+		private void CopyValue ()
+		{
+			Clipboard.Contents = $"U+{SelectedGlyph:x5}";
+		}
+
+		private void CopyGlyph ()
+		{
+			Clipboard.Contents = $"{new Rune (SelectedGlyph)}";
 		}
 
 		private void CharMap_DrawContent (Rect viewport)
 		{
-			//Rune ReplaceNonPrintables (Rune c)
-			//{
-			//	if (c < 0x20) {
-			//		return new Rune (c + 0x2400);         // U+25A1 □ WHITE SQUARE
-			//	} else {
-			//		return c;
-			//	}
-			//}
-
-			ContentSize = new Size (CharMap.RowWidth, MaxCodePointVal / 16 + Frame.Height - 1);
-
-			for (int header = 0; header < 16; header++) {
-				Move (viewport.X + RowHeaderWidth + (header * H_SPACE), 0);
-				Driver.AddStr ($" {header:x} ");
+			var oldClip = Driver.Clip;
+			Driver.Clip = Frame;
+			// Redraw doesn't know about the scroll indicators, so if off, add one to height
+			if (!ShowHorizontalScrollIndicator) {
+				Driver.Clip = new Rect (Frame.X, Frame.Y, Frame.Width, Frame.Height + 1);
 			}
-			for (int row = 0, y = 0; row < viewport.Height / 2 - 1; row++, y += V_SPACE) {
-				int val = (-viewport.Y + row) * 16;
-				if (val < MaxCodePointVal) {
-					var rowLabel = $"U+{val / 16:x4}x";
-					Move (0, y + 1);
-					Driver.AddStr (rowLabel);
-					var prevColWasWide = false;
+			Driver.SetAttribute (HasFocus ? ColorScheme.HotFocus : ColorScheme.Focus);
+			Move (0, 0);
+			Driver.AddStr (new string (' ', RowLabelWidth + 1));
+			for (int hexDigit = 0; hexDigit < 16; hexDigit++) {
+				var x = ContentOffset.X + RowLabelWidth + (hexDigit * COLUMN_WIDTH);
+				if (x > RowLabelWidth - 2) {
+					Move (x, 0);
+					Driver.AddStr ($" {hexDigit:x} ");
+				}
+			}
+
+			var firstColumnX = viewport.X + RowLabelWidth;
+			for (int row = -ContentOffset.Y, y = 0; row <= (-ContentOffset.Y) + (Bounds.Height / ROW_HEIGHT); row++, y += ROW_HEIGHT) {
+				int val = (row) * 16;
+				Driver.SetAttribute (GetNormalColor ());
+				Move (firstColumnX, y + 1);
+				Driver.AddStr (new string (' ', 16 * COLUMN_WIDTH));
+				if (val <= MaxCodePointVal) {
+					Driver.SetAttribute (GetNormalColor ());
 					for (int col = 0; col < 16; col++) {
-						var rune = new Rune ((uint)((uint)(-viewport.Y + row) * 16 + col));
-						Move (viewport.X + RowHeaderWidth + (col * H_SPACE) + (prevColWasWide ? 0 : 1), y + 1);
-						if (rune >= 0x00D800 && rune <= 0x00DFFF) {
-							if (col == 0) {
-								Driver.AddStr ("Reserved to surrogate pairs.");
-							}
-							continue;
+						uint glyph = (uint)((uint)val + col);
+						var rune = new Rune (glyph);
+						//if (rune >= 0x00D800 && rune <= 0x00DFFF) {
+						//	if (col == 0) {
+						//		Driver.AddStr ("Reserved for surrogate pairs.");
+						//	}
+						//	continue;
+						//}						
+						Move (firstColumnX + (col * COLUMN_WIDTH) + 1, y + 1);
+						if (glyph == SelectedGlyph) {
+							Driver.SetAttribute (HasFocus ? ColorScheme.HotFocus : ColorScheme.HotNormal);
+						} else {
+							Driver.SetAttribute (GetNormalColor ());
 						}
 						Driver.AddRune (rune);
-						//prevColWasWide = Rune.ColumnWidth (rune) > 1;
 					}
+					Move (0, y + 1);
+					Driver.SetAttribute (HasFocus ? ColorScheme.HotFocus : ColorScheme.Focus);
+					var rowLabel = $"U+{val / 16:x5}_ ";
+					Driver.AddStr (rowLabel);
 				}
 			}
+			Driver.Clip = oldClip;
 		}
-#if BASE_DRAW_CONTENT
-		public override void OnDrawContent (Rect viewport)
-		{
-			CharMap_DrawContent (viewport);
-			base.OnDrawContent (viewport);
-		}
-#endif
 
-		public override bool ProcessKey (KeyEvent kb)
+		ContextMenu _contextMenu = new ContextMenu ();
+		void Handle_MouseClick (MouseEventArgs args)
 		{
-			if (kb.Key == Key.PageDown) {
-				ContentOffset = new Point (0, ContentOffset.Y - Bounds.Height / 2 + 1);
-				return true;
+			var me = args.MouseEvent;
+			if (me.Flags == MouseFlags.ReportMousePosition || (me.Flags != MouseFlags.Button1Clicked &&
+				me.Flags != MouseFlags.Button1DoubleClicked &&
+				me.Flags != _contextMenu.MouseFlags)) {
+				return;
 			}
-			if (kb.Key == Key.PageUp) {
-				if (ContentOffset.Y + Bounds.Height / 2 - 1 < 0) {
-					ContentOffset = new Point (0, ContentOffset.Y + Bounds.Height / 2 - 1);
-				} else {
-					ContentOffset = Point.Empty;
-				}
-				return true;
+
+			if (me.X < RowLabelWidth) {
+				return;
+			}
+
+			if (me.Y < 1) {
+				return;
+			}
+
+			var row = (me.Y - 1);
+			var col = (me.X - RowLabelWidth - ContentOffset.X) / COLUMN_WIDTH;
+			uint val = (uint)((((uint)row - (uint)ContentOffset.Y) * 16) + col);
+			if (val > MaxCodePointVal) {
+				return;
+			}
+
+			if (me.Flags == MouseFlags.Button1Clicked) {
+				SelectedGlyph = (uint)val;
+				return;
+			}
+
+			if (me.Flags == MouseFlags.Button1DoubleClicked) {
+				SelectedGlyph = (uint)val;
+				MessageBox.Query ("Glyph", $"{new Rune (val)} U+{SelectedGlyph:x4}", "Ok");
+				return;
+			}
+
+			if (me.Flags == _contextMenu.MouseFlags) {
+				SelectedGlyph = (uint)val;
+				_contextMenu = new ContextMenu (me.X + 1, me.Y + 1,
+					new MenuBarItem (new MenuItem [] {
+					new MenuItem ("_Copy Glyph", "", () => CopyGlyph (), null, null, Key.C | Key.CtrlMask),
+					new MenuItem ("Copy _Value", "", () => CopyValue (), null, null, Key.C | Key.ShiftMask | Key.CtrlMask),
+					}) {
+
+					}
+				);
+				_contextMenu.Show ();
 			}
-			return base.ProcessKey (kb);
 		}
 
 		protected override void Dispose (bool disposing)
@@ -197,4 +335,164 @@ namespace UICatalog.Scenarios {
 			base.Dispose (disposing);
 		}
 	}
+
+	class UnicodeRange {
+		public uint Start;
+		public uint End;
+		public string Category;
+		public UnicodeRange (uint start, uint end, string category)
+		{
+			this.Start = start;
+			this.End = end;
+			this.Category = category;
+		}
+			
+		public static List<UnicodeRange> Ranges = new List<UnicodeRange> {
+			new UnicodeRange (0x0000, 0x001F, "ASCII Control Characters"),
+			new UnicodeRange (0x0080, 0x009F, "C0 Control Characters"),
+			new UnicodeRange(0x1100, 0x11ff,"Hangul Jamo"),	// This is where wide chars tend to start
+			new UnicodeRange(0x20A0, 0x20CF,"Currency Symbols"),
+			new UnicodeRange(0x2100, 0x214F,"Letterlike Symbols"),
+			new UnicodeRange(0x2160, 0x218F, "Roman Numerals"),
+			new UnicodeRange(0x2190, 0x21ff,"Arrows" ),
+			new UnicodeRange(0x2200, 0x22ff,"Mathematical symbols"),
+			new UnicodeRange(0x2300, 0x23ff,"Miscellaneous Technical"),
+			new UnicodeRange(0x24B6, 0x24e9,"Circled Latin Capital Letters"), 
+			new UnicodeRange(0x1F130, 0x1F149,"Squared Latin Capital Letters"), 
+			new UnicodeRange(0x2500, 0x25ff,"Box Drawing & Geometric Shapes"),
+			new UnicodeRange(0x2600, 0x26ff,"Miscellaneous Symbols"),
+			new UnicodeRange(0x2700, 0x27ff,"Dingbats"),
+			new UnicodeRange(0x2800, 0x28ff,"Braille"),
+			new UnicodeRange(0x2b00, 0x2bff,"Miscellaneous Symbols and Arrows"),
+			new UnicodeRange(0xFB00, 0xFb4f,"Alphabetic Presentation Forms"),
+			new UnicodeRange(0x12400, 0x1240f,"Cuneiform Numbers and Punctuation"),
+			new UnicodeRange(0x1FA00, 0x1FA0f,"Chess Symbols"),
+
+			new UnicodeRange (0x0020 ,0x007F        ,"Basic Latin"),
+			new UnicodeRange (0x00A0 ,0x00FF        ,"Latin-1 Supplement"),
+			new UnicodeRange (0x0100 ,0x017F        ,"Latin Extended-A"),
+			new UnicodeRange (0x0180 ,0x024F        ,"Latin Extended-B"),
+			new UnicodeRange (0x0250 ,0x02AF        ,"IPA Extensions"),
+			new UnicodeRange (0x02B0 ,0x02FF        ,"Spacing Modifier Letters"),
+			new UnicodeRange (0x0300 ,0x036F        ,"Combining Diacritical Marks"),
+			new UnicodeRange (0x0370 ,0x03FF        ,"Greek and Coptic"),
+			new UnicodeRange (0x0400 ,0x04FF        ,"Cyrillic"),
+			new UnicodeRange (0x0500 ,0x052F        ,"Cyrillic Supplementary"),
+			new UnicodeRange (0x0530 ,0x058F        ,"Armenian"),
+			new UnicodeRange (0x0590 ,0x05FF        ,"Hebrew"),
+			new UnicodeRange (0x0600 ,0x06FF        ,"Arabic"),
+			new UnicodeRange (0x0700 ,0x074F        ,"Syriac"),
+			new UnicodeRange (0x0780 ,0x07BF        ,"Thaana"),
+			new UnicodeRange (0x0900 ,0x097F        ,"Devanagari"),
+			new UnicodeRange (0x0980 ,0x09FF        ,"Bengali"),
+			new UnicodeRange (0x0A00 ,0x0A7F        ,"Gurmukhi"),
+			new UnicodeRange (0x0A80 ,0x0AFF        ,"Gujarati"),
+			new UnicodeRange (0x0B00 ,0x0B7F        ,"Oriya"),
+			new UnicodeRange (0x0B80 ,0x0BFF        ,"Tamil"),
+			new UnicodeRange (0x0C00 ,0x0C7F        ,"Telugu"),
+			new UnicodeRange (0x0C80 ,0x0CFF        ,"Kannada"),
+			new UnicodeRange (0x0D00 ,0x0D7F        ,"Malayalam"),
+			new UnicodeRange (0x0D80 ,0x0DFF        ,"Sinhala"),
+			new UnicodeRange (0x0E00 ,0x0E7F        ,"Thai"),
+			new UnicodeRange (0x0E80 ,0x0EFF        ,"Lao"),
+			new UnicodeRange (0x0F00 ,0x0FFF        ,"Tibetan"),
+			new UnicodeRange (0x1000 ,0x109F        ,"Myanmar"),
+			new UnicodeRange (0x10A0 ,0x10FF        ,"Georgian"),
+			new UnicodeRange (0x1100 ,0x11FF        ,"Hangul Jamo"),
+			new UnicodeRange (0x1200 ,0x137F        ,"Ethiopic"),
+			new UnicodeRange (0x13A0 ,0x13FF        ,"Cherokee"),
+			new UnicodeRange (0x1400 ,0x167F        ,"Unified Canadian Aboriginal Syllabics"),
+			new UnicodeRange (0x1680 ,0x169F        ,"Ogham"),
+			new UnicodeRange (0x16A0 ,0x16FF        ,"Runic"),
+			new UnicodeRange (0x1700 ,0x171F        ,"Tagalog"),
+			new UnicodeRange (0x1720 ,0x173F        ,"Hanunoo"),
+			new UnicodeRange (0x1740 ,0x175F        ,"Buhid"),
+			new UnicodeRange (0x1760 ,0x177F        ,"Tagbanwa"),
+			new UnicodeRange (0x1780 ,0x17FF        ,"Khmer"),
+			new UnicodeRange (0x1800 ,0x18AF        ,"Mongolian"),
+			new UnicodeRange (0x1900 ,0x194F        ,"Limbu"),
+			new UnicodeRange (0x1950 ,0x197F        ,"Tai Le"),
+			new UnicodeRange (0x19E0 ,0x19FF        ,"Khmer Symbols"),
+			new UnicodeRange (0x1D00 ,0x1D7F        ,"Phonetic Extensions"),
+			new UnicodeRange (0x1E00 ,0x1EFF        ,"Latin Extended Additional"),
+			new UnicodeRange (0x1F00 ,0x1FFF        ,"Greek Extended"),
+			new UnicodeRange (0x2000 ,0x206F        ,"General Punctuation"),
+			new UnicodeRange (0x2070 ,0x209F        ,"Superscripts and Subscripts"),
+			new UnicodeRange (0x20A0 ,0x20CF        ,"Currency Symbols"),
+			new UnicodeRange (0x20D0 ,0x20FF        ,"Combining Diacritical Marks for Symbols"),
+			new UnicodeRange (0x2100 ,0x214F        ,"Letterlike Symbols"),
+			new UnicodeRange (0x2150 ,0x218F        ,"Number Forms"),
+			new UnicodeRange (0x2190 ,0x21FF        ,"Arrows"),
+			new UnicodeRange (0x2200 ,0x22FF        ,"Mathematical Operators"),
+			new UnicodeRange (0x2300 ,0x23FF        ,"Miscellaneous Technical"),
+			new UnicodeRange (0x2400 ,0x243F        ,"Control Pictures"),
+			new UnicodeRange (0x2440 ,0x245F        ,"Optical Character Recognition"),
+			new UnicodeRange (0x2460 ,0x24FF        ,"Enclosed Alphanumerics"),
+			new UnicodeRange (0x2500 ,0x257F        ,"Box Drawing"),
+			new UnicodeRange (0x2580 ,0x259F        ,"Block Elements"),
+			new UnicodeRange (0x25A0 ,0x25FF        ,"Geometric Shapes"),
+			new UnicodeRange (0x2600 ,0x26FF        ,"Miscellaneous Symbols"),
+			new UnicodeRange (0x2700 ,0x27BF        ,"Dingbats"),
+			new UnicodeRange (0x27C0 ,0x27EF        ,"Miscellaneous Mathematical Symbols-A"),
+			new UnicodeRange (0x27F0 ,0x27FF        ,"Supplemental Arrows-A"),
+			new UnicodeRange (0x2800 ,0x28FF        ,"Braille Patterns"),
+			new UnicodeRange (0x2900 ,0x297F        ,"Supplemental Arrows-B"),
+			new UnicodeRange (0x2980 ,0x29FF        ,"Miscellaneous Mathematical Symbols-B"),
+			new UnicodeRange (0x2A00 ,0x2AFF        ,"Supplemental Mathematical Operators"),
+			new UnicodeRange (0x2B00 ,0x2BFF        ,"Miscellaneous Symbols and Arrows"),
+			new UnicodeRange (0x2E80 ,0x2EFF        ,"CJK Radicals Supplement"),
+			new UnicodeRange (0x2F00 ,0x2FDF        ,"Kangxi Radicals"),
+			new UnicodeRange (0x2FF0 ,0x2FFF        ,"Ideographic Description Characters"),
+			new UnicodeRange (0x3000 ,0x303F        ,"CJK Symbols and Punctuation"),
+			new UnicodeRange (0x3040 ,0x309F        ,"Hiragana"),
+			new UnicodeRange (0x30A0 ,0x30FF        ,"Katakana"),
+			new UnicodeRange (0x3100 ,0x312F        ,"Bopomofo"),
+			new UnicodeRange (0x3130 ,0x318F        ,"Hangul Compatibility Jamo"),
+			new UnicodeRange (0x3190 ,0x319F        ,"Kanbun"),
+			new UnicodeRange (0x31A0 ,0x31BF        ,"Bopomofo Extended"),
+			new UnicodeRange (0x31F0 ,0x31FF        ,"Katakana Phonetic Extensions"),
+			new UnicodeRange (0x3200 ,0x32FF        ,"Enclosed CJK Letters and Months"),
+			new UnicodeRange (0x3300 ,0x33FF        ,"CJK Compatibility"),
+			new UnicodeRange (0x3400 ,0x4DBF        ,"CJK Unified Ideographs Extension A"),
+			new UnicodeRange (0x4DC0 ,0x4DFF        ,"Yijing Hexagram Symbols"),
+			new UnicodeRange (0x4E00 ,0x9FFF        ,"CJK Unified Ideographs"),
+			new UnicodeRange (0xA000 ,0xA48F        ,"Yi Syllables"),
+			new UnicodeRange (0xA490 ,0xA4CF        ,"Yi Radicals"),
+			new UnicodeRange (0xAC00 ,0xD7AF        ,"Hangul Syllables"),
+			new UnicodeRange (0xD800 ,0xDB7F        ,"High Surrogates"),
+			new UnicodeRange (0xDB80 ,0xDBFF        ,"High Private Use Surrogates"),
+			new UnicodeRange (0xDC00 ,0xDFFF        ,"Low Surrogates"),
+			new UnicodeRange (0xE000 ,0xF8FF        ,"Private Use Area"),
+			new UnicodeRange (0xF900 ,0xFAFF        ,"CJK Compatibility Ideographs"),
+			new UnicodeRange (0xFB00 ,0xFB4F        ,"Alphabetic Presentation Forms"),
+			new UnicodeRange (0xFB50 ,0xFDFF        ,"Arabic Presentation Forms-A"),
+			new UnicodeRange (0xFE00 ,0xFE0F        ,"Variation Selectors"),
+			new UnicodeRange (0xFE20 ,0xFE2F        ,"Combining Half Marks"),
+			new UnicodeRange (0xFE30 ,0xFE4F        ,"CJK Compatibility Forms"),
+			new UnicodeRange (0xFE50 ,0xFE6F        ,"Small Form Variants"),
+			new UnicodeRange (0xFE70 ,0xFEFF        ,"Arabic Presentation Forms-B"),
+			new UnicodeRange (0xFF00 ,0xFFEF        ,"Halfwidth and Fullwidth Forms"),
+			new UnicodeRange (0xFFF0 ,0xFFFF        ,"Specials"),
+			new UnicodeRange (0x10000, 0x1007F   ,"Linear B Syllabary"),
+			new UnicodeRange (0x10080, 0x100FF   ,"Linear B Ideograms"),
+			new UnicodeRange (0x10100, 0x1013F   ,"Aegean Numbers"),
+			new UnicodeRange (0x10300, 0x1032F   ,"Old Italic"),
+			new UnicodeRange (0x10330, 0x1034F   ,"Gothic"),
+			new UnicodeRange (0x10380, 0x1039F   ,"Ugaritic"),
+			new UnicodeRange (0x10400, 0x1044F   ,"Deseret"),
+			new UnicodeRange (0x10450, 0x1047F   ,"Shavian"),
+			new UnicodeRange (0x10480, 0x104AF   ,"Osmanya"),
+			new UnicodeRange (0x10800, 0x1083F   ,"Cypriot Syllabary"),
+			new UnicodeRange (0x1D000, 0x1D0FF   ,"Byzantine Musical Symbols"),
+			new UnicodeRange (0x1D100, 0x1D1FF   ,"Musical Symbols"),
+			new UnicodeRange (0x1D300, 0x1D35F   ,"Tai Xuan Jing Symbols"),
+			new UnicodeRange (0x1D400, 0x1D7FF   ,"Mathematical Alphanumeric Symbols"),
+			new UnicodeRange (0x1F600, 0x1F532   ,"Emojis Symbols"),
+			new UnicodeRange (0x20000, 0x2A6DF   ,"CJK Unified Ideographs Extension B"),
+			new UnicodeRange (0x2F800, 0x2FA1F   ,"CJK Compatibility Ideographs Supplement"),
+			new UnicodeRange (0xE0000, 0xE007F   ,"Tags"),
+			new UnicodeRange((uint)(CharMap.MaxCodePointVal - 16), (uint)CharMap.MaxCodePointVal,"End"),
+		};
+	}
+	
 }

+ 20 - 7
UICatalog/Scenarios/ClassExplorer.cs

@@ -53,27 +53,34 @@ namespace UICatalog.Scenarios {
 			}
 		}
 
+		MenuItem highlightModelTextOnly;
+
 		public override void Setup ()
 		{
 			Win.Title = this.GetName ();
 			Win.Y = 1; // menu
 			Win.Height = Dim.Fill (1); // status bar
-			Top.LayoutSubviews ();
+			Application.Top.LayoutSubviews ();
 
 			var menu = new MenuBar (new MenuBarItem [] {
 				new MenuBarItem ("_File", new MenuItem [] {
 					new MenuItem ("_Quit", "", () => Quit()),
-				})
-				,
+				}),
 				new MenuBarItem ("_View", new MenuItem [] {
 					miShowPrivate = new MenuItem ("_Include Private", "", () => ShowPrivate()){
 						Checked = false,
 						CheckType = MenuItemCheckStyle.Checked
 					},
-				new MenuItem ("_Expand All", "", () => treeView.ExpandAll()),
-				new MenuItem ("_Collapse All", "", () => treeView.CollapseAll()) }),
+					new MenuItem ("_Expand All", "", () => treeView.ExpandAll()),
+					new MenuItem ("_Collapse All", "", () => treeView.CollapseAll())
+				}),
+				new MenuBarItem ("_Style", new MenuItem [] {
+					highlightModelTextOnly = new MenuItem ("_Highlight Model Text Only", "", () => OnCheckHighlightModelTextOnly()) {
+						CheckType = MenuItemCheckStyle.Checked
+					},
+				}) 
 			});
-			Top.Add (menu);
+			Application.Top.Add (menu);
 
 			treeView = new TreeView<object> () {
 				X = 0,
@@ -82,7 +89,6 @@ namespace UICatalog.Scenarios {
 				Height = Dim.Fill (),
 			};
 
-
 			treeView.AddObjects (AppDomain.CurrentDomain.GetAssemblies ());
 			treeView.AspectGetter = GetRepresentation;
 			treeView.TreeBuilder = new DelegateTreeBuilder<object> (ChildGetter, CanExpand);
@@ -100,6 +106,13 @@ namespace UICatalog.Scenarios {
 			Win.Add (textView);
 		}
 
+		private void OnCheckHighlightModelTextOnly ()
+		{
+			treeView.Style.HighlightModelTextOnly = !treeView.Style.HighlightModelTextOnly;
+			highlightModelTextOnly.Checked = treeView.Style.HighlightModelTextOnly;
+			treeView.SetNeedsDisplay ();
+		}
+
 		private void ShowPrivate ()
 		{
 			miShowPrivate.Checked = !miShowPrivate.Checked;

+ 4 - 17
UICatalog/Scenarios/Clipping.cs

@@ -7,23 +7,10 @@ namespace UICatalog.Scenarios {
 
 	public class Clipping : Scenario {
 
-		public override void Init (Toplevel top, ColorScheme colorScheme)
+		public override void Init (ColorScheme colorScheme)
 		{
 			Application.Init ();
-
-			Top = top;
-			if (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);
+			Application.Top.ColorScheme = Colors.Base;
 		}
 
 		public override void Setup ()
@@ -36,7 +23,7 @@ namespace UICatalog.Scenarios {
 				X = 0, Y = 0,
 				//ColorScheme = Colors.Dialog
 			};
-			Top.Add (label);
+			Application.Top.Add (label);
 
 			var scrollView = new ScrollView (new Rect (3, 3, 50, 20));
 			scrollView.ColorScheme = Colors.Menu;
@@ -79,7 +66,7 @@ namespace UICatalog.Scenarios {
 
 			scrollView.Add (embedded1);
 
-			Top.Add (scrollView);
+			Application.Top.Add (scrollView);
 		}
 	}
 }

+ 192 - 0
UICatalog/Scenarios/CollectionNavigatorTester.cs

@@ -0,0 +1,192 @@
+using System;
+using System.IO;
+using System.Linq;
+using Terminal.Gui;
+using Terminal.Gui.Trees;
+
+namespace UICatalog.Scenarios {
+
+	[ScenarioMetadata (Name: "Collection Navigator", Description: "Demonstrates keyboard navigation in ListView & TreeView (CollectionNavigator).")]
+	[ScenarioCategory ("Controls"),
+		ScenarioCategory ("ListView"),
+		ScenarioCategory ("TreeView"),
+		ScenarioCategory ("Text and Formatting"),
+		ScenarioCategory ("Mouse and Keyboard")]
+	public class CollectionNavigatorTester : Scenario {
+
+		// Don't create a Window, just return the top-level view
+		public override void Init (ColorScheme colorScheme)
+		{
+			Application.Init ();
+			Application.Top.ColorScheme = Colors.Base;
+		}
+
+		System.Collections.Generic.List<string> _items = new string [] {
+				"a",
+				"b",
+				"bb",
+				"c",
+				"ccc",
+				"ccc",
+				"cccc",
+				"ddd",
+				"dddd",
+				"dddd",
+				"ddddd",
+				"dddddd",
+				"ddddddd",
+				"this",
+				"this is a test",
+				"this was a test",
+				"this and",
+				"that and that",
+				"the",
+				"think",
+				"thunk",
+				"thunks",
+				"zip",
+				"zap",
+				"zoo",
+				"@jack",
+				"@sign",
+				"@at",
+				"@ateme",
+				"n@",
+				"n@brown",
+				".net",
+				"$100.00",
+				"$101.00",
+				"$101.10",
+				"$101.11",
+				"$200.00",
+				"$210.99",
+				"$$",
+				"appricot",
+				"arm",
+				"丗丙业丞",
+				"丗丙丛",
+				"text",
+				"egg",
+				"candle",
+				" <- space",
+				"\t<- tab",
+				"\n<- newline",
+				"\r<- formfeed",
+				"q",
+				"quit",
+				"quitter"
+			}.ToList<string> ();
+
+		public override void Setup ()
+		{
+			var allowMarking = new MenuItem ("Allow _Marking", "", null) {
+				CheckType = MenuItemCheckStyle.Checked,
+				Checked = false
+			};
+			allowMarking.Action = () => allowMarking.Checked = _listView.AllowsMarking = !_listView.AllowsMarking;
+
+			var allowMultiSelection = new MenuItem ("Allow Multi _Selection", "", null) {
+				CheckType = MenuItemCheckStyle.Checked,
+				Checked = false
+			};
+			allowMultiSelection.Action = () => allowMultiSelection.Checked = _listView.AllowsMultipleSelection = !_listView.AllowsMultipleSelection;
+			allowMultiSelection.CanExecute = () => allowMarking.Checked;
+
+			var menu = new MenuBar (new MenuBarItem [] {
+				new MenuBarItem ("_Configure", new MenuItem [] {
+					allowMarking,
+					allowMultiSelection,
+					null,
+					new MenuItem ("_Quit", "", () => Quit(), null, null, Key.Q | Key.CtrlMask),
+				}),
+				new MenuBarItem("_Quit", "CTRL-Q", () => Quit()),
+			});
+
+			Application.Top.Add (menu);
+
+			_items.Sort (StringComparer.OrdinalIgnoreCase);
+
+			CreateListView ();
+			var vsep = new LineView (Terminal.Gui.Graphs.Orientation.Vertical) {
+				X = Pos.Right (_listView),
+				Y = 1,
+				Height = Dim.Fill ()
+			};
+			Application.Top.Add (vsep);
+			CreateTreeView ();
+		}
+
+		ListView _listView = null;
+
+		private void CreateListView ()
+		{
+			var label = new Label () {
+				Text = "ListView",
+				TextAlignment = TextAlignment.Centered,
+				X = 0,
+				Y = 1, // for menu
+				Width = Dim.Percent (50),
+				Height = 1,
+			};
+			Application.Top.Add (label);
+
+			_listView = new ListView () {
+				X = 0,
+				Y = Pos.Bottom (label),
+				Width = Dim.Percent (50) - 1,
+				Height = Dim.Fill (),
+				AllowsMarking = false,
+				AllowsMultipleSelection = false,
+			};
+			Application.Top.Add (_listView);
+
+			_listView.SetSource (_items);
+
+			_listView.KeystrokeNavigator.SearchStringChanged += (state) => {
+				label.Text = $"ListView: {state.SearchString}";
+			};
+		}
+
+		TreeView _treeView = null;
+
+		private void CreateTreeView ()
+		{
+			var label = new Label () {
+				Text = "TreeView",
+				TextAlignment = TextAlignment.Centered,
+				X = Pos.Right (_listView) + 2,
+				Y = 1, // for menu
+				Width = Dim.Percent	 (50),
+				Height = 1,
+			};
+			Application.Top.Add (label);
+
+			_treeView = new TreeView () {
+				X = Pos.Right (_listView) + 1,
+				Y = Pos.Bottom (label),
+				Width = Dim.Fill (),
+				Height = Dim.Fill ()
+			};
+			_treeView.Style.HighlightModelTextOnly = true;
+			Application.Top.Add (_treeView);
+
+			var root = new TreeNode ("IsLetterOrDigit examples");
+			root.Children = _items.Where (i => char.IsLetterOrDigit (i [0])).Select (i => new TreeNode (i)).Cast<ITreeNode> ().ToList ();
+			_treeView.AddObject (root);
+			root = new TreeNode ("Non-IsLetterOrDigit examples");
+			root.Children = _items.Where (i => !char.IsLetterOrDigit (i [0])).Select (i => new TreeNode (i)).Cast<ITreeNode> ().ToList ();
+			_treeView.AddObject (root);
+			_treeView.ExpandAll ();
+			_treeView.GoToFirst ();
+
+			_treeView.KeystrokeNavigator.SearchStringChanged += (state) => {
+				label.Text = $"TreeView: {state.SearchString}";
+			};
+		}
+
+		private void Quit ()
+		{
+			Application.RequestStop ();
+		}
+	}
+}

+ 2 - 2
UICatalog/Scenarios/ComputedLayout.cs

@@ -25,12 +25,12 @@ namespace UICatalog.Scenarios {
 					new MenuItem ("_Quit", "", () => Quit()),
 				}),
 			});
-			Top.Add (menu);
+			Application.Top.Add (menu);
 
 			var statusBar = new StatusBar (new StatusItem [] {
 				new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()),
 			});
-			Top.Add (statusBar);
+			Application.Top.Add (statusBar);
 
 			//Top.LayoutStyle = LayoutStyle.Computed;
 			// Demonstrate using Dim to create a horizontal ruler that always measures the parent window's width

+ 1 - 1
UICatalog/Scenarios/ContextMenus.cs

@@ -81,7 +81,7 @@ namespace UICatalog.Scenarios {
 
 			Win.WantMousePositionReports = true;
 
-			Top.Closed += (_) => {
+			Application.Top.Closed += (_) => {
 				Thread.CurrentThread.CurrentUICulture = new CultureInfo ("en-US");
 				Application.RootMouseEvent -= Application_RootMouseEvent;
 			};

+ 211 - 196
UICatalog/Scenarios/CsvEditor.cs

@@ -9,6 +9,7 @@ using System.IO;
 using System.Text;
 using NStack;
 using System.Text.RegularExpressions;
+using CsvHelper;
 
 namespace UICatalog.Scenarios {
 
@@ -20,8 +21,7 @@ namespace UICatalog.Scenarios {
 	[ScenarioCategory ("Dialogs")]
 	[ScenarioCategory ("Top Level Windows")]
 	[ScenarioCategory ("Files and IO")]
-	public class CsvEditor : Scenario 
-	{
+	public class CsvEditor : Scenario {
 		TableView tableView;
 		private string currentFile;
 		private MenuItem miLeft;
@@ -31,10 +31,10 @@ namespace UICatalog.Scenarios {
 
 		public override void Setup ()
 		{
-			Win.Title = this.GetName();
+			Win.Title = this.GetName ();
 			Win.Y = 1; // menu
 			Win.Height = Dim.Fill (1); // status bar
-			Top.LayoutSubviews ();
+			Application.Top.LayoutSubviews ();
 
 			this.tableView = new TableView () {
 				X = 0,
@@ -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()),
@@ -68,33 +70,33 @@ namespace UICatalog.Scenarios {
 					miCentered = new MenuItem ("_Set Format Pattern", "", () => SetFormat()),
 				})
 			});
-			Top.Add (menu);
+			Application.Top.Add (menu);
 
 			var statusBar = new StatusBar (new StatusItem [] {
 				new StatusItem(Key.CtrlMask | Key.O, "~^O~ Open", () => Open()),
 				new StatusItem(Key.CtrlMask | Key.S, "~^S~ Save", () => Save()),
 				new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()),
 			});
-			Top.Add (statusBar);
+			Application.Top.Add (statusBar);
 
 			Win.Add (tableView);
 
-			selectedCellLabel = new TextField(){
+			selectedCellLabel = new TextField () {
 				X = 0,
-				Y = Pos.Bottom(tableView),
+				Y = Pos.Bottom (tableView),
 				Text = "0,0",
-				Width = Dim.Fill(),
-				TextAlignment = TextAlignment.Right				
+				Width = Dim.Fill (),
+				TextAlignment = TextAlignment.Right
 			};
 			selectedCellLabel.TextChanged += SelectedCellLabel_TextChanged;
 
-			Win.Add(selectedCellLabel);
+			Win.Add (selectedCellLabel);
 
 			tableView.SelectedCellChanged += OnSelectedCellChanged;
 			tableView.CellActivated += EditCurrentCell;
 			tableView.KeyPress += TableViewKeyPress;
 
-			SetupScrollBar();
+			SetupScrollBar ();
 		}
 
 		private void SelectedCellLabel_TextChanged (ustring last)
@@ -102,10 +104,10 @@ namespace UICatalog.Scenarios {
 			// if user is in the text control and editing the selected cell
 			if (!selectedCellLabel.HasFocus)
 				return;
-			
+
 			// change selected cell to the one the user has typed into the box
-			var match = Regex.Match (selectedCellLabel.Text.ToString(), "^(\\d+),(\\d+)$");
-			if(match.Success) {
+			var match = Regex.Match (selectedCellLabel.Text.ToString (), "^(\\d+),(\\d+)$");
+			if (match.Success) {
 
 				tableView.SelectedColumn = int.Parse (match.Groups [1].Value);
 				tableView.SelectedRow = int.Parse (match.Groups [2].Value);
@@ -117,149 +119,147 @@ namespace UICatalog.Scenarios {
 			// only update the text box if the user is not manually editing it
 			if (!selectedCellLabel.HasFocus)
 				selectedCellLabel.Text = $"{tableView.SelectedRow},{tableView.SelectedColumn}";
-			
-			if(tableView.Table == null || tableView.SelectedColumn == -1)
+
+			if (tableView.Table == null || tableView.SelectedColumn == -1)
 				return;
 
-			var col = tableView.Table.Columns[tableView.SelectedColumn];
+			var col = tableView.Table.Columns [tableView.SelectedColumn];
+
+			var style = tableView.Style.GetColumnStyleIfAny (col);
 
-			var style = tableView.Style.GetColumnStyleIfAny(col);
-			
 			miLeft.Checked = style?.Alignment == TextAlignment.Left;
 			miRight.Checked = style?.Alignment == TextAlignment.Right;
-			miCentered.Checked = style?.Alignment == TextAlignment.Centered;			
+			miCentered.Checked = style?.Alignment == TextAlignment.Centered;
 		}
 
 		private void RenameColumn ()
 		{
-			if(NoTableLoaded()) {
+			if (NoTableLoaded ()) {
 				return;
 			}
 
-			var currentCol = tableView.Table.Columns[tableView.SelectedColumn];
+			var currentCol = tableView.Table.Columns [tableView.SelectedColumn];
 
-			if(GetText("Rename Column","Name:",currentCol.ColumnName,out string newName)) {
+			if (GetText ("Rename Column", "Name:", currentCol.ColumnName, out string newName)) {
 				currentCol.ColumnName = newName;
-				tableView.Update();
+				tableView.Update ();
 			}
 		}
 
-		private void DeleteColum()
+		private void DeleteColum ()
 		{
-			if(NoTableLoaded()) {
+			if (NoTableLoaded ()) {
 				return;
 			}
 
-			if(tableView.SelectedColumn == -1) {
-				
-				MessageBox.ErrorQuery("No Column","No column selected", "Ok");
+			if (tableView.SelectedColumn == -1) {
+
+				MessageBox.ErrorQuery ("No Column", "No column selected", "Ok");
 				return;
 			}
 
 
 			try {
-				tableView.Table.Columns.RemoveAt(tableView.SelectedColumn);
-				tableView.Update();
+				tableView.Table.Columns.RemoveAt (tableView.SelectedColumn);
+				tableView.Update ();
 
 			} catch (Exception ex) {
-				MessageBox.ErrorQuery("Could not remove column",ex.Message, "Ok");
+				MessageBox.ErrorQuery ("Could not remove column", ex.Message, "Ok");
 			}
 		}
 
 		private void MoveColumn ()
 		{
-			if(NoTableLoaded()) {
+			if (NoTableLoaded ()) {
 				return;
 			}
 
-			if(tableView.SelectedColumn == -1) {
-				
-				MessageBox.ErrorQuery("No Column","No column selected", "Ok");
+			if (tableView.SelectedColumn == -1) {
+
+				MessageBox.ErrorQuery ("No Column", "No column selected", "Ok");
 				return;
 			}
-			
-			try{
 
-				var currentCol = tableView.Table.Columns[tableView.SelectedColumn];
+			try {
+
+				var currentCol = tableView.Table.Columns [tableView.SelectedColumn];
 
-				if(GetText("Move Column","New Index:",currentCol.Ordinal.ToString(),out string newOrdinal)) {
+				if (GetText ("Move Column", "New Index:", currentCol.Ordinal.ToString (), out string newOrdinal)) {
 
-					var newIdx = Math.Min(Math.Max(0,int.Parse(newOrdinal)),tableView.Table.Columns.Count-1);
+					var newIdx = Math.Min (Math.Max (0, int.Parse (newOrdinal)), tableView.Table.Columns.Count - 1);
 
-					currentCol.SetOrdinal(newIdx);
+					currentCol.SetOrdinal (newIdx);
 
-					tableView.SetSelection(newIdx,tableView.SelectedRow,false);
-					tableView.EnsureSelectedCellIsVisible();
-					tableView.SetNeedsDisplay();
+					tableView.SetSelection (newIdx, tableView.SelectedRow, false);
+					tableView.EnsureSelectedCellIsVisible ();
+					tableView.SetNeedsDisplay ();
 				}
 
-			}catch(Exception ex)
-			{
-				MessageBox.ErrorQuery("Error moving column",ex.Message, "Ok");
+			} catch (Exception ex) {
+				MessageBox.ErrorQuery ("Error moving column", ex.Message, "Ok");
 			}
 		}
 		private void Sort (bool asc)
 		{
 
-			if(NoTableLoaded()) {
+			if (NoTableLoaded ()) {
 				return;
 			}
 
-			if(tableView.SelectedColumn == -1) {
-				
-				MessageBox.ErrorQuery("No Column","No column selected", "Ok");
+			if (tableView.SelectedColumn == -1) {
+
+				MessageBox.ErrorQuery ("No Column", "No column selected", "Ok");
 				return;
 			}
 
-			var colName = tableView.Table.Columns[tableView.SelectedColumn].ColumnName;
+			var colName = tableView.Table.Columns [tableView.SelectedColumn].ColumnName;
 
 			tableView.Table.DefaultView.Sort = colName + (asc ? " asc" : " desc");
-			tableView.Table = tableView.Table.DefaultView.ToTable();
+			tableView.Table = tableView.Table.DefaultView.ToTable ();
 		}
 
 		private void MoveRow ()
 		{
-			if(NoTableLoaded()) {
+			if (NoTableLoaded ()) {
 				return;
 			}
 
-			if(tableView.SelectedRow == -1) {
-				
-				MessageBox.ErrorQuery("No Rows","No row selected", "Ok");
+			if (tableView.SelectedRow == -1) {
+
+				MessageBox.ErrorQuery ("No Rows", "No row selected", "Ok");
 				return;
 			}
-			
-			try{
+
+			try {
 
 				int oldIdx = tableView.SelectedRow;
 
-				var currentRow = tableView.Table.Rows[oldIdx];
+				var currentRow = tableView.Table.Rows [oldIdx];
 
-				if(GetText("Move Row","New Row:",oldIdx.ToString(),out string newOrdinal)) {
+				if (GetText ("Move Row", "New Row:", oldIdx.ToString (), out string newOrdinal)) {
 
-					var newIdx = Math.Min(Math.Max(0,int.Parse(newOrdinal)),tableView.Table.Rows.Count-1);
+					var newIdx = Math.Min (Math.Max (0, int.Parse (newOrdinal)), tableView.Table.Rows.Count - 1);
 
 
-					if(newIdx == oldIdx)
+					if (newIdx == oldIdx)
 						return;
 
 					var arrayItems = currentRow.ItemArray;
-					tableView.Table.Rows.Remove(currentRow);
+					tableView.Table.Rows.Remove (currentRow);
 
 					// Removing and Inserting the same DataRow seems to result in it loosing its values so we have to create a new instance
-					var newRow = tableView.Table.NewRow();
+					var newRow = tableView.Table.NewRow ();
 					newRow.ItemArray = arrayItems;
-					
-					tableView.Table.Rows.InsertAt(newRow,newIdx);
-					
-					tableView.SetSelection(tableView.SelectedColumn,newIdx,false);
-					tableView.EnsureSelectedCellIsVisible();
-					tableView.SetNeedsDisplay();
+
+					tableView.Table.Rows.InsertAt (newRow, newIdx);
+
+					tableView.SetSelection (tableView.SelectedColumn, newIdx, false);
+					tableView.EnsureSelectedCellIsVisible ();
+					tableView.SetNeedsDisplay ();
 				}
 
-			}catch(Exception ex)
-			{
-				MessageBox.ErrorQuery("Error moving column",ex.Message, "Ok");
+			} catch (Exception ex) {
+				MessageBox.ErrorQuery ("Error moving column", ex.Message, "Ok");
 			}
 		}
 
@@ -269,43 +269,43 @@ namespace UICatalog.Scenarios {
 				return;
 			}
 
-			var col = tableView.Table.Columns[tableView.SelectedColumn];
+			var col = tableView.Table.Columns [tableView.SelectedColumn];
 
-			var style = tableView.Style.GetOrCreateColumnStyle(col);
+			var style = tableView.Style.GetOrCreateColumnStyle (col);
 			style.Alignment = newAlignment;
 
 			miLeft.Checked = style.Alignment == TextAlignment.Left;
 			miRight.Checked = style.Alignment == TextAlignment.Right;
-			miCentered.Checked = style.Alignment == TextAlignment.Centered;	
-			
-			tableView.Update();
+			miCentered.Checked = style.Alignment == TextAlignment.Centered;
+
+			tableView.Update ();
 		}
-		
-		private void SetFormat()
+
+		private void SetFormat ()
 		{
 			if (NoTableLoaded ()) {
 				return;
 			}
 
-			var col = tableView.Table.Columns[tableView.SelectedColumn];
+			var col = tableView.Table.Columns [tableView.SelectedColumn];
 
-			if(col.DataType == typeof(string)) {
-				MessageBox.ErrorQuery("Cannot Format Column","String columns cannot be Formatted, try adding a new column to the table with a date/numerical Type","Ok");
+			if (col.DataType == typeof (string)) {
+				MessageBox.ErrorQuery ("Cannot Format Column", "String columns cannot be Formatted, try adding a new column to the table with a date/numerical Type", "Ok");
 				return;
 			}
 
-			var style = tableView.Style.GetOrCreateColumnStyle(col);
+			var style = tableView.Style.GetOrCreateColumnStyle (col);
 
-			if(GetText("Format","Pattern:",style.Format ?? "",out string newPattern)) {
+			if (GetText ("Format", "Pattern:", style.Format ?? "", out string newPattern)) {
 				style.Format = newPattern;
-				tableView.Update();
+				tableView.Update ();
 			}
 		}
 
 		private bool NoTableLoaded ()
 		{
-			if(tableView.Table == null) {
-				MessageBox.ErrorQuery("No Table Loaded","No table has currently be opened","Ok");
+			if (tableView.Table == null) {
+				MessageBox.ErrorQuery ("No Table Loaded", "No table has currently be opened", "Ok");
 				return true;
 			}
 
@@ -314,112 +314,132 @@ namespace UICatalog.Scenarios {
 
 		private void AddRow ()
 		{
-			if(NoTableLoaded()) {
+			if (NoTableLoaded ()) {
 				return;
 			}
 
-			var newRow = tableView.Table.NewRow();
+			var newRow = tableView.Table.NewRow ();
 
-			var newRowIdx = Math.Min(Math.Max(0,tableView.SelectedRow+1),tableView.Table.Rows.Count);
+			var newRowIdx = Math.Min (Math.Max (0, tableView.SelectedRow + 1), tableView.Table.Rows.Count);
 
-			tableView.Table.Rows.InsertAt(newRow,newRowIdx);
-			tableView.Update();
+			tableView.Table.Rows.InsertAt (newRow, newRowIdx);
+			tableView.Update ();
 		}
 
 		private void AddColumn ()
 		{
-			if(NoTableLoaded()) {
+			if (NoTableLoaded ()) {
 				return;
 			}
 
-			if(GetText("Enter column name","Name:","",out string colName)) {
+			if (GetText ("Enter column name", "Name:", "", out string colName)) {
 
-				var col = new DataColumn(colName);
+				var col = new DataColumn (colName);
 
-				var newColIdx = Math.Min(Math.Max(0,tableView.SelectedColumn + 1),tableView.Table.Columns.Count);
-				
-				int result = MessageBox.Query(40,15,"Column Type","Pick a data type for the column",new ustring[]{"Date","Integer","Double","Text","Cancel"});
+				var newColIdx = Math.Min (Math.Max (0, tableView.SelectedColumn + 1), tableView.Table.Columns.Count);
 
-				if(result <= -1 || result >= 4)
+				int result = MessageBox.Query ("Column Type", "Pick a data type for the column", new ustring [] { "Date", "Integer", "Double", "Text", "Cancel" });
+
+				if (result <= -1 || result >= 4)
 					return;
-				switch(result) {
-					case 0: col.DataType = typeof(DateTime);
-						break;
-					case 1: col.DataType = typeof(int);
-						break;
-					case 2: col.DataType = typeof(double);
-						break;
-					case 3: col.DataType = typeof(string);
-						break;
+				switch (result) {
+				case 0:
+					col.DataType = typeof (DateTime);
+					break;
+				case 1:
+					col.DataType = typeof (int);
+					break;
+				case 2:
+					col.DataType = typeof (double);
+					break;
+				case 3:
+					col.DataType = typeof (string);
+					break;
 				}
 
-				tableView.Table.Columns.Add(col);
-				col.SetOrdinal(newColIdx);
-				tableView.Update();
+				tableView.Table.Columns.Add (col);
+				col.SetOrdinal (newColIdx);
+				tableView.Update ();
 			}
 
-			
-				
+
+
 		}
 
-		private void Save()
+		private void Save ()
 		{
-			if(tableView.Table == null || string.IsNullOrWhiteSpace(currentFile)) {
-				MessageBox.ErrorQuery("No file loaded","No file is currently loaded","Ok");
+			if (tableView.Table == null || string.IsNullOrWhiteSpace (currentFile)) {
+				MessageBox.ErrorQuery ("No file loaded", "No file is currently loaded", "Ok");
 				return;
 			}
+			using var writer = new CsvWriter (
+				new StreamWriter (File.OpenWrite (currentFile)),
+				CultureInfo.InvariantCulture);
 
-			var sb = new StringBuilder();
+			foreach (var col in tableView.Table.Columns.Cast<DataColumn> ().Select (c => c.ColumnName)) {
+				writer.WriteField (col);
+			}
 
-			sb.AppendLine(string.Join(",",tableView.Table.Columns.Cast<DataColumn>().Select(c=>c.ColumnName)));
+			writer.NextRecord ();
 
-			foreach(DataRow row in tableView.Table.Rows) {
-				sb.AppendLine(string.Join(",",row.ItemArray));
+			foreach (DataRow row in tableView.Table.Rows) {
+				foreach (var item in row.ItemArray) {
+					writer.WriteField (item);
+				}
+				writer.NextRecord ();
 			}
-			
-			File.WriteAllText(currentFile,sb.ToString());
+
 		}
 
-		private void Open()
+		private void Open ()
 		{
-			var ofd = new FileDialog("Select File","Open","File","Select a CSV file to open (does not support newlines, escaping etc)");
-			ofd.AllowedFileTypes = new string[]{".csv" };
-
-			Application.Run(ofd);
-			
-			if(!ofd.Canceled && !string.IsNullOrWhiteSpace(ofd.FilePath?.ToString()))
-			{
-				Open(ofd.FilePath.ToString());
+			var ofd = new FileDialog ("Select File", "Open", "File", "Select a CSV file to open (does not support newlines, escaping etc)") {
+				AllowedFileTypes = new string [] { ".csv" }
+			};
+
+			Application.Run (ofd);
+
+			if (!ofd.Canceled && !string.IsNullOrWhiteSpace (ofd.FilePath?.ToString ())) {
+				Open (ofd.FilePath.ToString ());
 			}
 		}
-		
-		private void Open(string filename)
+
+		private void Open (string filename)
 		{
-			
+
 			int lineNumber = 0;
 			currentFile = null;
 
+			using var reader = new CsvReader (File.OpenText (filename), CultureInfo.InvariantCulture);
+
 			try {
-				var dt = new DataTable();
-				var lines = File.ReadAllLines(filename);
-			
-				foreach(var h in lines[0].Split(',')){
-					dt.Columns.Add(h);
+				var dt = new DataTable ();
+
+				reader.Read ();
+
+				if (reader.ReadHeader ()) {
+					foreach (var h in reader.HeaderRecord) {
+						dt.Columns.Add (h);
+					}
 				}
-				
 
-				foreach(var line in lines.Skip(1)) {
+				while (reader.Read ()) {
 					lineNumber++;
-					dt.Rows.Add(line.Split(','));
+
+					var newRow = dt.Rows.Add ();
+					for (int i = 0; i < dt.Columns.Count; i++) {
+						newRow [i] = reader [i];
+					}
 				}
-				
+
 				tableView.Table = dt;
-				
-				// Only set the current filename if we succesfully loaded the entire file
+
+				// Only set the current filename if we successfully loaded the entire file
 				currentFile = filename;
-			}
-			catch(Exception ex) {
-				MessageBox.ErrorQuery("Open Failed",$"Error on line {lineNumber}{Environment.NewLine}{ex.Message}","Ok");
+				Win.Title = $"{this.GetName ()} - {Path.GetFileName(currentFile)}";
+
+			} catch (Exception ex) {
+				MessageBox.ErrorQuery ("Open Failed", $"Error on line {lineNumber}{Environment.NewLine}{ex.Message}", "Ok");
 			}
 		}
 		private void SetupScrollBar ()
@@ -443,45 +463,42 @@ namespace UICatalog.Scenarios {
 			};*/
 
 			tableView.DrawContent += (e) => {
-				_scrollBar.Size = tableView.Table?.Rows?.Count ??0;
+				_scrollBar.Size = tableView.Table?.Rows?.Count ?? 0;
 				_scrollBar.Position = tableView.RowOffset;
-			//	_scrollBar.OtherScrollBarView.Size = _listView.Maxlength - 1;
-			//	_scrollBar.OtherScrollBarView.Position = _listView.LeftItem;
+				//	_scrollBar.OtherScrollBarView.Size = _listView.Maxlength - 1;
+				//	_scrollBar.OtherScrollBarView.Position = _listView.LeftItem;
 				_scrollBar.Refresh ();
 			};
-		
+
 		}
 
 		private void TableViewKeyPress (View.KeyEventEventArgs e)
 		{
-			if(e.KeyEvent.Key == Key.DeleteChar){
+			if (e.KeyEvent.Key == Key.DeleteChar) {
 
-				if(tableView.FullRowSelect)
-				{
+				if (tableView.FullRowSelect) {
 					// Delete button deletes all rows when in full row mode
-					foreach(int toRemove in tableView.GetAllSelectedCells().Select(p=>p.Y).Distinct().OrderByDescending(i=>i))
-						tableView.Table.Rows.RemoveAt(toRemove);
-				}
-				else{
+					foreach (int toRemove in tableView.GetAllSelectedCells ().Select (p => p.Y).Distinct ().OrderByDescending (i => i))
+						tableView.Table.Rows.RemoveAt (toRemove);
+				} else {
 
 					// otherwise set all selected cells to null
-					foreach(var pt in tableView.GetAllSelectedCells())
-					{
-						tableView.Table.Rows[pt.Y][pt.X] = DBNull.Value;
+					foreach (var pt in tableView.GetAllSelectedCells ()) {
+						tableView.Table.Rows [pt.Y] [pt.X] = DBNull.Value;
 					}
 				}
 
-				tableView.Update();
+				tableView.Update ();
 				e.Handled = true;
 			}
 		}
 
 		private void ClearColumnStyles ()
 		{
-			tableView.Style.ColumnStyles.Clear();
-			tableView.Update();
+			tableView.Style.ColumnStyles.Clear ();
+			tableView.Update ();
 		}
-			
+
 
 		private void CloseExample ()
 		{
@@ -492,7 +509,7 @@ namespace UICatalog.Scenarios {
 		{
 			Application.RequestStop ();
 		}
-		private bool GetText(string title, string label, string initialText, out string enteredText)
+		private bool GetText (string title, string label, string initialText, out string enteredText)
 		{
 			bool okPressed = false;
 
@@ -502,44 +519,42 @@ namespace UICatalog.Scenarios {
 			cancel.Clicked += () => { Application.RequestStop (); };
 			var d = new Dialog (title, 60, 20, ok, cancel);
 
-			var lbl = new Label() {
+			var lbl = new Label () {
 				X = 0,
 				Y = 1,
 				Text = label
 			};
 
-			var tf = new TextField()
-				{
-					Text = initialText,
-					X = 0,
-					Y = 2,
-					Width = Dim.Fill()
-				};
-			
-			d.Add (lbl,tf);
-			tf.SetFocus();
+			var tf = new TextField () {
+				Text = initialText,
+				X = 0,
+				Y = 2,
+				Width = Dim.Fill ()
+			};
+
+			d.Add (lbl, tf);
+			tf.SetFocus ();
 
 			Application.Run (d);
 
-			enteredText = okPressed? tf.Text.ToString() : null;
+			enteredText = okPressed ? tf.Text.ToString () : null;
 			return okPressed;
 		}
 		private void EditCurrentCell (TableView.CellActivatedEventArgs e)
 		{
-			if(e.Table == null)
+			if (e.Table == null)
 				return;
 
-			var oldValue = e.Table.Rows[e.Row][e.Col].ToString();
+			var oldValue = e.Table.Rows [e.Row] [e.Col].ToString ();
 
-			if(GetText("Enter new value",e.Table.Columns[e.Col].ColumnName,oldValue, out string newText)) {
+			if (GetText ("Enter new value", e.Table.Columns [e.Col].ColumnName, oldValue, out string newText)) {
 				try {
-					e.Table.Rows[e.Row][e.Col] = string.IsNullOrWhiteSpace(newText) ? DBNull.Value : (object)newText;
-				}
-				catch(Exception ex) {
-					MessageBox.ErrorQuery(60,20,"Failed to set text", ex.Message,"Ok");
+					e.Table.Rows [e.Row] [e.Col] = string.IsNullOrWhiteSpace (newText) ? DBNull.Value : (object)newText;
+				} catch (Exception ex) {
+					MessageBox.ErrorQuery (60, 20, "Failed to set text", ex.Message, "Ok");
 				}
-				
-				tableView.Update();
+
+				tableView.Update ();
 			}
 		}
 	}

+ 2 - 2
UICatalog/Scenarios/Dialogs.cs

@@ -116,9 +116,9 @@ namespace UICatalog.Scenarios {
 			{
 				frame.Height = Dim.Height (widthEdit) + Dim.Height (heightEdit) + Dim.Height (titleEdit)
 					+ Dim.Height (numButtonsEdit) + Dim.Height (styleRadioGroup) + Dim.Height(glyphsNotWords) + 2;
-				Top.Loaded -= Top_Loaded;
+				Application.Top.Loaded -= Top_Loaded;
 			}
-			Top.Loaded += Top_Loaded;
+			Application.Top.Loaded += Top_Loaded;
 
 			label = new Label ("Button Pressed:") {
 				X = Pos.Center (),

+ 3 - 4
UICatalog/Scenarios/DynamicMenuBar.cs

@@ -13,11 +13,10 @@ namespace UICatalog.Scenarios {
 	[ScenarioCategory ("Top Level Windows")]
 	[ScenarioCategory ("Menus")]
 	public class DynamicMenuBar : Scenario {
-		public override void Init (Toplevel top, ColorScheme colorScheme)
+		public override void Init (ColorScheme colorScheme)
 		{
 			Application.Init ();
-			Top = Application.Top;
-			Top.Add (new DynamicMenuBarSample ($"CTRL-Q to Close - Scenario: {GetName ()}"));
+			Application.Top.Add (new DynamicMenuBarSample ($"CTRL-Q to Close - Scenario: {GetName ()}"));
 		}
 
 		public class DynamicMenuItemList {
@@ -144,7 +143,7 @@ namespace UICatalog.Scenarios {
 					TextAlignment = TextAlignment.Centered,
 					X = Pos.Right (_btnPrevious) + 1,
 					Y = Pos.Top (_btnPrevious),
-					Width = Dim.Fill () - Dim.Width (_btnAdd) - 1,
+					Width = Dim.Fill () - Dim.Function (() => _btnAdd.Frame.Width + 1),
 					Height = 1
 				};
 				_frmMenu.Add (_lblMenuBar);

+ 2 - 3
UICatalog/Scenarios/DynamicStatusBar.cs

@@ -12,11 +12,10 @@ namespace UICatalog.Scenarios {
 	[ScenarioMetadata (Name: "Dynamic StatusBar", Description: "Demonstrates how to add and remove a StatusBar and change items dynamically.")]
 	[ScenarioCategory ("Top Level Windows")]
 	public class DynamicStatusBar : Scenario {
-		public override void Init (Toplevel top, ColorScheme colorScheme)
+		public override void Init (ColorScheme colorScheme)
 		{
 			Application.Init ();
-			Top = Application.Top;
-			Top.Add (new DynamicStatusBarSample ($"CTRL-Q to Close - Scenario: {GetName ()}"));
+			Application.Top.Add (new DynamicStatusBarSample ($"CTRL-Q to Close - Scenario: {GetName ()}"));
 		}
 
 		public class DynamicStatusItemList {

+ 10 - 12
UICatalog/Scenarios/Editor.cs

@@ -30,15 +30,12 @@ namespace UICatalog.Scenarios {
 		private TabView _tabView;
 		private MenuItem _miForceMinimumPosToZero;
 		private bool _forceMinimumPosToZero = true;
-		private readonly List<CultureInfo> _cultureInfos = Application.SupportedCultures;
+		private List<CultureInfo> _cultureInfos;
 
-		public override void Init (Toplevel top, ColorScheme colorScheme)
+		public override void Init (ColorScheme colorScheme)
 		{
 			Application.Init ();
-			Top = top;
-			if (Top == null) {
-				Top = Application.Top;
-			}
+			_cultureInfos = Application.SupportedCultures;
 
 			Win = new Window (_fileName ?? "Untitled") {
 				X = 0,
@@ -47,7 +44,7 @@ namespace UICatalog.Scenarios {
 				Height = Dim.Fill (),
 				ColorScheme = colorScheme,
 			};
-			Top.Add (Win);
+			Application.Top.Add (Win);
 
 			_textView = new TextView () {
 				X = 0,
@@ -116,7 +113,8 @@ namespace UICatalog.Scenarios {
 					new MenuBarItem ("_Languages", GetSupportedCultures ())
 				})
 			});
-			Top.Add (menu);
+
+			Application.Top.Add (menu);
 
 			var statusBar = new StatusBar (new StatusItem [] {
 				siCursorPosition,
@@ -126,7 +124,7 @@ namespace UICatalog.Scenarios {
 				new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()),
 				new StatusItem(Key.Null, $"OS Clipboard IsSupported : {Clipboard.IsSupported}", null)
 			});
-			Top.Add (statusBar);
+			Application.Top.Add (statusBar);
 
 			_scrollBar = new ScrollBarView (_textView, true);
 
@@ -198,7 +196,7 @@ namespace UICatalog.Scenarios {
 				}
 			};
 
-			Top.Closed += (_) => Thread.CurrentThread.CurrentUICulture = new CultureInfo ("en-US");
+			Application.Top.Closed += (_) => Thread.CurrentThread.CurrentUICulture = new CultureInfo ("en-US");
 		}
 
 		private void DisposeWinDialog ()
@@ -345,7 +343,7 @@ namespace UICatalog.Scenarios {
 		private bool CanCloseFile ()
 		{
 			if (_textView.Text == _originalText) {
-				System.Diagnostics.Debug.Assert (!_textView.IsDirty);
+				//System.Diagnostics.Debug.Assert (!_textView.IsDirty);
 				return true;
 			}
 
@@ -367,7 +365,7 @@ namespace UICatalog.Scenarios {
 			if (!CanCloseFile ()) {
 				return;
 			}
-			var aTypes = new List<string> () { ".txt;.bin;.xml;.json", ".txt", ".bin", ".xml", ".*" };
+			var aTypes = new List<string> () { ".txt;.bin;.xml;.json", ".txt", ".bin", ".xml", ".json", ".*" };
 			var d = new OpenDialog ("Open", "Choose the path where to open the file.", aTypes) { AllowsMultipleSelection = false };
 			Application.Run (d);
 

+ 3 - 3
UICatalog/Scenarios/GraphViewExample.cs

@@ -23,7 +23,7 @@ namespace UICatalog.Scenarios {
 			Win.Title = this.GetName ();
 			Win.Y = 1; // menu
 			Win.Height = Dim.Fill (1); // status bar
-			Top.LayoutSubviews ();
+			Application.Top.LayoutSubviews ();
 
 			graphs = new Action [] {
 				 ()=>SetupPeriodicTableScatterPlot(),    //0
@@ -59,7 +59,7 @@ namespace UICatalog.Scenarios {
 				}),
 
 				});
-			Top.Add (menu);
+			Application.Top.Add (menu);
 
 			graphView = new GraphView () {
 				X = 1,
@@ -92,7 +92,7 @@ namespace UICatalog.Scenarios {
 				new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()),
 				new StatusItem(Key.CtrlMask | Key.G, "~^G~ Next", ()=>graphs[currentGraph++%graphs.Length]()),
 			});
-			Top.Add (statusBar);
+			Application.Top.Add (statusBar);
 		}
 
 		private void MultiBarGraph ()

+ 2 - 2
UICatalog/Scenarios/HexEditor.cs

@@ -52,7 +52,7 @@ namespace UICatalog.Scenarios {
 					miAllowEdits = new MenuItem ("_AllowEdits", "", () => ToggleAllowEdits ()){Checked = _hexView.AllowEdits, CheckType = MenuItemCheckStyle.Checked}
 				})
 			});
-			Top.Add (menu);
+			Application.Top.Add (menu);
 
 			statusBar = new StatusBar (new StatusItem [] {
 				new StatusItem(Key.F2, "~F2~ Open", () => Open()),
@@ -61,7 +61,7 @@ namespace UICatalog.Scenarios {
 				siPositionChanged = new StatusItem(Key.Null,
 					$"Position: {_hexView.Position} Line: {_hexView.CursorPosition.Y} Col: {_hexView.CursorPosition.X} Line length: {_hexView.BytesPerLine}", () => {})
 			});
-			Top.Add (statusBar);
+			Application.Top.Add (statusBar);
 		}
 
 		private void _hexView_PositionChanged (HexView.HexViewEventArgs obj)

+ 3 - 3
UICatalog/Scenarios/InteractiveTree.cs

@@ -20,14 +20,14 @@ namespace UICatalog.Scenarios {
 			Win.Title = this.GetName ();
 			Win.Y = 1; // menu
 			Win.Height = Dim.Fill (1); // status bar
-			Top.LayoutSubviews ();
+			Application.Top.LayoutSubviews ();
 
 			var menu = new MenuBar (new MenuBarItem [] {
 				new MenuBarItem ("_File", new MenuItem [] {
 					new MenuItem ("_Quit", "", () => Quit()),
 				})
 				});
-			Top.Add (menu);
+			Application.Top.Add (menu);
 
 			treeView = new TreeView () {
 				X = 0,
@@ -45,7 +45,7 @@ namespace UICatalog.Scenarios {
 				new StatusItem(Key.CtrlMask | Key.T, "~^T~ Add Root", () => AddRootNode()),
 				new StatusItem(Key.CtrlMask | Key.R, "~^R~ Rename Node", () => RenameNode()),
 			});
-			Top.Add (statusBar);
+			Application.Top.Add (statusBar);
 
 		}
 

+ 7 - 8
UICatalog/Scenarios/Keys.cs

@@ -48,11 +48,10 @@ namespace UICatalog.Scenarios {
 			}
 		}
 
-		public override void Init (Toplevel top, ColorScheme colorScheme)
+		public override void Init (ColorScheme colorScheme)
 		{
 			Application.Init ();
-			Top = top;
-
+			
 			Win = new TestWindow ($"CTRL-Q to Close - Scenario: {GetName ()}") {
 				X = 0,
 				Y = 0,
@@ -60,7 +59,7 @@ namespace UICatalog.Scenarios {
 				Height = Dim.Fill (),
 				ColorScheme = colorScheme,
 			};
-			Top.Add (Win);
+			Application.Top.Add (Win);
 		}
 
 		public override void Setup ()
@@ -107,7 +106,7 @@ namespace UICatalog.Scenarios {
 				Shift = true
 			});
 			var maxLogEntry = $"Key{"",-5}: {fakeKeyPress}".Length;
-			var yOffset = (Top == Application.Top ? 1 : 6);
+			var yOffset = (Application.Top == Application.Top ? 1 : 6);
 			var keyStrokelist = new List<string> ();
 			var keyStrokeListView = new ListView (keyStrokelist) {
 				X = 0,
@@ -126,7 +125,7 @@ namespace UICatalog.Scenarios {
 			Win.Add (processKeyLogLabel);
 
 			maxLogEntry = $"{fakeKeyPress}".Length;
-			yOffset = (Top == Application.Top ? 1 : 6);
+			yOffset = (Application.Top == Application.Top ? 1 : 6);
 			var processKeyListView = new ListView (((TestWindow)Win)._processKeyList) {
 				X = Pos.Left (processKeyLogLabel),
 				Y = Pos.Top (processKeyLogLabel) + yOffset,
@@ -144,7 +143,7 @@ namespace UICatalog.Scenarios {
 			};
 			Win.Add (processHotKeyLogLabel);
 
-			yOffset = (Top == Application.Top ? 1 : 6);
+			yOffset = (Application.Top == Application.Top ? 1 : 6);
 			var processHotKeyListView = new ListView (((TestWindow)Win)._processHotKeyList) {
 				X = Pos.Left (processHotKeyLogLabel),
 				Y = Pos.Top (processHotKeyLogLabel) + yOffset,
@@ -162,7 +161,7 @@ namespace UICatalog.Scenarios {
 			};
 			Win.Add (processColdKeyLogLabel);
 
-			yOffset = (Top == Application.Top ? 1 : 6);
+			yOffset = (Application.Top == Application.Top ? 1 : 6);
 			var processColdKeyListView = new ListView (((TestWindow)Win)._processColdKeyList) {
 				X = Pos.Left (processColdKeyLogLabel),
 				Y = Pos.Top (processColdKeyLogLabel) + yOffset,

+ 3 - 3
UICatalog/Scenarios/LabelsAsButtons.cs

@@ -59,7 +59,7 @@ namespace UICatalog.Scenarios {
 			};
 			Win.Add (colorLabelsLabel);
 
-			//With this method there is no need to call Top.Ready += () => Top.Redraw (Top.Bounds);
+			//With this method there is no need to call Application.TopReady += () => Application.TopRedraw (Top.Bounds);
 			var x = Pos.Right (colorLabelsLabel) + 2;
 			foreach (var colorScheme in Colors.ColorSchemes) {
 				var colorLabel = new Label ($"{colorScheme.Key}") {
@@ -73,7 +73,7 @@ namespace UICatalog.Scenarios {
 				Win.Add (colorLabel);
 				x += colorLabel.Text.Length + 2;
 			}
-			Top.Ready += () => Top.Redraw (Top.Bounds);
+			Application.Top.Ready += () => Application.Top.Redraw (Application.Top.Bounds);
 
 			Label Label;
 			Win.Add (Label = new Label ("A super long _Label that will probably expose a bug in clipping or wrapping of text. Will it?") {
@@ -306,7 +306,7 @@ namespace UICatalog.Scenarios {
 				}
 			};
 
-			Top.Ready += () => radioGroup.Refresh ();
+			Application.Top.Ready += () => radioGroup.Refresh ();
 		}
 	}
 }

+ 3 - 3
UICatalog/Scenarios/LineViewExample.cs

@@ -17,14 +17,14 @@ namespace UICatalog.Scenarios {
 			Win.Title = this.GetName ();
 			Win.Y = 1; // menu
 			Win.Height = Dim.Fill (1); // status bar
-			Top.LayoutSubviews ();
+			Application.Top.LayoutSubviews ();
 
 			var menu = new MenuBar (new MenuBarItem [] {
 			new MenuBarItem ("_File", new MenuItem [] {
 				new MenuItem ("_Quit", "", () => Quit()),
 			})
 			});
-			Top.Add (menu);
+			Application.Top.Add (menu);
 
 
 			Win.Add (new Label ("Regular Line") { Y = 0 });
@@ -94,7 +94,7 @@ namespace UICatalog.Scenarios {
 			var statusBar = new StatusBar (new StatusItem [] {
 				new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit())
 			});
-			Top.Add (statusBar);
+			Application.Top.Add (statusBar);
 
 		}
 

+ 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 ();
+
+			_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;

+ 2 - 2
UICatalog/Scenarios/MessageBoxes.cs

@@ -156,9 +156,9 @@ namespace UICatalog.Scenarios {
 			{
 				frame.Height = Dim.Height (widthEdit) + Dim.Height (heightEdit) + Dim.Height (titleEdit) + Dim.Height (messageEdit)
 				+ Dim.Height (numButtonsEdit) + Dim.Height (defaultButtonEdit) + Dim.Height (styleRadioGroup) + 2 + Dim.Height (ckbEffect3D);
-				Top.Loaded -= Top_Loaded;
+				Application.Top.Loaded -= Top_Loaded;
 			}
-			Top.Loaded += Top_Loaded;
+			Application.Top.Loaded += Top_Loaded;
 
 			label = new Label ("Button Pressed:") {
 				X = Pos.Center (),

+ 3 - 3
UICatalog/Scenarios/MultiColouredTable.cs

@@ -16,7 +16,7 @@ namespace UICatalog.Scenarios {
 			Win.Title = this.GetName ();
 			Win.Y = 1; // menu
 			Win.Height = Dim.Fill (1); // status bar
-			Top.LayoutSubviews ();
+			Application.Top.LayoutSubviews ();
 
 			this.tableView = new TableViewColors () {
 				X = 0,
@@ -30,12 +30,12 @@ namespace UICatalog.Scenarios {
 					new MenuItem ("_Quit", "", () => Quit()),
 				}),
 			});
-			Top.Add (menu);
+			Application.Top.Add (menu);
 
 			var statusBar = new StatusBar (new StatusItem [] {
 				new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()),
 			});
-			Top.Add (statusBar);
+			Application.Top.Add (statusBar);
 
 			Win.Add (tableView);
 

+ 57 - 39
UICatalog/Scenarios/Notepad.cs

@@ -1,54 +1,51 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+using System.IO;
 using Terminal.Gui;
-using static UICatalog.Scenario;
 
 namespace UICatalog.Scenarios {
 
-	[ScenarioMetadata (Name: "Notepad", Description: "Multi tab text editor uising the TabView control.")]
+	[ScenarioMetadata (Name: "Notepad", Description: "Multi-tab text editor uising the TabView control.")]
 	[ScenarioCategory ("Controls"), ScenarioCategory ("TabView")]
 	public class Notepad : Scenario {
-
 		TabView tabView;
-		Label lblStatus;
 
 		private int numbeOfNewTabs = 1;
 
-		public override void Setup ()
+		// Don't create a Window, just return the top-level view
+		public override void Init (ColorScheme colorScheme)
 		{
-			Win.Title = this.GetName ();
-			Win.Y = 1; // menu
-			Win.Height = Dim.Fill (1); // status bar
-			Top.LayoutSubviews ();
+			Application.Init ();
+			Application.Top.ColorScheme = Colors.Base;
+		}
 
+		public override void Setup ()
+		{
 			var menu = new MenuBar (new MenuBarItem [] {
 				new MenuBarItem ("_File", new MenuItem [] {
 					new MenuItem ("_New", "", () => New()),
 					new MenuItem ("_Open", "", () => Open()),
 					new MenuItem ("_Save", "", () => Save()),
-					new MenuItem ("_Save As", "", () => SaveAs()),
+					new MenuItem ("Save _As", "", () => SaveAs()),
 					new MenuItem ("_Close", "", () => Close()),
 					new MenuItem ("_Quit", "", () => Quit()),
 				})
 				});
-			Top.Add (menu);
+			Application.Top.Add (menu);
 
 			tabView = new TabView () {
 				X = 0,
-				Y = 0,
+				Y = 1,
 				Width = Dim.Fill (),
 				Height = Dim.Fill (1),
 			};
 
-			tabView.Style.ShowBorder = false;
+			tabView.TabClicked += TabView_TabClicked;
+
+			tabView.Style.ShowBorder = true;
 			tabView.ApplyStyleChanges ();
 
-			Win.Add (tabView);
+			Application.Top.Add (tabView);
 
+			var lenStatusItem = new StatusItem (Key.CharMask, "Len: ", null);
 			var statusBar = new StatusBar (new StatusItem [] {
 				new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()),
 
@@ -58,24 +55,42 @@ namespace UICatalog.Scenarios {
 
 				new StatusItem(Key.CtrlMask | Key.S, "~^S~ Save", () => Save()),
 				new StatusItem(Key.CtrlMask | Key.W, "~^W~ Close", () => Close()),
+				lenStatusItem,
 			});
 
-			Win.Add (lblStatus = new Label ("Len:") {
-				Y = Pos.Bottom (tabView),
-				Width = Dim.Fill (),
-				TextAlignment = TextAlignment.Right
-			});
+			tabView.SelectedTabChanged += (s, e) => lenStatusItem.Title = $"Len:{(e.NewTab?.View?.Text?.Length ?? 0)}";
 
-			tabView.SelectedTabChanged += (s, e) => UpdateStatus (e.NewTab);
-
-			Top.Add (statusBar);
+			Application.Top.Add (statusBar);
 
 			New ();
 		}
 
-		private void UpdateStatus (TabView.Tab newTab)
+		private void TabView_TabClicked (object sender, TabView.TabMouseEventArgs e)
 		{
-			lblStatus.Text = $"Len:{(newTab?.View?.Text?.Length ?? 0)}";
+			// we are only interested in right clicks
+			if(!e.MouseEvent.Flags.HasFlag(MouseFlags.Button3Clicked)) {
+				return;
+			}
+
+			MenuBarItem items;
+
+			if (e.Tab == null) {
+				items = new MenuBarItem (new MenuItem [] {
+					new MenuItem ($"Open", "", () => Open()),
+				});
+
+			} else {
+				items = new MenuBarItem (new MenuItem [] {
+					new MenuItem ($"Save", "", () => Save(e.Tab)),
+					new MenuItem ($"Close", "", () => Close(e.Tab)),
+				});
+			}
+
+
+			var contextMenu = new ContextMenu (e.MouseEvent.X + 1, e.MouseEvent.Y + 1, items);
+
+			contextMenu.Show ();
+			e.MouseEvent.Handled = true;
 		}
 
 		private void New ()
@@ -85,7 +100,11 @@ namespace UICatalog.Scenarios {
 
 		private void Close ()
 		{
-			var tab = tabView.SelectedTab as OpenedFile;
+			Close (tabView.SelectedTab);
+		}
+		private void Close (TabView.Tab tabToClose)
+		{
+			var tab = tabToClose as OpenedFile;
 
 			if (tab == null) {
 				return;
@@ -109,12 +128,10 @@ namespace UICatalog.Scenarios {
 			// close and dispose the tab
 			tabView.RemoveTab (tab);
 			tab.View.Dispose ();
-
 		}
 
 		private void Open ()
 		{
-
 			var open = new OpenDialog ("Open", "Open a file") { AllowsMultipleSelection = true };
 
 			Application.Run (open);
@@ -130,7 +147,6 @@ namespace UICatalog.Scenarios {
 					Open (File.ReadAllText (path), new FileInfo (path), Path.GetFileName (path));
 				}
 			}
-
 		}
 
 		/// <summary>
@@ -140,7 +156,6 @@ namespace UICatalog.Scenarios {
 		/// <param name="fileInfo">File that was read or null if a new blank document</param>
 		private void Open (string initialText, FileInfo fileInfo, string tabName)
 		{
-
 			var textView = new TextView () {
 				X = 0,
 				Y = 0,
@@ -177,7 +192,11 @@ namespace UICatalog.Scenarios {
 
 		public void Save ()
 		{
-			var tab = tabView.SelectedTab as OpenedFile;
+			Save (tabView.SelectedTab);
+		}
+		public void Save (TabView.Tab tabToSave)
+		{
+			var tab = tabToSave as OpenedFile;
 
 			if (tab == null) {
 				return;
@@ -188,7 +207,7 @@ namespace UICatalog.Scenarios {
 			}
 
 			tab.Save ();
-
+			tabView.SetNeedsDisplay ();
 		}
 
 		public bool SaveAs ()
@@ -207,14 +226,13 @@ namespace UICatalog.Scenarios {
 			}
 
 			tab.File = new FileInfo (fd.FilePath.ToString ());
+			tab.Text = fd.FileName.ToString ();
 			tab.Save ();
 
 			return true;
 		}
 
 		private class OpenedFile : TabView.Tab {
-
-
 			public FileInfo File { get; set; }
 
 			/// <summary>

+ 2 - 2
UICatalog/Scenarios/ProgressBarStyles.cs

@@ -131,7 +131,7 @@ namespace UICatalog.Scenarios {
 				Application.MainLoop.Driver.Wakeup ();
 			}, null, 0, 300);
 
-			Top.Unloaded += Top_Unloaded;
+			Application.Top.Unloaded += Top_Unloaded;
 
 			void Top_Unloaded ()
 			{
@@ -143,7 +143,7 @@ namespace UICatalog.Scenarios {
 					_pulseTimer.Dispose ();
 					_pulseTimer = null;
 				}
-				Top.Unloaded -= Top_Unloaded;
+				Application.Top.Unloaded -= Top_Unloaded;
 			}
 		}
 	}

+ 76 - 0
UICatalog/Scenarios/RunTExample.cs

@@ -0,0 +1,76 @@
+using Terminal.Gui;
+
+namespace UICatalog.Scenarios {
+	[ScenarioMetadata (Name: "Run<T> Example", Description: "Illustrates using Application.Run<T> to run a custom class")]
+	[ScenarioCategory ("Top Level Windows")]
+	public class RunTExample : Scenario {
+		public override void Setup ()
+		{
+			// No need to call Init if Application.Run<T> is used
+		}
+
+		public override void Run ()
+		{
+			Application.Run<ExampleWindow> ();
+		}
+
+		public class ExampleWindow : Window {
+			public TextField usernameText;
+
+			public ExampleWindow ()
+			{
+				Title = "Example App (Ctrl+Q to quit)";
+
+				// Create input components and labels
+				var usernameLabel = new Label () {
+					Text = "Username:"
+				};
+
+				usernameText = new TextField ("") {
+					// Position text field adjacent to the label
+					X = Pos.Right (usernameLabel) + 1,
+
+					// Fill remaining horizontal space
+					Width = Dim.Fill (),
+				};
+
+				var passwordLabel = new Label () {
+					Text = "Password:",
+					X = Pos.Left (usernameLabel),
+					Y = Pos.Bottom (usernameLabel) + 1
+				};
+
+				var passwordText = new TextField ("") {
+					Secret = true,
+					// align with the text box above
+					X = Pos.Left (usernameText),
+					Y = Pos.Top (passwordLabel),
+					Width = Dim.Fill (),
+				};
+
+				// Create login button
+				var btnLogin = new Button () {
+					Text = "Login",
+					Y = Pos.Bottom (passwordLabel) + 1,
+					// center the login button horizontally
+					X = Pos.Center (),
+					IsDefault = true,
+				};
+
+				// When login button is clicked display a message popup
+				btnLogin.Clicked += () => {
+					if (usernameText.Text == "admin" && passwordText.Text == "password") {
+						MessageBox.Query ("Login Successful", $"Username: {usernameText.Text}", "Ok");
+						Application.RequestStop ();
+					} else {
+						MessageBox.ErrorQuery ("Error Logging In", "Incorrect username or password (hint: admin/password)", "Ok");
+					}
+				};
+
+				// Add the views to the Window
+				Add (usernameLabel, usernameText, passwordLabel, passwordText, btnLogin);
+			}
+		}
+
+	}
+}

+ 4 - 4
UICatalog/Scenarios/RuneWidthGreaterThanOne.cs

@@ -16,7 +16,7 @@ namespace UICatalog.Scenarios {
 		private Window _win;
 		private string _lastRunesUsed;
 
-		public override void Init (Toplevel top, ColorScheme colorScheme)
+		public override void Init (ColorScheme colorScheme)
 		{
 			Application.Init ();
 
@@ -38,19 +38,19 @@ namespace UICatalog.Scenarios {
 
 			_label = new Label () {
 				X = Pos.Center (),
-				Y = 0,
+				Y = 1,
 				ColorScheme = new ColorScheme () {
 					Normal = Colors.Base.Focus
 				}
 			};
 			_text = new TextField () {
 				X = Pos.Center (),
-				Y = 2,
+				Y = 3,
 				Width = 20
 			};
 			_button = new Button () {
 				X = Pos.Center (),
-				Y = 4
+				Y = 5
 			};
 			_labelR = new Label () {
 				X = Pos.AnchorEnd (30),

+ 33 - 20
UICatalog/Scenarios/Scrolling.cs

@@ -105,8 +105,8 @@ namespace UICatalog.Scenarios {
 		{
 			Win.X = 3;
 			Win.Y = 3;
-			Win.Width = Dim.Fill () - 3;
-			Win.Height = Dim.Fill () - 3;
+			Win.Width = Dim.Fill (3);
+			Win.Height = Dim.Fill (3);
 			var label = new Label ("ScrollView (new Rect (2, 2, 50, 20)) with a 200, 100 ContentSize...") {
 				X = 0,
 				Y = 0,
@@ -114,8 +114,12 @@ namespace UICatalog.Scenarios {
 			};
 			Win.Add (label);
 
-			// BUGBUG: ScrollView only supports Absolute Positioning (#72)
-			var scrollView = new ScrollView (new Rect (2, 2, 50, 20)) {
+			// FIXED: ScrollView only supports Absolute Positioning (#72)
+			var scrollView = new ScrollView {
+				X = 2,
+				Y = 2,
+				Width = 50,
+				Height = 20,
 				ColorScheme = Colors.TopLevel,
 				ContentSize = new Size (200, 100),
 				//ContentOffset = new Point (0, 0),
@@ -124,6 +128,7 @@ namespace UICatalog.Scenarios {
 			};
 
 			const string rule = "0123456789";
+
 			var horizontalRuler = new Label () {
 				X = 0,
 				Y = 0,
@@ -133,6 +138,7 @@ namespace UICatalog.Scenarios {
 				AutoSize = false
 			};
 			scrollView.Add (horizontalRuler);
+
 			const string vrule = "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n";
 
 			var verticalRuler = new Label () {
@@ -148,11 +154,11 @@ namespace UICatalog.Scenarios {
 			void Top_Loaded ()
 			{
 				horizontalRuler.Text = rule.Repeat ((int)Math.Ceiling ((double)(horizontalRuler.Bounds.Width) / (double)rule.Length)) [0..(horizontalRuler.Bounds.Width)] +
-				"\n" + "|         ".Repeat ((int)Math.Ceiling ((double)(horizontalRuler.Bounds.Width) / (double)rule.Length)) [0..(horizontalRuler.Bounds.Width)];
+					"\n" + "|         ".Repeat ((int)Math.Ceiling ((double)(horizontalRuler.Bounds.Width) / (double)rule.Length)) [0..(horizontalRuler.Bounds.Width)];
 				verticalRuler.Text = vrule.Repeat ((int)Math.Ceiling ((double)(verticalRuler.Bounds.Height * 2) / (double)rule.Length)) [0..(verticalRuler.Bounds.Height * 2)];
-				Top.Loaded -= Top_Loaded;
+				Application.Top.Loaded -= Top_Loaded;
 			}
-			Top.Loaded += Top_Loaded;
+			Application.Top.Loaded += Top_Loaded;
 
 			var pressMeButton = new Button ("Press me!") {
 				X = 3,
@@ -164,7 +170,7 @@ namespace UICatalog.Scenarios {
 			var aLongButton = new Button ("A very long button. Should be wide enough to demo clipping!") {
 				X = 3,
 				Y = 4,
-				Width = Dim.Fill (6),
+				Width = Dim.Fill (3),
 			};
 			aLongButton.Clicked += () => MessageBox.Query (20, 7, "MessageBox", "Neat?", "Yes", "No");
 			scrollView.Add (aLongButton);
@@ -206,6 +212,8 @@ namespace UICatalog.Scenarios {
 			};
 			scrollView.Add (anchorButton);
 
+			Win.Add (scrollView);
+
 			var hCheckBox = new CheckBox ("Horizontal Scrollbar", scrollView.ShowHorizontalScrollIndicator) {
 				X = Pos.X (scrollView),
 				Y = Pos.Bottom (scrollView) + 1,
@@ -265,6 +273,7 @@ namespace UICatalog.Scenarios {
 			scrollView2.DrawContent += (r) => {
 				scrollView2.ContentSize = filler.GetContentSize ();
 			};
+			Win.Add (scrollView2);
 
 			// This is just to debug the visuals of the scrollview when small
 			var scrollView3 = new ScrollView (new Rect (55, 15, 3, 3)) {
@@ -273,20 +282,26 @@ namespace UICatalog.Scenarios {
 				ShowHorizontalScrollIndicator = true
 			};
 			scrollView3.Add (new Box10x (0, 0));
+			Win.Add (scrollView3);
 
 			int count = 0;
-			var mousePos = new Label ("Mouse: ");
-			mousePos.X = Pos.Right (scrollView) + 1;
-			mousePos.Y = Pos.AnchorEnd (1);
-			mousePos.Width = 50;
+			var mousePos = new Label ("Mouse: ") {
+				X = Pos.Right (scrollView) + 1,
+				Y = Pos.AnchorEnd (1),
+				Width = 50,
+			};
+			Win.Add (mousePos);
 			Application.RootMouseEvent += delegate (MouseEvent me) {
 				mousePos.Text = $"Mouse: ({me.X},{me.Y}) - {me.Flags} {count++}";
 			};
 
-			var progress = new ProgressBar ();
-			progress.X = Pos.Right (scrollView) + 1;
-			progress.Y = Pos.AnchorEnd (2);
-			progress.Width = 50;
+			var progress = new ProgressBar {
+				X = Pos.Right (scrollView) + 1,
+				Y = Pos.AnchorEnd (2),
+				Width = 50
+			};
+			Win.Add (progress);
+
 			bool pulsing = true;
 			bool timer (MainLoop caller)
 			{
@@ -298,11 +313,9 @@ namespace UICatalog.Scenarios {
 			void Top_Unloaded ()
 			{
 				pulsing = false;
-				Top.Unloaded -= Top_Unloaded;
+				Application.Top.Unloaded -= Top_Unloaded;
 			}
-			Top.Unloaded += Top_Unloaded;
-
-			Win.Add (scrollView, scrollView2, scrollView3, mousePos, progress);
+			Application.Top.Unloaded += Top_Unloaded;
 		}
 	}
 }

+ 2 - 2
UICatalog/Scenarios/SingleBackgroundWorker.cs

@@ -11,11 +11,11 @@ namespace UICatalog.Scenarios {
 	public class SingleBackgroundWorker : Scenario {
 		public override void Run ()
 		{
-			Top.Dispose ();
+			Application.Top.Dispose ();
 
 			Application.Run<MainApp> ();
 
-			Top.Dispose ();
+			Application.Top.Dispose ();
 		}
 
 		public class MainApp : Toplevel {

+ 3 - 3
UICatalog/Scenarios/SyntaxHighlighting.cs

@@ -21,7 +21,7 @@ namespace UICatalog.Scenarios {
 			Win.Title = this.GetName ();
 			Win.Y = 1; // menu
 			Win.Height = Dim.Fill (1); // status bar
-			Top.LayoutSubviews ();
+			Application.Top.LayoutSubviews ();
 
 			var menu = new MenuBar (new MenuBarItem [] {
 			new MenuBarItem ("_File", new MenuItem [] {
@@ -29,7 +29,7 @@ namespace UICatalog.Scenarios {
 				new MenuItem ("_Quit", "", () => Quit()),
 			})
 			});
-			Top.Add (menu);
+			Application.Top.Add (menu);
 
 			textView = new SqlTextView () {
 				X = 0,
@@ -49,7 +49,7 @@ namespace UICatalog.Scenarios {
 			});
 
 
-			Top.Add (statusBar);
+			Application.Top.Add (statusBar);
 		}
 
 		private void WordWrap ()

+ 3 - 7
UICatalog/Scenarios/TabViewExample.cs

@@ -24,7 +24,7 @@ namespace UICatalog.Scenarios {
 			Win.Title = this.GetName ();
 			Win.Y = 1; // menu
 			Win.Height = Dim.Fill (1); // status bar
-			Top.LayoutSubviews ();
+			Application.Top.LayoutSubviews ();
 
 			var menu = new MenuBar (new MenuBarItem [] {
 				new MenuBarItem ("_File", new MenuItem [] {
@@ -50,7 +50,7 @@ namespace UICatalog.Scenarios {
 
 					})
 				});
-			Top.Add (menu);
+			Application.Top.Add (menu);
 
 			tabView = new TabView () {
 				X = 0,
@@ -85,7 +85,6 @@ namespace UICatalog.Scenarios {
 				Height = Dim.Fill (),
 			};
 
-
 			frameRight.Add (new TextView () {
 				Text = "This demos the tabs control\nSwitch between tabs using cursor keys",
 				Width = Dim.Fill (),
@@ -94,8 +93,6 @@ namespace UICatalog.Scenarios {
 
 			Win.Add (frameRight);
 
-
-
 			var frameBelow = new FrameView ("Bottom Frame") {
 				X = 0,
 				Y = Pos.Bottom (tabView),
@@ -103,7 +100,6 @@ namespace UICatalog.Scenarios {
 				Height = Dim.Fill (),
 			};
 
-
 			frameBelow.Add (new TextView () {
 				Text = "This frame exists to check you can still tab here\nand that the tab control doesn't overspill it's bounds",
 				Width = Dim.Fill (),
@@ -115,7 +111,7 @@ namespace UICatalog.Scenarios {
 			var statusBar = new StatusBar (new StatusItem [] {
 				new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()),
 			});
-			Top.Add (statusBar);
+			Application.Top.Add (statusBar);
 		}
 
 		private void AddBlankTab ()

+ 108 - 4
UICatalog/Scenarios/TableEditor.cs

@@ -38,7 +38,7 @@ namespace UICatalog.Scenarios {
 			Win.Title = this.GetName();
 			Win.Y = 1; // menu
 			Win.Height = Dim.Fill (1); // status bar
-			Top.LayoutSubviews ();
+			Application.Top.LayoutSubviews ();
 
 			this.tableView = new TableView () {
 				X = 0,
@@ -70,6 +70,7 @@ namespace UICatalog.Scenarios {
 					miAlternatingColors = new MenuItem ("Alternating Colors", "", () => ToggleAlternatingColors()){CheckType = MenuItemCheckStyle.Checked},
 					miCursor = new MenuItem ("Invert Selected Cell First Character", "", () => ToggleInvertSelectedCellFirstCharacter()){Checked = tableView.Style.InvertSelectedCellFirstCharacter,CheckType = MenuItemCheckStyle.Checked},
 					new MenuItem ("_ClearColumnStyles", "", () => ClearColumnStyles()),
+					new MenuItem ("Sho_w All Columns", "", ()=>ShowAllColumns())
 				}),
 				new MenuBarItem ("_Column", new MenuItem [] {
 					new MenuItem ("_Set Max Width", "", SetMaxWidth),
@@ -78,9 +79,9 @@ namespace UICatalog.Scenarios {
 					new MenuItem ("_Set All MinAcceptableWidth=1", "",SetMinAcceptableWidthToOne),
 				}),
 			});
-		
 
-		Top.Add (menu);
+
+			Application.Top.Add (menu);
 
 			var statusBar = new StatusBar (new StatusItem [] {
 				new StatusItem(Key.F2, "~F2~ OpenExample", () => OpenExample(true)),
@@ -88,7 +89,7 @@ namespace UICatalog.Scenarios {
 				new StatusItem(Key.F4, "~F4~ OpenSimple", () => OpenSimple(true)),
 				new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()),
 			});
-			Top.Add (statusBar);
+			Application.Top.Add (statusBar);
 
 			Win.Add (tableView);
 
@@ -130,8 +131,111 @@ namespace UICatalog.Scenarios {
 				Focus = Win.ColorScheme.Focus,
 				Normal = Application.Driver.MakeAttribute(Color.Red,Color.BrightBlue)
 			};
+
+			// if user clicks the mouse in TableView
+			tableView.MouseClick += e => {
+
+				tableView.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out DataColumn clickedCol);
+
+				if (clickedCol != null) {
+					if (e.MouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked)) {
+						
+						// left click in a header
+						SortColumn (clickedCol);
+					} else if (e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)) {
+
+						// right click in a header
+						ShowHeaderContextMenu (clickedCol, e);
+					}
+				}
+			};
 		}
 
+		private void ShowAllColumns ()
+		{
+			foreach(var colStyle in tableView.Style.ColumnStyles) {
+				colStyle.Value.Visible = true;
+			}
+			tableView.Update ();
+		}
+
+		private void SortColumn (DataColumn clickedCol)
+		{
+			var sort = GetProposedNewSortOrder (clickedCol, out var isAsc);
+
+			SortColumn (clickedCol, sort, isAsc);
+		}
+
+		private void SortColumn (DataColumn clickedCol, string sort, bool isAsc)
+		{
+			// set a sort order
+			tableView.Table.DefaultView.Sort = sort;
+
+			// copy the rows from the view
+			var sortedCopy = tableView.Table.DefaultView.ToTable ();
+			tableView.Table.Rows.Clear ();
+			foreach (DataRow r in sortedCopy.Rows) {
+				tableView.Table.ImportRow (r);
+			}
+
+			foreach (DataColumn col in tableView.Table.Columns) {
+
+				// remove any lingering sort indicator
+				col.ColumnName = TrimArrows(col.ColumnName);
+
+				// add a new one if this the one that is being sorted
+				if (col == clickedCol) {
+					col.ColumnName += isAsc ? '▲' : '▼';
+				}
+			}
+
+			tableView.Update ();
+		}
+
+		private string TrimArrows (string columnName)
+		{
+			return columnName.TrimEnd ('▼', '▲');
+		}
+		private string StripArrows (string columnName)
+		{
+			return columnName.Replace ("▼", "").Replace ("▲", "");
+		}
+		private string GetProposedNewSortOrder (DataColumn clickedCol, out bool isAsc)
+		{
+			// work out new sort order
+			var sort = tableView.Table.DefaultView.Sort;
+
+			if (sort?.EndsWith ("ASC") ?? false) {
+				sort = $"{clickedCol.ColumnName} DESC";
+				isAsc = false;
+			} else {
+				sort = $"{clickedCol.ColumnName} ASC";
+				isAsc = true;
+			}
+
+			return sort;
+		}
+
+		private void ShowHeaderContextMenu (DataColumn clickedCol, View.MouseEventArgs e)
+		{
+			var sort = GetProposedNewSortOrder (clickedCol, out var isAsc);
+
+			var contextMenu = new ContextMenu (e.MouseEvent.X + 1, e.MouseEvent.Y + 1,
+				new MenuBarItem (new MenuItem [] {
+					new MenuItem ($"Hide {TrimArrows(clickedCol.ColumnName)}", "", () => HideColumn(clickedCol)),
+					new MenuItem ($"Sort {StripArrows(sort)}","",()=>SortColumn(clickedCol,sort,isAsc)),
+				})
+			);
+
+			contextMenu.Show ();
+		}
+
+		private void HideColumn (DataColumn clickedCol)
+		{
+			var style = tableView.Style.GetOrCreateColumnStyle (clickedCol);
+			style.Visible = false;
+			tableView.Update ();
+		}
 
 		private DataColumn GetColumn ()
 		{

+ 84 - 33
UICatalog/Scenarios/Text.cs

@@ -1,5 +1,6 @@
 using NStack;
 using System;
+using System.IO;
 using System.Linq;
 using System.Text;
 using System.Text.RegularExpressions;
@@ -16,12 +17,12 @@ namespace UICatalog.Scenarios {
 	public class Text : Scenario {
 		public override void Setup ()
 		{
-			var s = "TAB to jump between text fields.";
-			var textField = new TextField (s) {
+			// TextField is a simple, single-line text input control
+			var textField = new TextField ("TextField with test text. Unicode shouldn't 𝔹Aℝ𝔽!") {
 				X = 1,
-				Y = 1,
-				Width = Dim.Percent (50),
-				//ColorScheme = Colors.Dialog
+				Y = 0,
+				Width = Dim.Percent (50) - 1,
+				Height = 2
 			};
 			textField.TextChanging += TextField_TextChanging;
 
@@ -36,7 +37,7 @@ namespace UICatalog.Scenarios {
 			var labelMirroringTextField = new Label (textField.Text) {
 				X = Pos.Right (textField) + 1,
 				Y = Pos.Top (textField),
-				Width = Dim.Fill (1)
+				Width = Dim.Fill (1) - 1
 			};
 			Win.Add (labelMirroringTextField);
 
@@ -44,15 +45,17 @@ namespace UICatalog.Scenarios {
 				labelMirroringTextField.Text = textField.Text;
 			};
 
+			// TextView is a rich (as in functionality, not formatting) text editing control
 			var textView = new TextView () {
 				X = 1,
-				Y = 3,
-				Width = Dim.Percent (50),
+				Y = Pos.Bottom (textField),
+				Width = Dim.Percent (50) - 1,
 				Height = Dim.Percent (30),
 			};
-			textView.Text = s;
+			textView.Text = "TextView with some more test text. Unicode shouldn't 𝔹Aℝ𝔽!" ;
 			textView.DrawContent += TextView_DrawContent;
 
+			// This shows how to enable autocomplete in TextView.
 			void TextView_DrawContent (Rect e)
 			{
 				textView.Autocomplete.AllSuggestions = Regex.Matches (textView.Text.ToString (), "\\w+")
@@ -61,40 +64,89 @@ namespace UICatalog.Scenarios {
 			}
 			Win.Add (textView);
 
-			var labelMirroringTextView = new Label (textView.Text) {
+			var labelMirroringTextView = new Label () {
 				X = Pos.Right (textView) + 1,
 				Y = Pos.Top (textView),
-				Width = Dim.Fill (1),
-				Height = Dim.Height (textView),
+				Width = Dim.Fill (1) - 1,
+				Height = Dim.Height (textView) - 1,
 			};
 			Win.Add (labelMirroringTextView);
 
-			textView.TextChanged += () => {
+			// Use ContentChanged to detect if the user has typed something in a TextView.
+			// The TextChanged property is only fired if the TextView.Text property is
+			// explicitly set
+			textView.ContentsChanged += (a) => {
+				labelMirroringTextView.Enabled = !labelMirroringTextView.Enabled;
 				labelMirroringTextView.Text = textView.Text;
 			};
 
-			var btnMultiline = new Button ("Toggle Multiline") {
-				X = Pos.Right (textView) + 1,
-				Y = Pos.Top (textView) + 1
+			// By default TextView is a multi-line control. It can be forced to 
+			// single-line mode.
+			var chxMultiline = new CheckBox ("Multiline") {
+				X = Pos.Left (textView),
+				Y = Pos.Bottom (textView), 
+				Checked = true
+			};
+			chxMultiline.Toggled += (b) => textView.Multiline = b;
+			Win.Add (chxMultiline);
+
+			var chxWordWrap = new CheckBox ("Word Wrap") {
+				X = Pos.Right (chxMultiline) + 2,
+				Y = Pos.Top (chxMultiline)
+			};
+			chxWordWrap.Toggled += (b) => textView.WordWrap = b;
+			Win.Add (chxWordWrap);
+
+			// TextView captures Tabs (so users can enter /t into text) by default;
+			// This means using Tab to navigate doesn't work by default. This shows
+			// how to turn tab capture off.
+			var chxCaptureTabs = new CheckBox ("Capture Tabs") {
+				X = Pos.Right (chxWordWrap) + 2,
+				Y = Pos.Top (chxWordWrap),
+				Checked = true
 			};
-			btnMultiline.Clicked += () => textView.Multiline = !textView.Multiline;
-			Win.Add (btnMultiline);
 
-			// BUGBUG: 531 - TAB doesn't go to next control from HexView
-			var hexView = new HexView (new System.IO.MemoryStream (Encoding.ASCII.GetBytes (s))) {
+			Key keyTab = textView.GetKeyFromCommand (Command.Tab);
+			Key keyBackTab = textView.GetKeyFromCommand (Command.BackTab);
+			chxCaptureTabs.Toggled += (b) => { 
+				if (b) {
+					textView.AddKeyBinding (keyTab, Command.Tab);
+					textView.AddKeyBinding (keyBackTab, Command.BackTab);
+				} else {
+					textView.ClearKeybinding (keyTab);
+					textView.ClearKeybinding (keyBackTab);
+				}
+				textView.WordWrap = b; 
+			};
+			Win.Add (chxCaptureTabs);
+
+			var hexEditor = new HexView (new MemoryStream (Encoding.UTF8.GetBytes ("HexEditor Unicode that shouldn't 𝔹Aℝ𝔽!"))) {
 				X = 1,
-				Y = Pos.Bottom (textView) + 1,
-				Width = Dim.Fill (1),
+				Y = Pos.Bottom (chxMultiline) + 1,
+				Width = Dim.Percent (50) - 1,
 				Height = Dim.Percent (30),
-				//ColorScheme = Colors.Dialog
 			};
-			Win.Add (hexView);
+			Win.Add (hexEditor);
+
+			var labelMirroringHexEditor = new Label () {
+				X = Pos.Right (hexEditor) + 1,
+				Y = Pos.Top (hexEditor),
+				Width = Dim.Fill (1) - 1,
+				Height = Dim.Height (hexEditor) - 1,
+			};
+			var array = ((MemoryStream)hexEditor.Source).ToArray ();
+			labelMirroringHexEditor.Text = Encoding.UTF8.GetString (array, 0, array.Length);
+			hexEditor.Edited += (kv) => {
+				hexEditor.ApplyEdits ();
+				var array = ((MemoryStream)hexEditor.Source).ToArray (); 
+				labelMirroringHexEditor.Text = Encoding.UTF8.GetString (array, 0, array.Length);
+			};
+			Win.Add (labelMirroringHexEditor);
 
 			var dateField = new DateField (System.DateTime.Now) {
 				X = 1,
-				Y = Pos.Bottom (hexView) + 1,
+				Y = Pos.Bottom (hexEditor) + 1,
 				Width = 20,
-				//ColorScheme = Colors.Dialog,
 				IsShortFormat = false
 			};
 			Win.Add (dateField);
@@ -113,9 +165,8 @@ namespace UICatalog.Scenarios {
 
 			_timeField = new TimeField (DateTime.Now.TimeOfDay) {
 				X = Pos.Right (labelMirroringDateField) + 5,
-				Y = Pos.Bottom (hexView) + 1,
+				Y = Pos.Bottom (hexEditor) + 1,
 				Width = 20,
-				//ColorScheme = Colors.Dialog,
 				IsShortFormat = false
 			};
 			Win.Add (_timeField);
@@ -130,8 +181,8 @@ namespace UICatalog.Scenarios {
 
 			_timeField.TimeChanged += TimeChanged;
 
-			// MaskedTextProvider
-			var netProviderLabel = new Label (".Net MaskedTextProvider [ 999 000 LLL >LLL| AAA aaa ]") {
+			// MaskedTextProvider - uses .NET MaskedTextProvider
+			var netProviderLabel = new Label ("NetMaskedTextProvider [ 999 000 LLL >LLL| AAA aaa ]") {
 				X = Pos.Left (dateField),
 				Y = Pos.Bottom (dateField) + 1
 			};
@@ -141,13 +192,13 @@ namespace UICatalog.Scenarios {
 
 			var netProviderField = new TextValidateField (netProvider) {
 				X = Pos.Right (netProviderLabel) + 1,
-				Y = Pos.Y (netProviderLabel)
+				Y = Pos.Y (netProviderLabel),
 			};
 
 			Win.Add (netProviderField);
 
-			// TextRegexProvider
-			var regexProvider = new Label ("Gui.cs TextRegexProvider [ ^([0-9]?[0-9]?[0-9]|1000)$ ]") {
+			// TextRegexProvider - Regex provider implemented by Terminal.Gui
+			var regexProvider = new Label ("TextRegexProvider [ ^([0-9]?[0-9]?[0-9]|1000)$ ]") {
 				X = Pos.Left (netProviderLabel),
 				Y = Pos.Bottom (netProviderLabel) + 1
 			};

+ 1 - 1
UICatalog/Scenarios/TextFormatterDemo.cs

@@ -42,7 +42,7 @@ namespace UICatalog.Scenarios {
 			blockText.Text = ustring.Make (block.ToString ()); // .Replace(" ", "\u00A0"); // \u00A0 is 'non-breaking space
 			Win.Add (blockText);
 
-			var unicodeCheckBox = new CheckBox ("Unicode", Top.HotKeySpecifier == (Rune)' ') {
+			var unicodeCheckBox = new CheckBox ("Unicode", Application.Top.HotKeySpecifier == (Rune)' ') {
 				X = 0,
 				Y = Pos.Bottom (blockText) + 1,
 			};

+ 2 - 2
UICatalog/Scenarios/TextViewAutocompletePopup.cs

@@ -33,7 +33,7 @@ namespace UICatalog.Scenarios {
 					new MenuItem ("_Quit", "", () => Quit())
 				})
 			});
-			Top.Add (menu);
+			Application.Top.Add (menu);
 
 			textViewTopLeft = new TextView () {
 				Width = width,
@@ -89,7 +89,7 @@ namespace UICatalog.Scenarios {
 				siMultiline = new StatusItem(Key.Null, "", null),
 				siWrap = new StatusItem(Key.Null, "", null)
 			});
-			Top.Add (statusBar);
+			Application.Top.Add (statusBar);
 
 			Win.LayoutStarted += Win_LayoutStarted;
 		}

+ 2 - 2
UICatalog/Scenarios/Threading.cs

@@ -96,9 +96,9 @@ namespace UICatalog.Scenarios {
 			void Top_Loaded ()
 			{
 				_btnActionCancel.SetFocus ();
-				Top.Loaded -= Top_Loaded;
+				Application.Top.Loaded -= Top_Loaded;
 			}
-			Top.Loaded += Top_Loaded;
+			Application.Top.Loaded += Top_Loaded;
 		}
 
 		private async void LoadData ()

+ 3 - 3
UICatalog/Scenarios/TreeUseCases.cs

@@ -17,7 +17,7 @@ namespace UICatalog.Scenarios {
 			Win.Title = this.GetName ();
 			Win.Y = 1; // menu
 			Win.Height = Dim.Fill (1); // status bar
-			Top.LayoutSubviews ();
+			Application.Top.LayoutSubviews ();
 
 			var menu = new MenuBar (new MenuBarItem [] {
 				new MenuBarItem ("_File", new MenuItem [] {
@@ -31,13 +31,13 @@ namespace UICatalog.Scenarios {
 				}),
 			});
 
-			Top.Add (menu);
+			Application.Top.Add (menu);
 
 			var statusBar = new StatusBar (new StatusItem [] {
 				new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()),
 			});
 
-			Top.Add (statusBar);
+			Application.Top.Add (statusBar);
 
 			// Start with the most basic use case
 			LoadSimpleNodes ();

+ 119 - 74
UICatalog/Scenarios/TreeViewFileSystem.cs

@@ -2,11 +2,12 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
+using System.Reflection.PortableExecutable;
 using Terminal.Gui;
 using Terminal.Gui.Trees;
 
 namespace UICatalog.Scenarios {
-	[ScenarioMetadata (Name: "TreeViewFileSystem", Description: "Hierarchical file system explorer based on TreeView.")]
+	[ScenarioMetadata (Name: "File System Explorer", Description: "Hierarchical file system explorer demonstrating TreeView.")]
 	[ScenarioCategory ("Controls"), ScenarioCategory ("TreeView"), ScenarioCategory ("Files and IO")]
 	public class TreeViewFileSystem : Scenario {
 
@@ -24,82 +25,97 @@ namespace UICatalog.Scenarios {
 		private MenuItem miUnicodeSymbols;
 		private MenuItem miFullPaths;
 		private MenuItem miLeaveLastRow;
+		private MenuItem miHighlightModelTextOnly;
 		private MenuItem miCustomColors;
-		private Terminal.Gui.Attribute green;
-		private Terminal.Gui.Attribute red;
+		private MenuItem miCursor;
+		private MenuItem miMultiSelect;
+
+		private DetailsFrame detailsFrame;
 
 		public override void Setup ()
 		{
 			Win.Title = this.GetName ();
 			Win.Y = 1; // menu
-			Win.Height = Dim.Fill (1); // status bar
-			Top.LayoutSubviews ();
+			Win.Height = Dim.Fill ();
+			Application.Top.LayoutSubviews ();
 
 			var menu = new MenuBar (new MenuBarItem [] {
 				new MenuBarItem ("_File", new MenuItem [] {
-					new MenuItem ("_Quit", "", () => Quit()),
+					new MenuItem ("_Quit", "CTRL-Q", () => Quit()),
 				}),
 				new MenuBarItem ("_View", new MenuItem [] {
-					miShowLines = new MenuItem ("_ShowLines", "", () => ShowLines()){
+					miFullPaths = new MenuItem ("_Full Paths", "", () => SetFullName()){Checked = false, CheckType = MenuItemCheckStyle.Checked},
+					miMultiSelect = new MenuItem ("_Multi Select", "", () => SetMultiSelect()){Checked = true, CheckType = MenuItemCheckStyle.Checked},
+				}),
+				new MenuBarItem ("_Style", new MenuItem [] {
+					miShowLines = new MenuItem ("_Show Lines", "", () => ShowLines()){
 					Checked = true, CheckType = MenuItemCheckStyle.Checked
 						},
 					null /*separator*/,
-					miPlusMinus = new MenuItem ("_PlusMinusSymbols", "", () => SetExpandableSymbols('+','-')){Checked = true, CheckType = MenuItemCheckStyle.Radio},
-					miArrowSymbols = new MenuItem ("_ArrowSymbols", "", () => SetExpandableSymbols('>','v')){Checked = false, CheckType = MenuItemCheckStyle.Radio},
-					miNoSymbols = new MenuItem ("_NoSymbols", "", () => SetExpandableSymbols(null,null)){Checked = false, CheckType = MenuItemCheckStyle.Radio},
-					miUnicodeSymbols = new MenuItem ("_Unicode", "", () => SetExpandableSymbols('ஹ','﷽')){Checked = false, CheckType = MenuItemCheckStyle.Radio},
+					miPlusMinus = new MenuItem ("_Plus Minus Symbols", "+ -", () => SetExpandableSymbols('+','-')){Checked = true, CheckType = MenuItemCheckStyle.Radio},
+					miArrowSymbols = new MenuItem ("_Arrow Symbols", "> v", () => SetExpandableSymbols('>','v')){Checked = false, CheckType = MenuItemCheckStyle.Radio},
+					miNoSymbols = new MenuItem ("_No Symbols", "", () => SetExpandableSymbols(null,null)){Checked = false, CheckType = MenuItemCheckStyle.Radio},
+					miUnicodeSymbols = new MenuItem ("_Unicode", "ஹ ﷽", () => SetExpandableSymbols('ஹ','﷽')){Checked = false, CheckType = MenuItemCheckStyle.Radio},
+					null /*separator*/,
+					miColoredSymbols = new MenuItem ("_Colored Symbols", "", () => ShowColoredExpandableSymbols()){Checked = false, CheckType = MenuItemCheckStyle.Checked},
+					miInvertSymbols = new MenuItem ("_Invert Symbols", "", () => InvertExpandableSymbols()){Checked = false, CheckType = MenuItemCheckStyle.Checked},
 					null /*separator*/,
-					miColoredSymbols = new MenuItem ("_ColoredSymbols", "", () => ShowColoredExpandableSymbols()){Checked = false, CheckType = MenuItemCheckStyle.Checked},
-					miInvertSymbols = new MenuItem ("_InvertSymbols", "", () => InvertExpandableSymbols()){Checked = false, CheckType = MenuItemCheckStyle.Checked},
-					miFullPaths = new MenuItem ("_FullPaths", "", () => SetFullName()){Checked = false, CheckType = MenuItemCheckStyle.Checked},
-					miLeaveLastRow = new MenuItem ("_LeaveLastRow", "", () => SetLeaveLastRow()){Checked = true, CheckType = MenuItemCheckStyle.Checked},
-					miCustomColors = new MenuItem ("C_ustomColors", "", () => SetCustomColors()){Checked = false, CheckType = MenuItemCheckStyle.Checked},
+					miLeaveLastRow = new MenuItem ("_Leave Last Row", "", () => SetLeaveLastRow()){Checked = true, CheckType = MenuItemCheckStyle.Checked},
+					miHighlightModelTextOnly = new MenuItem ("_Highlight Model Text Only", "", () => SetCheckHighlightModelTextOnly()){Checked = false, CheckType = MenuItemCheckStyle.Checked},
+					null /*separator*/,
+					miCustomColors = new MenuItem ("C_ustom Colors Hidden Files", "Yellow/Red", () => SetCustomColors()){Checked = false, CheckType = MenuItemCheckStyle.Checked},
+					null /*separator*/,
+					miCursor = new MenuItem ("Curs_or (MultiSelect only)", "", () => SetCursor()){Checked = false, CheckType = MenuItemCheckStyle.Checked},
 				}),
 			});
-			Top.Add (menu);
-
-			var statusBar = new StatusBar (new StatusItem [] {
-				new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()),
-			});
-			Top.Add (statusBar);
+			Application.Top.Add (menu);
 
-			var lblFiles = new Label ("File Tree:") {
+			treeViewFiles = new TreeView<FileSystemInfo> () {
 				X = 0,
-				Y = 1
+				Y = 0,
+				Width = Dim.Percent (50),
+				Height = Dim.Fill (),
 			};
-			Win.Add (lblFiles);
 
-			treeViewFiles = new TreeView<FileSystemInfo> () {
-				X = 0,
-				Y = Pos.Bottom (lblFiles),
+			detailsFrame = new DetailsFrame () {
+				X = Pos.Right (treeViewFiles),
+				Y = 0,
 				Width = Dim.Fill (),
 				Height = Dim.Fill (),
 			};
 
-			treeViewFiles.ObjectActivated += TreeViewFiles_ObjectActivated;
+			Win.Add (detailsFrame);
 			treeViewFiles.MouseClick += TreeViewFiles_MouseClick;
 			treeViewFiles.KeyPress += TreeViewFiles_KeyPress;
+			treeViewFiles.SelectionChanged += TreeViewFiles_SelectionChanged;
 
 			SetupFileTree ();
 
 			Win.Add (treeViewFiles);
+			treeViewFiles.GoToFirst ();
+			treeViewFiles.Expand ();
 
 			SetupScrollBar ();
 
-			green = Application.Driver.MakeAttribute (Color.Green, Color.Blue);
-			red = Application.Driver.MakeAttribute (Color.Red, Color.Blue);
+			treeViewFiles.SetFocus ();
+
+		}
+
+		private void TreeViewFiles_SelectionChanged (object sender, SelectionChangedEventArgs<FileSystemInfo> e)
+		{
+			ShowPropertiesOf (e.NewValue);
 		}
 
 		private void TreeViewFiles_KeyPress (View.KeyEventEventArgs obj)
 		{
-			if(obj.KeyEvent.Key == (Key.R | Key.CtrlMask)) {
+			if (obj.KeyEvent.Key == (Key.R | Key.CtrlMask)) {
 
 				var selected = treeViewFiles.SelectedObject;
-				
+
 				// nothing is selected
 				if (selected == null)
 					return;
-				
+
 				var location = treeViewFiles.GetObjectRow (selected);
 
 				//selected object is offscreen or somehow not found
@@ -116,9 +132,9 @@ namespace UICatalog.Scenarios {
 		private void TreeViewFiles_MouseClick (View.MouseEventArgs obj)
 		{
 			// if user right clicks
-			if (obj.MouseEvent.Flags.HasFlag(MouseFlags.Button3Clicked)) {
+			if (obj.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)) {
 
-				var rightClicked = treeViewFiles.GetObjectOnRow ( obj.MouseEvent.Y);
+				var rightClicked = treeViewFiles.GetObjectOnRow (obj.MouseEvent.Y);
 
 				// nothing was clicked
 				if (rightClicked == null)
@@ -137,33 +153,50 @@ namespace UICatalog.Scenarios {
 			menu.Position = screenPoint;
 
 			menu.MenuItems = new MenuBarItem (new [] { new MenuItem ("Properties", null, () => ShowPropertiesOf (forObject)) });
-			
-			Application.MainLoop.Invoke(menu.Show);
-		}
 
-		private void ShowPropertiesOf (FileSystemInfo fileSystemInfo)
-		{
-			if (fileSystemInfo is FileInfo f) {
-				System.Text.StringBuilder sb = new System.Text.StringBuilder ();
-				sb.AppendLine ($"Path:{f.DirectoryName}");
-				sb.AppendLine ($"Size:{f.Length:N0} bytes");
-				sb.AppendLine ($"Modified:{ f.LastWriteTime}");
-				sb.AppendLine ($"Created:{ f.CreationTime}");
-
-				MessageBox.Query (f.Name, sb.ToString (), "Close");
-			}
+			Application.MainLoop.Invoke (menu.Show);
+		}
 
-			if (fileSystemInfo is DirectoryInfo dir) {
+		class DetailsFrame : FrameView {
+			private FileSystemInfo fileInfo;
 
-				System.Text.StringBuilder sb = new System.Text.StringBuilder ();
-				sb.AppendLine ($"Path:{dir.Parent?.FullName}");
-				sb.AppendLine ($"Modified:{ dir.LastWriteTime}");
-				sb.AppendLine ($"Created:{ dir.CreationTime}");
+			public DetailsFrame ()
+			{
+				Title = "Details";
+				Visible = true;
+				CanFocus = true;				
+			}
 
-				MessageBox.Query (dir.Name, sb.ToString (), "Close");
+			public FileSystemInfo FileInfo {
+				get => fileInfo; set {
+					fileInfo = value;
+					System.Text.StringBuilder sb = null;
+					if (fileInfo is FileInfo f) {
+						Title = $"File: {f.Name}";
+						sb = new System.Text.StringBuilder ();
+						sb.AppendLine ($"Path:\n {f.FullName}\n");
+						sb.AppendLine ($"Size:\n {f.Length:N0} bytes\n");
+						sb.AppendLine ($"Modified:\n {f.LastWriteTime}\n");
+						sb.AppendLine ($"Created:\n {f.CreationTime}");
+					}
+
+					if (fileInfo is DirectoryInfo dir) {
+						Title = $"Directory: {dir.Name}";
+						sb = new System.Text.StringBuilder ();
+						sb.AppendLine ($"Path:\n {dir?.FullName}\n");
+						sb.AppendLine ($"Modified:\n {dir.LastWriteTime}\n");
+						sb.AppendLine ($"Created:\n {dir.CreationTime}\n");
+					}
+					Text = sb.ToString ();
+				}
 			}
 		}
 
+		private void ShowPropertiesOf (FileSystemInfo fileSystemInfo)
+		{
+			detailsFrame.FileInfo = fileSystemInfo;
+		}
+
 		private void SetupScrollBar ()
 		{
 			// When using scroll bar leave the last row of the control free (for over-rendering with scroll bar)
@@ -214,11 +247,6 @@ namespace UICatalog.Scenarios {
 			treeViewFiles.AddObjects (DriveInfo.GetDrives ().Select (d => d.RootDirectory));
 		}
 
-		private void TreeViewFiles_ObjectActivated (ObjectActivatedEventArgs<FileSystemInfo> obj)
-		{
-			ShowPropertiesOf (obj.ActivatedObject);
-		}
-
 		private void ShowLines ()
 		{
 			miShowLines.Checked = !miShowLines.Checked;
@@ -262,6 +290,7 @@ namespace UICatalog.Scenarios {
 			} else {
 				treeViewFiles.AspectGetter = (f) => f.Name;
 			}
+			treeViewFiles.SetNeedsDisplay ();
 		}
 
 		private void SetLeaveLastRow ()
@@ -269,29 +298,45 @@ namespace UICatalog.Scenarios {
 			miLeaveLastRow.Checked = !miLeaveLastRow.Checked;
 			treeViewFiles.Style.LeaveLastRow = miLeaveLastRow.Checked;
 		}
-		private void SetCustomColors()
+		private void SetCursor ()
 		{
-			var yellow = new ColorScheme
-			{
-				Focus = new Terminal.Gui.Attribute(Color.BrightYellow,treeViewFiles.ColorScheme.Focus.Background),
-				Normal = new Terminal.Gui.Attribute (Color.BrightYellow,treeViewFiles.ColorScheme.Normal.Background),
+			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 hidden = new ColorScheme {
+				Focus = new Terminal.Gui.Attribute (Color.BrightRed, treeViewFiles.ColorScheme.Focus.Background),
+				Normal = new Terminal.Gui.Attribute (Color.BrightYellow, treeViewFiles.ColorScheme.Normal.Background),
 			};
 
 			miCustomColors.Checked = !miCustomColors.Checked;
 
-			if(miCustomColors.Checked)
-			{
-				treeViewFiles.ColorGetter = (m)=>
-				{
-					return m is DirectoryInfo ? yellow : null;
+			if (miCustomColors.Checked) {
+				treeViewFiles.ColorGetter = (m) => {
+					if (m is DirectoryInfo && m.Attributes.HasFlag (FileAttributes.Hidden)) return hidden;
+					if (m is FileInfo && m.Attributes.HasFlag (FileAttributes.Hidden)) return hidden;
+					return null;
 				};
-			}
-			else
-			{
+			} else {
 				treeViewFiles.ColorGetter = null;
 			}
+			treeViewFiles.SetNeedsDisplay ();
 		}
 
+		private void SetCheckHighlightModelTextOnly ()
+		{
+			treeViewFiles.Style.HighlightModelTextOnly = !treeViewFiles.Style.HighlightModelTextOnly;
+			miHighlightModelTextOnly.Checked = treeViewFiles.Style.HighlightModelTextOnly;
+			treeViewFiles.SetNeedsDisplay ();
+		}
 
 		private IEnumerable<FileSystemInfo> GetChildren (FileSystemInfo model)
 		{

+ 3 - 3
UICatalog/Scenarios/Unicode.cs

@@ -34,14 +34,14 @@ namespace UICatalog.Scenarios {
 					new MenuItem ("_Paste", "", null)
 				})
 			});
-			Top.Add (menu);
+			Application.Top.Add (menu);
 
 			var statusBar = new StatusBar (new StatusItem [] {
 				new StatusItem (Key.CtrlMask | Key.Q, "~^Q~ Выход", () => Application.RequestStop()),
 				new StatusItem (Key.Unknown, "~F2~ Создать", null),
 				new StatusItem(Key.Unknown, "~F3~ Со_хранить", null),
 			});
-			Top.Add (statusBar);
+			Application.Top.Add (statusBar);
 
 			var label = new Label ("Label:") { X = 0, Y = 1 };
 			Win.Add (label);
@@ -97,7 +97,7 @@ namespace UICatalog.Scenarios {
 
 			label = new Label ("RadioGroup:") { X = Pos.X (label), Y = Pos.Bottom (listView) + 1 };
 			Win.Add (label);
-			var radioGroup = new RadioGroup (new ustring [] { "item #1", gitString, "Со_хранить" }, selected: 0) {
+			var radioGroup = new RadioGroup (new ustring [] { "item #1", gitString, "Со_хранить", "𝔽𝕆𝕆𝔹𝔸ℝ" }, selected: 0) {
 				X = 20,
 				Y = Pos.Y (label),
 				Width = Dim.Percent (60),

部分文件因文件數量過多而無法顯示