Procházet zdrojové kódy

Merge remote-tracking branch 'upstream/main' into main

Thomas Nind před 4 roky
rodič
revize
df7a7cdafe

+ 2 - 2
ReactiveExample/ReactiveExample.csproj

@@ -7,7 +7,7 @@
         <PackageReference Include="Pharmacist.MsBuild" Version="2.0.8" PrivateAssets="all" />
         <PackageReference Include="Pharmacist.Common" Version="2.0.8" />
         <PackageReference Include="Terminal.Gui" Version="1.0.0.*" />
-        <PackageReference Include="ReactiveUI.Fody" Version="13.2.18" />
-        <PackageReference Include="ReactiveUI" Version="13.2.18" />
+        <PackageReference Include="ReactiveUI.Fody" Version="13.3.2" />
+        <PackageReference Include="ReactiveUI" Version="13.3.2" />
     </ItemGroup>
 </Project>

+ 9 - 0
Terminal.Gui/Core/Application.cs

@@ -110,6 +110,15 @@ namespace Terminal.Gui {
 			}
 		}
 
+		/// <summary>
+		/// Alternative key to navigate forwards through all views. Ctrl+Tab is always used.
+		/// </summary>
+		public static Key AlternateForwardKey { get; set; } = Key.PageDown | Key.CtrlMask;
+		/// <summary>
+		/// Alternative key to navigate backwards through all views. Shift+Ctrl+Tab is always used.
+		/// </summary>
+		public static Key AlternateBackwardKey { get; set; } = Key.PageUp | Key.CtrlMask;
+
 		/// <summary>
 		/// The <see cref="MainLoop"/>  driver for the application
 		/// </summary>

+ 43 - 10
Terminal.Gui/Core/PosDim.cs

