From 212a08345a52f52c9d05d124a5959b73d9549c01 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Fri, 4 Feb 2011 22:24:16 +0100 Subject: [PATCH] Implemented search filter for TreeView. --- ILSpy/AssemblyListTreeNode.cs | 1 - ILSpy/AssemblyTreeNode.cs | 9 ++ ILSpy/CueBannerService.cs | 198 +++++++++++++++++++++++ ILSpy/EventTreeNode.cs | 8 + ILSpy/FieldTreeNode.cs | 8 + ILSpy/FilterSettings.cs | 12 ++ ILSpy/ILSpy.csproj | 1 + ILSpy/ILSpyTreeNode.cs | 36 ++++- ILSpy/MainWindow.xaml | 19 ++- ILSpy/MainWindow.xaml.cs | 9 ++ ILSpy/MethodTreeNode.cs | 8 + ILSpy/NamespaceTreeNode.cs | 10 +- ILSpy/PropertyTreeNode.cs | 8 + ILSpy/TypeTreeNode.cs | 10 ++ SharpTreeView/SharpTreeNodeCollection.cs | 8 +- SharpTreeView/SharpTreeNodeView.cs | 2 + 16 files changed, 332 insertions(+), 15 deletions(-) create mode 100644 ILSpy/CueBannerService.cs diff --git a/ILSpy/AssemblyListTreeNode.cs b/ILSpy/AssemblyListTreeNode.cs index 620b5668d..2ea490d4c 100644 --- a/ILSpy/AssemblyListTreeNode.cs +++ b/ILSpy/AssemblyListTreeNode.cs @@ -48,7 +48,6 @@ namespace ICSharpCode.ILSpy if (assemblyList == null) throw new ArgumentNullException("assemblyList"); this.assemblyList = assemblyList; - this.FilterSettings = new FilterSettings(); // default filter } public override bool CanDelete(SharpTreeNode[] nodes) diff --git a/ILSpy/AssemblyTreeNode.cs b/ILSpy/AssemblyTreeNode.cs index 4175a5777..765a519fc 100644 --- a/ILSpy/AssemblyTreeNode.cs +++ b/ILSpy/AssemblyTreeNode.cs @@ -197,5 +197,14 @@ namespace ICSharpCode.ILSpy return null; } } + + public override FilterResult Filter(FilterSettings settings) + { + // avoid accessing this.AssemblyDefinition (waiting for background thread) if settings.SearchTerm == null + if (settings.SearchTerm == null || settings.SearchTermMatches(this.AssemblyDefinition.Name.Name)) + return FilterResult.Match; + else + return FilterResult.Recurse; + } } } diff --git a/ILSpy/CueBannerService.cs b/ILSpy/CueBannerService.cs new file mode 100644 index 000000000..e9d3919c5 --- /dev/null +++ b/ILSpy/CueBannerService.cs @@ -0,0 +1,198 @@ +// Copyright (c) 2008 Jason Kemp +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Documents; +using System.Windows.Media; + +namespace ICSharpCode.ILSpy +{ + /// + /// Watermark for text boxes; from http://www.ageektrapped.com/blog/the-missing-net-4-cue-banner-in-wpf-i-mean-watermark-in-wpf/. + /// + public static class CueBannerService + { + //there is absolutely no way to write this statement out + //to look pretty + public static readonly DependencyProperty CueBannerProperty = DependencyProperty.RegisterAttached( + "CueBanner", typeof (object), typeof (CueBannerService), + new FrameworkPropertyMetadata("", CueBannerPropertyChanged)); + + public static object GetCueBanner(Control control) + { + return control.GetValue(CueBannerProperty); + } + + public static void SetCueBanner(Control control, object value) + { + control.SetValue(CueBannerProperty, value); + } + + private static void CueBannerPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + Control control = (Control)d; + control.Loaded += control_Loaded; + if (d is ComboBox || d is TextBox) + { + control.GotFocus += control_GotFocus; + control.LostFocus += control_Loaded; + } + if (d is ItemsControl && !(d is ComboBox)) + { + ItemsControl i = (ItemsControl) d; + //for Items property + i.ItemContainerGenerator.ItemsChanged += ItemsChanged; + itemsControls.Add(i.ItemContainerGenerator, i); + //for ItemsSource property + DependencyPropertyDescriptor prop = + DependencyPropertyDescriptor.FromProperty(ItemsControl.ItemsSourceProperty, i.GetType()); + prop.AddValueChanged(i, ItemsSourceChanged); + } + } + + private static readonly Dictionary itemsControls = new Dictionary(); + private static void ItemsSourceChanged(object sender, EventArgs e) + { + ItemsControl c = (ItemsControl)sender; + if (c.ItemsSource != null) + RemoveCueBanner(c); + else + ShowCueBanner(c); + } + + private static void ItemsChanged(object sender, ItemsChangedEventArgs e) + { + ItemsControl control; + if (itemsControls.TryGetValue(sender, out control)) + { + if (e.ItemCount > 0) + RemoveCueBanner(control); + else + ShowCueBanner(control); + } + } + + private static void control_GotFocus(object sender, RoutedEventArgs e) + { + Control c = (Control)sender; + if (ShouldShowCueBanner(c)) + { + RemoveCueBanner(c); + } + } + + private static void control_Loaded(object sender, RoutedEventArgs e) + { + Control control = (Control)sender; + if (ShouldShowCueBanner(control)) + { + ShowCueBanner(control); + } + } + + private static void RemoveCueBanner(UIElement control) + { + AdornerLayer layer = AdornerLayer.GetAdornerLayer(control); + + Adorner[] adorners = layer.GetAdorners(control); + if (adorners == null) return; + foreach (Adorner adorner in adorners) + { + if (adorner is CueBannerAdorner) + { + adorner.Visibility = Visibility.Hidden; + layer.Remove(adorner); + } + } + } + + private static void ShowCueBanner(Control control) + { + AdornerLayer layer = AdornerLayer.GetAdornerLayer(control); + layer.Add(new CueBannerAdorner(control, GetCueBanner(control))); + } + + private static bool ShouldShowCueBanner(Control c) + { + DependencyProperty dp = GetDependencyProperty(c); + if (dp == null) return true; + return c.GetValue(dp).Equals(""); + } + + private static DependencyProperty GetDependencyProperty (Control control) + { + if (control is ComboBox) + return ComboBox.TextProperty; + if (control is TextBoxBase) + return TextBox.TextProperty; + return null; + } + } + + internal class CueBannerAdorner : Adorner + { + private readonly ContentPresenter contentPresenter; + + public CueBannerAdorner(UIElement adornedElement, object cueBanner) : + base(adornedElement) + { + this.IsHitTestVisible = false; + + contentPresenter = new ContentPresenter(); + contentPresenter.Content = cueBanner; + contentPresenter.Opacity = 0.7; + contentPresenter.Margin = + new Thickness(Control.Margin.Left + Control.Padding.Left, + Control.Margin.Top + Control.Padding.Top, 0, 0); + } + + private Control Control + { + get { return (Control) this.AdornedElement; } + } + + protected override Visual GetVisualChild(int index) + { + return contentPresenter; + } + + protected override int VisualChildrenCount + { + get { return 1; } + } + + protected override Size MeasureOverride(Size constraint) + { + contentPresenter.Measure(Control.RenderSize); + return Control.RenderSize; + } + + protected override Size ArrangeOverride(Size finalSize) + { + contentPresenter.Arrange(new Rect(finalSize)); + return finalSize; + } + } +} diff --git a/ILSpy/EventTreeNode.cs b/ILSpy/EventTreeNode.cs index ff1b3017f..8a726c224 100644 --- a/ILSpy/EventTreeNode.cs +++ b/ILSpy/EventTreeNode.cs @@ -60,5 +60,13 @@ namespace ICSharpCode.ILSpy this.Children.Add(new MethodTreeNode(m)); } } + + public override FilterResult Filter(FilterSettings settings) + { + if (settings.SearchTermMatches(ev.Name)) + return FilterResult.Match; + else + return FilterResult.Hidden; + } } } diff --git a/ILSpy/FieldTreeNode.cs b/ILSpy/FieldTreeNode.cs index 4b8fcee36..5f3fcc9b4 100644 --- a/ILSpy/FieldTreeNode.cs +++ b/ILSpy/FieldTreeNode.cs @@ -48,5 +48,13 @@ namespace ICSharpCode.ILSpy return Images.Field; } } + + public override FilterResult Filter(FilterSettings settings) + { + if (settings.SearchTermMatches(field.Name)) + return FilterResult.Match; + else + return FilterResult.Hidden; + } } } diff --git a/ILSpy/FilterSettings.cs b/ILSpy/FilterSettings.cs index b82ab0180..4b25fa7b6 100644 --- a/ILSpy/FilterSettings.cs +++ b/ILSpy/FilterSettings.cs @@ -24,6 +24,11 @@ namespace ICSharpCode.ILSpy /// /// Represents the filters applied to the tree view. /// + /// + /// This class is mutable; but the ILSpyTreeNode filtering assumes that filter settings are immutable. + /// Thus, the main window will use one mutable instance (for data-binding), and assign a new clone to the ILSpyTreeNodes whenever the main + /// mutable instance changes. + /// public class FilterSettings : INotifyPropertyChanged { string searchTerm; @@ -38,6 +43,13 @@ namespace ICSharpCode.ILSpy } } + public bool SearchTermMatches(string text) + { + if (searchTerm == null) + return true; + return text.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) >= 0; + } + bool showInternalApi; public bool ShowInternalApi { diff --git a/ILSpy/ILSpy.csproj b/ILSpy/ILSpy.csproj index 5e470fd6b..e4148e053 100644 --- a/ILSpy/ILSpy.csproj +++ b/ILSpy/ILSpy.csproj @@ -73,6 +73,7 @@ + diff --git a/ILSpy/ILSpyTreeNode.cs b/ILSpy/ILSpyTreeNode.cs index 685362e5b..f8489db54 100644 --- a/ILSpy/ILSpyTreeNode.cs +++ b/ILSpy/ILSpyTreeNode.cs @@ -38,11 +38,18 @@ namespace ICSharpCode.ILSpy } } + public SharpTreeNodeCollection VisibleChildren { + get { return base.Children; } + } + protected abstract void OnFilterSettingsChanged(); public virtual FilterResult Filter(FilterSettings settings) { - return FilterResult.Match; + if (string.IsNullOrEmpty(settings.SearchTerm)) + return FilterResult.Match; + else + return FilterResult.Hidden; } public virtual void Decompile(Language language, ITextOutput output) @@ -52,8 +59,18 @@ namespace ICSharpCode.ILSpy public enum FilterResult { + /// + /// Hides the node. + /// Hidden, - Match + /// + /// Shows the node. + /// + Match, + /// + /// Hides the node only if all children are hidden. + /// + Recurse } /// @@ -97,10 +114,15 @@ namespace ICSharpCode.ILSpy FilterChild(child); } } - + void FilterChild(T child) { - switch (child.Filter(this.FilterSettings)) { + FilterResult r; + if (this.FilterSettings == null) + r = FilterResult.Match; + else + r = child.Filter(this.FilterSettings); + switch (r) { case FilterResult.Hidden: // don't add to base.Children break; @@ -108,6 +130,12 @@ namespace ICSharpCode.ILSpy base.Children.Add(child); child.FilterSettings = StripSearchTerm(this.FilterSettings); break; + case FilterResult.Recurse: + child.FilterSettings = this.FilterSettings; + child.EnsureLazyChildren(); + if (child.VisibleChildren.Count > 0) + base.Children.Add(child); + break; default: throw new InvalidEnumArgumentException(); } diff --git a/ILSpy/MainWindow.xaml b/ILSpy/MainWindow.xaml index abb7e5140..11070a008 100644 --- a/ILSpy/MainWindow.xaml +++ b/ILSpy/MainWindow.xaml @@ -1,6 +1,7 @@  - - + + + + + + - + { string name; @@ -44,5 +44,13 @@ namespace ICSharpCode.ILSpy public override object Icon { get { return Images.Namespace; } } + + public override FilterResult Filter(FilterSettings settings) + { + if (settings.SearchTermMatches(name)) + return FilterResult.Match; + else + return FilterResult.Recurse; + } } } diff --git a/ILSpy/PropertyTreeNode.cs b/ILSpy/PropertyTreeNode.cs index bea981450..c7ab300a9 100644 --- a/ILSpy/PropertyTreeNode.cs +++ b/ILSpy/PropertyTreeNode.cs @@ -60,5 +60,13 @@ namespace ICSharpCode.ILSpy this.Children.Add(new MethodTreeNode(m)); } } + + public override FilterResult Filter(FilterSettings settings) + { + if (settings.SearchTermMatches(property.Name)) + return FilterResult.Match; + else + return FilterResult.Hidden; + } } } diff --git a/ILSpy/TypeTreeNode.cs b/ILSpy/TypeTreeNode.cs index d0df8522b..552df5033 100644 --- a/ILSpy/TypeTreeNode.cs +++ b/ILSpy/TypeTreeNode.cs @@ -77,6 +77,16 @@ namespace ICSharpCode.ILSpy } } + public override FilterResult Filter(FilterSettings settings) + { + if (settings.ShowInternalApi == false && IsPublicAPI) + return FilterResult.Hidden; + if (settings.SearchTermMatches(type.Name)) + return FilterResult.Match; + else + return FilterResult.Recurse; + } + protected override void LoadChildren() { if (type.BaseType != null || type.HasInterfaces) diff --git a/SharpTreeView/SharpTreeNodeCollection.cs b/SharpTreeView/SharpTreeNodeCollection.cs index 9e51d94f9..681ffa787 100644 --- a/SharpTreeView/SharpTreeNodeCollection.cs +++ b/SharpTreeView/SharpTreeNodeCollection.cs @@ -34,10 +34,14 @@ namespace ICSharpCode.TreeView protected override void ClearItems() { - foreach (var node in this) { + /*foreach (var node in this) { node.Parent = null; } - base.ClearItems(); + base.ClearItems();*/ + + // workaround for bug (reproducable when using ILSpy search filter) + while (Count > 0) + RemoveAt(Count - 1); } protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) diff --git a/SharpTreeView/SharpTreeNodeView.cs b/SharpTreeView/SharpTreeNodeView.cs index b85c1104a..5077a17fd 100644 --- a/SharpTreeView/SharpTreeNodeView.cs +++ b/SharpTreeView/SharpTreeNodeView.cs @@ -143,6 +143,8 @@ namespace ICSharpCode.TreeView else { result -= 19; } + if (result < 0) + throw new InvalidOperationException(); return result; } }