Browse Source

feat(DevTools): Pin properties (#13371)

* feat(DevTools): Pin properties

* fix: Address review

* fix: revert using

* feat: Show Pin ToggleButton on pointer over.
pull/13506/head
workgroupengineering 3 years ago
committed by GitHub
parent
commit
52cbe29916
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 39
      src/Avalonia.Diagnostics/Assets/Icons.axaml
  2. 25
      src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToImageConverter.cs
  3. 12
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
  4. 12
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
  5. 126
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  6. 8
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  7. 44
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs
  8. 11
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
  9. 50
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml

39
src/Avalonia.Diagnostics/Assets/Icons.axaml

@ -0,0 +1,39 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:ClassModifier="internal">
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
<SolidColorBrush Color="Black" x:Key="PinColor"/>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush Color="White" x:Key="PinColor"/>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<!-- Add Resources Here -->
<DrawingGroup x:Key="Pin_xaml">
<DrawingGroup.ClipGeometry>
<RectangleGeometry Rect="0.0,-960.0,960.0,960.0"/>
</DrawingGroup.ClipGeometry>
<GeometryDrawing Brush="{DynamicResource PinColor}">
<GeometryDrawing.Geometry>
<PathGeometry Figures="m 640 -480 l 80 80 v 80 H 520 v 240 l -40 40 l -40 -40 v -240 H 240 v -80 l 80 -80 v -280 h -40 v -80 h 400 v 80 h -40 v 280 Z m -286 80 h 252 l -46 -46 v -314 H 400 v 314 l -46 46 Z m 126 0 Z" FillRule="NonZero"/>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
<DrawingGroup x:Key="unpin_xaml">
<DrawingGroup.ClipGeometry>
<RectangleGeometry Rect="0.0,-960.0,960.0,960.0"/>
</DrawingGroup.ClipGeometry>
<GeometryDrawing Brush="{DynamicResource PinColor}">
<GeometryDrawing.Geometry>
<PathGeometry Figures="m 440 -600 l 80 -80 h 80 v 200 h 240 l 40 40 l -40 40 H 600 v 200 h -80 l -80 -80 H 160 v 40 H 80 v -400 h 80 v 40 z m 80 286 v -252 l -46 46 H 160 v 160 h 314 z m 0 -126 z" FillRule="NonZero"/>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
<DrawingImage Drawing="{StaticResource unpin_xaml}" x:Key="UnpinIcon"/>
<DrawingImage Drawing="{StaticResource Pin_xaml}" x:Key="PinIcon"/>
</ResourceDictionary>

25
src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToImageConverter.cs

@ -0,0 +1,25 @@
using System;
using System.Globalization;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.Media;
namespace Avalonia.Diagnostics.Converters;
internal class BoolToImageConverter : IValueConverter
{
public IImage? TrueImage { get; set; }
public IImage? FalseImage { get; set; }
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) =>
value switch
{
true => TrueImage,
false => FalseImage,
_ => null
};
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
BindingOperations.DoNothing;
}

12
src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs

@ -1,4 +1,5 @@
using System;
using System.ComponentModel;
using Avalonia.Data;
namespace Avalonia.Diagnostics.ViewModels
@ -49,7 +50,7 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
public override string Group => _group;
public override string Group => IsPinned ? "Pinned" : _group;
public override Type? DeclaringType { get; }
public override Type PropertyType => _propertyType;
@ -114,5 +115,14 @@ namespace Avalonia.Diagnostics.ViewModels
}
RaisePropertyChanged(nameof(Type));
}
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.PropertyName == nameof(IsPinned))
{
RaisePropertyChanged(nameof(Group));
}
}
}
}

12
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs

@ -1,4 +1,5 @@
using System;
using System.ComponentModel;
using System.Reflection;
namespace Avalonia.Diagnostics.ViewModels
@ -36,7 +37,7 @@ namespace Avalonia.Diagnostics.ViewModels
public PropertyInfo Property { get; }
public override object Key => Name;
public override string Name { get; }
public override string Group => "CLR Properties";
public override string Group => IsPinned ? "Pinned" : "CLR Properties";
public override Type AssignedType => _assignedType;
public override Type PropertyType => _propertyType;
@ -82,5 +83,14 @@ namespace Avalonia.Diagnostics.ViewModels
RaiseAndSetIfChanged(ref _assignedType, valueType ?? Property.PropertyType, nameof(AssignedType));
RaisePropertyChanged(nameof(Type));
}
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.PropertyName == nameof(IsPinned))
{
RaisePropertyChanged(nameof(Group));
}
}
}
}

126
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs

