Răsfoiți Sursa

Merge pull request #5991 from AvaloniaUI/feature/devtools-filter-text-box

Feature/devtools filter text box
Max Katz 4 ani în urmă
părinte
comite
df892e4a9c

+ 97 - 0
src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml

@@ -0,0 +1,97 @@
+<Styles xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:controls="using:Avalonia.Diagnostics.Controls"
+        x:CompileBindings="True"
+        x:DataType="controls:FilterTextBox">
+
+  <Design.PreviewWith>
+    <Border Padding="10">
+      <controls:FilterTextBox Width="200" Text="Hello World" />
+    </Border>
+  </Design.PreviewWith>
+
+  <Style Selector="TextBox.filter-text-box">
+    <Setter Property="VerticalContentAlignment" Value="Center" />
+    <Setter Property="InnerRightContent">
+      <Template>
+        <StackPanel Orientation="Horizontal" Spacing="1">
+          <Button Margin="0,0,2,0"
+                  Classes="textBoxClearButton"
+                  ToolTip.Tip="Clear"
+                  Cursor="Hand"
+                  Command="{ReflectionBinding $parent[TextBox].Clear}"
+                  Opacity="0.5" />
+          <ToggleButton Classes="filter-text-box-toggle"
+                        ToolTip.Tip="Match Case"
+                        IsChecked="{Binding $parent[controls:FilterTextBox].UseCaseSensitiveFilter}">
+            <Image>
+              <DrawingImage>
+                <GeometryDrawing Brush="{Binding $parent[ToggleButton].Foreground}">
+                  M7.495 9.052L8.386 11.402H9.477L6.237 3H5.217L2 11.402H3.095L3.933 9.052H7.495ZM5.811 4.453L5.855 4.588L7.173 8.162H4.255L5.562 4.588L5.606 4.453L5.644 4.297L5.676 4.145L5.697 4.019H5.72L5.744 4.145L5.773 4.297L5.811 4.453ZM13.795 10.464V11.4H14.755V7.498C14.755 6.779 14.575 6.226 14.216 5.837C13.857 5.448 13.327 5.254 12.628 5.254C12.429 5.254 12.227 5.273 12.022 5.31C11.817 5.347 11.622 5.394 11.439 5.451C11.256 5.508 11.091 5.569 10.944 5.636C10.797 5.703 10.683 5.765 10.601 5.824V6.808C10.867 6.578 11.167 6.397 11.505 6.268C11.843 6.139 12.194 6.075 12.557 6.075C12.745 6.075 12.915 6.103 13.07 6.16C13.225 6.217 13.357 6.306 13.466 6.427C13.575 6.548 13.659 6.706 13.718 6.899C13.777 7.092 13.806 7.326 13.806 7.599L11.995 7.851C11.651 7.898 11.355 7.977 11.107 8.088C10.859 8.199 10.654 8.339 10.492 8.507C10.33 8.675 10.21 8.868 10.132 9.087C10.054 9.306 10.015 9.546 10.015 9.808C10.015 10.054 10.057 10.283 10.139 10.496C10.221 10.709 10.342 10.893 10.502 11.047C10.662 11.201 10.862 11.323 11.1 11.413C11.338 11.503 11.613 11.548 11.926 11.548C12.328 11.548 12.686 11.456 13.001 11.27C13.316 11.084 13.573 10.816 13.772 10.464H13.795ZM11.667 8.721C11.843 8.657 12.068 8.607 12.341 8.572L13.806 8.367V8.976C13.806 9.222 13.765 9.451 13.683 9.664C13.601 9.877 13.486 10.063 13.34 10.221C13.194 10.379 13.019 10.503 12.816 10.593C12.613 10.683 12.39 10.728 12.148 10.728C11.961 10.728 11.795 10.703 11.653 10.652C11.511 10.601 11.392 10.53 11.296 10.441C11.2 10.352 11.127 10.247 11.076 10.125C11.025 10.003 11 9.873 11 9.732C11 9.568 11.018 9.421 11.055 9.292C11.092 9.163 11.16 9.051 11.257 8.958C11.354 8.865 11.491 8.785 11.667 8.721Z
+                </GeometryDrawing>
+              </DrawingImage>
+            </Image>
+          </ToggleButton>
+          <ToggleButton Classes="filter-text-box-toggle"
+                        ToolTip.Tip="Match Whole Word"
+                        IsChecked="{Binding $parent[controls:FilterTextBox].UseWholeWordFilter}">
+            <Image>
+              <DrawingImage>
+                <GeometryDrawing Brush="{Binding $parent[ToggleButton].Foreground}">
+                  M1 2H15V3H1V2ZM14 4H13V12H14V4ZM11.272 8.387C11.194 8.088 11.073 7.825 10.912 7.601C10.751 7.377 10.547 7.2 10.303 7.071C10.059 6.942 9.769 6.878 9.437 6.878C9.239 6.878 9.057 6.902 8.89 6.951C8.725 7 8.574 7.068 8.437 7.156C8.301 7.244 8.18 7.35 8.072 7.474L7.893 7.732V4.578H7V12H7.893V11.425L8.019 11.6C8.106 11.702 8.208 11.79 8.323 11.869C8.44 11.947 8.572 12.009 8.721 12.055C8.87 12.101 9.035 12.123 9.219 12.123C9.572 12.123 9.885 12.052 10.156 11.911C10.428 11.768 10.655 11.573 10.838 11.325C11.021 11.075 11.159 10.782 11.252 10.446C11.345 10.108 11.392 9.743 11.392 9.349C11.391 9.007 11.352 8.686 11.272 8.387ZM9.793 7.78C9.944 7.851 10.075 7.956 10.183 8.094C10.292 8.234 10.377 8.407 10.438 8.611C10.489 8.785 10.52 8.982 10.527 9.198L10.52 9.323C10.52 9.65 10.487 9.943 10.42 10.192C10.353 10.438 10.259 10.645 10.142 10.806C10.025 10.968 9.882 11.091 9.721 11.172C9.399 11.334 8.961 11.338 8.652 11.187C8.499 11.112 8.366 11.012 8.259 10.891C8.174 10.795 8.103 10.675 8.041 10.524C8.041 10.524 7.862 10.077 7.862 9.577C7.862 9.077 8.041 8.575 8.041 8.575C8.103 8.398 8.177 8.257 8.265 8.145C8.379 8.002 8.521 7.886 8.689 7.8C8.857 7.714 9.054 7.671 9.276 7.671C9.466 7.671 9.64 7.708 9.793 7.78ZM15 13H1V14H15V13ZM2.813 10L2.085 12.031H1L1.025 11.959L3.466 4.87305H4.407L6.892 12.031H5.81L5.032 10H2.813ZM3.934 6.42205H3.912L3.007 9.17505H4.848L3.934 6.42205Z
+                </GeometryDrawing>
+              </DrawingImage>
+            </Image>
+          </ToggleButton>
+          <ToggleButton Classes="filter-text-box-toggle"
+                        ToolTip.Tip="Use Regular Expression"
+                        IsChecked="{Binding $parent[controls:FilterTextBox].UseRegexFilter}">
+            <Image>
+              <DrawingImage>
+                <GeometryDrawing Brush="{Binding $parent[ToggleButton].Foreground}">
+                  M10.0122 2H10.9879V5.11346L13.5489 3.55609L14.034 4.44095L11.4702 6L14.034 7.55905L13.5489 8.44391L10.9879 6.88654V10H10.0122V6.88654L7.45114 8.44391L6.96606 7.55905L9.5299 6L6.96606 4.44095L7.45114 3.55609L10.0122 5.11346V2ZM2 10H6V14H2V10Z
+                </GeometryDrawing>
+              </DrawingImage>
+            </Image>
+          </ToggleButton>
+        </StackPanel>
+      </Template>
+    </Setter>
+  </Style>
+
+  <Style Selector="TextBox.filter-text-box Button.textBoxClearButton">
+    <Setter Property="IsVisible" Value="False" />
+  </Style>
+  <Style Selector="TextBox.filter-text-box[AcceptsReturn=False][IsReadOnly=False]:focus:not(TextBox:empty) Button.textBoxClearButton">
+    <Setter Property="IsVisible" Value="True" />
+  </Style>
+
+  <Style Selector="ToggleButton.filter-text-box-toggle > Image">
+    <Setter Property="Width" Value="16" />
+    <Setter Property="Height" Value="16" />
+    <Setter Property="Opacity" Value="0.5" />
+  </Style>
+  <Style Selector="ToggleButton.filter-text-box-toggle:pointerover > Image">
+    <Setter Property="Opacity" Value="0.7" />
+  </Style>
+  <Style Selector="ToggleButton.filter-text-box-toggle:pressed > Image, ToggleButton.filter-text-box-toggle:checked > Image">
+    <Setter Property="Opacity" Value="0.7" />
+  </Style>
+
+  <Style Selector="ToggleButton.filter-text-box-toggle">
+    <Setter Property="Cursor" Value="Hand" />
+    <Setter Property="Padding" Value="0,1" />
+    <Setter Property="Width" Value="24" />
+    <Setter Property="BorderThickness" Value="1" />
+    <Setter Property="VerticalAlignment" Value="Top" />
+  </Style>
+  <Style Selector="ToggleButton.filter-text-box-toggle /template/ ContentPresenter">
+    <Setter Property="Background" Value="Transparent" />
+    <Setter Property="BorderBrush" Value="Transparent" />
+    <Setter Property="CornerRadius" Value="0" />
+  </Style>
+  <Style Selector="ToggleButton.filter-text-box-toggle:pressed /template/ ContentPresenter, ToggleButton.filter-text-box-toggle:checked /template/ ContentPresenter">
+    <Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentColor}" />
+    <Setter Property="Background" Value="{DynamicResource ThemeAccentColor4}" />
+  </Style>
+</Styles>

