Browse Source

Added SelectAll, better selection iteration and validation

tznind 4 years ago
parent
commit
d988a34444
3 changed files with 294 additions and 5 deletions
  1. 107 4
      Terminal.Gui/Views/TableView.cs
  2. 28 1
      UICatalog/Scenarios/TableEditor.cs
  3. 159 0
      UnitTests/TableViewTests.cs

+ 107 - 4
Terminal.Gui/Views/TableView.cs

@@ -624,6 +624,10 @@ namespace Terminal.Gui {
 				SetSelection(Table.Columns.Count - 1, Table.Rows.Count - 1, keyEvent.Key.HasFlag(Key.ShiftMask));
 				Update ();
 				break;
+			case Key.A | Key.CtrlMask:
+				SelectAll();
+				Update ();
+				break;
 			case Key.End:
 			case Key.End | Key.ShiftMask:
 				//jump to end of row
@@ -682,6 +686,71 @@ namespace Terminal.Gui {
 			SetSelection(SelectedColumn + offsetX, SelectedRow + offsetY,extendExistingSelection);
 		}
 
+		/// <summary>
+		/// When <see cref="MultiSelect"/> is on, creates selection over all cells in the table (replacing any old selection regions)
+		/// </summary>
+		public void SelectAll()
+		{
+			if(Table == null || !MultiSelect || Table.Rows.Count == 0)
+				return;
+
+			MultiSelectedRegions.Clear();
+
+			// Create a single region over entire table, set the origin of the selection to the active cell so that a followup spread selection e.g. shift-right behaves properly
+			MultiSelectedRegions.Push(new TableSelection(new Point(SelectedColumn,SelectedRow), new Rect(0,0,Table.Columns.Count,table.Rows.Count)));
+			Update();
+		}
+
+		/// <summary>
+		/// Returns all cells in any <see cref="MultiSelectedRegions"/> (if <see cref="MultiSelect"/> is enabled) and the selected cell
+		/// </summary>
+		/// <returns></returns>
+		public IEnumerable<Point> GetAllSelectedCells()
+		{
+			if(Table == null || Table.Rows.Count == 0)
+				yield break;
+
+			EnsureValidSelection();
+
+			// If there are one or more rectangular selections
+			if(MultiSelect && MultiSelectedRegions.Any()){
+				
+				// Quiz any cells for whether they are selected.  For performance we only need to check those between the top left and lower right vertex of selection regions
+				var yMin = MultiSelectedRegions.Min(r=>r.Rect.Top);
+				var yMax = MultiSelectedRegions.Max(r=>r.Rect.Bottom);
+
+				var xMin = FullRowSelect ? 0 : MultiSelectedRegions.Min(r=>r.Rect.Left);
+				var xMax = FullRowSelect ? Table.Columns.Count : MultiSelectedRegions.Max(r=>r.Rect.Right);
+
+				for(int y = yMin ; y < yMax ; y++)
+				{
+					for(int x = xMin ; x < xMax ; x++)
+					{
+						if(IsSelected(x,y)){
+							yield return new Point(x,y);
+						}
+					}
+				}
+			}
+			else{
+
+				// if there are no region selections then it is just the active cell
+
+				// if we are selecting the full row
+				if(FullRowSelect)
+				{
+					// all cells in active row are selected
+					for(int x =0;x<Table.Columns.Count;x++){
+						yield return new Point(x,SelectedRow);
+					}
+				}
+				else
+					{
+						// Not full row select and no multi selections
+						yield return new Point(SelectedColumn,SelectedRow);
+					}
+			}
+		}
 
 		/// <summary>
 		/// Returns a new rectangle between the two points with positive width/height regardless of relative positioning of the points.  pt1 is always considered the <see cref="TableSelection.Origin"/> point
@@ -901,17 +970,51 @@ namespace Terminal.Gui {
 
 
 		/// <summary>
-		/// Updates <see cref="SelectedColumn"/> and <see cref="SelectedRow"/> where they are outside the bounds of the table (by adjusting them to the nearest existing cell).  Has no effect if <see cref="Table"/> has not been set.
+		/// Updates <see cref="SelectedColumn"/>, <see cref="SelectedRow"/> and <see cref="MultiSelectedRegions"/> where they are outside the bounds of the table (by adjusting them to the nearest existing cell).  Has no effect if <see cref="Table"/> has not been set.
 		/// </summary>
 		/// <remarks>Changes will not be immediately visible in the display until you call <see cref="View.SetNeedsDisplay()"/></remarks>
-		public void EnsureValidSelection ()
+		public void EnsureValidSelection()
 		{
 			if(Table == null){
+
+				// Table doesn't exist, we should probably clear those selections
+				MultiSelectedRegions.Clear();
 				return;
 			}
 
 			SelectedColumn = Math.Max(Math.Min(SelectedColumn,Table.Columns.Count -1),0);
 			SelectedRow = Math.Max(Math.Min(SelectedRow,Table.Rows.Count -1),0);
+
+			var oldRegions = MultiSelectedRegions.ToArray().Reverse();
+
+			MultiSelectedRegions.Clear();
+
+			// evaluate 
+			foreach(var region in oldRegions)
+			{
+				// ignore regions entirely below current table state
+				if(region.Rect.Top >= Table.Rows.Count)
+					continue;
+
+				// ignore regions entirely too far right of table columns
+				if(region.Rect.Left >= Table.Columns.Count)
+					continue;
+
+				// ensure region's origin exists
+				region.Origin = new Point(
+					Math.Max(Math.Min(region.Origin.X,Table.Columns.Count -1),0),
+					Math.Max(Math.Min(region.Origin.Y,Table.Rows.Count -1),0));
+
+				// ensure regions do not go over edge of table bounds
+				region.Rect = Rect.FromLTRB(region.Rect.Left,
+					region.Rect.Top,
+					Math.Max(Math.Min(region.Rect.Right, Table.Columns.Count ),0),
+					Math.Max(Math.Min(region.Rect.Bottom,Table.Rows.Count),0)
+					);
+
+				MultiSelectedRegions.Push(region);
+			}
+
 		}
 
 		/// <summary>
@@ -1165,13 +1268,13 @@ namespace Terminal.Gui {
 		/// Corner of the <see cref="Rect"/> where selection began
 		/// </summary>
 		/// <value></value>
-		public Point Origin{get;}
+		public Point Origin{get;set;}
 
 		/// <summary>
 		/// Area selected
 		/// </summary>
 		/// <value></value>
-		public Rect Rect { get; }
+		public Rect Rect { get; set;}
 
 		/// <summary>
 		/// Creates a new selected area starting at the origin corner and covering the provided rectangular area

+ 28 - 1
UICatalog/Scenarios/TableEditor.cs

@@ -2,6 +2,7 @@
 using System.Collections.Generic;
 using System.Data;
 using Terminal.Gui;
+using System.Linq;
 using System.Globalization;
 
 namespace UICatalog.Scenarios {
@@ -60,7 +61,6 @@ namespace UICatalog.Scenarios {
 
 
 			var statusBar = new StatusBar (new StatusItem [] {
-				//new StatusItem(Key.Enter, "~ENTER~ ApplyEdits", () => { _hexView.ApplyEdits(); }),
 				new StatusItem(Key.F2, "~F2~ OpenExample", () => OpenExample(true)),
 				new StatusItem(Key.F3, "~F3~ CloseExample", () => CloseExample()),
 				new StatusItem(Key.F4, "~F4~ OpenSimple", () => OpenSimple(true)),
@@ -83,6 +83,33 @@ namespace UICatalog.Scenarios {
 
 			tableView.SelectedCellChanged += (e)=>{selectedCellLabel.Text = $"{tableView.SelectedRow},{tableView.SelectedColumn}";};
 			tableView.CellActivated += EditCurrentCell;
+			tableView.KeyPress += TableViewKeyPress;
+		}
+
+		private void TableViewKeyPress (View.KeyEventEventArgs e)
+		{
+			if(e.KeyEvent.Key == Key.DeleteChar){
+
+				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{
+
+					// otherwise set all selected cells to null
+					foreach(var pt in tableView.GetAllSelectedCells())
+					{
+						tableView.Table.Rows[pt.Y][pt.X] = DBNull.Value;
+					}
+				}
+
+				tableView.Update();
+				e.Handled = true;
+			}
+
+
 		}
 
 		private void ClearColumnStyles ()

+ 159 - 0
UnitTests/TableViewTests.cs

@@ -257,7 +257,166 @@ namespace UnitTests {
             
             Assert.Equal(8,tableView.RowOffset);
         }
+
+        [Fact]
+        public void DeleteRow_SelectAll_AdjustsSelectionToPreventOverrun()
+        {
+            // create a 4 by 4 table
+            var tableView = new TableView(){
+                Table = BuildTable(4,4),
+                MultiSelect = true,
+                Bounds = new Rect(0,0,10,5)
+            };
+
+            tableView.SelectAll();
+            Assert.Equal(16,tableView.GetAllSelectedCells().Count());
+
+            // delete one of the columns
+            tableView.Table.Columns.RemoveAt(2);
+
+            // table should now be 3x4
+            Assert.Equal(12,tableView.GetAllSelectedCells().Count());
+
+            // remove a row
+            tableView.Table.Rows.RemoveAt(1);
+
+            // table should now be 3x3
+            Assert.Equal(9,tableView.GetAllSelectedCells().Count());
+        }
+
+
+        [Fact]
+        public void DeleteRow_SelectLastRow_AdjustsSelectionToPreventOverrun()
+        {
+            // create a 4 by 4 table
+            var tableView = new TableView(){
+                Table = BuildTable(4,4),
+                MultiSelect = true,
+                Bounds = new Rect(0,0,10,5)
+            };
+
+            // select the last row
+            tableView.MultiSelectedRegions.Clear();
+            tableView.MultiSelectedRegions.Push(new TableSelection(new Point(0,3), new Rect(0,3,4,1)));
+
+            Assert.Equal(4,tableView.GetAllSelectedCells().Count());
+
+            // remove a row
+            tableView.Table.Rows.RemoveAt(0);
+
+            tableView.EnsureValidSelection();
+
+            // since the selection no longer exists it should be removed
+            Assert.Empty(tableView.MultiSelectedRegions);
+        }
+
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public void GetAllSelectedCells_SingleCellSelected_ReturnsOne(bool multiSelect)
+        {
+            var tableView = new TableView(){
+                Table = BuildTable(3,3),
+                MultiSelect = multiSelect,
+                Bounds = new Rect(0,0,10,5)
+            };
+
+            tableView.SetSelection(1,1,false);
+
+            Assert.Single(tableView.GetAllSelectedCells());
+            Assert.Equal(new Point(1,1),tableView.GetAllSelectedCells().Single());
+        }
+
+
+        [Fact]
+        public void GetAllSelectedCells_SquareSelection_ReturnsFour()
+        {
+            var tableView = new TableView(){
+                Table = BuildTable(3,3),
+                MultiSelect = true,
+                Bounds = new Rect(0,0,10,5)
+            };
+
+            // move cursor to 1,1
+            tableView.SetSelection(1,1,false);
+            // spread selection across to 2,2 (e.g. shift+right then shift+down)
+            tableView.SetSelection(2,2,true);
+
+            var selected = tableView.GetAllSelectedCells().ToArray();
+
+            Assert.Equal(4,selected.Length);
+            Assert.Equal(new Point(1,1),selected[0]);
+            Assert.Equal(new Point(2,1),selected[1]);
+            Assert.Equal(new Point(1,2),selected[2]);
+            Assert.Equal(new Point(2,2),selected[3]);
+        }
+        
+
+        [Fact]
+        public void GetAllSelectedCells_SquareSelection_FullRowSelect()
+        {
+            var tableView = new TableView(){
+                Table = BuildTable(3,3),
+                MultiSelect = true,
+                FullRowSelect = true,
+                Bounds = new Rect(0,0,10,5)
+            };
+
+            // move cursor to 1,1
+            tableView.SetSelection(1,1,false);
+            // spread selection across to 2,2 (e.g. shift+right then shift+down)
+            tableView.SetSelection(2,2,true);
+
+            var selected = tableView.GetAllSelectedCells().ToArray();
+
+            Assert.Equal(6,selected.Length);
+            Assert.Equal(new Point(0,1),selected[0]);
+            Assert.Equal(new Point(1,1),selected[1]);
+            Assert.Equal(new Point(2,1),selected[2]);
+            Assert.Equal(new Point(0,2),selected[3]);
+            Assert.Equal(new Point(1,2),selected[4]);
+            Assert.Equal(new Point(2,2),selected[5]);
+        }
         
+
+        [Fact]
+        public void GetAllSelectedCells_TwoIsolatedSelections_ReturnsSix()
+        {
+            var tableView = new TableView(){
+                Table = BuildTable(20,20),
+                MultiSelect = true,
+                Bounds = new Rect(0,0,10,5)
+            };
+
+            /*  
+                    Sets up disconnected selections like:
+
+                    00000000000
+                    01100000000
+                    01100000000
+                    00000001100
+                    00000000000
+            */
+
+            tableView.MultiSelectedRegions.Clear();
+            tableView.MultiSelectedRegions.Push(new TableSelection(new Point(1,1),new Rect(1,1,2,2)));
+            tableView.MultiSelectedRegions.Push(new TableSelection(new Point(7,3),new Rect(7,3,2,1)));
+            
+            tableView.SelectedColumn = 8;
+            tableView.SelectedRow = 3;
+
+            var selected = tableView.GetAllSelectedCells().ToArray();
+
+            Assert.Equal(6,selected.Length);
+
+            Assert.Equal(new Point(1,1),selected[0]);
+            Assert.Equal(new Point(2,1),selected[1]);
+            Assert.Equal(new Point(1,2),selected[2]);
+            Assert.Equal(new Point(2,2),selected[3]);
+            Assert.Equal(new Point(7,3),selected[4]);
+            Assert.Equal(new Point(8,3),selected[5]);
+        }
+
         /// <summary>
 		/// Builds a simple table of string columns with the requested number of columns and rows
 		/// </summary>