@@ -242,13 +242,12 @@ namespace Terminal.Gui {
 		/// <returns>The <see cref="Pos"/> that is the sum of the values of <c>left</c> and <c>right</c>.</returns>
 		public static Pos operator + (Pos left, Pos right)
 		{
-			PosCombine newPos = new PosCombine (true, left, right);
-			if (posCombine?.ToString () != newPos.ToString ()) {
-				var view = left as PosView;
-				if (view != null) {
-					view.Target.SetNeedsLayout ();
-				}
+			if (left is PosAbsolute && right is PosAbsolute) {
+				posCombine = null;
+				return new PosAbsolute (left.Anchor (0) + right.Anchor (0));
 			}
+			PosCombine newPos = new PosCombine (true, left, right);
+			SetPosCombine (left, newPos);
 			return posCombine = newPos;
 		}
 
@@ -260,13 +259,23 @@ namespace Terminal.Gui {
 		/// <returns>The <see cref="Pos"/> that is the <c>left</c> minus <c>right</c>.</returns>
 		public static Pos operator - (Pos left, Pos right)
 		{
+			if (left is PosAbsolute && right is PosAbsolute) {
+				posCombine = null;
+				return new PosAbsolute (left.Anchor (0) - right.Anchor (0));
+			}
 			PosCombine newPos = new PosCombine (false, left, right);
+			SetPosCombine (left, newPos);
+			return posCombine = newPos;
+		}
+
+		static void SetPosCombine (Pos left, PosCombine newPos)
+		{
 			if (posCombine?.ToString () != newPos.ToString ()) {
 				var view = left as PosView;
-				if (view != null)
+				if (view != null) {
 					view.Target.SetNeedsLayout ();
+				}
 			}
-			return posCombine = newPos;
 		}
 
 		internal class PosView : Pos {
@@ -526,6 +535,8 @@ namespace Terminal.Gui {
 
 		}
 
+		static DimCombine dimCombine;
+
 		/// <summary>
 		/// Adds a <see cref="Terminal.Gui.Dim"/> to a <see cref="Terminal.Gui.Dim"/>, yielding a new <see cref="Dim"/>.
 		/// </summary>
@@ -534,7 +545,13 @@ namespace Terminal.Gui {
 		/// <returns>The <see cref="Dim"/> that is the sum of the values of <c>left</c> and <c>right</c>.</returns>
 		public static Dim operator + (Dim left, Dim right)
 		{
-			return new DimCombine (true, left, right);
+			if (left is DimAbsolute && right is DimAbsolute) {
+				dimCombine = null;
+				return new DimAbsolute (left.Anchor (0) + right.Anchor (0));
+			}
+			DimCombine newDim = new DimCombine (true, left, right);
+			SetDimCombine (left, newDim);
+			return dimCombine = newDim;
 		}
 
 		/// <summary>
@@ -545,7 +562,23 @@ namespace Terminal.Gui {
 		/// <returns>The <see cref="Dim"/> that is the <c>left</c> minus <c>right</c>.</returns>
 		public static Dim operator - (Dim left, Dim right)
 		{
-			return new DimCombine (false, left, right);
+			if (left is DimAbsolute && right is DimAbsolute) {
+				dimCombine = null;
+				return new DimAbsolute (left.Anchor (0) - right.Anchor (0));
+			}
+			DimCombine newDim = new DimCombine (false, left, right);
+			SetDimCombine (left, newDim);
+			return dimCombine = newDim;
+		}
+
+		static void SetDimCombine (Dim left, DimCombine newPos)
+		{
+			if (dimCombine?.ToString () != newPos.ToString ()) {
+				var view = left as DimView;
+				if (view != null) {
+					view.Target.SetNeedsLayout ();
+				}
+			}
 		}
 
 		internal class DimView : Dim {

+ 8 - 0
Terminal.Gui/Core/Toplevel.cs

@@ -238,10 +238,18 @@ namespace Terminal.Gui {
 				}
 				return true;
 			case Key.Tab | Key.CtrlMask:
+			case Key key when key == Application.AlternateForwardKey: // Needed on Unix
 				Application.Top.FocusNext ();
+				if (Application.Top.Focused == null) {
+					Application.Top.FocusNext ();
+				}
 				return true;
 			case Key.Tab | Key.ShiftMask | Key.CtrlMask:
+			case Key key when key == Application.AlternateBackwardKey: // Needed on Unix
 				Application.Top.FocusPrev ();
+				if (Application.Top.Focused == null) {
+					Application.Top.FocusPrev ();
+				}
 				return true;
 			case Key.L | Key.CtrlMask:
 				Application.Refresh ();

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

@@ -8,7 +8,7 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.1" PrivateAssets="true" />
+    <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.2" PrivateAssets="true" />
     <PackageReference Include="NStack.Core" Version="0.16.0" />
     <PackageReference Include="MinVer" Version="2.5.0">
       <PrivateAssets>all</PrivateAssets>

+ 61 - 32
Terminal.Gui/Views/TableView.cs

@@ -300,10 +300,15 @@ namespace Terminal.Gui {
 					// if the next column is the start of a header
 					else if(columnsToRender.Any(r=>r.X == c+1)){
 						rune = Driver.TopTee;
-					}
+					} 
 					else if(c == availableWidth -1){
 						rune = Driver.URCorner;
 					}
+					// if the next console column is the lastcolumns end
+					else if ( Style.ExpandLastColumn == false &&
+						 columnsToRender.Any (r => r.IsVeryLast && r.X + r.Width-1 == c)) {
+						rune = Driver.TopTee;
+					}
 				}
 
 				AddRuneAt(Driver,c,row,rune);
@@ -324,45 +329,26 @@ namespace Terminal.Gui {
 			for(int i =0 ; i<columnsToRender.Length;i++) {
 				
 				var current =  columnsToRender[i];
-				var availableWidthForCell = GetCellWidth(columnsToRender,i);
 
 				var colStyle = Style.GetColumnStyleIfAny(current.Column);
 				var colName = current.Column.ColumnName;
 
 				RenderSeparator(current.X-1,row,true);
-									
+
 				Move (current.X, row);
 				
-				Driver.AddStr(TruncateOrPad(colName,colName,availableWidthForCell ,colStyle));
+				Driver.AddStr(TruncateOrPad(colName,colName,current.Width ,colStyle));
 
+				if (Style.ExpandLastColumn == false && current.IsVeryLast) {
+					RenderSeparator (current.X + current.Width-1, row, true);
+				}
 			}
 
 			//render end of line
-			if(style.ShowVerticalHeaderLines)
+			if (style.ShowVerticalHeaderLines)
 				AddRune(Bounds.Width-1,row,Driver.VLine);
 		}
-
-		/// <summary>
-		/// Calculates how much space is available to render index <paramref name="i"/> of the <paramref name="columnsToRender"/> given the remaining horizontal space
-		/// </summary>
-		/// <param name="columnsToRender"></param>
-		/// <param name="i"></param>
-		private int GetCellWidth (ColumnToRender [] columnsToRender, int i)
-		{
-			var current =  columnsToRender[i];
-			var next = i+1 < columnsToRender.Length ? columnsToRender[i+1] : null;
-
-			if(next == null) {
-				// cell can fill to end of the line
-				return Bounds.Width - current.X;
-			}
-			else {
-				// cell can fill up to next cell start				
-				return next.X - current.X;
-			}
-
-		}
-
+	
 		private void RenderHeaderUnderline(int row,int availableWidth, ColumnToRender[] columnsToRender)
 		{
 			// Renders a line below the table headers (when visible) like:
@@ -385,6 +371,11 @@ namespace Terminal.Gui {
 					else if(c == availableWidth -1){
 						rune = Style.ShowVerticalCellLines ? Driver.RightTee : Driver.LRCorner;
 					}
+					// if the next console column is the lastcolumns end
+					else if (Style.ExpandLastColumn == false &&
+							columnsToRender.Any (r => r.IsVeryLast && r.X + r.Width-1 == c)) {
+						rune = Style.ShowVerticalCellLines ? '┼' : Driver.BottomTee;
+					} 
 				}
 
 				AddRuneAt(Driver,c,row,rune);
@@ -397,11 +388,15 @@ namespace Terminal.Gui {
 			if(style.ShowVerticalCellLines)
 				AddRune(0,row,Driver.VLine);
 
+			//start by clearing the entire line
+			Move (0,row);
+			Driver.SetAttribute (FullRowSelect &&  IsSelected(0,rowToRender) ? ColorScheme.HotFocus : ColorScheme.Normal);
+			Driver.AddStr (new string(' ',Bounds.Width));
+
 			// Render cells for each visible header for the current row
 			for(int i=0;i< columnsToRender.Length ;i++) {
 
 				var current = columnsToRender[i];
-				var availableWidthForCell = GetCellWidth(columnsToRender,i);
 
 				var colStyle = Style.GetColumnStyleIfAny(current.Column);
 
@@ -418,13 +413,17 @@ namespace Terminal.Gui {
 				// Render the (possibly truncated) cell value
 				var representation = GetRepresentation(val,colStyle);
 				
-				Driver.AddStr (TruncateOrPad(val,representation,availableWidthForCell,colStyle));
+				Driver.AddStr (TruncateOrPad(val,representation, current.Width, colStyle));
 				
 				// If not in full row select mode always, reset color scheme to normal and render the vertical line (or space) at the end of the cell
 				if(!FullRowSelect)
 					Driver.SetAttribute (ColorScheme.Normal);
 
 				RenderSeparator(current.X-1,row,false);
+
+				if (Style.ExpandLastColumn == false && current.IsVeryLast) {
+					RenderSeparator (current.X + current.Width-1, row, false);
+				}
 			}
 
 			//render end of line
@@ -1019,21 +1018,26 @@ namespace Terminal.Gui {
 				rowsToRender -= GetHeaderHeight(); 
 
 			bool first = true;
+			var lastColumn = Table.Columns.Cast<DataColumn> ().Last ();
 
 			foreach (var col in Table.Columns.Cast<DataColumn>().Skip (ColumnOffset)) {
 
 				int startingIdxForCurrentHeader = usedSpace;
 				var colStyle = Style.GetColumnStyleIfAny(col);
+				int colWidth;
 
 				// is there enough space for this column (and it's data)?
-				usedSpace += CalculateMaxCellWidth (col, rowsToRender,colStyle) + padding;
+				usedSpace += colWidth = CalculateMaxCellWidth (col, rowsToRender,colStyle) + padding;
 
 				// no (don't render it) unless its the only column we are render (that must be one massively wide column!)
 				if (!first && usedSpace > availableHorizontalSpace)
 					yield break;
 
 				// there is space
-				yield return new ColumnToRender(col, startingIdxForCurrentHeader);
+				yield return new ColumnToRender(col, startingIdxForCurrentHeader, 
+					// required for if we end up here because first == true i.e. we have a single massive width (overspilling bounds) column to present
+					Math.Min(availableHorizontalSpace,colWidth),
+					lastColumn == col);
 				first=false;
 			}
 		}
@@ -1215,6 +1219,17 @@ namespace Terminal.Gui {
 			/// </summary>
 			public Dictionary<DataColumn, ColumnStyle> ColumnStyles { get; set; } = new Dictionary<DataColumn, ColumnStyle> ();
 
+
+			/// <summary>
+			/// Determines rendering when the last column in the table is visible but it's
+			/// content or <see cref="ColumnStyle.MaxWidth"/> is less than the remaining 
+			/// space in the control.  True (the default) will expand the column to fill
+			/// the remaining bounds of the control.  False will draw a column ending line
+			/// and leave a blank column that cannot be selected in the remaining space.  
+			/// </summary>
+			/// <value></value>
+			public bool ExpandLastColumn {get;set;} = true;
+			
 			/// <summary>
 			/// Returns the entry from <see cref="ColumnStyles"/> for the given <paramref name="col"/> or null if no custom styling is defined for it
 			/// </summary>
@@ -1254,11 +1269,25 @@ namespace Terminal.Gui {
 			/// </summary>
 			public int X { get; set; }
 
-			public ColumnToRender (DataColumn col, int x)
+			/// <summary>
+			/// The width that the column should occupy as calculated by <see cref="CalculateViewport(Rect, int)"/>.  Note that this includes
+			/// space for padding i.e. the separator between columns.
+			/// </summary>
+			public int Width { get; }
+
+			/// <summary>
+			/// True if this column is the very last column in the <see cref="Table"/> (not just the last visible column)
+			/// </summary>
+			public bool IsVeryLast { get; }
+
+			public ColumnToRender (DataColumn col, int x, int width, bool isVeryLast)
 			{
 				Column = col;
 				X = x;
+				Width = width;
+				IsVeryLast = isVeryLast;
 			}
+
 		}
 
 		/// <summary>

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

@@ -2017,7 +2017,7 @@ namespace Terminal.Gui {
 					StopSelecting ();
 				}
 				int nPageDnShift = Frame.Height - 1;
-				if (currentRow > 0 && currentRow < model.Count) {
+				if (currentRow >= 0 && currentRow < model.Count) {
 					if (columnTrack == -1)
 						columnTrack = currentColumn;
 					currentRow = (currentRow + nPageDnShift) > model.Count

+ 11 - 0
UICatalog/Scenarios/TableEditor.cs

@@ -22,6 +22,7 @@ namespace UICatalog.Scenarios {
 		private MenuItem miHeaderUnderline;
 		private MenuItem miCellLines;
 		private MenuItem miFullRowSelect;
+		private MenuItem miExpandLastColumn;
 
 		public override void Setup ()
 		{
@@ -51,6 +52,7 @@ namespace UICatalog.Scenarios {
 					miHeaderUnderline =new MenuItem ("_HeaderUnderLine", "", () => ToggleUnderline()){Checked = tableView.Style.ShowHorizontalHeaderUnderline, CheckType = MenuItemCheckStyle.Checked },
 					miFullRowSelect =new MenuItem ("_FullRowSelect", "", () => ToggleFullRowSelect()){Checked = tableView.FullRowSelect, CheckType = MenuItemCheckStyle.Checked },
 					miCellLines =new MenuItem ("_CellLines", "", () => ToggleCellLines()){Checked = tableView.Style.ShowVerticalCellLines, CheckType = MenuItemCheckStyle.Checked },
+					miExpandLastColumn = new MenuItem ("_ExpandLastColumn", "", () => ToggleExpandLastColumn()){Checked = tableView.Style.ExpandLastColumn, CheckType = MenuItemCheckStyle.Checked },
 					new MenuItem ("_AllLines", "", () => ToggleAllCellLines()),
 					new MenuItem ("_NoLines", "", () => ToggleNoCellLines()),
 					new MenuItem ("_ClearColumnStyles", "", () => ClearColumnStyles()),
@@ -181,6 +183,15 @@ namespace UICatalog.Scenarios {
 			tableView.FullRowSelect= miFullRowSelect.Checked;
 			tableView.Update();
 		}
+
+		private void ToggleExpandLastColumn()
+		{
+			miExpandLastColumn.Checked = !miExpandLastColumn.Checked;
+			tableView.Style.ExpandLastColumn = miExpandLastColumn.Checked;
+
+			tableView.Update();
+
+		}
 		private void ToggleCellLines()
 		{
 			miCellLines.Checked = !miCellLines.Checked;

+ 104 - 3
UnitTests/ApplicationTests.cs

@@ -4,7 +4,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using Xunit;
 
-// Alais Console to MockConsole so we don't accidentally use Console
+// Alias Console to MockConsole so we don't accidentally use Console
 using Console = Terminal.Gui.FakeConsole;
 
 namespace Terminal.Gui.Core {
@@ -19,7 +19,7 @@ namespace Terminal.Gui.Core {
 		[Fact]
 		public void Init_Shutdown_Cleans_Up ()
 		{
-			// Verify inital state is per spec
+			// Verify initial state is per spec
 			Pre_Init_State ();
 
 			Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
@@ -175,7 +175,7 @@ namespace Terminal.Gui.Core {
 			// Setup Mock driver
 			Init ();
 
-			// Setup some fake kepresses (This)
+			// Setup some fake keypresses (This)
 			var input = "Tests";
 
 			// Put a control-q in at the end
@@ -268,5 +268,106 @@ namespace Terminal.Gui.Core {
 			Application.Shutdown ();
 			Assert.Null (SynchronizationContext.Current);
 		}
+
+		[Fact]
+		public void AlternateForwardKey_AlternateBackwardKey_Tests ()
+		{
+			Init ();
+
+			var top = Application.Top;
+			var w1 = new Window ();
+			var v1 = new TextField ();
+			var v2 = new TextView ();
+			w1.Add (v1, v2);
+
+			var w2 = new Window ();
+			var v3 = new CheckBox ();
+			var v4 = new Button ();
+			w2.Add (v3, v4);
+
+			top.Add (w1, w2);
+
+			Application.Iteration += () => {
+				Assert.True (v1.HasFocus);
+				// Using default keys.
+				top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.Tab,
+					new KeyModifiers () { Ctrl = true }));
+				Assert.True (v2.HasFocus);
+				top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.Tab,
+					new KeyModifiers () { Ctrl = true }));
+				Assert.True (v3.HasFocus);
+				top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.Tab,
+					new KeyModifiers () { Ctrl = true }));
+				Assert.True (v4.HasFocus);
+				top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.Tab,
+					new KeyModifiers () { Ctrl = true }));
+				Assert.True (v1.HasFocus);
+
+				top.ProcessKey (new KeyEvent (Key.ShiftMask | Key.CtrlMask | Key.Tab,
+					new KeyModifiers () { Shift = true, Ctrl = true }));
+				Assert.True (v4.HasFocus);
+				top.ProcessKey (new KeyEvent (Key.ShiftMask | Key.CtrlMask | Key.Tab,
+					new KeyModifiers () { Shift = true, Ctrl = true }));
+				Assert.True (v3.HasFocus);
+				top.ProcessKey (new KeyEvent (Key.ShiftMask | Key.CtrlMask | Key.Tab,
+					new KeyModifiers () { Shift = true, Ctrl = true }));
+				Assert.True (v2.HasFocus);
+				top.ProcessKey (new KeyEvent (Key.ShiftMask | Key.CtrlMask | Key.Tab,
+					new KeyModifiers () { Shift = true, Ctrl = true }));
+				Assert.True (v1.HasFocus);
+
+				top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.PageDown,
+					new KeyModifiers () { Ctrl = true }));
+				Assert.True (v2.HasFocus);
+				top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.PageDown,
+					new KeyModifiers () { Ctrl = true }));
+				Assert.True (v3.HasFocus);
+				top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.PageDown,
+					new KeyModifiers () { Ctrl = true }));
+				Assert.True (v4.HasFocus);
+				top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.PageDown,
+					new KeyModifiers () { Ctrl = true }));
+				Assert.True (v1.HasFocus);
+
+				top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.PageUp,
+					new KeyModifiers () { Ctrl = true }));
+				Assert.True (v4.HasFocus);
+				top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.PageUp,
+					new KeyModifiers () { Ctrl = true }));
+				Assert.True (v3.HasFocus);
+				top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.PageUp,
+					new KeyModifiers () { Ctrl = true }));
+				Assert.True (v2.HasFocus);
+				top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.PageUp,
+					new KeyModifiers () { Ctrl = true }));
+				Assert.True (v1.HasFocus);
+
+				// Using another's alternate keys.
+				Application.AlternateForwardKey = Key.F7;
+				Application.AlternateBackwardKey = Key.F6;
+
+				top.ProcessKey (new KeyEvent (Key.F7, new KeyModifiers ()));
+				Assert.True (v2.HasFocus);
+				top.ProcessKey (new KeyEvent (Key.F7, new KeyModifiers ()));
+				Assert.True (v3.HasFocus);
+				top.ProcessKey (new KeyEvent (Key.F7, new KeyModifiers ()));
+				Assert.True (v4.HasFocus);
+				top.ProcessKey (new KeyEvent (Key.F7, new KeyModifiers ()));
+				Assert.True (v1.HasFocus);
+
+				top.ProcessKey (new KeyEvent (Key.F6, new KeyModifiers ()));
+				Assert.True (v4.HasFocus);
+				top.ProcessKey (new KeyEvent (Key.F6, new KeyModifiers ()));
+				Assert.True (v3.HasFocus);
+				top.ProcessKey (new KeyEvent (Key.F6, new KeyModifiers ()));
+				Assert.True (v2.HasFocus);
+				top.ProcessKey (new KeyEvent (Key.F6, new KeyModifiers ()));
+				Assert.True (v1.HasFocus);
+
+				Application.RequestStop ();
+			};
+
+			Application.Run (top);
+		}
 	}
 }

