2
0
Эх сурвалжийг харах

Fixes #2581 Refactor CollectionNavigator so it supports TableView (#2586)

* Refactor CollectionNavigator to a base and a collection implementation

* Refactor CollectionNavigatorBase to look for first match smartly

* Add TableCollectionNavigator

* Make TableCollectionNavigator a core part of TableView

* Fix bad merge

* Added tests for tableview collection navigator

* Add FileDialogCollectionNavigator which ignores . and directory separator prefixes on file names

* whitespace fixes

---------

Co-authored-by: Tig <[email protected]>
Thomas Nind 2 жил өмнө
parent
commit
01544dc60c

+ 16 - 201
Terminal.Gui/Text/CollectionNavigator.cs

@@ -1,225 +1,40 @@
 using System;
 using System;
+using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 
 
 namespace Terminal.Gui {
 namespace Terminal.Gui {
-	/// <summary>
-	/// Navigates a collection of items using keystrokes. The keystrokes are used to build a search string. 
-	/// The <see cref="SearchString"/> is used to find the next item in the collection that matches the search string
-	/// when <see cref="GetNextMatchingItem(int, char)"/> is called.
-	/// <para>
-	/// If the user types keystrokes that can't be found in the collection, 
-	/// the search string is cleared and the next item is found that starts with the last keystroke.
-	/// </para>
-	/// <para>
-	/// If the user pauses keystrokes for a short time (see <see cref="TypingDelay"/>), the search string is cleared.
-	/// </para>
-	/// </summary>
-	public partial class CollectionNavigator {
-		/// <summary>
-		/// Constructs a new CollectionNavigator.
-		/// </summary>
-		public CollectionNavigator () { }
-
-		/// <summary>
-		/// Constructs a new CollectionNavigator for the given collection.
-		/// </summary>
-		/// <param name="collection"></param>
-		public CollectionNavigator (IEnumerable<object> collection) => Collection = collection;
-
-		DateTime lastKeystroke = DateTime.Now;
-		/// <summary>
-		/// Gets or sets the number of milliseconds to delay before clearing the search string. The delay is
-		/// reset on each call to <see cref="GetNextMatchingItem(int, char)"/>. The default is 500ms.
-		/// </summary>
-		public int TypingDelay { get; set; } = 500;
-
-		/// <summary>
-		/// The compararer function to use when searching the collection.
-		/// </summary>
-		public StringComparer Comparer { get; set; } = StringComparer.InvariantCultureIgnoreCase;
 
 
+	/// <inheritdoc/>
+	/// <remarks>This implementation is based on a static <see cref="Collection"/> of objects.</remarks>
+	public class CollectionNavigator : CollectionNavigatorBase {
 		/// <summary>
 		/// <summary>
 		/// The collection of objects to search. <see cref="object.ToString()"/> is used to search the collection.
 		/// The collection of objects to search. <see cref="object.ToString()"/> is used to search the collection.
 		/// </summary>
 		/// </summary>
-		public IEnumerable<object> Collection { get; set; }
-
-		/// <summary>
-		/// This event is invoked when <see cref="SearchString"/>  changes. Useful for debugging.
-		/// </summary>
-		public event EventHandler<KeystrokeNavigatorEventArgs> SearchStringChanged;
-
-		private string _searchString = "";
-		/// <summary>
-		/// Gets the current search string. This includes the set of keystrokes that have been pressed
-		/// since the last unsuccessful match or after <see cref="TypingDelay"/>) milliseconds. Useful for debugging.
-		/// </summary>
-		public string SearchString {
-			get => _searchString;
-			private set {
-				_searchString = value;
-				OnSearchStringChanged (new KeystrokeNavigatorEventArgs (value));
-			}
-		}
+		public IList Collection { get; set; }
 
 
 		/// <summary>
 		/// <summary>
-		/// Invoked when the <see cref="SearchString"/> changes. Useful for debugging. Invokes the <see cref="SearchStringChanged"/> event.
-		/// </summary>
-		/// <param name="e"></param>
-		public virtual void OnSearchStringChanged (KeystrokeNavigatorEventArgs e)
-		{
-			SearchStringChanged?.Invoke (this, e);
-		}
-
-		/// <summary>
-		/// Gets the index of the next item in the collection that matches the current <see cref="SearchString"/> plus the provided character (typically
-		/// from a key press).
+		/// Constructs a new CollectionNavigator.
 		/// </summary>
 		/// </summary>
-		/// <param name="currentIndex">The index in the collection to start the search from.</param>
-		/// <param name="keyStruck">The character of the key the user pressed.</param>
-		/// <returns>The index of the item that matches what the user has typed. 
-		/// Returns <see langword="-1"/> if no item in the collection matched.</returns>
-		public int GetNextMatchingItem (int currentIndex, char keyStruck)
-		{
-			AssertCollectionIsNotNull ();
-			if (!char.IsControl (keyStruck)) {
-
-				// maybe user pressed 'd' and now presses 'd' again.
-				// a candidate search is things that begin with "dd"
-				// but if we find none then we must fallback on cycling
-				// d instead and discard the candidate state
-				string candidateState = "";
-
-				// is it a second or third (etc) keystroke within a short time
-				if (SearchString.Length > 0 && DateTime.Now - lastKeystroke < TimeSpan.FromMilliseconds (TypingDelay)) {
-					// "dd" is a candidate
-					candidateState = SearchString + keyStruck;
-				} else {
-					// its a fresh keystroke after some time
-					// or its first ever key press
-					SearchString = new string (keyStruck, 1);
-				}
-
-				var idxCandidate = GetNextMatchingItem (currentIndex, candidateState,
-					// prefer not to move if there are multiple characters e.g. "ca" + 'r' should stay on "car" and not jump to "cart"
-					candidateState.Length > 1);
-
-				if (idxCandidate != -1) {
-					// found "dd" so candidate searchstring is accepted
-					lastKeystroke = DateTime.Now;
-					SearchString = candidateState;
-					return idxCandidate;
-				}
-
-				//// nothing matches "dd" so discard it as a candidate
-				//// and just cycle "d" instead
-				lastKeystroke = DateTime.Now;
-				idxCandidate = GetNextMatchingItem (currentIndex, candidateState);
-
-				// if a match wasn't found, the user typed a 'wrong' key in their search ("can" + 'z'
-				// instead of "can" + 'd').
-				if (SearchString.Length > 1 && idxCandidate == -1) {
-					// ignore it since we're still within the typing delay
-					// don't add it to SearchString either
-					return currentIndex;
-				}
-
-				// if no changes to current state manifested
-				if (idxCandidate == currentIndex || idxCandidate == -1) {
-					// clear history and treat as a fresh letter
-					ClearSearchString ();
-					
-					// match on the fresh letter alone
-					SearchString = new string (keyStruck, 1);
-					idxCandidate = GetNextMatchingItem (currentIndex, SearchString);
-					return idxCandidate == -1 ? currentIndex : idxCandidate;
-				}
-
-				// Found another "d" or just leave index as it was
-				return idxCandidate;
-
-			} else {
-				// clear state because keypress was a control char
-				ClearSearchString ();
-
-				// control char indicates no selection
-				return -1;
-			}
-		}
+		public CollectionNavigator () { }
 
 
 		/// <summary>
 		/// <summary>
-		/// Gets the index of the next item in the collection that matches <paramref name="search"/>. 
+		/// Constructs a new CollectionNavigator for the given collection.
 		/// </summary>
 		/// </summary>
-		/// <param name="currentIndex">The index in the collection to start the search from.</param>
-		/// <param name="search">The search string to use.</param>
-		/// <param name="minimizeMovement">Set to <see langword="true"/> to stop the search on the first match
-		/// if there are multiple matches for <paramref name="search"/>.
-		/// e.g. "ca" + 'r' should stay on "car" and not jump to "cart". If <see langword="false"/> (the default), 
-		/// the next matching item will be returned, even if it is above in the collection.
-		/// </param>
-		/// <returns>The index of the next matching item or <see langword="-1"/> if no match was found.</returns>
-		internal int GetNextMatchingItem (int currentIndex, string search, bool minimizeMovement = false)
-		{
-			if (string.IsNullOrEmpty (search)) {
-				return -1;
-			}
-			AssertCollectionIsNotNull ();
-
-			// find indexes of items that start with the search text
-			int [] matchingIndexes = Collection.Select ((item, idx) => (item, idx))
-				  .Where (k => k.item?.ToString ().StartsWith (search, StringComparison.InvariantCultureIgnoreCase) ?? false)
-				  .Select (k => k.idx)
-				  .ToArray ();
-
-			// if there are items beginning with search
-			if (matchingIndexes.Length > 0) {
-				// is one of them currently selected?
-				var currentlySelected = Array.IndexOf (matchingIndexes, currentIndex);
-
-				if (currentlySelected == -1) {
-					// we are not currently selecting any item beginning with the search
-					// so jump to first item in list that begins with the letter
-					return matchingIndexes [0];
-				} else {
-
-					// the current index is part of the matching collection
-					if (minimizeMovement) {
-						// if we would rather not jump around (e.g. user is typing lots of text to get this match)
-						return matchingIndexes [currentlySelected];
-					}
-
-					// cycle to next (circular)
-					return matchingIndexes [(currentlySelected + 1) % matchingIndexes.Length];
-				}
-			}
-
-			// nothing starts with the search
-			return -1;
-		}
+		/// <param name="collection"></param>
+		public CollectionNavigator (IList collection) => Collection = collection;
 
 
-		private void AssertCollectionIsNotNull ()
+		/// <inheritdoc/>
+		protected override object ElementAt (int idx)
 		{
 		{
-			if (Collection == null) {
-				throw new InvalidOperationException ("Collection is null");
-			}
+			return Collection [idx];
 		}
 		}
 
 
-		private void ClearSearchString ()
+		/// <inheritdoc/>
+		protected override int GetCollectionLength ()
 		{
 		{
-			SearchString = "";
-			lastKeystroke = DateTime.Now;
+			return Collection.Count;
 		}
 		}
 
 
-		/// <summary>
-		/// Returns true if <paramref name="kb"/> is a searchable key
-		/// (e.g. letters, numbers, etc) that are valid to pass to this
-		/// class for search filtering.
-		/// </summary>
-		/// <param name="kb"></param>
-		/// <returns></returns>
-		public static bool IsCompatibleKey (KeyEvent kb)
-		{
-			return !kb.IsAlt && !kb.IsCtrl;
-		}
 	}
 	}
 }
 }

