48 changed files with 1286 additions and 746 deletions
Binary file not shown.
@ -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> |
|||
@ -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()); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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,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)); |
|||
} |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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}" |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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() |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
} |
|||
@ -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…
Reference in new issue