+ 104 - 0
UnitTests/DimTests.cs

@@ -590,5 +590,109 @@ namespace Terminal.Gui.Core {
 			Assert.Throws<InvalidOperationException> (() => Application.Run ());
 			Application.Shutdown ();
 		}
+
+
+		[Fact]
+		public void Dim_Add_Operator ()
+		{
+
+			Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+
+			var top = Application.Top;
+
+			var view = new View () { X = 0, Y = 0, Width = 20, Height = 0 };
+			var field = new TextField () { X = 0, Y = Pos.Bottom (view), Width = 20 };
+			var count = 0;
+
+			field.KeyDown += (k) => {
+				if (k.KeyEvent.Key == Key.Enter) {
+					field.Text = $"Label {count}";
+					var label = new Label (field.Text) { X = 0, Y = view.Bounds.Height, Width = 20 };
+					view.Add (label);
+					Assert.Equal ($"Label {count}", label.Text);
+					Assert.Equal ($"Pos.Absolute({count})", label.Y.ToString ());
+
+					Assert.Equal ($"Dim.Absolute({count})", view.Height.ToString ());
+					view.Height += 1;
+					count++;
+					Assert.Equal ($"Dim.Absolute({count})", view.Height.ToString ());
+				}
+			};
+
+			Application.Iteration += () => {
+				while (count < 20) {
+					field.OnKeyDown (new KeyEvent (Key.Enter, new KeyModifiers ()));
+				}
+
+				Application.RequestStop ();
+			};
+
+			var win = new Window ();
+			win.Add (view);
+			win.Add (field);
+
+			top.Add (win);
+
+			Application.Run (top);
+
+			Assert.Equal (20, count);
+		}
+
+		[Fact]
+		public void Dim_Subtract_Operator ()
+		{
+
+			Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+
+			var top = Application.Top;
+
+			var view = new View () { X = 0, Y = 0, Width = 20, Height = 0 };
+			var field = new TextField () { X = 0, Y = Pos.Bottom (view), Width = 20 };
+			var count = 20;
+			var listLabels = new List<Label> ();
+
+			for (int i = 0; i < count; i++) {
+				field.Text = $"Label {i}";
+				var label = new Label (field.Text) { X = 0, Y = view.Bounds.Height, Width = 20 };
+				view.Add (label);
+				Assert.Equal ($"Label {i}", label.Text);
+				Assert.Equal ($"Pos.Absolute({i})", label.Y.ToString ());
+				listLabels.Add (label);
+
+				Assert.Equal ($"Dim.Absolute({i})", view.Height.ToString ());
+				view.Height += 1;
+				Assert.Equal ($"Dim.Absolute({i + 1})", view.Height.ToString ());
+			}
+
+			field.KeyDown += (k) => {
+				if (k.KeyEvent.Key == Key.Enter) {
+					Assert.Equal ($"Label {count - 1}", listLabels [count - 1].Text);
+					view.Remove (listLabels [count - 1]);
+
+					Assert.Equal ($"Dim.Absolute({count})", view.Height.ToString ());
+					view.Height -= 1;
+					count--;
+					Assert.Equal ($"Dim.Absolute({count})", view.Height.ToString ());
+				}
+			};
+
+			Application.Iteration += () => {
+				while (count > 0) {
+					field.OnKeyDown (new KeyEvent (Key.Enter, new KeyModifiers ()));
+				}
+
+				Application.RequestStop ();
+			};
+
+			var win = new Window ();
+			win.Add (view);
+			win.Add (field);
+
+			top.Add (win);
+
+			Application.Run (top);
+
+			Assert.Equal (0, count);
+		}
 	}
 }

