committed by
GitHub
68 changed files with 1393 additions and 757 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,31 @@ |
|||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Diagnostics; |
||||
|
using Avalonia.Input; |
||||
|
|
||||
|
namespace Avalonia |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Extension methods for attaching DevTools..
|
||||
|
/// </summary>
|
||||
|
public static class DevToolsExtensions |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Attaches DevTools to a window, to be opened with the F12 key.
|
||||
|
/// </summary>
|
||||
|
/// <param name="root">The window to attach DevTools to.</param>
|
||||
|
public static void AttachDevTools(this TopLevel root) |
||||
|
{ |
||||
|
DevTools.Attach(root, new KeyGesture(Key.F12)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Attaches DevTools to a window, to be opened with the specified key gesture.
|
||||
|
/// </summary>
|
||||
|
/// <param name="root">The window to attach DevTools to.</param>
|
||||
|
/// <param name="gesture">The key gesture to open DevTools.</param>
|
||||
|
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 |
||||
|
{ |
||||
|
internal 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,95 @@ |
|||||
|
using System.ComponentModel; |
||||
|
using Avalonia.Collections; |
||||
|
|
||||
|
namespace Avalonia.Diagnostics.ViewModels |
||||
|
{ |
||||
|
internal class AvaloniaPropertyViewModel : PropertyViewModel |
||||
|
{ |
||||
|
private readonly AvaloniaObject _target; |
||||
|
private string _type; |
||||
|
private object _value; |
||||
|
private string _priority; |
||||
|
private string _group; |
||||
|
|
||||
|
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 Type => _type; |
||||
|
|
||||
|
public override string Value |
||||
|
{ |
||||
|
get => ConvertToString(_value); |
||||
|
set |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var convertedValue = ConvertFromString(value, Property.PropertyType); |
||||
|
_target.SetValue(Property, convertedValue); |
||||
|
} |
||||
|
catch { } |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public override string Group |
||||
|
{ |
||||
|
get => _group; |
||||
|
} |
||||
|
|
||||
|
public override void Update() |
||||
|
{ |
||||
|
if (Property.IsDirect) |
||||
|
{ |
||||
|
RaiseAndSetIfChanged(ref _value, _target.GetValue(Property), nameof(Value)); |
||||
|
RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type)); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var val = _target.GetDiagnostic(Property); |
||||
|
|
||||
|
RaiseAndSetIfChanged(ref _value, val?.Value, nameof(Value)); |
||||
|
RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type)); |
||||
|
|
||||
|
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,57 @@ |
|||||
|
using System.ComponentModel; |
||||
|
using System.Reflection; |
||||
|
|
||||
|
namespace Avalonia.Diagnostics.ViewModels |
||||
|
{ |
||||
|
internal class ClrPropertyViewModel : PropertyViewModel |
||||
|
{ |
||||
|
private readonly object _target; |
||||
|
private string _type; |
||||
|
private object _value; |
||||
|
|
||||
|
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 Type => _type; |
||||
|
|
||||
|
public override string Value |
||||
|
{ |
||||
|
get => ConvertToString(_value); |
||||
|
set |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var convertedValue = ConvertFromString(value, Property.PropertyType); |
||||
|
Property.SetValue(_target, convertedValue); |
||||
|
} |
||||
|
catch { } |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public override void Update() |
||||
|
{ |
||||
|
var val = Property.GetValue(_target); |
||||
|
RaiseAndSetIfChanged(ref _value, val, nameof(Value)); |
||||
|
RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
{ |
||||
|
private readonly ConsoleContext _context; |
||||
|
private 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,179 @@ |
|||||
|
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 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.CompareOrdinal(x.Name, y.Name); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private int GroupIndex(string group) |
||||
|
{ |
||||
|
switch (group) |
||||
|
{ |
||||
|
case "Properties": return 0; |
||||
|
case "Attached Properties": return 1; |
||||
|
case "CLR Properties": return 2; |
||||
|
default: return 3; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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,60 @@ |
|||||
|
using System; |
||||
|
using System.ComponentModel; |
||||
|
using System.Globalization; |
||||
|
using System.Reflection; |
||||
|
|
||||
|
namespace Avalonia.Diagnostics.ViewModels |
||||
|
{ |
||||
|
internal abstract class PropertyViewModel : ViewModelBase |
||||
|
{ |
||||
|
private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static; |
||||
|
private static readonly Type[] StringParameter = new[] { typeof(string) }; |
||||
|
private static readonly Type[] StringIFormatProviderParameters = new[] { typeof(string), typeof(IFormatProvider) }; |
||||
|
|
||||
|
public abstract object Key { get; } |
||||
|
public abstract string Name { get; } |
||||
|
public abstract string Group { get; } |
||||
|
public abstract string Type { get; } |
||||
|
public abstract string Value { get; set; } |
||||
|
public abstract void Update(); |
||||
|
|
||||
|
protected static string ConvertToString(object value) |
||||
|
{ |
||||
|
if (value is null) |
||||
|
{ |
||||
|
return "(null)"; |
||||
|
} |
||||
|
|
||||
|
var converter = TypeDescriptor.GetConverter(value); |
||||
|
return converter?.ConvertToString(value) ?? value.ToString(); |
||||
|
} |
||||
|
|
||||
|
protected static object ConvertFromString(string s, Type targetType) |
||||
|
{ |
||||
|
var converter = TypeDescriptor.GetConverter(targetType); |
||||
|
|
||||
|
if (converter != null && converter.CanConvertFrom(typeof(string))) |
||||
|
{ |
||||
|
return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null); |
||||
|
|
||||
|
if (method != null) |
||||
|
{ |
||||
|
return method.Invoke(null, new object[] { s, CultureInfo.InvariantCulture }); |
||||
|
} |
||||
|
|
||||
|
method = targetType.GetMethod("Parse", PublicStatic, null, StringParameter, null); |
||||
|
|
||||
|
if (method != null) |
||||
|
{ |
||||
|
return method.Invoke(null, new object[] { s }); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
throw new InvalidCastException("Unable to convert value."); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,34 +1,42 @@ |
|||||
// Copyright (c) The Avalonia Project. All rights reserved.
|
using System; |
||||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
||||
|
|
||||
using System.Collections.Generic; |
using System.Collections.Generic; |
||||
using System.ComponentModel; |
using System.ComponentModel; |
||||
using System.Runtime.CompilerServices; |
using System.Runtime.CompilerServices; |
||||
using JetBrains.Annotations; |
|
||||
|
|
||||
namespace Avalonia.Diagnostics.ViewModels |
namespace Avalonia.Diagnostics.ViewModels |
||||
{ |
{ |
||||
internal class ViewModelBase : INotifyPropertyChanged |
internal 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) |
protected bool RaiseAndSetIfChanged<T>(ref T field, T value, [CallerMemberName] string propertyName = null) |
||||
{ |
{ |
||||
if (!EqualityComparer<T>.Default.Equals(field, value)) |
if (!EqualityComparer<T>.Default.Equals(field, value)) |
||||
{ |
{ |
||||
field = value; |
field = value; |
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); |
RaisePropertyChanged(propertyName); |
||||
return true; |
return true; |
||||
} |
} |
||||
|
|
||||
return false; |
return false; |
||||
} |
} |
||||
|
|
||||
[NotifyPropertyChangedInvocator] |
|
||||
protected void RaisePropertyChanged([CallerMemberName] string propertyName = null) |
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,25 @@ |
|||||
|
<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}" |
||||
|
CanUserResizeColumns="true"> |
||||
|
<DataGrid.Columns> |
||||
|
<DataGridTextColumn Header="Property" Binding="{Binding Name}" IsReadOnly="True"/> |
||||
|
<DataGridTextColumn Header="Value" Binding="{Binding Value}"/> |
||||
|
<DataGridTextColumn Header="Type" Binding="{Binding Type}"/> |
||||
|
<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" |
<UserControl xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels" |
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> |
<UserControl.Resources> |
||||
<vm:BoolToBrushConverter x:Key="boolToBrush" /> |
<conv:BoolToBrushConverter x:Key="boolToBrush" Brush="#d9ffdc"/> |
||||
</UserControl.Resources> |
</UserControl.Resources> |
||||
<Grid ColumnDefinitions="*,4,3*"> |
<Grid ColumnDefinitions="*,4,3*"> |
||||
<TreeView Name="tree" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}" |
<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" |
<UserControl xmlns="https://github.com/avaloniaui" |
||||
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels" |
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels" |
||||
x:Class="Avalonia.Diagnostics.Views.TreePageView"> |
x:Class="Avalonia.Diagnostics.Views.TreePageView"> |
||||
<Grid ColumnDefinitions="*,Auto,3*"> |
<Grid ColumnDefinitions="*,4,3*"> |
||||
<TreeView Name="tree" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}"> |
<TreeView Name="tree" |
||||
|
BorderThickness="0" |
||||
|
Items="{Binding Nodes}" |
||||
|
SelectedItem="{Binding SelectedNode, Mode=TwoWay}"> |
||||
<TreeView.DataTemplates> |
<TreeView.DataTemplates> |
||||
<TreeDataTemplate DataType="vm:TreeNode" |
<TreeDataTemplate DataType="vm:TreeNode" |
||||
ItemsSource="{Binding Children}"> |
ItemsSource="{Binding Children}"> |
||||
<StackPanel Orientation="Horizontal" Spacing="8"> |
<StackPanel Orientation="Horizontal" Spacing="8"> |
||||
<TextBlock Text="{Binding Type}" /> |
<TextBlock Text="{Binding Type}"/> |
||||
<TextBlock Text="{Binding Classes}" /> |
<TextBlock Text="{Binding Classes}"/> |
||||
</StackPanel> |
</StackPanel> |
||||
</TreeDataTemplate> |
</TreeDataTemplate> |
||||
</TreeView.DataTemplates> |
</TreeView.DataTemplates> |
||||
<TreeView.Styles> |
<TreeView.Styles> |
||||
<Style Selector="TreeViewItem"> |
<Style Selector="TreeViewItem"> |
||||
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" /> |
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/> |
||||
</Style> |
</Style> |
||||
</TreeView.Styles> |
</TreeView.Styles> |
||||
</TreeView> |
</TreeView> |
||||
|
|
||||
<GridSplitter Grid.Column="1" /> |
<GridSplitter Background="{DynamicResource ThemeControlMidBrush}" Width="1" Grid.Column="1"/> |
||||
<ContentControl Content="{Binding Details}" Grid.Column="2" /> |
<ContentControl Content="{Binding Details}" Grid.Column="2"/> |
||||
</Grid> |
</Grid> |
||||
</UserControl> |
</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