mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 15:57:56 +01:00
* 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 <tig@users.noreply.github.com>
This commit is contained in:
@@ -58,7 +58,7 @@ public class CollectionNavigatorTester : Scenario
|
||||
"$200.00",
|
||||
"$210.99",
|
||||
"$$",
|
||||
"appricot",
|
||||
"apricot",
|
||||
"arm",
|
||||
"丗丙业丞",
|
||||
"丗丙丛",
|
||||
|
||||
@@ -380,11 +380,6 @@ public class UICatalogTop : Toplevel
|
||||
|
||||
public static ObservableCollection<Scenario>? 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<object> firstColumnList = [];
|
||||
|
||||
for (var i = 0; i < _scenarioList.Table.Rows; i++)
|
||||
{
|
||||
firstColumnList.Add (_scenarioList.Table [i, 0]);
|
||||
}
|
||||
|
||||
_scenarioCollectionNav.Collection = firstColumnList;
|
||||
}
|
||||
|
||||
#endregion Category List
|
||||
|
||||
124
Terminal.Gui/Views/CollectionNavigation/CollectionNavigation.cd
Normal file
124
Terminal.Gui/Views/CollectionNavigation/CollectionNavigation.cd
Normal file
@@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ClassDiagram MajorVersion="1" MinorVersion="1">
|
||||
<Comment CommentText="Views that use the CollectionNavigation system">
|
||||
<Position X="0.5" Y="0.5" Height="0.458" Width="1.856" />
|
||||
</Comment>
|
||||
<Comment CommentText="Specialized navigators for each collection type (e.g. list, tree etc)">
|
||||
<Position X="4.646" Y="0.5" Height="0.5" Width="3.169" />
|
||||
</Comment>
|
||||
<Comment CommentText="Shared matching component (users should provide alternative implementations of this class if they want to modify collection navigation behaviour)">
|
||||
<Position X="9.448" Y="0.5" Height="0.708" Width="3.169" />
|
||||
</Comment>
|
||||
<Class Name="Terminal.Gui.CollectionNavigatorBase" Collapsed="true">
|
||||
<Position X="6.25" Y="1.5" Width="2" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAgEAAAAAAAQAAAIAAEAAgAAAAAABAAEAAAAACwAAAA=</HashCode>
|
||||
<FileName>Views\CollectionNavigation\CollectionNavigatorBase.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
<ShowAsAssociation>
|
||||
<Property Name="Matcher" />
|
||||
</ShowAsAssociation>
|
||||
<Lollipop Position="0.2" />
|
||||
</Class>
|
||||
<Class Name="Terminal.Gui.CollectionNavigator" Collapsed="true">
|
||||
<Position X="4.5" Y="3.5" Width="2" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAAAAAAAAAAAQAAAAAAAAgAAAAAAAAAEAAAAAAAAAAA=</HashCode>
|
||||
<FileName>Views\CollectionNavigation\CollectionNavigator.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
<Lollipop Position="0.2" />
|
||||
</Class>
|
||||
<Class Name="Terminal.Gui.DefaultCollectionNavigatorMatcher">
|
||||
<Position X="9.5" Y="2.5" Width="2.75" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAQA=</HashCode>
|
||||
<FileName>Views\CollectionNavigation\DefaultCollectionNavigatorMatcher.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
<Lollipop Position="0.2" />
|
||||
</Class>
|
||||
<Class Name="Terminal.Gui.TableCollectionNavigator" Collapsed="true">
|
||||
<Position X="4.75" Y="6.5" Width="2.25" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAEAAAAIAAAAAA=</HashCode>
|
||||
<FileName>Views\CollectionNavigation\TableCollectionNavigator.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
</Class>
|
||||
<Class Name="Terminal.Gui.ListView" Collapsed="true">
|
||||
<Position X="0.5" Y="4.25" Width="1.5" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAE+ASAkEnAAABAAKGAggYAZJAIAABEAcBAaAwAQIAA=</HashCode>
|
||||
<FileName>Views\ListView.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
<ShowAsAssociation>
|
||||
<Property Name="KeystrokeNavigator" />
|
||||
</ShowAsAssociation>
|
||||
<Lollipop Position="0.2" />
|
||||
</Class>
|
||||
<Class Name="Terminal.Gui.FileDialog" Collapsed="true">
|
||||
<Position X="0.5" Y="5.5" Width="1.75" />
|
||||
<Compartments>
|
||||
<Compartment Name="Nested Types" Collapsed="false" />
|
||||
</Compartments>
|
||||
<TypeIdentifier>
|
||||
<HashCode>iIY4LQFUHDKVIHIESBgigQcFT6GxhBDABGJItBQAwAQ=</HashCode>
|
||||
<FileName>Views\FileDialog.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
<Lollipop Position="0.2" />
|
||||
</Class>
|
||||
<Class Name="Terminal.Gui.FileDialogCollectionNavigator" Collapsed="true">
|
||||
<Position X="4.75" Y="5.5" Width="2.25" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAEAAAAAAAAAAA=</HashCode>
|
||||
<FileName>Views\FileDialogCollectionNavigator.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
</Class>
|
||||
<Class Name="Terminal.Gui.TableView" Collapsed="true" BaseTypeListCollapsed="true">
|
||||
<Position X="0.5" Y="6.5" Width="1.5" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>QwUeAxwgICIAcABIABeR0oBAkhoFGGOBDABgAN3oPEI=</HashCode>
|
||||
<FileName>Views\TableView\TableView.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
<Lollipop Position="0.2" />
|
||||
</Class>
|
||||
<Class Name="Terminal.Gui.TreeView" Collapsed="true">
|
||||
<Position X="0.5" Y="3" Width="1.5" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAA=</HashCode>
|
||||
<FileName>Views\TreeView\TreeView.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
<Lollipop Position="0.2" />
|
||||
</Class>
|
||||
<Class Name="Terminal.Gui.TreeView<T>" Collapsed="true">
|
||||
<Position X="0.5" Y="2" Width="1.5" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>UwAGySBgBSBGMAQgIiCaBDUItJIBSAWwRMQOSgQCwJI=</HashCode>
|
||||
<FileName>Views\TreeView\TreeView.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
<ShowAsAssociation>
|
||||
<Property Name="KeystrokeNavigator" />
|
||||
</ShowAsAssociation>
|
||||
<Lollipop Position="0.2" />
|
||||
</Class>
|
||||
<Interface Name="Terminal.Gui.ICollectionNavigatorMatcher" Collapsed="true">
|
||||
<Position X="9.5" Y="1.5" Width="2.75" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAA=</HashCode>
|
||||
<FileName>Views\CollectionNavigation\ICollectionNavigatorMatcher.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
</Interface>
|
||||
<Interface Name="Terminal.Gui.IListCollectionNavigator" Collapsed="true">
|
||||
<Position X="3.75" Y="2.25" Width="2" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
|
||||
<FileName>Views\CollectionNavigation\IListCollectionNavigator.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
</Interface>
|
||||
<Interface Name="Terminal.Gui.ICollectionNavigator" Collapsed="true">
|
||||
<Position X="3.75" Y="1.5" Width="2" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAgAAAAAAAAAAAAIAAAAAAAAAAAABAAAAAAAACgAAAA=</HashCode>
|
||||
<FileName>Views\CollectionNavigation\ICollectionNavigator.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
</Interface>
|
||||
<Font Name="Segoe UI" Size="9" />
|
||||
</ClassDiagram>
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <inheritdoc cref="CollectionNavigatorBase"/>
|
||||
/// <remarks>This implementation is based on a static <see cref="Collection"/> of objects.</remarks>
|
||||
public class CollectionNavigator : CollectionNavigatorBase
|
||||
internal class CollectionNavigator : CollectionNavigatorBase, IListCollectionNavigator
|
||||
{
|
||||
/// <summary>Constructs a new CollectionNavigator.</summary>
|
||||
public CollectionNavigator () { }
|
||||
@@ -13,7 +13,7 @@ public class CollectionNavigator : CollectionNavigatorBase
|
||||
/// <param name="collection"></param>
|
||||
public CollectionNavigator (IList collection) { Collection = collection; }
|
||||
|
||||
/// <summary>The collection of objects to search. <see cref="object.ToString()"/> is used to search the collection.</summary>
|
||||
/// <inheritdoc/>
|
||||
public IList Collection { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -21,4 +21,4 @@ public class CollectionNavigator : CollectionNavigatorBase
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override int GetCollectionLength () { return Collection.Count; }
|
||||
}
|
||||
}
|
||||
@@ -1,55 +1,31 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
#nullable enable
|
||||
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// Navigates a collection of items using keystrokes. The keystrokes are used to build a search string. The
|
||||
/// <see cref="SearchString"/> is used to find the next item in the collection that matches the search string when
|
||||
/// <see cref="GetNextMatchingItem(int, char)"/> is called.
|
||||
/// <para>
|
||||
/// If the user types keystrokes that can't be found in the collection, the search string is cleared and the next
|
||||
/// item is found that starts with the last keystroke.
|
||||
/// </para>
|
||||
/// <para>If the user pauses keystrokes for a short time (see <see cref="TypingDelay"/>), the search string is cleared.</para>
|
||||
/// </summary>
|
||||
public abstract class CollectionNavigatorBase
|
||||
/// <inheritdoc/>
|
||||
internal abstract class CollectionNavigatorBase : ICollectionNavigator
|
||||
{
|
||||
private DateTime _lastKeystroke = DateTime.Now;
|
||||
private string _searchString = "";
|
||||
|
||||
/// <summary>The comparer function to use when searching the collection.</summary>
|
||||
public StringComparer Comparer { get; set; } = StringComparer.InvariantCultureIgnoreCase;
|
||||
/// <inheritdoc/>
|
||||
public ICollectionNavigatorMatcher Matcher { get; set; } = new DefaultCollectionNavigatorMatcher ();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current search string. This includes the set of keystrokes that have been pressed since the last
|
||||
/// unsuccessful match or after <see cref="TypingDelay"/>) milliseconds. Useful for debugging.
|
||||
/// </summary>
|
||||
/// <inheritdoc/>
|
||||
public string SearchString
|
||||
{
|
||||
get => _searchString;
|
||||
private set
|
||||
{
|
||||
_searchString = value;
|
||||
OnSearchStringChanged (new KeystrokeNavigatorEventArgs (value));
|
||||
OnSearchStringChanged (new (value));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of milliseconds to delay before clearing the search string. The delay is reset on each
|
||||
/// call to <see cref="GetNextMatchingItem(int, char)"/>. The default is 500ms.
|
||||
/// </summary>
|
||||
/// <inheritdoc/>
|
||||
public int TypingDelay { get; set; } = 500;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index of the next item in the collection that matches the current <see cref="SearchString"/> plus the
|
||||
/// provided character (typically from a key press).
|
||||
/// </summary>
|
||||
/// <param name="currentIndex">The index in the collection to start the search from.</param>
|
||||
/// <param name="keyStruck">The character of the key the user pressed.</param>
|
||||
/// <returns>
|
||||
/// The index of the item that matches what the user has typed. Returns <see langword="-1"/> if no item in the
|
||||
/// collection matched.
|
||||
/// </returns>
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if <paramref name="a"/> is a searchable key (e.g. letters, numbers, etc) that are valid to pass
|
||||
/// to this class for search filtering.
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsCompatibleKey (Key a)
|
||||
{
|
||||
Rune rune = a.AsRune;
|
||||
|
||||
return rune != default (Rune) && !Rune.IsControl (rune);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the <see cref="SearchString"/> changes. Useful for debugging. Invokes the
|
||||
/// Raised when the <see cref="SearchString"/> is changed. Useful for debugging. Raises the
|
||||
/// <see cref="SearchStringChanged"/> event.
|
||||
/// </summary>
|
||||
/// <param name="e"></param>
|
||||
public virtual void OnSearchStringChanged (KeystrokeNavigatorEventArgs e) { SearchStringChanged?.Invoke (this, e); }
|
||||
protected virtual void OnSearchStringChanged (KeystrokeNavigatorEventArgs e) { SearchStringChanged?.Invoke (this, e); }
|
||||
|
||||
/// <summary>This event is invoked when <see cref="SearchString"/> changes. Useful for debugging.</summary>
|
||||
[CanBeNull]
|
||||
public event EventHandler<KeystrokeNavigatorEventArgs> SearchStringChanged;
|
||||
/// <summary>This event is raised when <see cref="SearchString"/> is changed. Useful for debugging.</summary>
|
||||
public event EventHandler<KeystrokeNavigatorEventArgs>? SearchStringChanged;
|
||||
|
||||
/// <summary>Returns the collection being navigated element at <paramref name="idx"/>.</summary>
|
||||
/// <returns></returns>
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#nullable enable
|
||||
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="ICollectionNavigatorMatcher"/>, performs
|
||||
/// case-insensitive (see <see cref="Comparer"/>) matching of items based on
|
||||
/// <see cref="object.ToString()"/>.
|
||||
/// </summary>
|
||||
internal class DefaultCollectionNavigatorMatcher : ICollectionNavigatorMatcher
|
||||
{
|
||||
/// <summary>The comparer function to use when searching the collection.</summary>
|
||||
public StringComparison Comparer { get; set; } = StringComparison.InvariantCultureIgnoreCase;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsMatch (string search, object? value) { return value?.ToString ()?.StartsWith (search, Comparer) ?? false; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if <paramref name="key"/> is key searchable key (e.g. letters, numbers, etc) that are valid to pass
|
||||
/// to this class for search filtering.
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
public bool IsCompatibleKey (Key key)
|
||||
{
|
||||
Rune rune = key.AsRune;
|
||||
|
||||
return rune != default && !Rune.IsControl (rune);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#nullable enable
|
||||
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// Navigates a collection of items using keystrokes. The keystrokes are used to build a search string. The
|
||||
/// <see cref="SearchString"/> is used to find the next item in the collection that matches the search string when
|
||||
/// <see cref="GetNextMatchingItem(int, char)"/> is called.
|
||||
/// <para>
|
||||
/// If the user types keystrokes that can't be found in the collection, the search string is cleared and the next
|
||||
/// item is found that starts with the last keystroke.
|
||||
/// </para>
|
||||
/// <para>If the user pauses keystrokes for a short time (see <see cref="TypingDelay"/>), the search string is cleared.</para>
|
||||
/// </summary>
|
||||
public interface ICollectionNavigator
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the number of milliseconds to delay before clearing the search string. The delay is reset on each
|
||||
/// call to <see cref="GetNextMatchingItem(int, char)"/>. The default is 500ms.
|
||||
/// </summary>
|
||||
public int TypingDelay { get; set; }
|
||||
|
||||
/// <summary>This event is invoked when <see cref="SearchString"/> changes. Useful for debugging.</summary>
|
||||
public event EventHandler<KeystrokeNavigatorEventArgs>? SearchStringChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current search string. This includes the set of keystrokes that have been pressed since the last
|
||||
/// unsuccessful match or after <see cref="TypingDelay"/>) milliseconds. Useful for debugging.
|
||||
/// </summary>
|
||||
string SearchString { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Class responsible for deciding whether given entries in the collection match
|
||||
/// the search term the user is typing.
|
||||
/// </summary>
|
||||
ICollectionNavigatorMatcher Matcher { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index of the next item in the collection that matches the current <see cref="SearchString"/> plus the
|
||||
/// provided character (typically from a key press).
|
||||
/// </summary>
|
||||
/// <param name="currentIndex">The index in the collection to start the search from.</param>
|
||||
/// <param name="keyStruck">The character of the key the user pressed.</param>
|
||||
/// <returns>
|
||||
/// The index of the item that matches what the user has typed. Returns <see langword="-1"/> if no item in the
|
||||
/// collection matched.
|
||||
/// </returns>
|
||||
int GetNextMatchingItem (int currentIndex, char keyStruck);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// Determines which keys trigger collection manager navigation
|
||||
/// and how to match typed strings to objects in the collection.
|
||||
/// Default implementation is <see cref="DefaultCollectionNavigatorMatcher"/>.
|
||||
/// </summary>
|
||||
public interface ICollectionNavigatorMatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns true if <paramref name="key"/> is key searchable key (e.g. letters, numbers, etc) that are valid to pass
|
||||
/// to this class for search filtering.
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
bool IsCompatibleKey (Key key);
|
||||
|
||||
/// <summary>
|
||||
/// Return true if the <paramref name="value"/> matches (e.g. starts with)
|
||||
/// the <paramref name="search"/> term.
|
||||
/// </summary>
|
||||
/// <param name="search"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
bool IsMatch (string search, object value);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Collections;
|
||||
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="ICollectionNavigator"/> sub-interface for <see cref="ListView"/> and <see cref="TreeView"/>. See also
|
||||
/// <see cref="ListView.KeystrokeNavigator"/> / <see cref="TreeView.KeystrokeNavigator"/>
|
||||
/// </summary>
|
||||
public interface IListCollectionNavigator : ICollectionNavigator
|
||||
{
|
||||
/// <summary>The collection of objects to search. <see cref="object.ToString()"/> is used to search the collection.</summary>
|
||||
IList Collection { get; set; }
|
||||
}
|
||||
@@ -1,24 +1,24 @@
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>Collection navigator for cycling selections in a <see cref="TableView"/>.</summary>
|
||||
public class TableCollectionNavigator : CollectionNavigatorBase
|
||||
internal class TableCollectionNavigator : CollectionNavigatorBase
|
||||
{
|
||||
private readonly TableView tableView;
|
||||
private readonly TableView _tableView;
|
||||
|
||||
/// <summary>Creates a new instance for navigating the data in the wrapped <paramref name="tableView"/>.</summary>
|
||||
public TableCollectionNavigator (TableView tableView) { this.tableView = tableView; }
|
||||
public TableCollectionNavigator (TableView tableView) { this._tableView = tableView; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override int GetCollectionLength () { return tableView.Table.Rows; }
|
||||
protected override int GetCollectionLength () { return _tableView.Table.Rows; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
/// <summary>State representing a recursive search from <see cref="FileDialogState.Directory"/> downwards.</summary>
|
||||
internal class SearchState : FileDialogState
|
||||
{
|
||||
@@ -1639,4 +1616,4 @@ public class FileDialog : Dialog, IDesignable
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
21
Terminal.Gui/Views/FileDialogCollectionNavigator.cs
Normal file
21
Terminal.Gui/Views/FileDialogCollectionNavigator.cs
Normal file
@@ -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; }
|
||||
}
|
||||
@@ -234,7 +234,7 @@ public class ListView : View, IDesignable
|
||||
/// Gets the <see cref="CollectionNavigator"/> that searches the <see cref="ListView.Source"/> collection as the
|
||||
/// user types.
|
||||
/// </summary>
|
||||
public CollectionNavigator KeystrokeNavigator { get; } = new ();
|
||||
public IListCollectionNavigator KeystrokeNavigator { get; } = new CollectionNavigator();
|
||||
|
||||
/// <summary>Gets or sets the leftmost column that is currently visible (when scrolling horizontally).</summary>
|
||||
/// <value>The left position.</value>
|
||||
@@ -809,27 +809,7 @@ public class ListView : View, IDesignable
|
||||
/// <inheritdoc/>
|
||||
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<Key> 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);
|
||||
|
||||
|
||||
@@ -306,7 +306,7 @@ public class TableView : View, IDesignable
|
||||
}
|
||||
|
||||
/// <summary>Navigator for cycling the selected item in the table by typing. Set to null to disable this feature.</summary>
|
||||
public CollectionNavigatorBase CollectionNavigator { get; set; }
|
||||
public ICollectionNavigator CollectionNavigator { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Horizontal scroll offset. The index of the first column in <see cref="Table"/> 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))
|
||||
|
||||
@@ -350,7 +350,7 @@ public class TreeView<T> : View, ITreeView where T : class
|
||||
/// Gets the <see cref="CollectionNavigator"/> that searches the <see cref="Objects"/> collection as the user
|
||||
/// types.
|
||||
/// </summary>
|
||||
public CollectionNavigator KeystrokeNavigator { get; } = new ();
|
||||
public IListCollectionNavigator KeystrokeNavigator { get; } = new CollectionNavigator();
|
||||
|
||||
/// <summary>Maximum number of nodes that can be expanded in any given branch.</summary>
|
||||
public int MaxDepth { get; set; } = 100;
|
||||
@@ -1206,8 +1206,15 @@ public class TreeView<T> : 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
|
||||
|
||||
@@ -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<string> source = new () { "apricot", "arm", "bat", "batman", "bates hotel", "candle" };
|
||||
ListView lv = new ListView { Source = new ListWrapper<string> (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 ();
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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<ICollectionNavigatorMatcher> ();
|
||||
|
||||
matchNone.Setup (m => m.IsMatch (It.IsAny<string> (), It.IsAny<object> ()))
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
||||
120
Tests/UnitTestsParallelizable/Views/ListViewTests.cs
Normal file
120
Tests/UnitTestsParallelizable/Views/ListViewTests.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using Moq;
|
||||
|
||||
namespace Terminal.Gui.ViewsTests;
|
||||
|
||||
public class ListViewTests
|
||||
{
|
||||
[Fact]
|
||||
public void ListViewCollectionNavigatorMatcher_DefaultBehaviour ()
|
||||
{
|
||||
ObservableCollection<string> source = new () { "apricot", "arm", "bat", "batman", "bates hotel", "candle" };
|
||||
ListView lv = new ListView { Source = new ListWrapper<string> (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<string> source = new () { "apricot", "arm", "bat", "batman", "bates hotel", "candle" };
|
||||
ListView lv = new ListView { Source = new ListWrapper<string> (source) };
|
||||
|
||||
|
||||
var matchNone = new Mock<ICollectionNavigatorMatcher> ();
|
||||
|
||||
matchNone.Setup (m => m.IsCompatibleKey (It.IsAny<Key> ()))
|
||||
.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<string> (), It.IsAny<object> ()), Times.Never ());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ListViewCollectionNavigatorMatcher_OverrideMatching ()
|
||||
{
|
||||
ObservableCollection<string> source = new () { "apricot", "arm", "bat", "batman", "bates hotel", "candle" };
|
||||
ListView lv = new ListView { Source = new ListWrapper<string> (source) };
|
||||
|
||||
|
||||
var matchNone = new Mock<ICollectionNavigatorMatcher> ();
|
||||
|
||||
matchNone.Setup (m => m.IsCompatibleKey (It.IsAny<Key> ()))
|
||||
.Returns (true);
|
||||
|
||||
// Match any string starting with b to "candle" (psych!)
|
||||
matchNone.Setup (m => m.IsMatch (It.IsAny<string> (), It.IsAny<object> ()))
|
||||
.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<string> source = new () { "apricot", "arm", "bat", "batman", "bates hotel", "candle" };
|
||||
ListView lv = new ListView { Source = new ListWrapper<string> (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<string> source = new () { "apricot", "arm", "bat", "batman", "bates hotel", "candle" };
|
||||
ListView lv = new ListView { Source = new ListWrapper<string> (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);
|
||||
}
|
||||
}
|
||||
45
Tests/UnitTestsParallelizable/Views/TableViewTests.cs
Normal file
45
Tests/UnitTestsParallelizable/Views/TableViewTests.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
40
Tests/UnitTestsParallelizable/Views/TreeViewTests.cs
Normal file
40
Tests/UnitTestsParallelizable/Views/TreeViewTests.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user