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

Near final fixes? Refactored/renamed stuff

Charlie Kindel 2 жил өмнө
parent
commit
60d116617a

+ 127 - 42
Terminal.Gui/Core/SearchCollectionNavigator.cs

@@ -4,25 +4,100 @@ using System.Linq;
 
 namespace Terminal.Gui {
 	/// <summary>
-	/// Changes the index in a collection based on keys pressed
-	/// and the current state
+	/// 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 (250ms), the search string is cleared.
+	/// </para>
 	/// </summary>
-	class SearchCollectionNavigator {
-		string state = "";
-		DateTime lastKeystroke = DateTime.MinValue;
-		const int TypingDelay = 250;
+	public class SearchCollectionNavigator {
+		/// <summary>
+		/// Constructs a new SearchCollectionNavigator.
+		/// </summary>
+		public SearchCollectionNavigator () { }
+
+		/// <summary>
+		/// Constructs a new SearchCollectionNavigator for the given collection.
+		/// </summary>
+		/// <param name="collection"></param>
+		public SearchCollectionNavigator (IEnumerable<object> collection) => Collection = collection;
+
+		DateTime lastKeystroke = DateTime.Now;
+		internal int TypingDelay { get; set; } = 250;
+
+		/// <summary>
+		/// The compararer function to use when searching the collection.
+		/// </summary>
 		public StringComparer Comparer { get; set; } = StringComparer.InvariantCultureIgnoreCase;
-		private IEnumerable<object> Collection { get => _collection; set => _collection = value; }
 
-		private IEnumerable<object> _collection;
+		/// <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="SearchCollectionNavigator.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;
+			}
+		}
 
-		public SearchCollectionNavigator (IEnumerable<object> collection) { _collection = collection; }
+		/// <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 a 250ms delay. Useful for debugging.
+		/// </summary>
+		public string SearchString {
+			get => _searchString;
+			private set {
+				_searchString = value;
+				OnSearchStringChanged (new KeystrokeNavigatorEventArgs (value));
+			}
+		}
 
-		public int CalculateNewIndex (IEnumerable<object> collection, int currentIndex, char keyStruck)
+		/// <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)
 		{
-			// if user presses a key
-			if (!char.IsControl(keyStruck)) {//char.IsLetterOrDigit (keyStruck) || char.IsPunctuation (keyStruck) || char.IsSymbol(keyStruck)) {
+			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"
@@ -31,40 +106,39 @@ namespace Terminal.Gui {
 				string candidateState = "";
 
 				// is it a second or third (etc) keystroke within a short time
-				if (state.Length > 0 && DateTime.Now - lastKeystroke < TimeSpan.FromMilliseconds (TypingDelay)) {
+				if (SearchString.Length > 0 && DateTime.Now - lastKeystroke < TimeSpan.FromMilliseconds (TypingDelay)) {
 					// "dd" is a candidate
-					candidateState = state + keyStruck;
+					candidateState = SearchString + keyStruck;
 				} else {
 					// its a fresh keystroke after some time
 					// or its first ever key press
-					state = new string (keyStruck, 1);
+					SearchString = new string (keyStruck, 1);
 				}
 
-				var idxCandidate = GetNextIndexMatching (collection, currentIndex, candidateState,
+				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 state is accepted
 					lastKeystroke = DateTime.Now;
-					state = candidateState;
+					SearchString = candidateState;
 					return idxCandidate;
 				}
 
-
-				// nothing matches "dd" so discard it as a candidate
-				// and just cycle "d" instead
+				//// nothing matches "dd" so discard it as a candidate
+				//// and just cycle "d" instead
 				lastKeystroke = DateTime.Now;
-				idxCandidate = GetNextIndexMatching (collection, currentIndex, state);
+				idxCandidate = GetNextMatchingItem (currentIndex, candidateState);
 
 				// if no changes to current state manifested
 				if (idxCandidate == currentIndex || idxCandidate == -1) {
 					// clear history and treat as a fresh letter
 					ClearState ();
-
+					
 					// match on the fresh letter alone
-					state = new string (keyStruck, 1);
-					idxCandidate = GetNextIndexMatching (collection, currentIndex, state);
+					SearchString = new string (keyStruck, 1);
+					idxCandidate = GetNextMatchingItem (currentIndex, SearchString);
 					return idxCandidate == -1 ? currentIndex : idxCandidate;
 				}
 
@@ -72,28 +146,35 @@ namespace Terminal.Gui {
 				return idxCandidate;
 
 			} else {
-				// clear state because keypress was non letter
+				// clear state because keypress was a control char
 				ClearState ();
 
-				// no change in index for non letter keystrokes
-				return currentIndex;
+				// control char indicates no selection
+				return -1;
 			}
 		}
 
-		public int CalculateNewIndex (int currentIndex, char keyStruck)
-		{
-			return CalculateNewIndex (Collection, currentIndex, keyStruck);
-		}
-
-		private int GetNextIndexMatching (IEnumerable<object> collection, int currentIndex, string search, bool preferNotToMoveToNewIndexes = false)
+		/// <summary>
+		/// Gets the index of the next item in the collection that matches the current <see cref="SearchString"/> 
+		/// </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>
+		/// </param>
+		/// <returns></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)
+			int [] matchingIndexes = Collection.Select ((item, idx) => (item, idx))
+				  .Where (k => k.item?.ToString ().StartsWith (search, StringComparison.InvariantCultureIgnoreCase) ?? false)
 				  .Select (k => k.idx)
 				  .ToArray ();
 
@@ -109,7 +190,7 @@ namespace Terminal.Gui {
 				} else {
 
 					// the current index is part of the matching collection
-					if (preferNotToMoveToNewIndexes) {
+					if (minimizeMovement) {
 						// if we would rather not jump around (e.g. user is typing lots of text to get this match)
 						return matchingIndexes [currentlySelected];
 					}
@@ -123,25 +204,29 @@ namespace Terminal.Gui {
 			return -1;
 		}
 
-		private void ClearState ()
+		private void AssertCollectionIsNotNull ()
 		{
-			state = "";
-			lastKeystroke = DateTime.MinValue;
+			if (Collection == null) {
+				throw new InvalidOperationException ("Collection is null");
+			}
+		}
 
+		private void ClearState ()
+		{
+			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
+		/// class for search filtering.
 		/// </summary>
 		/// <param name="kb"></param>
 		/// <returns></returns>
 		public static bool IsCompatibleKey (KeyEvent kb)
 		{
-			// For some reason, at least on Windows/Windows Terminal, `$` is coming through with `IsAlt == true`
 			return !kb.IsAlt && !kb.IsCapslock && !kb.IsCtrl && !kb.IsScrolllock && !kb.IsNumlock;
-			//return !kb.IsCapslock && !kb.IsCtrl && !kb.IsScrolllock && !kb.IsNumlock;
 		}
 	}
 }

+ 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) {

+ 12 - 28
Terminal.Gui/Views/ListView.cs

@@ -59,21 +59,6 @@ namespace Terminal.Gui {
 		IList ToList ();
 	}
 
-	/// <summary>
-	/// Implement <see cref="IListDataSourceSearchable"/> to provide custom rendering for a <see cref="ListView"/> that 
-	/// supports searching for items.
-	/// </summary>
-	public interface IListDataSourceSearchable : IListDataSource {
-			/// <summary>
-			/// Finds the first item that starts with the specified search string. Used by the default implementation
-			/// to support typing the first characters of an item to find it and move the selection to i.
-			/// </summary>
-			/// <param name="search">Text to search for.</param>
-			/// <returns>The index of the first <see cref="ListView"/> item that starts with <paramref name="search"/>. 
-			/// Returns <see langword="-1"/> if <paramref name="search"/> was not found.</returns>
-			int StartsWith (string search);
-	}
-
 	/// <summary>
 	/// ListView <see cref="View"/> renders a scrollable list of data where each item can be activated to perform an action.
 	/// </summary>
@@ -87,7 +72,7 @@ namespace Terminal.Gui {
 	///   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 <see cref="IListDataSource"/>
-	///   or <see cref="IListDataSourceSearchable"/> can be provided giving full control of what is rendered.
+	///   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.
@@ -105,8 +90,7 @@ namespace Terminal.Gui {
 	///   marking style set <see cref="AllowsMarking"/> to false and implement custom rendering.
 	/// </para>
 	/// <para>
-	///   By default or if <see cref="Source"/> is set to an object that implements
-	///   <see cref="IListDataSourceSearchable"/>, searching the ListView with the keyboard is supported. Users type the
+	///   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>
@@ -126,7 +110,7 @@ namespace Terminal.Gui {
 			get => source;
 			set {
 				source = value;
-				navigator = null;
+				Navigator.Collection = source?.ToList ()?.Cast<object> ();
 				top = 0;
 				selected = 0;
 				lastSelectedItem = -1;
@@ -423,7 +407,10 @@ namespace Terminal.Gui {
 		/// </summary>
 		public event Action<ListViewRowEventArgs> RowRender;
 
-		private SearchCollectionNavigator navigator;
+		/// <summary>
+		/// Gets the <see cref="SearchCollectionNavigator"/> that is used to navigate the <see cref="ListView"/> when searching.
+		/// </summary>
+		public SearchCollectionNavigator Navigator { get; private set; } = new SearchCollectionNavigator ();
 
 		///<inheritdoc/>
 		public override bool ProcessKey (KeyEvent kb)
@@ -436,15 +423,12 @@ namespace Terminal.Gui {
 			if (result != null) {
 				return (bool)result;
 			}
-
+			
 			// Enable user to find & select an item by typing text
 			if (SearchCollectionNavigator.IsCompatibleKey(kb)) {
-				if (navigator == null) {
-					navigator = new SearchCollectionNavigator (source.ToList ().Cast<object> ());
-				}
-				var newItem = navigator.CalculateNewIndex (SelectedItem, (char)kb.KeyValue);
-				if (newItem != SelectedItem) {
-					SelectedItem = newItem;
+				var newItem = Navigator?.GetNextMatchingItem (SelectedItem, (char)kb.KeyValue);
+				if (newItem is int && newItem != -1) {
+					SelectedItem = (int)newItem;
 					EnsuresVisibilitySelectedItem ();
 					SetNeedsDisplay ();
 					return true;
@@ -829,7 +813,7 @@ namespace Terminal.Gui {
 	}
 
 	/// <inheritdoc/>
-	public class ListWrapper : IListDataSourceSearchable {
+	public class ListWrapper : IListDataSource {
 		IList src;
 		BitArray marks;
 		int count, len;

+ 131 - 156
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,24 +201,23 @@ 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 () ?? "";
 
 		CursorVisibility desiredCursorVisibility = CursorVisibility.Invisible;
-		private SearchCollectionNavigator searchCollectionNavigator;
 
 		/// <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 {
@@ -242,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 ()
 		{
@@ -301,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 ()
@@ -318,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)
@@ -330,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 ()
 		{
@@ -347,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)
 		{
@@ -363,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;
@@ -384,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);
@@ -405,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 ()
 		{
@@ -418,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)
 		{
@@ -434,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)
 		{
@@ -474,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 ();
@@ -502,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)
 		{
@@ -537,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>
@@ -554,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
+			Navigator.Collection = cachedLineMap.Select (b => AspectGetter (b.Model)).ToArray ();
+			return cachedLineMap;
 		}
 
 		private IEnumerable<Branch<T>> AddToLineMap (Branch<T> currentBranch)
@@ -562,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;
@@ -571,6 +564,12 @@ namespace Terminal.Gui {
 			}
 		}
 
+		/// <summary>
+		/// Gets the <see cref="SearchCollectionNavigator"/> that is used to navigate the <see cref="TreeView"/> 
+		/// when searching with the keyboard.
+		/// </summary>
+		public SearchCollectionNavigator Navigator { get; private set; } = new SearchCollectionNavigator ();
+
 		/// <inheritdoc/>
 		public override bool ProcessKey (KeyEvent keyEvent)
 		{
@@ -579,7 +578,6 @@ namespace Terminal.Gui {
 			}
 
 			try {
-
 				// First of all deal with any registered keybindings
 				var result = InvokeKeybindings (keyEvent);
 				if (result != null) {
@@ -588,35 +586,24 @@ namespace Terminal.Gui {
 
 				// If not a keybinding, is the key a searchable key press?
 				if (SearchCollectionNavigator.IsCompatibleKey (keyEvent) && AllowLetterBasedNavigation) {
-
 					IReadOnlyCollection<Branch<T>> map;
 
-					// If there has been a call to InvalidateMap since the last time we allocated a 
-					// SearchCollectionNavigator then we need a new one to reflect the new exposed
-					// tree state
-					if (cachedLineMap == null || searchCollectionNavigator == null) {
-						map = BuildLineMap ();
-						searchCollectionNavigator = new SearchCollectionNavigator (map.Select (b => AspectGetter (b.Model)).ToArray ());
-					}
-					else {
-						// we still need the map, handily its the cached one which means super fast access
-						map = BuildLineMap ();
-					}
-					
+					// 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 = searchCollectionNavigator.CalculateNewIndex (current, (char)keyEvent.KeyValue);
+					var newIndex = Navigator?.GetNextMatchingItem (current, (char)keyEvent.KeyValue);
 
-					if (newIndex != current) {
-						SelectedObject = map.ElementAt (newIndex).Model;
+					if (newIndex is int && newIndex != -1) {
+						SelectedObject = map.ElementAt ((int)newIndex).Model;
 						EnsureVisible (selectedObject);
 						SetNeedsDisplay ();
 						return true;
 					}
 				}
-
 			} finally {
-
 				PositionCursor ();
 			}
 
@@ -627,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 ()
 		{
@@ -663,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
@@ -680,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)
 		{
@@ -690,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)
 		{
@@ -698,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 ()
 		{
@@ -707,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 ()
 		{
@@ -716,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)
@@ -725,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;
@@ -758,7 +745,6 @@ namespace Terminal.Gui {
 				SetFocus ();
 			}
 
-
 			if (me.Flags == MouseFlags.WheeledDown) {
 
 				ScrollDown ();
@@ -814,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 ();
@@ -844,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 ();
@@ -870,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 ()
 		{
@@ -891,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)
 		{
@@ -919,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 ()
 		{
@@ -931,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 ()
 		{
@@ -944,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)
@@ -960,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
@@ -983,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 {
@@ -1007,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 ()
 		{
@@ -1054,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 ()
 		{
@@ -1088,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)
@@ -1132,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)
 		{
@@ -1159,7 +1138,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Expands the currently <see cref="SelectedObject"/>
+		/// Expands the currently <see cref="SelectedObject"/>.
 		/// </summary>
 		public void Expand ()
 		{
@@ -1168,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) {
@@ -1183,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) {
@@ -1198,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 ()
 		{
@@ -1211,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>
@@ -1222,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>
@@ -1240,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 ()
 		{
@@ -1272,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
@@ -1317,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));
@@ -1330,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>
@@ -1365,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 ()
 		{
@@ -1387,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)
@@ -1431,5 +1407,4 @@ namespace Terminal.Gui {
 			return included.Contains (model);
 		}
 	}
-
 }

+ 4 - 0
UICatalog/Properties/launchSettings.json

@@ -44,6 +44,10 @@
     "WSL": {
       "commandName": "WSL2",
       "distributionName": ""
+    },
+    "SearchCollectionNavigatorTester": {
+      "commandName": "Project",
+      "commandLineArgs": "\"Search Collection Nav\""
     }
   }
 }

+ 18 - 8
UICatalog/Scenarios/SearchCollectionNavigatorTester.cs

@@ -11,6 +11,7 @@ namespace UICatalog.Scenarios {
 	[ScenarioCategory ("Controls"), ScenarioCategory ("TreeView")]
 	[ScenarioCategory ("Controls"), ScenarioCategory ("Text")]
 	public class SearchCollectionNavigatorTester : Scenario {
+
 		// Don't create a Window, just return the top-level view
 		public override void Init (Toplevel top, ColorScheme colorScheme)
 		{
@@ -94,7 +95,7 @@ namespace UICatalog.Scenarios {
 					null,
 					new MenuItem ("_Quit", "", () => Quit(), null, null, Key.Q | Key.CtrlMask),
 				}),
-				new MenuBarItem("_Quit", "CTRL-Q", () => Quit())
+				new MenuBarItem("_Quit", "CTRL-Q", () => Quit()),
 			});
 
 			Top.Add (menu);
@@ -109,7 +110,6 @@ namespace UICatalog.Scenarios {
 			};
 			Top.Add (vsep);
 			CreateTreeView ();
-
 		}
 
 		ListView _listView = null;
@@ -128,7 +128,7 @@ namespace UICatalog.Scenarios {
 
 			_listView = new ListView () {
 				X = 0,
-				Y = Pos.Bottom(label), 
+				Y = Pos.Bottom (label),
 				Width = Dim.Percent (50) - 1,
 				Height = Dim.Fill (),
 				AllowsMarking = false,
@@ -136,8 +136,12 @@ namespace UICatalog.Scenarios {
 				ColorScheme = Colors.TopLevel
 			};
 			Top.Add (_listView);
-			
+
 			_listView.SetSource (_items);
+
+			_listView.Navigator.SearchStringChanged += (state) => {
+				label.Text = $"ListView: {state.SearchString}";
+			};
 		}
 
 		TreeView _treeView = null;
@@ -147,7 +151,7 @@ namespace UICatalog.Scenarios {
 			var label = new Label () {
 				Text = "TreeView",
 				TextAlignment = TextAlignment.Centered,
-				X = Pos.Right(_listView) + 2,
+				X = Pos.Right (_listView) + 2,
 				Y = 1, // for menu
 				Width = Dim.Percent (50),
 				Height = 1,
@@ -162,15 +166,21 @@ namespace UICatalog.Scenarios {
 				ColorScheme = Colors.TopLevel
 			};
 			Top.Add (_treeView);
-			
+
 			var root = new TreeNode ("Alpha examples");
-			//root.Children = items.Where (i => char.IsLetterOrDigit (i [0])).Select (i => new TreeNode (i)).Cast<ITreeNode>().ToList ();
-			//_treeView.AddObject (root);
+			root.Children = _items.Where (i => char.IsLetterOrDigit (i [0])).Select (i => new TreeNode (i)).Cast<ITreeNode> ().ToList ();
+			_treeView.AddObject (root);
 			root = new TreeNode ("Non-Alpha 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.Navigator.SearchStringChanged += (state) => {
+				label.Text = $"TreeView: {state.SearchString}";
+			};
 		}
+
 		private void Quit ()
 		{
 			Application.RequestStop ();

+ 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" };
 			}
 		}
 

+ 259 - 67
UnitTests/SearchCollectionNavigatorTests.cs

@@ -1,142 +1,334 @@
-using Terminal.Gui;
+using System.Threading;
 using Xunit;
 
 namespace Terminal.Gui.Core {
 	public class SearchCollectionNavigatorTests {
 		static string [] simpleStrings = new string []{
-    "appricot", // 0
-    "arm",      // 1
-    "bat",      // 2
-    "batman",   // 3
-    "candle"    // 4
-  };
+		    "appricot", // 0
+		    "arm",      // 1
+		    "bat",      // 2
+		    "batman",   // 3
+		    "candle"    // 4
+		  };
+
 		[Fact]
-		public void TestSearchCollectionNavigator_ShouldAcceptNegativeOne ()
+		public void ShouldAcceptNegativeOne ()
 		{
 			var n = new SearchCollectionNavigator (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.CalculateNewIndex (-1, 'b'));
+			Assert.Equal (2, n.GetNextMatchingItem (-1, 'b'));
 		}
 		[Fact]
-		public void TestSearchCollectionNavigator_OutOfBoundsShouldBeIgnored()
+		public void OutOfBoundsShouldBeIgnored ()
 		{
 			var n = new SearchCollectionNavigator (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.CalculateNewIndex (500, 'b'));
+			Assert.Equal (2, n.GetNextMatchingItem (500, 'b'));
 		}
 
 		[Fact]
-		public void TestSearchCollectionNavigator_Cycling ()
+		public void Cycling ()
 		{
 			var n = new SearchCollectionNavigator (simpleStrings);
-			Assert.Equal (2, n.CalculateNewIndex ( 0, 'b'));
-			Assert.Equal (3, n.CalculateNewIndex ( 2, 'b'));
+			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.CalculateNewIndex ( 4, 'b'));
+			Assert.Equal (2, n.GetNextMatchingItem (4, 'b'));
 		}
 
 
 		[Fact]
-		public void TestSearchCollectionNavigator_ToSearchText ()
+		public void ToSearchText ()
 		{
 			var strings = new string []{
-    "appricot",
-    "arm",
-    "bat",
-    "batman",
-    "bbfish",
-    "candle"
-  };
+			    "appricot",
+			    "arm",
+			    "bat",
+			    "batman",
+			    "bbfish",
+			    "candle"
+			  };
 
+			int current = 0;
 			var n = new SearchCollectionNavigator (strings);
-			Assert.Equal (2, n.CalculateNewIndex (0, 'b'));
-			Assert.Equal (4, n.CalculateNewIndex (2, 'b'));
+			Assert.Equal (2, current = n.GetNextMatchingItem (current, 'b')); // match bat
+			Assert.Equal (4, current = n.GetNextMatchingItem (current, 'b')); // match bbfish
 
 			// another 'b' means searching for "bbb" which does not exist
 			// so we go back to looking for "b" as a fresh key strike
-			Assert.Equal (4, n.CalculateNewIndex (2, 'b'));
+			Assert.Equal (2, current = n.GetNextMatchingItem (current, 'b')); // match bat
 		}
 
 		[Fact]
-		public void TestSearchCollectionNavigator_FullText ()
+		public void FullText ()
 		{
 			var strings = new string []{
-    "appricot",
-    "arm",
-    "ta",
-    "target",
-    "text",
-    "egg",
-    "candle"
-  };
+			    "appricot",
+			    "arm",
+			    "ta",
+			    "target",
+			    "text",
+			    "egg",
+			    "candle"
+			  };
 
 			var n = new SearchCollectionNavigator (strings);
-			Assert.Equal (2, n.CalculateNewIndex (0, 't'));
+			Assert.Equal (2, n.GetNextMatchingItem (0, 't'));
 
 			// should match "te" in "text"
-			Assert.Equal (4, n.CalculateNewIndex (2, 'e'));
+			Assert.Equal (4, n.GetNextMatchingItem (2, 'e'));
 
 			// still matches text
-			Assert.Equal (4, n.CalculateNewIndex (4, 'x'));
+			Assert.Equal (4, n.GetNextMatchingItem (4, 'x'));
 
 			// nothing starts texa so it jumps to a for appricot
-			Assert.Equal (0, n.CalculateNewIndex (4, 'a'));
+			Assert.Equal (0, n.GetNextMatchingItem (4, 'a'));
 		}
 
 		[Fact]
-		public void TestSearchCollectionNavigator_Unicode ()
+		public void Unicode ()
 		{
 			var strings = new string []{
-    "appricot",
-    "arm",
-    "ta",
-    "丗丙业丞",
-    "丗丙丛",
-    "text",
-    "egg",
-    "candle"
-  };
+			    "appricot",
+			    "arm",
+			    "ta",
+			    "丗丙业丞",
+			    "丗丙丛",
+			    "text",
+			    "egg",
+			    "candle"
+			  };
 
 			var n = new SearchCollectionNavigator (strings);
-			Assert.Equal (3, n.CalculateNewIndex (0, '丗'));
+			Assert.Equal (3, n.GetNextMatchingItem (0, '丗'));
 
 			// 丗丙业丞 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 (3, n.CalculateNewIndex (3, '丙'));
+			Assert.Equal (3, n.GetNextMatchingItem (3, '丙'));
 
 			// No longer matches 丗丙业丞 and now only matches 丗丙丛
 			// so we should move to the new match
-			Assert.Equal (4, n.CalculateNewIndex (3, '丛'));
+			Assert.Equal (4, n.GetNextMatchingItem (3, '丛'));
 
 			// nothing starts "丗丙丛a" so it jumps to a for appricot
-			Assert.Equal (0, n.CalculateNewIndex (4, 'a'));
+			Assert.Equal (0, n.GetNextMatchingItem (4, 'a'));
+		}
+
+		[Fact]
+		public void AtSymbol ()
+		{
+			var strings = new string []{
+			    "appricot",
+			    "arm",
+			    "ta",
+			    "@bob",
+			    "@bb",
+			    "text",
+			    "egg",
+			    "candle"
+			  };
+
+			var n = new SearchCollectionNavigator (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 SearchCollectionNavigator (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
+
+			// another 'b' means searching for "bates b" which does not exist
+			// so we go back to looking for "b" as a fresh key strike
+			Assert.Equal (strings.IndexOf<string> ("bat"), current = n.GetNextMatchingItem (current, 'b')); // match bat
+		}
+
+		[Fact]
+		public void Symbols ()
+		{
+			var strings = new string []{
+			    "$$",
+			    "$100.00",
+			    "$101.00",
+			    "$101.10",
+			    "$200.00",
+			    "appricot"
+			  };
+			int current = 0;
+			var n = new SearchCollectionNavigator (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);
+
+			Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a'));
+			Assert.Equal ("a", n.SearchString);
+
+			// another '$' means searching for "$" again
+			Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$'));
+			Assert.Equal ("$", n.SearchString);
+
+			Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$'));
+			Assert.Equal ("$$", n.SearchString);
+
 		}
 
 		[Fact]
-		public void TestSearchCollectionNavigator_AtSymbol ()
+		public void Delay ()
 		{
 			var strings = new string []{
-    "appricot",
-    "arm",
-    "ta",
-    "@bob",
-    "@bb",
-    "text",
-    "egg",
-    "candle"
-  };
+			    "$$",
+			    "$100.00",
+			    "$101.00",
+			    "$101.10",
+			    "$200.00",
+			    "appricot"
+			  };
+			int current = 0;
+			var n = new SearchCollectionNavigator (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 
+			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 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 SearchCollectionNavigator (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 SearchCollectionNavigator (strings);
-			Assert.Equal (3, n.CalculateNewIndex (0, '@'));
-			Assert.Equal (3, n.CalculateNewIndex (3, 'b'));
-			Assert.Equal (4, n.CalculateNewIndex (3, 'b'));
+			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));
 		}
 	}
 }