+ 217 - 0
Terminal.Gui/Text/CollectionNavigatorBase.cs

@@ -0,0 +1,217 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Terminal.Gui {
+	/// <summary>
+	/// Navigates a collection of items using keystrokes. The keystrokes are used to build a search string. 
+	/// The <see cref="SearchString"/> is used to find the next item in the collection that matches the search string
+	/// when <see cref="GetNextMatchingItem(int, char)"/> is called.
+	/// <para>
+	/// If the user types keystrokes that can't be found in the collection, 
+	/// the search string is cleared and the next item is found that starts with the last keystroke.
+	/// </para>
+	/// <para>
+	/// If the user pauses keystrokes for a short time (see <see cref="TypingDelay"/>), the search string is cleared.
+	/// </para>
+	/// </summary>
+	public abstract class CollectionNavigatorBase {
+
+		DateTime lastKeystroke = DateTime.Now;
+		/// <summary>
+		/// Gets or sets the number of milliseconds to delay before clearing the search string. The delay is
+		/// reset on each call to <see cref="GetNextMatchingItem(int, char)"/>. The default is 500ms.
+		/// </summary>
+		public int TypingDelay { get; set; } = 500;
+
+		/// <summary>
+		/// The compararer function to use when searching the collection.
+		/// </summary>
+		public StringComparer Comparer { get; set; } = StringComparer.InvariantCultureIgnoreCase;
+
+		/// <summary>
+		/// This event is invoked when <see cref="SearchString"/>  changes. Useful for debugging.
+		/// </summary>
+		public event EventHandler<KeystrokeNavigatorEventArgs> SearchStringChanged;
+
+		private string _searchString = "";
+		/// <summary>
+		/// Gets the current search string. This includes the set of keystrokes that have been pressed
+		/// since the last unsuccessful match or after <see cref="TypingDelay"/>) milliseconds. Useful for debugging.
+		/// </summary>
+		public string SearchString {
+			get => _searchString;
+			private set {
+				_searchString = value;
+				OnSearchStringChanged (new KeystrokeNavigatorEventArgs (value));
+			}
+		}
+
+		/// <summary>
+		/// Invoked when the <see cref="SearchString"/> changes. Useful for debugging. Invokes the <see cref="SearchStringChanged"/> event.
+		/// </summary>
+		/// <param name="e"></param>
+		public virtual void OnSearchStringChanged (KeystrokeNavigatorEventArgs e)
+		{
+			SearchStringChanged?.Invoke (this, e);
+		}
+
+		/// <summary>
+		/// Gets the index of the next item in the collection that matches the current <see cref="SearchString"/> plus the provided character (typically
+		/// from a key press).
+		/// </summary>
+		/// <param name="currentIndex">The index in the collection to start the search from.</param>
+		/// <param name="keyStruck">The character of the key the user pressed.</param>
+		/// <returns>The index of the item that matches what the user has typed. 
+		/// Returns <see langword="-1"/> if no item in the collection matched.</returns>
+		public int GetNextMatchingItem (int currentIndex, char keyStruck)
+		{
+			if (!char.IsControl (keyStruck)) {
+
+				// maybe user pressed 'd' and now presses 'd' again.
+				// a candidate search is things that begin with "dd"
+				// but if we find none then we must fallback on cycling
+				// d instead and discard the candidate state
+				string candidateState = "";
+
+				// is it a second or third (etc) keystroke within a short time
+				if (SearchString.Length > 0 && DateTime.Now - lastKeystroke < TimeSpan.FromMilliseconds (TypingDelay)) {
+					// "dd" is a candidate
+					candidateState = SearchString + keyStruck;
+				} else {
+					// its a fresh keystroke after some time
+					// or its first ever key press
+					SearchString = new string (keyStruck, 1);
+				}
+
+				var idxCandidate = GetNextMatchingItem (currentIndex, candidateState,
+					// prefer not to move if there are multiple characters e.g. "ca" + 'r' should stay on "car" and not jump to "cart"
+					candidateState.Length > 1);
+
+				if (idxCandidate != -1) {
+					// found "dd" so candidate searchstring is accepted
+					lastKeystroke = DateTime.Now;
+					SearchString = candidateState;
+					return idxCandidate;
+				}
+
+				//// nothing matches "dd" so discard it as a candidate
+				//// and just cycle "d" instead
+				lastKeystroke = DateTime.Now;
+				idxCandidate = GetNextMatchingItem (currentIndex, candidateState);
+
+				// if a match wasn't found, the user typed a 'wrong' key in their search ("can" + 'z'
+				// instead of "can" + 'd').
+				if (SearchString.Length > 1 && idxCandidate == -1) {
+					// ignore it since we're still within the typing delay
+					// don't add it to SearchString either
+					return currentIndex;
+				}
+
+				// if no changes to current state manifested
+				if (idxCandidate == currentIndex || idxCandidate == -1) {
+					// clear history and treat as a fresh letter
+					ClearSearchString ();
+
+					// match on the fresh letter alone
+					SearchString = new string (keyStruck, 1);
+					idxCandidate = GetNextMatchingItem (currentIndex, SearchString);
+					return idxCandidate == -1 ? currentIndex : idxCandidate;
+				}
+
+				// Found another "d" or just leave index as it was
+				return idxCandidate;
+
+			} else {
+				// clear state because keypress was a control char
+				ClearSearchString ();
+
+				// control char indicates no selection
+				return -1;
+			}
+		}
+
+		/// <summary>
+		/// Gets the index of the next item in the collection that matches <paramref name="search"/>. 
+		/// </summary>
+		/// <param name="currentIndex">The index in the collection to start the search from.</param>
+		/// <param name="search">The search string to use.</param>
+		/// <param name="minimizeMovement">Set to <see langword="true"/> to stop the search on the first match
+		/// if there are multiple matches for <paramref name="search"/>.
+		/// e.g. "ca" + 'r' should stay on "car" and not jump to "cart". If <see langword="false"/> (the default), 
+		/// the next matching item will be returned, even if it is above in the collection.
+		/// </param>
+		/// <returns>The index of the next matching item or <see langword="-1"/> if no match was found.</returns>
+		internal int GetNextMatchingItem (int currentIndex, string search, bool minimizeMovement = false)
+		{
+			if (string.IsNullOrEmpty (search)) {
+				return -1;
+			}
+
+			var collectionLength = GetCollectionLength ();
+
+			if (currentIndex != -1 && currentIndex < collectionLength && IsMatch (search, ElementAt (currentIndex))) {
+				// we are already at a match
+				if (minimizeMovement) {
+					// if we would rather not jump around (e.g. user is typing lots of text to get this match)
+					return currentIndex;
+				}
+
+				for (int i = 1; i < collectionLength; i++) {
+					//circular
+					var idxCandidate = (i + currentIndex) % collectionLength;
+					if (IsMatch (search, ElementAt (idxCandidate))) {
+						return idxCandidate;
+					}
+				}
+
+				// nothing else starts with the search term
+				return currentIndex;
+			} else {
+				// search terms no longer match the current selection or there is none
+				for (int i = 0; i < collectionLength; i++) {
+					if (IsMatch (search, ElementAt (i))) {
+						return i;
+					}
+				}
+
+				// Nothing matches
+				return -1;
+			}
+		}
+
+		/// <summary>
+		/// Return the number of elements in the collection
+		/// </summary>
+		protected abstract int GetCollectionLength ();
+
+		private bool IsMatch (string search, object value)
+		{
+			return value?.ToString ().StartsWith (search, StringComparison.InvariantCultureIgnoreCase) ?? false;
+		}
+
+		/// <summary>
+		/// Returns the collection being navigated element at <paramref name="idx"/>.
+		/// </summary>
+		/// <returns></returns>
+		protected abstract object ElementAt (int idx);
+
+		private void ClearSearchString ()
+		{
+			SearchString = "";
+			lastKeystroke = DateTime.Now;
+		}
+
+		/// <summary>
+		/// Returns true if <paramref name="kb"/> is a searchable key
+		/// (e.g. letters, numbers, etc) that are valid to pass to this
+		/// class for search filtering.
+		/// </summary>
+		/// <param name="kb"></param>
+		/// <returns></returns>
+		public static bool IsCompatibleKey (KeyEvent kb)
+		{
+			return !kb.IsAlt && !kb.IsCtrl;
+		}
+	}
+}

