فهرست منبع

merge develop branch

BDisp 2 سال پیش
والد
کامیت
1b9b50dd12

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

@@ -10,7 +10,7 @@ jobs:
 
     steps:
     - name: Checkout
-      uses: actions/checkout@v2
+      uses: actions/checkout@v3
 
     - name: Setup .NET Core
       uses: actions/[email protected]

+ 2 - 2
ReactiveExample/ReactiveExample.csproj

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

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

@@ -1483,7 +1483,13 @@ namespace Terminal.Gui {
 							output.Append (WriteAttributes (attr));
 						}
 						outputWidth++;
-						output.Append ((char)contents [row, col, 0]);
+						var rune = contents [row, col, 0];
+						char [] spair;
+						if (Rune.DecodeSurrogatePair((uint) rune, out spair)) {
+							output.Append (spair);
+						} else {
+							output.Append ((char)rune);
+						}
 						contents [row, col, 2] = 0;
 					}
 				}

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

@@ -613,11 +613,8 @@ namespace Terminal.Gui {
 			}
 			nx = Math.Max (x, 0);
 			nx = nx + top.Frame.Width > l ? Math.Max (l - top.Frame.Width, 0) : nx;
-			var canChange = SetWidth (top.Frame.Width, out int rWidth);
-			if (canChange && rWidth < 0 && nx >= top.Frame.X) {
-				nx = Math.Max (top.Frame.Right - 2, 0);
-			} else if (rWidth < 0 && nx >= top.Frame.X) {
-				nx = Math.Min (nx + 1, top.Frame.Right - 2);
+			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}");
 			bool m, s;
@@ -656,11 +653,8 @@ namespace Terminal.Gui {
 			}
 			ny = Math.Min (ny, l);
 			ny = ny + top.Frame.Height >= l ? Math.Max (l - top.Frame.Height, m ? 1 : 0) : ny;
