Browse Source

Initial implementation of new DevTools.

pull/3462/head
Steven Kirk 6 years ago
parent
commit
9fb970523f
  1. BIN
      src/Avalonia.Diagnostics/Assets/Fonts/SourceSansPro-Regular.ttf
  2. 12
      src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj
  3. 24
      src/Avalonia.Diagnostics/DevTools.xaml
  4. 159
      src/Avalonia.Diagnostics/DevTools.xaml.cs
  5. 19
      src/Avalonia.Diagnostics/DevToolsExtensions.cs
  6. 22
      src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToBrushConverter.cs
  7. 61
      src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
  8. 36
      src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs
  9. 19
      src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleHistoryItem.cs
  10. 0
      src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs
  11. 8
      src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs
  12. 120
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
  13. 82
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
  14. 112
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs
  15. 185
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  16. 2
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs
  17. 22
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs
  18. 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs
  19. 17
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs
  20. 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs
  21. 6
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs
  22. 126
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  23. 11
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs
  24. 15
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
  25. 49
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
  26. 28
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs
  27. 5
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs
  28. 56
      src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml
  29. 64
      src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs
  30. 24
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
  31. 18
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs
  32. 5
      src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml
  33. 4
      src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs
  34. 55
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
  35. 66
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs
  36. 18
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
  37. 74
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
  38. 19
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
  39. 9
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
  40. 0
      src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs
  41. 25
      src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs
  42. 76
      src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs
  43. 16
      src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs
  44. 58
      src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs
  45. 75
      src/Avalonia.Diagnostics/Views/ControlDetailsView.cs
  46. 51
      src/Avalonia.Diagnostics/Views/GridRepeater.cs
  47. 33
      src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs
  48. 146
      src/Avalonia.Diagnostics/Views/SimpleGrid.cs

BIN
src/Avalonia.Diagnostics/Assets/Fonts/SourceSansPro-Regular.ttf

Binary file not shown.

12
src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj

@ -1,8 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>Avalonia</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Update="**\*.xaml.cs">
<DependentUpon>%(Filename)</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
<ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
<ProjectReference Include="..\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />
@ -14,7 +21,10 @@
<ProjectReference Include="..\Avalonia.Visuals\Avalonia.Visuals.csproj" />
<ProjectReference Include="..\Avalonia.Styling\Avalonia.Styling.csproj" />
<ProjectReference Include="..\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj" />
</ItemGroup>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="3.4.0" />
</ItemGroup>
<Import Project="..\..\build\EmbedXaml.props" />
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\BuildTargets.targets" />

24
src/Avalonia.Diagnostics/DevTools.xaml

@ -1,24 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Avalonia.Diagnostics.DevTools">
<Grid RowDefinitions="*,Auto" Margin="4">
<TabControl Grid.Row="0" Items="{Binding Tools}" SelectedItem="{Binding SelectedTool}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
<StackPanel Grid.Row="1" Spacing="4" Orientation="Horizontal">
<TextBlock>Hold Ctrl+Shift over a control to inspect.</TextBlock>
<Separator Width="8" />
<TextBlock>Focused:</TextBlock>
<TextBlock Text="{Binding FocusedControl}" />
<Separator Width="8" />
<TextBlock>Pointer Over:</TextBlock>
<TextBlock Text="{Binding PointerOverElement}" />
</StackPanel>
</Grid>
</UserControl>

159
src/Avalonia.Diagnostics/DevTools.xaml.cs

