Browse Source

Merge with develop

Charlie Kindel 2 years ago
parent
commit
86a7b0764c

+ 244 - 0
Terminal.Gui/Core/CollectionNavigator.cs

@@ -0,0 +1,244 @@
+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 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;
+
+		/// <summary>
+		/// The collection of objects to search. <see cref="object.ToString()"/> is used to search the collection.
+		/// </summary>
+		public IEnumerable<object> Collection { get; set; }
+
+		/// <summary>
+		/// Event arguments for the <see cref="CollectionNavigator.SearchStringChanged"/> event.
+		/// </summary>
+		public class KeystrokeNavigatorEventArgs {
+			/// <summary>
+			/// he current <see cref="SearchString"/>.
+			/// </summary>
+			public string SearchString { get; }
+
+			/// <summary>
+			/// Initializes a new instance of <see cref="KeystrokeNavigatorEventArgs"/>
+			/// </summary>
+			/// <param name="searchString">The current <see cref="SearchString"/>.</param>
+			public KeystrokeNavigatorEventArgs (string searchString)
+			{
+				SearchString = searchString;
+			}
+		}
+
+		/// <summary>
+		/// This event is invoked when <see cref="SearchString"/>  changes. Useful for debugging.
+		/// </summary>
+		public event Action<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 (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)
+		{
+			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;
+			}
+		}
+
+		/// <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;
+			}
+			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;
+		}
+
+		private void AssertCollectionIsNotNull ()
+		{
+			if (Collection == null) {
+				throw new InvalidOperationException ("Collection is null");
+			}
+		}
+
+		private void ClearSearchString ()
+		{
+			SearchString = "";
+			lastKeystroke = DateTime.Now;
+		}
+
+		/// <summary>
+		/// Returns true if <paramref name="kb"/> is a searchable key
+		/// (e.g. letters, numbers etc) that is valid to pass to to this
+		/// class for search filtering.
+		/// </summary>
+		/// <param name="kb"></param>
+		/// <returns></returns>
+		public static bool IsCompatibleKey (KeyEvent kb)
+		{
+			return !kb.IsAlt && !kb.IsCapslock && !kb.IsCtrl && !kb.IsScrolllock && !kb.IsNumlock;
+		}
+	}
+}

+ 41 - 41
Terminal.Gui/Core/Command.cs

