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">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net461;netcoreapp2.0;net6.0</TargetFrameworks>
<TargetFrameworks>net6.0;netstandard2.0;net461;netcoreapp2.0</TargetFrameworks>
<PackageId>Avalonia</PackageId>
</PropertyGroup>

38
samples/ControlCatalog/App.xaml.cs

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

1
samples/ControlCatalog/DecoratedWindow.xaml.cs

@ -11,7 +11,6 @@ namespace ControlCatalog
public DecoratedWindow()
{
this.InitializeComponent();
this.AttachDevTools();
}
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.Controls;
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.Immutable;
using Avalonia.Platform;
using ControlCatalog.Pages;
using Avalonia.Themes.Fluent;
using ControlCatalog.Models;
using ControlCatalog.Pages;
namespace ControlCatalog
{
@ -43,14 +41,36 @@ namespace ControlCatalog
{
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,
CatalogTheme.FluentDark => App.FluentDark,
CatalogTheme.DefaultLight => App.DefaultLight,
CatalogTheme.DefaultDark => App.DefaultDark,
_ => Application.Current.Styles[0]
};
Application.Current.Styles[0] = App.DefaultDark;
Application.Current.Styles[1] = App.DataGridDefault;
}
}
};

5
samples/ControlCatalog/MainWindow.xaml.cs

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

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

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

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

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

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

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

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

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

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

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<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.Primitives;
using Avalonia.Data;
@ -196,13 +196,14 @@ namespace Avalonia.Controls
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{
base.OnPointerWheelChanged(e);
if (!e.Handled && AllowSpin)
if (AllowSpin && IsKeyboardFocusWithin)
{
if (e.Delta.Y != 0)
{
var spinnerEventArgs = new SpinEventArgs(SpinEvent, (e.Delta.Y < 0) ? SpinDirection.Decrease : SpinDirection.Increase, true);
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))
{
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
switch (e.ClickCount)
#pragma warning restore CS0618 // Type or member is obsolete
{
case 1:
SelectionStart = SelectionEnd = index;
if (clickToSelect)
{
SelectionStart = Math.Min(index, CaretIndex);
SelectionEnd = Math.Max(index, CaretIndex);
}
else
{
SelectionStart = SelectionEnd = index;
}
break;
case 2:
if (!StringUtils.IsStartOfWord(text, index))

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

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<!-- WARNING! The designer support version number needs to be frozen
To allow projects that implement designer functionality to still
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">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<PackageId>Avalonia.Desktop</PackageId>
</PropertyGroup>

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

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

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

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

44
src/Avalonia.Diagnostics/DevToolsExtensions.cs

@ -37,5 +37,49 @@ namespace Avalonia
{
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.Collections.Generic;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Diagnostics.Views;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
namespace Avalonia.Diagnostics
{
public static class DevTools
{
private static readonly Dictionary<TopLevel, MainWindow> s_open =
new Dictionary<TopLevel, MainWindow>();
private static readonly Dictionary<AvaloniaObject, MainWindow> s_open =
new Dictionary<AvaloniaObject, MainWindow>();
private static bool s_attachedToApplication;
public static IDisposable Attach(TopLevel root, KeyGesture gesture)
{
@ -23,6 +27,11 @@ namespace Avalonia.Diagnostics
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)
{
if (options.Gesture.Matches(e))
@ -37,45 +46,95 @@ namespace Avalonia.Diagnostics
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();
}
else
{
window = new MainWindow
{
Root = root,
Root = new Controls.Application(application),
Width = options.Size.Width,
Height = options.Size.Height,
};
window.SetOptions(options);
window.Closed += DevToolsClosed;
s_open.Add(root, window);
if (options.ShowAsChildWindow && root is Window inspectedWindow)
s_open.Add(application, window);
if (options.ShowAsChildWindow && owner is { })
{
window.Show(inspectedWindow);
window.Show(owner);
}
else
{
window.Show();
}
}
return Disposable.Create(() => window?.Close());
}
private static void DevToolsClosed(object? sender, EventArgs e)
{
var window = (MainWindow)sender!;
s_open.Remove(window.Root!);
window.Closed -= DevToolsClosed;
}
}
}

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
/// of the window being inspected. The default value is true.
/// </summary>
/// <remarks>This setting is ignored if DevTools is attached to <see cref="Application"/></remarks>
public bool ShowAsChildWindow { get; set; } = true;
/// <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
{
private readonly AvaloniaObject _target;
private System.Type _type;
private System.Type _assignedType;
private object? _value;
private string _priority;
private string _group;
private readonly System.Type _propertyType;
#nullable disable
// Remove "nullable disable" after MemberNotNull will work on our CI.
@ -20,6 +21,7 @@ namespace Avalonia.Diagnostics.ViewModels
$"[{property.OwnerType.Name}.{property.Name}]" :
property.Name;
DeclaringType = property.OwnerType;
_propertyType = property.PropertyType;
Update();
}
@ -32,7 +34,7 @@ namespace Avalonia.Diagnostics.ViewModels
public override string Priority =>
_priority;
public override System.Type Type => _type;
public override System.Type AssignedType => _assignedType;
public override string? Value
{
@ -43,6 +45,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
var convertedValue = ConvertFromString(value, Property.PropertyType);
_target.SetValue(Property, convertedValue);
Update();
}
catch { }
}
@ -51,6 +54,7 @@ namespace Avalonia.Diagnostics.ViewModels
public override string Group => _group;
public override System.Type? DeclaringType { get; }
public override System.Type PropertyType => _propertyType;
// [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))]
public override void Update()
@ -58,7 +62,7 @@ namespace Avalonia.Diagnostics.ViewModels
if (Property.IsDirect)
{
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));
_group = "Properties";
@ -68,7 +72,7 @@ namespace Avalonia.Diagnostics.ViewModels
var val = _target.GetDiagnostic(Property);
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)
{
@ -81,6 +85,7 @@ namespace Avalonia.Diagnostics.ViewModels
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
{
private readonly object _target;
private System.Type _type;
private System.Type _assignedType;
private object? _value;
private readonly System.Type _propertyType;
#nullable disable
// Remove "nullable disable" after MemberNotNull will work on our CI.
@ -25,6 +26,8 @@ namespace Avalonia.Diagnostics.ViewModels
Name = property.DeclaringType.Name + '.' + property.Name;
}
DeclaringType = property.DeclaringType;
_propertyType = property.PropertyType;
Update();
}
@ -33,7 +36,8 @@ namespace Avalonia.Diagnostics.ViewModels
public override string Name { get; }
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
{
@ -44,6 +48,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
var convertedValue = ConvertFromString(value, Property.PropertyType);
Property.SetValue(_target, convertedValue);
Update();
}
catch { }
}
@ -62,7 +67,8 @@ namespace Avalonia.Diagnostics.ViewModels
{
var val = Property.GetValue(_target);
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
{
private readonly IVisual _control;
private readonly IAvaloniaObject _avaloniaObject;
private IDictionary<object, List<PropertyViewModel>>? _propertyIndex;
private PropertyViewModel? _selectedProperty;
private DataGridCollectionView? _propertiesView;
@ -28,20 +28,21 @@ namespace Avalonia.Diagnostics.ViewModels
private string? _selectedEntityName;
private string? _selectedEntityType;
public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control)
public ControlDetailsViewModel(TreePageViewModel treePage, IAvaloniaObject avaloniaObject)
{
_control = control;
_avaloniaObject = avaloniaObject;
TreePage = treePage;
Layout = new ControlLayoutViewModel(control);
TreePage = treePage;
Layout = avaloniaObject is IVisual
? 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>();
PseudoClasses = new ObservableCollection<PseudoClassViewModel>();
if (control is StyledElement styledElement)
if (avaloniaObject is StyledElement styledElement)
{
styledElement.Classes.CollectionChanged += OnClassesChanged;
@ -181,7 +182,7 @@ namespace Avalonia.Diagnostics.ViewModels
set => RaiseAndSetIfChanged(ref _styleStatus, value);
}
public ControlLayoutViewModel Layout { get; }
public ControlLayoutViewModel? Layout { get; }
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
@ -215,17 +216,17 @@ namespace Avalonia.Diagnostics.ViewModels
public void Dispose()
{
if (_control is INotifyPropertyChanged inpc)
if (_avaloniaObject is INotifyPropertyChanged inpc)
{
inpc.PropertyChanged -= ControlPropertyChanged;
}
if (_control is AvaloniaObject ao)
if (_avaloniaObject is AvaloniaObject ao)
{
ao.PropertyChanged -= ControlPropertyChanged;
}
if (_control is StyledElement se)
if (_avaloniaObject is StyledElement se)
{
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)
@ -405,8 +406,8 @@ namespace Avalonia.Diagnostics.ViewModels
var selectedEntityName = SelectedEntityName;
if (selectedEntity == null
|| selectedProperty == null
|| selectedProperty.Type == typeof(string)
|| selectedProperty.Type.IsValueType
|| selectedProperty.PropertyType == typeof(string)
|| selectedProperty.PropertyType.IsValueType
)
return;
@ -420,7 +421,7 @@ namespace Avalonia.Diagnostics.ViewModels
property = selectedEntity.GetType().GetProperties()
.FirstOrDefault(pi => pi.Name == selectedProperty.Name
&& pi.DeclaringType == selectedProperty.DeclaringType
&& pi.PropertyType.Name == selectedProperty.Type.Name)
&& pi.PropertyType.Name == selectedProperty.PropertyType.Name)
?.GetValue(selectedEntity);
}
if (property == null) return;

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

@ -1,23 +1,31 @@
using System;
using System.Reactive.Disposables;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.LogicalTree;
using Lifetimes = Avalonia.Controls.ApplicationLifetimes;
using System.Linq;
namespace Avalonia.Diagnostics.ViewModels
{
internal class LogicalTreeNode : TreeNode
{
public LogicalTreeNode(ILogical logical, TreeNode? parent)
: base((Control)logical, parent)
public LogicalTreeNode(IAvaloniaObject avaloniaObject, TreeNode? 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 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>();
}
@ -41,10 +49,82 @@ namespace Avalonia.Diagnostics.ViewModels
protected override void Initialize(AvaloniaList<TreeNode> nodes)
{
_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),
() => 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.Input;
using Avalonia.Threading;
using System.Reactive.Linq;
using System.Linq;
namespace Avalonia.Diagnostics.ViewModels
{
internal class MainViewModel : ViewModelBase, IDisposable
{
private readonly TopLevel _root;
private readonly AvaloniaObject _root;
private readonly TreePageViewModel _logicalTree;
private readonly TreePageViewModel _visualTree;
private readonly EventsPageViewModel _events;
@ -17,13 +19,15 @@ namespace Avalonia.Diagnostics.ViewModels
private ViewModelBase? _content;
private int _selectedTab;
private string? _focusedControl;
private string? _pointerOverElement;
private IInputElement? _pointerOverElement;
private bool _shouldVisualizeMarginPadding = true;
private bool _shouldVisualizeDirtyRects;
private bool _showFpsOverlay;
private bool _freezePopups;
public MainViewModel(TopLevel root)
private string? _pointerOverElementName;
private IInputRoot? _pointerOverRoot;
private bool _showPropertyType;
public MainViewModel(AvaloniaObject root)
{
_root = root;
_logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root));
@ -35,8 +39,24 @@ namespace Avalonia.Diagnostics.ViewModels
if (KeyboardDevice.Instance is not null)
KeyboardDevice.Instance.PropertyChanged += KeyboardPropertyChanged;
SelectedTab = 0;
_pointerOverSubscription = root.GetObservable(TopLevel.PointerOverElementProperty)
.Subscribe(x => PointerOverElement = x?.GetType().Name);
if (root is TopLevel topLevel)
{
_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);
}
@ -51,13 +71,26 @@ namespace Avalonia.Diagnostics.ViewModels
get => _shouldVisualizeMarginPadding;
set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value);
}
public bool ShouldVisualizeDirtyRects
{
get => _shouldVisualizeDirtyRects;
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);
}
}
@ -77,8 +110,21 @@ namespace Avalonia.Diagnostics.ViewModels
get => _showFpsOverlay;
set
{
_root.Renderer.DrawFps = value;
RaiseAndSetIfChanged(ref _showFpsOverlay, value);
var changed = true;
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); }
}
public string? PointerOverElement
public IInputRoot? PointerOverRoot
{
get => _pointerOverRoot;
private set => RaiseAndSetIfChanged( ref _pointerOverRoot , value);
}
public IInputElement? 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)
{
context.root = _root;
@ -188,8 +250,11 @@ namespace Avalonia.Diagnostics.ViewModels
_pointerOverSubscription.Dispose();
_logicalTree.Dispose();
_visualTree.Dispose();
_root.Renderer.DrawDirtyRects = false;
_root.Renderer.DrawFps = false;
if (_root is TopLevel top)
{
top.Renderer.DrawDirtyRects = false;
top.Renderer.DrawFps = false;
}
}
private void UpdateFocusedControl()
@ -225,5 +290,16 @@ namespace Avalonia.Diagnostics.ViewModels
{
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 string Name { get; }
public abstract string Group { get; }
public abstract Type Type { get; }
public abstract Type AssignedType { get; }
public abstract Type? DeclaringType { get; }
public abstract string? Value { get; set; }
public abstract string Priority { get; }
public abstract bool? IsAttached { get; }
public abstract void Update();
public abstract bool? IsAttached { get; }
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)
{
@ -31,7 +36,7 @@ namespace Avalonia.Diagnostics.ViewModels
var converter = TypeDescriptor.GetConverter(value);
//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))
{
return value.ToString() ?? "(null)";

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

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

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

@ -10,6 +10,20 @@ namespace Avalonia.Diagnostics.ViewModels
{
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;
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.Styling;
using Avalonia.VisualTree;
using Lifetimes = Avalonia.Controls.ApplicationLifetimes;
using System.Linq;
namespace Avalonia.Diagnostics.ViewModels
{
internal class VisualTreeNode : TreeNode
{
public VisualTreeNode(IVisual visual, TreeNode? parent, string? customName = null)
: base(visual, parent, customName)
public VisualTreeNode(IAvaloniaObject avaloniaObject, TreeNode? parent, string? customName = null)
: 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)
IsInTemplate = styleable.TemplatedParent != null;
@ -27,7 +34,7 @@ namespace Avalonia.Diagnostics.ViewModels
public static VisualTreeNode[] Create(object control)
{
return control is IVisual visual ?
return control is IAvaloniaObject visual ?
new[] { new VisualTreeNode(visual, null) } :
Array.Empty<VisualTreeNode>();
}
@ -130,7 +137,7 @@ namespace Avalonia.Diagnostics.ViewModels
_subscriptions.Add(
_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),
() => nodes.Clear()));
}
@ -147,5 +154,68 @@ namespace Avalonia.Diagnostics.ViewModels
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:controls="clr-namespace:Avalonia.Diagnostics.Controls"
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
xmlns:lb="using:Avalonia.Diagnostics.Behaviors"
x:Class="Avalonia.Diagnostics.Views.ControlDetailsView"
x:Name="Main">
<UserControl.Resources>
<conv:BoolToOpacityConverter x:Key="BoolToOpacity" Opacity="0.6"/>
<conv:GetTypeNameConverter x:Key="GetTypeName"/>
</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 ColumnDefinitions="Auto, *" RowDefinitions="Auto, Auto">
@ -40,7 +54,18 @@
<DataGrid.Columns>
<DataGridTextColumn Header="Property" Binding="{Binding Name}" IsReadOnly="True" />
<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" />
</DataGrid.Columns>
@ -53,9 +78,10 @@
</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">
<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" />
</MenuItem.Icon>
</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 Header="_Options">
<MenuItem Header="Visualize margin/padding" Command="{Binding ToggleVisualizeMarginPadding}">
@ -73,7 +83,7 @@
<TextBlock Text="{Binding FocusedControl}" />
<Separator Width="8" />
<TextBlock>Pointer Over:</TextBlock>
<TextBlock Text="{Binding PointerOverElement}" />
<TextBlock Text="{Binding PointerOverElementName}" />
</StackPanel>
<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 Dictionary<Popup, IDisposable> _frozenPopupStates;
private TopLevel? _root;
private AvaloniaObject? _root;
public MainWindow()
{
@ -50,23 +50,23 @@ namespace Avalonia.Diagnostics.Views
this.Opened += lh;
}
public TopLevel? Root
public AvaloniaObject? Root
{
get => _root;
set
{
if (_root != value)
{
if (_root != null)
if (_root is ICloseable oldClosable)
{
_root.Closed -= RootClosed;
oldClosable.Closed -= RootClosed;
}
_root = value;
if (_root != null)
if (_root is ICloseable newClosable)
{
_root.Closed += RootClosed;
newClosable.Closed += RootClosed;
DataContext = new MainViewModel(_root);
}
else
@ -91,9 +91,9 @@ namespace Avalonia.Diagnostics.Views
_frozenPopupStates.Clear();
if (_root != null)
if (_root is ICloseable cloneable)
{
_root.Closed -= RootClosed;
cloneable.Closed -= RootClosed;
_root = null;
}
@ -123,7 +123,7 @@ namespace Avalonia.Diagnostics.Views
.FirstOrDefault();
}
private static List<PopupRoot> GetPopupRoots(IVisual root)
private static List<PopupRoot> GetPopupRoots(TopLevel root)
{
var popupRoots = new List<PopupRoot>();
@ -160,7 +160,8 @@ namespace Avalonia.Diagnostics.Views
return;
}
var root = Root;
var root = Root as TopLevel
?? vm.PointerOverRoot as TopLevel;
if (root is null)
{
return;

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<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>
<IsPackable>$(PackAvaloniaNative)</IsPackable>
<IsPackable Condition="'$([MSBuild]::IsOSPlatform(OSX))' == 'True'">true</IsPackable>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<MicroComGeneratorRuntimeNamespace>Avalonia.MicroCom</MicroComGeneratorRuntimeNamespace>
</PropertyGroup>

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

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

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

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

57
src/Avalonia.ReactiveUI/RoutedViewHost.cs

@ -57,7 +57,13 @@ namespace Avalonia.ReactiveUI
/// </summary>
public static readonly StyledProperty<RoutingState?> RouterProperty =
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>
/// Initializes a new instance of the <see cref="RoutedViewHost"/> class.
/// </summary>
@ -70,15 +76,18 @@ namespace Avalonia.ReactiveUI
.Where(router => router == null)!
.Cast<object?>();
var viewContract = this.WhenAnyValue(x => x.ViewContract);
this.WhenAnyValue(x => x.Router)
.Where(router => router != null)
.SelectMany(router => router!.CurrentViewModel)
.Merge(routerRemoved)
.Subscribe(NavigateToViewModel)
.CombineLatest(viewContract)
.Subscribe(tuple => NavigateToViewModel(tuple.First, tuple.Second))
.DisposeWith(disposables);
});
}
/// <summary>
/// Gets or sets the <see cref="RoutingState"/> of the view model stack.
/// </summary>
@ -87,17 +96,27 @@ namespace Avalonia.ReactiveUI
get => GetValue(RouterProperty);
set => SetValue(RouterProperty, value);
}
/// <summary>
/// Gets or sets the view contract.
/// </summary>
public string? ViewContract
{
get => GetValue(ViewContractProperty);
set => SetValue(ViewContractProperty, value);
}
/// <summary>
/// Gets or sets the ReactiveUI view locator used by this router.
/// </summary>
public IViewLocator? ViewLocator { get; set; }
/// <summary>
/// Invoked when ReactiveUI router navigates to a view model.
/// </summary>
/// <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)
{
@ -112,17 +131,33 @@ namespace Avalonia.ReactiveUI
Content = DefaultContent;
return;
}
var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current;
var viewInstance = viewLocator.ResolveView(viewModel);
var viewInstance = viewLocator.ResolveView(viewModel, contract);
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;
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;
if (viewInstance is IDataContextProvider provider)
provider.DataContext = viewModel;

