Browse Source

Merge branch 'master' into features/use-external-microcom-generator

pull/7259/head
Nikita Tsukanov 5 years ago
committed by GitHub
parent
commit
b92007f154
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      packages/Avalonia/Avalonia.csproj
  2. 38
      samples/ControlCatalog/App.xaml.cs
  3. 1
      samples/ControlCatalog/DecoratedWindow.xaml.cs
  4. 42
      samples/ControlCatalog/MainView.xaml.cs
  5. 5
      samples/ControlCatalog/MainWindow.xaml.cs
  6. 2
      src/Avalonia.Animation/Avalonia.Animation.csproj
  7. 2
      src/Avalonia.Base/Avalonia.Base.csproj
  8. 2
      src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs
  9. 2
      src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj
  10. 2
      src/Avalonia.Controls/Avalonia.Controls.csproj
  11. 7
      src/Avalonia.Controls/ButtonSpinner.cs
  12. 19
      src/Avalonia.Controls/TextBox.cs
  13. 2
      src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj
  14. 2
      src/Avalonia.Desktop/Avalonia.Desktop.csproj
  15. 2
      src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj
  16. 2
      src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj
  17. 44
      src/Avalonia.Diagnostics/DevToolsExtensions.cs
  18. 47
      src/Avalonia.Diagnostics/Diagnostics/Behaviors/ColumnDefinition.cs
  19. 119
      src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs
  20. 24
      src/Avalonia.Diagnostics/Diagnostics/Converters/GetTypeNameConverter.cs
  21. 95
      src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
  22. 1
      src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
  23. 28
      src/Avalonia.Diagnostics/Diagnostics/KeyGestureExtesions.cs
  24. 33
      src/Avalonia.Diagnostics/Diagnostics/TypeExtesnions.cs
  25. 13
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
  26. 12
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
  27. 33
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  28. 90
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs
  29. 106
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  30. 13
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs
  31. 9
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
  32. 14
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs
  33. 80
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs
  34. 36
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
  35. 12
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
  36. 21
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
  37. 7
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
  38. 2
      src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
  39. 2
      src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
  40. 2
      src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj
  41. 2
      src/Avalonia.Headless/Avalonia.Headless.csproj
  42. 2
      src/Avalonia.Input/Avalonia.Input.csproj
  43. 2
      src/Avalonia.Interactivity/Avalonia.Interactivity.csproj
  44. 2
      src/Avalonia.Layout/Avalonia.Layout.csproj
  45. 2
      src/Avalonia.Native/Avalonia.Native.csproj
  46. 2
      src/Avalonia.OpenGL/Avalonia.OpenGL.csproj
  47. 2
      src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj
  48. 57
      src/Avalonia.ReactiveUI/RoutedViewHost.cs
  49. 52
      src/Avalonia.ReactiveUI/ViewModelViewHost.cs
  50. 2
      src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj
  51. 2
      src/Avalonia.Styling/Avalonia.Styling.csproj
  52. 2
      src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj
  53. 1
      src/Avalonia.Themes.Default/ButtonSpinner.xaml
  54. 2
      src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj
  55. 1
      src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml
  56. 96
      src/Avalonia.Themes.Fluent/FluentTheme.cs
  57. 2
      src/Avalonia.Visuals/Avalonia.Visuals.csproj
  58. 4
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs
  59. 2
      src/Avalonia.X11/Avalonia.X11.csproj
  60. 2
      src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj
  61. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj
  62. 2
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  63. 2
      src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
  64. 2
      src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
  65. 2
      src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs
  66. 2
      src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj
  67. 2
      src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
  68. 73
      tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs
  69. 72
      tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs

2
packages/Avalonia/Avalonia.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net461;netcoreapp2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0;net461;netcoreapp2.0</TargetFrameworks>
<PackageId>Avalonia</PackageId> <PackageId>Avalonia</PackageId>
</PropertyGroup> </PropertyGroup>

38
samples/ControlCatalog/App.xaml.cs

@ -5,6 +5,7 @@ using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.Styling; using Avalonia.Markup.Xaml.Styling;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.Themes.Fluent;
using ControlCatalog.ViewModels; using ControlCatalog.ViewModels;
namespace ControlCatalog namespace ControlCatalog
@ -16,33 +17,17 @@ namespace ControlCatalog
DataContext = new ApplicationViewModel(); DataContext = new ApplicationViewModel();
} }
private static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) public static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{ {
Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml") Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml")
}; };
private static readonly StyleInclude DataGridDefault = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) public static readonly StyleInclude DataGridDefault = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{ {
Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Default.xaml") Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Default.xaml")
}; };
public static Styles FluentDark = new Styles public static FluentTheme Fluent = new FluentTheme(new Uri("avares://ControlCatalog/Styles"));
{
new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{
Source = new Uri("avares://Avalonia.Themes.Fluent/FluentDark.xaml")
},
DataGridFluent
};
public static Styles FluentLight = new Styles
{
new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{
Source = new Uri("avares://Avalonia.Themes.Fluent/FluentLight.xaml")
},
DataGridFluent
};
public static Styles DefaultLight = new Styles public static Styles DefaultLight = new Styles
{ {
@ -65,8 +50,7 @@ namespace ControlCatalog
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{ {
Source = new Uri("avares://Avalonia.Themes.Default/DefaultTheme.xaml") Source = new Uri("avares://Avalonia.Themes.Default/DefaultTheme.xaml")
}, }
DataGridDefault
}; };
public static Styles DefaultDark = new Styles public static Styles DefaultDark = new Styles
@ -90,14 +74,13 @@ namespace ControlCatalog
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{ {
Source = new Uri("avares://Avalonia.Themes.Default/DefaultTheme.xaml") Source = new Uri("avares://Avalonia.Themes.Default/DefaultTheme.xaml")
}, }
DataGridDefault
}; };
public override void Initialize() public override void Initialize()
{ {
Styles.Insert(0, FluentLight); Styles.Insert(0, Fluent);
Styles.Insert(1, DataGridFluent);
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
@ -111,6 +94,11 @@ namespace ControlCatalog
singleViewLifetime.MainView = new MainView(); singleViewLifetime.MainView = new MainView();
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();
this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions()
{
StartupScreenIndex = 1,
});
} }
} }
} }

1
samples/ControlCatalog/DecoratedWindow.xaml.cs

@ -11,7 +11,6 @@ namespace ControlCatalog
public DecoratedWindow() public DecoratedWindow()
{ {
this.InitializeComponent(); this.InitializeComponent();
this.AttachDevTools();
} }
void SetupSide(string name, StandardCursorType cursor, WindowEdge edge) void SetupSide(string name, StandardCursorType cursor, WindowEdge edge)

42
samples/ControlCatalog/MainView.xaml.cs