+ 52 - 0
src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.cs

@@ -0,0 +1,52 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Styling;
+
+namespace Avalonia.Diagnostics.Controls
+{
+    internal class FilterTextBox : TextBox, IStyleable
+    {
+        public static readonly DirectProperty<FilterTextBox, bool> UseRegexFilterProperty =
+            AvaloniaProperty.RegisterDirect<FilterTextBox, bool>(nameof(UseRegexFilter),
+                o => o.UseRegexFilter, (o, v) => o.UseRegexFilter = v,
+                defaultBindingMode: BindingMode.TwoWay);
+
+        public static readonly DirectProperty<FilterTextBox, bool> UseCaseSensitiveFilterProperty =
+            AvaloniaProperty.RegisterDirect<FilterTextBox, bool>(nameof(UseCaseSensitiveFilter),
+                o => o.UseCaseSensitiveFilter, (o, v) => o.UseCaseSensitiveFilter = v,
+                defaultBindingMode: BindingMode.TwoWay);
+
+        public static readonly DirectProperty<FilterTextBox, bool> UseWholeWordFilterProperty =
+            AvaloniaProperty.RegisterDirect<FilterTextBox, bool>(nameof(UseWholeWordFilter),
+                o => o.UseWholeWordFilter, (o, v) => o.UseWholeWordFilter = v,
+                defaultBindingMode: BindingMode.TwoWay);
+
+        private bool _useRegexFilter, _useCaseSensitiveFilter, _useWholeWordFilter;
+
+        public FilterTextBox()
+        {
+            Classes.Add("filter-text-box");
+        }
+
+        public bool UseRegexFilter
+        {
+            get => _useRegexFilter;
+            set => SetAndRaise(UseRegexFilterProperty, ref _useRegexFilter, value);
+        }
+
+        public bool UseCaseSensitiveFilter
+        {
+            get => _useCaseSensitiveFilter;
+            set => SetAndRaise(UseCaseSensitiveFilterProperty, ref _useCaseSensitiveFilter, value);
+        }
+
+        public bool UseWholeWordFilter
+        {
+            get => _useWholeWordFilter;
+            set => SetAndRaise(UseWholeWordFilterProperty, ref _useWholeWordFilter, value);
+        }
+
+        Type IStyleable.StyleKey => typeof(TextBox);
+    }
+}