52
src/Avalonia.ReactiveUI/ViewModelViewHost.cs

@ -3,7 +3,7 @@ using System.Reactive.Disposables;
using ReactiveUI;
using Splat;
namespace Avalonia.ReactiveUI
namespace Avalonia.ReactiveUI
{
/// <summary>
/// This content control will automatically load the View associated with
@ -18,6 +18,12 @@ namespace Avalonia.ReactiveUI
public static readonly AvaloniaProperty<object?> ViewModelProperty =
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>
/// Initializes a new instance of the <see cref="ViewModelViewHost"/> class.
/// </summary>
@ -25,8 +31,8 @@ namespace Avalonia.ReactiveUI
{
this.WhenActivated(disposables =>
{
this.WhenAnyValue(x => x.ViewModel)
.Subscribe(NavigateToViewModel)
this.WhenAnyValue(x => x.ViewModel, x => x.ViewContract)
.Subscribe(tuple => NavigateToViewModel(tuple.Item1, tuple.Item2))
.DisposeWith(disposables);
});
}
@ -39,7 +45,16 @@ namespace Avalonia.ReactiveUI
get => GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
/// <summary>
/// Gets or sets the view contract.
/// </summary>
public string? ViewContract
{
get => GetValue(ViewContractProperty);
set => SetValue(ViewContractProperty, value);
}
/// <summary>
/// Gets or sets the view locator.
/// </summary>
@ -49,7 +64,8 @@ namespace Avalonia.ReactiveUI
/// Invoked when ReactiveUI router navigates to a view model.
/// </summary>
/// <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)
{
@ -57,17 +73,33 @@ namespace Avalonia.ReactiveUI
Content = DefaultContent;
return;
}
var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current;
var viewInstance = viewLocator.ResolveView(viewModel);
var viewInstance = viewLocator.ResolveView(viewModel, contract);
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;
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;
if (viewInstance is IStyledElement styled)
styled.DataContext = viewModel;

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

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

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

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

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

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<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="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Focusable" Value="True"/>
</Style>
<Style Selector="ButtonSpinner /template/ RepeatButton">
<Setter Property="Background" Value="Transparent"/>

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

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<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="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Focusable" Value="True"/>
<Setter Property="Template">
<ControlTemplate>
<DataValidationErrors>

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

@ -2,6 +2,7 @@
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.Styling;
using Avalonia.Styling;
#nullable enable
@ -17,11 +18,14 @@ namespace Avalonia.Themes.Fluent
/// <summary>
/// Includes the fluent theme in an application.
/// </summary>
public class FluentTheme : IStyle, IResourceProvider
public class FluentTheme : AvaloniaObject, IStyle, IResourceProvider
{
private readonly Uri _baseUri;
private IStyle[]? _loaded;
private Styles _fluentDark = new();
private Styles _fluentLight = new();
private Styles _sharedStyles = new();
private bool _isLoading;
private IStyle? _loaded;
/// <summary>
/// Initializes a new instance of the <see cref="FluentTheme"/> class.
@ -30,6 +34,7 @@ namespace Avalonia.Themes.Fluent
public FluentTheme(Uri baseUri)
{
_baseUri = baseUri;
InitStyles(baseUri);
}
/// <summary>
@ -39,12 +44,37 @@ namespace Avalonia.Themes.Fluent
public FluentTheme(IServiceProvider serviceProvider)
{
_baseUri = ((IUriContext)serviceProvider.GetService(typeof(IUriContext))).BaseUri;
InitStyles(_baseUri);
}
public static readonly StyledProperty<FluentThemeMode> ModeProperty =
AvaloniaProperty.Register<FluentTheme, FluentThemeMode>(nameof(Mode));
/// <summary>
/// Gets or sets the mode of the fluent theme (light, dark).
/// </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;
@ -58,18 +88,25 @@ namespace Avalonia.Themes.Fluent
if (_loaded == null)
{
_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;
}
return _loaded?[0]!;
return _loaded!;
}
}
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
{
@ -105,10 +142,47 @@ namespace Avalonia.Themes.Fluent
void IResourceProvider.AddOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.AddOwner(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),
_ => new Uri("avares://Avalonia.Themes.Fluent/FluentLight.xaml", UriKind.Absolute),
};
_sharedStyles = new Styles
{
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">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<RootNamespace>Avalonia</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</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
_lb8a = _nextClass == LineBreakClass.ZWJ;
if (shouldBreak.Value)
if (shouldBreak)
{
Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition);
return true;

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

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

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

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

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

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

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

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<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>
<UseCecil>false</UseCecil>
<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">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<RootNamespace>Avalonia</RootNamespace>
</PropertyGroup>
<ItemGroup>

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

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<RootNamespace>Avalonia.Skia</RootNamespace>
<AssemblyName>Avalonia.Skia</AssemblyName>
<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? FrameSize => null;
public double RenderScaling => 1;
public double RenderScaling => _currentSurface?.Scaling ?? 1;
public IEnumerable<object> Surfaces => new object[] { _currentSurface! };

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

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

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

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PackageId>Avalonia.Win32</PackageId>
<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 AlternativeFirstRoutableView : ReactiveUserControl<FirstRoutableViewModel> { }
public class SecondRoutableViewModel : ReactiveObject, IRoutableViewModel
{
public string UrlPathSegment => "second";
@ -38,16 +40,22 @@ namespace Avalonia.ReactiveUI.UnitTests
public class SecondRoutableView : ReactiveUserControl<SecondRoutableViewModel> { }
public class AlternativeSecondRoutableView : ReactiveUserControl<SecondRoutableViewModel> { }
public class ScreenViewModel : ReactiveObject, IScreen
{
public RoutingState Router { get; } = new RoutingState();
}
public static string AlternativeViewContract => "AlternativeView";
public RoutedViewHostTest()
{
Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher));
Locator.CurrentMutable.Register(() => new FirstRoutableView(), typeof(IViewFor<FirstRoutableViewModel>));
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]
@ -101,6 +109,71 @@ namespace Avalonia.ReactiveUI.UnitTests
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]
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 AlternativeFirstView : ReactiveUserControl<FirstViewModel> { }
public class SecondViewModel : ReactiveObject { }
public class SecondView : ReactiveUserControl<SecondViewModel> { }
public class AlternativeSecondView : ReactiveUserControl<SecondViewModel> { }
public static string AlternativeViewContract => "AlternativeView";
public ViewModelViewHostTest()
{
Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher));
Locator.CurrentMutable.Register(() => new FirstView(), typeof(IViewFor<FirstViewModel>));
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]
@ -67,5 +75,67 @@ namespace Avalonia.ReactiveUI.UnitTests
Assert.Equal(first, ((FirstView)host.Content).DataContext);
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