Browse Source

Merge branch 'master' into refactor/use-selectionmodel

fixes/tree-selectionmodel
Steven Kirk 6 years ago
committed by GitHub
parent
commit
8d7a723616
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      build/CoreLibraries.props
  2. 1
      samples/BindingDemo/BindingDemo.csproj
  3. 1
      samples/ControlCatalog/ControlCatalog.csproj
  4. 1
      samples/RenderDemo/RenderDemo.csproj
  5. 1
      samples/VirtualizationDemo/VirtualizationDemo.csproj
  6. 9
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  7. 2
      src/Avalonia.Base/ValueStore.cs
  8. 8
      src/Avalonia.Controls.DataGrid/Themes/Default.xaml
  9. BIN
      src/Avalonia.Diagnostics/Assets/Fonts/SourceSansPro-Regular.ttf
  10. 12
      src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj
  11. 24
      src/Avalonia.Diagnostics/DevTools.xaml
  12. 159
      src/Avalonia.Diagnostics/DevTools.xaml.cs
  13. 31
      src/Avalonia.Diagnostics/DevToolsExtensions.cs
  14. 22
      src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToBrushConverter.cs
  15. 61
      src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
  16. 36
      src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs
  17. 19
      src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleHistoryItem.cs
  18. 0
      src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs
  19. 8
      src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs
  20. 95
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
  21. 57
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
  22. 112
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs
  23. 179
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  24. 2
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs
  25. 22
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs
  26. 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs
  27. 17
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs
  28. 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs
  29. 6
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs
  30. 126
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  31. 60
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs
  32. 15
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
  33. 47
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
  34. 26
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs
  35. 5
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs
  36. 56
      src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml
  37. 64
      src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs
  38. 25
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
  39. 18
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs
  40. 5
      src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml
  41. 4
      src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs
  42. 55
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
  43. 66
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs
  44. 18
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
  45. 74
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
  46. 19
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
  47. 9
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
  48. 0
      src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs
  49. 25
      src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs
  50. 76
      src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs
  51. 16
      src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs
  52. 58
      src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs
  53. 75
      src/Avalonia.Diagnostics/Views/ControlDetailsView.cs
  54. 51
      src/Avalonia.Diagnostics/Views/GridRepeater.cs
  55. 33
      src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs
  56. 146
      src/Avalonia.Diagnostics/Views/SimpleGrid.cs
  57. 1
      src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
  58. 9
      src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs
  59. 2
      src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs
  60. 5
      src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs
  61. 5
      src/Avalonia.Visuals/Media/ArcSegment.cs
  62. 3
      src/Avalonia.Visuals/Media/BezierSegment .cs
  63. 3
      src/Avalonia.Visuals/Media/LineSegment.cs
  64. 3
      src/Avalonia.Visuals/Media/PathFigure.cs
  65. 4
      src/Avalonia.Visuals/Media/PathGeometry.cs
  66. 3
      src/Avalonia.Visuals/Media/QuadraticBezierSegment .cs
  67. 12
      tests/Avalonia.Base.UnitTests/Data/DefaultValueConverterTests.cs
  68. 42
      tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs

1
build/CoreLibraries.props

@ -4,7 +4,6 @@
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Animation/Avalonia.Animation.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Controls/Avalonia.Controls.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Input/Avalonia.Input.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Interactivity/Avalonia.Interactivity.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Layout/Avalonia.Layout.csproj" />

1
samples/BindingDemo/BindingDemo.csproj

@ -4,6 +4,7 @@
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
</ItemGroup>

1
samples/ControlCatalog/ControlCatalog.csproj

@ -21,6 +21,7 @@
<ItemGroup>
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
</ItemGroup>

1
samples/RenderDemo/RenderDemo.csproj

@ -4,6 +4,7 @@
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
</ItemGroup>

1
samples/VirtualizationDemo/VirtualizationDemo.csproj

@ -4,6 +4,7 @@
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
</ItemGroup>

9
src/Avalonia.Base/Utilities/TypeUtilities.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
@ -185,6 +186,14 @@ namespace Avalonia.Utilities
}
}
var typeConverter = TypeDescriptor.GetConverter(to);
if (typeConverter.CanConvertFrom(from) == true)
{
result = typeConverter.ConvertFrom(null, culture, value);
return true;
}
var cast = FindTypeConversionOperatorMethod(from, to, OperatorType.Implicit | OperatorType.Explicit);
if (cast != null)

