Browse Source

Merge branch 'splitcontainer' of https://github.com/tznind/gui.cs into splitcontainer

tznind 2 years ago
parent
commit
1a62121029
38 changed files with 1568 additions and 446 deletions
  1. 4 1
      Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs
  2. 8 3
      Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
  3. 7 1
      Terminal.Gui/ConsoleDrivers/NetDriver.cs
  4. 3 4
      Terminal.Gui/Core/ConsoleDriver.cs
  5. 8 14
      Terminal.Gui/Core/TextFormatter.cs
  6. 5 11
      Terminal.Gui/Core/Toplevel.cs
  7. 2 2
      Terminal.Gui/Core/View.cs
  8. 3 4
      Terminal.Gui/Core/Window.cs
  9. 1 1
      Terminal.Gui/Terminal.Gui.csproj
  10. 7 2
      Terminal.Gui/Views/ComboBox.cs
  11. 20 2
      Terminal.Gui/Views/Menu.cs
  12. 1 50
      Terminal.Gui/Views/StatusBar.cs
  13. 192 22
      Terminal.Gui/Views/TableView.cs
  14. 4 1
      Terminal.Gui/Views/TextField.cs
  15. 5 5
      Terminal.Gui/Views/TextView.cs
  16. 7 3
      Terminal.Gui/Windows/Dialog.cs
  17. 27 6
      Terminal.Gui/Windows/FileDialog.cs
  18. 1 1
      Terminal.Gui/Windows/MessageBox.cs
  19. 202 189
      UICatalog/Scenarios/CsvEditor.cs
  20. 1 1
      UICatalog/Scenarios/DynamicMenuBar.cs
  21. 1 1
      UICatalog/Scenarios/Editor.cs
  22. 3 3
      UICatalog/Scenarios/RuneWidthGreaterThanOne.cs
  23. 90 30
      UICatalog/Scenarios/TableEditor.cs
  24. 1 1
      UICatalog/Scenarios/Unicode.cs
  25. 2 2
      UICatalog/UICatalog.cs
  26. 3 0
      UICatalog/UICatalog.csproj
  27. 1 1
      UnitTests/ConsoleDriverTests.cs
  28. 83 21
      UnitTests/DialogTests.cs
  29. 99 1
      UnitTests/MenuTests.cs
  30. 3 4
      UnitTests/StatusBarTests.cs
  31. 325 0
      UnitTests/TableViewTests.cs
  32. 16 8
      UnitTests/TestHelpers.cs
  33. 15 0
      UnitTests/TextFieldTests.cs
  34. 104 8
      UnitTests/TextFormatterTests.cs
  35. 87 41
      UnitTests/TextViewTests.cs
  36. 143 1
      UnitTests/ToplevelTests.cs
  37. 1 1
      UnitTests/UnitTests.csproj
  38. 83 0
      UnitTests/WindowTests.cs

+ 4 - 1
Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs

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

+ 8 - 3
Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs

@@ -104,12 +104,12 @@ namespace Terminal.Gui {
 					needMove = false;
 					needMove = false;
 				}
 				}
 				if (runeWidth < 2 && ccol > 0
 				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)' ';
 					contents [crow, ccol - 1, 0] = (int)(uint)' ';
 
 
 				} else if (runeWidth < 2 && ccol <= Clip.Right - 1
 				} 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, 0] = (int)(uint)' ';
 					contents [crow, ccol + 1, 2] = 1;
 					contents [crow, ccol + 1, 2] = 1;
@@ -234,7 +234,12 @@ namespace Terminal.Gui {
 						if (color != redrawColor)
 						if (color != redrawColor)
 							SetColor (color);
 							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;
 						contents [row, col, 2] = 0;
 					}
 					}
 				}
 				}

+ 7 - 1
Terminal.Gui/ConsoleDrivers/NetDriver.cs

@@ -1483,7 +1483,13 @@ namespace Terminal.Gui {
 							output.Append (WriteAttributes (attr));
 							output.Append (WriteAttributes (attr));
 						}
 						}
 						outputWidth++;
 						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;
 						contents [row, col, 2] = 0;
 					}
 					}
 				}
 				}

+ 3 - 4
Terminal.Gui/Core/ConsoleDriver.cs

@@ -681,7 +681,7 @@ namespace Terminal.Gui {
 		/// <param name="col">Column to move the cursor to.</param>
 		/// <param name="col">Column to move the cursor to.</param>
 		/// <param name="row">Row to move the cursor to.</param>
 		/// <param name="row">Row to move the cursor to.</param>
 		public abstract void Move (int col, int row);
 		public abstract void Move (int col, int row);
-		
+
 		/// <summary>
 		/// <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>
 		/// </summary>
@@ -696,11 +696,10 @@ namespace Terminal.Gui {
 		/// <returns></returns>
 		/// <returns></returns>
 		public static Rune MakePrintable (Rune c)
 		public static Rune MakePrintable (Rune c)
 		{
 		{
-			var controlChars = c & 0xFFFF;
-			if (controlChars <= 0x1F || controlChars >= 0X7F && controlChars <= 0x9F) {
+			if (c <= 0x1F || (c >= 0X7F && c <= 0x9F)) {
 				// ASCII (C0) control characters.
 				// ASCII (C0) control characters.
 				// C1 control characters (https://www.aivosto.com/articles/control-characters.html#c1)
 				// C1 control characters (https://www.aivosto.com/articles/control-characters.html#c1)
-				return new Rune (controlChars + 0x2400);
+				return new Rune (c + 0x2400);
 			}
 			}
 
 
 			return c;
 			return c;

+ 8 - 14
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>
 		/// <summary>
 		/// Gets the cursor position from <see cref="HotKey"/>. If the <see cref="HotKey"/> is defined, the cursor will be positioned over it.
 		/// Gets the cursor position from <see cref="HotKey"/>. If the <see cref="HotKey"/> is defined, the cursor will be positioned over it.
 		/// </summary>
 		/// </summary>
@@ -317,8 +311,9 @@ namespace Terminal.Gui {
 			get {
 			get {
 				// With this check, we protect against subclasses with overrides of Text
 				// With this check, we protect against subclasses with overrides of Text
 				if (ustring.IsNullOrEmpty (Text) || Size.IsEmpty) {
 				if (ustring.IsNullOrEmpty (Text) || Size.IsEmpty) {
-					lines = new List<ustring> ();
-					lines.Add (ustring.Empty);
+					lines = new List<ustring> {
+						ustring.Empty
+					};
 					NeedsFormat = false;
 					NeedsFormat = false;
 					return lines;
 					return lines;
 				}
 				}
@@ -716,7 +711,7 @@ namespace Terminal.Gui {
 		}
 		}
 
 
 		static char [] whitespace = new char [] { ' ', '\t' };
 		static char [] whitespace = new char [] { ' ', '\t' };
-		private int hotKeyPos;
+		private int hotKeyPos = -1;
 
 
 		/// <summary>
 		/// <summary>
 		/// Reformats text into lines, applying text alignment and optionally wrapping text to new lines on word boundaries.
 		/// 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>
 		/// <returns>The text with the hotkey tagged.</returns>
 		/// <remarks>
 		/// <remarks>
 		/// The returned string will not render correctly without first un-doing the tag. To undo the tag, search for 
 		/// 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>
 		/// </remarks>
 		public ustring ReplaceHotKeyWithTag (ustring text, int hotPos)
 		public ustring ReplaceHotKeyWithTag (ustring text, int hotPos)
 		{
 		{
 			// Set the high bit
 			// Set the high bit
 			var runes = text.ToRuneList ();
 			var runes = text.ToRuneList ();
 			if (Rune.IsLetterOrNumber (runes [hotPos])) {
 			if (Rune.IsLetterOrNumber (runes [hotPos])) {
-				runes [hotPos] = new Rune ((uint)runes [hotPos] | HotKeyTagMask);
+				runes [hotPos] = new Rune ((uint)runes [hotPos]);
 			}
 			}
 			return ustring.Make (runes);
 			return ustring.Make (runes);
 		}
 		}
@@ -1297,13 +1291,13 @@ namespace Terminal.Gui {
 							rune = runes [idx];
 							rune = runes [idx];
 						}
 						}
 					}
 					}
-					if ((rune & HotKeyTagMask) == HotKeyTagMask) {
+					if (idx == HotKeyPos) {
 						if ((isVertical && textVerticalAlignment == VerticalTextAlignment.Justified) ||
 						if ((isVertical && textVerticalAlignment == VerticalTextAlignment.Justified) ||
-						    (!isVertical && textAlignment == TextAlignment.Justified)) {
+						(!isVertical && textAlignment == TextAlignment.Justified)) {
 							CursorPosition = idx - start;
 							CursorPosition = idx - start;
 						}
 						}
 						Application.Driver?.SetAttribute (hotColor);
 						Application.Driver?.SetAttribute (hotColor);
-						Application.Driver?.AddRune ((Rune)((uint)rune & ~HotKeyTagMask));
+						Application.Driver?.AddRune (rune);
 						Application.Driver?.SetAttribute (normalColor);
 						Application.Driver?.SetAttribute (normalColor);
 					} else {
 					} else {
 						Application.Driver?.AddRune (rune);
 						Application.Driver?.AddRune (rune);

+ 5 - 11
Terminal.Gui/Core/Toplevel.cs

@@ -613,11 +613,8 @@ namespace Terminal.Gui {
 			}
 			}
 			nx = Math.Max (x, 0);
 			nx = Math.Max (x, 0);
 			nx = nx + top.Frame.Width > l ? Math.Max (l - top.Frame.Width, 0) : nx;
 			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);
+			if (nx + (top.Border != null && top.Border.DrawMarginFrame ? 2 : 1) > top.Frame.X + top.Frame.Width) {
+				nx = Math.Max (top.Frame.Right - (top.Border.DrawMarginFrame ? 2 : 1), 0);
 			}
 			}
 			//System.Diagnostics.Debug.WriteLine ($"nx:{nx}, rWidth:{rWidth}");
 			//System.Diagnostics.Debug.WriteLine ($"nx:{nx}, rWidth:{rWidth}");
 			bool m, s;
 			bool m, s;
@@ -656,11 +653,8 @@ namespace Terminal.Gui {
 			}
 			}
 			ny = Math.Min (ny, l);
 			ny = Math.Min (ny, l);
 			ny = ny + top.Frame.Height >= l ? Math.Max (l - top.Frame.Height, m ? 1 : 0) : ny;
 			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 + (top.Border != null && top.Border.DrawMarginFrame ? 2 : 1) > top.Frame.Y + top.Frame.Height) {
+				ny = Math.Max (top.Frame.Bottom - (top.Border.DrawMarginFrame ? 2 : 1), 0);
 			}
 			}
 			//System.Diagnostics.Debug.WriteLine ($"ny:{ny}, rHeight:{rHeight}");
 			//System.Diagnostics.Debug.WriteLine ($"ny:{ny}, rHeight:{rHeight}");
 
 
@@ -701,7 +695,7 @@ namespace Terminal.Gui {
 			}
 			}
 
 
 			if (sb != null && ny + top.Frame.Height != superView.Frame.Height - (sb.Visible ? 1 : 0)
 			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);
 				top.Height = Dim.Fill (sb.Visible ? 1 : 0);
 				layoutSubviews = true;
 				layoutSubviews = true;

+ 2 - 2
Terminal.Gui/Core/View.cs

@@ -987,13 +987,13 @@ namespace Terminal.Gui {
 			if (view == null || subviews == null)
 			if (view == null || subviews == null)
 				return;
 				return;
 
 
-			SetNeedsLayout ();
-			SetNeedsDisplay ();
 			var touched = view.Frame;
 			var touched = view.Frame;
 			subviews.Remove (view);
 			subviews.Remove (view);
 			tabIndexes.Remove (view);
 			tabIndexes.Remove (view);
 			view.container = null;
 			view.container = null;
 			view.tabIndex = -1;
 			view.tabIndex = -1;
+			SetNeedsLayout ();
+			SetNeedsDisplay ();
 			if (subviews.Count < 1) {
 			if (subviews.Count < 1) {
 				CanFocus = false;
 				CanFocus = false;
 			}
 			}

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

@@ -277,18 +277,17 @@ namespace Terminal.Gui {
 		{
 		{
 			var padding = Border.GetSumThickness ();
 			var padding = Border.GetSumThickness ();
 			var scrRect = ViewToScreen (new Rect (0, 0, Frame.Width, Frame.Height));
 			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 ());
 				Driver.SetAttribute (GetNormalColor ());
 				Clear ();
 				Clear ();
+				contentView.SetNeedsDisplay ();
 			}
 			}
 			var savedClip = contentView.ClipToBounds ();
 			var savedClip = contentView.ClipToBounds ();
 
 
 			// Redraw our contentView
 			// Redraw our contentView
 			// DONE: smartly constrict contentView.Bounds to just be what intersects with the 'bounds' we were passed
 			// 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;
 			Driver.Clip = savedClip;
 
 
 			ClearLayoutNeeded ();
 			ClearLayoutNeeded ();

+ 1 - 1
Terminal.Gui/Terminal.Gui.csproj

@@ -17,7 +17,7 @@
   </PropertyGroup>
   </PropertyGroup>
   <ItemGroup>
   <ItemGroup>
     <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
     <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
-    <PackageReference Include="NStack.Core" Version="1.0.5" />
+    <PackageReference Include="NStack.Core" Version="1.0.7" />
     <InternalsVisibleTo Include="UnitTests" />
     <InternalsVisibleTo Include="UnitTests" />
   </ItemGroup>
   </ItemGroup>
   <!-- Uncomment the RestoreSources element to have dotnet restore pull NStack from a local dir for testing -->
   <!-- Uncomment the RestoreSources element to have dotnet restore pull NStack from a local dir for testing -->

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

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

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

@@ -690,6 +690,7 @@ namespace Terminal.Gui {
 				}
 				}
 			} while (barItems.Children [current] == null || disabled);
 			} while (barItems.Children [current] == null || disabled);
 			SetNeedsDisplay ();
 			SetNeedsDisplay ();
+			SetParentSetNeedsDisplay ();
 			if (!host.UseSubMenusSingleFrame)
 			if (!host.UseSubMenusSingleFrame)
 				host.OnMenuOpened ();
 				host.OnMenuOpened ();
 			return true;
 			return true;
