Browse Source

Merge branch 'develop' into csv-robust

tznind 2 years ago
parent
commit
8a6dfe75db

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

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

+ 2 - 2
ReactiveExample/ReactiveExample.csproj

@@ -11,8 +11,8 @@
     <InformationalVersion>1.0</InformationalVersion>
     <InformationalVersion>1.0</InformationalVersion>
   </PropertyGroup>
   </PropertyGroup>
   <ItemGroup>
   <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" />
     <PackageReference Include="ReactiveMarbles.ObservableEvents.SourceGenerator" Version="1.2.3" PrivateAssets="all" />
   </ItemGroup>
   </ItemGroup>
   <ItemGroup>
   <ItemGroup>

+ 5 - 6
Terminal.Gui/Core/Application.cs

@@ -414,7 +414,7 @@ namespace Terminal.Gui {
 				// In this case, we want to throw a more specific exception.
 				// In this case, we want to throw a more specific exception.
 				throw new InvalidOperationException ("Unable to initialize the console. This can happen if the console is already in use by another process or in unit tests.", ex);
 				throw new InvalidOperationException ("Unable to initialize the console. This can happen if the console is already in use by another process or in unit tests.", ex);
 			}
 			}
-			
+
 			SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext (MainLoop));
 			SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext (MainLoop));
 
 
 			Top = topLevelFactory ();
 			Top = topLevelFactory ();
@@ -757,8 +757,7 @@ namespace Terminal.Gui {
 
 
 			if (mouseGrabView != null) {
 			if (mouseGrabView != null) {
 				if (view == null) {
 				if (view == null) {
-					UngrabMouse ();
-					return;
+					view = mouseGrabView;
 				}
 				}
 
 
 				var newxy = mouseGrabView.ScreenToView (me.X, me.Y);
 				var newxy = mouseGrabView.ScreenToView (me.X, me.Y);
@@ -773,7 +772,7 @@ namespace Terminal.Gui {
 				if (OutsideFrame (new Point (nme.X, nme.Y), mouseGrabView.Frame)) {
 				if (OutsideFrame (new Point (nme.X, nme.Y), mouseGrabView.Frame)) {
 					lastMouseOwnerView?.OnMouseLeave (me);
 					lastMouseOwnerView?.OnMouseLeave (me);
 				}
 				}
-				// System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}");
+				//System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}");
 				if (mouseGrabView?.OnMouseEvent (nme) == true) {
 				if (mouseGrabView?.OnMouseEvent (nme) == true) {
 					return;
 					return;
 				}
 				}
@@ -901,7 +900,7 @@ namespace Terminal.Gui {
 			}
 			}
 
 
 			var rs = new RunState (toplevel);
 			var rs = new RunState (toplevel);
-			
+
 			if (toplevel is ISupportInitializeNotification initializableNotification &&
 			if (toplevel is ISupportInitializeNotification initializableNotification &&
 			    !initializableNotification.IsInitialized) {
 			    !initializableNotification.IsInitialized) {
 				initializableNotification.BeginInit ();
 				initializableNotification.BeginInit ();
@@ -915,7 +914,7 @@ namespace Terminal.Gui {
 				// If Top was already initialized with Init, and Begin has never been called
 				// If Top was already initialized with Init, and Begin has never been called
 				// Top was not added to the toplevels Stack. It will thus never get disposed.
 				// Top was not added to the toplevels Stack. It will thus never get disposed.
 				// Clean it up here:
 				// Clean it up here:
-				if (Top != null && toplevel != Top && !toplevels.Contains(Top)) {
+				if (Top != null && toplevel != Top && !toplevels.Contains (Top)) {
 					Top.Dispose ();
 					Top.Dispose ();
 					Top = null;
 					Top = null;
 				}
 				}

+ 9 - 12
Terminal.Gui/Core/Toplevel.cs

@@ -775,6 +775,8 @@ namespace Terminal.Gui {
 				return true;
 				return true;
 			}
 			}
 
 
+			//System.Diagnostics.Debug.WriteLine ($"dragPosition before: {dragPosition.HasValue}");
+
 			int nx, ny;
 			int nx, ny;
 			if (!dragPosition.HasValue && (mouseEvent.Flags == MouseFlags.Button1Pressed
 			if (!dragPosition.HasValue && (mouseEvent.Flags == MouseFlags.Button1Pressed
 				|| mouseEvent.Flags == MouseFlags.Button2Pressed
 				|| mouseEvent.Flags == MouseFlags.Button2Pressed
@@ -809,32 +811,27 @@ namespace Terminal.Gui {
 						SuperView.SetNeedsDisplay ();
 						SuperView.SetNeedsDisplay ();
 					}
 					}
 					EnsureVisibleBounds (this, mouseEvent.X + (SuperView == null ? mouseEvent.OfX - start.X : Frame.X - start.X),
 					EnsureVisibleBounds (this, mouseEvent.X + (SuperView == null ? mouseEvent.OfX - start.X : Frame.X - start.X),
-						mouseEvent.Y + (SuperView == null ? mouseEvent.OfY : Frame.Y),
+						mouseEvent.Y + (SuperView == null ? mouseEvent.OfY - start.Y : Frame.Y - start.Y),
 						out nx, out ny, out _, out _);
 						out nx, out ny, out _, out _);
 
 
 					dragPosition = new Point (nx, ny);
 					dragPosition = new Point (nx, ny);
-					LayoutSubviews ();
-					Frame = new Rect (nx, ny, Frame.Width, Frame.Height);
-					if (X == null || X is Pos.PosAbsolute) {
-						X = nx;
-					}
-					if (Y == null || Y is Pos.PosAbsolute) {
-						Y = ny;
-					}
-					//System.Diagnostics.Debug.WriteLine ($"nx:{nx},ny:{ny}");
+					X = nx;
+					Y = ny;
+					//System.Diagnostics.Debug.WriteLine ($"Drag: nx:{nx},ny:{ny}");
 
 
 					SetNeedsDisplay ();
 					SetNeedsDisplay ();
 					return true;
 					return true;
 				}
 				}
 			}
 			}
 
 
-			if (mouseEvent.Flags == MouseFlags.Button1Released && dragPosition.HasValue) {
+			if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Released) && dragPosition.HasValue) {
 				Application.UngrabMouse ();
 				Application.UngrabMouse ();
 				Driver.UncookMouse ();
 				Driver.UncookMouse ();
 				dragPosition = null;
 				dragPosition = null;
 			}
 			}
 
 
-			//System.Diagnostics.Debug.WriteLine (mouseEvent.ToString ());
+			//System.Diagnostics.Debug.WriteLine ($"dragPosition after: {dragPosition.HasValue}");
+			//System.Diagnostics.Debug.WriteLine ($"Toplevel: {mouseEvent}");
 			return false;
 			return false;
 		}
 		}
 
 