+ 103 - 0
UnitTests/PosTests.cs

@@ -536,5 +536,108 @@ namespace Terminal.Gui.Core {
 			Assert.Throws<InvalidOperationException> (() => Application.Run ());
 			Application.Shutdown ();
 		}
+
+		[Fact]
+		public void Pos_Add_Operator ()
+		{
+
+			Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+
+			var top = Application.Top;
+
+			var view = new View () { X = 0, Y = 0, Width = 20, Height = 20 };
+			var field = new TextField () { X = 0, Y = 0, Width = 20 };
+			var count = 0;
+
+			field.KeyDown += (k) => {
+				if (k.KeyEvent.Key == Key.Enter) {
+					field.Text = $"Label {count}";
+					var label = new Label (field.Text) { X = 0, Y = field.Y, Width = 20 };
+					view.Add (label);
+					Assert.Equal ($"Label {count}", label.Text);
+					Assert.Equal ($"Pos.Absolute({count})", label.Y.ToString ());
+
+					Assert.Equal ($"Pos.Absolute({count})", field.Y.ToString ());
+					field.Y += 1;
+					count++;
+					Assert.Equal ($"Pos.Absolute({count})", field.Y.ToString ());
+				}
+			};
+
+			Application.Iteration += () => {
+				while (count < 20) {
+					field.OnKeyDown (new KeyEvent (Key.Enter, new KeyModifiers ()));
+				}
+
+				Application.RequestStop ();
+			};
+
+			var win = new Window ();
+			win.Add (view);
+			win.Add (field);
+
+			top.Add (win);
+
+			Application.Run (top);
+
+			Assert.Equal (20, count);
+		}
+
+		[Fact]
+		public void Pos_Subtract_Operator ()
+		{
+
+			Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+
+			var top = Application.Top;
+
+			var view = new View () { X = 0, Y = 0, Width = 20, Height = 20 };
+			var field = new TextField () { X = 0, Y = 0, Width = 20 };
+			var count = 20;
+			var listLabels = new List<Label> ();
+
+			for (int i = 0; i < count; i++) {
+				field.Text = $"Label {i}";
+				var label = new Label (field.Text) { X = 0, Y = field.Y, Width = 20 };
+				view.Add (label);
+				Assert.Equal ($"Label {i}", label.Text);
+				Assert.Equal ($"Pos.Absolute({i})", field.Y.ToString ());
+				listLabels.Add (label);
+
+				Assert.Equal ($"Pos.Absolute({i})", field.Y.ToString ());
+				field.Y += 1;
+				Assert.Equal ($"Pos.Absolute({i + 1})", field.Y.ToString ());
+			}
+
+			field.KeyDown += (k) => {
+				if (k.KeyEvent.Key == Key.Enter) {
+					Assert.Equal ($"Label {count - 1}", listLabels [count - 1].Text);
+					view.Remove (listLabels [count - 1]);
+
+					Assert.Equal ($"Pos.Absolute({count})", field.Y.ToString ());
+					field.Y -= 1;
+					count--;
+					Assert.Equal ($"Pos.Absolute({count})", field.Y.ToString ());
+				}
+			};
+
+			Application.Iteration += () => {
+				while (count > 0) {
+					field.OnKeyDown (new KeyEvent (Key.Enter, new KeyModifiers ()));
+				}
+
+				Application.RequestStop ();
+			};
+
+			var win = new Window ();
+			win.Add (view);
+			win.Add (field);
+
+			top.Add (win);
+
+			Application.Run (top);
+
+			Assert.Equal (0, count);
+		}
 	}
 }

