Fixes #2601 - Adds ability to filter TreeView branches/leaves (v2) (#2604)

* Add ITreeViewFilter

* Fix for v2

* Fix for new private variable name
This commit is contained in:
Thomas Nind
2023-05-10 06:32:45 +01:00
committed by GitHub
parent 5d1fe43362
commit 5317950928
5 changed files with 214 additions and 8 deletions

View File

@@ -0,0 +1,14 @@
namespace Terminal.Gui {
/// <summary>
/// Provides filtering for a <see cref="TreeView"/>.
/// </summary>
public interface ITreeViewFilter<T> where T : class {
/// <summary>
/// Return <see langword="true"/> if the <paramref name="model"/> should
/// be included in the tree.
/// </summary>
bool IsMatch (T model);
}
}

View File

@@ -214,6 +214,13 @@ namespace Terminal.Gui {
CursorVisibility desiredCursorVisibility = CursorVisibility.Invisible;
/// <summary>
/// Interface for filtering which lines of the tree are displayed
/// e.g. to provide text searching. Defaults to <see langword="null"/>
/// (no filtering).
/// </summary>
public ITreeViewFilter<T> Filter = null;
/// <summary>
/// Get / Set the wished cursor when the tree is focused.
/// Only applies when <see cref="MultiSelect"/> is true.
@@ -545,7 +552,12 @@ namespace Terminal.Gui {
List<Branch<T>> toReturn = new List<Branch<T>> ();
foreach (var root in roots.Values) {
toReturn.AddRange (AddToLineMap (root));
var toAdd = AddToLineMap (root, false, out var isMatch);
if(isMatch)
{
toReturn.AddRange (toAdd);
}
}
cachedLineMap = new ReadOnlyCollection<Branch<T>> (toReturn);
@@ -555,17 +567,44 @@ namespace Terminal.Gui {
return cachedLineMap;
}
private IEnumerable<Branch<T>> AddToLineMap (Branch<T> currentBranch)
private bool IsFilterMatch (Branch<T> branch)
{
yield return currentBranch;
return Filter?.IsMatch(branch.Model) ?? true;
}
private IEnumerable<Branch<T>> AddToLineMap (Branch<T> currentBranch,bool parentMatches, out bool match)
{
bool weMatch = IsFilterMatch(currentBranch);
bool anyChildMatches = false;
var toReturn = new List<Branch<T>>();
var children = new List<Branch<T>>();
if (currentBranch.IsExpanded) {
foreach (var subBranch in currentBranch.ChildBranches.Values) {
foreach (var sub in AddToLineMap (subBranch)) {
yield return sub;
foreach (var sub in AddToLineMap (subBranch, weMatch, out var childMatch)) {
if(childMatch)
{
children.Add(sub);
anyChildMatches = true;
}
}
}
}
if(parentMatches || weMatch || anyChildMatches)
{
match = true;
toReturn.Add(currentBranch);
}
else{
match = false;
}
toReturn.AddRange(children);
return toReturn;
}
/// <summary>
@@ -1290,9 +1329,9 @@ namespace Terminal.Gui {
}
/// <summary>
/// Clears any cached results of <see cref="BuildLineMap"/>
/// Clears any cached results of the tree state.
/// </summary>
protected void InvalidateLineMap ()
public void InvalidateLineMap ()
{
cachedLineMap = null;
}

View File

@@ -0,0 +1,65 @@
using System;
namespace Terminal.Gui {
/// <summary>
/// <see cref="ITreeViewFilter{T}"/> implementation which searches the
/// <see cref="TreeView{T}.AspectGetter"/> of the model for the given
/// <see cref="Text"/>.
/// </summary>
/// <typeparam name="T"></typeparam>
public class TreeViewTextFilter<T> : ITreeViewFilter<T> where T : class {
readonly TreeView<T> _forTree;
/// <summary>
/// Creates a new instance of the filter for use with <paramref name="forTree"/>.
/// Set <see cref="Text"/> to begin filtering.
/// </summary>
/// <param name="forTree"></param>
/// <exception cref="ArgumentNullException"></exception>
public TreeViewTextFilter (TreeView<T> forTree)
{
_forTree = forTree ?? throw new ArgumentNullException (nameof (forTree));
}
/// <summary>
/// The case sensitivity of the search match.
/// Defaults to <see cref="StringComparison.OrdinalIgnoreCase"/>.
/// </summary>
public StringComparison Comparer { get; set; } = StringComparison.OrdinalIgnoreCase;
private string text;
/// <summary>
/// The text that will be searched for in the <see cref="TreeView{T}"/>
/// </summary>
public string Text {
get { return text; }
set {
text = value;
RefreshTreeView ();
}
}
private void RefreshTreeView ()
{
_forTree.InvalidateLineMap ();
_forTree.SetNeedsDisplay ();
}
/// <summary>
/// Returns <typeparamref name="T"/> if there is no <see cref="Text"/> or
/// the text matches the <see cref="TreeView{T}.AspectGetter"/> of the
/// <paramref name="model"/>.
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
public bool IsMatch (T model)
{
if (string.IsNullOrWhiteSpace (Text)) {
return true;
}
return _forTree.AspectGetter (model)?.IndexOf (Text, Comparer) != -1;
}
}
}

View File

@@ -83,11 +83,30 @@ namespace UICatalog.Scenarios {
_treeView = new TreeView<object> () {
X = 0,
Y = 0,
Y = 1,
Width = Dim.Percent (50),
Height = Dim.Fill (),
};
var lblSearch = new Label("Search");
var tfSearch = new TextField(){
Width = 20,
X = Pos.Right(lblSearch),
};
Win.Add(lblSearch);
Win.Add(tfSearch);
var filter = new TreeViewTextFilter<object>(_treeView);
_treeView.Filter = filter;
tfSearch.TextChanged += (s,e)=>{
filter.Text = tfSearch.Text.ToString();
if(_treeView.SelectedObject != null)
{
_treeView.EnsureVisible(_treeView.SelectedObject);
}
};
_treeView.AddObjects (AppDomain.CurrentDomain.GetAssemblies ());
_treeView.AspectGetter = GetRepresentation;
_treeView.TreeBuilder = new DelegateTreeBuilder<object> (ChildGetter, CanExpand);

View File

@@ -902,6 +902,75 @@ namespace Terminal.Gui.ViewsTests {
new [] { tv.ColorScheme.Normal, pink });
}
[Fact, AutoInitShutdown]
public void TestTreeView_Filter ()
{
var tv = new TreeView { Width = 20, Height = 10 };
var n1 = new TreeNode ("root one");
var n1_1 = new TreeNode ("leaf 1");
var n1_2 = new TreeNode ("leaf 2");
n1.Children.Add (n1_1);
n1.Children.Add (n1_2);
var n2 = new TreeNode ("root two");
tv.AddObject (n1);
tv.AddObject (n2);
tv.Expand (n1);
tv.ColorScheme = new ColorScheme ();
tv.LayoutSubviews ();
tv.Draw ();
// Normal drawing of the tree view
TestHelpers.AssertDriverContentsAre (
@"
├-root one
│ ├─leaf 1
│ └─leaf 2
└─root two
", output);
var filter = new TreeViewTextFilter<ITreeNode> (tv);
tv.Filter = filter;
// matches nothing
filter.Text = "asdfjhasdf";
tv.Draw ();
// Normal drawing of the tree view
TestHelpers.AssertDriverContentsAre (
@"", output);
// Matches everything
filter.Text = "root";
tv.Draw ();
TestHelpers.AssertDriverContentsAre (
@"
├-root one
│ ├─leaf 1
│ └─leaf 2
└─root two
", output);
// Matches 2 leaf nodes
filter.Text = "leaf";
tv.Draw ();
TestHelpers.AssertDriverContentsAre (
@"
├-root one
│ ├─leaf 1
│ └─leaf 2
", output);
// Matches 1 leaf nodes
filter.Text = "leaf 1";
tv.Draw ();
TestHelpers.AssertDriverContentsAre (
@"
├-root one
│ ├─leaf 1
", output);
}
[Fact, AutoInitShutdown]
public void DesiredCursorVisibility_MultiSelect ()
{