@ -3,14 +3,12 @@ using System.Collections;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Markup.Xaml.Styling;
using Avalonia.Markup.Xaml.XamlIl;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.Platform; using Avalonia.Platform;
using ControlCatalog.Pages; using Avalonia.Themes.Fluent;
using ControlCatalog.Models; using ControlCatalog.Models;
using ControlCatalog.Pages;
namespace ControlCatalog namespace ControlCatalog
{ {
@ -43,14 +41,36 @@ namespace ControlCatalog
{ {
if (themes.SelectedItem is CatalogTheme theme) if (themes.SelectedItem is CatalogTheme theme)
{ {
Application.Current.Styles[0] = theme switch var themeStyle = Application.Current.Styles[0];
if (theme == CatalogTheme.FluentLight)
{
if (App.Fluent.Mode != FluentThemeMode.Light)
{
App.Fluent.Mode = FluentThemeMode.Light;
}
Application.Current.Styles[0] = App.Fluent;
Application.Current.Styles[1] = App.DataGridFluent;
}
else if (theme == CatalogTheme.FluentDark)
{
if (App.Fluent.Mode != FluentThemeMode.Dark)
{
App.Fluent.Mode = FluentThemeMode.Dark;
}
Application.Current.Styles[0] = App.Fluent;
Application.Current.Styles[1] = App.DataGridFluent;
}
else if (theme == CatalogTheme.DefaultLight)
{
Application.Current.Styles[0] = App.DefaultLight;
Application.Current.Styles[1] = App.DataGridDefault;
}
else if (theme == CatalogTheme.DefaultDark)
{ {
CatalogTheme.FluentLight => App.FluentLight, Application.Current.Styles[0] = App.DefaultDark;
CatalogTheme.FluentDark => App.FluentDark, Application.Current.Styles[1] = App.DataGridDefault;
CatalogTheme.DefaultLight => App.DefaultLight, }
CatalogTheme.DefaultDark => App.DefaultDark,
_ => Application.Current.Styles[0]
};
} }
}; };

5
samples/ControlCatalog/MainWindow.xaml.cs

@ -17,10 +17,7 @@ namespace ControlCatalog
public MainWindow() public MainWindow()
{ {
this.InitializeComponent(); this.InitializeComponent();
this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions()
{
StartupScreenIndex = 1,
});
//Renderer.DrawFps = true; //Renderer.DrawFps = true;
//Renderer.DrawDirtyRects = Renderer.DrawFps = true; //Renderer.DrawDirtyRects = Renderer.DrawFps = true;

2
src/Avalonia.Animation/Avalonia.Animation.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" /> <ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />

2
src/Avalonia.Base/Avalonia.Base.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<AssemblyName>Avalonia.Base</AssemblyName> <AssemblyName>Avalonia.Base</AssemblyName>
<RootNamespace>Avalonia</RootNamespace> <RootNamespace>Avalonia</RootNamespace>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks> <AllowUnsafeBlocks>True</AllowUnsafeBlocks>

2
src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs

@ -180,7 +180,7 @@ namespace Avalonia.Utilities
{ {
var r = _data[c]; var r = _data[c];
if (r?.TryGetTarget(out var sub) == true) if (r?.TryGetTarget(out var sub) == true)
sub.OnEvent(sender, eventArgs); sub!.OnEvent(sender, eventArgs);
else else
needCompact = true; needCompact = true;
} }

2
src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<PackageId>Avalonia.Controls.DataGrid</PackageId> <PackageId>Avalonia.Controls.DataGrid</PackageId>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

2
src/Avalonia.Controls/Avalonia.Controls.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" /> <Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />

7
src/Avalonia.Controls/ButtonSpinner.cs

@ -1,4 +1,4 @@
using System; using System;
using Avalonia.Controls.Metadata; using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Data; using Avalonia.Data;
@ -196,13 +196,14 @@ namespace Avalonia.Controls
protected override void OnPointerWheelChanged(PointerWheelEventArgs e) protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{ {
base.OnPointerWheelChanged(e); base.OnPointerWheelChanged(e);
if (!e.Handled && AllowSpin)
if (AllowSpin && IsKeyboardFocusWithin)
{ {
if (e.Delta.Y != 0) if (e.Delta.Y != 0)
{ {
var spinnerEventArgs = new SpinEventArgs(SpinEvent, (e.Delta.Y < 0) ? SpinDirection.Decrease : SpinDirection.Increase, true); var spinnerEventArgs = new SpinEventArgs(SpinEvent, (e.Delta.Y < 0) ? SpinDirection.Decrease : SpinDirection.Increase, true);
OnSpin(spinnerEventArgs); OnSpin(spinnerEventArgs);
e.Handled = spinnerEventArgs.Handled; e.Handled = true;
} }
} }
} }

19
src/Avalonia.Controls/TextBox.cs

@ -1006,13 +1006,28 @@ namespace Avalonia.Controls
if (text != null && clickInfo.Properties.IsLeftButtonPressed && !(clickInfo.Pointer?.Captured is Border)) if (text != null && clickInfo.Properties.IsLeftButtonPressed && !(clickInfo.Pointer?.Captured is Border))
{ {
var point = e.GetPosition(_presenter); var point = e.GetPosition(_presenter);
var index = CaretIndex = _presenter.GetCaretIndex(point); var index = _presenter.GetCaretIndex(point);
var clickToSelect = index != CaretIndex && e.KeyModifiers.HasFlag(KeyModifiers.Shift);
if (!clickToSelect)
{
CaretIndex = index;
}
#pragma warning disable CS0618 // Type or member is obsolete #pragma warning disable CS0618 // Type or member is obsolete
switch (e.ClickCount) switch (e.ClickCount)
#pragma warning restore CS0618 // Type or member is obsolete #pragma warning restore CS0618 // Type or member is obsolete
{ {
case 1: case 1:
SelectionStart = SelectionEnd = index; if (clickToSelect)
{
SelectionStart = Math.Min(index, CaretIndex);
SelectionEnd = Math.Max(index, CaretIndex);
}
else
{
SelectionStart = SelectionEnd = index;
}
break; break;
case 2: case 2:
if (!StringUtils.IsStartOfWord(text, index)) if (!StringUtils.IsStartOfWord(text, index))

2
src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<!-- WARNING! The designer support version number needs to be frozen <!-- WARNING! The designer support version number needs to be frozen
To allow projects that implement designer functionality to still To allow projects that implement designer functionality to still
work with newer versions of Avalonia. This version number only work with newer versions of Avalonia. This version number only

2
src/Avalonia.Desktop/Avalonia.Desktop.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<PackageId>Avalonia.Desktop</PackageId> <PackageId>Avalonia.Desktop</PackageId>
</PropertyGroup> </PropertyGroup>

2
src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net461;netcoreapp2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;net461;netcoreapp2.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

2
src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<RootNamespace>Avalonia</RootNamespace> <RootNamespace>Avalonia</RootNamespace>
<PackageId>Avalonia.Diagnostics</PackageId> <PackageId>Avalonia.Diagnostics</PackageId>
</PropertyGroup> </PropertyGroup>

44
src/Avalonia.Diagnostics/DevToolsExtensions.cs

@ -37,5 +37,49 @@ namespace Avalonia
{ {
DevTools.Attach(root, options); DevTools.Attach(root, options);
} }
/// <summary>
/// Attaches DevTools to a Application, to be opened with the specified options.
/// </summary>
/// <param name="application">The Application to attach DevTools to.</param>
public static void AttachDevTools(this Application application)
{
DevTools.Attach(application, new DevToolsOptions());
}
/// <summary>
/// Attaches DevTools to a Application, to be opened with the specified options.
/// </summary>
/// <param name="application">The Application to attach DevTools to.</param>
/// <param name="options">Additional settings of DevTools.</param>
/// <remarks>
/// Attach DevTools should only be called after application initialization is complete. A good point is <see cref="Application.OnFrameworkInitializationCompleted"/>
/// </remarks>
/// <example>
/// <code>
/// public class App : Application
/// {
/// public override void OnFrameworkInitializationCompleted()
/// {
/// if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
/// {
/// desktopLifetime.MainWindow = new MainWindow();
/// }
/// else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
/// singleViewLifetime.MainView = new MainView();
///
/// base.OnFrameworkInitializationCompleted();
/// this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions()
/// {
/// StartupScreenIndex = 1,
/// });
/// }
/// }
/// </code>
/// </example>
public static void AttachDevTools(this Application application, DevToolsOptions options)
{
DevTools.Attach(application, options);
}
} }
} }

47
src/Avalonia.Diagnostics/Diagnostics/Behaviors/ColumnDefinition.cs

@ -0,0 +1,47 @@
namespace Avalonia.Diagnostics.Behaviors
{
/// <summary>
/// See discussion https://github.com/AvaloniaUI/Avalonia/discussions/6773
/// </summary>
static class ColumnDefinition
{
private readonly static Avalonia.Controls.GridLength ZeroWidth =
new Avalonia.Controls.GridLength(0, Avalonia.Controls.GridUnitType.Pixel);
private readonly static AttachedProperty<Avalonia.Controls.GridLength?> LastWidthProperty =
AvaloniaProperty.RegisterAttached<Avalonia.Controls.ColumnDefinition, Avalonia.Controls.GridLength?>("LastWidth"
, typeof(ColumnDefinition)
, default);
public readonly static AttachedProperty<bool> IsVisibleProperty =
AvaloniaProperty.RegisterAttached<Avalonia.Controls.ColumnDefinition, bool>("IsVisible"
, typeof(ColumnDefinition)
, true
, coerce: (element, visibility) =>
{
var lastWidth = element.GetValue(LastWidthProperty);
if (visibility == true && lastWidth is { })
{
element.SetValue(Avalonia.Controls.ColumnDefinition.WidthProperty, lastWidth);
}
else if (visibility == false)
{
element.SetValue(LastWidthProperty, element.GetValue(Avalonia.Controls.ColumnDefinition.WidthProperty));
element.SetValue(Avalonia.Controls.ColumnDefinition.WidthProperty, ZeroWidth);
}
return visibility;
}
);
public static bool GetIsVisible(Avalonia.Controls.ColumnDefinition columnDefinition)
{
return columnDefinition.GetValue(IsVisibleProperty);
}
public static void SetIsVisible(Avalonia.Controls.ColumnDefinition columnDefinition, bool visibility)
{
columnDefinition.SetValue(IsVisibleProperty, visibility);
}
}
}

119
src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs

@ -0,0 +1,119 @@
using System;
using Avalonia.Controls;
using Lifetimes = Avalonia.Controls.ApplicationLifetimes;
using App = Avalonia.Application;
namespace Avalonia.Diagnostics.Controls
{
class Application : AvaloniaObject
, Input.ICloseable
{
private readonly App _application;
private static readonly Version s_version = typeof(IAvaloniaObject).Assembly?.GetName()?.Version
?? Version.Parse("0.0.00");
public event EventHandler? Closed;
public Application(App application)
{
_application = application;
if (_application.ApplicationLifetime is Lifetimes.IControlledApplicationLifetime controller)
{
EventHandler<Lifetimes.ControlledApplicationLifetimeExitEventArgs> eh = default!;
eh = (s, e) =>
{
controller.Exit -= eh;
Closed?.Invoke(s, e);
};
controller.Exit += eh;
}
RendererRoot = application.ApplicationLifetime switch
{
Lifetimes.IClassicDesktopStyleApplicationLifetime classic => classic.MainWindow.Renderer,
Lifetimes.ISingleViewApplicationLifetime single => (single.MainView as VisualTree.IVisual)?.VisualRoot?.Renderer,
_ => null
};
}
internal App Instance => _application;
/// <summary>
/// Defines the <see cref="DataContext"/> property.
/// </summary>
public object? DataContext =>
_application.DataContext;
/// <summary>
/// Gets or sets the application's global data templates.
/// </summary>
/// <value>
/// The application's global data templates.
/// </value>
public Avalonia.Controls.Templates.DataTemplates DataTemplates =>
_application.DataTemplates;
/// <summary>
/// Gets the application's focus manager.
/// </summary>
/// <value>
/// The application's focus manager.
/// </value>
public Input.IFocusManager? FocusManager =>
_application.FocusManager;
/// <summary>
/// Gets the application's input manager.
/// </summary>
/// <value>
/// The application's input manager.
/// </value>
public Input.InputManager? InputManager =>
_application.InputManager;
/// <summary>
/// Gets the application clipboard.
/// </summary>
public Input.Platform.IClipboard? Clipboard =>
_application.Clipboard;
/// <summary>
/// Gets the application's global resource dictionary.
/// </summary>
public IResourceDictionary Resources =>
_application.Resources;
/// <summary>
/// Gets the application's global styles.
/// </summary>
/// <value>
/// The application's global styles.
/// </value>
/// <remarks>
/// Global styles apply to all windows in the application.
/// </remarks>
public Styling.Styles Styles =>
_application.Styles;
/// <summary>
/// Application lifetime, use it for things like setting the main window and exiting the app from code
/// Currently supported lifetimes are:
/// - <see cref="Lifetimes.IClassicDesktopStyleApplicationLifetime"/>
/// - <see cref="Lifetimes.ISingleViewApplicationLifetime"/>
/// - <see cref="Lifetimes.IControlledApplicationLifetime"/>
/// </summary>
public Lifetimes.IApplicationLifetime? ApplicationLifetime =>
_application.ApplicationLifetime;
/// <summary>
/// Application name to be used for various platform-specific purposes
/// </summary>
public string? Name =>
_application.Name;
/// <summary>
/// Gets the root of the visual tree, if the control is attached to a visual tree.
/// </summary>
internal Rendering.IRenderer? RendererRoot { get; }
}
}

24
src/Avalonia.Diagnostics/Diagnostics/Converters/GetTypeNameConverter.cs

@ -0,0 +1,24 @@
using System;
using System.Globalization;
using Avalonia.Data;
using Avalonia.Data.Converters;
namespace Avalonia.Diagnostics.Converters
{
internal class GetTypeNameConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is Type type)
{
return type.GetTypeName();
}
return BindingOperations.DoNothing;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return BindingOperations.DoNothing;
}
}
}

95
src/Avalonia.Diagnostics/Diagnostics/DevTools.cs

