Răsfoiți Sursa

Merge pull request #2239 from tznind/table-header-hit-detection

Adds the ability to detect clicks in TableView headers
Tig 2 ani în urmă
părinte
comite
7b027772e5

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

@@ -997,14 +997,27 @@ 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)
 		{
+			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)
 				return null;
 
@@ -1015,11 +1028,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);

+ 44 - 0
UICatalog/Scenarios/TableEditor.cs

@@ -130,6 +130,50 @@ 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) {
+
+					// 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 ();
+				}
+			};
 		}
 
 

+ 178 - 0
UnitTests/TableViewTests.cs

@@ -1338,5 +1338,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;
+		}
 	}
 }