Browse Source

WIP setup for inspecting styles.

Dariusz Komosinski 4 years ago
parent
commit
188faa07c5

+ 208 - 0
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<SetterViewModel> 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<SetterViewModel> 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<StyleViewModel>();
+            PseudoClasses = new ObservableCollection<PseudoClassesViewModel>();
+
+            if (control is StyledElement styledElement)
+            {
+                styledElement.Classes.CollectionChanged += OnClassesChanged;
+
+                var pseudoClassAttributes = styledElement.GetType().GetCustomAttributes<PseudoClassesAttribute>(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<SetterViewModel>();
+
+                    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<StyleViewModel> AppliedStyles { get; }
+
+        public ObservableCollection<PseudoClassesViewModel> 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<PropertyViewModel> 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)

+ 47 - 2
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml

@@ -105,7 +105,7 @@
 
     <GridSplitter Grid.Column="1" />
 
-    <Grid Grid.Column="2" RowDefinitions="Auto,*" >
+    <Grid Grid.Column="2" RowDefinitions="Auto,*, Auto,*,Auto" >
       <TextBlock Grid.Row="0" Text="Layout Visualizer" Margin="4" />
       
       <Grid Grid.Row="1" x:Name="LayoutRoot" Margin="8,0,8,8" RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,Auto">
@@ -148,7 +148,52 @@
           <Rectangle x:Name="VerticalSizeEnd" />
         </Canvas>
       </Grid>
-      
+
+      <TextBlock Grid.Row="2" Text="Styles" Margin="4" />
+
+      <ScrollViewer Grid.Row="3" HorizontalScrollBarVisibility="Disabled">
+        <ItemsControl Items="{Binding AppliedStyles}" >
+          <ItemsControl.ItemTemplate>
+            <DataTemplate>
+              <Border BorderThickness="0,0,0,1" BorderBrush="#6C6C6C">
+                <Expander IsExpanded="True" Margin="0" Padding="8,0" ContentTransition="{x:Null}" IsVisible="{Binding IsActive}" >
+                  <Expander.Header>
+                    <TextBlock Grid.Row="0" Text="{Binding Name}" />
+                  </Expander.Header>
+                  <ItemsControl Margin="20,0,0,0" Grid.Row="1" Items="{Binding Setters}">
+                    <ItemsControl.ItemTemplate>
+                      <DataTemplate>
+                        <StackPanel Orientation="Horizontal" Spacing="2">
+                          <TextBlock Text="{Binding Name}" FontWeight="SemiBold" />
+                          <TextBlock Text=":" />
+                          <Border IsVisible="{Binding IsSpecialKind}" Height="8" Width="8" VerticalAlignment="Center" Background="{Binding KindColor}"/>
+                          <TextBlock Text="{Binding Value}" />
+                        </StackPanel>
+                      </DataTemplate>
+                    </ItemsControl.ItemTemplate>
+                  </ItemsControl>
+                </Expander>
+              </Border>
+            </DataTemplate>
+          </ItemsControl.ItemTemplate>
+        </ItemsControl>
+      </ScrollViewer>
+
+      <Expander Header="Pseudo Classes" Grid.Row="4">
+        <ItemsControl Items="{Binding PseudoClasses}">
+          <ItemsControl.ItemsPanel>
+            <ItemsPanelTemplate>
+              <WrapPanel />
+            </ItemsPanelTemplate>
+          </ItemsControl.ItemsPanel>
+          <ItemsControl.ItemTemplate>
+            <DataTemplate>
+              <CheckBox Margin="2" Content="{Binding Name}" IsChecked="{Binding IsActive, Mode=TwoWay}" />
+            </DataTemplate>
+          </ItemsControl.ItemTemplate>
+        </ItemsControl>
+      </Expander>
+
     </Grid>
   </Grid>
 

+ 30 - 0
src/Avalonia.Styling/StyledElement.cs

@@ -16,6 +16,24 @@ using Avalonia.Styling;
 
 namespace Avalonia
 {
+    public class StyleDiagnostics
+    {
+        public IReadOnlyList<IStyleInstance> AppliedStyles { get; }
+
+        public StyleDiagnostics(IReadOnlyList<IStyleInstance> appliedStyles)
+        {
+            AppliedStyles = appliedStyles;
+        }
+    }
+
+    public static class StyledElementExtensions
+    {
+        public static StyleDiagnostics GetStyleDiagnostics(this StyledElement styledElement)
+        {
+            return styledElement.GetStyleDiagnosticsInternal();
+        }
+    }
+
     /// <summary>
     /// Extends an <see cref="Animatable"/> with the following features:
     /// 
@@ -356,6 +374,18 @@ namespace Avalonia
             }
         }
 
+        internal StyleDiagnostics GetStyleDiagnosticsInternal()
+        {
+            IReadOnlyList<IStyleInstance>? appliedStyles = _appliedStyles;
+
+            if (appliedStyles is null)
+            {
+                appliedStyles = Array.Empty<IStyleInstance>();
+            }
+
+            return new StyleDiagnostics(appliedStyles);
+        }
+
         /// <inheritdoc/>
         void ILogical.NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
         {

+ 2 - 0
src/Avalonia.Styling/Styling/IStyleInstance.cs

@@ -14,6 +14,8 @@ namespace Avalonia.Styling
         /// </summary>
         IStyle Source { get; }
 
+        bool IsActive { get; }
+
         /// <summary>
         /// Instructs the style to start acting upon the control.
         /// </summary>

+ 5 - 4
src/Avalonia.Styling/Styling/StyleInstance.cs

@@ -17,7 +17,6 @@ namespace Avalonia.Styling
         private readonly List<IDisposable>? _animations;
         private readonly IStyleActivator? _activator;
         private readonly Subject<bool>? _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)
                         {