@ -1,17 +1,21 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Diagnostics.Views; using Avalonia.Diagnostics.Views;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Interactivity; using Avalonia.Interactivity;
namespace Avalonia.Diagnostics namespace Avalonia.Diagnostics
{ {
public static class DevTools public static class DevTools
{ {
private static readonly Dictionary<TopLevel, MainWindow> s_open = private static readonly Dictionary<AvaloniaObject, MainWindow> s_open =
new Dictionary<TopLevel, MainWindow>(); new Dictionary<AvaloniaObject, MainWindow>();
private static bool s_attachedToApplication;
public static IDisposable Attach(TopLevel root, KeyGesture gesture) public static IDisposable Attach(TopLevel root, KeyGesture gesture)
{ {
@ -23,6 +27,11 @@ namespace Avalonia.Diagnostics
public static IDisposable Attach(TopLevel root, DevToolsOptions options) public static IDisposable Attach(TopLevel root, DevToolsOptions options)
{ {
if (s_attachedToApplication == true)
{
throw new ArgumentException("DevTools already attached to application", nameof(root));
}
void PreviewKeyDown(object? sender, KeyEventArgs e) void PreviewKeyDown(object? sender, KeyEventArgs e)
{ {
if (options.Gesture.Matches(e)) if (options.Gesture.Matches(e))
@ -37,45 +46,95 @@ namespace Avalonia.Diagnostics
RoutingStrategies.Tunnel); RoutingStrategies.Tunnel);
} }
public static IDisposable Open(TopLevel root) => Open(root, new DevToolsOptions()); public static IDisposable Open(TopLevel root) =>
Open(Application.Current,new DevToolsOptions(),root as Window);
public static IDisposable Open(TopLevel root, DevToolsOptions options) =>
Open(Application.Current, options, root as Window);
public static IDisposable Open(TopLevel root, DevToolsOptions options) private static void DevToolsClosed(object? sender, EventArgs e)
{ {
if (s_open.TryGetValue(root, out var window)) var window = (MainWindow)sender!;
window.Closed -= DevToolsClosed;
if (window.Root is Controls.Application host)
{
s_open.Remove(host.Instance);
}
else
{ {
s_open.Remove(window.Root!);
}
}
internal static IDisposable Attach(Application? application, DevToolsOptions options, Window? owner = null)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
var result = Disposable.Empty;
// Skip if call on Design Mode
if (!Avalonia.Controls.Design.IsDesignMode
&& !s_attachedToApplication)
{
var lifeTime = application.ApplicationLifetime
as Avalonia.Controls.ApplicationLifetimes.IControlledApplicationLifetime;
if (lifeTime is null)
{
throw new ArgumentNullException(nameof(Application.ApplicationLifetime));
}
if (application.InputManager is { })
{
s_attachedToApplication = true;
application.InputManager.PreProcess.OfType<RawKeyEventArgs>().Subscribe(e =>
{
if (options.Gesture.Matches(e))
{
result = Open(application, options, owner);
}
});
}
}
return result;
}
private static IDisposable Open(Application? application, DevToolsOptions options, Window? owner = default)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
if (s_open.TryGetValue(application, out var window))
{
window.Activate(); window.Activate();
} }
else else
{ {
window = new MainWindow window = new MainWindow
{ {
Root = root, Root = new Controls.Application(application),
Width = options.Size.Width, Width = options.Size.Width,
Height = options.Size.Height, Height = options.Size.Height,
}; };
window.SetOptions(options); window.SetOptions(options);
window.Closed += DevToolsClosed; window.Closed += DevToolsClosed;
s_open.Add(root, window); s_open.Add(application, window);
if (options.ShowAsChildWindow && owner is { })
if (options.ShowAsChildWindow && root is Window inspectedWindow)
{ {
window.Show(inspectedWindow); window.Show(owner);
} }
else else
{ {
window.Show(); window.Show();
} }
} }
return Disposable.Create(() => window?.Close()); 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;
}
} }
} }

1
src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs

@ -16,6 +16,7 @@ namespace Avalonia.Diagnostics
/// Gets or sets a value indicating whether DevTools should be displayed as a child window /// Gets or sets a value indicating whether DevTools should be displayed as a child window
/// of the window being inspected. The default value is true. /// of the window being inspected. The default value is true.
/// </summary> /// </summary>
/// <remarks>This setting is ignored if DevTools is attached to <see cref="Application"/></remarks>
public bool ShowAsChildWindow { get; set; } = true; public bool ShowAsChildWindow { get; set; } = true;
/// <summary> /// <summary>

28
src/Avalonia.Diagnostics/Diagnostics/KeyGestureExtesions.cs

@ -0,0 +1,28 @@
using Avalonia.Input;
using Avalonia.Input.Raw;
namespace Avalonia.Diagnostics
{
static class KeyGestureExtesions
{
public static bool Matches(this KeyGesture gesture, RawKeyEventArgs keyEvent) =>
keyEvent != null &&
(KeyModifiers)(keyEvent.Modifiers & RawInputModifiers.KeyboardMask) == gesture.KeyModifiers &&
ResolveNumPadOperationKey(keyEvent.Key) == ResolveNumPadOperationKey(gesture.Key);
private static Key ResolveNumPadOperationKey(Key key)
{
switch (key)
{
case Key.Add:
return Key.OemPlus;
case Key.Subtract:
return Key.OemMinus;
case Key.Decimal:
return Key.OemPeriod;
default:
return key;
}
}
}
}

33
src/Avalonia.Diagnostics/Diagnostics/TypeExtesnions.cs

@ -0,0 +1,33 @@
using System;
using System.Runtime.CompilerServices;
using System.Linq;
namespace Avalonia.Diagnostics
{
internal static class TypeExtesnions
{
private static readonly ConditionalWeakTable<Type, string> s_getTypeNameCache =
new ConditionalWeakTable<Type, string>();
public static string GetTypeName(this Type type)
{
if (!s_getTypeNameCache.TryGetValue(type, out var name))
{
name = type.Name;
if (Nullable.GetUnderlyingType(type) is Type nullable)
{
name = nullable.Name + "?";
}
else if (type.IsGenericType)
{
var definition = type.GetGenericTypeDefinition();
var arguments = type.GetGenericArguments();
name = definition.Name.Substring(0, definition.Name.IndexOf('`'));
name = $"{name}<{string.Join(",", arguments.Select(GetTypeName))}>";
}
s_getTypeNameCache.Add(type, name);
}
return name;
}
}
}

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

@ -3,10 +3,11 @@ namespace Avalonia.Diagnostics.ViewModels
internal class AvaloniaPropertyViewModel : PropertyViewModel internal class AvaloniaPropertyViewModel : PropertyViewModel
{ {
private readonly AvaloniaObject _target; private readonly AvaloniaObject _target;
private System.Type _type; private System.Type _assignedType;
private object? _value; private object? _value;
private string _priority; private string _priority;
private string _group; private string _group;
private readonly System.Type _propertyType;
#nullable disable #nullable disable
// Remove "nullable disable" after MemberNotNull will work on our CI. // Remove "nullable disable" after MemberNotNull will work on our CI.
@ -20,6 +21,7 @@ namespace Avalonia.Diagnostics.ViewModels
$"[{property.OwnerType.Name}.{property.Name}]" : $"[{property.OwnerType.Name}.{property.Name}]" :
property.Name; property.Name;
DeclaringType = property.OwnerType; DeclaringType = property.OwnerType;
_propertyType = property.PropertyType;
Update(); Update();
} }
@ -32,7 +34,7 @@ namespace Avalonia.Diagnostics.ViewModels
public override string Priority => public override string Priority =>
_priority; _priority;
public override System.Type Type => _type; public override System.Type AssignedType => _assignedType;
public override string? Value public override string? Value
{ {
@ -43,6 +45,7 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
var convertedValue = ConvertFromString(value, Property.PropertyType); var convertedValue = ConvertFromString(value, Property.PropertyType);
_target.SetValue(Property, convertedValue); _target.SetValue(Property, convertedValue);
Update();
} }
catch { } catch { }
} }
@ -51,6 +54,7 @@ namespace Avalonia.Diagnostics.ViewModels
public override string Group => _group; public override string Group => _group;
public override System.Type? DeclaringType { get; } public override System.Type? DeclaringType { get; }
public override System.Type PropertyType => _propertyType;
// [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))] // [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))]
public override void Update() public override void Update()
@ -58,7 +62,7 @@ namespace Avalonia.Diagnostics.ViewModels
if (Property.IsDirect) if (Property.IsDirect)
{ {
RaiseAndSetIfChanged(ref _value, _target.GetValue(Property), nameof(Value)); RaiseAndSetIfChanged(ref _value, _target.GetValue(Property), nameof(Value));
RaiseAndSetIfChanged(ref _type, _value?.GetType() ?? Property.PropertyType, nameof(Type)); RaiseAndSetIfChanged(ref _assignedType,_value?.GetType() ?? Property.PropertyType, nameof(AssignedType));
RaiseAndSetIfChanged(ref _priority, "Direct", nameof(Priority)); RaiseAndSetIfChanged(ref _priority, "Direct", nameof(Priority));
_group = "Properties"; _group = "Properties";
@ -68,7 +72,7 @@ namespace Avalonia.Diagnostics.ViewModels
var val = _target.GetDiagnostic(Property); var val = _target.GetDiagnostic(Property);
RaiseAndSetIfChanged(ref _value, val?.Value, nameof(Value)); RaiseAndSetIfChanged(ref _value, val?.Value, nameof(Value));
RaiseAndSetIfChanged(ref _type, _value?.GetType() ?? Property.PropertyType, nameof(Type)); RaiseAndSetIfChanged(ref _assignedType, _value?.GetType() ?? Property.PropertyType, nameof(AssignedType));
if (val != null) if (val != null)
{ {
@ -81,6 +85,7 @@ namespace Avalonia.Diagnostics.ViewModels
RaiseAndSetIfChanged(ref _group, "Unset", nameof(Group)); RaiseAndSetIfChanged(ref _group, "Unset", nameof(Group));
} }
} }
RaisePropertyChanged(nameof(Type));
} }
} }
} }

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

@ -5,8 +5,9 @@ namespace Avalonia.Diagnostics.ViewModels
internal class ClrPropertyViewModel : PropertyViewModel internal class ClrPropertyViewModel : PropertyViewModel
{ {
private readonly object _target; private readonly object _target;
private System.Type _type; private System.Type _assignedType;
private object? _value; private object? _value;
private readonly System.Type _propertyType;
#nullable disable #nullable disable
// Remove "nullable disable" after MemberNotNull will work on our CI. // Remove "nullable disable" after MemberNotNull will work on our CI.
@ -25,6 +26,8 @@ namespace Avalonia.Diagnostics.ViewModels
Name = property.DeclaringType.Name + '.' + property.Name; Name = property.DeclaringType.Name + '.' + property.Name;
} }
DeclaringType = property.DeclaringType; DeclaringType = property.DeclaringType;
_propertyType = property.PropertyType;
Update(); Update();
} }
@ -33,7 +36,8 @@ namespace Avalonia.Diagnostics.ViewModels
public override string Name { get; } public override string Name { get; }
public override string Group => "CLR Properties"; public override string Group => "CLR Properties";
public override System.Type Type => _type; public override System.Type AssignedType => _assignedType;
public override System.Type PropertyType => _propertyType;
public override string? Value public override string? Value
{ {
@ -44,6 +48,7 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
var convertedValue = ConvertFromString(value, Property.PropertyType); var convertedValue = ConvertFromString(value, Property.PropertyType);
Property.SetValue(_target, convertedValue); Property.SetValue(_target, convertedValue);
Update();
} }
catch { } catch { }
} }
@ -62,7 +67,8 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
var val = Property.GetValue(_target); var val = Property.GetValue(_target);
RaiseAndSetIfChanged(ref _value, val, nameof(Value)); RaiseAndSetIfChanged(ref _value, val, nameof(Value));
RaiseAndSetIfChanged(ref _type, _value?.GetType() ?? Property.PropertyType, nameof(Type)); RaiseAndSetIfChanged(ref _assignedType, _value?.GetType() ?? Property.PropertyType, nameof(AssignedType));
RaisePropertyChanged(nameof(Type));
} }
} }
} }

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