@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
@ -10,13 +11,13 @@ 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, IClassesChangedListener
{
private readonly AvaloniaObject _avaloniaObject;
private readonly ISet<string> _pinnedProperties;
private IDictionary<object, PropertyViewModel[]>? _propertyIndex;
private PropertyViewModel? _selectedProperty;
private DataGridCollectionView? _propertiesView;
@ -24,19 +25,29 @@ namespace Avalonia.Diagnostics.ViewModels
private bool _showInactiveStyles;
private string? _styleStatus;
private object? _selectedEntity;
private readonly Stack<(string Name,object Entry)> _selectedEntitiesStack = new();
private readonly Stack<(string Name, object Entry)> _selectedEntitiesStack = new();
private string? _selectedEntityName;
private string? _selectedEntityType;
private bool _showImplementedInterfaces;
// new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group))
private readonly static IReadOnlyList<DataGridPathGroupDescription> GroupDescriptors = new DataGridPathGroupDescription[]
{
new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group))
};
public ControlDetailsViewModel(TreePageViewModel treePage, AvaloniaObject avaloniaObject)
private readonly static IReadOnlyList<DataGridSortDescription> SortDescriptions = new DataGridSortDescription[]
{
_avaloniaObject = avaloniaObject;
new DataGridComparerSortDescription(PropertyComparer.Instance!, ListSortDirection.Ascending),
};
public ControlDetailsViewModel(TreePageViewModel treePage, AvaloniaObject avaloniaObject, ISet<string> pinnedProperties)
{
_avaloniaObject = avaloniaObject;
_pinnedProperties = pinnedProperties;
TreePage = treePage;
Layout = avaloniaObject is Visual visual
? new ControlLayoutViewModel(visual)
: default;
Layout = avaloniaObject is Visual visual
? new ControlLayoutViewModel(visual)
: default;
NavigateToProperty(_avaloniaObject, (_avaloniaObject as Control)?.Name ?? _avaloniaObject.ToString());
@ -84,7 +95,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
var setterValue = regularSetter.Value;
var resourceInfo = GetResourceInfo(setterValue);
var resourceInfo = GetResourceInfo(setterValue);
SetterViewModel setterVm;
@ -175,13 +186,13 @@ namespace Avalonia.Diagnostics.ViewModels
get => _selectedEntityName;
set => RaiseAndSetIfChanged(ref _selectedEntityName, value);
}
public string? SelectedEntityType
{
get => _selectedEntityType;
set => RaiseAndSetIfChanged(ref _selectedEntityType, value);
}
public PropertyViewModel? SelectedProperty
{
get => _selectedProperty;
@ -395,14 +406,23 @@ namespace Avalonia.Diagnostics.ViewModels
return !(arg is PropertyViewModel property) || TreePage.PropertiesFilter.Filter(property.Name);
}
private class PropertyComparer : IComparer<PropertyViewModel>
private class PropertyComparer : IComparer<PropertyViewModel>, IComparer
{
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 (x is null && y is null)
return 0;
if (x is null && y is not null)
return -1;
if (x is not null && y is null)
return 1;
var groupX = GroupIndex(x!.Group);
var groupY = GroupIndex(y!.Group);
if (groupX != groupY)
{
@ -410,7 +430,7 @@ namespace Avalonia.Diagnostics.ViewModels
}
else
{
return string.CompareOrdinal(x?.Name, y?.Name);
return string.CompareOrdinal(x.Name, y.Name);
}
}
@ -418,12 +438,21 @@ namespace Avalonia.Diagnostics.ViewModels
{
switch (group)
{
case "Properties": return 0;
case "Attached Properties": return 1;
case "CLR Properties": return 2;
default: return 3;
case "Pinned":
return -1;
case "Properties":
return 0;
case "Attached Properties":
return 1;
case "CLR Properties":
return 2;
default:
return 3;
}
}
public int Compare(object? x, object? y) =>
Compare(x as PropertyViewModel, y as PropertyViewModel);
}
private static IEnumerable<PropertyInfo> GetAllPublicProperties(Type type)
@ -438,8 +467,8 @@ namespace Avalonia.Diagnostics.ViewModels
var selectedProperty = SelectedProperty;
var selectedEntity = SelectedEntity;
var selectedEntityName = SelectedEntityName;
if (selectedEntity == null
|| selectedProperty == null
if (selectedEntity == null
|| selectedProperty == null
|| selectedProperty.PropertyType == typeof(string)
|| selectedProperty.PropertyType.IsValueType)
return;
@ -455,19 +484,19 @@ namespace Avalonia.Diagnostics.ViewModels
break;
case ClrPropertyViewModel clrProperty:
{
property = GetAllPublicProperties(selectedEntity.GetType())
.FirstOrDefault(pi => clrProperty.Property == pi)?
.GetValue(selectedEntity);
{
property = GetAllPublicProperties(selectedEntity.GetType())
.FirstOrDefault(pi => clrProperty.Property == pi)?
.GetValue(selectedEntity);
break;
}
break;
}
}
if (property == null)
if (property == null)
return;
_selectedEntitiesStack.Push((Name:selectedEntityName!, Entry:selectedEntity));
_selectedEntitiesStack.Push((Name: selectedEntityName!, Entry: selectedEntity));
var propertyName = selectedProperty.Name;
@ -492,7 +521,7 @@ namespace Avalonia.Diagnostics.ViewModels
RaisePropertyChanged(nameof(CanNavigateToParentProperty));
}
}
protected void NavigateToProperty(object o, string? entityName)
{
var oldSelectedEntity = SelectedEntity;
@ -514,17 +543,19 @@ namespace Avalonia.Diagnostics.ViewModels
var properties = GetAvaloniaProperties(o)
.Concat(GetClrProperties(o, _showImplementedInterfaces))
.OrderBy(x => x, PropertyComparer.Instance)
.ThenBy(x => x.Name)
.Do(p =>
{
p.IsPinned = _pinnedProperties.Contains(p.FullName);
})
.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.GroupDescriptions.AddRange(GroupDescriptors);
view.SortDescriptions.AddRange(SortDescriptions);
view.Filter = FilterProperty;
PropertiesView = view;
@ -539,7 +570,7 @@ namespace Avalonia.Diagnostics.ViewModels
break;
}
}
internal void SelectProperty(AvaloniaProperty property)
{
SelectedProperty = null;
@ -547,10 +578,10 @@ namespace Avalonia.Diagnostics.ViewModels
if (SelectedEntity != _avaloniaObject)
{
NavigateToProperty(
_avaloniaObject,
(_avaloniaObject as Control)?.Name ?? _avaloniaObject.ToString());
_avaloniaObject,
(_avaloniaObject as Control)?.Name ?? _avaloniaObject.ToString());
}
if (PropertiesView is null)
{
return;
@ -561,7 +592,7 @@ namespace Avalonia.Diagnostics.ViewModels
if (o is AvaloniaPropertyViewModel propertyVm && propertyVm.Property == property)
{
SelectedProperty = propertyVm;
break;
}
}
@ -573,5 +604,24 @@ namespace Avalonia.Diagnostics.ViewModels
SelectedProperty = null;
NavigateToProperty(_avaloniaObject, (_avaloniaObject as Control)?.Name ?? _avaloniaObject.ToString());
}
public void TogglePinnedProperty(object parameter)
{
if (parameter is PropertyViewModel model)
{
var fullname = model.FullName;
if (_pinnedProperties.Contains(fullname))
{
_pinnedProperties.Remove(fullname);
model.IsPinned = false;
}
else
{
_pinnedProperties.Add(fullname);
model.IsPinned = true;
}
PropertiesView?.Refresh();
}
}
}
}