+ 4 - 29
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs

@@ -19,7 +19,6 @@ namespace Avalonia.Diagnostics.ViewModels
         private readonly IVisual _control;
         private readonly IDictionary<object, List<PropertyViewModel>> _propertyIndex;
         private AvaloniaPropertyViewModel _selectedProperty;
-        private string _styleFilter;
         private bool _snapshotStyles;
         private bool _showInactiveStyles;
         private string _styleStatus;
@@ -144,12 +143,6 @@ namespace Avalonia.Diagnostics.ViewModels
             set => RaiseAndSetIfChanged(ref _selectedProperty, value);
         }
 
-        public string StyleFilter
-        {
-            get => _styleFilter;
-            set => RaiseAndSetIfChanged(ref _styleFilter, value);
-        }
-
         public bool SnapshotStyles
         {
             get => _snapshotStyles;
@@ -174,11 +167,7 @@ namespace Avalonia.Diagnostics.ViewModels
         {
             base.OnPropertyChanged(e);
 
-            if (e.PropertyName == nameof(StyleFilter))
-            {
-                UpdateStyleFilters();
-            }
-            else if (e.PropertyName == nameof(SnapshotStyles))
+            if (e.PropertyName == nameof(SnapshotStyles))
             {
                 if (!SnapshotStyles)
                 {
@@ -187,19 +176,15 @@ namespace Avalonia.Diagnostics.ViewModels
             }
         }
 
-        private void UpdateStyleFilters()
+        public void UpdateStyleFilters()
         {
-            var filter = StyleFilter;
-            bool hasFilter = !string.IsNullOrEmpty(filter);
-
             foreach (var style in AppliedStyles)
             {
                 var hasVisibleSetter = false;
 
                 foreach (var setter in style.Setters)
                 {
-                    setter.IsVisible =
-                        !hasFilter || setter.Name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;
+                    setter.IsVisible = TreePage.SettersFilter.Filter(setter.Name);
 
                     hasVisibleSetter |= setter.IsVisible;
                 }
@@ -357,17 +342,7 @@ namespace Avalonia.Diagnostics.ViewModels
 
         private bool FilterProperty(object arg)
         {
-            if (!string.IsNullOrWhiteSpace(TreePage.PropertyFilter) && arg is PropertyViewModel property)
-            {
-                if (TreePage.UseRegexFilter)
-                {
-                    return TreePage.FilterRegex?.IsMatch(property.Name) ?? true;
-                }
-
-                return property.Name.IndexOf(TreePage.PropertyFilter, StringComparison.OrdinalIgnoreCase) != -1;
-            }
-
-            return true;
+            return !(arg is PropertyViewModel property) || TreePage.PropertiesFilter.Filter(property.Name);
         }
 
         private class PropertyComparer : IComparer<PropertyViewModel>

+ 5 - 20
src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs

@@ -23,7 +23,6 @@ namespace Avalonia.Diagnostics.ViewModels
         };
 
         private readonly MainViewModel _mainViewModel;
-        private string _eventTypeFilter;
         private FiredEvent _selectedEvent;
         private EventTreeNodeBase _selectedNode;
 
@@ -37,6 +36,9 @@ namespace Avalonia.Diagnostics.ViewModels
                 .Select(g => new EventOwnerTreeNode(g.Key, g, this))
                 .ToArray();
 
+            EventsFilter = new FilterViewModel();
+            EventsFilter.RefreshFilter += (s, e) => UpdateEventFilters();
+
             EnableDefault();
         }
 
@@ -58,11 +60,7 @@ namespace Avalonia.Diagnostics.ViewModels
             set => RaiseAndSetIfChanged(ref _selectedNode, value);
         }
 
-        public string EventTypeFilter
-        {
-            get => _eventTypeFilter;
-            set => RaiseAndSetIfChanged(ref _eventTypeFilter, value);
-        }
+        public FilterViewModel EventsFilter { get; }
 
         public void Clear()
         {
@@ -125,16 +123,6 @@ namespace Avalonia.Diagnostics.ViewModels
             }
         }
 