2
src/Avalonia.Base/ValueStore.cs

@ -173,7 +173,7 @@ namespace Avalonia
{
return new Diagnostics.AvaloniaPropertyValue(
property,
slot.Value.HasValue ? (object)slot.Value : AvaloniaProperty.UnsetValue,
slot.Value.HasValue ? slot.Value.Value : AvaloniaProperty.UnsetValue,
slot.ValuePriority,
null);
}

8
src/Avalonia.Controls.DataGrid/Themes/Default.xaml

@ -202,7 +202,7 @@
<Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}" />
<Setter Property="DropLocationIndicatorTemplate">
<Template>
<Rectangle Fill="#FF3F4346" Width="2"/>
<Rectangle Fill="{DynamicResource ThemeBorderHighColor}" Width="2"/>
</Template>
</Setter>
<Setter Property="Template">
@ -215,11 +215,11 @@
<DataGridColumnHeader Name="PART_TopLeftCornerHeader" Width="22" />
<DataGridColumnHeadersPresenter Name="PART_ColumnHeadersPresenter" Grid.Column="1"/>
<DataGridColumnHeader Name="PART_TopRightCornerHeader" Grid.Column="2"/>
<Rectangle Name="PART_ColumnHeadersAndRowsSeparator" Grid.ColumnSpan="3" VerticalAlignment="Bottom" StrokeThickness="1" Height="1" Fill="#FFC9CACA"/>
<Rectangle Name="PART_ColumnHeadersAndRowsSeparator" Grid.ColumnSpan="3" VerticalAlignment="Bottom" StrokeThickness="1" Height="1" Fill="{DynamicResource ThemeControlMidHighBrush}"/>
<DataGridRowsPresenter Name="PART_RowsPresenter" Grid.ColumnSpan="2" Grid.Row="1" />
<Rectangle Name="BottomRightCorner" Fill="#FFE9EEF4" Grid.Column="2" Grid.Row="2" />
<Rectangle Name="BottomLeftCorner" Fill="#FFE9EEF4" Grid.Row="2" Grid.ColumnSpan="2" />
<Rectangle Name="BottomRightCorner" Fill="{DynamicResource ThemeControlMidHighBrush}" Grid.Column="2" Grid.Row="2" />
<Rectangle Name="BottomLeftCorner" Fill="{DynamicResource ThemeControlMidHighBrush}" Grid.Row="2" Grid.ColumnSpan="2" />
<ScrollBar Name="PART_VerticalScrollbar" Orientation="Vertical" Grid.Column="2" Grid.Row="1" Width="{DynamicResource ScrollBarThickness}"/>
<Grid Grid.Column="1" Grid.Row="2"

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

Binary file not shown.

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

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

24
src/Avalonia.Diagnostics/DevTools.xaml

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

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

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

31
src/Avalonia.Diagnostics/DevToolsExtensions.cs

@ -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);
}
}
}

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

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

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

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

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

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

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

@ -0,0 +1,19 @@
using System;
using Avalonia.Media;
namespace Avalonia.Diagnostics.Models
{
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
src/Avalonia.Diagnostics/Models/EventChainLink.cs → src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs

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

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

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

@ -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));
}
}
}

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

@ -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));
}
}
}

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

@ -0,0 +1,112 @@
using System;
using System.Reflection;
using System.Threading.Tasks;
using Avalonia.Collections;
using Avalonia.Diagnostics.Models;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
namespace Avalonia.Diagnostics.ViewModels
{
internal class ConsoleViewModel : ViewModelBase
{
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;
}
}

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

@ -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;
}
}
}
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -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.");
}
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -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>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1
src/Avalonia.Dialogs/Avalonia.Dialogs.csproj

@ -13,7 +13,6 @@
<Import Project="..\..\build\BuildTargets.targets" />
<ItemGroup>
<ProjectReference Include="..\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
</ItemGroup>
</Project>

9
src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs

@ -111,9 +111,7 @@ namespace Avalonia.Input.GestureRecognizers
_pointerGrabs.Remove(e.Pointer);
foreach (var r in _recognizers)
{
if(e.Handled)
break;
r.PointerCaptureLost(e);
r.PointerCaptureLost(e.Pointer);
}
}
@ -121,6 +119,11 @@ namespace Avalonia.Input.GestureRecognizers
{
pointer.Capture(_inputElement);
_pointerGrabs[pointer] = recognizer;
foreach (var r in _recognizers)
{
if (r != recognizer)
r.PointerCaptureLost(pointer);
}
}
}

2
src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs

@ -6,7 +6,7 @@ namespace Avalonia.Input.GestureRecognizers
void PointerPressed(PointerPressedEventArgs e);
void PointerReleased(PointerReleasedEventArgs e);
void PointerMoved(PointerEventArgs e);
void PointerCaptureLost(PointerCaptureLostEventArgs e);
void PointerCaptureLost(IPointer pointer);
}
public interface IGestureRecognizerActionsDispatcher

5
src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs

@ -116,9 +116,9 @@ namespace Avalonia.Input.GestureRecognizers
}
}
public void PointerCaptureLost(PointerCaptureLostEventArgs e)
public void PointerCaptureLost(IPointer pointer)
{
if (e.Pointer == _tracking) EndGesture();
if (pointer == _tracking) EndGesture();
}
void EndGesture()
@ -148,6 +148,7 @@ namespace Avalonia.Input.GestureRecognizers
EndGesture();
else
{
_tracking = null;
var savedGestureId = _gestureId;
var st = Stopwatch.StartNew();
var lastTime = TimeSpan.Zero;

5
src/Avalonia.Visuals/Media/ArcSegment.cs

@ -1,6 +1,8 @@
// 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.Globalization;
namespace Avalonia.Media
{
public sealed class ArcSegment : PathSegment
@ -99,5 +101,8 @@ namespace Avalonia.Media
{
ctx.ArcTo(Point, Size, RotationAngle, IsLargeArc, SweepDirection);
}
public override string ToString()
=> $"A {Size} {RotationAngle.ToString(CultureInfo.InvariantCulture)} {(IsLargeArc ? 1 : 0)} {(int)SweepDirection} {Point}";
}
}

3
src/Avalonia.Visuals/Media/BezierSegment .cs

@ -61,5 +61,8 @@ namespace Avalonia.Media
{
ctx.CubicBezierTo(Point1, Point2, Point3);
}
public override string ToString()
=> $"C {Point1} {Point2} {Point3}";
}
}

3
src/Avalonia.Visuals/Media/LineSegment.cs

@ -27,5 +27,8 @@ namespace Avalonia.Media
{
ctx.LineTo(Point);
}
public override string ToString()
=> $"L {Point}";
}
}

3
src/Avalonia.Visuals/Media/PathFigure.cs

@ -98,5 +98,8 @@ namespace Avalonia.Media
}
private PathSegments _segments;
public override string ToString()
=> $"M {StartPoint} {string.Join(" ", _segments)}{(IsClosed ? "Z" : "")}";
}
}

4
src/Avalonia.Visuals/Media/PathGeometry.cs

@ -112,5 +112,9 @@ namespace Avalonia.Media
() => InvalidateGeometry());
_figuresPropertiesObserver = figures?.TrackItemPropertyChanged(_ => InvalidateGeometry());
}
public override string ToString()
=> $"{(FillRule != FillRule.EvenOdd ? "F1 " : "")}{(string.Join(" ", Figures))}";
}
}

3
src/Avalonia.Visuals/Media/QuadraticBezierSegment .cs

@ -42,5 +42,8 @@ namespace Avalonia.Media
{
ctx.QuadraticBezierTo(Point1, Point2);
}
public override string ToString()
=> $"Q {Point1} {Point2}";
}
}

12
tests/Avalonia.Base.UnitTests/Data/DefaultValueConverterTests.cs

