浏览代码

Builds CollectionNavigator support into UI Catalog for TableView (#2584)

* Builds collectionnav support into UI cat for TableView

* Fixes keyboard mapping

* MultiSelect = false for TableView

* MultiSelect = false doesn't unbind ctrl-a
Tig 2 年之前
父节点
当前提交
5cf90b8bd0

+ 4 - 4
Terminal.Gui/View/ViewKeyboard.cs

@@ -25,7 +25,7 @@ namespace Terminal.Gui {
 					var v = value == Key.Unknown ? Key.Null : value;
 					if (_hotKey != Key.Null && ContainsKeyBinding (Key.Space | _hotKey)) {
 						if (v == Key.Null) {
-							ClearKeybinding (Key.Space | _hotKey);
+							ClearKeyBinding (Key.Space | _hotKey);
 						} else {
 							ReplaceKeyBinding (Key.Space | _hotKey, Key.Space | v);
 						}
@@ -273,7 +273,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Removes all bound keys from the View and resets the default bindings.
 		/// </summary>
-		public void ClearKeybindings ()
+		public void ClearKeyBindings ()
 		{
 			KeyBindings.Clear ();
 		}
@@ -282,7 +282,7 @@ namespace Terminal.Gui {
 		/// Clears the existing keybinding (if any) for the given <paramref name="key"/>.
 		/// </summary>
 		/// <param name="key"></param>
-		public void ClearKeybinding (Key key)
+		public void ClearKeyBinding (Key key)
 		{
 			KeyBindings.Remove (key);
 		}
@@ -292,7 +292,7 @@ namespace Terminal.Gui {
 		/// keys bound to the same command and this method will clear all of them.
 		/// </summary>
 		/// <param name="command"></param>
-		public void ClearKeybinding (params Command [] command)
+		public void ClearKeyBinding (params Command [] command)
 		{
 			foreach (var kvp in KeyBindings.Where (kvp => kvp.Value.SequenceEqual (command)).ToArray ()) {
 				KeyBindings.Remove (kvp.Key);

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

@@ -151,7 +151,7 @@ namespace Terminal.Gui {
 					var v = value == Key.Unknown ? Key.Null : value;
 					if (base.HotKey != Key.Null && ContainsKeyBinding (Key.Space | base.HotKey)) {
 						if (v == Key.Null) {
-							ClearKeybinding (Key.Space | base.HotKey);
+							ClearKeyBinding (Key.Space | base.HotKey);
 						} else {
 							ReplaceKeyBinding (Key.Space | base.HotKey, Key.Space | v);
 						}

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

@@ -168,7 +168,7 @@ namespace Terminal.Gui {
 				if (allowsMarking) {
 					AddKeyBinding (Key.Space, Command.ToggleChecked);
 				} else {
-					ClearKeybinding (Key.Space);
+					ClearKeyBinding (Key.Space);
 				}
 
 				SetNeedsDisplay ();

+ 50 - 51
Terminal.Gui/Views/TableView/TableView.cs

@@ -4,6 +4,7 @@ using System.Collections;
 using System.Collections.Generic;
 using System.Data;
 using System.Linq;
+using static Terminal.Gui.SpinnerStyle;
 
 namespace Terminal.Gui {
 
@@ -38,7 +39,13 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// The data table to render in the view.  Setting this property automatically updates and redraws the control.
 		/// </summary>
-		public ITableSource Table { get => table; set { table = value; Update (); } }
+		public ITableSource Table {
+			get => table;
+			set {
+				table = value;
+				Update ();
+			}
+		}
 
 		/// <summary>
 		/// Contains options for changing how the table is rendered
@@ -285,9 +292,9 @@ namespace Terminal.Gui {
 					continue;
 
 				// No more data
-				if(rowToRender >= Table.Rows) {
+				if (rowToRender >= Table.Rows) {
 
-					if(rowToRender == Table.Rows && Style.ShowHorizontalBottomline) {
+					if (rowToRender == Table.Rows && Style.ShowHorizontalBottomline) {
 						RenderBottomLine (line, bounds.Width, columnsToRender);
 					}
 
@@ -383,7 +390,7 @@ namespace Terminal.Gui {
 				var current = columnsToRender [i];
 
 				var colStyle = Style.GetColumnStyleIfAny (current.Column);
-				var colName = table.ColumnNames[current.Column];
+				var colName = table.ColumnNames [current.Column];
 
 				RenderSeparator (current.X - 1, row, true);
 
@@ -507,7 +514,7 @@ namespace Terminal.Gui {
 					}
 					// if the next column is the start of a header
 					else if (columnsToRender.Any (r => r.X == c + 1)) {
-						rune =  Driver.BottomTee;
+						rune = Driver.BottomTee;
 					} else if (c == availableWidth - 1) {
 
 						// for the last character in the table
@@ -610,7 +617,7 @@ namespace Terminal.Gui {
 				if (!FullRowSelect)
 					Driver.SetAttribute (Enabled ? rowScheme.Normal : rowScheme.Disabled);
 
-				if(style.AlwaysUseNormalColorForVerticalCellLines && style.ShowVerticalCellLines) {
+				if (style.AlwaysUseNormalColorForVerticalCellLines && style.ShowVerticalCellLines) {
 
 					Driver.SetAttribute (rowScheme.Normal);
 				}
@@ -630,7 +637,7 @@ namespace Terminal.Gui {
 				AddRune (0, row, Driver.VLine);
 				AddRune (Bounds.Width - 1, row, Driver.VLine);
 			}
-				
+
 		}
 
 		/// <summary>
@@ -694,7 +701,7 @@ namespace Terminal.Gui {
 		private string TruncateOrPad (object originalCellValue, string representation, int availableHorizontalSpace, ColumnStyle colStyle)
 		{
 			if (string.IsNullOrEmpty (representation))
-				return new string(' ',availableHorizontalSpace);
+				return new string (' ', availableHorizontalSpace);
 
 			// if value is not wide enough
 			if (representation.Sum (c => Rune.ColumnWidth (c)) < availableHorizontalSpace) {
@@ -723,6 +730,8 @@ namespace Terminal.Gui {
 			return new string (representation.TakeWhile (c => (availableHorizontalSpace -= Rune.ColumnWidth (c)) > 0).ToArray ());
 		}
 
+
+		
 		/// <inheritdoc/>
 		public override bool ProcessKey (KeyEvent keyEvent)
 		{
@@ -761,7 +770,7 @@ namespace Terminal.Gui {
 			if (extendExistingSelection) {
 
 				// If we are extending current selection but there isn't one
-				if (MultiSelectedRegions.Count == 0 || MultiSelectedRegions.All(m=>m.IsToggled)) {
+				if (MultiSelectedRegions.Count == 0 || MultiSelectedRegions.All (m => m.IsToggled)) {
 					// Create a new region between the old active cell and the new cell
 					var rect = CreateTableSelection (SelectedColumn, SelectedRow, col, row);
 					MultiSelectedRegions.Push (rect);
@@ -928,14 +937,13 @@ namespace Terminal.Gui {
 		/// <returns></returns>
 		public IEnumerable<Point> GetAllSelectedCells ()
 		{
-			if (TableIsNullOrInvisible () || Table.Rows == 0)
-			{
-				return Enumerable.Empty<Point>();				
+			if (TableIsNullOrInvisible () || Table.Rows == 0) {
+				return Enumerable.Empty<Point> ();
 			}
 
 			EnsureValidSelection ();
 
-			var toReturn = new HashSet<Point>();
+			var toReturn = new HashSet<Point> ();
 
 			// If there are one or more rectangular selections
 			if (MultiSelect && MultiSelectedRegions.Any ()) {
@@ -950,11 +958,11 @@ namespace Terminal.Gui {
 				for (int y = yMin; y < yMax; y++) {
 					for (int x = xMin; x < xMax; x++) {
 						if (IsSelected (x, y)) {
-							toReturn.Add(new Point (x, y));
+							toReturn.Add (new Point (x, y));
 						}
 					}
 				}
-			} 
+			}
 
 			// if there are no region selections then it is just the active cell
 
@@ -962,14 +970,14 @@ namespace Terminal.Gui {
 			if (FullRowSelect) {
 				// all cells in active row are selected
 				for (int x = 0; x < Table.Columns; x++) {
-					toReturn.Add(new Point (x, SelectedRow));
+					toReturn.Add (new Point (x, SelectedRow));
 				}
 			} else {
 				// Not full row select and no multi selections
-				toReturn.Add(new Point (SelectedColumn, SelectedRow));
+				toReturn.Add (new Point (SelectedColumn, SelectedRow));
 			}
 
-			return toReturn;		
+			return toReturn;
 		}
 
 		/// <summary>
@@ -1001,8 +1009,8 @@ namespace Terminal.Gui {
 				return;
 			}
 
-			var regions = GetMultiSelectedRegionsContaining(selectedColumn, selectedRow).ToArray();
-			var toggles = regions.Where(s=>s.IsToggled).ToArray ();
+			var regions = GetMultiSelectedRegionsContaining (selectedColumn, selectedRow).ToArray ();
+			var toggles = regions.Where (s => s.IsToggled).ToArray ();
 
 			// Toggle it off
 			if (toggles.Any ()) {
@@ -1015,17 +1023,14 @@ namespace Terminal.Gui {
 						MultiSelectedRegions.Push (region);
 				}
 			} else {
-				
+
 				// user is toggling selection within a rectangular
 				// select.  So toggle the full region
-				if(regions.Any())
-				{
-					foreach(var r in regions)
-					{
+				if (regions.Any ()) {
+					foreach (var r in regions) {
 						r.IsToggled = true;
 					}
-				}
-				else{
+				} else {
 					// Toggle on a single cell selection
 					MultiSelectedRegions.Push (
 					CreateTableSelection (selectedColumn, SelectedRow, selectedColumn, selectedRow, true)
@@ -1060,8 +1065,7 @@ namespace Terminal.Gui {
 				return false;
 			}
 
-			if(GetMultiSelectedRegionsContaining(col,row).Any())
-			{
+			if (GetMultiSelectedRegionsContaining (col, row).Any ()) {
 				return true;
 			}
 
@@ -1069,19 +1073,15 @@ namespace Terminal.Gui {
 					(col == SelectedColumn || FullRowSelect);
 		}
 
-		private IEnumerable<TableSelection> GetMultiSelectedRegionsContaining(int col, int row)
+		private IEnumerable<TableSelection> GetMultiSelectedRegionsContaining (int col, int row)
 		{
-			if(!MultiSelect)
-			{
-				return Enumerable.Empty<TableSelection>();
+			if (!MultiSelect) {
+				return Enumerable.Empty<TableSelection> ();
 			}
-		
-			if(FullRowSelect)
-			{
+
+			if (FullRowSelect) {
 				return MultiSelectedRegions.Where (r => r.Rect.Bottom > row && r.Rect.Top <= row);
-			}
-			else
-			{
+			} else {
 				return MultiSelectedRegions.Where (r => r.Rect.Contains (col, row));
 			}
 		}
@@ -1388,7 +1388,7 @@ namespace Terminal.Gui {
 		{
 			return Table == null ||
 				Table.Columns <= 0 ||
-				Enumerable.Range(0,Table.Columns).All (
+				Enumerable.Range (0, Table.Columns).All (
 				c => (Style.GetColumnStyleIfAny (c)?.Visible ?? true) == false);
 		}
 
@@ -1424,8 +1424,8 @@ namespace Terminal.Gui {
 			}
 
 			// get the column visibility by index (if no style visible is true)
-			bool [] columnVisibility = 
-				Enumerable.Range(0,Table.Columns)
+			bool [] columnVisibility =
+				Enumerable.Range (0, Table.Columns)
 				.Select (c => this.Style.GetColumnStyleIfAny (c)?.Visible ?? true)
 				.ToArray ();
 
@@ -1526,7 +1526,7 @@ namespace Terminal.Gui {
 		/// </summary>
 		protected virtual void OnSelectedCellChanged (SelectedCellChangedEventArgs args)
 		{
-			SelectedCellChanged?.Invoke (this,args);
+			SelectedCellChanged?.Invoke (this, args);
 		}
 
 		/// <summary>
@@ -1548,7 +1548,7 @@ namespace Terminal.Gui {
 		{
 			if (TableIsNullOrInvisible ()) {
 				return Enumerable.Empty<ColumnToRender> ();
-			}	
+			}
 
 			var toReturn = new List<ColumnToRender> ();
 			int usedSpace = 0;
@@ -1568,7 +1568,7 @@ namespace Terminal.Gui {
 			var lastColumn = Table.Columns - 1;
 
 			// TODO : Maybe just a for loop?
-			foreach (var col in Enumerable.Range(0,Table.Columns).Skip (ColumnOffset)) {
+			foreach (var col in Enumerable.Range (0, Table.Columns).Skip (ColumnOffset)) {
 
 				int startingIdxForCurrentHeader = usedSpace;
 				var colStyle = Style.GetColumnStyleIfAny (col);
@@ -1618,12 +1618,11 @@ namespace Terminal.Gui {
 				var isVeryLast = lastColumn == col;
 
 				// there is space
-				toReturn.Add(new ColumnToRender (col, startingIdxForCurrentHeader, colWidth, isVeryLast));
+				toReturn.Add (new ColumnToRender (col, startingIdxForCurrentHeader, colWidth, isVeryLast));
 				first = false;
 			}
 
-			if(Style.ExpandLastColumn)
-			{
+			if (Style.ExpandLastColumn) {
 				var last = toReturn.Last ();
 				last.Width = Math.Max (last.Width, availableHorizontalSpace - last.X);
 			}
@@ -1648,7 +1647,7 @@ namespace Terminal.Gui {
 		/// <returns></returns>
 		private int CalculateMaxCellWidth (int col, int rowsToRender, ColumnStyle colStyle)
 		{
-			int spaceRequired = table.ColumnNames[col].Sum (c => Rune.ColumnWidth (c));
+			int spaceRequired = table.ColumnNames [col].Sum (c => Rune.ColumnWidth (c));
 
 			// if table has no rows
 			if (RowOffset < 0)
@@ -1660,7 +1659,7 @@ namespace Terminal.Gui {
 				//expand required space if cell is bigger than the last biggest cell or header
 				spaceRequired = Math.Max (
 					spaceRequired,
-					GetRepresentation (Table [i,col], colStyle).Sum (c => Rune.ColumnWidth (c)));
+					GetRepresentation (Table [i, col], colStyle).Sum (c => Rune.ColumnWidth (c)));
 			}
 
 			// Don't require more space than the style allows
@@ -1861,7 +1860,7 @@ namespace Terminal.Gui {
 			/// </summary>
 			public bool ShowHorizontalScrollIndicators { get; set; } = true;
 
-			
+
 			/// <summary>
 			/// Gets or sets a flag indicating whether there should be a horizontal line after all the data
 			/// in the table. Defaults to <see langword="false"/>.

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

@@ -298,7 +298,7 @@ namespace Terminal.Gui {
 			TitleChanged += Wizard_TitleChanged;
 
 			if (Modal) {
-				ClearKeybinding (Command.QuitToplevel);
+				ClearKeyBinding (Command.QuitToplevel);
 				AddKeyBinding (Key.Esc, Command.QuitToplevel);
 			}
 			SetNeedsLayout ();

+ 1 - 1
UICatalog/KeyBindingsDialog.cs

@@ -109,7 +109,7 @@ namespace UICatalog {
 						if(supported.Contains(kvp.Key))
 						{
 							// if the key was bound to any other commands clear that
-							view.ClearKeybinding (kvp.Key);
+							view.ClearKeyBinding (kvp.Key);
 							view.AddKeyBinding (kvp.Value,kvp.Key);
 						}
 

+ 1 - 1
UICatalog/Scenarios/ASCIICustomButton.cs

@@ -193,7 +193,7 @@ namespace UICatalog.Scenarios {
 					};
 				}
 
-				scrollView.ClearKeybindings ();
+				scrollView.ClearKeyBindings ();
 
 				buttons = new List<Button> ();
 				Button prevButton = null;

+ 2 - 2
UICatalog/Scenarios/Text.cs

@@ -126,8 +126,8 @@ namespace UICatalog.Scenarios {
 					textView.AddKeyBinding (keyTab, Command.Tab);
 					textView.AddKeyBinding (keyBackTab, Command.BackTab);
 				} else {
-					textView.ClearKeybinding (keyTab);
-					textView.ClearKeybinding (keyBackTab);
+					textView.ClearKeyBinding (keyTab);
+					textView.ClearKeyBinding (keyBackTab);
 				}
 				textView.AllowsTab = (bool)e.NewValue;
 			};

+ 50 - 18
UICatalog/UICatalog.cs

@@ -255,7 +255,12 @@ namespace UICatalog {
 			public MenuItem? miEnableConsoleScrolling;
 
 			public ListView CategoryList;
+
+			// UI Catalog uses TableView for the scenario list instead of a ListView to demonstate how
+			// TableView works. There's no real reason not to use ListView. Because we use TableView, and TableView
+			// doesn't (currently) have CollectionNavigator support built in, we implement it here, within the app.
 			public TableView ScenarioList;
+			private CollectionNavigator _scenarioCollectionNav = new CollectionNavigator ();
 
 			public StatusItem Capslock;
 			public StatusItem Numlock;
@@ -311,19 +316,7 @@ namespace UICatalog {
 					OS
 				};
 
-				//ContentPane = new TileView () {
-				//	Id = "ContentPane",
-				//	X = 0,
-				//	Y = 1, // for menu
-				//	Width = Dim.Fill (),
-				//	Height = Dim.Fill (1),
-				//	CanFocus = true,
-				//	Shortcut = Key.CtrlMask | Key.C,
-				//};
-				//ContentPane.LineStyle = LineStyle.Single;
-				//ContentPane.SetSplitterPos (0, 25);
-				//ContentPane.ShortcutAction = () => ContentPane.SetFocus ();
-
+				// Create the Category list view. This list never changes.
 				CategoryList = new ListView (_categories) {
 					X = 0,
 					Y = 1,
@@ -340,6 +333,9 @@ namespace UICatalog {
 				};
 				CategoryList.SelectedItemChanged += CategoryView_SelectedChanged;
 
+				// Create the scenario list. The contents of the scenario list changes whenever the
+				// Category list selection changes (to show just the scenarios that belong to the selected
+				// category).
 				ScenarioList = new TableView () {
 					X = Pos.Right (CategoryList) - 1,
 					Y = 1,
@@ -351,10 +347,14 @@ namespace UICatalog {
 					BorderStyle = LineStyle.Single,
 					SuperViewRendersLineCanvas = true
 				};
+
+				// TableView provides many options for table headers. For simplicity we turn all 
+				// of these off. By enabling FullRowSelect and turning off headers, TableView looks just
+				// like a ListView
 				ScenarioList.FullRowSelect = true;
-				//ScenarioList.Style.ShowHeaders = false;
+				ScenarioList.Style.ShowHeaders = false;
 				ScenarioList.Style.ShowHorizontalHeaderOverline = false;
-				//ScenarioList.Style.ShowHorizontalHeaderUnderline = false;
+				ScenarioList.Style.ShowHorizontalHeaderUnderline = false;
 				ScenarioList.Style.ShowHorizontalBottomline = false;
 				ScenarioList.Style.ShowVerticalCellLines = false;
 				ScenarioList.Style.ShowVerticalHeaderLines = false;
@@ -371,13 +371,36 @@ namespace UICatalog {
 				 * we just measure all the data ourselves and set the appropriate
 				 * max widths as ColumnStyles 
 				 */
-
 				var longestName = _scenarios!.Max (s => s.GetName ().Length);
 				ScenarioList.Style.ColumnStyles.Add (0, new ColumnStyle () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName });
-				ScenarioList.Style.ColumnStyles.Add (1, new ColumnStyle () {  MaxWidth = 1 });
-
+				ScenarioList.Style.ColumnStyles.Add (1, new ColumnStyle () { MaxWidth = 1 });
+
+				// Enable user to find & select a scenario by typing text
+				// TableView does not (currently) have built-in CollectionNavigator support (the ability for the 
+				// user to type and the items that match get selected). We implement it in the app instead. 
+				ScenarioList.KeyDown += (s, a) => {
+					if (CollectionNavigator.IsCompatibleKey (a.KeyEvent)) {
+						var newItem = _scenarioCollectionNav?.GetNextMatchingItem (ScenarioList.SelectedRow, (char)a.KeyEvent.KeyValue);
+						if (newItem is int && newItem != -1) {
+							ScenarioList.SelectedRow = (int)newItem;
+							ScenarioList.EnsureSelectedCellIsVisible ();
+							ScenarioList.SetNeedsDisplay ();
+							a.Handled = true;
+						}
+					}
+				};
 				ScenarioList.CellActivated += ScenarioView_OpenSelectedItem;
 
+				// TableView typically is a grid where nav keys are biased for moving left/right.
+				ScenarioList.AddKeyBinding (Key.Home, Command.TopHome);
+				ScenarioList.AddKeyBinding (Key.End, Command.BottomEnd);
+
+				// Ideally, TableView.MultiSelect = false would turn off any keybindings for
+				// multi-select options. But it currently does not. UI Catalog uses Ctrl-A for
+				// a shortcut to About.
+				ScenarioList.MultiSelect = false;
+				ScenarioList.ClearKeyBinding (Key.CtrlMask | Key.A);
+
 				KeyDown += KeyDownHandler;
 
 				Add (CategoryList);
@@ -764,6 +787,15 @@ namespace UICatalog {
 					{ "Name", (s) => s.GetName() },
 					{ "Description", (s) => s.GetDescription() },
 				});
+
+				// Create a collection of just the scenario names (the 1st column in our TableView)
+				// for CollectionNavigator. 
+				var firstColumnList = new List<object> ();
+				for (var i = 0; i < ScenarioList.Table.Rows; i++) {
+					firstColumnList.Add (ScenarioList.Table [i, 0]);
+				}
+				_scenarioCollectionNav.Collection = firstColumnList;
+
 			}
 		}
 

+ 1 - 1
UnitTests/Views/ButtonTests.cs

@@ -161,7 +161,7 @@ namespace Terminal.Gui.ViewsTests {
 			Assert.Equal (1, pressed);
 
 			// remove the default keybinding (Enter)
-			btn.ClearKeybinding (Command.Accept);
+			btn.ClearKeyBinding (Command.Accept);
 
 			// After clearing the default keystroke the Enter button no longer does anything for the Button
 			Application.Driver.SendKeys ('\n', ConsoleKey.Enter, false, false, false);

+ 2 - 2
UnitTests/Views/TableViewTests.cs

@@ -611,7 +611,7 @@ namespace Terminal.Gui.ViewsTests {
 			activatedValue = null;
 
 			// clear keybindings and ensure that Enter does not trigger the event anymore
-			tv.ClearKeybindings ();
+			tv.ClearKeyBindings ();
 			tv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()));
 			Assert.Null (activatedValue);
 
@@ -622,7 +622,7 @@ namespace Terminal.Gui.ViewsTests {
 
 			// reset the test
 			activatedValue = null;
-			tv.ClearKeybindings ();
+			tv.ClearKeyBindings ();
 
 			// Old method for changing the activation key
 			tv.CellActivationKey = Key.z;