2
0
Эх сурвалжийг харах

Add ITreeViewFilter (#2599)

Co-authored-by: Tig <[email protected]>
Thomas Nind 2 жил өмнө
parent
commit
8f199cd765

+ 14 - 0
Terminal.Gui/Views/ITreeViewFilter.cs

@@ -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);
+	}
+}

+ 46 - 7
Terminal.Gui/Views/TreeView.cs

@@ -214,6 +214,13 @@ namespace Terminal.Gui {
 
 
 		CursorVisibility desiredCursorVisibility = CursorVisibility.Invisible;
 		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>
 		/// <summary>
 		/// Get / Set the wished cursor when the tree is focused.
 		/// Get / Set the wished cursor when the tree is focused.
 		/// Only applies when <see cref="MultiSelect"/> is true.
 		/// Only applies when <see cref="MultiSelect"/> is true.
@@ -541,7 +548,12 @@ namespace Terminal.Gui {
 			List<Branch<T>> toReturn = new List<Branch<T>> ();
 			List<Branch<T>> toReturn = new List<Branch<T>> ();
 
 
 			foreach (var root in roots.Values) {
 			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);
 			cachedLineMap = new ReadOnlyCollection<Branch<T>> (toReturn);
@@ -551,17 +563,44 @@ namespace Terminal.Gui {
 			return cachedLineMap;
 			return cachedLineMap;
 		}
 		}
 
 
-		private IEnumerable<Branch<T>> AddToLineMap (Branch<T> currentBranch)
+		private bool IsFilterMatch (Branch<T> branch)
+		{
+			return Filter?.IsMatch(branch.Model) ?? true;
+		}
+
+		private IEnumerable<Branch<T>> AddToLineMap (Branch<T> currentBranch,bool parentMatches, out bool match)
 		{
 		{
-			yield return currentBranch;
+			bool weMatch = IsFilterMatch(currentBranch);
+			bool anyChildMatches = false;
+			
+			var toReturn = new List<Branch<T>>();
+			var children = new List<Branch<T>>();
 
 
 			if (currentBranch.IsExpanded) {
 			if (currentBranch.IsExpanded) {
 				foreach (var subBranch in currentBranch.ChildBranches.Values) {
 				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>
 		/// <summary>
@@ -1289,9 +1328,9 @@ namespace Terminal.Gui {
 		}
 		}
 
 
 		/// <summary>
 		/// <summary>
-		/// Clears any cached results of <see cref="BuildLineMap"/>
+		/// Clears any cached results of the tree state.
 		/// </summary>
 		/// </summary>
-		protected void InvalidateLineMap ()
+		public void InvalidateLineMap ()
 		{
 		{
 			cachedLineMap = null;
 			cachedLineMap = null;
 		}
 		}

+ 65 - 0
Terminal.Gui/Views/TreeViewTextFilter.cs

@@ -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;
+		}
+	}
+}

+ 20 - 1
UICatalog/Scenarios/ClassExplorer.cs

@@ -84,11 +84,30 @@ namespace UICatalog.Scenarios {
 
 
 			treeView = new TreeView<object> () {
 			treeView = new TreeView<object> () {
 				X = 0,
 				X = 0,
-				Y = 0,
+				Y = 1,
 				Width = Dim.Percent (50),
 				Width = Dim.Percent (50),
 				Height = Dim.Fill (),
 				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)=>{
+				filter.Text = tfSearch.Text.ToString();
+				if(treeView.SelectedObject != null)
+				{
+					treeView.EnsureVisible(treeView.SelectedObject);
+				}
+			};
+
 			treeView.AddObjects (AppDomain.CurrentDomain.GetAssemblies ());
 			treeView.AddObjects (AppDomain.CurrentDomain.GetAssemblies ());
 			treeView.AspectGetter = GetRepresentation;
 			treeView.AspectGetter = GetRepresentation;
 			treeView.TreeBuilder = new DelegateTreeBuilder<object> (ChildGetter, CanExpand);
 			treeView.TreeBuilder = new DelegateTreeBuilder<object> (ChildGetter, CanExpand);

+ 68 - 0
UnitTests/Views/TreeViewTests.cs

@@ -908,6 +908,74 @@ namespace Terminal.Gui.ViewTests {
 				new [] { tv.ColorScheme.Normal, pink });
 				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.Redraw (tv.Bounds);
+
+			// 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.Redraw (tv.Bounds);
+			// Normal drawing of the tree view
+			TestHelpers.AssertDriverContentsAre (
+@"", output);
+
+
+			// Matches everything
+			filter.Text = "root";
+			tv.Redraw (tv.Bounds);
+			TestHelpers.AssertDriverContentsAre (
+@"
+├-root one
+│ ├─leaf 1
+│ └─leaf 2
+└─root two
+", output);
+			// Matches 2 leaf nodes
+			filter.Text = "leaf";
+			tv.Redraw (tv.Bounds);
+			TestHelpers.AssertDriverContentsAre (
+@"
+├-root one
+│ ├─leaf 1
+│ └─leaf 2
+", output);
+
+			// Matches 1 leaf nodes
+			filter.Text = "leaf 1";
+			tv.Redraw (tv.Bounds);
+			TestHelpers.AssertDriverContentsAre (
+@"
+├-root one
+│ ├─leaf 1
+", output);
+		}
+
 		[Fact, AutoInitShutdown]
 		[Fact, AutoInitShutdown]
 		public void DesiredCursorVisibility_MultiSelect ()
 		public void DesiredCursorVisibility_MultiSelect ()
 		{
 		{