-        protected override void OnPropertyChanged(PropertyChangedEventArgs e)
-        {
-            base.OnPropertyChanged(e);
-
-            if (e.PropertyName == nameof(EventTypeFilter))
-            {
-                UpdateEventFilters();
-            }
-        }
-
         private void EvaluateNodeEnabled(Func<EventTreeNode, bool> eval)
         {
             void ProcessNode(EventTreeNodeBase node)
@@ -161,9 +149,6 @@ namespace Avalonia.Diagnostics.ViewModels
 
         private void UpdateEventFilters()
         {
-            var filter = EventTypeFilter;
-            bool hasFilter = !string.IsNullOrEmpty(filter);
-
             foreach (var node in Nodes)
             {
                 FilterNode(node, false);
@@ -171,7 +156,7 @@ namespace Avalonia.Diagnostics.ViewModels
 
             bool FilterNode(EventTreeNodeBase node, bool isParentVisible)
             {
-                bool matchesFilter = !hasFilter || node.Text.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;
+                bool matchesFilter = EventsFilter.Filter(node.Text);
                 bool hasVisibleChild = false;
 
                 if (node.Children != null)

+ 124 - 0
src/Avalonia.Diagnostics/Diagnostics/ViewModels/FilterViewModel.cs

@@ -0,0 +1,124 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Text.RegularExpressions;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+    internal class FilterViewModel : ViewModelBase, INotifyDataErrorInfo
+    {
+        private readonly Dictionary<string, string> _errors = new Dictionary<string, string>();
+        private string _filterString = string.Empty;
+        private bool _useRegexFilter, _useCaseSensitiveFilter, _useWholeWordFilter;
+        private string _processedFilter;
+        private Regex _filterRegex;
+
+        public event EventHandler RefreshFilter;
+
+        public bool Filter(string input)
+        {
+            return _filterRegex?.IsMatch(input) ?? true;
+        }
+
+        private void UpdateFilterRegex()
+        {
+            void ClearError()
+            {
+                if (_errors.Remove(nameof(FilterString)))
+                {
+                    ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(FilterString)));
+                }
+            }
+
+            _processedFilter = FilterString.Trim();
+
+            try
+            {
+                var options = RegexOptions.Compiled;
+                var pattern = UseRegexFilter
+                    ? _processedFilter : Regex.Escape(_processedFilter);
+                if (!UseCaseSensitiveFilter)
+                {
+                    options |= RegexOptions.IgnoreCase;
+                }
+                if (UseWholeWordFilter)
+                {
+                    pattern = $"\\b(?:{pattern})\\b";
+                }
+
+                _filterRegex = new Regex(pattern, options);
+                ClearError();
+            }
+            catch (Exception exception)
+            {
+                _errors[nameof(FilterString)] = exception.Message;
+                ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(FilterString)));
+            }
+        }
+
+        public string FilterString
+        {
+            get => _filterString;
+            set
+            {
+                if (RaiseAndSetIfChanged(ref _filterString, value))
+                {
+                    UpdateFilterRegex();
+                    RefreshFilter?.Invoke(this, EventArgs.Empty);
+                }
+            }
+        }
+
+        public bool UseRegexFilter
+        {
+            get => _useRegexFilter;
+            set
+            {
+                if (RaiseAndSetIfChanged(ref _useRegexFilter, value))
+                {
+                    UpdateFilterRegex();
+                    RefreshFilter?.Invoke(this, EventArgs.Empty);
+                }
+            }
+        }
+
+        public bool UseCaseSensitiveFilter
+        {
+            get => _useCaseSensitiveFilter;
+            set
+            {
+                if (RaiseAndSetIfChanged(ref _useCaseSensitiveFilter, value))
+                {
+                    UpdateFilterRegex();
+                    RefreshFilter?.Invoke(this, EventArgs.Empty);
+                }
+            }
+        }
+
+        public bool UseWholeWordFilter
+        {
+            get => _useWholeWordFilter;
+            set
+            {
+                if (RaiseAndSetIfChanged(ref _useWholeWordFilter, value))
+                {
+                    UpdateFilterRegex();
+                    RefreshFilter?.Invoke(this, EventArgs.Empty);
+                }
+            }
+        }
+
+        public IEnumerable GetErrors(string propertyName)
+        {
+            if (_errors.TryGetValue(propertyName, out var error))
+            {
+                yield return error;
+            }
+        }
+
+        public bool HasErrors => _errors.Count > 0;
+
+        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
+    }
+}

