diff --git a/Terminal.Gui/Core/SearchCollectionNavigator.cs b/Terminal.Gui/Core/SearchCollectionNavigator.cs index 34424dc42..1ebb0dd19 100644 --- a/Terminal.Gui/Core/SearchCollectionNavigator.cs +++ b/Terminal.Gui/Core/SearchCollectionNavigator.cs @@ -129,5 +129,17 @@ namespace Terminal.Gui { 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) + { + return !kb.IsAlt && !kb.IsCapslock && !kb.IsCtrl && !kb.IsScrolllock && !kb.IsNumlock; + } } } diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 1513cb7ed..2cf853f29 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -438,7 +438,7 @@ namespace Terminal.Gui { } // Enable user to find & select an item by typing text - if (!kb.IsAlt && !kb.IsCapslock && !kb.IsCtrl && !kb.IsScrolllock && !kb.IsNumlock) { + if (SearchCollectionNavigator.IsCompatibleKey(kb)) { if (navigator == null) { navigator = new SearchCollectionNavigator (source.ToList ().Cast ()); } diff --git a/Terminal.Gui/Views/TreeView.cs b/Terminal.Gui/Views/TreeView.cs index 4f8692d74..b26791608 100644 --- a/Terminal.Gui/Views/TreeView.cs +++ b/Terminal.Gui/Views/TreeView.cs @@ -140,12 +140,12 @@ namespace Terminal.Gui { /// public MouseFlags? ObjectActivationButton { get; set; } = MouseFlags.Button1DoubleClicked; - + /// /// 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;} + public Func ColorGetter { get; set; } /// /// Secondary selected regions of tree when is true @@ -220,6 +220,7 @@ namespace Terminal.Gui { 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. @@ -227,7 +228,7 @@ namespace Terminal.Gui { /// Defaults to /// public CursorVisibility DesiredCursorVisibility { - get { + get { return MultiSelect ? desiredCursorVisibility : CursorVisibility.Invisible; } set { @@ -576,19 +577,44 @@ namespace Terminal.Gui { return false; } - // if it is a single character pressed without any control keys - if (keyEvent.KeyValue > 0 && keyEvent.KeyValue < 0xFFFF) { + try { + + // First of all deal with any registered keybindings + var result = InvokeKeybindings (keyEvent); + if (result != null) { + return (bool)result; + } + + // 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 (); + } + + // Find the current selected object within the tree + var current = map.IndexOf (b => b.Model == SelectedObject); + var newIndex = searchCollectionNavigator.CalculateNewIndex (current, (char)keyEvent.KeyValue); + + if (newIndex != -1) { + SelectedObject = map.ElementAt (newIndex).Model; + EnsureVisible (selectedObject); + SetNeedsDisplay (); + } - if (char.IsLetterOrDigit ((char)keyEvent.KeyValue) && AllowLetterBasedNavigation && !keyEvent.IsShift && !keyEvent.IsAlt && !keyEvent.IsCtrl) { - AdjustSelectionToNextItemBeginningWith ((char)keyEvent.KeyValue); return true; } - } - try { - var result = InvokeKeybindings (keyEvent); - if (result != null) - return (bool)result; } finally { PositionCursor (); @@ -626,7 +652,7 @@ namespace Terminal.Gui { /// /// /// - public int? GetObjectRow(T toFind) + public int? GetObjectRow (T toFind) { var idx = BuildLineMap ().IndexOf (o => o.Model.Equals (toFind)); diff --git a/UnitTests/SearchCollectionNavigatorTests.cs b/UnitTests/SearchCollectionNavigatorTests.cs index eea4c76d0..b59f8f734 100644 --- a/UnitTests/SearchCollectionNavigatorTests.cs +++ b/UnitTests/SearchCollectionNavigatorTests.cs @@ -3,25 +3,41 @@ using Xunit; namespace Terminal.Gui.Core { public class SearchCollectionNavigatorTests { + static string [] simpleStrings = new string []{ + "appricot", // 0 + "arm", // 1 + "bat", // 2 + "batman", // 3 + "candle" // 4 + }; + [Fact] + public void TestSearchCollectionNavigator_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')); + } + [Fact] + public void TestSearchCollectionNavigator_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')); + } [Fact] public void TestSearchCollectionNavigator_Cycling () { - var strings = new string []{ - "appricot", - "arm", - "bat", - "batman", - "candle" - }; - - var n = new SearchCollectionNavigator (strings); + var n = new SearchCollectionNavigator (simpleStrings); Assert.Equal (2, n.CalculateNewIndex ( 0, 'b')); Assert.Equal (3, n.CalculateNewIndex ( 2, 'b')); // if 4 (candle) is selected it should loop back to bat Assert.Equal (2, n.CalculateNewIndex ( 4, 'b')); - }