@ -50,6 +50,18 @@ namespace Avalonia.Base.UnitTests.Data.Converters
Assert.Equal(TestEnum.Bar, result);
}
[Fact]
public void Can_Convert_String_To_TimeSpan()
{
var result = DefaultValueConverter.Instance.Convert(
"00:00:10",
typeof(TimeSpan),
null,
CultureInfo.InvariantCulture);
Assert.Equal(TimeSpan.FromSeconds(10), result);
}
[Fact]
public void Can_Convert_Int_To_Enum()
{

42
tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs

@ -221,6 +221,48 @@ namespace Avalonia.Visuals.UnitTests.Media
context.Verify(v => v.EndFigure(It.IsAny<bool>()), Times.AtLeastOnce());
}
[Theory]
[InlineData("M 5.5, 5 L 5.5, 5 L 5.5, 5")]
[InlineData("F1 M 9.0771, 11 C 9.1161, 10.701 9.1801, 10.352 9.3031, 10 L 9.0001, 10 L 9.0001, 6.166 L 3.0001, 9.767 L 3.0001, 10 "
+ "L 9.99999999997669E-05, 10 L 9.99999999997669E-05, 0 L 3.0001, 0 L 3.0001, 0.234 L 9.0001, 3.834 L 9.0001, 0 "
+ "L 12.0001, 0 L 12.0001, 8.062 C 12.1861, 8.043 12.3821, 8.031 12.5941, 8.031 C 15.3481, 8.031 15.7961, 9.826 "
+ "15.9201, 11 L 16.0001, 16 L 9.0001, 16 L 9.0001, 12.562 L 9.0001, 11Z")]
[InlineData("F1 M 24, 14 A 2, 2 0 1 1 20, 14 A 2, 2 0 1 1 24, 14Z")]
[InlineData("M 0, 0 L 10, 10Z")]
[InlineData("M 50, 50 L 100, 100 L 150, 50")]
[InlineData("M 50, 50 L -10, -10 L 10, 50")]
[InlineData("M 50, 50 L 100, 100 L 150, 50Z M 50, 50 L 70, 70 L 120, 50Z")]
[InlineData("M 80, 200 A 100, 50 45 1 0 100, 50")]
[InlineData("F1 M 16, 12 C 16, 14.209 14.209, 16 12, 16 C 9.791, 16 8, 14.209 8, 12 C 8, 11.817 8.03, 11.644 8.054, 11.467 L 6.585, 10 "
+ "L 4, 10 L 4, 6.414 L 2.5, 7.914 L 0, 5.414 L 0, 3.586 L 3.586, 0 L 4.414, 0 L 7.414, 3 L 7.586, 3 L 9, 1.586 L "
+ "11.914, 4.5 L 10.414, 6 L 12.461, 8.046 C 14.45, 8.278 16, 9.949 16, 12")]
public void Parsed_Geometry_ToString_Should_Produce_Valid_Value(string pathData)
{
var target = PathGeometry.Parse(pathData);
string output = target.ToString();
Assert.Equal(pathData, output);
}
[Theory]
[InlineData("M5.5.5 5.5.5 5.5.5", "M 5.5, 0.5 L 5.5, 0.5 L 5.5, 0.5")]
[InlineData("F1 M24,14 A2,2,0,1,1,20,14 A2,2,0,1,1,24,14 z", "F1 M 24, 14 A 2, 2 0 1 1 20, 14 A 2, 2 0 1 1 24, 14Z")]
[InlineData("F1M16,12C16,14.209 14.209,16 12,16 9.791,16 8,14.209 8,12 8,11.817 8.03,11.644 8.054,11.467L6.585,10 4,10 "
+ "4,6.414 2.5,7.914 0,5.414 0,3.586 3.586,0 4.414,0 7.414,3 7.586,3 9,1.586 11.914,4.5 10.414,6 "
+ "12.461,8.046C14.45,8.278,16,9.949,16,12",
"F1 M 16, 12 C 16, 14.209 14.209, 16 12, 16 C 9.791, 16 8, 14.209 8, 12 C 8, 11.817 8.03, 11.644 8.054, 11.467 L 6.585, 10 "
+ "L 4, 10 L 4, 6.414 L 2.5, 7.914 L 0, 5.414 L 0, 3.586 L 3.586, 0 L 4.414, 0 L 7.414, 3 L 7.586, 3 L 9, 1.586 L "
+ "11.914, 4.5 L 10.414, 6 L 12.461, 8.046 C 14.45, 8.278 16, 9.949 16, 12")]
public void Parsed_Geometry_ToString_Should_Format_Value(string pathData, string formattedPathData)
{
var target = PathGeometry.Parse(pathData);
string output = target.ToString();
Assert.Equal(formattedPathData, output);
}
[Theory]
[InlineData("0 0")]
[InlineData("j")]

Loading…
Cancel
Save