+ 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
 			// Remove focus down the chain of subviews if focus is removed
 			if (!value && focused != null) {
 			if (!value && focused != null) {
-				focused.OnLeave (view);
-				focused.SetHasFocus (false, view);
+				var f = focused;
+				f.OnLeave (view);
+				f.SetHasFocus (false, view);
 				focused = null;
 				focused = null;
 			}
 			}
 		}
 		}
@@ -3063,6 +3064,17 @@ namespace Terminal.Gui {
 			return Enabled ? ColorScheme.Normal : ColorScheme.Disabled;
 			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>
 		/// <summary>
 		/// Get the top superview of a given <see cref="View"/>.
 		/// Get the top superview of a given <see cref="View"/>.
 		/// </summary>
 		/// </summary>

+ 5 - 2
Terminal.Gui/Views/ContextMenu.cs

@@ -52,7 +52,10 @@ namespace Terminal.Gui {
 		public ContextMenu (int x, int y, MenuBarItem menuItems)
 		public ContextMenu (int x, int y, MenuBarItem menuItems)
 		{
 		{
 			if (IsShow) {
 			if (IsShow) {
-				Hide ();
+				if (menuBar.SuperView != null) {
+					Hide ();
+				}
+				IsShow = false;
 			}
 			}
 			MenuItems = menuItems;
 			MenuItems = menuItems;
 			Position = new Point (x, y);
 			Position = new Point (x, y);
@@ -126,7 +129,7 @@ namespace Terminal.Gui {
 			} else if (ForceMinimumPosToZero && position.Y < 0) {
 			} else if (ForceMinimumPosToZero && position.Y < 0) {
 				position.Y = 0;
 				position.Y = 0;
 			}
 			}
-			
+
 			menuBar = new MenuBar (new [] { MenuItems }) {
 			menuBar = new MenuBar (new [] { MenuItems }) {
 				X = position.X,
 				X = position.X,
 				Y = position.Y,
 				Y = position.Y,

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

@@ -67,6 +67,7 @@ namespace Terminal.Gui {
 				Frame = rect;
 				Frame = rect;
 			}
 			}
 			CanFocus = true;
 			CanFocus = true;
+			HotKeySpecifier = new Rune ('_');
 
 
 			// Things this view knows how to do
 			// Things this view knows how to do
 			AddCommand (Command.LineUp, () => { MoveUp (); return true; });
 			AddCommand (Command.LineUp, () => { MoveUp (); return true; });
@@ -215,9 +216,36 @@ namespace Terminal.Gui {
 					Move (horizontal [i].pos, 0);
 					Move (horizontal [i].pos, 0);
 					break;
 					break;
 				}
 				}
+				var rl = radioLabels [i];
 				Driver.SetAttribute (GetNormalColor ());
 				Driver.SetAttribute (GetNormalColor ());
 				Driver.AddStr (ustring.Make (new Rune [] { i == selected ? Driver.Selected : Driver.UnSelected, ' ' }));
 				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);
 				key = Char.ToUpper ((char)key);
 				foreach (var l in radioLabels) {
 				foreach (var l in radioLabels) {
 					bool nextIsHot = false;
 					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;
 							nextIsHot = true;
-						else {
-							if (nextIsHot && c == key) {
+						} else {
+							if ((nextIsHot && Rune.ToUpper (c) == key) || (key == (uint)hotKey)) {
 								SelectedItem = i;
 								SelectedItem = i;
 								cursor = i;
 								cursor = i;
 								if (!HasFocus)
 								if (!HasFocus)

+ 6 - 0
Terminal.Gui/Views/StatusBar.cs

@@ -55,6 +55,12 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// </summary>
 		/// <value>Action to invoke.</value>
 		/// <value>Action to invoke.</value>
 		public Action Action { get; }
 		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>
 	/// <summary>

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

@@ -997,14 +997,27 @@ namespace Terminal.Gui {
 			return false;
 			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>
 		/// </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)
 		public Point? ScreenToCell (int clientX, int clientY)
 		{
 		{
+			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 (Table == null || Table.Columns.Count <= 0)
 			if (Table == null || Table.Columns.Count <= 0)
 				return null;
 				return null;
 
 
@@ -1015,11 +1028,20 @@ namespace Terminal.Gui {
 			var col = viewPort.LastOrDefault (c => c.X <= clientX);
 			var col = viewPort.LastOrDefault (c => c.X <= clientX);
 
 
 			// Click is on the header section of rendered UI
 			// Click is on the header section of rendered UI
-			if (clientY < headerHeight)
+			if (clientY < headerHeight) {
+				headerIfAny = col?.Column;
 				return null;
 				return null;
+			}
+				
 
 
 			var rowIdx = RowOffset - headerHeight + clientY;
 			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) {
 			if (col != null && rowIdx >= 0) {
 
 
 				return new Point (col.Column.Ordinal, rowIdx);
 				return new Point (col.Column.Ordinal, rowIdx);

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

@@ -116,11 +116,17 @@ namespace Terminal.Gui {
 
 
 		void Watcher_Error (object sender, ErrorEventArgs e)
 		void Watcher_Error (object sender, ErrorEventArgs e)
 		{
 		{
+			if (Application.MainLoop == null)
+				return;
+
 			Application.MainLoop.Invoke (() => Reload ());
 			Application.MainLoop.Invoke (() => Reload ());
 		}
 		}
 
 
 		void Watcher_Changed (object sender, FileSystemEventArgs e)
 		void Watcher_Changed (object sender, FileSystemEventArgs e)
 		{
 		{
+			if (Application.MainLoop == null)
+				return;
+
 			Application.MainLoop.Invoke (() => Reload ());
 			Application.MainLoop.Invoke (() => Reload ());
 		}
 		}
 
 

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

@@ -238,7 +238,10 @@ namespace Terminal.Gui {
 		static int QueryFull (bool useErrorColors, int width, int height, ustring title, ustring message,
 		static int QueryFull (bool useErrorColors, int width, int height, ustring title, ustring message,
 			int defaultButton = 0, Border border = null, params ustring [] buttons)
 			int defaultButton = 0, Border border = null, params ustring [] buttons)
 		{
 		{
-			const int defaultWidth = 50;
+			int defaultWidth = 50;
+			if (defaultWidth > Application.Driver.Cols / 2) {
+				defaultWidth = (int)(Application.Driver.Cols * 0.60f);
+			}
 			int maxWidthLine = TextFormatter.MaxWidthLine (message);
 			int maxWidthLine = TextFormatter.MaxWidthLine (message);
 			if (maxWidthLine > Application.Driver.Cols) {
 			if (maxWidthLine > Application.Driver.Cols) {
 				maxWidthLine = Application.Driver.Cols;
 				maxWidthLine = Application.Driver.Cols;

+ 44 - 0
UICatalog/Scenarios/TableEditor.cs

@@ -130,6 +130,50 @@ namespace UICatalog.Scenarios {
 				Focus = Win.ColorScheme.Focus,
 				Focus = Win.ColorScheme.Focus,
 				Normal = Application.Driver.MakeAttribute(Color.Red,Color.BrightBlue)
 				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) {
+
+					// work out new sort order
+					var sort = tableView.Table.DefaultView.Sort;
+					bool isAsc;
+
+					if(sort?.EndsWith("ASC") ?? false) {
+						sort = $"{clickedCol.ColumnName} DESC";
+						isAsc = false;
+					} else {
+						sort = $"{clickedCol.ColumnName} ASC";
+						isAsc = true;
+					}
+					
+					// set a sort order
+					tableView.Table.DefaultView.Sort = sort;
+					
+					// copy the rows from the view
+					var sortedCopy = tableView.Table.DefaultView.ToTable ();
+					tableView.Table.Rows.Clear ();
+					foreach(DataRow r in sortedCopy.Rows) {
+						tableView.Table.ImportRow (r);
+					}
+
+					foreach(DataColumn col in tableView.Table.Columns) {
+
+						// remove any lingering sort indicator
+						col.ColumnName = col.ColumnName.TrimEnd ('▼', '▲');
+
+						// add a new one if this the one that is being sorted
+						if (col == clickedCol) {
+							col.ColumnName += isAsc ? '▲': '▼';
+						}
+					}
+
+					tableView.Update ();
+				}
+			};
 		}
 		}
 
 
 
 

+ 1 - 1
UnitTests/ApplicationTests.cs

@@ -905,7 +905,7 @@ namespace Terminal.Gui.Core {
 							Flags = MouseFlags.ReportMousePosition
 							Flags = MouseFlags.ReportMousePosition
 						});
 						});
 
 
-					Assert.Null (Application.MouseGrabView);
+					Assert.Equal (sv, Application.MouseGrabView);
 
 
 					ReflectionTools.InvokePrivate (
 					ReflectionTools.InvokePrivate (
 						typeof (Application),
 						typeof (Application),

+ 0 - 1
UnitTests/ConsoleDriverTests.cs

@@ -721,7 +721,6 @@ namespace Terminal.Gui.ConsoleDrivers {
 				yield return new object [] { '1', true, true, false, '1', 2, Key.D1 | Key.ShiftMask | Key.AltMask, '1', 2 };
 				yield return new object [] { '1', true, true, false, '1', 2, Key.D1 | Key.ShiftMask | Key.AltMask, '1', 2 };
 				yield return new object [] { '1', true, true, true, '1', 2, Key.D1 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '1', 2 };
 				yield return new object [] { '1', true, true, true, '1', 2, Key.D1 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '1', 2 };
 				yield return new object [] { '1', false, true, true, '1', 2, Key.D1 | Key.AltMask | Key.CtrlMask, '1', 2 };
 				yield return new object [] { '1', false, true, true, '1', 2, Key.D1 | Key.AltMask | Key.CtrlMask, '1', 2 };
-				yield return new object [] { '1', false, true, true, '1', 2, Key.D1 | Key.AltMask | Key.CtrlMask, '1', 2 };
 				yield return new object [] { '2', false, false, false, '2', 3, Key.D2, '2', 3 };
 				yield return new object [] { '2', false, false, false, '2', 3, Key.D2, '2', 3 };
 				yield return new object [] { '"', true, false, false, '2', 3, (Key)'"' | Key.ShiftMask, '2', 3 };
 				yield return new object [] { '"', true, false, false, '2', 3, (Key)'"' | Key.ShiftMask, '2', 3 };
 				yield return new object [] { '2', true, true, false, '2', 3, Key.D2 | Key.ShiftMask | Key.AltMask, '2', 3 };
 				yield return new object [] { '2', true, true, false, '2', 3, Key.D2 | Key.ShiftMask | Key.AltMask, '2', 3 };

+ 5 - 5
UnitTests/MessageBoxTests.cs

@@ -30,11 +30,11 @@ namespace Terminal.Gui.Views {
 				} else if (iterations == 1) {
 				} else if (iterations == 1) {
 					Application.Top.Redraw (Application.Top.Bounds);
 					Application.Top.Redraw (Application.Top.Bounds);
 					TestHelpers.AssertDriverContentsWithFrameAre (@"
 					TestHelpers.AssertDriverContentsWithFrameAre (@"
-               ┌ Title ─────────────────────────────────────────┐
-                                   Message 
-               
-               
-               └────────────────────────────────────────────────┘
+                ┌ Title ───────────────────────────────────────┐
+                                   Message                    │
+               
+               
+                └──────────────────────────────────────────────┘
 ", output);
 ", output);
 
 
 					Application.RequestStop ();
 					Application.RequestStop ();

+ 15 - 0
UnitTests/RadioGroupTests.cs

@@ -172,5 +172,20 @@ namespace Terminal.Gui.Views {
 			Assert.True (rg.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ())));
 			Assert.True (rg.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ())));
 			Assert.Equal (1, rg.SelectedItem);
 			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);
+		}
 	}
 	}
 }
 }

+ 178 - 0
UnitTests/TableViewTests.cs

@@ -1338,5 +1338,183 @@ namespace Terminal.Gui.Views {
 
 
 			return dt;
 			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";
 			text = $"First Line 界\nSecond Line 界\nThird Line 界\n";
 			Assert.Equal (14, TextFormatter.MaxWidthLine (text));
 			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 ());
+		}
 	}
 	}
 }
 }

+ 164 - 0
UnitTests/ToplevelTests.cs

@@ -1,8 +1,16 @@
 using System;
 using System;
 using Xunit;
 using Xunit;
+using Xunit.Abstractions;
 
 
 namespace Terminal.Gui.Core {
 namespace Terminal.Gui.Core {
 	public class ToplevelTests {
 	public class ToplevelTests {
+		readonly ITestOutputHelper output;
+
+		public ToplevelTests (ITestOutputHelper output)
+		{
+			this.output = output;
+		}
+
 		[Fact]
 		[Fact]
 		[AutoInitShutdown]
 		[AutoInitShutdown]
 		public void Constructor_Default ()
 		public void Constructor_Default ()
@@ -661,5 +669,161 @@ namespace Terminal.Gui.Core {
 				Application.Run (fd);
 				Application.Run (fd);
 			}
 			}
 		}
 		}
+
+		[Fact, AutoInitShutdown]
+		public void Mouse_Drag ()
+		{
+			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");
+			var top = Application.Top;
+			top.Add (menu, sbar, win);
+
+			var iterations = -1;
+
+			Application.Iteration = () => {
+				iterations++;
+				if (iterations == 0) {
+					((FakeDriver)Application.Driver).SetBufferSize (40, 15);
+					MessageBox.Query ("About", "Hello Word", "Ok");
+
+				} else if (iterations == 1) {
+					TestHelpers.AssertDriverContentsWithFrameAre (@"
+ File                                   
+┌ Window ──────────────────────────────┐
+│                                      │
+│                                      │
+│                                      │
+│       ┌ About ───────────────┐       │
+│       │      Hello Word      │       │
+│       │                      │       │
+│       │       [◦ Ok ◦]       │       │
+│       └──────────────────────┘       │
+│                                      │
+│                                      │
+│                                      │
+└──────────────────────────────────────┘
+ CTRL-N New                             ", output);
+
+				} else if (iterations == 2) {
+					Assert.Null (Application.MouseGrabView);
+					// Grab the mouse
+					ReflectionTools.InvokePrivate (
+						typeof (Application),
+						"ProcessMouseEvent",
+						new MouseEvent () {
+							X = 8,
+							Y = 5,
+							Flags = MouseFlags.Button1Pressed
+						});
+
+					Assert.Equal (Application.Current, Application.MouseGrabView);
+					Assert.Equal (new Rect (8, 5, 24, 5), Application.MouseGrabView.Frame);
+
+				} else if (iterations == 3) {
+					Assert.Equal (Application.Current, Application.MouseGrabView);
+					// Grab to left
+					ReflectionTools.InvokePrivate (
+						typeof (Application),
+						"ProcessMouseEvent",
+						new MouseEvent () {
+							X = 7,
+							Y = 5,
+							Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition
+						});
+
+					Assert.Equal (Application.Current, Application.MouseGrabView);
+					Assert.Equal (new Rect (7, 5, 24, 5), Application.MouseGrabView.Frame);
+
+				} else if (iterations == 4) {
+					Assert.Equal (Application.Current, Application.MouseGrabView);
+
+					TestHelpers.AssertDriverContentsWithFrameAre (@"
+ File                                   
+┌ Window ──────────────────────────────┐
+│                                      │
+│                                      │
+│                                      │
+│      ┌ About ───────────────┐        │
+│      │      Hello Word      │        │
+│      │                      │        │
+│      │       [◦ Ok ◦]       │        │
+│      └──────────────────────┘        │
+│                                      │
+│                                      │
+│                                      │
+└──────────────────────────────────────┘
+ CTRL-N New                             ", output);
+
+					Assert.Equal (Application.Current, Application.MouseGrabView);
+				} else if (iterations == 5) {
+					Assert.Equal (Application.Current, Application.MouseGrabView);
+					// Grab to top
+					ReflectionTools.InvokePrivate (
+						typeof (Application),
+						"ProcessMouseEvent",
+						new MouseEvent () {
+							X = 7,
+							Y = 4,
+							Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition
+						});
+
+					Assert.Equal (Application.Current, Application.MouseGrabView);
+					Assert.Equal (new Rect (7, 4, 24, 5), Application.MouseGrabView.Frame);
+
+				} else if (iterations == 6) {
+					Assert.Equal (Application.Current, Application.MouseGrabView);
+
+					TestHelpers.AssertDriverContentsWithFrameAre (@"
+ File                                   
+┌ Window ──────────────────────────────┐
+│                                      │
+│                                      │
+│      ┌ About ───────────────┐        │
+│      │      Hello Word      │        │
+│      │                      │        │
+│      │       [◦ Ok ◦]       │        │
+│      └──────────────────────┘        │
+│                                      │
+│                                      │
+│                                      │
+│                                      │
+└──────────────────────────────────────┘
+ CTRL-N New                             ", output);
+
+					Assert.Equal (Application.Current, Application.MouseGrabView);
+					Assert.Equal (new Rect (7, 4, 24, 5), Application.MouseGrabView.Frame);
+
+				} else if (iterations == 7) {
+					Assert.Equal (Application.Current, 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 ();
+				} else if (iterations == 9) {
+					Application.RequestStop ();
+				}
+			};
+
+			Application.Run ();
+		}
 	}
 	}
 }
 }

+ 2 - 2
UnitTests/UnitTests.csproj

@@ -18,8 +18,8 @@
     <DefineConstants>TRACE;DEBUG_IDISPOSABLE</DefineConstants>
     <DefineConstants>TRACE;DEBUG_IDISPOSABLE</DefineConstants>
   </PropertyGroup>
   </PropertyGroup>
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
-    <PackageReference Include="ReportGenerator" Version="5.1.10" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
+    <PackageReference Include="ReportGenerator" Version="5.1.12" />
     <PackageReference Include="System.Collections" Version="4.3.0" />
     <PackageReference Include="System.Collections" Version="4.3.0" />
     <PackageReference Include="xunit" Version="2.4.2" />
     <PackageReference Include="xunit" Version="2.4.2" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
     <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.False (view.IsKeyPress);
 			Assert.True (view.IsKeyUp);
 			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 ());
+		}
 	}
 	}
 }
 }