@ -16,7 +16,7 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
internal class ControlDetailsViewModel : ViewModelBase, IDisposable internal class ControlDetailsViewModel : ViewModelBase, IDisposable
{ {
private readonly IVisual _control; private readonly IAvaloniaObject _avaloniaObject;
private IDictionary<object, List<PropertyViewModel>>? _propertyIndex; private IDictionary<object, List<PropertyViewModel>>? _propertyIndex;
private PropertyViewModel? _selectedProperty; private PropertyViewModel? _selectedProperty;
private DataGridCollectionView? _propertiesView; private DataGridCollectionView? _propertiesView;
@ -28,20 +28,21 @@ namespace Avalonia.Diagnostics.ViewModels
private string? _selectedEntityName; private string? _selectedEntityName;
private string? _selectedEntityType; private string? _selectedEntityType;
public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control) public ControlDetailsViewModel(TreePageViewModel treePage, IAvaloniaObject avaloniaObject)
{ {
_control = control; _avaloniaObject = avaloniaObject;
TreePage = treePage; TreePage = treePage;
Layout = avaloniaObject is IVisual
Layout = new ControlLayoutViewModel(control); ? new ControlLayoutViewModel((IVisual)avaloniaObject)
: default;
NavigateToProperty(control, (control as IControl)?.Name ?? control.ToString()); NavigateToProperty(_avaloniaObject, (_avaloniaObject as IControl)?.Name ?? _avaloniaObject.ToString());
AppliedStyles = new ObservableCollection<StyleViewModel>(); AppliedStyles = new ObservableCollection<StyleViewModel>();
PseudoClasses = new ObservableCollection<PseudoClassViewModel>(); PseudoClasses = new ObservableCollection<PseudoClassViewModel>();
if (control is StyledElement styledElement) if (avaloniaObject is StyledElement styledElement)
{ {
styledElement.Classes.CollectionChanged += OnClassesChanged; styledElement.Classes.CollectionChanged += OnClassesChanged;
@ -181,7 +182,7 @@ namespace Avalonia.Diagnostics.ViewModels
set => RaiseAndSetIfChanged(ref _styleStatus, value); set => RaiseAndSetIfChanged(ref _styleStatus, value);
} }
public ControlLayoutViewModel Layout { get; } public ControlLayoutViewModel? Layout { get; }
protected override void OnPropertyChanged(PropertyChangedEventArgs e) protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{ {
@ -215,17 +216,17 @@ namespace Avalonia.Diagnostics.ViewModels
public void Dispose() public void Dispose()
{ {
if (_control is INotifyPropertyChanged inpc) if (_avaloniaObject is INotifyPropertyChanged inpc)
{ {
inpc.PropertyChanged -= ControlPropertyChanged; inpc.PropertyChanged -= ControlPropertyChanged;
} }
if (_control is AvaloniaObject ao) if (_avaloniaObject is AvaloniaObject ao)
{ {
ao.PropertyChanged -= ControlPropertyChanged; ao.PropertyChanged -= ControlPropertyChanged;
} }
if (_control is StyledElement se) if (_avaloniaObject is StyledElement se)
{ {
se.Classes.CollectionChanged -= OnClassesChanged; se.Classes.CollectionChanged -= OnClassesChanged;
} }
@ -278,7 +279,7 @@ namespace Avalonia.Diagnostics.ViewModels
} }
} }
Layout.ControlPropertyChanged(sender, e); Layout?.ControlPropertyChanged(sender, e);
} }
private void ControlPropertyChanged(object? sender, PropertyChangedEventArgs e) private void ControlPropertyChanged(object? sender, PropertyChangedEventArgs e)
@ -405,8 +406,8 @@ namespace Avalonia.Diagnostics.ViewModels
var selectedEntityName = SelectedEntityName; var selectedEntityName = SelectedEntityName;
if (selectedEntity == null if (selectedEntity == null
|| selectedProperty == null || selectedProperty == null
|| selectedProperty.Type == typeof(string) || selectedProperty.PropertyType == typeof(string)
|| selectedProperty.Type.IsValueType || selectedProperty.PropertyType.IsValueType
) )
return; return;
@ -420,7 +421,7 @@ namespace Avalonia.Diagnostics.ViewModels
property = selectedEntity.GetType().GetProperties() property = selectedEntity.GetType().GetProperties()
.FirstOrDefault(pi => pi.Name == selectedProperty.Name .FirstOrDefault(pi => pi.Name == selectedProperty.Name
&& pi.DeclaringType == selectedProperty.DeclaringType && pi.DeclaringType == selectedProperty.DeclaringType
&& pi.PropertyType.Name == selectedProperty.Type.Name) && pi.PropertyType.Name == selectedProperty.PropertyType.Name)
?.GetValue(selectedEntity); ?.GetValue(selectedEntity);
} }
if (property == null) return; if (property == null) return;

90
src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs

@ -1,23 +1,31 @@
using System; using System;
using System.Reactive.Disposables;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Lifetimes = Avalonia.Controls.ApplicationLifetimes;
using System.Linq;
namespace Avalonia.Diagnostics.ViewModels namespace Avalonia.Diagnostics.ViewModels
{ {
internal class LogicalTreeNode : TreeNode internal class LogicalTreeNode : TreeNode
{ {
public LogicalTreeNode(ILogical logical, TreeNode? parent) public LogicalTreeNode(IAvaloniaObject avaloniaObject, TreeNode? parent)
: base((Control)logical, parent) : base(avaloniaObject, parent)
{ {
Children = new LogicalTreeNodeCollection(this, logical); Children = avaloniaObject switch
{
ILogical logical => new LogicalTreeNodeCollection(this, logical),
Controls.Application host => new ApplicationHostLogical(this, host),
_ => TreeNodeCollection.Empty
};
} }
public override TreeNodeCollection Children { get; } public override TreeNodeCollection Children { get; }
public static LogicalTreeNode[] Create(object control) public static LogicalTreeNode[] Create(object control)
{ {
var logical = control as ILogical; var logical = control as IAvaloniaObject;
return logical != null ? new[] { new LogicalTreeNode(logical, null) } : Array.Empty<LogicalTreeNode>(); return logical != null ? new[] { new LogicalTreeNode(logical, null) } : Array.Empty<LogicalTreeNode>();
} }
@ -41,10 +49,82 @@ namespace Avalonia.Diagnostics.ViewModels
protected override void Initialize(AvaloniaList<TreeNode> nodes) protected override void Initialize(AvaloniaList<TreeNode> nodes)
{ {
_subscription = _control.LogicalChildren.ForEachItem( _subscription = _control.LogicalChildren.ForEachItem(
(i, item) => nodes.Insert(i, new LogicalTreeNode(item, Owner)), (i, item) => nodes.Insert(i, new LogicalTreeNode((IAvaloniaObject)item, Owner)),
(i, item) => nodes.RemoveAt(i), (i, item) => nodes.RemoveAt(i),
() => nodes.Clear()); () => nodes.Clear());
} }
} }
internal class ApplicationHostLogical : TreeNodeCollection
{
readonly Controls.Application _application;
CompositeDisposable _subscriptions = new CompositeDisposable(2);
public ApplicationHostLogical(TreeNode owner, Controls.Application host) :
base(owner)
{
_application = host;
}
protected override void Initialize(AvaloniaList<TreeNode> nodes)
{
if (_application.ApplicationLifetime is Lifetimes.ISingleViewApplicationLifetime single)
{
nodes.Add(new LogicalTreeNode(single.MainView, Owner));
}
if (_application.ApplicationLifetime is Lifetimes.IClassicDesktopStyleApplicationLifetime classic)
{
for (int i = 0; i < classic.Windows.Count; i++)
{
var window = classic.Windows[i];
if (window is Views.MainWindow)
{
continue;
}
nodes.Add(new LogicalTreeNode(window, Owner));
}
_subscriptions = new System.Reactive.Disposables.CompositeDisposable()
{
Window.WindowOpenedEvent.AddClassHandler(typeof(Window), (s,e)=>
{
if (s is Views.MainWindow)
{
return;
}
nodes.Add(new LogicalTreeNode((IAvaloniaObject)s!,Owner));
}),
Window.WindowClosedEvent.AddClassHandler(typeof(Window), (s,e)=>
{
if (s is Views.MainWindow)
{
return;
}
var item = nodes.FirstOrDefault(node=>object.ReferenceEquals(node.Visual,s));
if(!(item is null))
{
nodes.Remove(item);
}
if(nodes.Count == 0)
{
if (Avalonia.Application.Current?.ApplicationLifetime is Lifetimes.IControlledApplicationLifetime controller)
{
controller.Shutdown();
}
else
{
Environment.Exit(0);
}
}
}),
};
}
}
public override void Dispose()
{
_subscriptions?.Dispose();
base.Dispose();
}
}
} }
} }

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

@ -4,12 +4,14 @@ using Avalonia.Controls;
using Avalonia.Diagnostics.Models; using Avalonia.Diagnostics.Models;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Threading; using Avalonia.Threading;
using System.Reactive.Linq;
using System.Linq;
namespace Avalonia.Diagnostics.ViewModels namespace Avalonia.Diagnostics.ViewModels
{ {
internal class MainViewModel : ViewModelBase, IDisposable internal class MainViewModel : ViewModelBase, IDisposable
{ {
private readonly TopLevel _root; private readonly AvaloniaObject _root;
private readonly TreePageViewModel _logicalTree; private readonly TreePageViewModel _logicalTree;
private readonly TreePageViewModel _visualTree; private readonly TreePageViewModel _visualTree;
private readonly EventsPageViewModel _events; private readonly EventsPageViewModel _events;
@ -17,13 +19,15 @@ namespace Avalonia.Diagnostics.ViewModels
private ViewModelBase? _content; private ViewModelBase? _content;
private int _selectedTab; private int _selectedTab;
private string? _focusedControl; private string? _focusedControl;
private string? _pointerOverElement; private IInputElement? _pointerOverElement;
private bool _shouldVisualizeMarginPadding = true; private bool _shouldVisualizeMarginPadding = true;
private bool _shouldVisualizeDirtyRects; private bool _shouldVisualizeDirtyRects;
private bool _showFpsOverlay; private bool _showFpsOverlay;
private bool _freezePopups; private bool _freezePopups;
private string? _pointerOverElementName;
public MainViewModel(TopLevel root) private IInputRoot? _pointerOverRoot;
private bool _showPropertyType;
public MainViewModel(AvaloniaObject root)
{ {
_root = root; _root = root;
_logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root)); _logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root));
@ -35,8 +39,24 @@ namespace Avalonia.Diagnostics.ViewModels
if (KeyboardDevice.Instance is not null) if (KeyboardDevice.Instance is not null)
KeyboardDevice.Instance.PropertyChanged += KeyboardPropertyChanged; KeyboardDevice.Instance.PropertyChanged += KeyboardPropertyChanged;
SelectedTab = 0; SelectedTab = 0;
_pointerOverSubscription = root.GetObservable(TopLevel.PointerOverElementProperty) if (root is TopLevel topLevel)
.Subscribe(x => PointerOverElement = x?.GetType().Name); {
_pointerOverSubscription = topLevel.GetObservable(TopLevel.PointerOverElementProperty)
.Subscribe(x => PointerOverElement = x);
}
else
{
#nullable disable
_pointerOverSubscription = InputManager.Instance.PreProcess
.OfType<Input.Raw.RawPointerEventArgs>()
.Subscribe(e =>
{
PointerOverRoot = e.Root;
PointerOverElement = e.Root.GetInputElementsAt(e.Position).FirstOrDefault();
});
#nullable restore
}
Console = new ConsoleViewModel(UpdateConsoleContext); Console = new ConsoleViewModel(UpdateConsoleContext);
} }
@ -51,13 +71,26 @@ namespace Avalonia.Diagnostics.ViewModels
get => _shouldVisualizeMarginPadding; get => _shouldVisualizeMarginPadding;
set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value); set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value);
} }
public bool ShouldVisualizeDirtyRects public bool ShouldVisualizeDirtyRects
{ {
get => _shouldVisualizeDirtyRects; get => _shouldVisualizeDirtyRects;
set set
{ {
_root.Renderer.DrawDirtyRects = value; var changed = true;
if (_root is TopLevel topLevel && topLevel.Renderer is { })
{
topLevel.Renderer.DrawDirtyRects = value;
}
else if (_root is Controls.Application app && app.RendererRoot is { })
{
app.RendererRoot.DrawDirtyRects = value;
}
else
{
changed = false;
}
if (changed)
RaiseAndSetIfChanged(ref _shouldVisualizeDirtyRects, value); RaiseAndSetIfChanged(ref _shouldVisualizeDirtyRects, value);
} }
} }
@ -77,8 +110,21 @@ namespace Avalonia.Diagnostics.ViewModels
get => _showFpsOverlay; get => _showFpsOverlay;
set set
{ {
_root.Renderer.DrawFps = value; var changed = true;
RaiseAndSetIfChanged(ref _showFpsOverlay, value); if (_root is TopLevel topLevel && topLevel.Renderer is { })
{
topLevel.Renderer.DrawFps = value;
}
else if (_root is Controls.Application app && app.RendererRoot is { })
{
app.RendererRoot.DrawFps = value;
}
else
{
changed = false;
}
if(changed)
RaiseAndSetIfChanged(ref _showFpsOverlay, value);
} }
} }
@ -150,12 +196,28 @@ namespace Avalonia.Diagnostics.ViewModels
private set { RaiseAndSetIfChanged(ref _focusedControl, value); } private set { RaiseAndSetIfChanged(ref _focusedControl, value); }
} }
public string? PointerOverElement public IInputRoot? PointerOverRoot
{
get => _pointerOverRoot;
private set => RaiseAndSetIfChanged( ref _pointerOverRoot , value);
}
public IInputElement? PointerOverElement
{ {
get { return _pointerOverElement; } get { return _pointerOverElement; }
private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); } private set
{
RaiseAndSetIfChanged(ref _pointerOverElement, value);
PointerOverElementName = value?.GetType()?.Name;
}
}
public string? PointerOverElementName
{
get => _pointerOverElementName;
private set => RaiseAndSetIfChanged(ref _pointerOverElementName, value);
} }
private void UpdateConsoleContext(ConsoleContext context) private void UpdateConsoleContext(ConsoleContext context)
{ {
context.root = _root; context.root = _root;
@ -188,8 +250,11 @@ namespace Avalonia.Diagnostics.ViewModels
_pointerOverSubscription.Dispose(); _pointerOverSubscription.Dispose();
_logicalTree.Dispose(); _logicalTree.Dispose();
_visualTree.Dispose(); _visualTree.Dispose();
_root.Renderer.DrawDirtyRects = false; if (_root is TopLevel top)
_root.Renderer.DrawFps = false; {
top.Renderer.DrawDirtyRects = false;
top.Renderer.DrawFps = false;
}
} }
private void UpdateFocusedControl() private void UpdateFocusedControl()
@ -225,5 +290,16 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
StartupScreenIndex = options.StartupScreenIndex; StartupScreenIndex = options.StartupScreenIndex;
} }
public bool ShowDettailsPropertyType
{
get => _showPropertyType;
private set => RaiseAndSetIfChanged(ref _showPropertyType , value);
}
public void ToggleShowDettailsPropertyType(object paramter)
{
ShowDettailsPropertyType = !ShowDettailsPropertyType;
}
} }
} }

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

