From 3f38d8104e2522ccfa66f3b8324c5dc8bf8803a7 Mon Sep 17 00:00:00 2001 From: Thomas Nind <31306100+tznind@users.noreply.github.com> Date: Mon, 28 Apr 2025 13:39:11 +0100 Subject: [PATCH] Fixes #4027. Add collection search matcher (#4029) * Add collection search matcher * Fix naming * fix naming * Move FileDialogCollectionNavigator to its own file (no longer private class) Add class diagram for collectionNavigation * Add ICollectionNavigator interface * Move to separate file IListCollectionNavigator * Update class diagram * update class diagram * Add tests for overriding ICollectionNavigatorMatcher * xmldoc and nullability warning fixes * Code Cleanup * Make requested changes to naming and terminology * Move to seperate namespace * Update class diagram and change TreeView to reference the interface not concrete class * Switch to implicit new * highlight that this class also works with tree view * Apply tig patch to ensure keybindings get priority over navigator See: https://github.com/gui-cs/Terminal.Gui/issues/4027#issuecomment-2810020893 * Apply 'keybinding has priority' fix to TreeView too * Apply 'keybindngs priority over navigation' fix to TableView * Remove entire branch for selectively returning false now that it is default when there is a keybinding collision * Make classes internal and remove 'custom' navigator that was configured in UICatlaogToplevel * Change logging in collection navigator from Trace to Debug * Switch to NewKeyDownEvent and directly setting HasFocus * Remove application top dependency * Remove references to application * Remove Application * Move new tests to parallel --------- Co-authored-by: Tig --- .../Scenarios/CollectionNavigatorTester.cs | 2 +- Examples/UICatalog/UICatalogTop.cs | 15 --- .../CollectionNavigation.cd | 124 ++++++++++++++++++ .../CollectionNavigator.cs | 8 +- .../CollectionNavigatorBase.cs | 94 ++++--------- .../DefaultCollectionNavigatorMatcher.cs | 30 +++++ .../ICollectionNavigator.cs | 49 +++++++ .../ICollectionNavigatorMatcher.cs | 26 ++++ .../IListCollectionNavigator.cs | 13 ++ .../TableCollectionNavigator.cs | 14 +- Terminal.Gui/Views/FileDialog.cs | 27 +--- .../Views/FileDialogCollectionNavigator.cs | 21 +++ Terminal.Gui/Views/ListView.cs | 26 +--- Terminal.Gui/Views/TableView/TableView.cs | 11 +- Terminal.Gui/Views/TreeView/TreeView.cs | 11 +- Tests/UnitTests/Views/ListViewTests.cs | 31 +---- Tests/UnitTests/Views/TableViewTests.cs | 9 +- .../Text/CollectionNavigatorTests.cs | 64 +++++---- .../Views/ListViewTests.cs | 120 +++++++++++++++++ .../Views/TableViewTests.cs | 45 +++++++ .../Views/TreeViewTests.cs | 40 ++++++ 21 files changed, 577 insertions(+), 203 deletions(-) create mode 100644 Terminal.Gui/Views/CollectionNavigation/CollectionNavigation.cd rename Terminal.Gui/{Text => Views/CollectionNavigation}/CollectionNavigator.cs (77%) rename Terminal.Gui/{Text => Views/CollectionNavigation}/CollectionNavigatorBase.cs (57%) create mode 100644 Terminal.Gui/Views/CollectionNavigation/DefaultCollectionNavigatorMatcher.cs create mode 100644 Terminal.Gui/Views/CollectionNavigation/ICollectionNavigator.cs create mode 100644 Terminal.Gui/Views/CollectionNavigation/ICollectionNavigatorMatcher.cs create mode 100644 Terminal.Gui/Views/CollectionNavigation/IListCollectionNavigator.cs rename Terminal.Gui/{Text => Views/CollectionNavigation}/TableCollectionNavigator.cs (53%) create mode 100644 Terminal.Gui/Views/FileDialogCollectionNavigator.cs create mode 100644 Tests/UnitTestsParallelizable/Views/ListViewTests.cs create mode 100644 Tests/UnitTestsParallelizable/Views/TableViewTests.cs create mode 100644 Tests/UnitTestsParallelizable/Views/TreeViewTests.cs diff --git a/Examples/UICatalog/Scenarios/CollectionNavigatorTester.cs b/Examples/UICatalog/Scenarios/CollectionNavigatorTester.cs index 650e5e180..5698cd310 100644 --- a/Examples/UICatalog/Scenarios/CollectionNavigatorTester.cs +++ b/Examples/UICatalog/Scenarios/CollectionNavigatorTester.cs @@ -58,7 +58,7 @@ public class CollectionNavigatorTester : Scenario "$200.00", "$210.99", "$$", - "appricot", + "apricot", "arm", "丗丙业丞", "丗丙丛", diff --git a/Examples/UICatalog/UICatalogTop.cs b/Examples/UICatalog/UICatalogTop.cs index 86cad6bf9..27c544a5b 100644 --- a/Examples/UICatalog/UICatalogTop.cs +++ b/Examples/UICatalog/UICatalogTop.cs @@ -380,11 +380,6 @@ public class UICatalogTop : Toplevel public static ObservableCollection? CachedScenarios { get; set; } - // UI Catalog uses TableView for the scenario list instead of a ListView to demonstrate how - // TableView works. There's no real reason not to use ListView. Because we use TableView, and TableView - // doesn't (currently) have CollectionNavigator support built in, we implement it here, within the app. - private readonly CollectionNavigator _scenarioCollectionNav = new (); - // If set, holds the scenario the user selected to run public static Scenario? CachedSelectedScenario { get; set; } @@ -561,16 +556,6 @@ public class UICatalogTop : Toplevel } ); - // Create a collection of just the scenario names (the 1st column in our TableView) - // for CollectionNavigator. - List firstColumnList = []; - - for (var i = 0; i < _scenarioList.Table.Rows; i++) - { - firstColumnList.Add (_scenarioList.Table [i, 0]); - } - - _scenarioCollectionNav.Collection = firstColumnList; } #endregion Category List diff --git a/Terminal.Gui/Views/CollectionNavigation/CollectionNavigation.cd b/Terminal.Gui/Views/CollectionNavigation/CollectionNavigation.cd new file mode 100644 index 000000000..7236c3adf --- /dev/null +++ b/Terminal.Gui/Views/CollectionNavigation/CollectionNavigation.cd @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + AAgEAAAAAAAQAAAIAAEAAgAAAAAABAAEAAAAACwAAAA= + Views\CollectionNavigation\CollectionNavigatorBase.cs + + + + + + + + + + AAAAAAAAAAAAQAAAAAAAAgAAAAAAAAAEAAAAAAAAAAA= + Views\CollectionNavigation\CollectionNavigator.cs + + + + + + + AAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAQA= + Views\CollectionNavigation\DefaultCollectionNavigatorMatcher.cs + + + + + + + AAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAEAAAAIAAAAAA= + Views\CollectionNavigation\TableCollectionNavigator.cs + + + + + + AAE+ASAkEnAAABAAKGAggYAZJAIAABEAcBAaAwAQIAA= + Views\ListView.cs + + + + + + + + + + + + + iIY4LQFUHDKVIHIESBgigQcFT6GxhBDABGJItBQAwAQ= + Views\FileDialog.cs + + + + + + + AAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAEAAAAAAAAAAA= + Views\FileDialogCollectionNavigator.cs + + + + + + QwUeAxwgICIAcABIABeR0oBAkhoFGGOBDABgAN3oPEI= + Views\TableView\TableView.cs + + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAA= + Views\TreeView\TreeView.cs + + + + + + + UwAGySBgBSBGMAQgIiCaBDUItJIBSAWwRMQOSgQCwJI= + Views\TreeView\TreeView.cs + + + + + + + + + + AAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAA= + Views\CollectionNavigation\ICollectionNavigatorMatcher.cs + + + + + + AAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Views\CollectionNavigation\IListCollectionNavigator.cs + + + + + + AAgAAAAAAAAAAAAIAAAAAAAAAAAABAAAAAAAACgAAAA= + Views\CollectionNavigation\ICollectionNavigator.cs + + + + \ No newline at end of file diff --git a/Terminal.Gui/Text/CollectionNavigator.cs b/Terminal.Gui/Views/CollectionNavigation/CollectionNavigator.cs similarity index 77% rename from Terminal.Gui/Text/CollectionNavigator.cs rename to Terminal.Gui/Views/CollectionNavigation/CollectionNavigator.cs index 43b5ced63..5bbb04485 100644 --- a/Terminal.Gui/Text/CollectionNavigator.cs +++ b/Terminal.Gui/Views/CollectionNavigation/CollectionNavigator.cs @@ -2,9 +2,9 @@ namespace Terminal.Gui; -/// +/// /// This implementation is based on a static of objects. -public class CollectionNavigator : CollectionNavigatorBase +internal class CollectionNavigator : CollectionNavigatorBase, IListCollectionNavigator { /// Constructs a new CollectionNavigator. public CollectionNavigator () { } @@ -13,7 +13,7 @@ public class CollectionNavigator : CollectionNavigatorBase /// public CollectionNavigator (IList collection) { Collection = collection; } - /// The collection of objects to search. is used to search the collection. + /// public IList Collection { get; set; } /// @@ -21,4 +21,4 @@ public class CollectionNavigator : CollectionNavigatorBase /// protected override int GetCollectionLength () { return Collection.Count; } -} +} \ No newline at end of file diff --git a/Terminal.Gui/Text/CollectionNavigatorBase.cs b/Terminal.Gui/Views/CollectionNavigation/CollectionNavigatorBase.cs similarity index 57% rename from Terminal.Gui/Text/CollectionNavigatorBase.cs rename to Terminal.Gui/Views/CollectionNavigation/CollectionNavigatorBase.cs index 3ffe411d5..61b21a12b 100644 --- a/Terminal.Gui/Text/CollectionNavigatorBase.cs +++ b/Terminal.Gui/Views/CollectionNavigation/CollectionNavigatorBase.cs @@ -1,55 +1,31 @@ -using Microsoft.Extensions.Logging; +#nullable enable namespace Terminal.Gui; -/// -/// 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 (see ), the search string is cleared. -/// -public abstract class CollectionNavigatorBase +/// +internal abstract class CollectionNavigatorBase : ICollectionNavigator { private DateTime _lastKeystroke = DateTime.Now; private string _searchString = ""; - /// The comparer function to use when searching the collection. - public StringComparer Comparer { get; set; } = StringComparer.InvariantCultureIgnoreCase; + /// + public ICollectionNavigatorMatcher Matcher { get; set; } = new DefaultCollectionNavigatorMatcher (); - /// - /// Gets the current search string. This includes the set of keystrokes that have been pressed since the last - /// unsuccessful match or after ) milliseconds. Useful for debugging. - /// + /// public string SearchString { get => _searchString; private set { _searchString = value; - OnSearchStringChanged (new KeystrokeNavigatorEventArgs (value)); + OnSearchStringChanged (new (value)); } } - /// - /// Gets or sets the number of milliseconds to delay before clearing the search string. The delay is reset on each - /// call to . The default is 500ms. - /// + /// public int TypingDelay { get; set; } = 500; - /// - /// 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) { if (!char.IsControl (keyStruck)) @@ -61,21 +37,21 @@ public abstract class CollectionNavigatorBase var candidateState = ""; var elapsedTime = DateTime.Now - _lastKeystroke; - Logging.Trace($"CollectionNavigator began processing '{keyStruck}', it has been {elapsedTime} since last keystroke"); + Logging.Debug ($"CollectionNavigator began processing '{keyStruck}', it has been {elapsedTime} since last keystroke"); // is it a second or third (etc) keystroke within a short time if (SearchString.Length > 0 && elapsedTime < TimeSpan.FromMilliseconds (TypingDelay)) { // "dd" is a candidate candidateState = SearchString + keyStruck; - Logging.Trace($"Appending, search is now for '{candidateState}'"); + Logging.Debug ($"Appending, search is now for '{candidateState}'"); } else { // its a fresh keystroke after some time // or its first ever key press SearchString = new string (keyStruck, 1); - Logging.Trace($"It has been too long since last key press so beginning new search"); + Logging.Debug ($"It has been too long since last key press so beginning new search"); } int idxCandidate = GetNextMatchingItem ( @@ -86,14 +62,14 @@ public abstract class CollectionNavigatorBase candidateState.Length > 1 ); - Logging.Trace($"CollectionNavigator searching (preferring minimum movement) matched:{idxCandidate}"); + Logging.Debug ($"CollectionNavigator searching (preferring minimum movement) matched:{idxCandidate}"); if (idxCandidate != -1) { // found "dd" so candidate search string is accepted _lastKeystroke = DateTime.Now; SearchString = candidateState; - Logging.Trace($"Found collection item that matched search:{idxCandidate}"); + Logging.Debug ($"Found collection item that matched search:{idxCandidate}"); return idxCandidate; } @@ -102,13 +78,13 @@ public abstract class CollectionNavigatorBase _lastKeystroke = DateTime.Now; idxCandidate = GetNextMatchingItem (currentIndex, candidateState); - Logging.Trace($"CollectionNavigator searching (any match) matched:{idxCandidate}"); + Logging.Debug ($"CollectionNavigator searching (any match) matched:{idxCandidate}"); // if a match wasn't found, the user typed a 'wrong' key in their search ("can" + 'z' // instead of "can" + 'd'). if (SearchString.Length > 1 && idxCandidate == -1) { - Logging.Trace("CollectionNavigator ignored key and returned existing index"); + Logging.Debug ("CollectionNavigator ignored key and returned existing index"); // ignore it since we're still within the typing delay // don't add it to SearchString either return currentIndex; @@ -117,7 +93,7 @@ public abstract class CollectionNavigatorBase // if no changes to current state manifested if (idxCandidate == currentIndex || idxCandidate == -1) { - Logging.Trace("CollectionNavigator found no changes to current index, so clearing search"); + Logging.Debug ("CollectionNavigator found no changes to current index, so clearing search"); // clear history and treat as a fresh letter ClearSearchString (); @@ -126,17 +102,17 @@ public abstract class CollectionNavigatorBase SearchString = new string (keyStruck, 1); idxCandidate = GetNextMatchingItem (currentIndex, SearchString); - Logging.Trace($"CollectionNavigator new SearchString {SearchString} matched index:{idxCandidate}" ); + Logging.Debug ($"CollectionNavigator new SearchString {SearchString} matched index:{idxCandidate}"); return idxCandidate == -1 ? currentIndex : idxCandidate; } - Logging.Trace($"CollectionNavigator final answer was:{idxCandidate}" ); + Logging.Debug ($"CollectionNavigator final answer was:{idxCandidate}"); // Found another "d" or just leave index as it was return idxCandidate; } - Logging.Trace("CollectionNavigator found key press was not actionable so clearing search and returning -1"); + Logging.Debug ("CollectionNavigator found key press was not actionable so clearing search and returning -1"); // clear state because keypress was a control char ClearSearchString (); @@ -145,29 +121,17 @@ public abstract class CollectionNavigatorBase return -1; } - /// - /// Returns true if is a searchable key (e.g. letters, numbers, etc) that are valid to pass - /// to this class for search filtering. - /// - /// - /// - public static bool IsCompatibleKey (Key a) - { - Rune rune = a.AsRune; - return rune != default (Rune) && !Rune.IsControl (rune); - } /// - /// Invoked when the changes. Useful for debugging. Invokes the + /// Raised when the is changed. Useful for debugging. Raises the /// event. /// /// - public virtual void OnSearchStringChanged (KeystrokeNavigatorEventArgs e) { SearchStringChanged?.Invoke (this, e); } + protected virtual void OnSearchStringChanged (KeystrokeNavigatorEventArgs e) { SearchStringChanged?.Invoke (this, e); } - /// This event is invoked when changes. Useful for debugging. - [CanBeNull] - public event EventHandler SearchStringChanged; + /// This event is raised when is changed. Useful for debugging. + public event EventHandler? SearchStringChanged; /// Returns the collection being navigated element at . /// @@ -195,7 +159,7 @@ public abstract class CollectionNavigatorBase int collectionLength = GetCollectionLength (); - if (currentIndex != -1 && currentIndex < collectionLength && IsMatch (search, ElementAt (currentIndex))) + if (currentIndex != -1 && currentIndex < collectionLength && Matcher.IsMatch (search, ElementAt (currentIndex))) { // we are already at a match if (minimizeMovement) @@ -209,7 +173,7 @@ public abstract class CollectionNavigatorBase //circular int idxCandidate = (i + currentIndex) % collectionLength; - if (IsMatch (search, ElementAt (idxCandidate))) + if (Matcher.IsMatch (search, ElementAt (idxCandidate))) { return idxCandidate; } @@ -222,7 +186,7 @@ public abstract class CollectionNavigatorBase // search terms no longer match the current selection or there is none for (var i = 0; i < collectionLength; i++) { - if (IsMatch (search, ElementAt (i))) + if (Matcher.IsMatch (search, ElementAt (i))) { return i; } @@ -237,6 +201,4 @@ public abstract class CollectionNavigatorBase SearchString = ""; _lastKeystroke = DateTime.Now; } - - private bool IsMatch (string search, object value) { return value?.ToString ().StartsWith (search, StringComparison.InvariantCultureIgnoreCase) ?? false; } -} +} \ No newline at end of file diff --git a/Terminal.Gui/Views/CollectionNavigation/DefaultCollectionNavigatorMatcher.cs b/Terminal.Gui/Views/CollectionNavigation/DefaultCollectionNavigatorMatcher.cs new file mode 100644 index 000000000..2fb65a7a3 --- /dev/null +++ b/Terminal.Gui/Views/CollectionNavigation/DefaultCollectionNavigatorMatcher.cs @@ -0,0 +1,30 @@ +#nullable enable + +namespace Terminal.Gui; + +/// +/// Default implementation of , performs +/// case-insensitive (see ) matching of items based on +/// . +/// +internal class DefaultCollectionNavigatorMatcher : ICollectionNavigatorMatcher +{ + /// The comparer function to use when searching the collection. + public StringComparison Comparer { get; set; } = StringComparison.InvariantCultureIgnoreCase; + + /// + public bool IsMatch (string search, object? value) { return value?.ToString ()?.StartsWith (search, Comparer) ?? false; } + + /// + /// Returns true if is key searchable key (e.g. letters, numbers, etc) that are valid to pass + /// to this class for search filtering. + /// + /// + /// + public bool IsCompatibleKey (Key key) + { + Rune rune = key.AsRune; + + return rune != default && !Rune.IsControl (rune); + } +} diff --git a/Terminal.Gui/Views/CollectionNavigation/ICollectionNavigator.cs b/Terminal.Gui/Views/CollectionNavigation/ICollectionNavigator.cs new file mode 100644 index 000000000..9b7a1b3e0 --- /dev/null +++ b/Terminal.Gui/Views/CollectionNavigation/ICollectionNavigator.cs @@ -0,0 +1,49 @@ +#nullable enable + +namespace Terminal.Gui; + +/// +/// 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 (see ), the search string is cleared. +/// +public interface ICollectionNavigator +{ + /// + /// Gets or sets the number of milliseconds to delay before clearing the search string. The delay is reset on each + /// call to . The default is 500ms. + /// + public int TypingDelay { get; set; } + + /// This event is invoked when changes. Useful for debugging. + public event EventHandler? SearchStringChanged; + + /// + /// Gets the current search string. This includes the set of keystrokes that have been pressed since the last + /// unsuccessful match or after ) milliseconds. Useful for debugging. + /// + string SearchString { get; } + + /// + /// Class responsible for deciding whether given entries in the collection match + /// the search term the user is typing. + /// + ICollectionNavigatorMatcher Matcher { get; set; } + + /// + /// 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. + /// + int GetNextMatchingItem (int currentIndex, char keyStruck); +} diff --git a/Terminal.Gui/Views/CollectionNavigation/ICollectionNavigatorMatcher.cs b/Terminal.Gui/Views/CollectionNavigation/ICollectionNavigatorMatcher.cs new file mode 100644 index 000000000..ec8cbc3e6 --- /dev/null +++ b/Terminal.Gui/Views/CollectionNavigation/ICollectionNavigatorMatcher.cs @@ -0,0 +1,26 @@ +namespace Terminal.Gui; + +/// +/// Determines which keys trigger collection manager navigation +/// and how to match typed strings to objects in the collection. +/// Default implementation is . +/// +public interface ICollectionNavigatorMatcher +{ + /// + /// Returns true if is key searchable key (e.g. letters, numbers, etc) that are valid to pass + /// to this class for search filtering. + /// + /// + /// + bool IsCompatibleKey (Key key); + + /// + /// Return true if the matches (e.g. starts with) + /// the term. + /// + /// + /// + /// + bool IsMatch (string search, object value); +} diff --git a/Terminal.Gui/Views/CollectionNavigation/IListCollectionNavigator.cs b/Terminal.Gui/Views/CollectionNavigation/IListCollectionNavigator.cs new file mode 100644 index 000000000..ca640975f --- /dev/null +++ b/Terminal.Gui/Views/CollectionNavigation/IListCollectionNavigator.cs @@ -0,0 +1,13 @@ +using System.Collections; + +namespace Terminal.Gui; + +/// +/// sub-interface for and . See also +/// / +/// +public interface IListCollectionNavigator : ICollectionNavigator +{ + /// The collection of objects to search. is used to search the collection. + IList Collection { get; set; } +} diff --git a/Terminal.Gui/Text/TableCollectionNavigator.cs b/Terminal.Gui/Views/CollectionNavigation/TableCollectionNavigator.cs similarity index 53% rename from Terminal.Gui/Text/TableCollectionNavigator.cs rename to Terminal.Gui/Views/CollectionNavigation/TableCollectionNavigator.cs index 002bd6a8d..ea1faba88 100644 --- a/Terminal.Gui/Text/TableCollectionNavigator.cs +++ b/Terminal.Gui/Views/CollectionNavigation/TableCollectionNavigator.cs @@ -1,24 +1,24 @@ namespace Terminal.Gui; /// Collection navigator for cycling selections in a . -public class TableCollectionNavigator : CollectionNavigatorBase +internal class TableCollectionNavigator : CollectionNavigatorBase { - private readonly TableView tableView; + private readonly TableView _tableView; /// Creates a new instance for navigating the data in the wrapped . - public TableCollectionNavigator (TableView tableView) { this.tableView = tableView; } + public TableCollectionNavigator (TableView tableView) { this._tableView = tableView; } /// protected override object ElementAt (int idx) { - int col = tableView.FullRowSelect ? 0 : tableView.SelectedColumn; - object rawValue = tableView.Table [idx, col]; + int col = _tableView.FullRowSelect ? 0 : _tableView.SelectedColumn; + object rawValue = _tableView.Table [idx, col]; - ColumnStyle style = tableView.Style.GetColumnStyleIfAny (col); + ColumnStyle style = _tableView.Style.GetColumnStyleIfAny (col); return style?.RepresentationGetter?.Invoke (rawValue) ?? rawValue; } /// - protected override int GetCollectionLength () { return tableView.Table.Rows; } + protected override int GetCollectionLength () { return _tableView.Table.Rows; } } diff --git a/Terminal.Gui/Views/FileDialog.cs b/Terminal.Gui/Views/FileDialog.cs index 2f1aa21ab..e20ee3310 100644 --- a/Terminal.Gui/Views/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialog.cs @@ -170,8 +170,8 @@ public class FileDialog : Dialog, IDesignable Width = Dim.Fill (), Height = Dim.Fill (), FullRowSelect = true, - CollectionNavigator = new FileDialogCollectionNavigator (this) }; + _tableView.CollectionNavigator = new FileDialogCollectionNavigator (this, _tableView); _tableView.KeyBindings.ReplaceCommands (Key.Space, Command.Select); _tableView.MouseClick += OnTableViewMouseClick; _tableView.Style.InvertSelectedCellFirstCharacter = true; @@ -1465,29 +1465,6 @@ public class FileDialog : Dialog, IDesignable _tableView.Update (); } - internal class FileDialogCollectionNavigator : CollectionNavigatorBase - { - private readonly FileDialog _fileDialog; - public FileDialogCollectionNavigator (FileDialog fileDialog) { _fileDialog = fileDialog; } - - protected override object ElementAt (int idx) - { - object val = FileDialogTableSource.GetRawColumnValue ( - _fileDialog._tableView.SelectedColumn, - _fileDialog.State?.Children [idx] - ); - - if (val is null) - { - return string.Empty; - } - - return val.ToString ().Trim ('.'); - } - - protected override int GetCollectionLength () { return _fileDialog.State?.Children.Length ?? 0; } - } - /// State representing a recursive search from downwards. internal class SearchState : FileDialogState { @@ -1639,4 +1616,4 @@ public class FileDialog : Dialog, IDesignable return true; } -} +} \ No newline at end of file diff --git a/Terminal.Gui/Views/FileDialogCollectionNavigator.cs b/Terminal.Gui/Views/FileDialogCollectionNavigator.cs new file mode 100644 index 000000000..481581de5 --- /dev/null +++ b/Terminal.Gui/Views/FileDialogCollectionNavigator.cs @@ -0,0 +1,21 @@ +namespace Terminal.Gui; + +internal class FileDialogCollectionNavigator (FileDialog fileDialog, TableView tableView) : CollectionNavigatorBase +{ + protected override object ElementAt (int idx) + { + object val = FileDialogTableSource.GetRawColumnValue ( + tableView.SelectedColumn, + fileDialog.State?.Children [idx] + ); + + if (val is null) + { + return string.Empty; + } + + return val.ToString ().Trim ('.'); + } + + protected override int GetCollectionLength () { return fileDialog.State?.Children.Length ?? 0; } +} diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index f81f16270..9a041963b 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -234,7 +234,7 @@ public class ListView : View, IDesignable /// Gets the that searches the collection as the /// user types. /// - public CollectionNavigator KeystrokeNavigator { get; } = new (); + public IListCollectionNavigator KeystrokeNavigator { get; } = new CollectionNavigator(); /// Gets or sets the leftmost column that is currently visible (when scrolling horizontally). /// The left position. @@ -809,27 +809,7 @@ public class ListView : View, IDesignable /// protected override bool OnKeyDown (Key key) { - // If marking is enabled and the user presses the space key don't let CollectionNavigator - // at it - if (AllowsMarking) - { - IEnumerable keys = KeyBindings.GetAllFromCommands (Command.Select); - - if (keys.Contains (key)) - { - return false; - } - - keys = KeyBindings.GetAllFromCommands ([Command.Select, Command.Down]); - - if (keys.Contains (key)) - { - return false; - } - - } - - // If the key was bound to a command, invoke the command. This enables overriding the default handling. + // If the key was bound to key command, let normal KeyDown processing happen. This enables overriding the default handling. // See: https://github.com/gui-cs/Terminal.Gui/issues/3950#issuecomment-2807350939 if (KeyBindings.TryGet (key, out _)) { @@ -837,7 +817,7 @@ public class ListView : View, IDesignable } // Enable user to find & select an item by typing text - if (CollectionNavigatorBase.IsCompatibleKey (key)) + if (KeystrokeNavigator.Matcher.IsCompatibleKey (key)) { int? newItem = KeystrokeNavigator?.GetNextMatchingItem (SelectedItem, (char)key); diff --git a/Terminal.Gui/Views/TableView/TableView.cs b/Terminal.Gui/Views/TableView/TableView.cs index 9c8c5b0d7..e1017e66a 100644 --- a/Terminal.Gui/Views/TableView/TableView.cs +++ b/Terminal.Gui/Views/TableView/TableView.cs @@ -306,7 +306,7 @@ public class TableView : View, IDesignable } /// Navigator for cycling the selected item in the table by typing. Set to null to disable this feature. - public CollectionNavigatorBase CollectionNavigator { get; set; } + public ICollectionNavigator CollectionNavigator { get; set; } /// /// Horizontal scroll offset. The index of the first column in to display when when rendering @@ -1010,12 +1010,19 @@ public class TableView : View, IDesignable return false; } + // If the key was bound to key command, let normal KeyDown processing happen. This enables overriding the default handling. + // See: https://github.com/gui-cs/Terminal.Gui/issues/3950#issuecomment-2807350939 + if (KeyBindings.TryGet (key, out _)) + { + return false; + } + if (CollectionNavigator != null && HasFocus && Table.Rows != 0 && key != KeyBindings.GetFirstFromCommands (Command.Accept) && key != CellActivationKey - && CollectionNavigatorBase.IsCompatibleKey (key) + && CollectionNavigator.Matcher.IsCompatibleKey (key) && !key.KeyCode.HasFlag (KeyCode.CtrlMask) && !key.KeyCode.HasFlag (KeyCode.AltMask) && Rune.IsLetterOrDigit ((Rune)key)) diff --git a/Terminal.Gui/Views/TreeView/TreeView.cs b/Terminal.Gui/Views/TreeView/TreeView.cs index af2245efa..b3da43d92 100644 --- a/Terminal.Gui/Views/TreeView/TreeView.cs +++ b/Terminal.Gui/Views/TreeView/TreeView.cs @@ -350,7 +350,7 @@ public class TreeView : View, ITreeView where T : class /// Gets the that searches the collection as the user /// types. /// - public CollectionNavigator KeystrokeNavigator { get; } = new (); + public IListCollectionNavigator KeystrokeNavigator { get; } = new CollectionNavigator(); /// Maximum number of nodes that can be expanded in any given branch. public int MaxDepth { get; set; } = 100; @@ -1206,8 +1206,15 @@ public class TreeView : View, ITreeView where T : class return false; } + // If the key was bound to key command, let normal KeyDown processing happen. This enables overriding the default handling. + // See: https://github.com/gui-cs/Terminal.Gui/issues/3950#issuecomment-2807350939 + if (KeyBindings.TryGet (key, out _)) + { + return false; + } + // If not a keybinding, is the key a searchable key press? - if (CollectionNavigatorBase.IsCompatibleKey (key) && AllowLetterBasedNavigation) + if (KeystrokeNavigator.Matcher.IsCompatibleKey (key) && AllowLetterBasedNavigation) { // If there has been a call to InvalidateMap since the last time // we need a new one to reflect the new exposed tree state diff --git a/Tests/UnitTests/Views/ListViewTests.cs b/Tests/UnitTests/Views/ListViewTests.cs index f99869c19..91f4d6d11 100644 --- a/Tests/UnitTests/Views/ListViewTests.cs +++ b/Tests/UnitTests/Views/ListViewTests.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Collections.ObjectModel; using System.Collections.Specialized; +using Moq; using UnitTests; using Xunit.Abstractions; @@ -1157,34 +1158,4 @@ Item 6", } } } - - [Fact] - public void CollectionNavigatorMatcher_KeybindingsOverrideNavigator () - { - Application.Top = new (); - - ObservableCollection source = new () { "apricot", "arm", "bat", "batman", "bates hotel", "candle" }; - ListView lv = new ListView { Source = new ListWrapper (source) }; - - Application.Top.Add (lv); - lv.SetFocus (); - - lv.KeyBindings.Add (Key.B, Command.Down); - - Assert.Equal (-1, lv.SelectedItem); - - // Keys should be consumed to move down the navigation i.e. to apricot - Assert.True (Application.RaiseKeyDownEvent (Key.B)); - Assert.Equal (0, lv.SelectedItem); - - Assert.True (Application.RaiseKeyDownEvent (Key.B)); - Assert.Equal (1, lv.SelectedItem); - - // There is no keybinding for Key.C so it hits collection navigator i.e. we jump to candle - Assert.True (Application.RaiseKeyDownEvent (Key.C)); - Assert.Equal (5, lv.SelectedItem); - - Application.Top.Dispose (); - Application.ResetState (); - } } \ No newline at end of file diff --git a/Tests/UnitTests/Views/TableViewTests.cs b/Tests/UnitTests/Views/TableViewTests.cs index bef6582ef..2274419de 100644 --- a/Tests/UnitTests/Views/TableViewTests.cs +++ b/Tests/UnitTests/Views/TableViewTests.cs @@ -68,11 +68,7 @@ public class TableViewTests (ITestOutputHelper output) tv.Table = new DataTableSource (dt); tv.NullSymbol = string.Empty; - - var top = new Toplevel (); - top.Add (tv); - Application.Begin (top); - + tv.ColorScheme = new ColorScheme (); tv.Draw (); var expected = @@ -105,6 +101,8 @@ public class TableViewTests (ITestOutputHelper output) style.ColorGetter = e => { return scheme; }; } + // Required or won't draw properly + Application.Driver.Clip = new Region (tv.Frame); tv.SetNeedsDraw (); tv.Draw (); @@ -116,7 +114,6 @@ public class TableViewTests (ITestOutputHelper output) 01111101101111111110 "; DriverAssert.AssertDriverAttributesAre (expected, output, Application.Driver, tv.ColorScheme.Normal, color); - top.Dispose (); } [Fact] diff --git a/Tests/UnitTestsParallelizable/Text/CollectionNavigatorTests.cs b/Tests/UnitTestsParallelizable/Text/CollectionNavigatorTests.cs index 12b14e479..0b3401b19 100644 --- a/Tests/UnitTestsParallelizable/Text/CollectionNavigatorTests.cs +++ b/Tests/UnitTestsParallelizable/Text/CollectionNavigatorTests.cs @@ -1,4 +1,5 @@ -using Xunit.Abstractions; +using Moq; +using Xunit.Abstractions; namespace Terminal.Gui.TextTests; @@ -6,7 +7,7 @@ public class CollectionNavigatorTests { private static readonly string [] simpleStrings = { - "appricot", // 0 + "apricot", // 0 "arm", // 1 "bat", // 2 "batman", // 3 @@ -20,7 +21,7 @@ public class CollectionNavigatorTests [Fact] public void AtSymbol () { - var strings = new [] { "appricot", "arm", "ta", "@bob", "@bb", "text", "egg", "candle" }; + var strings = new [] { "apricot", "arm", "ta", "@bob", "@bb", "text", "egg", "candle" }; var n = new CollectionNavigator (strings); Assert.Equal (3, n.GetNextMatchingItem (0, '@')); @@ -44,19 +45,19 @@ public class CollectionNavigatorTests Assert.Equal (0, n.GetNextMatchingItem (-1, 'a')); Assert.Equal (1, n.GetNextMatchingItem (0, 'a')); - // if 4 (candle) is selected it should loop back to appricot + // if 4 (candle) is selected it should loop back to apricot Assert.Equal (0, n.GetNextMatchingItem (4, 'a')); } [Fact] public void Delay () { - var strings = new [] { "$$", "$100.00", "$101.00", "$101.10", "$200.00", "appricot" }; + var strings = new [] { "$$", "$100.00", "$101.00", "$101.10", "$200.00", "apricot" }; var current = 0; var n = new CollectionNavigator (strings); // No delay - Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a')); + Assert.Equal (strings.IndexOf ("apricot"), current = n.GetNextMatchingItem (current, 'a')); Assert.Equal ("a", n.SearchString); Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$')); Assert.Equal ("$", n.SearchString); @@ -65,7 +66,7 @@ public class CollectionNavigatorTests // Delay Thread.Sleep (n.TypingDelay + 10); - Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a')); + Assert.Equal (strings.IndexOf ("apricot"), current = n.GetNextMatchingItem (current, 'a')); Assert.Equal ("a", n.SearchString); Thread.Sleep (n.TypingDelay + 10); @@ -92,7 +93,7 @@ public class CollectionNavigatorTests [Fact] public void FullText () { - var strings = new [] { "appricot", "arm", "ta", "target", "text", "egg", "candle" }; + var strings = new [] { "apricot", "arm", "ta", "target", "text", "egg", "candle" }; var n = new CollectionNavigator (strings); var current = 0; @@ -104,13 +105,13 @@ public class CollectionNavigatorTests // still matches text Assert.Equal (strings.IndexOf ("text"), current = n.GetNextMatchingItem (current, 'x')); - // nothing starts texa so it should NOT jump to appricot + // nothing starts texa so it should NOT jump to apricot Assert.Equal (strings.IndexOf ("text"), current = n.GetNextMatchingItem (current, 'a')); Thread.Sleep (n.TypingDelay + 100); - // nothing starts "texa". Since were past timedelay we DO jump to appricot - Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a')); + // nothing starts "texa". Since were past timedelay we DO jump to apricot + Assert.Equal (strings.IndexOf ("apricot"), current = n.GetNextMatchingItem (current, 'a')); } [Theory] @@ -128,13 +129,14 @@ public class CollectionNavigatorTests [InlineData (KeyCode.ShiftMask, false)] public void IsCompatibleKey_Does_Not_Allow_Alt_And_Ctrl_Keys (KeyCode keyCode, bool compatible) { - Assert.Equal (compatible, CollectionNavigatorBase.IsCompatibleKey (keyCode)); + var m = new DefaultCollectionNavigatorMatcher (); + Assert.Equal (compatible, m.IsCompatibleKey (keyCode)); } [Fact] public void MinimizeMovement_False_ShouldMoveIfMultipleMatches () { - var strings = new [] { "$$", "$100.00", "$101.00", "$101.10", "$200.00", "appricot", "c", "car", "cart" }; + var strings = new [] { "$$", "$100.00", "$101.00", "$101.10", "$200.00", "apricot", "c", "car", "cart" }; var current = 0; var n = new CollectionNavigator (strings); Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$")); @@ -146,7 +148,7 @@ public class CollectionNavigatorTests Assert.Equal (strings.IndexOf ("$200.00"), current = n.GetNextMatchingItem (current, "$")); Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$")); // back to top - Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, "a")); + Assert.Equal (strings.IndexOf ("apricot"), current = n.GetNextMatchingItem (current, "a")); Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$")); // back to top Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$100.00")); @@ -170,7 +172,7 @@ public class CollectionNavigatorTests [Fact] public void MinimizeMovement_True_ShouldStayOnCurrentIfMultipleMatches () { - var strings = new [] { "$$", "$100.00", "$101.00", "$101.10", "$200.00", "appricot", "c", "car", "cart" }; + var strings = new [] { "$$", "$100.00", "$101.00", "$101.10", "$200.00", "apricot", "c", "car", "cart" }; var current = 0; var n = new CollectionNavigator (strings); Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$", true)); @@ -250,10 +252,10 @@ public class CollectionNavigatorTests [Fact] public void Symbols () { - var strings = new [] { "$$", "$100.00", "$101.00", "$101.10", "$200.00", "appricot" }; + var strings = new [] { "$$", "$100.00", "$101.00", "$101.10", "$200.00", "apricot" }; var current = 0; var n = new CollectionNavigator (strings); - Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a')); + Assert.Equal (strings.IndexOf ("apricot"), current = n.GetNextMatchingItem (current, 'a')); Assert.Equal ("a", n.SearchString); Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$')); @@ -288,7 +290,7 @@ public class CollectionNavigatorTests [Fact] public void Unicode () { - var strings = new [] { "appricot", "arm", "ta", "丗丙业丞", "丗丙丛", "text", "egg", "candle" }; + var strings = new [] { "apricot", "arm", "ta", "丗丙业丞", "丗丙丛", "text", "egg", "candle" }; var n = new CollectionNavigator (strings); var current = 0; @@ -304,19 +306,19 @@ public class CollectionNavigatorTests // so we should move to the new match Assert.Equal (strings.IndexOf ("丗丙丛"), current = n.GetNextMatchingItem (current, '丛')); - // nothing starts "丗丙丛a". Since were still in the timedelay we do not jump to appricot + // nothing starts "丗丙丛a". Since were still in the timedelay we do not jump to apricot Assert.Equal (strings.IndexOf ("丗丙丛"), current = n.GetNextMatchingItem (current, 'a')); Thread.Sleep (n.TypingDelay + 100); - // nothing starts "丗丙丛a". Since were past timedelay we DO jump to appricot - Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a')); + // nothing starts "丗丙丛a". Since were past timedelay we DO jump to apricot + Assert.Equal (strings.IndexOf ("apricot"), current = n.GetNextMatchingItem (current, 'a')); } [Fact] public void Word () { - var strings = new [] { "appricot", "arm", "bat", "batman", "bates hotel", "candle" }; + var strings = new [] { "apricot", "arm", "bat", "batman", "bates hotel", "candle" }; var current = 0; var n = new CollectionNavigator (strings); Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 'b')); // match bat @@ -338,4 +340,22 @@ public class CollectionNavigatorTests current = n.GetNextMatchingItem (current, ' ') ); // match bates hotel } + [Fact] + public void CustomMatcher_NeverMatches () + { + var strings = new [] { "apricot", "arm", "bat", "batman", "bates hotel", "candle" }; + var current = 0; + var n = new CollectionNavigator (strings); + + var matchNone = new Mock (); + + matchNone.Setup (m => m.IsMatch (It.IsAny (), It.IsAny ())) + .Returns (false); + + n.Matcher = matchNone.Object; + + Assert.Equal (0, current = n.GetNextMatchingItem (current, 'b')); // no matches + Assert.Equal (0, current = n.GetNextMatchingItem (current, 'a')); // no matches + Assert.Equal (0, current = n.GetNextMatchingItem (current, 't')); // no matches + } } diff --git a/Tests/UnitTestsParallelizable/Views/ListViewTests.cs b/Tests/UnitTestsParallelizable/Views/ListViewTests.cs new file mode 100644 index 000000000..aee5db005 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Views/ListViewTests.cs @@ -0,0 +1,120 @@ +using System.Collections.ObjectModel; +using Moq; + +namespace Terminal.Gui.ViewsTests; + +public class ListViewTests +{ + [Fact] + public void ListViewCollectionNavigatorMatcher_DefaultBehaviour () + { + ObservableCollection source = new () { "apricot", "arm", "bat", "batman", "bates hotel", "candle" }; + ListView lv = new ListView { Source = new ListWrapper (source) }; + + // Keys are consumed during navigation + Assert.True (lv.NewKeyDownEvent (Key.B)); + Assert.True (lv.NewKeyDownEvent (Key.A)); + Assert.True (lv.NewKeyDownEvent (Key.T)); + + Assert.Equal ("bat", (string)lv.Source.ToList () [lv.SelectedItem]); + } + + [Fact] + public void ListViewCollectionNavigatorMatcher_IgnoreKeys () + { + ObservableCollection source = new () { "apricot", "arm", "bat", "batman", "bates hotel", "candle" }; + ListView lv = new ListView { Source = new ListWrapper (source) }; + + + var matchNone = new Mock (); + + matchNone.Setup (m => m.IsCompatibleKey (It.IsAny ())) + .Returns (false); + + lv.KeystrokeNavigator.Matcher = matchNone.Object; + + // Keys are ignored because IsCompatibleKey returned false i.e. don't use these keys for navigation + Assert.False (lv.NewKeyDownEvent (Key.B)); + Assert.False (lv.NewKeyDownEvent (Key.A)); + Assert.False (lv.NewKeyDownEvent (Key.T)); + + // assert IsMatch never called + matchNone.Verify (m => m.IsMatch (It.IsAny (), It.IsAny ()), Times.Never ()); + } + + [Fact] + public void ListViewCollectionNavigatorMatcher_OverrideMatching () + { + ObservableCollection source = new () { "apricot", "arm", "bat", "batman", "bates hotel", "candle" }; + ListView lv = new ListView { Source = new ListWrapper (source) }; + + + var matchNone = new Mock (); + + matchNone.Setup (m => m.IsCompatibleKey (It.IsAny ())) + .Returns (true); + + // Match any string starting with b to "candle" (psych!) + matchNone.Setup (m => m.IsMatch (It.IsAny (), It.IsAny ())) + .Returns ((string s, object key) => s.StartsWith ('B') && key?.ToString () == "candle"); + + lv.KeystrokeNavigator.Matcher = matchNone.Object; + // Keys are consumed during navigation + Assert.True (lv.NewKeyDownEvent (Key.B)); + Assert.Equal (5, lv.SelectedItem); + Assert.True (lv.NewKeyDownEvent (Key.A)); + Assert.Equal (5, lv.SelectedItem); + Assert.True (lv.NewKeyDownEvent (Key.T)); + Assert.Equal (5, lv.SelectedItem); + + Assert.Equal ("candle", (string)lv.Source.ToList () [lv.SelectedItem]); + } + + [Fact] + public void ListView_CollectionNavigatorMatcher_KeybindingsOverrideNavigator () + { + ObservableCollection source = new () { "apricot", "arm", "bat", "batman", "bates hotel", "candle" }; + ListView lv = new ListView { Source = new ListWrapper (source) }; + + lv.SetFocus (); + + lv.KeyBindings.Add (Key.B, Command.Down); + + Assert.Equal (-1, lv.SelectedItem); + + // Keys should be consumed to move down the navigation i.e. to apricot + Assert.True (lv.NewKeyDownEvent (Key.B)); + Assert.Equal (0, lv.SelectedItem); + + Assert.True (lv.NewKeyDownEvent (Key.B)); + Assert.Equal (1, lv.SelectedItem); + + // There is no keybinding for Key.C so it hits collection navigator i.e. we jump to candle + Assert.True (lv.NewKeyDownEvent (Key.C)); + Assert.Equal (5, lv.SelectedItem); + } + + [Fact] + public void CollectionNavigatorMatcher_KeybindingsOverrideNavigator () + { + ObservableCollection source = new () { "apricot", "arm", "bat", "batman", "bates hotel", "candle" }; + ListView lv = new ListView { Source = new ListWrapper (source) }; + + lv.SetFocus (); + + lv.KeyBindings.Add (Key.B, Command.Down); + + Assert.Equal (-1, lv.SelectedItem); + + // Keys should be consumed to move down the navigation i.e. to apricot + Assert.True (lv.NewKeyDownEvent (Key.B)); + Assert.Equal (0, lv.SelectedItem); + + Assert.True (lv.NewKeyDownEvent (Key.B)); + Assert.Equal (1, lv.SelectedItem); + + // There is no keybinding for Key.C so it hits collection navigator i.e. we jump to candle + Assert.True (lv.NewKeyDownEvent (Key.C)); + Assert.Equal (5, lv.SelectedItem); + } +} diff --git a/Tests/UnitTestsParallelizable/Views/TableViewTests.cs b/Tests/UnitTestsParallelizable/Views/TableViewTests.cs new file mode 100644 index 000000000..13ec674cd --- /dev/null +++ b/Tests/UnitTestsParallelizable/Views/TableViewTests.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Terminal.Gui.ViewsTests; + +[TestSubject (typeof (TableView))] +public class TableViewTests +{ + [Fact] + public void TableView_CollectionNavigatorMatcher_KeybindingsOverrideNavigator () + { + var dt = new DataTable (); + dt.Columns.Add ("blah"); + + dt.Rows.Add ("apricot"); + dt.Rows.Add ("arm"); + dt.Rows.Add ("bat"); + dt.Rows.Add ("batman"); + dt.Rows.Add ("bates hotel"); + dt.Rows.Add ("candle"); + + var tableView = new TableView (); + tableView.Table = new DataTableSource (dt); + tableView.HasFocus = true; + tableView.KeyBindings.Add (Key.B, Command.Down); + + Assert.Equal (0, tableView.SelectedRow); + + // Keys should be consumed to move down the navigation i.e. to apricot + Assert.True (tableView.NewKeyDownEvent (Key.B)); + Assert.Equal (1, tableView.SelectedRow); + + Assert.True (tableView.NewKeyDownEvent (Key.B)); + Assert.Equal (2, tableView.SelectedRow); + + // There is no keybinding for Key.C so it hits collection navigator i.e. we jump to candle + Assert.True (tableView.NewKeyDownEvent (Key.C)); + Assert.Equal (5, tableView.SelectedRow); + } +} diff --git a/Tests/UnitTestsParallelizable/Views/TreeViewTests.cs b/Tests/UnitTestsParallelizable/Views/TreeViewTests.cs new file mode 100644 index 000000000..774e3ebe8 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Views/TreeViewTests.cs @@ -0,0 +1,40 @@ +using JetBrains.Annotations; + +namespace Terminal.Gui.ViewsTests; + +[TestSubject (typeof (TreeView))] +public class TreeViewTests +{ + + [Fact] + public void TreeView_CollectionNavigatorMatcher_KeybindingsOverrideNavigator () + { + var tree = new TreeView (); + tree.AddObjects ([ + new TreeNode(){ Text="apricot" }, + new TreeNode(){ Text="arm" }, + new TreeNode(){ Text="bat" }, + new TreeNode(){ Text="batman" }, + new TreeNode(){ Text="bates hotel" }, + new TreeNode(){ Text="candle" }, + ]); + + tree.SetFocus (); + + tree.KeyBindings.Add (Key.B, Command.Down); + + Assert.Equal ("apricot", tree.SelectedObject.Text); + + // Keys should be consumed to move down the navigation i.e. to apricot + Assert.True (tree.NewKeyDownEvent (Key.B)); + Assert.NotNull (tree.SelectedObject); + Assert.Equal ("arm", tree.SelectedObject.Text); + + Assert.True (tree.NewKeyDownEvent (Key.B)); + Assert.Equal ("bat", tree.SelectedObject.Text); + + // There is no keybinding for Key.C so it hits collection navigator i.e. we jump to candle + Assert.True (tree.NewKeyDownEvent (Key.C)); + Assert.Equal ("candle", tree.SelectedObject.Text); + } +}