@@ -737,11 +738,24 @@ namespace Terminal.Gui {
 				}
 				}
 			} while (barItems.Children [current] == null || disabled);
 			} while (barItems.Children [current] == null || disabled);
 			SetNeedsDisplay ();
 			SetNeedsDisplay ();
+			SetParentSetNeedsDisplay ();
 			if (!host.UseSubMenusSingleFrame)
 			if (!host.UseSubMenusSingleFrame)
 				host.OnMenuOpened ();
 				host.OnMenuOpened ();
 			return true;
 			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)
 		public override bool MouseEvent (MouseEvent me)
 		{
 		{
 			if (!host.handled && !host.HandleGrabView (me, this)) {
 			if (!host.handled && !host.HandleGrabView (me, this)) {
@@ -778,6 +792,7 @@ namespace Terminal.Gui {
 					current = me.Y - 1;
 					current = me.Y - 1;
 				if (host.UseSubMenusSingleFrame || !CheckSubMenu ()) {
 				if (host.UseSubMenusSingleFrame || !CheckSubMenu ()) {
 					SetNeedsDisplay ();
 					SetNeedsDisplay ();
+					SetParentSetNeedsDisplay ();
 					return true;
 					return true;
 				}
 				}
 				host.OnMenuOpened ();
 				host.OnMenuOpened ();
@@ -806,6 +821,7 @@ namespace Terminal.Gui {
 				return host.CloseMenu (false, true);
 				return host.CloseMenu (false, true);
 			} else {
 			} else {
 				SetNeedsDisplay ();
 				SetNeedsDisplay ();
+				SetParentSetNeedsDisplay ();
 			}
 			}
 			return true;
 			return true;
 		}
 		}
@@ -1182,7 +1198,9 @@ namespace Terminal.Gui {
 		public virtual void OnMenuOpened ()
 		public virtual void OnMenuOpened ()
 		{
 		{
 			MenuItem mi = null;
 			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];
 				mi = openCurrentMenu.barItems.Children [openCurrentMenu.current];
 			} else if (openCurrentMenu.barItems.IsTopLevel) {
 			} else if (openCurrentMenu.barItems.IsTopLevel) {
 				mi = openCurrentMenu.barItems;
 				mi = openCurrentMenu.barItems;
@@ -1587,7 +1605,7 @@ namespace Terminal.Gui {
 					var subMenu = openCurrentMenu.current > -1 && openCurrentMenu.barItems.Children.Length > 0
 					var subMenu = openCurrentMenu.current > -1 && openCurrentMenu.barItems.Children.Length > 0
 						? openCurrentMenu.barItems.SubMenu (openCurrentMenu.barItems.Children [openCurrentMenu.current])
 						? openCurrentMenu.barItems.SubMenu (openCurrentMenu.barItems.Children [openCurrentMenu.current])
 						: null;
 						: 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))
 						if (openSubMenu != null && !CloseMenu (false, true))
 							return;
 							return;
 						NextMenu (false, ignoreUseSubMenusSingleFrame);
 						NextMenu (false, ignoreUseSubMenusSingleFrame);

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

@@ -70,8 +70,6 @@ namespace Terminal.Gui {
 	/// So for each context must be a new instance of a statusbar.
 	/// So for each context must be a new instance of a statusbar.
 	/// </summary>
 	/// </summary>
 	public class StatusBar : View {
 	public class StatusBar : View {
-		bool disposedValue;
-
 		/// <summary>
 		/// <summary>
 		/// The items that compose the <see cref="StatusBar"/>
 		/// The items that compose the <see cref="StatusBar"/>
 		/// </summary>
 		/// </summary>
@@ -93,39 +91,9 @@ namespace Terminal.Gui {
 			CanFocus = false;
 			CanFocus = false;
 			ColorScheme = Colors.Menu;
 			ColorScheme = Colors.Menu;
 			X = 0;
 			X = 0;
+			Y = Pos.AnchorEnd (1);
 			Width = Dim.Fill ();
 			Width = Dim.Fill ();
 			Height = 1;
 			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 = "-";
 		static ustring shortcutDelimiter = "-";
@@ -151,12 +119,6 @@ namespace Terminal.Gui {
 		///<inheritdoc/>
 		///<inheritdoc/>
 		public override void Redraw (Rect bounds)
 		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);
 			Move (0, 0);
 			Driver.SetAttribute (GetNormalColor ());
 			Driver.SetAttribute (GetNormalColor ());
 			for (int i = 0; i < Frame.Width; i++)
 			for (int i = 0; i < Frame.Width; i++)
@@ -234,17 +196,6 @@ namespace Terminal.Gui {
 			});
 			});
 		}
 		}
 
 
-		/// <inheritdoc/>
-		protected override void Dispose (bool disposing)
-		{
-			if (!disposedValue) {
-				if (disposing) {
-					Application.Resized -= Application_Resized ();
-				}
-				disposedValue = true;
-			}
-		}
-
 		///<inheritdoc/>
 		///<inheritdoc/>
 		public override bool OnEnter (View view)
 		public override bool OnEnter (View view)
 		{
 		{

+ 192 - 22
Terminal.Gui/Views/TableView.cs

@@ -109,7 +109,7 @@ namespace Terminal.Gui {
 			get => columnOffset;
 			get => columnOffset;
 
 
 			//try to prevent this being set to an out of bounds column
 			//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>
 		/// <summary>
@@ -117,7 +117,7 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// </summary>
 		public int RowOffset {
 		public int RowOffset {
 			get => 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>
 		/// <summary>
@@ -130,7 +130,7 @@ namespace Terminal.Gui {
 				var oldValue = selectedColumn;
 				var oldValue = selectedColumn;
 
 
 				//try to prevent this being set to an out of bounds column
 				//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)
 				if (oldValue != selectedColumn)
 					OnSelectedCellChanged (new SelectedCellChangedEventArgs (Table, oldValue, SelectedColumn, SelectedRow, SelectedRow));
 					OnSelectedCellChanged (new SelectedCellChangedEventArgs (Table, oldValue, SelectedColumn, SelectedRow, SelectedRow));
@@ -146,7 +146,7 @@ namespace Terminal.Gui {
 
 
 				var oldValue = selectedRow;
 				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)
 				if (oldValue != selectedRow)
 					OnSelectedCellChanged (new SelectedCellChangedEventArgs (Table, SelectedColumn, SelectedColumn, oldValue, selectedRow));
 					OnSelectedCellChanged (new SelectedCellChangedEventArgs (Table, SelectedColumn, SelectedColumn, oldValue, selectedRow));
@@ -315,7 +315,7 @@ namespace Terminal.Gui {
 				var rowToRender = RowOffset + (line - headerLinesConsumed);
 				var rowToRender = RowOffset + (line - headerLinesConsumed);
 
 
 				//if we have run off the end of the table
 				//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;
 					continue;
 
 
 				RenderRow (line, rowToRender, columnsToRender);
 				RenderRow (line, rowToRender, columnsToRender);
@@ -427,6 +427,36 @@ namespace Terminal.Gui {
 
 
 		private void RenderHeaderUnderline (int row, int availableWidth, ColumnToRender [] columnsToRender)
 		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:
 			// 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
 				// whole way but update to instead draw a header indicator
 				// or scroll arrow etc
 				// or scroll arrow etc
 				var rune = Driver.HLine;
 				var rune = Driver.HLine;
-
+				
 				if (Style.ShowVerticalHeaderLines) {
 				if (Style.ShowVerticalHeaderLines) {
 					if (c == 0) {
 					if (c == 0) {
 						// for first character render line
 						// for first character render line
@@ -445,7 +475,7 @@ namespace Terminal.Gui {
 						// unless we have horizontally scrolled along
 						// unless we have horizontally scrolled along
 						// in which case render an arrow, to indicate user
 						// in which case render an arrow, to indicate user
 						// can scroll left
 						// can scroll left
-						if(Style.ShowHorizontalScrollIndicators && ColumnOffset > 0)
+						if(Style.ShowHorizontalScrollIndicators && moreColumnsToLeft)
 						{
 						{
 							rune = Driver.LeftArrow;
 							rune = Driver.LeftArrow;
 							scrollLeftPoint = new Point(c,row);
 							scrollLeftPoint = new Point(c,row);
@@ -465,8 +495,7 @@ namespace Terminal.Gui {
 						// unless there is more of the table we could horizontally
 						// unless there is more of the table we could horizontally
 						// scroll along to see. In which case render an arrow,
 						// scroll along to see. In which case render an arrow,
 						// to indicate user can scroll right
 						// to indicate user can scroll right
-						if(Style.ShowHorizontalScrollIndicators &&
-							ColumnOffset + columnsToRender.Length < Table.Columns.Count)
+						if(Style.ShowHorizontalScrollIndicators && moreColumnsToRight)
 						{
 						{
 							rune = Driver.RightArrow;
 							rune = Driver.RightArrow;
 							scrollRightPoint = new Point(c,row);
 							scrollRightPoint = new Point(c,row);
@@ -683,7 +712,7 @@ namespace Terminal.Gui {
 		/// <inheritdoc/>
 		/// <inheritdoc/>
 		public override bool ProcessKey (KeyEvent keyEvent)
 		public override bool ProcessKey (KeyEvent keyEvent)
 		{
 		{
-			if (Table == null || Table.Columns.Count <= 0) {
+			if (TableIsNullOrInvisible ()) {
 				PositionCursor ();
 				PositionCursor ();
 				return false;
 				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>
 		/// <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)
 		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)
 			if (!MultiSelect || !extendExistingSelection)
 				MultiSelectedRegions.Clear ();
 				MultiSelectedRegions.Clear ();
 
 
@@ -804,7 +839,7 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// </summary>
 		public void SelectAll ()
 		public void SelectAll ()
 		{
 		{
-			if (Table == null || !MultiSelect || Table.Rows.Count == 0)
+			if (TableIsNullOrInvisible() || !MultiSelect || Table.Rows.Count == 0)
 				return;
 				return;
 
 
 			MultiSelectedRegions.Clear ();
 			MultiSelectedRegions.Clear ();
@@ -820,7 +855,7 @@ namespace Terminal.Gui {
 		/// <returns></returns>
 		/// <returns></returns>
 		public IEnumerable<Point> GetAllSelectedCells ()
 		public IEnumerable<Point> GetAllSelectedCells ()
 		{
 		{
-			if (Table == null || Table.Rows.Count == 0)
+			if (TableIsNullOrInvisible () || Table.Rows.Count == 0)
 				yield break;
 				yield break;
 
 
 			EnsureValidSelection ();
 			EnsureValidSelection ();
@@ -880,13 +915,20 @@ namespace Terminal.Gui {
 		}
 		}
 
 
 		/// <summary>
 		/// <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"/>)
+		/// <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>
 		/// </summary>
 		/// <param name="col"></param>
 		/// <param name="col"></param>
 		/// <param name="row"></param>
 		/// <param name="row"></param>
 		/// <returns></returns>
 		/// <returns></returns>
 		public bool IsSelected (int col, int row)
 		public bool IsSelected (int col, int row)
 		{
 		{
+			if(!IsColumnVisible(col)) {
+				return false;
+			}	
+
 			// Cell is also selected if in any multi selection region
 			// Cell is also selected if in any multi selection region
 			if (MultiSelect && MultiSelectedRegions.Any (r => r.Rect.Contains (col, row)))
 			if (MultiSelect && MultiSelectedRegions.Any (r => r.Rect.Contains (col, row)))
 				return true;
 				return true;
@@ -899,12 +941,28 @@ namespace Terminal.Gui {
 					(col == SelectedColumn || FullRowSelect);
 					(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>
 		/// <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
 		/// 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>
 		/// </summary>
 		public override void PositionCursor ()
 		public override void PositionCursor ()
 		{
 		{
-			if (Table == null) {
+			if (TableIsNullOrInvisible ()) {
 				base.PositionCursor ();
 				base.PositionCursor ();
 				return;
 				return;
 			}
 			}
@@ -927,7 +985,7 @@ namespace Terminal.Gui {
 				SetFocus ();
 				SetFocus ();
 			}
 			}
 
 
-			if (Table == null || Table.Columns.Count <= 0) {
+			if (TableIsNullOrInvisible ()) {
 				return false;
 				return false;
 			}
 			}
 
 
@@ -1018,7 +1076,7 @@ namespace Terminal.Gui {
 		{
 		{
 			headerIfAny = null;
 			headerIfAny = null;
 
 
-			if (Table == null || Table.Columns.Count <= 0)
+			if (TableIsNullOrInvisible ())
 				return null;
 				return null;
 
 
 			var viewPort = CalculateViewport (Bounds);
 			var viewPort = CalculateViewport (Bounds);
@@ -1058,7 +1116,7 @@ namespace Terminal.Gui {
 		/// <returns></returns>
 		/// <returns></returns>
 		public Point? CellToScreen (int tableColumn, int tableRow)
 		public Point? CellToScreen (int tableColumn, int tableRow)
 		{
 		{
-			if (Table == null || Table.Columns.Count <= 0)
+			if (TableIsNullOrInvisible ())
 				return null;
 				return null;
 
 
 			var viewPort = CalculateViewport (Bounds);
 			var viewPort = CalculateViewport (Bounds);
@@ -1087,7 +1145,7 @@ namespace Terminal.Gui {
 		/// <remarks>This always calls <see cref="View.SetNeedsDisplay()"/></remarks>
 		/// <remarks>This always calls <see cref="View.SetNeedsDisplay()"/></remarks>
 		public void Update ()
 		public void Update ()
 		{
 		{
-			if (Table == null) {
+			if (TableIsNullOrInvisible ()) {
 				SetNeedsDisplay ();
 				SetNeedsDisplay ();
 				return;
 				return;
 			}
 			}
@@ -1106,7 +1164,7 @@ namespace Terminal.Gui {
 		/// <remarks>Changes will not be immediately visible in the display until you call <see cref="View.SetNeedsDisplay()"/></remarks>
 		/// <remarks>Changes will not be immediately visible in the display until you call <see cref="View.SetNeedsDisplay()"/></remarks>
 		public void EnsureValidScrollOffsets ()
 		public void EnsureValidScrollOffsets ()
 		{
 		{
-			if (Table == null) {
+			if (TableIsNullOrInvisible ()) {
 				return;
 				return;
 			}
 			}
 
 
@@ -1121,7 +1179,7 @@ namespace Terminal.Gui {
 		/// <remarks>Changes will not be immediately visible in the display until you call <see cref="View.SetNeedsDisplay()"/></remarks>
 		/// <remarks>Changes will not be immediately visible in the display until you call <see cref="View.SetNeedsDisplay()"/></remarks>
 		public void EnsureValidSelection ()
 		public void EnsureValidSelection ()
 		{
 		{
-			if (Table == null) {
+			if (TableIsNullOrInvisible()) {
 
 
 				// Table doesn't exist, we should probably clear those selections
 				// Table doesn't exist, we should probably clear those selections
 				MultiSelectedRegions.Clear ();
 				MultiSelectedRegions.Clear ();
@@ -1131,6 +1189,9 @@ namespace Terminal.Gui {
 			SelectedColumn = Math.Max (Math.Min (SelectedColumn, Table.Columns.Count - 1), 0);
 			SelectedColumn = Math.Max (Math.Min (SelectedColumn, Table.Columns.Count - 1), 0);
 			SelectedRow = Math.Max (Math.Min (SelectedRow, Table.Rows.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 ();
 			var oldRegions = MultiSelectedRegions.ToArray ().Reverse ();
 
 
 			MultiSelectedRegions.Clear ();
 			MultiSelectedRegions.Clear ();
@@ -1159,7 +1220,100 @@ namespace Terminal.Gui {
 
 
 				MultiSelectedRegions.Push (region);
 				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>
 		/// <summary>
@@ -1239,7 +1393,7 @@ namespace Terminal.Gui {
 		/// <returns></returns>
 		/// <returns></returns>
 		private IEnumerable<ColumnToRender> CalculateViewport (Rect bounds, int padding = 1)
 		private IEnumerable<ColumnToRender> CalculateViewport (Rect bounds, int padding = 1)
 		{
 		{
-			if (Table == null || Table.Columns.Count <= 0)
+			if (TableIsNullOrInvisible ())
 				yield break;
 				yield break;
 
 
 			int usedSpace = 0;
 			int usedSpace = 0;
@@ -1264,6 +1418,12 @@ namespace Terminal.Gui {
 				var colStyle = Style.GetColumnStyleIfAny (col);
 				var colStyle = Style.GetColumnStyleIfAny (col);
 				int colWidth;
 				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)?
 				// is there enough space for this column (and it's data)?
 				colWidth = CalculateMaxCellWidth (col, rowsToRender, colStyle) + padding;
 				colWidth = CalculateMaxCellWidth (col, rowsToRender, colStyle) + padding;
 
 
@@ -1311,7 +1471,7 @@ namespace Terminal.Gui {
 
 
 		private bool ShouldRenderHeaders ()
 		private bool ShouldRenderHeaders ()
 		{
 		{
-			if (Table == null || Table.Columns.Count == 0)
+			if (TableIsNullOrInvisible ())
 				return false;
 				return false;
 
 
 			return Style.AlwaysShowHeaders || rowOffset == 0;
 			return Style.AlwaysShowHeaders || rowOffset == 0;
@@ -1419,6 +1579,7 @@ namespace Terminal.Gui {
 			/// Return null for the default
 			/// Return null for the default
 			/// </summary>
 			/// </summary>
 			public CellColorGetterDelegate ColorGetter;
 			public CellColorGetterDelegate ColorGetter;
+			private bool visible = true;
 
 
 			/// <summary>
 			/// <summary>
 			/// Defines the format for values e.g. "yyyy-MM-dd" for dates
 			/// Defines the format for values e.g. "yyyy-MM-dd" for dates
@@ -1449,6 +1610,15 @@ namespace Terminal.Gui {
 			/// </summary>
 			/// </summary>
 			public int MinAcceptableWidth { get; set; } = DefaultMinAcceptableWidth;
 			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>
 			/// <summary>
 			/// Returns the alignment for the cell based on <paramref name="cellValue"/> and <see cref="AlignmentGetter"/>/<see cref="Alignment"/>
 			/// Returns the alignment for the cell based on <paramref name="cellValue"/> and <see cref="AlignmentGetter"/>/<see cref="Alignment"/>
 			/// </summary>
 			/// </summary>

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

@@ -235,7 +235,10 @@ namespace Terminal.Gui {
 
 
 		private void HistoryText_ChangeText (HistoryText.HistoryTextItem obj)
 		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;
 			CursorPosition = obj.CursorPosition.X;
 			Adjust ();
 			Adjust ();
 		}
 		}

+ 5 - 5
Terminal.Gui/Views/TextView.cs

@@ -851,7 +851,7 @@ namespace Terminal.Gui {
 			var firstLine = wrappedModelLines.IndexOf (r => r.ModelLine == modelLine);
 			var firstLine = wrappedModelLines.IndexOf (r => r.ModelLine == modelLine);
 			int modelCol = 0;
 			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];
 				var wLine = wrappedModelLines [i];
 
 
 				if (i < line) {
 				if (i < line) {
@@ -1165,7 +1165,7 @@ namespace Terminal.Gui {
 		/// Unlike the <see cref="TextChanged"/> event, this event is raised whenever the user types or
 		/// Unlike the <see cref="TextChanged"/> event, this event is raised whenever the user types or
 		/// otherwise changes the contents of the <see cref="TextView"/>.
 		/// otherwise changes the contents of the <see cref="TextView"/>.
 		/// </remarks>
 		/// </remarks>
-		public Action<ContentsChangedEventArgs> ContentsChanged;
+		public event Action<ContentsChangedEventArgs> ContentsChanged;
 
 
 		/// <summary>
 		/// <summary>
 		/// Invoked with the unwrapped <see cref="CursorPosition"/>.
 		/// Invoked with the unwrapped <see cref="CursorPosition"/>.
@@ -1432,7 +1432,7 @@ namespace Terminal.Gui {
 			}
 			}
 
 
 			UpdateWrapModel ();
 			UpdateWrapModel ();
-			
+
 			Adjust ();
 			Adjust ();
 			OnContentsChanged ();
 			OnContentsChanged ();
 		}
 		}
@@ -1830,6 +1830,7 @@ namespace Terminal.Gui {
 			try {
 			try {
 				SetWrapModel ();
 				SetWrapModel ();
 				res = model.LoadFile (path);
 				res = model.LoadFile (path);
+				historyText.Clear (Text);
 				ResetPosition ();
 				ResetPosition ();
 			} catch (Exception) {
 			} catch (Exception) {
 				throw;
 				throw;
@@ -1837,7 +1838,6 @@ namespace Terminal.Gui {
 				UpdateWrapModel ();
 				UpdateWrapModel ();
 				SetNeedsDisplay ();
 				SetNeedsDisplay ();
 				Adjust ();
 				Adjust ();
-				OnContentsChanged ();
 			}
 			}
 			return res;
 			return res;
 		}
 		}
@@ -1850,9 +1850,9 @@ namespace Terminal.Gui {
 		public void LoadStream (Stream stream)
 		public void LoadStream (Stream stream)
 		{
 		{
 			model.LoadStream (stream);
 			model.LoadStream (stream);
+			historyText.Clear (Text);
 			ResetPosition ();
 			ResetPosition ();
 			SetNeedsDisplay ();
 			SetNeedsDisplay ();
-			OnContentsChanged ();
 		}
 		}
 
 
 		/// <summary>
 		/// <summary>

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

+ 27 - 6
Terminal.Gui/Windows/FileDialog.cs

@@ -662,12 +662,17 @@ namespace Terminal.Gui {
 				X = Pos.Right (nameEntry) + 2,
 				X = Pos.Right (nameEntry) + 2,
 				Y = Pos.Top (nameEntry),
 				Y = Pos.Top (nameEntry),
 				Width = Dim.Fill (1),
 				Width = Dim.Fill (1),
-				Height = allowedTypes != null ? allowedTypes.Count + 1 : 1,
+				Height = SetComboBoxHeight (allowedTypes),
 				Text = allowedTypes?.Count > 0 ? allowedTypes [0] : string.Empty,
 				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.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);
 			Add (cmbAllowedTypes);
 
 
 			dirListView = new DirListView (this) {
 			dirListView = new DirListView (this) {
@@ -679,7 +684,7 @@ namespace Terminal.Gui {
 			DirectoryPath = Path.GetFullPath (Environment.CurrentDirectory);
 			DirectoryPath = Path.GetFullPath (Environment.CurrentDirectory);
 			Add (dirListView);
 			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.DirectoryChanged = (dir) => { nameEntry.Text = ustring.Empty; dirEntry.Text = dir; };
 			dirListView.FileChanged = (file) => nameEntry.Text = file == ".." ? "" : file;
 			dirListView.FileChanged = (file) => nameEntry.Text = file == ".." ? "" : file;
 			dirListView.SelectedChanged = (file) => nameEntry.Text = file.Item1 == ".." ? "" : file.Item1;
 			dirListView.SelectedChanged = (file) => nameEntry.Text = file.Item1 == ".." ? "" : file.Item1;
@@ -744,6 +749,11 @@ namespace Terminal.Gui {
 			}
 			}
 		}
 		}
 
 
+		private static int SetComboBoxHeight (List<string> allowedTypes)
+		{
+			return allowedTypes != null ? Math.Min (allowedTypes.Count + 1, 8) : 8;
+		}
+
 		internal bool canceled;
 		internal bool canceled;
 
 
 		///<inheritdoc/>
 		///<inheritdoc/>
@@ -827,13 +837,24 @@ namespace Terminal.Gui {
 			}
 			}
 		}
 		}
 
 
+		private string [] allowedFileTypes;
+
 		/// <summary>
 		/// <summary>
 		/// The array of filename extensions allowed, or null if all file extensions are allowed.
 		/// The array of filename extensions allowed, or null if all file extensions are allowed.
 		/// </summary>
 		/// </summary>
 		/// <value>The allowed file types.</value>
 		/// <value>The allowed file types.</value>
 		public string [] AllowedFileTypes {
 		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>
 		/// <summary>

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

@@ -303,7 +303,7 @@ namespace Terminal.Gui {
 
 
 			if (width == 0 & height == 0) {
 			if (width == 0 & height == 0) {
 				// Dynamically size Width
 				// 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
 			// Setup actions

+ 202 - 189
UICatalog/Scenarios/CsvEditor.cs

@@ -9,6 +9,7 @@ using System.IO;
 using System.Text;
 using System.Text;
 using NStack;
 using NStack;
 using System.Text.RegularExpressions;
 using System.Text.RegularExpressions;
+using CsvHelper;
 
 
 namespace UICatalog.Scenarios {
 namespace UICatalog.Scenarios {
 
 
@@ -20,8 +21,7 @@ namespace UICatalog.Scenarios {
 	[ScenarioCategory ("Dialogs")]
 	[ScenarioCategory ("Dialogs")]
 	[ScenarioCategory ("Top Level Windows")]
 	[ScenarioCategory ("Top Level Windows")]
 	[ScenarioCategory ("Files and IO")]
 	[ScenarioCategory ("Files and IO")]
-	public class CsvEditor : Scenario 
-	{
+	public class CsvEditor : Scenario {
 		TableView tableView;
 		TableView tableView;
 		private string currentFile;
 		private string currentFile;
 		private MenuItem miLeft;
 		private MenuItem miLeft;
@@ -31,7 +31,7 @@ namespace UICatalog.Scenarios {
 
 
 		public override void Setup ()
 		public override void Setup ()
 		{
 		{
-			Win.Title = this.GetName();
+			Win.Title = this.GetName ();
 			Win.Y = 1; // menu
 			Win.Y = 1; // menu
 			Win.Height = Dim.Fill (1); // status bar
 			Win.Height = Dim.Fill (1); // status bar
 			Application.Top.LayoutSubviews ();
 			Application.Top.LayoutSubviews ();
@@ -81,22 +81,22 @@ namespace UICatalog.Scenarios {
 
 
 			Win.Add (tableView);
 			Win.Add (tableView);
 
 
-			selectedCellLabel = new TextField(){
+			selectedCellLabel = new TextField () {
 				X = 0,
 				X = 0,
-				Y = Pos.Bottom(tableView),
+				Y = Pos.Bottom (tableView),
 				Text = "0,0",
 				Text = "0,0",
-				Width = Dim.Fill(),
-				TextAlignment = TextAlignment.Right				
+				Width = Dim.Fill (),
+				TextAlignment = TextAlignment.Right
 			};
 			};
 			selectedCellLabel.TextChanged += SelectedCellLabel_TextChanged;
 			selectedCellLabel.TextChanged += SelectedCellLabel_TextChanged;
 
 
-			Win.Add(selectedCellLabel);
+			Win.Add (selectedCellLabel);
 
 
 			tableView.SelectedCellChanged += OnSelectedCellChanged;
 			tableView.SelectedCellChanged += OnSelectedCellChanged;
 			tableView.CellActivated += EditCurrentCell;
 			tableView.CellActivated += EditCurrentCell;
 			tableView.KeyPress += TableViewKeyPress;
 			tableView.KeyPress += TableViewKeyPress;
 
 
-			SetupScrollBar();
+			SetupScrollBar ();
 		}
 		}
 
 
 		private void SelectedCellLabel_TextChanged (ustring last)
 		private void SelectedCellLabel_TextChanged (ustring last)
@@ -104,10 +104,10 @@ namespace UICatalog.Scenarios {
 			// if user is in the text control and editing the selected cell
 			// if user is in the text control and editing the selected cell
 			if (!selectedCellLabel.HasFocus)
 			if (!selectedCellLabel.HasFocus)
 				return;
 				return;
-			
+
 			// change selected cell to the one the user has typed into the box
 			// 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.SelectedColumn = int.Parse (match.Groups [1].Value);
 				tableView.SelectedRow = int.Parse (match.Groups [2].Value);
 				tableView.SelectedRow = int.Parse (match.Groups [2].Value);
@@ -119,149 +119,147 @@ namespace UICatalog.Scenarios {
 			// only update the text box if the user is not manually editing it
 			// only update the text box if the user is not manually editing it
 			if (!selectedCellLabel.HasFocus)
 			if (!selectedCellLabel.HasFocus)
 				selectedCellLabel.Text = $"{tableView.SelectedRow},{tableView.SelectedColumn}";
 				selectedCellLabel.Text = $"{tableView.SelectedRow},{tableView.SelectedColumn}";
-			
-			if(tableView.Table == null || tableView.SelectedColumn == -1)
+
+			if (tableView.Table == null || tableView.SelectedColumn == -1)
 				return;
 				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;
 			miLeft.Checked = style?.Alignment == TextAlignment.Left;
 			miRight.Checked = style?.Alignment == TextAlignment.Right;
 			miRight.Checked = style?.Alignment == TextAlignment.Right;
-			miCentered.Checked = style?.Alignment == TextAlignment.Centered;			
+			miCentered.Checked = style?.Alignment == TextAlignment.Centered;
 		}
 		}
 
 
 		private void RenameColumn ()
 		private void RenameColumn ()
 		{
 		{
-			if(NoTableLoaded()) {
+			if (NoTableLoaded ()) {
 				return;
 				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;
 				currentCol.ColumnName = newName;
-				tableView.Update();
+				tableView.Update ();
 			}
 			}
 		}
 		}
 
 
-		private void DeleteColum()
+		private void DeleteColum ()
 		{
 		{
-			if(NoTableLoaded()) {
+			if (NoTableLoaded ()) {
 				return;
 				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;
 				return;
 			}
 			}
 
 
 
 
 			try {
 			try {
-				tableView.Table.Columns.RemoveAt(tableView.SelectedColumn);
-				tableView.Update();
+				tableView.Table.Columns.RemoveAt (tableView.SelectedColumn);
+				tableView.Update ();
 
 
 			} catch (Exception ex) {
 			} catch (Exception ex) {
-				MessageBox.ErrorQuery("Could not remove column",ex.Message, "Ok");
+				MessageBox.ErrorQuery ("Could not remove column", ex.Message, "Ok");
 			}
 			}
 		}
 		}
 
 
 		private void MoveColumn ()
 		private void MoveColumn ()
 		{
 		{
-			if(NoTableLoaded()) {
+			if (NoTableLoaded ()) {
 				return;
 				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;
 				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)
 		private void Sort (bool asc)
 		{
 		{
 
 
-			if(NoTableLoaded()) {
+			if (NoTableLoaded ()) {
 				return;
 				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;
 				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.DefaultView.Sort = colName + (asc ? " asc" : " desc");
-			tableView.Table = tableView.Table.DefaultView.ToTable();
+			tableView.Table = tableView.Table.DefaultView.ToTable ();
 		}
 		}
 
 
 		private void MoveRow ()
 		private void MoveRow ()
 		{
 		{
-			if(NoTableLoaded()) {
+			if (NoTableLoaded ()) {
 				return;
 				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;
 				return;
 			}
 			}
-			
-			try{
+
+			try {
 
 
 				int oldIdx = tableView.SelectedRow;
 				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;
 						return;
 
 
 					var arrayItems = currentRow.ItemArray;
 					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
 					// 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;
 					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");
 			}
 			}
 		}
 		}
 
 
@@ -271,43 +269,43 @@ namespace UICatalog.Scenarios {
 				return;
 				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;
 			style.Alignment = newAlignment;
 
 
 			miLeft.Checked = style.Alignment == TextAlignment.Left;
 			miLeft.Checked = style.Alignment == TextAlignment.Left;
 			miRight.Checked = style.Alignment == TextAlignment.Right;
 			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 ()) {
 			if (NoTableLoaded ()) {
 				return;
 				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;
 				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;
 				style.Format = newPattern;
-				tableView.Update();
+				tableView.Update ();
 			}
 			}
 		}
 		}
 
 
 		private bool NoTableLoaded ()
 		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;
 				return true;
 			}
 			}
 
 
@@ -316,112 +314,132 @@ namespace UICatalog.Scenarios {
 
 
 		private void AddRow ()
 		private void AddRow ()
 		{
 		{
-			if(NoTableLoaded()) {
+			if (NoTableLoaded ()) {
 				return;
 				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 ()
 		private void AddColumn ()
 		{
 		{
-			if(NoTableLoaded()) {
+			if (NoTableLoaded ()) {
 				return;
 				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);
 
 
-				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"});
+				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)
+				if (result <= -1 || result >= 4)
 					return;
 					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;
 				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;
 			int lineNumber = 0;
 			currentFile = null;
 			currentFile = null;
 
 
+			using var reader = new CsvReader (File.OpenText (filename), CultureInfo.InvariantCulture);
+
 			try {
 			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++;
 					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;
 				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;
 				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 ()
 		private void SetupScrollBar ()
@@ -445,45 +463,42 @@ namespace UICatalog.Scenarios {
 			};*/
 			};*/
 
 
 			tableView.DrawContent += (e) => {
 			tableView.DrawContent += (e) => {
-				_scrollBar.Size = tableView.Table?.Rows?.Count ??0;
+				_scrollBar.Size = tableView.Table?.Rows?.Count ?? 0;
 				_scrollBar.Position = tableView.RowOffset;
 				_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 ();
 				_scrollBar.Refresh ();
 			};
 			};
-		
+
 		}
 		}
 
 
 		private void TableViewKeyPress (View.KeyEventEventArgs e)
 		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
 					// 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
 					// 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;
 				e.Handled = true;
 			}
 			}
 		}
 		}
 
 
 		private void ClearColumnStyles ()
 		private void ClearColumnStyles ()
 		{
 		{
-			tableView.Style.ColumnStyles.Clear();
-			tableView.Update();
+			tableView.Style.ColumnStyles.Clear ();
+			tableView.Update ();
 		}
 		}
-			
+
 
 
 		private void CloseExample ()
 		private void CloseExample ()
 		{
 		{
@@ -494,7 +509,7 @@ namespace UICatalog.Scenarios {
 		{
 		{
 			Application.RequestStop ();
 			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;
 			bool okPressed = false;
 
 
@@ -504,44 +519,42 @@ namespace UICatalog.Scenarios {
 			cancel.Clicked += () => { Application.RequestStop (); };
 			cancel.Clicked += () => { Application.RequestStop (); };
 			var d = new Dialog (title, 60, 20, ok, cancel);
 			var d = new Dialog (title, 60, 20, ok, cancel);
 
 
-			var lbl = new Label() {
+			var lbl = new Label () {
 				X = 0,
 				X = 0,
 				Y = 1,
 				Y = 1,
 				Text = label
 				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);
 			Application.Run (d);
 
 
-			enteredText = okPressed? tf.Text.ToString() : null;
+			enteredText = okPressed ? tf.Text.ToString () : null;
 			return okPressed;
 			return okPressed;
 		}
 		}
 		private void EditCurrentCell (TableView.CellActivatedEventArgs e)
 		private void EditCurrentCell (TableView.CellActivatedEventArgs e)
 		{
 		{
-			if(e.Table == null)
+			if (e.Table == null)
 				return;
 				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 {
 				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 ();
 			}
 			}
 		}
 		}
 	}
 	}

+ 1 - 1
UICatalog/Scenarios/DynamicMenuBar.cs

@@ -143,7 +143,7 @@ namespace UICatalog.Scenarios {
 					TextAlignment = TextAlignment.Centered,
 					TextAlignment = TextAlignment.Centered,
 					X = Pos.Right (_btnPrevious) + 1,
 					X = Pos.Right (_btnPrevious) + 1,
 					Y = Pos.Top (_btnPrevious),
 					Y = Pos.Top (_btnPrevious),
-					Width = Dim.Fill () - Dim.Width (_btnAdd) - 1,
+					Width = Dim.Fill () - Dim.Function (() => _btnAdd.Frame.Width + 1),
 					Height = 1
 					Height = 1
 				};
 				};
 				_frmMenu.Add (_lblMenuBar);
 				_frmMenu.Add (_lblMenuBar);

+ 1 - 1
UICatalog/Scenarios/Editor.cs

@@ -365,7 +365,7 @@ namespace UICatalog.Scenarios {
 			if (!CanCloseFile ()) {
 			if (!CanCloseFile ()) {
 				return;
 				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 };
 			var d = new OpenDialog ("Open", "Choose the path where to open the file.", aTypes) { AllowsMultipleSelection = false };
 			Application.Run (d);
 			Application.Run (d);
 
 

+ 3 - 3
UICatalog/Scenarios/RuneWidthGreaterThanOne.cs

@@ -38,19 +38,19 @@ namespace UICatalog.Scenarios {
 
 
 			_label = new Label () {
 			_label = new Label () {
 				X = Pos.Center (),
 				X = Pos.Center (),
-				Y = 0,
+				Y = 1,
 				ColorScheme = new ColorScheme () {
 				ColorScheme = new ColorScheme () {
 					Normal = Colors.Base.Focus
 					Normal = Colors.Base.Focus
 				}
 				}
 			};
 			};
 			_text = new TextField () {
 			_text = new TextField () {
 				X = Pos.Center (),
 				X = Pos.Center (),
-				Y = 2,
+				Y = 3,
 				Width = 20
 				Width = 20
 			};
 			};
 			_button = new Button () {
 			_button = new Button () {
 				X = Pos.Center (),
 				X = Pos.Center (),
-				Y = 4
+				Y = 5
 			};
 			};
 			_labelR = new Label () {
 			_labelR = new Label () {
 				X = Pos.AnchorEnd (30),
 				X = Pos.AnchorEnd (30),

+ 90 - 30
UICatalog/Scenarios/TableEditor.cs

@@ -70,6 +70,7 @@ namespace UICatalog.Scenarios {
 					miAlternatingColors = new MenuItem ("Alternating Colors", "", () => ToggleAlternatingColors()){CheckType = MenuItemCheckStyle.Checked},
 					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},
 					miCursor = new MenuItem ("Invert Selected Cell First Character", "", () => ToggleInvertSelectedCellFirstCharacter()){Checked = tableView.Style.InvertSelectedCellFirstCharacter,CheckType = MenuItemCheckStyle.Checked},
 					new MenuItem ("_ClearColumnStyles", "", () => ClearColumnStyles()),
 					new MenuItem ("_ClearColumnStyles", "", () => ClearColumnStyles()),
+					new MenuItem ("Sho_w All Columns", "", ()=>ShowAllColumns())
 				}),
 				}),
 				new MenuBarItem ("_Column", new MenuItem [] {
 				new MenuBarItem ("_Column", new MenuItem [] {
 					new MenuItem ("_Set Max Width", "", SetMaxWidth),
 					new MenuItem ("_Set Max Width", "", SetMaxWidth),
@@ -137,45 +138,104 @@ namespace UICatalog.Scenarios {
 				tableView.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out DataColumn clickedCol);
 				tableView.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out DataColumn clickedCol);
 
 
 				if (clickedCol != null) {
 				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)) {
 
 
-					// work out new sort order
-					var sort = tableView.Table.DefaultView.Sort;
-					bool isAsc;
-
-					if(sort?.EndsWith("ASC") ?? false) {
-						sort = $"{clickedCol.ColumnName} DESC";
-						isAsc = false;
-					} else {
-						sort = $"{clickedCol.ColumnName} ASC";
-						isAsc = true;
-					}
-					
-					// 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);
+						// right click in a header
+						ShowHeaderContextMenu (clickedCol, e);
 					}
 					}
+				}
+			};
+		}
 
 
-					foreach(DataColumn col in tableView.Table.Columns) {
+		private void ShowAllColumns ()
+		{
+			foreach(var colStyle in tableView.Style.ColumnStyles) {
+				colStyle.Value.Visible = true;
+			}
+			tableView.Update ();
+		}
 
 
-						// remove any lingering sort indicator
-						col.ColumnName = col.ColumnName.TrimEnd ('▼', '▲');
+		private void SortColumn (DataColumn clickedCol)
+		{
+			var sort = GetProposedNewSortOrder (clickedCol, out var isAsc);
 
 
-						// add a new one if this the one that is being sorted
-						if (col == clickedCol) {
-							col.ColumnName += 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);
 
 
-					tableView.Update ();
+				// 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 ()
 		private DataColumn GetColumn ()
 		{
 		{

+ 1 - 1
UICatalog/Scenarios/Unicode.cs

@@ -97,7 +97,7 @@ namespace UICatalog.Scenarios {
 
 
 			label = new Label ("RadioGroup:") { X = Pos.X (label), Y = Pos.Bottom (listView) + 1 };
 			label = new Label ("RadioGroup:") { X = Pos.X (label), Y = Pos.Bottom (listView) + 1 };
 			Win.Add (label);
 			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,
 				X = 20,
 				Y = Pos.Y (label),
 				Y = Pos.Y (label),
 				Width = Dim.Percent (60),
 				Width = Dim.Percent (60),

+ 2 - 2
UICatalog/UICatalog.cs

@@ -150,12 +150,12 @@ namespace UICatalog {
 		class UICatalogTopLevel : Toplevel {
 		class UICatalogTopLevel : Toplevel {
 			public MenuItem miIsMouseDisabled;
 			public MenuItem miIsMouseDisabled;
 			public MenuItem miHeightAsBuffer;
 			public MenuItem miHeightAsBuffer;
-	    
+
 			public FrameView LeftPane;
 			public FrameView LeftPane;
 			public ListView CategoryListView;
 			public ListView CategoryListView;
 			public FrameView RightPane;
 			public FrameView RightPane;
 			public ListView ScenarioListView;
 			public ListView ScenarioListView;
-	    
+
 			public StatusItem Capslock;
 			public StatusItem Capslock;
 			public StatusItem Numlock;
 			public StatusItem Numlock;
 			public StatusItem Scrolllock;
 			public StatusItem Scrolllock;

+ 3 - 0
UICatalog/UICatalog.csproj

@@ -18,6 +18,9 @@
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
     <DefineConstants>TRACE;DEBUG_IDISPOSABLE</DefineConstants>
     <DefineConstants>TRACE;DEBUG_IDISPOSABLE</DefineConstants>
   </PropertyGroup>
   </PropertyGroup>
+  <ItemGroup>
+    <PackageReference Include="CsvHelper" Version="30.0.1" />
+  </ItemGroup>
   <ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\Terminal.Gui\Terminal.Gui.csproj" />
     <ProjectReference Include="..\Terminal.Gui\Terminal.Gui.csproj" />
   </ItemGroup>
   </ItemGroup>

+ 1 - 1
UnitTests/ConsoleDriverTests.cs

@@ -614,7 +614,7 @@ namespace Terminal.Gui.ConsoleDrivers {
 		[InlineData (0x0000001F, 0x241F)]
 		[InlineData (0x0000001F, 0x241F)]
 		[InlineData (0x0000007F, 0x247F)]
 		[InlineData (0x0000007F, 0x247F)]
 		[InlineData (0x0000009F, 0x249F)]
 		[InlineData (0x0000009F, 0x249F)]
-		[InlineData (0x0001001A, 0x241A)]
+		[InlineData (0x0001001A, 0x1001A)]
 		public void MakePrintable_Converts_Control_Chars_To_Proper_Unicode (uint code, uint expected)
 		public void MakePrintable_Converts_Control_Chars_To_Proper_Unicode (uint code, uint expected)
 		{
 		{
 			var actual = ConsoleDriver.MakePrintable (code);
 			var actual = ConsoleDriver.MakePrintable (code);

+ 83 - 21
UnitTests/DialogTests.cs

@@ -142,15 +142,15 @@ namespace Terminal.Gui.Views {
 			Dialog dlg = null;
 			Dialog dlg = null;
 			Button button1, button2;
 			Button button1, button2;
 
 
-			//// Default (Center)
-			//button1 = new Button (btn1Text);
-			//button2 = new Button (btn2Text);
-			//(runstate, dlg) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, button1, button2);
-			//button1.Visible = false;
-			//Application.RunMainLoopIteration (ref runstate, true, ref firstIteration);
-			//buttonRow = $@"{d.VLine}         {btn2} {d.VLine}";
-			//DriverAsserts.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
-			//Application.End (runstate);
+			// Default (Center)
+			button1 = new Button (btn1Text);
+			button2 = new Button (btn2Text);
+			(runstate, dlg) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, button1, button2);
+			button1.Visible = false;
+			Application.RunMainLoopIteration (ref runstate, true, ref firstIteration);
+			buttonRow = $@"{d.VLine}         {btn2} {d.VLine}";
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
 
 
 			// Justify
 			// Justify
 			Assert.Equal (width, buttonRow.Length);
 			Assert.Equal (width, buttonRow.Length);
@@ -163,19 +163,26 @@ namespace Terminal.Gui.Views {
 			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 			Application.End (runstate);
 
 
-			//// Right
-			//buttonRow = $@"{d.VLine}  {btn1} {btn2}{d.VLine}";
-			//Assert.Equal (width, buttonRow.Length);
-			//(runstate, dlg) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btn1Text), new Button (btn2Text));
-			//DriverAsserts.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
-			//Application.End (runstate);
+			// Right
+			Assert.Equal (width, buttonRow.Length);
+			button1 = new Button (btn1Text);
+			button2 = new Button (btn2Text);
+			(runstate, dlg) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, button1, button2);
+			button1.Visible = false;
+			Application.RunMainLoopIteration (ref runstate, true, ref firstIteration);
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
 
 
-			//// Left
-			//buttonRow = $@"{d.VLine}{btn1} {btn2}  {d.VLine}";
-			//Assert.Equal (width, buttonRow.Length);
-			//(runstate, dlg) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btn1Text), new Button (btn2Text));
-			//DriverAsserts.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
-			//Application.End (runstate);
+			// Left
+			Assert.Equal (width, buttonRow.Length);
+			button1 = new Button (btn1Text);
+			button2 = new Button (btn2Text);
+			(runstate, dlg) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, button1, button2);
+			button1.Visible = false;
+			Application.RunMainLoopIteration (ref runstate, true, ref firstIteration);
+			buttonRow = $@"{d.VLine}        {btn2}  {d.VLine}";
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
 		}
 		}
 
 
 		[Fact]
 		[Fact]
@@ -281,6 +288,61 @@ namespace Terminal.Gui.Views {
 			Application.End (runstate);
 			Application.End (runstate);
 		}
 		}
 
 
+		[Fact]
+		[AutoInitShutdown]
+		public void ButtonAlignment_Four_On_Smaller_Width ()
+		{
+			Application.RunState runstate = null;
+
+			var d = ((FakeDriver)Application.Driver);
+
+			var title = "1234";
+
+			// E.g "|[ yes ][ no ][ maybe ][ never ]|"
+			var btn1Text = "yes";
+			var btn1 = $"{d.LeftBracket} {btn1Text} {d.RightBracket}";
+			var btn2Text = "no";
+			var btn2 = $"{d.LeftBracket} {btn2Text} {d.RightBracket}";
+			var btn3Text = "maybe";
+			var btn3 = $"{d.LeftBracket} {btn3Text} {d.RightBracket}";
+			var btn4Text = "never";
+			var btn4 = $"{d.LeftBracket} {btn4Text} {d.RightBracket}";
+
+			var buttonRow = $"{d.VLine} {btn1} {btn2} {btn3} {btn4} {d.VLine}";
+			var width = buttonRow.Length;
+			var topRow = "34 ───────────────────────────";
+			var bottomRow = "──────────────────────────────";
+			d.SetBufferSize (30, 3);
+
+			// Default - Center
+			buttonRow = $"yes ] {btn2} {btn3} [ never";
+			Assert.NotEqual (width, buttonRow.Length);
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+
+			// Justify
+			buttonRow = $"es ] {btn2}  {btn3}  [ neve";
+			Assert.NotEqual (width, buttonRow.Length);
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+
+			// Right
+			buttonRow = $" yes ] {btn2} {btn3} [ neve";
+			Assert.NotEqual (width, buttonRow.Length);
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+
+			// Left
+			buttonRow = $"es ] {btn2} {btn3} [ never ";
+			Assert.NotEqual (width, buttonRow.Length);
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+		}
+
 		[Fact]
 		[Fact]
 		[AutoInitShutdown]
 		[AutoInitShutdown]
 		public void ButtonAlignment_Four_Wider ()
 		public void ButtonAlignment_Four_Wider ()

+ 99 - 1
UnitTests/MenuTests.cs

@@ -1498,7 +1498,7 @@ Edit
 			Assert.True (menu.IsMenuOpen);
 			Assert.True (menu.IsMenuOpen);
 			Assert.False (tf.HasFocus);
 			Assert.False (tf.HasFocus);
 			Application.Top.Redraw (Application.Top.Bounds);
 			Application.Top.Redraw (Application.Top.Bounds);
-			TestHelpers.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen(0), output);
+			TestHelpers.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (0), output);
 
 
 			// Right - Edit has no sub menu; this tests that no sub menu shows
 			// Right - Edit has no sub menu; this tests that no sub menu shows
 			Assert.True (menu.openMenu.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
 			Assert.True (menu.openMenu.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
@@ -1636,5 +1636,103 @@ Edit
 00000000000000
 00000000000000
 00000000000000", attributes);
 00000000000000", attributes);
 		}
 		}
+
+		[Fact, AutoInitShutdown]
+		public void MenuBar_With_Action_But_Without_MenuItems_Not_Throw ()
+		{
+			var menu = new MenuBar (
+			    menus: new []
+			    {
+				new MenuBarItem { Title = "Test 1", Action = () => { } },
+				new MenuBarItem { Title = "Test 2", Action = () => { } },
+			    });
+
+			Application.Top.Add (menu);
+			Application.Begin (Application.Top);
+
+			Assert.False (Application.Top.OnKeyDown (new KeyEvent (Key.AltMask, new KeyModifiers { Alt = true })));
+			Assert.True (menu.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+			Assert.True (menu.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+		}
+
+		[Fact, AutoInitShutdown]
+		public void MenuBar_In_Window_Without_Other_Views ()
+		{
+			var win = new Window ();
+			var menu = new MenuBar (new MenuBarItem [] {
+				new MenuBarItem ("File", new MenuItem [] {
+					new MenuItem ("New", "", null)
+				}),
+				new MenuBarItem ("Edit", new MenuItem [] {
+					new MenuBarItem ("Delete", new MenuItem [] {
+						new MenuItem ("All", "", null),
+						new MenuItem ("Selected", "", null)
+					})
+				})
+			}); ;
+			win.Add (menu);
+			var top = Application.Top;
+			top.Add (win);
+			Application.Begin (top);
+			((FakeDriver)Application.Driver).SetBufferSize (40, 8);
+
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+┌──────────────────────────────────────┐
+│ File  Edit                           │
+│                                      │
+│                                      │
+│                                      │
+│                                      │
+│                                      │
+└──────────────────────────────────────┘", output);
+
+			Assert.True (win.ProcessHotKey (new KeyEvent (Key.F9, new KeyModifiers ())));
+			win.Redraw (win.Bounds);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+┌──────────────────────────────────────┐
+│ File  Edit                           │
+│┌──────┐                              │
+││ New  │                              │
+│└──────┘                              │
+│                                      │
+│                                      │
+└──────────────────────────────────────┘", output);
+
+			Assert.True (menu.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+			win.Redraw (win.Bounds);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+┌──────────────────────────────────────┐
+│ File  Edit                           │
+│      ┌─────────┐                     │
+│      │ Delete ►│                     │
+│      └─────────┘                     │
+│                                      │
+│                                      │
+└──────────────────────────────────────┘", output);
+
+			Assert.True (menu.openMenu.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+			win.Redraw (win.Bounds);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+┌──────────────────────────────────────┐
+│ File  Edit                           │
+│      ┌─────────┐                     │
+│      │ Delete ►│┌───────────┐        │
+│      └─────────┘│ All       │        │
+│                 │ Selected  │        │
+│                 └───────────┘        │
+└──────────────────────────────────────┘", output);
+
+			Assert.True (menu.openMenu.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+			win.Redraw (win.Bounds);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+┌──────────────────────────────────────┐
+│ File  Edit                           │
+│┌──────┐                              │
+││ New  │                              │
+│└──────┘                              │
+│                                      │
+│                                      │
+└──────────────────────────────────────┘", output);
+		}
 	}
 	}
 }
 }

+ 3 - 4
UnitTests/StatusBarTests.cs

@@ -31,11 +31,10 @@ namespace Terminal.Gui.Views {
 			Assert.False (sb.CanFocus);
 			Assert.False (sb.CanFocus);
 			Assert.Equal (Colors.Menu, sb.ColorScheme);
 			Assert.Equal (Colors.Menu, sb.ColorScheme);
 			Assert.Equal (0, sb.X);
 			Assert.Equal (0, sb.X);
+			Assert.Equal ("Pos.AnchorEnd(margin=1)", sb.Y.ToString ());
 			Assert.Equal (Dim.Fill (), sb.Width);
 			Assert.Equal (Dim.Fill (), sb.Width);
 			Assert.Equal (1, sb.Height);
 			Assert.Equal (1, sb.Height);
 
 
-			Assert.Null (sb.Y);
-
 			var driver = new FakeDriver ();
 			var driver = new FakeDriver ();
 			Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true)));
 			Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true)));
 
 
@@ -47,11 +46,11 @@ namespace Terminal.Gui.Views {
 			Assert.True (FakeConsole.CursorVisible);
 			Assert.True (FakeConsole.CursorVisible);
 
 
 			Application.Iteration += () => {
 			Application.Iteration += () => {
-				Assert.Equal (24, sb.Y);
+				Assert.Equal (24, sb.Frame.Y);
 
 
 				driver.SetWindowSize (driver.Cols, 15);
 				driver.SetWindowSize (driver.Cols, 15);
 
 
-				Assert.Equal (14, sb.Y);
+				Assert.Equal (14, sb.Frame.Y);
 
 
 				sb.OnEnter (null);
 				sb.OnEnter (null);
 				driver.GetCursorVisibility (out cv);
 				driver.GetCursorVisibility (out cv);

+ 325 - 0
UnitTests/TableViewTests.cs

@@ -1098,6 +1098,331 @@ namespace Terminal.Gui.Views {
 			Application.Shutdown ();
 			Application.Shutdown ();
 		}
 		}
 
 
+		private TableView GetABCDEFTableView (out DataTable dt)
+		{
+			var tableView = new TableView ();
+			tableView.ColorScheme = Colors.TopLevel;
+
+			// 3 columns are visible
+			tableView.Bounds = new Rect (0, 0, 7, 5);
+			tableView.Style.ShowHorizontalHeaderUnderline = false;
+			tableView.Style.ShowHorizontalHeaderOverline = false;
+			tableView.Style.AlwaysShowHeaders = true;
+			tableView.Style.SmoothHorizontalScrolling = false;
+
+			dt = new DataTable ();
+			dt.Columns.Add ("A");
+			dt.Columns.Add ("B");
+			dt.Columns.Add ("C");
+			dt.Columns.Add ("D");
+			dt.Columns.Add ("E");
+			dt.Columns.Add ("F");
+
+
+			dt.Rows.Add (1, 2, 3, 4, 5, 6);
+			tableView.Table = dt;
+
+			return tableView;
+		}
+
+		[Fact, AutoInitShutdown]
+		public void TestColumnStyle_VisibleFalse_IsNotRendered()
+		{
+			var tableView = GetABCDEFTableView (out DataTable dt);
+
+			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["B"]).Visible = false;
+
+			tableView.Redraw (tableView.Bounds);
+
+			string expected =
+				@"
+│A│C│D│
+│1│3│4│";
+
+			TestHelpers.AssertDriverContentsAre (expected, output);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void TestColumnStyle_FirstColumnVisibleFalse_IsNotRendered ()
+		{
+			var tableView = GetABCDEFTableView (out DataTable dt);
+
+			tableView.Style.ShowHorizontalScrollIndicators = true;
+			tableView.Style.ShowHorizontalHeaderUnderline = true;
+			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["A"]).Visible = false;
+
+			tableView.Redraw (tableView.Bounds);
+
+			string expected =
+				@"
+│B│C│D│
+├─┼─┼─►
+│2│3│4│";
+
+			TestHelpers.AssertDriverContentsAre (expected, output);
+		}
+
+
+		[Fact, AutoInitShutdown]
+		public void TestColumnStyle_AllColumnsVisibleFalse_BehavesAsTableNull ()
+		{
+			var tableView = GetABCDEFTableView (out DataTable dt);
+
+			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["A"]).Visible = false;
+			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["B"]).Visible = false;
+			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["C"]).Visible = false;
+			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["D"]).Visible = false;
+			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["E"]).Visible = false;
+			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["F"]).Visible = false;
+
+
+			// expect nothing to be rendered when all columns are invisible
+			string expected =
+				@"
+";
+
+			tableView.Redraw (tableView.Bounds);
+			TestHelpers.AssertDriverContentsAre (expected, output);
+
+
+			// expect behavior to match when Table is null
+			tableView.Table = null;
+
+			tableView.Redraw (tableView.Bounds);
+			TestHelpers.AssertDriverContentsAre (expected, output);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void TestColumnStyle_RemainingColumnsInvisible_NoScrollIndicator ()
+		{
+			var tableView = GetABCDEFTableView (out DataTable dt);
+
+			tableView.Style.ShowHorizontalScrollIndicators = true;
+			tableView.Style.ShowHorizontalHeaderUnderline = true;
+
+			tableView.Redraw (tableView.Bounds);
+
+			// normally we should have scroll indicators because DEF are of screen
+			string expected =
+				@"
+│A│B│C│
+├─┼─┼─►
+│1│2│3│";
+
+			TestHelpers.AssertDriverContentsAre (expected, output);
+
+			// but if DEF are invisible we shouldn't be showing the indicator
+			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["D"]).Visible = false;
+			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["E"]).Visible = false;
+			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["F"]).Visible = false;
+
+			expected =
+			       @"
+│A│B│C│
+├─┼─┼─┤
+│1│2│3│";
+			tableView.Redraw (tableView.Bounds);
+			TestHelpers.AssertDriverContentsAre (expected, output);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void TestColumnStyle_PreceedingColumnsInvisible_NoScrollIndicator ()
+		{
+			var tableView = GetABCDEFTableView (out DataTable dt);
+
+			tableView.Style.ShowHorizontalScrollIndicators = true;
+			tableView.Style.ShowHorizontalHeaderUnderline = true;
+
+			tableView.ColumnOffset = 1;
+			tableView.Redraw (tableView.Bounds);
+
+			// normally we should have scroll indicators because A,E and F are of screen
+			string expected =
+				@"
+│B│C│D│
+◄─┼─┼─►
+│2│3│4│";
+
+			TestHelpers.AssertDriverContentsAre (expected, output);
+
+			// but if E and F are invisible so we shouldn't show right
+			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["E"]).Visible = false;
+			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["F"]).Visible = false;
+
+			expected =
+			       @"
+│B│C│D│
+◄─┼─┼─┤
+│2│3│4│";
+			tableView.Redraw (tableView.Bounds);
+			TestHelpers.AssertDriverContentsAre (expected, output);
+
+			// now also A is invisible so we cannot scroll in either direction
+			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["A"]).Visible = false;
+
+			expected =
+			       @"
+│B│C│D│
+├─┼─┼─┤
+│2│3│4│";
+			tableView.Redraw (tableView.Bounds);
+			TestHelpers.AssertDriverContentsAre (expected, output);
+		}
+		[Fact, AutoInitShutdown]
+		public void TestColumnStyle_VisibleFalse_CursorStepsOverInvisibleColumns ()
+		{
+			var tableView = GetABCDEFTableView (out var dt);
+			
+			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["B"]).Visible = false;
+			tableView.SelectedColumn = 0;
+
+			tableView.ProcessKey (new KeyEvent { Key = Key.CursorRight });
+
+			// Expect the cursor navigation to skip over the invisible column(s)
+			Assert.Equal(2,tableView.SelectedColumn);
+
+			tableView.ProcessKey (new KeyEvent { Key = Key.CursorLeft });
+
+			// Expect the cursor navigation backwards to skip over invisible column too
+			Assert.Equal (0, tableView.SelectedColumn);
+		}
+
+		[InlineData(true)]
+		[InlineData (false)]
+		[Theory, AutoInitShutdown]
+		public void TestColumnStyle_FirstColumnVisibleFalse_CursorStaysAt1(bool useHome)
+		{
+			var tableView = GetABCDEFTableView (out var dt);
+
+			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["A"]).Visible = false;
+			tableView.SelectedColumn = 0;
+
+			Assert.Equal (0, tableView.SelectedColumn);
+
+			// column 0 is invisible so this method should move to 1
+			tableView.EnsureValidSelection();
+			Assert.Equal (1, tableView.SelectedColumn);
+
+			tableView.ProcessKey (new KeyEvent 
+			{
+				Key = useHome ? Key.Home : Key.CursorLeft 
+			});
+
+			// Expect the cursor to stay at 1
+			Assert.Equal (1, tableView.SelectedColumn);
+		}
+
+		[InlineData (true)]
+		[InlineData (false)]
+		[Theory, AutoInitShutdown]
+		public void TestColumnStyle_LastColumnVisibleFalse_CursorStaysAt2 (bool useEnd)
+		{
+			var tableView = GetABCDEFTableView (out var dt);
+						
+			// select D 
+			tableView.SelectedColumn = 3;
+			Assert.Equal (3, tableView.SelectedColumn);
+
+			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["D"]).Visible = false;
+			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["E"]).Visible = false;
+			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["F"]).Visible = false;
+
+			// column D is invisible so this method should move to 2 (C)
+			tableView.EnsureValidSelection ();
+			Assert.Equal (2, tableView.SelectedColumn);
+
+			tableView.ProcessKey (new KeyEvent {
+				Key = useEnd ? Key.End : Key.CursorRight
+			});
+
+			// Expect the cursor to stay at 2
+			Assert.Equal (2, tableView.SelectedColumn);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void TestColumnStyle_VisibleFalse_MultiSelected ()
+		{
+			var tableView = GetABCDEFTableView (out var dt);
+
+			// user has rectangular selection 
+			tableView.MultiSelectedRegions.Push (
+				new TableView.TableSelection(
+					new Point(0,0),
+					new Rect(0, 0, 3, 1))
+				);
+
+			Assert.Equal (3, tableView.GetAllSelectedCells ().Count());
+			Assert.True (tableView.IsSelected (0, 0));
+			Assert.True (tableView.IsSelected (1, 0));
+			Assert.True (tableView.IsSelected (2, 0));
+			Assert.False (tableView.IsSelected (3, 0));
+
+			// if middle column is invisible
+			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["B"]).Visible = false;
+
+			// it should not be included in the selection
+			Assert.Equal (2, tableView.GetAllSelectedCells ().Count ());
+			Assert.True (tableView.IsSelected (0, 0));
+			Assert.False (tableView.IsSelected (1, 0));
+			Assert.True (tableView.IsSelected (2, 0));
+			Assert.False (tableView.IsSelected (3, 0));
+
+			Assert.DoesNotContain(new Point(1,0),tableView.GetAllSelectedCells ());
+		}
+
+		[Fact, AutoInitShutdown]
+		public void TestColumnStyle_VisibleFalse_MultiSelectingStepsOverInvisibleColumns ()
+		{
+			var tableView = GetABCDEFTableView (out var dt);
+
+			// if middle column is invisible
+			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["B"]).Visible = false;
+
+			tableView.ProcessKey (new KeyEvent { Key = Key.CursorRight | Key.ShiftMask });
+
+			// Selection should extend from A to C but skip B
+			Assert.Equal (2, tableView.GetAllSelectedCells ().Count ());
+			Assert.True (tableView.IsSelected (0, 0));
+			Assert.False (tableView.IsSelected (1, 0));
+			Assert.True (tableView.IsSelected (2, 0));
+			Assert.False (tableView.IsSelected (3, 0));
+
+			Assert.DoesNotContain (new Point (1, 0), tableView.GetAllSelectedCells ());
+		}
+		
+		[Theory, AutoInitShutdown]
+		[InlineData(new object[] { true,true })]
+		[InlineData (new object[] { false,true })]
+		[InlineData (new object [] { true, false})]
+		[InlineData (new object [] { false, false})]
+		public void TestColumnStyle_VisibleFalse_DoesNotEffect_EnsureSelectedCellIsVisible (bool smooth, bool invisibleCol)
+		{
+			var tableView = GetABCDEFTableView (out var dt);
+			tableView.Style.SmoothHorizontalScrolling = smooth;
+			
+			if(invisibleCol) {
+				tableView.Style.GetOrCreateColumnStyle (dt.Columns ["D"]).Visible = false;
+			}
+
+			// New TableView should have first cell selected 
+			Assert.Equal (0,tableView.SelectedColumn);
+			// With no scrolling
+			Assert.Equal (0, tableView.ColumnOffset);
+
+			// A,B and C are visible on screen at the moment so these should have no effect
+			tableView.SelectedColumn = 1;
+			tableView.EnsureSelectedCellIsVisible ();
+			Assert.Equal (0, tableView.ColumnOffset);
+
+			tableView.SelectedColumn = 2;
+			tableView.EnsureSelectedCellIsVisible ();
+			Assert.Equal (0, tableView.ColumnOffset);
+
+			// Selecting D should move the visible table area to fit D onto the screen
+			tableView.SelectedColumn = 3;
+			tableView.EnsureSelectedCellIsVisible ();
+			Assert.Equal (smooth ? 1 : 3, tableView.ColumnOffset);
+		}
 		[Fact]
 		[Fact]
 		public void LongColumnTest ()
 		public void LongColumnTest ()
 		{
 		{

+ 16 - 8
UnitTests/TestHelpers.cs

@@ -53,7 +53,15 @@ class TestHelpers {
 
 
 		for (int r = 0; r < driver.Rows; r++) {
 		for (int r = 0; r < driver.Rows; r++) {
 			for (int c = 0; c < driver.Cols; c++) {
 			for (int c = 0; c < driver.Cols; c++) {
-				sb.Append ((char)contents [r, c, 0]);
+				Rune rune = contents [r, c, 0];
+				if (Rune.DecodeSurrogatePair (rune, out char [] spair)) {
+					sb.Append (spair);
+				} else {
+					sb.Append ((char)rune);
+				}
+				if (Rune.ColumnWidth (rune) > 1) {
+					c++;
+				}
 			}
 			}
 			sb.AppendLine ();
 			sb.AppendLine ();
 		}
 		}
@@ -82,7 +90,7 @@ class TestHelpers {
 
 
 	public static Rect AssertDriverContentsWithFrameAre (string expectedLook, ITestOutputHelper output)
 	public static Rect AssertDriverContentsWithFrameAre (string expectedLook, ITestOutputHelper output)
 	{
 	{
-		var lines = new List<List<char>> ();
+		var lines = new List<List<Rune>> ();
 		var sb = new StringBuilder ();
 		var sb = new StringBuilder ();
 		var driver = ((FakeDriver)Application.Driver);
 		var driver = ((FakeDriver)Application.Driver);
 		var x = -1;
 		var x = -1;
@@ -93,15 +101,15 @@ class TestHelpers {
 		var contents = driver.Contents;
 		var contents = driver.Contents;
 
 
 		for (int r = 0; r < driver.Rows; r++) {
 		for (int r = 0; r < driver.Rows; r++) {
-			var runes = new List<char> ();
+			var runes = new List<Rune> ();
 			for (int c = 0; c < driver.Cols; c++) {
 			for (int c = 0; c < driver.Cols; c++) {
-				var rune = (char)contents [r, c, 0];
+				var rune = (Rune)contents [r, c, 0];
 				if (rune != ' ') {
 				if (rune != ' ') {
 					if (x == -1) {
 					if (x == -1) {
 						x = c;
 						x = c;
 						y = r;
 						y = r;
 						for (int i = 0; i < c; i++) {
 						for (int i = 0; i < c; i++) {
-							runes.InsertRange (i, new List<char> () { ' ' });
+							runes.InsertRange (i, new List<Rune> () { ' ' });
 						}
 						}
 					}
 					}
 					if (Rune.ColumnWidth (rune) > 1) {
 					if (Rune.ColumnWidth (rune) > 1) {
@@ -130,7 +138,7 @@ class TestHelpers {
 
 
 		// Remove trailing whitespace on each line
 		// Remove trailing whitespace on each line
 		for (int r = 0; r < lines.Count; r++) {
 		for (int r = 0; r < lines.Count; r++) {
-			List<char> row = lines [r];
+			List<Rune> row = lines [r];
 			for (int c = row.Count - 1; c >= 0; c--) {
 			for (int c = row.Count - 1; c >= 0; c--) {
 				var rune = row [c];
 				var rune = row [c];
 				if (rune != ' ' || (row.Sum (x => Rune.ColumnWidth (x)) == w)) {
 				if (rune != ' ' || (row.Sum (x => Rune.ColumnWidth (x)) == w)) {
@@ -140,9 +148,9 @@ class TestHelpers {
 			}
 			}
 		}
 		}
 
 
-		// Convert char list to string
+		// Convert Rune list to string
 		for (int r = 0; r < lines.Count; r++) {
 		for (int r = 0; r < lines.Count; r++) {
-			var line = new string (lines [r].ToArray ());
+			var line = NStack.ustring.Make (lines [r]).ToString ();
 			if (r == lines.Count - 1) {
 			if (r == lines.Count - 1) {
 				sb.Append (line);
 				sb.Append (line);
 			} else {
 			} else {

+ 15 - 0
UnitTests/TextFieldTests.cs

@@ -1281,5 +1281,20 @@ namespace Terminal.Gui.Views {
 			Assert.Equal (0, tf.ScrollOffset);
 			Assert.Equal (0, tf.ScrollOffset);
 			Assert.Equal (16, tf.CursorPosition);
 			Assert.Equal (16, tf.CursorPosition);
 		}
 		}
+
+		[Fact]
+		public void HistoryText_IsDirty_ClearHistoryChanges ()
+		{
+			var text = "Testing";
+			var tf = new TextField (text);
+
+			Assert.Equal (text, tf.Text);
+			tf.ClearHistoryChanges ();
+			Assert.False (tf.IsDirty);
+
+			Assert.True (tf.ProcessKey (new KeyEvent (Key.A, new KeyModifiers ())));
+			Assert.Equal ($"{text}A", tf.Text);
+			Assert.True (tf.IsDirty);
+		}
 	}
 	}
 }
 }

+ 104 - 8
UnitTests/TextFormatterTests.cs

@@ -2424,38 +2424,38 @@ namespace Terminal.Gui.Core {
 			var tf = new TextFormatter ();
 			var tf = new TextFormatter ();
 			ustring text = "test";
 			ustring text = "test";
 			int hotPos = 0;
 			int hotPos = 0;
-			uint tag = tf.HotKeyTagMask | 't';
+			uint tag = 't';
 
 
 			Assert.Equal (ustring.Make (new Rune [] { tag, 'e', 's', 't' }), tf.ReplaceHotKeyWithTag (text, hotPos));
 			Assert.Equal (ustring.Make (new Rune [] { tag, 'e', 's', 't' }), tf.ReplaceHotKeyWithTag (text, hotPos));
 
 
-			tag = tf.HotKeyTagMask | 'e';
+			tag = 'e';
 			hotPos = 1;
 			hotPos = 1;
 			Assert.Equal (ustring.Make (new Rune [] { 't', tag, 's', 't' }), tf.ReplaceHotKeyWithTag (text, hotPos));
 			Assert.Equal (ustring.Make (new Rune [] { 't', tag, 's', 't' }), tf.ReplaceHotKeyWithTag (text, hotPos));
 
 
 			var result = tf.ReplaceHotKeyWithTag (text, hotPos);
 			var result = tf.ReplaceHotKeyWithTag (text, hotPos);
-			Assert.Equal ('e', (uint)(result.ToRunes () [1] & ~tf.HotKeyTagMask));
+			Assert.Equal ('e', (uint)(result.ToRunes () [1]));
 
 
 			text = "Ok";
 			text = "Ok";
-			tag = 0x100000 | 'O';
+			tag = 'O';
 			hotPos = 0;
 			hotPos = 0;
 			Assert.Equal (ustring.Make (new Rune [] { tag, 'k' }), result = tf.ReplaceHotKeyWithTag (text, hotPos));
 			Assert.Equal (ustring.Make (new Rune [] { tag, 'k' }), result = tf.ReplaceHotKeyWithTag (text, hotPos));
-			Assert.Equal ('O', (uint)(result.ToRunes () [0] & ~tf.HotKeyTagMask));
+			Assert.Equal ('O', (uint)(result.ToRunes () [0]));
 
 
 			text = "[◦ Ok ◦]";
 			text = "[◦ Ok ◦]";
 			text = ustring.Make (new Rune [] { '[', '◦', ' ', 'O', 'k', ' ', '◦', ']' });
 			text = ustring.Make (new Rune [] { '[', '◦', ' ', 'O', 'k', ' ', '◦', ']' });
 			var runes = text.ToRuneList ();
 			var runes = text.ToRuneList ();
 			Assert.Equal (text.RuneCount, runes.Count);
 			Assert.Equal (text.RuneCount, runes.Count);
 			Assert.Equal (text, ustring.Make (runes));
 			Assert.Equal (text, ustring.Make (runes));
-			tag = tf.HotKeyTagMask | 'O';
+			tag = 'O';
 			hotPos = 3;
 			hotPos = 3;
 			Assert.Equal (ustring.Make (new Rune [] { '[', '◦', ' ', tag, 'k', ' ', '◦', ']' }), result = tf.ReplaceHotKeyWithTag (text, hotPos));
 			Assert.Equal (ustring.Make (new Rune [] { '[', '◦', ' ', tag, 'k', ' ', '◦', ']' }), result = tf.ReplaceHotKeyWithTag (text, hotPos));
-			Assert.Equal ('O', (uint)(result.ToRunes () [3] & ~tf.HotKeyTagMask));
+			Assert.Equal ('O', (uint)(result.ToRunes () [3]));
 
 
 			text = "^k";
 			text = "^k";
 			tag = '^';
 			tag = '^';
 			hotPos = 0;
 			hotPos = 0;
 			Assert.Equal (ustring.Make (new Rune [] { tag, 'k' }), result = tf.ReplaceHotKeyWithTag (text, hotPos));
 			Assert.Equal (ustring.Make (new Rune [] { tag, 'k' }), result = tf.ReplaceHotKeyWithTag (text, hotPos));
-			Assert.Equal ('^', (uint)(result.ToRunes () [0] & ~tf.HotKeyTagMask));
+			Assert.Equal ('^', (uint)(result.ToRunes () [0]));
 		}
 		}
 
 
 		[Fact]
 		[Fact]
@@ -4163,5 +4163,101 @@ This TextFormatter (tf2) is rewritten.
 			Assert.Equal ("你", ((Rune)usToRunes [9]).ToString ());
 			Assert.Equal ("你", ((Rune)usToRunes [9]).ToString ());
 			Assert.Equal ("你", s [9].ToString ());
 			Assert.Equal ("你", s [9].ToString ());
 		}
 		}
+
+		[Fact, AutoInitShutdown]
+		public void Non_Bmp_ConsoleWidth_ColumnWidth_Equal_Two ()
+		{
+			ustring us = "\U0001d539";
+			Rune r = 0x1d539;
+
+			Assert.Equal ("𝔹", us);
+			Assert.Equal ("𝔹", r.ToString ());
+			Assert.Equal (us, r.ToString ());
+
+			Assert.Equal (2, us.ConsoleWidth);
+			Assert.Equal (2, Rune.ColumnWidth (r));
+
+			var win = new Window (us);
+			var label = new Label (ustring.Make (r));
+			var tf = new TextField (us) { Y = 1, Width = 3 };
+			win.Add (label, tf);
+			var top = Application.Top;
+			top.Add (win);
+
+			Application.Begin (top);
+			((FakeDriver)Application.Driver).SetBufferSize (10, 4);
+
+			var expected = @"
+┌ 𝔹 ────┐
+│𝔹      │
+│𝔹      │
+└────────┘";
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+
+			TestHelpers.AssertDriverContentsAre (expected, output);
+
+			var expectedColors = new Attribute [] {
+				// 0
+				Colors.Base.Normal,
+				// 1
+				Colors.Base.Focus,
+				// 2
+				Colors.Base.HotNormal
+			};
+
+			TestHelpers.AssertDriverColorsAre (@"
+0222200000
+0000000000
+0111000000
+0000000000", expectedColors);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void CJK_Compatibility_Ideographs_ConsoleWidth_ColumnWidth_Equal_Two ()
+		{
+			ustring us = "\U0000f900";
+			Rune r = 0xf900;
+
+			Assert.Equal ("豈", us);
+			Assert.Equal ("豈", r.ToString ());
+			Assert.Equal (us, r.ToString ());
+
+			Assert.Equal (2, us.ConsoleWidth);
+			Assert.Equal (2, Rune.ColumnWidth (r));
+
+			var win = new Window (us);
+			var label = new Label (ustring.Make (r));
+			var tf = new TextField (us) { Y = 1, Width = 3 };
+			win.Add (label, tf);
+			var top = Application.Top;
+			top.Add (win);
+
+			Application.Begin (top);
+			((FakeDriver)Application.Driver).SetBufferSize (10, 4);
+
+			var expected = @"
+┌ 豈 ────┐
+│豈      │
+│豈      │
+└────────┘";
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+
+			TestHelpers.AssertDriverContentsAre (expected, output);
+
+			var expectedColors = new Attribute [] {
+				// 0
+				Colors.Base.Normal,
+				// 1
+				Colors.Base.Focus,
+				// 2
+				Colors.Base.HotNormal
+			};
+
+			TestHelpers.AssertDriverColorsAre (@"
+0222200000
+0000000000
+0111000000
+0000000000", expectedColors);
+		}
 	}
 	}
 }
 }

+ 87 - 41
UnitTests/TextViewTests.cs

@@ -1,6 +1,5 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
-using System.Diagnostics.Tracing;
 using System.Linq;
 using System.Linq;
 using System.Reflection;
 using System.Reflection;
 using System.Text.RegularExpressions;
 using System.Text.RegularExpressions;
@@ -1930,6 +1929,51 @@ namespace Terminal.Gui.Views {
 			Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}", tv.Text);
 			Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}", tv.Text);
 		}
 		}
 
 
+		[Fact]
+		public void LoadStream_IsDirty ()
+		{
+			var text = "Testing";
+			using (System.IO.MemoryStream stream = new System.IO.MemoryStream ()) {
+
+				var writer = new System.IO.StreamWriter (stream);
+				writer.Write (text);
+				writer.Flush ();
+				stream.Position = 0;
+
+				var tv = new TextView ();
+				tv.LoadStream (stream);
+
+				Assert.Equal (7, text.Length);
+				Assert.Equal (text.Length, tv.Text.Length);
+				Assert.Equal (text, tv.Text);
+				Assert.False (tv.IsDirty);
+			}
+		}
+
+		[Fact]
+		public void LoadStream_IsDirty_With_Null_On_The_Text ()
+		{
+			var text = "Test\0ing";
+			using (System.IO.MemoryStream stream = new System.IO.MemoryStream ()) {
+
+				var writer = new System.IO.StreamWriter (stream);
+				writer.Write (text);
+				writer.Flush ();
+				stream.Position = 0;
+
+				var tv = new TextView ();
+				tv.LoadStream (stream);
+
+				Assert.Equal (8, text.Length);
+				Assert.Equal (text.Length, tv.Text.Length);
+				Assert.Equal (8, text.Length);
+				Assert.Equal (8, tv.Text.Length);
+				Assert.Equal (text, tv.Text);
+				Assert.False (tv.IsDirty);
+				Assert.Equal ('\u2400', ConsoleDriver.MakePrintable ((Rune)tv.Text [4]));
+			}
+		}
+
 		[Fact]
 		[Fact]
 		public void StringToRunes_Slipts_CRLF ()
 		public void StringToRunes_Slipts_CRLF ()
 		{
 		{
@@ -6430,16 +6474,17 @@ This is the second line.
 		[Fact, AutoInitShutdown]
 		[Fact, AutoInitShutdown]
 		public void ContentsChanged_Event_NoFires_On_CursorPosition ()
 		public void ContentsChanged_Event_NoFires_On_CursorPosition ()
 		{
 		{
+			var eventcount = 0;
+
 			var tv = new TextView {
 			var tv = new TextView {
 				Width = 50,
 				Width = 50,
 				Height = 10,
 				Height = 10,
 			};
 			};
 
 
-			var eventcount = 0;
-			Assert.Null (tv.ContentsChanged);
 			tv.ContentsChanged += (e) => {
 			tv.ContentsChanged += (e) => {
 				eventcount++;
 				eventcount++;
 			};
 			};
+			Assert.Equal (0, eventcount);
 
 
 			tv.CursorPosition = new Point (0, 0);
 			tv.CursorPosition = new Point (0, 0);
 
 
@@ -6449,19 +6494,19 @@ This is the second line.
 		[Fact, AutoInitShutdown]
 		[Fact, AutoInitShutdown]
 		public void ContentsChanged_Event_Fires_On_InsertText ()
 		public void ContentsChanged_Event_Fires_On_InsertText ()
 		{
 		{
+			var eventcount = 0;
+
 			var tv = new TextView {
 			var tv = new TextView {
 				Width = 50,
 				Width = 50,
 				Height = 10,
 				Height = 10,
 			};
 			};
 			tv.CursorPosition = new Point (0, 0);
 			tv.CursorPosition = new Point (0, 0);
 
 
-			var eventcount = 0;
-
-			Assert.Null (tv.ContentsChanged);
 			tv.ContentsChanged += (e) => {
 			tv.ContentsChanged += (e) => {
 				eventcount++;
 				eventcount++;
 			};
 			};
 
 
+			Assert.Equal (0, eventcount);
 
 
 			tv.InsertText ("a");
 			tv.InsertText ("a");
 			Assert.Equal (1, eventcount);
 			Assert.Equal (1, eventcount);
@@ -6469,7 +6514,7 @@ This is the second line.
 			tv.CursorPosition = new Point (0, 0);
 			tv.CursorPosition = new Point (0, 0);
 			tv.InsertText ("bcd");
 			tv.InsertText ("bcd");
 			Assert.Equal (4, eventcount);
 			Assert.Equal (4, eventcount);
-			
+
 			tv.InsertText ("e");
 			tv.InsertText ("e");
 			Assert.Equal (5, eventcount);
 			Assert.Equal (5, eventcount);
 
 
@@ -6494,11 +6539,11 @@ This is the second line.
 			var tv = new TextView {
 			var tv = new TextView {
 				Width = 50,
 				Width = 50,
 				Height = 10,
 				Height = 10,
-				ContentsChanged = (e) => {
-					eventcount++;
-					Assert.Equal (expectedRow, e.Row);
-					Assert.Equal (expectedCol, e.Col);
-				}
+			};
+			tv.ContentsChanged += (e) => {
+				eventcount++;
+				Assert.Equal (expectedRow, e.Row);
+				Assert.Equal (expectedCol, e.Col);
 			};
 			};
 
 
 			Application.Top.Add (tv);
 			Application.Top.Add (tv);
@@ -6523,12 +6568,13 @@ This is the second line.
 				// you'd think col would be 3, but it's 0 because TextView sets
 				// you'd think col would be 3, but it's 0 because TextView sets
 				// row/col = 0 when you set Text
 				// row/col = 0 when you set Text
 				Text = "abc",
 				Text = "abc",
-				ContentsChanged = (e) => {
-					eventcount++;
-					Assert.Equal (expectedRow, e.Row);
-					Assert.Equal (expectedCol, e.Col);
-				}
 			};
 			};
+			tv.ContentsChanged += (e) => {
+				eventcount++;
+				Assert.Equal (expectedRow, e.Row);
+				Assert.Equal (expectedCol, e.Col);
+			};
+
 			Assert.Equal ("abc", tv.Text);
 			Assert.Equal ("abc", tv.Text);
 
 
 			Application.Top.Add (tv);
 			Application.Top.Add (tv);
@@ -6554,11 +6600,11 @@ This is the second line.
 			var tv = new TextView {
 			var tv = new TextView {
 				Width = 50,
 				Width = 50,
 				Height = 10,
 				Height = 10,
-				ContentsChanged = (e) => {
-					eventcount++;
-					Assert.Equal (expectedRow, e.Row);
-					Assert.Equal (expectedCol, e.Col);
-				}
+			};
+			tv.ContentsChanged += (e) => {
+				eventcount++;
+				Assert.Equal (expectedRow, e.Row);
+				Assert.Equal (expectedCol, e.Col);
 			};
 			};
 
 
 			Application.Top.Add (tv);
 			Application.Top.Add (tv);
@@ -6580,7 +6626,7 @@ This is the second line.
 		{
 		{
 			var eventcount = 0;
 			var eventcount = 0;
 
 
-			_textView.ContentsChanged = (e) => {
+			_textView.ContentsChanged += (e) => {
 				eventcount++;
 				eventcount++;
 			};
 			};
 
 
@@ -6601,13 +6647,12 @@ This is the second line.
 			Assert.Equal (expectedEventCount, eventcount);
 			Assert.Equal (expectedEventCount, eventcount);
 		}
 		}
 
 
-
 		[Fact, InitShutdown]
 		[Fact, InitShutdown]
 		public void ContentsChanged_Event_Fires_Using_Copy_Or_Cut_Tests ()
 		public void ContentsChanged_Event_Fires_Using_Copy_Or_Cut_Tests ()
 		{
 		{
 			var eventcount = 0;
 			var eventcount = 0;
 
 
-			_textView.ContentsChanged = (e) => {
+			_textView.ContentsChanged += (e) => {
 				eventcount++;
 				eventcount++;
 			};
 			};
 
 
@@ -6656,7 +6701,7 @@ This is the second line.
 			expectedEventCount += 4;
 			expectedEventCount += 4;
 			Copy_Without_Selection ();
 			Copy_Without_Selection ();
 			Assert.Equal (expectedEventCount, eventcount);
 			Assert.Equal (expectedEventCount, eventcount);
-			
+
 			// reset
 			// reset
 			expectedEventCount += 1;
 			expectedEventCount += 1;
 			_textView.Text = InitShutdown.txt;
 			_textView.Text = InitShutdown.txt;
@@ -6673,7 +6718,7 @@ This is the second line.
 			var eventcount = 0;
 			var eventcount = 0;
 			var expectedEventCount = 0;
 			var expectedEventCount = 0;
 
 
-			_textView.ContentsChanged = (e) => {
+			_textView.ContentsChanged += (e) => {
 				eventcount++;
 				eventcount++;
 			};
 			};
 
 
@@ -6716,9 +6761,9 @@ This is the second line.
 				Width = 50,
 				Width = 50,
 				Height = 10,
 				Height = 10,
 				Text = text,
 				Text = text,
-				ContentsChanged = (e) => {
-					eventcount++;
-				}
+			};
+			tv.ContentsChanged += (e) => {
+				eventcount++;
 			};
 			};
 
 
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
@@ -6727,46 +6772,47 @@ This is the second line.
 
 
 			var expectedEventCount = 1; // for ENTER key
 			var expectedEventCount = 1; // for ENTER key
 			Assert.Equal (expectedEventCount, eventcount);
 			Assert.Equal (expectedEventCount, eventcount);
-			
+
 			tv.ClearHistoryChanges ();
 			tv.ClearHistoryChanges ();
 			expectedEventCount = 2;
 			expectedEventCount = 2;
 			Assert.Equal (expectedEventCount, eventcount);
 			Assert.Equal (expectedEventCount, eventcount);
 		}
 		}
 
 
 		[Fact]
 		[Fact]
-		public void ContentsChanged_Event_Fires_LoadStream ()
+		public void ContentsChanged_Event_Fires_LoadStream_By_Calling_HistoryText_Clear ()
 		{
 		{
 			var eventcount = 0;
 			var eventcount = 0;
 
 
 			var tv = new TextView {
 			var tv = new TextView {
 				Width = 50,
 				Width = 50,
 				Height = 10,
 				Height = 10,
-				ContentsChanged = (e) => {
-					eventcount++;
-				}
+			};
+			tv.ContentsChanged += (e) => {
+				eventcount++;
 			};
 			};
 
 
 			var text = "This is the first line.\r\nThis is the second line.\r\n";
 			var text = "This is the first line.\r\nThis is the second line.\r\n";
 			tv.LoadStream (new System.IO.MemoryStream (System.Text.Encoding.ASCII.GetBytes (text)));
 			tv.LoadStream (new System.IO.MemoryStream (System.Text.Encoding.ASCII.GetBytes (text)));
 			Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}", tv.Text);
 			Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}", tv.Text);
-			
+
 			Assert.Equal (1, eventcount);
 			Assert.Equal (1, eventcount);
 		}
 		}
 
 
 		[Fact]
 		[Fact]
-		public void ContentsChanged_Event_Fires_LoadFile ()
+		public void ContentsChanged_Event_Fires_On_LoadFile_By_Calling_HistoryText_Clear ()
 		{
 		{
 			var eventcount = 0;
 			var eventcount = 0;
 
 
 			var tv = new TextView {
 			var tv = new TextView {
 				Width = 50,
 				Width = 50,
 				Height = 10,
 				Height = 10,
-				ContentsChanged = (e) => {
-					eventcount++;
-				}
 			};
 			};
+			tv.ContentsChanged += (e) => {
+				eventcount++;
+			};
+
 			var fileName = "textview.txt";
 			var fileName = "textview.txt";
-			System.IO.File.WriteAllText (fileName, "This is the first line.\r\nThis is the second line.\r\n") ;
+			System.IO.File.WriteAllText (fileName, "This is the first line.\r\nThis is the second line.\r\n");
 
 
 			tv.LoadFile (fileName);
 			tv.LoadFile (fileName);
 			Assert.Equal (1, eventcount);
 			Assert.Equal (1, eventcount);

+ 143 - 1
UnitTests/ToplevelTests.cs

@@ -671,7 +671,7 @@ namespace Terminal.Gui.Core {
 		}
 		}
 
 
 		[Fact, AutoInitShutdown]
 		[Fact, AutoInitShutdown]
-		public void Mouse_Drag ()
+		public void Mouse_Drag_On_Top_With_Superview_Null ()
 		{
 		{
 			var menu = new MenuBar (new MenuBarItem [] {
 			var menu = new MenuBar (new MenuBarItem [] {
 				new MenuBarItem("File", new MenuItem [] {
 				new MenuBarItem("File", new MenuItem [] {
@@ -825,5 +825,147 @@ namespace Terminal.Gui.Core {
 
 
 			Application.Run ();
 			Application.Run ();
 		}
 		}
+
+		[Fact, AutoInitShutdown]
+		public void Mouse_Drag_On_Top_With_Superview_Not_Null ()
+		{
+			var menu = new MenuBar (new MenuBarItem [] {
+				new MenuBarItem("File", new MenuItem [] {
+					new MenuItem("New", "", null)
+				})
+			});
+
+			var sbar = new StatusBar (new StatusItem [] {
+				new StatusItem(Key.N, "~CTRL-N~ New", null)
+			});
+
+			var win = new Window ("Window") {
+				X = 3,
+				Y = 2,
+				Width = Dim.Fill (10),
+				Height = Dim.Fill (5)
+			};
+			var top = Application.Top;
+			top.Add (menu, sbar, win);
+
+			var iterations = -1;
+
+			Application.Iteration = () => {
+				iterations++;
+				if (iterations == 0) {
+					((FakeDriver)Application.Driver).SetBufferSize (20, 10);
+
+					Assert.Null (Application.MouseGrabView);
+					// Grab the mouse
+					ReflectionTools.InvokePrivate (
+						typeof (Application),
+						"ProcessMouseEvent",
+						new MouseEvent () {
+							X = 4,
+							Y = 2,
+							Flags = MouseFlags.Button1Pressed
+						});
+
+					Assert.Equal (win, Application.MouseGrabView);
+					Assert.Equal (new Rect (3, 2, 7, 3), Application.MouseGrabView.Frame);
+
+					TestHelpers.AssertDriverContentsWithFrameAre (@"
+ File      
+           
+   ┌─────┐ 
+   │     │ 
+   └─────┘ 
+           
+           
+           
+           
+ CTRL-N New", output);
+
+
+				} else if (iterations == 1) {
+					Assert.Equal (win, Application.MouseGrabView);
+					// Grab to left
+					ReflectionTools.InvokePrivate (
+						typeof (Application),
+						"ProcessMouseEvent",
+						new MouseEvent () {
+							X = 5,
+							Y = 2,
+							Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition
+						});
+
+					Assert.Equal (win, Application.MouseGrabView);
+
+				} else if (iterations == 2) {
+					Assert.Equal (win, Application.MouseGrabView);
+
+					TestHelpers.AssertDriverContentsWithFrameAre (@"
+ File      
+           
+    ┌────┐ 
+    │    │ 
+    └────┘ 
+           
+           
+           
+           
+ CTRL-N New", output);
+
+					Assert.Equal (win, Application.MouseGrabView);
+					Assert.Equal (new Rect (4, 2, 6, 3), Application.MouseGrabView.Frame);
+
+				} else if (iterations == 3) {
+					Assert.Equal (win, Application.MouseGrabView);
+					// Grab to top
+					ReflectionTools.InvokePrivate (
+						typeof (Application),
+						"ProcessMouseEvent",
+						new MouseEvent () {
+							X = 5,
+							Y = 1,
+							Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition
+						});
+
+					Assert.Equal (win, Application.MouseGrabView);
+
+				} else if (iterations == 4) {
+					Assert.Equal (win, Application.MouseGrabView);
+
+					TestHelpers.AssertDriverContentsWithFrameAre (@"
+ File      
+    ┌────┐ 
+    │    │ 
+    │    │ 
+    └────┘ 
+           
+           
+           
+           
+ CTRL-N New", output);
+
+					Assert.Equal (win, Application.MouseGrabView);
+					Assert.Equal (new Rect (4, 1, 6, 4), Application.MouseGrabView.Frame);
+
+				} else if (iterations == 5) {
+					Assert.Equal (win, Application.MouseGrabView);
+					// Ungrab the mouse
+					ReflectionTools.InvokePrivate (
+						typeof (Application),
+						"ProcessMouseEvent",
+						new MouseEvent () {
+							X = 7,
+							Y = 4,
+							Flags = MouseFlags.Button1Released
+						});
+
+					Assert.Null (Application.MouseGrabView);
+
+				} else if (iterations == 8) {
+					Application.RequestStop ();
+				}
+			};
+
+			Application.Run ();
+		}
 	}
 	}
 }
 }

+ 1 - 1
UnitTests/UnitTests.csproj

@@ -18,7 +18,7 @@
     <DefineConstants>TRACE;DEBUG_IDISPOSABLE</DefineConstants>
     <DefineConstants>TRACE;DEBUG_IDISPOSABLE</DefineConstants>
   </PropertyGroup>
   </PropertyGroup>
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
     <PackageReference Include="ReportGenerator" Version="5.1.12" />
     <PackageReference Include="ReportGenerator" Version="5.1.12" />
     <PackageReference Include="System.Collections" Version="4.3.0" />
     <PackageReference Include="System.Collections" Version="4.3.0" />
     <PackageReference Include="xunit" Version="2.4.2" />
     <PackageReference Include="xunit" Version="2.4.2" />

+ 83 - 0
UnitTests/WindowTests.cs

@@ -153,5 +153,88 @@ namespace Terminal.Gui.Core {
 			Assert.Equal (expected, r.Title.ToString ());
 			Assert.Equal (expected, r.Title.ToString ());
 			r.Dispose ();
 			r.Dispose ();
 		}
 		}
+
+		[Fact,AutoInitShutdown]
+		public void MenuBar_And_StatusBar_Inside_Window ()
+		{
+			var menu = new MenuBar (new MenuBarItem [] {
+				new MenuBarItem ("File", new MenuItem [] {
+					new MenuItem ("Open", "", null),
+					new MenuItem ("Quit", "", null),
+				}),
+				new MenuBarItem ("Edit", new MenuItem [] {
+					new MenuItem ("Copy", "", null),
+				})
+			});
+
+			var sb = new StatusBar (new StatusItem [] {
+				new StatusItem (Key.CtrlMask | Key.Q, "~^Q~ Quit", null),
+				new StatusItem (Key.CtrlMask | Key.O, "~^O~ Open", null),
+				new StatusItem (Key.CtrlMask | Key.C, "~^C~ Copy", null),
+			});
+
+			var fv = new FrameView ("Frame View") {
+				Y = 1,
+				Width = Dim.Fill(),
+				Height = Dim.Fill (1)
+			};
+			var win = new Window ();
+			win.Add (menu, sb, fv);
+			var top = Application.Top;
+			top.Add (win);
+			Application.Begin (top);
+			((FakeDriver)Application.Driver).SetBufferSize (20, 10);
+
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+┌──────────────────┐
+│ File  Edit       │
+│┌ Frame View ────┐│
+││                ││
+││                ││
+││                ││
+││                ││
+│└────────────────┘│
+│ ^Q Quit │ ^O Open│
+└──────────────────┘", output);
+
+			((FakeDriver)Application.Driver).SetBufferSize (40, 20);
+
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+┌──────────────────────────────────────┐
+│ File  Edit                           │
+│┌ Frame View ────────────────────────┐│
+││                                    ││
+││                                    ││
+││                                    ││
+││                                    ││
+││                                    ││
+││                                    ││
+││                                    ││
+││                                    ││
+││                                    ││
+││                                    ││
+││                                    ││
+││                                    ││
+││                                    ││
+││                                    ││
+│└────────────────────────────────────┘│
+│ ^Q Quit │ ^O Open │ ^C Copy          │
+└──────────────────────────────────────┘", output);
+
+			((FakeDriver)Application.Driver).SetBufferSize (20, 10);
+
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+┌──────────────────┐
+│ File  Edit       │
+│┌ Frame View ────┐│
+││                ││
+││                ││
+││                ││
+││                ││
+│└────────────────┘│
+│ ^Q Quit │ ^O Open│
+└──────────────────┘", output);
+
+		}
 	}
 	}
 }
 }