diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml new file mode 100644 index 0000000000..52720e652f --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.cs new file mode 100644 index 0000000000..66fad557d5 --- /dev/null +++ b/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 UseRegexFilterProperty = + AvaloniaProperty.RegisterDirect(nameof(UseRegexFilter), + o => o.UseRegexFilter, (o, v) => o.UseRegexFilter = v, + defaultBindingMode: BindingMode.TwoWay); + + public static readonly DirectProperty UseCaseSensitiveFilterProperty = + AvaloniaProperty.RegisterDirect(nameof(UseCaseSensitiveFilter), + o => o.UseCaseSensitiveFilter, (o, v) => o.UseCaseSensitiveFilter = v, + defaultBindingMode: BindingMode.TwoWay); + + public static readonly DirectProperty UseWholeWordFilterProperty = + AvaloniaProperty.RegisterDirect(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); + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index 32592559e5..b1ff8ae98d 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -19,7 +19,6 @@ namespace Avalonia.Diagnostics.ViewModels private readonly IVisual _control; private readonly IDictionary> _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 diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs index dd85fcf14c..7a157dec62 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs +++ b/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 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) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FilterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FilterViewModel.cs new file mode 100644 index 0000000000..0d472a5d8f --- /dev/null +++ b/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 _errors = new Dictionary(); + 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 ErrorsChanged; + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs index 6b779cd6ac..85a7cb69a3 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs +++ b/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 _errors = new Dictionary(); 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 ErrorsChanged; } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index ab651b2a06..4b37438993 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/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 @@ - - - - - - + + + + - + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml index d16fc40aac..a9c2688a18 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml +++ b/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"> @@ -35,7 +36,13 @@ - + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml index 7628ee2d8c..70d70f0b79 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml @@ -16,6 +16,7 @@ + diff --git a/src/Avalonia.Themes.Default/TextBox.xaml b/src/Avalonia.Themes.Default/TextBox.xaml index 29b611279e..37598d4b79 100644 --- a/src/Avalonia.Themes.Default/TextBox.xaml +++ b/src/Avalonia.Themes.Default/TextBox.xaml @@ -113,13 +113,13 @@ - +