@ -1,159 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Rendering;
using Avalonia.VisualTree;
namespace Avalonia
{
public static class DevToolsExtensions
{
public static void AttachDevTools(this TopLevel control)
{
Diagnostics.DevTools.Attach(control, new KeyGesture(Key.F12));
}
public static void AttachDevTools(this TopLevel control, KeyGesture gesture)
{
Diagnostics.DevTools.Attach(control, gesture);
}
public static void OpenDevTools(this TopLevel control)
{
Diagnostics.DevTools.OpenDevTools(control);
}
}
}
namespace Avalonia.Diagnostics
{
public class DevTools : UserControl
{
private static readonly Dictionary<TopLevel, Window> s_open = new Dictionary<TopLevel, Window>();
private static readonly HashSet<IRenderRoot> s_visualTreeRoots = new HashSet<IRenderRoot>();
private readonly IDisposable _keySubscription;
public DevTools(IControl root)
{
InitializeComponent();
Root = root;
DataContext = new DevToolsViewModel(root);
_keySubscription = InputManager.Instance.Process
.OfType<RawKeyEventArgs>()
.Subscribe(RawKeyDown);
}
// HACK: needed for XAMLIL, will fix that later
public DevTools()
{
}
public IControl Root { get; }
public static IDisposable Attach(TopLevel control, KeyGesture gesture)
{
void PreviewKeyDown(object sender, KeyEventArgs e)
{
if (gesture.Matches(e))
{
OpenDevTools(control);
}
}
return control.AddHandler(
KeyDownEvent,
PreviewKeyDown,
RoutingStrategies.Tunnel);
}
internal static void OpenDevTools(TopLevel control)
{
if (s_open.TryGetValue(control, out var devToolsWindow))
{
devToolsWindow.Activate();
}
else
{
var devTools = new DevTools(control);
devToolsWindow = new Window
{
Width = 1024,
Height = 512,
Content = devTools,
DataTemplates = { new ViewLocator<ViewModelBase>() },
Title = "Avalonia DevTools"
};
devToolsWindow.Closed += devTools.DevToolsClosed;
s_open.Add(control, devToolsWindow);
MarkAsDevTool(devToolsWindow);
devToolsWindow.Show();
}
}
private void DevToolsClosed(object sender, EventArgs e)
{
var devToolsWindow = (Window)sender;
var devTools = (DevTools)devToolsWindow.Content;
s_open.Remove((TopLevel)devTools.Root);
RemoveDevTool(devToolsWindow);
_keySubscription.Dispose();
devToolsWindow.Closed -= DevToolsClosed;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void RawKeyDown(RawKeyEventArgs e)
{
const RawInputModifiers modifiers = RawInputModifiers.Control | RawInputModifiers.Shift;
if (e.Modifiers == modifiers)
{
var point = (Root.VisualRoot as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default(Point);
var control = Root.GetVisualsAt(point, x => (!(x is AdornerLayer) && x.IsVisible))
.FirstOrDefault();
if (control != null)
{
var vm = (DevToolsViewModel)DataContext;
vm.SelectControl((IControl)control);
}
}
}
/// <summary>
/// Marks a visual as part of the DevTools, so it can be excluded from event tracking.
/// </summary>
/// <param name="visual">The visual whose root is to be marked.</param>
public static void MarkAsDevTool(IVisual visual)
{
s_visualTreeRoots.Add(visual.GetVisualRoot());
}
public static void RemoveDevTool(IVisual visual)
{
s_visualTreeRoots.Remove(visual.GetVisualRoot());
}
public static bool BelongsToDevTool(IVisual visual)
{
return s_visualTreeRoots.Contains(visual.GetVisualRoot());
}
}
}

19
src/Avalonia.Diagnostics/DevToolsExtensions.cs

@ -0,0 +1,19 @@
using Avalonia.Controls;
using Avalonia.Diagnostics;
using Avalonia.Input;
namespace Avalonia
{
public static class DevToolsExtensions
{
public static void AttachDevTools(this TopLevel root)
{
DevTools.Attach(root, new KeyGesture(Key.F12));
}
public static void AttachDevTools(this TopLevel root, KeyGesture gesture)
{
DevTools.Attach(root, gesture);
}
}
}

22
src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToBrushConverter.cs

@ -0,0 +1,22 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media;
namespace Avalonia.Diagnostics.Converters
{
internal class BoolToBrushConverter : IValueConverter
{
public IBrush Brush { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (bool)value ? Brush : Brushes.Transparent;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

61
src/Avalonia.Diagnostics/Diagnostics/DevTools.cs

@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using Avalonia.Controls;
using Avalonia.Diagnostics.Views;
using Avalonia.Input;
using Avalonia.Interactivity;
namespace Avalonia.Diagnostics
{
public static class DevTools
{
private static readonly Dictionary<TopLevel, Window> s_open = new Dictionary<TopLevel, Window>();
public static IDisposable Attach(TopLevel root, KeyGesture gesture)
{
void PreviewKeyDown(object sender, KeyEventArgs e)
{
if (gesture.Matches(e))
{
Open(root);
}
}
return root.AddHandler(
InputElement.KeyDownEvent,
PreviewKeyDown,
RoutingStrategies.Tunnel);
}
public static IDisposable Open(TopLevel root)
{
if (s_open.TryGetValue(root, out var window))
{
window.Activate();
}
else
{
window = new MainWindow
{
Width = 1024,
Height = 512,
Root = root,
};
window.Closed += DevToolsClosed;
s_open.Add(root, window);
window.Show();
}
return Disposable.Create(() => window?.Close());
}
private static void DevToolsClosed(object sender, EventArgs e)
{
var window = (MainWindow)sender;
s_open.Remove(window.Root);
window.Closed -= DevToolsClosed;
}
}
}

36
src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs

@ -0,0 +1,36 @@
#pragma warning disable IDE1006 // Naming Styles
using Avalonia.Diagnostics.ViewModels;
namespace Avalonia.Diagnostics.Models
{
public class ConsoleContext
{
private readonly ConsoleViewModel _owner;
internal ConsoleContext(ConsoleViewModel owner) => _owner = owner;
public readonly string help = @"Welcome to Avalonia DevTools. Here you can execute arbitrary C# code using Roslyn scripting.
The following variables are available:
e: The control currently selected in the logical or visual tree view
root: The root of the visual tree
The following commands are available:
clear(): Clear the output history
";
public dynamic e { get; internal set; }
public dynamic root { get; internal set; }
internal static object NoOutput { get; } = new object();
public object clear()
{
_owner.History.Clear();
return NoOutput;
}
}
}

19
src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleHistoryItem.cs

@ -0,0 +1,19 @@
using System;
using Avalonia.Media;
namespace Avalonia.Diagnostics.Models
{
public class ConsoleHistoryItem
{
public ConsoleHistoryItem(string input, object output)
{
Input = input;
Output = output;
Foreground = output is Exception ? Brushes.Red : Brushes.Green;
}
public string Input { get; }
public object Output { get; }
public IBrush Foreground { get; }
}
}

0
src/Avalonia.Diagnostics/Models/EventChainLink.cs → src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs

8
src/Avalonia.Diagnostics/ViewLocator.cs → src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs

@ -1,13 +1,11 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Diagnostics.ViewModels;
namespace Avalonia.Diagnostics
{
internal class ViewLocator<TViewModel> : IDataTemplate
internal class ViewLocator : IDataTemplate
{
public bool SupportsRecycling => false;
@ -28,7 +26,7 @@ namespace Avalonia.Diagnostics
public bool Match(object data)
{
return data is TViewModel;
return data is ViewModelBase;
}
}
}

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

@ -0,0 +1,120 @@
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Data.Converters;
namespace Avalonia.Diagnostics.ViewModels
{
internal class AvaloniaPropertyViewModel : PropertyViewModel
{
private readonly AvaloniaObject _target;
private object _value;
private string _priority;
private TypeConverter _converter;
private string _group;
private DataGridCollectionView _bindingsView;
public AvaloniaPropertyViewModel(AvaloniaObject o, AvaloniaProperty property)
{
_target = o;
Property = property;
Name = property.IsAttached ?
$"[{property.OwnerType.Name}.{property.Name}]" :
property.Name;
if (property.IsDirect)
{
_group = "Properties";
Priority = "Direct";
}
Update();
}
public AvaloniaProperty Property { get; }
public override object Key => Property;
public override string Name { get; }
public bool IsAttached => Property.IsAttached;
public string Priority
{
get => _priority;
private set => RaiseAndSetIfChanged(ref _priority, value);
}
public override string Value
{
get
{
if (_value == null)
{
return "(null)";
}
return Converter?.CanConvertTo(typeof(string)) == true ?
Converter.ConvertToString(_value) :
_value.ToString();
}
set
{
try
{
var convertedValue = Converter?.CanConvertFrom(typeof(string)) == true ?
Converter.ConvertFromString(value) :
DefaultValueConverter.Instance.ConvertBack(value, Property.PropertyType, null, CultureInfo.CurrentCulture);
_target.SetValue(Property, convertedValue);
}
catch { }
}
}
public override string Group
{
get => _group;
}
private TypeConverter Converter
{
get
{
if (_converter == null)
{
_converter = TypeDescriptor.GetConverter(_value.GetType());
}
return _converter;
}
}
public override void Update()
{
if (Property.IsDirect)
{
RaiseAndSetIfChanged(ref _value, _target.GetValue(Property), nameof(Value));
}
else
{
var val = _target.GetDiagnostic(Property);
RaiseAndSetIfChanged(ref _value, val?.Value, nameof(Value));
if (val != null)
{
SetGroup(IsAttached ? "Attached Properties" : "Properties");
Priority = val.Priority.ToString();
}
else
{
SetGroup(Priority = "Unset");
}
}
}
private void SetGroup(string group)
{
RaiseAndSetIfChanged(ref _group, group, nameof(Group));
}
}
}

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

@ -0,0 +1,82 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Reflection;
using Avalonia.Data.Converters;
namespace Avalonia.Diagnostics.ViewModels
{
internal class ClrPropertyViewModel : PropertyViewModel
{
private readonly object _target;
private object _value;
private TypeConverter _converter;
public ClrPropertyViewModel(object o, PropertyInfo property)
{
_target = o;
Property = property;
if (!property.DeclaringType.IsInterface)
{
Name = property.Name;
}
else
{
Name = property.DeclaringType.Name + '.' + property.Name;
}
Update();
}
public PropertyInfo Property { get; }
public override object Key => Name;
public override string Name { get; }
public override string Group => "CLR Properties";
public override string Value
{
get
{
if (_value == null)
{
return "(null)";
}
return Converter?.CanConvertTo(typeof(string)) == true ?
Converter.ConvertToString(_value) :
_value.ToString();
}
set
{
try
{
var convertedValue = Converter?.CanConvertFrom(typeof(string)) == true ?
Converter.ConvertFromString(value) :
DefaultValueConverter.Instance.ConvertBack(value, Property.PropertyType, null, CultureInfo.CurrentCulture);
Property.SetValue(_target, convertedValue);
}
catch { }
}
}
private TypeConverter Converter
{
get
{
if (_converter == null)
{
_converter = TypeDescriptor.GetConverter(_value.GetType());
}
return _converter;
}
}
public override void Update()
{
var val = Property.GetValue(_target);
RaiseAndSetIfChanged(ref _value, val, nameof(Value));
}
}
}

112
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs

@ -0,0 +1,112 @@
using System;
using System.Reflection;
using System.Threading.Tasks;
using Avalonia.Collections;
using Avalonia.Diagnostics.Models;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
namespace Avalonia.Diagnostics.ViewModels
{
internal class ConsoleViewModel : ViewModelBase
{
readonly ConsoleContext _context;
readonly Action<ConsoleContext> _updateContext;
private int _historyIndex = -1;
private string _input;
private bool _isVisible;
private ScriptState<object> _state;
public ConsoleViewModel(Action<ConsoleContext> updateContext)
{
_context = new ConsoleContext(this);
_updateContext = updateContext;
}
public string Input
{
get => _input;
set => RaiseAndSetIfChanged(ref _input, value);
}
public bool IsVisible
{
get => _isVisible;
set => RaiseAndSetIfChanged(ref _isVisible, value);
}
public AvaloniaList<ConsoleHistoryItem> History { get; } = new AvaloniaList<ConsoleHistoryItem>();
public async Task Execute()
{
if (string.IsNullOrWhiteSpace(Input))
{
return;
}
try
{
var options = ScriptOptions.Default
.AddReferences(Assembly.GetAssembly(typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo)));
_updateContext(_context);
if (_state == null)
{
_state = await CSharpScript.RunAsync(Input, options: options, globals: _context);
}
else
{
_state = await _state.ContinueWithAsync(Input);
}
if (_state.ReturnValue != ConsoleContext.NoOutput)
{
History.Add(new ConsoleHistoryItem(Input, _state.ReturnValue ?? "(null)"));
}
}
catch (Exception ex)
{
History.Add(new ConsoleHistoryItem(Input, ex));
}
Input = string.Empty;
_historyIndex = -1;
}
public void HistoryUp()
{
if (History.Count > 0)
{
if (_historyIndex == -1)
{
_historyIndex = History.Count - 1;
}
else if (_historyIndex > 0)
{
--_historyIndex;
}
Input = History[_historyIndex].Input;
}
}
public void HistoryDown()
{
if (History.Count > 0 && _historyIndex >= 0)
{
if (_historyIndex == History.Count - 1)
{
_historyIndex = -1;
Input = string.Empty;
}
else
{
Input = History[++_historyIndex].Input;
}
}
}
public void ToggleVisibility() => IsVisible = !IsVisible;
}
}

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

@ -0,0 +1,185 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Avalonia.Collections;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
{
internal class ControlDetailsViewModel : ViewModelBase, IDisposable
{
private readonly IVisual _control;
private readonly IDictionary<object, List<PropertyViewModel>> _propertyIndex;
private AvaloniaPropertyViewModel _selectedProperty;
private string _propertyFilter;
public ControlDetailsViewModel(IVisual control, string propertyFilter)
{
_control = control;
var properties = GetAvaloniaProperties(control)
.Concat(GetClrProperties(control))
.OrderBy(x => x, PropertyComparer.Instance)
.ThenBy(x => x.Name)
.ToList();
_propertyIndex = properties.GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.ToList());
_propertyFilter = propertyFilter;
var view = new DataGridCollectionView(properties);
view.GroupDescriptions.Add(new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group)));
view.Filter = FilterProperty;
PropertiesView = view;
if (control is INotifyPropertyChanged inpc)
{
inpc.PropertyChanged += ControlPropertyChanged;
}
if (control is AvaloniaObject ao)
{
ao.PropertyChanged += ControlPropertyChanged;
}
}
public IEnumerable<string> Classes
{
get;
private set;
}
public DataGridCollectionView PropertiesView { get; }
public string PropertyFilter
{
get => _propertyFilter;
set
{
if (RaiseAndSetIfChanged(ref _propertyFilter, value))
{
PropertiesView.Refresh();
}
}
}
public AvaloniaPropertyViewModel SelectedProperty
{
get => _selectedProperty;
set => RaiseAndSetIfChanged(ref _selectedProperty, value);
}
public void Dispose()
{
if (_control is INotifyPropertyChanged inpc)
{
inpc.PropertyChanged -= ControlPropertyChanged;
}
if (_control is AvaloniaObject ao)
{
ao.PropertyChanged -= ControlPropertyChanged;
}
}
private IEnumerable<PropertyViewModel> GetAvaloniaProperties(object o)
{
if (o is AvaloniaObject ao)
{
return AvaloniaPropertyRegistry.Instance.GetRegistered(ao)
.Concat(AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(ao.GetType()))
.Select(x => new AvaloniaPropertyViewModel(ao, x));
}
else
{
return Enumerable.Empty<AvaloniaPropertyViewModel>();
}
}
private IEnumerable<PropertyViewModel> GetClrProperties(object o)
{
foreach (var p in GetClrProperties(o, o.GetType()))
{
yield return p;
}
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.TryGetValue(e.Property, out var properties))
{
foreach (var property in properties)
{
property.Update();
}
}
}
private void ControlPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (_propertyIndex.TryGetValue(e.PropertyName, out var properties))
{
foreach (var property in properties)
{
property.Update();
}
}
}
private bool FilterProperty(object arg)
{
if (!string.IsNullOrWhiteSpace(PropertyFilter) && arg is PropertyViewModel property)
{
return property.Name.IndexOf(PropertyFilter, StringComparison.OrdinalIgnoreCase) != -1;
}
return true;
}
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.Compare(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;
}
}
}
}
}

