diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index fa41eacbeb..6c622ad6c3 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -1,12 +1,131 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; using System.ComponentModel; using System.Linq; +using System.Reflection; using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Markup.Xaml.MarkupExtensions; +using Avalonia.Media; +using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels { + internal class StyleViewModel : ViewModelBase + { + private readonly IStyleInstance _styleInstance; + private bool _isActive; + + public StyleViewModel(IStyleInstance styleInstance, string name, List setters) + { + _styleInstance = styleInstance; + IsActive = styleInstance.IsActive; + Name = name; + Setters = setters; + } + + public bool IsActive + { + get => _isActive; + set => RaiseAndSetIfChanged(ref _isActive, value); + } + + public string Name { get; } + + public List Setters { get; } + + public void Update() + { + IsActive = _styleInstance.IsActive; + } + } + + internal enum ValueKind + { + Regular, + Resource + } + + internal class SetterViewModel : ViewModelBase + { + public string Name { get; } + + public object Value { get; } + + public ValueKind Kind { get; } + + public bool IsSpecialKind => Kind != ValueKind.Regular; + + public IBrush KindColor { get; } + + public SetterViewModel(string name, object value, ValueKind kind) + { + Name = name; + Value = value; + Kind = kind; + + if (Kind == ValueKind.Resource) + { + KindColor = Brushes.Brown; + } + else + { + KindColor = Brushes.Transparent; + } + } + } + + internal class PseudoClassesViewModel : ViewModelBase + { + private readonly StyledElement _source; + private readonly IPseudoClasses _pseudoClasses; + private bool _isActive; + private bool _isUpdating; + + public PseudoClassesViewModel(string name, StyledElement source) + { + Name = name; + _source = source; + _pseudoClasses = _source.Classes; + + Update(); + } + + public string Name { get; } + + public bool IsActive + { + get => _isActive; + set + { + RaiseAndSetIfChanged(ref _isActive, value); + + if (!_isUpdating) + { + _pseudoClasses.Set(Name, value); + } + } + } + + public void Update() + { + try + { + _isUpdating = true; + + IsActive = _source.Classes.Contains(Name); + } + finally + { + _isUpdating = false; + } + } + } + internal class ControlDetailsViewModel : ViewModelBase, IDisposable { private readonly IVisual _control; @@ -43,12 +162,76 @@ namespace Avalonia.Diagnostics.ViewModels { ao.PropertyChanged += ControlPropertyChanged; } + + AppliedStyles = new ObservableCollection(); + PseudoClasses = new ObservableCollection(); + + if (control is StyledElement styledElement) + { + styledElement.Classes.CollectionChanged += OnClassesChanged; + + var pseudoClassAttributes = styledElement.GetType().GetCustomAttributes(true); + + foreach (var classAttribute in pseudoClassAttributes) + { + foreach (var className in classAttribute.PseudoClasses) + { + PseudoClasses.Add(new PseudoClassesViewModel(className, styledElement)); + } + } + + var styleDiagnostics = styledElement.GetStyleDiagnostics(); + + foreach (var appliedStyle in styleDiagnostics.AppliedStyles) + { + var styleSource = appliedStyle.Source; + + var setters = new List(); + + if (styleSource is Style style) + { + foreach (var setter in style.Setters) + { + if (setter is Setter regularSetter) + { + var setterValue = regularSetter.Value; + + ValueKind kind = ValueKind.Regular; + + if (setterValue is DynamicResourceExtension dynResource) + { + var resolved = styledElement.FindResource(dynResource.ResourceKey); + + if (resolved != null) + { + setterValue = $"{resolved} ({dynResource.ResourceKey})"; + } + else + { + setterValue = dynResource.ResourceKey; + } + + kind = ValueKind.Resource; + } + + setters.Add(new SetterViewModel(regularSetter.Property?.Name ?? "?", setterValue, kind)); + } + } + + AppliedStyles.Add(new StyleViewModel(appliedStyle, style.Selector?.ToString() ?? "No selector", setters)); + } + } + } } public TreePageViewModel TreePage { get; } public DataGridCollectionView PropertiesView { get; } + public ObservableCollection AppliedStyles { get; } + + public ObservableCollection PseudoClasses { get; } + public AvaloniaPropertyViewModel SelectedProperty { get => _selectedProperty; @@ -68,6 +251,11 @@ namespace Avalonia.Diagnostics.ViewModels { ao.PropertyChanged -= ControlPropertyChanged; } + + if (_control is StyledElement se) + { + se.Classes.CollectionChanged -= OnClassesChanged; + } } private IEnumerable GetAvaloniaProperties(object o) @@ -129,6 +317,26 @@ namespace Avalonia.Diagnostics.ViewModels property.Update(); } } + + UpdateStyles(); + } + + private void OnClassesChanged(object sender, NotifyCollectionChangedEventArgs e) + { + UpdateStyles(); + } + + private void UpdateStyles() + { + foreach (var style in AppliedStyles) + { + style.Update(); + } + + foreach (var pseudoClass in PseudoClasses) + { + pseudoClass.Update(); + } } private bool FilterProperty(object arg) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index 2e0b6813ba..9b341c1388 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -105,7 +105,7 @@ - + @@ -148,7 +148,52 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index cc8d91462d..2f7ec13775 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -16,6 +16,24 @@ using Avalonia.Styling; namespace Avalonia { + public class StyleDiagnostics + { + public IReadOnlyList AppliedStyles { get; } + + public StyleDiagnostics(IReadOnlyList appliedStyles) + { + AppliedStyles = appliedStyles; + } + } + + public static class StyledElementExtensions + { + public static StyleDiagnostics GetStyleDiagnostics(this StyledElement styledElement) + { + return styledElement.GetStyleDiagnosticsInternal(); + } + } + /// /// Extends an with the following features: /// @@ -356,6 +374,18 @@ namespace Avalonia } } + internal StyleDiagnostics GetStyleDiagnosticsInternal() + { + IReadOnlyList? appliedStyles = _appliedStyles; + + if (appliedStyles is null) + { + appliedStyles = Array.Empty(); + } + + return new StyleDiagnostics(appliedStyles); + } + /// void ILogical.NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { diff --git a/src/Avalonia.Styling/Styling/IStyleInstance.cs b/src/Avalonia.Styling/Styling/IStyleInstance.cs index cb094badd2..cdccc552a3 100644 --- a/src/Avalonia.Styling/Styling/IStyleInstance.cs +++ b/src/Avalonia.Styling/Styling/IStyleInstance.cs @@ -14,6 +14,8 @@ namespace Avalonia.Styling /// IStyle Source { get; } + bool IsActive { get; } + /// /// Instructs the style to start acting upon the control. /// diff --git a/src/Avalonia.Styling/Styling/StyleInstance.cs b/src/Avalonia.Styling/Styling/StyleInstance.cs index 8ca31d654f..830cf49a0d 100644 --- a/src/Avalonia.Styling/Styling/StyleInstance.cs +++ b/src/Avalonia.Styling/Styling/StyleInstance.cs @@ -17,7 +17,6 @@ namespace Avalonia.Styling private readonly List? _animations; private readonly IStyleActivator? _activator; private readonly Subject? _animationTrigger; - private bool _active; public StyleInstance( IStyle source, @@ -29,6 +28,7 @@ namespace Avalonia.Styling Source = source ?? throw new ArgumentNullException(nameof(source)); Target = target ?? throw new ArgumentNullException(nameof(target)); _activator = activator; + IsActive = _activator is null; if (setters is object) { @@ -56,6 +56,7 @@ namespace Avalonia.Styling } } + public bool IsActive { get; private set; } public IStyle Source { get; } public IStyleable Target { get; } @@ -104,15 +105,15 @@ namespace Avalonia.Styling private void ActivatorChanged(bool value) { - if (_active != value) + if (IsActive != value) { - _active = value; + IsActive = value; _animationTrigger?.OnNext(value); if (_setters is object) { - if (_active) + if (IsActive) { foreach (var setter in _setters) {