8
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@ -8,6 +8,7 @@ using Avalonia.Metadata;
using Avalonia.Threading;
using Avalonia.Reactive;
using Avalonia.Rendering;
using System.Collections.Generic;
namespace Avalonia.Diagnostics.ViewModels
{
@ -27,14 +28,15 @@ namespace Avalonia.Diagnostics.ViewModels
private string? _pointerOverElementName;
private IInputRoot? _pointerOverRoot;
private IScreenshotHandler? _screenshotHandler;
private bool _showPropertyType;
private bool _showPropertyType;
private bool _showImplementedInterfaces;
private readonly HashSet<string> _pinnedProperties = new();
public MainViewModel(AvaloniaObject root)
{
_root = root;
_logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root));
_visualTree = new TreePageViewModel(this, VisualTreeNode.Create(root));
_logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root), _pinnedProperties);
_visualTree = new TreePageViewModel(this, VisualTreeNode.Create(root), _pinnedProperties);
_events = new EventsPageViewModel(this);
UpdateFocusedControl();

44
src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs

@ -1,27 +1,29 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Reflection;
namespace Avalonia.Diagnostics.ViewModels
namespace Avalonia.Diagnostics.ViewModels;
internal abstract class PropertyViewModel : ViewModelBase
{
internal abstract class PropertyViewModel : ViewModelBase
{
public abstract object Key { get; }
public abstract string Name { get; }
public abstract string Group { get; }
public abstract Type AssignedType { get; }
public abstract Type? DeclaringType { get; }
public abstract object? Value { get; set; }
public abstract string Priority { get; }
public abstract bool? IsAttached { get; }
public abstract void Update();
public abstract Type PropertyType { get; }
private bool _isPinned;
public abstract object Key { get; }
public abstract string Name { get; }
public abstract string Group { get; }
public abstract Type AssignedType { get; }
public abstract Type? DeclaringType { get; }
public abstract object? Value { get; set; }
public abstract string Priority { get; }
public abstract bool? IsAttached { get; }
public abstract void Update();
public abstract Type PropertyType { get; }
public string Type => PropertyType == AssignedType ?
PropertyType.GetTypeName() :
$"{PropertyType.GetTypeName()} {{{AssignedType.GetTypeName()}}}";
public abstract bool IsReadonly { get; }
public string Type => PropertyType == AssignedType ?
PropertyType.GetTypeName() :
$"{PropertyType.GetTypeName()} {{{AssignedType.GetTypeName()}}}";
public bool IsPinned { get => _isPinned; set => RaiseAndSetIfChanged(ref _isPinned, value); }
public abstract bool IsReadonly { get; }
}
public string FullName => $"{GetType().Name.Replace("PropertyViewModel","")}:{DeclaringType?.FullName}.{Name}";
}