2
src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs

@ -19,7 +19,7 @@ namespace Avalonia.Diagnostics.ViewModels
InputElement.PointerReleasedEvent, InputElement.PointerPressedEvent
};
public EventOwnerTreeNode(Type type, IEnumerable<RoutedEvent> events, EventsViewModel vm)
public EventOwnerTreeNode(Type type, IEnumerable<RoutedEvent> events, EventsPageViewModel vm)
: base(null, type.Name)
{
Children = new AvaloniaList<EventTreeNodeBase>(events.OrderBy(e => e.Name)

22
src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs

@ -3,6 +3,7 @@
using System;
using Avalonia.Diagnostics.Models;
using Avalonia.Diagnostics.Views;
using Avalonia.Interactivity;
using Avalonia.Threading;
using Avalonia.VisualTree;
@ -12,11 +13,11 @@ namespace Avalonia.Diagnostics.ViewModels
internal class EventTreeNode : EventTreeNodeBase
{
private readonly RoutedEvent _event;
private readonly EventsViewModel _parentViewModel;
private readonly EventsPageViewModel _parentViewModel;
private bool _isRegistered;
private FiredEvent _currentEvent;
public EventTreeNode(EventOwnerTreeNode parent, RoutedEvent @event, EventsViewModel vm)
public EventTreeNode(EventOwnerTreeNode parent, RoutedEvent @event, EventsPageViewModel vm)
: base(parent, @event.Name)
{
Contract.Requires<ArgumentNullException>(@event != null);
@ -65,7 +66,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
if (!_isRegistered || IsEnabled == false)
return;
if (sender is IVisual v && DevTools.BelongsToDevTool(v))
if (sender is IVisual v && BelongsToDevTool(v))
return;
var s = sender;
@ -94,5 +95,20 @@ namespace Avalonia.Diagnostics.ViewModels
else
handler();
}
private static bool BelongsToDevTool(IVisual v)
{
while (v != null)
{
if (v is MainView || v is MainWindow)
{
return true;
}
v = v.VisualParent;
}
return false;
}
}
}

0
src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs

17
src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs

@ -12,12 +12,12 @@ using Avalonia.Media;
namespace Avalonia.Diagnostics.ViewModels
{
internal class EventsViewModel : ViewModelBase, IDevToolViewModel
internal class EventsPageViewModel : ViewModelBase
{
private readonly IControl _root;
private FiredEvent _selectedEvent;
public EventsViewModel(IControl root)
public EventsPageViewModel(IControl root)
{
_root = root;
@ -45,17 +45,4 @@ namespace Avalonia.Diagnostics.ViewModels
RecordedEvents.Clear();
}
}
internal class BoolToBrushConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (bool)value ? Brushes.Green : Brushes.Transparent;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

0
src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs

6
src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs

@ -1,6 +1,3 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.LogicalTree;
@ -17,7 +14,8 @@ namespace Avalonia.Diagnostics.ViewModels
public static LogicalTreeNode[] Create(object control)
{
return control is ILogical logical ? new[] { new LogicalTreeNode(logical, null) } : null;
var logical = control as ILogical;
return logical != null ? new[] { new LogicalTreeNode(logical, null) } : null;
}
}
}

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