@ -14,12 +14,17 @@ namespace Avalonia.Diagnostics.ViewModels
public abstract object Key { get; } public abstract object Key { get; }
public abstract string Name { get; } public abstract string Name { get; }
public abstract string Group { get; } public abstract string Group { get; }
public abstract Type Type { get; } public abstract Type AssignedType { get; }
public abstract Type? DeclaringType { get; } public abstract Type? DeclaringType { get; }
public abstract string? Value { get; set; } public abstract string? Value { get; set; }
public abstract string Priority { get; } public abstract string Priority { get; }
public abstract bool? IsAttached { get; } public abstract bool? IsAttached { get; }
public abstract void Update(); public abstract void Update();
public abstract Type PropertyType { get; }
public string Type => PropertyType == AssignedType
? PropertyType.GetTypeName()
: $"{PropertyType.GetTypeName()} {{{AssignedType.GetTypeName()}}}";
protected static string? ConvertToString(object? value) protected static string? ConvertToString(object? value)
{ {
@ -31,7 +36,7 @@ namespace Avalonia.Diagnostics.ViewModels
var converter = TypeDescriptor.GetConverter(value); var converter = TypeDescriptor.GetConverter(value);
//CollectionConverter does not deliver any important information. It just displays "(Collection)". //CollectionConverter does not deliver any important information. It just displays "(Collection)".
if (!converter.CanConvertTo(typeof(string)) || if (!converter.CanConvertTo(typeof(string)) ||
converter.GetType() == typeof(CollectionConverter)) converter.GetType() == typeof(CollectionConverter))
{ {
return value.ToString() ?? "(null)"; return value.ToString() ?? "(null)";

9
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs

@ -16,12 +16,13 @@ namespace Avalonia.Diagnostics.ViewModels
private string _classes; private string _classes;
private bool _isExpanded; private bool _isExpanded;
protected TreeNode(IVisual visual, TreeNode? parent, string? customName = null) protected TreeNode(IAvaloniaObject avaloniaObject, TreeNode? parent, string? customName = null)
{ {
_classes = string.Empty; _classes = string.Empty;
Parent = parent; Parent = parent;
Type = customName ?? visual.GetType().Name; var visual = avaloniaObject ;
Visual = visual; Type = customName ?? avaloniaObject.GetType().Name;
Visual = visual!;
FontWeight = IsRoot ? FontWeight.Bold : FontWeight.Normal; FontWeight = IsRoot ? FontWeight.Bold : FontWeight.Normal;
if (visual is IControl control) if (visual is IControl control)
@ -76,7 +77,7 @@ namespace Avalonia.Diagnostics.ViewModels
get; get;
} }
public IVisual Visual public IAvaloniaObject Visual
{ {
get; get;
} }

14
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs

@ -10,6 +10,20 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
internal abstract class TreeNodeCollection : IAvaloniaReadOnlyList<TreeNode>, IDisposable internal abstract class TreeNodeCollection : IAvaloniaReadOnlyList<TreeNode>, IDisposable
{ {
private class EmptyTreeNodeCollection : TreeNodeCollection
{
public EmptyTreeNodeCollection():base(default!)
{
}
protected override void Initialize(AvaloniaList<TreeNode> nodes)
{
}
}
static readonly internal TreeNodeCollection Empty = new EmptyTreeNodeCollection();
private AvaloniaList<TreeNode>? _inner; private AvaloniaList<TreeNode>? _inner;
public TreeNodeCollection(TreeNode owner) => Owner = owner; public TreeNodeCollection(TreeNode owner) => Owner = owner;

80
src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs

@ -7,15 +7,22 @@ using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Lifetimes = Avalonia.Controls.ApplicationLifetimes;
using System.Linq;
namespace Avalonia.Diagnostics.ViewModels namespace Avalonia.Diagnostics.ViewModels
{ {
internal class VisualTreeNode : TreeNode internal class VisualTreeNode : TreeNode
{ {
public VisualTreeNode(IVisual visual, TreeNode? parent, string? customName = null) public VisualTreeNode(IAvaloniaObject avaloniaObject, TreeNode? parent, string? customName = null)
: base(visual, parent, customName) : base(avaloniaObject, parent, customName)
{ {
Children = new VisualTreeNodeCollection(this, visual); Children = avaloniaObject switch
{
IVisual visual => new VisualTreeNodeCollection(this, visual),
Controls.Application host => new ApplicationHostVisuals(this, host),
_ => TreeNodeCollection.Empty
};
if (Visual is IStyleable styleable) if (Visual is IStyleable styleable)
IsInTemplate = styleable.TemplatedParent != null; IsInTemplate = styleable.TemplatedParent != null;
@ -27,7 +34,7 @@ namespace Avalonia.Diagnostics.ViewModels
public static VisualTreeNode[] Create(object control) public static VisualTreeNode[] Create(object control)
{ {
return control is IVisual visual ? return control is IAvaloniaObject visual ?
new[] { new VisualTreeNode(visual, null) } : new[] { new VisualTreeNode(visual, null) } :
Array.Empty<VisualTreeNode>(); Array.Empty<VisualTreeNode>();
} }
@ -130,7 +137,7 @@ namespace Avalonia.Diagnostics.ViewModels
_subscriptions.Add( _subscriptions.Add(
_control.VisualChildren.ForEachItem( _control.VisualChildren.ForEachItem(
(i, item) => nodes.Insert(i, new VisualTreeNode(item, Owner)), (i, item) => nodes.Insert(i, new VisualTreeNode((IAvaloniaObject)item, Owner)),
(i, item) => nodes.RemoveAt(i), (i, item) => nodes.RemoveAt(i),
() => nodes.Clear())); () => nodes.Clear()));
} }
@ -147,5 +154,68 @@ namespace Avalonia.Diagnostics.ViewModels
public string? CustomName { get; } public string? CustomName { get; }
} }
} }
internal class ApplicationHostVisuals : TreeNodeCollection
{
readonly Controls.Application _application;
CompositeDisposable _subscriptions = new CompositeDisposable(2);
public ApplicationHostVisuals(TreeNode owner, Controls.Application host) :
base(owner)
{
_application = host;
}
protected override void Initialize(AvaloniaList<TreeNode> nodes)
{
if (_application.ApplicationLifetime is Lifetimes.ISingleViewApplicationLifetime single)
{
nodes.Add(new VisualTreeNode(single.MainView, Owner));
}
if (_application.ApplicationLifetime is Lifetimes.IClassicDesktopStyleApplicationLifetime classic)
{
for (int i = 0; i < classic.Windows.Count; i++)
{
var window = classic.Windows[i];
if (window is Views.MainWindow)
{
continue;
}
nodes.Add(new VisualTreeNode(window, Owner));
}
_subscriptions = new System.Reactive.Disposables.CompositeDisposable()
{
Window.WindowOpenedEvent.AddClassHandler(typeof(Window), (s,e)=>
{
if (s is Views.MainWindow)
{
return;
}
nodes.Add(new VisualTreeNode((IAvaloniaObject)s!,Owner));
}),
Window.WindowClosedEvent.AddClassHandler(typeof(Window), (s,e)=>
{
if (s is Views.MainWindow)
{
return;
}
var item = nodes.FirstOrDefault(node=>object.ReferenceEquals(node.Visual,s));
if(!(item is null))
{
nodes.Remove(item);
}
}),
};
}
}
public override void Dispose()
{
_subscriptions?.Dispose();
base.Dispose();
}
}
} }
} }

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

@ -4,15 +4,29 @@
xmlns:local="clr-namespace:Avalonia.Diagnostics.Views" xmlns:local="clr-namespace:Avalonia.Diagnostics.Views"
xmlns:controls="clr-namespace:Avalonia.Diagnostics.Controls" xmlns:controls="clr-namespace:Avalonia.Diagnostics.Controls"
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels" xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
xmlns:lb="using:Avalonia.Diagnostics.Behaviors"
x:Class="Avalonia.Diagnostics.Views.ControlDetailsView" x:Class="Avalonia.Diagnostics.Views.ControlDetailsView"
x:Name="Main"> x:Name="Main">
<UserControl.Resources> <UserControl.Resources>
<conv:BoolToOpacityConverter x:Key="BoolToOpacity" Opacity="0.6"/> <conv:BoolToOpacityConverter x:Key="BoolToOpacity" Opacity="0.6"/>
<conv:GetTypeNameConverter x:Key="GetTypeName"/>
</UserControl.Resources> </UserControl.Resources>
<Grid ColumnDefinitions="*,Auto,320"> <Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<!--
When selecting the Application node, we need this trick to hide Layout Visualizer
because when using the GridSplitter it sets the Witdth property of ColumnDefinition
(see https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Controls/GridSplitter.cs#L528)
and if we hide the contents of the column, the space is not reclaimed
(see discussion https://github.com/AvaloniaUI/Avalonia/discussions/6773).
-->
<ColumnDefinition Width="320" lb:ColumnDefinition.IsVisible="{Binding Layout, Converter={x:Static ObjectConverters.IsNotNull}}" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" RowDefinitions="Auto,Auto,*"> <Grid Grid.Column="0" RowDefinitions="Auto,Auto,*">
<Grid ColumnDefinitions="Auto, *" RowDefinitions="Auto, Auto"> <Grid ColumnDefinitions="Auto, *" RowDefinitions="Auto, Auto">
@ -40,7 +54,18 @@
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn Header="Property" Binding="{Binding Name}" IsReadOnly="True" /> <DataGridTextColumn Header="Property" Binding="{Binding Name}" IsReadOnly="True" />
<DataGridTextColumn Header="Value" Binding="{Binding Value}" /> <DataGridTextColumn Header="Value" Binding="{Binding Value}" />
<DataGridTextColumn Header="Type" Binding="{Binding Type.Name}" /> <DataGridTextColumn Header="Type" Binding="{Binding Type}"
IsReadOnly="True"
IsVisible="{Binding !$parent[UserControl;2].DataContext.ShowDettailsPropertyType}"
/>
<DataGridTextColumn Header="Assinged Type" Binding="{Binding AssignedType, Converter={StaticResource GetTypeName}}"
IsReadOnly="True"
IsVisible="{Binding $parent[UserControl;2].DataContext.ShowDettailsPropertyType}"
/>
<DataGridTextColumn Header="Property Type" Binding="{Binding PropertyType, Converter={StaticResource GetTypeName}}"
IsReadOnly="True"
IsVisible="{Binding $parent[UserControl;2].DataContext.ShowDettailsPropertyType}"
/>
<DataGridTextColumn Header="Priority" Binding="{Binding Priority}" IsReadOnly="True" /> <DataGridTextColumn Header="Priority" Binding="{Binding Priority}" IsReadOnly="True" />
</DataGrid.Columns> </DataGrid.Columns>
@ -53,9 +78,10 @@
</Grid> </Grid>
<GridSplitter Grid.Column="1" /> <GridSplitter Grid.Column="1"/>
<Grid Grid.Column="2" RowDefinitions="*,Auto,*" > <Grid Grid.Column="2" RowDefinitions="*,Auto,*"
IsVisible="{Binding Layout, Converter={x:Static ObjectConverters.IsNotNull}}">
<Grid RowDefinitions="Auto,*" Grid.Row="0"> <Grid RowDefinitions="Auto,*" Grid.Row="0">
<TextBlock FontWeight="Bold" Grid.Row="0" Text="Layout Visualizer" Margin="4" /> <TextBlock FontWeight="Bold" Grid.Row="0" Text="Layout Visualizer" Margin="4" />

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

@ -15,6 +15,16 @@
IsEnabled="False" /> IsEnabled="False" />
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem> </MenuItem>
<MenuItem Header="_Details">
<MenuItem Header="Split Property Type" Command="{Binding ToggleShowDettailsPropertyType}">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="{Binding ShowDettailsPropertyType}"
IsEnabled="False"/>
</MenuItem.Icon>
</MenuItem>
</MenuItem>
</MenuItem> </MenuItem>
<MenuItem Header="_Options"> <MenuItem Header="_Options">
<MenuItem Header="Visualize margin/padding" Command="{Binding ToggleVisualizeMarginPadding}"> <MenuItem Header="Visualize margin/padding" Command="{Binding ToggleVisualizeMarginPadding}">
@ -73,7 +83,7 @@
<TextBlock Text="{Binding FocusedControl}" /> <TextBlock Text="{Binding FocusedControl}" />
<Separator Width="8" /> <Separator Width="8" />
<TextBlock>Pointer Over:</TextBlock> <TextBlock>Pointer Over:</TextBlock>
<TextBlock Text="{Binding PointerOverElement}" /> <TextBlock Text="{Binding PointerOverElementName}" />
</StackPanel> </StackPanel>
<TextBlock Grid.Column="1" <TextBlock Grid.Column="1"

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

@ -19,7 +19,7 @@ namespace Avalonia.Diagnostics.Views
{ {
private readonly IDisposable? _keySubscription; private readonly IDisposable? _keySubscription;
private readonly Dictionary<Popup, IDisposable> _frozenPopupStates; private readonly Dictionary<Popup, IDisposable> _frozenPopupStates;
private TopLevel? _root; private AvaloniaObject? _root;
public MainWindow() public MainWindow()
{ {
@ -50,23 +50,23 @@ namespace Avalonia.Diagnostics.Views
this.Opened += lh; this.Opened += lh;
} }
public TopLevel? Root public AvaloniaObject? Root
{ {
get => _root; get => _root;
set set
{ {
if (_root != value) if (_root != value)
{ {
if (_root != null) if (_root is ICloseable oldClosable)
{ {
_root.Closed -= RootClosed; oldClosable.Closed -= RootClosed;
} }
_root = value; _root = value;
if (_root != null) if (_root is ICloseable newClosable)
{ {
_root.Closed += RootClosed; newClosable.Closed += RootClosed;
DataContext = new MainViewModel(_root); DataContext = new MainViewModel(_root);
} }
else else
@ -91,9 +91,9 @@ namespace Avalonia.Diagnostics.Views
_frozenPopupStates.Clear(); _frozenPopupStates.Clear();
if (_root != null) if (_root is ICloseable cloneable)
{ {
_root.Closed -= RootClosed; cloneable.Closed -= RootClosed;
_root = null; _root = null;
} }
@ -123,7 +123,7 @@ namespace Avalonia.Diagnostics.Views
.FirstOrDefault(); .FirstOrDefault();
} }
private static List<PopupRoot> GetPopupRoots(IVisual root) private static List<PopupRoot> GetPopupRoots(TopLevel root)
{ {
var popupRoots = new List<PopupRoot>(); var popupRoots = new List<PopupRoot>();
@ -160,7 +160,8 @@ namespace Avalonia.Diagnostics.Views
return; return;
} }
var root = Root; var root = Root as TopLevel
?? vm.PointerOverRoot as TopLevel;
if (root is null) if (root is null)
{ {
return; return;

7
src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs

@ -47,7 +47,12 @@ namespace Avalonia.Diagnostics.Views
return; return;
} }
var visual = (Visual)node.Visual; var visual = node.Visual as Visual;
if (visual is null)
{
return;
}
_currentLayer = AdornerLayer.GetAdornerLayer(visual); _currentLayer = AdornerLayer.GetAdornerLayer(visual);

2
src/Avalonia.Dialogs/Avalonia.Dialogs.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

2
src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

2
src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

2
src/Avalonia.Headless/Avalonia.Headless.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

2
src/Avalonia.Input/Avalonia.Input.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" /> <Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />

2
src/Avalonia.Interactivity/Avalonia.Interactivity.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" /> <ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />

2
src/Avalonia.Layout/Avalonia.Layout.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" /> <ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />

2
src/Avalonia.Native/Avalonia.Native.csproj

@ -4,7 +4,7 @@
<PackAvaloniaNative Condition="'$(PackAvaloniaNative)' == ''">$([MSBuild]::IsOSPlatform(OSX))</PackAvaloniaNative> <PackAvaloniaNative Condition="'$(PackAvaloniaNative)' == ''">$([MSBuild]::IsOSPlatform(OSX))</PackAvaloniaNative>
<IsPackable>$(PackAvaloniaNative)</IsPackable> <IsPackable>$(PackAvaloniaNative)</IsPackable>
<IsPackable Condition="'$([MSBuild]::IsOSPlatform(OSX))' == 'True'">true</IsPackable> <IsPackable Condition="'$([MSBuild]::IsOSPlatform(OSX))' == 'True'">true</IsPackable>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<MicroComGeneratorRuntimeNamespace>Avalonia.MicroCom</MicroComGeneratorRuntimeNamespace> <MicroComGeneratorRuntimeNamespace>Avalonia.MicroCom</MicroComGeneratorRuntimeNamespace>
</PropertyGroup> </PropertyGroup>

2
src/Avalonia.OpenGL/Avalonia.OpenGL.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>

2
src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<PackageId>Avalonia.ReactiveUI</PackageId> <PackageId>Avalonia.ReactiveUI</PackageId>
<SignAssembly>false</SignAssembly> <SignAssembly>false</SignAssembly>
</PropertyGroup> </PropertyGroup>

57
src/Avalonia.ReactiveUI/RoutedViewHost.cs

@ -57,7 +57,13 @@ namespace Avalonia.ReactiveUI
/// </summary> /// </summary>
public static readonly StyledProperty<RoutingState?> RouterProperty = public static readonly StyledProperty<RoutingState?> RouterProperty =
AvaloniaProperty.Register<RoutedViewHost, RoutingState?>(nameof(Router)); AvaloniaProperty.Register<RoutedViewHost, RoutingState?>(nameof(Router));
/// <summary>
/// <see cref="AvaloniaProperty"/> for the <see cref="ViewContract"/> property.
/// </summary>
public static readonly StyledProperty<string?> ViewContractProperty =
AvaloniaProperty.Register<ViewModelViewHost, string?>(nameof(ViewContract));
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="RoutedViewHost"/> class. /// Initializes a new instance of the <see cref="RoutedViewHost"/> class.
/// </summary> /// </summary>
@ -70,15 +76,18 @@ namespace Avalonia.ReactiveUI
.Where(router => router == null)! .Where(router => router == null)!
.Cast<object?>(); .Cast<object?>();
var viewContract = this.WhenAnyValue(x => x.ViewContract);
this.WhenAnyValue(x => x.Router) this.WhenAnyValue(x => x.Router)
.Where(router => router != null) .Where(router => router != null)
.SelectMany(router => router!.CurrentViewModel) .SelectMany(router => router!.CurrentViewModel)
.Merge(routerRemoved) .Merge(routerRemoved)
.Subscribe(NavigateToViewModel) .CombineLatest(viewContract)
.Subscribe(tuple => NavigateToViewModel(tuple.First, tuple.Second))
.DisposeWith(disposables); .DisposeWith(disposables);
}); });
} }
/// <summary> /// <summary>
/// Gets or sets the <see cref="RoutingState"/> of the view model stack. /// Gets or sets the <see cref="RoutingState"/> of the view model stack.
/// </summary> /// </summary>
@ -87,17 +96,27 @@ namespace Avalonia.ReactiveUI
get => GetValue(RouterProperty); get => GetValue(RouterProperty);
set => SetValue(RouterProperty, value); set => SetValue(RouterProperty, value);
} }
/// <summary>
/// Gets or sets the view contract.
/// </summary>
public string? ViewContract
{
get => GetValue(ViewContractProperty);
set => SetValue(ViewContractProperty, value);
}
/// <summary> /// <summary>
/// Gets or sets the ReactiveUI view locator used by this router. /// Gets or sets the ReactiveUI view locator used by this router.
/// </summary> /// </summary>
public IViewLocator? ViewLocator { get; set; } public IViewLocator? ViewLocator { get; set; }
/// <summary> /// <summary>
/// Invoked when ReactiveUI router navigates to a view model. /// Invoked when ReactiveUI router navigates to a view model.
/// </summary> /// </summary>
/// <param name="viewModel">ViewModel to which the user navigates.</param> /// <param name="viewModel">ViewModel to which the user navigates.</param>
private void NavigateToViewModel(object? viewModel) /// <param name="contract">The contract for view resolution.</param>
private void NavigateToViewModel(object? viewModel, string? contract)
{ {
if (Router == null) if (Router == null)
{ {
@ -112,17 +131,33 @@ namespace Avalonia.ReactiveUI
Content = DefaultContent; Content = DefaultContent;
return; return;
} }
var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current;
var viewInstance = viewLocator.ResolveView(viewModel); var viewInstance = viewLocator.ResolveView(viewModel, contract);
if (viewInstance == null) if (viewInstance == null)
{ {
this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); if (contract == null)
{
this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content.");
}
else
{
this.Log().Warn($"Couldn't find view with contract '{contract}' for '{viewModel}'. Is it registered? Falling back to default content.");
}
Content = DefaultContent; Content = DefaultContent;
return; return;
} }
this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); if (contract == null)
{
this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}.");
}
else
{
this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel} and contract '{contract}'.");
}
viewInstance.ViewModel = viewModel; viewInstance.ViewModel = viewModel;
if (viewInstance is IDataContextProvider provider) if (viewInstance is IDataContextProvider provider)
provider.DataContext = viewModel; provider.DataContext = viewModel;