+ 12 - 84
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs

@@ -1,29 +1,32 @@
 using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Text.RegularExpressions;
 using Avalonia.Controls;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
-    internal class TreePageViewModel : ViewModelBase, IDisposable, INotifyDataErrorInfo
+    internal class TreePageViewModel : ViewModelBase, IDisposable
     {
-        private readonly Dictionary<string, string> _errors = new Dictionary<string, string>();
         private TreeNode _selectedNode;
         private ControlDetailsViewModel _details;
-        private string _propertyFilter = string.Empty;
-        private bool _useRegexFilter;
 
         public TreePageViewModel(MainViewModel mainView, TreeNode[] nodes)
         {
             MainView = mainView;
             Nodes = nodes;
+
+            PropertiesFilter = new FilterViewModel();
+            PropertiesFilter.RefreshFilter += (s, e) => Details?.PropertiesView.Refresh();
+
+            SettersFilter = new FilterViewModel();
+            SettersFilter.RefreshFilter += (s, e) => Details?.UpdateStyleFilters();
         }
 
         public MainViewModel MainView { get; }
 
+        public FilterViewModel PropertiesFilter { get; }
+
+        public FilterViewModel SettersFilter { get; }
+
         public TreeNode[] Nodes { get; protected set; }
 
         public TreeNode SelectedNode
@@ -31,18 +34,12 @@ namespace Avalonia.Diagnostics.ViewModels
             get => _selectedNode;
             private set
             {
-                var oldDetails = Details;
-
                 if (RaiseAndSetIfChanged(ref _selectedNode, value))
                 {
                     Details = value != null ?
                         new ControlDetailsViewModel(this, value.Visual) :
                         null;
-
-                    if (Details != null && oldDetails != null)
-                    {
-                        Details.StyleFilter = oldDetails.StyleFilter;
-                    }
+                    Details?.UpdateStyleFilters();
                 }
             }
         }