@ -0,0 +1,126 @@
using System;
using Avalonia.Controls;
using Avalonia.Diagnostics.Models;
using Avalonia.Input;
namespace Avalonia.Diagnostics.ViewModels
{
internal class MainViewModel : ViewModelBase, IDisposable
{
private readonly IControl _root;
private readonly TreePageViewModel _logicalTree;
private readonly TreePageViewModel _visualTree;
private readonly EventsPageViewModel _events;
private ViewModelBase _content;
private int _selectedTab;
private string _focusedControl;
private string _pointerOverElement;
public MainViewModel(IControl root)
{
_root = root;
_logicalTree = new TreePageViewModel(LogicalTreeNode.Create(root));
_visualTree = new TreePageViewModel(VisualTreeNode.Create(root));
_events = new EventsPageViewModel(root);
UpdateFocusedControl();
KeyboardDevice.Instance.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement))
{
UpdateFocusedControl();
}
};
SelectedTab = 0;
root.GetObservable(TopLevel.PointerOverElementProperty)
.Subscribe(x => PointerOverElement = x?.GetType().Name);
Console = new ConsoleViewModel(UpdateConsoleContext);
}
public ConsoleViewModel Console { get; }
public ViewModelBase Content
{
get { return _content; }
private set
{
if (_content is TreePageViewModel oldTree &&
value is TreePageViewModel newTree &&
oldTree?.SelectedNode?.Visual is IControl control)
{
newTree.SelectControl(control);
}
RaiseAndSetIfChanged(ref _content, value);
}
}
public int SelectedTab
{
get { return _selectedTab; }
set
{
_selectedTab = value;
switch (value)
{
case 0:
Content = _logicalTree;
break;
case 1:
Content = _visualTree;
break;
case 2:
Content = _events;
break;
}
RaisePropertyChanged();
}
}
public string FocusedControl
{
get { return _focusedControl; }
private set { RaiseAndSetIfChanged(ref _focusedControl, value); }
}
public string PointerOverElement
{
get { return _pointerOverElement; }
private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); }
}
private void UpdateConsoleContext(ConsoleContext context)
{
context.root = _root;
if (Content is TreePageViewModel tree)
{
context.e = tree.SelectedNode?.Visual;
}
}
public void SelectControl(IControl control)
{
var tree = Content as TreePageViewModel;
if (tree != null)
{
tree.SelectControl(control);
}
}
public void Dispose()
{
_logicalTree.Dispose();
_visualTree.Dispose();
}
private void UpdateFocusedControl()
{
FocusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name;
}
}
}

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

@ -0,0 +1,11 @@
namespace Avalonia.Diagnostics.ViewModels
{
internal abstract class PropertyViewModel : ViewModelBase
{
public abstract object Key { get; }
public abstract string Name { get; }
public abstract string Group { get; }
public abstract string Value { get; set; }
public abstract void Update();
}
}

15
src/Avalonia.Diagnostics/ViewModels/TreeNode.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs

@ -27,9 +27,9 @@ namespace Avalonia.Diagnostics.ViewModels
var classesChanged = Observable.FromEventPattern<
NotifyCollectionChangedEventHandler,
NotifyCollectionChangedEventArgs>(
x => styleable.Classes.CollectionChanged += x,
x => styleable.Classes.CollectionChanged -= x)
.TakeUntil(styleable.StyleDetach);
x => styleable.Classes.CollectionChanged += x,
x => styleable.Classes.CollectionChanged -= x)
.TakeUntil(((IStyleable)styleable).StyleDetach);
classesChanged.Select(_ => Unit.Default)
.StartWith(Unit.Default)
@ -55,8 +55,8 @@ namespace Avalonia.Diagnostics.ViewModels
public string Classes
{
get => _classes;
private set => RaiseAndSetIfChanged(ref _classes, value);
get { return _classes; }
private set { RaiseAndSetIfChanged(ref _classes, value); }
}
public IVisual Visual
@ -66,8 +66,8 @@ namespace Avalonia.Diagnostics.ViewModels
public bool IsExpanded
{
get => _isExpanded;
set => RaiseAndSetIfChanged(ref _isExpanded, value);
get { return _isExpanded; }
set { RaiseAndSetIfChanged(ref _isExpanded, value); }
}
public TreeNode Parent
@ -78,6 +78,7 @@ namespace Avalonia.Diagnostics.ViewModels
public string Type
{
get;
private set;
}
}
}