52
src/Avalonia.ReactiveUI/ViewModelViewHost.cs

@ -3,7 +3,7 @@ using System.Reactive.Disposables;
using ReactiveUI; using ReactiveUI;
using Splat; using Splat;
namespace Avalonia.ReactiveUI namespace Avalonia.ReactiveUI
{ {
/// <summary> /// <summary>
/// This content control will automatically load the View associated with /// This content control will automatically load the View associated with
@ -18,6 +18,12 @@ namespace Avalonia.ReactiveUI
public static readonly AvaloniaProperty<object?> ViewModelProperty = public static readonly AvaloniaProperty<object?> ViewModelProperty =
AvaloniaProperty.Register<ViewModelViewHost, object?>(nameof(ViewModel)); AvaloniaProperty.Register<ViewModelViewHost, object?>(nameof(ViewModel));
/// <summary>
/// <see cref="AvaloniaProperty"/> for the <see cref="ViewContract"/> property.
/// </summary>
public static readonly StyledProperty<string?> ViewContractProperty =
AvaloniaProperty.Register<ViewModelViewHost, string?>(nameof(ViewContract));
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ViewModelViewHost"/> class. /// Initializes a new instance of the <see cref="ViewModelViewHost"/> class.
/// </summary> /// </summary>
@ -25,8 +31,8 @@ namespace Avalonia.ReactiveUI
{ {
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
this.WhenAnyValue(x => x.ViewModel) this.WhenAnyValue(x => x.ViewModel, x => x.ViewContract)
.Subscribe(NavigateToViewModel) .Subscribe(tuple => NavigateToViewModel(tuple.Item1, tuple.Item2))
.DisposeWith(disposables); .DisposeWith(disposables);
}); });
} }
@ -39,7 +45,16 @@ namespace Avalonia.ReactiveUI
get => GetValue(ViewModelProperty); get => GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value); set => SetValue(ViewModelProperty, value);
} }
/// <summary>
/// Gets or sets the view contract.
/// </summary>
public string? ViewContract
{
get => GetValue(ViewContractProperty);
set => SetValue(ViewContractProperty, value);
}
/// <summary> /// <summary>
/// Gets or sets the view locator. /// Gets or sets the view locator.
/// </summary> /// </summary>
@ -49,7 +64,8 @@ namespace Avalonia.ReactiveUI
/// Invoked when ReactiveUI router navigates to a view model. /// Invoked when ReactiveUI router navigates to a view model.
/// </summary> /// </summary>
/// <param name="viewModel">ViewModel to which the user navigates.</param> /// <param name="viewModel">ViewModel to which the user navigates.</param>
private void NavigateToViewModel(object? viewModel) /// <param name="contract">The contract for view resolution.</param>
private void NavigateToViewModel(object? viewModel, string? contract)
{ {
if (viewModel == null) if (viewModel == null)
{ {
@ -57,17 +73,33 @@ namespace Avalonia.ReactiveUI
Content = DefaultContent; Content = DefaultContent;
return; return;
} }
var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current;
var viewInstance = viewLocator.ResolveView(viewModel); var viewInstance = viewLocator.ResolveView(viewModel, contract);
if (viewInstance == null) if (viewInstance == null)
{ {
this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); if (contract == null)
{
this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content.");
}
else
{
this.Log().Warn($"Couldn't find view with contract '{contract}' for '{viewModel}'. Is it registered? Falling back to default content.");
}
Content = DefaultContent; Content = DefaultContent;
return; return;
} }
this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); if (contract == null)
{
this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}.");
}
else
{
this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel} and contract '{contract}'.");
}
viewInstance.ViewModel = viewModel; viewInstance.ViewModel = viewModel;
if (viewInstance is IStyledElement styled) if (viewInstance is IStyledElement styled)
styled.DataContext = viewModel; styled.DataContext = viewModel;

