diff --git a/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs index 393779c618..e0a31cc94f 100644 --- a/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -7,52 +7,189 @@ using System.Linq; using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Layout; -using Avalonia.Threading; -using Avalonia.VisualTree; +using Avalonia.Markup.Xaml.Styling; +using Avalonia.Styling; namespace Avalonia.Diagnostics.ViewModels { + using WellKNownProperties = List; + using WellKNownPropertiesReg = Dictionary, List>; + + internal class WellKnownProperty + { + private static IObservable LayoutUpdatedEvent(object ctrl) => Observable.FromEventPattern(ctrl, nameof(Layoutable.LayoutUpdated)); + + private static WellKNownPropertiesReg _registry = new WellKNownPropertiesReg + { + { + o => true, + new WellKNownProperties + { + new WellKnownProperty() + { + Name = "ToString()", + Type = typeof(string), + Getter = o => o.ToString(), + Changed = o => Observable.Never() + } + } + }, + { + o => o is Style, + new WellKNownProperties + { + new WellKnownProperty() + { + Name = "Selector", + Type = typeof(Selector), + Getter = o => (o as Style).Selector.ToString(), + Changed = o => Observable.Never() + } + } + }, + { + o => o is Control, + new WellKNownProperties + { + new WellKnownProperty() + { + Name = nameof(StyledElement.Classes), + Type = typeof(string), + Getter = o => string.Join(" ", (o as StyledElement).Classes), + Setter = (o, v) => (o as StyledElement).Classes = Classes.Parse((v??"").ToString()), + Changed = o => Observable.FromEventPattern((o as StyledElement).Classes, nameof(StyledElement.Classes.CollectionChanged)) + }, + new WellKnownProperty() + { + Name = "Layout State", + Type = typeof(string), + Getter = o => + { + var l = o as ILayoutable; + return $"measured: {l.IsMeasureValid} -> {l.PreviousMeasure} arranged: {l.IsArrangeValid} -> {l.PreviousArrange}"; + }, + Changed = o => LayoutUpdatedEvent(o) + }, + } + }, + { + o => o is Grid, + new WellKNownProperties + { + new WellKnownProperty() + { + Name = nameof(Grid.ColumnDefinitions), + Type = typeof(string), + Getter = o => string.Join(",", (o as Grid).ColumnDefinitions.Select(c=>c.Width.ToString())), + Setter = (o,v) => (o as Grid).ColumnDefinitions = ColumnDefinitions.Parse(v?.ToString()??""), + Changed = o => Observable.Never() + }, + new WellKnownProperty() + { + Name = nameof(Grid.ColumnDefinitions) + " (Actual)", + Type = typeof(string), + Getter = o => string.Join(",", (o as Grid).ColumnDefinitions.Select(c=>c.ActualWidth.ToString())), + Changed = o => LayoutUpdatedEvent(o) + }, + new WellKnownProperty() + { + Name = nameof(Grid.RowDefinitions), + Type = typeof(string), + Getter = o => string.Join(",", (o as Grid).RowDefinitions.Select(c=>c.Height.ToString())), + Setter = (o,v) => (o as Grid).RowDefinitions = RowDefinitions.Parse(v?.ToString()??""), + Changed = o => Observable.Never() + }, + new WellKnownProperty() + { + Name = nameof(Grid.RowDefinitions) + " (Actual)", + Type = typeof(string), + Getter = o => string.Join(",", (o as Grid).RowDefinitions.Select(c=>c.ActualHeight.ToString())), + Changed = o => LayoutUpdatedEvent(o) + }, + } + }, + }; + + public static IEnumerable Get(AvaloniaObject obj) + { + return _registry.Where(v => v.Key(obj)).SelectMany(v => v.Value); + } + + public string Name; + public Type Type; + public Func Getter; + public Action Setter; + public Func> Changed; + } + internal class ControlDetailsViewModel : ViewModelBase, IDisposable { - private IVisual _control; + private object _control; - public ControlDetailsViewModel(IVisual control) + public ControlDetailsViewModel(object avObject) { - if (control is AvaloniaObject avaloniaObject) + if (avObject is AvaloniaObject avaloniaObject) { - var props = AvaloniaPropertyRegistry.Instance.GetRegistered(avaloniaObject) + var props = WellKnownProperty.Get(avaloniaObject) + .Select(x => new PropertyDetails(avaloniaObject, x)) + .ToList(); + + var avProps = AvaloniaPropertyRegistry.Instance.GetRegistered(avaloniaObject) .Select(x => new PropertyDetails(avaloniaObject, x)) .OrderBy(x => x.IsAttached) .ThenBy(x => x.Name) .ToList(); - if (control is Control c) + props.AddRange(avProps); + + if (avObject is Control c) { - var classesProp = new PropertyDetails(c, nameof(c.Classes), - () => string.Join(" ", c.Classes), - v => c.Classes.Replace(Classes.Parse(v as string)), - Observable.FromEventPattern(c.Classes, nameof(c.Classes.CollectionChanged)) - ); + if (c.Parent != null) + { + var attached = AvaloniaPropertyRegistry.Instance.GetRegistered((AvaloniaObject)c.Parent) + .Where(p => p.IsAttached) + .Select(x => new PropertyDetails(avaloniaObject, x)) + .OrderBy(x => x.Name) + .ToList(); - props.Insert(0, classesProp); + props.AddRange(attached); + } + } - var l = c as ILayoutable; - DateTime? last = null; - var layoutProps = new[] + if (avObject is Style style) + { + WellKnownProperty forSetter(ISetter setter) { - new PropertyDetails(c, "Layout Props", - () => $"measured: {l.IsMeasureValid} -> {l.PreviousMeasure} arranged: {l.IsArrangeValid} -> {l.PreviousArrange} ({last?.TimeOfDay})", - null, - Observable.FromEventPattern(c, nameof(c.LayoutUpdated)).Select(_=>(object)(last=DateTime.Now)) - ), - }; - props.InsertRange(0, layoutProps); + if (setter is Setter sett) + { + return new WellKnownProperty() + { + Name = sett.Property.Name, + Type = sett.Property.PropertyType, + Getter = o => sett.Value, + Setter = (o,v) => sett.Value = v, + Changed = o => Observable.Never() + }; + } + + return new WellKnownProperty() + { + Name = setter.GetType().Name, + Type = setter.GetType(), + Getter = o => setter.ToString(), + Changed = o => Observable.Never() + }; + } + + var setters = style.Setters.Select(s => new PropertyDetails(style, forSetter(s))).ToList(); + + props.AddRange(setters); } Properties = props; } - _control = control; + _control = avObject; } public IEnumerable Properties diff --git a/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs index 0b9bd85b4f..5495cc607e 100644 --- a/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs +++ b/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs @@ -12,7 +12,8 @@ namespace Avalonia.Diagnostics.ViewModels public LogicalTreeNode(ILogical logical, TreeNode parent) : base((Control)logical, parent) { - Children = logical.LogicalChildren.CreateDerivedList(x => new LogicalTreeNode(x, this)); + var children = logical.LogicalChildren.CreateDerivedList(x => new LogicalTreeNode(x, this)); + Children = StyleTreeNode.WithStyles(this, children); } public static LogicalTreeNode[] Create(object control) diff --git a/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs b/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs index c0525108df..d0e4d754fd 100644 --- a/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs +++ b/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs @@ -11,7 +11,10 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; using Avalonia.Data; +using Avalonia.Diagnostics.Views; +using Avalonia.Input; using Avalonia.Media; +using Avalonia.Platform; namespace Avalonia.Diagnostics.ViewModels { @@ -19,8 +22,46 @@ namespace Avalonia.Diagnostics.ViewModels { public class ParseTypeConverter : TypeConverter { + private static Dictionary _standardCursors; + + private static string TryGetCursorName(Cursor cursor) + { + if (cursor?.PlatformCursor == null) + return ""; + + if (_standardCursors == null) + { + _standardCursors = new Dictionary(); + try + { + var platform = AvaloniaLocator.Current.GetService(); + + foreach (StandardCursorType c in Enum.GetValues(typeof(StandardCursorType))) + { + _standardCursors[platform.GetCursor(c)] = c.ToString(); + } + } + catch + { + } + } + + return cursor?.PlatformCursor != null && _standardCursors.TryGetValue(cursor.PlatformCursor, out string r) ? r : cursor?.PlatformCursor?.ToString(); + } + private readonly Func _parse; + private static Dictionary> _customToString = new Dictionary>() + { + //TODO: may be override ToString and remove this hardcoded functionality + { typeof(RelativePoint), o => + { + var rp = (RelativePoint)o; + return rp.Unit== RelativeUnit.Absolute?rp.Point.ToString():$"{rp.Point.X*100}%,{rp.Point.Y*100}%"; + } }, + { typeof(Cursor), o => TryGetCursorName(o as Cursor) }, + }; + public static Func TryGetParse(Type type) { var bf = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static; @@ -39,6 +80,17 @@ namespace Avalonia.Diagnostics.ViewModels return (s, c) => parse.Invoke(null, new object[] { s }); } + parse = type.GetMethod("Parse", bf); + if (parse?.ReturnParameter?.ParameterType == type) + { + var pars = parse.GetParameters(); + //parse with string parameter and default second argument + if (pars.Length == 2 && pars[0].ParameterType == typeof(string) && pars[1].IsOptional) + { + return (s, c) => parse.Invoke(null, new object[] { s, Type.Missing }); + } + } + return null; } @@ -48,23 +100,21 @@ namespace Avalonia.Diagnostics.ViewModels } public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - return sourceType == typeof(string); - } + => sourceType == typeof(string); public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) - { - return _parse((string)value, culture); - } + => _parse((string)value, culture); + + public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) + => _customToString.TryGetValue(value?.GetType() ?? typeof(object), out var ts) ? ts(value) : value?.ToString(); + + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + => destinationType == typeof(string); } private static Dictionary _converters = new Dictionary() { - {typeof(TimeSpan), new Markup.Xaml.Converters.TimeSpanTypeConverter() }, - {typeof(FontFamily), new Markup.Xaml.Converters.FontFamilyTypeConverter() }, - {typeof(int), null}, - {typeof(double), null}, - {typeof(string), null} + //here hard coded type converters if needed }; static public TypeConverter TryGetTypeConverter(Type type) @@ -97,33 +147,36 @@ namespace Avalonia.Diagnostics.ViewModels private object _originalValue; private object _value; private bool _isChanged = false; - private string _priority; + private string _priority = ""; private string _diagnostic; - private AvaloniaObject _object; private AvaloniaProperty _property; + private WellKnownProperty _wellKnownProperty; private TypeConverter _typeConverter; - private IEnumerable _possibleValues; - private Func _getter; - private Action _setter; + private IEnumerable _hintValues; private bool _setActive = false; - private static TypeConverter TryGetTypeConverter(AvaloniaProperty property) + private static TypeConverter TryGetTypeConverter(Type propertyType) { + if (propertyType == null) + { + return null; + } + TypeConverter result; - if (_typeConverters.TryGetValue(property.PropertyType, out result)) + if (_typeConverters.TryGetValue(propertyType, out result)) return result; - result = AvaloniaTypeConverters.TryGetTypeConverter(property.PropertyType) ?? - TypeDescriptor.GetConverter(property.PropertyType); + result = AvaloniaTypeConverters.TryGetTypeConverter(propertyType) ?? + TypeDescriptor.GetConverter(propertyType); if (result?.CanConvertFrom(typeof(string)) == false) { result = null; } - return _typeConverters[property.PropertyType] = result; + return _typeConverters[propertyType] = result; } public PropertyDetails(AvaloniaObject o, AvaloniaProperty property) @@ -132,11 +185,10 @@ namespace Avalonia.Diagnostics.ViewModels $"[{property.OwnerType.Name}.{property.Name}]" : property.Name; - _typeConverter = property.IsReadOnly ? null : TryGetTypeConverter(property); + _typeConverter = TryGetTypeConverter(property.PropertyType); IsAttached = property.IsAttached; - IsReadOnly = property.IsReadOnly || _typeConverter == null; + IsReadOnly = property.IsReadOnly || !(_typeConverter?.CanConvertFrom(typeof(string)) ?? false); bool first = true; - // TODO: Unsubscribe when view model is deactivated. _disposable = o.GetObservable(property).Where(_ => !_setActive).Subscribe(x => { var diagnostic = o.GetDiagnostic(property); @@ -160,20 +212,26 @@ namespace Avalonia.Diagnostics.ViewModels _property = property; } - public PropertyDetails(AvaloniaObject o, string propertyName, Func getter, Action setter, IObservable changed) + public PropertyDetails(AvaloniaObject o, WellKnownProperty property) { + _wellKnownProperty = property; _object = o; - Name = propertyName; - _getter = getter; - _setter = setter; - IsReadOnly = setter == null; - _originalValue = _getter(); - SetValue(_getter(), false); + Name = _wellKnownProperty.Name; + _typeConverter = TryGetTypeConverter(_wellKnownProperty.Type); + IsReadOnly = !(_wellKnownProperty.Setter != null && (_typeConverter?.CanConvertFrom(typeof(string)) ?? true)); + + var getter = _wellKnownProperty.Getter; + + if (_typeConverter != null) + { + getter = x => _typeConverter.ConvertTo(_wellKnownProperty.Getter(x), typeof(string)) ?? "(null)"; + } + + _originalValue = getter(o); + SetValue(_originalValue, false); var inpc = o as INotifyPropertyChanged; - changed = changed ?? Observable.FromEventPattern - (v => inpc.PropertyChanged += v, v => inpc.PropertyChanged -= v) - .Where(v => v.EventArgs.PropertyName == propertyName); - _disposable = changed.Where(_ => !_setActive).Subscribe(_ => SetValue(_getter() ?? "(null)", false)); + var changed = _wellKnownProperty.Changed(_object) ?? inpc.GetObservable(Name); + _disposable = changed.Where(_ => !_setActive).Subscribe(_ => SetValue(getter(o) ?? "(null)", false)); } private static Dictionary _typespossibleValues = new Dictionary(); @@ -182,26 +240,32 @@ namespace Avalonia.Diagnostics.ViewModels { string[] result; - if (_typespossibleValues.TryGetValue(_property.PropertyType, out result)) + var propertyType = _property?.PropertyType ?? _wellKnownProperty?.Type ?? typeof(object); + + if (_typespossibleValues.TryGetValue(propertyType, out result)) return result; if (_property != null) { - if (_property.PropertyType.IsEnum) + if (propertyType.IsEnum) { - result = Enum.GetNames(_property.PropertyType); + result = Enum.GetNames(propertyType); } - else if (_property.PropertyType == typeof(IBrush)) + else if (propertyType == typeof(IBrush)) { result = typeof(Brushes).GetProperties().Select(p => p.Name).ToArray(); } - else if (_property.PropertyType == typeof(bool)) + else if (propertyType == typeof(bool) || propertyType == typeof(bool?)) { result = new[] { "True", "False" }; } + else if (propertyType == typeof(Cursor)) + { + result = Enum.GetNames(typeof(StandardCursorType)); + } } - return _typespossibleValues[_property.PropertyType] = result; + return _typespossibleValues[propertyType] = result ?? Array.Empty(); } public string Name { get; } @@ -236,7 +300,7 @@ namespace Avalonia.Diagnostics.ViewModels { string stringValue = (value as string)?.TrimStart(' '); - if (setback && !IsReadOnly) + if (setback && !IsReadOnly && _disposable != null) { try { @@ -244,9 +308,9 @@ namespace Avalonia.Diagnostics.ViewModels null : (_typeConverter?.ConvertFrom(stringValue) ?? stringValue); _setActive = true; - if (_setter != null) + if (_wellKnownProperty != null) { - _setter(propValue); + _wellKnownProperty?.Setter?.Invoke(_object, propValue); } else { @@ -304,24 +368,30 @@ namespace Avalonia.Diagnostics.ViewModels set { this.RaiseAndSetIfChanged(ref _hasValueError, value); } } - public async Task> PossibleValuesPopulator(string text, CancellationToken token) + public async Task> HintValuesPopulator(string text, CancellationToken token) { if (text.Equals(Value)) - return Enumerable.Empty(); + return Array.Empty(); await Task.Delay(100, token); - return PossibleValues; + if (token.IsCancellationRequested) + { + return Array.Empty(); + } + + return HintValues; } public void Dispose() { _disposable?.Dispose(); + _disposable = null; } - public IEnumerable PossibleValues + public IEnumerable HintValues { - get => _possibleValues ?? (_possibleValues = GetPossibleValues()); + get => _hintValues ?? (_hintValues = GetPossibleValues()); } } } diff --git a/src/Avalonia.Diagnostics/ViewModels/StylesNode.cs b/src/Avalonia.Diagnostics/ViewModels/StylesNode.cs new file mode 100644 index 0000000000..66b4a2196b --- /dev/null +++ b/src/Avalonia.Diagnostics/ViewModels/StylesNode.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Styling; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal class StyleTreeNode : TreeNode + { + public static IAvaloniaReadOnlyList WithStyles(TreeNode node, IAvaloniaReadOnlyList children) where T : TreeNode + { + if (node.Visual is Control ctrl && (ctrl.Styles.Count > 0 || ctrl is TopLevel)) + { + var result = new AvaloniaList(); + if (ctrl is TopLevel) + { + result.Add(new StyleTreeNode(Application.Current.Styles, node)); + } + if (ctrl.Styles.Count > 0) + { + result.Add(new StyleTreeNode(ctrl.Styles, node)); + } + var cnt = result.Count; + children.ForEachItem((i, v) => result.Insert(i + cnt, v), + (i, v) => result.RemoveAt(i + cnt), + () => result.RemoveRange(cnt, result.Count - cnt), + true); + return result; + } + return children; + } + public StyleTreeNode(IStyle style, TreeNode parent) : base((IAvaloniaObject)style, parent) + { + Children = new AvaloniaList(((style as Styles)?.OfType - +