49
src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs

@ -1,24 +1,20 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Controls;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
{
internal class TreePageViewModel : ViewModelBase, IDevToolViewModel
internal class TreePageViewModel : ViewModelBase, IDisposable
{
private TreeNode _selected;
private ControlDetailsViewModel _details;
private string _propertyFilter;
public TreePageViewModel(TreeNode[] nodes, string name)
public TreePageViewModel(TreeNode[] nodes)
{
Nodes = nodes;
Name = name;
}
public string Name { get; }
public TreeNode[] Nodes { get; protected set; }
public TreeNode SelectedNode
@ -26,9 +22,16 @@ namespace Avalonia.Diagnostics.ViewModels
get => _selected;
set
{
if (Details != null)
{
_propertyFilter = Details.PropertyFilter;
}
if (RaiseAndSetIfChanged(ref _selected, value))
{
Details = value != null ? new ControlDetailsViewModel(value.Visual) : null;
Details = value != null ?
new ControlDetailsViewModel(value.Visual, _propertyFilter) :
null;
}
}
}
@ -36,9 +39,19 @@ namespace Avalonia.Diagnostics.ViewModels
public ControlDetailsViewModel Details
{
get => _details;
private set => RaiseAndSetIfChanged(ref _details, value);
private set
{
var oldValue = _details;
if (RaiseAndSetIfChanged(ref _details, value))
{
oldValue?.Dispose();
}
}
}
public void Dispose() => _details?.Dispose();
public TreeNode FindNode(IControl control)
{
foreach (var node in Nodes)
@ -66,7 +79,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
control = control.GetVisualParent<IControl>();
}
}
}
if (node != null)
{
@ -90,14 +103,16 @@ namespace Avalonia.Diagnostics.ViewModels
{
return node;
}
foreach (var child in node.Children)
else
{
var result = FindNode(child, control);
if (result != null)
foreach (var child in node.Children)
{
return result;
var result = FindNode(child, control);
if (result != null)
{
return result;
}
}
}

28
src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs

@ -1,34 +1,42 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using JetBrains.Annotations;
namespace Avalonia.Diagnostics.ViewModels
{
internal class ViewModelBase : INotifyPropertyChanged
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private PropertyChangedEventHandler _propertyChanged;
private List<string> events = new List<string>();
public event PropertyChangedEventHandler PropertyChanged
{
add { _propertyChanged += value; events.Add("added"); }
remove { _propertyChanged -= value; events.Add("removed"); }
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
}
[NotifyPropertyChangedInvocator]
protected bool RaiseAndSetIfChanged<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (!EqualityComparer<T>.Default.Equals(field, value))
{
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
RaisePropertyChanged(propertyName);
return true;
}
return false;
}
[NotifyPropertyChangedInvocator]
protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
var e = new PropertyChangedEventArgs(propertyName);
OnPropertyChanged(e);
_propertyChanged?.Invoke(this, e);
}
}
}

5
src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs

@ -29,11 +29,12 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
public bool IsInTemplate { get; }
public bool IsInTemplate { get; private set; }
public static VisualTreeNode[] Create(object control)
{
return control is IVisual visual ? new[] { new VisualTreeNode(visual, null) } : null;
var visual = control as IVisual;
return visual != null ? new[] { new VisualTreeNode(visual, null) } : null;
}
}
}

56
src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml

@ -0,0 +1,56 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Avalonia.Diagnostics.Views.ConsoleView">
<UserControl.Styles>
<Style Selector="TextBox.console">
<Setter Property="FontFamily" Value="/Assets/Fonts/SourceSansPro-Regular.ttf"/>
<Setter Property="Template">
<ControlTemplate>
<Border Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<DockPanel Margin="{TemplateBinding Padding}">
<TextBlock DockPanel.Dock="Left" Margin="0,0,4,0">></TextBlock>
<TextPresenter Name="PART_TextPresenter"
Text="{TemplateBinding Text, Mode=TwoWay}"
CaretIndex="{TemplateBinding CaretIndex}"
SelectionStart="{TemplateBinding SelectionStart}"
SelectionEnd="{TemplateBinding SelectionEnd}"
TextAlignment="{TemplateBinding TextAlignment}"
TextWrapping="{TemplateBinding TextWrapping}"
PasswordChar="{TemplateBinding PasswordChar}"/>
</DockPanel>
</Border>
</ControlTemplate>
</Setter>
</Style>
</UserControl.Styles>
<DockPanel>
<TextBox Name="input"
Classes="console"
DockPanel.Dock="Bottom"
BorderThickness="0"
Text="{Binding Input}"/>
<ListBox Name="historyList"
BorderBrush="{DynamicResource ThemeControlMidBrush}"
BorderThickness="0,0,0,1"
FontFamily="/Assets/Fonts/SourceSansPro-Regular.ttf"
Items="{Binding History}"
VirtualizationMode="None">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<DockPanel>
<TextBlock DockPanel.Dock="Left" Margin="0,0,4,0">></TextBlock>
<TextBlock Text="{Binding Input}"/>
</DockPanel>
<TextBlock Foreground="{Binding Foreground}" Text="{Binding Output}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</UserControl>

64
src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs

@ -0,0 +1,64 @@
using System;
using System.Collections.Specialized;
using Avalonia.Controls;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
namespace Avalonia.Diagnostics.Views
{
internal class ConsoleView : UserControl
{
private readonly ListBox _historyList;
private readonly TextBox _input;
public ConsoleView()
{
this.InitializeComponent();
_historyList = this.FindControl<ListBox>("historyList");
((ILogical)_historyList).LogicalChildren.CollectionChanged += HistoryChanged;
_input = this.FindControl<TextBox>("input");
_input.KeyDown += InputKeyDown;
}
public void FocusInput() => _input.Focus();
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void HistoryChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems[0] is IControl control)
{
DispatcherTimer.RunOnce(control.BringIntoView, TimeSpan.Zero);
}
}
private void InputKeyDown(object sender, KeyEventArgs e)
{
var vm = (ConsoleViewModel)DataContext;
switch (e.Key)
{
case Key.Enter:
vm.Execute();
e.Handled = true;
break;
case Key.Up:
vm.HistoryUp();
_input.CaretIndex = _input.Text.Length;
e.Handled = true;
break;
case Key.Down:
vm.HistoryDown();
_input.CaretIndex = _input.Text.Length;
e.Handled = true;
break;
}
}
}
}

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

