using System; using System.Collections.Generic; using System.Linq; namespace Terminal.Gui { /// /// Changes the index in a collection based on keys pressed /// and the current state /// class SearchCollectionNavigator { string state = ""; DateTime lastKeystroke = DateTime.MinValue; const int TypingDelay = 250; public StringComparer Comparer { get; set; } = StringComparer.InvariantCultureIgnoreCase; private IEnumerable Collection { get => _collection; set => _collection = value; } private IEnumerable _collection; public SearchCollectionNavigator (IEnumerable collection) { _collection = collection; } public int CalculateNewIndex (IEnumerable collection, int currentIndex, char keyStruck) { // if user presses a key if (!char.IsControl(keyStruck)) {//char.IsLetterOrDigit (keyStruck) || char.IsPunctuation (keyStruck) || char.IsSymbol(keyStruck)) { // maybe user pressed 'd' and now presses 'd' again. // a candidate search is things that begin with "dd" // but if we find none then we must fallback on cycling // d instead and discard the candidate state string candidateState = ""; // is it a second or third (etc) keystroke within a short time if (state.Length > 0 && DateTime.Now - lastKeystroke < TimeSpan.FromMilliseconds (TypingDelay)) { // "dd" is a candidate candidateState = state + keyStruck; } else { // its a fresh keystroke after some time // or its first ever key press state = new string (keyStruck, 1); } var idxCandidate = GetNextIndexMatching (collection, 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; return idxCandidate; } // nothing matches "dd" so discard it as a candidate // and just cycle "d" instead lastKeystroke = DateTime.Now; idxCandidate = GetNextIndexMatching (collection, currentIndex, state); // 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); return idxCandidate == -1 ? currentIndex : idxCandidate; } // Found another "d" or just leave index as it was return idxCandidate; } else { // clear state because keypress was non letter ClearState (); // no change in index for non letter keystrokes return currentIndex; } } public int CalculateNewIndex (int currentIndex, char keyStruck) { return CalculateNewIndex (Collection, currentIndex, keyStruck); } private int GetNextIndexMatching (IEnumerable collection, int currentIndex, string search, bool preferNotToMoveToNewIndexes = false) { if (string.IsNullOrEmpty (search)) { return -1; } // find indexes of items that start with the search text int [] matchingIndexes = collection.Select ((item, idx) => (item, idx)) .Where (k => k.item?.ToString().StartsWith (search, StringComparison.InvariantCultureIgnoreCase) ?? false) .Select (k => k.idx) .ToArray (); // if there are items beginning with search if (matchingIndexes.Length > 0) { // is one of them currently selected? var currentlySelected = Array.IndexOf (matchingIndexes, currentIndex); if (currentlySelected == -1) { // we are not currently selecting any item beginning with the search // so jump to first item in list that begins with the letter return matchingIndexes [0]; } else { // the current index is part of the matching collection if (preferNotToMoveToNewIndexes) { // if we would rather not jump around (e.g. user is typing lots of text to get this match) return matchingIndexes [currentlySelected]; } // cycle to next (circular) return matchingIndexes [(currentlySelected + 1) % matchingIndexes.Length]; } } // nothing starts with the search return -1; } private void ClearState () { state = ""; lastKeystroke = DateTime.MinValue; } /// /// Returns true if is a searchable key /// (e.g. letters, numbers etc) that is valid to pass to to this /// 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; } } }