11
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs

@ -2,10 +2,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.Styling;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
@ -14,12 +10,13 @@ namespace Avalonia.Diagnostics.ViewModels
{
private TreeNode? _selectedNode;
private ControlDetailsViewModel? _details;
private readonly ISet<string> _pinnedProperties;
public TreePageViewModel(MainViewModel mainView, TreeNode[] nodes)
public TreePageViewModel(MainViewModel mainView, TreeNode[] nodes, ISet<string> pinnedProperties)
{
MainView = mainView;
Nodes = nodes;
_pinnedProperties = pinnedProperties;
PropertiesFilter = new FilterViewModel();
PropertiesFilter.RefreshFilter += (s, e) => Details?.PropertiesView?.Refresh();
@ -45,7 +42,7 @@ namespace Avalonia.Diagnostics.ViewModels
if (RaiseAndSetIfChanged(ref _selectedNode, value))
{
Details = value != null ?
new ControlDetailsViewModel(this, value.Visual) :
new ControlDetailsViewModel(this, value.Visual, _pinnedProperties) :
null;
Details?.UpdatePropertiesView(MainView.ShowImplementedInterfaces);
Details?.UpdateStyleFilters();

50
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml

@ -10,10 +10,23 @@
x:DataType="vm:ControlDetailsViewModel">
<UserControl.Resources>
<conv:BoolToOpacityConverter x:Key="BoolToOpacity" Opacity="0.6"/>
<conv:GetTypeNameConverter x:Key="GetTypeName"/>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="/Assets/Icons.axaml"/>
</ResourceDictionary.MergedDictionaries>
<conv:BoolToOpacityConverter x:Key="BoolToOpacity" Opacity="0.6"/>
<conv:GetTypeNameConverter x:Key="GetTypeName"/>
<conv:BoolToImageConverter x:Key="pc"
TrueImage="{StaticResource PinIcon}"
FalseImage="{StaticResource UnpinIcon}"/>
</ResourceDictionary>
</UserControl.Resources>
<UserControl.Styles>
<Style Selector="TextBlock.Pinned">
<Setter Property="FontWeight" Value="Bold"/>
</Style>
</UserControl.Styles>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
@ -59,7 +72,38 @@
CanUserResizeColumns="true"
DoubleTapped="PropertiesGrid_OnDoubleTapped">
<DataGrid.Columns>
<DataGridTextColumn Header="Property" Binding="{Binding Name}" IsReadOnly="True" x:DataType="vm:PropertyViewModel" />
<DataGridTemplateColumn Header="Property" IsReadOnly="True" x:DataType="vm:PropertyViewModel">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="vm:PropertyViewModel">
<!--
Do not remove the DockPanel background set to Transparent
because if Background is null HitTest on empty space returns false
-->
<DockPanel Background="Transparent">
<DockPanel.Styles>
<Style Selector="DockPanel[IsPointerOver=False] > ToggleButton">
<Setter Property="IsVisible" Value="False"/>
</Style>
</DockPanel.Styles>
<ToggleButton
IsChecked="{Binding IsPinned}"
Background="Transparent"
BorderBrush="Transparent"
DockPanel.Dock="Right"
Command="{Binding #Main.((vm:ControlDetailsViewModel)DataContext).TogglePinnedProperty}"
CommandParameter="{Binding}">
<Image Source="{Binding IsPinned, Converter={StaticResource pc}}"
Width="12"
Height="12"/>
</ToggleButton>
<TextBlock Text="{Binding Name}"
Classes.Pinned="{Binding IsPinned}"
VerticalAlignment="Center"
/>
</DockPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Value" Width="100">
<DataTemplate>
<local:PropertyValueEditorView />

Loading…
Cancel
Save