@@ -61,63 +58,6 @@ namespace Avalonia.Diagnostics.ViewModels
             }
         }
 
-        public Regex FilterRegex { get; set; }
-
-        private void UpdateFilterRegex()
-        {
-            void ClearError()
-            {
-                if (_errors.Remove(nameof(PropertyFilter)))
-                {
-                    ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(PropertyFilter)));
-                }
-            }
-
-            if (UseRegexFilter)
-            {
-                try
-                {
-                    FilterRegex = new Regex(PropertyFilter, RegexOptions.Compiled);
-                    ClearError();
-                }
-                catch (Exception exception)
-                {
-                    _errors[nameof(PropertyFilter)] = exception.Message;
-                    ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(PropertyFilter)));
-                }
-            }
-            else
-            {
-                ClearError();
-            }
-        }
-
-        public string PropertyFilter
-        {
-            get => _propertyFilter;
-            set
-            {
-                if (RaiseAndSetIfChanged(ref _propertyFilter, value))
-                {
-                    UpdateFilterRegex();
-                    Details.PropertiesView.Refresh();
-                }
-            }
-        }
-
-        public bool UseRegexFilter
-        {
-            get => _useRegexFilter;
-            set
-            {
-                if (RaiseAndSetIfChanged(ref _useRegexFilter, value))
-                {
-                    UpdateFilterRegex();
-                    Details.PropertiesView.Refresh();
-                }
-            }
-        }
-
         public void Dispose()
         {
             foreach (var node in Nodes)
@@ -194,17 +134,5 @@ namespace Avalonia.Diagnostics.ViewModels
 
             return null;
         }
-
-        public IEnumerable GetErrors(string propertyName)
-        {
-            if (_errors.TryGetValue(propertyName, out var error))
-            {
-                yield return error;
-            }
-        }
-
-        public bool HasErrors => _errors.Count > 0;
-
-        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
     }
 }

+ 19 - 17
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml

@@ -2,6 +2,7 @@
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters"
              xmlns:local="clr-namespace:Avalonia.Diagnostics.Views"
+             xmlns:controls="clr-namespace:Avalonia.Diagnostics.Controls"
              xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
              x:Class="Avalonia.Diagnostics.Views.ControlDetailsView"
              x:Name="Main">
@@ -12,24 +13,19 @@
 
   <Grid ColumnDefinitions="*,Auto,320">
 
-    <Grid Grid.Column="0" ColumnDefinitions="*,Auto,Auto,Auto" RowDefinitions="Auto,*">
-
-      <TextBox Grid.Row="0" 
-               Grid.Column="0"
-               BorderThickness="0"
-               Text="{Binding TreePage.PropertyFilter}"
-               Watermark="Filter properties" />
-
-      <CheckBox Grid.Row="0"
-                Grid.Column="1"
-                Margin="0,0,4,0"
-                Content="Regex"
-                IsChecked="{Binding TreePage.UseRegexFilter}" />
-
+    <Grid Grid.Column="0" RowDefinitions="Auto,*">
+
+      <controls:FilterTextBox Grid.Row="0"
+                              BorderThickness="0"
+                              DataContext="{Binding TreePage.PropertiesFilter}"
+                              Text="{Binding FilterString}"
+                              Watermark="Filter properties"
+                              UseCaseSensitiveFilter="{Binding UseCaseSensitiveFilter}"
+                              UseWholeWordFilter="{Binding UseWholeWordFilter}"
+                              UseRegexFilter="{Binding UseRegexFilter}" />
+      
       <DataGrid Items="{Binding PropertiesView}"
                 Grid.Row="1"