+ 86 - 1
UnitTests/TableViewTests.cs

@@ -418,7 +418,92 @@ namespace Terminal.Gui.Views {
             Assert.Equal(new Point(8,3),selected[5]);
         }
 
-        /// <summary>
+        [Fact]
+        public void TableView_ExpandLastColumn_True()
+        {
+            var tv = SetUpMiniTable();
+            
+            // the thing we are testing
+            tv.Style.ExpandLastColumn = true;
+
+            tv.Redraw(tv.Bounds);
+            
+            string expected = @"
+┌─┬──────┐
+│A│B     │
+├─┼──────┤
+│1│2     │
+";
+            GraphViewTests.AssertDriverContentsAre(expected);
+        }
+
+
+        [Fact]
+        public void TableView_ExpandLastColumn_False()
+        {
+            var tv = SetUpMiniTable();
+            
+            // the thing we are testing
+            tv.Style.ExpandLastColumn = false;
+
+            tv.Redraw(tv.Bounds);
+            
+            string expected = @"
+┌─┬─┬────┐
+│A│B│    │
+├─┼─┼────┤
+│1│2│    │
+";
+            GraphViewTests.AssertDriverContentsAre(expected);
+        }
+
+        [Fact]
+        public void TableView_ExpandLastColumn_False_ExactBounds()
+        {
+            var tv = SetUpMiniTable();
+            
+            // the thing we are testing
+            tv.Style.ExpandLastColumn = false;
+            // width exactly matches the max col widths
+            tv.Bounds = new Rect(0,0,5,4);
+
+            tv.Redraw(tv.Bounds);
+            
+            string expected = @"
+┌─┬─┐
+│A│B│
+├─┼─┤
+│1│2│
+";
+            GraphViewTests.AssertDriverContentsAre(expected);
+        }
+
+		private TableView SetUpMiniTable ()
+		{
+			
+            var tv = new TableView();
+            tv.Bounds = new Rect(0,0,10,4);
+
+            var dt = new DataTable();
+            var colA = dt.Columns.Add("A");
+            var colB = dt.Columns.Add("B");
+            dt.Rows.Add(1,2);
+
+            tv.Table = dt;
+            tv.Style.GetOrCreateColumnStyle(colA).MinWidth=1;
+            tv.Style.GetOrCreateColumnStyle(colA).MinWidth=1;
+            tv.Style.GetOrCreateColumnStyle(colB).MaxWidth=1;
+            tv.Style.GetOrCreateColumnStyle(colB).MaxWidth=1;
+
+            GraphViewTests.InitFakeDriver();
+            tv.ColorScheme = new ColorScheme(){
+                Normal = Application.Driver.MakeAttribute(Color.White,Color.Black),
+                HotFocus = Application.Driver.MakeAttribute(Color.White,Color.Black)
+                };
+            return tv;
+		}
+
+		/// <summary>
 		/// Builds a simple table of string columns with the requested number of columns and rows
 		/// </summary>
 		/// <param name="cols"></param>