2
src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<DefineConstants>AVALONIA_REMOTE_PROTOCOL;$(DefineConstants)</DefineConstants> <DefineConstants>AVALONIA_REMOTE_PROTOCOL;$(DefineConstants)</DefineConstants>
<SignAssembly>true</SignAssembly> <SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>Key.snk</AssemblyOriginatorKeyFile> <AssemblyOriginatorKeyFile>Key.snk</AssemblyOriginatorKeyFile>

2
src/Avalonia.Styling/Avalonia.Styling.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<AssemblyName>Avalonia.Styling</AssemblyName> <AssemblyName>Avalonia.Styling</AssemblyName>
<RootNamespace>Avalonia</RootNamespace> <RootNamespace>Avalonia</RootNamespace>
</PropertyGroup> </PropertyGroup>

2
src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" /> <ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />

1
src/Avalonia.Themes.Default/ButtonSpinner.xaml

@ -5,6 +5,7 @@
<Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/> <Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/> <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Center"/> <Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Focusable" Value="True"/>
</Style> </Style>
<Style Selector="ButtonSpinner /template/ RepeatButton"> <Style Selector="ButtonSpinner /template/ RepeatButton">
<Setter Property="Background" Value="Transparent"/> <Setter Property="Background" Value="Transparent"/>

2
src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" /> <ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />

1
src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml

@ -64,6 +64,7 @@
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" /> <Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" /> <Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" /> <Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Focusable" Value="True"/>
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<DataValidationErrors> <DataValidationErrors>

96
src/Avalonia.Themes.Fluent/FluentTheme.cs

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.Styling;
using Avalonia.Styling; using Avalonia.Styling;
#nullable enable #nullable enable
@ -17,11 +18,14 @@ namespace Avalonia.Themes.Fluent
/// <summary> /// <summary>
/// Includes the fluent theme in an application. /// Includes the fluent theme in an application.
/// </summary> /// </summary>
public class FluentTheme : IStyle, IResourceProvider public class FluentTheme : AvaloniaObject, IStyle, IResourceProvider
{ {
private readonly Uri _baseUri; private readonly Uri _baseUri;
private IStyle[]? _loaded; private Styles _fluentDark = new();
private Styles _fluentLight = new();
private Styles _sharedStyles = new();
private bool _isLoading; private bool _isLoading;
private IStyle? _loaded;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="FluentTheme"/> class. /// Initializes a new instance of the <see cref="FluentTheme"/> class.
@ -30,6 +34,7 @@ namespace Avalonia.Themes.Fluent
public FluentTheme(Uri baseUri) public FluentTheme(Uri baseUri)
{ {
_baseUri = baseUri; _baseUri = baseUri;
InitStyles(baseUri);
} }
/// <summary> /// <summary>
@ -39,12 +44,37 @@ namespace Avalonia.Themes.Fluent
public FluentTheme(IServiceProvider serviceProvider) public FluentTheme(IServiceProvider serviceProvider)
{ {
_baseUri = ((IUriContext)serviceProvider.GetService(typeof(IUriContext))).BaseUri; _baseUri = ((IUriContext)serviceProvider.GetService(typeof(IUriContext))).BaseUri;
InitStyles(_baseUri);
} }
public static readonly StyledProperty<FluentThemeMode> ModeProperty =
AvaloniaProperty.Register<FluentTheme, FluentThemeMode>(nameof(Mode));
/// <summary> /// <summary>
/// Gets or sets the mode of the fluent theme (light, dark). /// Gets or sets the mode of the fluent theme (light, dark).
/// </summary> /// </summary>
public FluentThemeMode Mode { get; set; } public FluentThemeMode Mode
{
get => GetValue(ModeProperty);
set => SetValue(ModeProperty, value);
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);
if (change.Property == ModeProperty)
{
if (Mode == FluentThemeMode.Dark)
{
(Loaded as Styles)![1] = _fluentDark[0];
(Loaded as Styles)![2] = _fluentDark[1];
}
else
{
(Loaded as Styles)![1] = _fluentLight[0];
(Loaded as Styles)![2] = _fluentLight[1];
}
}
}
public IResourceHost? Owner => (Loaded as IResourceProvider)?.Owner; public IResourceHost? Owner => (Loaded as IResourceProvider)?.Owner;
@ -58,18 +88,25 @@ namespace Avalonia.Themes.Fluent
if (_loaded == null) if (_loaded == null)
{ {
_isLoading = true; _isLoading = true;
var loaded = (IStyle)AvaloniaXamlLoader.Load(GetUri(), _baseUri);
_loaded = new[] { loaded }; if (Mode == FluentThemeMode.Light)
{
_loaded = new Styles() { _sharedStyles , _fluentLight[0], _fluentLight[1] };
}
else if (Mode == FluentThemeMode.Dark)
{
_loaded = new Styles() { _sharedStyles, _fluentDark[0], _fluentDark[1] };
}
_isLoading = false; _isLoading = false;
} }
return _loaded?[0]!; return _loaded!;
} }
} }
bool IResourceNode.HasResources => (Loaded as IResourceProvider)?.HasResources ?? false; bool IResourceNode.HasResources => (Loaded as IResourceProvider)?.HasResources ?? false;
IReadOnlyList<IStyle> IStyle.Children => _loaded ?? Array.Empty<IStyle>(); IReadOnlyList<IStyle> IStyle.Children => _loaded?.Children ?? Array.Empty<IStyle>();
public event EventHandler OwnerChanged public event EventHandler OwnerChanged
{ {
@ -105,10 +142,47 @@ namespace Avalonia.Themes.Fluent
void IResourceProvider.AddOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.AddOwner(owner); void IResourceProvider.AddOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.AddOwner(owner);
void IResourceProvider.RemoveOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.RemoveOwner(owner); void IResourceProvider.RemoveOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.RemoveOwner(owner);
private Uri GetUri() => Mode switch private void InitStyles(Uri baseUri)
{ {
FluentThemeMode.Dark => new Uri("avares://Avalonia.Themes.Fluent/FluentDark.xaml", UriKind.Absolute), _sharedStyles = new Styles
_ => new Uri("avares://Avalonia.Themes.Fluent/FluentLight.xaml", UriKind.Absolute), {
}; new StyleInclude(baseUri)
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/AccentColors.xaml")
},
new StyleInclude(baseUri)
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/Base.xaml")
},
new StyleInclude(baseUri)
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Controls/FluentControls.xaml")
}
};
_fluentLight = new Styles
{
new StyleInclude(baseUri)
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseLight.xaml")
},
new StyleInclude(baseUri)
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml")
}
};
_fluentDark = new Styles
{
new StyleInclude(baseUri)
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseDark.xaml")
},
new StyleInclude(baseUri)
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml")
}
};
}
} }
} }

2
src/Avalonia.Visuals/Avalonia.Visuals.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<RootNamespace>Avalonia</RootNamespace> <RootNamespace>Avalonia</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>

4
src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs

@ -80,12 +80,12 @@ namespace Avalonia.Media.TextFormatting.Unicode
} }
} }
var shouldBreak = GetSimpleBreak() ?? (bool?)GetPairTableBreak(lastClass); var shouldBreak = GetSimpleBreak() ?? GetPairTableBreak(lastClass);
// Rule LB8a // Rule LB8a
_lb8a = _nextClass == LineBreakClass.ZWJ; _lb8a = _nextClass == LineBreakClass.ZWJ;
if (shouldBreak.Value) if (shouldBreak)
{ {
Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition); Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition);
return true; return true;

2
src/Avalonia.X11/Avalonia.X11.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PackageId>Avalonia.X11</PackageId> <PackageId>Avalonia.X11</PackageId>
</PropertyGroup> </PropertyGroup>

2
src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PackageId>Avalonia.LinuxFramebuffer</PackageId> <PackageId>Avalonia.LinuxFramebuffer</PackageId>
</PropertyGroup> </PropertyGroup>

2
src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
<PackageId>Avalonia.Markup.Xaml.Loader</PackageId> <PackageId>Avalonia.Markup.Xaml.Loader</PackageId>
<DefineConstants>$(DefineConstants);XAMLX_INTERNAL</DefineConstants> <DefineConstants>$(DefineConstants);XAMLX_INTERNAL</DefineConstants>