@@ -10,54 +10,54 @@ namespace Terminal.Gui {
 	public enum Command {
 
 		/// <summary>
-		/// Moves the caret down one line.
+		/// Moves down one item (cell, line, etc...).
 		/// </summary>
 		LineDown,
 
 		/// <summary>
-		/// Extends the selection down one line.
+		/// Extends the selection down one (cell, line, etc...).
 		/// </summary>
 		LineDownExtend,
 
 		/// <summary>
-		/// Moves the caret down to the last child node of the branch that holds the current selection
+		/// Moves down to the last child node of the branch that holds the current selection.
 		/// </summary>
 		LineDownToLastBranch,
 
 		/// <summary>
-		/// Scrolls down one line (without changing the selection).
+		/// Scrolls down one (cell, line, etc...) (without changing the selection).
 		/// </summary>
 		ScrollDown,
 
 		// --------------------------------------------------------------------
 
 		/// <summary>
-		/// Moves the caret up one line.
+		/// Moves up one (cell, line, etc...).
 		/// </summary>
 		LineUp,
 
 		/// <summary>
-		/// Extends the selection up one line.
+		/// Extends the selection up one item (cell, line, etc...).
 		/// </summary>
 		LineUpExtend,
 
 		/// <summary>
-		/// Moves the caret up to the first child node of the branch that holds the current selection
+		/// Moves up to the first child node of the branch that holds the current selection.
 		/// </summary>
 		LineUpToFirstBranch,
 
 		/// <summary>
-		/// Scrolls up one line (without changing the selection).
+		/// Scrolls up one item (cell, line, etc...) (without changing the selection).
 		/// </summary>
 		ScrollUp,
 
 		/// <summary>
-		/// Moves the selection left one by the minimum increment supported by the view e.g. single character, cell, item etc.
+		/// Moves the selection left one by the minimum increment supported by the <see cref="View"/> e.g. single character, cell, item etc.
 		/// </summary>
 		Left,
 
 		/// <summary>
-		/// Scrolls one character to the left
+		/// Scrolls one item (cell, character, etc...) to the left
 		/// </summary>
 		ScrollLeft,
 
@@ -72,7 +72,7 @@ namespace Terminal.Gui {
 		Right,
 
 		/// <summary>
-		/// Scrolls one character to the right.
+		/// Scrolls one item (cell, character, etc...) to the right.
 		/// </summary>
 		ScrollRight,
 
@@ -102,12 +102,12 @@ namespace Terminal.Gui {
 		WordRightExtend,
 
 		/// <summary>
-		/// Deletes and copies to the clipboard the characters from the current position to the end of the line.
+		/// Cuts to the clipboard the characters from the current position to the end of the line.
 		/// </summary>
 		CutToEndLine,
 
 		/// <summary>
-		/// Deletes and copies to the clipboard the characters from the current position to the start of the line.
+		/// Cuts to the clipboard the characters from the current position to the start of the line.
 		/// </summary>
 		CutToStartLine,
 
@@ -140,47 +140,47 @@ namespace Terminal.Gui {
 		DisableOverwrite,
 
 		/// <summary>
-		/// Move the page down.
+		/// Move one page down.
 		/// </summary>
 		PageDown,
 
 		/// <summary>
-		/// Move the page down increase selection area to cover revealed objects/characters.
+		/// Move one page page extending the selection to cover revealed objects/characters.
 		/// </summary>
 		PageDownExtend,
 
 		/// <summary>
-		/// Move the page up.
+		/// Move one page up.
 		/// </summary>
 		PageUp,
 
 		/// <summary>
-		/// Move the page up increase selection area to cover revealed objects/characters.
+		/// Move one page up extending the selection to cover revealed objects/characters.
 		/// </summary>
 		PageUpExtend,
 
 		/// <summary>
-		/// Moves to top begin.
+		/// Moves to the top/home.
 		/// </summary>
 		TopHome,
 
 		/// <summary>
-		/// Extends the selection to the top begin.
+		/// Extends the selection to the top/home.
 		/// </summary>
 		TopHomeExtend,
 
 		/// <summary>
-		/// Moves to bottom end.
+		/// Moves to the bottom/end.
 		/// </summary>
 		BottomEnd,
 
 		/// <summary>
-		/// Extends the selection to the bottom end.
+		/// Extends the selection to the bottom/end.
 		/// </summary>
 		BottomEndExtend,
 
 		/// <summary>
-		/// Open selected item.
+		/// Open the selected item.
 		/// </summary>
 		OpenSelectedItem,
 
@@ -190,43 +190,43 @@ namespace Terminal.Gui {
 		ToggleChecked,
 
 		/// <summary>
-		/// Accepts the current state (e.g. selection, button press etc)
+		/// Accepts the current state (e.g. selection, button press etc).
 		/// </summary>
 		Accept,
 
 		/// <summary>
-		/// Toggles the Expanded or collapsed state of a a list or item (with subitems)
+		/// Toggles the Expanded or collapsed state of a a list or item (with subitems).
 		/// </summary>
 		ToggleExpandCollapse,
 
 		/// <summary>
-		/// Expands a list or item (with subitems)
+		/// Expands a list or item (with subitems).
 		/// </summary>
 		Expand,
 
 		/// <summary>
-		/// Recursively Expands all child items and their child items (if any)
+		/// Recursively Expands all child items and their child items (if any).
 		/// </summary>
 		ExpandAll,
 
 		/// <summary>
-		/// Collapses a list or item (with subitems)
+		/// Collapses a list or item (with subitems).
 		/// </summary>
 		Collapse,
 
 		/// <summary>
-		/// Recursively collapses a list items of their children (if any)
+		/// Recursively collapses a list items of their children (if any).
 		/// </summary>
 		CollapseAll,
 
 		/// <summary>
-		/// Cancels any current temporary states on the control e.g. expanding
-		/// a combo list
+		/// Cancels an action or any temporary states on the control e.g. expanding
+		/// a combo list.
 		/// </summary>
 		Cancel,
 
 		/// <summary>
-		/// Unix emulation
+		/// Unix emulation.
 		/// </summary>
 		UnixEmulation,
 
@@ -241,12 +241,12 @@ namespace Terminal.Gui {
 		DeleteCharLeft,
 
 		/// <summary>
-		/// Selects all objects in the control.
+		/// Selects all objects.
 		/// </summary>
 		SelectAll,
 
 		/// <summary>
-		/// Deletes all objects in the control.
+		/// Deletes all objects.
 		/// </summary>
 		DeleteAll,
 
@@ -336,7 +336,7 @@ namespace Terminal.Gui {
 		Paste,
 
 		/// <summary>
-		/// Quit a toplevel.
+		/// Quit a <see cref="Toplevel"/>.
 		/// </summary>
 		QuitToplevel,
 
@@ -356,37 +356,37 @@ namespace Terminal.Gui {
 		PreviousView,
 
 		/// <summary>
-		/// Moves focus to the next view or toplevel (case of Mdi).
+		/// Moves focus to the next view or toplevel (case of MDI).
 		/// </summary>
 		NextViewOrTop,
 
 		/// <summary>
-		/// Moves focus to the next previous or toplevel (case of Mdi).
+		/// Moves focus to the next previous or toplevel (case of MDI).
 		/// </summary>
 		PreviousViewOrTop,
 
 		/// <summary>
-		/// Refresh the application.
+		/// Refresh.
 		/// </summary>
 		Refresh,
 
 		/// <summary>
-		/// Toggles the extended selection.
+		/// Toggles the selection.
 		/// </summary>
 		ToggleExtend,
 
 		/// <summary>
-		/// Inserts a new line.
+		/// Inserts a new item.
 		/// </summary>
 		NewLine,
 
 		/// <summary>
-		/// Inserts a tab.
+		/// Tabs to the next item.
 		/// </summary>
 		Tab,
 
 		/// <summary>
-		/// Inserts a shift tab.
+		/// Tabs back to the previous item.
 		/// </summary>
 		BackTab
 	}

+ 20 - 0
Terminal.Gui/Core/Responder.cs

@@ -16,6 +16,7 @@
 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
+using System.Reflection;
 
 namespace Terminal.Gui {
 	/// <summary>
@@ -236,6 +237,25 @@ namespace Terminal.Gui {
 		/// </summary>
 		public virtual void OnVisibleChanged () { }
 
+		/// <summary>
+		/// Utilty function to determine <paramref name="method"/> is overridden in the <paramref name="subclass"/>.
+		/// </summary>
+		/// <param name="subclass">The view.</param>
+		/// <param name="method">The method name.</param>
+		/// <returns><see langword="true"/> if it's overridden, <see langword="false"/> otherwise.</returns>
+		internal static bool IsOverridden (Responder subclass, string method)
+		{
+			MethodInfo m = subclass.GetType ().GetMethod (method,
+				BindingFlags.Instance
+				| BindingFlags.Public
+				| BindingFlags.NonPublic
+				| BindingFlags.DeclaredOnly);
+			if (m == null) {
+				return false;
+			}
+			return m.GetBaseDefinition ().DeclaringType != m.DeclaringType;
+		}
+		
 		/// <summary>
 		/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
 		/// </summary>

+ 3 - 3
Terminal.Gui/Core/Trees/Branch.cs

@@ -89,8 +89,8 @@ namespace Terminal.Gui.Trees {
 		public virtual void Draw (ConsoleDriver driver, ColorScheme colorScheme, int y, int availableWidth)
 		{
 			// true if the current line of the tree is the selected one and control has focus
-			bool isSelected = tree.IsSelected (Model) && tree.HasFocus;
-			Attribute lineColor = isSelected ? colorScheme.Focus : colorScheme.Normal;
+			bool isSelected = tree.IsSelected (Model);// && tree.HasFocus;
+			Attribute lineColor = isSelected ? (tree.HasFocus ? colorScheme.HotFocus : colorScheme.HotNormal) : colorScheme.Normal ;
 
 			driver.SetAttribute (lineColor);
 
@@ -418,7 +418,7 @@ namespace Terminal.Gui.Trees {
 		/// Expands the current branch and all children branches
 		/// </summary>
 		internal void ExpandAll ()
-		{
+			{
 			Expand ();
 
 			if (ChildBranches != null) {

+ 0 - 19
Terminal.Gui/Core/View.cs

@@ -3073,24 +3073,5 @@ namespace Terminal.Gui {
 
 			return top;
 		}
-
-		/// <summary>
-		/// Check if the <paramref name="method"/> is overridden in the <paramref name="view"/>.
-		/// </summary>
-		/// <param name="view">The view.</param>
-		/// <param name="method">The method name.</param>
-		/// <returns><see langword="true"/> if it's overridden, <see langword="false"/> otherwise.</returns>
-		public static bool IsOverridden (View view, string method)
-		{
-			MethodInfo m = view.GetType ().GetMethod (method, 
-				BindingFlags.Instance 
-				| BindingFlags.Public 
-				| BindingFlags.NonPublic 
-				| BindingFlags.DeclaredOnly);
-			if (m == null) {
-				return false;
-			}
-			return m.GetBaseDefinition ().DeclaringType != m.DeclaringType;
-		}
 	}
 }

+ 1 - 1
Terminal.Gui/Terminal.Gui.csproj

@@ -23,7 +23,7 @@
   <!-- Uncomment the RestoreSources element to have dotnet restore pull NStack from a local dir for testing -->
   <PropertyGroup>
     <!-- See https://stackoverflow.com/a/44463578/297526 -->
-    <!--<RestoreSources>$(RestoreSources);../../local-packages;https://api.nuget.org/v3/index.json</RestoreSources>-->
+    <!--<RestoreSources>$(RestoreSources);..\..\NStack\NStack\bin\Debug;https://api.nuget.org/v3/index.json</RestoreSources>-->
   </PropertyGroup>
   <!-- API Documentation -->
   <ItemGroup>

+ 120 - 116
Terminal.Gui/Views/ListView.cs

@@ -1,25 +1,7 @@
-//
-// ListView.cs: ListView control
-//
-// Authors:
-//   Miguel de Icaza ([email protected])
-//
-//
-// TODO:
-//   - Should we support multiple columns, if so, how should that be done?
-//   - Show mark for items that have been marked.
-//   - Mouse support
-//   - Scrollbars?
-//
-// Column considerations:
-//   - Would need a way to specify widths
-//   - Should it automatically extract data out of structs/classes based on public fields/properties?
-//   - It seems that this would be useful just for the "simple" API, not the IListDAtaSource, as that one has full support for it.
-//   - Should a function be specified that retrieves the individual elements?
-//
 using System;
 using System.Collections;
 using System.Collections.Generic;
+using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using NStack;
@@ -59,7 +41,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Should return whether the specified item is currently marked.
 		/// </summary>
-		/// <returns><c>true</c>, if marked, <c>false</c> otherwise.</returns>
+		/// <returns><see langword="true"/>, if marked, <see langword="false"/> otherwise.</returns>
 		/// <param name="item">Item index.</param>
 		bool IsMarked (int item);
 
@@ -67,7 +49,7 @@ namespace Terminal.Gui {
 		/// Flags the item as marked.
 		/// </summary>
 		/// <param name="item">Item index.</param>
-		/// <param name="value">If set to <c>true</c> value.</param>
+		/// <param name="value">If set to <see langword="true"/> value.</param>
 		void SetMark (int item, bool value);
 
 		/// <summary>
@@ -89,8 +71,8 @@ namespace Terminal.Gui {
 	/// <para>
 	///   By default <see cref="ListView"/> uses <see cref="object.ToString"/> to render the items of any
 	///   <see cref="IList"/> object (e.g. arrays, <see cref="List{T}"/>,
-	///   and other collections). Alternatively, an object that implements the <see cref="IListDataSource"/>
-	///   interface can be provided giving full control of what is rendered.
+	///   and other collections). Alternatively, an object that implements <see cref="IListDataSource"/>
+	///   can be provided giving full control of what is rendered.
 	/// </para>
 	/// <para>
 	///   <see cref="ListView"/> can display any object that implements the <see cref="IList"/> interface.
@@ -107,6 +89,10 @@ namespace Terminal.Gui {
 	///   [x] or [ ] and bind the SPACE key to toggle the selection. To implement a different
 	///   marking style set <see cref="AllowsMarking"/> to false and implement custom rendering.
 	/// </para>
+	/// <para>
+	///   Searching the ListView with the keyboard is supported. Users type the
+	///   first characters of an item, and the first item that starts with what the user types will be selected.
+	/// </para>
 	/// </remarks>
 	public class ListView : View {
 		int top, left;
@@ -124,6 +110,7 @@ namespace Terminal.Gui {
 			get => source;
 			set {
 				source = value;
+				KeystrokeNavigator.Collection = source?.ToList ()?.Cast<object> ();
 				top = 0;
 				selected = 0;
 				lastSelectedItem = -1;
@@ -169,22 +156,28 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Gets or sets whether this <see cref="ListView"/> allows items to be marked.
 		/// </summary>
-		/// <value><c>true</c> if allows marking elements of the list; otherwise, <c>false</c>.
-		/// </value>
+		/// <value>Set to <see langword="true"/> to allow marking elements of the list.</value>
 		/// <remarks>
-		/// If set to true, <see cref="ListView"/> will render items marked items with "[x]", and unmarked items with "[ ]"
-		/// spaces. SPACE key will toggle marking.
+		/// If set to <see langword="true"/>, <see cref="ListView"/> will render items marked items with "[x]", and unmarked items with "[ ]"
+		/// spaces. SPACE key will toggle marking. The default is <see langword="false"/>.
 		/// </remarks>
 		public bool AllowsMarking {
 			get => allowsMarking;
 			set {
 				allowsMarking = value;
+				if (allowsMarking) {
+					AddKeyBinding (Key.Space, Command.ToggleChecked);
+				} else {
+					ClearKeybinding (Key.Space);
+				}
+
 				SetNeedsDisplay ();
 			}
 		}
 
 		/// <summary>
-		/// If set to true allows more than one item to be selected. If false only allow one item selected.
+		/// If set to <see langword="true"/> more than one item can be selected. If <see langword="false"/> selecting
+		/// an item will cause all others to be un-selected. The default is <see langword="false"/>.
 		/// </summary>
 		public bool AllowsMultipleSelection {
 			get => allowsMultipleSelection;
@@ -198,6 +191,7 @@ namespace Terminal.Gui {
 						}
 					}
 				}
+				SetNeedsDisplay ();
 			}
 		}
 
@@ -219,7 +213,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Gets or sets the left column where the item start to be displayed at on the <see cref="ListView"/>.
+		/// Gets or sets the leftmost column that is currently visible (when scrolling horizontally).
 		/// </summary>
 		/// <value>The left position.</value>
 		public int LeftItem {
@@ -236,7 +230,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Gets the widest item.
+		/// Gets the widest item in the list.
 		/// </summary>
 		public int Maxlength => (source?.Length) ?? 0;
 
@@ -264,10 +258,12 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Initializes a new instance of <see cref="ListView"/> that will display the contents of the object implementing the <see cref="IList"/> interface, 
+		/// Initializes a new instance of <see cref="ListView"/> that will display the 
+		/// contents of the object implementing the <see cref="IList"/> interface, 
 		/// with relative positioning.
 		/// </summary>
-		/// <param name="source">An <see cref="IList"/> data source, if the elements are strings or ustrings, the string is rendered, otherwise the ToString() method is invoked on the result.</param>
+		/// <param name="source">An <see cref="IList"/> data source, if the elements are strings or ustrings, 
+		/// the string is rendered, otherwise the ToString() method is invoked on the result.</param>
 		public ListView (IList source) : this (MakeWrapper (source))
 		{
 		}
@@ -296,7 +292,8 @@ namespace Terminal.Gui {
 		/// Initializes a new instance of <see cref="ListView"/> that will display the contents of the object implementing the <see cref="IList"/> interface with an absolute position.
 		/// </summary>
 		/// <param name="rect">Frame for the listview.</param>
-		/// <param name="source">An IList data source, if the elements of the IList are strings or ustrings, the string is rendered, otherwise the ToString() method is invoked on the result.</param>
+		/// <param name="source">An IList data source, if the elements of the IList are strings or ustrings, 
+		/// the string is rendered, otherwise the ToString() method is invoked on the result.</param>
 		public ListView (Rect rect, IList source) : this (rect, MakeWrapper (source))
 		{
 			Initialize ();
@@ -306,7 +303,9 @@ namespace Terminal.Gui {
 		/// Initializes a new instance of <see cref="ListView"/> with the provided data source and an absolute position
 		/// </summary>
 		/// <param name="rect">Frame for the listview.</param>
-		/// <param name="source">IListDataSource object that provides a mechanism to render the data. The number of elements on the collection should not change, if you must change, set the "Source" property to reset the internal settings of the ListView.</param>
+		/// <param name="source">IListDataSource object that provides a mechanism to render the data. 
+		/// The number of elements on the collection should not change, if you must change, 
+		/// set the "Source" property to reset the internal settings of the ListView.</param>
 		public ListView (Rect rect, IListDataSource source) : base (rect)
 		{
 			this.source = source;
@@ -331,13 +330,13 @@ namespace Terminal.Gui {
 			AddCommand (Command.ToggleChecked, () => MarkUnmarkRow ());
 
 			// Default keybindings for all ListViews
-			AddKeyBinding (Key.CursorUp,Command.LineUp);
+			AddKeyBinding (Key.CursorUp, Command.LineUp);
 			AddKeyBinding (Key.P | Key.CtrlMask, Command.LineUp);
 
 			AddKeyBinding (Key.CursorDown, Command.LineDown);
 			AddKeyBinding (Key.N | Key.CtrlMask, Command.LineDown);
 
-			AddKeyBinding(Key.PageUp,Command.PageUp);
+			AddKeyBinding (Key.PageUp, Command.PageUp);
 
 			AddKeyBinding (Key.PageDown, Command.PageDown);
 			AddKeyBinding (Key.V | Key.CtrlMask, Command.PageDown);
@@ -347,8 +346,6 @@ namespace Terminal.Gui {
 			AddKeyBinding (Key.End, Command.BottomEnd);
 
 			AddKeyBinding (Key.Enter, Command.OpenSelectedItem);
-
-			AddKeyBinding (Key.Space, Command.ToggleChecked);
 		}
 
 		///<inheritdoc/>
@@ -386,7 +383,8 @@ namespace Terminal.Gui {
 						Driver.SetAttribute (current);
 					}
 					if (allowsMarking) {
-						Driver.AddRune (source.IsMarked (item) ? (AllowsMultipleSelection ? Driver.Checked : Driver.Selected) : (AllowsMultipleSelection ? Driver.UnChecked : Driver.UnSelected));
+						Driver.AddRune (source.IsMarked (item) ? (AllowsMultipleSelection ? Driver.Checked : Driver.Selected) :
+							(AllowsMultipleSelection ? Driver.UnChecked : Driver.UnSelected));
 						Driver.AddRune (' ');
 					}
 					Source.Render (this, Driver, isSelected, item, col, row, f.Width - col, start);
@@ -409,23 +407,43 @@ namespace Terminal.Gui {
 		/// </summary>
 		public event Action<ListViewRowEventArgs> RowRender;
 
+		/// <summary>
+		/// Gets the <see cref="CollectionNavigator"/> that searches the <see cref="ListView.Source"/> collection as
+		/// the user types.
+		/// </summary>
+		public CollectionNavigator KeystrokeNavigator { get; private set; } = new CollectionNavigator ();
+
 		///<inheritdoc/>
 		public override bool ProcessKey (KeyEvent kb)
 		{
-			if (source == null)
+			if (source == null) {
 				return base.ProcessKey (kb);
+			}
 
 			var result = InvokeKeybindings (kb);
-			if (result != null)
+			if (result != null) {
 				return (bool)result;
+			}
+
+			// Enable user to find & select an item by typing text
+			if (CollectionNavigator.IsCompatibleKey (kb)) {
+				var newItem = KeystrokeNavigator?.GetNextMatchingItem (SelectedItem, (char)kb.KeyValue);
+				if (newItem is int && newItem != -1) {
+					SelectedItem = (int)newItem;
+					EnsuresVisibilitySelectedItem ();
+					SetNeedsDisplay ();
+					return true;
+				}
+			}
 
 			return false;
 		}
 
 		/// <summary>
-		/// Prevents marking if it's not allowed mark and if it's not allows multiple selection.
+		/// If <see cref="AllowsMarking"/> and <see cref="AllowsMultipleSelection"/> are both <see langword="true"/>,
+		/// unmarks all marked items other than the currently selected. 
 		/// </summary>
-		/// <returns></returns>
+		/// <returns><see langword="true"/> if unmarking was successful.</returns>
 		public virtual bool AllowsAll ()
 		{
 			if (!allowsMarking)
@@ -442,9 +460,9 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Marks an unmarked row.
+		/// Marks the <see cref="SelectedItem"/> if it is not already marked.
 		/// </summary>
-		/// <returns></returns>
+		/// <returns><see langword="true"/> if the <see cref="SelectedItem"/> was marked.</returns>
 		public virtual bool MarkUnmarkRow ()
 		{
 			if (AllowsAll ()) {
@@ -457,7 +475,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Moves the selected item index to the next page.
+		/// Changes the <see cref="SelectedItem"/> to the item at the top of the visible list.
 		/// </summary>
 		/// <returns></returns>
 		public virtual bool MovePageUp ()
@@ -476,7 +494,8 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Moves the selected item index to the previous page.
+		/// Changes the <see cref="SelectedItem"/> to the item just below the bottom 
+		/// of the visible list, scrolling if needed.
 		/// </summary>
 		/// <returns></returns>
 		public virtual bool MovePageDown ()
@@ -498,7 +517,8 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Moves the selected item index to the next row.
+		/// Changes the <see cref="SelectedItem"/> to the next item in the list, 
+		/// scrolling the list if needed.
 		/// </summary>
 		/// <returns></returns>
 		public virtual bool MoveDown ()
@@ -538,7 +558,8 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Moves the selected item index to the previous row.
+		/// Changes the <see cref="SelectedItem"/> to the previous item in the list, 
+		/// scrolling the list if needed.
 		/// </summary>
 		/// <returns></returns>
 		public virtual bool MoveUp ()
@@ -574,7 +595,8 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Moves the selected item index to the last row.
+		/// Changes the <see cref="SelectedItem"/> to last item in the list, 
+		/// scrolling the list if needed.
 		/// </summary>
 		/// <returns></returns>
 		public virtual bool MoveEnd ()
@@ -592,7 +614,8 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Moves the selected item index to the first row.
+		/// Changes the <see cref="SelectedItem"/> to the first item in the list, 
+		/// scrolling the list if needed.
 		/// </summary>
 		/// <returns></returns>
 		public virtual bool MoveHome ()
@@ -608,23 +631,23 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Scrolls the view down.
+		/// Scrolls the view down by <paramref name="items"/> items.
 		/// </summary>
-		/// <param name="lines">Number of lines to scroll down.</param>
-		public virtual bool ScrollDown (int lines)
+		/// <param name="items">Number of items to scroll down.</param>
+		public virtual bool ScrollDown (int items)
 		{
-			top = Math.Max (Math.Min (top + lines, source.Count - 1), 0);
+			top = Math.Max (Math.Min (top + items, source.Count - 1), 0);
 			SetNeedsDisplay ();
 			return true;
 		}
 
 		/// <summary>
-		/// Scrolls the view up.
+		/// Scrolls the view up by <paramref name="items"/> items.
 		/// </summary>
-		/// <param name="lines">Number of lines to scroll up.</param>
-		public virtual bool ScrollUp (int lines)
+		/// <param name="items">Number of items to scroll up.</param>
+		public virtual bool ScrollUp (int items)
 		{
-			top = Math.Max (top - lines, 0);
+			top = Math.Max (top - items, 0);
 			SetNeedsDisplay ();
 			return true;
 		}
@@ -655,7 +678,7 @@ namespace Terminal.Gui {
 		private bool allowsMultipleSelection = true;
 
 		/// <summary>
-		/// Invokes the SelectedChanged event if it is defined.
+		/// Invokes the <see cref="SelectedItemChanged"/> event if it is defined.
 		/// </summary>
 		/// <returns></returns>
 		public virtual bool OnSelectedChanged ()
@@ -673,7 +696,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Invokes the OnOpenSelectedItem event if it is defined.
+		/// Invokes the <see cref="OpenSelectedItem"/> event if it is defined.
 		/// </summary>
 		/// <returns></returns>
 		public virtual bool OnOpenSelectedItem ()
@@ -788,23 +811,15 @@ namespace Terminal.Gui {
 
 			return true;
 		}
-
-
 	}
 
-	/// <summary>
-	/// Implements an <see cref="IListDataSource"/> that renders arbitrary <see cref="IList"/> instances for <see cref="ListView"/>.
-	/// </summary>
-	/// <remarks>Implements support for rendering marked items.</remarks>
+	/// <inheritdoc/>
 	public class ListWrapper : IListDataSource {
 		IList src;
 		BitArray marks;
 		int count, len;
 
-		/// <summary>
-		/// Initializes a new instance of <see cref="ListWrapper"/> given an <see cref="IList"/>
-		/// </summary>
-		/// <param name="source"></param>
+		/// <inheritdoc/>
 		public ListWrapper (IList source)
 		{
 			if (source != null) {
@@ -815,14 +830,10 @@ namespace Terminal.Gui {
 			}
 		}
 
-		/// <summary>
-		/// Gets the number of items in the <see cref="IList"/>.
-		/// </summary>
+		/// <inheritdoc/>
 		public int Count => src != null ? src.Count : 0;
 
-		/// <summary>
-		/// Gets the maximum item length in the <see cref="IList"/>.
-		/// </summary>
+		/// <inheritdoc/>
 		public int Length => len;
 
 		int GetMaxLengthItem ()
@@ -830,13 +841,13 @@ namespace Terminal.Gui {
 			if (src == null || src?.Count == 0) {
 				return 0;
 			}
-
+			
 			int maxLength = 0;
 			for (int i = 0; i < src.Count; i++) {
 				var t = src [i];
 				int l;
 				if (t is ustring u) {
-					l = u.RuneCount;
+					l = TextFormatter.GetTextWidth (u);
 				} else if (t is string s) {
 					l = s.Length;
 				} else {
@@ -853,33 +864,15 @@ namespace Terminal.Gui {
 
 		void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width, int start = 0)
 		{
-			int byteLen = ustr.Length;
-			int used = 0;
-			for (int i = start; i < byteLen;) {
-				(var rune, var size) = Utf8.DecodeRune (ustr, i, i - byteLen);
-				var count = Rune.ColumnWidth (rune);
-				if (used + count > width)
-					break;
-				driver.AddRune (rune);
-				used += count;
-				i += size;
-			}
-			for (; used < width; used++) {
+			var u = TextFormatter.ClipAndJustify (ustr, width, TextAlignment.Left);
+			driver.AddStr (u);
+			width -= TextFormatter.GetTextWidth (u);
+			while (width-- > 0) {
 				driver.AddRune (' ');
 			}
 		}
 
-		/// <summary>
-		/// Renders a <see cref="ListView"/> item to the appropriate type.
-		/// </summary>
-		/// <param name="container">The ListView.</param>
-		/// <param name="driver">The driver used by the caller.</param>
-		/// <param name="marked">Informs if it's marked or not.</param>
-		/// <param name="item">The item.</param>
-		/// <param name="col">The col where to move.</param>
-		/// <param name="line">The line where to move.</param>
-		/// <param name="width">The item width.</param>
-		/// <param name="start">The index of the string to be displayed.</param>
+		/// <inheritdoc/>
 		public void Render (ListView container, ConsoleDriver driver, bool marked, int item, int col, int line, int width, int start = 0)
 		{
 			container.Move (col, line);
@@ -897,11 +890,7 @@ namespace Terminal.Gui {
 			}
 		}
 
-		/// <summary>
-		/// Returns true if the item is marked, false otherwise.
-		/// </summary>
-		/// <param name="item">The item.</param>
-		/// <returns><c>true</c>If is marked.<c>false</c>otherwise.</returns>
+		/// <inheritdoc/>
 		public bool IsMarked (int item)
 		{
 			if (item >= 0 && item < count)
@@ -909,25 +898,40 @@ namespace Terminal.Gui {
 			return false;
 		}
 
-		/// <summary>
-		/// Sets the item as marked or unmarked based on the value is true or false, respectively.
-		/// </summary>
-		/// <param name="item">The item</param>
-		/// <param name="value"><true>Marks the item.</true><false>Unmarked the item.</false>The value.</param>
+		/// <inheritdoc/>
 		public void SetMark (int item, bool value)
 		{
 			if (item >= 0 && item < count)
 				marks [item] = value;
 		}
 
-		/// <summary>
-		/// Returns the source as IList.
-		/// </summary>
-		/// <returns></returns>
+		/// <inheritdoc/>
 		public IList ToList ()
 		{
 			return src;
 		}
+
+		/// <inheritdoc/>
+		public int StartsWith (string search)
+		{
+			if (src == null || src?.Count == 0) {
+				return -1;
+			}
+
+			for (int i = 0; i < src.Count; i++) {
+				var t = src [i];
+				if (t is ustring u) {
+					if (u.ToUpper ().StartsWith (search.ToUpperInvariant ())) {
+						return i;
+					}
+				} else if (t is string s) {
+					if (s.ToUpperInvariant ().StartsWith (search.ToUpperInvariant ())) {
+						return i;
+					}
+				}
+			}
+			return -1;
+		}
 	}
 
 	/// <summary>

+ 147 - 147
Terminal.Gui/Views/TreeView.cs

@@ -1,5 +1,5 @@
 // This code is based on http://objectlistview.sourceforge.net (GPLv3 tree/list controls 
-// by [email protected]).  Phillip has explicitly granted permission for his design
+// by [email protected]). Phillip has explicitly granted permission for his design
 // and code to be used in this library under the MIT license.
 
 using NStack;
@@ -12,18 +12,18 @@ using Terminal.Gui.Trees;
 namespace Terminal.Gui {
 
 	/// <summary>
-	/// Interface for all non generic members of <see cref="TreeView{T}"/>
+	/// Interface for all non generic members of <see cref="TreeView{T}"/>.
 	/// 
 	/// <a href="https://gui-cs.github.io/Terminal.Gui/articles/treeview.html">See TreeView Deep Dive for more information</a>.
 	/// </summary>
 	public interface ITreeView {
 		/// <summary>
-		/// Contains options for changing how the tree is rendered
+		/// Contains options for changing how the tree is rendered.
 		/// </summary>
 		TreeStyle Style { get; set; }
 
 		/// <summary>
-		/// Removes all objects from the tree and clears selection
+		/// Removes all objects from the tree and clears selection.
 		/// </summary>
 		void ClearObjects ();
 
@@ -43,7 +43,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Creates a new instance of the tree control with absolute positioning and initialises
-		/// <see cref="TreeBuilder{T}"/> with default <see cref="ITreeNode"/> based builder
+		/// <see cref="TreeBuilder{T}"/> with default <see cref="ITreeNode"/> based builder.
 		/// </summary>
 		public TreeView ()
 		{
@@ -53,8 +53,8 @@ namespace Terminal.Gui {
 	}
 
 	/// <summary>
-	/// Hierarchical tree view with expandable branches.  Branch objects are dynamically determined
-	/// when expanded using a user defined <see cref="ITreeBuilder{T}"/>
+	/// Hierarchical tree view with expandable branches. Branch objects are dynamically determined
+	/// when expanded using a user defined <see cref="ITreeBuilder{T}"/>.
 	/// 
 	/// <a href="https://gui-cs.github.io/Terminal.Gui/articles/treeview.html">See TreeView Deep Dive for more information</a>.
 	/// </summary>
@@ -64,7 +64,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Determines how sub branches of the tree are dynamically built at runtime as the user
-		/// expands root nodes
+		/// expands root nodes.
 		/// </summary>
 		/// <value></value>
 		public ITreeBuilder<T> TreeBuilder { get; set; }
@@ -74,30 +74,27 @@ namespace Terminal.Gui {
 		/// </summary>
 		T selectedObject;
 
-
 		/// <summary>
-		/// Contains options for changing how the tree is rendered
+		/// Contains options for changing how the tree is rendered.
 		/// </summary>
 		public TreeStyle Style { get; set; } = new TreeStyle ();
 
-
 		/// <summary>
-		/// True to allow multiple objects to be selected at once
+		/// True to allow multiple objects to be selected at once.
 		/// </summary>
 		/// <value></value>
 		public bool MultiSelect { get; set; } = true;
 
-
 		/// <summary>
 		/// True makes a letter key press navigate to the next visible branch that begins with
-		/// that letter/digit
+		/// that letter/digit.
 		/// </summary>
 		/// <value></value>
 		public bool AllowLetterBasedNavigation { get; set; } = true;
 
 		/// <summary>
-		/// The currently selected object in the tree.  When <see cref="MultiSelect"/> is true this
-		/// is the object at which the cursor is at
+		/// The currently selected object in the tree. When <see cref="MultiSelect"/> is true this
+		/// is the object at which the cursor is at.
 		/// </summary>
 		public T SelectedObject {
 			get => selectedObject;
@@ -111,16 +108,15 @@ namespace Terminal.Gui {
 			}
 		}
 
-
 		/// <summary>
 		/// This event is raised when an object is activated e.g. by double clicking or 
-		/// pressing <see cref="ObjectActivationKey"/>
+		/// pressing <see cref="ObjectActivationKey"/>.
 		/// </summary>
 		public event Action<ObjectActivatedEventArgs<T>> ObjectActivated;
 
 		/// <summary>
 		/// Key which when pressed triggers <see cref="TreeView{T}.ObjectActivated"/>.
-		/// Defaults to Enter
+		/// Defaults to Enter.
 		/// </summary>
 		public Key ObjectActivationKey {
 			get => objectActivationKey;
@@ -140,15 +136,14 @@ namespace Terminal.Gui {
 		/// <value></value>
 		public MouseFlags? ObjectActivationButton { get; set; } = MouseFlags.Button1DoubleClicked;
 
-
 		/// <summary>
-		/// Delegate for multi colored tree views.  Return the <see cref="ColorScheme"/> to use
+		/// Delegate for multi colored tree views. Return the <see cref="ColorScheme"/> to use
 		/// for each passed object or null to use the default.
 		/// </summary>
 		public Func<T, ColorScheme> ColorGetter { get; set; }
 
 		/// <summary>
-		/// Secondary selected regions of tree when <see cref="MultiSelect"/> is true
+		/// Secondary selected regions of tree when <see cref="MultiSelect"/> is true.
 		/// </summary>
 		private Stack<TreeSelection<T>> multiSelectedRegions = new Stack<TreeSelection<T>> ();
 
@@ -157,36 +152,35 @@ namespace Terminal.Gui {
 		/// </summary>
 		private IReadOnlyCollection<Branch<T>> cachedLineMap;
 
-
 		/// <summary>
 		/// Error message to display when the control is not properly initialized at draw time 
-		/// (nodes added but no tree builder set)
+		/// (nodes added but no tree builder set).
 		/// </summary>
 		public static ustring NoBuilderError = "ERROR: TreeBuilder Not Set";
 		private Key objectActivationKey = Key.Enter;
 
 		/// <summary>
-		/// Called when the <see cref="SelectedObject"/> changes
+		/// Called when the <see cref="SelectedObject"/> changes.
 		/// </summary>
 		public event EventHandler<SelectionChangedEventArgs<T>> SelectionChanged;
 
 		/// <summary>
-		/// The root objects in the tree, note that this collection is of root objects only
+		/// The root objects in the tree, note that this collection is of root objects only.
 		/// </summary>
 		public IEnumerable<T> Objects { get => roots.Keys; }
 
 		/// <summary>
-		/// Map of root objects to the branches under them.  All objects have 
-		/// a <see cref="Branch{T}"/> even if that branch has no children
+		/// Map of root objects to the branches under them. All objects have 
+		/// a <see cref="Branch{T}"/> even if that branch has no children.
 		/// </summary>
 		internal Dictionary<T, Branch<T>> roots { get; set; } = new Dictionary<T, Branch<T>> ();
 
 		/// <summary>
 		/// The amount of tree view that has been scrolled off the top of the screen (by the user 
-		/// scrolling down)
+		/// scrolling down).
 		/// </summary>
-		/// <remarks>Setting a value of less than 0 will result in a offset of 0.  To see changes 
-		/// in the UI call <see cref="View.SetNeedsDisplay()"/></remarks>
+		/// <remarks>Setting a value of less than 0 will result in a offset of 0. To see changes 
+		/// in the UI call <see cref="View.SetNeedsDisplay()"/>.</remarks>
 		public int ScrollOffsetVertical {
 			get => scrollOffsetVertical;
 			set {
@@ -194,12 +188,11 @@ namespace Terminal.Gui {
 			}
 		}
 
-
 		/// <summary>
-		/// The amount of tree view that has been scrolled to the right (horizontally)
+		/// The amount of tree view that has been scrolled to the right (horizontally).
 		/// </summary>
-		/// <remarks>Setting a value of less than 0 will result in a offset of 0.  To see changes 
-		/// in the UI call <see cref="View.SetNeedsDisplay()"/></remarks>
+		/// <remarks>Setting a value of less than 0 will result in a offset of 0. To see changes 
+		/// in the UI call <see cref="View.SetNeedsDisplay()"/>.</remarks>
 		public int ScrollOffsetHorizontal {
 			get => scrollOffsetHorizontal;
 			set {
@@ -208,13 +201,13 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// The current number of rows in the tree (ignoring the controls bounds)
+		/// The current number of rows in the tree (ignoring the controls bounds).
 		/// </summary>
 		public int ContentHeight => BuildLineMap ().Count ();
 
 		/// <summary>
-		/// Returns the string representation of model objects hosted in the tree.  Default 
-		/// implementation is to call <see cref="object.ToString"/>
+		/// Returns the string representation of model objects hosted in the tree. Default 
+		/// implementation is to call <see cref="object.ToString"/>.
 		/// </summary>
 		/// <value></value>
 		public AspectGetterDelegate<T> AspectGetter { get; set; } = (o) => o.ToString () ?? "";
@@ -224,7 +217,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Get / Set the wished cursor when the tree is focused.
 		/// Only applies when <see cref="MultiSelect"/> is true.
-		/// Defaults to <see cref="CursorVisibility.Invisible"/>
+		/// Defaults to <see cref="CursorVisibility.Invisible"/>.
 		/// </summary>
 		public CursorVisibility DesiredCursorVisibility {
 			get {
@@ -241,9 +234,9 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Creates a new tree view with absolute positioning.  
+		/// Creates a new tree view with absolute positioning. 
 		/// Use <see cref="AddObjects(IEnumerable{T})"/> to set set root objects for the tree.
-		/// Children will not be rendered until you set <see cref="TreeBuilder"/>
+		/// Children will not be rendered until you set <see cref="TreeBuilder"/>.
 		/// </summary>
 		public TreeView () : base ()
 		{
@@ -300,7 +293,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Initialises <see cref="TreeBuilder"/>.Creates a new tree view with absolute 
-		/// positioning.  Use <see cref="AddObjects(IEnumerable{T})"/> to set set root 
+		/// positioning. Use <see cref="AddObjects(IEnumerable{T})"/> to set set root 
 		/// objects for the tree.
 		/// </summary>
 		public TreeView (ITreeBuilder<T> builder) : this ()
@@ -317,7 +310,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Adds a new root level object unless it is already a root of the tree
+		/// Adds a new root level object unless it is already a root of the tree.
 		/// </summary>
 		/// <param name="o"></param>
 		public void AddObject (T o)
@@ -329,9 +322,8 @@ namespace Terminal.Gui {
 			}
 		}
 
-
 		/// <summary>
-		/// Removes all objects from the tree and clears <see cref="SelectedObject"/>
+		/// Removes all objects from the tree and clears <see cref="SelectedObject"/>.
 		/// </summary>
 		public void ClearObjects ()
 		{
@@ -346,7 +338,7 @@ namespace Terminal.Gui {
 		/// Removes the given root object from the tree
 		/// </summary>
 		/// <remarks>If <paramref name="o"/> is the currently <see cref="SelectedObject"/> then the
-		/// selection is cleared</remarks>
+		/// selection is cleared</remarks>.
 		/// <param name="o"></param>
 		public void Remove (T o)
 		{
@@ -362,9 +354,9 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Adds many new root level objects.  Objects that are already root objects are ignored
+		/// Adds many new root level objects. Objects that are already root objects are ignored.
 		/// </summary>
-		/// <param name="collection">Objects to add as new root level objects</param>
+		/// <param name="collection">Objects to add as new root level objects.</param>.\
 		public void AddObjects (IEnumerable<T> collection)
 		{
 			bool objectsAdded = false;
@@ -383,13 +375,13 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Refreshes the state of the object <paramref name="o"/> in the tree.  This will 
-		/// recompute children, string representation etc
+		/// Refreshes the state of the object <paramref name="o"/> in the tree. This will 
+		/// recompute children, string representation etc.
 		/// </summary>
 		/// <remarks>This has no effect if the object is not exposed in the tree.</remarks>
 		/// <param name="o"></param>
 		/// <param name="startAtTop">True to also refresh all ancestors of the objects branch 
-		/// (starting with the root).  False to refresh only the passed node</param>
+		/// (starting with the root). False to refresh only the passed node.</param>
 		public void RefreshObject (T o, bool startAtTop = false)
 		{
 			var branch = ObjectToBranch (o);
@@ -404,7 +396,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Rebuilds the tree structure for all exposed objects starting with the root objects.
 		/// Call this method when you know there are changes to the tree but don't know which 
-		/// objects have changed (otherwise use <see cref="RefreshObject(T, bool)"/>)
+		/// objects have changed (otherwise use <see cref="RefreshObject(T, bool)"/>).
 		/// </summary>
 		public void RebuildTree ()
 		{
@@ -417,10 +409,10 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Returns the currently expanded children of the passed object.  Returns an empty
-		/// collection if the branch is not exposed or not expanded
+		/// Returns the currently expanded children of the passed object. Returns an empty
+		/// collection if the branch is not exposed or not expanded.
 		/// </summary>
-		/// <param name="o">An object in the tree</param>
+		/// <param name="o">An object in the tree.</param>
 		/// <returns></returns>
 		public IEnumerable<T> GetChildren (T o)
 		{
@@ -433,10 +425,10 @@ namespace Terminal.Gui {
 			return branch.ChildBranches?.Values?.Select (b => b.Model)?.ToArray () ?? new T [0];
 		}
 		/// <summary>
-		/// Returns the parent object of <paramref name="o"/> in the tree.  Returns null if 
-		/// the object is not exposed in the tree
+		/// Returns the parent object of <paramref name="o"/> in the tree. Returns null if 
+		/// the object is not exposed in the tree.
 		/// </summary>
-		/// <param name="o">An object in the tree</param>
+		/// <param name="o">An object in the tree.</param>
 		/// <returns></returns>
 		public T GetParent (T o)
 		{
@@ -473,20 +465,19 @@ namespace Terminal.Gui {
 					Driver.SetAttribute (GetNormalColor ());
 					Driver.AddStr (new string (' ', bounds.Width));
 				}
-
 			}
 		}
 
 		/// <summary>
 		/// Returns the index of the object <paramref name="o"/> if it is currently exposed (it's 
-		/// parent(s) have been expanded).  This can be used with <see cref="ScrollOffsetVertical"/>
-		/// and <see cref="View.SetNeedsDisplay()"/> to scroll to a specific object
+		/// parent(s) have been expanded). This can be used with <see cref="ScrollOffsetVertical"/>
+		/// and <see cref="View.SetNeedsDisplay()"/> to scroll to a specific object.
 		/// </summary>
 		/// <remarks>Uses the Equals method and returns the first index at which the object is found
-		///  or -1 if it is not found</remarks>
-		/// <param name="o">An object that appears in your tree and is currently exposed</param>
+		/// or -1 if it is not found.</remarks>
+		/// <param name="o">An object that appears in your tree and is currently exposed.</param>
 		/// <returns>The index the object was found at or -1 if it is not currently revealed or
-		/// not in the tree at all</returns>
+		/// not in the tree at all.</returns>
 		public int GetScrollOffsetOf (T o)
 		{
 			var map = BuildLineMap ();
@@ -501,11 +492,11 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Returns the maximum width line in the tree including prefix and expansion symbols
+		/// Returns the maximum width line in the tree including prefix and expansion symbols.
 		/// </summary>
 		/// <param name="visible">True to consider only rows currently visible (based on window
-		///  bounds and <see cref="ScrollOffsetVertical"/>.  False to calculate the width of 
-		/// every exposed branch in the tree</param>
+		/// bounds and <see cref="ScrollOffsetVertical"/>. False to calculate the width of 
+		/// every exposed branch in the tree.</param>
 		/// <returns></returns>
 		public int GetContentWidth (bool visible)
 		{
@@ -536,7 +527,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Calculates all currently visible/expanded branches (including leafs) and outputs them 
-		/// by index from the top of the screen
+		/// by index from the top of the screen.
 		/// </summary>
 		/// <remarks>Index 0 of the returned array is the first item that should be visible in the
 		/// top of the control, index 1 is the next etc.</remarks>
@@ -553,7 +544,11 @@ namespace Terminal.Gui {
 				toReturn.AddRange (AddToLineMap (root));
 			}
 
-			return cachedLineMap = new ReadOnlyCollection<Branch<T>> (toReturn);
+			cachedLineMap = new ReadOnlyCollection<Branch<T>> (toReturn);
+			
+			// Update the collection used for search-typing
+			KeystrokeNavigator.Collection = cachedLineMap.Select (b => AspectGetter (b.Model)).ToArray ();
+			return cachedLineMap;
 		}
 
 		private IEnumerable<Branch<T>> AddToLineMap (Branch<T> currentBranch)
@@ -561,7 +556,6 @@ namespace Terminal.Gui {
 			yield return currentBranch;
 
 			if (currentBranch.IsExpanded) {
-
 				foreach (var subBranch in currentBranch.ChildBranches.Values) {
 					foreach (var sub in AddToLineMap (subBranch)) {
 						yield return sub;
@@ -570,6 +564,12 @@ namespace Terminal.Gui {
 			}
 		}
 
+		/// <summary>
+		/// Gets the <see cref="CollectionNavigator"/> that searches the <see cref="Objects"/> collection as
+		/// the user types.
+		/// </summary>
+		public CollectionNavigator KeystrokeNavigator { get; private set; } = new CollectionNavigator ();
+
 		/// <inheritdoc/>
 		public override bool ProcessKey (KeyEvent keyEvent)
 		{
@@ -577,21 +577,33 @@ namespace Terminal.Gui {
 				return false;
 			}
 
-			// if it is a single character pressed without any control keys
-			if (keyEvent.KeyValue > 0 && keyEvent.KeyValue < 0xFFFF) {
-
-				if (char.IsLetterOrDigit ((char)keyEvent.KeyValue) && AllowLetterBasedNavigation && !keyEvent.IsShift && !keyEvent.IsAlt && !keyEvent.IsCtrl) {
-					AdjustSelectionToNextItemBeginningWith ((char)keyEvent.KeyValue);
-					return true;
-				}
-			}
-
 			try {
+				// First of all deal with any registered keybindings
 				var result = InvokeKeybindings (keyEvent);
-				if (result != null)
+				if (result != null) {
 					return (bool)result;
-			} finally {
+				}
 
+				// If not a keybinding, is the key a searchable key press?
+				if (CollectionNavigator.IsCompatibleKey (keyEvent) && AllowLetterBasedNavigation) {
+					IReadOnlyCollection<Branch<T>> map;
+
+					// If there has been a call to InvalidateMap since the last time
+					// we need a new one to reflect the new exposed tree state
+					map = BuildLineMap ();
+
+					// Find the current selected object within the tree
+					var current = map.IndexOf (b => b.Model == SelectedObject);
+					var newIndex = KeystrokeNavigator?.GetNextMatchingItem (current, (char)keyEvent.KeyValue);
+
+					if (newIndex is int && newIndex != -1) {
+						SelectedObject = map.ElementAt ((int)newIndex).Model;
+						EnsureVisible (selectedObject);
+						SetNeedsDisplay ();
+						return true;
+					}
+				}
+			} finally {
 				PositionCursor ();
 			}
 
@@ -602,7 +614,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// <para>Triggers the <see cref="ObjectActivated"/> event with the <see cref="SelectedObject"/>.</para>
 		/// 
-		/// <para>This method also ensures that the selected object is visible</para>
+		/// <para>This method also ensures that the selected object is visible.</para>
 		/// </summary>
 		public void ActivateSelectedObjectIfAny ()
 		{
@@ -638,11 +650,11 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// <para>Moves the <see cref="SelectedObject"/> to the next item that begins with <paramref name="character"/></para>
-		/// <para>This method will loop back to the start of the tree if reaching the end without finding a match</para>
+		/// <para>Moves the <see cref="SelectedObject"/> to the next item that begins with <paramref name="character"/>.</para>
+		/// <para>This method will loop back to the start of the tree if reaching the end without finding a match.</para>
 		/// </summary>
-		/// <param name="character">The first character of the next item you want selected</param>
-		/// <param name="caseSensitivity">Case sensitivity of the search</param>
+		/// <param name="character">The first character of the next item you want selected.</param>
+		/// <param name="caseSensitivity">Case sensitivity of the search.</param>
 		public void AdjustSelectionToNextItemBeginningWith (char character, StringComparison caseSensitivity = StringComparison.CurrentCultureIgnoreCase)
 		{
 			// search for next branch that begins with that letter
@@ -655,7 +667,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Moves the selection up by the height of the control (1 page).
 		/// </summary>
-		/// <param name="expandSelection">True if the navigation should add the covered nodes to the selected current selection</param>
+		/// <param name="expandSelection">True if the navigation should add the covered nodes to the selected current selection.</param>
 		/// <exception cref="NotImplementedException"></exception>
 		public void MovePageUp (bool expandSelection = false)
 		{
@@ -665,7 +677,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Moves the selection down by the height of the control (1 page).
 		/// </summary>
-		/// <param name="expandSelection">True if the navigation should add the covered nodes to the selected current selection</param>
+		/// <param name="expandSelection">True if the navigation should add the covered nodes to the selected current selection.</param>
 		/// <exception cref="NotImplementedException"></exception>
 		public void MovePageDown (bool expandSelection = false)
 		{
@@ -673,7 +685,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Scrolls the view area down a single line without changing the current selection
+		/// Scrolls the view area down a single line without changing the current selection.
 		/// </summary>
 		public void ScrollDown ()
 		{
@@ -682,7 +694,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Scrolls the view area up a single line without changing the current selection
+		/// Scrolls the view area up a single line without changing the current selection.
 		/// </summary>
 		public void ScrollUp ()
 		{
@@ -691,7 +703,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Raises the <see cref="ObjectActivated"/> event
+		/// Raises the <see cref="ObjectActivated"/> event.
 		/// </summary>
 		/// <param name="e"></param>
 		protected virtual void OnObjectActivated (ObjectActivatedEventArgs<T> e)
@@ -700,15 +712,15 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Returns the object in the tree list that is currently visible
-		/// at the provided row.  Returns null if no object is at that location.
+		/// Returns the object in the tree list that is currently visible.
+		/// at the provided row. Returns null if no object is at that location.
 		/// <remarks>
 		/// </remarks>
 		/// If you have screen coordinates then use <see cref="View.ScreenToView(int, int)"/>
 		/// to translate these into the client area of the <see cref="TreeView{T}"/>.
 		/// </summary>
-		/// <param name="row">The row of the <see cref="View.Bounds"/> of the <see cref="TreeView{T}"/></param>
-		/// <returns>The object currently displayed on this row or null</returns>
+		/// <param name="row">The row of the <see cref="View.Bounds"/> of the <see cref="TreeView{T}"/>.</param>
+		/// <returns>The object currently displayed on this row or null.</returns>
 		public T GetObjectOnRow (int row)
 		{
 			return HitTest (row)?.Model;
@@ -733,7 +745,6 @@ namespace Terminal.Gui {
 				SetFocus ();
 			}
 
-
 			if (me.Flags == MouseFlags.WheeledDown) {
 
 				ScrollDown ();
@@ -789,7 +800,6 @@ namespace Terminal.Gui {
 						multiSelectedRegions.Clear ();
 					}
 				} else {
-
 					// It is a first click somewhere in the current line that doesn't look like an expansion/collapse attempt
 					SelectedObject = clickedBranch.Model;
 					multiSelectedRegions.Clear ();
@@ -819,16 +829,15 @@ namespace Terminal.Gui {
 				// mouse event is handled.
 				return true;
 			}
-
 			return false;
 		}
 
 		/// <summary>
 		/// Returns the branch at the given <paramref name="y"/> client
-		/// coordinate e.g. following a click event
+		/// coordinate e.g. following a click event.
 		/// </summary>
-		/// <param name="y">Client Y position in the controls bounds</param>
-		/// <returns>The clicked branch or null if outside of tree region</returns>
+		/// <param name="y">Client Y position in the controls bounds.</param>
+		/// <returns>The clicked branch or null if outside of tree region.</returns>
 		private Branch<T> HitTest (int y)
 		{
 			var map = BuildLineMap ();
@@ -845,7 +854,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Positions the cursor at the start of the selected objects line (if visible)
+		/// Positions the cursor at the start of the selected objects line (if visible).
 		/// </summary>
 		public override void PositionCursor ()
 		{
@@ -866,11 +875,10 @@ namespace Terminal.Gui {
 			}
 		}
 
-
 		/// <summary>
-		/// Determines systems behaviour when the left arrow key is pressed.  Default behaviour is
+		/// Determines systems behaviour when the left arrow key is pressed. Default behaviour is
 		/// to collapse the current tree node if possible otherwise changes selection to current 
-		/// branches parent
+		/// branches parent.
 		/// </summary>
 		protected virtual void CursorLeft (bool ctrl)
 		{
@@ -894,7 +902,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Changes the <see cref="SelectedObject"/> to the first root object and resets 
-		/// the <see cref="ScrollOffsetVertical"/> to 0
+		/// the <see cref="ScrollOffsetVertical"/> to 0.
 		/// </summary>
 		public void GoToFirst ()
 		{
@@ -906,7 +914,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Changes the <see cref="SelectedObject"/> to the last object in the tree and scrolls so
-		/// that it is visible
+		/// that it is visible.
 		/// </summary>
 		public void GoToEnd ()
 		{
@@ -919,8 +927,8 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Changes the <see cref="SelectedObject"/> to <paramref name="toSelect"/> and scrolls to ensure
-		/// it is visible.  Has no effect if <paramref name="toSelect"/> is not exposed in the tree (e.g. 
-		/// its parents are collapsed)
+		/// it is visible. Has no effect if <paramref name="toSelect"/> is not exposed in the tree (e.g. 
+		/// its parents are collapsed).
 		/// </summary>
 		/// <param name="toSelect"></param>
 		public void GoTo (T toSelect)
@@ -935,14 +943,14 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// The number of screen lines to move the currently selected object by.  Supports negative 
-		/// <paramref name="offset"/>.  Each branch occupies 1 line on screen
+		/// The number of screen lines to move the currently selected object by. Supports negative values.
+		/// <paramref name="offset"/>. Each branch occupies 1 line on screen.
 		/// </summary>
 		/// <remarks>If nothing is currently selected or the selected object is no longer in the tree
-		/// then the first object in the tree is selected instead</remarks>
+		/// then the first object in the tree is selected instead.</remarks>
 		/// <param name="offset">Positive to move the selection down the screen, negative to move it up</param>
 		/// <param name="expandSelection">True to expand the selection (assuming 
-		/// <see cref="MultiSelect"/> is enabled).  False to replace</param>
+		/// <see cref="MultiSelect"/> is enabled). False to replace.</param>
 		public void AdjustSelection (int offset, bool expandSelection = false)
 		{
 			// if it is not a shift click or we don't allow multi select
@@ -958,7 +966,6 @@ namespace Terminal.Gui {
 				var idx = map.IndexOf (b => b.Model.Equals (SelectedObject));
 
 				if (idx == -1) {
-
 					// The current selection has disapeared!
 					SelectedObject = roots.Keys.FirstOrDefault ();
 				} else {
@@ -982,14 +989,12 @@ namespace Terminal.Gui {
 
 					EnsureVisible (SelectedObject);
 				}
-
 			}
-
 			SetNeedsDisplay ();
 		}
 
 		/// <summary>
-		/// Moves the selection to the first child in the currently selected level
+		/// Moves the selection to the first child in the currently selected level.
 		/// </summary>
 		public void AdjustSelectionToBranchStart ()
 		{
@@ -1029,7 +1034,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Moves the selection to the last child in the currently selected level
+		/// Moves the selection to the last child in the currently selected level.
 		/// </summary>
 		public void AdjustSelectionToBranchEnd ()
 		{
@@ -1063,13 +1068,12 @@ namespace Terminal.Gui {
 				currentBranch = next;
 				next = map.ElementAt (currentIdx);
 			}
-
 			GoToEnd ();
 		}
 
 
 		/// <summary>
-		/// Sets the selection to the next branch that matches the <paramref name="predicate"/>
+		/// Sets the selection to the next branch that matches the <paramref name="predicate"/>.
 		/// </summary>
 		/// <param name="predicate"></param>
 		private void AdjustSelectionToNext (Func<Branch<T>, bool> predicate)
@@ -1107,7 +1111,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Adjusts the <see cref="ScrollOffsetVertical"/> to ensure the given
-		/// <paramref name="model"/> is visible.  Has no effect if already visible
+		/// <paramref name="model"/> is visible. Has no effect if already visible.
 		/// </summary>
 		public void EnsureVisible (T model)
 		{
@@ -1134,7 +1138,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Expands the currently <see cref="SelectedObject"/>
+		/// Expands the currently <see cref="SelectedObject"/>.
 		/// </summary>
 		public void Expand ()
 		{
@@ -1143,9 +1147,9 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Expands the supplied object if it is contained in the tree (either as a root object or 
-		/// as an exposed branch object)
+		/// as an exposed branch object).
 		/// </summary>
-		/// <param name="toExpand">The object to expand</param>
+		/// <param name="toExpand">The object to expand.</param>
 		public void Expand (T toExpand)
 		{
 			if (toExpand == null) {
@@ -1158,9 +1162,9 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Expands the supplied object and all child objects
+		/// Expands the supplied object and all child objects.
 		/// </summary>
-		/// <param name="toExpand">The object to expand</param>
+		/// <param name="toExpand">The object to expand.</param>
 		public void ExpandAll (T toExpand)
 		{
 			if (toExpand == null) {
@@ -1173,7 +1177,7 @@ namespace Terminal.Gui {
 		}
 		/// <summary>
 		/// Fully expands all nodes in the tree, if the tree is very big and built dynamically this
-		/// may take a while (e.g. for file system)
+		/// may take a while (e.g. for file system).
 		/// </summary>
 		public void ExpandAll ()
 		{
@@ -1186,7 +1190,7 @@ namespace Terminal.Gui {
 		}
 		/// <summary>
 		/// Returns true if the given object <paramref name="o"/> is exposed in the tree and can be
-		/// expanded otherwise false
+		/// expanded otherwise false.
 		/// </summary>
 		/// <param name="o"></param>
 		/// <returns></returns>
@@ -1197,7 +1201,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Returns true if the given object <paramref name="o"/> is exposed in the tree and 
-		/// expanded otherwise false
+		/// expanded otherwise false.
 		/// </summary>
 		/// <param name="o"></param>
 		/// <returns></returns>
@@ -1215,26 +1219,26 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Collapses the supplied object if it is currently expanded 
+		/// Collapses the supplied object if it is currently expanded .
 		/// </summary>
-		/// <param name="toCollapse">The object to collapse</param>
+		/// <param name="toCollapse">The object to collapse.</param>
 		public void Collapse (T toCollapse)
 		{
 			CollapseImpl (toCollapse, false);
 		}
 
 		/// <summary>
-		/// Collapses the supplied object if it is currently expanded.  Also collapses all children
-		/// branches (this will only become apparent when/if the user expands it again)
+		/// Collapses the supplied object if it is currently expanded. Also collapses all children
+		/// branches (this will only become apparent when/if the user expands it again).
 		/// </summary>
-		/// <param name="toCollapse">The object to collapse</param>
+		/// <param name="toCollapse">The object to collapse.</param>
 		public void CollapseAll (T toCollapse)
 		{
 			CollapseImpl (toCollapse, true);
 		}
 
 		/// <summary>
-		/// Collapses all root nodes in the tree
+		/// Collapses all root nodes in the tree.
 		/// </summary>
 		public void CollapseAll ()
 		{
@@ -1247,19 +1251,17 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Implementation of <see cref="Collapse(T)"/> and <see cref="CollapseAll(T)"/>.  Performs
-		/// operation and updates selection if disapeared
+		/// Implementation of <see cref="Collapse(T)"/> and <see cref="CollapseAll(T)"/>. Performs
+		/// operation and updates selection if disapeared.
 		/// </summary>
 		/// <param name="toCollapse"></param>
 		/// <param name="all"></param>
 		protected void CollapseImpl (T toCollapse, bool all)
 		{
-
 			if (toCollapse == null) {
 				return;
 			}
 
-
 			var branch = ObjectToBranch (toCollapse);
 
 			// Nothing to collapse
@@ -1292,12 +1294,12 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Returns the corresponding <see cref="Branch{T}"/> in the tree for
-		/// <paramref name="toFind"/>.  This will not work for objects hidden
-		/// by their parent being collapsed
+		/// <paramref name="toFind"/>. This will not work for objects hidden
+		/// by their parent being collapsed.
 		/// </summary>
 		/// <param name="toFind"></param>
 		/// <returns>The branch for <paramref name="toFind"/> or null if it is not currently 
-		/// exposed in the tree</returns>
+		/// exposed in the tree.</returns>
 		private Branch<T> ObjectToBranch (T toFind)
 		{
 			return BuildLineMap ().FirstOrDefault (o => o.Model.Equals (toFind));
@@ -1305,7 +1307,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Returns true if the <paramref name="model"/> is either the 
-		/// <see cref="SelectedObject"/> or part of a <see cref="MultiSelect"/>
+		/// <see cref="SelectedObject"/> or part of a <see cref="MultiSelect"/>.
 		/// </summary>
 		/// <param name="model"></param>
 		/// <returns></returns>
@@ -1340,7 +1342,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Selects all objects in the tree when <see cref="MultiSelect"/> is enabled otherwise 
-		/// does nothing
+		/// does nothing.
 		/// </summary>
 		public void SelectAll ()
 		{
@@ -1362,9 +1364,8 @@ namespace Terminal.Gui {
 			OnSelectionChanged (new SelectionChangedEventArgs<T> (this, SelectedObject, SelectedObject));
 		}
 
-
 		/// <summary>
-		/// Raises the SelectionChanged event
+		/// Raises the SelectionChanged event.
 		/// </summary>
 		/// <param name="e"></param>
 		protected virtual void OnSelectionChanged (SelectionChangedEventArgs<T> e)
@@ -1406,5 +1407,4 @@ namespace Terminal.Gui {
 			return included.Contains (model);
 		}
 	}
-
 }

+ 8 - 8
UICatalog/Properties/launchSettings.json

@@ -23,14 +23,18 @@
       "commandName": "Project",
       "commandLineArgs": "WizardAsView"
     },
-    "Issue1719Repro": {
-      "commandName": "Project",
-      "commandLineArgs": "\"ProgressBar Styles\""
-    },
     "VkeyPacketSimulator": {
       "commandName": "Project",
       "commandLineArgs": "VkeyPacketSimulator"
     },
+    "CollectionNavigatorTester": {
+      "commandName": "Project",
+      "commandLineArgs": "\"Search Collection Nav\""
+    },
+    "Charmap": {
+      "commandName": "Project",
+      "commandLineArgs": "\"Character Map\""
+    },
     "WSL2": {
       "commandName": "Executable",
       "executablePath": "wsl",
@@ -44,10 +48,6 @@
     "WSL": {
       "commandName": "WSL2",
       "distributionName": ""
-    },
-    "Charmap": {
-      "commandName": "Project",
-      "commandLineArgs": "\"Character Map\""
     }
   }
 }

+ 2 - 2
UICatalog/Scenario.cs

@@ -233,7 +233,7 @@ namespace UICatalog {
 		}
 
 		/// <summary>
-		/// Returns an instance of each <see cref="Scenario"/> defined in the project. 
+		/// Returns a list of all <see cref="Scenario"/> instanaces defined in the project, sorted by <see cref="ScenarioMetadata.Name"/>.
 		/// https://stackoverflow.com/questions/5411694/get-all-inherited-classes-of-an-abstract-class
 		/// </summary>
 		public static List<Scenario> GetScenarios ()
@@ -245,7 +245,7 @@ namespace UICatalog {
 				objects.Add (scenario);
 				_maxScenarioNameLen = Math.Max (_maxScenarioNameLen, scenario.GetName ().Length + 1);
 			}
-			return objects;
+			return objects.OrderBy (s => s.GetName ()).ToList ();
 		}
 
 		protected virtual void Dispose (bool disposing)

+ 194 - 0
UICatalog/Scenarios/CollectionNavigatorTester.cs

@@ -0,0 +1,194 @@
+using System;
+using System.IO;
+using System.Linq;
+using Terminal.Gui;
+using Terminal.Gui.Trees;
+
+namespace UICatalog.Scenarios {
+
+	[ScenarioMetadata (Name: "Collection Navigator", Description: "Demonstrates keyboard navigation in ListView & TreeView (CollectionNavigator).")]
+	[ScenarioCategory ("Controls"),
+		ScenarioCategory ("ListView"),
+		ScenarioCategory ("TreeView"),
+		ScenarioCategory ("Text and Formatting"),
+		ScenarioCategory ("Mouse and Keyboard")]
+	public class CollectionNavigatorTester : Scenario {
+
+		// Don't create a Window, just return the top-level view
+		public override void Init (Toplevel top, ColorScheme colorScheme)
+		{
+			Application.Init ();
+			Top = top != null ? top : Application.Top;
+			Top.ColorScheme = Colors.Base;
+		}
+
+		System.Collections.Generic.List<string> _items = new string [] {
+				"a",
+				"b",
+				"bb",
+				"c",
+				"ccc",
+				"ccc",
+				"cccc",
+				"ddd",
+				"dddd",
+				"dddd",
+				"ddddd",
+				"dddddd",
+				"ddddddd",
+				"this",
+				"this is a test",
+				"this was a test",
+				"this and",
+				"that and that",
+				"the",
+				"think",
+				"thunk",
+				"thunks",
+				"zip",
+				"zap",
+				"zoo",
+				"@jack",
+				"@sign",
+				"@at",
+				"@ateme",
+				"n@",
+				"n@brown",
+				".net",
+				"$100.00",
+				"$101.00",
+				"$101.10",
+				"$101.11",
+				"$200.00",
+				"$210.99",
+				"$$",
+				"appricot",
+				"arm",
+				"丗丙业丞",
+				"丗丙丛",
+				"text",
+				"egg",
+				"candle",
+				" <- space",
+				"\t<- tab",
+				"\n<- newline",
+				"\r<- formfeed",
+				"q",
+				"quit",
+				"quitter"
+			}.ToList<string> ();
+
+		public override void Setup ()
+		{
+			var allowMarking = new MenuItem ("Allow _Marking", "", null) {
+				CheckType = MenuItemCheckStyle.Checked,
+				Checked = false
+			};
+			allowMarking.Action = () => allowMarking.Checked = _listView.AllowsMarking = !_listView.AllowsMarking;
+
+			var allowMultiSelection = new MenuItem ("Allow Multi _Selection", "", null) {
+				CheckType = MenuItemCheckStyle.Checked,
+				Checked = false
+			};
+			allowMultiSelection.Action = () => allowMultiSelection.Checked = _listView.AllowsMultipleSelection = !_listView.AllowsMultipleSelection;
+			allowMultiSelection.CanExecute = () => allowMarking.Checked;
+
+			var menu = new MenuBar (new MenuBarItem [] {
+				new MenuBarItem ("_Configure", new MenuItem [] {
+					allowMarking,
+					allowMultiSelection,
+					null,
+					new MenuItem ("_Quit", "", () => Quit(), null, null, Key.Q | Key.CtrlMask),
+				}),
+				new MenuBarItem("_Quit", "CTRL-Q", () => Quit()),
+			});
+
+			Top.Add (menu);
+
+			_items.Sort (StringComparer.OrdinalIgnoreCase);
+
+			CreateListView ();
+			var vsep = new LineView (Terminal.Gui.Graphs.Orientation.Vertical) {
+				X = Pos.Right (_listView),
+				Y = 1,
+				Height = Dim.Fill ()
+			};
+			Top.Add (vsep);
+			CreateTreeView ();
+		}
+
+		ListView _listView = null;
+
+		private void CreateListView ()
+		{
+			var label = new Label () {
+				Text = "ListView",
+				TextAlignment = TextAlignment.Centered,
+				X = 0,
+				Y = 1, // for menu
+				Width = Dim.Percent (50),
+				Height = 1,
+			};
+			Top.Add (label);
+
+			_listView = new ListView () {
+				X = 0,
+				Y = Pos.Bottom (label),
+				Width = Dim.Percent (50) - 1,
+				Height = Dim.Fill (),
+				AllowsMarking = false,
+				AllowsMultipleSelection = false,
+				ColorScheme = Colors.TopLevel
+			};
+			Top.Add (_listView);
+
+			_listView.SetSource (_items);
+
+			_listView.KeystrokeNavigator.SearchStringChanged += (state) => {
+				label.Text = $"ListView: {state.SearchString}";
+			};
+		}
+
+		TreeView _treeView = null;
+
+		private void CreateTreeView ()
+		{
+			var label = new Label () {
+				Text = "TreeView",
+				TextAlignment = TextAlignment.Centered,
+				X = Pos.Right (_listView) + 2,
+				Y = 1, // for menu
+				Width = Dim.Percent (50),
+				Height = 1,
+			};
+			Top.Add (label);
+
+			_treeView = new TreeView () {
+				X = Pos.Right (_listView) + 1,
+				Y = Pos.Bottom (label),
+				Width = Dim.Fill (),
+				Height = Dim.Fill (),
+				ColorScheme = Colors.TopLevel
+			};
+			Top.Add (_treeView);
+
+			var root = new TreeNode ("IsLetterOrDigit examples");
+			root.Children = _items.Where (i => char.IsLetterOrDigit (i [0])).Select (i => new TreeNode (i)).Cast<ITreeNode> ().ToList ();
+			_treeView.AddObject (root);
+			root = new TreeNode ("Non-IsLetterOrDigit examples");
+			root.Children = _items.Where (i => !char.IsLetterOrDigit (i [0])).Select (i => new TreeNode (i)).Cast<ITreeNode> ().ToList ();
+			_treeView.AddObject (root);
+			_treeView.ExpandAll ();
+			_treeView.GoToFirst ();
+
+			_treeView.KeystrokeNavigator.SearchStringChanged += (state) => {
+				label.Text = $"TreeView: {state.SearchString}";
+			};
+		}
+
+		private void Quit ()
+		{
+			Application.RequestStop ();
+		}
+	}
+}

+ 1 - 1
UICatalog/Scenarios/Keys.cs

@@ -52,7 +52,7 @@ namespace UICatalog.Scenarios {
 		{
 			Application.Init ();
 			Top = top != null ? top : Application.Top;
-
+			
 			Win = new TestWindow ($"CTRL-Q to Close - Scenario: {GetName ()}") {
 				X = 0,
 				Y = 0,

+ 1 - 1
UICatalog/Scenarios/ListViewWithSelection.cs

@@ -21,7 +21,7 @@ namespace UICatalog.Scenarios {
 
 		public override void Setup ()
 		{
-			_scenarios = Scenario.GetScenarios ().OrderBy (s => s.GetName ()).ToList ();
+			_scenarios = Scenario.GetScenarios ();
 
 			_customRenderCB = new CheckBox ("Use custom rendering") {
 				X = 0,

+ 1 - 1
UICatalog/Scenarios/VkeyPacketSimulator.cs

@@ -6,7 +6,7 @@ using Terminal.Gui;
 
 namespace UICatalog.Scenarios {
 	[ScenarioMetadata (Name: "VkeyPacketSimulator", Description: "Simulates the Virtual Key Packet")]
-	[ScenarioCategory ("Keys")]
+	[ScenarioCategory ("Mouse and Keyboard")]
 	public class VkeyPacketSimulator : Scenario {
 		List<int> _keyboardStrokes = new List<int> ();
 		bool _outputStarted = false;

+ 370 - 0
UnitTests/CollectionNavigatorTests.cs

@@ -0,0 +1,370 @@
+using System;
+using System.Threading;
+using Xunit;
+
+namespace Terminal.Gui.Core {
+	public class CollectionNavigatorTests {
+		static string [] simpleStrings = new string []{
+		    "appricot", // 0
+		    "arm",      // 1
+		    "bat",      // 2
+		    "batman",   // 3
+		    "candle"    // 4
+		  };
+
+		[Fact]
+		public void ShouldAcceptNegativeOne ()
+		{
+			var n = new CollectionNavigator (simpleStrings);
+
+			// Expect that index of -1 (i.e. no selection) should work correctly
+			// and select the first entry of the letter 'b'
+			Assert.Equal (2, n.GetNextMatchingItem (-1, 'b'));
+		}
+		[Fact]
+		public void OutOfBoundsShouldBeIgnored ()
+		{
+			var n = new CollectionNavigator (simpleStrings);
+
+			// Expect saying that index 500 is the current selection should not cause
+			// error and just be ignored (treated as no selection)
+			Assert.Equal (2, n.GetNextMatchingItem (500, 'b'));
+		}
+
+		[Fact]
+		public void Cycling ()
+		{
+			var n = new CollectionNavigator (simpleStrings);
+			Assert.Equal (2, n.GetNextMatchingItem (0, 'b'));
+			Assert.Equal (3, n.GetNextMatchingItem (2, 'b'));
+
+			// if 4 (candle) is selected it should loop back to bat
+			Assert.Equal (2, n.GetNextMatchingItem (4, 'b'));
+		}
+
+		[Fact]
+		public void FullText ()
+		{
+			var strings = new string []{
+			    "appricot",
+			    "arm",
+			    "ta",
+			    "target",
+			    "text",
+			    "egg",
+			    "candle"
+			  };
+
+			var n = new CollectionNavigator (strings);
+			int current = 0;
+			Assert.Equal (strings.IndexOf ("ta"), current = n.GetNextMatchingItem (current, 't'));
+
+			// should match "te" in "text"
+			Assert.Equal (strings.IndexOf ("text"), current = n.GetNextMatchingItem (current, 'e'));
+
+			// still matches text
+			Assert.Equal (strings.IndexOf ("text"), current = n.GetNextMatchingItem (current, 'x'));
+
+			// nothing starts texa so it should NOT jump to appricot
+			Assert.Equal (strings.IndexOf ("text"), current = n.GetNextMatchingItem (current, 'a'));
+
+			Thread.Sleep (n.TypingDelay + 100);
+			// nothing starts "texa". Since were past timedelay we DO jump to appricot
+			Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a'));
+		}
+
+		[Fact]
+		public void Unicode ()
+		{
+			var strings = new string []{
+			    "appricot",
+			    "arm",
+			    "ta",
+			    "丗丙业丞",
+			    "丗丙丛",
+			    "text",
+			    "egg",
+			    "candle"
+			  };
+
+			var n = new CollectionNavigator (strings);
+			int current = 0;
+			Assert.Equal (strings.IndexOf ("丗丙业丞"), current = n.GetNextMatchingItem (current, '丗'));
+
+			// 丗丙业丞 is as good a match as 丗丙丛
+			// so when doing multi character searches we should
+			// prefer to stay on the same index unless we invalidate
+			// our typed text
+			Assert.Equal (strings.IndexOf ("丗丙业丞"), current = n.GetNextMatchingItem (current, '丙'));
+
+			// No longer matches 丗丙业丞 and now only matches 丗丙丛
+			// so we should move to the new match
+			Assert.Equal (strings.IndexOf ("丗丙丛"), current = n.GetNextMatchingItem (current, '丛'));
+
+			// nothing starts "丗丙丛a". Since were still in the timedelay we do not jump to appricot
+			Assert.Equal (strings.IndexOf ("丗丙丛"), current = n.GetNextMatchingItem (current, 'a'));
+
+			Thread.Sleep (n.TypingDelay + 100);
+			// nothing starts "丗丙丛a". Since were past timedelay we DO jump to appricot
+			Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a'));
+		}
+
+		[Fact]
+		public void AtSymbol ()
+		{
+			var strings = new string []{
+			    "appricot",
+			    "arm",
+			    "ta",
+			    "@bob",
+			    "@bb",
+			    "text",
+			    "egg",
+			    "candle"
+			  };
+
+			var n = new CollectionNavigator (strings);
+			Assert.Equal (3, n.GetNextMatchingItem (0, '@'));
+			Assert.Equal (3, n.GetNextMatchingItem (3, 'b'));
+			Assert.Equal (4, n.GetNextMatchingItem (3, 'b'));
+		}
+
+		[Fact]
+		public void Word ()
+		{
+			var strings = new string []{
+			    "appricot",
+			    "arm",
+			    "bat",
+			    "batman",
+			    "bates hotel",
+			    "candle"
+			  };
+			int current = 0;
+			var n = new CollectionNavigator (strings);
+			Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 'b')); // match bat
+			Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 'a')); // match bat
+			Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 't')); // match bat
+			Assert.Equal (strings.IndexOf ("bates hotel"), current = n.GetNextMatchingItem (current, 'e')); // match bates hotel
+			Assert.Equal (strings.IndexOf ("bates hotel"), current = n.GetNextMatchingItem (current, 's')); // match bates hotel
+			Assert.Equal (strings.IndexOf ("bates hotel"), current = n.GetNextMatchingItem (current, ' ')); // match bates hotel
+		}
+
+		[Fact]
+		public void Symbols ()
+		{
+			var strings = new string []{
+			    "$$",
+			    "$100.00",
+			    "$101.00",
+			    "$101.10",
+			    "$200.00",
+			    "appricot"
+			  };
+			int current = 0;
+			var n = new CollectionNavigator (strings);
+			Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a'));
+			Assert.Equal ("a", n.SearchString);
+
+			Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$'));
+			Assert.Equal ("$", n.SearchString);
+
+			Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, '1'));
+			Assert.Equal ("$1", n.SearchString);
+
+			Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, '0'));
+			Assert.Equal ("$10", n.SearchString);
+
+			Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, '1'));
+			Assert.Equal ("$101", n.SearchString);
+
+			Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, '.'));
+			Assert.Equal ("$101.", n.SearchString);
+
+			// stay on the same item becuase still in timedelay
+			Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, 'a'));
+			Assert.Equal ("$101.", n.SearchString);
+
+			Thread.Sleep (n.TypingDelay + 100);
+			// another '$' means searching for "$" again
+			Assert.Equal (strings.IndexOf ("$101.10"), current = n.GetNextMatchingItem (current, '$'));
+			Assert.Equal ("$", n.SearchString);
+
+			Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$'));
+			Assert.Equal ("$$", n.SearchString);
+
+		}
+
+		[Fact]
+		public void Delay ()
+		{
+			var strings = new string []{
+			    "$$",
+			    "$100.00",
+			    "$101.00",
+			    "$101.10",
+			    "$200.00",
+			    "appricot"
+			  };
+			int current = 0;
+			var n = new CollectionNavigator (strings);
+
+			// No delay
+			Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a'));
+			Assert.Equal ("a", n.SearchString);
+			Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$'));
+			Assert.Equal ("$", n.SearchString);
+			Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$'));
+			Assert.Equal ("$$", n.SearchString);
+
+			// Delay 
+			Thread.Sleep (n.TypingDelay + 10);
+			Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a'));
+			Assert.Equal ("a", n.SearchString);
+
+			Thread.Sleep (n.TypingDelay + 10);
+			Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$'));
+			Assert.Equal ("$", n.SearchString);
+
+			Thread.Sleep (n.TypingDelay + 10);
+			Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, '$'));
+			Assert.Equal ("$", n.SearchString);
+
+			Thread.Sleep (n.TypingDelay + 10);
+			Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, '$'));
+			Assert.Equal ("$", n.SearchString);
+
+			Thread.Sleep (n.TypingDelay + 10);
+			Assert.Equal (strings.IndexOf ("$101.10"), current = n.GetNextMatchingItem (current, '$'));
+			Assert.Equal ("$", n.SearchString);
+
+			Thread.Sleep (n.TypingDelay + 10);
+			Assert.Equal (strings.IndexOf ("$101.10"), current = n.GetNextMatchingItem (current, '2')); // Shouldn't move
+			Assert.Equal ("2", n.SearchString);
+		}
+
+		[Fact]
+		public void MutliKeySearchPlusWrongKeyStays ()
+		{
+			var strings = new string []{
+				"a",
+			    "c",
+			    "can",
+			    "candle",
+			    "candy",
+			    "yellow",
+				"zebra"
+			  };
+			int current = 0;
+			var n = new CollectionNavigator (strings);
+
+			// https://github.com/gui-cs/Terminal.Gui/pull/2132#issuecomment-1298425573
+			// One thing that it currently does that is different from Explorer is that as soon as you hit a wrong key then it jumps to that index.
+			// So if you type cand then z it jumps you to something beginning with z. In the same situation Windows Explorer beeps (not the best!)
+			// but remains on candle.
+			// We might be able to update the behaviour so that a 'wrong' keypress (z) within 500ms of a 'right' keypress ("can" + 'd') is
+			// simply ignored (possibly ending the search process though). That would give a short delay for user to realise the thing
+			// they typed doesn't exist and then start a new search (which would be possible 500ms after the last 'good' keypress).
+			// This would only apply for 2+ character searches where theres been a successful 2+ character match right before.
+
+			Assert.Equal (strings.IndexOf ("a"), current = n.GetNextMatchingItem (current, 'a'));
+			Assert.Equal ("a", n.SearchString);
+			Assert.Equal (strings.IndexOf ("c"), current = n.GetNextMatchingItem (current, 'c'));
+			Assert.Equal ("c", n.SearchString);
+			Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'a'));
+			Assert.Equal ("ca", n.SearchString);
+			Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'n'));
+			Assert.Equal ("can", n.SearchString);
+			Assert.Equal (strings.IndexOf ("candle"), current = n.GetNextMatchingItem (current, 'd'));
+			Assert.Equal ("cand", n.SearchString);
+
+			// Same as above, but with a 'wrong' key (z)
+			Thread.Sleep (n.TypingDelay + 10);
+			Assert.Equal (strings.IndexOf ("a"), current = n.GetNextMatchingItem (current, 'a'));
+			Assert.Equal ("a", n.SearchString);
+			Assert.Equal (strings.IndexOf ("c"), current = n.GetNextMatchingItem (current, 'c'));
+			Assert.Equal ("c", n.SearchString);
+			Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'a'));
+			Assert.Equal ("ca", n.SearchString);
+			Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'n'));
+			Assert.Equal ("can", n.SearchString);
+			Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'z')); // Shouldn't move
+			Assert.Equal ("can", n.SearchString); // Shouldn't change
+		}
+
+		[Fact]
+		public void MinimizeMovement_False_ShouldMoveIfMultipleMatches ()
+		{
+			var strings = new string [] {
+				"$$",
+				"$100.00",
+				"$101.00",
+				"$101.10",
+				"$200.00",
+				"appricot",
+				"c",
+				"car",
+				"cart",
+			};
+			int current = 0;
+			var n = new CollectionNavigator (strings);
+			Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$", false));
+			Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$", false));
+			Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$", false)); // back to top
+			Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$", false));
+			Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, "$", false));
+			Assert.Equal (strings.IndexOf ("$101.10"), current = n.GetNextMatchingItem (current, "$", false));
+			Assert.Equal (strings.IndexOf ("$200.00"), current = n.GetNextMatchingItem (current, "$", false));
+
+			Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$", false)); // back to top
+			Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, "a", false));
+			Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$", false)); // back to top
+
+			Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$100.00", false));
+			Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, "$", false));
+			Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, "$101.00", false));
+			Assert.Equal (strings.IndexOf ("$200.00"), current = n.GetNextMatchingItem (current, "$2", false));
+
+			Assert.Equal (strings.IndexOf ("$200.00"), current = n.GetNextMatchingItem (current, "$200.00", false));
+			Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, "$101.00", false));
+			Assert.Equal (strings.IndexOf ("$200.00"), current = n.GetNextMatchingItem (current, "$2", false));
+
+			Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, "$101.00", false));
+			Assert.Equal (strings.IndexOf ("$200.00"), current = n.GetNextMatchingItem (current, "$2", false));
+
+			Assert.Equal (strings.IndexOf ("car"), current = n.GetNextMatchingItem (current, "car", false));
+			Assert.Equal (strings.IndexOf ("cart"), current = n.GetNextMatchingItem (current, "car", false));
+
+			Assert.Equal (-1, current = n.GetNextMatchingItem (current, "x", false));
+		}
+
+		[Fact]
+		public void  MinimizeMovement_True_ShouldStayOnCurrentIfMultipleMatches ()
+		{
+			var strings = new string [] {
+				"$$",
+				"$100.00",
+				"$101.00",
+				"$101.10",
+				"$200.00",
+				"appricot",
+				"c",
+				"car",
+				"cart",
+			};
+			int current = 0;
+			var n = new CollectionNavigator (strings);
+			Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$", true));
+			Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$", true));
+			Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$", true)); // back to top
+			Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$1", true));
+			Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$", true));
+			Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$", true));
+
+			Assert.Equal (strings.IndexOf ("car"), current = n.GetNextMatchingItem (current, "car", true));
+			Assert.Equal (strings.IndexOf ("car"), current = n.GetNextMatchingItem (current, "car", true));
+
+			Assert.Equal (-1, current = n.GetNextMatchingItem (current, "x", true));
+		}
+	}
+}

+ 1 - 1
UnitTests/ListViewTests.cs

@@ -151,7 +151,7 @@ namespace Terminal.Gui.Views {
 
 			public IList ToList ()
 			{
-				throw new NotImplementedException ();
+				return new List<string> () { "One", "Two", "Three" };
 			}
 		}
 

+ 48 - 0
UnitTests/ResponderTests.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Linq;
 using Terminal.Gui;
 using Xunit;
+using static Terminal.Gui.Core.ViewTests;
 
 // Alias Console to MockConsole so we don't accidentally use Console
 using Console = Terminal.Gui.FakeConsole;
@@ -44,5 +45,52 @@ namespace Terminal.Gui.Core {
 		{
 
 		}
+		
+		public class DerivedView : View {
+			public DerivedView ()
+			{
+			}
+
+			public override bool OnKeyDown (KeyEvent keyEvent)
+			{
+				return true;
+			}
+		}
+
+		[Fact]
+		public void IsOverridden_False_IfNotOverridden ()
+		{
+			// MouseEvent IS defined on Responder but NOT overridden
+			Assert.False (Responder.IsOverridden (new Responder () { }, "MouseEvent"));
+
+			// MouseEvent is defined on Responder and NOT overrident on View
+			Assert.False (Responder.IsOverridden (new View () { Text = "View does not override MouseEvent" }, "MouseEvent"));
+			Assert.False (Responder.IsOverridden (new DerivedView () { Text = "DerivedView does not override MouseEvent" }, "MouseEvent"));
+
+			// MouseEvent is NOT defined on DerivedView 
+			Assert.False (Responder.IsOverridden (new DerivedView () { Text = "DerivedView does not override MouseEvent" }, "MouseEvent"));
+
+			// OnKeyDown is defined on View and NOT overrident on Button
+			Assert.False (Responder.IsOverridden (new Button () { Text = "Button does not override OnKeyDown" }, "OnKeyDown"));
+		}
+
+		[Fact]
+		public void IsOverridden_True_IfOverridden ()
+		{
+			// MouseEvent is defined on Responder IS overriden on ScrollBarView (but not View)
+			Assert.True (Responder.IsOverridden (new ScrollBarView () { Text = "ScrollBarView overrides MouseEvent" }, "MouseEvent"));
+
+			// OnKeyDown is defined on View
+			Assert.True (Responder.IsOverridden (new View () { Text = "View overrides OnKeyDown" }, "OnKeyDown"));
+
+			// OnKeyDown is defined on DerivedView
+			Assert.True (Responder.IsOverridden (new DerivedView () { Text = "DerivedView overrides OnKeyDown" }, "OnKeyDown"));
+			
+			// ScrollBarView overrides both MouseEvent (from Responder) and Redraw (from View)
+			Assert.True (Responder.IsOverridden (new ScrollBarView () { Text = "ScrollBarView overrides MouseEvent" }, "MouseEvent"));
+			Assert.True (Responder.IsOverridden (new ScrollBarView () { Text = "ScrollBarView overrides Redraw" }, "Redraw"));
+
+			Assert.True (Responder.IsOverridden (new Button () { Text = "Button overrides MouseEvent" }, "MouseEvent"));
+		}
 	}
 }