From 60d116617ae4fe1895343ff073428202dc3638da Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Mon, 31 Oct 2022 21:23:26 -0600 Subject: [PATCH] Near final fixes? Refactored/renamed stuff --- .../Core/SearchCollectionNavigator.cs | 169 ++++++--- Terminal.Gui/Core/Trees/Branch.cs | 6 +- Terminal.Gui/Views/ListView.cs | 40 +-- Terminal.Gui/Views/TreeView.cs | 287 +++++++-------- UICatalog/Properties/launchSettings.json | 4 + .../SearchCollectionNavigatorTester.cs | 26 +- UnitTests/ListViewTests.cs | 2 +- UnitTests/SearchCollectionNavigatorTests.cs | 326 ++++++++++++++---- 8 files changed, 555 insertions(+), 305 deletions(-) diff --git a/Terminal.Gui/Core/SearchCollectionNavigator.cs b/Terminal.Gui/Core/SearchCollectionNavigator.cs index 360aab5cf..6c02b9664 100644 --- a/Terminal.Gui/Core/SearchCollectionNavigator.cs +++ b/Terminal.Gui/Core/SearchCollectionNavigator.cs @@ -4,25 +4,100 @@ using System.Linq; namespace Terminal.Gui { /// - /// 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 is used to find the next item in the collection that matches the search string + /// when is called. + /// + /// 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. + /// + /// + /// If the user pauses keystrokes for a short time (250ms), the search string is cleared. + /// /// - class SearchCollectionNavigator { - string state = ""; - DateTime lastKeystroke = DateTime.MinValue; - const int TypingDelay = 250; + public class SearchCollectionNavigator { + /// + /// Constructs a new SearchCollectionNavigator. + /// + public SearchCollectionNavigator () { } + + /// + /// Constructs a new SearchCollectionNavigator for the given collection. + /// + /// + public SearchCollectionNavigator (IEnumerable collection) => Collection = collection; + + DateTime lastKeystroke = DateTime.Now; + internal int TypingDelay { get; set; } = 250; + + /// + /// The compararer function to use when searching the collection. + /// public StringComparer Comparer { get; set; } = StringComparer.InvariantCultureIgnoreCase; - private IEnumerable Collection { get => _collection; set => _collection = value; } - private IEnumerable _collection; + /// + /// The collection of objects to search. is used to search the collection. + /// + public IEnumerable Collection { get; set; } - public SearchCollectionNavigator (IEnumerable collection) { _collection = collection; } + /// + /// Event arguments for the event. + /// + public class KeystrokeNavigatorEventArgs { + /// + /// he current . + /// + public string SearchString { get; } + /// + /// Initializes a new instance of + /// + /// The current . + public KeystrokeNavigatorEventArgs (string searchString) + { + SearchString = searchString; + } + } - public int CalculateNewIndex (IEnumerable collection, int currentIndex, char keyStruck) + /// + /// This event is invoked when changes. Useful for debugging. + /// + public event Action SearchStringChanged; + + private string _searchString = ""; + /// + /// 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. + /// + public string SearchString { + get => _searchString; + private set { + _searchString = value; + OnSearchStringChanged (new KeystrokeNavigatorEventArgs (value)); + } + } + + /// + /// Invoked when the changes. Useful for debugging. Invokes the event. + /// + /// + 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); + } + + /// + /// Gets the index of the next item in the collection that matches the current plus the provided character (typically + /// from a key press). + /// + /// The index in the collection to start the search from. + /// The character of the key the user pressed. + /// The index of the item that matches what the user has typed. + /// Returns if no item in the collection matched. + 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 collection, int currentIndex, string search, bool preferNotToMoveToNewIndexes = false) + /// + /// Gets the index of the next item in the collection that matches the current + /// + /// The index in the collection to start the search from. + /// The search string to use. + /// Set to to stop the search on the first match + /// if there are multiple matches for . + /// e.g. "ca" + 'r' should stay on "car" and not jump to "cart". If (the default), + /// the next matching item will be returned, even if it is above in the collection. + /// + /// + 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 AssertCollectionIsNotNull () + { + if (Collection == null) { + throw new InvalidOperationException ("Collection is null"); + } + } + private void ClearState () { - state = ""; - lastKeystroke = DateTime.MinValue; - + SearchString = ""; + lastKeystroke = DateTime.Now; } /// /// Returns true if 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. /// /// /// 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; } } } diff --git a/Terminal.Gui/Core/Trees/Branch.cs b/Terminal.Gui/Core/Trees/Branch.cs index 35a81965a..a6d43cb0b 100644 --- a/Terminal.Gui/Core/Trees/Branch.cs +++ b/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 /// internal void ExpandAll () - { + { Expand (); if (ChildBranches != null) { diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 95a96dce6..388def50a 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -59,21 +59,6 @@ namespace Terminal.Gui { IList ToList (); } - /// - /// Implement to provide custom rendering for a that - /// supports searching for items. - /// - public interface IListDataSourceSearchable : IListDataSource { - /// - /// 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. - /// - /// Text to search for. - /// The index of the first item that starts with . - /// Returns if was not found. - int StartsWith (string search); - } - /// /// ListView renders a scrollable list of data where each item can be activated to perform an action. /// @@ -87,7 +72,7 @@ namespace Terminal.Gui { /// By default uses to render the items of any /// object (e.g. arrays, , /// and other collections). Alternatively, an object that implements - /// or can be provided giving full control of what is rendered. + /// can be provided giving full control of what is rendered. /// /// /// can display any object that implements the interface. @@ -105,8 +90,7 @@ namespace Terminal.Gui { /// marking style set to false and implement custom rendering. /// /// - /// By default or if is set to an object that implements - /// , 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. /// /// @@ -126,7 +110,7 @@ namespace Terminal.Gui { get => source; set { source = value; - navigator = null; + Navigator.Collection = source?.ToList ()?.Cast (); top = 0; selected = 0; lastSelectedItem = -1; @@ -423,7 +407,10 @@ namespace Terminal.Gui { /// public event Action RowRender; - private SearchCollectionNavigator navigator; + /// + /// Gets the that is used to navigate the when searching. + /// + public SearchCollectionNavigator Navigator { get; private set; } = new SearchCollectionNavigator (); /// 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 ()); - } - 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 { } /// - public class ListWrapper : IListDataSourceSearchable { + public class ListWrapper : IListDataSource { IList src; BitArray marks; int count, len; diff --git a/Terminal.Gui/Views/TreeView.cs b/Terminal.Gui/Views/TreeView.cs index c6c2e0038..5ccf8b8c1 100644 --- a/Terminal.Gui/Views/TreeView.cs +++ b/Terminal.Gui/Views/TreeView.cs @@ -1,5 +1,5 @@ // This code is based on http://objectlistview.sourceforge.net (GPLv3 tree/list controls -// by phillip.piper@gmail.com). Phillip has explicitly granted permission for his design +// by phillip.piper@gmail.com). 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 { /// - /// Interface for all non generic members of + /// Interface for all non generic members of . /// /// See TreeView Deep Dive for more information. /// public interface ITreeView { /// - /// Contains options for changing how the tree is rendered + /// Contains options for changing how the tree is rendered. /// TreeStyle Style { get; set; } /// - /// Removes all objects from the tree and clears selection + /// Removes all objects from the tree and clears selection. /// void ClearObjects (); @@ -43,7 +43,7 @@ namespace Terminal.Gui { /// /// Creates a new instance of the tree control with absolute positioning and initialises - /// with default based builder + /// with default based builder. /// public TreeView () { @@ -53,8 +53,8 @@ namespace Terminal.Gui { } /// - /// Hierarchical tree view with expandable branches. Branch objects are dynamically determined - /// when expanded using a user defined + /// Hierarchical tree view with expandable branches. Branch objects are dynamically determined + /// when expanded using a user defined . /// /// See TreeView Deep Dive for more information. /// @@ -64,7 +64,7 @@ namespace Terminal.Gui { /// /// Determines how sub branches of the tree are dynamically built at runtime as the user - /// expands root nodes + /// expands root nodes. /// /// public ITreeBuilder TreeBuilder { get; set; } @@ -74,30 +74,27 @@ namespace Terminal.Gui { /// T selectedObject; - /// - /// Contains options for changing how the tree is rendered + /// Contains options for changing how the tree is rendered. /// public TreeStyle Style { get; set; } = new TreeStyle (); - /// - /// True to allow multiple objects to be selected at once + /// True to allow multiple objects to be selected at once. /// /// public bool MultiSelect { get; set; } = true; - /// /// True makes a letter key press navigate to the next visible branch that begins with - /// that letter/digit + /// that letter/digit. /// /// public bool AllowLetterBasedNavigation { get; set; } = true; /// - /// The currently selected object in the tree. When is true this - /// is the object at which the cursor is at + /// The currently selected object in the tree. When is true this + /// is the object at which the cursor is at. /// public T SelectedObject { get => selectedObject; @@ -111,16 +108,15 @@ namespace Terminal.Gui { } } - /// /// This event is raised when an object is activated e.g. by double clicking or - /// pressing + /// pressing . /// public event Action> ObjectActivated; /// /// Key which when pressed triggers . - /// Defaults to Enter + /// Defaults to Enter. /// public Key ObjectActivationKey { get => objectActivationKey; @@ -140,15 +136,14 @@ namespace Terminal.Gui { /// public MouseFlags? ObjectActivationButton { get; set; } = MouseFlags.Button1DoubleClicked; - /// - /// Delegate for multi colored tree views. Return the to use + /// Delegate for multi colored tree views. Return the to use /// for each passed object or null to use the default. /// public Func ColorGetter { get; set; } /// - /// Secondary selected regions of tree when is true + /// Secondary selected regions of tree when is true. /// private Stack> multiSelectedRegions = new Stack> (); @@ -157,36 +152,35 @@ namespace Terminal.Gui { /// private IReadOnlyCollection> cachedLineMap; - /// /// 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). /// public static ustring NoBuilderError = "ERROR: TreeBuilder Not Set"; private Key objectActivationKey = Key.Enter; /// - /// Called when the changes + /// Called when the changes. /// public event EventHandler> SelectionChanged; /// - /// 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. /// public IEnumerable Objects { get => roots.Keys; } /// - /// Map of root objects to the branches under them. All objects have - /// a even if that branch has no children + /// Map of root objects to the branches under them. All objects have + /// a even if that branch has no children. /// internal Dictionary> roots { get; set; } = new Dictionary> (); /// /// The amount of tree view that has been scrolled off the top of the screen (by the user - /// scrolling down) + /// scrolling down). /// - /// Setting a value of less than 0 will result in a offset of 0. To see changes - /// in the UI call + /// Setting a value of less than 0 will result in a offset of 0. To see changes + /// in the UI call . public int ScrollOffsetVertical { get => scrollOffsetVertical; set { @@ -194,12 +188,11 @@ namespace Terminal.Gui { } } - /// - /// 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). /// - /// Setting a value of less than 0 will result in a offset of 0. To see changes - /// in the UI call + /// Setting a value of less than 0 will result in a offset of 0. To see changes + /// in the UI call . public int ScrollOffsetHorizontal { get => scrollOffsetHorizontal; set { @@ -208,24 +201,23 @@ namespace Terminal.Gui { } /// - /// The current number of rows in the tree (ignoring the controls bounds) + /// The current number of rows in the tree (ignoring the controls bounds). /// public int ContentHeight => BuildLineMap ().Count (); /// - /// Returns the string representation of model objects hosted in the tree. Default - /// implementation is to call + /// Returns the string representation of model objects hosted in the tree. Default + /// implementation is to call . /// /// public AspectGetterDelegate AspectGetter { get; set; } = (o) => o.ToString () ?? ""; CursorVisibility desiredCursorVisibility = CursorVisibility.Invisible; - private SearchCollectionNavigator searchCollectionNavigator; /// /// Get / Set the wished cursor when the tree is focused. /// Only applies when is true. - /// Defaults to + /// Defaults to . /// public CursorVisibility DesiredCursorVisibility { get { @@ -242,9 +234,9 @@ namespace Terminal.Gui { } /// - /// Creates a new tree view with absolute positioning. + /// Creates a new tree view with absolute positioning. /// Use to set set root objects for the tree. - /// Children will not be rendered until you set + /// Children will not be rendered until you set . /// public TreeView () : base () { @@ -301,7 +293,7 @@ namespace Terminal.Gui { /// /// Initialises .Creates a new tree view with absolute - /// positioning. Use to set set root + /// positioning. Use to set set root /// objects for the tree. /// public TreeView (ITreeBuilder builder) : this () @@ -318,7 +310,7 @@ namespace Terminal.Gui { } /// - /// 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. /// /// public void AddObject (T o) @@ -330,9 +322,8 @@ namespace Terminal.Gui { } } - /// - /// Removes all objects from the tree and clears + /// Removes all objects from the tree and clears . /// public void ClearObjects () { @@ -347,7 +338,7 @@ namespace Terminal.Gui { /// Removes the given root object from the tree /// /// If is the currently then the - /// selection is cleared + /// selection is cleared. /// public void Remove (T o) { @@ -363,9 +354,9 @@ namespace Terminal.Gui { } /// - /// 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. /// - /// Objects to add as new root level objects + /// Objects to add as new root level objects..\ public void AddObjects (IEnumerable collection) { bool objectsAdded = false; @@ -384,13 +375,13 @@ namespace Terminal.Gui { } /// - /// Refreshes the state of the object in the tree. This will - /// recompute children, string representation etc + /// Refreshes the state of the object in the tree. This will + /// recompute children, string representation etc. /// /// This has no effect if the object is not exposed in the tree. /// /// True to also refresh all ancestors of the objects branch - /// (starting with the root). False to refresh only the passed node + /// (starting with the root). False to refresh only the passed node. public void RefreshObject (T o, bool startAtTop = false) { var branch = ObjectToBranch (o); @@ -405,7 +396,7 @@ namespace Terminal.Gui { /// /// 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 ) + /// objects have changed (otherwise use ). /// public void RebuildTree () { @@ -418,10 +409,10 @@ namespace Terminal.Gui { } /// - /// 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. /// - /// An object in the tree + /// An object in the tree. /// public IEnumerable GetChildren (T o) { @@ -434,10 +425,10 @@ namespace Terminal.Gui { return branch.ChildBranches?.Values?.Select (b => b.Model)?.ToArray () ?? new T [0]; } /// - /// Returns the parent object of in the tree. Returns null if - /// the object is not exposed in the tree + /// Returns the parent object of in the tree. Returns null if + /// the object is not exposed in the tree. /// - /// An object in the tree + /// An object in the tree. /// public T GetParent (T o) { @@ -474,20 +465,19 @@ namespace Terminal.Gui { Driver.SetAttribute (GetNormalColor ()); Driver.AddStr (new string (' ', bounds.Width)); } - } } /// /// Returns the index of the object if it is currently exposed (it's - /// parent(s) have been expanded). This can be used with - /// and to scroll to a specific object + /// parent(s) have been expanded). This can be used with + /// and to scroll to a specific object. /// /// Uses the Equals method and returns the first index at which the object is found - /// or -1 if it is not found - /// An object that appears in your tree and is currently exposed + /// or -1 if it is not found. + /// An object that appears in your tree and is currently exposed. /// The index the object was found at or -1 if it is not currently revealed or - /// not in the tree at all + /// not in the tree at all. public int GetScrollOffsetOf (T o) { var map = BuildLineMap (); @@ -502,11 +492,11 @@ namespace Terminal.Gui { } /// - /// 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. /// /// True to consider only rows currently visible (based on window - /// bounds and . False to calculate the width of - /// every exposed branch in the tree + /// bounds and . False to calculate the width of + /// every exposed branch in the tree. /// public int GetContentWidth (bool visible) { @@ -537,7 +527,7 @@ namespace Terminal.Gui { /// /// 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. /// /// 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. @@ -554,7 +544,11 @@ namespace Terminal.Gui { toReturn.AddRange (AddToLineMap (root)); } - return cachedLineMap = new ReadOnlyCollection> (toReturn); + cachedLineMap = new ReadOnlyCollection> (toReturn); + + // Update the collection used for search-typing + Navigator.Collection = cachedLineMap.Select (b => AspectGetter (b.Model)).ToArray (); + return cachedLineMap; } private IEnumerable> AddToLineMap (Branch 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 { } } + /// + /// Gets the that is used to navigate the + /// when searching with the keyboard. + /// + public SearchCollectionNavigator Navigator { get; private set; } = new SearchCollectionNavigator (); + /// 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> 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 { /// /// Triggers the event with the . /// - /// This method also ensures that the selected object is visible + /// This method also ensures that the selected object is visible. /// public void ActivateSelectedObjectIfAny () { @@ -663,11 +650,11 @@ namespace Terminal.Gui { } /// - /// Moves the to the next item that begins with - /// This method will loop back to the start of the tree if reaching the end without finding a match + /// Moves the to the next item that begins with . + /// This method will loop back to the start of the tree if reaching the end without finding a match. /// - /// The first character of the next item you want selected - /// Case sensitivity of the search + /// The first character of the next item you want selected. + /// Case sensitivity of the search. 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 { /// /// Moves the selection up by the height of the control (1 page). /// - /// True if the navigation should add the covered nodes to the selected current selection + /// True if the navigation should add the covered nodes to the selected current selection. /// public void MovePageUp (bool expandSelection = false) { @@ -690,7 +677,7 @@ namespace Terminal.Gui { /// /// Moves the selection down by the height of the control (1 page). /// - /// True if the navigation should add the covered nodes to the selected current selection + /// True if the navigation should add the covered nodes to the selected current selection. /// public void MovePageDown (bool expandSelection = false) { @@ -698,7 +685,7 @@ namespace Terminal.Gui { } /// - /// 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. /// public void ScrollDown () { @@ -707,7 +694,7 @@ namespace Terminal.Gui { } /// - /// 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. /// public void ScrollUp () { @@ -716,7 +703,7 @@ namespace Terminal.Gui { } /// - /// Raises the event + /// Raises the event. /// /// protected virtual void OnObjectActivated (ObjectActivatedEventArgs e) @@ -725,15 +712,15 @@ namespace Terminal.Gui { } /// - /// 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. /// /// /// If you have screen coordinates then use /// to translate these into the client area of the . /// - /// The row of the of the - /// The object currently displayed on this row or null + /// The row of the of the . + /// The object currently displayed on this row or null. 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; } /// /// Returns the branch at the given client - /// coordinate e.g. following a click event + /// coordinate e.g. following a click event. /// - /// Client Y position in the controls bounds - /// The clicked branch or null if outside of tree region + /// Client Y position in the controls bounds. + /// The clicked branch or null if outside of tree region. private Branch HitTest (int y) { var map = BuildLineMap (); @@ -870,7 +854,7 @@ namespace Terminal.Gui { } /// - /// 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). /// public override void PositionCursor () { @@ -891,11 +875,10 @@ namespace Terminal.Gui { } } - /// - /// 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. /// protected virtual void CursorLeft (bool ctrl) { @@ -919,7 +902,7 @@ namespace Terminal.Gui { /// /// Changes the to the first root object and resets - /// the to 0 + /// the to 0. /// public void GoToFirst () { @@ -931,7 +914,7 @@ namespace Terminal.Gui { /// /// Changes the to the last object in the tree and scrolls so - /// that it is visible + /// that it is visible. /// public void GoToEnd () { @@ -944,8 +927,8 @@ namespace Terminal.Gui { /// /// Changes the to and scrolls to ensure - /// it is visible. Has no effect if is not exposed in the tree (e.g. - /// its parents are collapsed) + /// it is visible. Has no effect if is not exposed in the tree (e.g. + /// its parents are collapsed). /// /// public void GoTo (T toSelect) @@ -960,14 +943,14 @@ namespace Terminal.Gui { } /// - /// The number of screen lines to move the currently selected object by. Supports negative - /// . Each branch occupies 1 line on screen + /// The number of screen lines to move the currently selected object by. Supports negative values. + /// . Each branch occupies 1 line on screen. /// /// 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 + /// then the first object in the tree is selected instead. /// Positive to move the selection down the screen, negative to move it up /// True to expand the selection (assuming - /// is enabled). False to replace + /// is enabled). False to replace. 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 (); } /// - /// Moves the selection to the first child in the currently selected level + /// Moves the selection to the first child in the currently selected level. /// public void AdjustSelectionToBranchStart () { @@ -1054,7 +1034,7 @@ namespace Terminal.Gui { } /// - /// Moves the selection to the last child in the currently selected level + /// Moves the selection to the last child in the currently selected level. /// public void AdjustSelectionToBranchEnd () { @@ -1088,13 +1068,12 @@ namespace Terminal.Gui { currentBranch = next; next = map.ElementAt (currentIdx); } - GoToEnd (); } /// - /// Sets the selection to the next branch that matches the + /// Sets the selection to the next branch that matches the . /// /// private void AdjustSelectionToNext (Func, bool> predicate) @@ -1132,7 +1111,7 @@ namespace Terminal.Gui { /// /// Adjusts the to ensure the given - /// is visible. Has no effect if already visible + /// is visible. Has no effect if already visible. /// public void EnsureVisible (T model) { @@ -1159,7 +1138,7 @@ namespace Terminal.Gui { } /// - /// Expands the currently + /// Expands the currently . /// public void Expand () { @@ -1168,9 +1147,9 @@ namespace Terminal.Gui { /// /// 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). /// - /// The object to expand + /// The object to expand. public void Expand (T toExpand) { if (toExpand == null) { @@ -1183,9 +1162,9 @@ namespace Terminal.Gui { } /// - /// Expands the supplied object and all child objects + /// Expands the supplied object and all child objects. /// - /// The object to expand + /// The object to expand. public void ExpandAll (T toExpand) { if (toExpand == null) { @@ -1198,7 +1177,7 @@ namespace Terminal.Gui { } /// /// 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). /// public void ExpandAll () { @@ -1211,7 +1190,7 @@ namespace Terminal.Gui { } /// /// Returns true if the given object is exposed in the tree and can be - /// expanded otherwise false + /// expanded otherwise false. /// /// /// @@ -1222,7 +1201,7 @@ namespace Terminal.Gui { /// /// Returns true if the given object is exposed in the tree and - /// expanded otherwise false + /// expanded otherwise false. /// /// /// @@ -1240,26 +1219,26 @@ namespace Terminal.Gui { } /// - /// Collapses the supplied object if it is currently expanded + /// Collapses the supplied object if it is currently expanded . /// - /// The object to collapse + /// The object to collapse. public void Collapse (T toCollapse) { CollapseImpl (toCollapse, false); } /// - /// 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). /// - /// The object to collapse + /// The object to collapse. public void CollapseAll (T toCollapse) { CollapseImpl (toCollapse, true); } /// - /// Collapses all root nodes in the tree + /// Collapses all root nodes in the tree. /// public void CollapseAll () { @@ -1272,19 +1251,17 @@ namespace Terminal.Gui { } /// - /// Implementation of and . Performs - /// operation and updates selection if disapeared + /// Implementation of and . Performs + /// operation and updates selection if disapeared. /// /// /// 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 { /// /// Returns the corresponding in the tree for - /// . This will not work for objects hidden - /// by their parent being collapsed + /// . This will not work for objects hidden + /// by their parent being collapsed. /// /// /// The branch for or null if it is not currently - /// exposed in the tree + /// exposed in the tree. private Branch ObjectToBranch (T toFind) { return BuildLineMap ().FirstOrDefault (o => o.Model.Equals (toFind)); @@ -1330,7 +1307,7 @@ namespace Terminal.Gui { /// /// Returns true if the is either the - /// or part of a + /// or part of a . /// /// /// @@ -1365,7 +1342,7 @@ namespace Terminal.Gui { /// /// Selects all objects in the tree when is enabled otherwise - /// does nothing + /// does nothing. /// public void SelectAll () { @@ -1387,9 +1364,8 @@ namespace Terminal.Gui { OnSelectionChanged (new SelectionChangedEventArgs (this, SelectedObject, SelectedObject)); } - /// - /// Raises the SelectionChanged event + /// Raises the SelectionChanged event. /// /// protected virtual void OnSelectionChanged (SelectionChangedEventArgs e) @@ -1431,5 +1407,4 @@ namespace Terminal.Gui { return included.Contains (model); } } - } \ No newline at end of file diff --git a/UICatalog/Properties/launchSettings.json b/UICatalog/Properties/launchSettings.json index eda283ad8..e1f2b1db2 100644 --- a/UICatalog/Properties/launchSettings.json +++ b/UICatalog/Properties/launchSettings.json @@ -44,6 +44,10 @@ "WSL": { "commandName": "WSL2", "distributionName": "" + }, + "SearchCollectionNavigatorTester": { + "commandName": "Project", + "commandLineArgs": "\"Search Collection Nav\"" } } } \ No newline at end of file diff --git a/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs b/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs index 10feff7c3..80b514893 100644 --- a/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs +++ b/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().ToList (); - //_treeView.AddObject (root); + root.Children = _items.Where (i => char.IsLetterOrDigit (i [0])).Select (i => new TreeNode (i)).Cast ().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 ().ToList (); _treeView.AddObject (root); _treeView.ExpandAll (); + _treeView.GoToFirst (); + + _treeView.Navigator.SearchStringChanged += (state) => { + label.Text = $"TreeView: {state.SearchString}"; + }; } + private void Quit () { Application.RequestStop (); diff --git a/UnitTests/ListViewTests.cs b/UnitTests/ListViewTests.cs index a9a29543d..3c2d12b4a 100644 --- a/UnitTests/ListViewTests.cs +++ b/UnitTests/ListViewTests.cs @@ -151,7 +151,7 @@ namespace Terminal.Gui.Views { public IList ToList () { - throw new NotImplementedException (); + return new List () { "One", "Two", "Three" }; } } diff --git a/UnitTests/SearchCollectionNavigatorTests.cs b/UnitTests/SearchCollectionNavigatorTests.cs index b59f8f734..b7e0a4df8 100644 --- a/UnitTests/SearchCollectionNavigatorTests.cs +++ b/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 TestSearchCollectionNavigator_AtSymbol () + public void AtSymbol () { var strings = new string []{ - "appricot", - "arm", - "ta", - "@bob", - "@bb", - "text", - "egg", - "candle" - }; + "appricot", + "arm", + "ta", + "@bob", + "@bb", + "text", + "egg", + "candle" + }; 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 (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 ("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 Delay () + { + var strings = new string []{ + "$$", + "$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 (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)); } } }