-                Grid.Column="0"
-                Grid.ColumnSpan="2"
                 BorderThickness="0"
                 RowBackground="Transparent"
                 SelectedItem="{Binding SelectedProperty, Mode=TwoWay}"
@@ -71,7 +67,13 @@
             <ToggleButton Margin="2,0,0,0" Grid.Column="3" ToolTip.Tip="Snapshot current styles (Alt+S/Alt+D to enable/disable within debugged window)" Content="Snapshot" IsChecked="{Binding SnapshotStyles}" />
           </Grid>
 
-          <TextBox Grid.Row="1" Margin="2" Grid.Column="0" Watermark="Filter" Text="{Binding StyleFilter}" />
+          <controls:FilterTextBox Grid.Row="1" Margin="2" Grid.Column="0"
+                                  DataContext="{Binding TreePage.SettersFilter}"
+                                  Text="{Binding FilterString}"
+                                  Watermark="Filter setters"
+                                  UseCaseSensitiveFilter="{Binding UseCaseSensitiveFilter}"
+                                  UseWholeWordFilter="{Binding UseWholeWordFilter}"
+                                  UseRegexFilter="{Binding UseRegexFilter}" />
         </Grid>
 
         <ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Disabled">

+ 8 - 1
src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml

@@ -2,6 +2,7 @@
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
              xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters"
+             xmlns:controls="clr-namespace:Avalonia.Diagnostics.Controls"
              x:Class="Avalonia.Diagnostics.Views.EventsPageView"
              Margin="2">
   <UserControl.Styles>
@@ -35,7 +36,13 @@
 
     <Grid Grid.Column="0" RowDefinitions="Auto,*,Auto">
 
-      <TextBox Classes="clearButton" Grid.Row="0" Margin="0,0,0,2" Text="{Binding EventTypeFilter}" Watermark="Search event types" />
+      <controls:FilterTextBox Grid.Row="0" Margin="0,0,0,2"
+                              DataContext="{Binding EventsFilter}"
+                              Text="{Binding FilterString}"
+                              Watermark="Filter events"
+                              UseCaseSensitiveFilter="{Binding UseCaseSensitiveFilter}"
+                              UseWholeWordFilter="{Binding UseWholeWordFilter}"
+                              UseRegexFilter="{Binding UseRegexFilter}" />
 
       <TreeView Grid.Row="1" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}" >
         <TreeView.DataTemplates>

+ 1 - 0
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml

@@ -16,6 +16,7 @@
       <Setter Property="Foreground" Value="Black"/>
     </Style>
     <StyleInclude Source="avares://Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.axaml" />
+    <StyleInclude Source="avares://Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml" />
   </Window.Styles>
   
   <views:MainView/>

+ 4 - 3
src/Avalonia.Themes.Default/TextBox.xaml

@@ -113,13 +113,13 @@
   </Style>
   
   <Style Selector="Button.textBoxClearButton">
+    <Setter Property="Cursor" Value="Arrow" />
     <Setter Property="Template">
       <Setter.Value>
         <ControlTemplate TargetType="Button">
           <Border x:Name="PART_ButtonLayoutBorder"
                   BorderThickness="{TemplateBinding BorderThickness}" 
-                  Background="Transparent" 
-                  Cursor="Arrow">
+                  Background="Transparent">
             <Path x:Name="PART_GlyphElement"
                   Fill="{DynamicResource ThemeForegroundColor}"
                   Data="{StaticResource TextBoxClearButtonData}"
@@ -146,13 +146,14 @@
               <Setter Property="(ToolTip.Tip)" Value="Show Password" />
             </Style>
           </Panel.Styles>
-          <ToggleButton Classes="passwordBoxRevealButton" Background="Transparent" Cursor="Arrow" IsChecked="{Binding $parent[TextBox].RevealPassword, Mode=TwoWay}" />
+          <ToggleButton Classes="passwordBoxRevealButton" Background="Transparent" IsChecked="{Binding $parent[TextBox].RevealPassword, Mode=TwoWay}" />
         </Panel>
       </Template>
     </Setter>
   </Style>
   
   <Style Selector="ToggleButton.passwordBoxRevealButton">
+    <Setter Property="Cursor" Value="Arrow" />
     <Setter Property="Template">
       <Setter.Value>
         <ControlTemplate TargetType="ToggleButton">