csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
527 lines
18 KiB
527 lines
18 KiB
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.Data;
|
|
using Avalonia.Markup.Xaml.MarkupExtensions;
|
|
using Avalonia.Styling;
|
|
using Avalonia.VisualTree;
|
|
|
|
namespace Avalonia.Diagnostics.ViewModels
|
|
{
|
|
internal class ControlDetailsViewModel : ViewModelBase, IDisposable
|
|
{
|
|
private readonly IAvaloniaObject _avaloniaObject;
|
|
private IDictionary<object, PropertyViewModel[]>? _propertyIndex;
|
|
private PropertyViewModel? _selectedProperty;
|
|
private DataGridCollectionView? _propertiesView;
|
|
private bool _snapshotStyles;
|
|
private bool _showInactiveStyles;
|
|
private string? _styleStatus;
|
|
private object? _selectedEntity;
|
|
private readonly Stack<(string Name,object Entry)> _selectedEntitiesStack = new();
|
|
private string? _selectedEntityName;
|
|
private string? _selectedEntityType;
|
|
private bool _showImplementedInterfaces;
|
|
|
|
public ControlDetailsViewModel(TreePageViewModel treePage, IAvaloniaObject avaloniaObject)
|
|
{
|
|
_avaloniaObject = avaloniaObject;
|
|
|
|
TreePage = treePage;
|
|
Layout = avaloniaObject is IVisual
|
|
? new ControlLayoutViewModel((IVisual)avaloniaObject)
|
|
: default;
|
|
|
|
NavigateToProperty(_avaloniaObject, (_avaloniaObject as IControl)?.Name ?? _avaloniaObject.ToString());
|
|
|
|
AppliedStyles = new ObservableCollection<StyleViewModel>();
|
|
PseudoClasses = new ObservableCollection<PseudoClassViewModel>();
|
|
|
|
if (avaloniaObject 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 PseudoClassViewModel(className, styledElement));
|
|
}
|
|
}
|
|
|
|
var styleDiagnostics = styledElement.GetStyleDiagnostics();
|
|
|
|
// We need to place styles without activator first, such styles will be overwritten by ones with activators.
|
|
foreach (var appliedStyle in styleDiagnostics.AppliedStyles.OrderBy(s => s.HasActivator))
|
|
{
|
|
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
|
|
&& regularSetter.Property != null)
|
|
{
|
|
var setterValue = regularSetter.Value;
|
|
|
|
var resourceInfo = GetResourceInfo(setterValue);
|
|
|
|
SetterViewModel setterVm;
|
|
|
|
if (resourceInfo.HasValue)
|
|
{
|
|
var resourceKey = resourceInfo.Value.resourceKey;
|
|
var resourceValue = styledElement.FindResource(resourceKey);
|
|
|
|
setterVm = new ResourceSetterViewModel(regularSetter.Property, resourceKey, resourceValue, resourceInfo.Value.isDynamic);
|
|
}
|
|
else
|
|
{
|
|
var isBinding = IsBinding(setterValue);
|
|
|
|
if (isBinding)
|
|
{
|
|
setterVm = new BindingSetterViewModel(regularSetter.Property, setterValue);
|
|
}
|
|
else
|
|
{
|
|
setterVm = new SetterViewModel(regularSetter.Property, setterValue);
|
|
}
|
|
}
|
|
|
|
setters.Add(setterVm);
|
|
}
|
|
}
|
|
|
|
AppliedStyles.Add(new StyleViewModel(appliedStyle, style.Selector?.ToString() ?? "No selector", setters));
|
|
}
|
|
}
|
|
|
|
UpdateStyles();
|
|
}
|
|
}
|
|
|
|
private (object resourceKey, bool isDynamic)? GetResourceInfo(object? value)
|
|
{
|
|
if (value is StaticResourceExtension staticResource)
|
|
{
|
|
return (staticResource.ResourceKey, false);
|
|
}
|
|
else if (value is DynamicResourceExtension dynamicResource
|
|
&& dynamicResource.ResourceKey != null)
|
|
{
|
|
return (dynamicResource.ResourceKey, true);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private bool IsBinding(object? value)
|
|
{
|
|
switch (value)
|
|
{
|
|
case Binding:
|
|
case CompiledBindingExtension:
|
|
case TemplateBinding:
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public TreePageViewModel TreePage { get; }
|
|
|
|
public DataGridCollectionView? PropertiesView
|
|
{
|
|
get => _propertiesView;
|
|
private set => RaiseAndSetIfChanged(ref _propertiesView, value);
|
|
}
|
|
|
|
public ObservableCollection<StyleViewModel> AppliedStyles { get; }
|
|
|
|
public ObservableCollection<PseudoClassViewModel> PseudoClasses { get; }
|
|
|
|
public object? SelectedEntity
|
|
{
|
|
get => _selectedEntity;
|
|
set => RaiseAndSetIfChanged(ref _selectedEntity, value);
|
|
}
|
|
|
|
public string? SelectedEntityName
|
|
{
|
|
get => _selectedEntityName;
|
|
set => RaiseAndSetIfChanged(ref _selectedEntityName, value);
|
|
}
|
|
|
|
public string? SelectedEntityType
|
|
{
|
|
get => _selectedEntityType;
|
|
set => RaiseAndSetIfChanged(ref _selectedEntityType, value);
|
|
}
|
|
|
|
public PropertyViewModel? SelectedProperty
|
|
{
|
|
get => _selectedProperty;
|
|
set => RaiseAndSetIfChanged(ref _selectedProperty, value);
|
|
}
|
|
|
|
public bool SnapshotStyles
|
|
{
|
|
get => _snapshotStyles;
|
|
set => RaiseAndSetIfChanged(ref _snapshotStyles, value);
|
|
}
|
|
|
|
public bool ShowInactiveStyles
|
|
{
|
|
get => _showInactiveStyles;
|
|
set => RaiseAndSetIfChanged(ref _showInactiveStyles, value);
|
|
}
|
|
|
|
public string? StyleStatus
|
|
{
|
|
get => _styleStatus;
|
|
set => RaiseAndSetIfChanged(ref _styleStatus, value);
|
|
}
|
|
|
|
public ControlLayoutViewModel? Layout { get; }
|
|
|
|
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
|
|
{
|
|
base.OnPropertyChanged(e);
|
|
|
|
if (e.PropertyName == nameof(SnapshotStyles))
|
|
{
|
|
if (!SnapshotStyles)
|
|
{
|
|
UpdateStyles();
|
|
}
|
|
}
|
|
}
|
|
|
|
public void UpdateStyleFilters()
|
|
{
|
|
foreach (var style in AppliedStyles)
|
|
{
|
|
var hasVisibleSetter = false;
|
|
|
|
foreach (var setter in style.Setters)
|
|
{
|
|
setter.IsVisible = TreePage.SettersFilter.Filter(setter.Name);
|
|
|
|
hasVisibleSetter |= setter.IsVisible;
|
|
}
|
|
|
|
style.IsVisible = hasVisibleSetter;
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_avaloniaObject is INotifyPropertyChanged inpc)
|
|
{
|
|
inpc.PropertyChanged -= ControlPropertyChanged;
|
|
}
|
|
|
|
if (_avaloniaObject is AvaloniaObject ao)
|
|
{
|
|
ao.PropertyChanged -= ControlPropertyChanged;
|
|
}
|
|
|
|
if (_avaloniaObject is StyledElement se)
|
|
{
|
|
se.Classes.CollectionChanged -= OnClassesChanged;
|
|
}
|
|
}
|
|
|
|
private IEnumerable<PropertyViewModel> GetAvaloniaProperties(object o)
|
|
{
|
|
if (o is AvaloniaObject ao)
|
|
{
|
|
return AvaloniaPropertyRegistry.Instance.GetRegistered(ao)
|
|
.Union(AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(ao.GetType()))
|
|
.Select(x => new AvaloniaPropertyViewModel(ao, x));
|
|
}
|
|
else
|
|
{
|
|
return Enumerable.Empty<AvaloniaPropertyViewModel>();
|
|
}
|
|
}
|
|
|
|
private IEnumerable<PropertyViewModel> GetClrProperties(object o, bool showImplementedInterfaces)
|
|
{
|
|
foreach (var p in GetClrProperties(o, o.GetType()))
|
|
{
|
|
yield return p;
|
|
}
|
|
|
|
if (showImplementedInterfaces)
|
|
{
|
|
foreach (var i in o.GetType().GetInterfaces())
|
|
{
|
|
foreach (var p in GetClrProperties(o, i))
|
|
{
|
|
yield return p;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private IEnumerable<PropertyViewModel> GetClrProperties(object o, Type t)
|
|
{
|
|
return t.GetProperties()
|
|
.Where(x => x.GetIndexParameters().Length == 0)
|
|
.Select(x => new ClrPropertyViewModel(o, x));
|
|
}
|
|
|
|
private void ControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
|
|
{
|
|
if (_propertyIndex is { } && _propertyIndex.TryGetValue(e.Property, out var properties))
|
|
{
|
|
foreach (var property in properties)
|
|
{
|
|
property.Update();
|
|
}
|
|
}
|
|
|
|
Layout?.ControlPropertyChanged(sender, e);
|
|
}
|
|
|
|
private void ControlPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
{
|
|
if (e.PropertyName != null
|
|
&& _propertyIndex is { }
|
|
&& _propertyIndex.TryGetValue(e.PropertyName, out var properties))
|
|
{
|
|
foreach (var property in properties)
|
|
{
|
|
property.Update();
|
|
}
|
|
}
|
|
|
|
if (!SnapshotStyles)
|
|
{
|
|
UpdateStyles();
|
|
}
|
|
}
|
|
|
|
private void OnClassesChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
|
{
|
|
if (!SnapshotStyles)
|
|
{
|
|
UpdateStyles();
|
|
}
|
|
}
|
|
|
|
private void UpdateStyles()
|
|
{
|
|
int activeCount = 0;
|
|
|
|
foreach (var style in AppliedStyles)
|
|
{
|
|
style.Update();
|
|
|
|
if (style.IsActive)
|
|
{
|
|
activeCount++;
|
|
}
|
|
}
|
|
|
|
var propertyBuckets = new Dictionary<AvaloniaProperty, List<SetterViewModel>>();
|
|
|
|
foreach (var style in AppliedStyles)
|
|
{
|
|
if (!style.IsActive)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var setter in style.Setters)
|
|
{
|
|
if (propertyBuckets.TryGetValue(setter.Property, out var setters))
|
|
{
|
|
foreach (var otherSetter in setters)
|
|
{
|
|
otherSetter.IsActive = false;
|
|
}
|
|
|
|
setter.IsActive = true;
|
|
|
|
setters.Add(setter);
|
|
}
|
|
else
|
|
{
|
|
setter.IsActive = true;
|
|
|
|
setters = new List<SetterViewModel> { setter };
|
|
|
|
propertyBuckets.Add(setter.Property, setters);
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (var pseudoClass in PseudoClasses)
|
|
{
|
|
pseudoClass.Update();
|
|
}
|
|
|
|
StyleStatus = $"Styles ({activeCount}/{AppliedStyles.Count} active)";
|
|
}
|
|
|
|
private bool FilterProperty(object arg)
|
|
{
|
|
return !(arg is PropertyViewModel property) || TreePage.PropertiesFilter.Filter(property.Name);
|
|
}
|
|
|
|
private class PropertyComparer : IComparer<PropertyViewModel>
|
|
{
|
|
public static PropertyComparer Instance { get; } = new PropertyComparer();
|
|
|
|
public int Compare(PropertyViewModel? x, PropertyViewModel? y)
|
|
{
|
|
var groupX = GroupIndex(x?.Group);
|
|
var groupY = GroupIndex(y?.Group);
|
|
|
|
if (groupX != groupY)
|
|
{
|
|
return groupX - groupY;
|
|
}
|
|
else
|
|
{
|
|
return string.CompareOrdinal(x?.Name, y?.Name);
|
|
}
|
|
}
|
|
|
|
private int GroupIndex(string? group)
|
|
{
|
|
switch (group)
|
|
{
|
|
case "Properties": return 0;
|
|
case "Attached Properties": return 1;
|
|
case "CLR Properties": return 2;
|
|
default: return 3;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ApplySelectedProperty()
|
|
{
|
|
var selectedProperty = SelectedProperty;
|
|
var selectedEntity = SelectedEntity;
|
|
var selectedEntityName = SelectedEntityName;
|
|
if (selectedEntity == null
|
|
|| selectedProperty == null
|
|
|| selectedProperty.PropertyType == typeof(string)
|
|
|| selectedProperty.PropertyType.IsValueType
|
|
)
|
|
return;
|
|
|
|
object? property;
|
|
if (selectedProperty.Key is AvaloniaProperty avaloniaProperty)
|
|
{
|
|
property = (_selectedEntity as IControl)?.GetValue(avaloniaProperty);
|
|
}
|
|
else
|
|
{
|
|
property = selectedEntity.GetType().GetProperties()
|
|
.FirstOrDefault(pi => pi.Name == selectedProperty.Name
|
|
&& pi.DeclaringType == selectedProperty.DeclaringType
|
|
&& pi.PropertyType.Name == selectedProperty.PropertyType.Name)
|
|
?.GetValue(selectedEntity);
|
|
}
|
|
if (property == null) return;
|
|
_selectedEntitiesStack.Push((Name:selectedEntityName!,Entry:selectedEntity));
|
|
NavigateToProperty(property, selectedProperty.Name);
|
|
}
|
|
|
|
public void ApplyParentProperty()
|
|
{
|
|
if (_selectedEntitiesStack.Any())
|
|
{
|
|
var property = _selectedEntitiesStack.Pop();
|
|
NavigateToProperty(property.Entry, property.Name);
|
|
}
|
|
}
|
|
|
|
protected void NavigateToProperty(object o, string? entityName)
|
|
{
|
|
var oldSelectedEntity = SelectedEntity;
|
|
if (oldSelectedEntity is IAvaloniaObject ao1)
|
|
{
|
|
ao1.PropertyChanged -= ControlPropertyChanged;
|
|
}
|
|
else if (oldSelectedEntity is INotifyPropertyChanged inpc1)
|
|
{
|
|
inpc1.PropertyChanged -= ControlPropertyChanged;
|
|
}
|
|
|
|
SelectedEntity = o;
|
|
SelectedEntityName = entityName;
|
|
SelectedEntityType = o.ToString();
|
|
var properties = GetAvaloniaProperties(o)
|
|
.Concat(GetClrProperties(o, _showImplementedInterfaces))
|
|
.OrderBy(x => x, PropertyComparer.Instance)
|
|
.ThenBy(x => x.Name)
|
|
.ToArray();
|
|
|
|
_propertyIndex = properties.GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.ToArray());
|
|
|
|
var view = new DataGridCollectionView(properties);
|
|
view.GroupDescriptions.Add(new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group)));
|
|
view.Filter = FilterProperty;
|
|
PropertiesView = view;
|
|
|
|
if (o is IAvaloniaObject ao2)
|
|
{
|
|
ao2.PropertyChanged += ControlPropertyChanged;
|
|
}
|
|
else if (o is INotifyPropertyChanged inpc2)
|
|
{
|
|
inpc2.PropertyChanged += ControlPropertyChanged;
|
|
}
|
|
}
|
|
|
|
internal void SelectProperty(AvaloniaProperty property)
|
|
{
|
|
SelectedProperty = null;
|
|
|
|
if (SelectedEntity != _avaloniaObject)
|
|
{
|
|
NavigateToProperty(_avaloniaObject, (_avaloniaObject as IControl)?.Name ?? _avaloniaObject.ToString());
|
|
}
|
|
|
|
if (PropertiesView is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (object o in PropertiesView)
|
|
{
|
|
if (o is AvaloniaPropertyViewModel propertyVm && propertyVm.Property == property)
|
|
{
|
|
SelectedProperty = propertyVm;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
internal void UpdatePropertiesView(bool showImplementedInterfaces)
|
|
{
|
|
_showImplementedInterfaces = showImplementedInterfaces;
|
|
SelectedProperty = null;
|
|
NavigateToProperty(_avaloniaObject, (_avaloniaObject as IControl)?.Name ?? _avaloniaObject.ToString());
|
|
}
|
|
}
|
|
}
|
|
|