-			canChange = SetHeight (top.Frame.Height, out int rHeight);
-			if (canChange && rHeight < 0 && ny >= top.Frame.Y) {
-				ny = Math.Max (top.Frame.Bottom - 2, 0);
-			} else if (rHeight < 0 && ny >= top.Frame.Y) {
-				ny = Math.Min (ny + 1, top.Frame.Bottom - 2);
+			if (ny + (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}");
 
@@ -701,7 +695,7 @@ namespace Terminal.Gui {
 			}
 
 			if (sb != null && ny + top.Frame.Height != superView.Frame.Height - (sb.Visible ? 1 : 0)
-					&& top.Height is Dim.DimFill) {
+				&& top.Height is Dim.DimFill && -top.Height.Anchor (0) < 1) {
 
 				top.Height = Dim.Fill (sb.Visible ? 1 : 0);
 				layoutSubviews = true;

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

@@ -1321,8 +1321,9 @@ namespace Terminal.Gui {
 
 			// Remove focus down the chain of subviews if focus is removed
 			if (!value && focused != null) {
-				focused.OnLeave (view);
-				focused.SetHasFocus (false, view);
+				var f = focused;
+				f.OnLeave (view);
+				f.SetHasFocus (false, view);
 				focused = null;
 			}
 		}
@@ -3063,6 +3064,17 @@ namespace Terminal.Gui {
 			return Enabled ? ColorScheme.Normal : ColorScheme.Disabled;
 		}
 
+		/// <summary>
+		/// Determines the current <see cref="ColorScheme"/> based on the <see cref="Enabled"/> value.
+		/// </summary>
+		/// <returns><see cref="Terminal.Gui.ColorScheme.HotNormal"/> if <see cref="Enabled"/> is <see langword="true"/>
+		/// or <see cref="Terminal.Gui.ColorScheme.Disabled"/> if <see cref="Enabled"/> is <see langword="false"/>.
+		/// If it's overridden can return other values.</returns>
+		public virtual Attribute GetHotNormalColor ()
+		{
+			return Enabled ? ColorScheme.HotNormal : ColorScheme.Disabled;
+		}
+
 		/// <summary>
 		/// Get the top superview of a given <see cref="View"/>.
 		/// </summary>

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

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

+ 3 - 1
Terminal.Gui/Views/Menu.cs

@@ -1198,7 +1198,9 @@ namespace Terminal.Gui {
 		public virtual void OnMenuOpened ()
 		{
 			MenuItem mi = null;
-			if (openCurrentMenu.barItems.Children != null && openCurrentMenu?.current > -1) {
+			if (openCurrentMenu.barItems.Children != null && openCurrentMenu.barItems.Children.Length > 0
+				&& openCurrentMenu?.current > -1) {
+
 				mi = openCurrentMenu.barItems.Children [openCurrentMenu.current];
 			} else if (openCurrentMenu.barItems.IsTopLevel) {
 				mi = openCurrentMenu.barItems;

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

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

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

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

+ 220 - 28
Terminal.Gui/Views/TableView.cs

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

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

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

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

@@ -662,12 +662,17 @@ namespace Terminal.Gui {
 				X = Pos.Right (nameEntry) + 2,
 				Y = Pos.Top (nameEntry),
 				Width = Dim.Fill (1),
-				Height = allowedTypes != null ? allowedTypes.Count + 1 : 1,
+				Height = SetComboBoxHeight (allowedTypes),
 				Text = allowedTypes?.Count > 0 ? allowedTypes [0] : string.Empty,
-				ReadOnly = true
+				SelectedItem = allowedTypes?.Count > 0 ? 0 : -1,
+				ReadOnly = true,
+				HideDropdownListOnClick = true
 			};
 			cmbAllowedTypes.SetSource (allowedTypes ?? new List<string> ());
-			cmbAllowedTypes.OpenSelectedItem += (e) => AllowedFileTypes = cmbAllowedTypes.Text.ToString ().Split (';');
+			cmbAllowedTypes.OpenSelectedItem += (e) => {
+				dirListView.AllowedFileTypes = cmbAllowedTypes.Text.ToString ().Split (';');
+				dirListView.Reload ();
+			};
 			Add (cmbAllowedTypes);
 
 			dirListView = new DirListView (this) {
@@ -679,7 +684,7 @@ namespace Terminal.Gui {
 			DirectoryPath = Path.GetFullPath (Environment.CurrentDirectory);
 			Add (dirListView);
 
-			AllowedFileTypes = cmbAllowedTypes.Text.ToString ().Split (';');
+			AllowedFileTypes = allowedTypes?.Count > 0 ? allowedTypes?.ToArray () : null;
 			dirListView.DirectoryChanged = (dir) => { nameEntry.Text = ustring.Empty; dirEntry.Text = dir; };
 			dirListView.FileChanged = (file) => nameEntry.Text = file == ".." ? "" : file;
 			dirListView.SelectedChanged = (file) => nameEntry.Text = file.Item1 == ".." ? "" : file.Item1;
@@ -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;
 
 		///<inheritdoc/>
@@ -827,13 +837,24 @@ namespace Terminal.Gui {
 			}
 		}
 
+		private string [] allowedFileTypes;
+
 		/// <summary>
 		/// The array of filename extensions allowed, or null if all file extensions are allowed.
 		/// </summary>
 		/// <value>The allowed file types.</value>
 		public string [] AllowedFileTypes {
-			get => dirListView.AllowedFileTypes;
-			set => dirListView.AllowedFileTypes = value;
+			get => allowedFileTypes;
+			set {
+				allowedFileTypes = value;
+				var selected = cmbAllowedTypes.SelectedItem;
+				cmbAllowedTypes.SetSource (value);
+				cmbAllowedTypes.SelectedItem = selected > -1 ? selected : 0;
+				SetComboBoxHeight (value?.ToList ());
+				dirListView.AllowedFileTypes = value != null
+					? value [cmbAllowedTypes.SelectedItem].Split (';')
+					: null;
+			}
 		}
 
 		/// <summary>

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

@@ -303,7 +303,7 @@ namespace Terminal.Gui {
 
 			if (width == 0 & height == 0) {
 				// Dynamically size Width
-				d.Width = Math.Min (Math.Max (maxWidthLine, Math.Max (title.ConsoleWidth, Math.Max (textWidth + 2, d.GetButtonsWidth ()))), Application.Driver.Cols); // textWidth + (left + padding + padding + right)
+				d.Width = Math.Min (Math.Max (maxWidthLine, Math.Max (title.ConsoleWidth, Math.Max (textWidth + 2, d.GetButtonsWidth () + d.buttons.Count + 2))), Application.Driver.Cols); // textWidth + (left + padding + padding + right)
 			}
 
 			// Setup actions

+ 202 - 189
UICatalog/Scenarios/CsvEditor.cs

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

+ 1 - 1
UICatalog/Scenarios/DynamicMenuBar.cs

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

+ 1 - 1
UICatalog/Scenarios/Editor.cs

@@ -365,7 +365,7 @@ namespace UICatalog.Scenarios {
 			if (!CanCloseFile ()) {
 				return;
 			}
-			var aTypes = new List<string> () { ".txt;.bin;.xml;.json", ".txt", ".bin", ".xml", ".*" };
+			var aTypes = new List<string> () { ".txt;.bin;.xml;.json", ".txt", ".bin", ".xml", ".json", ".*" };
 			var d = new OpenDialog ("Open", "Choose the path where to open the file.", aTypes) { AllowsMultipleSelection = false };
 			Application.Run (d);
 

+ 3 - 3
UICatalog/Scenarios/RuneWidthGreaterThanOne.cs

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

+ 104 - 0
UICatalog/Scenarios/TableEditor.cs

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

+ 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 };
 			Win.Add (label);
-			var radioGroup = new RadioGroup (new ustring [] { "item #1", gitString, "Со_хранить" }, selected: 0) {
+			var radioGroup = new RadioGroup (new ustring [] { "item #1", gitString, "Со_хранить", "𝔽𝕆𝕆𝔹𝔸ℝ" }, selected: 0) {
 				X = 20,
 				Y = Pos.Y (label),
 				Width = Dim.Percent (60),

+ 2 - 2
UICatalog/UICatalog.cs

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

+ 3 - 0
UICatalog/UICatalog.csproj

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

+ 83 - 21
UnitTests/DialogTests.cs

@@ -142,15 +142,15 @@ namespace Terminal.Gui.Views {
 			Dialog dlg = null;
 			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
 			Assert.Equal (width, buttonRow.Length);
@@ -163,19 +163,26 @@ namespace Terminal.Gui.Views {
 			TestHelpers.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
-			//// Right
-			//buttonRow = $@"{d.VLine}  {btn1} {btn2}{d.VLine}";
-			//Assert.Equal (width, buttonRow.Length);
-			//(runstate, dlg) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btn1Text), new Button (btn2Text));
-			//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]
@@ -281,6 +288,61 @@ namespace Terminal.Gui.Views {
 			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]
 		[AutoInitShutdown]
 		public void ButtonAlignment_Four_Wider ()

+ 18 - 0
UnitTests/MenuTests.cs

@@ -1637,6 +1637,24 @@ Edit
 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 ()
 		{

+ 15 - 0
UnitTests/RadioGroupTests.cs

@@ -172,5 +172,20 @@ namespace Terminal.Gui.Views {
 			Assert.True (rg.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ())));
 			Assert.Equal (1, rg.SelectedItem);
 		}
+
+		[Fact]
+		public void ProcessColdKey_HotKey ()
+		{
+			var rg = new RadioGroup (new NStack.ustring [] { "Left", "Right", "Cen_tered", "Justified" });
+
+			Assert.True (rg.ProcessColdKey (new KeyEvent (Key.t, new KeyModifiers ())));
+			Assert.Equal (2, rg.SelectedItem);
+			Assert.True (rg.ProcessColdKey (new KeyEvent (Key.L, new KeyModifiers ())));
+			Assert.Equal (0, rg.SelectedItem);
+			Assert.True (rg.ProcessColdKey (new KeyEvent (Key.J, new KeyModifiers ())));
+			Assert.Equal (3, rg.SelectedItem);
+			Assert.True (rg.ProcessColdKey (new KeyEvent (Key.R, new KeyModifiers ())));
+			Assert.Equal (1, rg.SelectedItem);
+		}
 	}
 }

+ 3 - 4
UnitTests/StatusBarTests.cs

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

+ 503 - 0
UnitTests/TableViewTests.cs

@@ -1098,6 +1098,331 @@ namespace Terminal.Gui.Views {
 			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]
 		public void LongColumnTest ()
 		{
@@ -1338,5 +1663,183 @@ namespace Terminal.Gui.Views {
 
 			return dt;
 		}
+
+		[Fact, AutoInitShutdown]
+		public void Test_ScreenToCell ()
+		{
+			var tableView = GetTwoRowSixColumnTable ();
+
+			tableView.Redraw (tableView.Bounds);
+
+			// user can only scroll right so sees right indicator
+			// Because first column in table is A
+			string expected =
+				@"
+│A│B│C│
+├─┼─┼─►
+│1│2│3│
+│1│2│3│";
+
+			TestHelpers.AssertDriverContentsAre (expected, output);
+
+			// ---------------- X=0 -----------------------
+			// click is before first cell
+			Assert.Null (tableView.ScreenToCell (0, 0));
+			Assert.Null (tableView.ScreenToCell (0, 1));
+			Assert.Null (tableView.ScreenToCell (0, 2));
+			Assert.Null (tableView.ScreenToCell (0, 3));
+			Assert.Null (tableView.ScreenToCell (0, 4));
+
+			// ---------------- X=1 -----------------------
+			// click in header
+			Assert.Null (tableView.ScreenToCell (1, 0));
+			// click in header row line
+			Assert.Null (tableView.ScreenToCell (1, 1));
+			// click in cell 0,0
+			Assert.Equal (new Point(0,0),tableView.ScreenToCell (1, 2));
+			// click in cell 0,1
+			Assert.Equal (new Point (0, 1), tableView.ScreenToCell (1, 3));
+			// after last row
+			Assert.Null (tableView.ScreenToCell (1, 4));
+
+
+			// ---------------- X=2 -----------------------
+			// ( even though there is a horizontal dividing line here we treat it as a hit on the cell before)
+			// click in header
+			Assert.Null (tableView.ScreenToCell (2, 0));
+			// click in header row line
+			Assert.Null (tableView.ScreenToCell (2, 1));
+			// click in cell 0,0
+			Assert.Equal (new Point (0, 0), tableView.ScreenToCell (2, 2));
+			// click in cell 0,1
+			Assert.Equal (new Point (0, 1), tableView.ScreenToCell (2, 3));
+			// after last row
+			Assert.Null (tableView.ScreenToCell (2, 4));
+
+
+			// ---------------- X=3 -----------------------
+			// click in header
+			Assert.Null (tableView.ScreenToCell (3, 0));
+			// click in header row line
+			Assert.Null (tableView.ScreenToCell (3, 1));
+			// click in cell 1,0
+			Assert.Equal (new Point (1, 0), tableView.ScreenToCell (3, 2));
+			// click in cell 1,1
+			Assert.Equal (new Point (1, 1), tableView.ScreenToCell (3, 3));
+			// after last row
+			Assert.Null (tableView.ScreenToCell (3, 4));
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Test_ScreenToCell_DataColumnOverload ()
+		{
+			var tableView = GetTwoRowSixColumnTable ();
+
+			tableView.Redraw (tableView.Bounds);
+
+			// user can only scroll right so sees right indicator
+			// Because first column in table is A
+			string expected =
+				@"
+│A│B│C│
+├─┼─┼─►
+│1│2│3│
+│1│2│3│";
+
+			TestHelpers.AssertDriverContentsAre (expected, output);
+			DataColumn col;
+
+			// ---------------- X=0 -----------------------
+			// click is before first cell
+			Assert.Null (tableView.ScreenToCell (0, 0,out col));
+			Assert.Null (col);
+			Assert.Null (tableView.ScreenToCell (0, 1,out col));
+			Assert.Null (col);
+			Assert.Null (tableView.ScreenToCell (0, 2,out col));
+			Assert.Null (col);
+			Assert.Null (tableView.ScreenToCell (0, 3,out col));
+			Assert.Null (col);
+			Assert.Null (tableView.ScreenToCell (0, 4,out col));
+			Assert.Null (col);
+
+			// ---------------- X=1 -----------------------
+			// click in header
+			Assert.Null (tableView.ScreenToCell (1, 0, out col));
+			Assert.Equal ("A", col.ColumnName);
+			// click in header row line  (click in the horizontal line below header counts as click in header above - consistent with the column hit box)
+			Assert.Null (tableView.ScreenToCell (1, 1, out col));
+			Assert.Equal ("A", col.ColumnName);
+			// click in cell 0,0
+			Assert.Equal (new Point (0, 0), tableView.ScreenToCell (1, 2, out col));
+			Assert.Null (col);
+			// click in cell 0,1
+			Assert.Equal (new Point (0, 1), tableView.ScreenToCell (1, 3, out col));
+			Assert.Null (col);
+			// after last row
+			Assert.Null (tableView.ScreenToCell (1, 4, out col));
+			Assert.Null (col);
+
+
+			// ---------------- X=2 -----------------------
+			// click in header
+			Assert.Null (tableView.ScreenToCell (2, 0, out col));
+			Assert.Equal ("A", col.ColumnName);
+			// click in header row line
+			Assert.Null (tableView.ScreenToCell (2, 1, out col));
+			Assert.Equal ("A", col.ColumnName);
+			// click in cell 0,0
+			Assert.Equal (new Point (0, 0), tableView.ScreenToCell (2, 2, out col));
+			Assert.Null (col);
+			// click in cell 0,1
+			Assert.Equal (new Point (0, 1), tableView.ScreenToCell (2, 3, out col));
+			Assert.Null (col);
+			// after last row
+			Assert.Null (tableView.ScreenToCell (2, 4, out col));
+			Assert.Null (col);
+
+
+			// ---------------- X=3 -----------------------
+			// click in header
+			Assert.Null (tableView.ScreenToCell (3, 0, out col));
+			Assert.Equal ("B", col.ColumnName);
+			// click in header row line
+			Assert.Null (tableView.ScreenToCell (3, 1, out col));
+			Assert.Equal ("B", col.ColumnName);
+			// click in cell 1,0
+			Assert.Equal (new Point (1, 0), tableView.ScreenToCell (3, 2, out col));
+			Assert.Null (col);
+			// click in cell 1,1
+			Assert.Equal (new Point (1, 1), tableView.ScreenToCell (3, 3, out col));
+			Assert.Null (col);
+			// after last row
+			Assert.Null (tableView.ScreenToCell (3, 4, out col));
+			Assert.Null (col);
+		}
+		private TableView GetTwoRowSixColumnTable ()
+		{
+			var tableView = new TableView ();
+			tableView.ColorScheme = Colors.TopLevel;
+
+			// 3 columns are visible
+			tableView.Bounds = new Rect (0, 0, 7, 5);
+			tableView.Style.ShowHorizontalHeaderUnderline = true;
+			tableView.Style.ShowHorizontalHeaderOverline = false;
+			tableView.Style.AlwaysShowHeaders = true;
+			tableView.Style.SmoothHorizontalScrolling = true;
+
+			var 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);
+			dt.Rows.Add (1, 2, 3, 4, 5, 6);
+
+			tableView.Table = dt;
+			return tableView;
+		}
 	}
 }

+ 26 - 0
UnitTests/TextFormatterTests.cs

@@ -4137,5 +4137,31 @@ This TextFormatter (tf2) is rewritten.
 			text = $"First Line 界\nSecond Line 界\nThird Line 界\n";
 			Assert.Equal (14, TextFormatter.MaxWidthLine (text));
 		}
+
+		[Fact]
+		public void Ustring_Array_Is_Not_Equal_ToRunes_Array_And_String_Array ()
+		{
+			var text = "New Test 你";
+			ustring us = text;
+			string s = text;
+			Assert.Equal (10, us.RuneCount);
+			Assert.Equal (10, s.Length);
+			// The reason is ustring index is related to byte length and not rune length
+			Assert.Equal (12, us.Length);
+			Assert.NotEqual (20320, us [9]);
+			Assert.Equal (20320, s [9]);
+			Assert.Equal (228, us [9]);
+			Assert.Equal ("ä", ((Rune)us [9]).ToString ());
+			Assert.Equal ("你", s [9].ToString ());
+
+			// Rune array is equal to string array
+			var usToRunes = us.ToRunes ();
+			Assert.Equal (10, usToRunes.Length);
+			Assert.Equal (10, s.Length);
+			Assert.Equal (20320, (int)usToRunes [9]);
+			Assert.Equal (20320, s [9]);
+			Assert.Equal ("你", ((Rune)usToRunes [9]).ToString ());
+			Assert.Equal ("你", s [9].ToString ());
+		}
 	}
 }

+ 143 - 1
UnitTests/ToplevelTests.cs

@@ -671,7 +671,7 @@ namespace Terminal.Gui.Core {
 		}
 
 		[Fact, AutoInitShutdown]
-		public void Mouse_Drag ()
+		public void Mouse_Drag_On_Top_With_Superview_Null ()
 		{
 			var menu = new MenuBar (new MenuBarItem [] {
 				new MenuBarItem("File", new MenuItem [] {
@@ -825,5 +825,147 @@ namespace Terminal.Gui.Core {
 
 			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

@@ -19,7 +19,7 @@
   </PropertyGroup>
   <ItemGroup>
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
-    <PackageReference Include="ReportGenerator" Version="5.1.11" />
+    <PackageReference Include="ReportGenerator" Version="5.1.12" />
     <PackageReference Include="System.Collections" Version="4.3.0" />
     <PackageReference Include="xunit" Version="2.4.2" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">

+ 55 - 0
UnitTests/ViewTests.cs

@@ -4062,5 +4062,60 @@ This is a tes
 			Assert.False (view.IsKeyPress);
 			Assert.True (view.IsKeyUp);
 		}
+
+		[Fact, AutoInitShutdown]
+		public void SetHasFocus_Do_Not_Throws_If_OnLeave_Remove_Focused_Changing_To_Null ()
+		{
+			var view1Leave = false;
+			var subView1Leave = false;
+			var subView1subView1Leave = false;
+			var top = Application.Top;
+			var view1 = new View { CanFocus = true };
+			var subView1 = new View { CanFocus = true };
+			var subView1subView1 = new View { CanFocus = true };
+			view1.Leave += (e) => {
+				view1Leave = true;
+			};
+			subView1.Leave += (e) => {
+				subView1.Remove (subView1subView1);
+				subView1Leave = true;
+			};
+			view1.Add (subView1);
+			subView1subView1.Leave += (e) => {
+				// This is never invoked
+				subView1subView1Leave = true;
+			};
+			subView1.Add (subView1subView1);
+			var view2 = new View { CanFocus = true };
+			top.Add (view1, view2);
+			Application.Begin (top);
+
+			view2.SetFocus ();
+			Assert.True (view1Leave);
+			Assert.True (subView1Leave);
+			Assert.False (subView1subView1Leave);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void GetNormalColor_ColorScheme ()
+		{
+			var view = new View { ColorScheme = Colors.Base };
+
+			Assert.Equal (view.ColorScheme.Normal, view.GetNormalColor ());
+
+			view.Enabled = false;
+			Assert.Equal (view.ColorScheme.Disabled, view.GetNormalColor ());
+		}
+
+		[Fact, AutoInitShutdown]
+		public void GetHotNormalColor_ColorScheme ()
+		{
+			var view = new View { ColorScheme = Colors.Base };
+
+			Assert.Equal (view.ColorScheme.HotNormal, view.GetHotNormalColor ());
+
+			view.Enabled = false;
+			Assert.Equal (view.ColorScheme.Disabled, view.GetHotNormalColor ());
+		}
 	}
 }

+ 83 - 0
UnitTests/WindowTests.cs

@@ -153,5 +153,88 @@ namespace Terminal.Gui.Core {
 			Assert.Equal (expected, r.Title.ToString ());
 			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);
+
+		}
 	}
 }