+ 35 - 0
Terminal.Gui/Text/TableCollectionNavigator.cs

@@ -0,0 +1,35 @@
+
+
+namespace Terminal.Gui {
+
+	/// <summary>
+	/// Collection navigator for cycling selections in a <see cref="TableView"/>.
+	/// </summary>
+	public class TableCollectionNavigator : CollectionNavigatorBase {
+		readonly TableView tableView;
+
+		/// <summary>
+		/// Creates a new instance for navigating the data in the wrapped <paramref name="tableView"/>.
+		/// </summary>
+		public TableCollectionNavigator (TableView tableView)
+		{
+			this.tableView = tableView;
+		}
+
+		/// <inheritdoc/>
+		protected override object ElementAt (int idx)
+		{
+			var col = tableView.SelectedColumn;
+			var rawValue = tableView.Table [idx, col];
+
+			var style = this.tableView.Style.GetColumnStyleIfAny (col);
+			return style?.RepresentationGetter?.Invoke (rawValue) ?? rawValue;
+		}
+
+		/// <inheritdoc/>
+		protected override int GetCollectionLength ()
+		{
+			return tableView.Table.Rows;
+		}
+	}
+}

+ 62 - 97
Terminal.Gui/Views/FileDialog.cs

@@ -97,9 +97,6 @@ namespace Terminal.Gui {
 		private Button btnBack;
 		private Button btnBack;
 		private Button btnUp;
 		private Button btnUp;
 		private string feedback;
 		private string feedback;
-
-		private CollectionNavigator collectionNavigator = new CollectionNavigator ();
-
 		private TextField tbFind;
 		private TextField tbFind;
 		private SpinnerView spinnerView;
 		private SpinnerView spinnerView;
 		private MenuBar allowedTypeMenuBar;
 		private MenuBar allowedTypeMenuBar;
@@ -107,7 +104,7 @@ namespace Terminal.Gui {
 		private MenuItem [] allowedTypeMenuItems;
 		private MenuItem [] allowedTypeMenuItems;
 
 
 		private int currentSortColumn;
 		private int currentSortColumn;
-		
+
 		private bool currentSortIsAsc = true;
 		private bool currentSortIsAsc = true;
 
 
 		/// <summary>
 		/// <summary>
@@ -128,7 +125,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// <summary>
 		/// Initializes a new instance of the <see cref="FileDialog"/> class.
 		/// Initializes a new instance of the <see cref="FileDialog"/> class.
 		/// </summary>
 		/// </summary>
-		public FileDialog () : this(new FileSystem())
+		public FileDialog () : this (new FileSystem ())
 		{
 		{
 
 
 		}
 		}
@@ -184,7 +181,7 @@ namespace Terminal.Gui {
 			this.btnBack.Clicked += (s, e) => this.history.Back ();
 			this.btnBack.Clicked += (s, e) => this.history.Back ();
 
 
 			this.btnForward = new Button () { X = Pos.Right (btnBack) + 1, Y = 1, NoPadding = true };
 			this.btnForward = new Button () { X = Pos.Right (btnBack) + 1, Y = 1, NoPadding = true };
-			btnForward.Text = GetForwardButtonText();
+			btnForward.Text = GetForwardButtonText ();
 			this.btnForward.Clicked += (s, e) => this.history.Forward ();
 			this.btnForward.Clicked += (s, e) => this.history.Forward ();
 
 
 			this.tbPath = new TextField {
 			this.tbPath = new TextField {
@@ -211,15 +208,15 @@ namespace Terminal.Gui {
 				Height = Dim.Fill (1),
 				Height = Dim.Fill (1),
 			};
 			};
 			this.splitContainer.SetSplitterPos (0, 30);
 			this.splitContainer.SetSplitterPos (0, 30);
-//			this.splitContainer.Border.BorderStyle = BorderStyle.None;
+			//			this.splitContainer.Border.BorderStyle = BorderStyle.None;
 			this.splitContainer.Tiles.ElementAt (0).ContentView.Visible = false;
 			this.splitContainer.Tiles.ElementAt (0).ContentView.Visible = false;
 
 
-			this.tableView = new TableView () {
+			this.tableView = new TableView {
 				Width = Dim.Fill (),
 				Width = Dim.Fill (),
 				Height = Dim.Fill (),
 				Height = Dim.Fill (),
 				FullRowSelect = true,
 				FullRowSelect = true,
+				CollectionNavigator = new FileDialogCollectionNavigator (this)
 			};
 			};
-
 			this.tableView.AddKeyBinding (Key.Space, Command.ToggleChecked);
 			this.tableView.AddKeyBinding (Key.Space, Command.ToggleChecked);
 			this.tableView.MouseClick += OnTableViewMouseClick;
 			this.tableView.MouseClick += OnTableViewMouseClick;
 			Style.TableStyle = tableView.Style;
 			Style.TableStyle = tableView.Style;
@@ -255,14 +252,6 @@ namespace Terminal.Gui {
 				if (k.Handled) {
 				if (k.Handled) {
 					return;
 					return;
 				}
 				}
-
-				if (this.tableView.HasFocus &&
-				!k.KeyEvent.Key.HasFlag (Key.CtrlMask) &&
-				!k.KeyEvent.Key.HasFlag (Key.AltMask) &&
-					char.IsLetterOrDigit ((char)k.KeyEvent.KeyValue)) {
-					CycleToNextTableEntryBeginningWith (k);
-				}
-
 			};
 			};
 
 
 			this.treeView = new TreeView<object> () {
 			this.treeView = new TreeView<object> () {
@@ -288,7 +277,7 @@ namespace Terminal.Gui {
 				var newState = !tile.ContentView.Visible;
 				var newState = !tile.ContentView.Visible;
 				tile.ContentView.Visible = newState;
 				tile.ContentView.Visible = newState;
 				this.btnToggleSplitterCollapse.Text = GetToggleSplitterText (newState);
 				this.btnToggleSplitterCollapse.Text = GetToggleSplitterText (newState);
-				this.LayoutSubviews();
+				this.LayoutSubviews ();
 			};
 			};
 
 
 			tbFind = new TextField {
 			tbFind = new TextField {
@@ -311,12 +300,12 @@ namespace Terminal.Gui {
 					o.Handled = true;
 					o.Handled = true;
 				}
 				}
 
 
-				if(o.KeyEvent.Key == Key.Esc) {
-					if(CancelSearch()) {
+				if (o.KeyEvent.Key == Key.Esc) {
+					if (CancelSearch ()) {
 						o.Handled = true;
 						o.Handled = true;
 					}
 					}
 				}
 				}
-				if(tbFind.CursorIsAtEnd()) {
+				if (tbFind.CursorIsAtEnd ()) {
 					NavigateIf (o, Key.CursorRight, btnCancel);
 					NavigateIf (o, Key.CursorRight, btnCancel);
 				}
 				}
 				if (tbFind.CursorIsAtStart ()) {
 				if (tbFind.CursorIsAtStart ()) {
@@ -421,7 +410,7 @@ namespace Terminal.Gui {
 
 
 		private string GetToggleSplitterText (bool isExpanded)
 		private string GetToggleSplitterText (bool isExpanded)
 		{
 		{
-			return isExpanded ? 
+			return isExpanded ?
 				new string ((char)Driver.LeftArrow, 2) :
 				new string ((char)Driver.LeftArrow, 2) :
 				new string ((char)Driver.RightArrow, 2);
 				new string ((char)Driver.RightArrow, 2);
 		}
 		}
@@ -545,39 +534,6 @@ namespace Terminal.Gui {
 			feedback = null;
 			feedback = null;
 		}
 		}
 
 
-		private void CycleToNextTableEntryBeginningWith (KeyEventEventArgs keyEvent)
-		{
-			if (tableView.Table.Rows == 0) {
-				return;
-			}
-
-			var row = tableView.SelectedRow;
-
-			// There is a multi select going on and not just for the current row
-			if (tableView.GetAllSelectedCells ().Any (c => c.Y != row)) {
-				return;
-			}
-
-			int match = collectionNavigator.GetNextMatchingItem (row, (char)keyEvent.KeyEvent.KeyValue);
-
-			if (match != -1) {
-				tableView.SelectedRow = match;
-				tableView.EnsureValidSelection ();
-				tableView.EnsureSelectedCellIsVisible ();
-				keyEvent.Handled = true;
-			}
-		}
-
-		private void UpdateCollectionNavigator ()
-		{
-			tableView.EnsureValidSelection ();
-			var col = tableView.SelectedColumn;
-			var style = tableView.Style.GetColumnStyleIfAny (col);
-
-			var collection = State.Children.Select (s=> FileDialogTableSource.GetRawColumnValue(col,s));
-			collectionNavigator = new CollectionNavigator (collection);
-		}
-
 		/// <summary>
 		/// <summary>
 		/// Gets or Sets which <see cref="System.IO.FileSystemInfo"/> type can be selected.
 		/// Gets or Sets which <see cref="System.IO.FileSystemInfo"/> type can be selected.
 		/// Defaults to <see cref="OpenMode.Mixed"/> (i.e. <see cref="DirectoryInfo"/> or
 		/// Defaults to <see cref="OpenMode.Mixed"/> (i.e. <see cref="DirectoryInfo"/> or
@@ -672,11 +628,11 @@ namespace Terminal.Gui {
 
 
 			// May have been updated after instance was constructed
 			// May have been updated after instance was constructed
 			this.btnOk.Text = Style.OkButtonText;
 			this.btnOk.Text = Style.OkButtonText;
-			this.btnUp.Text = this.GetUpButtonText();
-			this.btnBack.Text = this.GetBackButtonText();
-			this.btnForward.Text = this.GetForwardButtonText();
-			this.btnToggleSplitterCollapse.Text = this.GetToggleSplitterText(false);
-			
+			this.btnUp.Text = this.GetUpButtonText ();
+			this.btnBack.Text = this.GetBackButtonText ();
+			this.btnForward.Text = this.GetForwardButtonText ();
+			this.btnToggleSplitterCollapse.Text = this.GetToggleSplitterText (false);
+
 			tbPath.Autocomplete.ColorScheme.Normal = Attribute.Make (Color.Black, tbPath.ColorScheme.Normal.Background);
 			tbPath.Autocomplete.ColorScheme.Normal = Attribute.Make (Color.Black, tbPath.ColorScheme.Normal.Background);
 
 
 			treeView.AddObjects (Style.TreeRootGetter ());
 			treeView.AddObjects (Style.TreeRootGetter ());
@@ -714,7 +670,7 @@ namespace Terminal.Gui {
 				};
 				};
 
 
 				allowedTypeMenuBar.DrawContentComplete += (s, e) => {
 				allowedTypeMenuBar.DrawContentComplete += (s, e) => {
-					
+
 					allowedTypeMenuBar.Move (e.Rect.Width - 1, 0);
 					allowedTypeMenuBar.Move (e.Rect.Width - 1, 0);
 					Driver.AddRune (Driver.DownArrow);
 					Driver.AddRune (Driver.DownArrow);
 
 
@@ -812,7 +768,7 @@ namespace Terminal.Gui {
 
 
 			// Don't include ".." (IsParent) in multiselections
 			// Don't include ".." (IsParent) in multiselections
 			this.MultiSelected = toMultiAccept
 			this.MultiSelected = toMultiAccept
-				.Where(s=>!s.IsParent)
+				.Where (s => !s.IsParent)
 				.Select (s => s.FileSystemInfo.FullName)
 				.Select (s => s.FileSystemInfo.FullName)
 				.ToList ().AsReadOnly ();
 				.ToList ().AsReadOnly ();
 
 
@@ -840,8 +796,7 @@ namespace Terminal.Gui {
 
 
 		private void Accept (bool allowMulti)
 		private void Accept (bool allowMulti)
 		{
 		{
-			if(allowMulti && TryAcceptMulti())
-			{
+			if (allowMulti && TryAcceptMulti ()) {
 				return;
 				return;
 			}
 			}
 
 
@@ -953,10 +908,6 @@ namespace Terminal.Gui {
 
 
 				this.pushingState = false;
 				this.pushingState = false;
 			}
 			}
-
-			if (obj.NewCol != obj.OldCol) {
-				UpdateCollectionNavigator ();
-			}
 		}
 		}
 
 
 		private bool TableView_KeyUp (KeyEvent keyEvent)
 		private bool TableView_KeyUp (KeyEvent keyEvent)
@@ -990,8 +941,7 @@ namespace Terminal.Gui {
 
 
 		private void CellActivate (object sender, CellActivatedEventArgs obj)
 		private void CellActivate (object sender, CellActivatedEventArgs obj)
 		{
 		{
-			if(TryAcceptMulti())
-			{
+			if (TryAcceptMulti ()) {
 				return;
 				return;
 			}
 			}
 
 
@@ -1011,20 +961,16 @@ namespace Terminal.Gui {
 		{
 		{
 			var multi = this.MultiRowToStats ();
 			var multi = this.MultiRowToStats ();
 			string reason = null;
 			string reason = null;
-			
-			if (!multi.Any ())
-			{
+
+			if (!multi.Any ()) {
 				return false;
 				return false;
 			}
 			}
-			
+
 			if (multi.All (m => this.IsCompatibleWithOpenMode (
 			if (multi.All (m => this.IsCompatibleWithOpenMode (
-				m.FileSystemInfo.FullName, out reason)))
-			{
+				m.FileSystemInfo.FullName, out reason))) {
 				this.Accept (multi);
 				this.Accept (multi);
 				return true;
 				return true;
-			} 
-			else 
-			{
+			} else {
 				if (reason != null) {
 				if (reason != null) {
 					feedback = reason;
 					feedback = reason;
 					SetNeedsDisplay ();
 					SetNeedsDisplay ();
@@ -1156,12 +1102,10 @@ namespace Terminal.Gui {
 
 
 				this.tbPath.Autocomplete.ClearSuggestions ();
 				this.tbPath.Autocomplete.ClearSuggestions ();
 
 
-				if(pathText != null)
-				{
+				if (pathText != null) {
 					this.tbPath.Text = pathText;
 					this.tbPath.Text = pathText;
 					this.tbPath.MoveEnd ();
 					this.tbPath.MoveEnd ();
-				}
-				else
+				} else
 				if (setPathText) {
 				if (setPathText) {
 					this.tbPath.Text = newState.Directory.FullName;
 					this.tbPath.Text = newState.Directory.FullName;
 					this.tbPath.MoveEnd ();
 					this.tbPath.MoveEnd ();
@@ -1199,7 +1143,6 @@ namespace Terminal.Gui {
 
 
 			this.ApplySort ();
 			this.ApplySort ();
 			this.tableView.Update ();
 			this.tableView.Update ();
-			UpdateCollectionNavigator ();
 		}
 		}
 
 
 		private ColorScheme ColorGetter (TableView.CellColorGetterArgs args)
 		private ColorScheme ColorGetter (TableView.CellColorGetterArgs args)
@@ -1252,7 +1195,7 @@ namespace Terminal.Gui {
 		{
 		{
 			return this.State?.Children [rowIndex];
 			return this.State?.Children [rowIndex];
 		}
 		}
-	
+
 		private void PathChanged ()
 		private void PathChanged ()
 		{
 		{
 			// avoid re-entry
 			// avoid re-entry
@@ -1284,10 +1227,10 @@ namespace Terminal.Gui {
 			// where the FullName is in fact the current working directory.
 			// where the FullName is in fact the current working directory.
 			// really not what most users would expect
 			// really not what most users would expect
 			if (Regex.IsMatch (path, "^\\w:$")) {
 			if (Regex.IsMatch (path, "^\\w:$")) {
-				return fileSystem.DirectoryInfo.New(path + System.IO.Path.DirectorySeparatorChar);
+				return fileSystem.DirectoryInfo.New (path + System.IO.Path.DirectorySeparatorChar);
 			}
 			}
 
 
-			return fileSystem.DirectoryInfo.New(path);
+			return fileSystem.DirectoryInfo.New (path);
 		}
 		}
 
 
 		/// <summary>
 		/// <summary>
@@ -1296,7 +1239,7 @@ namespace Terminal.Gui {
 		/// <param name="toRestore"></param>
 		/// <param name="toRestore"></param>
 		internal void RestoreSelection (IFileSystemInfo toRestore)
 		internal void RestoreSelection (IFileSystemInfo toRestore)
 		{
 		{
-			tableView.SelectedRow = State.Children.IndexOf (r=>r.FileSystemInfo == toRestore);
+			tableView.SelectedRow = State.Children.IndexOf (r => r.FileSystemInfo == toRestore);
 			tableView.EnsureSelectedCellIsVisible ();
 			tableView.EnsureSelectedCellIsVisible ();
 		}
 		}
 
 
@@ -1307,25 +1250,24 @@ namespace Terminal.Gui {
 			// This portion is never reordered (aways .. at top then folders)
 			// This portion is never reordered (aways .. at top then folders)
 			var forcedOrder = stats
 			var forcedOrder = stats
 			.OrderByDescending (f => f.IsParent)
 			.OrderByDescending (f => f.IsParent)
-					.ThenBy (f => f.IsDir() ? -1:100);
+					.ThenBy (f => f.IsDir () ? -1 : 100);
 
 
 			// This portion is flexible based on the column clicked (e.g. alphabetical)
 			// This portion is flexible based on the column clicked (e.g. alphabetical)
-			var ordered = 
+			var ordered =
 				this.currentSortIsAsc ?
 				this.currentSortIsAsc ?
-					forcedOrder.ThenBy (f => FileDialogTableSource.GetRawColumnValue(currentSortColumn,f)):
+					forcedOrder.ThenBy (f => FileDialogTableSource.GetRawColumnValue (currentSortColumn, f)) :
 					forcedOrder.ThenByDescending (f => FileDialogTableSource.GetRawColumnValue (currentSortColumn, f));
 					forcedOrder.ThenByDescending (f => FileDialogTableSource.GetRawColumnValue (currentSortColumn, f));
 
 
-			State.Children = ordered.ToArray();
+			State.Children = ordered.ToArray ();
 
 
 			this.tableView.Update ();
 			this.tableView.Update ();
-			UpdateCollectionNavigator ();
 		}
 		}
 
 
 		private void SortColumn (int clickedCol)
 		private void SortColumn (int clickedCol)
 		{
 		{
 			this.GetProposedNewSortOrder (clickedCol, out var isAsc);
 			this.GetProposedNewSortOrder (clickedCol, out var isAsc);
 			this.SortColumn (clickedCol, isAsc);
 			this.SortColumn (clickedCol, isAsc);
-			this.tableView.Table = new FileDialogTableSource(State,Style,currentSortColumn,currentSortIsAsc);
+			this.tableView.Table = new FileDialogTableSource (State, Style, currentSortColumn, currentSortIsAsc);
 		}
 		}
 
 
 		internal void SortColumn (int col, bool isAsc)
 		internal void SortColumn (int col, bool isAsc)
@@ -1342,7 +1284,7 @@ namespace Terminal.Gui {
 			// work out new sort order
 			// work out new sort order
 			if (this.currentSortColumn == clickedCol && this.currentSortIsAsc) {
 			if (this.currentSortColumn == clickedCol && this.currentSortIsAsc) {
 				isAsc = false;
 				isAsc = false;
-				return $"{tableView.Table.ColumnNames[clickedCol]} DESC";
+				return $"{tableView.Table.ColumnNames [clickedCol]} DESC";
 			} else {
 			} else {
 				isAsc = true;
 				isAsc = true;
 				return $"{tableView.Table.ColumnNames [clickedCol]} ASC";
 				return $"{tableView.Table.ColumnNames [clickedCol]} ASC";
@@ -1399,7 +1341,7 @@ namespace Terminal.Gui {
 			style.Visible = false;
 			style.Visible = false;
 			this.tableView.Update ();
 			this.tableView.Update ();
 		}
 		}
-		
+
 		/// <summary>
 		/// <summary>
 		/// State representing a recursive search from <see cref="FileDialogState.Directory"/>
 		/// State representing a recursive search from <see cref="FileDialogState.Directory"/>
 		/// downwards.
 		/// downwards.
@@ -1520,7 +1462,7 @@ namespace Terminal.Gui {
 			/// <returns></returns>
 			/// <returns></returns>
 			internal bool Cancel ()
 			internal bool Cancel ()
 			{
 			{
-				var alreadyCancelled = token.IsCancellationRequested || cancel; 
+				var alreadyCancelled = token.IsCancellationRequested || cancel;
 
 
 				cancel = true;
 				cancel = true;
 				token.Cancel ();
 				token.Cancel ();
@@ -1528,5 +1470,28 @@ namespace Terminal.Gui {
 				return !alreadyCancelled;
 				return !alreadyCancelled;
 			}
 			}
 		}
 		}
+		internal class FileDialogCollectionNavigator : CollectionNavigatorBase {
+			private FileDialog fileDialog;
+
+			public FileDialogCollectionNavigator (FileDialog fileDialog)
+			{
+				this.fileDialog = fileDialog;
+			}
+
+			protected override object ElementAt (int idx)
+			{
+				var val = FileDialogTableSource.GetRawColumnValue (fileDialog.tableView.SelectedColumn, fileDialog.State?.Children [idx]);
+				if (val == null) {
+					return string.Empty;
+				}
+
+				return val.ToString ().Trim ('.');
+			}
+
+			protected override int GetCollectionLength ()
+			{
+				return fileDialog.State?.Children.Length ?? 0;
+			}
+		}
 	}
 	}
 }
 }

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

@@ -110,7 +110,7 @@ namespace Terminal.Gui {
 			get => source;
 			get => source;
 			set {
 			set {
 				source = value;
 				source = value;
-				KeystrokeNavigator.Collection = source?.ToList ()?.Cast<object> ();
+				KeystrokeNavigator.Collection = source?.ToList ();
 				top = 0;
 				top = 0;
 				selected = -1;
 				selected = -1;
 				lastSelectedItem = -1;
 				lastSelectedItem = -1;

+ 39 - 0
Terminal.Gui/Views/TableView/TableView.cs

@@ -163,6 +163,12 @@ namespace Terminal.Gui {
 			}
 			}
 		}
 		}
 
 
+		/// <summary>
+		/// Navigator for cycling the selected item in the table by typing.
+		/// Set to null to disable this feature.
+		/// </summary>
+		public CollectionNavigatorBase CollectionNavigator { get; set; }
+
 		/// <summary>
 		/// <summary>
 		/// Initialzies a <see cref="TableView"/> class using <see cref="LayoutStyle.Computed"/> layout. 
 		/// Initialzies a <see cref="TableView"/> class using <see cref="LayoutStyle.Computed"/> layout. 
 		/// </summary>
 		/// </summary>
@@ -179,6 +185,8 @@ namespace Terminal.Gui {
 		{
 		{
 			CanFocus = true;
 			CanFocus = true;
 
 
+			this.CollectionNavigator = new TableCollectionNavigator (this);
+
 			// Things this view knows how to do
 			// Things this view knows how to do
 			AddCommand (Command.Right, () => { ChangeSelectionByOffset (1, 0, false); return true; });
 			AddCommand (Command.Right, () => { ChangeSelectionByOffset (1, 0, false); return true; });
 			AddCommand (Command.Left, () => { ChangeSelectionByOffset (-1, 0, false); return true; });
 			AddCommand (Command.Left, () => { ChangeSelectionByOffset (-1, 0, false); return true; });
@@ -746,6 +754,37 @@ namespace Terminal.Gui {
 				return true;
 				return true;
 			}
 			}
 
 
+			if (CollectionNavigator != null &&
+				this.HasFocus &&
+				Table.Rows != 0 &&
+				Terminal.Gui.CollectionNavigator.IsCompatibleKey (keyEvent) &&
+				!keyEvent.Key.HasFlag (Key.CtrlMask) &&
+				!keyEvent.Key.HasFlag (Key.AltMask) &&
+				char.IsLetterOrDigit ((char)keyEvent.KeyValue)) {
+				return CycleToNextTableEntryBeginningWith (keyEvent);
+			}
+
+			return false;
+		}
+
+		private bool CycleToNextTableEntryBeginningWith (KeyEvent keyEvent)
+		{
+			var row = SelectedRow;
+
+			// There is a multi select going on and not just for the current row
+			if (GetAllSelectedCells ().Any (c => c.Y != row)) {
+				return false;
+			}
+
+			int match = CollectionNavigator.GetNextMatchingItem (row, (char)keyEvent.KeyValue);
+
+			if (match != -1) {
+				SelectedRow = match;
+				EnsureValidSelection ();
+				EnsureSelectedCellIsVisible ();
+				return true;
+			}
+
 			return false;
 			return false;
 		}
 		}
 
 

+ 107 - 20
UnitTests/Views/TableViewTests.cs

@@ -28,7 +28,7 @@ namespace Terminal.Gui.ViewsTests {
 			Assert.Equal (0, tableView.ColumnOffset);
 			Assert.Equal (0, tableView.ColumnOffset);
 
 
 			// Set empty table
 			// Set empty table
-			tableView.Table = new DataTableSource(new DataTable ());
+			tableView.Table = new DataTableSource (new DataTable ());
 
 
 			// Since table has no rows or columns scroll offset should default to 0
 			// Since table has no rows or columns scroll offset should default to 0
 			tableView.EnsureValidScrollOffsets ();
 			tableView.EnsureValidScrollOffsets ();
@@ -598,7 +598,7 @@ namespace Terminal.Gui.ViewsTests {
 		{
 		{
 			string activatedValue = null;
 			string activatedValue = null;
 			var tv = new TableView (BuildTable (1, 1));
 			var tv = new TableView (BuildTable (1, 1));
-			tv.CellActivated += (s, c) => activatedValue = c.Table [c.Row,c.Col].ToString();
+			tv.CellActivated += (s, c) => activatedValue = c.Table [c.Row, c.Col].ToString ();
 
 
 			Application.Top.Add (tv);
 			Application.Top.Add (tv);
 			Application.Begin (Application.Top);
 			Application.Begin (Application.Top);
@@ -907,7 +907,7 @@ namespace Terminal.Gui.ViewsTests {
 			};
 			};
 
 
 			// when B is 2 use the custom highlight colour for the row
 			// when B is 2 use the custom highlight colour for the row
-			tv.Style.RowColorGetter += (e) => Convert.ToInt32 (e.Table[e.RowIndex,1]) == 2 ? rowHighlight : null;
+			tv.Style.RowColorGetter += (e) => Convert.ToInt32 (e.Table [e.RowIndex, 1]) == 2 ? rowHighlight : null;
 
 
 			// private method for forcing the view to be focused/not focused
 			// private method for forcing the view to be focused/not focused
 			var setFocusMethod = typeof (View).GetMethod ("SetHasFocus", BindingFlags.Instance | BindingFlags.NonPublic);
 			var setFocusMethod = typeof (View).GetMethod ("SetHasFocus", BindingFlags.Instance | BindingFlags.NonPublic);
@@ -944,7 +944,7 @@ namespace Terminal.Gui.ViewsTests {
 			// it no longer matches the RowColorGetter
 			// it no longer matches the RowColorGetter
 			// delegate conditional ( which checks for
 			// delegate conditional ( which checks for
 			// the value 2)
 			// the value 2)
-			dt.Rows [0][1] = 5;
+			dt.Rows [0] [1] = 5;
 
 
 			tv.Redraw (tv.Bounds);
 			tv.Redraw (tv.Bounds);
 			expected = @"
 			expected = @"
@@ -1080,7 +1080,7 @@ namespace Terminal.Gui.ViewsTests {
 			dt.Columns.Add ("B");
 			dt.Columns.Add ("B");
 			dt.Rows.Add (1, 2);
 			dt.Rows.Add (1, 2);
 
 
-			tv.Table = new DataTableSource(dt);
+			tv.Table = new DataTableSource (dt);
 			tv.Style.GetOrCreateColumnStyle (0).MinWidth = 1;
 			tv.Style.GetOrCreateColumnStyle (0).MinWidth = 1;
 			tv.Style.GetOrCreateColumnStyle (0).MinWidth = 1;
 			tv.Style.GetOrCreateColumnStyle (0).MinWidth = 1;
 			tv.Style.GetOrCreateColumnStyle (1).MaxWidth = 1;
 			tv.Style.GetOrCreateColumnStyle (1).MaxWidth = 1;
@@ -1144,7 +1144,7 @@ namespace Terminal.Gui.ViewsTests {
 
 
 			dt.Rows.Add (1, 2, 3, 4, 5, 6);
 			dt.Rows.Add (1, 2, 3, 4, 5, 6);
 
 
-			tableView.Table = new DataTableSource(dt);
+			tableView.Table = new DataTableSource (dt);
 
 
 			// select last visible column
 			// select last visible column
 			tableView.SelectedColumn = 2; // column C
 			tableView.SelectedColumn = 2; // column C
@@ -1205,7 +1205,7 @@ namespace Terminal.Gui.ViewsTests {
 
 
 			dt.Rows.Add (1, 2, 3, 4, 5, 6);
 			dt.Rows.Add (1, 2, 3, 4, 5, 6);
 
 
-			tableView.Table = new DataTableSource(dt);
+			tableView.Table = new DataTableSource (dt);
 
 
 			// select last visible column
 			// select last visible column
 			tableView.SelectedColumn = 2; // column C
 			tableView.SelectedColumn = 2; // column C
@@ -1264,7 +1264,7 @@ namespace Terminal.Gui.ViewsTests {
 			dt.Columns.Add ("F");
 			dt.Columns.Add ("F");
 
 
 			dt.Rows.Add (1, 2, 3, 4, 5, 6);
 			dt.Rows.Add (1, 2, 3, 4, 5, 6);
-			tableView.Table = new DataTableSource(dt);
+			tableView.Table = new DataTableSource (dt);
 
 
 			return tableView;
 			return tableView;
 		}
 		}
@@ -1315,7 +1315,7 @@ namespace Terminal.Gui.ViewsTests {
 			for (int i = 0; i < 6; i++) {
 			for (int i = 0; i < 6; i++) {
 				tableView.Style.GetOrCreateColumnStyle (i).Visible = false;
 				tableView.Style.GetOrCreateColumnStyle (i).Visible = false;
 			}
 			}
-			
+
 			tableView.LayoutSubviews ();
 			tableView.LayoutSubviews ();
 
 
 			// expect nothing to be rendered when all columns are invisible
 			// expect nothing to be rendered when all columns are invisible
@@ -1814,7 +1814,7 @@ namespace Terminal.Gui.ViewsTests {
 			dt.Rows.Add (1, 2, new string ('a', 500));
 			dt.Rows.Add (1, 2, new string ('a', 500));
 			dt.Rows.Add (1, 2, "aaa");
 			dt.Rows.Add (1, 2, "aaa");
 
 
-			tableView.Table = new DataTableSource(dt);
+			tableView.Table = new DataTableSource (dt);
 			tableView.LayoutSubviews ();
 			tableView.LayoutSubviews ();
 			tableView.Redraw (tableView.Bounds);
 			tableView.Redraw (tableView.Bounds);
 
 
@@ -1957,7 +1957,7 @@ namespace Terminal.Gui.ViewsTests {
 
 
 			dt.Rows.Add (1, 2, 3, 4, 5, 6);
 			dt.Rows.Add (1, 2, 3, 4, 5, 6);
 
 
-			tableView.Table = new DataTableSource(dt);
+			tableView.Table = new DataTableSource (dt);
 
 
 			// select last visible column
 			// select last visible column
 			tableView.SelectedColumn = 2; // column C
 			tableView.SelectedColumn = 2; // column C
@@ -2022,7 +2022,7 @@ namespace Terminal.Gui.ViewsTests {
 
 
 			dt.Rows.Add ("Hello", DBNull.Value, "f");
 			dt.Rows.Add ("Hello", DBNull.Value, "f");
 
 
-			tv.Table = new DataTableSource(dt);
+			tv.Table = new DataTableSource (dt);
 			tv.NullSymbol = string.Empty;
 			tv.NullSymbol = string.Empty;
 
 
 			Application.Top.Add (tv);
 			Application.Top.Add (tv);
@@ -2334,7 +2334,7 @@ A B C
 				dt.Rows.Add (newRow);
 				dt.Rows.Add (newRow);
 			}
 			}
 
 
-			return new DataTableSource(dt);
+			return new DataTableSource (dt);
 		}
 		}
 
 
 		[Fact, AutoInitShutdown]
 		[Fact, AutoInitShutdown]
@@ -2439,10 +2439,10 @@ A B C
 			// ---------------- X=1 -----------------------
 			// ---------------- X=1 -----------------------
 			// click in header
 			// click in header
 			Assert.Null (tableView.ScreenToCell (1, 0, out col));
 			Assert.Null (tableView.ScreenToCell (1, 0, out col));
-			Assert.Equal ("A", tableView.Table.ColumnNames[col.Value]);
+			Assert.Equal ("A", tableView.Table.ColumnNames [col.Value]);
 			// click in header row line  (click in the horizontal line below header counts as click in header above - consistent with the column hit box)
 			// 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.Null (tableView.ScreenToCell (1, 1, out col));
-			Assert.Equal ("A", tableView.Table.ColumnNames[col.Value]);
+			Assert.Equal ("A", tableView.Table.ColumnNames [col.Value]);
 			// click in cell 0,0
 			// click in cell 0,0
 			Assert.Equal (new Point (0, 0), tableView.ScreenToCell (1, 2, out col));
 			Assert.Equal (new Point (0, 0), tableView.ScreenToCell (1, 2, out col));
 			Assert.Null (col);
 			Assert.Null (col);
@@ -2456,7 +2456,7 @@ A B C
 			// ---------------- X=2 -----------------------
 			// ---------------- X=2 -----------------------
 			// click in header
 			// click in header
 			Assert.Null (tableView.ScreenToCell (2, 0, out col));
 			Assert.Null (tableView.ScreenToCell (2, 0, out col));
-			Assert.Equal ("A", tableView.Table.ColumnNames[col.Value]);
+			Assert.Equal ("A", tableView.Table.ColumnNames [col.Value]);
 			// click in header row line
 			// click in header row line
 			Assert.Null (tableView.ScreenToCell (2, 1, out col));
 			Assert.Null (tableView.ScreenToCell (2, 1, out col));
 			Assert.Equal ("A", tableView.Table.ColumnNames [col.Value]);
 			Assert.Equal ("A", tableView.Table.ColumnNames [col.Value]);
@@ -2476,7 +2476,7 @@ A B C
 			Assert.Equal ("B", tableView.Table.ColumnNames [col.Value]);
 			Assert.Equal ("B", tableView.Table.ColumnNames [col.Value]);
 			// click in header row line
 			// click in header row line
 			Assert.Null (tableView.ScreenToCell (3, 1, out col));
 			Assert.Null (tableView.ScreenToCell (3, 1, out col));
-			Assert.Equal ("B", tableView.Table.ColumnNames[col.Value]);
+			Assert.Equal ("B", tableView.Table.ColumnNames [col.Value]);
 			// click in cell 1,0
 			// click in cell 1,0
 			Assert.Equal (new Point (1, 0), tableView.ScreenToCell (3, 2, out col));
 			Assert.Equal (new Point (1, 0), tableView.ScreenToCell (3, 2, out col));
 			Assert.Null (col);
 			Assert.Null (col);
@@ -2488,8 +2488,8 @@ A B C
 			Assert.Null (col);
 			Assert.Null (col);
 		}
 		}
 
 
-		[Fact,AutoInitShutdown]
-		public void TestEnumerableDataSource_BasicTypes()
+		[Fact, AutoInitShutdown]
+		public void TestEnumerableDataSource_BasicTypes ()
 		{
 		{
 			var tv = new TableView ();
 			var tv = new TableView ();
 			tv.ColorScheme = Colors.TopLevel;
 			tv.ColorScheme = Colors.TopLevel;
@@ -2517,6 +2517,93 @@ A B C
 │Single│System   │System.ValueType               │";
 │Single│System   │System.ValueType               │";
 
 
 			TestHelpers.AssertDriverContentsAre (expected, output);
 			TestHelpers.AssertDriverContentsAre (expected, output);
+		}
+		[Fact, AutoInitShutdown]
+		public void Test_CollectionNavigator ()
+		{
+			var tv = new TableView ();
+			tv.ColorScheme = Colors.TopLevel;
+			tv.Bounds = new Rect (0, 0, 50, 7);
+
+			tv.Table = new EnumerableTableSource<string> (
+				new string [] { "fish", "troll", "trap", "zoo" },
+				new () {
+					{ "Name", (t)=>t},
+					{ "EndsWith", (t)=>t.Last()}
+				});
+
+			tv.LayoutSubviews ();
+
+			tv.Redraw (tv.Bounds);
+
+			string expected =
+				@"
+┌─────┬──────────────────────────────────────────┐
+│Name │EndsWith                                  │
+├─────┼──────────────────────────────────────────┤
+│fish │h                                         │
+│troll│l                                         │
+│trap │p                                         │
+│zoo  │o                                         │";
+
+			TestHelpers.AssertDriverContentsAre (expected, output);
+
+			Assert.Equal (0, tv.SelectedRow);
+
+			// this test assumes no focus
+			Assert.False (tv.HasFocus);
+
+			// already on fish
+			tv.ProcessKey (new KeyEvent { Key = Key.f });
+			Assert.Equal (0, tv.SelectedRow);
+
+			// not focused
+			tv.ProcessKey (new KeyEvent { Key = Key.z });
+			Assert.Equal (0, tv.SelectedRow);
+
+			// ensure that TableView has the input focus
+			Application.Top.Add (tv);
+			Application.Begin (Application.Top);
+
+			Application.Top.FocusFirst ();
+			Assert.True (tv.HasFocus);
+
+			// already on fish
+			tv.ProcessKey (new KeyEvent { Key = Key.f });
+			Assert.Equal (0, tv.SelectedRow);
+
+			// move to zoo
+			tv.ProcessKey (new KeyEvent { Key = Key.z });
+			Assert.Equal (3, tv.SelectedRow);
+
+			// move to troll
+			tv.ProcessKey (new KeyEvent { Key = Key.t });
+			Assert.Equal (1, tv.SelectedRow);
+
+			// move to trap
+			tv.ProcessKey (new KeyEvent { Key = Key.t });
+			Assert.Equal (2, tv.SelectedRow);
+
+			// change columns to navigate by column 2
+			Assert.Equal (0, tv.SelectedColumn);
+			Assert.Equal (2, tv.SelectedRow);
+			tv.ProcessKey (new KeyEvent { Key = Key.CursorRight });
+			Assert.Equal (1, tv.SelectedColumn);
+			Assert.Equal (2, tv.SelectedRow);
+
+			// nothing ends with t so stay where you are
+			tv.ProcessKey (new KeyEvent { Key = Key.t });
+			Assert.Equal (2, tv.SelectedRow);
+
+			//jump to fish which ends in h
+			tv.ProcessKey (new KeyEvent { Key = Key.h });
+			Assert.Equal (0, tv.SelectedRow);
+
+			// jump to zoo which ends in o
+			tv.ProcessKey (new KeyEvent { Key = Key.o });
+			Assert.Equal (3, tv.SelectedRow);
+
+
 		}
 		}
 		private TableView GetTwoRowSixColumnTable ()
 		private TableView GetTwoRowSixColumnTable ()
 		{
 		{
@@ -2545,7 +2632,7 @@ A B C
 			dt.Rows.Add (1, 2, 3, 4, 5, 6);
 			dt.Rows.Add (1, 2, 3, 4, 5, 6);
 			dt.Rows.Add (1, 2, 3, 4, 5, 6);
 			dt.Rows.Add (1, 2, 3, 4, 5, 6);
 
 
-			tableView.Table = new DataTableSource(dt);
+			tableView.Table = new DataTableSource (dt);
 			return tableView;
 			return tableView;
 		}
 		}
 	}
 	}