@ -0,0 +1,24 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters"
x:Class="Avalonia.Diagnostics.Views.ControlDetailsView">
<Grid ColumnDefinitions="*">
<DockPanel Grid.Column="0">
<TextBox DockPanel.Dock="Top"
BorderThickness="0"
Text="{Binding PropertyFilter}"
Watermark="Filter properties"/>
<DataGrid Items="{Binding PropertiesView}"
BorderThickness="0"
RowBackground="Transparent"
SelectedItem="{Binding SelectedProperty, Mode=TwoWay}"
HeadersVisibility="None">
<DataGrid.Columns>
<DataGridTextColumn Header="Property" Binding="{Binding Name}" IsReadOnly="True"/>
<DataGridTextColumn Header="Value" Binding="{Binding Value}"/>
<DataGridTextColumn Header="Priority" Binding="{Binding Priority}" IsReadOnly="True"/>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</Grid>
</UserControl>

18
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs

@ -0,0 +1,18 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Avalonia.Diagnostics.Views
{
internal class ControlDetailsView : UserControl
{
public ControlDetailsView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

5
src/Avalonia.Diagnostics/Views/EventsView.xaml → src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml

@ -1,9 +1,10 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
x:Class="Avalonia.Diagnostics.Views.EventsView">
xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters"
x:Class="Avalonia.Diagnostics.Views.EventsPageView">
<UserControl.Resources>
<vm:BoolToBrushConverter x:Key="boolToBrush" />
<conv:BoolToBrushConverter x:Key="boolToBrush" Brush="#d9ffdc"/>
</UserControl.Resources>
<Grid ColumnDefinitions="*,4,3*">
<TreeView Name="tree" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}"

4
src/Avalonia.Diagnostics/Views/EventsView.xaml.cs → src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs

@ -8,11 +8,11 @@ using Avalonia.Markup.Xaml;
namespace Avalonia.Diagnostics.Views
{
public class EventsView : UserControl
public class EventsPageView : UserControl
{
private readonly ListBox _events;
public EventsView()
public EventsPageView()
{
InitializeComponent();
_events = this.FindControl<ListBox>("events");

55
src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml

@ -0,0 +1,55 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:Avalonia.Diagnostics.Views"
x:Class="Avalonia.Diagnostics.Views.MainView">
<Grid Name="rootGrid" RowDefinitions="Auto,Auto,*,Auto,0,Auto">
<Menu>
<MenuItem Header="_File">
<MenuItem Header="E_xit" Command="{Binding $parent[Window].Close}"/>
</MenuItem>
<MenuItem Header="_View">
<MenuItem Header="_Console" Command="{Binding $parent[UserControl].ToggleConsole}">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="{Binding Console.IsVisible}"
IsEnabled="False"/>
</MenuItem.Icon>
</MenuItem>
</MenuItem>
</Menu>
<TabStrip Grid.Row="1" SelectedIndex="{Binding SelectedTab, Mode=TwoWay}">
<TabStripItem Content="Logical Tree"/>
<TabStripItem Content="Visual Tree"/>
<TabStripItem Content="Events"/>
</TabStrip>
<ContentControl Grid.Row="2"
BorderBrush="{DynamicResource ThemeControlMidBrush}"
BorderThickness="0,1,0,0"
Content="{Binding Content}"/>
<GridSplitter Name="consoleSplitter" Grid.Row="3" Height="1"
Background="{DynamicResource ThemeControlMidBrush}"
IsVisible="False"/>
<views:ConsoleView Name="console"
Grid.Row="4"
DataContext="{Binding Console}"
IsVisible="{Binding IsVisible}"/>
<Border Grid.Row="5"
BorderBrush="{DynamicResource ThemeControlMidBrush}"
BorderThickness="0,1,0,0">
<StackPanel Spacing="4" Orientation="Horizontal">
<TextBlock>Hold Ctrl+Shift over a control to inspect.</TextBlock>
<Separator Width="8"/>
<TextBlock>Focused:</TextBlock>
<TextBlock Text="{Binding FocusedControl}"/>
<Separator Width="8"/>
<TextBlock>Pointer Over:</TextBlock>
<TextBlock Text="{Binding PointerOverElement}"/>
</StackPanel>
</Border>
</Grid>
</UserControl>

66
src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs

@ -0,0 +1,66 @@
using Avalonia.Controls;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
namespace Avalonia.Diagnostics.Views
{
internal class MainView : UserControl
{
private readonly ConsoleView _console;
private readonly GridSplitter _consoleSplitter;
private readonly Grid _rootGrid;
private readonly int _consoleRow;
private double _consoleHeight = -1;
public MainView()
{
InitializeComponent();
AddHandler(KeyDownEvent, PreviewKeyDown, RoutingStrategies.Tunnel);
_console = this.FindControl<ConsoleView>("console");
_consoleSplitter = this.FindControl<GridSplitter>("consoleSplitter");
_rootGrid = this.FindControl<Grid>("rootGrid");
_consoleRow = Grid.GetRow(_console);
}
public void ToggleConsole()
{
var vm = (MainViewModel)DataContext;
if (_consoleHeight == -1)
{
_consoleHeight = Bounds.Height / 3;
}
vm.Console.ToggleVisibility();
_consoleSplitter.IsVisible = vm.Console.IsVisible;
if (vm.Console.IsVisible)
{
_rootGrid.RowDefinitions[_consoleRow].Height = new GridLength(_consoleHeight, GridUnitType.Pixel);
Dispatcher.UIThread.Post(() => _console.FocusInput(), DispatcherPriority.Background);
}
else
{
_consoleHeight = _rootGrid.RowDefinitions[_consoleRow].Height.Value;
_rootGrid.RowDefinitions[_consoleRow].Height = new GridLength(0, GridUnitType.Pixel);
}
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
ToggleConsole();
e.Handled = true;
}
}
}
}

18
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml

@ -0,0 +1,18 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:Avalonia.Diagnostics.Views"
xmlns:diag="clr-namespace:Avalonia.Diagnostics"
Title="Avalonia DevTools"
x:Class="Avalonia.Diagnostics.Views.MainWindow">
<Window.DataTemplates>
<diag:ViewLocator/>
</Window.DataTemplates>
<Window.Styles>
<StyleInclude Source="resm:Avalonia.Themes.Default.DefaultTheme.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Controls.DataGrid.Themes.Default.xaml?assembly=Avalonia.Controls.DataGrid"/>
</Window.Styles>
<views:MainView/>
</Window>

74
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs

@ -0,0 +1,74 @@
using System;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Markup.Xaml;
using Avalonia.Styling;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.Views
{
internal class MainWindow : Window, IStyleHost
{
private TopLevel _root;
private IDisposable _keySubscription;
public MainWindow()
{
InitializeComponent();
_keySubscription = InputManager.Instance.Process
.OfType<RawKeyEventArgs>()
.Subscribe(RawKeyDown);
}
public TopLevel Root
{
get => _root;
set
{
if (_root != value)
{
_root = value;
DataContext = new MainViewModel(value);
}
}
}
IStyleHost IStyleHost.StylingParent => null;
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
_keySubscription.Dispose();
((MainViewModel)DataContext)?.Dispose();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void RawKeyDown(RawKeyEventArgs e)
{
const RawInputModifiers modifiers = RawInputModifiers.Control | RawInputModifiers.Shift;
if (e.Modifiers == modifiers)
{
var point = (Root as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default;
var control = Root.GetVisualsAt(point, x => (!(x is AdornerLayer) && x.IsVisible))
.FirstOrDefault();
if (control != null)
{
var vm = (MainViewModel)DataContext;
vm.SelectControl((IControl)control);
}
}
}
}
}

19
src/Avalonia.Diagnostics/Views/TreePageView.xaml → src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml

@ -1,26 +1,29 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
x:Class="Avalonia.Diagnostics.Views.TreePageView">
<Grid ColumnDefinitions="*,Auto,3*">
<TreeView Name="tree" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}">
<Grid ColumnDefinitions="*,4,3*">
<TreeView Name="tree"
BorderThickness="0"
Items="{Binding Nodes}"
SelectedItem="{Binding SelectedNode, Mode=TwoWay}">
<TreeView.DataTemplates>
<TreeDataTemplate DataType="vm:TreeNode"
ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="{Binding Type}" />
<TextBlock Text="{Binding Classes}" />
<TextBlock Text="{Binding Type}"/>
<TextBlock Text="{Binding Classes}"/>
</StackPanel>
</TreeDataTemplate>
</TreeView.DataTemplates>
<TreeView.Styles>
<Style Selector="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
</Style>
</TreeView.Styles>
</TreeView>
<GridSplitter Grid.Column="1" />
<ContentControl Content="{Binding Details}" Grid.Column="2" />
<GridSplitter Background="{DynamicResource ThemeControlMidBrush}" Width="1" Grid.Column="1"/>
<ContentControl Content="{Binding Details}" Grid.Column="2"/>
</Grid>
</UserControl>

9
src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs → src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs

@ -1,6 +1,3 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Controls;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Primitives;
@ -12,14 +9,14 @@ using Avalonia.Media;
namespace Avalonia.Diagnostics.Views
{
public class TreePageView : UserControl
internal class TreePageView : UserControl
{
private Control _adorner;
private TreeView _tree;
public TreePageView()
{
InitializeComponent();
this.InitializeComponent();
_tree.ItemContainerGenerator.Index.Materialized += TreeViewItemMaterialized;
}
@ -39,7 +36,7 @@ namespace Avalonia.Diagnostics.Views
_adorner = new Rectangle
{
Fill = new SolidColorBrush(0x80a0c5e8),
[AdornerLayer.AdornedElementProperty] = node.Visual
[AdornerLayer.AdornedElementProperty] = node.Visual,
};
layer.Children.Add(_adorner);

0
src/Avalonia.Diagnostics/VisualTreeDebug.cs → src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs

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

@ -1,25 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Generic;
using System.Linq;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
{
internal class ControlDetailsViewModel : ViewModelBase
{
public ControlDetailsViewModel(IVisual control)
{
if (control is AvaloniaObject avaloniaObject)
{
Properties = AvaloniaPropertyRegistry.Instance.GetRegistered(avaloniaObject)
.Select(x => new PropertyDetails(avaloniaObject, x))
.OrderBy(x => x.IsAttached)
.ThenBy(x => x.Name);
}
}
public IEnumerable<PropertyDetails> Properties { get; }
}
}

76
src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs

@ -1,76 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Input;
namespace Avalonia.Diagnostics.ViewModels
{
internal class DevToolsViewModel : ViewModelBase
{
private IDevToolViewModel _selectedTool;
private string _focusedControl;
private string _pointerOverElement;
public DevToolsViewModel(IControl root)
{
Tools = new ObservableCollection<IDevToolViewModel>
{
new TreePageViewModel(LogicalTreeNode.Create(root), "Logical Tree"),
new TreePageViewModel(VisualTreeNode.Create(root), "Visual Tree"),
new EventsViewModel(root)
};
SelectedTool = Tools.First();
UpdateFocusedControl();
KeyboardDevice.Instance.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement))
{
UpdateFocusedControl();
}
};
root.GetObservable(TopLevel.PointerOverElementProperty)
.Subscribe(x => PointerOverElement = x?.GetType().Name);
}
public IDevToolViewModel SelectedTool
{
get => _selectedTool;
set => RaiseAndSetIfChanged(ref _selectedTool, value);
}
public ObservableCollection<IDevToolViewModel> Tools { get; }
public string FocusedControl
{
get => _focusedControl;
private set => RaiseAndSetIfChanged(ref _focusedControl, value);
}
public string PointerOverElement
{
get => _pointerOverElement;
private set => RaiseAndSetIfChanged(ref _pointerOverElement, value);
}
public void SelectControl(IControl control)
{
if (SelectedTool is TreePageViewModel tree)
{
tree.SelectControl(control);
}
}
private void UpdateFocusedControl()
{
FocusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name;
}
}
}

16
src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs

@ -1,16 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Diagnostics.ViewModels
{
/// <summary>
/// View model interface for tool showing up in DevTools
/// </summary>
public interface IDevToolViewModel
{
/// <summary>
/// Name of a tool.
/// </summary>
string Name { get; }
}
}

58
src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs

@ -1,58 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Data;
namespace Avalonia.Diagnostics.ViewModels
{
internal class PropertyDetails : ViewModelBase
{
private object _value;
private string _priority;
private string _diagnostic;
public PropertyDetails(AvaloniaObject o, AvaloniaProperty property)
{
Name = property.IsAttached ?
$"[{property.OwnerType.Name}.{property.Name}]" :
property.Name;
IsAttached = property.IsAttached;
// TODO: Unsubscribe when view model is deactivated.
o.GetObservable(property).Subscribe(x =>
{
var diagnostic = o.GetDiagnostic(property);
Value = diagnostic.Value ?? "(null)";
Priority = (diagnostic.Priority != BindingPriority.Unset) ?
diagnostic.Priority.ToString() :
diagnostic.Property.Inherits ?
"Inherited" :
"Unset";
Diagnostic = diagnostic.Diagnostic;
});
}
public string Name { get; }
public bool IsAttached { get; }
public string Priority
{
get => _priority;
private set => RaiseAndSetIfChanged(ref _priority, value);
}
public string Diagnostic
{
get => _diagnostic;
private set => RaiseAndSetIfChanged(ref _diagnostic, value);
}
public object Value
{
get => _value;
private set => RaiseAndSetIfChanged(ref _value, value);
}
}
}

75
src/Avalonia.Diagnostics/Views/ControlDetailsView.cs

@ -1,75 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Media;
namespace Avalonia.Diagnostics.Views
{
internal class ControlDetailsView : UserControl
{
private static readonly StyledProperty<ControlDetailsViewModel> ViewModelProperty =
AvaloniaProperty.Register<ControlDetailsView, ControlDetailsViewModel>(nameof(ViewModel));
private SimpleGrid _grid;
public ControlDetailsView()
{
InitializeComponent();
this.GetObservable(DataContextProperty)
.Subscribe(x => ViewModel = (ControlDetailsViewModel)x);
}
public ControlDetailsViewModel ViewModel
{
get => GetValue(ViewModelProperty);
private set
{
SetValue(ViewModelProperty, value);
_grid[GridRepeater.ItemsProperty] = value?.Properties;
}
}
private void InitializeComponent()
{
Func<object, IEnumerable<Control>> pt = PropertyTemplate;
Content = new ScrollViewer { Content = _grid = new SimpleGrid { [GridRepeater.TemplateProperty] = pt } };
}
private IEnumerable<Control> PropertyTemplate(object i)
{
var property = (PropertyDetails)i;
var margin = new Thickness(2);
yield return new TextBlock
{
Margin = margin,
Text = property.Name,
TextWrapping = TextWrapping.NoWrap,
[!ToolTip.TipProperty] = property.GetObservable<string>(nameof(property.Diagnostic)).ToBinding()
};
yield return new TextBlock
{
Margin = margin,
TextWrapping = TextWrapping.NoWrap,
[!TextBlock.TextProperty] = property.GetObservable<object>(nameof(property.Value))
.Select(v => v?.ToString())
.ToBinding()
};
yield return new TextBlock
{
Margin = margin,
TextWrapping = TextWrapping.NoWrap,
[!TextBlock.TextProperty] = property.GetObservable<string>((nameof(property.Priority))).ToBinding()
};
}
}
}

51
src/Avalonia.Diagnostics/Views/GridRepeater.cs

@ -1,51 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections;
using System.Collections.Generic;
using Avalonia.Controls;
namespace Avalonia.Diagnostics.Views
{
internal static class GridRepeater
{
public static readonly AttachedProperty<IEnumerable> ItemsProperty =
AvaloniaProperty.RegisterAttached<SimpleGrid, IEnumerable>("Items", typeof(GridRepeater));
public static readonly AttachedProperty<Func<object, IEnumerable<Control>>> TemplateProperty =
AvaloniaProperty.RegisterAttached<SimpleGrid, Func<object, IEnumerable<Control>>>("Template",
typeof(GridRepeater));
static GridRepeater()
{
ItemsProperty.Changed.Subscribe(ItemsChanged);
}
private static void ItemsChanged(AvaloniaPropertyChangedEventArgs e)
{
var grid = (SimpleGrid)e.Sender;
var items = (IEnumerable)e.NewValue;
var template = grid.GetValue(TemplateProperty);
grid.Children.Clear();
if (items != null)
{
int count = 0;
int cols = 3;
foreach (var item in items)
{
foreach (var control in template(item))
{
grid.Children.Add(control);
SimpleGrid.SetColumn(control, count % cols);
SimpleGrid.SetRow(control, count / cols);
++count;
}
}
}
}
}
}

33
src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs

@ -1,33 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.ComponentModel;
using System.Reactive.Linq;
using System.Reflection;
namespace Avalonia.Diagnostics.Views
{
internal static class PropertyChangedExtensions
{
public static IObservable<T> GetObservable<T>(this INotifyPropertyChanged source, string propertyName)
{
Contract.Requires<ArgumentNullException>(source != null);
Contract.Requires<ArgumentNullException>(propertyName != null);
var property = source.GetType().GetTypeInfo().GetDeclaredProperty(propertyName);
if (property == null)
{
throw new ArgumentException($"Property '{propertyName}' not found on '{source}.");
}
return Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
e => source.PropertyChanged += e,
e => source.PropertyChanged -= e)
.Where(e => e.EventArgs.PropertyName == propertyName)
.Select(_ => (T)property.GetValue(source))
.StartWith((T)property.GetValue(source));
}
}
}

146
src/Avalonia.Diagnostics/Views/SimpleGrid.cs

@ -1,146 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Generic;
using Avalonia.Controls;
namespace Avalonia.Diagnostics.Views
{
/// <summary>
/// A simple grid control that lays out columns with a equal width and rows to their desired
/// size.
/// </summary>
/// <remarks>
/// This is used in the devtools because our <see cref="Grid"/> performance sucks.
/// </remarks>
public class SimpleGrid : Panel
{
private readonly List<double> _columnWidths = new List<double>();
private readonly List<double> _rowHeights = new List<double>();
private double _totalWidth;
private double _totalHeight;
/// <summary>
/// Defines the Column attached property.
/// </summary>
public static readonly AttachedProperty<int> ColumnProperty =
AvaloniaProperty.RegisterAttached<SimpleGrid, Control, int>("Column");
/// <summary>
/// Defines the Row attached property.
/// </summary>
public static readonly AttachedProperty<int> RowProperty =
AvaloniaProperty.RegisterAttached<SimpleGrid, Control, int>("Row");
/// <summary>
/// Gets the value of the Column attached property for a control.
/// </summary>
/// <param name="control">The control.</param>
/// <returns>The control's column.</returns>
public static int GetColumn(IControl control)
{
return control.GetValue(ColumnProperty);
}
/// <summary>
/// Gets the value of the Row attached property for a control.
/// </summary>
/// <param name="control">The control.</param>
/// <returns>The control's row.</returns>
public static int GetRow(IControl control)
{
return control.GetValue(RowProperty);
}
/// <summary>
/// Sets the value of the Column attached property for a control.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="value">The column value.</param>
public static void SetColumn(IControl control, int value)
{
control.SetValue(ColumnProperty, value);
}
/// <summary>
/// Sets the value of the Row attached property for a control.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="value">The row value.</param>
public static void SetRow(IControl control, int value)
{
control.SetValue(RowProperty, value);
}
protected override Size MeasureOverride(Size availableSize)
{
_columnWidths.Clear();
_rowHeights.Clear();
_totalWidth = 0;
_totalHeight = 0;
foreach (var child in Children)
{
var column = GetColumn(child);
var row = GetRow(child);
child.Measure(availableSize);
var desired = child.DesiredSize;
UpdateCell(_columnWidths, column, desired.Width, ref _totalWidth);
UpdateCell(_rowHeights, row, desired.Height, ref _totalHeight);
}
return new Size(_totalWidth, _totalHeight);
}
protected override Size ArrangeOverride(Size finalSize)
{
var columnWidth = finalSize.Width / _columnWidths.Count;
foreach (var child in Children)
{
var column = GetColumn(child);
var row = GetRow(child);
var rect = new Rect(column * columnWidth, GetRowTop(row), columnWidth, _rowHeights[row]);
child.Arrange(rect);
}
return new Size(finalSize.Width, _totalHeight);
}
private double UpdateCell(IList<double> cells, int cell, double value, ref double total)
{
while (cells.Count < cell + 1)
{
cells.Add(0);
}
var existing = cells[cell];
if (value > existing)
{
cells[cell] = value;
total += value - existing;
return value;
}
else
{
return existing;
}
}
private double GetRowTop(int row)
{
var result = 0.0;
for (var i = 0; i < row; ++i)
{
result += _rowHeights[i];
}
return result;
}
}
}
Loading…
Cancel
Save