2
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<DefineConstants>$(DefineConstants);PCL;NETSTANDARD;NETSTANDARD2_0;HAS_TYPE_CONVERTER;HAS_CUSTOM_ATTRIBUTE_PROVIDER;XAMLX_INTERNAL</DefineConstants> <DefineConstants>$(DefineConstants);PCL;NETSTANDARD;NETSTANDARD2_0;HAS_TYPE_CONVERTER;HAS_CUSTOM_ATTRIBUTE_PROVIDER;XAMLX_INTERNAL</DefineConstants>
<UseCecil>false</UseCecil> <UseCecil>false</UseCecil>
<DefineConstants Condition="$(UseCecil) == true">$(DefineConstants);RUNTIME_XAML_CECIL</DefineConstants> <DefineConstants Condition="$(UseCecil) == true">$(DefineConstants);RUNTIME_XAML_CECIL</DefineConstants>

2
src/Markup/Avalonia.Markup/Avalonia.Markup.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<RootNamespace>Avalonia</RootNamespace> <RootNamespace>Avalonia</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

2
src/Skia/Avalonia.Skia/Avalonia.Skia.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<RootNamespace>Avalonia.Skia</RootNamespace> <RootNamespace>Avalonia.Skia</RootNamespace>
<AssemblyName>Avalonia.Skia</AssemblyName> <AssemblyName>Avalonia.Skia</AssemblyName>
<PackageId>Avalonia.Skia</PackageId> <PackageId>Avalonia.Skia</PackageId>

2
src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs

@ -142,7 +142,7 @@ namespace Avalonia.Web.Blazor
public Size ClientSize => _clientSize; public Size ClientSize => _clientSize;
public Size? FrameSize => null; public Size? FrameSize => null;
public double RenderScaling => 1; public double RenderScaling => _currentSurface?.Scaling ?? 1;
public IEnumerable<object> Surfaces => new object[] { _currentSurface! }; public IEnumerable<object> Surfaces => new object[] { _currentSurface! };

2
src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PackageId>Avalonia.Direct2D1</PackageId> <PackageId>Avalonia.Direct2D1</PackageId>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>

2
src/Windows/Avalonia.Win32/Avalonia.Win32.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PackageId>Avalonia.Win32</PackageId> <PackageId>Avalonia.Win32</PackageId>
<MicroComGeneratorRuntimeNamespace>Avalonia.MicroCom</MicroComGeneratorRuntimeNamespace> <MicroComGeneratorRuntimeNamespace>Avalonia.MicroCom</MicroComGeneratorRuntimeNamespace>

73
tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs

@ -29,6 +29,8 @@ namespace Avalonia.ReactiveUI.UnitTests
public class FirstRoutableView : ReactiveUserControl<FirstRoutableViewModel> { } public class FirstRoutableView : ReactiveUserControl<FirstRoutableViewModel> { }
public class AlternativeFirstRoutableView : ReactiveUserControl<FirstRoutableViewModel> { }
public class SecondRoutableViewModel : ReactiveObject, IRoutableViewModel public class SecondRoutableViewModel : ReactiveObject, IRoutableViewModel
{ {
public string UrlPathSegment => "second"; public string UrlPathSegment => "second";
@ -38,16 +40,22 @@ namespace Avalonia.ReactiveUI.UnitTests
public class SecondRoutableView : ReactiveUserControl<SecondRoutableViewModel> { } public class SecondRoutableView : ReactiveUserControl<SecondRoutableViewModel> { }
public class AlternativeSecondRoutableView : ReactiveUserControl<SecondRoutableViewModel> { }
public class ScreenViewModel : ReactiveObject, IScreen public class ScreenViewModel : ReactiveObject, IScreen
{ {
public RoutingState Router { get; } = new RoutingState(); public RoutingState Router { get; } = new RoutingState();
} }
public static string AlternativeViewContract => "AlternativeView";
public RoutedViewHostTest() public RoutedViewHostTest()
{ {
Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher));
Locator.CurrentMutable.Register(() => new FirstRoutableView(), typeof(IViewFor<FirstRoutableViewModel>)); Locator.CurrentMutable.Register(() => new FirstRoutableView(), typeof(IViewFor<FirstRoutableViewModel>));
Locator.CurrentMutable.Register(() => new SecondRoutableView(), typeof(IViewFor<SecondRoutableViewModel>)); Locator.CurrentMutable.Register(() => new SecondRoutableView(), typeof(IViewFor<SecondRoutableViewModel>));
Locator.CurrentMutable.Register(() => new AlternativeFirstRoutableView(), typeof(IViewFor<FirstRoutableViewModel>), AlternativeViewContract);
Locator.CurrentMutable.Register(() => new AlternativeSecondRoutableView(), typeof(IViewFor<SecondRoutableViewModel>), AlternativeViewContract);
} }
[Fact] [Fact]
@ -101,6 +109,71 @@ namespace Avalonia.ReactiveUI.UnitTests
Assert.Equal(defaultContent, host.Content); Assert.Equal(defaultContent, host.Content);
} }
[Fact]
public void RoutedViewHost_Should_Stay_In_Sync_With_RoutingState_And_Contract()
{
var screen = new ScreenViewModel();
var defaultContent = new TextBlock();
var host = new RoutedViewHost
{
Router = screen.Router,
DefaultContent = defaultContent,
PageTransition = null
};
var root = new TestRoot
{
Child = host
};
Assert.NotNull(host.Content);
Assert.IsType<TextBlock>(host.Content);
Assert.Equal(defaultContent, host.Content);
var first = new FirstRoutableViewModel();
screen.Router.Navigate.Execute(first).Subscribe();
host.ViewContract = null;
Assert.NotNull(host.Content);
Assert.IsType<FirstRoutableView>(host.Content);
Assert.Equal(first, ((FirstRoutableView)host.Content).DataContext);
Assert.Equal(first, ((FirstRoutableView)host.Content).ViewModel);
host.ViewContract = AlternativeViewContract;
Assert.NotNull(host.Content);
Assert.IsType<AlternativeFirstRoutableView>(host.Content);
Assert.Equal(first, ((AlternativeFirstRoutableView)host.Content).DataContext);
Assert.Equal(first, ((AlternativeFirstRoutableView)host.Content).ViewModel);
var second = new SecondRoutableViewModel();
screen.Router.Navigate.Execute(second).Subscribe();
host.ViewContract = null;
Assert.NotNull(host.Content);
Assert.IsType<SecondRoutableView>(host.Content);
Assert.Equal(second, ((SecondRoutableView)host.Content).DataContext);
Assert.Equal(second, ((SecondRoutableView)host.Content).ViewModel);
host.ViewContract = AlternativeViewContract;
Assert.NotNull(host.Content);
Assert.IsType<AlternativeSecondRoutableView>(host.Content);
Assert.Equal(second, ((AlternativeSecondRoutableView)host.Content).DataContext);
Assert.Equal(second, ((AlternativeSecondRoutableView)host.Content).ViewModel);
screen.Router.NavigateBack.Execute(Unit.Default).Subscribe();
Assert.NotNull(host.Content);
Assert.IsType<AlternativeFirstRoutableView>(host.Content);
Assert.Equal(first, ((AlternativeFirstRoutableView)host.Content).DataContext);
Assert.Equal(first, ((AlternativeFirstRoutableView)host.Content).ViewModel);
screen.Router.NavigateBack.Execute(Unit.Default).Subscribe();
Assert.NotNull(host.Content);
Assert.IsType<TextBlock>(host.Content);
Assert.Equal(defaultContent, host.Content);
}
[Fact] [Fact]
public void RoutedViewHost_Should_Show_Default_Content_When_Router_Is_Null() public void RoutedViewHost_Should_Show_Default_Content_When_Router_Is_Null()
{ {

72
tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs

@ -12,15 +12,23 @@ namespace Avalonia.ReactiveUI.UnitTests
public class FirstView : ReactiveUserControl<FirstViewModel> { } public class FirstView : ReactiveUserControl<FirstViewModel> { }
public class AlternativeFirstView : ReactiveUserControl<FirstViewModel> { }
public class SecondViewModel : ReactiveObject { } public class SecondViewModel : ReactiveObject { }
public class SecondView : ReactiveUserControl<SecondViewModel> { } public class SecondView : ReactiveUserControl<SecondViewModel> { }
public class AlternativeSecondView : ReactiveUserControl<SecondViewModel> { }
public static string AlternativeViewContract => "AlternativeView";
public ViewModelViewHostTest() public ViewModelViewHostTest()
{ {
Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher));
Locator.CurrentMutable.Register(() => new FirstView(), typeof(IViewFor<FirstViewModel>)); Locator.CurrentMutable.Register(() => new FirstView(), typeof(IViewFor<FirstViewModel>));
Locator.CurrentMutable.Register(() => new SecondView(), typeof(IViewFor<SecondViewModel>)); Locator.CurrentMutable.Register(() => new SecondView(), typeof(IViewFor<SecondViewModel>));
Locator.CurrentMutable.Register(() => new AlternativeFirstView(), typeof(IViewFor<FirstViewModel>), AlternativeViewContract);
Locator.CurrentMutable.Register(() => new AlternativeSecondView(), typeof(IViewFor<SecondViewModel>), AlternativeViewContract);
} }
[Fact] [Fact]
@ -67,5 +75,67 @@ namespace Avalonia.ReactiveUI.UnitTests
Assert.Equal(first, ((FirstView)host.Content).DataContext); Assert.Equal(first, ((FirstView)host.Content).DataContext);
Assert.Equal(first, ((FirstView)host.Content).ViewModel); Assert.Equal(first, ((FirstView)host.Content).ViewModel);
} }
[Fact]
public void ViewModelViewHost_View_Should_Stay_In_Sync_With_ViewModel_And_Contract()
{
var defaultContent = new TextBlock();
var host = new ViewModelViewHost
{
DefaultContent = defaultContent,
PageTransition = null
};
var root = new TestRoot
{
Child = host
};
Assert.NotNull(host.Content);
Assert.Equal(typeof(TextBlock), host.Content.GetType());
Assert.Equal(defaultContent, host.Content);
var first = new FirstViewModel();
host.ViewModel = first;
host.ViewContract = null;
Assert.NotNull(host.Content);
Assert.Equal(typeof(FirstView), host.Content.GetType());
Assert.Equal(first, ((FirstView)host.Content).DataContext);
Assert.Equal(first, ((FirstView)host.Content).ViewModel);
host.ViewContract = AlternativeViewContract;
Assert.NotNull(host.Content);
Assert.Equal(typeof(AlternativeFirstView), host.Content.GetType());
Assert.Equal(first, ((AlternativeFirstView)host.Content).DataContext);
Assert.Equal(first, ((AlternativeFirstView)host.Content).ViewModel);
var second = new SecondViewModel();
host.ViewModel = second;
host.ViewContract = null;
Assert.NotNull(host.Content);
Assert.Equal(typeof(SecondView), host.Content.GetType());
Assert.Equal(second, ((SecondView)host.Content).DataContext);
Assert.Equal(second, ((SecondView)host.Content).ViewModel);
host.ViewContract = AlternativeViewContract;
Assert.NotNull(host.Content);
Assert.Equal(typeof(AlternativeSecondView), host.Content.GetType());
Assert.Equal(second, ((AlternativeSecondView)host.Content).DataContext);
Assert.Equal(second, ((AlternativeSecondView)host.Content).ViewModel);
host.ViewModel = null;
host.ViewContract = null;
Assert.NotNull(host.Content);
Assert.Equal(typeof(TextBlock), host.Content.GetType());
Assert.Equal(defaultContent, host.Content);
host.ViewContract = AlternativeViewContract;
Assert.NotNull(host.Content);
Assert.Equal(typeof(TextBlock), host.Content.GetType());
Assert.Equal(defaultContent, host.Content);
}
} }
} }

Loading…
Cancel
Save