From 4d34a2c6e76fc415d2c62b22142aa937305cdf0c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 24 Feb 2023 21:26:43 -0500 Subject: [PATCH 001/110] Move headless projects to a subfolder --- Avalonia.sln | 8 ++++++-- .../ControlCatalog.NetCore/ControlCatalog.NetCore.csproj | 2 +- .../MobileSandbox.Desktop/MobileSandbox.Desktop.csproj | 2 +- .../Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj | 2 +- .../Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs | 0 .../HeadlessVncPlatformExtensions.cs | 0 .../Avalonia.Headless/Avalonia.Headless.csproj | 6 +++--- .../Avalonia.Headless/AvaloniaHeadlessPlatform.cs | 0 .../Avalonia.Headless/HeadlessPlatformRenderInterface.cs | 0 .../Avalonia.Headless/HeadlessPlatformStubs.cs | 0 .../HeadlessPlatformThreadingInterface.cs | 0 .../Avalonia.Headless/HeadlessWindowImpl.cs | 0 src/{ => Headless}/Avalonia.Headless/IHeadlessWindow.cs | 0 13 files changed, 12 insertions(+), 8 deletions(-) rename src/{ => Headless}/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj (85%) rename src/{ => Headless}/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs (100%) rename src/{ => Headless}/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs (100%) rename src/{ => Headless}/Avalonia.Headless/Avalonia.Headless.csproj (60%) rename src/{ => Headless}/Avalonia.Headless/AvaloniaHeadlessPlatform.cs (100%) rename src/{ => Headless}/Avalonia.Headless/HeadlessPlatformRenderInterface.cs (100%) rename src/{ => Headless}/Avalonia.Headless/HeadlessPlatformStubs.cs (100%) rename src/{ => Headless}/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs (100%) rename src/{ => Headless}/Avalonia.Headless/HeadlessWindowImpl.cs (100%) rename src/{ => Headless}/Avalonia.Headless/IHeadlessWindow.cs (100%) diff --git a/Avalonia.sln b/Avalonia.sln index e66b73de0e..295f7cb149 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -181,9 +181,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Fluent", "src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj", "{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless", "src\Avalonia.Headless\Avalonia.Headless.csproj", "{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless", "src\Headless\Avalonia.Headless\Avalonia.Headless.csproj", "{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.Vnc", "src\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj", "{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.Vnc", "src\Headless\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj", "{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.Loader", "src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj", "{909A8CBD-7D0E-42FD-B841-022AD8925820}" EndProject @@ -246,6 +246,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepe EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Fonts.Inter", "src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj", "{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Headless", "Headless", "{FF237916-7150-496B-89ED-6CA3292896E7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -644,6 +646,8 @@ Global {C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC} = {FF237916-7150-496B-89ED-6CA3292896E7} + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E} = {FF237916-7150-496B-89ED-6CA3292896E7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index e465e9caf3..877d475fb6 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -26,7 +26,7 @@ - + diff --git a/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj b/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj index a24e55de81..31a6b05175 100644 --- a/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj +++ b/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj @@ -19,7 +19,7 @@ - + diff --git a/src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj similarity index 85% rename from src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj rename to src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj index c713440dc9..5bdedec81d 100644 --- a/src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj +++ b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj @@ -9,5 +9,5 @@ - + diff --git a/src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs similarity index 100% rename from src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs rename to src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs diff --git a/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs similarity index 100% rename from src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs rename to src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs diff --git a/src/Avalonia.Headless/Avalonia.Headless.csproj b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj similarity index 60% rename from src/Avalonia.Headless/Avalonia.Headless.csproj rename to src/Headless/Avalonia.Headless/Avalonia.Headless.csproj index 95f7b79009..c3a472e950 100644 --- a/src/Avalonia.Headless/Avalonia.Headless.csproj +++ b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj @@ -7,7 +7,7 @@ - - - + + + diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs similarity index 100% rename from src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs rename to src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs similarity index 100% rename from src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs rename to src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs similarity index 100% rename from src/Avalonia.Headless/HeadlessPlatformStubs.cs rename to src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs diff --git a/src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs similarity index 100% rename from src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs rename to src/Headless/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs similarity index 100% rename from src/Avalonia.Headless/HeadlessWindowImpl.cs rename to src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs diff --git a/src/Avalonia.Headless/IHeadlessWindow.cs b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs similarity index 100% rename from src/Avalonia.Headless/IHeadlessWindow.cs rename to src/Headless/Avalonia.Headless/IHeadlessWindow.cs From a2bd18457463a847566ac2095abbc8bc62d80cde Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 24 Feb 2023 21:46:22 -0500 Subject: [PATCH 002/110] Move NoopStorageProvider to the base project --- .../Platform/Storage/NoopStorageProvider.cs | 27 +++++++++++++++++++ src/Avalonia.Controls/TopLevel.cs | 5 +++- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Base/Platform/Storage/NoopStorageProvider.cs diff --git a/src/Avalonia.Base/Platform/Storage/NoopStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/NoopStorageProvider.cs new file mode 100644 index 0000000000..153634027c --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/NoopStorageProvider.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Platform.Storage.FileIO; + +namespace Avalonia.Platform.Storage; + +internal class NoopStorageProvider : BclStorageProvider +{ + public override bool CanOpen => false; + public override Task> OpenFilePickerAsync(FilePickerOpenOptions options) + { + return Task.FromResult>(Array.Empty()); + } + + public override bool CanSave => false; + public override Task SaveFilePickerAsync(FilePickerSaveOptions options) + { + return Task.FromResult(null); + } + + public override bool CanPickFolder => false; + public override Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) + { + return Task.FromResult>(Array.Empty()); + } +} diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index fdcb8cc537..eec6b08ecb 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -387,10 +387,13 @@ namespace Avalonia.Controls IStyleHost IStyleHost.StylingParent => _globalStyles!; + /// + /// File System storage service used for file pickers and bookmarks. + /// public IStorageProvider StorageProvider => _storageProvider ??= AvaloniaLocator.Current.GetService()?.CreateProvider(this) ?? PlatformImpl?.TryGetFeature() - ?? throw new InvalidOperationException("StorageProvider platform implementation is not available."); + ?? new NoopStorageProvider(); /// Point IRenderRoot.PointToClient(PixelPoint p) From 9e93791ea59a767067c4fb04b5f9641ded6e358b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 24 Feb 2023 21:46:28 -0500 Subject: [PATCH 003/110] Enable nullable in headless platform --- .../Avalonia.Controls.csproj | 1 + .../Platform/ScreenHelper.cs | 3 +- .../Avalonia.Headless.Vnc.csproj | 3 + .../HeadlessVncFramebufferSource.cs | 2 +- .../HeadlessVncPlatformExtensions.cs | 5 +- .../Avalonia.Headless.csproj | 5 +- .../AvaloniaHeadlessPlatform.cs | 13 ++- .../HeadlessPlatformRenderInterface.cs | 43 +++++----- .../HeadlessPlatformStubs.cs | 73 +++++++--------- .../HeadlessPlatformThreadingInterface.cs | 14 ++-- .../Avalonia.Headless/HeadlessWindowImpl.cs | 83 +++++++++---------- .../Avalonia.Headless/IHeadlessWindow.cs | 2 +- 12 files changed, 120 insertions(+), 127 deletions(-) diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 3195c38eef..5adb5a8f2e 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -16,5 +16,6 @@ + diff --git a/src/Avalonia.Controls/Platform/ScreenHelper.cs b/src/Avalonia.Controls/Platform/ScreenHelper.cs index 0bd2be69d0..59b29b4748 100644 --- a/src/Avalonia.Controls/Platform/ScreenHelper.cs +++ b/src/Avalonia.Controls/Platform/ScreenHelper.cs @@ -1,11 +1,12 @@ using System.Collections.Generic; +using Avalonia.Controls; using Avalonia.Utilities; #nullable enable namespace Avalonia.Platform { - public static class ScreenHelper + internal static class ScreenHelper { public static Screen? ScreenFromPoint(PixelPoint point, IReadOnlyList screens) { diff --git a/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj index 5bdedec81d..3812fb196d 100644 --- a/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj +++ b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj @@ -9,5 +9,8 @@ + + + diff --git a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs index 18c149ce2e..be0f3579a5 100644 --- a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs +++ b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs @@ -17,7 +17,7 @@ namespace Avalonia.Headless.Vnc private VncButton _previousButtons; public HeadlessVncFramebufferSource(VncServerSession session, Window window) { - Window = (IHeadlessWindow)window.PlatformImpl; + Window = window.PlatformImpl as IHeadlessWindow ?? throw new InvalidOperationException("Invalid window parameter"); session.PointerChanged += (_, args) => { var pt = new Point(args.X, args.Y); diff --git a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs index efc8c66fde..8e5cd1a316 100644 --- a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs +++ b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.Net; using System.Net.Sockets; using Avalonia.Controls; @@ -25,7 +26,7 @@ namespace Avalonia }) .AfterSetup(_ => { - var lt = ((IClassicDesktopStyleApplicationLifetime)builder.Instance.ApplicationLifetime); + var lt = ((IClassicDesktopStyleApplicationLifetime)builder.Instance!.ApplicationLifetime!); lt.Startup += async delegate { while (true) @@ -38,7 +39,7 @@ namespace Avalonia var session = new VncServerSession(); session.SetFramebufferSource(new HeadlessVncFramebufferSource( - session, lt.MainWindow)); + session, lt.MainWindow ?? throw new InvalidOperationException("MainWindow wasn't initialized"))); session.Connect(client.GetStream(), options); } diff --git a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj index c3a472e950..fbfa89f5a4 100644 --- a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj +++ b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj @@ -1,13 +1,14 @@  net6.0;netstandard2.0 - + - + + diff --git a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index b0b1d731d2..6619b533f6 100644 --- a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -1,8 +1,6 @@ using System; using System.Diagnostics; using Avalonia.Reactive; -using Avalonia.Controls; -using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Platform; @@ -14,11 +12,12 @@ namespace Avalonia.Headless { public static class AvaloniaHeadlessPlatform { - internal static Compositor Compositor { get; private set; } - class RenderTimer : DefaultRenderTimer + internal static Compositor? Compositor { get; private set; } + + private class RenderTimer : DefaultRenderTimer { private readonly int _framesPerSecond; - private Action _forceTick; + private Action? _forceTick; protected override IDisposable StartCore(Action tick) { bool cancelled = false; @@ -46,7 +45,7 @@ namespace Avalonia.Headless public void ForceTick() => _forceTick?.Invoke(); } - class HeadlessWindowingPlatform : IWindowingPlatform + private class HeadlessWindowingPlatform : IWindowingPlatform { public IWindowImpl CreateWindow() => new HeadlessWindowImpl(false); @@ -54,7 +53,7 @@ namespace Avalonia.Headless public IPopupImpl CreatePopup() => new HeadlessWindowImpl(true); - public ITrayIconImpl CreateTrayIcon() => null; + public ITrayIconImpl? CreateTrayIcon() => null; } internal static void Initialize(AvaloniaHeadlessPlatformOptions opts) diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 31aaebcdc7..9944a10f94 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Numerics; using System.Runtime.InteropServices; @@ -23,7 +24,7 @@ namespace Avalonia.Headless public IEnumerable InstalledFontNames { get; } = new[] { "Tahoma" }; - public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) => this; + public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext? graphicsContext) => this; public bool SupportsIndividualRoundRects => false; @@ -52,7 +53,7 @@ namespace Avalonia.Headless public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => new HeadlessRenderTarget(); public bool IsLost => false; - public object TryGetFeature(Type featureType) => null; + public object? TryGetFeature(Type featureType) => null; public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) { @@ -130,7 +131,7 @@ namespace Avalonia.Headless return new HeadlessGlyphRunStub(); } - class HeadlessGlyphRunStub : IGlyphRunImpl + private class HeadlessGlyphRunStub : IGlyphRunImpl { public Size Size => new Size(8, 12); @@ -144,7 +145,7 @@ namespace Avalonia.Headless => Array.Empty(); } - class HeadlessGeometryStub : IGeometryImpl + private class HeadlessGeometryStub : IGeometryImpl { public HeadlessGeometryStub(Rect bounds) { @@ -157,7 +158,7 @@ namespace Avalonia.Headless public virtual bool FillContains(Point point) => Bounds.Contains(point); - public Rect GetRenderBounds(IPen pen) + public Rect GetRenderBounds(IPen? pen) { if(pen is null) { @@ -167,7 +168,7 @@ namespace Avalonia.Headless return Bounds.Inflate(pen.Thickness / 2); } - public bool StrokeContains(IPen pen, Point point) + public bool StrokeContains(IPen? pen, Point point) { return false; } @@ -191,21 +192,21 @@ namespace Avalonia.Headless return false; } - public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, out IGeometryImpl segmentGeometry) + public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, [NotNullWhen(true)] out IGeometryImpl? segmentGeometry) { segmentGeometry = null; return false; } } - class HeadlessTransformedGeometryStub : HeadlessGeometryStub, ITransformedGeometryImpl + private class HeadlessTransformedGeometryStub : HeadlessGeometryStub, ITransformedGeometryImpl { public HeadlessTransformedGeometryStub(IGeometryImpl b, Matrix transform) : this(Fix(b, transform)) { } - static (IGeometryImpl, Matrix, Rect) Fix(IGeometryImpl b, Matrix transform) + private static (IGeometryImpl, Matrix, Rect) Fix(IGeometryImpl b, Matrix transform) { if (b is HeadlessTransformedGeometryStub transformed) { @@ -227,7 +228,7 @@ namespace Avalonia.Headless public Matrix Transform { get; } } - class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl + private class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl { public HeadlessStreamingGeometryStub() : base(default) { @@ -243,7 +244,7 @@ namespace Avalonia.Headless return new HeadlessStreamingGeometryContextStub(this); } - class HeadlessStreamingGeometryContextStub : IStreamGeometryContextImpl + private class HeadlessStreamingGeometryContextStub : IStreamGeometryContextImpl { private readonly HeadlessStreamingGeometryStub _parent; private double _x1, _y1, _x2, _y2; @@ -252,7 +253,7 @@ namespace Avalonia.Headless _parent = parent; } - void Track(Point pt) + private void Track(Point pt) { if (_x1 > pt.X) _x1 = pt.X; @@ -301,7 +302,7 @@ namespace Avalonia.Headless } } - class HeadlessBitmapStub : IBitmapImpl, IDrawingContextLayerImpl, IWriteableBitmapImpl + private class HeadlessBitmapStub : IBitmapImpl, IDrawingContextLayerImpl, IWriteableBitmapImpl { public Size Size { get; } @@ -363,7 +364,7 @@ namespace Avalonia.Headless } } - class HeadlessDrawingContextStub : IDrawingContextImpl + private class HeadlessDrawingContextStub : IDrawingContextImpl { public void Dispose() { @@ -437,16 +438,16 @@ namespace Avalonia.Headless } - public object GetFeature(Type t) + public object? GetFeature(Type t) { return null; } - public void DrawLine(IPen pen, Point p1, Point p2) + public void DrawLine(IPen? pen, Point p1, Point p2) { } - public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) + public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) { } @@ -464,16 +465,16 @@ namespace Avalonia.Headless } - public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadow = default) + public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadow = default) { } - public void DrawEllipse(IBrush brush, IPen pen, Rect rect) + public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) { } - public void DrawGlyphRun(IBrush foreground, IRef glyphRun) + public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) { } @@ -484,7 +485,7 @@ namespace Avalonia.Headless } } - class HeadlessRenderTarget : IRenderTarget + private class HeadlessRenderTarget : IRenderTarget { public void Dispose() { diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs index 46e3515d11..ecdc7a1d35 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -18,17 +18,17 @@ using Avalonia.Utilities; namespace Avalonia.Headless { - class HeadlessClipboardStub : IClipboard + internal class HeadlessClipboardStub : IClipboard { - private string _text; - private IDataObject _data; + private string? _text; + private IDataObject? _data; - public Task GetTextAsync() + public Task GetTextAsync() { return Task.Run(() => _text); } - public Task SetTextAsync(string text) + public Task SetTextAsync(string? text) { return Task.Run(() => _text = text); } @@ -45,16 +45,29 @@ namespace Avalonia.Headless public Task GetFormatsAsync() { - throw new NotImplementedException(); + return Task.Run(() => + { + if (_data is not null) + { + return _data.GetDataFormats().ToArray(); + } + + if (_text is not null) + { + return new[] { DataFormats.Text }; + } + + return Array.Empty(); + }); } - public async Task GetDataAsync(string format) + public async Task GetDataAsync(string format) { return await Task.Run(() => _data); } } - class HeadlessCursorFactoryStub : ICursorFactory + internal class HeadlessCursorFactoryStub : ICursorFactory { public ICursorImpl GetCursor(StandardCursorType cursorType) => new CursorStub(); public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new CursorStub(); @@ -65,7 +78,7 @@ namespace Avalonia.Headless } } - class HeadlessGlyphTypefaceImpl : IGlyphTypeface + internal class HeadlessGlyphTypefaceImpl : IGlyphTypeface { public FontMetrics Metrics => new FontMetrics { @@ -117,7 +130,7 @@ namespace Avalonia.Headless public bool TryGetTable(uint tag, out byte[] table) { - table = null; + table = null!; return false; } @@ -133,7 +146,7 @@ namespace Avalonia.Headless } } - class HeadlessTextShaperStub : ITextShaperImpl + internal class HeadlessTextShaperStub : ITextShaperImpl { public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) { @@ -145,7 +158,7 @@ namespace Avalonia.Headless } } - class HeadlessFontManagerStub : IFontManagerImpl + internal class HeadlessFontManagerStub : IFontManagerImpl { public IGlyphTypeface CreateGlyphTypeface(Typeface typeface) { @@ -163,17 +176,16 @@ namespace Avalonia.Headless } public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, - FontFamily fontFamily, CultureInfo culture, out Typeface typeface) + FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface) { typeface = new Typeface("Arial", fontStyle, fontWeight, fontStretch); return true; } } - class HeadlessIconLoaderStub : IPlatformIconLoader + internal class HeadlessIconLoaderStub : IPlatformIconLoader { - - class IconStub : IWindowIconImpl + private class IconStub : IWindowIconImpl { public void Save(Stream outputStream) { @@ -196,7 +208,7 @@ namespace Avalonia.Headless } } - class HeadlessScreensStub : IScreenImpl + internal class HeadlessScreensStub : IScreenImpl { public int ScreenCount { get; } = 1; @@ -206,40 +218,19 @@ namespace Avalonia.Headless new PixelRect(0, 0, 1920, 1280), true), }; - public Screen ScreenFromPoint(PixelPoint point) + public Screen? ScreenFromPoint(PixelPoint point) { return ScreenHelper.ScreenFromPoint(point, AllScreens); } - public Screen ScreenFromRect(PixelRect rect) + public Screen? ScreenFromRect(PixelRect rect) { return ScreenHelper.ScreenFromRect(rect, AllScreens); } - public Screen ScreenFromWindow(IWindowBaseImpl window) + public Screen? ScreenFromWindow(IWindowBaseImpl window) { return ScreenHelper.ScreenFromWindow(window, AllScreens); } } - - internal class NoopStorageProvider : BclStorageProvider - { - public override bool CanOpen => false; - public override Task> OpenFilePickerAsync(FilePickerOpenOptions options) - { - return Task.FromResult>(Array.Empty()); - } - - public override bool CanSave => false; - public override Task SaveFilePickerAsync(FilePickerSaveOptions options) - { - return Task.FromResult(null); - } - - public override bool CanPickFolder => false; - public override Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) - { - return Task.FromResult>(Array.Empty()); - } - } } diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs index 046e4645e3..fa455c8032 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs @@ -6,16 +6,16 @@ using Avalonia.Threading; namespace Avalonia.Headless { - class HeadlessPlatformThreadingInterface : IPlatformThreadingInterface + internal class HeadlessPlatformThreadingInterface : IPlatformThreadingInterface { public HeadlessPlatformThreadingInterface() { _thread = Thread.CurrentThread; } - private AutoResetEvent _event = new AutoResetEvent(false); - private Thread _thread; - private object _lock = new object(); + private readonly AutoResetEvent _event = new AutoResetEvent(false); + private readonly Thread? _thread; + private readonly object _lock = new object(); private DispatcherPriority? _signaledPriority; public void RunLoop(CancellationToken cancellationToken) @@ -40,7 +40,7 @@ namespace Avalonia.Headless interval = TimeSpan.FromMilliseconds(10); var stopped = false; - Timer timer = null; + Timer? timer = null; timer = new Timer(_ => { if (stopped) @@ -55,7 +55,7 @@ namespace Avalonia.Headless finally { if (!stopped) - timer.Change(interval, Timeout.InfiniteTimeSpan); + timer?.Change(interval, Timeout.InfiniteTimeSpan); } }); }, @@ -81,6 +81,6 @@ namespace Avalonia.Headless } public bool CurrentThreadIsLoopThread => _thread == Thread.CurrentThread; - public event Action Signaled; + public event Action? Signaled; } } diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs index 15e2c696ac..f2f31debad 100644 --- a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs @@ -18,20 +18,20 @@ using Avalonia.Utilities; namespace Avalonia.Headless { - class HeadlessWindowImpl : IWindowImpl, IPopupImpl, IFramebufferPlatformSurface, IHeadlessWindow + internal class HeadlessWindowImpl : IWindowImpl, IPopupImpl, IFramebufferPlatformSurface, IHeadlessWindow { - private IKeyboardDevice _keyboard; - private Stopwatch _st = Stopwatch.StartNew(); - private Pointer _mousePointer; - private WriteableBitmap _lastRenderedFrame; - private object _sync = new object(); + private readonly IKeyboardDevice _keyboard; + private readonly Stopwatch _st = Stopwatch.StartNew(); + private readonly Pointer _mousePointer; + private WriteableBitmap? _lastRenderedFrame; + private readonly object _sync = new object(); public bool IsPopup { get; } public HeadlessWindowImpl(bool isPopup) { IsPopup = isPopup; Surfaces = new object[] { this }; - _keyboard = AvaloniaLocator.Current.GetService(); + _keyboard = AvaloniaLocator.Current.GetRequiredService(); _mousePointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); MouseDevice = new MouseDevice(_mousePointer); ClientSize = new Size(1024, 768); @@ -49,13 +49,13 @@ namespace Avalonia.Headless public double RenderScaling { get; } = 1; public double DesktopScaling => RenderScaling; public IEnumerable Surfaces { get; } - public Action Input { get; set; } - public Action Paint { get; set; } - public Action Resized { get; set; } - public Action ScalingChanged { get; set; } + public Action? Input { get; set; } + public Action? Paint { get; set; } + public Action? Resized { get; set; } + public Action? ScalingChanged { get; set; } public IRenderer CreateRenderer(IRenderRoot root) => - new CompositingRenderer(root, AvaloniaHeadlessPlatform.Compositor, () => Surfaces); + new CompositingRenderer(root, AvaloniaHeadlessPlatform.Compositor!, () => Surfaces); public void Invalidate(Rect rect) { @@ -66,18 +66,18 @@ namespace Avalonia.Headless InputRoot = inputRoot; } - public IInputRoot InputRoot { get; set; } + public IInputRoot? InputRoot { get; set; } public Point PointToClient(PixelPoint point) => point.ToPoint(RenderScaling); public PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, RenderScaling); - public void SetCursor(ICursorImpl cursor) + public void SetCursor(ICursorImpl? cursor) { } - public Action Closed { get; set; } + public Action? Closed { get; set; } public IMouseDevice MouseDevice { get; } public void Show(bool activate, bool isDialog) @@ -102,14 +102,14 @@ namespace Avalonia.Headless } public PixelPoint Position { get; set; } - public Action PositionChanged { get; set; } + public Action? PositionChanged { get; set; } public void Activate() { Dispatcher.UIThread.Post(() => Activated?.Invoke(), DispatcherPriority.Input); } - public Action Deactivated { get; set; } - public Action Activated { get; set; } + public Action? Deactivated { get; set; } + public Action? Activated { get; set; } public IPlatformHandle Handle { get; } = new PlatformHandle(IntPtr.Zero, "STUB"); public Size MaxClientSize { get; } = new Size(1920, 1280); public void Resize(Size clientSize, PlatformResizeReason reason) @@ -124,7 +124,7 @@ namespace Avalonia.Headless }); } - void DoResize(Size clientSize) + private void DoResize(Size clientSize) { // Uncomment this check and experience a weird bug in layout engine if (ClientSize != clientSize) @@ -146,8 +146,8 @@ namespace Avalonia.Headless public IScreenImpl Screen { get; } = new HeadlessScreensStub(); public WindowState WindowState { get; set; } - public Action WindowStateChanged { get; set; } - public void SetTitle(string title) + public Action? WindowStateChanged { get; set; } + public void SetTitle(string? title) { } @@ -157,7 +157,7 @@ namespace Avalonia.Headless } - public void SetIcon(IWindowIconImpl icon) + public void SetIcon(IWindowIconImpl? icon) { } @@ -172,9 +172,9 @@ namespace Avalonia.Headless } - public Func Closing { get; set; } + public Func? Closing { get; set; } - class FramebufferProxy : ILockedFramebuffer + private class FramebufferProxy : ILockedFramebuffer { private readonly ILockedFramebuffer _fb; private readonly Action _onDispose; @@ -215,7 +215,7 @@ namespace Avalonia.Headless }); } - public IRef GetLastRenderedFrame() + public IRef? GetLastRenderedFrame() { lock (_sync) return _lastRenderedFrame?.PlatformImpl?.CloneAs(); @@ -224,19 +224,19 @@ namespace Avalonia.Headless private ulong Timestamp => (ulong)_st.ElapsedMilliseconds; // TODO: Hook recent Popup changes. - IPopupPositioner IPopupImpl.PopupPositioner => null; + IPopupPositioner IPopupImpl.PopupPositioner => null!; public Size MaxAutoSizeHint => new Size(1920, 1080); - public Action TransparencyLevelChanged { get; set; } + public Action? TransparencyLevelChanged { get; set; } public WindowTransparencyLevel TransparencyLevel => WindowTransparencyLevel.None; - public Action GotInputWhenDisabled { get; set; } + public Action? GotInputWhenDisabled { get; set; } public bool IsClientAreaExtendedToDecorations => false; - public Action ExtendClientAreaToDecorationsChanged { get; set; } + public Action? ExtendClientAreaToDecorationsChanged { get; set; } public bool NeedsManagedDecorations => false; @@ -244,32 +244,27 @@ namespace Avalonia.Headless public Thickness OffScreenMargin => new Thickness(); - public Action LostFocus { get; set; } + public Action? LostFocus { get; set; } public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1); - public object TryGetFeature(Type featureType) + public object? TryGetFeature(Type featureType) { - if (featureType == typeof(IStorageProvider)) - { - return new NoopStorageProvider(); - } - return null; } void IHeadlessWindow.KeyPress(Key key, RawInputModifiers modifiers) { - Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot, RawKeyEventType.KeyDown, key, modifiers)); + Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot!, RawKeyEventType.KeyDown, key, modifiers)); } void IHeadlessWindow.KeyRelease(Key key, RawInputModifiers modifiers) { - Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot, RawKeyEventType.KeyUp, key, modifiers)); + Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot!, RawKeyEventType.KeyUp, key, modifiers)); } void IHeadlessWindow.MouseDown(Point point, int button, RawInputModifiers modifiers) { - Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot, + Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot!, button == 0 ? RawPointerEventType.LeftButtonDown : button == 1 ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.RightButtonDown, point, modifiers)); @@ -277,13 +272,13 @@ namespace Avalonia.Headless void IHeadlessWindow.MouseMove(Point point, RawInputModifiers modifiers) { - Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot, + Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot!, RawPointerEventType.Move, point, modifiers)); } void IHeadlessWindow.MouseUp(Point point, int button, RawInputModifiers modifiers) { - Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot, + Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot!, button == 0 ? RawPointerEventType.LeftButtonUp : button == 1 ? RawPointerEventType.MiddleButtonUp : RawPointerEventType.RightButtonUp, point, modifiers)); @@ -291,14 +286,14 @@ namespace Avalonia.Headless void IHeadlessWindow.MouseWheel(Point point, Vector delta, RawInputModifiers modifiers) { - Input?.Invoke(new RawMouseWheelEventArgs(MouseDevice, Timestamp, InputRoot, + Input?.Invoke(new RawMouseWheelEventArgs(MouseDevice, Timestamp, InputRoot!, point, delta, modifiers)); } void IHeadlessWindow.DragDrop(Point point, RawDragEventType type, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers) { var device = AvaloniaLocator.Current.GetRequiredService(); - Input?.Invoke(new RawDragEvent(device, type, InputRoot, point, data, effects, modifiers)); + Input?.Invoke(new RawDragEvent(device, type, InputRoot!, point, data, effects, modifiers)); } void IWindowImpl.Move(PixelPoint point) @@ -306,7 +301,7 @@ namespace Avalonia.Headless } - public IPopupImpl CreatePopup() + public IPopupImpl? CreatePopup() { // TODO: Hook recent Popup changes. return null; diff --git a/src/Headless/Avalonia.Headless/IHeadlessWindow.cs b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs index dfb3a4c433..42ec38a934 100644 --- a/src/Headless/Avalonia.Headless/IHeadlessWindow.cs +++ b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs @@ -8,7 +8,7 @@ namespace Avalonia.Headless { public interface IHeadlessWindow { - IRef GetLastRenderedFrame(); + IRef? GetLastRenderedFrame(); void KeyPress(Key key, RawInputModifiers modifiers); void KeyRelease(Key key, RawInputModifiers modifiers); void MouseDown(Point point, int button, RawInputModifiers modifiers = RawInputModifiers.None); From a26566548a548c63b55385619c656ebcdb387479 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 25 Feb 2023 00:29:58 -0500 Subject: [PATCH 004/110] Add headless xunit integration project --- Avalonia.sln | 7 +++ src/Avalonia.Controls/AppBuilder.cs | 39 +++++++++++- .../Avalonia.Controls.csproj | 1 + .../Remote/RemoteDesignerEntryPoint.cs | 12 +--- .../Avalonia.Headless.XUnit.csproj | 19 ++++++ .../AvaloniaTestFramework.cs | 35 +++++++++++ .../AvaloniaTestFrameworkAttribute.cs | 45 ++++++++++++++ .../AvaloniaTestRunner.cs | 61 +++++++++++++++++++ 8 files changed, 208 insertions(+), 11 deletions(-) create mode 100644 src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj create mode 100644 src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs create mode 100644 src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs create mode 100644 src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs diff --git a/Avalonia.sln b/Avalonia.sln index 295f7cb149..6d0c4dee2a 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -248,6 +248,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Fonts.Inter", "src EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Headless", "Headless", "{FF237916-7150-496B-89ED-6CA3292896E7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.XUnit", "src\Headless\Avalonia.Headless.XUnit\Avalonia.Headless.XUnit.csproj", "{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -579,6 +581,10 @@ Global {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.Build.0 = Debug|Any CPU {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.ActiveCfg = Release|Any CPU {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.Build.0 = Release|Any CPU + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -648,6 +654,7 @@ Global {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC} = {FF237916-7150-496B-89ED-6CA3292896E7} {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E} = {FF237916-7150-496B-89ED-6CA3292896E7} + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119} = {FF237916-7150-496B-89ED-6CA3292896E7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/src/Avalonia.Controls/AppBuilder.cs b/src/Avalonia.Controls/AppBuilder.cs index cf79fcd1a8..871d6bf52a 100644 --- a/src/Avalonia.Controls/AppBuilder.cs +++ b/src/Avalonia.Controls/AppBuilder.cs @@ -116,6 +116,43 @@ namespace Avalonia }; } + /// + /// Begin configuring an . + /// Should only be used for testing and design purposes, as it relies on dynamic code. + /// + /// + /// Parameter from which should be created. + /// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application. + /// + /// An instance. If can't be created, thrown an exception. + internal static AppBuilder Configure( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type entryPointType) + { + var appBuilderObj = entryPointType + .GetMethod( + "BuildAvaloniaApp", + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy, + null, + Array.Empty(), + null)? + .Invoke(null, Array.Empty()); + + if (appBuilderObj is AppBuilder appBuilder) + { + return appBuilder; + } + + if (typeof(Application).IsAssignableFrom(entryPointType)) + { + return Configure(() => (Application)Activator.CreateInstance(entryPointType)!); + } + + throw new InvalidOperationException( + $"Unable to create AppBuilder from type {entryPointType.Name}." + + $"Input type either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application type."); + } + protected AppBuilder Self => this; public AppBuilder AfterSetup(Action callback) @@ -204,7 +241,7 @@ namespace Avalonia _optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind().ToFunc(options); }; return Self; } - + /// /// Sets up the platform-specific services for the . /// diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 5adb5a8f2e..b9e2d3d259 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -17,5 +17,6 @@ + diff --git a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs index 85605ccd9d..313063269b 100644 --- a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs +++ b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs @@ -179,17 +179,9 @@ namespace Avalonia.DesignerSupport.Remote var entryPoint = asm.EntryPoint; if (entryPoint == null) throw Die($"Assembly {args.AppPath} doesn't have an entry point"); - var builderMethod = entryPoint.DeclaringType.GetMethod( - BuilderMethodName, - BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy, - null, - Array.Empty(), - null); - if (builderMethod == null) - throw Die($"{entryPoint.DeclaringType.FullName} doesn't have a method named {BuilderMethodName}"); + Log($"Obtaining AppBuilder instance from {entryPoint.DeclaringType!.FullName}"); + var appBuilder = AppBuilder.Configure(entryPoint.DeclaringType); Design.IsDesignMode = true; - Log($"Obtaining AppBuilder instance from {builderMethod.DeclaringType.FullName}.{builderMethod.Name}"); - var appBuilder = builderMethod.Invoke(null, null); Log($"Initializing application in design mode"); var initializer =(IAppInitializer)Activator.CreateInstance(typeof(AppInitializer)); transport = initializer.ConfigureApp(transport, args, appBuilder); diff --git a/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj b/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj new file mode 100644 index 0000000000..c2c58b4f94 --- /dev/null +++ b/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj @@ -0,0 +1,19 @@ + + + net6.0 + enable + enable + + + + + + + + + + + + + + diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs new file mode 100644 index 0000000000..21086fa946 --- /dev/null +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Avalonia.Headless.XUnit; + +internal class AvaloniaTestFramework : XunitTestFramework +{ + public AvaloniaTestFramework(IMessageSink messageSink) : base(messageSink) + { + } + + protected override ITestFrameworkExecutor CreateExecutor(AssemblyName assemblyName) + => new Executor(assemblyName, SourceInformationProvider, DiagnosticMessageSink); + + + private class Executor : XunitTestFrameworkExecutor + { + public Executor(AssemblyName assemblyName, ISourceInformationProvider sourceInformationProvider, + IMessageSink diagnosticMessageSink) : base(assemblyName, sourceInformationProvider, + diagnosticMessageSink) + { + } + + protected override async void RunTestCases(IEnumerable testCases, + IMessageSink executionMessageSink, + ITestFrameworkExecutionOptions executionOptions) + { + executionOptions.SetValue("xunit.execution.DisableParallelization", false); + using (var assemblyRunner = new AvaloniaTestRunner( + TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink, + executionOptions)) await assemblyRunner.RunAsync(); + } + } +} diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs new file mode 100644 index 0000000000..d0249a5b25 --- /dev/null +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs @@ -0,0 +1,45 @@ +using System.Diagnostics.CodeAnalysis; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Avalonia.Headless.XUnit; + +/// +/// +/// +[TestFrameworkDiscoverer("Avalonia.Headless.XUnit.AvaloniaTestFrameworkTypeDiscoverer", "Avalonia.Headless.XUnit")] +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)] +public sealed class AvaloniaTestFrameworkAttribute : Attribute, ITestFrameworkAttribute +{ + /// + /// Creates instance of . + /// + /// + /// Parameter from which should be created. + /// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application. + /// + public AvaloniaTestFrameworkAttribute( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type appBuilderEntryPointType) { } +} + +/// +/// Discoverer implementation for the Avalonia testing framework. +/// +public class AvaloniaTestFrameworkTypeDiscoverer : ITestFrameworkTypeDiscoverer +{ + /// + /// Creates instance of . + /// + public AvaloniaTestFrameworkTypeDiscoverer(IMessageSink _) + { + } + + /// + public Type GetTestFrameworkType(IAttributeInfo attribute) + { + var builderType = attribute.GetConstructorArguments().First() as Type + ?? throw new InvalidOperationException("AppBuilderEntryPointType parameter must be defined on the AvaloniaTestFrameworkAttribute attribute."); + return typeof(AvaloniaTestFramework<>).MakeGenericType(builderType); + } +} diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs new file mode 100644 index 0000000000..579232521d --- /dev/null +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs @@ -0,0 +1,61 @@ +using Avalonia.Threading; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Avalonia.Headless.XUnit; + +internal class AvaloniaTestRunner : XunitTestAssemblyRunner +{ + private CancellationTokenSource? _cancellationTokenSource; + + public AvaloniaTestRunner(ITestAssembly testAssembly, IEnumerable testCases, + IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink, + ITestFrameworkExecutionOptions executionOptions) : base(testAssembly, testCases, diagnosticMessageSink, + executionMessageSink, executionOptions) + { + } + + protected override void SetupSyncContext(int maxParallelThreads) + { + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = new CancellationTokenSource(); + SynchronizationContext.SetSynchronizationContext(InitNewApplicationContext(_cancellationTokenSource.Token).Result); + } + + public override void Dispose() + { + _cancellationTokenSource?.Dispose(); + base.Dispose(); + } + + internal static Task InitNewApplicationContext(CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource(); + + new Thread(() => + { + try + { + var appBuilder = AppBuilder.Configure(typeof(TAppBuilderEntry)); + + // If windowing subsystem wasn't initialized by user, force headless with default parameters. + if (appBuilder.WindowingSubsystemName != "Headless") + { + appBuilder = appBuilder.UseHeadless(new AvaloniaHeadlessPlatformOptions()); + } + + appBuilder.SetupWithoutStarting(); + + tcs.SetResult(SynchronizationContext.Current!); + } + catch (Exception e) + { + tcs.SetException(e); + } + + Dispatcher.UIThread.MainLoop(cancellationToken); + }) { IsBackground = true }.Start(); + + return tcs.Task; + } +} From 5e4509deb167115b7b403754b6d2a38b2eceade9 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 25 Feb 2023 00:30:20 -0500 Subject: [PATCH 005/110] Run tests on Avalonia.Headless --- Avalonia.sln | 7 ++++ nukebuild/Build.cs | 1 + .../HeadlessPlatformRenderInterface.cs | 3 +- .../Avalonia.Headless/HeadlessWindowImpl.cs | 13 ++++++- .../Avalonia.Headless/IHeadlessWindow.cs | 2 +- .../Avalonia.Headless.UnitTests.csproj | 19 +++++++++ .../Avalonia.Headless.UnitTests/InputTests.cs | 37 ++++++++++++++++++ .../RenderingTests.cs | 39 +++++++++++++++++++ .../TestApplication.cs | 24 ++++++++++++ .../ThreadingTests.cs | 32 +++++++++++++++ 10 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 tests/Avalonia.Headless.UnitTests/Avalonia.Headless.UnitTests.csproj create mode 100644 tests/Avalonia.Headless.UnitTests/InputTests.cs create mode 100644 tests/Avalonia.Headless.UnitTests/RenderingTests.cs create mode 100644 tests/Avalonia.Headless.UnitTests/TestApplication.cs create mode 100644 tests/Avalonia.Headless.UnitTests/ThreadingTests.cs diff --git a/Avalonia.sln b/Avalonia.sln index 6d0c4dee2a..597eb632ca 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -250,6 +250,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Headless", "Headless", "{FF EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.XUnit", "src\Headless\Avalonia.Headless.XUnit\Avalonia.Headless.XUnit.csproj", "{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.UnitTests", "tests\Avalonia.Headless.UnitTests\Avalonia.Headless.UnitTests.csproj", "{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -585,6 +587,10 @@ Global {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.Build.0 = Debug|Any CPU {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.ActiveCfg = Release|Any CPU {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.Build.0 = Release|Any CPU + {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -655,6 +661,7 @@ Global {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC} = {FF237916-7150-496B-89ED-6CA3292896E7} {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E} = {FF237916-7150-496B-89ED-6CA3292896E7} {F47F8316-4D4B-4026-8EF3-16B2CFDA8119} = {FF237916-7150-496B-89ED-6CA3292896E7} + {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 3704cee890..9883745bfa 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -212,6 +212,7 @@ partial class Build : NukeBuild RunCoreTest("Avalonia.Markup.Xaml.UnitTests"); RunCoreTest("Avalonia.Skia.UnitTests"); RunCoreTest("Avalonia.ReactiveUI.UnitTests"); + RunCoreTest("Avalonia.Headless.UnitTests"); }); Target RunRenderTests => _ => _ diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 9944a10f94..7fa2e46c42 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -19,7 +19,8 @@ namespace Avalonia.Headless public static void Initialize() { AvaloniaLocator.CurrentMutable - .Bind().ToConstant(new HeadlessPlatformRenderInterface()); + .Bind().ToConstant(new HeadlessPlatformRenderInterface()) + .Bind().ToConstant(new HeadlessFontManagerStub()); } public IEnumerable InstalledFontNames { get; } = new[] { "Tahoma" }; diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs index f2f31debad..e1c09107c8 100644 --- a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs @@ -215,10 +215,19 @@ namespace Avalonia.Headless }); } - public IRef? GetLastRenderedFrame() + public Bitmap? GetLastRenderedFrame() { lock (_sync) - return _lastRenderedFrame?.PlatformImpl?.CloneAs(); + { + if (_lastRenderedFrame is null) + { + return null; + } + + using var lockedFramebuffer = _lastRenderedFrame.Lock(); + return new Bitmap(lockedFramebuffer.Format, AlphaFormat.Opaque, lockedFramebuffer.Address, + lockedFramebuffer.Size, lockedFramebuffer.Dpi, lockedFramebuffer.RowBytes); + } } private ulong Timestamp => (ulong)_st.ElapsedMilliseconds; diff --git a/src/Headless/Avalonia.Headless/IHeadlessWindow.cs b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs index 42ec38a934..3391a103d1 100644 --- a/src/Headless/Avalonia.Headless/IHeadlessWindow.cs +++ b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs @@ -8,7 +8,7 @@ namespace Avalonia.Headless { public interface IHeadlessWindow { - IRef? GetLastRenderedFrame(); + Bitmap? GetLastRenderedFrame(); void KeyPress(Key key, RawInputModifiers modifiers); void KeyRelease(Key key, RawInputModifiers modifiers); void MouseDown(Point point, int button, RawInputModifiers modifiers = RawInputModifiers.None); diff --git a/tests/Avalonia.Headless.UnitTests/Avalonia.Headless.UnitTests.csproj b/tests/Avalonia.Headless.UnitTests/Avalonia.Headless.UnitTests.csproj new file mode 100644 index 0000000000..78a3ab186e --- /dev/null +++ b/tests/Avalonia.Headless.UnitTests/Avalonia.Headless.UnitTests.csproj @@ -0,0 +1,19 @@ + + + + net6.0 + true + + + + + + + + + + + + + + diff --git a/tests/Avalonia.Headless.UnitTests/InputTests.cs b/tests/Avalonia.Headless.UnitTests/InputTests.cs new file mode 100644 index 0000000000..c4d9f1a517 --- /dev/null +++ b/tests/Avalonia.Headless.UnitTests/InputTests.cs @@ -0,0 +1,37 @@ +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Threading; +using Xunit; + +namespace Avalonia.Headless.XUnit.Tests; + +public class InputTests +{ + [Fact] + public void Should_Click_Button_On_Window() + { + var buttonClicked = false; + var button = new Button + { + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch + }; + + button.Click += (_, _) => buttonClicked = true; + + var window = new Window + { + Width = 100, + Height = 100, + Content = button + }; + window.Show(); + + Dispatcher.UIThread.RunJobs(); + + ((IHeadlessWindow)window.PlatformImpl!).MouseDown(new Point(50, 50), 0); + ((IHeadlessWindow)window.PlatformImpl!).MouseUp(new Point(50, 50), 0); + + Assert.True(buttonClicked); + } +} diff --git a/tests/Avalonia.Headless.UnitTests/RenderingTests.cs b/tests/Avalonia.Headless.UnitTests/RenderingTests.cs new file mode 100644 index 0000000000..67b99541d6 --- /dev/null +++ b/tests/Avalonia.Headless.UnitTests/RenderingTests.cs @@ -0,0 +1,39 @@ +using System.IO; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Threading; +using Xunit; + +namespace Avalonia.Headless.XUnit.Tests; + +public class RenderingTests +{ + [Fact] + public void Should_Render_Last_Frame_To_Bitmap() + { + var window = new Window + { + Content = new ContentControl + { + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch, + Padding = new Thickness(4), + Content = new PathIcon + { + Data = StreamGeometry.Parse("M0,9 L10,0 20,9 19,10 10,2 1,10 z") + } + }, + SizeToContent = SizeToContent.WidthAndHeight + }; + window.Show(); + + Dispatcher.UIThread.RunJobs(); + AvaloniaHeadlessPlatform.ForceRenderTimerTick(); + + var frame = ((IHeadlessWindow)window.PlatformImpl!).GetLastRenderedFrame(); + Assert.NotNull(frame); + } +} diff --git a/tests/Avalonia.Headless.UnitTests/TestApplication.cs b/tests/Avalonia.Headless.UnitTests/TestApplication.cs new file mode 100644 index 0000000000..0b2927fa29 --- /dev/null +++ b/tests/Avalonia.Headless.UnitTests/TestApplication.cs @@ -0,0 +1,24 @@ +using Avalonia.Headless.XUnit; +using Avalonia.Headless.XUnit.Tests; +using Avalonia.Themes.Simple; +using Xunit; + +[assembly: AvaloniaTestFramework(typeof(TestApplication))] +[assembly: CollectionBehavior(DisableTestParallelization = true)] + +namespace Avalonia.Headless.XUnit.Tests; + +public class TestApplication : Application +{ + public TestApplication() + { + Styles.Add(new SimpleTheme()); + } + + public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() + .UseSkia() + .UseHeadless(new AvaloniaHeadlessPlatformOptions + { + UseHeadlessDrawing = false + }); +} diff --git a/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs b/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs new file mode 100644 index 0000000000..efcd2d9081 --- /dev/null +++ b/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Threading; +using Xunit; + +namespace Avalonia.Headless.XUnit.Tests; + +public class ThreadingTests +{ + [Fact] + public void Should_Be_On_Dispatcher_Thread() + { + Dispatcher.UIThread.VerifyAccess(); + } + + [Fact] + public async Task DispatcherTimer_Works_On_The_Same_Thread() + { + var currentThread = Thread.CurrentThread; + var tcs = new TaskCompletionSource(); + + DispatcherTimer.RunOnce(() => + { + Assert.Equal(currentThread, Thread.CurrentThread); + + tcs.SetResult(); + }, TimeSpan.FromTicks(1)); + + await tcs.Task; + } +} From 6b81b5a93543bb8dcf679b6fca4e2e6c6b2a4261 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 25 Feb 2023 00:51:42 -0500 Subject: [PATCH 006/110] Make headless easier to use with HeadlessWindowExtensions --- .../HeadlessWindowExtensions.cs | 50 +++++++++++++++++++ .../Avalonia.Headless/HeadlessWindowImpl.cs | 28 ++++++++--- .../Avalonia.Headless/IHeadlessWindow.cs | 8 +-- .../Avalonia.Headless.UnitTests/InputTests.cs | 9 ++-- .../RenderingTests.cs | 10 ++-- .../TestApplication.cs | 6 +-- .../ThreadingTests.cs | 2 +- 7 files changed, 85 insertions(+), 28 deletions(-) create mode 100644 src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs new file mode 100644 index 0000000000..ad5b7e680c --- /dev/null +++ b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs @@ -0,0 +1,50 @@ +using System; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Media.Imaging; +using Avalonia.Threading; + +namespace Avalonia.Headless; + +public static class HeadlessWindowExtensions +{ + public static Bitmap? CaptureRenderedFrame(this TopLevel topLevel) + { + var impl = GetImpl(topLevel); + AvaloniaHeadlessPlatform.ForceRenderTimerTick(); + return impl.GetLastRenderedFrame(); + } + + public static Bitmap? GetLastRenderedFrame(this TopLevel topLevel) => + GetImpl(topLevel).GetLastRenderedFrame(); + + public static void KeyPress(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => + GetImpl(topLevel).KeyPress(key, modifiers); + + public static void KeyRelease(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => + GetImpl(topLevel).KeyRelease(key, modifiers); + + public static void MouseDown(this TopLevel topLevel, Point point, MouseButton button, + RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseDown(point, button, modifiers); + + public static void MouseMove(this TopLevel topLevel, Point point, + RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseMove(point, modifiers); + + public static void MouseUp(this TopLevel topLevel, Point point, MouseButton button, + RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseUp(point, button, modifiers); + + public static void MouseWheel(this TopLevel topLevel, Point point, Vector delta, + RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseWheel(point, delta, modifiers); + + public static void DragDrop(this TopLevel topLevel, Point point, RawDragEventType type, IDataObject data, + DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None) => + GetImpl(topLevel).DragDrop(point, type, data, effects, modifiers); + + private static IHeadlessWindow GetImpl(this TopLevel topLevel) + { + Dispatcher.UIThread.RunJobs(); + return topLevel.PlatformImpl as IHeadlessWindow ?? + throw new InvalidOperationException("TopLevel must be a headless window."); + } +} diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs index e1c09107c8..35f2774965 100644 --- a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs @@ -271,12 +271,18 @@ namespace Avalonia.Headless Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot!, RawKeyEventType.KeyUp, key, modifiers)); } - void IHeadlessWindow.MouseDown(Point point, int button, RawInputModifiers modifiers) + void IHeadlessWindow.MouseDown(Point point, MouseButton button, RawInputModifiers modifiers) { Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot!, - button == 0 ? RawPointerEventType.LeftButtonDown : - button == 1 ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.RightButtonDown, - point, modifiers)); + button switch + { + MouseButton.Left => RawPointerEventType.LeftButtonDown, + MouseButton.Right => RawPointerEventType.RightButtonDown, + MouseButton.Middle => RawPointerEventType.MiddleButtonDown, + MouseButton.XButton1 => RawPointerEventType.XButton1Down, + MouseButton.XButton2 => RawPointerEventType.XButton2Down, + _ => RawPointerEventType.Move, + }, point, modifiers)); } void IHeadlessWindow.MouseMove(Point point, RawInputModifiers modifiers) @@ -285,12 +291,18 @@ namespace Avalonia.Headless RawPointerEventType.Move, point, modifiers)); } - void IHeadlessWindow.MouseUp(Point point, int button, RawInputModifiers modifiers) + void IHeadlessWindow.MouseUp(Point point, MouseButton button, RawInputModifiers modifiers) { Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot!, - button == 0 ? RawPointerEventType.LeftButtonUp : - button == 1 ? RawPointerEventType.MiddleButtonUp : RawPointerEventType.RightButtonUp, - point, modifiers)); + button switch + { + MouseButton.Left => RawPointerEventType.LeftButtonUp, + MouseButton.Right => RawPointerEventType.RightButtonUp, + MouseButton.Middle => RawPointerEventType.MiddleButtonUp, + MouseButton.XButton1 => RawPointerEventType.XButton1Up, + MouseButton.XButton2 => RawPointerEventType.XButton2Up, + _ => RawPointerEventType.Move, + }, point, modifiers)); } void IHeadlessWindow.MouseWheel(Point point, Vector delta, RawInputModifiers modifiers) diff --git a/src/Headless/Avalonia.Headless/IHeadlessWindow.cs b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs index 3391a103d1..f3da2335bc 100644 --- a/src/Headless/Avalonia.Headless/IHeadlessWindow.cs +++ b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs @@ -6,15 +6,15 @@ using Avalonia.Utilities; namespace Avalonia.Headless { - public interface IHeadlessWindow + internal interface IHeadlessWindow { Bitmap? GetLastRenderedFrame(); void KeyPress(Key key, RawInputModifiers modifiers); void KeyRelease(Key key, RawInputModifiers modifiers); - void MouseDown(Point point, int button, RawInputModifiers modifiers = RawInputModifiers.None); + void MouseDown(Point point, MouseButton button, RawInputModifiers modifiers = RawInputModifiers.None); void MouseMove(Point point, RawInputModifiers modifiers = RawInputModifiers.None); - void MouseUp(Point point, int button, RawInputModifiers modifiers = RawInputModifiers.None); + void MouseUp(Point point, MouseButton button, RawInputModifiers modifiers = RawInputModifiers.None); void MouseWheel(Point point, Vector delta, RawInputModifiers modifiers = RawInputModifiers.None); - void DragDrop(Point point, RawDragEventType type, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers); + void DragDrop(Point point, RawDragEventType type, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None); } } diff --git a/tests/Avalonia.Headless.UnitTests/InputTests.cs b/tests/Avalonia.Headless.UnitTests/InputTests.cs index c4d9f1a517..3c0ecbfdb7 100644 --- a/tests/Avalonia.Headless.UnitTests/InputTests.cs +++ b/tests/Avalonia.Headless.UnitTests/InputTests.cs @@ -1,9 +1,10 @@ using Avalonia.Controls; +using Avalonia.Input; using Avalonia.Layout; using Avalonia.Threading; using Xunit; -namespace Avalonia.Headless.XUnit.Tests; +namespace Avalonia.Headless.UnitTests; public class InputTests { @@ -27,10 +28,8 @@ public class InputTests }; window.Show(); - Dispatcher.UIThread.RunJobs(); - - ((IHeadlessWindow)window.PlatformImpl!).MouseDown(new Point(50, 50), 0); - ((IHeadlessWindow)window.PlatformImpl!).MouseUp(new Point(50, 50), 0); + window.MouseDown(new Point(50, 50), MouseButton.Left); + window.MouseUp(new Point(50, 50), MouseButton.Left); Assert.True(buttonClicked); } diff --git a/tests/Avalonia.Headless.UnitTests/RenderingTests.cs b/tests/Avalonia.Headless.UnitTests/RenderingTests.cs index 67b99541d6..33f15bce1a 100644 --- a/tests/Avalonia.Headless.UnitTests/RenderingTests.cs +++ b/tests/Avalonia.Headless.UnitTests/RenderingTests.cs @@ -1,13 +1,10 @@ -using System.IO; -using System.Linq; -using Avalonia.Controls; +using Avalonia.Controls; using Avalonia.Layout; using Avalonia.Media; -using Avalonia.Media.Imaging; using Avalonia.Threading; using Xunit; -namespace Avalonia.Headless.XUnit.Tests; +namespace Avalonia.Headless.UnitTests; public class RenderingTests { @@ -32,8 +29,7 @@ public class RenderingTests Dispatcher.UIThread.RunJobs(); AvaloniaHeadlessPlatform.ForceRenderTimerTick(); - - var frame = ((IHeadlessWindow)window.PlatformImpl!).GetLastRenderedFrame(); + var frame = window.CaptureRenderedFrame(); Assert.NotNull(frame); } } diff --git a/tests/Avalonia.Headless.UnitTests/TestApplication.cs b/tests/Avalonia.Headless.UnitTests/TestApplication.cs index 0b2927fa29..7bfa0144f3 100644 --- a/tests/Avalonia.Headless.UnitTests/TestApplication.cs +++ b/tests/Avalonia.Headless.UnitTests/TestApplication.cs @@ -1,12 +1,12 @@ -using Avalonia.Headless.XUnit; -using Avalonia.Headless.XUnit.Tests; +using Avalonia.Headless.UnitTests; +using Avalonia.Headless.XUnit; using Avalonia.Themes.Simple; using Xunit; [assembly: AvaloniaTestFramework(typeof(TestApplication))] [assembly: CollectionBehavior(DisableTestParallelization = true)] -namespace Avalonia.Headless.XUnit.Tests; +namespace Avalonia.Headless.UnitTests; public class TestApplication : Application { diff --git a/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs b/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs index efcd2d9081..419ee5519e 100644 --- a/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs +++ b/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using Avalonia.Threading; using Xunit; -namespace Avalonia.Headless.XUnit.Tests; +namespace Avalonia.Headless.UnitTests; public class ThreadingTests { From 2450cf05077af78ba09ae86b41f085a53d81f0a3 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 25 Feb 2023 01:16:31 -0500 Subject: [PATCH 007/110] Update VNC project --- .../Avalonia.Headless.Vnc.csproj | 1 + .../HeadlessVncFramebufferSource.cs | 35 +++++++++---------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj index 3812fb196d..1f06f28687 100644 --- a/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj +++ b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj @@ -2,6 +2,7 @@ net6.0;netstandard2.0 + true diff --git a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs index be0f3579a5..7389a9ba15 100644 --- a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs +++ b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs @@ -10,22 +10,28 @@ namespace Avalonia.Headless.Vnc { public class HeadlessVncFramebufferSource : IVncFramebufferSource { - public IHeadlessWindow Window { get; set; } + public Window Window { get; set; } private object _lock = new object(); public VncFramebuffer _framebuffer = new VncFramebuffer("Avalonia", 1, 1, VncPixelFormat.RGB32); private VncButton _previousButtons; public HeadlessVncFramebufferSource(VncServerSession session, Window window) { - Window = window.PlatformImpl as IHeadlessWindow ?? throw new InvalidOperationException("Invalid window parameter"); + Window = window; session.PointerChanged += (_, args) => { var pt = new Point(args.X, args.Y); var buttons = (VncButton)args.PressedButtons; - int TranslateButton(VncButton vncButton) => - vncButton == VncButton.Left ? 0 : vncButton == VncButton.Right ? 1 : 2; + MouseButton TranslateButton(VncButton vncButton) => + vncButton switch + { + VncButton.Left => MouseButton.Left, + VncButton.Middle => MouseButton.Middle, + VncButton.Right => MouseButton.Right, + _ => MouseButton.None + }; var modifiers = (RawInputModifiers)(((int)buttons & 7) << 4); @@ -58,34 +64,25 @@ namespace Avalonia.Headless.Vnc private static VncButton[] CheckedButtons = new[] {VncButton.Left, VncButton.Middle, VncButton.Right}; - public VncFramebuffer Capture() + public unsafe VncFramebuffer Capture() { lock (_lock) { using (var bmpRef = Window.GetLastRenderedFrame()) { - if (bmpRef?.Item == null) + if (bmpRef == null) return _framebuffer; - var bmp = bmpRef.Item; + var bmp = bmpRef; if (bmp.PixelSize.Width != _framebuffer.Width || bmp.PixelSize.Height != _framebuffer.Height) { _framebuffer = new VncFramebuffer("Avalonia", bmp.PixelSize.Width, bmp.PixelSize.Height, VncPixelFormat.RGB32); } - using (var fb = bmp.Lock()) + var buffer = _framebuffer.GetBuffer(); + fixed (byte* bufferPtr = buffer) { - var buf = _framebuffer.GetBuffer(); - if (_framebuffer.Stride == fb.RowBytes) - Marshal.Copy(fb.Address, buf, 0, buf.Length); - else - for (var y = 0; y < fb.Size.Height; y++) - { - var sourceStart = fb.RowBytes * y; - var dstStart = _framebuffer.Stride * y; - var row = fb.Size.Width * 4; - Marshal.Copy(new IntPtr(sourceStart + fb.Address.ToInt64()), buf, dstStart, row); - } + bmp.CopyPixels(new PixelRect(default, bmp.PixelSize), (IntPtr)bufferPtr, buffer.Length, _framebuffer.Stride); } } } From f61f8f2bb65aad48761f47c825d490a41def4bbb Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 25 Feb 2023 01:25:24 -0500 Subject: [PATCH 008/110] Minor changes, missing documentation --- .../HeadlessVncFramebufferSource.cs | 3 +- .../AvaloniaTestFrameworkAttribute.cs | 2 +- .../Avalonia.Headless.csproj | 4 ++ .../AvaloniaHeadlessPlatform.cs | 6 ++- .../HeadlessWindowExtensions.cs | 46 +++++++++++++++++-- .../RenderingTests.cs | 2 - 6 files changed, 54 insertions(+), 9 deletions(-) diff --git a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs index 7389a9ba15..85caeb1ac5 100644 --- a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs +++ b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs @@ -2,6 +2,7 @@ using System.Runtime.InteropServices; using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Platform; using Avalonia.Threading; using RemoteViewing.Vnc; using RemoteViewing.Vnc.Server; @@ -68,7 +69,7 @@ namespace Avalonia.Headless.Vnc { lock (_lock) { - using (var bmpRef = Window.GetLastRenderedFrame()) + using (var bmpRef = Window.CaptureRenderedFrame()) { if (bmpRef == null) return _framebuffer; diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs index d0249a5b25..3eace30805 100644 --- a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs @@ -5,7 +5,7 @@ using Xunit.Sdk; namespace Avalonia.Headless.XUnit; /// -/// +/// Sets up global avalonia test framework using avalonia application builder passed as a parameter. /// [TestFrameworkDiscoverer("Avalonia.Headless.XUnit.AvaloniaTestFrameworkTypeDiscoverer", "Avalonia.Headless.XUnit")] [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)] diff --git a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj index fbfa89f5a4..b626eaeb68 100644 --- a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj +++ b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index 6619b533f6..882f8b842d 100644 --- a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -72,7 +72,11 @@ namespace Avalonia.Headless Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService(), null); } - + /// + /// Forces renderer to process a rendering timer tick. + /// Use this method before calling . + /// + /// Count of frames to be ticked on the timer. public static void ForceRenderTimerTick(int count = 1) { var timer = AvaloniaLocator.Current.GetService() as RenderTimer; diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs index ad5b7e680c..5487a7943a 100644 --- a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs @@ -3,40 +3,78 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Media.Imaging; +using Avalonia.Platform; using Avalonia.Threading; namespace Avalonia.Headless; public static class HeadlessWindowExtensions { + /// + /// Triggers a renderer timer tick and captures last rendered frame. + /// + /// Bitmap with last rendered frame. Null, if nothing was rendered. public static Bitmap? CaptureRenderedFrame(this TopLevel topLevel) { - var impl = GetImpl(topLevel); + Dispatcher.UIThread.RunJobs(); AvaloniaHeadlessPlatform.ForceRenderTimerTick(); - return impl.GetLastRenderedFrame(); + return topLevel.GetLastRenderedFrame(); } - public static Bitmap? GetLastRenderedFrame(this TopLevel topLevel) => - GetImpl(topLevel).GetLastRenderedFrame(); + /// + /// Reads last rendered frame. + /// Note, in order to trigger rendering timer, call method. + /// + /// Bitmap with last rendered frame. Null, if nothing was rendered. + public static Bitmap? GetLastRenderedFrame(this TopLevel topLevel) + { + if (AvaloniaLocator.Current.GetService() is HeadlessPlatformRenderInterface) + { + throw new NotSupportedException("To capture a rendered frame, make sure that headless application was initialized with '.UseSkia()' and disabled 'UseHeadlessDrawing' in the 'AvaloniaHeadlessPlatformOptions'."); + } + + return GetImpl(topLevel).GetLastRenderedFrame(); + } + /// + /// Simulates keyboard press on the headless window/toplevel. + /// public static void KeyPress(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => GetImpl(topLevel).KeyPress(key, modifiers); + /// + /// Simulates keyboard release on the headless window/toplevel. + /// public static void KeyRelease(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => GetImpl(topLevel).KeyRelease(key, modifiers); + /// + /// Simulates mouse down on the headless window/toplevel. + /// public static void MouseDown(this TopLevel topLevel, Point point, MouseButton button, RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseDown(point, button, modifiers); + /// + /// Simulates mouse move on the headless window/toplevel. + /// public static void MouseMove(this TopLevel topLevel, Point point, RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseMove(point, modifiers); + /// + /// Simulates mouse up on the headless window/toplevel. + /// public static void MouseUp(this TopLevel topLevel, Point point, MouseButton button, RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseUp(point, button, modifiers); + /// + /// Simulates mouse wheel on the headless window/toplevel. + /// public static void MouseWheel(this TopLevel topLevel, Point point, Vector delta, RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseWheel(point, delta, modifiers); + /// + /// Simulates drag'n'drop target on the headless window/toplevel. + /// public static void DragDrop(this TopLevel topLevel, Point point, RawDragEventType type, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).DragDrop(point, type, data, effects, modifiers); diff --git a/tests/Avalonia.Headless.UnitTests/RenderingTests.cs b/tests/Avalonia.Headless.UnitTests/RenderingTests.cs index 33f15bce1a..bc50686235 100644 --- a/tests/Avalonia.Headless.UnitTests/RenderingTests.cs +++ b/tests/Avalonia.Headless.UnitTests/RenderingTests.cs @@ -27,8 +27,6 @@ public class RenderingTests }; window.Show(); - Dispatcher.UIThread.RunJobs(); - AvaloniaHeadlessPlatform.ForceRenderTimerTick(); var frame = window.CaptureRenderedFrame(); Assert.NotNull(frame); } From c691972f44753cd9b13c0d6f4677a0189c6947cc Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 25 Feb 2023 01:35:29 -0500 Subject: [PATCH 009/110] Add missing InternalsVisibleTo as ScreenHelper was moved --- src/Avalonia.Controls/Avalonia.Controls.csproj | 4 ++++ src/Avalonia.Native/Avalonia.Native.csproj | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index b9e2d3d259..9c4bacbedf 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -18,5 +18,9 @@ + + + + diff --git a/src/Avalonia.Native/Avalonia.Native.csproj b/src/Avalonia.Native/Avalonia.Native.csproj index 095662a538..e69c39a41e 100644 --- a/src/Avalonia.Native/Avalonia.Native.csproj +++ b/src/Avalonia.Native/Avalonia.Native.csproj @@ -26,8 +26,4 @@ - - - - From 46c5db37258d0b1f4cdfc29f18efbf391553ae50 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 25 Feb 2023 01:53:49 -0500 Subject: [PATCH 010/110] Fixes after VNC check --- .../Properties/launchSettings.json | 6 +++- .../HeadlessVncFramebufferSource.cs | 2 +- .../HeadlessWindowExtensions.cs | 35 +++++++++++++------ 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/samples/ControlCatalog.NetCore/Properties/launchSettings.json b/samples/ControlCatalog.NetCore/Properties/launchSettings.json index 5964ca320e..11feb94bb3 100644 --- a/samples/ControlCatalog.NetCore/Properties/launchSettings.json +++ b/samples/ControlCatalog.NetCore/Properties/launchSettings.json @@ -6,6 +6,10 @@ "Dxgi": { "commandName": "Project", "commandLineArgs": "--dxgi" + }, + "VNC": { + "commandName": "Project", + "commandLineArgs": "--vnc" } } -} \ No newline at end of file +} diff --git a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs index 85caeb1ac5..24703003da 100644 --- a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs +++ b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs @@ -69,7 +69,7 @@ namespace Avalonia.Headless.Vnc { lock (_lock) { - using (var bmpRef = Window.CaptureRenderedFrame()) + using (var bmpRef = Window.GetLastRenderedFrame()) { if (bmpRef == null) return _framebuffer; diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs index 5487a7943a..8fbc5ec6ef 100644 --- a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs @@ -8,6 +8,9 @@ using Avalonia.Threading; namespace Avalonia.Headless; +/// +/// Set of extension methods to simplify usage of Avalonia.Headless platform. +/// public static class HeadlessWindowExtensions { /// @@ -20,7 +23,7 @@ public static class HeadlessWindowExtensions AvaloniaHeadlessPlatform.ForceRenderTimerTick(); return topLevel.GetLastRenderedFrame(); } - + /// /// Reads last rendered frame. /// Note, in order to trigger rendering timer, call method. @@ -30,7 +33,8 @@ public static class HeadlessWindowExtensions { if (AvaloniaLocator.Current.GetService() is HeadlessPlatformRenderInterface) { - throw new NotSupportedException("To capture a rendered frame, make sure that headless application was initialized with '.UseSkia()' and disabled 'UseHeadlessDrawing' in the 'AvaloniaHeadlessPlatformOptions'."); + throw new NotSupportedException( + "To capture a rendered frame, make sure that headless application was initialized with '.UseSkia()' and disabled 'UseHeadlessDrawing' in the 'AvaloniaHeadlessPlatformOptions'."); } return GetImpl(topLevel).GetLastRenderedFrame(); @@ -40,49 +44,58 @@ public static class HeadlessWindowExtensions /// Simulates keyboard press on the headless window/toplevel. /// public static void KeyPress(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => - GetImpl(topLevel).KeyPress(key, modifiers); + RunJobsAndGetImpl(topLevel).KeyPress(key, modifiers); /// /// Simulates keyboard release on the headless window/toplevel. /// public static void KeyRelease(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => - GetImpl(topLevel).KeyRelease(key, modifiers); + RunJobsAndGetImpl(topLevel).KeyRelease(key, modifiers); /// /// Simulates mouse down on the headless window/toplevel. /// public static void MouseDown(this TopLevel topLevel, Point point, MouseButton button, - RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseDown(point, button, modifiers); + RawInputModifiers modifiers = RawInputModifiers.None) => + RunJobsAndGetImpl(topLevel).MouseDown(point, button, modifiers); /// /// Simulates mouse move on the headless window/toplevel. /// public static void MouseMove(this TopLevel topLevel, Point point, - RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseMove(point, modifiers); + RawInputModifiers modifiers = RawInputModifiers.None) => + RunJobsAndGetImpl(topLevel).MouseMove(point, modifiers); /// /// Simulates mouse up on the headless window/toplevel. /// public static void MouseUp(this TopLevel topLevel, Point point, MouseButton button, - RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseUp(point, button, modifiers); + RawInputModifiers modifiers = RawInputModifiers.None) => + RunJobsAndGetImpl(topLevel).MouseUp(point, button, modifiers); /// /// Simulates mouse wheel on the headless window/toplevel. /// public static void MouseWheel(this TopLevel topLevel, Point point, Vector delta, - RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseWheel(point, delta, modifiers); + RawInputModifiers modifiers = RawInputModifiers.None) => + RunJobsAndGetImpl(topLevel).MouseWheel(point, delta, modifiers); /// /// Simulates drag'n'drop target on the headless window/toplevel. /// public static void DragDrop(this TopLevel topLevel, Point point, RawDragEventType type, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None) => - GetImpl(topLevel).DragDrop(point, type, data, effects, modifiers); + RunJobsAndGetImpl(topLevel).DragDrop(point, type, data, effects, modifiers); - private static IHeadlessWindow GetImpl(this TopLevel topLevel) + private static IHeadlessWindow RunJobsAndGetImpl(this TopLevel topLevel) { Dispatcher.UIThread.RunJobs(); + return GetImpl(topLevel); + } + + private static IHeadlessWindow GetImpl(this TopLevel topLevel) + { return topLevel.PlatformImpl as IHeadlessWindow ?? - throw new InvalidOperationException("TopLevel must be a headless window."); + throw new InvalidOperationException("TopLevel must be a headless window."); } } From 4d415d544912fb5e1f7e61b801e2588f9956bac0 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 25 Feb 2023 02:23:03 -0500 Subject: [PATCH 011/110] Use Cancel instead of Dispose in CTS --- src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs index 579232521d..42604adf46 100644 --- a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs @@ -24,7 +24,7 @@ internal class AvaloniaTestRunner : XunitTestAssemblyRunner public override void Dispose() { - _cancellationTokenSource?.Dispose(); + _cancellationTokenSource?.Cancel(); base.Dispose(); } From dbc3c802a32e632263270cacdcc5bcf572cd5f0a Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 17 Apr 2023 01:29:06 -0400 Subject: [PATCH 012/110] Add Analyzers project to the main package --- Avalonia.sln | 2 +- build/DevAnalyzers.props | 2 +- packages/Avalonia/Avalonia.csproj | 4 ++ .../Avalonia.Analyzers.csproj | 2 +- ...valoniaPropertyAnalyzer.CompileAnalyzer.cs | 0 .../AvaloniaPropertyAnalyzer.cs | 1 - .../Avalonia.Analyzers/GlobalSuppressions.cs | 8 +++ .../OnPropertyChangedOverrideAnalyzer.cs | 59 +++++++++++++++++++ 8 files changed, 74 insertions(+), 4 deletions(-) rename src/tools/{PublicAnalyzers => Avalonia.Analyzers}/Avalonia.Analyzers.csproj (97%) rename src/tools/{PublicAnalyzers => Avalonia.Analyzers}/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs (100%) rename src/tools/{PublicAnalyzers => Avalonia.Analyzers}/AvaloniaPropertyAnalyzer.cs (99%) create mode 100644 src/tools/Avalonia.Analyzers/GlobalSuppressions.cs create mode 100644 src/tools/Avalonia.Analyzers/OnPropertyChangedOverrideAnalyzer.cs diff --git a/Avalonia.sln b/Avalonia.sln index f33b782479..acbf1e6bbb 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -233,7 +233,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\R EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Analyzers", "src\tools\PublicAnalyzers\Avalonia.Analyzers.csproj", "{C692FE73-43DB-49CE-87FC-F03ED61F25C9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Analyzers", "src\tools\Avalonia.Analyzers\Avalonia.Analyzers.csproj", "{C692FE73-43DB-49CE-87FC-F03ED61F25C9}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{176582E8-46AF-416A-85C1-13A5C6744497}" ProjectSection(SolutionItems) = preProject diff --git a/build/DevAnalyzers.props b/build/DevAnalyzers.props index 7d021d051f..dffd3098c3 100644 --- a/build/DevAnalyzers.props +++ b/build/DevAnalyzers.props @@ -5,7 +5,7 @@ ReferenceOutputAssembly="false" OutputItemType="Analyzer" SetTargetFramework="TargetFramework=netstandard2.0"/> - + diff --git a/src/tools/PublicAnalyzers/Avalonia.Analyzers.csproj b/src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj similarity index 97% rename from src/tools/PublicAnalyzers/Avalonia.Analyzers.csproj rename to src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj index 31b8d08541..39eaab1289 100644 --- a/src/tools/PublicAnalyzers/Avalonia.Analyzers.csproj +++ b/src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj @@ -11,7 +11,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs b/src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs similarity index 100% rename from src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs rename to src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs b/src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.cs similarity index 99% rename from src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs rename to src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.cs index d1d9071d17..d1ffd82f99 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs +++ b/src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.cs @@ -14,7 +14,6 @@ using Microsoft.CodeAnalysis.Operations; namespace Avalonia.Analyzers; [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] -[SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking")] public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer { private const string Category = "AvaloniaProperty"; diff --git a/src/tools/Avalonia.Analyzers/GlobalSuppressions.cs b/src/tools/Avalonia.Analyzers/GlobalSuppressions.cs new file mode 100644 index 0000000000..9428b904b8 --- /dev/null +++ b/src/tools/Avalonia.Analyzers/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking")] diff --git a/src/tools/Avalonia.Analyzers/OnPropertyChangedOverrideAnalyzer.cs b/src/tools/Avalonia.Analyzers/OnPropertyChangedOverrideAnalyzer.cs new file mode 100644 index 0000000000..6fbfe28bd8 --- /dev/null +++ b/src/tools/Avalonia.Analyzers/OnPropertyChangedOverrideAnalyzer.cs @@ -0,0 +1,59 @@ +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Avalonia.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class OnPropertyChangedOverrideAnalyzer : DiagnosticAnalyzer +{ + public const string DiagnosticId = "AVA2001"; + + private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( + DiagnosticId, + "Missing invoke base.OnPropertyChanged", + "Method '{0}' do not invoke base.{0}", + "Potential issue", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "The OnPropertyChanged of the base class was not invoked in the override method declaration, which could lead to unwanted behavior."); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeMethod, SyntaxKind.MethodDeclaration); + } + + private static void AnalyzeMethod(SyntaxNodeAnalysisContext context) + { + var method = (MethodDeclarationSyntax)context.Node; + if (context.SemanticModel.GetDeclaredSymbol(method, context.CancellationToken) is IMethodSymbol currentMethod + && currentMethod.Name == "OnPropertyChanged" + && currentMethod.OverriddenMethod is IMethodSymbol originalMethod) + { + var baseInvocations = method.Body?.DescendantNodes().OfType(); + if (baseInvocations?.Any() == true) + { + foreach (var baseInvocation in baseInvocations) + { + if (baseInvocation.Parent is SyntaxNode parent) + { + var targetSymbol = context.SemanticModel.GetSymbolInfo(parent, context.CancellationToken); + if (SymbolEqualityComparer.Default.Equals(targetSymbol.Symbol, originalMethod)) + { + return; + } + } + } + } + context.ReportDiagnostic(Diagnostic.Create(Rule, currentMethod.Locations[0], currentMethod.Name)); + } + } + +} From 4dbd7cb2a0e0878367ecf7dc5a38609f0b2066d4 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 17 Apr 2023 01:36:30 -0400 Subject: [PATCH 013/110] Disable most of analyzers --- .../AvaloniaPropertyAnalyzer.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.cs b/src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.cs index d1ffd82f99..9eb4cecc8b 100644 --- a/src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.cs +++ b/src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.cs @@ -3,7 +3,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.Serialization; using Microsoft.CodeAnalysis; @@ -67,7 +66,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer "Type mismatch: AvaloniaProperty owner is {0}, which is not the containing type", Category, DiagnosticSeverity.Warning, - isEnabledByDefault: true, + isEnabledByDefault: false, // TODO: autogenerate property metadata preserved in ref assembly "The owner of an AvaloniaProperty should generally be the containing type. This ensures that the property can be used as expected in XAML.", TypeMismatchTag); @@ -77,7 +76,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer "Unexpected property use: {0} is neither owned by nor attached to {1}", Category, DiagnosticSeverity.Warning, - isEnabledByDefault: true, + isEnabledByDefault: false, // TODO: autogenerate property metadata preserved in ref assembly "It is possible to use any AvaloniaProperty with any AvaloniaObject. However, each AvaloniaProperty an object uses on itself should be either owned by that object, or attached to that object.", InappropriateReadWriteTag); @@ -87,7 +86,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer "Inappropriate assignment: An AvaloniaObject should use SetCurrentValue when setting its own StyledProperty or AttachedProperty values", Category, DiagnosticSeverity.Warning, - isEnabledByDefault: true, + isEnabledByDefault: false, // TODO: autogenerate property metadata preserved in ref assembly "The standard means of setting an AvaloniaProperty is to call the SetValue method (often via a CLR property setter). This will forcibly overwrite values from sources like styles and templates, " + "which is something that should only be done by consumers of the control, not the control itself. Controls which want to set their own values should instead call the SetCurrentValue method, or " + "refactor the property into a DirectProperty. An assignment is exempt from this diagnostic in two scenarios: when it is forwarding a constructor parameter, and when the target object is derived " + @@ -100,7 +99,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer "Superfluous owner: {0} is already an owner of {1} via {2}", Category, DiagnosticSeverity.Warning, - isEnabledByDefault: true, + isEnabledByDefault: false, // TODO: autogenerate property metadata preserved in ref assembly "Ownership of an AvaloniaProperty is inherited along the type hierarchy. There is no need for a derived type to assert ownership over a base type's properties. This diagnostic can be a symptom of an incorrect property owner elsewhere.", InappropriateReadWriteTag); @@ -110,7 +109,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer "Name collision: {0} has the same name as {1}", Category, DiagnosticSeverity.Warning, - isEnabledByDefault: true, + isEnabledByDefault: false, // TODO: autogenerate property metadata preserved in ref assembly "Querying for an AvaloniaProperty by name requires that each property associated with a type have a unique name.", NameCollisionTag); @@ -120,7 +119,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer "Name collision: {0} owns multiple Avalonia properties with the name '{1}' {2}", Category, DiagnosticSeverity.Warning, - isEnabledByDefault: true, + isEnabledByDefault: false, // TODO: autogenerate property metadata preserved in ref assembly "It is unclear which AvaloniaProperty this CLR property refers to. Ensure that each AvaloniaProperty associated with a type has a unique name. If you need to change behaviour of a base property in your class, call its OverrideMetadata or OverrideDefaultValue methods.", NameCollisionTag); @@ -170,7 +169,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer "Type mismatch: CLR property type differs from the value type of {0} {1}", Category, DiagnosticSeverity.Warning, - isEnabledByDefault: true, + isEnabledByDefault: false, // TODO: autogenerate property metadata preserved in ref assembly "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call CLR properties on the owning type. A CLR property changing the value type (even when an implicit cast is possible) is ineffective and can lead to InvalidCastException to be thrown.", TypeMismatchTag, AssociatedClrPropertyTag); From ead92a6122a4617b19b40356fd537b50d91fa449 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 17 Apr 2023 01:37:47 -0400 Subject: [PATCH 014/110] Fix SetCurrentValue warnings --- src/Avalonia.Base/Media/DashStyle.cs | 2 +- src/Avalonia.Base/Media/GradientBrush.cs | 2 +- src/Avalonia.Base/Media/PolyLineSegment.cs | 2 +- src/Avalonia.Base/Media/TransformGroup.cs | 9 +++++---- src/Avalonia.Controls/Calendar/CalendarButton.cs | 2 +- src/Avalonia.Controls/Calendar/CalendarDayButton.cs | 2 +- src/Avalonia.Controls/ColumnDefinition.cs | 2 +- src/Avalonia.Controls/Documents/Span.cs | 4 ++-- src/Avalonia.Controls/GridSplitter.cs | 2 +- src/Avalonia.Controls/LayoutTransformControl.cs | 4 ++-- src/Avalonia.Controls/NativeMenuItemSeparator.cs | 2 +- src/Avalonia.Controls/Primitives/OverlayPopupHost.cs | 2 +- .../Primitives/SelectingItemsControl.cs | 8 ++++---- src/Avalonia.Controls/Primitives/UniformGrid.cs | 2 +- .../PullToRefresh/RefreshVisualizer.cs | 2 +- src/Avalonia.Controls/RowDefinition.cs | 4 ++-- src/Avalonia.Controls/TabControl.cs | 11 ++++++----- src/Avalonia.Controls/TabItem.cs | 8 ++++---- src/Avalonia.Controls/ToggleSwitch.cs | 2 +- .../Diagnostics/Controls/Application.cs | 4 ++-- 20 files changed, 39 insertions(+), 37 deletions(-) diff --git a/src/Avalonia.Base/Media/DashStyle.cs b/src/Avalonia.Base/Media/DashStyle.cs index 4749bfa401..338737d2ab 100644 --- a/src/Avalonia.Base/Media/DashStyle.cs +++ b/src/Avalonia.Base/Media/DashStyle.cs @@ -46,7 +46,7 @@ namespace Avalonia.Media /// The dash sequence offset. public DashStyle(IEnumerable? dashes, double offset) { - Dashes = (dashes as AvaloniaList) ?? new AvaloniaList(dashes ?? Array.Empty()); + SetCurrentValue(DashesProperty, (dashes as AvaloniaList) ?? new AvaloniaList(dashes ?? Array.Empty())); Offset = offset; } diff --git a/src/Avalonia.Base/Media/GradientBrush.cs b/src/Avalonia.Base/Media/GradientBrush.cs index e1654a01b2..0ec753201a 100644 --- a/src/Avalonia.Base/Media/GradientBrush.cs +++ b/src/Avalonia.Base/Media/GradientBrush.cs @@ -40,7 +40,7 @@ namespace Avalonia.Media /// public GradientBrush() { - this.GradientStops = new GradientStops(); + SetCurrentValue(GradientStopsProperty, new GradientStops()); } /// diff --git a/src/Avalonia.Base/Media/PolyLineSegment.cs b/src/Avalonia.Base/Media/PolyLineSegment.cs index 5c48c11e19..7de5e1afa5 100644 --- a/src/Avalonia.Base/Media/PolyLineSegment.cs +++ b/src/Avalonia.Base/Media/PolyLineSegment.cs @@ -30,7 +30,7 @@ namespace Avalonia.Media /// public PolyLineSegment() { - Points = new Points(); + SetCurrentValue(PointsProperty, new Points()); } /// diff --git a/src/Avalonia.Base/Media/TransformGroup.cs b/src/Avalonia.Base/Media/TransformGroup.cs index 0465efd5a5..c048234f8c 100644 --- a/src/Avalonia.Base/Media/TransformGroup.cs +++ b/src/Avalonia.Base/Media/TransformGroup.cs @@ -13,15 +13,16 @@ namespace Avalonia.Media public TransformGroup() { - Children = new Transforms(); - Children.ResetBehavior = ResetBehavior.Remove; - Children.CollectionChanged += delegate + var children = new Transforms(); + children.ResetBehavior = ResetBehavior.Remove; + children.CollectionChanged += delegate { - Children.ForEachItem( + children.ForEachItem( (tr) => tr.Changed += ChildTransform_Changed, (tr) => tr.Changed -= ChildTransform_Changed, () => { }); }; + SetCurrentValue(ChildrenProperty, children); } private void ChildTransform_Changed(object? sender, System.EventArgs e) diff --git a/src/Avalonia.Controls/Calendar/CalendarButton.cs b/src/Avalonia.Controls/Calendar/CalendarButton.cs index d8672cbf18..b35298b101 100644 --- a/src/Avalonia.Controls/Calendar/CalendarButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarButton.cs @@ -39,7 +39,7 @@ namespace Avalonia.Controls.Primitives public CalendarButton() : base() { - Content = DateTimeHelper.GetCurrentDateFormat().AbbreviatedMonthNames[0]; + SetCurrentValue(ContentProperty, DateTimeHelper.GetCurrentDateFormat().AbbreviatedMonthNames[0]); } /// diff --git a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs index 3d0befdba7..ea3ca8e8fc 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs @@ -34,7 +34,7 @@ namespace Avalonia.Controls.Primitives : base() { //Focusable = false; - Content = DefaultContent.ToString(CultureInfo.CurrentCulture); + SetCurrentValue(ContentProperty, DefaultContent.ToString(CultureInfo.CurrentCulture)); } /// diff --git a/src/Avalonia.Controls/ColumnDefinition.cs b/src/Avalonia.Controls/ColumnDefinition.cs index 2eb3ae3010..b28faba863 100644 --- a/src/Avalonia.Controls/ColumnDefinition.cs +++ b/src/Avalonia.Controls/ColumnDefinition.cs @@ -46,8 +46,8 @@ namespace Avalonia.Controls /// The width of the column. /// The width unit of the column. public ColumnDefinition(double value, GridUnitType type) + : this(new GridLength(value, type)) { - Width = new GridLength(value, type); } /// diff --git a/src/Avalonia.Controls/Documents/Span.cs b/src/Avalonia.Controls/Documents/Span.cs index d3565cbdd5..a4f3264c4d 100644 --- a/src/Avalonia.Controls/Documents/Span.cs +++ b/src/Avalonia.Controls/Documents/Span.cs @@ -20,10 +20,10 @@ namespace Avalonia.Controls.Documents public Span() { - Inlines = new InlineCollection + SetCurrentValue(InlinesProperty, new InlineCollection { LogicalChildren = LogicalChildren - }; + }); } /// diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 4684304725..50d1004f34 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -707,7 +707,7 @@ namespace Avalonia.Controls RenderTransform = _translation }; - Child = _decorator; + SetCurrentValue(ChildProperty, _decorator); } /// diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index f747e278f0..02d2eeef5b 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -63,7 +63,7 @@ namespace Avalonia.Controls { if (TransformRoot == null || LayoutTransform == null) { - LayoutTransform = RenderTransform; + SetCurrentValue(LayoutTransformProperty, RenderTransform); return base.ArrangeOverride(finalSize); } @@ -176,7 +176,7 @@ namespace Avalonia.Controls else { _renderTransformChangedEvent?.Dispose(); - LayoutTransform = null; + SetCurrentValue(LayoutTransformProperty, null); } } } diff --git a/src/Avalonia.Controls/NativeMenuItemSeparator.cs b/src/Avalonia.Controls/NativeMenuItemSeparator.cs index f55d714884..55b3fd08de 100644 --- a/src/Avalonia.Controls/NativeMenuItemSeparator.cs +++ b/src/Avalonia.Controls/NativeMenuItemSeparator.cs @@ -4,7 +4,7 @@ { public NativeMenuItemSeparator() { - Header = "-"; + SetCurrentValue(HeaderProperty, "-"); } } } diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs index 7ed055f2e5..0acc488885 100644 --- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs @@ -31,7 +31,7 @@ namespace Avalonia.Controls.Primitives /// public void SetChild(Control? control) { - Content = control; + SetCurrentValue(ContentProperty, control); } /// diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 663a315732..ac3e87c03d 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -701,7 +701,7 @@ namespace Avalonia.Controls.Primitives if (value is null) { // Clearing SelectedValueBinding makes the SelectedValue the item itself - SelectedValue = SelectedItem; + SetCurrentValue(SelectedValueProperty, SelectedItem); return; } @@ -721,7 +721,7 @@ namespace Avalonia.Controls.Primitives } // Re-evaluate SelectedValue with the new binding - SelectedValue = _bindingHelper.Evaluate(selectedItem); + SetCurrentValue(SelectedValueProperty, _bindingHelper.Evaluate(selectedItem)); } finally { @@ -1092,7 +1092,7 @@ namespace Avalonia.Controls.Primitives try { _isSelectionChangeActive = true; - SelectedValue = item; + SetCurrentValue(SelectedValueProperty, item); } finally { @@ -1106,7 +1106,7 @@ namespace Avalonia.Controls.Primitives try { _isSelectionChangeActive = true; - SelectedValue = _bindingHelper.Evaluate(item); + SetCurrentValue(SelectedValueProperty, _bindingHelper.Evaluate(item)); } finally { diff --git a/src/Avalonia.Controls/Primitives/UniformGrid.cs b/src/Avalonia.Controls/Primitives/UniformGrid.cs index 09554412db..fea35d867a 100644 --- a/src/Avalonia.Controls/Primitives/UniformGrid.cs +++ b/src/Avalonia.Controls/Primitives/UniformGrid.cs @@ -123,7 +123,7 @@ namespace Avalonia.Controls.Primitives if (FirstColumn >= Columns) { - FirstColumn = 0; + SetCurrentValue(FirstColumnProperty, 0); } var itemCount = FirstColumn; diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs index 8dc19eb1d4..e110396d95 100644 --- a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs @@ -192,7 +192,7 @@ namespace Avalonia.Controls UpdateContent(); }; - Content = _content; + SetCurrentValue(ContentProperty, _content); } else { diff --git a/src/Avalonia.Controls/RowDefinition.cs b/src/Avalonia.Controls/RowDefinition.cs index fac795035b..8aaaff8cc9 100644 --- a/src/Avalonia.Controls/RowDefinition.cs +++ b/src/Avalonia.Controls/RowDefinition.cs @@ -46,8 +46,8 @@ namespace Avalonia.Controls /// The height of the row. /// The height unit of the column. public RowDefinition(double value, GridUnitType type) + : this(new GridLength(value, type)) { - Height = new GridLength(value, type); } /// @@ -56,7 +56,7 @@ namespace Avalonia.Controls /// The height of the column. public RowDefinition(GridLength height) { - Height = height; + SetCurrentValue(HeightProperty, height); } /// diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 74cf54beb8..502e128641 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -162,8 +162,8 @@ namespace Avalonia.Controls if (index == SelectedIndex && element is ContentControl container) { - SelectedContentTemplate = container.ContentTemplate; - SelectedContent = container.Content; + SetCurrentValue(SelectedContentTemplateProperty, container.ContentTemplate); + SetCurrentValue(SelectedContentProperty, container.Content); } } @@ -187,14 +187,15 @@ namespace Avalonia.Controls { if (SelectedIndex == -1) { - SelectedContent = SelectedContentTemplate = null; + SetCurrentValue(SelectedContentProperty, null); + SetCurrentValue(SelectedContentTemplateProperty, null); } else { var container = SelectedItem as IContentControl ?? ContainerFromIndex(SelectedIndex) as IContentControl; - SelectedContentTemplate = container?.ContentTemplate; - SelectedContent = container?.Content; + SetCurrentValue(SelectedContentTemplateProperty, container?.ContentTemplate); + SetCurrentValue(SelectedContentProperty, container?.Content); } } diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index 46265fb5bc..8303153a4b 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -66,14 +66,14 @@ namespace Avalonia.Controls { if (Header != headered.Header) { - Header = headered.Header; + SetCurrentValue(HeaderProperty, headered.Header); } } else { if (!(obj.NewValue is Control)) { - Header = obj.NewValue; + SetCurrentValue(HeaderProperty, obj.NewValue); } } } @@ -81,9 +81,9 @@ namespace Avalonia.Controls { if (Header == obj.OldValue) { - Header = obj.NewValue; + SetCurrentValue(HeaderProperty, obj.NewValue); } - } + } } } } diff --git a/src/Avalonia.Controls/ToggleSwitch.cs b/src/Avalonia.Controls/ToggleSwitch.cs index a28e9791f6..108deba257 100644 --- a/src/Avalonia.Controls/ToggleSwitch.cs +++ b/src/Avalonia.Controls/ToggleSwitch.cs @@ -201,7 +201,7 @@ namespace Avalonia.Controls } else { - IsChecked = shouldBecomeChecked; + SetCurrentValue(IsCheckedProperty, shouldBecomeChecked); } } else diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs index 5bc9909ef3..fb0504ba1e 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs @@ -37,7 +37,7 @@ namespace Avalonia.Diagnostics.Controls _ => null }; - RequestedThemeVariant = application.RequestedThemeVariant; + SetCurrentValue(RequestedThemeVariantProperty, application.RequestedThemeVariant); _application.PropertyChanged += ApplicationOnPropertyChanged; } @@ -131,7 +131,7 @@ namespace Avalonia.Diagnostics.Controls { if (e.Property == Avalonia.Application.RequestedThemeVariantProperty) { - RequestedThemeVariant = e.GetNewValue(); + SetCurrentValue(RequestedThemeVariantProperty, e.GetNewValue()); } } From e909bdb005121766a253194663c0f3e5bd636d0d Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 17 Apr 2023 01:38:32 -0400 Subject: [PATCH 015/110] Fix some other AVP warnings --- .../Pages/CustomDrawingExampleControl.cs | 11 +++++------ src/Avalonia.Controls/DefinitionBase.cs | 12 ++---------- src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs | 2 +- src/Avalonia.Controls/MenuItem.cs | 6 ------ .../PullToRefresh/RefreshVisualizer.cs | 2 +- src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj | 1 + .../Avalonia.Themes.Fluent.csproj | 1 + .../Avalonia.Themes.Simple.csproj | 1 + 8 files changed, 12 insertions(+), 24 deletions(-) diff --git a/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs b/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs index 549cf3d740..782435ae06 100644 --- a/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs +++ b/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs @@ -19,18 +19,16 @@ namespace ControlCatalog.Pages public static readonly StyledProperty ScaleProperty = AvaloniaProperty.Register(nameof(Scale), 1.0d); public double Scale { get => GetValue(ScaleProperty); set => SetValue(ScaleProperty, value); } - public static readonly StyledProperty RotationProperty = AvaloniaProperty.Register(nameof(Rotation)); + public static readonly StyledProperty RotationProperty = AvaloniaProperty.Register(nameof(Rotation), + coerce: (_, val) => val % (Math.PI * 2)); + /// /// Rotation, measured in Radians! /// public double Rotation { get => GetValue(RotationProperty); - set - { - double valueToUse = value % (Math.PI * 2); - SetValue(RotationProperty, valueToUse); - } + set => SetValue(RotationProperty, value); } public static readonly StyledProperty ViewportCenterYProperty = AvaloniaProperty.Register(nameof(ViewportCenterY), 0.0d); @@ -213,5 +211,6 @@ namespace ControlCatalog.Pages return workingPoint; } + } } diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index eb587fb157..d0752b8aa6 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -37,7 +37,7 @@ namespace Avalonia.Controls { // start with getting SharedSizeGroup value. // this property is NOT inherited which should result in better overall perf. - if (SharedSizeGroup is { } sharedSizeGroupId && PrivateSharedSizeScope is { } privateSharedSizeScope) + if (SharedSizeGroup is { } sharedSizeGroupId && GetValue(PrivateSharedSizeScopeProperty) is { } privateSharedSizeScope) { _sharedState = privateSharedSizeScope.EnsureSharedState(sharedSizeGroupId); _sharedState.AddMember(this); @@ -333,7 +333,7 @@ namespace Avalonia.Controls if (definition._sharedState == null && sharedSizeGroupId != null - && definition.PrivateSharedSizeScope is { } privateSharedSizeScope) + && definition.GetValue(PrivateSharedSizeScopeProperty) is { } privateSharedSizeScope) { // if definition is not registered and both: shared size group id AND private shared scope // are available, then register definition. @@ -412,14 +412,6 @@ namespace Avalonia.Controls } } - /// - /// Private getter of shared state collection dynamic property. - /// - private SharedSizeScope? PrivateSharedSizeScope - { - get { return GetValue(PrivateSharedSizeScopeProperty); } - } - /// /// Convenience accessor to UseSharedMinimum flag /// diff --git a/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs index 9ed4737c7c..5b23b5030f 100644 --- a/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs @@ -44,7 +44,7 @@ namespace Avalonia.Controls.Primitives /// Defines the property /// public static readonly StyledProperty OverlayInputPassThroughElementProperty = - Popup.OverlayInputPassThroughElementProperty.AddOwner(); + Popup.OverlayInputPassThroughElementProperty.AddOwner(); private readonly Lazy _popupLazy; private Rect? _enlargedPopupRect; diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index a0dbf33a1d..72febcfedb 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -53,12 +53,6 @@ namespace Avalonia.Controls public static readonly StyledProperty InputGestureProperty = AvaloniaProperty.Register(nameof(InputGesture)); - /// - /// Defines the property. - /// - public static readonly StyledProperty IsSelectedProperty = - SelectingItemsControl.IsSelectedProperty.AddOwner(); - /// /// Defines the property. /// diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs index e110396d95..db83977bc6 100644 --- a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs @@ -99,7 +99,7 @@ namespace Avalonia.Controls } } - internal PullDirection PullDirection + public PullDirection PullDirection { get => GetValue(PullDirectionProperty); set => SetValue(PullDirectionProperty, value); diff --git a/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj b/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj index 22ee548823..4cae8e82df 100644 --- a/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj +++ b/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj @@ -12,4 +12,5 @@ + diff --git a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj index 8d266ce82f..e63a0c9c26 100644 --- a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj +++ b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj @@ -15,4 +15,5 @@ + diff --git a/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj b/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj index 4aa6b66743..03193599ed 100644 --- a/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj +++ b/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj @@ -15,4 +15,5 @@ + From a9688a83a1d7aa68068434a625078b114b5af998 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 17 Apr 2023 01:38:55 -0400 Subject: [PATCH 016/110] Suppress warnings that are expected behavior --- src/Avalonia.Base/ClassBindingManager.cs | 2 ++ src/Avalonia.Base/StyledElement.cs | 1 + src/Avalonia.Base/Styling/ThemeVariant.cs | 4 ++++ src/Avalonia.Base/Visual.cs | 1 + src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs | 2 ++ src/Avalonia.Controls/ContextMenu.cs | 2 ++ src/Avalonia.Controls/ListBox.cs | 8 ++++++++ src/Avalonia.Controls/Primitives/VisualLayerManager.cs | 3 +++ src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs | 1 + .../SplitView/SplitViewTemplateSettings.cs | 2 ++ src/Avalonia.Controls/TabControl.cs | 2 ++ src/Avalonia.Controls/TabItem.cs | 2 ++ src/Avalonia.Controls/TopLevel.cs | 4 ++-- src/Avalonia.ReactiveUI/ReactiveUserControl.cs | 1 + src/Avalonia.ReactiveUI/ReactiveWindow.cs | 1 + 15 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/ClassBindingManager.cs b/src/Avalonia.Base/ClassBindingManager.cs index a9726cb86e..55f3a7892a 100644 --- a/src/Avalonia.Base/ClassBindingManager.cs +++ b/src/Avalonia.Base/ClassBindingManager.cs @@ -17,6 +17,8 @@ namespace Avalonia return target.Bind(prop, source, anchor); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1001:The same AvaloniaProperty should not be registered twice", + Justification = "Classes.attr binding feature is implemented using intermediate avalonia properties for each class")] private static AvaloniaProperty RegisterClassProxyProperty(string className) { var prop = AvaloniaProperty.Register("__AvaloniaReserved::Classes::" + className); diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 5881efce1e..3270fe4614 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -289,6 +289,7 @@ namespace Avalonia public StyledElement? Parent { get; private set; } /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1030:StyledProperty accessors should not have side effects", Justification = "False positive?")] public ThemeVariant ActualThemeVariant => GetValue(ThemeVariant.ActualThemeVariantProperty); /// diff --git a/src/Avalonia.Base/Styling/ThemeVariant.cs b/src/Avalonia.Base/Styling/ThemeVariant.cs index 389136b0f5..23bc15dfa7 100644 --- a/src/Avalonia.Base/Styling/ThemeVariant.cs +++ b/src/Avalonia.Base/Styling/ThemeVariant.cs @@ -9,6 +9,10 @@ namespace Avalonia.Styling; /// Specifies a UI theme variant that should be used for the Control and Application types. /// [TypeConverter(typeof(ThemeVariantTypeConverter))] +[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1010:AvaloniaProperty objects should be owned by the type in which they are stored", + Justification = "ActualThemeVariant and RequestedThemeVariant properties are shared Avalonia.Base and Avalonia.Controls projects," + + "but shouldn't be visible on the StyledElement class." + + "Ideally we woould introduce readonly styled properties.")] public sealed record ThemeVariant { /// diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 79cc760fc6..8717b5340a 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -329,6 +329,7 @@ namespace Avalonia /// /// Gets the control's parent visual. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1032", Justification = "GetVisualParent extension method is supposed to be used instead.")] internal Visual? VisualParent => _visualParent; /// diff --git a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs index e10cc1d100..20711eecbc 100644 --- a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs @@ -2042,6 +2042,8 @@ namespace Avalonia.Controls /// /// Identifies the Value dependency property. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1002:AvaloniaProperty objects should not be owned by a generic type", + Justification = "This property is not supposed to be used from XAML.")] public static readonly StyledProperty ValueProperty = AvaloniaProperty.Register, T>(nameof(Value)); diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index fc4b11bd4a..63e28ea14d 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -56,6 +56,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1013", + Justification = "We keep PlacementModeProperty for backward compatibility.")] public static readonly StyledProperty PlacementProperty = Popup.PlacementProperty.AddOwner(); diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index e665c2db90..e5f0b50555 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -29,18 +29,24 @@ namespace Avalonia.Controls /// /// Defines the property. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1010", + Justification = "This property is owned by SelectingItemsControl, but protected there. ListBox changes its visibility.")] public static readonly new DirectProperty SelectedItemsProperty = SelectingItemsControl.SelectedItemsProperty; /// /// Defines the property. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1010", + Justification = "This property is owned by SelectingItemsControl, but protected there. ListBox changes its visibility.")] public static readonly new DirectProperty SelectionProperty = SelectingItemsControl.SelectionProperty; /// /// Defines the property. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1010", + Justification = "This property is owned by SelectingItemsControl, but protected there. ListBox changes its visibility.")] public static readonly new StyledProperty SelectionModeProperty = SelectingItemsControl.SelectionModeProperty; @@ -84,6 +90,8 @@ namespace Avalonia.Controls /// Note that the selection mode only applies to selections made via user interaction. /// Multiple selections can be made programmatically regardless of the value of this property. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1012", + Justification = "This property is owned by SelectingItemsControl, but protected there. ListBox changes its visibility.")] public new SelectionMode SelectionMode { get { return base.SelectionMode; } diff --git a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs index 351ed5d716..35676474dd 100644 --- a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs +++ b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs @@ -29,6 +29,9 @@ namespace Avalonia.Controls.Primitives } } + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1030")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1031", + Justification = "A hack to make ChromeOverlayLayer lazily creatable. It is expected that GetValue(ChromeOverlayLayerProperty) alone won't work.")] public ChromeOverlayLayer ChromeOverlayLayer { get diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs index db83977bc6..45227eef77 100644 --- a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs @@ -71,6 +71,7 @@ namespace Avalonia.Controls /// /// Gets or sets a value that indicates the refresh state of the visualizer. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1032", Justification = "False positive")] protected RefreshVisualizerState RefreshVisualizerState { get diff --git a/src/Avalonia.Controls/SplitView/SplitViewTemplateSettings.cs b/src/Avalonia.Controls/SplitView/SplitViewTemplateSettings.cs index f2cbf55986..8c59de7420 100644 --- a/src/Avalonia.Controls/SplitView/SplitViewTemplateSettings.cs +++ b/src/Avalonia.Controls/SplitView/SplitViewTemplateSettings.cs @@ -17,12 +17,14 @@ AvaloniaProperty.Register( nameof(PaneColumnGridLength)); + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1032", Justification = "This property is supposed to be a styled readonly property.")] public double ClosedPaneWidth { get => GetValue(ClosedPaneWidthProperty); internal set => SetValue(ClosedPaneWidthProperty, value); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1032", Justification = "This property is supposed to be a styled readonly property.")] public GridLength PaneColumnGridLength { get => GetValue(PaneColumnGridLengthProperty); diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 502e128641..2e27cfd123 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -115,6 +115,7 @@ namespace Avalonia.Controls /// /// The content of the selected tab. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1032", Justification = "This property is supposed to be a styled readonly property.")] public object? SelectedContent { get { return GetValue(SelectedContentProperty); } @@ -127,6 +128,7 @@ namespace Avalonia.Controls /// /// The content template of the selected tab. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1032", Justification = "This property is supposed to be a styled readonly property.")] public IDataTemplate? SelectedContentTemplate { get { return GetValue(SelectedContentTemplateProperty); } diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index 8303153a4b..60e9a7c88c 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -42,6 +42,8 @@ namespace Avalonia.Controls /// /// The tab strip placement. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1031", + Justification = "This property is supposed to be inherited only and settable on parent TabControl.")] public Dock TabStripPlacement { get { return GetValue(TabStripPlacementProperty); } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 07b1e9b51f..f86554ec4f 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -83,11 +83,11 @@ namespace Avalonia.Controls /// public static readonly StyledProperty ActualThemeVariantProperty = - ThemeVariantScope.ActualThemeVariantProperty.AddOwner(); + ThemeVariantScope.ActualThemeVariantProperty.AddOwner(); /// public static readonly StyledProperty RequestedThemeVariantProperty = - ThemeVariantScope.RequestedThemeVariantProperty.AddOwner(); + ThemeVariantScope.RequestedThemeVariantProperty.AddOwner(); /// /// Defines the SystemBarColor attached property. diff --git a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs index 21cdef2634..b0978dc3f6 100644 --- a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs +++ b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs @@ -17,6 +17,7 @@ namespace Avalonia.ReactiveUI /// ViewModel type. public class ReactiveUserControl : UserControl, IViewFor where TViewModel : class { + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1002", Justification = "Generic avalonia property is expected here.")] public static readonly StyledProperty ViewModelProperty = AvaloniaProperty .Register, TViewModel?>(nameof(ViewModel)); diff --git a/src/Avalonia.ReactiveUI/ReactiveWindow.cs b/src/Avalonia.ReactiveUI/ReactiveWindow.cs index 726fb3d661..14e9353096 100644 --- a/src/Avalonia.ReactiveUI/ReactiveWindow.cs +++ b/src/Avalonia.ReactiveUI/ReactiveWindow.cs @@ -17,6 +17,7 @@ namespace Avalonia.ReactiveUI /// ViewModel type. public class ReactiveWindow : Window, IViewFor where TViewModel : class { + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1002", Justification = "Generic avalonia property is expected here.")] public static readonly StyledProperty ViewModelProperty = AvaloniaProperty .Register, TViewModel?>(nameof(ViewModel)); From ad96c64aa26710a20eaafaeb06e62948b4b46ecf Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 17 Apr 2023 01:39:13 -0400 Subject: [PATCH 017/110] Mark Avalonia Analyzers as errors --- .editorconfig | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.editorconfig b/.editorconfig index a144ec8843..6337257e8f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -186,6 +186,23 @@ csharp_wrap_before_ternary_opsigns = false # Avalonia DevAnalyzer preferences dotnet_diagnostic.AVADEV2001.severity = error +# Avalonia PublicAnalyzer preferences +dotnet_diagnostic.AVP1000.severity = error +dotnet_diagnostic.AVP1001.severity = error +dotnet_diagnostic.AVP1002.severity = error +dotnet_diagnostic.AVP1010.severity = error +dotnet_diagnostic.AVP1011.severity = error +dotnet_diagnostic.AVP1012.severity = error +dotnet_diagnostic.AVP1013.severity = error +dotnet_diagnostic.AVP1020.severity = error +dotnet_diagnostic.AVP1021.severity = error +dotnet_diagnostic.AVP1022.severity = error +dotnet_diagnostic.AVP1030.severity = error +dotnet_diagnostic.AVP1031.severity = error +dotnet_diagnostic.AVP1032.severity = error +dotnet_diagnostic.AVP1040.severity = error +dotnet_diagnostic.AVA2001.severity = error + # Xaml files [*.{xaml,axaml}] indent_size = 2 From 3c6fa1a578841b74d213b65ef1dac0191e19da26 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 17 Apr 2023 01:39:20 -0400 Subject: [PATCH 018/110] Fix missed ItemsSource --- src/Avalonia.Themes.Fluent/Controls/AutoCompleteBox.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/AutoCompleteBox.xaml b/src/Avalonia.Themes.Fluent/Controls/AutoCompleteBox.xaml index 6004d42120..5ec91ec9b9 100644 --- a/src/Avalonia.Themes.Fluent/Controls/AutoCompleteBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/AutoCompleteBox.xaml @@ -3,7 +3,7 @@ - + Alabama Alaska Arizona @@ -12,7 +12,7 @@ Colorado Connecticut Delaware - + From d3fb8dee89dc9fd398ece99dd743b3b94bc4666f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 17 Apr 2023 02:16:37 -0400 Subject: [PATCH 019/110] Build fixes --- samples/IntegrationTestApp/IntegrationTestApp.csproj | 1 + src/Avalonia.Controls/Application.cs | 2 ++ src/Avalonia.Controls/AutoCompleteBox/AutoCompleteFilterMode.cs | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/samples/IntegrationTestApp/IntegrationTestApp.csproj b/samples/IntegrationTestApp/IntegrationTestApp.csproj index 1356eeb526..398743a353 100644 --- a/samples/IntegrationTestApp/IntegrationTestApp.csproj +++ b/samples/IntegrationTestApp/IntegrationTestApp.csproj @@ -3,6 +3,7 @@ WinExe net7.0 enable + $(NoWarn);AVP1012 diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index be3d5424fb..e907fd5988 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -94,6 +94,8 @@ namespace Avalonia } /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1031", Justification = "This property is supposed to be a styled readonly property.")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1030", Justification = "False positive.")] public ThemeVariant ActualThemeVariant => GetValue(ActualThemeVariantProperty); /// diff --git a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteFilterMode.cs b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteFilterMode.cs index c17f5a19ab..fa956a79fe 100644 --- a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteFilterMode.cs +++ b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteFilterMode.cs @@ -9,7 +9,7 @@ namespace Avalonia.Controls { /// /// Specifies how text in the text box portion of the - /// control is used to filter items specified by the + /// control is used to filter items specified by the /// property for display in the drop-down. /// public enum AutoCompleteFilterMode From 7e0a7d5aab0b9f83e5d423267031d1c0292b5040 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 18 Apr 2023 22:32:42 -0400 Subject: [PATCH 020/110] Disable more analyzers --- src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.cs b/src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.cs index 9eb4cecc8b..7de0e8eac4 100644 --- a/src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.cs +++ b/src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.cs @@ -129,7 +129,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer "Bad name: An AvaloniaProperty named '{0}' is being assigned to {1}. These names do not relate.", Category, DiagnosticSeverity.Warning, - isEnabledByDefault: true, + isEnabledByDefault: false, // TODO: autogenerate property metadata preserved in ref assembly "An AvaloniaProperty should be stored in a field or property which contains its name. For example, a property named \"Brush\" should be assigned to a field called \"BrushProperty\".\nPrivate symbols are exempt from this diagnostic.", NameCollisionTag); @@ -139,7 +139,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer "Side effects: '{0}' is an AvaloniaProperty which can be {1} without the use of this CLR property. This {2} accessor should do nothing except call {3}.", Category, DiagnosticSeverity.Warning, - isEnabledByDefault: true, + isEnabledByDefault: false, // TODO: autogenerate property metadata preserved in ref assembly "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call any user CLR properties. To execute code before or after the property is set, consider: 1) adding a Coercion method, b) adding a static observer with AvaloniaProperty.Changed.AddClassHandler, and/or c) overriding the AvaloniaObject.OnPropertyChanged method.", AssociatedClrPropertyTag); @@ -149,7 +149,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer "Missing accessor: {0} is {1}, but this CLR property lacks a {2} accessor", Category, DiagnosticSeverity.Warning, - isEnabledByDefault: true, + isEnabledByDefault: false, // TODO: autogenerate property metadata preserved in ref assembly "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call CLR properties on the owning type. Not providing both CLR property accessors is ineffective.", AssociatedClrPropertyTag); @@ -159,7 +159,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer "Inconsistent accessibility: CLR {0} accessibility does not match accessibility of {1}", Category, DiagnosticSeverity.Warning, - isEnabledByDefault: true, + isEnabledByDefault: false, // TODO: autogenerate property metadata preserved in ref assembly "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call CLR properties on the owning type. Defining a CLR property with different accessibility from its associated AvaloniaProperty is ineffective.", AssociatedClrPropertyTag); From f522845a583cf1f911c909625a64119c7e70b972 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 18 Apr 2023 22:37:34 -0400 Subject: [PATCH 021/110] Fix AutoCompleteBox --- .../Controls/AutoCompleteBox.xaml | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/AutoCompleteBox.xaml b/src/Avalonia.Themes.Fluent/Controls/AutoCompleteBox.xaml index 5ec91ec9b9..2113ae4cb0 100644 --- a/src/Avalonia.Themes.Fluent/Controls/AutoCompleteBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/AutoCompleteBox.xaml @@ -1,17 +1,20 @@  + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:generic="using:System.Collections.Generic"> - Alabama - Alaska - Arizona - Arkansas - California - Colorado - Connecticut - Delaware + + Alabama + Alaska + Arizona + Arkansas + California + Colorado + Connecticut + Delaware + From 6523320a7b73f13aba091069b2b58fd69692d25c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 18 Apr 2023 22:43:38 -0400 Subject: [PATCH 022/110] Revert some of SetCurrentValue This reverts commit ead92a6122a4617b19b40356fd537b50d91fa449. --- src/Avalonia.Base/Media/DashStyle.cs | 2 +- src/Avalonia.Base/Media/GradientBrush.cs | 2 +- src/Avalonia.Base/Media/PolyLineSegment.cs | 2 +- src/Avalonia.Base/Media/TransformGroup.cs | 9 ++++----- src/Avalonia.Controls/Calendar/CalendarButton.cs | 2 +- src/Avalonia.Controls/Calendar/CalendarDayButton.cs | 2 +- src/Avalonia.Controls/Documents/Span.cs | 4 ++-- src/Avalonia.Controls/GridSplitter.cs | 2 +- src/Avalonia.Controls/LayoutTransformControl.cs | 2 +- src/Avalonia.Controls/NativeMenuItemSeparator.cs | 2 +- src/Avalonia.Controls/Primitives/OverlayPopupHost.cs | 2 +- .../Primitives/SelectingItemsControl.cs | 8 ++++---- .../PullToRefresh/RefreshVisualizer.cs | 2 +- src/Avalonia.Controls/TabControl.cs | 11 +++++------ src/Avalonia.Controls/TabItem.cs | 6 +++--- 15 files changed, 28 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.Base/Media/DashStyle.cs b/src/Avalonia.Base/Media/DashStyle.cs index 338737d2ab..4749bfa401 100644 --- a/src/Avalonia.Base/Media/DashStyle.cs +++ b/src/Avalonia.Base/Media/DashStyle.cs @@ -46,7 +46,7 @@ namespace Avalonia.Media /// The dash sequence offset. public DashStyle(IEnumerable? dashes, double offset) { - SetCurrentValue(DashesProperty, (dashes as AvaloniaList) ?? new AvaloniaList(dashes ?? Array.Empty())); + Dashes = (dashes as AvaloniaList) ?? new AvaloniaList(dashes ?? Array.Empty()); Offset = offset; } diff --git a/src/Avalonia.Base/Media/GradientBrush.cs b/src/Avalonia.Base/Media/GradientBrush.cs index 0ec753201a..e1654a01b2 100644 --- a/src/Avalonia.Base/Media/GradientBrush.cs +++ b/src/Avalonia.Base/Media/GradientBrush.cs @@ -40,7 +40,7 @@ namespace Avalonia.Media /// public GradientBrush() { - SetCurrentValue(GradientStopsProperty, new GradientStops()); + this.GradientStops = new GradientStops(); } /// diff --git a/src/Avalonia.Base/Media/PolyLineSegment.cs b/src/Avalonia.Base/Media/PolyLineSegment.cs index 7de5e1afa5..5c48c11e19 100644 --- a/src/Avalonia.Base/Media/PolyLineSegment.cs +++ b/src/Avalonia.Base/Media/PolyLineSegment.cs @@ -30,7 +30,7 @@ namespace Avalonia.Media /// public PolyLineSegment() { - SetCurrentValue(PointsProperty, new Points()); + Points = new Points(); } /// diff --git a/src/Avalonia.Base/Media/TransformGroup.cs b/src/Avalonia.Base/Media/TransformGroup.cs index c048234f8c..0465efd5a5 100644 --- a/src/Avalonia.Base/Media/TransformGroup.cs +++ b/src/Avalonia.Base/Media/TransformGroup.cs @@ -13,16 +13,15 @@ namespace Avalonia.Media public TransformGroup() { - var children = new Transforms(); - children.ResetBehavior = ResetBehavior.Remove; - children.CollectionChanged += delegate + Children = new Transforms(); + Children.ResetBehavior = ResetBehavior.Remove; + Children.CollectionChanged += delegate { - children.ForEachItem( + Children.ForEachItem( (tr) => tr.Changed += ChildTransform_Changed, (tr) => tr.Changed -= ChildTransform_Changed, () => { }); }; - SetCurrentValue(ChildrenProperty, children); } private void ChildTransform_Changed(object? sender, System.EventArgs e) diff --git a/src/Avalonia.Controls/Calendar/CalendarButton.cs b/src/Avalonia.Controls/Calendar/CalendarButton.cs index b35298b101..d8672cbf18 100644 --- a/src/Avalonia.Controls/Calendar/CalendarButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarButton.cs @@ -39,7 +39,7 @@ namespace Avalonia.Controls.Primitives public CalendarButton() : base() { - SetCurrentValue(ContentProperty, DateTimeHelper.GetCurrentDateFormat().AbbreviatedMonthNames[0]); + Content = DateTimeHelper.GetCurrentDateFormat().AbbreviatedMonthNames[0]; } /// diff --git a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs index ea3ca8e8fc..3d0befdba7 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs @@ -34,7 +34,7 @@ namespace Avalonia.Controls.Primitives : base() { //Focusable = false; - SetCurrentValue(ContentProperty, DefaultContent.ToString(CultureInfo.CurrentCulture)); + Content = DefaultContent.ToString(CultureInfo.CurrentCulture); } /// diff --git a/src/Avalonia.Controls/Documents/Span.cs b/src/Avalonia.Controls/Documents/Span.cs index a4f3264c4d..d3565cbdd5 100644 --- a/src/Avalonia.Controls/Documents/Span.cs +++ b/src/Avalonia.Controls/Documents/Span.cs @@ -20,10 +20,10 @@ namespace Avalonia.Controls.Documents public Span() { - SetCurrentValue(InlinesProperty, new InlineCollection + Inlines = new InlineCollection { LogicalChildren = LogicalChildren - }); + }; } /// diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 50d1004f34..4684304725 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -707,7 +707,7 @@ namespace Avalonia.Controls RenderTransform = _translation }; - SetCurrentValue(ChildProperty, _decorator); + Child = _decorator; } /// diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index 02d2eeef5b..06069a897e 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -176,7 +176,7 @@ namespace Avalonia.Controls else { _renderTransformChangedEvent?.Dispose(); - SetCurrentValue(LayoutTransformProperty, null); + ClearValue(LayoutTransformProperty); } } } diff --git a/src/Avalonia.Controls/NativeMenuItemSeparator.cs b/src/Avalonia.Controls/NativeMenuItemSeparator.cs index 55b3fd08de..f55d714884 100644 --- a/src/Avalonia.Controls/NativeMenuItemSeparator.cs +++ b/src/Avalonia.Controls/NativeMenuItemSeparator.cs @@ -4,7 +4,7 @@ { public NativeMenuItemSeparator() { - SetCurrentValue(HeaderProperty, "-"); + Header = "-"; } } } diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs index 0acc488885..7ed055f2e5 100644 --- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs @@ -31,7 +31,7 @@ namespace Avalonia.Controls.Primitives /// public void SetChild(Control? control) { - SetCurrentValue(ContentProperty, control); + Content = control; } /// diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index ac3e87c03d..663a315732 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -701,7 +701,7 @@ namespace Avalonia.Controls.Primitives if (value is null) { // Clearing SelectedValueBinding makes the SelectedValue the item itself - SetCurrentValue(SelectedValueProperty, SelectedItem); + SelectedValue = SelectedItem; return; } @@ -721,7 +721,7 @@ namespace Avalonia.Controls.Primitives } // Re-evaluate SelectedValue with the new binding - SetCurrentValue(SelectedValueProperty, _bindingHelper.Evaluate(selectedItem)); + SelectedValue = _bindingHelper.Evaluate(selectedItem); } finally { @@ -1092,7 +1092,7 @@ namespace Avalonia.Controls.Primitives try { _isSelectionChangeActive = true; - SetCurrentValue(SelectedValueProperty, item); + SelectedValue = item; } finally { @@ -1106,7 +1106,7 @@ namespace Avalonia.Controls.Primitives try { _isSelectionChangeActive = true; - SetCurrentValue(SelectedValueProperty, _bindingHelper.Evaluate(item)); + SelectedValue = _bindingHelper.Evaluate(item); } finally { diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs index 45227eef77..0ee2d69a73 100644 --- a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs @@ -193,7 +193,7 @@ namespace Avalonia.Controls UpdateContent(); }; - SetCurrentValue(ContentProperty, _content); + Content = _content; } else { diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 2e27cfd123..310dd34382 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -164,8 +164,8 @@ namespace Avalonia.Controls if (index == SelectedIndex && element is ContentControl container) { - SetCurrentValue(SelectedContentTemplateProperty, container.ContentTemplate); - SetCurrentValue(SelectedContentProperty, container.Content); + SelectedContentTemplate = container.ContentTemplate; + SelectedContent = container.Content; } } @@ -189,15 +189,14 @@ namespace Avalonia.Controls { if (SelectedIndex == -1) { - SetCurrentValue(SelectedContentProperty, null); - SetCurrentValue(SelectedContentTemplateProperty, null); + SelectedContent = SelectedContentTemplate = null; } else { var container = SelectedItem as IContentControl ?? ContainerFromIndex(SelectedIndex) as IContentControl; - SetCurrentValue(SelectedContentTemplateProperty, container?.ContentTemplate); - SetCurrentValue(SelectedContentProperty, container?.Content); + SelectedContentTemplate = container?.ContentTemplate; + SelectedContent = container?.Content; } } diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index 60e9a7c88c..4068404952 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -68,14 +68,14 @@ namespace Avalonia.Controls { if (Header != headered.Header) { - SetCurrentValue(HeaderProperty, headered.Header); + Header = headered.Header; } } else { if (!(obj.NewValue is Control)) { - SetCurrentValue(HeaderProperty, obj.NewValue); + Header = obj.NewValue; } } } @@ -83,7 +83,7 @@ namespace Avalonia.Controls { if (Header == obj.OldValue) { - SetCurrentValue(HeaderProperty, obj.NewValue); + Header = obj.NewValue; } } } From 7d26c16a10ca97a69995af32cb913a6b11fcda6a Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 18 Apr 2023 22:47:53 -0400 Subject: [PATCH 023/110] Configure AVP1012 as a warning --- .editorconfig | 2 +- src/Avalonia.Base/Media/DashStyle.cs | 2 ++ src/Avalonia.Base/Media/GradientBrush.cs | 2 ++ src/Avalonia.Base/Media/PolyLineSegment.cs | 2 ++ src/Avalonia.Base/Media/TransformGroup.cs | 2 ++ src/Avalonia.Controls/Documents/Span.cs | 2 ++ 6 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 6337257e8f..d07618df6c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -192,7 +192,7 @@ dotnet_diagnostic.AVP1001.severity = error dotnet_diagnostic.AVP1002.severity = error dotnet_diagnostic.AVP1010.severity = error dotnet_diagnostic.AVP1011.severity = error -dotnet_diagnostic.AVP1012.severity = error +dotnet_diagnostic.AVP1012.severity = warning dotnet_diagnostic.AVP1013.severity = error dotnet_diagnostic.AVP1020.severity = error dotnet_diagnostic.AVP1021.severity = error diff --git a/src/Avalonia.Base/Media/DashStyle.cs b/src/Avalonia.Base/Media/DashStyle.cs index 4749bfa401..2529b9317d 100644 --- a/src/Avalonia.Base/Media/DashStyle.cs +++ b/src/Avalonia.Base/Media/DashStyle.cs @@ -44,6 +44,8 @@ namespace Avalonia.Media /// /// The dashes collection. /// The dash sequence offset. + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1012", + Justification = "Collection properties shouldn't be set with SetCurrentValue.")] public DashStyle(IEnumerable? dashes, double offset) { Dashes = (dashes as AvaloniaList) ?? new AvaloniaList(dashes ?? Array.Empty()); diff --git a/src/Avalonia.Base/Media/GradientBrush.cs b/src/Avalonia.Base/Media/GradientBrush.cs index e1654a01b2..971d4fdd58 100644 --- a/src/Avalonia.Base/Media/GradientBrush.cs +++ b/src/Avalonia.Base/Media/GradientBrush.cs @@ -38,6 +38,8 @@ namespace Avalonia.Media /// /// Initializes a new instance of the class. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1012", + Justification = "Collection properties shouldn't be set with SetCurrentValue.")] public GradientBrush() { this.GradientStops = new GradientStops(); diff --git a/src/Avalonia.Base/Media/PolyLineSegment.cs b/src/Avalonia.Base/Media/PolyLineSegment.cs index 5c48c11e19..276bb66a4a 100644 --- a/src/Avalonia.Base/Media/PolyLineSegment.cs +++ b/src/Avalonia.Base/Media/PolyLineSegment.cs @@ -28,6 +28,8 @@ namespace Avalonia.Media /// /// Initializes a new instance of the class. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1012", + Justification = "Collection properties shouldn't be set with SetCurrentValue.")] public PolyLineSegment() { Points = new Points(); diff --git a/src/Avalonia.Base/Media/TransformGroup.cs b/src/Avalonia.Base/Media/TransformGroup.cs index 0465efd5a5..ae5e54c414 100644 --- a/src/Avalonia.Base/Media/TransformGroup.cs +++ b/src/Avalonia.Base/Media/TransformGroup.cs @@ -11,6 +11,8 @@ namespace Avalonia.Media public static readonly StyledProperty ChildrenProperty = AvaloniaProperty.Register(nameof(Children)); + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1012", + Justification = "Collection properties shouldn't be set with SetCurrentValue.")] public TransformGroup() { Children = new Transforms(); diff --git a/src/Avalonia.Controls/Documents/Span.cs b/src/Avalonia.Controls/Documents/Span.cs index d3565cbdd5..7931ecbbde 100644 --- a/src/Avalonia.Controls/Documents/Span.cs +++ b/src/Avalonia.Controls/Documents/Span.cs @@ -18,6 +18,8 @@ namespace Avalonia.Controls.Documents AvaloniaProperty.Register( nameof(Inlines)); + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1012", + Justification = "Collection properties shouldn't be set with SetCurrentValue.")] public Span() { Inlines = new InlineCollection From 3da148f5c2732622db5eaeec0906416900635c49 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 19 Apr 2023 00:33:44 -0400 Subject: [PATCH 024/110] Fix analyzers nuget package --- nukebuild/numerge.config | 5 +++++ .../Avalonia.Analyzers.csproj | 19 ++++++++++++------- .../GeneratorContextExtensions.cs | 2 +- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/nukebuild/numerge.config b/nukebuild/numerge.config index 09f22ec527..71b77bee93 100644 --- a/nukebuild/numerge.config +++ b/nukebuild/numerge.config @@ -16,6 +16,11 @@ "Id": "Avalonia.Generators", "IgnoreMissingFrameworkBinaries": true, "DoNotMergeDependencies": true + }, + { + "Id": "Avalonia.Analyzers", + "IgnoreMissingFrameworkBinaries": true, + "DoNotMergeDependencies": true } ] } diff --git a/src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj b/src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj index 39eaab1289..c27801db61 100644 --- a/src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj +++ b/src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj @@ -1,17 +1,22 @@  - netstandard2.0 - enable + false + Avalonia.Analyzers true + true + true - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + + + + + diff --git a/src/tools/Avalonia.Generators/GeneratorContextExtensions.cs b/src/tools/Avalonia.Generators/GeneratorContextExtensions.cs index d057e8732e..3e4426e6bb 100644 --- a/src/tools/Avalonia.Generators/GeneratorContextExtensions.cs +++ b/src/tools/Avalonia.Generators/GeneratorContextExtensions.cs @@ -20,7 +20,7 @@ internal static class GeneratorContextExtensions public static void ReportNameGeneratorUnhandledError(this GeneratorExecutionContext context, Exception error) => context.Report(UnhandledErrorDescriptorId, "Unhandled exception occured while generating typed Name references. " + - "Please file an issue: https://github.com/avaloniaui/Avalonia.Generators", + "Please file an issue: https://github.com/avaloniaui/Avalonia", error.ToString()); public static void ReportNameGeneratorInvalidType(this GeneratorExecutionContext context, string typeName) => From 8df3b340322c9e185e723cf1f930aefef963af86 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 19 Apr 2023 16:59:09 +0200 Subject: [PATCH 025/110] Add TextEndOfLine runs to TextBounds.TextRunBounds --- .../Media/TextFormatting/TextLineImpl.cs | 20 +++++++++++--- .../Media/TextFormatting/TextLineTests.cs | 27 +++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 3264d5e88a..935347ed85 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -698,7 +698,7 @@ namespace Avalonia.Media.TextFormatting i = lastRunIndex; //Possible overlap at runs of different direction - if (directionalWidth == 0) + if (directionalWidth == 0 && i < _textRuns.Length - 1) { //In case a run only contains a linebreak we don't want to skip it. if (currentRun is ShapedTextRun shaped) @@ -844,7 +844,7 @@ namespace Avalonia.Media.TextFormatting i = firstRunIndex; //Possible overlap at runs of different direction - if (directionalWidth == 0) + if (directionalWidth == 0 && i > 0) { //In case a run only contains a linebreak we don't want to skip it. if (currentRun is ShapedTextRun shaped) @@ -860,8 +860,8 @@ namespace Avalonia.Media.TextFormatting } } - TextBounds? textBounds = null; int coveredLength; + TextBounds? textBounds; switch (currentDirection) { @@ -942,6 +942,13 @@ namespace Avalonia.Media.TextFormatting new TextRunBounds( new Rect(startX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun)); } + else + { + //Add potential TextEndOfParagraph + textRunBounds.Add( + new TextRunBounds( + new Rect(endX, 0, 0, Height), currentPosition, currentRun.Length, currentRun)); + } currentPosition += currentRun.Length; @@ -1007,6 +1014,13 @@ namespace Avalonia.Media.TextFormatting endX += drawableTextRun.Size.Width; } + else + { + //Add potential TextEndOfParagraph + textRunBounds.Add( + new TextRunBounds( + new Rect(endX, 0, 0, Height), currentPosition, currentRun.Length, currentRun)); + } currentPosition += currentRun.Length; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index d605ecbfda..1a39dd5223 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -1021,6 +1021,33 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_GetTextBounds_With_EndOfParagraph() + { + var text = "abc"; + + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + var textSource = new SingleBufferTextSource(text, defaultProperties, true); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, + true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); + + var textBounds = textLine.GetTextBounds(3, 1); + + Assert.Equal(1, textBounds.Count); + + var firstBounds = textBounds.First(); + + Assert.True(firstBounds.TextRunBounds.Count > 0); + } + } + private class FixedRunsTextSource : ITextSource { private readonly IReadOnlyList _textRuns; From c63571dcdadb3ae88688184c6050aad3742488aa Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 20 Apr 2023 16:19:46 +0600 Subject: [PATCH 026/110] Reuse SpanHelpers in Avalonia.Build.Tasks --- src/Avalonia.Base/Media/Color.cs | 2 +- src/Avalonia.Base/Utilities/SpanHelpers.cs | 5 +++- .../Avalonia.Build.Tasks.csproj | 1 + src/Avalonia.Build.Tasks/SpanCompat.cs | 26 +------------------ 4 files changed, 7 insertions(+), 27 deletions(-) diff --git a/src/Avalonia.Base/Media/Color.cs b/src/Avalonia.Base/Media/Color.cs index f06f272e51..7b29ec640a 100644 --- a/src/Avalonia.Base/Media/Color.cs +++ b/src/Avalonia.Base/Media/Color.cs @@ -9,8 +9,8 @@ using System; using System.Globalization; #if !BUILDTASK using Avalonia.Animation.Animators; -using static Avalonia.Utilities.SpanHelpers; #endif +using static Avalonia.Utilities.SpanHelpers; namespace Avalonia.Media { diff --git a/src/Avalonia.Base/Utilities/SpanHelpers.cs b/src/Avalonia.Base/Utilities/SpanHelpers.cs index 9a5dce9798..f80ac7c046 100644 --- a/src/Avalonia.Base/Utilities/SpanHelpers.cs +++ b/src/Avalonia.Base/Utilities/SpanHelpers.cs @@ -4,7 +4,10 @@ using System.Runtime.CompilerServices; namespace Avalonia.Utilities { - public static class SpanHelpers +#if !BUILDTASK + public +#endif + static class SpanHelpers { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool TryParseUInt(this ReadOnlySpan span, NumberStyles style, IFormatProvider provider, out uint value) diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index e44b7290af..b654c66157 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -115,6 +115,7 @@ + diff --git a/src/Avalonia.Build.Tasks/SpanCompat.cs b/src/Avalonia.Build.Tasks/SpanCompat.cs index be59ff8b6c..00892d56e6 100644 --- a/src/Avalonia.Build.Tasks/SpanCompat.cs +++ b/src/Avalonia.Build.Tasks/SpanCompat.cs @@ -85,31 +85,7 @@ namespace System { return TrimStart().TrimEnd(); } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryParseUInt(NumberStyles style, IFormatProvider provider, out uint value) - { - return uint.TryParse(ToString(), style, provider, out value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryParseInt(out int value) - { - return int.TryParse(ToString(), out value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryParseDouble(NumberStyles style, IFormatProvider provider, out double value) - { - return double.TryParse(ToString(), style, provider, out value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryParseByte(NumberStyles style, IFormatProvider provider, out byte value) - { - return byte.TryParse(ToString(), style, provider, out value); - } - + public override string ToString() => _length == 0 ? string.Empty : _s.Substring(_start, _length); internal int IndexOf(ReadOnlySpan v, StringComparison ordinal, int start = 0) From e5acebabcbf8163642a633ee0d9fc696eec17fc1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 20 Apr 2023 18:20:49 +0200 Subject: [PATCH 027/110] Make thumb drag delta relative to root. #10892 changed the thumb drag delta to be relative to the parent, but the problem was that is that if the thumb controls the position of the parent then the delta will be incorrect. This was causing a bug in `TreeDataGrid` headers which were jumping around: the `Thumb` is a child of the header and causes the header to move. --- src/Avalonia.Controls/Primitives/Thumb.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index 993d054f87..5dd2bd067b 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -85,7 +85,7 @@ namespace Avalonia.Controls.Primitives { if (_lastPoint.HasValue) { - var point = e.GetPosition(this.GetVisualParent()); + var point = e.GetPosition((Visual?)this.GetVisualRoot()); var ev = new VectorEventArgs { RoutedEvent = DragDeltaEvent, @@ -100,7 +100,7 @@ namespace Avalonia.Controls.Primitives protected override void OnPointerPressed(PointerPressedEventArgs e) { e.Handled = true; - _lastPoint = e.GetPosition(this.GetVisualParent()); + _lastPoint = e.GetPosition((Visual?)this.GetVisualRoot()); var ev = new VectorEventArgs { @@ -123,7 +123,7 @@ namespace Avalonia.Controls.Primitives var ev = new VectorEventArgs { RoutedEvent = DragCompletedEvent, - Vector = (Vector)e.GetPosition(this.GetVisualParent()), + Vector = (Vector)e.GetPosition((Visual?)this.GetVisualRoot()), }; RaiseEvent(ev); From bf06f075cbe851e4115928098167ca9600001a05 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 21 Apr 2023 02:00:40 -0400 Subject: [PATCH 028/110] Fix crash on Cursor.Parse in DevTools --- src/Avalonia.Base/Input/Cursor.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Input/Cursor.cs b/src/Avalonia.Base/Input/Cursor.cs index c555087879..49660e508e 100644 --- a/src/Avalonia.Base/Input/Cursor.cs +++ b/src/Avalonia.Base/Input/Cursor.cs @@ -42,19 +42,21 @@ namespace Avalonia.Input public class Cursor : IDisposable { public static readonly Cursor Default = new Cursor(StandardCursorType.Arrow); + private string _name; - internal Cursor(ICursorImpl platformImpl) + private Cursor(ICursorImpl platformImpl, string name) { PlatformImpl = platformImpl; + _name = name; } public Cursor(StandardCursorType cursorType) - : this(GetCursorFactory().GetCursor(cursorType)) + : this(GetCursorFactory().GetCursor(cursorType), cursorType.ToString()) { } public Cursor(IBitmap cursor, PixelPoint hotSpot) - : this(GetCursorFactory().CreateCursor(cursor.PlatformImpl.Item, hotSpot)) + : this(GetCursorFactory().CreateCursor(cursor.PlatformImpl.Item, hotSpot), "BitmapCursor") { } @@ -73,5 +75,10 @@ namespace Avalonia.Input { return AvaloniaLocator.Current.GetRequiredService(); } + + public override string ToString() + { + return _name; + } } } From 8ab2aa7dc922e02da61832c0aeeff27033fbbf75 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 21 Apr 2023 02:01:42 -0400 Subject: [PATCH 029/110] Add context flyout to the dev tools tree --- .../ViewModels/TreePageViewModel.cs | 107 ++++++++++++++++++ .../Diagnostics/Views/TreePageView.xaml | 14 +++ .../Diagnostics/Views/TreePageView.xaml.cs | 11 ++ 3 files changed, 132 insertions(+) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs index e916995bae..4e8b4c66a2 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs @@ -1,5 +1,11 @@ using System; +using System.Collections.Generic; +using System.Linq; using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.LogicalTree; +using Avalonia.Metadata; +using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels @@ -21,6 +27,8 @@ namespace Avalonia.Diagnostics.ViewModels SettersFilter.RefreshFilter += (s, e) => Details?.UpdateStyleFilters(); } + public event EventHandler? ClipboardCopyRequested; + public MainViewModel MainView { get; } public FilterViewModel PropertiesFilter { get; } @@ -106,6 +114,105 @@ namespace Avalonia.Diagnostics.ViewModels } } + public void CopySelector() + { + var currentVisual = SelectedNode?.Visual as Visual; + if (currentVisual is not null) + { + var selector = GetVisualSelector(currentVisual); + + ClipboardCopyRequested?.Invoke(this, selector); + } + } + + public void CopySelectorFromTemplateParent() + { + var parts = new List(); + + var currentVisual = SelectedNode?.Visual as Visual; + while (currentVisual is not null) + { + parts.Add(GetVisualSelector(currentVisual)); + + currentVisual = currentVisual.TemplatedParent as Visual; + } + + if (parts.Any()) + { + parts.Reverse(); + var selector = string.Join(" /template/ ", parts); + + ClipboardCopyRequested?.Invoke(this, selector); + } + } + + public void ExpandRecursively() + { + if (SelectedNode is { } selectedNode) + { + ExpandNode(selectedNode); + + var stack = new Stack(); + stack.Push(selectedNode); + + while (stack.Count > 0) + { + var item = stack.Pop(); + item.IsExpanded = true; + foreach (var child in item.Children) + { + stack.Push(child); + } + } + } + } + + public void CollapseChildren() + { + if (SelectedNode is { } selectedNode) + { + var stack = new Stack(); + stack.Push(selectedNode); + + while (stack.Count > 0) + { + var item = stack.Pop(); + item.IsExpanded = false; + foreach (var child in item.Children) + { + stack.Push(child); + } + } + } + } + + public void CaptureNodeScreenshot() + { + MainView.Shot(null); + } + + public void BringIntoView() + { + (SelectedNode?.Visual as Control)?.BringIntoView(); + } + + + public void Focus() + { + (SelectedNode?.Visual as Control)?.Focus(); + } + + private static string GetVisualSelector(Visual visual) + { + var name = string.IsNullOrEmpty(visual.Name) ? "" : $"#{visual.Name}"; + var classes = string.Concat(visual.Classes + .Where(c => !c.StartsWith(":")) + .Select(c => '.' + c)); + var typeName = ((IStyleable)visual).StyleKey.Name; + + return $"{typeName}{name}{classes}"; + } + private void ExpandNode(TreeNode? node) { if (node != null) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml index 9ffc301dc1..ecdd46dd74 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml @@ -26,6 +26,20 @@ + + + + + + + + + + + + + + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs index 1a4bb6170a..66be6b5041 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics; using System.Linq; using Avalonia.Controls; @@ -38,6 +39,16 @@ namespace Avalonia.Diagnostics.Views AdornerLayer.SetIsClipEnabled(_adorner, false); } + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + + ((TreePageViewModel)DataContext!).ClipboardCopyRequested += (sender, s) => + { + TopLevel.GetTopLevel(this)?.Clipboard?.SetTextAsync(s); + }; + } + protected void AddAdorner(object? sender, PointerEventArgs e) { var node = (TreeNode?)((Control)sender!).DataContext; From 5262eec4cf1f5134ac873a67a386bde9678913ab Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 21 Apr 2023 10:38:00 +0200 Subject: [PATCH 030/110] Passing null gives us the point relative to the root. And update the documentation for `GetPosition` to explain what `null` does (as in `GetCurrentPoint`). --- src/Avalonia.Base/Input/PointerEventArgs.cs | 4 ++-- src/Avalonia.Controls/Primitives/Thumb.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs index 28a3c3aefb..beb953ce8f 100644 --- a/src/Avalonia.Base/Input/PointerEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerEventArgs.cs @@ -77,14 +77,14 @@ namespace Avalonia.Input /// /// Gets the pointer position relative to a control. /// - /// The control. + /// The visual whose coordinate system to use. Pass null for toplevel coordinate system /// The pointer position in the control's coordinates. public Point GetPosition(Visual? relativeTo) => GetPosition(_rootVisualPosition, relativeTo); /// /// Returns the PointerPoint associated with the current event /// - /// The visual which coordinate system to use. Pass null for toplevel coordinate system + /// The visual whose coordinate system to use. Pass null for toplevel coordinate system /// public PointerPoint GetCurrentPoint(Visual? relativeTo) => new PointerPoint(Pointer, GetPosition(relativeTo), _properties); diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index 5dd2bd067b..9854bdbea6 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -85,7 +85,7 @@ namespace Avalonia.Controls.Primitives { if (_lastPoint.HasValue) { - var point = e.GetPosition((Visual?)this.GetVisualRoot()); + var point = e.GetPosition(null); var ev = new VectorEventArgs { RoutedEvent = DragDeltaEvent, @@ -100,7 +100,7 @@ namespace Avalonia.Controls.Primitives protected override void OnPointerPressed(PointerPressedEventArgs e) { e.Handled = true; - _lastPoint = e.GetPosition((Visual?)this.GetVisualRoot()); + _lastPoint = e.GetPosition(null); var ev = new VectorEventArgs { @@ -123,7 +123,7 @@ namespace Avalonia.Controls.Primitives var ev = new VectorEventArgs { RoutedEvent = DragCompletedEvent, - Vector = (Vector)e.GetPosition((Visual?)this.GetVisualRoot()), + Vector = (Vector)e.GetPosition(null), }; RaiseEvent(ev); From dd1df9a539e601791867832c1bdb75dfaf8ef3ed Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 21 Apr 2023 13:03:49 +0200 Subject: [PATCH 031/110] Do not execute OnClick when access key is pressed in combination with AltGr --- src/Avalonia.Base/Input/AccessKeyHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Input/AccessKeyHandler.cs b/src/Avalonia.Base/Input/AccessKeyHandler.cs index 13ca140565..2bd9fce947 100644 --- a/src/Avalonia.Base/Input/AccessKeyHandler.cs +++ b/src/Avalonia.Base/Input/AccessKeyHandler.cs @@ -176,7 +176,7 @@ namespace Avalonia.Input { bool menuIsOpen = MainMenu?.IsOpen == true; - if (e.KeyModifiers.HasAllFlags(KeyModifiers.Alt) || menuIsOpen) + if (e.KeyModifiers.HasAllFlags(KeyModifiers.Alt) && !e.KeyModifiers.HasAllFlags(KeyModifiers.Control) || menuIsOpen) { // If any other key is pressed with the Alt key held down, or the main menu is open, // find all controls who have registered that access key. From 981dfd29d74ba2c64b9cca220d2f0e7ec830e201 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 21 Apr 2023 13:49:44 +0200 Subject: [PATCH 032/110] Added WindowBase.Resized event. Which exposes the resize reason and new client size. Required renaming `PlatformResizeReason` to `WindowResizeReason`. Made `TopLevel.HandleResized` method internal. --- .../Platform/SkiaPlatform/TopLevelImpl.cs | 6 +- .../Offscreen/OffscreenTopLevelImpl.cs | 4 +- .../Platform/ITopLevelImpl.cs | 36 +---------- src/Avalonia.Controls/Platform/IWindowImpl.cs | 2 +- src/Avalonia.Controls/TopLevel.cs | 2 +- src/Avalonia.Controls/Window.cs | 14 ++--- src/Avalonia.Controls/WindowBase.cs | 15 ++++- .../WindowResizedEventArgs.cs | 61 +++++++++++++++++++ .../Remote/PreviewerWindowImpl.cs | 2 +- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 6 +- src/Avalonia.Headless/HeadlessWindowImpl.cs | 6 +- src/Avalonia.Native/PopupImpl.cs | 2 +- src/Avalonia.Native/WindowImplBase.cs | 8 +-- src/Avalonia.X11/X11Window.cs | 14 ++--- .../Avalonia.Browser/BrowserTopLevelImpl.cs | 4 +- .../FramebufferToplevelImpl.cs | 2 +- .../Wpf/WpfTopLevelImpl.cs | 4 +- src/Windows/Avalonia.Win32/PopupImpl.cs | 3 +- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 2 +- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 6 +- src/Windows/Avalonia.Win32/WindowImpl.cs | 14 ++--- src/iOS/Avalonia.iOS/AvaloniaView.cs | 4 +- .../Primitives/PopupTests.cs | 4 +- .../TopLevelTests.cs | 2 +- .../WindowTests.cs | 14 ++--- .../CompositorTestServices.cs | 2 +- .../MockWindowingPlatform.cs | 9 +-- 27 files changed, 145 insertions(+), 103 deletions(-) create mode 100644 src/Avalonia.Controls/WindowResizedEventArgs.cs diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 126c488d59..fae1aacf61 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -91,7 +91,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } @@ -156,12 +156,12 @@ namespace Avalonia.Android.Platform.SkiaPlatform protected virtual void OnResized(Size size) { - Resized?.Invoke(size, PlatformResizeReason.Unspecified); + Resized?.Invoke(size, WindowResizeReason.Unspecified); } internal void Resize(Size size) { - Resized?.Invoke(size, PlatformResizeReason.Layout); + Resized?.Invoke(size, WindowResizeReason.Layout); } class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback, IInitEditorInfo diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index 3a4ae80cf4..387357dddd 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -47,7 +47,7 @@ namespace Avalonia.Controls.Embedding.Offscreen set { _clientSize = value; - Resized?.Invoke(value, PlatformResizeReason.Unspecified); + Resized?.Invoke(value, WindowResizeReason.Unspecified); } } @@ -65,7 +65,7 @@ namespace Avalonia.Controls.Embedding.Offscreen public Action? Input { get; set; } public Action? Paint { get; set; } - public Action? Resized { get; set; } + public Action? Resized { get; set; } public Action? ScalingChanged { get; set; } public Action? TransparencyLevelChanged { get; set; } diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index 29156f4030..bb6b2304af 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -9,40 +9,6 @@ using Avalonia.Rendering; namespace Avalonia.Platform { - /// - /// Describes the reason for a message. - /// - public enum PlatformResizeReason - { - /// - /// The resize reason is unknown or unspecified. - /// - Unspecified, - - /// - /// The resize was due to the user resizing the window, for example by dragging the - /// window frame. - /// - User, - - /// - /// The resize was initiated by the application, for example by setting one of the sizing- - /// related properties on such as or - /// . - /// - Application, - - /// - /// The resize was initiated by the layout system. - /// - Layout, - - /// - /// The resize was due to a change in DPI. - /// - DpiChange, - } - /// /// Defines a platform-specific top-level window implementation. /// @@ -93,7 +59,7 @@ namespace Avalonia.Platform /// /// Gets or sets a method called when the toplevel is resized. /// - Action? Resized { get; set; } + Action? Resized { get; set; } /// /// Gets or sets a method called when the toplevel's scaling changes. diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 31b144ce00..5591e68235 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -114,7 +114,7 @@ namespace Avalonia.Platform /// /// The new client size. /// The reason for the resize. - void Resize(Size clientSize, PlatformResizeReason reason = PlatformResizeReason.Application); + void Resize(Size clientSize, WindowResizeReason reason = WindowResizeReason.Application); /// /// Sets the client size of the top level. diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 07b1e9b51f..eeb44e7bd8 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -569,7 +569,7 @@ namespace Avalonia.Controls /// /// The new client size. /// The reason for the resize. - protected virtual void HandleResized(Size clientSize, PlatformResizeReason reason) + internal virtual void HandleResized(Size clientSize, WindowResizeReason reason) { ClientSize = clientSize; FrameSize = PlatformImpl!.FrameSize; diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index f9593f1c1b..48edb81b16 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -227,7 +227,7 @@ namespace Avalonia.Controls impl.WindowStateChanged = HandleWindowStateChanged; _maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size); impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged; - this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x, PlatformResizeReason.Application)); + this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x, WindowResizeReason.Application)); PlatformImpl?.ShowTaskbarIcon(ShowInTaskbar); } @@ -700,7 +700,7 @@ namespace Avalonia.Controls if (initialSize != ClientSize) { - PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout); + PlatformImpl?.Resize(initialSize, WindowResizeReason.Layout); } LayoutManager.ExecuteInitialLayoutPass(); @@ -778,7 +778,7 @@ namespace Avalonia.Controls if (initialSize != ClientSize) { - PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout); + PlatformImpl?.Resize(initialSize, WindowResizeReason.Layout); } LayoutManager.ExecuteInitialLayoutPass(); @@ -975,7 +975,7 @@ namespace Avalonia.Controls protected sealed override Size ArrangeSetBounds(Size size) { - PlatformImpl?.Resize(size, PlatformResizeReason.Layout); + PlatformImpl?.Resize(size, WindowResizeReason.Layout); return ClientSize; } @@ -994,7 +994,7 @@ namespace Avalonia.Controls } /// - protected sealed override void HandleResized(Size clientSize, PlatformResizeReason reason) + internal override void HandleResized(Size clientSize, WindowResizeReason reason) { if (ClientSize != clientSize || double.IsNaN(Width) || double.IsNaN(Height)) { @@ -1005,8 +1005,8 @@ namespace Avalonia.Controls // to the requested size. if (sizeToContent != SizeToContent.Manual && CanResize && - reason == PlatformResizeReason.Unspecified || - reason == PlatformResizeReason.User) + reason == WindowResizeReason.Unspecified || + reason == WindowResizeReason.User) { if (clientSize.Width != ClientSize.Width) sizeToContent &= ~SizeToContent.Width; diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 814a9b5960..d42244ba5c 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -80,6 +80,11 @@ namespace Avalonia.Controls /// public event EventHandler? PositionChanged; + /// + /// Occurs when the window is resized. + /// + public event EventHandler? Resized; + public new IWindowBaseImpl? PlatformImpl => (IWindowBaseImpl?) base.PlatformImpl; /// @@ -188,6 +193,12 @@ namespace Avalonia.Controls base.OnOpened(e); } + /// + /// Raises the event. + /// + /// An that contains the event data. + protected virtual void OnResized(WindowResizedEventArgs e) => Resized?.Invoke(this, e); + protected override void HandleClosed() { using (FreezeVisibilityChangeHandling()) @@ -208,7 +219,7 @@ namespace Avalonia.Controls /// /// The new client size. /// The reason for the resize. - protected override void HandleResized(Size clientSize, PlatformResizeReason reason) + internal override void HandleResized(Size clientSize, WindowResizeReason reason) { FrameSize = PlatformImpl?.FrameSize; @@ -218,6 +229,8 @@ namespace Avalonia.Controls LayoutManager.ExecuteLayoutPass(); Renderer.Resized(clientSize); } + + OnResized(new WindowResizedEventArgs(clientSize, reason)); } /// diff --git a/src/Avalonia.Controls/WindowResizedEventArgs.cs b/src/Avalonia.Controls/WindowResizedEventArgs.cs new file mode 100644 index 0000000000..daa8aa0f09 --- /dev/null +++ b/src/Avalonia.Controls/WindowResizedEventArgs.cs @@ -0,0 +1,61 @@ +using System; +using Avalonia.Layout; + +namespace Avalonia.Controls +{ + /// + /// Describes the reason for a event. + /// + public enum WindowResizeReason + { + /// + /// The resize reason is unknown or unspecified. + /// + Unspecified, + + /// + /// The resize was due to the user resizing the window, for example by dragging the + /// window frame. + /// + User, + + /// + /// The resize was initiated by the application, for example by setting one of the sizing- + /// related properties on such as or + /// . + /// + Application, + + /// + /// The resize was initiated by the layout system. + /// + Layout, + + /// + /// The resize was due to a change in DPI. + /// + DpiChange, + } + + /// + /// Provides data for the event. + /// + public class WindowResizedEventArgs : EventArgs + { + internal WindowResizedEventArgs(Size clientSize, WindowResizeReason reason) + { + ClientSize = clientSize; + Reason = reason; + } + + /// + /// Gets the new client size of the window in device-independent pixels. + /// + public Size ClientSize { get; } + + /// + /// Gets the reason for the resize. + /// + public WindowResizeReason Reason { get; } + } +} diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 2da8f38ea9..e0fcf8e530 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -59,7 +59,7 @@ namespace Avalonia.DesignerSupport.Remote base.OnMessage(transport, obj); } - public void Resize(Size clientSize, PlatformResizeReason reason) + public void Resize(Size clientSize, WindowResizeReason reason) { _transport.Send(new RequestViewportResizeMessage { diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index ea427e4c92..f6f5c185e9 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -32,7 +32,7 @@ namespace Avalonia.DesignerSupport.Remote public IEnumerable Surfaces { get; } public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } public Func Closing { get; set; } public Action Closed { get; set; } @@ -59,7 +59,7 @@ namespace Avalonia.DesignerSupport.Remote PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, (_, size, __) => { - Resize(size, PlatformResizeReason.Unspecified); + Resize(size, WindowResizeReason.Unspecified); })); } @@ -112,7 +112,7 @@ namespace Avalonia.DesignerSupport.Remote { } - public void Resize(Size clientSize, PlatformResizeReason reason) + public void Resize(Size clientSize, WindowResizeReason reason) { } diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index a801474f21..3405a4d4e7 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -50,7 +50,7 @@ namespace Avalonia.Headless public IEnumerable Surfaces { get; } public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } public IRenderer CreateRenderer(IRenderRoot root) => @@ -111,7 +111,7 @@ namespace Avalonia.Headless public Action Activated { get; set; } public IPlatformHandle Handle { get; } = new PlatformHandle(IntPtr.Zero, "STUB"); public Size MaxClientSize { get; } = new Size(1920, 1280); - public void Resize(Size clientSize, PlatformResizeReason reason) + public void Resize(Size clientSize, WindowResizeReason reason) { // Emulate X11 behavior here if (IsPopup) @@ -129,7 +129,7 @@ namespace Avalonia.Headless if (ClientSize != clientSize) { ClientSize = clientSize; - Resized?.Invoke(clientSize, PlatformResizeReason.Unspecified); + Resized?.Invoke(clientSize, WindowResizeReason.Unspecified); } } diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index 0953527284..516f8f99f1 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -29,7 +29,7 @@ namespace Avalonia.Native private void MoveResize(PixelPoint position, Size size, double scaling) { Position = position; - Resize(size, PlatformResizeReason.Layout); + Resize(size, ResizeReason.Layout); //TODO: We ignore the scaling override for now } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 0dff46057e..26c3da9d50 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -95,7 +95,7 @@ namespace Avalonia.Native var monitor = Screen.AllScreens.OrderBy(x => x.Scaling) .FirstOrDefault(m => m.Bounds.Contains(Position)); - Resize(new Size(monitor.WorkingArea.Width * 0.75d, monitor.WorkingArea.Height * 0.7d), PlatformResizeReason.Layout); + Resize(new Size(monitor.WorkingArea.Width * 0.75d, monitor.WorkingArea.Height * 0.7d), WindowResizeReason.Layout); } public IAvnWindowBase Native => _native; @@ -160,7 +160,7 @@ namespace Avalonia.Native public Action LostFocus { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action Closed { get; set; } public IMouseDevice MouseDevice => _mouse; public abstract IPopupImpl CreatePopup(); @@ -211,7 +211,7 @@ namespace Avalonia.Native { var s = new Size(size->Width, size->Height); _parent._savedLogicalSize = s; - _parent.Resized?.Invoke(s, (PlatformResizeReason)reason); + _parent.Resized?.Invoke(s, (WindowResizeReason)reason); } } @@ -360,7 +360,7 @@ namespace Avalonia.Native } } - public void Resize(Size clientSize, PlatformResizeReason reason) + public void Resize(Size clientSize, WindowResizeReason reason) { _native?.Resize(clientSize.Width, clientSize.Height, (AvnPlatformResizeReason)reason); } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 38af1b6d7b..0a535d2f57 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -352,7 +352,7 @@ namespace Avalonia.X11 public IEnumerable Surfaces { get; } public Action? Input { get; set; } public Action? Paint { get; set; } - public Action? Resized { get; set; } + public Action? Resized { get; set; } //TODO public Action? ScalingChanged { get; set; } public Action? Deactivated { get; set; } @@ -509,7 +509,7 @@ namespace Avalonia.X11 UpdateImePosition(); if (changedSize && !updatedSizeViaScaling && !_popup) - Resized?.Invoke(ClientSize, PlatformResizeReason.Unspecified); + Resized?.Invoke(ClientSize, WindowResizeReason.Unspecified); }, DispatcherPriority.Layout); if (_useRenderWindow) @@ -590,7 +590,7 @@ namespace Avalonia.X11 UpdateImePosition(); SetMinMaxSize(_scaledMinMaxSize.minSize, _scaledMinMaxSize.maxSize); if(!skipResize) - Resize(oldScaledSize, true, PlatformResizeReason.DpiChange); + Resize(oldScaledSize, true, WindowResizeReason.DpiChange); return true; } @@ -642,7 +642,7 @@ namespace Avalonia.X11 { // Occurs once the window has been mapped, which is the earliest the extents // can be retrieved, so invoke event to force update of TopLevel.FrameSize. - Resized?.Invoke(ClientSize, PlatformResizeReason.Unspecified); + Resized?.Invoke(ClientSize, WindowResizeReason.Unspecified); } if (atom == _x11.Atoms._NET_WM_STATE) @@ -959,19 +959,19 @@ namespace Avalonia.X11 } - public void Resize(Size clientSize, PlatformResizeReason reason) => Resize(clientSize, false, reason); + public void Resize(Size clientSize, WindowResizeReason reason) => Resize(clientSize, false, reason); public void Move(PixelPoint point) => Position = point; private void MoveResize(PixelPoint position, Size size, double scaling) { Move(position); _scalingOverride = scaling; UpdateScaling(true); - Resize(size, true, PlatformResizeReason.Layout); + Resize(size, true, WindowResizeReason.Layout); } private PixelSize ToPixelSize(Size size) => new PixelSize((int)(size.Width * RenderScaling), (int)(size.Height * RenderScaling)); - private void Resize(Size clientSize, bool force, PlatformResizeReason reason) + private void Resize(Size clientSize, bool force, WindowResizeReason reason) { if (!force && clientSize == ClientSize) return; diff --git a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs index d33f773bfa..8456dc92d0 100644 --- a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs +++ b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs @@ -74,7 +74,7 @@ namespace Avalonia.Browser surface.Size = new PixelSize((int)newSize.Width, (int)newSize.Height); } - Resized?.Invoke(newSize, PlatformResizeReason.User); + Resized?.Invoke(newSize, WindowResizeReason.User); (_insetsManager as BrowserInsetsManager)?.NotifySafeAreaPaddingChanged(); } @@ -241,7 +241,7 @@ namespace Avalonia.Browser public Action? SetCssCursor { get; set; } public Action? Input { get; set; } public Action? Paint { get; set; } - public Action? Resized { get; set; } + public Action? Resized { get; set; } public Action? ScalingChanged { get; set; } public Action? TransparencyLevelChanged { get; set; } public Action? Closed { get; set; } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index af4a70f128..ccc8cab8ae 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -65,7 +65,7 @@ using Avalonia.Rendering.Composition; public IEnumerable Surfaces { get; } public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } public Action TransparencyLevelChanged { get; set; } diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index 13eae1992c..24fd7e3933 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -108,7 +108,7 @@ namespace Avalonia.Win32.Interop.Wpf if (_finalSize == _previousSize) return finalSize; _previousSize = _finalSize; - _ttl.Resized?.Invoke(finalSize.ToAvaloniaSize(), PlatformResizeReason.Unspecified); + _ttl.Resized?.Invoke(finalSize.ToAvaloniaSize(), WindowResizeReason.Unspecified); return base.ArrangeOverride(finalSize); } @@ -229,7 +229,7 @@ namespace Avalonia.Win32.Interop.Wpf Action ITopLevelImpl.Input { get; set; } //TODO Action ITopLevelImpl.Paint { get; set; } - Action ITopLevelImpl.Resized { get; set; } + Action ITopLevelImpl.Resized { get; set; } Action ITopLevelImpl.ScalingChanged { get; set; } Action ITopLevelImpl.TransparencyLevelChanged { get; set; } diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs index 75c1a2d564..1470435134 100644 --- a/src/Windows/Avalonia.Win32/PopupImpl.cs +++ b/src/Windows/Avalonia.Win32/PopupImpl.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Platform; using Avalonia.Win32.Interop; @@ -135,7 +136,7 @@ namespace Avalonia.Win32 private void MoveResize(PixelPoint position, Size size, double scaling) { Move(position); - Resize(size, PlatformResizeReason.Layout); + Resize(size, WindowResizeReason.Layout); //TODO: We ignore the scaling override for now } diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index 05d9faa97b..8f9fc5fa80 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -212,7 +212,7 @@ namespace Avalonia.Win32 if (PlatformImpl is { } platformImpl) { platformImpl.Move(position); - platformImpl.Resize(size, PlatformResizeReason.Layout); + platformImpl.Resize(size, WindowResizeReason.Layout); } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 0cb8b09579..2a3255bb70 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -133,7 +133,7 @@ namespace Avalonia.Win32 _scaling = dpi / 96.0; ScalingChanged?.Invoke(_scaling); - using (SetResizeReason(PlatformResizeReason.DpiChange)) + using (SetResizeReason(WindowResizeReason.DpiChange)) { SetWindowPos(hWnd, IntPtr.Zero, @@ -611,7 +611,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_ENTERSIZEMOVE: - _resizeReason = PlatformResizeReason.User; + _resizeReason = WindowResizeReason.User; break; case WindowsMessage.WM_SIZE: @@ -658,7 +658,7 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_EXITSIZEMOVE: - _resizeReason = PlatformResizeReason.Unspecified; + _resizeReason = WindowResizeReason.Unspecified; break; case WindowsMessage.WM_MOVE: diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 545513c732..9217f42952 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -59,7 +59,7 @@ namespace Avalonia.Win32 private double _extendTitleBarHint = -1; private readonly bool _isUsingComposition; private readonly IBlurHost? _blurHost; - private PlatformResizeReason _resizeReason; + private WindowResizeReason _resizeReason; private MOUSEMOVEPOINT _lastWmMousePoint; #if USE_MANAGED_DRAG @@ -200,7 +200,7 @@ namespace Avalonia.Win32 public Action? Paint { get; set; } - public Action? Resized { get; set; } + public Action? Resized { get; set; } public Action? ScalingChanged { get; set; } @@ -588,7 +588,7 @@ namespace Avalonia.Win32 public IRenderer CreateRenderer(IRenderRoot root) => new CompositingRenderer(root, Win32Platform.Compositor, () => Surfaces); - public void Resize(Size value, PlatformResizeReason reason) + public void Resize(Size value, WindowResizeReason reason) { if (WindowState != WindowState.Normal) return; @@ -1053,7 +1053,7 @@ namespace Avalonia.Win32 _offScreenMargin = new Thickness(); _extendedMargins = new Thickness(); - Resize(new Size(rcWindow.Width / RenderScaling, rcWindow.Height / RenderScaling), PlatformResizeReason.Layout); + Resize(new Size(rcWindow.Width / RenderScaling, rcWindow.Height / RenderScaling), WindowResizeReason.Layout); unsafe { @@ -1462,7 +1462,7 @@ namespace Avalonia.Win32 /// public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0); - private ResizeReasonScope SetResizeReason(PlatformResizeReason reason) + private ResizeReasonScope SetResizeReason(WindowResizeReason reason) { var old = _resizeReason; _resizeReason = reason; @@ -1487,9 +1487,9 @@ namespace Avalonia.Win32 private struct ResizeReasonScope : IDisposable { private readonly WindowImpl _owner; - private readonly PlatformResizeReason _restore; + private readonly WindowResizeReason _restore; - public ResizeReasonScope(WindowImpl owner, PlatformResizeReason restore) + public ResizeReasonScope(WindowImpl owner, WindowResizeReason restore) { _owner = owner; _restore = restore; diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index ec6ea56d9d..6ca0cf7ace 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -150,7 +150,7 @@ namespace Avalonia.iOS public IEnumerable Surfaces { get; set; } public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } public Action TransparencyLevelChanged { get; set; } public Action Closed { get; set; } @@ -225,7 +225,7 @@ namespace Avalonia.iOS public override void LayoutSubviews() { - _topLevelImpl.Resized?.Invoke(_topLevelImpl.ClientSize, PlatformResizeReason.Layout); + _topLevelImpl.Resized?.Invoke(_topLevelImpl.ClientSize, WindowResizeReason.Layout); base.LayoutSubviews(); } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 34311949ef..765f2d1c19 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -830,7 +830,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } }; } - window.PlatformImpl?.Resize(new Size(700D, 500D), PlatformResizeReason.Unspecified); + window.PlatformImpl?.Resize(new Size(700D, 500D), WindowResizeReason.Unspecified); Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); Assert.True(raised); } @@ -886,7 +886,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } }; } - window.PlatformImpl?.Resize(new Size(700D, 500D), PlatformResizeReason.Unspecified); + window.PlatformImpl?.Resize(new Size(700D, 500D), WindowResizeReason.Unspecified); Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); Assert.False(raised); } diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index 62b5d889a8..0884dd306a 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -142,7 +142,7 @@ namespace Avalonia.Controls.UnitTests // The user has resized the window, so we can no longer auto-size. var target = new TestTopLevel(impl.Object); - impl.Object.Resized(new Size(100, 200), PlatformResizeReason.Unspecified); + impl.Object.Resized(new Size(100, 200), WindowResizeReason.Unspecified); Assert.Equal(100, target.Width); Assert.Equal(200, target.Height); diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index cada2bfa6f..b59f6e03f7 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -704,8 +704,8 @@ namespace Avalonia.Controls.UnitTests var clientSize = new Size(200, 200); var maxClientSize = new Size(480, 480); - windowImpl.Setup(x => x.Resize(It.IsAny(), It.IsAny())) - .Callback((size, reason) => + windowImpl.Setup(x => x.Resize(It.IsAny(), It.IsAny())) + .Callback((size, reason) => { clientSize = size.Constrain(maxClientSize); windowImpl.Object.Resized?.Invoke(clientSize, reason); @@ -853,7 +853,7 @@ namespace Avalonia.Controls.UnitTests target.PlatformImpl.ScalingChanged(1.5); target.PlatformImpl.Resized( new Size(210.66666666666666, 118.66666666666667), - PlatformResizeReason.DpiChange); + WindowResizeReason.DpiChange); Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent); } @@ -911,7 +911,7 @@ namespace Avalonia.Controls.UnitTests target.LayoutManager.ExecuteLayoutPass(); var windowImpl = Mock.Get(target.PlatformImpl); - windowImpl.Verify(x => x.Resize(new Size(410, 800), PlatformResizeReason.Application)); + windowImpl.Verify(x => x.Resize(new Size(410, 800), WindowResizeReason.Application)); Assert.Equal(410, target.Width); } } @@ -936,7 +936,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(400, target.Width); Assert.Equal(800, target.Height); - target.PlatformImpl.Resized(new Size(410, 800), PlatformResizeReason.User); + target.PlatformImpl.Resized(new Size(410, 800), WindowResizeReason.User); Assert.Equal(410, target.Width); Assert.Equal(800, target.Height); @@ -963,7 +963,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(400, target.Width); Assert.Equal(800, target.Height); - target.PlatformImpl.Resized(new Size(400, 810), PlatformResizeReason.User); + target.PlatformImpl.Resized(new Size(400, 810), WindowResizeReason.User); Assert.Equal(400, target.Width); Assert.Equal(810, target.Height); @@ -991,7 +991,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(400, target.Width); Assert.Equal(800, target.Height); - target.PlatformImpl.Resized(new Size(410, 810), PlatformResizeReason.Unspecified); + target.PlatformImpl.Resized(new Size(410, 810), WindowResizeReason.Unspecified); Assert.Equal(400, target.Width); Assert.Equal(800, target.Height); diff --git a/tests/Avalonia.UnitTests/CompositorTestServices.cs b/tests/Avalonia.UnitTests/CompositorTestServices.cs index 5ef09a4d0f..de7cbc873c 100644 --- a/tests/Avalonia.UnitTests/CompositorTestServices.cs +++ b/tests/Avalonia.UnitTests/CompositorTestServices.cs @@ -152,7 +152,7 @@ public class CompositorTestServices : IDisposable public IEnumerable Surfaces { get; } = new[] { new DummyFramebufferSurface() }; public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } public Action TransparencyLevelChanged { get; set; } diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 142a9cd8ee..ca71a97a6e 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -3,6 +3,7 @@ using Avalonia.Controls.Primitives.PopupPositioning; using Moq; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Controls; namespace Avalonia.UnitTests { @@ -54,8 +55,8 @@ namespace Avalonia.UnitTests windowImpl.Object.PositionChanged?.Invoke(x); }); - windowImpl.Setup(x => x.Resize(It.IsAny(), It.IsAny())) - .Callback((x, y) => + windowImpl.Setup(x => x.Resize(It.IsAny(), It.IsAny())) + .Callback((x, y) => { var constrainedSize = x.Constrain(s_screenSize); @@ -68,7 +69,7 @@ namespace Avalonia.UnitTests windowImpl.Setup(x => x.Show(true, It.IsAny())).Callback(() => { - windowImpl.Object.Resized?.Invoke(windowImpl.Object.ClientSize, PlatformResizeReason.Unspecified); + windowImpl.Object.Resized?.Invoke(windowImpl.Object.ClientSize, WindowResizeReason.Unspecified); windowImpl.Object.Activated?.Invoke(); }); @@ -87,7 +88,7 @@ namespace Avalonia.UnitTests { clientSize = size.Constrain(s_screenSize); popupImpl.Object.PositionChanged?.Invoke(pos); - popupImpl.Object.Resized?.Invoke(clientSize, PlatformResizeReason.Unspecified); + popupImpl.Object.Resized?.Invoke(clientSize, WindowResizeReason.Unspecified); }); var positioner = new ManagedPopupPositioner(positionerHelper); From cf52ab43cde367fc12ecad06340ce21b3df31fa7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 21 Apr 2023 14:03:34 +0200 Subject: [PATCH 033/110] Added WindowBase.TryGetPlatformHandle. #11062 made `WindowBase.PlatformImpl` internal so we need to expose a different want to get the window handle. --- src/Avalonia.Controls/WindowBase.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index d42244ba5c..c2523207e4 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -160,6 +160,15 @@ namespace Avalonia.Controls } } + /// + /// Trys to get the platform handle for the window. + /// + /// + /// An describing the window handle, or null if the handle + /// could not be retrieved. + /// + public IPlatformHandle? TryGetPlatformHandle() => PlatformImpl?.Handle; + /// /// Ensures that the window is initialized. /// From 4fbcfac67659e3c7725ed24f82080dba132aae11 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 21 Apr 2023 13:47:02 +0100 Subject: [PATCH 034/110] fix build. --- src/Avalonia.Native/PopupImpl.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index 516f8f99f1..6b7f7e8883 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Native.Interop; using Avalonia.Platform; @@ -29,7 +30,7 @@ namespace Avalonia.Native private void MoveResize(PixelPoint position, Size size, double scaling) { Position = position; - Resize(size, ResizeReason.Layout); + Resize(size, WindowResizeReason.Layout); //TODO: We ignore the scaling override for now } From 2f5586fad0b096a4e4053257c1ed65e8b73cb5aa Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 18 Apr 2023 08:09:51 +0000 Subject: [PATCH 035/110] make refresh visualizer PullDirection internal --- src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs index 8dc19eb1d4..530f28fbb6 100644 --- a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs @@ -38,7 +38,7 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty PullDirectionProperty = + internal static readonly StyledProperty PullDirectionProperty = AvaloniaProperty.Register(nameof(PullDirection), PullDirection.TopToBottom); /// From 845c220c153d2a1ce832ddd0f566b1779e9b94b0 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 21 Apr 2023 19:56:43 +0100 Subject: [PATCH 036/110] Add avalonia version property to nuget package. --- packages/Avalonia/Avalonia.csproj | 33 +++++++++++++++++++++++++++++++ packages/Avalonia/Avalonia.props | 1 + 2 files changed, 34 insertions(+) diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index f21d6fefb4..4cd4983b55 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -5,6 +5,7 @@ + all @@ -60,4 +61,36 @@ + + + + + + + + + + + + " + + "" + Version + "" + + ""); +]]> + + + + + + + $(IntermediateOutputPath)/AvaloniaVersion.props + + + + + true + build + + + diff --git a/packages/Avalonia/Avalonia.props b/packages/Avalonia/Avalonia.props index 2334aa91bc..37263a07ed 100644 --- a/packages/Avalonia/Avalonia.props +++ b/packages/Avalonia/Avalonia.props @@ -7,6 +7,7 @@ + From 6e2dc88622d791b405dea9459003e7b20e24af95 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 21 Apr 2023 20:10:24 +0100 Subject: [PATCH 037/110] add UsedAvaloniaProducts property --- packages/Avalonia/Avalonia.props | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/Avalonia/Avalonia.props b/packages/Avalonia/Avalonia.props index 37263a07ed..a2da228887 100644 --- a/packages/Avalonia/Avalonia.props +++ b/packages/Avalonia/Avalonia.props @@ -4,6 +4,7 @@ $(MSBuildThisFileDirectory)\..\tools\net461\designer\Avalonia.Designer.HostApp.exe $(MSBuildThisFileDirectory)\..\tools\netstandard2.0\Avalonia.Build.Tasks.dll false + $(UsedAvaloniaProducts);AvaloniaUI From 6b13c81527796fd1be7792badd0ce1faadf6e222 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 22 Apr 2023 00:42:43 +0200 Subject: [PATCH 038/110] Raise WindowBase.Resized before SizeChanged. --- src/Avalonia.Controls/WindowBase.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index c2523207e4..463eda4f3c 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -232,14 +232,16 @@ namespace Avalonia.Controls { FrameSize = PlatformImpl?.FrameSize; - if (ClientSize != clientSize) + var clientSizeChanged = ClientSize != clientSize; + + ClientSize = clientSize; + OnResized(new WindowResizedEventArgs(clientSize, reason)); + + if (clientSizeChanged) { - ClientSize = clientSize; LayoutManager.ExecuteLayoutPass(); Renderer.Resized(clientSize); } - - OnResized(new WindowResizedEventArgs(clientSize, reason)); } /// From 1ff87d8dd59819dc937ab9d601f9e67de43f496c Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Sat, 22 Apr 2023 02:04:59 +0300 Subject: [PATCH 039/110] [Input] [MacOS] fix timestamp overflow on objC side --- native/Avalonia.Native/src/OSX/AvnView.mm | 2 +- native/Avalonia.Native/src/OSX/AvnWindow.mm | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index 6d1ff7cf12..9a43d9ddcf 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -275,7 +275,7 @@ delta.Y = [event deltaY]; } - uint32 timestamp = static_cast([event timestamp] * 1000); + uint64 timestamp = static_cast([event timestamp] * 1000); auto modifiers = [self getModifiers:[event modifierFlags]]; if(type != Move || diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index 16e1486acc..ef50cdab84 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -460,7 +460,7 @@ auto point = [self translateLocalPoint:avnPoint]; AvnVector delta = { 0, 0 }; - _parent->BaseEvents->RawMouseEvent(NonClientLeftButtonDown, static_cast([event timestamp] * 1000), AvnInputModifiersNone, point, delta); + _parent->BaseEvents->RawMouseEvent(NonClientLeftButtonDown, static_cast([event timestamp] * 1000), AvnInputModifiersNone, point, delta); } if(!_isTransitioningToFullScreen) From 1bce8de6867a125cb46d8c1b2f5c24a0240d0477 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 21 Apr 2023 22:09:25 -0400 Subject: [PATCH 040/110] Fix merge conflict --- src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index b83de21ac4..cefb6772c9 100644 --- a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using Avalonia.Controls.Platform; using Avalonia.Reactive; using Avalonia.Input; using Avalonia.Input.Platform; From bf8bf14c549745a2a5ac495ad743a3e544b915b2 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sun, 23 Apr 2023 17:31:47 +0100 Subject: [PATCH 041/110] write the version number more simply. --- packages/Avalonia/Avalonia.csproj | 35 +++++-------------------------- 1 file changed, 5 insertions(+), 30 deletions(-) diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index 4cd4983b55..8d56b557b3 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -5,7 +5,7 @@ - + all @@ -62,35 +62,10 @@ - - - - - - - - - - - " + - "" + Version + "" + - ""); -]]> - - - - - - $(IntermediateOutputPath)/AvaloniaVersion.props - - - - - true - build - - + From 51ba74fb4687a06daf9fef24ca950c2e0066e438 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sun, 23 Apr 2023 17:57:09 +0100 Subject: [PATCH 042/110] fix build. --- packages/Avalonia/Avalonia.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index 8d56b557b3..39120f64f5 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -63,6 +63,9 @@ + + $(IntermediateOutputPath)/AvaloniaVersion.props + Date: Sun, 23 Apr 2023 18:29:28 +0100 Subject: [PATCH 043/110] fix including version file in nuget package. --- packages/Avalonia/Avalonia.csproj | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index 39120f64f5..db5721b0ab 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -70,5 +70,11 @@ File="$(PackageVersionPropsPath)" Overwrite="true" Lines="<Project><PropertyGroup><AvaloniaMainPackageVersion>$(PackageVersion)</AvaloniaMainPackageVersion></PropertyGroup></Project>" /> + + + true + build + + From d136d03fd1badabb9161f532b5a64df6bd26c5e4 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Sun, 23 Apr 2023 22:24:17 +0200 Subject: [PATCH 044/110] Fix slnf Headless path --- Avalonia.Desktop.slnf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 76620e8b93..73e38f8cb9 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -23,8 +23,6 @@ "src\\Avalonia.Dialogs\\Avalonia.Dialogs.csproj", "src\\Avalonia.Fonts.Inter\\Avalonia.Fonts.Inter.csproj", "src\\Avalonia.FreeDesktop\\Avalonia.FreeDesktop.csproj", - "src\\Avalonia.Headless.Vnc\\Avalonia.Headless.Vnc.csproj", - "src\\Avalonia.Headless\\Avalonia.Headless.csproj", "src\\Avalonia.MicroCom\\Avalonia.MicroCom.csproj", "src\\Avalonia.Native\\Avalonia.Native.csproj", "src\\Avalonia.OpenGL\\Avalonia.OpenGL.csproj", @@ -33,6 +31,8 @@ "src\\Avalonia.Themes.Fluent\\Avalonia.Themes.Fluent.csproj", "src\\Avalonia.Themes.Simple\\Avalonia.Themes.Simple.csproj", "src\\Avalonia.X11\\Avalonia.X11.csproj", + "src\\Headless\\Avalonia.Headless.Vnc\\Avalonia.Headless.Vnc.csproj", + "src\\Headless\\Avalonia.Headless\\Avalonia.Headless.csproj", "src\\Linux\\Avalonia.LinuxFramebuffer\\Avalonia.LinuxFramebuffer.csproj", "src\\Markup\\Avalonia.Markup.Xaml.Loader\\Avalonia.Markup.Xaml.Loader.csproj", "src\\Markup\\Avalonia.Markup.Xaml\\Avalonia.Markup.Xaml.csproj", @@ -65,4 +65,4 @@ "tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj" ] } -} \ No newline at end of file +} From 1117332e4d6e83244f8c3dbf44d4f2f10f4d4005 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Sun, 23 Apr 2023 22:24:56 +0200 Subject: [PATCH 045/110] Add AccentColor support for KDE --- .../DBusPlatformSettings.cs | 78 ++++++++++++++----- 1 file changed, 57 insertions(+), 21 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs index a25bb68458..2b124499b3 100644 --- a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs +++ b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Avalonia.Logging; +using Avalonia.Media; using Avalonia.Platform; using Tmds.DBus.SourceGenerator; @@ -9,7 +10,10 @@ namespace Avalonia.FreeDesktop internal class DBusPlatformSettings : DefaultPlatformSettings { private readonly OrgFreedesktopPortalSettings? _settings; + private PlatformColorValues? _lastColorValues; + private PlatformThemeVariant? _themeVariant; + private Color? _accentColor; public DBusPlatformSettings() { @@ -21,24 +25,33 @@ namespace Avalonia.FreeDesktop _ = TryGetInitialValueAsync(); } - public override PlatformColorValues GetColorValues() - { - return _lastColorValues ?? base.GetColorValues(); - } + public override PlatformColorValues GetColorValues() => _lastColorValues ?? base.GetColorValues(); private async Task TryGetInitialValueAsync() { try { var value = await _settings!.ReadAsync("org.freedesktop.appearance", "color-scheme"); - _lastColorValues = GetColorValuesFromSetting(value); - OnColorValuesChanged(_lastColorValues); + _themeVariant = ReadAsColorScheme(value); + } + catch (Exception ex) + { + Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, "Unable to get org.freedesktop.appearance.color-scheme value", ex); + } + + try + { + var value = await _settings!.ReadAsync("org.kde.kdeglobals.General", "AccentColor"); + _accentColor = ReadAsAccentColor(value); } catch (Exception ex) { - _lastColorValues = base.GetColorValues(); - Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, "Unable to get setting value", ex); + Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, "Unable to get org.kde.kdeglobals.General.AccentColor value", ex); } + + _lastColorValues = BuildPlatformColorValues(); + if (_lastColorValues is not null) + OnColorValuesChanged(_lastColorValues); } private void SettingsChangedHandler(Exception? exception, (string @namespace, string key, DBusVariantItem value) valueTuple) @@ -46,25 +59,48 @@ namespace Avalonia.FreeDesktop if (exception is not null) return; - if (valueTuple is ("org.freedesktop.appearance", "color-scheme", { } value)) + switch (valueTuple) { - /* - 0: No preference - 1: Prefer dark appearance - 2: Prefer light appearance - */ - _lastColorValues = GetColorValuesFromSetting(value); - OnColorValuesChanged(_lastColorValues); + case ("org.freedesktop.appearance", "color-scheme", { } colorScheme): + /* + 0: No preference + 1: Prefer dark appearance + 2: Prefer light appearance + */ + _themeVariant = ReadAsColorScheme(colorScheme); + _lastColorValues = BuildPlatformColorValues(); + OnColorValuesChanged(_lastColorValues!); + break; + case ("org.kde.kdeglobals.General", "AccentColor", { } accentColor): + _accentColor = ReadAsAccentColor(accentColor); + _lastColorValues = BuildPlatformColorValues(); + OnColorValuesChanged(_lastColorValues!); + break; } } - private static PlatformColorValues GetColorValuesFromSetting(DBusVariantItem value) + private PlatformColorValues? BuildPlatformColorValues() + { + if (_themeVariant is { } themeVariant && _accentColor is { } accentColor) + return new PlatformColorValues { ThemeVariant = themeVariant, AccentColor1 = accentColor }; + if (_themeVariant is { } themeVariant1) + return new PlatformColorValues { ThemeVariant = themeVariant1 }; + if (_accentColor is { } accentColor1) + return new PlatformColorValues { AccentColor1 = accentColor1 }; + return null; + } + + private static PlatformThemeVariant ReadAsColorScheme(DBusVariantItem value) { var isDark = ((value.Value as DBusVariantItem)!.Value as DBusUInt32Item)!.Value == 1; - return new PlatformColorValues - { - ThemeVariant = isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light - }; + return isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light; + } + + private static Color ReadAsAccentColor(DBusVariantItem value) + { + var colorStr = ((value.Value as DBusVariantItem)!.Value as DBusStringItem)!.Value; + var rgb = colorStr.Split(','); + return new Color(255, byte.Parse(rgb[0]), byte.Parse(rgb[1]), byte.Parse(rgb[2])); } } } From f77b67db97487825d172401f118e205c6bae0510 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Sun, 23 Apr 2023 22:32:51 +0200 Subject: [PATCH 046/110] Move comment --- src/Avalonia.FreeDesktop/DBusPlatformSettings.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs index 2b124499b3..8b2b38bb82 100644 --- a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs +++ b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs @@ -62,11 +62,6 @@ namespace Avalonia.FreeDesktop switch (valueTuple) { case ("org.freedesktop.appearance", "color-scheme", { } colorScheme): - /* - 0: No preference - 1: Prefer dark appearance - 2: Prefer light appearance - */ _themeVariant = ReadAsColorScheme(colorScheme); _lastColorValues = BuildPlatformColorValues(); OnColorValuesChanged(_lastColorValues!); @@ -92,6 +87,11 @@ namespace Avalonia.FreeDesktop private static PlatformThemeVariant ReadAsColorScheme(DBusVariantItem value) { + /* + 0: No preference + 1: Prefer dark appearance + 2: Prefer light appearance + */ var isDark = ((value.Value as DBusVariantItem)!.Value as DBusUInt32Item)!.Value == 1; return isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light; } From 3c85bcc15e9f8a3693c271b142c45f518104a3f9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sun, 23 Apr 2023 21:41:58 +0100 Subject: [PATCH 047/110] update. --- packages/Avalonia/Avalonia.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index db5721b0ab..a024e0155a 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -5,7 +5,7 @@ - + all From a507e92b31f24bc5cf88bedea89f47eb93c2ce42 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 24 Apr 2023 02:00:23 -0400 Subject: [PATCH 048/110] Fix theme-dependend markup extensions not knowing current theme context --- .../Controls/IResourceDictionary.cs | 2 +- .../Controls/IThemeVariantProvider.cs | 22 +++ .../Controls/ResourceDictionary.cs | 12 +- .../Controls/ResourceNodeExtensions.cs | 46 +++--- .../Styling/IThemeVariantHost.cs | 1 - .../AvaloniaXamlIlCompiler.cs | 3 +- ...iaXamlIlThemeVariantProviderTransformer.cs | 31 ++++ .../AvaloniaXamlIlWellKnownTypes.cs | 2 + .../DynamicResourceExtension.cs | 6 +- .../StaticResourceExtension.cs | 22 ++- .../Styling/ResourceInclude.cs | 4 +- .../Themes/FluentBenchmark.cs | 22 ++- .../Themes/ThemeBenchmark.cs | 4 +- .../Xaml/ThemeDictionariesTests.cs | 136 +++++++++++++++++- 14 files changed, 271 insertions(+), 42 deletions(-) create mode 100644 src/Avalonia.Base/Controls/IThemeVariantProvider.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs diff --git a/src/Avalonia.Base/Controls/IResourceDictionary.cs b/src/Avalonia.Base/Controls/IResourceDictionary.cs index 2bd1f65638..6712498bf4 100644 --- a/src/Avalonia.Base/Controls/IResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/IResourceDictionary.cs @@ -18,6 +18,6 @@ namespace Avalonia.Controls /// /// Gets a collection of merged resource dictionaries that are specifically keyed and composed to address theme scenarios. /// - IDictionary ThemeDictionaries { get; } + IDictionary ThemeDictionaries { get; } } } diff --git a/src/Avalonia.Base/Controls/IThemeVariantProvider.cs b/src/Avalonia.Base/Controls/IThemeVariantProvider.cs new file mode 100644 index 0000000000..d1dca2efbf --- /dev/null +++ b/src/Avalonia.Base/Controls/IThemeVariantProvider.cs @@ -0,0 +1,22 @@ +using Avalonia.Metadata; +using Avalonia.Styling; + +namespace Avalonia.Controls; + +/// +/// Resource provider with theme variant awareness. +/// Can be used with . +/// +/// +/// This is a helper interface for the XAML compiler to make Key property accessibly by the markup extensions. +/// Which means, it can only be used with ResourceDictionaries and markup extensions in the XAML code. +/// This API might be removed in the future minor updates. +/// +[Unstable] +public interface IThemeVariantProvider : IResourceProvider +{ + /// + /// Key property set by the compiler. + /// + ThemeVariant? Key { get; set; } +} diff --git a/src/Avalonia.Base/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs index 231a19baab..b928cf0672 100644 --- a/src/Avalonia.Base/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/ResourceDictionary.cs @@ -13,13 +13,13 @@ namespace Avalonia.Controls /// /// An indexed dictionary of resources. /// - public class ResourceDictionary : IResourceDictionary + public class ResourceDictionary : IResourceDictionary, IThemeVariantProvider { private object? lastDeferredItemKey; private Dictionary? _inner; private IResourceHost? _owner; private AvaloniaList? _mergedDictionaries; - private AvaloniaDictionary? _themeDictionary; + private AvaloniaDictionary? _themeDictionary; /// /// Initializes a new instance of the class. @@ -93,13 +93,13 @@ namespace Avalonia.Controls } } - public IDictionary ThemeDictionaries + public IDictionary ThemeDictionaries { get { if (_themeDictionary == null) { - _themeDictionary = new AvaloniaDictionary(2); + _themeDictionary = new AvaloniaDictionary(2); _themeDictionary.ForEachItem( (_, x) => { @@ -120,6 +120,8 @@ namespace Avalonia.Controls return _themeDictionary; } } + + ThemeVariant? IThemeVariantProvider.Key { get; set; } bool IResourceNode.HasResources { @@ -192,7 +194,7 @@ namespace Avalonia.Controls if (_themeDictionary is not null) { - IResourceProvider? themeResourceProvider; + IThemeVariantProvider? themeResourceProvider; if (theme is not null && theme != ThemeVariant.Default) { if (_themeDictionary.TryGetValue(theme, out themeResourceProvider) diff --git a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs index 8aed1545a5..382ebac0e3 100644 --- a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs +++ b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs @@ -119,7 +119,19 @@ namespace Avalonia.Controls resourceProvider = resourceProvider ?? throw new ArgumentNullException(nameof(resourceProvider)); key = key ?? throw new ArgumentNullException(nameof(key)); - return new FloatingResourceObservable(resourceProvider, key, converter); + return new FloatingResourceObservable(resourceProvider, key, null, converter); + } + + public static IObservable GetResourceObservable( + this IResourceProvider resourceProvider, + object key, + ThemeVariant? defaultThemeVariant, + Func? converter = null) + { + resourceProvider = resourceProvider ?? throw new ArgumentNullException(nameof(resourceProvider)); + key = key ?? throw new ArgumentNullException(nameof(key)); + + return new FloatingResourceObservable(resourceProvider, key, defaultThemeVariant, converter); } private class ResourceObservable : LightweightObservableBase @@ -128,7 +140,10 @@ namespace Avalonia.Controls private readonly object _key; private readonly Func? _converter; - public ResourceObservable(IResourceHost target, object key, Func? converter) + public ResourceObservable( + IResourceHost target, + object key, + Func? converter) { _target = target; _key = key; @@ -170,11 +185,8 @@ namespace Avalonia.Controls private object? GetValue() { - if (_target is not IThemeVariantHost themeVariantHost - || !_target.TryFindResource(_key, themeVariantHost.ActualThemeVariant, out var value)) - { - value = _target.FindResource(_key) ?? AvaloniaProperty.UnsetValue; - } + var theme = (_target as IThemeVariantHost)?.ActualThemeVariant; + var value = _target.FindResource(theme, _key) ?? AvaloniaProperty.UnsetValue; return _converter?.Invoke(value) ?? value; } @@ -183,14 +195,20 @@ namespace Avalonia.Controls private class FloatingResourceObservable : LightweightObservableBase { private readonly IResourceProvider _target; + private readonly ThemeVariant? _overrideThemeVariant; private readonly object _key; private readonly Func? _converter; private IResourceHost? _owner; - public FloatingResourceObservable(IResourceProvider target, object key, Func? converter) + public FloatingResourceObservable( + IResourceProvider target, + object key, + ThemeVariant? overrideThemeVariant, + Func? converter) { _target = target; _key = key; + _overrideThemeVariant = overrideThemeVariant; _converter = converter; } @@ -233,7 +251,7 @@ namespace Avalonia.Controls { _owner.ResourcesChanged -= ResourcesChanged; } - if (_owner is IThemeVariantHost themeVariantHost) + if (_overrideThemeVariant is null && _owner is IThemeVariantHost themeVariantHost) { themeVariantHost.ActualThemeVariantChanged += ActualThemeVariantChanged; } @@ -244,12 +262,11 @@ namespace Avalonia.Controls { _owner.ResourcesChanged += ResourcesChanged; } - if (_owner is IThemeVariantHost themeVariantHost2) + if (_overrideThemeVariant is null && _owner is IThemeVariantHost themeVariantHost2) { themeVariantHost2.ActualThemeVariantChanged -= ActualThemeVariantChanged; } - PublishNext(); } @@ -265,11 +282,8 @@ namespace Avalonia.Controls private object? GetValue() { - if (!(_target.Owner is IThemeVariantHost themeVariantHost) - || !_target.Owner.TryFindResource(_key, themeVariantHost.ActualThemeVariant, out var value)) - { - value = _target.Owner?.FindResource(_key) ?? AvaloniaProperty.UnsetValue; - } + var theme = _overrideThemeVariant ?? (_target.Owner as IThemeVariantHost)?.ActualThemeVariant; + var value = _target.Owner?.FindResource(theme, _key) ?? AvaloniaProperty.UnsetValue; return _converter?.Invoke(value) ?? value; } diff --git a/src/Avalonia.Base/Styling/IThemeVariantHost.cs b/src/Avalonia.Base/Styling/IThemeVariantHost.cs index 01583148a8..740887970b 100644 --- a/src/Avalonia.Base/Styling/IThemeVariantHost.cs +++ b/src/Avalonia.Base/Styling/IThemeVariantHost.cs @@ -7,7 +7,6 @@ namespace Avalonia.Styling; /// /// Interface for the host element with a theme variant. /// -[Unstable] public interface IThemeVariantHost : IResourceHost { /// diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 5ca2b09eba..23c67df810 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -58,7 +58,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions new AvaloniaXamlIlSetterTransformer(), new AvaloniaXamlIlConstructorServiceProviderTransformer(), new AvaloniaXamlIlTransitionsTypeMetadataTransformer(), - new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer() + new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer(), + new AvaloniaXamlIlThemeVariantProviderTransformer() ); InsertBefore( new AvaloniaXamlIlOptionMarkupExtensionTransformer()); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs new file mode 100644 index 0000000000..05df8be1b6 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs @@ -0,0 +1,31 @@ +using System.Linq; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; + +internal class AvaloniaXamlIlThemeVariantProviderTransformer : IXamlAstTransformer +{ + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + var type = context.GetAvaloniaTypes().IThemeVariantProvider; + if (!(node is XamlAstObjectNode on + && type.IsAssignableFrom(on.Type.GetClrType()))) + return node; + + var keyDirective = on.Children.FirstOrDefault(n => n is XamlAstXmlDirective d + && d.Namespace == XamlNamespaces.Xaml2006 && + d.Name == "Key") as XamlAstXmlDirective; + if (keyDirective is null) + return node; + + var keyProp = type.Properties.First(p => p.Name == "Key"); + on.Children.Add(new XamlAstXamlPropertyValueNode(keyDirective, + new XamlAstClrProperty(keyDirective, keyProp, context.Configuration), + keyDirective.Values, true)); + + return node; + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 63683da0db..8ab84f4615 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -110,6 +110,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType IResourceDictionary { get; } public IXamlType ResourceDictionary { get; } public IXamlMethod ResourceDictionaryDeferredAdd { get; } + public IXamlType IThemeVariantProvider { get; } public IXamlType UriKind { get; } public IXamlConstructor UriConstructor { get; } public IXamlType Style { get; } @@ -250,6 +251,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers cfg.TypeSystem.GetType("System.Func`2").MakeGenericType( cfg.TypeSystem.GetType("System.IServiceProvider"), XamlIlTypes.Object)); + IThemeVariantProvider = cfg.TypeSystem.GetType("Avalonia.Controls.IThemeVariantProvider"); UriKind = cfg.TypeSystem.GetType("System.UriKind"); UriConstructor = Uri.GetConstructor(new List() { cfg.WellKnownTypes.String, UriKind }); Style = cfg.TypeSystem.GetType("Avalonia.Styling.Style"); diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs index e1b594e331..7f52c872ed 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs @@ -3,6 +3,7 @@ using Avalonia.Controls; using Avalonia.Data; using Avalonia.Markup.Xaml.Converters; using Avalonia.Media; +using Avalonia.Styling; namespace Avalonia.Markup.Xaml.MarkupExtensions { @@ -10,6 +11,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions { private object? _anchor; private BindingPriority _priority; + private ThemeVariant? _currentThemeVariant; public DynamicResourceExtension() { @@ -36,6 +38,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions (object?)serviceProvider.GetFirstParent(); } + _currentThemeVariant = StaticResourceExtension.GetDictionaryVariant(serviceProvider); + return this; } @@ -59,7 +63,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions } else if (_anchor is IResourceProvider resourceProvider) { - var source = resourceProvider.GetResourceObservable(ResourceKey, GetConverter(targetProperty)); + var source = resourceProvider.GetResourceObservable(ResourceKey, _currentThemeVariant, GetConverter(targetProperty)); return InstancedBinding.OneWay(source, _priority); } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index 3de669b1e4..c23c31e24c 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -33,7 +33,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions var provideTarget = serviceProvider.GetService(); var targetObject = provideTarget?.TargetObject; var targetProperty = provideTarget?.TargetProperty; - var themeVariant = (targetObject as IThemeVariantHost)?.ActualThemeVariant; + var themeVariant = (targetObject as IThemeVariantHost)?.ActualThemeVariant + ?? GetDictionaryVariant(serviceProvider); var targetType = targetProperty switch { @@ -78,6 +79,25 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions { return ColorToBrushConverter.Convert(control.FindResource(ResourceKey!), targetType); } + + internal static ThemeVariant? GetDictionaryVariant(IServiceProvider serviceProvider) + { + var parents = serviceProvider.GetService()?.Parents; + if (parents is null) + { + return null; + } + + foreach (var parent in parents) + { + if (parent is IThemeVariantProvider { Key: { } setKey }) + { + return setKey; + } + } + + return null; + } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs index fbcfdde565..eee02ea0d8 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs @@ -13,7 +13,7 @@ namespace Avalonia.Markup.Xaml.Styling /// When used in runtime, this type might be unsafe with trimming and AOT. /// [RequiresUnreferencedCode(TrimmingMessages.StyleResourceIncludeRequiresUnreferenceCodeMessage)] - public class ResourceInclude : IResourceProvider + public class ResourceInclude : IResourceProvider, IThemeVariantProvider { private readonly IServiceProvider? _serviceProvider; private readonly Uri? _baseUri; @@ -65,6 +65,8 @@ namespace Avalonia.Markup.Xaml.Styling /// public Uri? Source { get; set; } + ThemeVariant? IThemeVariantProvider.Key { get; set; } + bool IResourceNode.HasResources => Loaded.HasResources; public event EventHandler? OwnerChanged diff --git a/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs b/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs index e9b82d5381..8eadb3a3f0 100644 --- a/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs @@ -1,7 +1,9 @@ using System; +using System.Runtime.CompilerServices; using Avalonia.Controls; using Avalonia.Platform; using Avalonia.Styling; +using Avalonia.Threading; using Avalonia.UnitTests; using BenchmarkDotNet.Attributes; using Moq; @@ -30,27 +32,23 @@ namespace Avalonia.Benchmarks.Themes _app.Dispose(); } - [Benchmark] - public void RepeatButton() + [Benchmark()] + [MethodImpl(MethodImplOptions.NoInlining)] + public void CreateButton() { - var button = new RepeatButton(); + var button = new Button(); _root.Child = button; _root.LayoutManager.ExecuteLayoutPass(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); } private static IDisposable CreateApp() { var services = new TestServices( - assetLoader: new AssetLoader(), - globalClock: new MockGlobalClock(), - platform: new AppBuilder().RuntimePlatform, - renderInterface: new MockPlatformRenderInterface(), - standardCursorFactory: Mock.Of(), - theme: () => LoadFluentTheme(), + renderInterface: new NullRenderingPlatform(), dispatcherImpl: new NullThreadingPlatform(), - fontManagerImpl: new MockFontManagerImpl(), - textShaperImpl: new MockTextShaperImpl(), - windowingPlatform: new MockWindowingPlatform()); + standardCursorFactory: new NullCursorFactory(), + theme: () => LoadFluentTheme()); return UnitTestApplication.Start(services); } diff --git a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs index 7c0a3f8bdf..ac174e4bc2 100644 --- a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs @@ -1,5 +1,5 @@ using System; - +using System.Runtime.CompilerServices; using Avalonia.Controls; using Avalonia.Markup.Xaml.Styling; using Avalonia.Platform; @@ -29,6 +29,7 @@ namespace Avalonia.Benchmarks.Themes } [Benchmark] + [MethodImpl(MethodImplOptions.NoInlining)] public bool InitFluentTheme() { UnitTestApplication.Current.Styles[0] = new FluentTheme(); @@ -36,6 +37,7 @@ namespace Avalonia.Benchmarks.Themes } [Benchmark] + [MethodImpl(MethodImplOptions.NoInlining)] public bool InitSimpleTheme() { UnitTestApplication.Current.Styles[0] = new SimpleTheme(); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs index 3ac4677694..2def84bb18 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs @@ -1,9 +1,12 @@ -using System.Linq; +using System; +using System.Linq; using Avalonia.Controls; +using Avalonia.Controls.Documents; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Markup.Data; using Avalonia.Markup.Xaml.MarkupExtensions; +using Avalonia.Markup.Xaml.Templates; using Avalonia.Media; using Avalonia.Styling; using Avalonia.UnitTests; @@ -140,7 +143,7 @@ public class ThemeDictionariesTests : XamlTestBase Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } - [Fact(Skip = "Not implemented")] + [Fact] public void StaticResource_Inside_Of_ThemeDictionaries_Should_Use_Same_Theme_Key() { var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" @@ -183,6 +186,135 @@ public class ThemeDictionariesTests : XamlTestBase Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } + + [Fact] + public void StaticResource_Inside_Of_ThemeDictionaries_Should_Use_Same_Theme_Key_From_Inner_File() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Inner.xaml"), @" + + +"), + new RuntimeXamlLoaderDocument(@" + + + + Green + + + + + + White + + + + + +") + }; + + var parsed = AvaloniaRuntimeXamlLoader.LoadGroup(documents); + var dictionary = (ResourceDictionary)parsed[1]!; + + dictionary.TryGetResource("InnerKey", ThemeVariant.Dark, out var resource); + var colorResource = Assert.IsType(resource); + Assert.Equal(Colors.White, colorResource); + + dictionary.TryGetResource("InnerKey", ThemeVariant.Light, out resource); + colorResource = Assert.IsType(resource); + Assert.Equal(Colors.Green, colorResource); + } + + [Fact] + public void DynamicResource_Inside_Of_ThemeDictionaries_Should_Use_Same_Theme_Key_From_Inner_File() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Inner.xaml"), @" + + +"), + new RuntimeXamlLoaderDocument(@" + + + + Green + + + + + + White + + + + + +") + }; + + var parsed = AvaloniaRuntimeXamlLoader.LoadGroup(documents); + var dictionary1 = (ResourceDictionary)parsed[0]!; + var dictionary2 = (ResourceDictionary)parsed[1]!; + var ownerApp = new Application(); // DynamicResource needs an owner to work + ownerApp.RequestedThemeVariant = new ThemeVariant("FakeOne", null); + ownerApp.Resources.MergedDictionaries.Add(dictionary1); + ownerApp.Resources.MergedDictionaries.Add(dictionary2); + + dictionary2.TryGetResource("InnerKey", ThemeVariant.Dark, out var resource); + var colorResource = Assert.IsAssignableFrom(resource); + Assert.Equal(Colors.White, colorResource.Color); + + dictionary2.TryGetResource("InnerKey", ThemeVariant.Light, out resource); + colorResource = Assert.IsAssignableFrom(resource); + Assert.Equal(Colors.Green, colorResource.Color); + } + + [Fact] + public void DynamicResource_Inside_Control_Inside_Of_ThemeDictionaries_Should_Use_Control_Theme_Variant() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(@" + + + + Green + + + + White + + + +") + }; + + var parsed = AvaloniaRuntimeXamlLoader.LoadGroup(documents); + var dictionary = (ResourceDictionary)parsed[0]!; + + dictionary.TryGetResource("Template", ThemeVariant.Dark, out var resource); + var control = Assert.IsType((resource as Template)?.Build()); + control.Resources.MergedDictionaries.Add(dictionary); + Assert.Equal(Colors.Green, ((ISolidColorBrush)control[TextElement.ForegroundProperty]!).Color); + control.Resources.MergedDictionaries.Remove(dictionary); + + dictionary.TryGetResource("Template", ThemeVariant.Light, out resource); + control = Assert.IsType((resource as Template)?.Build()); + control.Resources.MergedDictionaries.Add(dictionary); + Assert.Equal(Colors.White, ((ISolidColorBrush)control[TextElement.ForegroundProperty]!).Color); + } [Fact] public void StaticResource_Outside_Of_Dictionaries_Should_Use_Control_ThemeVariant() From a09c182e89245160b64677f369216d56711faeb9 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 21 Apr 2023 22:37:16 -0400 Subject: [PATCH 049/110] Implement SystemAccentColors backed by the actual system values --- samples/ControlCatalog/App.xaml | 2 - samples/ControlCatalog/MainView.xaml.cs | 48 ------ .../Accents/AccentColors.xaml | 12 -- .../Accents/SystemAccentColors.cs | 163 ++++++++++++++++++ src/Avalonia.Themes.Fluent/FluentTheme.xaml | 2 +- .../XamlMergeResourceGroupTransformer.cs | 12 -- 6 files changed, 164 insertions(+), 75 deletions(-) delete mode 100644 src/Avalonia.Themes.Fluent/Accents/AccentColors.xaml create mode 100644 src/Avalonia.Themes.Fluent/Accents/SystemAccentColors.cs diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 3b847adcbb..64bf3e53b3 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -26,8 +26,6 @@ #FFFFFFFF - #FF0078D7 - #FF005A9E diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 9c439c874f..9c511f9eb0 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -19,13 +19,9 @@ namespace ControlCatalog { public class MainView : UserControl { - private readonly IPlatformSettings _platformSettings; - public MainView() { AvaloniaXamlLoader.Load(this); - _platformSettings = AvaloniaLocator.Current.GetRequiredService(); - PlatformSettingsOnColorValuesChanged(_platformSettings, _platformSettings.GetColorValues()); var sideBar = this.Get("Sidebar"); @@ -141,50 +137,6 @@ namespace ControlCatalog ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? true; }; } - - _platformSettings.ColorValuesChanged += PlatformSettingsOnColorValuesChanged; - PlatformSettingsOnColorValuesChanged(_platformSettings, _platformSettings.GetColorValues()); - } - - protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) - { - base.OnDetachedFromLogicalTree(e); - - _platformSettings.ColorValuesChanged -= PlatformSettingsOnColorValuesChanged; - } - - private void PlatformSettingsOnColorValuesChanged(object? sender, PlatformColorValues e) - { - Application.Current!.Resources["SystemAccentColor"] = e.AccentColor1; - Application.Current.Resources["SystemAccentColorDark1"] = ChangeColorLuminosity(e.AccentColor1, -0.3); - Application.Current.Resources["SystemAccentColorDark2"] = ChangeColorLuminosity(e.AccentColor1, -0.5); - Application.Current.Resources["SystemAccentColorDark3"] = ChangeColorLuminosity(e.AccentColor1, -0.7); - Application.Current.Resources["SystemAccentColorLight1"] = ChangeColorLuminosity(e.AccentColor1, 0.3); - Application.Current.Resources["SystemAccentColorLight2"] = ChangeColorLuminosity(e.AccentColor1, 0.5); - Application.Current.Resources["SystemAccentColorLight3"] = ChangeColorLuminosity(e.AccentColor1, 0.7); - - static Color ChangeColorLuminosity(Color color, double luminosityFactor) - { - var red = (double)color.R; - var green = (double)color.G; - var blue = (double)color.B; - - if (luminosityFactor < 0) - { - luminosityFactor = 1 + luminosityFactor; - red *= luminosityFactor; - green *= luminosityFactor; - blue *= luminosityFactor; - } - else if (luminosityFactor >= 0) - { - red = (255 - red) * luminosityFactor + red; - green = (255 - green) * luminosityFactor + green; - blue = (255 - blue) * luminosityFactor + blue; - } - - return new Color(color.A, (byte)red, (byte)green, (byte)blue); - } } } } diff --git a/src/Avalonia.Themes.Fluent/Accents/AccentColors.xaml b/src/Avalonia.Themes.Fluent/Accents/AccentColors.xaml deleted file mode 100644 index 0fb3ab73c2..0000000000 --- a/src/Avalonia.Themes.Fluent/Accents/AccentColors.xaml +++ /dev/null @@ -1,12 +0,0 @@ - - - - #FF0078D7 - #FF005A9E - #FF004275 - #FF002642 - #FF429CE3 - #FF76B9ED - #FFA6D8FF - diff --git a/src/Avalonia.Themes.Fluent/Accents/SystemAccentColors.cs b/src/Avalonia.Themes.Fluent/Accents/SystemAccentColors.cs new file mode 100644 index 0000000000..a4ef15f950 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/SystemAccentColors.cs @@ -0,0 +1,163 @@ +using System; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Styling; + +namespace Avalonia.Themes.Fluent.Accents; + +internal class SystemAccentColors : IResourceProvider +{ + public const string AccentKey = "SystemAccentColor"; + public const string AccentDark1Key = "SystemAccentColorDark1"; + public const string AccentDark2Key = "SystemAccentColorDark2"; + public const string AccentDark3Key = "SystemAccentColorDark3"; + public const string AccentLight1Key = "SystemAccentColorLight1"; + public const string AccentLight2Key = "SystemAccentColorLight2"; + public const string AccentLight3Key = "SystemAccentColorLight3"; + + private static readonly Color s_defaultSystemAccentColor = Color.FromRgb(0, 120, 215); + private readonly IPlatformSettings? _platformSettings; + private bool _invalidateColors = true; + private Color _systemAccentColor; + private Color _systemAccentColorDark1, _systemAccentColorDark2, _systemAccentColorDark3; + private Color _systemAccentColorLight1, _systemAccentColorLight2, _systemAccentColorLight3; + + public SystemAccentColors() + { + _platformSettings = AvaloniaLocator.Current.GetService(); + } + + public bool HasResources => true; + public bool TryGetResource(object key, ThemeVariant? theme, out object? value) + { + if (key is string strKey) + { + if (strKey.Equals(AccentKey, StringComparison.InvariantCulture)) + { + EnsureColors(); + value = _systemAccentColor; + return true; + } + + if (strKey.Equals(AccentDark1Key, StringComparison.InvariantCulture)) + { + EnsureColors(); + value = _systemAccentColorDark1; + return true; + } + + if (strKey.Equals(AccentDark2Key, StringComparison.InvariantCulture)) + { + EnsureColors(); + value = _systemAccentColorDark2; + return true; + } + + if (strKey.Equals(AccentDark3Key, StringComparison.InvariantCulture)) + { + EnsureColors(); + value = _systemAccentColorDark3; + return true; + } + + if (strKey.Equals(AccentLight1Key, StringComparison.InvariantCulture)) + { + EnsureColors(); + value = _systemAccentColorLight1; + return true; + } + + if (strKey.Equals(AccentLight2Key, StringComparison.InvariantCulture)) + { + EnsureColors(); + value = _systemAccentColorLight2; + return true; + } + + if (strKey.Equals(AccentLight3Key, StringComparison.InvariantCulture)) + { + EnsureColors(); + value = _systemAccentColorLight3; + return true; + } + } + + value = null; + return false; + } + + public IResourceHost? Owner { get; private set; } + public event EventHandler? OwnerChanged; + public void AddOwner(IResourceHost owner) + { + if (Owner != owner) + { + Owner = owner; + OwnerChanged?.Invoke(this, EventArgs.Empty); + + if (_platformSettings is not null) + { + _platformSettings.ColorValuesChanged += PlatformSettingsOnColorValuesChanged; + } + } + } + + public void RemoveOwner(IResourceHost owner) + { + if (Owner == owner) + { + Owner = null; + OwnerChanged?.Invoke(this, EventArgs.Empty); + + if (_platformSettings is not null) + { + _platformSettings.ColorValuesChanged -= PlatformSettingsOnColorValuesChanged; + } + } + } + + private void EnsureColors() + { + if (_invalidateColors) + { + _invalidateColors = false; + + _systemAccentColor = _platformSettings?.GetColorValues().AccentColor1 ?? s_defaultSystemAccentColor; + (_systemAccentColorDark1,_systemAccentColorDark2, _systemAccentColorDark3, + _systemAccentColorLight1, _systemAccentColorLight2, _systemAccentColorLight3) = CalculateAccentShades(_systemAccentColor); + } + } + + public static (Color d1, Color d2, Color d3, Color l1, Color l2, Color l3) CalculateAccentShades(Color accentColor) + { + // dark1step = (hslAccent.L - SystemAccentColorDark1.L) * 255 + const double dark1step = 28.5 / 255d; + const double dark2step = 49 / 255d; + const double dark3step = 74.5 / 255d; + // light1step = (SystemAccentColorLight1.L - hslAccent.L) * 255 + const double light1step = 39 / 255d; + const double light2step = 70 / 255d; + const double light3step = 103 / 255d; + + var hslAccent = accentColor.ToHsl(); + + return ( + // Darker shades + new HslColor(hslAccent.A, hslAccent.H, hslAccent.S, hslAccent.L - dark1step).ToRgb(), + new HslColor(hslAccent.A, hslAccent.H, hslAccent.S, hslAccent.L - dark2step).ToRgb(), + new HslColor(hslAccent.A, hslAccent.H, hslAccent.S, hslAccent.L - dark3step).ToRgb(), + + // Lighter shades + new HslColor(hslAccent.A, hslAccent.H, hslAccent.S, hslAccent.L + light1step).ToRgb(), + new HslColor(hslAccent.A, hslAccent.H, hslAccent.S, hslAccent.L + light2step).ToRgb(), + new HslColor(hslAccent.A, hslAccent.H, hslAccent.S, hslAccent.L + light3step).ToRgb() + ); + } + + private void PlatformSettingsOnColorValuesChanged(object? sender, PlatformColorValues e) + { + _invalidateColors = true; + Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } +} diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml b/src/Avalonia.Themes.Fluent/FluentTheme.xaml index e83257fd9f..9154505c28 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml @@ -4,9 +4,9 @@ - + diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs index db8d604154..7d68979c26 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs @@ -24,7 +24,6 @@ internal class XamlMergeResourceGroupTransformer : IXamlAstGroupTransformer var mergeResourceIncludeType = context.GetAvaloniaTypes().MergeResourceInclude; var mergeSourceNodes = new List(); - var hasAnyNonMergedResource = false; foreach (var manipulationNode in resourceDictionaryManipulation.Children.ToArray()) { void ProcessXamlPropertyAssignmentNode(XamlManipulationGroupNode parent, XamlPropertyAssignmentNode assignmentNode) @@ -47,17 +46,6 @@ internal class XamlMergeResourceGroupTransformer : IXamlAstGroupTransformer valueNode); } } - else - { - hasAnyNonMergedResource = true; - } - - if (hasAnyNonMergedResource && mergeSourceNodes.Any()) - { - throw new XamlDocumentParseException(context.CurrentDocument, - "Mix of MergeResourceInclude and other dictionaries inside of the ResourceDictionary.MergedDictionaries is not allowed", - valueNode); - } } } From c9b1ed8f51f41dea62021f7b4b689ec21f149e37 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 21 Apr 2023 22:37:27 -0400 Subject: [PATCH 050/110] Implement ColorPaletteResources as a public API --- .../AvaloniaDictionaryExtensions.cs | 2 +- .../Accents/BaseColorsPalette.xaml | 69 ++++++++ .../Accents/{Base.xaml => BaseResources.xaml} | 154 +++--------------- .../ColorPaletteResources.Properties.cs | 153 +++++++++++++++++ .../ColorPaletteResources.cs | 118 ++++++++++++++ .../ColorPaletteResourcesCollection.cs | 65 ++++++++ src/Avalonia.Themes.Fluent/FluentTheme.xaml | 12 +- .../FluentTheme.xaml.cs | 6 + 8 files changed, 447 insertions(+), 132 deletions(-) create mode 100644 src/Avalonia.Themes.Fluent/Accents/BaseColorsPalette.xaml rename src/Avalonia.Themes.Fluent/Accents/{Base.xaml => BaseResources.xaml} (79%) create mode 100644 src/Avalonia.Themes.Fluent/ColorPaletteResources.Properties.cs create mode 100644 src/Avalonia.Themes.Fluent/ColorPaletteResources.cs create mode 100644 src/Avalonia.Themes.Fluent/ColorPaletteResourcesCollection.cs diff --git a/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs b/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs index e350a019d4..8c731c188f 100644 --- a/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs +++ b/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs @@ -35,7 +35,7 @@ namespace Avalonia.Collections /// Indicates if a weak subscription should be used to track changes to the collection. /// /// A disposable used to terminate the subscription. - internal static IDisposable ForEachItem( + public static IDisposable ForEachItem( this IAvaloniaReadOnlyDictionary collection, Action added, Action removed, diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseColorsPalette.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseColorsPalette.xaml new file mode 100644 index 0000000000..b8b5fcf1f4 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/BaseColorsPalette.xaml @@ -0,0 +1,69 @@ + + + + + #FFFFFFFF + #33FFFFFF + #99FFFFFF + #CCFFFFFF + #66FFFFFF + #FF000000 + #33000000 + #99000000 + #CC000000 + #66000000 + #FF171717 + #FF000000 + #33000000 + #66000000 + #CC000000 + #FFCCCCCC + #FF7A7A7A + #FFCCCCCC + #FFF2F2F2 + #FFE6E6E6 + #FFF2F2F2 + #FFFFFFFF + #FF767676 + #19000000 + #33000000 + #C50500 + #FFFFFFFF + #17000000 + #2E000000 + + + #FF000000 + #33000000 + #99000000 + #CC000000 + #66000000 + #FFFFFFFF + #33FFFFFF + #99FFFFFF + #CCFFFFFF + #66FFFFFF + #FFF2F2F2 + #FF000000 + #33000000 + #66000000 + #CC000000 + #FF333333 + #FF858585 + #FF767676 + #FF171717 + #FF1F1F1F + #FF2B2B2B + #FFFFFFFF + #FF767676 + #19FFFFFF + #33FFFFFF + #FFF000 + #FF000000 + #18FFFFFF + #30FFFFFF + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/Base.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseResources.xaml similarity index 79% rename from src/Avalonia.Themes.Fluent/Accents/Base.xaml rename to src/Avalonia.Themes.Fluent/Accents/BaseResources.xaml index c19a4f5c09..c1f79e45d5 100644 --- a/src/Avalonia.Themes.Fluent/Accents/Base.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/BaseResources.xaml @@ -2,7 +2,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="using:System" xmlns:converters="using:Avalonia.Controls.Converters"> - fonts:Inter#Inter, $Default 14 @@ -28,39 +27,33 @@ - - - - #FFFFFFFF - #33FFFFFF - #99FFFFFF - #CCFFFFFF - #66FFFFFF - #FF000000 - #33000000 - #99000000 - #CC000000 - #66000000 - #FF171717 - #FF000000 - #33000000 - #66000000 - #CC000000 - #FFCCCCCC - #FF7A7A7A - #FFCCCCCC - #FFF2F2F2 - #FFE6E6E6 - #FFF2F2F2 - #FFFFFFFF - #FF767676 - #19000000 - #33000000 - #C50500 + 374 + 0,2,0,2 + 1 + -1,0,-1,0 + 32 + 64 + 456 + 0 + 1 + 0 + + 12,11,12,12 + 96 + 40 + 758 - #17000000 - #2E000000 + + 0 + + 0,4,0,4 + + + 12,0,12,0 + + + - - - - - - - - #FFFFFFFF - - - 374 - 0,2,0,2 - 1 - -1,0,-1,0 - 32 - 64 - 456 - 0 - 1 - 0 - - 12,11,12,12 - 96 - 40 - 758 - - - 0 - - - 0,4,0,4 - - - 12,0,12,0 - - #FF000000 - #33000000 - #99000000 - #CC000000 - #66000000 - #FFFFFFFF - #33FFFFFF - #99FFFFFF - #CCFFFFFF - #66FFFFFF - #FFF2F2F2 - #FF000000 - #33000000 - #66000000 - #CC000000 - #FF333333 - #FF858585 - #FF767676 - #FF171717 - #FF1F1F1F - #FF2B2B2B - #FFFFFFFF - #FF767676 - #19FFFFFF - #33FFFFFF - #FFF000 - - #18FFFFFF - #30FFFFFF - - - - - - - - #FF000000 - - 374 - 0,2,0,2 - 1 - -1,0,-1,0 - 32 - 64 - 456 - 0 - 1 - 0 - - 12,11,12,12 - 96 - 40 - 758 - - - 0 - - - 0,4,0,4 - - - 12,0,12,0 diff --git a/src/Avalonia.Themes.Fluent/ColorPaletteResources.Properties.cs b/src/Avalonia.Themes.Fluent/ColorPaletteResources.Properties.cs new file mode 100644 index 0000000000..6d284150e4 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ColorPaletteResources.Properties.cs @@ -0,0 +1,153 @@ +using Avalonia.Media; + +namespace Avalonia.Themes.Fluent; + +public partial class ColorPaletteResources +{ + private bool _hasAccentColor; + private Color _accentColor; + private Color _accentColorDark1, _accentColorDark2, _accentColorDark3; + private Color _accentColorLight1, _accentColorLight2, _accentColorLight3; + + public static readonly DirectProperty AccentProperty + = AvaloniaProperty.RegisterDirect(nameof(Accent), r => r.Accent, (r, v) => r.Accent = v); + + /// + /// Gets or sets the Accent color value. + /// + public Color Accent + { + get => _accentColor; + set => SetAndRaise(AccentProperty, ref _accentColor, value); + } + + /// + /// Gets or sets the AltHigh color value. + /// + public Color AltHigh { get => GetColor("SystemAltHighColor"); set => SetColor("SystemAltHighColor", value); } + + /// + /// Gets or sets the AltLow color value. + /// + public Color AltLow { get => GetColor("SystemAltLowColor"); set => SetColor("SystemAltLowColor", value); } + + /// + /// Gets or sets the AltMedium color value. + /// + public Color AltMedium { get => GetColor("SystemAltMediumColor"); set => SetColor("SystemAltMediumColor", value); } + + /// + /// Gets or sets the AltMediumHigh color value. + /// + public Color AltMediumHigh { get => GetColor("SystemAltMediumHighColor"); set => SetColor("SystemAltMediumHighColor", value); } + + /// + /// Gets or sets the AltMediumLow color value. + /// + public Color AltMediumLow { get => GetColor("SystemAltMediumLowColor"); set => SetColor("SystemAltMediumLowColor", value); } + + /// + /// Gets or sets the BaseHigh color value. + /// + public Color BaseHigh { get => GetColor("SystemBaseHighColor"); set => SetColor("SystemBaseHighColor", value); } + + /// + /// Gets or sets the BaseLow color value. + /// + public Color BaseLow { get => GetColor("SystemBaseLowColor"); set => SetColor("SystemBaseLowColor", value); } + + /// + /// Gets or sets the BaseMedium color value. + /// + public Color BaseMedium { get => GetColor("SystemBaseMediumColor"); set => SetColor("SystemBaseMediumColor", value); } + + /// + /// Gets or sets the BaseMediumHigh color value. + /// + public Color BaseMediumHigh { get => GetColor("SystemBaseMediumHighColor"); set => SetColor("SystemBaseMediumHighColor", value); } + + /// + /// Gets or sets the BaseMediumLow color value. + /// + public Color BaseMediumLow { get => GetColor("SystemBaseMediumLowColor"); set => SetColor("SystemBaseMediumLowColor", value); } + + /// + /// Gets or sets the ChromeAltLow color value. + /// + public Color ChromeAltLow { get => GetColor("SystemChromeAltLowColor"); set => SetColor("SystemChromeAltLowColor", value); } + + /// + /// Gets or sets the ChromeBlackHigh color value. + /// + public Color ChromeBlackHigh { get => GetColor("SystemChromeBlackHighColor"); set => SetColor("SystemChromeBlackHighColor", value); } + + /// + /// Gets or sets the ChromeBlackLow color value. + /// + public Color ChromeBlackLow { get => GetColor("SystemChromeBlackLowColor"); set => SetColor("SystemChromeBlackLowColor", value); } + + /// + /// Gets or sets the ChromeBlackMedium color value. + /// + public Color ChromeBlackMedium { get => GetColor("SystemChromeBlackMediumColor"); set => SetColor("SystemChromeBlackMediumColor", value); } + + /// + /// Gets or sets the ChromeBlackMediumLow color value. + /// + public Color ChromeBlackMediumLow { get => GetColor("SystemChromeBlackMediumLowColor"); set => SetColor("SystemChromeBlackMediumLowColor", value); } + + /// + /// Gets or sets the ChromeDisabledHigh color value. + /// + public Color ChromeDisabledHigh { get => GetColor("SystemChromeDisabledHighColor"); set => SetColor("SystemChromeDisabledHighColor", value); } + + /// + /// Gets or sets the ChromeDisabledLow color value. + /// + public Color ChromeDisabledLow { get => GetColor("SystemChromeDisabledLowColor"); set => SetColor("SystemChromeDisabledLowColor", value); } + + /// + /// Gets or sets the ChromeGray color value. + /// + public Color ChromeGray { get => GetColor("SystemChromeGrayColor"); set => SetColor("SystemChromeGrayColor", value); } + + /// + /// Gets or sets the ChromeHigh color value. + /// + public Color ChromeHigh { get => GetColor("SystemChromeHighColor"); set => SetColor("SystemChromeHighColor", value); } + + /// + /// Gets or sets the ChromeLow color value. + /// + public Color ChromeLow { get => GetColor("SystemChromeLowColor"); set => SetColor("SystemChromeLowColor", value); } + + /// + /// Gets or sets the ChromeMedium color value. + /// + public Color ChromeMedium { get => GetColor("SystemChromeMediumColor"); set => SetColor("SystemChromeMediumColor", value); } + + /// + /// Gets or sets the ChromeMediumLow color value. + /// + public Color ChromeMediumLow { get => GetColor("SystemChromeMediumLowColor"); set => SetColor("SystemChromeMediumLowColor", value); } + + /// + /// Gets or sets the ChromeWhite color value. + /// + public Color ChromeWhite { get => GetColor("SystemChromeWhiteColor"); set => SetColor("SystemChromeWhiteColor", value); } + + /// + /// Gets or sets the ErrorText color value. + /// + public Color ErrorText { get => GetColor("SystemErrorTextColor"); set => SetColor("SystemErrorTextColor", value); } + + /// + /// Gets or sets the ListLow color value. + /// + public Color ListLow { get => GetColor("SystemListLowColor"); set => SetColor("SystemListLowColor", value); } + + /// + /// Gets or sets the ListMedium color value. + /// + public Color ListMedium { get => GetColor("SystemListMediumColor"); set => SetColor("SystemListMediumColor", value); } +} diff --git a/src/Avalonia.Themes.Fluent/ColorPaletteResources.cs b/src/Avalonia.Themes.Fluent/ColorPaletteResources.cs new file mode 100644 index 0000000000..ce52f51752 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ColorPaletteResources.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.Themes.Fluent.Accents; + +namespace Avalonia.Themes.Fluent; + +/// +/// Represents a specialized resource dictionary that contains color resources used by FluentTheme elements. +/// +/// +/// This class can only be used in . +/// +public partial class ColorPaletteResources : AvaloniaObject, IResourceNode +{ + private readonly Dictionary _colors = new(StringComparer.InvariantCulture); + + public bool HasResources => _hasAccentColor || _colors.Count > 0; + + public bool TryGetResource(object key, ThemeVariant? theme, out object? value) + { + if (key is string strKey) + { + if (strKey.Equals(SystemAccentColors.AccentKey, StringComparison.InvariantCulture)) + { + value = _accentColor; + return _hasAccentColor; + } + + if (strKey.Equals(SystemAccentColors.AccentDark1Key, StringComparison.InvariantCulture)) + { + value = _accentColorDark1; + return _hasAccentColor; + } + + if (strKey.Equals(SystemAccentColors.AccentDark2Key, StringComparison.InvariantCulture)) + { + value = _accentColorDark2; + return _hasAccentColor; + } + + if (strKey.Equals(SystemAccentColors.AccentDark3Key, StringComparison.InvariantCulture)) + { + value = _accentColorDark3; + return _hasAccentColor; + } + + if (strKey.Equals(SystemAccentColors.AccentLight1Key, StringComparison.InvariantCulture)) + { + value = _accentColorLight1; + return _hasAccentColor; + } + + if (strKey.Equals(SystemAccentColors.AccentLight2Key, StringComparison.InvariantCulture)) + { + value = _accentColorLight2; + return _hasAccentColor; + } + + if (strKey.Equals(SystemAccentColors.AccentLight3Key, StringComparison.InvariantCulture)) + { + value = _accentColorLight3; + return _hasAccentColor; + } + + if (_colors.TryGetValue(strKey, out var color)) + { + value = color; + return true; + } + } + + value = null; + return false; + } + + private Color GetColor(string key) + { + if (_colors.TryGetValue(key, out var color)) + { + return color; + } + + return default; + } + + private void SetColor(string key, Color value) + { + if (value == default) + { + _colors.Remove(key); + } + else + { + _colors[key] = value; + } + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == AccentProperty) + { + _hasAccentColor = _accentColor != default; + + if (_hasAccentColor) + { + (_accentColorDark1, _accentColorDark2, _accentColorDark3, + _accentColorLight1, _accentColorLight2, _accentColorLight3) = + SystemAccentColors.CalculateAccentShades(_accentColor); + } + } + } +} diff --git a/src/Avalonia.Themes.Fluent/ColorPaletteResourcesCollection.cs b/src/Avalonia.Themes.Fluent/ColorPaletteResourcesCollection.cs new file mode 100644 index 0000000000..261de5497d --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ColorPaletteResourcesCollection.cs @@ -0,0 +1,65 @@ +using System; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Styling; + +namespace Avalonia.Themes.Fluent; + +internal class ColorPaletteResourcesCollection : AvaloniaDictionary, IResourceProvider +{ + public ColorPaletteResourcesCollection() : base(2) + { + this.ForEachItem( + (_, x) => + { + if (Owner is not null) + { + x.PropertyChanged += Palette_PropertyChanged; + } + }, + (_, x) => + { + if (Owner is not null) + { + x.PropertyChanged -= Palette_PropertyChanged; + } + }, + () => throw new NotSupportedException("Dictionary reset not supported")); + } + + public bool HasResources => Count > 0; + public bool TryGetResource(object key, ThemeVariant? theme, out object? value) + { + theme ??= ThemeVariant.Default; + if (base.TryGetValue(theme, out var paletteResources) + && paletteResources.TryGetResource(key, theme, out value)) + { + return true; + } + + value = null; + return false; + } + + public IResourceHost? Owner { get; private set; } + public event EventHandler? OwnerChanged; + public void AddOwner(IResourceHost owner) + { + Owner = owner; + OwnerChanged?.Invoke(this, EventArgs.Empty); + } + + public void RemoveOwner(IResourceHost owner) + { + Owner = null; + OwnerChanged?.Invoke(this, EventArgs.Empty); + } + + private void Palette_PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == ColorPaletteResources.AccentProperty) + { + Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } + } +} diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml b/src/Avalonia.Themes.Fluent/FluentTheme.xaml index 9154505c28..8f3c96d96a 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml @@ -1,11 +1,19 @@  + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:fluent="using:Avalonia.Themes.Fluent" + xmlns:accents="clr-namespace:Avalonia.Themes.Fluent.Accents"> - + + + + + + diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs b/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs index 95539bc08a..5af22dbd1d 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Styling; @@ -31,6 +32,9 @@ namespace Avalonia.Themes.Fluent EnsureCompactStyles(); + Palettes = Resources.MergedDictionaries.OfType().FirstOrDefault() + ?? throw new InvalidOperationException("FluentTheme was initialized with missing ColorPaletteResourcesCollection."); + object GetAndRemove(string key) { var val = Resources[key] @@ -52,6 +56,8 @@ namespace Avalonia.Themes.Fluent set => SetValue(DensityStyleProperty, value); } + public IDictionary Palettes { get; } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); From 1aba81a27f1f568d41c948676ac3580c58cb6a5b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 22 Apr 2023 22:41:45 -0400 Subject: [PATCH 051/110] Fix non dynamic accent brushes --- .../Accents/FluentControlResources.xaml | 114 +++++++++++------- 1 file changed, 69 insertions(+), 45 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml index a9bc622221..61a74f26a4 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml @@ -4,8 +4,8 @@ - - + + @@ -52,7 +52,8 @@ - + @@ -291,15 +292,17 @@ ResourceKey="SystemControlHighlightBaseHighBrush" /> - + - - + + @@ -309,13 +312,17 @@ ResourceKey="SystemControlBackgroundBaseMediumLowBrush" /> - - + + - - + + - - + + - - + + @@ -470,8 +481,8 @@ - - + + @@ -502,8 +513,8 @@ - - + + @@ -701,8 +712,9 @@ - - + + @@ -775,8 +787,8 @@ - - + + @@ -823,7 +835,8 @@ - + @@ -1065,14 +1078,17 @@ ResourceKey="SystemControlHighlightBaseHighBrush" /> - - + + - - + + @@ -1082,13 +1098,17 @@ ResourceKey="SystemControlBackgroundBaseMediumLowBrush" /> - - + + - - + + - - + + - - + + @@ -1243,8 +1267,8 @@ - - + + @@ -1275,12 +1299,12 @@ - - + + - - + + @@ -1476,8 +1500,8 @@ - - + + From bbbc1280ff4f671073ebd033133cea9f62891579 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 23 Apr 2023 00:44:48 -0400 Subject: [PATCH 052/110] Rename RegionBrush to SystemRegionColor and use it in default templates --- src/Avalonia.Themes.Fluent/Accents/BaseColorsPalette.xaml | 4 ++-- src/Avalonia.Themes.Fluent/Accents/BaseResources.xaml | 4 ++-- .../ColorPaletteResources.Properties.cs | 5 +++++ .../Controls/EmbeddableControlRoot.xaml | 2 +- src/Avalonia.Themes.Fluent/Controls/Window.xaml | 2 +- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseColorsPalette.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseColorsPalette.xaml index b8b5fcf1f4..362d543646 100644 --- a/src/Avalonia.Themes.Fluent/Accents/BaseColorsPalette.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/BaseColorsPalette.xaml @@ -29,7 +29,7 @@ #19000000 #33000000 #C50500 - #FFFFFFFF + #FFFFFFFF #17000000 #2E000000 @@ -60,7 +60,7 @@ #19FFFFFF #33FFFFFF #FFF000 - #FF000000 + #FF000000 #18FFFFFF #30FFFFFF diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseResources.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseResources.xaml index c1f79e45d5..517a80fd7e 100644 --- a/src/Avalonia.Themes.Fluent/Accents/BaseResources.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/BaseResources.xaml @@ -212,7 +212,7 @@ - + @@ -372,7 +372,7 @@ - + diff --git a/src/Avalonia.Themes.Fluent/ColorPaletteResources.Properties.cs b/src/Avalonia.Themes.Fluent/ColorPaletteResources.Properties.cs index 6d284150e4..366af8e227 100644 --- a/src/Avalonia.Themes.Fluent/ColorPaletteResources.Properties.cs +++ b/src/Avalonia.Themes.Fluent/ColorPaletteResources.Properties.cs @@ -150,4 +150,9 @@ public partial class ColorPaletteResources /// Gets or sets the ListMedium color value. /// public Color ListMedium { get => GetColor("SystemListMediumColor"); set => SetColor("SystemListMediumColor", value); } + + /// + /// Gets or sets the RegionColor color value. + /// + public Color RegionColor { get => GetColor("SystemRegionColor"); set => SetColor("SystemRegionColor", value); } } diff --git a/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml index f60424a2dc..ee51ef8085 100644 --- a/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml @@ -2,7 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> - + diff --git a/src/Avalonia.Themes.Fluent/Controls/Window.xaml b/src/Avalonia.Themes.Fluent/Controls/Window.xaml index ff27cce800..8db01fa4c8 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Window.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Window.xaml @@ -1,7 +1,7 @@ - + From 753b821035769f054c5e03717a57a7c69abc856f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 23 Apr 2023 00:45:03 -0400 Subject: [PATCH 053/110] Fix merged dictionaries ordering --- src/Avalonia.Themes.Fluent/FluentTheme.xaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml b/src/Avalonia.Themes.Fluent/FluentTheme.xaml index 8f3c96d96a..0528c40c21 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml @@ -6,15 +6,15 @@ - - - - - - + + + + + + From b8ecad2cbcfef7141d46dc6be6219ba960ca5e5b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 24 Apr 2023 02:42:46 -0400 Subject: [PATCH 054/110] Restore resource include validation --- .../XamlMergeResourceGroupTransformer.cs | 10 ++++++- .../Xaml/MergeResourceIncludeTests.cs | 28 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs index 7d68979c26..8e04a7d467 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs @@ -24,6 +24,7 @@ internal class XamlMergeResourceGroupTransformer : IXamlAstGroupTransformer var mergeResourceIncludeType = context.GetAvaloniaTypes().MergeResourceInclude; var mergeSourceNodes = new List(); + var mergedResourceWasAdded = false; foreach (var manipulationNode in resourceDictionaryManipulation.Children.ToArray()) { void ProcessXamlPropertyAssignmentNode(XamlManipulationGroupNode parent, XamlPropertyAssignmentNode assignmentNode) @@ -37,7 +38,8 @@ internal class XamlMergeResourceGroupTransformer : IXamlAstGroupTransformer && objectInitialization.Manipulation is XamlPropertyAssignmentNode sourceAssignmentNode) { parent.Children.Remove(assignmentNode); - mergeSourceNodes.Add(sourceAssignmentNode); + mergeSourceNodes.Add(sourceAssignmentNode); + mergedResourceWasAdded = true; } else { @@ -46,6 +48,12 @@ internal class XamlMergeResourceGroupTransformer : IXamlAstGroupTransformer valueNode); } } + else if (mergeSourceNodes.Any()) + { + throw new XamlDocumentParseException(context.CurrentDocument, + "MergeResourceInclude should always be included last when mixing with other dictionaries inside of the ResourceDictionary.MergedDictionaries.", + valueNode); + } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs index aa76756069..d6f554cdfe 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs @@ -83,6 +83,34 @@ public class MergeResourceIncludeTests Assert.ThrowsAny(() => AvaloniaRuntimeXamlLoader.LoadGroup(documents)); } + [Fact] + public void MergeResourceInclude_Is_Allowed_After_ResourceInclude() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources1.xaml"), @" + + Red +"), + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources2.xaml"), @" + + Blue +"), + new RuntimeXamlLoaderDocument(@" + + + + + +") + }; + + AvaloniaRuntimeXamlLoader.LoadGroup(documents); + } + [Fact] public void MergeResourceInclude_Works_With_Multiple_Resources() { From ce8c3be43fa40d23f9aecb76598c4064adcac640 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 24 Apr 2023 10:45:20 +0100 Subject: [PATCH 055/110] update --- packages/Avalonia/Avalonia.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index a024e0155a..26bf7dbeaa 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -5,7 +5,7 @@ - + all From 0921a980671a33ef9d94c3ce7f8134d2ab8a69d7 Mon Sep 17 00:00:00 2001 From: Tom Deseyn Date: Mon, 24 Apr 2023 11:46:22 +0200 Subject: [PATCH 056/110] Use latest Tmds.DBus version. --- src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index d8162c0486..4805c3a034 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -12,7 +12,7 @@ - + From 990b1341597e2a54fee514741953b30fa4343334 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 24 Apr 2023 11:54:54 +0100 Subject: [PATCH 057/110] update --- packages/Avalonia/Avalonia.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index 26bf7dbeaa..dcef4cb7c8 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -5,7 +5,7 @@ - + all From 6b4d286f4318e3ebd18ffa0a7f9190bf66bd6e96 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 24 Apr 2023 13:53:02 +0100 Subject: [PATCH 058/110] update --- packages/Avalonia/Avalonia.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index dcef4cb7c8..f309a5b711 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -5,7 +5,7 @@ - + all From 66f07356bf4ef2a876281f58b3b45996fdf09e6d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 24 Apr 2023 17:00:38 +0100 Subject: [PATCH 059/110] update. --- packages/Avalonia/Avalonia.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index f309a5b711..27b9453103 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -5,7 +5,7 @@ - + all From 06b7515b48e284750872875e2cd53d1f77c2284c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 24 Apr 2023 18:55:19 +0200 Subject: [PATCH 060/110] Added failing test for #11076. --- .../Layout/LayoutManagerTests.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs b/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs index 9f13520086..09f78c5a6c 100644 --- a/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs +++ b/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs @@ -59,6 +59,29 @@ namespace Avalonia.Base.UnitTests.Layout Assert.False(control.Arranged); } + [Fact] + public void Lays_Out_Descendents_That_Were_Invalidated_While_Ancestor_Was_Not_Visible() + { + // Issue #11076 + var control = new LayoutTestControl(); + var parent = new Decorator { Child = control }; + var grandparent = new Decorator { Child = parent }; + var root = new LayoutTestRoot { Child = grandparent }; + + root.LayoutManager.ExecuteInitialLayoutPass(); + + grandparent.IsVisible = false; + control.InvalidateMeasure(); + root.LayoutManager.ExecuteInitialLayoutPass(); + + grandparent.IsVisible = true; + + root.LayoutManager.ExecuteLayoutPass(); + + Assert.True(control.IsMeasureValid); + Assert.True(control.IsArrangeValid); + } + [Fact] public void Arranges_InvalidateArranged_Control() { From a3917d6b73b703c739ff888cee5db471e05c4ec7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 24 Apr 2023 18:05:02 +0100 Subject: [PATCH 061/110] update --- packages/Avalonia/Avalonia.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index 27b9453103..547af5d0dc 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -5,7 +5,7 @@ - + all From 41d76f256b12501c698c4a0d4142688c1c2e210f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 24 Apr 2023 18:13:47 -0400 Subject: [PATCH 062/110] Add AvaloniaListAttribute --- .../Metadata/AvaloniaListAttribute.cs | 23 +++++++++++++++++++ .../AvaloniaXamlIlLanguageParseIntrinsics.cs | 14 +++++++++++ .../AvaloniaXamlIlWellKnownTypes.cs | 2 ++ .../Avalonia.Markup.Xaml.Loader/xamlil.github | 2 +- 4 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Base/Metadata/AvaloniaListAttribute.cs diff --git a/src/Avalonia.Base/Metadata/AvaloniaListAttribute.cs b/src/Avalonia.Base/Metadata/AvaloniaListAttribute.cs new file mode 100644 index 0000000000..d523808d32 --- /dev/null +++ b/src/Avalonia.Base/Metadata/AvaloniaListAttribute.cs @@ -0,0 +1,23 @@ +using System; + +namespace Avalonia.Metadata; + +/// +/// Defines how compiler should split avalonia list string value before parsing individual items. +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] +public sealed class AvaloniaListAttribute : Attribute +{ + /// + /// Separator used to split input string. + /// Default value is ','. + /// + public string[]? Separators { get; init; } + + /// + /// Split options used to split input string. + /// Default value is RemoveEmptyEntries with TrimEntries. + /// + // StringSplitOptions.TrimEntries = 2, but only on net6 target. + public StringSplitOptions SplitOptions { get; init; } = StringSplitOptions.RemoveEmptyEntries | (StringSplitOptions)2; +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs index cd005ce24d..6c7b04dbfb 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs @@ -337,6 +337,20 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions var separators = new[] { "," }; var splitOptions = StringSplitOptions.RemoveEmptyEntries | trimOption; + var attribute = type.GetAllCustomAttributes().FirstOrDefault(a => a.Type == types.AvaloniaListAttribute); + if (attribute is not null) + { + if (attribute.Properties.TryGetValue("Separators", out var separatorsArray)) + { + separators = ((Array)separatorsArray)?.OfType().ToArray(); + } + + if (attribute.Properties.TryGetValue("SplitOptions", out var splitOptionsObj)) + { + splitOptions = (StringSplitOptions)splitOptionsObj; + } + } + items = text.Split(separators, splitOptions ^ trimOption); // Compiler targets netstandard, so we need to emulate StringSplitOptions.TrimEntries, if it was requested. if (splitOptions.HasFlag(trimOption)) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 8ab84f4615..b5c0c7734d 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -33,6 +33,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType InheritDataTypeFromItemsAttribute { get; } public IXamlType MarkupExtensionOptionAttribute { get; } public IXamlType MarkupExtensionDefaultOptionAttribute { get; } + public IXamlType AvaloniaListAttribute { get; } public IXamlType AvaloniaList { get; } public IXamlType OnExtensionType { get; } public IXamlType UnsetValueType { get; } @@ -143,6 +144,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers InheritDataTypeFromItemsAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.InheritDataTypeFromItemsAttribute"); MarkupExtensionOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionOptionAttribute"); MarkupExtensionDefaultOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionDefaultOptionAttribute"); + AvaloniaListAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.AvaloniaListAttribute"); AvaloniaList = cfg.TypeSystem.GetType("Avalonia.Collections.AvaloniaList`1"); OnExtensionType = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.On"); AvaloniaObjectBindMethod = AvaloniaObjectExtensions.FindMethod("Bind", IDisposable, false, AvaloniaObject, diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github index 5d1025f30d..e5254eb1b2 160000 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github @@ -1 +1 @@ -Subproject commit 5d1025f30d0ed6d8f419d82959c148276301f393 +Subproject commit e5254eb1b2017f78a92acd466c8fa1e47401056b From 89e1680501859c0f31fa4da65b2f5c28e3205e82 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 24 Apr 2023 18:13:55 -0400 Subject: [PATCH 063/110] Fix DefinitionList parsing --- src/Avalonia.Controls/DefinitionList.cs | 2 ++ .../Xaml/BasicTests.cs | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/Avalonia.Controls/DefinitionList.cs b/src/Avalonia.Controls/DefinitionList.cs index c850647bf4..63a54731e0 100644 --- a/src/Avalonia.Controls/DefinitionList.cs +++ b/src/Avalonia.Controls/DefinitionList.cs @@ -1,9 +1,11 @@ using System.Collections; using System.Collections.Specialized; using Avalonia.Collections; +using Avalonia.Metadata; namespace Avalonia.Controls { + [AvaloniaList(Separators = new [] { ",", " " })] public abstract class DefinitionList : AvaloniaList where T : DefinitionBase { public DefinitionList() diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index 421ed2c979..70541b6196 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -261,6 +261,37 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml var grid = AvaloniaRuntimeXamlLoader.Parse(xaml); + Assert.Equal(4, grid.ColumnDefinitions.Count); + Assert.Equal(4, grid.RowDefinitions.Count); + + var expected1 = new GridLength(100); + var expected2 = GridLength.Auto; + var expected3 = new GridLength(1, GridUnitType.Star); + var expected4 = new GridLength(100, GridUnitType.Star); + + Assert.Equal(expected1, grid.ColumnDefinitions[0].Width); + Assert.Equal(expected2, grid.ColumnDefinitions[1].Width); + Assert.Equal(expected3, grid.ColumnDefinitions[2].Width); + Assert.Equal(expected4, grid.ColumnDefinitions[3].Width); + + Assert.Equal(expected1, grid.RowDefinitions[0].Height); + Assert.Equal(expected2, grid.RowDefinitions[1].Height); + Assert.Equal(expected3, grid.RowDefinitions[2].Height); + Assert.Equal(expected4, grid.RowDefinitions[3].Height); + } + + [Fact] + public void Grid_Row_Col_Definitions_Are_Parsed_Space_Delimiter() + { + var xaml = @" + +"; + + var grid = AvaloniaRuntimeXamlLoader.Parse(xaml); + + Assert.Equal(4, grid.ColumnDefinitions.Count); Assert.Equal(4, grid.RowDefinitions.Count); From decf80cb7736c4e099b10703df7cb5dce84c8d2a Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 25 Apr 2023 00:47:14 -0400 Subject: [PATCH 064/110] Adds RenderScaling and DesktopScaling back to the public API together with ScalingChanged --- src/Avalonia.Controls/TopLevel.cs | 8 +++++++- src/Avalonia.Controls/WindowBase.cs | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 8a1cdf3f80..881474c761 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -285,6 +285,11 @@ namespace Avalonia.Controls /// public event EventHandler? Closed; + /// + /// Gets or sets a method called when the TopLevel's scaling changes. + /// + public event EventHandler? ScalingChanged; + /// /// Gets or sets the client size of the window. /// @@ -428,7 +433,7 @@ namespace Avalonia.Controls double ILayoutRoot.LayoutScaling => PlatformImpl?.RenderScaling ?? 1; /// - double IRenderRoot.RenderScaling => PlatformImpl?.RenderScaling ?? 1; + public double RenderScaling => PlatformImpl?.RenderScaling ?? 1; IStyleHost IStyleHost.StylingParent => _globalStyles!; @@ -590,6 +595,7 @@ namespace Avalonia.Controls protected virtual void HandleScalingChanged(double scaling) { LayoutHelper.InvalidateSelfAndChildrenMeasure(this); + ScalingChanged?.Invoke(this, EventArgs.Empty); } private static bool TransparencyLevelsMatch (WindowTransparencyLevel requested, WindowTransparencyLevel received) diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index c2523207e4..d640a0c2dc 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -116,6 +116,11 @@ namespace Avalonia.Controls set { SetValue(TopmostProperty, value); } } + /// + /// Gets the scaling factor for Window positioning and sizing. + /// + public double DesktopScaling => PlatformImpl?.DesktopScaling ?? 1; + /// /// Activates the window. /// From c2220039dadd13583f871e592ebf6c7d04254919 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 24 Apr 2023 18:56:03 +0200 Subject: [PATCH 065/110] Re-layout descendants when made effectively visible. Fixes #11076. --- src/Avalonia.Base/Layout/Layoutable.cs | 40 +++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs index ed88b73149..08f327d048 100644 --- a/src/Avalonia.Base/Layout/Layoutable.cs +++ b/src/Avalonia.Base/Layout/Layoutable.cs @@ -776,10 +776,24 @@ namespace Avalonia.Layout // All changes to visibility cause the parent element to be notified. this.GetVisualParent()?.ChildDesiredSizeChanged(this); - // We only invalidate outselves when visibility is changed to true. if (change.GetNewValue()) { + // We only invalidate ourselves when visibility is changed to true. InvalidateMeasure(); + + // If any descendant had its measure/arrange invalidated while we were hidden, + // they will need to to be registered with the layout manager now that they + // are again effectively visible. If IsEffectivelyVisible becomes an observable + // property then we can piggy-pack on that; for the moment we do this manually. + if (VisualRoot is ILayoutRoot layoutRoot) + { + var count = VisualChildren.Count; + + for (var i = 0; i < count; ++i) + { + (VisualChildren[i] as Layoutable)?.AncestorBecameVisible(layoutRoot.LayoutManager); + } + } } } } @@ -804,6 +818,30 @@ namespace Avalonia.Layout InvalidateMeasure(); } + private void AncestorBecameVisible(ILayoutManager layoutManager) + { + if (!IsVisible) + return; + + if (!IsMeasureValid) + { + layoutManager.InvalidateMeasure(this); + InvalidateVisual(); + } + else if (!IsArrangeValid) + { + layoutManager.InvalidateArrange(this); + InvalidateVisual(); + } + + var count = VisualChildren.Count; + + for (var i = 0; i < count; ++i) + { + (VisualChildren[i] as Layoutable)?.AncestorBecameVisible(layoutManager); + } + } + /// /// Called when the layout manager raises a LayoutUpdated event. /// From b8fed4cbeafb10c4e2ccca068a9ace9c7249fd43 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 24 Apr 2023 19:03:26 +0200 Subject: [PATCH 066/110] Remove hack from VirtualizingStackPanel. No longer needed since layout invalidation was fixed in last commit. --- .../VirtualizingStackPanel.cs | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 77268c7831..071ac95f45 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -556,7 +556,6 @@ namespace Avalonia.Controls GetItemIsOwnContainer(items, index) ?? GetRecycledElement(items, index) ?? CreateElement(items, index); - InvalidateHack(e); return e; } @@ -705,39 +704,6 @@ namespace Avalonia.Controls } } - private static void InvalidateHack(Control c) - { - bool HasInvalidations(Control c) - { - if (!c.IsMeasureValid) - return true; - - for (var i = 0; i < c.VisualChildren.Count; ++i) - { - if (c.VisualChildren[i] is Control child) - { - if (!child.IsMeasureValid || HasInvalidations(child)) - return true; - } - } - - return false; - } - - void Invalidate(Control c) - { - c.InvalidateMeasure(); - for (var i = 0; i < c.VisualChildren.Count; ++i) - { - if (c.VisualChildren[i] is Control child) - Invalidate(child); - } - } - - if (HasInvalidations(c)) - Invalidate(c); - } - private void OnUnrealizedFocusedElementLostFocus(object? sender, RoutedEventArgs e) { if (_unrealizedFocusedElement is null || sender != _unrealizedFocusedElement) From 109c05aeab917174f4f60775357384b02ff9c43e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 24 Apr 2023 20:57:55 +0200 Subject: [PATCH 067/110] Increase max number of permitted layout attempts. And log a message when a control was skipped due to a layout cycle. --- src/Avalonia.Base/Layout/LayoutManager.cs | 2 +- src/Avalonia.Base/Layout/LayoutQueue.cs | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs index 94955a18ae..747ee1c082 100644 --- a/src/Avalonia.Base/Layout/LayoutManager.cs +++ b/src/Avalonia.Base/Layout/LayoutManager.cs @@ -17,7 +17,7 @@ namespace Avalonia.Layout /// public class LayoutManager : ILayoutManager, IDisposable { - private const int MaxPasses = 3; + private const int MaxPasses = 10; private readonly Layoutable _owner; private readonly LayoutQueue _toMeasure = new LayoutQueue(v => !v.IsMeasureValid); private readonly LayoutQueue _toArrange = new LayoutQueue(v => !v.IsArrangeValid); diff --git a/src/Avalonia.Base/Layout/LayoutQueue.cs b/src/Avalonia.Base/Layout/LayoutQueue.cs index 24adeb0793..48efa501f2 100644 --- a/src/Avalonia.Base/Layout/LayoutQueue.cs +++ b/src/Avalonia.Base/Layout/LayoutQueue.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using Avalonia.Logging; namespace Avalonia.Layout { @@ -48,10 +49,21 @@ namespace Avalonia.Layout { _loopQueueInfo.TryGetValue(item, out var info); - if (!info.Active && info.Count < _maxEnqueueCountPerLoop) + if (!info.Active) { - _inner.Enqueue(item); - _loopQueueInfo[item] = new Info() { Active = true, Count = info.Count + 1 }; + if (info.Count < _maxEnqueueCountPerLoop) + { + _inner.Enqueue(item); + _loopQueueInfo[item] = new Info() { Active = true, Count = info.Count + 1 }; + } + else + { + Logger.TryGet(LogEventLevel.Warning, LogArea.Layout)?.Log( + this, + "Layout cycle detected. Item {Item} was enqueued {Count} times.", + item, + info.Count); + } } } From 4ddd83b561dd78036416cbd6c4e11beb74da59f8 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 25 Apr 2023 17:46:31 +0200 Subject: [PATCH 068/110] Fix TextBlock Measure/Arrange --- .../Media/TextFormatting/TextLineImpl.cs | 13 ++-------- src/Avalonia.Controls/TextBlock.cs | 26 +++++++++++-------- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 935347ed85..1234067844 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -83,7 +83,7 @@ namespace Avalonia.Media.TextFormatting /// public override void Draw(DrawingContext drawingContext, Point lineOrigin) { - var (currentX, currentY) = lineOrigin; + var (currentX, currentY) = lineOrigin + new Point(Start, 0); foreach (var textRun in _textRuns) { @@ -1423,8 +1423,6 @@ namespace Avalonia.Media.TextFormatting var fontMetrics = _paragraphProperties.DefaultTextRunProperties.CachedGlyphTypeface.Metrics; var fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize; var scale = fontRenderingEmSize / fontMetrics.DesignEmHeight; - - var width = 0d; var widthIncludingWhitespace = 0d; var trailingWhitespaceLength = 0; var newLineLength = 0; @@ -1436,13 +1434,6 @@ namespace Avalonia.Media.TextFormatting var lineHeight = _paragraphProperties.LineHeight; - var lastRunIndex = _textRuns.Length - 1; - - if (lastRunIndex > 0 && _textRuns[lastRunIndex] is TextEndOfLine) - { - lastRunIndex--; - } - for (var index = 0; index < _textRuns.Length; index++) { switch (_textRuns[index]) @@ -1500,7 +1491,7 @@ namespace Avalonia.Media.TextFormatting } } - width = widthIncludingWhitespace; + var width = widthIncludingWhitespace; for (var i = _textRuns.Length - 1; i >= 0; i--) { diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 19eaaaa0d9..155d7d5f56 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -7,7 +7,6 @@ using Avalonia.Layout; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Metadata; -using Avalonia.Utilities; namespace Avalonia.Controls { @@ -565,7 +564,8 @@ namespace Avalonia.Controls context.FillRectangle(background, new Rect(Bounds.Size)); } - var padding = Padding; + var scale = LayoutHelper.GetLayoutScale(this); + var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale); var top = padding.Top; var textHeight = TextLayout.Bounds.Height; @@ -659,7 +659,6 @@ namespace Avalonia.Controls protected override Size MeasureOverride(Size availableSize) { var scale = LayoutHelper.GetLayoutScale(this); - var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale); _constraint = availableSize.Deflate(padding); @@ -703,19 +702,24 @@ namespace Avalonia.Controls } } - var measuredSize = TextLayout.Bounds.Size.Inflate(padding); - - return measuredSize; + return TextLayout.Bounds.Size.Inflate(padding); } protected override Size ArrangeOverride(Size finalSize) { - if (HasComplexContent) - { - var scale = LayoutHelper.GetLayoutScale(this); + var scale = LayoutHelper.GetLayoutScale(this); + var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale); - var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale); + //Fixes: #11019 + if (finalSize.Width < _constraint.Width) + { + _textLayout?.Dispose(); + _textLayout = null; + _constraint = finalSize.Deflate(padding); + } + if (HasComplexContent) + { var currentY = padding.Top; foreach (var textLine in TextLayout.TextLines) @@ -730,7 +734,7 @@ namespace Avalonia.Controls && controlRun.Control is Control control) { control.Arrange( - new Rect(new Point(currentX, currentY), + new Rect(new Point(currentX, currentY), new Size(control.DesiredSize.Width, textLine.Height))); } From ec74cb2dc685e02b2934d12617dbbb32f78da81e Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 25 Apr 2023 18:30:39 +0200 Subject: [PATCH 069/110] Fix font fallback for control characters --- src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 253c7075fa..7d4fac337d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -185,7 +185,9 @@ namespace Avalonia.Media.TextFormatting } //Stop at the first missing glyph - if (!currentCodepoint.IsBreakChar && !glyphTypeface.TryGetGlyph(currentCodepoint, out _)) + if (!currentCodepoint.IsBreakChar && + currentCodepoint.GeneralCategory != GeneralCategory.Control && + !glyphTypeface.TryGetGlyph(currentCodepoint, out _)) { break; } From 9f3dbfff6a3e66ac4284edf0332b8168df1d7b7e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Apr 2023 22:31:44 +0200 Subject: [PATCH 070/110] Added docs to clarify difference with SizeChanged. --- src/Avalonia.Controls/WindowBase.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 463eda4f3c..0017630a75 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -83,6 +83,19 @@ namespace Avalonia.Controls /// /// Occurs when the window is resized. /// + /// + /// Although this event is similar to the event, they are + /// conceptually different: + /// + /// - is a window-level event, fired when a resize notification arrives + /// from the platform windowing subsystem. The event args contain details of the source of + /// the resize event in the property. This + /// event is raised before layout has been run on the window's content. + /// - is a layout-level event, fired when a layout pass + /// completes on a control. is present on all controls + /// and is fired when the control's size changes for any reason, including a + /// event in the case of a Window. + /// public event EventHandler? Resized; public new IWindowBaseImpl? PlatformImpl => (IWindowBaseImpl?) base.PlatformImpl; From d5b280453caefc4ad6fc0338014fdb34b85a31d1 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Tue, 25 Apr 2023 23:53:52 +0300 Subject: [PATCH 071/110] [MacOS] [Input] use uint64 timestamp in other places, including avn.idl --- native/Avalonia.Native/src/OSX/AvnView.mm | 6 +++--- src/Avalonia.Native/avn.idl | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index 9a43d9ddcf..86bacfb819 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -275,7 +275,7 @@ delta.Y = [event deltaY]; } - uint64 timestamp = static_cast([event timestamp] * 1000); + uint64_t timestamp = static_cast([event timestamp] * 1000); auto modifiers = [self getModifiers:[event modifierFlags]]; if(type != Move || @@ -444,7 +444,7 @@ auto key = s_KeyMap[[event keyCode]]; - uint32_t timestamp = static_cast([event timestamp] * 1000); + uint64_t timestamp = static_cast([event timestamp] * 1000); auto modifiers = [self getModifiers:[event modifierFlags]]; if(_parent != nullptr) @@ -657,7 +657,7 @@ [self unmarkText]; - uint32_t timestamp = static_cast([NSDate timeIntervalSinceReferenceDate] * 1000); + uint64_t timestamp = static_cast([NSDate timeIntervalSinceReferenceDate] * 1000); _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(timestamp, [text UTF8String]); diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 09e9168d8f..a58a00d59d 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -1,6 +1,7 @@ @clr-namespace Avalonia.Native.Interop @clr-access internal @clr-map bool int +@clr-map u_int64_t ulong @cpp-preamble @@ #pragma once #include "com.h" @@ -583,12 +584,12 @@ interface IAvnWindowBaseEvents : IUnknown void Resized([const] AvnSize& size, AvnPlatformResizeReason reason); void PositionChanged(AvnPoint position); void RawMouseEvent(AvnRawMouseEventType type, - uint timeStamp, + u_int64_t timeStamp, AvnInputModifiers modifiers, AvnPoint point, AvnVector delta); - bool RawKeyEvent(AvnRawKeyEventType type, uint timeStamp, AvnInputModifiers modifiers, uint key); - bool RawTextInputEvent(uint timeStamp, [const] char* text); + bool RawKeyEvent(AvnRawKeyEventType type, u_int64_t timeStamp, AvnInputModifiers modifiers, uint key); + bool RawTextInputEvent(u_int64_t timeStamp, [const] char* text); void ScalingChanged(double scaling); void RunRenderPriorityJobs(); void LostFocus(); From 6c8afc714c2fa1a4797ab2d2a914c1645f086c00 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Apr 2023 22:54:52 +0200 Subject: [PATCH 072/110] Correctly handle TreeViewPage data context change. Fixes #11120. --- .../Diagnostics/Views/TreePageView.xaml.cs | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs index 66be6b5041..b0aea64994 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs @@ -39,16 +39,6 @@ namespace Avalonia.Diagnostics.Views AdornerLayer.SetIsClipEnabled(_adorner, false); } - protected override void OnDataContextChanged(EventArgs e) - { - base.OnDataContextChanged(e); - - ((TreePageViewModel)DataContext!).ClipboardCopyRequested += (sender, s) => - { - TopLevel.GetTopLevel(this)?.Clipboard?.SetTextAsync(s); - }; - } - protected void AddAdorner(object? sender, PointerEventArgs e) { var node = (TreeNode?)((Control)sender!).DataContext; @@ -108,9 +98,27 @@ namespace Avalonia.Diagnostics.Views _currentLayer = null; } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == DataContextProperty) + { + if (change.GetOldValue() is TreePageViewModel oldViewModel) + oldViewModel.ClipboardCopyRequested -= OnClipboardCopyRequested; + if (change.GetNewValue() is TreePageViewModel newViewModel) + newViewModel.ClipboardCopyRequested += OnClipboardCopyRequested; + } + } + private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } + + private void OnClipboardCopyRequested(object? sender, string e) + { + TopLevel.GetTopLevel(this)?.Clipboard?.SetTextAsync(e); + } } } From 4016da0a2c63d241b888b9682db186dddb631a16 Mon Sep 17 00:00:00 2001 From: Yannick Excoffier Date: Wed, 26 Apr 2023 09:24:17 +0200 Subject: [PATCH 073/110] Set double click on presenter instead of whole header --- src/Avalonia.Controls/TreeViewItem.cs | 6 ++++-- src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml | 2 +- src/Avalonia.Themes.Simple/Controls/TreeViewItem.xaml | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 806d7e320b..70ffd218b1 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -45,6 +45,7 @@ namespace Avalonia.Controls private TreeView? _treeView; private Control? _header; + private Control? _headerPresenter; private int _level; private bool _templateApplied; private bool _deferredBringIntoViewFlag; @@ -255,15 +256,16 @@ namespace Avalonia.Controls protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { - if (_header is InputElement previousInputMethod) + if (_headerPresenter is InputElement previousInputMethod) { previousInputMethod.DoubleTapped -= HeaderDoubleTapped; } _header = e.NameScope.Find("PART_Header"); + _headerPresenter = e.NameScope.Find("PART_HeaderPresenter"); _templateApplied = true; - if (_header is InputElement im) + if (_headerPresenter is InputElement im) { im.DoubleTapped += HeaderDoubleTapped; } diff --git a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml index b5f1220bc8..eff3920b12 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml @@ -75,7 +75,6 @@ MinHeight="{TemplateBinding MinHeight}" TemplatedControl.IsTemplateFocusTarget="True"> Date: Wed, 26 Apr 2023 11:31:58 +0200 Subject: [PATCH 074/110] Enable source generators in Sandbox. --- samples/Sandbox/MainWindow.axaml.cs | 10 ++-------- samples/Sandbox/Sandbox.csproj | 2 ++ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/samples/Sandbox/MainWindow.axaml.cs b/samples/Sandbox/MainWindow.axaml.cs index 23d45edf6a..b8e9f0ff42 100644 --- a/samples/Sandbox/MainWindow.axaml.cs +++ b/samples/Sandbox/MainWindow.axaml.cs @@ -6,17 +6,11 @@ using Avalonia.Win32.WinRT.Composition; namespace Sandbox { - public class MainWindow : Window + public partial class MainWindow : Window { public MainWindow() { - this.InitializeComponent(); - this.AttachDevTools(); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); + InitializeComponent(); } } } diff --git a/samples/Sandbox/Sandbox.csproj b/samples/Sandbox/Sandbox.csproj index f23e391a2a..d2e66988e0 100644 --- a/samples/Sandbox/Sandbox.csproj +++ b/samples/Sandbox/Sandbox.csproj @@ -4,6 +4,7 @@ WinExe net6.0 true + true @@ -17,4 +18,5 @@ + From 582248fb048b49f7f34897e227fae627d34f1e4b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 26 Apr 2023 12:45:12 +0200 Subject: [PATCH 075/110] Added failing test for #11128. --- .../ItemsControlTests.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 1a0ea5fdab..5e741cdc1d 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -828,6 +828,19 @@ namespace Avalonia.Controls.UnitTests Layout(target); } + [Fact] + public void ItemIsOwnContainer_Content_Should_Not_Be_Cleared_When_Removed() + { + // Issue #11128. + using var app = Start(); + var item = new ContentPresenter { Content = "foo" }; + var target = CreateTarget(items: new[] { item }); + + target.Items.RemoveAt(0); + + Assert.Equal("foo", item.Content); + } + private static ItemsControl CreateTarget( object? dataContext = null, IBinding? displayMemberBinding = null, From c77276d00a7dc3640172aeeb364fe846ae1d1a89 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 26 Apr 2023 12:47:28 +0200 Subject: [PATCH 076/110] Don't clear ItemIsOwnContainer in PanelContainerGenerator. One shouldn't call `ClearContainer` on a container that is an item. Adjusted `SelectingItemsControlTests` because selection is actually maintained on move with containers hold their own `IsSelected` state. Fixes #11128 --- .../Presenters/PanelContainerGenerator.cs | 5 +- .../Primitives/SelectingItemsControlTests.cs | 49 ++++++++++++++++--- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs b/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs index 796ee8433a..5a6f9fc4f9 100644 --- a/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs +++ b/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs @@ -67,9 +67,12 @@ namespace Avalonia.Controls.Presenters for (var i = 0; i < count; ++i) { var c = children[index + i]; + if (!c.IsSet(ItemIsOwnContainerProperty)) + { itemsControl.RemoveLogicalChild(children[i + index]); - generator.ClearItemContainer(c); + generator.ClearItemContainer(c); + } } children.RemoveRange(index, count); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index db6460e8aa..dfdcd09bf9 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -676,12 +676,8 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Moving_Selected_Item_Should_Clear_Selection() { - var items = new AvaloniaList - { - new Item(), - new Item(), - }; - + using var app = Start(); + var items = new ObservableCollection { "foo", "bar" }; var target = new SelectingItemsControl { ItemsSource = items, @@ -706,7 +702,46 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.NotNull(receivedArgs); Assert.Empty(receivedArgs.AddedItems); Assert.Equal(new[] { removed }, receivedArgs.RemovedItems); - Assert.All(items, x => Assert.False(x.IsSelected)); + } + + [Fact] + public void Moving_Selected_Container_Should_Not_Clear_Selection() + { + var items = new AvaloniaList + { + new Item(), + new Item(), + }; + + var target = new SelectingItemsControl + { + ItemsSource = items, + Template = Template(), + }; + + Prepare(target); + target.SelectedIndex = 1; + + Assert.Equal(items[1], target.SelectedItem); + Assert.Equal(1, target.SelectedIndex); + + var receivedArgs = new List(); + + target.SelectionChanged += (_, args) => receivedArgs.Add(args); + + var moved = items[1]; + items.Move(1, 0); + + // Because the moved container is still marked as selected on the insert part of the + // move, it will remain selected. + Assert.Same(moved, target.SelectedItem); + Assert.Equal(0, target.SelectedIndex); + Assert.NotNull(receivedArgs); + Assert.Equal(2, receivedArgs.Count); + Assert.Equal(new[] { moved }, receivedArgs[0].RemovedItems); + Assert.Equal(new[] { moved }, receivedArgs[1].AddedItems); + Assert.True(items[0].IsSelected); + Assert.False(items[1].IsSelected); } [Fact] From 2c9f286db066acfa44e7d3f938f5026a2e2ded09 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 26 Apr 2023 14:27:48 +0200 Subject: [PATCH 077/110] Added failing tests for #11119. --- .../CarouselTests.cs | 65 +++++++++++++++++++ .../SelectingItemsControlTests_Multiple.cs | 52 ++++++++++++++- 2 files changed, 116 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs index 6624d13165..2a35787f3b 100644 --- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs @@ -261,6 +261,71 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Can_Move_Forward_Back_Forward() + { + using var app = Start(); + var items = new[] { "foo", "bar" }; + var target = new Carousel + { + Template = CarouselTemplate(), + ItemsSource = items, + }; + + Prepare(target); + + target.SelectedIndex = 1; + Layout(target); + + Assert.Equal(1, target.SelectedIndex); + + target.SelectedIndex = 0; + Layout(target); + + Assert.Equal(0, target.SelectedIndex); + + target.SelectedIndex = 1; + Layout(target); + + Assert.Equal(1, target.SelectedIndex); + } + + [Fact] + public void Can_Move_Forward_Back_Forward_With_Control_Items() + { + // Issue #11119 + using var app = Start(); + var items = new[] { new Canvas(), new Canvas() }; + var target = new Carousel + { + Template = CarouselTemplate(), + ItemsSource = items, + }; + + Prepare(target); + + target.SelectedIndex = 1; + Layout(target); + + Assert.Equal(1, target.SelectedIndex); + + target.SelectedIndex = 0; + Layout(target); + + Assert.Equal(0, target.SelectedIndex); + + target.SelectedIndex = 1; + target.PropertyChanged += (s, e) => + { + if (e.Property == Carousel.SelectedIndexProperty) + { + } + }; + Layout(target); + + Assert.Equal(1, target.SelectedIndex); + } + private static IDisposable Start() => UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); private static void Prepare(Carousel target) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 0817979e33..928c0c94ef 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -1024,6 +1024,56 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(new[] { 15 }, SelectedContainers(target)); } + [Fact] + public void Can_Change_Selection_For_Containers_Outside_Of_Viewport() + { + // Issue #11119 + using var app = Start(); + var items = Enumerable.Range(0, 100).Select(x => new TestContainer + { + Content = $"Item {x}", + Height = 100, + }).ToList(); + + // Create a SelectingItemsControl with a virtualizing stack panel. + var target = CreateTarget(itemsSource: items, virtualizing: true); + target.AutoScrollToSelectedItem = false; + + var panel = Assert.IsType(target.ItemsPanelRoot); + var scroll = panel.FindAncestorOfType()!; + + // Select item 1. + target.SelectedIndex = 1; + + // Scroll item 1 and 2 out of view. + scroll.Offset = new(0, 1000); + Layout(target); + + Assert.Equal(10, panel.FirstRealizedIndex); + Assert.Equal(19, panel.LastRealizedIndex); + + // Select item 2 now that items 1 and 2 are both unrealized. + target.SelectedIndex = 2; + + // The selection should be updated. + Assert.Empty(SelectedContainers(target)); + Assert.Equal(2, target.SelectedIndex); + Assert.Same(items[2], target.SelectedItem); + Assert.Equal(new[] { 2 }, target.Selection.SelectedIndexes); + Assert.Equal(new[] { items[2] }, target.Selection.SelectedItems); + + // Scroll selected item back into view. + scroll.Offset = new(0, 0); + Layout(target); + + // The selection should be preserved. + Assert.Equal(new[] { 2 }, SelectedContainers(target)); + Assert.Equal(2, target.SelectedIndex); + Assert.Same(items[2], target.SelectedItem); + Assert.Equal(new[] { 2 }, target.Selection.SelectedIndexes); + Assert.Equal(new[] { items[2] }, target.Selection.SelectedItems); + } + [Fact] public void Selection_State_Change_On_Unrealized_Item_Is_Respected_With_IsSelected_Binding() { @@ -1197,7 +1247,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Setters = { - new Setter(TreeView.TemplateProperty, CreateTestContainerTemplate()), + new Setter(TestContainer.TemplateProperty, CreateTestContainerTemplate()), }, }; } From 8a354d8cb9251ff666d226000af1e5ae6d34cfd0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 26 Apr 2023 16:54:23 +0200 Subject: [PATCH 078/110] Only prepare items that are containers once. When an `ItemsControl` returns true for `IsItemItsOwnContainer`, `ItemContainerPrepared` should only be called once the first time the container is prepared. Requires that `ContainerFromIndex` returns `ItemIsOwnContainer` items that have previously been prepared in order for `SelectingItemsControl` to update their selection correctly when outside the realized viewport. Fixes #11119 --- .../Generators/ItemContainerGenerator.cs | 28 ++++++++++++++++--- .../VirtualizingCarouselPanel.cs | 9 ++++-- src/Avalonia.Controls/VirtualizingPanel.cs | 5 ++++ .../VirtualizingStackPanel.cs | 14 ++++++++-- 4 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index 57ed67b508..f2b105c901 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -7,8 +7,8 @@ namespace Avalonia.Controls.Generators /// /// /// When creating a container for an item from a , the following - /// method order should be followed: - /// + /// process should be followed: + /// /// - should first be called if the item is /// derived from the class. If this method returns true then the /// item itself should be used as the container. @@ -19,9 +19,29 @@ namespace Avalonia.Controls.Generators /// - The container should then be added to the panel using /// /// - Finally, should be called. - /// - When the item is ready to be recycled, should - /// be called if returned false. /// + /// NOTE: If in the first step above returns true + /// then the above steps should be carried out a single time; the first time the item is + /// displayed. Otherwise the steps should be carried out each time a new container is realized + /// for an item. + /// + /// When unrealizing a container, the following process should be followed: + /// + /// - If for the item returned true then the item + /// cannot be unrealized or recycled. + /// - Otherwise, should be called for the container + /// - If recycling is supported then the container should be added to a recycle pool. + /// - It is assumed that recyclable containers will not be removed from the panel but instead + /// hidden from view using e.g. `container.IsVisible = false`. + /// + /// When recycling an unrealized container, the following process should be followed: + /// + /// - An element should be taken from the recycle pool. + /// - The container should be made visible. + /// - method should be called for the + /// container. + /// - should be called. + /// /// NOTE: Although this class is similar to that found in WPF/UWP, in Avalonia this class only /// concerns itself with generating and clearing item containers; it does not maintain a /// record of the currently realized containers, that responsibility is delegated to the diff --git a/src/Avalonia.Controls/VirtualizingCarouselPanel.cs b/src/Avalonia.Controls/VirtualizingCarouselPanel.cs index da0ff1eb69..28d6a83309 100644 --- a/src/Avalonia.Controls/VirtualizingCarouselPanel.cs +++ b/src/Avalonia.Controls/VirtualizingCarouselPanel.cs @@ -168,7 +168,13 @@ namespace Avalonia.Controls protected internal override Control? ContainerFromIndex(int index) { - return index == _realizedIndex ? _realized : null; + if (index < 0 || index >= Items.Count) + return null; + if (index == _realizedIndex) + return _realized; + if (Items[index] is Control c && c.GetValue(ItemIsOwnContainerProperty)) + return c; + return null; } protected internal override IEnumerable? GetRealizedContainers() @@ -264,7 +270,6 @@ namespace Avalonia.Controls if (controlItem.IsSet(ItemIsOwnContainerProperty)) { controlItem.IsVisible = true; - generator.ItemContainerPrepared(controlItem, item, index); return controlItem; } else if (generator.IsItemItsOwnContainer(controlItem)) diff --git a/src/Avalonia.Controls/VirtualizingPanel.cs b/src/Avalonia.Controls/VirtualizingPanel.cs index a95d4f1ffa..ed92f30454 100644 --- a/src/Avalonia.Controls/VirtualizingPanel.cs +++ b/src/Avalonia.Controls/VirtualizingPanel.cs @@ -76,6 +76,11 @@ namespace Avalonia.Controls /// The container for the item at the specified index within the item collection, if the /// item is realized; otherwise, null. /// + /// + /// Note for implementors: if the item at the the specified index is an ItemIsOwnContainer + /// item that has previously been realized, then the item should be returned even if it + /// currently falls outside the realized viewport. + /// protected internal abstract Control? ContainerFromIndex(int index); /// diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 77268c7831..67ec238ceb 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Linq; -using System.Reflection; using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; using Avalonia.Input; @@ -326,7 +325,17 @@ namespace Avalonia.Controls return _realizedElements?.Elements.Where(x => x is not null)!; } - protected internal override Control? ContainerFromIndex(int index) => _realizedElements?.GetElement(index); + protected internal override Control? ContainerFromIndex(int index) + { + if (index < 0 || index >= Items.Count) + return null; + if (_realizedElements?.GetElement(index) is { } realized) + return realized; + if (Items[index] is Control c && c.GetValue(ItemIsOwnContainerProperty)) + return c; + return null; + } + protected internal override int IndexFromContainer(Control container) => _realizedElements?.GetIndex(container) ?? -1; protected internal override Control? ScrollIntoView(int index) @@ -578,7 +587,6 @@ namespace Avalonia.Controls if (controlItem.IsSet(ItemIsOwnContainerProperty)) { controlItem.IsVisible = true; - generator.ItemContainerPrepared(controlItem, item, index); return controlItem; } else if (generator.IsItemItsOwnContainer(controlItem)) From 1bdb067ab9e422ba89aea86871c0cf4304ff8931 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 27 Apr 2023 16:42:59 +0600 Subject: [PATCH 079/110] external consumers --- build/ExternalConsumers.props | 32 ++++++++++++++++++++++++++++++++ src/Directory.Build.props | 1 + 2 files changed, 33 insertions(+) create mode 100644 build/ExternalConsumers.props diff --git a/build/ExternalConsumers.props b/build/ExternalConsumers.props new file mode 100644 index 0000000000..d79e951330 --- /dev/null +++ b/build/ExternalConsumers.props @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 55a1014188..45cd4a932b 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,6 +3,7 @@ + Shared\_ModuleInitializer.cs From 271cb55b85c09903c49727c43596bcc4d643dd0e Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 27 Apr 2023 17:48:19 +0600 Subject: [PATCH 080/110] Remove ModuleInitializer duplicates --- src/Avalonia.Base/Avalonia.Base.csproj | 1 + src/Directory.Build.props | 6 ------ src/Windows/Avalonia.Win32/Avalonia.Win32.csproj | 1 - 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 639c27bf03..eafff3b780 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -22,6 +22,7 @@ + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 45cd4a932b..51f6cc92e9 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -4,10 +4,4 @@ - - - Shared\_ModuleInitializer.cs - false - - diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index 2c5f3e2ed1..61f5996a94 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -20,7 +20,6 @@ - From 5391361d4501ddde8eebeb0525fcf82e4b616754 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 27 Apr 2023 22:39:50 +0200 Subject: [PATCH 081/110] Failing test for selection w/ virtualization. --- .../SelectingItemsControlTests_Multiple.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 928c0c94ef..daebc1e709 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -1074,6 +1074,40 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(new[] { items[2] }, target.Selection.SelectedItems); } + [Fact] + public void Selection_Is_Not_Cleared_On_Recycling_Containers() + { + using var app = Start(); + var items = Enumerable.Range(0, 100).Select(x => new ItemViewModel($"Item {x}", false)).ToList(); + + // Create a SelectingItemsControl that creates containers that raise IsSelectedChanged, + // with a virtualizing stack panel. + var target = CreateTarget( + itemsSource: items, + virtualizing: true); + target.AutoScrollToSelectedItem = false; + + var panel = Assert.IsType(target.ItemsPanelRoot); + var scroll = panel.FindAncestorOfType()!; + + // Select item 1. + target.SelectedIndex = 1; + + // Scroll item 1 out of view. + scroll.Offset = new(0, 1000); + Layout(target); + + Assert.Equal(10, panel.FirstRealizedIndex); + Assert.Equal(19, panel.LastRealizedIndex); + + // The selection should be preserved. + Assert.Empty(SelectedContainers(target)); + Assert.Equal(1, target.SelectedIndex); + Assert.Same(items[1], target.SelectedItem); + Assert.Equal(new[] { 1 }, target.Selection.SelectedIndexes); + Assert.Equal(new[] { items[1] }, target.Selection.SelectedItems); + } + [Fact] public void Selection_State_Change_On_Unrealized_Item_Is_Respected_With_IsSelected_Binding() { @@ -1248,6 +1282,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Setters = { new Setter(TestContainer.TemplateProperty, CreateTestContainerTemplate()), + new Setter(TestContainer.HeightProperty, 100.0), }, }; } From 8a5a5c8d441d10699f5b696524064d0fddae3716 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 27 Apr 2023 22:51:43 +0200 Subject: [PATCH 082/110] Remove element from list before recycling. Remove the element from the realized element list before recycling it, so that `IndexFromContainer` and `ContainerFromIndex` report the container as not found during `ClearItemContainer`. Fixes selection being reset when a container is unrealized. --- .../Utils/RealizedStackElements.cs | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/Utils/RealizedStackElements.cs b/src/Avalonia.Controls/Utils/RealizedStackElements.cs index 8dbfb2c957..11bbaa11c4 100644 --- a/src/Avalonia.Controls/Utils/RealizedStackElements.cs +++ b/src/Avalonia.Controls/Utils/RealizedStackElements.cs @@ -353,7 +353,10 @@ namespace Avalonia.Controls.Utils for (var i = start; i < end; ++i) { if (_elements[i] is Control element) + { + _elements[i] = null; recycleElement(element); + } } _elements.RemoveRange(start, end - start); @@ -389,10 +392,13 @@ namespace Avalonia.Controls.Utils if (_elements is null || _elements.Count == 0) return; - foreach (var e in _elements) + for (var i = 0; i < _elements.Count; i++) { - if (e is not null) + if (_elements[i] is Control e) + { + _elements[i] = null; recycleElement(e); + } } _startU = _firstIndex = 0; @@ -422,7 +428,10 @@ namespace Avalonia.Controls.Utils for (var i = 0; i < endIndex; ++i) { if (_elements[i] is Control e) + { + _elements[i] = null; recycleElement(e, i + FirstIndex); + } } _elements.RemoveRange(0, endIndex); @@ -453,7 +462,10 @@ namespace Avalonia.Controls.Utils for (var i = startIndex; i < count; ++i) { if (_elements[i] is Control e) + { + _elements[i] = null; recycleElement(e, i + FirstIndex); + } } _elements.RemoveRange(startIndex, _elements.Count - startIndex); @@ -470,13 +482,13 @@ namespace Avalonia.Controls.Utils if (_elements is null || _elements.Count == 0) return; - var i = FirstIndex; - - foreach (var e in _elements) + for (var i = 0; i < _elements.Count; i++) { - if (e is not null) - recycleElement(e, i); - ++i; + if (_elements[i] is Control e) + { + _elements[i] = null; + recycleElement(e, i + FirstIndex); + } } _startU = _firstIndex = 0; From 3d693beffa5d548199b555febf3af1c6e3128bde Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 27 Apr 2023 22:57:17 +0200 Subject: [PATCH 083/110] Clarify `ClearItemContainer` docs. And change usages of "should" to "must" as it's not optional. --- .../Generators/ItemContainerGenerator.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index f2b105c901..b2c138599e 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -85,7 +85,7 @@ namespace Avalonia.Controls.Generators /// The index of the item to display. /// /// If is true for an item, then this method - /// only needs to be called a single time, otherwise this method should be called after the + /// must only be called a single time, otherwise this method must be called after the /// container is created, and each subsequent time the container is recycled to display a /// new item. /// @@ -100,10 +100,11 @@ namespace Avalonia.Controls.Generators /// The item being displayed. /// The index of the item being displayed. /// - /// This method should be called when a container has been fully prepared and added + /// This method must be called when a container has been fully prepared and added /// to the logical and visual trees, but may be called before a layout pass has completed. - /// It should be called regardless of the result of - /// . + /// It must be called regardless of the result of + /// but if that method returned true then + /// must be called only a single time. /// public void ItemContainerPrepared(Control container, object? item, int index) => _owner.ItemContainerPrepared(container, item, index); @@ -122,6 +123,12 @@ namespace Avalonia.Controls.Generators /// Undoes the effects of the method. /// /// The container control. + /// + /// This method must be called when a container is unrealized. The container must have + /// already have been removed from the virtualizing panel's list of realized containers before + /// this method is called. This method must not be called if + /// returned true for the item. + /// public void ClearItemContainer(Control container) => _owner.ClearItemContainer(container); [Obsolete("Use ItemsControl.ContainerFromIndex")] From 92b5a46b27bed1db89bc13f23aaa45ef015c1d70 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Fri, 28 Apr 2023 01:45:22 +0300 Subject: [PATCH 084/110] [Input] fix also timestamp on C# side --- src/Avalonia.Native/WindowImplBase.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 26c3da9d50..b802b1db71 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -220,17 +220,17 @@ namespace Avalonia.Native _parent.PositionChanged?.Invoke(position.ToAvaloniaPixelPoint()); } - void IAvnWindowBaseEvents.RawMouseEvent(AvnRawMouseEventType type, uint timeStamp, AvnInputModifiers modifiers, AvnPoint point, AvnVector delta) + void IAvnWindowBaseEvents.RawMouseEvent(AvnRawMouseEventType type, ulong timeStamp, AvnInputModifiers modifiers, AvnPoint point, AvnVector delta) { _parent.RawMouseEvent(type, timeStamp, modifiers, point, delta); } - int IAvnWindowBaseEvents.RawKeyEvent(AvnRawKeyEventType type, uint timeStamp, AvnInputModifiers modifiers, uint key) + int IAvnWindowBaseEvents.RawKeyEvent(AvnRawKeyEventType type, ulong timeStamp, AvnInputModifiers modifiers, uint key) { return _parent.RawKeyEvent(type, timeStamp, modifiers, key).AsComBool(); } - int IAvnWindowBaseEvents.RawTextInputEvent(uint timeStamp, string text) + int IAvnWindowBaseEvents.RawTextInputEvent(ulong timeStamp, string text) { return _parent.RawTextInputEvent(timeStamp, text).AsComBool(); } @@ -286,7 +286,7 @@ namespace Avalonia.Native _native?.Activate(); } - public bool RawTextInputEvent(uint timeStamp, string text) + public bool RawTextInputEvent(ulong timeStamp, string text) { if (_inputRoot is null) return false; @@ -300,7 +300,7 @@ namespace Avalonia.Native return args.Handled; } - public bool RawKeyEvent(AvnRawKeyEventType type, uint timeStamp, AvnInputModifiers modifiers, uint key) + public bool RawKeyEvent(AvnRawKeyEventType type, ulong timeStamp, AvnInputModifiers modifiers, uint key) { if (_inputRoot is null) return false; @@ -319,7 +319,7 @@ namespace Avalonia.Native return false; } - public void RawMouseEvent(AvnRawMouseEventType type, uint timeStamp, AvnInputModifiers modifiers, AvnPoint point, AvnVector delta) + public void RawMouseEvent(AvnRawMouseEventType type, ulong timeStamp, AvnInputModifiers modifiers, AvnPoint point, AvnVector delta) { if (_inputRoot is null) return; From 110b536738e5ca16776eb7b9d8bddd4822a42c77 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 28 Apr 2023 11:55:37 +0200 Subject: [PATCH 085/110] fix(Animation): fix Issue #11156 System.InvalidOperationException: Call from invalid thread --- src/Avalonia.Base/Animation/Animation.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Animation/Animation.cs b/src/Avalonia.Base/Animation/Animation.cs index d62acc0d52..dd99c40cd3 100644 --- a/src/Avalonia.Base/Animation/Animation.cs +++ b/src/Avalonia.Base/Animation/Animation.cs @@ -200,7 +200,7 @@ namespace Avalonia.Animation /// /// The animation setter. /// The property animator value. - public static void SetAnimator(IAnimationSetter setter, + public static void SetAnimator(IAnimationSetter setter, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicMethods)] Type value) { @@ -319,7 +319,7 @@ namespace Avalonia.Animation if (animators.Count == 1) { var subscription = animators[0].Apply(this, control, clock, match, onComplete); - + if (subscription is not null) { subscriptions.Add(subscription); @@ -348,9 +348,11 @@ namespace Avalonia.Animation if (onComplete != null) { - Task.WhenAll(completionTasks!).ContinueWith( - (_, state) => ((Action)state!).Invoke(), - onComplete); + Task.WhenAll(completionTasks!) + .ContinueWith((_, state) => ((Action)state!).Invoke() + , onComplete + , TaskScheduler.FromCurrentSynchronizationContext() + ); } } return new CompositeDisposable(subscriptions); From c4a5567090357c49d0b69634406d6cb3195cd625 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 28 Apr 2023 12:32:20 +0200 Subject: [PATCH 086/110] Added failing test for #11161. --- .../Layout/LayoutManagerTests.cs | 33 +++++++++++++++++++ .../Layout/LayoutTestControl.cs | 32 ++++++++++++++---- 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs b/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs index 09f78c5a6c..45a6efdd4a 100644 --- a/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs +++ b/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs @@ -514,5 +514,38 @@ namespace Avalonia.Base.UnitTests.Layout Assert.True(parent.IsMeasureValid); Assert.True(parent.IsArrangeValid); } + + [Fact] + public void Grandparent_Can_Invalidate_Root_Measure_During_Arrange() + { + // Issue #11161. + var child = new LayoutTestControl(); + var parent = new LayoutTestControl { Child = child }; + var grandparent = new LayoutTestControl { Child = parent }; + var root = new LayoutTestRoot { Child = grandparent }; + + root.LayoutManager.ExecuteInitialLayoutPass(); + + grandparent.DoArrangeOverride = (_, s) => + { + root.InvalidateMeasure(); + return s; + }; + grandparent.CallBaseArrange = true; + + child.InvalidateMeasure(); + grandparent.InvalidateMeasure(); + + root.LayoutManager.ExecuteLayoutPass(); + + Assert.True(child.IsMeasureValid); + Assert.True(child.IsArrangeValid); + Assert.True(parent.IsMeasureValid); + Assert.True(parent.IsArrangeValid); + Assert.True(grandparent.IsMeasureValid); + Assert.True(grandparent.IsArrangeValid); + Assert.True(root.IsMeasureValid); + Assert.True(root.IsArrangeValid); + } } } diff --git a/tests/Avalonia.Base.UnitTests/Layout/LayoutTestControl.cs b/tests/Avalonia.Base.UnitTests/Layout/LayoutTestControl.cs index 62de81006e..d85c7ed9bc 100644 --- a/tests/Avalonia.Base.UnitTests/Layout/LayoutTestControl.cs +++ b/tests/Avalonia.Base.UnitTests/Layout/LayoutTestControl.cs @@ -10,21 +10,41 @@ namespace Avalonia.Base.UnitTests.Layout public bool Arranged { get; set; } public Func DoMeasureOverride { get; set; } public Func DoArrangeOverride { get; set; } + public bool CallBaseMeasure { get; set; } + public bool CallBaseArrange { get; set; } protected override Size MeasureOverride(Size availableSize) { Measured = true; - return DoMeasureOverride != null ? - DoMeasureOverride(this, availableSize) : - base.MeasureOverride(availableSize); + + if (DoMeasureOverride is not null) + { + var overrideResult = DoMeasureOverride(this, availableSize); + return CallBaseMeasure ? + base.MeasureOverride(overrideResult) : + overrideResult; + } + else + { + return base.MeasureOverride(availableSize); + } } protected override Size ArrangeOverride(Size finalSize) { Arranged = true; - return DoArrangeOverride != null ? - DoArrangeOverride(this, finalSize) : - base.ArrangeOverride(finalSize); + + if (DoArrangeOverride is not null) + { + var overrideResult = DoArrangeOverride(this, finalSize); + return CallBaseArrange ? + base.ArrangeOverride(overrideResult) : + overrideResult; + } + else + { + return base.ArrangeOverride(finalSize); + } } } } From a03b73a7347fe442e141ead2662328cf686f3eee Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 28 Apr 2023 13:02:37 +0200 Subject: [PATCH 087/110] Always add control in measure queue to arrange queue. And also removed the checks for `IsAttachedToVisualTree`: that's going to be checked in `Arrange` and `Measure`. Fixes #11161 --- src/Avalonia.Base/Layout/LayoutManager.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs index 747ee1c082..f47738f2e4 100644 --- a/src/Avalonia.Base/Layout/LayoutManager.cs +++ b/src/Avalonia.Base/Layout/LayoutManager.cs @@ -249,10 +249,12 @@ namespace Avalonia.Layout { var control = _toMeasure.Dequeue(); - if (!control.IsMeasureValid && control.IsAttachedToVisualTree) + if (!control.IsMeasureValid) { Measure(control); } + + _toArrange.Enqueue(control); } } @@ -262,7 +264,7 @@ namespace Avalonia.Layout { var control = _toArrange.Dequeue(); - if (!control.IsArrangeValid && control.IsAttachedToVisualTree) + if (!control.IsArrangeValid) { Arrange(control); } @@ -297,8 +299,6 @@ namespace Avalonia.Layout { control.Measure(control.PreviousMeasure.Value); } - - _toArrange.Enqueue(control); } return true; From 9a062901425a5166c23a5334df649d4d50412903 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Fri, 28 Apr 2023 17:51:41 +0200 Subject: [PATCH 088/110] Correctly remove ContentPresenter's content from its parent host When the content is updated while being detached from the visual tree. --- src/Avalonia.Controls/Presenters/ContentPresenter.cs | 9 ++++++--- tests/Avalonia.Controls.UnitTests/ContentControlTests.cs | 5 +++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 329a0fa6ab..736c338c10 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -1,5 +1,5 @@ using System; - +using Avalonia.Collections; using Avalonia.Controls.Documents; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; @@ -442,7 +442,7 @@ namespace Avalonia.Controls.Presenters var contentTemplate = ContentTemplate; var oldChild = Child; var newChild = CreateChild(content, oldChild, contentTemplate); - var logicalChildren = Host?.LogicalChildren ?? LogicalChildren; + var logicalChildren = GetEffectiveLogicalChildren(); // Remove the old child if we're not recycling it. if (newChild != oldChild) @@ -488,6 +488,9 @@ namespace Avalonia.Controls.Presenters } + private IAvaloniaList GetEffectiveLogicalChildren() + => Host?.LogicalChildren ?? LogicalChildren; + /// protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { @@ -692,7 +695,7 @@ namespace Avalonia.Controls.Presenters else if (Child != null) { VisualChildren.Remove(Child); - LogicalChildren.Remove(Child); + GetEffectiveLogicalChildren().Remove(Child); ((ISetInheritanceParent)Child).SetParent(Child.Parent); Child = null; _recyclingDataTemplate = null; diff --git a/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs b/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs index bece711426..d5e4693666 100644 --- a/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs @@ -359,16 +359,21 @@ namespace Avalonia.Controls.UnitTests target.Presenter.ApplyTemplate(); Assert.Equal(target, target.Presenter.Child.GetLogicalParent()); + Assert.Equal(new[] { target.Presenter.Child }, target.LogicalChildren); root.Child = null; Assert.Null(target.Template); target.Content = null; + + Assert.Empty(target.LogicalChildren); + root.Child = target; target.Content = "Bar"; Assert.Equal(target, target.Presenter.Child.GetLogicalParent()); + Assert.Equal(new[] { target.Presenter.Child }, target.LogicalChildren); } private static FuncControlTemplate GetTemplate() From b9fd35051f9352148804e2f676623a2e1f30893d Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Thu, 27 Apr 2023 12:41:39 +0000 Subject: [PATCH 089/110] store IRuntimePlatform in DeferredParentServiceProvider --- .../XamlIl/Runtime/XamlIlRuntimeHelpers.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index 0cc7cc5468..6f5e822079 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -50,6 +50,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime private readonly IServiceProvider? _parentProvider; private readonly List? _parentResourceNodes; private readonly INameScope _nameScope; + private readonly IRuntimePlatform? _runtimePlatform; public DeferredParentServiceProvider(IServiceProvider? parentProvider, List? parentResourceNodes, object rootObject, INameScope nameScope) @@ -58,6 +59,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime _parentResourceNodes = parentResourceNodes; _nameScope = nameScope; RootObject = rootObject; + _runtimePlatform = AvaloniaLocator.Current.GetService(); } public IEnumerable Parents => GetParents(); @@ -80,6 +82,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime return this; if (serviceType == typeof(IAvaloniaXamlIlControlTemplateProvider)) return this; + if (serviceType == typeof(IRuntimePlatform)) + return _runtimePlatform; return _parentProvider?.GetService(serviceType); } From 75cbab78ad4f6b5f1fd22769bfdc1177594f26bb Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Sat, 29 Apr 2023 08:00:14 +0000 Subject: [PATCH 090/110] only store runtime platform when requested --- .../XamlIl/Runtime/XamlIlRuntimeHelpers.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index 6f5e822079..f8eab5b654 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -50,7 +50,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime private readonly IServiceProvider? _parentProvider; private readonly List? _parentResourceNodes; private readonly INameScope _nameScope; - private readonly IRuntimePlatform? _runtimePlatform; + private IRuntimePlatform? _runtimePlatform; public DeferredParentServiceProvider(IServiceProvider? parentProvider, List? parentResourceNodes, object rootObject, INameScope nameScope) @@ -59,7 +59,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime _parentResourceNodes = parentResourceNodes; _nameScope = nameScope; RootObject = rootObject; - _runtimePlatform = AvaloniaLocator.Current.GetService(); } public IEnumerable Parents => GetParents(); @@ -83,7 +82,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime if (serviceType == typeof(IAvaloniaXamlIlControlTemplateProvider)) return this; if (serviceType == typeof(IRuntimePlatform)) + { + if(_runtimePlatform == null) + _runtimePlatform = AvaloniaLocator.Current.GetService(); return _runtimePlatform; + } return _parentProvider?.GetService(serviceType); } From bc79b388b57703a20b09f7c8de38b2d19b6aaab0 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 29 Apr 2023 22:06:45 -0400 Subject: [PATCH 091/110] Use ImmediateDrawingContext in the ICustomDrawOperation interface --- samples/RenderDemo/Pages/CustomSkiaPage.cs | 4 ++-- .../Media/PlatformDrawingContext.cs | 16 ++++++++++++++-- .../Rendering/SceneGraph/CustomDrawOperation.cs | 14 ++++++++++++-- .../Avalonia.Browser/WebEmbeddableControlRoot.cs | 2 +- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/samples/RenderDemo/Pages/CustomSkiaPage.cs b/samples/RenderDemo/Pages/CustomSkiaPage.cs index 4a3e20ff5b..2d64ba8f7b 100644 --- a/samples/RenderDemo/Pages/CustomSkiaPage.cs +++ b/samples/RenderDemo/Pages/CustomSkiaPage.cs @@ -44,9 +44,9 @@ namespace RenderDemo.Pages public bool HitTest(Point p) => false; public bool Equals(ICustomDrawOperation other) => false; static Stopwatch St = Stopwatch.StartNew(); - public void Render(IDrawingContextImpl context) + public void Render(ImmediateDrawingContext context) { - var leaseFeature = context.GetFeature(); + var leaseFeature = context.TryGetFeature(); if (leaseFeature == null) context.DrawGlyphRun(Brushes.Black, _noSkia.PlatformImpl); else diff --git a/src/Avalonia.Base/Media/PlatformDrawingContext.cs b/src/Avalonia.Base/Media/PlatformDrawingContext.cs index eb8a93722c..4b683c6acb 100644 --- a/src/Avalonia.Base/Media/PlatformDrawingContext.cs +++ b/src/Avalonia.Base/Media/PlatformDrawingContext.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Avalonia.Logging; using Avalonia.Media.Imaging; using Avalonia.Media.Immutable; using Avalonia.Platform; @@ -41,8 +42,19 @@ internal sealed class PlatformDrawingContext : DrawingContext, IDrawingContextWi BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) => _impl.DrawBitmap(source, opacity, sourceRect, destRect, bitmapInterpolationMode); - public override void Custom(ICustomDrawOperation custom) => - custom.Render(_impl); + public override void Custom(ICustomDrawOperation custom) + { + using var immediateDrawingContext = new ImmediateDrawingContext(_impl, false); + try + { + custom.Render(immediateDrawingContext); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, LogArea.Visual) + ?.Log(custom, $"Exception in {custom.GetType().Name}.{nameof(ICustomDrawOperation.Render)} {{0}}", e); + } + } public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun) { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs index 8f5ccb4e51..7ce9e6a8af 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Logging; using Avalonia.Media; using Avalonia.Platform; @@ -17,7 +18,16 @@ namespace Avalonia.Rendering.SceneGraph public override void Render(IDrawingContextImpl context) { - Custom.Render(context); + using var immediateDrawingContext = new ImmediateDrawingContext(context, false); + try + { + Custom.Render(immediateDrawingContext); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, LogArea.Visual) + ?.Log(Custom, $"Exception in {Custom.GetType().Name}.{nameof(ICustomDrawOperation.Render)} {{0}}", e); + } } public override void Dispose() => Custom.Dispose(); @@ -48,6 +58,6 @@ namespace Avalonia.Rendering.SceneGraph /// Renders the node to a drawing context. /// /// The drawing context. - void Render(IDrawingContextImpl context); + void Render(ImmediateDrawingContext context); } } diff --git a/src/Browser/Avalonia.Browser/WebEmbeddableControlRoot.cs b/src/Browser/Avalonia.Browser/WebEmbeddableControlRoot.cs index df1a24fa0f..993414f17f 100644 --- a/src/Browser/Avalonia.Browser/WebEmbeddableControlRoot.cs +++ b/src/Browser/Avalonia.Browser/WebEmbeddableControlRoot.cs @@ -37,7 +37,7 @@ namespace Avalonia.Browser return false; } - public void Render(IDrawingContextImpl context) + public void Render(ImmediateDrawingContext context) { _hasRendered = true; _onFirstRender(); From cc0e2302e7a58dd5453dfa2e315179b506abbf37 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 29 Apr 2023 22:07:12 -0400 Subject: [PATCH 092/110] Remove ICustomDrawOperation from the IDrawingContextImpl as it seems useless there now --- src/Avalonia.Base/Media/ImmediateDrawingContext.cs | 1 - src/Avalonia.Base/Platform/IDrawingContextImpl.cs | 7 ------- .../Rendering/Composition/Server/DrawingContextProxy.cs | 5 ----- .../Avalonia.Headless/HeadlessPlatformRenderInterface.cs | 7 +------ src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 9 --------- .../Avalonia.Direct2D1/Media/DrawingContextImpl.cs | 5 +---- 6 files changed, 2 insertions(+), 32 deletions(-) diff --git a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs index 58b153482d..17c4560523 100644 --- a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs +++ b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using Avalonia.Platform; -using Avalonia.Rendering.SceneGraph; using Avalonia.Threading; using Avalonia.Utilities; using Avalonia.Media.Imaging; diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index 1359ad6603..ef53024508 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs @@ -1,6 +1,5 @@ using System; using Avalonia.Media; -using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; using Avalonia.Media.Imaging; using Avalonia.Metadata; @@ -168,12 +167,6 @@ namespace Avalonia.Platform /// void PopBitmapBlendMode(); - /// - /// Adds a custom draw operation - /// - /// Custom draw operation - void Custom(ICustomDrawOperation custom); - /// /// Attempts to get an optional feature from the drawing context implementation /// diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 1ec1362a4c..901bdaae0d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -143,11 +143,6 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, _impl.PopBitmapBlendMode(); } - public void Custom(ICustomDrawOperation custom) - { - _impl.Custom(custom); - } - public object? GetFeature(Type t) => _impl.GetFeature(t); diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index b23697fd2a..38375045cb 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -433,12 +433,7 @@ namespace Avalonia.Headless { } - - public void Custom(ICustomDrawOperation custom) - { - - } - + public object? GetFeature(Type t) { return null; diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index f48d45f961..cea55f4d29 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -5,12 +5,9 @@ using System.Linq; using System.Threading; using Avalonia.Media; using Avalonia.Platform; -using Avalonia.Rendering; -using Avalonia.Rendering.SceneGraph; using Avalonia.Rendering.Utilities; using Avalonia.Utilities; using Avalonia.Media.Imaging; -using Avalonia.Skia.Helpers; using SkiaSharp; using ISceneBrush = Avalonia.Media.ISceneBrush; @@ -667,12 +664,6 @@ namespace Avalonia.Skia _currentBlendingMode = _blendingModeStack.Pop(); } - public void Custom(ICustomDrawOperation custom) - { - CheckLease(); - custom.Render(this); - } - /// public void PushOpacityMask(IBrush mask, Rect bounds) { diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 318b0fe9ae..be5cef35b5 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.Numerics; using Avalonia.Media; using Avalonia.Platform; -using Avalonia.Rendering; -using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; using Avalonia.Media.Imaging; using SharpDX; @@ -608,8 +606,7 @@ namespace Avalonia.Direct2D1.Media { PopLayer(); } - - public void Custom(ICustomDrawOperation custom) => custom.Render(this); + public object GetFeature(Type t) => null; } } From 810558140f9ca3541b7709f2791add0b6db817e1 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 29 Apr 2023 23:25:05 -0400 Subject: [PATCH 093/110] Introduce static AssetLoader --- src/Avalonia.Base/Platform/AssetLoader.cs | 295 ++---------------- .../Platform/StandardAssetLoader.cs | 256 +++++++++++++++ .../StandardRuntimePlatformServices.cs | 2 +- .../AssetLoaderTests.cs | 23 +- .../Styling/ResourceBenchmarks.cs | 2 +- .../Xaml/StyleIncludeTests.cs | 2 +- tests/Avalonia.RenderTests/TestBase.cs | 2 +- tests/Avalonia.UnitTests/TestServices.cs | 8 +- 8 files changed, 304 insertions(+), 286 deletions(-) create mode 100644 src/Avalonia.Base/Platform/StandardAssetLoader.cs diff --git a/src/Avalonia.Base/Platform/AssetLoader.cs b/src/Avalonia.Base/Platform/AssetLoader.cs index 7df446e854..854610f1c9 100644 --- a/src/Avalonia.Base/Platform/AssetLoader.cs +++ b/src/Avalonia.Base/Platform/AssetLoader.cs @@ -1,281 +1,48 @@ -using System; +using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Linq; using System.Reflection; -#if !BUILDTASK -using Avalonia.Platform.Internal; -using Avalonia.Utilities; -#endif -namespace Avalonia.Platform -{ - /// - /// Loads assets compiled into the application binary. - /// - public class AssetLoader +namespace Avalonia.Platform; + #if !BUILDTASK - : IAssetLoader +/// #endif - { +public static class AssetLoader +{ #if !BUILDTASK - private static IAssemblyDescriptorResolver s_assemblyDescriptorResolver = new AssemblyDescriptorResolver(); - - private AssemblyDescriptor? _defaultResmAssembly; - - /// - /// Introduced for tests. - /// - internal static void SetAssemblyDescriptorResolver(IAssemblyDescriptorResolver resolver) => - s_assemblyDescriptorResolver = resolver; - - /// - /// Initializes a new instance of the class. - /// - /// - /// The default assembly from which to load resm: assets for which no assembly is specified. - /// - public AssetLoader(Assembly? assembly = null) - { - if (assembly == null) - assembly = Assembly.GetEntryAssembly(); - if (assembly != null) - _defaultResmAssembly = new AssemblyDescriptor(assembly); - } - - /// - /// Sets the default assembly from which to load assets for which no assembly is specified. - /// - /// The default assembly. - public void SetDefaultAssembly(Assembly assembly) - { - _defaultResmAssembly = new AssemblyDescriptor(assembly); - } - - /// - /// Checks if an asset with the specified URI exists. - /// - /// The URI. - /// - /// A base URI to use if is relative. - /// - /// True if the asset could be found; otherwise false. - public bool Exists(Uri uri, Uri? baseUri = null) - { - return TryGetAsset(uri, baseUri, out _); - } - - /// - /// Opens the asset with the requested URI. - /// - /// The URI. - /// - /// A base URI to use if is relative. - /// - /// A stream containing the asset contents. - /// - /// The asset could not be found. - /// - public Stream Open(Uri uri, Uri? baseUri = null) => OpenAndGetAssembly(uri, baseUri).Item1; - - /// - /// Opens the asset with the requested URI and returns the asset stream and the - /// assembly containing the asset. - /// - /// The URI. - /// - /// A base URI to use if is relative. - /// - /// - /// The stream containing the resource contents together with the assembly. - /// - /// - /// The asset could not be found. - /// - public (Stream stream, Assembly assembly) OpenAndGetAssembly(Uri uri, Uri? baseUri = null) - { - if (TryGetAsset(uri, baseUri, out var assetDescriptor)) - { - return (assetDescriptor.GetStream(), assetDescriptor.Assembly); - } - - throw new FileNotFoundException($"The resource {uri} could not be found."); - } - - public Assembly? GetAssembly(Uri uri, Uri? baseUri) - { - if (!uri.IsAbsoluteUri && baseUri != null) - { - uri = new Uri(baseUri, uri); - } - - if (TryGetAssembly(uri, out var assemblyDescriptor)) - { - return assemblyDescriptor.Assembly; - } - - return null; - } - - /// - /// Gets all assets of a folder and subfolders that match specified uri. - /// - /// The URI. - /// Base URI that is used if is relative. - /// All matching assets as a tuple of the absolute path to the asset and the assembly containing the asset - public IEnumerable GetAssets(Uri uri, Uri? baseUri) - { - if (uri.IsAbsoluteResm()) - { - if (!TryGetAssembly(uri, out var assembly)) - { - assembly = _defaultResmAssembly; - } - - return assembly?.Resources? - .Where(x => x.Key.Contains(uri.GetUnescapeAbsolutePath())) - .Select(x => new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ?? - Enumerable.Empty(); - } - - uri = uri.EnsureAbsolute(baseUri); - - if (uri.IsAvares()) - { - if (!TryGetResAsmAndPath(uri, out var assembly, out var path)) - { - return Enumerable.Empty(); - } + private static IAssetLoader GetAssetLoader() => AvaloniaLocator.Current.GetRequiredService(); - if (assembly?.AvaloniaResources == null) - { - return Enumerable.Empty(); - } + /// + public static void SetDefaultAssembly(Assembly assembly) => GetAssetLoader().SetDefaultAssembly(assembly); - if (path.Length > 0 && path[path.Length - 1] != '/') - { - path += '/'; - } + /// + public static bool Exists(Uri uri, Uri? baseUri = null) => GetAssetLoader().Exists(uri, baseUri); - return assembly.AvaloniaResources - .Where(r => r.Key.StartsWith(path, StringComparison.Ordinal)) - .Select(x => new Uri($"avares://{assembly.Name}{x.Key}")); - } + /// + public static Stream Open(Uri uri, Uri? baseUri = null) => GetAssetLoader().Open(uri, baseUri); - return Enumerable.Empty(); - } + /// + public static (Stream stream, Assembly assembly) OpenAndGetAssembly(Uri uri, Uri? baseUri = null) + => GetAssetLoader().OpenAndGetAssembly(uri, baseUri); - private bool TryGetAsset(Uri uri, Uri? baseUri, [NotNullWhen(true)] out IAssetDescriptor? assetDescriptor) - { - assetDescriptor = null; + /// + public static Assembly? GetAssembly(Uri uri, Uri? baseUri = null) + => GetAssetLoader().GetAssembly(uri, baseUri); - if (uri.IsAbsoluteResm()) - { - if (!TryGetAssembly(uri, out var assembly) && !TryGetAssembly(baseUri, out assembly)) - { - assembly = _defaultResmAssembly; - } - - if (assembly?.Resources != null) - { - var resourceKey = uri.AbsolutePath; - - if (assembly.Resources.TryGetValue(resourceKey, out assetDescriptor)) - { - return true; - } - } - } - - uri = uri.EnsureAbsolute(baseUri); - - if (uri.IsAvares()) - { - if (TryGetResAsmAndPath(uri, out var assembly, out var path)) - { - if (assembly.AvaloniaResources == null) - { - return false; - } - - if (assembly.AvaloniaResources.TryGetValue(path, out assetDescriptor)) - { - return true; - } - } - } - - return false; - } - - private static bool TryGetResAsmAndPath(Uri uri, [NotNullWhen(true)] out IAssemblyDescriptor? assembly, out string path) - { - path = uri.GetUnescapeAbsolutePath(); - - if (TryLoadAssembly(uri.Authority, out assembly)) - { - return true; - } - - return false; - } - - private static bool TryGetAssembly(Uri? uri, [NotNullWhen(true)] out IAssemblyDescriptor? assembly) - { - assembly = null; - - if (uri != null) - { - if (!uri.IsAbsoluteUri) - { - return false; - } - - if (uri.IsAvares() && TryGetResAsmAndPath(uri, out assembly, out _)) - { - return true; - } - - if (uri.IsResm()) - { - var assemblyName = uri.GetAssemblyNameFromQuery(); - - if (assemblyName.Length > 0 && TryLoadAssembly(assemblyName, out assembly)) - { - return true; - } - } - } - - return false; - } - - private static bool TryLoadAssembly(string assemblyName, [NotNullWhen(true)] out IAssemblyDescriptor? assembly) - { - assembly = null; - - try - { - assembly = s_assemblyDescriptorResolver.GetAssembly(assemblyName); - - return true; - } - catch (Exception) { } - - return false; - } + /// + public static IEnumerable GetAssets(Uri uri, Uri? baseUri) + => GetAssetLoader().GetAssets(uri, baseUri); #endif - public static void RegisterResUriParsers() - { - if (!UriParser.IsKnownScheme("avares")) - UriParser.Register(new GenericUriParser( - GenericUriParserOptions.GenericAuthority | - GenericUriParserOptions.NoUserInfo | - GenericUriParserOptions.NoPort | - GenericUriParserOptions.NoQuery | - GenericUriParserOptions.NoFragment), "avares", -1); - } + internal static void RegisterResUriParsers() + { + if (!UriParser.IsKnownScheme("avares")) + UriParser.Register(new GenericUriParser( + GenericUriParserOptions.GenericAuthority | + GenericUriParserOptions.NoUserInfo | + GenericUriParserOptions.NoPort | + GenericUriParserOptions.NoQuery | + GenericUriParserOptions.NoFragment), "avares", -1); } } diff --git a/src/Avalonia.Base/Platform/StandardAssetLoader.cs b/src/Avalonia.Base/Platform/StandardAssetLoader.cs new file mode 100644 index 0000000000..387f77f59b --- /dev/null +++ b/src/Avalonia.Base/Platform/StandardAssetLoader.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Reflection; +using Avalonia.Platform.Internal; +using Avalonia.Utilities; + +namespace Avalonia.Platform +{ + /// + /// Loads assets compiled into the application binary. + /// + internal class StandardAssetLoader : IAssetLoader + { + private readonly IAssemblyDescriptorResolver _assemblyDescriptorResolver; + private AssemblyDescriptor? _defaultResmAssembly; + + public StandardAssetLoader(IAssemblyDescriptorResolver resolver, Assembly? assembly = null) + { + if (assembly == null) + assembly = Assembly.GetEntryAssembly(); + if (assembly != null) + _defaultResmAssembly = new AssemblyDescriptor(assembly); + _assemblyDescriptorResolver = resolver; + } + + public StandardAssetLoader(Assembly? assembly = null) : this(new AssemblyDescriptorResolver(), assembly) + { + + } + + /// + /// Sets the default assembly from which to load assets for which no assembly is specified. + /// + /// The default assembly. + public void SetDefaultAssembly(Assembly assembly) + { + _defaultResmAssembly = new AssemblyDescriptor(assembly); + } + + /// + /// Checks if an asset with the specified URI exists. + /// + /// The URI. + /// + /// A base URI to use if is relative. + /// + /// True if the asset could be found; otherwise false. + public bool Exists(Uri uri, Uri? baseUri = null) + { + return TryGetAsset(uri, baseUri, out _); + } + + /// + /// Opens the asset with the requested URI. + /// + /// The URI. + /// + /// A base URI to use if is relative. + /// + /// A stream containing the asset contents. + /// + /// The asset could not be found. + /// + public Stream Open(Uri uri, Uri? baseUri = null) => OpenAndGetAssembly(uri, baseUri).Item1; + + /// + /// Opens the asset with the requested URI and returns the asset stream and the + /// assembly containing the asset. + /// + /// The URI. + /// + /// A base URI to use if is relative. + /// + /// + /// The stream containing the resource contents together with the assembly. + /// + /// + /// The asset could not be found. + /// + public (Stream stream, Assembly assembly) OpenAndGetAssembly(Uri uri, Uri? baseUri = null) + { + if (TryGetAsset(uri, baseUri, out var assetDescriptor)) + { + return (assetDescriptor.GetStream(), assetDescriptor.Assembly); + } + + throw new FileNotFoundException($"The resource {uri} could not be found."); + } + + public Assembly? GetAssembly(Uri uri, Uri? baseUri) + { + if (!uri.IsAbsoluteUri && baseUri != null) + { + uri = new Uri(baseUri, uri); + } + + if (TryGetAssembly(uri, out var assemblyDescriptor)) + { + return assemblyDescriptor.Assembly; + } + + return null; + } + + /// + /// Gets all assets of a folder and subfolders that match specified uri. + /// + /// The URI. + /// Base URI that is used if is relative. + /// All matching assets as a tuple of the absolute path to the asset and the assembly containing the asset + public IEnumerable GetAssets(Uri uri, Uri? baseUri) + { + if (uri.IsAbsoluteResm()) + { + if (!TryGetAssembly(uri, out var assembly)) + { + assembly = _defaultResmAssembly; + } + + return assembly?.Resources? + .Where(x => x.Key.Contains(uri.GetUnescapeAbsolutePath())) + .Select(x => new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ?? + Enumerable.Empty(); + } + + uri = uri.EnsureAbsolute(baseUri); + + if (uri.IsAvares()) + { + if (!TryGetResAsmAndPath(uri, out var assembly, out var path)) + { + return Enumerable.Empty(); + } + + if (assembly?.AvaloniaResources == null) + { + return Enumerable.Empty(); + } + + if (path.Length > 0 && path[path.Length - 1] != '/') + { + path += '/'; + } + + return assembly.AvaloniaResources + .Where(r => r.Key.StartsWith(path, StringComparison.Ordinal)) + .Select(x => new Uri($"avares://{assembly.Name}{x.Key}")); + } + + return Enumerable.Empty(); + } + + private bool TryGetAsset(Uri uri, Uri? baseUri, [NotNullWhen(true)] out IAssetDescriptor? assetDescriptor) + { + assetDescriptor = null; + + if (uri.IsAbsoluteResm()) + { + if (!TryGetAssembly(uri, out var assembly) && !TryGetAssembly(baseUri, out assembly)) + { + assembly = _defaultResmAssembly; + } + + if (assembly?.Resources != null) + { + var resourceKey = uri.AbsolutePath; + + if (assembly.Resources.TryGetValue(resourceKey, out assetDescriptor)) + { + return true; + } + } + } + + uri = uri.EnsureAbsolute(baseUri); + + if (uri.IsAvares()) + { + if (TryGetResAsmAndPath(uri, out var assembly, out var path)) + { + if (assembly.AvaloniaResources == null) + { + return false; + } + + if (assembly.AvaloniaResources.TryGetValue(path, out assetDescriptor)) + { + return true; + } + } + } + + return false; + } + + private bool TryGetResAsmAndPath(Uri uri, [NotNullWhen(true)] out IAssemblyDescriptor? assembly, out string path) + { + path = uri.GetUnescapeAbsolutePath(); + + if (TryLoadAssembly(uri.Authority, out assembly)) + { + return true; + } + + return false; + } + + private bool TryGetAssembly(Uri? uri, [NotNullWhen(true)] out IAssemblyDescriptor? assembly) + { + assembly = null; + + if (uri != null) + { + if (!uri.IsAbsoluteUri) + { + return false; + } + + if (uri.IsAvares() && TryGetResAsmAndPath(uri, out assembly, out _)) + { + return true; + } + + if (uri.IsResm()) + { + var assemblyName = uri.GetAssemblyNameFromQuery(); + + if (assemblyName.Length > 0 && TryLoadAssembly(assemblyName, out assembly)) + { + return true; + } + } + } + + return false; + } + + private bool TryLoadAssembly(string assemblyName, [NotNullWhen(true)] out IAssemblyDescriptor? assembly) + { + assembly = null; + + try + { + assembly = _assemblyDescriptorResolver.GetAssembly(assemblyName); + + return true; + } + catch (Exception) { } + + return false; + } + } +} diff --git a/src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs b/src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs index 0a36b4c9dd..800d9b390f 100644 --- a/src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs +++ b/src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs @@ -14,7 +14,7 @@ namespace Avalonia.Platform AssetLoader.RegisterResUriParsers(); AvaloniaLocator.CurrentMutable .Bind().ToConstant(standardPlatform) - .Bind().ToConstant(new AssetLoader(assembly)) + .Bind().ToConstant(new StandardAssetLoader(assembly)) .Bind().ToConstant( #if NET6_0_OR_GREATER new Net6Loader() diff --git a/tests/Avalonia.Base.UnitTests/AssetLoaderTests.cs b/tests/Avalonia.Base.UnitTests/AssetLoaderTests.cs index 894b6578e3..d840ea171e 100644 --- a/tests/Avalonia.Base.UnitTests/AssetLoaderTests.cs +++ b/tests/Avalonia.Base.UnitTests/AssetLoaderTests.cs @@ -7,8 +7,10 @@ using Xunit; namespace Avalonia.Base.UnitTests; -public class AssetLoaderTests : IDisposable +public class AssetLoaderTests { + private IAssemblyDescriptorResolver _resolver; + public class MockAssembly : Assembly { } private const string AssemblyNameWithWhitespace = "Awesome Library"; @@ -17,22 +19,20 @@ public class AssetLoaderTests : IDisposable public AssetLoaderTests() { - var resolver = Mock.Of(); + _resolver = Mock.Of(); var descriptor = CreateAssemblyDescriptor(AssemblyNameWithWhitespace); - Mock.Get(resolver).Setup(x => x.GetAssembly(AssemblyNameWithWhitespace)).Returns(descriptor); + Mock.Get(_resolver).Setup(x => x.GetAssembly(AssemblyNameWithWhitespace)).Returns(descriptor); descriptor = CreateAssemblyDescriptor(AssemblyNameWithNonAscii); - Mock.Get(resolver).Setup(x => x.GetAssembly(AssemblyNameWithNonAscii)).Returns(descriptor); - - AssetLoader.SetAssemblyDescriptorResolver(resolver); + Mock.Get(_resolver).Setup(x => x.GetAssembly(AssemblyNameWithNonAscii)).Returns(descriptor); } [Fact] public void AssemblyName_With_Whitespace_Should_Load_Resm() { var uri = new Uri($"resm:Avalonia.Base.UnitTests.Assets.something?assembly={AssemblyNameWithWhitespace}"); - var loader = new AssetLoader(); + var loader = new StandardAssetLoader(_resolver); var assemblyActual = loader.GetAssembly(uri, null); @@ -43,7 +43,7 @@ public class AssetLoaderTests : IDisposable public void AssemblyName_With_Non_ASCII_Should_Load_Avares() { var uri = new Uri($"avares://{AssemblyNameWithNonAscii}/Assets/something"); - var loader = new AssetLoader(); + var loader = new StandardAssetLoader(_resolver); var assemblyActual = loader.GetAssembly(uri, null); @@ -54,7 +54,7 @@ public class AssetLoaderTests : IDisposable public void Invalid_AssemblyName_Should_Yield_Empty_Enumerable() { var uri = new Uri($"avares://InvalidAssembly"); - var loader = new AssetLoader(); + var loader = new StandardAssetLoader(_resolver); var assemblyActual = loader.GetAssets(uri, null); @@ -71,9 +71,4 @@ public class AssetLoaderTests : IDisposable Mock.Get(descriptor).Setup(x => x.Assembly).Returns(assembly); return descriptor; } - - public void Dispose() - { - AssetLoader.SetAssemblyDescriptorResolver(new AssemblyDescriptorResolver()); - } } diff --git a/tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs b/tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs index b044bcde59..a32f98e462 100644 --- a/tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs +++ b/tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs @@ -17,7 +17,7 @@ namespace Avalonia.Benchmarks.Styling private static IDisposable CreateApp() { var services = new TestServices( - assetLoader: new AssetLoader(), + assetLoader: new StandardAssetLoader(), globalClock: new MockGlobalClock(), platform: new AppBuilder().RuntimePlatform, renderInterface: new MockPlatformRenderInterface(), diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs index 5d6d4a78e4..67e808221e 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs @@ -282,7 +282,7 @@ public class StyleIncludeTests public void StyleInclude_From_CodeBehind_Resolves_Compiled() { using var locatorScope = AvaloniaLocator.EnterScope(); - AvaloniaLocator.CurrentMutable.BindToSelf(new AssetLoader(GetType().Assembly)); + AvaloniaLocator.CurrentMutable.BindToSelf(new StandardAssetLoader(GetType().Assembly)); var sp = new TestServiceProvider(); var styleInclude = new StyleInclude(sp) diff --git a/tests/Avalonia.RenderTests/TestBase.cs b/tests/Avalonia.RenderTests/TestBase.cs index 4732099d60..a925b4e60c 100644 --- a/tests/Avalonia.RenderTests/TestBase.cs +++ b/tests/Avalonia.RenderTests/TestBase.cs @@ -45,7 +45,7 @@ namespace Avalonia.Direct2D1.RenderTests private static readonly TestDispatcherImpl threadingInterface = new TestDispatcherImpl(); - private static readonly IAssetLoader assetLoader = new AssetLoader(); + private static readonly IAssetLoader assetLoader = new StandardAssetLoader(); static TestBase() { diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index 800abbc2c7..9b95e71d8c 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -20,7 +20,7 @@ namespace Avalonia.UnitTests public class TestServices { public static readonly TestServices StyledWindow = new TestServices( - assetLoader: new AssetLoader(), + assetLoader: new StandardAssetLoader(), platform: new StandardRuntimePlatform(), renderInterface: new MockPlatformRenderInterface(), standardCursorFactory: Mock.Of(), @@ -31,7 +31,7 @@ namespace Avalonia.UnitTests windowingPlatform: new MockWindowingPlatform()); public static readonly TestServices MockPlatformRenderInterface = new TestServices( - assetLoader: new AssetLoader(), + assetLoader: new StandardAssetLoader(), renderInterface: new MockPlatformRenderInterface(), fontManagerImpl: new MockFontManagerImpl(), textShaperImpl: new MockTextShaperImpl()); @@ -50,13 +50,13 @@ namespace Avalonia.UnitTests keyboardDevice: () => new KeyboardDevice(), keyboardNavigation: new KeyboardNavigationHandler(), inputManager: new InputManager(), - assetLoader: new AssetLoader(), + assetLoader: new StandardAssetLoader(), renderInterface: new MockPlatformRenderInterface(), fontManagerImpl: new MockFontManagerImpl(), textShaperImpl: new MockTextShaperImpl()); public static readonly TestServices TextServices = new TestServices( - assetLoader: new AssetLoader(), + assetLoader: new StandardAssetLoader(), renderInterface: new MockPlatformRenderInterface(), fontManagerImpl: new HarfBuzzFontManagerImpl(), textShaperImpl: new HarfBuzzTextShaperImpl()); From ae3931faa70b9b408cfe00906da01fe3449dcc86 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 29 Apr 2023 23:29:37 -0400 Subject: [PATCH 094/110] Since git decided to mark StandardAssetLoader file as a new, why not fix formatting in here anyway --- .../Platform/StandardAssetLoader.cs | 345 +++++++++--------- 1 file changed, 172 insertions(+), 173 deletions(-) diff --git a/src/Avalonia.Base/Platform/StandardAssetLoader.cs b/src/Avalonia.Base/Platform/StandardAssetLoader.cs index 387f77f59b..118e57c7af 100644 --- a/src/Avalonia.Base/Platform/StandardAssetLoader.cs +++ b/src/Avalonia.Base/Platform/StandardAssetLoader.cs @@ -7,250 +7,249 @@ using System.Reflection; using Avalonia.Platform.Internal; using Avalonia.Utilities; -namespace Avalonia.Platform +namespace Avalonia.Platform; + +/// +/// Loads assets compiled into the application binary. +/// +internal class StandardAssetLoader : IAssetLoader { + private readonly IAssemblyDescriptorResolver _assemblyDescriptorResolver; + private AssemblyDescriptor? _defaultResmAssembly; + + public StandardAssetLoader(IAssemblyDescriptorResolver resolver, Assembly? assembly = null) + { + if (assembly == null) + assembly = Assembly.GetEntryAssembly(); + if (assembly != null) + _defaultResmAssembly = new AssemblyDescriptor(assembly); + _assemblyDescriptorResolver = resolver; + } + + public StandardAssetLoader(Assembly? assembly = null) : this(new AssemblyDescriptorResolver(), assembly) + { + + } + /// - /// Loads assets compiled into the application binary. + /// Sets the default assembly from which to load assets for which no assembly is specified. /// - internal class StandardAssetLoader : IAssetLoader + /// The default assembly. + public void SetDefaultAssembly(Assembly assembly) { - private readonly IAssemblyDescriptorResolver _assemblyDescriptorResolver; - private AssemblyDescriptor? _defaultResmAssembly; + _defaultResmAssembly = new AssemblyDescriptor(assembly); + } - public StandardAssetLoader(IAssemblyDescriptorResolver resolver, Assembly? assembly = null) - { - if (assembly == null) - assembly = Assembly.GetEntryAssembly(); - if (assembly != null) - _defaultResmAssembly = new AssemblyDescriptor(assembly); - _assemblyDescriptorResolver = resolver; - } + /// + /// Checks if an asset with the specified URI exists. + /// + /// The URI. + /// + /// A base URI to use if is relative. + /// + /// True if the asset could be found; otherwise false. + public bool Exists(Uri uri, Uri? baseUri = null) + { + return TryGetAsset(uri, baseUri, out _); + } - public StandardAssetLoader(Assembly? assembly = null) : this(new AssemblyDescriptorResolver(), assembly) + /// + /// Opens the asset with the requested URI. + /// + /// The URI. + /// + /// A base URI to use if is relative. + /// + /// A stream containing the asset contents. + /// + /// The asset could not be found. + /// + public Stream Open(Uri uri, Uri? baseUri = null) => OpenAndGetAssembly(uri, baseUri).Item1; + + /// + /// Opens the asset with the requested URI and returns the asset stream and the + /// assembly containing the asset. + /// + /// The URI. + /// + /// A base URI to use if is relative. + /// + /// + /// The stream containing the resource contents together with the assembly. + /// + /// + /// The asset could not be found. + /// + public (Stream stream, Assembly assembly) OpenAndGetAssembly(Uri uri, Uri? baseUri = null) + { + if (TryGetAsset(uri, baseUri, out var assetDescriptor)) { - + return (assetDescriptor.GetStream(), assetDescriptor.Assembly); } - /// - /// Sets the default assembly from which to load assets for which no assembly is specified. - /// - /// The default assembly. - public void SetDefaultAssembly(Assembly assembly) + throw new FileNotFoundException($"The resource {uri} could not be found."); + } + + public Assembly? GetAssembly(Uri uri, Uri? baseUri) + { + if (!uri.IsAbsoluteUri && baseUri != null) { - _defaultResmAssembly = new AssemblyDescriptor(assembly); + uri = new Uri(baseUri, uri); } - /// - /// Checks if an asset with the specified URI exists. - /// - /// The URI. - /// - /// A base URI to use if is relative. - /// - /// True if the asset could be found; otherwise false. - public bool Exists(Uri uri, Uri? baseUri = null) + if (TryGetAssembly(uri, out var assemblyDescriptor)) { - return TryGetAsset(uri, baseUri, out _); + return assemblyDescriptor.Assembly; } - /// - /// Opens the asset with the requested URI. - /// - /// The URI. - /// - /// A base URI to use if is relative. - /// - /// A stream containing the asset contents. - /// - /// The asset could not be found. - /// - public Stream Open(Uri uri, Uri? baseUri = null) => OpenAndGetAssembly(uri, baseUri).Item1; - - /// - /// Opens the asset with the requested URI and returns the asset stream and the - /// assembly containing the asset. - /// - /// The URI. - /// - /// A base URI to use if is relative. - /// - /// - /// The stream containing the resource contents together with the assembly. - /// - /// - /// The asset could not be found. - /// - public (Stream stream, Assembly assembly) OpenAndGetAssembly(Uri uri, Uri? baseUri = null) + return null; + } + + /// + /// Gets all assets of a folder and subfolders that match specified uri. + /// + /// The URI. + /// Base URI that is used if is relative. + /// All matching assets as a tuple of the absolute path to the asset and the assembly containing the asset + public IEnumerable GetAssets(Uri uri, Uri? baseUri) + { + if (uri.IsAbsoluteResm()) { - if (TryGetAsset(uri, baseUri, out var assetDescriptor)) + if (!TryGetAssembly(uri, out var assembly)) { - return (assetDescriptor.GetStream(), assetDescriptor.Assembly); + assembly = _defaultResmAssembly; } - throw new FileNotFoundException($"The resource {uri} could not be found."); + return assembly?.Resources? + .Where(x => x.Key.Contains(uri.GetUnescapeAbsolutePath())) + .Select(x => new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ?? + Enumerable.Empty(); } - public Assembly? GetAssembly(Uri uri, Uri? baseUri) + uri = uri.EnsureAbsolute(baseUri); + + if (uri.IsAvares()) { - if (!uri.IsAbsoluteUri && baseUri != null) + if (!TryGetResAsmAndPath(uri, out var assembly, out var path)) { - uri = new Uri(baseUri, uri); + return Enumerable.Empty(); } - if (TryGetAssembly(uri, out var assemblyDescriptor)) + if (assembly?.AvaloniaResources == null) { - return assemblyDescriptor.Assembly; + return Enumerable.Empty(); } - return null; + if (path.Length > 0 && path[path.Length - 1] != '/') + { + path += '/'; + } + + return assembly.AvaloniaResources + .Where(r => r.Key.StartsWith(path, StringComparison.Ordinal)) + .Select(x => new Uri($"avares://{assembly.Name}{x.Key}")); } - /// - /// Gets all assets of a folder and subfolders that match specified uri. - /// - /// The URI. - /// Base URI that is used if is relative. - /// All matching assets as a tuple of the absolute path to the asset and the assembly containing the asset - public IEnumerable GetAssets(Uri uri, Uri? baseUri) + return Enumerable.Empty(); + } + + private bool TryGetAsset(Uri uri, Uri? baseUri, [NotNullWhen(true)] out IAssetDescriptor? assetDescriptor) + { + assetDescriptor = null; + + if (uri.IsAbsoluteResm()) { - if (uri.IsAbsoluteResm()) + if (!TryGetAssembly(uri, out var assembly) && !TryGetAssembly(baseUri, out assembly)) { - if (!TryGetAssembly(uri, out var assembly)) - { - assembly = _defaultResmAssembly; - } - - return assembly?.Resources? - .Where(x => x.Key.Contains(uri.GetUnescapeAbsolutePath())) - .Select(x => new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ?? - Enumerable.Empty(); + assembly = _defaultResmAssembly; } - uri = uri.EnsureAbsolute(baseUri); - - if (uri.IsAvares()) + if (assembly?.Resources != null) { - if (!TryGetResAsmAndPath(uri, out var assembly, out var path)) - { - return Enumerable.Empty(); - } - - if (assembly?.AvaloniaResources == null) - { - return Enumerable.Empty(); - } + var resourceKey = uri.AbsolutePath; - if (path.Length > 0 && path[path.Length - 1] != '/') + if (assembly.Resources.TryGetValue(resourceKey, out assetDescriptor)) { - path += '/'; + return true; } - - return assembly.AvaloniaResources - .Where(r => r.Key.StartsWith(path, StringComparison.Ordinal)) - .Select(x => new Uri($"avares://{assembly.Name}{x.Key}")); } - - return Enumerable.Empty(); } - private bool TryGetAsset(Uri uri, Uri? baseUri, [NotNullWhen(true)] out IAssetDescriptor? assetDescriptor) - { - assetDescriptor = null; + uri = uri.EnsureAbsolute(baseUri); - if (uri.IsAbsoluteResm()) + if (uri.IsAvares()) + { + if (TryGetResAsmAndPath(uri, out var assembly, out var path)) { - if (!TryGetAssembly(uri, out var assembly) && !TryGetAssembly(baseUri, out assembly)) + if (assembly.AvaloniaResources == null) { - assembly = _defaultResmAssembly; + return false; } - if (assembly?.Resources != null) + if (assembly.AvaloniaResources.TryGetValue(path, out assetDescriptor)) { - var resourceKey = uri.AbsolutePath; - - if (assembly.Resources.TryGetValue(resourceKey, out assetDescriptor)) - { - return true; - } + return true; } } + } - uri = uri.EnsureAbsolute(baseUri); + return false; + } - if (uri.IsAvares()) - { - if (TryGetResAsmAndPath(uri, out var assembly, out var path)) - { - if (assembly.AvaloniaResources == null) - { - return false; - } - - if (assembly.AvaloniaResources.TryGetValue(path, out assetDescriptor)) - { - return true; - } - } - } + private bool TryGetResAsmAndPath(Uri uri, [NotNullWhen(true)] out IAssemblyDescriptor? assembly, out string path) + { + path = uri.GetUnescapeAbsolutePath(); - return false; + if (TryLoadAssembly(uri.Authority, out assembly)) + { + return true; } - private bool TryGetResAsmAndPath(Uri uri, [NotNullWhen(true)] out IAssemblyDescriptor? assembly, out string path) + return false; + } + + private bool TryGetAssembly(Uri? uri, [NotNullWhen(true)] out IAssemblyDescriptor? assembly) + { + assembly = null; + + if (uri != null) { - path = uri.GetUnescapeAbsolutePath(); + if (!uri.IsAbsoluteUri) + { + return false; + } - if (TryLoadAssembly(uri.Authority, out assembly)) + if (uri.IsAvares() && TryGetResAsmAndPath(uri, out assembly, out _)) { return true; } - return false; - } - - private bool TryGetAssembly(Uri? uri, [NotNullWhen(true)] out IAssemblyDescriptor? assembly) - { - assembly = null; - - if (uri != null) + if (uri.IsResm()) { - if (!uri.IsAbsoluteUri) - { - return false; - } + var assemblyName = uri.GetAssemblyNameFromQuery(); - if (uri.IsAvares() && TryGetResAsmAndPath(uri, out assembly, out _)) + if (assemblyName.Length > 0 && TryLoadAssembly(assemblyName, out assembly)) { return true; } - - if (uri.IsResm()) - { - var assemblyName = uri.GetAssemblyNameFromQuery(); - - if (assemblyName.Length > 0 && TryLoadAssembly(assemblyName, out assembly)) - { - return true; - } - } } - - return false; } - private bool TryLoadAssembly(string assemblyName, [NotNullWhen(true)] out IAssemblyDescriptor? assembly) - { - assembly = null; + return false; + } - try - { - assembly = _assemblyDescriptorResolver.GetAssembly(assemblyName); + private bool TryLoadAssembly(string assemblyName, [NotNullWhen(true)] out IAssemblyDescriptor? assembly) + { + assembly = null; - return true; - } - catch (Exception) { } + try + { + assembly = _assemblyDescriptorResolver.GetAssembly(assemblyName); - return false; + return true; } + catch (Exception) { } + + return false; } } From 8ba233b23facb9cc3784739f543dc996900440d3 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 29 Apr 2023 23:32:18 -0400 Subject: [PATCH 095/110] Fix Screens.ScreenFromWindow API --- src/Avalonia.Controls/Screens.cs | 12 +++++++++++- src/Avalonia.Controls/Window.cs | 6 +++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Screens.cs b/src/Avalonia.Controls/Screens.cs index 22f9c0832a..a9f52315f9 100644 --- a/src/Avalonia.Controls/Screens.cs +++ b/src/Avalonia.Controls/Screens.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Platform; -using Avalonia.VisualTree; #nullable enable @@ -43,6 +42,17 @@ namespace Avalonia.Controls return _iScreenImpl.ScreenFromRect(bounds); } + public Screen? ScreenFromWindow(WindowBase window) + { + if (window.PlatformImpl is null) + { + throw new ObjectDisposedException("Window platform implementation was already disposed."); + } + + return _iScreenImpl.ScreenFromWindow(window.PlatformImpl); + } + + [Obsolete("Use ScreenFromWindow(WindowBase) overload.")] public Screen? ScreenFromWindow(IWindowBaseImpl window) { return _iScreenImpl.ScreenFromWindow(window); diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 48edb81b16..66cce89b9d 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -713,7 +713,7 @@ namespace Avalonia.Controls Owner = owner; owner?.AddChild(this, false); - SetWindowStartupLocation(owner?.PlatformImpl); + SetWindowStartupLocation(owner); PlatformImpl?.Show(ShowActivated, false); Renderer.Start(); @@ -789,7 +789,7 @@ namespace Avalonia.Controls Owner = owner; owner.AddChild(this, true); - SetWindowStartupLocation(owner.PlatformImpl); + SetWindowStartupLocation(owner); PlatformImpl?.Show(ShowActivated, true); @@ -870,7 +870,7 @@ namespace Avalonia.Controls } } - private void SetWindowStartupLocation(IWindowBaseImpl? owner = null) + private void SetWindowStartupLocation(Window? owner = null) { var startupLocation = WindowStartupLocation; From 009000123ff1e35ee0bdbb7c5efb5f7ab0ccf3aa Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 29 Apr 2023 23:41:47 -0400 Subject: [PATCH 096/110] Update Screens docs --- src/Avalonia.Controls/Screens.cs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Screens.cs b/src/Avalonia.Controls/Screens.cs index a9f52315f9..60d5358c49 100644 --- a/src/Avalonia.Controls/Screens.cs +++ b/src/Avalonia.Controls/Screens.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Avalonia.Platform; @@ -37,11 +37,22 @@ namespace Avalonia.Controls _iScreenImpl = iScreenImpl; } + /// + /// Retrieves a Screen for the display that contains the rectangle. + /// + /// Bounds that specifies the area for which to retrieve the display. + /// The . public Screen? ScreenFromBounds(PixelRect bounds) { return _iScreenImpl.ScreenFromRect(bounds); } + /// + /// Retrieves a Screen for the display that contains the specified . + /// + /// The window for which to retrieve the Screen. + /// Window platform implementation was already disposed. + /// The . public Screen? ScreenFromWindow(WindowBase window) { if (window.PlatformImpl is null) @@ -52,17 +63,32 @@ namespace Avalonia.Controls return _iScreenImpl.ScreenFromWindow(window.PlatformImpl); } + /// + /// Retrieves a Screen for the display that contains the specified . + /// + /// The window impl for which to retrieve the Screen. + /// The . [Obsolete("Use ScreenFromWindow(WindowBase) overload.")] public Screen? ScreenFromWindow(IWindowBaseImpl window) { return _iScreenImpl.ScreenFromWindow(window); } + /// + /// Retrieves a Screen for the display that contains the specified point. + /// + /// A Point that specifies the location for which to retrieve a Screen. + /// The . public Screen? ScreenFromPoint(PixelPoint point) { return _iScreenImpl.ScreenFromPoint(point); } + /// + /// Retrieves a Screen for the display that contains the specified . + /// + /// A Visual for which to retrieve a Screen. + /// The . public Screen? ScreenFromVisual(Visual visual) { var tl = visual.PointToScreen(visual.Bounds.TopLeft); From a11bef715b283b99adb535dfa857e039573bce1f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 29 Apr 2023 23:44:21 -0400 Subject: [PATCH 097/110] Add Message parameter to the Unstable attribute --- nukebuild/RefAssemblyGenerator.cs | 24 ++++++++++++------- .../Metadata/UnstableAttribute.cs | 22 ++++++++++++++++- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/nukebuild/RefAssemblyGenerator.cs b/nukebuild/RefAssemblyGenerator.cs index cbe5236bca..f0d5c81a37 100644 --- a/nukebuild/RefAssemblyGenerator.cs +++ b/nukebuild/RefAssemblyGenerator.cs @@ -96,7 +96,7 @@ public class RefAssemblyGenerator | MethodAttributes.HideBySig, type.Module.TypeSystem.Void)); } - var forceUnstable = type.CustomAttributes.Any(a => + var forceUnstable = type.CustomAttributes.FirstOrDefault(a => a.AttributeType.FullName == "Avalonia.Metadata.UnstableAttribute"); foreach (var m in type.Methods) @@ -109,22 +109,28 @@ public class RefAssemblyGenerator } } - static void MarkAsUnstable(IMemberDefinition def, MethodReference obsoleteCtor, bool force) + static void MarkAsUnstable(IMemberDefinition def, MethodReference obsoleteCtor, ICustomAttribute unstableAttribute) { - if (!force && ( - def.HasCustomAttributes == false - || def.CustomAttributes.All(a => a.AttributeType.FullName != "Avalonia.Metadata.UnstableAttribute"))) + if (def.CustomAttributes.Any(a => a.AttributeType.FullName == "System.ObsoleteAttribute")) return; - if (def.CustomAttributes.Any(a => a.AttributeType.FullName == "System.ObsoleteAttribute")) + unstableAttribute = def.CustomAttributes.FirstOrDefault(a => + a.AttributeType.FullName == "Avalonia.Metadata.UnstableAttribute") ?? unstableAttribute; + + if (unstableAttribute is null) return; + var message = unstableAttribute.ConstructorArguments.FirstOrDefault().Value?.ToString(); + if (string.IsNullOrEmpty(message)) + { + message = "This is a part of unstable API and can be changed in minor releases. Consider replacing it with alternatives or reach out developers on GitHub."; + } + def.CustomAttributes.Add(new CustomAttribute(obsoleteCtor) { ConstructorArguments = { - new CustomAttributeArgument(obsoleteCtor.Module.TypeSystem.String, - "This is a part of unstable API and can be changed in minor releases. You have been warned") + new CustomAttributeArgument(obsoleteCtor.Module.TypeSystem.String, message) } }); } @@ -168,4 +174,4 @@ public class RefAssemblyGenerator } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Metadata/UnstableAttribute.cs b/src/Avalonia.Base/Metadata/UnstableAttribute.cs index 361f6d30fd..bbb298f7a6 100644 --- a/src/Avalonia.Base/Metadata/UnstableAttribute.cs +++ b/src/Avalonia.Base/Metadata/UnstableAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Avalonia.Metadata { @@ -9,5 +9,25 @@ namespace Avalonia.Metadata [AttributeUsage(AttributeTargets.All)] public sealed class UnstableAttribute : Attribute { + /// + /// Initializes a new instance of the class. + /// + public UnstableAttribute() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The text string that describes alternative workarounds. + public UnstableAttribute(string? message) + { + Message = message; + } + + /// + /// Gets a value that indicates whether the compiler will treat usage of the obsolete program element as an error. + /// + public string? Message { get; } } } From bbeef11aeddf8283efeb4bbdb7873f5863d29e56 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 29 Apr 2023 23:44:49 -0400 Subject: [PATCH 098/110] Adjust some Unstable attr usages --- src/Avalonia.Base/Controls/IThemeVariantProvider.cs | 3 +-- src/Avalonia.Base/Input/DragEventArgs.cs | 3 +-- src/Avalonia.Base/Input/PointerDeltaEventArgs.cs | 3 +-- src/Avalonia.Base/Input/PointerEventArgs.cs | 12 ++++-------- src/Avalonia.Base/Input/PointerWheelEventArgs.cs | 3 +-- src/Avalonia.Base/Platform/IAssetLoader.cs | 2 +- 6 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Base/Controls/IThemeVariantProvider.cs b/src/Avalonia.Base/Controls/IThemeVariantProvider.cs index d1dca2efbf..03a7fb1206 100644 --- a/src/Avalonia.Base/Controls/IThemeVariantProvider.cs +++ b/src/Avalonia.Base/Controls/IThemeVariantProvider.cs @@ -10,9 +10,8 @@ namespace Avalonia.Controls; /// /// This is a helper interface for the XAML compiler to make Key property accessibly by the markup extensions. /// Which means, it can only be used with ResourceDictionaries and markup extensions in the XAML code. -/// This API might be removed in the future minor updates. /// -[Unstable] +[Unstable("This XAML-only API might be removed in the future minor updates.")] public interface IThemeVariantProvider : IResourceProvider { /// diff --git a/src/Avalonia.Base/Input/DragEventArgs.cs b/src/Avalonia.Base/Input/DragEventArgs.cs index 8d7cc2b9a1..26ec98361b 100644 --- a/src/Avalonia.Base/Input/DragEventArgs.cs +++ b/src/Avalonia.Base/Input/DragEventArgs.cs @@ -25,8 +25,7 @@ namespace Avalonia.Input return _target.TranslatePoint(_targetLocation, relativeTo) ?? new Point(0, 0); } - [Unstable] - [Obsolete("This constructor might be removed in 12.0. For unit testing, consider using DragDrop.DoDragDrop or IHeadlessWindow.DragDrop.")] + [Unstable("This constructor might be removed in 12.0. For unit testing, consider using DragDrop.DoDragDrop or IHeadlessWindow.DragDrop.")] public DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers) : base(routedEvent) { diff --git a/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs b/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs index c405cdfacd..3c4562edf4 100644 --- a/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs @@ -9,8 +9,7 @@ namespace Avalonia.Input { public Vector Delta { get; } - [Unstable] - [Obsolete("This constructor might be removed in 12.0.")] + [Unstable("This constructor might be removed in 12.0.")] public PointerDeltaEventArgs(RoutedEvent routedEvent, object? source, IPointer pointer, Visual rootVisual, Point rootVisualPosition, ulong timestamp, PointerPointProperties properties, KeyModifiers modifiers, Vector delta) diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs index beb953ce8f..7f82199b56 100644 --- a/src/Avalonia.Base/Input/PointerEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerEventArgs.cs @@ -14,8 +14,7 @@ namespace Avalonia.Input private readonly PointerPointProperties _properties; private readonly Lazy?>? _previousPoints; - [Unstable] - [Obsolete("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow mouse methods.")] + [Unstable("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow mouse methods.")] public PointerEventArgs(RoutedEvent routedEvent, object? source, IPointer pointer, @@ -129,8 +128,7 @@ namespace Avalonia.Input public class PointerPressedEventArgs : PointerEventArgs { - [Unstable] - [Obsolete("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow mouse methods.")] + [Unstable("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow mouse methods.")] public PointerPressedEventArgs( object source, IPointer pointer, @@ -150,8 +148,7 @@ namespace Avalonia.Input public class PointerReleasedEventArgs : PointerEventArgs { - [Unstable] - [Obsolete("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow mouse methods.")] + [Unstable("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow mouse methods.")] public PointerReleasedEventArgs( object source, IPointer pointer, Visual rootVisual, Point rootVisualPosition, ulong timestamp, @@ -173,8 +170,7 @@ namespace Avalonia.Input { public IPointer Pointer { get; } - [Unstable] - [Obsolete("This constructor might be removed in 12.0. If you need to remove capture, use stable methods on the IPointer instance.,")] + [Unstable("This constructor might be removed in 12.0. If you need to remove capture, use stable methods on the IPointer instance.,")] public PointerCaptureLostEventArgs(object source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent) { Pointer = pointer; diff --git a/src/Avalonia.Base/Input/PointerWheelEventArgs.cs b/src/Avalonia.Base/Input/PointerWheelEventArgs.cs index 903019d85d..22624a61dd 100644 --- a/src/Avalonia.Base/Input/PointerWheelEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerWheelEventArgs.cs @@ -9,8 +9,7 @@ namespace Avalonia.Input { public Vector Delta { get; } - [Unstable] - [Obsolete("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow.MouseWheel.")] + [Unstable("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow.MouseWheel.")] public PointerWheelEventArgs(object source, IPointer pointer, Visual rootVisual, Point rootVisualPosition, ulong timestamp, PointerPointProperties properties, KeyModifiers modifiers, Vector delta) diff --git a/src/Avalonia.Base/Platform/IAssetLoader.cs b/src/Avalonia.Base/Platform/IAssetLoader.cs index b65d61803f..f1ce624c70 100644 --- a/src/Avalonia.Base/Platform/IAssetLoader.cs +++ b/src/Avalonia.Base/Platform/IAssetLoader.cs @@ -9,7 +9,7 @@ namespace Avalonia.Platform /// /// Loads assets compiled into the application binary. /// - [Unstable] + [Unstable("IAssetLoader interface and AvaloniaLocator usage is considered unstable. Please use AssetLoader static class instead.")] public interface IAssetLoader { /// From 04ff14f0cde3b93d29a4239dc1ca5bf9d36ccb26 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 29 Apr 2023 23:58:59 -0400 Subject: [PATCH 099/110] Return WriteableBitmap from the GetLastRenderedFrame API --- src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs | 4 ++-- src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs | 4 ++-- src/Headless/Avalonia.Headless/IHeadlessWindow.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs index 8fbc5ec6ef..7d4b7f5477 100644 --- a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs @@ -17,7 +17,7 @@ public static class HeadlessWindowExtensions /// Triggers a renderer timer tick and captures last rendered frame. /// /// Bitmap with last rendered frame. Null, if nothing was rendered. - public static Bitmap? CaptureRenderedFrame(this TopLevel topLevel) + public static WriteableBitmap? CaptureRenderedFrame(this TopLevel topLevel) { Dispatcher.UIThread.RunJobs(); AvaloniaHeadlessPlatform.ForceRenderTimerTick(); @@ -29,7 +29,7 @@ public static class HeadlessWindowExtensions /// Note, in order to trigger rendering timer, call method. /// /// Bitmap with last rendered frame. Null, if nothing was rendered. - public static Bitmap? GetLastRenderedFrame(this TopLevel topLevel) + public static WriteableBitmap? GetLastRenderedFrame(this TopLevel topLevel) { if (AvaloniaLocator.Current.GetService() is HeadlessPlatformRenderInterface) { diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs index b15c1eb327..93f92d46f8 100644 --- a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs @@ -214,7 +214,7 @@ namespace Avalonia.Headless }); } - public Bitmap? GetLastRenderedFrame() + public WriteableBitmap? GetLastRenderedFrame() { lock (_sync) { @@ -224,7 +224,7 @@ namespace Avalonia.Headless } using var lockedFramebuffer = _lastRenderedFrame.Lock(); - return new Bitmap(lockedFramebuffer.Format, AlphaFormat.Opaque, lockedFramebuffer.Address, + return new WriteableBitmap(lockedFramebuffer.Format, AlphaFormat.Opaque, lockedFramebuffer.Address, lockedFramebuffer.Size, lockedFramebuffer.Dpi, lockedFramebuffer.RowBytes); } } diff --git a/src/Headless/Avalonia.Headless/IHeadlessWindow.cs b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs index f3da2335bc..ef501b3800 100644 --- a/src/Headless/Avalonia.Headless/IHeadlessWindow.cs +++ b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs @@ -8,7 +8,7 @@ namespace Avalonia.Headless { internal interface IHeadlessWindow { - Bitmap? GetLastRenderedFrame(); + WriteableBitmap? GetLastRenderedFrame(); void KeyPress(Key key, RawInputModifiers modifiers); void KeyRelease(Key key, RawInputModifiers modifiers); void MouseDown(Point point, MouseButton button, RawInputModifiers modifiers = RawInputModifiers.None); From 8c31885a3da900ceec88a4b2a891949e9274cdc4 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 30 Apr 2023 11:32:04 -0400 Subject: [PATCH 100/110] Fix several issues with comments in Dispatcher.Invoke.cs --- .../Threading/Dispatcher.Invoke.cs | 47 +++++++++---------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs index 6842e4a255..699186868a 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs @@ -1,5 +1,4 @@ using System; -using System.ComponentModel; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; @@ -118,11 +117,11 @@ public partial class Dispatcher } /// - /// Executes the specified Func synchronously on the + /// Executes the specified Func<TResult> synchronously on the /// thread that the Dispatcher was created on. /// /// - /// A Func delegate to invoke through the dispatcher. + /// A Func<TResult> delegate to invoke through the dispatcher. /// /// /// The return value from the delegate being invoked. @@ -136,11 +135,11 @@ public partial class Dispatcher } /// - /// Executes the specified Func synchronously on the + /// Executes the specified Func<TResult> synchronously on the /// thread that the Dispatcher was created on. /// /// - /// A Func delegate to invoke through the dispatcher. + /// A Func<TResult> delegate to invoke through the dispatcher. /// /// /// The priority that determines in what order the specified @@ -156,11 +155,11 @@ public partial class Dispatcher } /// - /// Executes the specified Func synchronously on the + /// Executes the specified Func<TResult> synchronously on the /// thread that the Dispatcher was created on. /// /// - /// A Func delegate to invoke through the dispatcher. + /// A Func<TResult> delegate to invoke through the dispatcher. /// /// /// The priority that determines in what order the specified @@ -183,11 +182,11 @@ public partial class Dispatcher } /// - /// Executes the specified Func synchronously on the + /// Executes the specified Func<TResult> synchronously on the /// thread that the Dispatcher was created on. /// /// - /// A Func delegate to invoke through the dispatcher. + /// A Func<TResult> delegate to invoke through the dispatcher. /// /// /// The priority that determines in what order the specified @@ -317,11 +316,11 @@ public partial class Dispatcher } /// - /// Executes the specified Func asynchronously on the + /// Executes the specified Func<TResult> asynchronously on the /// thread that the Dispatcher was created on. /// /// - /// A Func delegate to invoke through the dispatcher. + /// A Func<TResult> delegate to invoke through the dispatcher. /// /// /// An operation representing the queued delegate to be invoked. @@ -335,11 +334,11 @@ public partial class Dispatcher } /// - /// Executes the specified Func asynchronously on the + /// Executes the specified Func<TResult> asynchronously on the /// thread that the Dispatcher was created on. /// /// - /// A Func delegate to invoke through the dispatcher. + /// A Func<TResult> delegate to invoke through the dispatcher. /// /// /// The priority that determines in what order the specified @@ -355,11 +354,11 @@ public partial class Dispatcher } /// - /// Executes the specified Func asynchronously on the + /// Executes the specified Func<TResult> asynchronously on the /// thread that the Dispatcher was created on. /// /// - /// A Func delegate to invoke through the dispatcher. + /// A Func<TResult> delegate to invoke through the dispatcher. /// /// /// The priority that determines in what order the specified @@ -479,7 +478,7 @@ public partial class Dispatcher // operation has already started when the timeout expires, // we still wait for it to complete. This is different // than simply waiting on the operation with a timeout - // because we are the ones queueing the dispatcher + // because we are the ones queuing the dispatcher // operation, not the caller. We can't leave the operation // in a state that it might execute if we return that it did not // invoke. @@ -492,12 +491,12 @@ public partial class Dispatcher // Old async semantics return from Wait without // throwing an exception if the operation was aborted. - // There is no need to test the timout condition, since + // There is no need to test the timeout condition, since // the old async semantics would just return the result, // which would be null. // This should not block because either the operation - // is using the old async sematics, or the operation + // is using the old async semantics, or the operation // completed successfully. result = operation.GetResult(); } @@ -543,11 +542,11 @@ public partial class Dispatcher } /// - /// Executes the specified Func asynchronously on the + /// Executes the specified Func<Task> asynchronously on the /// thread that the Dispatcher was created on /// /// - /// A Func delegate to invoke through the dispatcher. + /// A Func<Task> delegate to invoke through the dispatcher. /// /// /// The priority that determines in what order the specified @@ -564,11 +563,11 @@ public partial class Dispatcher } /// - /// Executes the specified Func> asynchronously on the + /// Executes the specified Func<Task<TResult>> asynchronously on the /// thread that the Dispatcher was created on /// - /// - /// A Func> delegate to invoke through the dispatcher. + /// + /// A Func<Task<TResult>> delegate to invoke through the dispatcher. /// /// /// The priority that determines in what order the specified @@ -595,4 +594,4 @@ public partial class Dispatcher _ = action ?? throw new ArgumentNullException(nameof(action)); InvokeAsyncImpl(new SendOrPostCallbackDispatcherOperation(this, priority, action, arg, true), CancellationToken.None); } -} \ No newline at end of file +} From b96f9abd06e866145080e51d74379749e4dc6a97 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 30 Apr 2023 12:05:43 -0400 Subject: [PATCH 101/110] Update ProgressBar TemplateSettings and comments --- src/Avalonia.Controls/ProgressBar.cs | 95 +++++++++++++++---- .../Controls/ProgressBar.xaml | 32 +++---- .../Controls/ProgressBar.xaml | 4 +- 3 files changed, 92 insertions(+), 39 deletions(-) diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index daf6be12d2..7dcdaa2f8d 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -17,7 +17,13 @@ namespace Avalonia.Controls [PseudoClasses(":vertical", ":horizontal", ":indeterminate")] public class ProgressBar : RangeBase { - public class ProgressBarTemplateProperties : AvaloniaObject + /// + /// Provides calculated values for use with the 's control theme or template. + /// + /// + /// This class is NOT intended for general use outside of control templates. + /// + public class ProgressBarTemplateSettings : AvaloniaObject { private double _container2Width; private double _containerWidth; @@ -26,38 +32,38 @@ namespace Avalonia.Controls private double _container2AnimationStartPosition; private double _container2AnimationEndPosition; - public static readonly DirectProperty ContainerAnimationStartPositionProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty ContainerAnimationStartPositionProperty = + AvaloniaProperty.RegisterDirect( nameof(ContainerAnimationStartPosition), p => p.ContainerAnimationStartPosition, (p, o) => p.ContainerAnimationStartPosition = o, 0d); - public static readonly DirectProperty ContainerAnimationEndPositionProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty ContainerAnimationEndPositionProperty = + AvaloniaProperty.RegisterDirect( nameof(ContainerAnimationEndPosition), p => p.ContainerAnimationEndPosition, (p, o) => p.ContainerAnimationEndPosition = o, 0d); - public static readonly DirectProperty Container2AnimationStartPositionProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty Container2AnimationStartPositionProperty = + AvaloniaProperty.RegisterDirect( nameof(Container2AnimationStartPosition), p => p.Container2AnimationStartPosition, (p, o) => p.Container2AnimationStartPosition = o, 0d); - public static readonly DirectProperty Container2AnimationEndPositionProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty Container2AnimationEndPositionProperty = + AvaloniaProperty.RegisterDirect( nameof(Container2AnimationEndPosition), p => p.Container2AnimationEndPosition, (p, o) => p.Container2AnimationEndPosition = o); - public static readonly DirectProperty Container2WidthProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty Container2WidthProperty = + AvaloniaProperty.RegisterDirect( nameof(Container2Width), p => p.Container2Width, (p, o) => p.Container2Width = o); - public static readonly DirectProperty ContainerWidthProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty ContainerWidthProperty = + AvaloniaProperty.RegisterDirect( nameof(ContainerWidth), p => p.ContainerWidth, (p, o) => p.ContainerWidth = o); @@ -103,29 +109,57 @@ namespace Avalonia.Controls private Border? _indicator; private IDisposable? _trackSizeChangedListener; + /// + /// Defines the property. + /// public static readonly StyledProperty IsIndeterminateProperty = AvaloniaProperty.Register(nameof(IsIndeterminate)); + /// + /// Defines the property. + /// public static readonly StyledProperty ShowProgressTextProperty = AvaloniaProperty.Register(nameof(ShowProgressText)); + /// + /// Defines the property. + /// public static readonly StyledProperty ProgressTextFormatProperty = AvaloniaProperty.Register(nameof(ProgressTextFormat), "{1:0}%"); + /// + /// Defines the property. + /// public static readonly StyledProperty OrientationProperty = AvaloniaProperty.Register(nameof(Orientation), Orientation.Horizontal); + /// + /// Defines the property. + /// public static readonly DirectProperty PercentageProperty = AvaloniaProperty.RegisterDirect( nameof(Percentage), o => o.Percentage); + /// + /// Defines the property. + /// public static readonly StyledProperty IndeterminateStartingOffsetProperty = AvaloniaProperty.Register(nameof(IndeterminateStartingOffset)); + /// + /// Defines the property. + /// public static readonly StyledProperty IndeterminateEndingOffsetProperty = AvaloniaProperty.Register(nameof(IndeterminateEndingOffset)); + /// + /// Gets the overall percentage complete of the progress + /// + /// + /// This read-only property is automatically calculated using the current and + /// the effective range ( - ). + /// public double Percentage { get { return _percentage; } @@ -154,31 +188,50 @@ namespace Avalonia.Controls OrientationProperty.Changed.AddClassHandler((x, e) => x.UpdateIndicatorWhenPropChanged(e)); } + /// + /// Initializes a new instance of the class. + /// public ProgressBar() { UpdatePseudoClasses(IsIndeterminate, Orientation); } - public ProgressBarTemplateProperties TemplateProperties { get; } = new ProgressBarTemplateProperties(); + /// + /// Gets or sets the TemplateSettings for the . + /// + public ProgressBarTemplateSettings TemplateSettings { get; } = new ProgressBarTemplateSettings(); + /// + /// Gets or sets a value indicating whether the progress bar shows the actual value or a generic, + /// continues progress indicator (indeterminate state). + /// public bool IsIndeterminate { get => GetValue(IsIndeterminateProperty); set => SetValue(IsIndeterminateProperty, value); } + /// + /// Gets or sets a value indicating whether progress text will be shown. + /// public bool ShowProgressText { get => GetValue(ShowProgressTextProperty); set => SetValue(ShowProgressTextProperty, value); } + /// + /// Gets or sets the format string applied to the internally calculated progress text before it is shown. + /// public string ProgressTextFormat { get => GetValue(ProgressTextFormatProperty); set => SetValue(ProgressTextFormatProperty, value); } + /// + /// Gets or sets the orientation of the . + /// public Orientation Orientation { get => GetValue(OrientationProperty); @@ -193,6 +246,7 @@ namespace Avalonia.Controls return result; } + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -242,15 +296,14 @@ namespace Avalonia.Controls var barIndicatorWidth = dim * 0.4; // Indicator width at 40% of ProgressBar var barIndicatorWidth2 = dim * 0.6; // Indicator width at 60% of ProgressBar - TemplateProperties.ContainerWidth = barIndicatorWidth; - TemplateProperties.Container2Width = barIndicatorWidth2; - - TemplateProperties.ContainerAnimationStartPosition = barIndicatorWidth * -1.8; // Position at -180% - TemplateProperties.ContainerAnimationEndPosition = barIndicatorWidth * 3.0; // Position at 300% + TemplateSettings.ContainerWidth = barIndicatorWidth; + TemplateSettings.Container2Width = barIndicatorWidth2; - TemplateProperties.Container2AnimationStartPosition = barIndicatorWidth2 * -1.5; // Position at -150% - TemplateProperties.Container2AnimationEndPosition = barIndicatorWidth2 * 1.66; // Position at 166% + TemplateSettings.ContainerAnimationStartPosition = barIndicatorWidth * -1.8; // Position at -180% + TemplateSettings.ContainerAnimationEndPosition = barIndicatorWidth * 3.0; // Position at 300% + TemplateSettings.Container2AnimationStartPosition = barIndicatorWidth2 * -1.5; // Position at -150% + TemplateSettings.Container2AnimationEndPosition = barIndicatorWidth2 * 1.66; // Position at 166% // Remove these properties when we switch to fluent as default and removed the old one. SetCurrentValue(IndeterminateStartingOffsetProperty,-dim); diff --git a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml index 253d85852e..64a598e359 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml @@ -125,13 +125,13 @@ - + - + - + @@ -140,13 +140,13 @@ - + - + - + @@ -155,13 +155,13 @@ - + - + - + @@ -170,28 +170,28 @@ - + - + - + diff --git a/src/Avalonia.Themes.Simple/Controls/ProgressBar.xaml b/src/Avalonia.Themes.Simple/Controls/ProgressBar.xaml index 3eb158d5b6..1f4bf006a0 100644 --- a/src/Avalonia.Themes.Simple/Controls/ProgressBar.xaml +++ b/src/Avalonia.Themes.Simple/Controls/ProgressBar.xaml @@ -93,7 +93,7 @@ - + From 24a92e73e7ae022fde9470177b05f56fb32e0b2d Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 30 Apr 2023 12:06:03 -0400 Subject: [PATCH 102/110] Update SplitView comments based on ProgressBar changes --- src/Avalonia.Controls/SplitView/SplitView.cs | 6 +++--- .../SplitView/SplitViewTemplateSettings.cs | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/SplitView/SplitView.cs b/src/Avalonia.Controls/SplitView/SplitView.cs index 8060ca9594..3ad656ee3c 100644 --- a/src/Avalonia.Controls/SplitView/SplitView.cs +++ b/src/Avalonia.Controls/SplitView/SplitView.cs @@ -217,8 +217,8 @@ namespace Avalonia.Controls /// /// Gets or sets whether WinUI equivalent LightDismissOverlayMode is enabled /// When enabled, and the pane is open in Overlay or CompactOverlay mode, - /// the contents of the splitview are darkened to visually separate the open pane - /// and the rest of the SplitView + /// the contents of the are darkened to visually separate the open pane + /// and the rest of the . /// public bool UseLightDismissOverlayMode { @@ -227,7 +227,7 @@ namespace Avalonia.Controls } /// - /// Gets or sets the TemplateSettings for the SplitView + /// Gets or sets the TemplateSettings for the . /// public SplitViewTemplateSettings TemplateSettings { diff --git a/src/Avalonia.Controls/SplitView/SplitViewTemplateSettings.cs b/src/Avalonia.Controls/SplitView/SplitViewTemplateSettings.cs index 8c59de7420..1794b9260f 100644 --- a/src/Avalonia.Controls/SplitView/SplitViewTemplateSettings.cs +++ b/src/Avalonia.Controls/SplitView/SplitViewTemplateSettings.cs @@ -2,8 +2,10 @@ { /// /// Provides calculated values for use with the 's control theme or template. - /// This class is NOT intended for general use. /// + /// + /// This class is NOT intended for general use outside of control templates. + /// public class SplitViewTemplateSettings : AvaloniaObject { internal SplitViewTemplateSettings() { } From 9d76cdd3ddb7440dbaffe868eac833ed5071380c Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 30 Apr 2023 12:18:59 -0400 Subject: [PATCH 103/110] Rename incorrect `Color.ToUint32` to `ToUInt32` --- .../RenderDemo/Pages/WriteableBitmapPage.cs | 2 +- src/Avalonia.Base/Media/Color.cs | 11 +++- src/Avalonia.Base/Media/GeometryDrawing.cs | 2 +- .../AvaloniaXamlIlLanguageParseIntrinsics.cs | 4 +- .../DynamicResourceExtensionTests.cs | 52 +++++++++---------- .../MarkupExtensions/ResourceIncludeTests.cs | 2 +- .../StaticResourceExtensionTests.cs | 32 ++++++------ .../Xaml/StyleTests.cs | 4 +- 8 files changed, 58 insertions(+), 51 deletions(-) diff --git a/samples/RenderDemo/Pages/WriteableBitmapPage.cs b/samples/RenderDemo/Pages/WriteableBitmapPage.cs index 850e398a93..a13a625d14 100644 --- a/samples/RenderDemo/Pages/WriteableBitmapPage.cs +++ b/samples/RenderDemo/Pages/WriteableBitmapPage.cs @@ -59,7 +59,7 @@ namespace RenderDemo.Pages color = new Color(fillAlpha, r, g, b); } - data[y * fb.Size.Width + x] = (int) color.ToUint32(); + data[y * fb.Size.Width + x] = (int) color.ToUInt32(); } } diff --git a/src/Avalonia.Base/Media/Color.cs b/src/Avalonia.Base/Media/Color.cs index 7b29ec640a..56a3b0d7a5 100644 --- a/src/Avalonia.Base/Media/Color.cs +++ b/src/Avalonia.Base/Media/Color.cs @@ -449,7 +449,7 @@ namespace Avalonia.Media /// public override string ToString() { - uint rgb = ToUint32(); + uint rgb = ToUInt32(); return KnownColors.GetKnownColorName(rgb) ?? $"#{rgb.ToString("x8", CultureInfo.InvariantCulture)}"; } @@ -459,11 +459,18 @@ namespace Avalonia.Media /// /// The integer representation of the color. /// - public uint ToUint32() + public uint ToUInt32() { return ((uint)A << 24) | ((uint)R << 16) | ((uint)G << 8) | (uint)B; } + /// + [Obsolete("Use Color.ToUInt32() instead.")] + public uint ToUint32() + { + return ToUInt32(); + } + /// /// Returns the HSL color model equivalent of this RGB color. /// diff --git a/src/Avalonia.Base/Media/GeometryDrawing.cs b/src/Avalonia.Base/Media/GeometryDrawing.cs index abfd2e33ac..75d7e44ab8 100644 --- a/src/Avalonia.Base/Media/GeometryDrawing.cs +++ b/src/Avalonia.Base/Media/GeometryDrawing.cs @@ -10,7 +10,7 @@ namespace Avalonia.Media public class GeometryDrawing : Drawing { // Adding the Pen's stroke thickness here could yield wrong results due to transforms. - private static readonly IPen s_boundsPen = new ImmutablePen(Colors.Black.ToUint32(), 0); + private static readonly IPen s_boundsPen = new ImmutablePen(Colors.Black.ToUInt32(), 0); /// /// Defines the property. diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs index 6c7b04dbfb..819e721b36 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs @@ -155,7 +155,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions result = new XamlStaticOrTargetedReturnMethodCallNode(node, type.GetMethod( new FindMethodMethodSignature("FromUInt32", type, types.UInt) { IsStatic = true }), - new[] { new XamlConstantNode(node, types.UInt, color.ToUint32()) }); + new[] { new XamlConstantNode(node, types.UInt, color.ToUInt32()) }); return true; } @@ -242,7 +242,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions result = new XamlAstNewClrObjectNode(node, brushTypeRef, types.ImmutableSolidColorBrushConstructorColor, - new List { new XamlConstantNode(node, types.UInt, color.ToUint32()) }); + new List { new XamlConstantNode(node, types.UInt, color.ToUInt32()) }); return true; } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs index 535b96420a..523b31e60a 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs @@ -34,7 +34,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions DelayedBinding.ApplyBindings(border); var brush = (ISolidColorBrush)border.Background; - Assert.Equal(0xff506070, brush.Color.ToUint32()); + Assert.Equal(0xff506070, brush.Color.ToUInt32()); } [Fact] @@ -81,7 +81,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions DelayedBinding.ApplyBindings(border); var brush = (ISolidColorBrush)border.Background; - Assert.Equal(0xff506070, brush.Color.ToUint32()); + Assert.Equal(0xff506070, brush.Color.ToUInt32()); } [Fact] @@ -109,7 +109,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions DelayedBinding.ApplyBindings(border); var brush = (ISolidColorBrush)border.Background; - Assert.Equal(0xff506070, brush.Color.ToUint32()); + Assert.Equal(0xff506070, brush.Color.ToUInt32()); } [Fact] @@ -141,7 +141,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions DelayedBinding.ApplyBindings(border); var brush = (ISolidColorBrush)border.Background; - Assert.Equal(0xff506070, brush.Color.ToUint32()); + Assert.Equal(0xff506070, brush.Color.ToUInt32()); } [Fact] @@ -161,7 +161,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions var border = window.FindControl("border"); var brush = (SolidColorBrush)border.Background; - Assert.Equal(0xff506070, brush.Color.ToUint32()); + Assert.Equal(0xff506070, brush.Color.ToUInt32()); } } @@ -187,7 +187,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions window.Show(); var brush = (SolidColorBrush)border.Background; - Assert.Equal(0xff506070, brush.Color.ToUint32()); + Assert.Equal(0xff506070, brush.Color.ToUInt32()); } } @@ -214,7 +214,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions var button = window.FindControl public IObservable Source { get; } - [Obsolete("Use Source property")] + [Obsolete("Use Source property"), EditorBrowsable(EditorBrowsableState.Never)] public IObservable Observable => Source; /// diff --git a/src/Avalonia.Base/Input/DataFormats.cs b/src/Avalonia.Base/Input/DataFormats.cs index 35d50e669a..f593ed205f 100644 --- a/src/Avalonia.Base/Input/DataFormats.cs +++ b/src/Avalonia.Base/Input/DataFormats.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; namespace Avalonia.Input { @@ -17,7 +18,7 @@ namespace Avalonia.Input /// /// Dataformat for one or more filenames /// - [Obsolete("Use DataFormats.Files, this format is supported only on desktop platforms.")] + [Obsolete("Use DataFormats.Files, this format is supported only on desktop platforms."), EditorBrowsable(EditorBrowsableState.Never)] public static readonly string FileNames = nameof(FileNames); } } diff --git a/src/Avalonia.Base/Input/DataObjectExtensions.cs b/src/Avalonia.Base/Input/DataObjectExtensions.cs index 6af531b0d8..d2e525cd68 100644 --- a/src/Avalonia.Base/Input/DataObjectExtensions.cs +++ b/src/Avalonia.Base/Input/DataObjectExtensions.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using Avalonia.Platform.Storage; @@ -25,7 +26,7 @@ namespace Avalonia.Input /// /// Collection of file names. If format isn't available, returns null. /// - [System.Obsolete("Use GetFiles, this method is supported only on desktop platforms.")] + [System.Obsolete("Use GetFiles, this method is supported only on desktop platforms."), EditorBrowsable(EditorBrowsableState.Never)] public static IEnumerable? GetFileNames(this IDataObject dataObject) { return (dataObject.Get(DataFormats.FileNames) as IEnumerable) diff --git a/src/Avalonia.Base/Media/Color.cs b/src/Avalonia.Base/Media/Color.cs index 56a3b0d7a5..50c2faacc0 100644 --- a/src/Avalonia.Base/Media/Color.cs +++ b/src/Avalonia.Base/Media/Color.cs @@ -6,6 +6,7 @@ // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; +using System.ComponentModel; using System.Globalization; #if !BUILDTASK using Avalonia.Animation.Animators; @@ -465,7 +466,7 @@ namespace Avalonia.Media } /// - [Obsolete("Use Color.ToUInt32() instead.")] + [Obsolete("Use Color.ToUInt32() instead."), EditorBrowsable(EditorBrowsableState.Never)] public uint ToUint32() { return ToUInt32(); diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs index 18d6968168..3ab946a1db 100644 --- a/src/Avalonia.Base/Media/DrawingContext.cs +++ b/src/Avalonia.Base/Media/DrawingContext.cs @@ -5,6 +5,7 @@ using Avalonia.Rendering.SceneGraph; using Avalonia.Threading; using Avalonia.Utilities; using Avalonia.Media.Imaging; +using System.ComponentModel; namespace Avalonia.Media { @@ -417,11 +418,11 @@ namespace Avalonia.Media return new PushedState(this); } - [Obsolete("Use PushTransform")] + [Obsolete("Use PushTransform"), EditorBrowsable(EditorBrowsableState.Never)] public PushedState PushPreTransform(Matrix matrix) => PushTransform(matrix); - [Obsolete("Use PushTransform")] + [Obsolete("Use PushTransform"), EditorBrowsable(EditorBrowsableState.Never)] public PushedState PushPostTransform(Matrix matrix) => PushTransform(matrix); - [Obsolete("Use PushTransform")] + [Obsolete("Use PushTransform"), EditorBrowsable(EditorBrowsableState.Never)] public PushedState PushTransformContainer() => PushTransform(Matrix.Identity); diff --git a/src/Avalonia.Base/Threading/DispatcherPriority.cs b/src/Avalonia.Base/Threading/DispatcherPriority.cs index 3017b45dc7..a43dd8e4a2 100644 --- a/src/Avalonia.Base/Threading/DispatcherPriority.cs +++ b/src/Avalonia.Base/Threading/DispatcherPriority.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; namespace Avalonia.Threading { @@ -100,7 +101,7 @@ namespace Avalonia.Threading /// /// The job will be processed with the same priority as data binding. /// - [Obsolete("WPF compatibility")] public static readonly DispatcherPriority DataBind = new(Layout); + [Obsolete("WPF compatibility"), EditorBrowsable(EditorBrowsableState.Never)] public static readonly DispatcherPriority DataBind = new(Layout); /// /// The job will be processed with normal priority. diff --git a/src/Avalonia.Base/VisualTree/IVisualWithRoundRectClip.cs b/src/Avalonia.Base/VisualTree/IVisualWithRoundRectClip.cs index 9ace215d03..0079515a63 100644 --- a/src/Avalonia.Base/VisualTree/IVisualWithRoundRectClip.cs +++ b/src/Avalonia.Base/VisualTree/IVisualWithRoundRectClip.cs @@ -1,14 +1,15 @@ using System; +using System.ComponentModel; namespace Avalonia.VisualTree { - [Obsolete("Internal API, will be removed in future versions, you've been warned")] + [Obsolete("Internal API, will be removed in future versions, you've been warned"), EditorBrowsable(EditorBrowsableState.Never)] public interface IVisualWithRoundRectClip { /// /// Gets a value indicating the corner radius of control's clip bounds /// - [Obsolete("Internal API, will be removed in future versions, you've been warned")] + [Obsolete("Internal API, will be removed in future versions, you've been warned"), EditorBrowsable(EditorBrowsableState.Never)] CornerRadius ClipToBoundsRadius { get; } } diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 63e28ea14d..97a8c6fe97 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -64,7 +64,7 @@ namespace Avalonia.Controls /// /// Defines the property. /// - [Obsolete("Use the Placement property instead.")] + [Obsolete("Use the Placement property instead."), EditorBrowsable(EditorBrowsableState.Never)] public static readonly StyledProperty PlacementModeProperty = PlacementProperty; /// @@ -157,7 +157,7 @@ namespace Avalonia.Controls } /// - [Obsolete("Use the Placement property instead.")] + [Obsolete("Use the Placement property instead."), EditorBrowsable(EditorBrowsableState.Never)] public PlacementMode PlacementMode { get => GetValue(PlacementProperty); diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index b2c138599e..d27479af18 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; namespace Avalonia.Controls.Generators { @@ -131,10 +132,10 @@ namespace Avalonia.Controls.Generators /// public void ClearItemContainer(Control container) => _owner.ClearItemContainer(container); - [Obsolete("Use ItemsControl.ContainerFromIndex")] + [Obsolete("Use ItemsControl.ContainerFromIndex"), EditorBrowsable(EditorBrowsableState.Never)] public Control? ContainerFromIndex(int index) => _owner.ContainerFromIndex(index); - [Obsolete("Use ItemsControl.IndexFromContainer")] + [Obsolete("Use ItemsControl.IndexFromContainer"), EditorBrowsable(EditorBrowsableState.Never)] public int IndexFromContainer(Control container) => _owner.IndexFromContainer(container); } } diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs index c1cae862a9..717dadb6ea 100644 --- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; namespace Avalonia.Controls.Generators { @@ -20,13 +21,13 @@ namespace Avalonia.Controls.Generators internal TreeContainerIndex(TreeView owner) => _owner = owner; - [Obsolete("Use TreeView.GetRealizedTreeContainers")] + [Obsolete("Use TreeView.GetRealizedTreeContainers"), EditorBrowsable(EditorBrowsableState.Never)] public IEnumerable Containers => _owner.GetRealizedTreeContainers(); - [Obsolete("Use TreeView.TreeContainerFromItem")] + [Obsolete("Use TreeView.TreeContainerFromItem"), EditorBrowsable(EditorBrowsableState.Never)] public Control? ContainerFromItem(object item) => _owner.TreeContainerFromItem(item); - [Obsolete("Use TreeView.TreeItemFromContainer")] + [Obsolete("Use TreeView.TreeItemFromContainer"), EditorBrowsable(EditorBrowsableState.Never)] public object? ItemFromContainer(Control container) => _owner.TreeItemFromContainer(container); } } diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 1c62de9bed..4a0b3c367e 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.ComponentModel; using Avalonia.Automation.Peers; using Avalonia.Controls.Generators; using Avalonia.Controls.Metadata; @@ -626,7 +627,7 @@ namespace Avalonia.Controls /// TreeView to be able to create a . Can be /// removed in 12.0. /// - [Obsolete] + [Obsolete, EditorBrowsable(EditorBrowsableState.Never)] private protected virtual ItemContainerGenerator CreateItemContainerGenerator() { return new ItemContainerGenerator(this); diff --git a/src/Avalonia.Controls/Platform/Dialogs/ISystemDialogImpl.cs b/src/Avalonia.Controls/Platform/Dialogs/ISystemDialogImpl.cs index 996fff6775..a593caecaf 100644 --- a/src/Avalonia.Controls/Platform/Dialogs/ISystemDialogImpl.cs +++ b/src/Avalonia.Controls/Platform/Dialogs/ISystemDialogImpl.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Threading.Tasks; using Avalonia.Metadata; @@ -7,7 +8,7 @@ namespace Avalonia.Controls.Platform /// /// Defines a platform-specific system dialog implementation. /// - [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")] + [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API"), EditorBrowsable(EditorBrowsableState.Never)] [Unstable] public interface ISystemDialogImpl { diff --git a/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs b/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs index 20bfb440e3..37e6272abd 100644 --- a/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs +++ b/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using Avalonia.Platform.Storage; @@ -10,7 +11,7 @@ namespace Avalonia.Controls.Platform /// /// Defines a platform-specific system dialog implementation. /// - [Obsolete] + [Obsolete, EditorBrowsable(EditorBrowsableState.Never)] internal class SystemDialogImpl : ISystemDialogImpl { public async Task ShowFileDialogAsync(FileDialog dialog, Window parent) diff --git a/src/Avalonia.Controls/Platform/Screen.cs b/src/Avalonia.Controls/Platform/Screen.cs index 4898c5f912..fde90dc589 100644 --- a/src/Avalonia.Controls/Platform/Screen.cs +++ b/src/Avalonia.Controls/Platform/Screen.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; namespace Avalonia.Platform { @@ -17,7 +18,7 @@ namespace Avalonia.Platform public double Scaling { get; } /// - [Obsolete("Use the Scaling property instead.")] + [Obsolete("Use the Scaling property instead."), EditorBrowsable(EditorBrowsableState.Never)] public double PixelDensity => Scaling; /// @@ -43,7 +44,7 @@ namespace Avalonia.Platform public bool IsPrimary { get; } /// - [Obsolete("Use the IsPrimary property instead.")] + [Obsolete("Use the IsPrimary property instead."), EditorBrowsable(EditorBrowsableState.Never)] public bool Primary => IsPrimary; /// diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index eda794f33a..80b7841fc7 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -74,7 +74,7 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - [Obsolete("Use the Placement property instead.")] + [Obsolete("Use the Placement property instead."), EditorBrowsable(EditorBrowsableState.Never)] public static readonly StyledProperty PlacementModeProperty = PlacementProperty; /// @@ -241,7 +241,7 @@ namespace Avalonia.Controls.Primitives } /// - [Obsolete("Use the Placement property instead.")] + [Obsolete("Use the Placement property instead."), EditorBrowsable(EditorBrowsableState.Never)] public PlacementMode PlacementMode { get => GetValue(PlacementProperty); diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index dfaf7bbc45..fa1bc76de4 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Data; @@ -28,7 +29,7 @@ namespace Avalonia.Controls.Primitives /// /// Defines the event. /// - [Obsolete("Use IsCheckedChangedEvent instead.")] + [Obsolete("Use IsCheckedChangedEvent instead."), EditorBrowsable(EditorBrowsableState.Never)] public static readonly RoutedEvent CheckedEvent = RoutedEvent.Register( nameof(Checked), @@ -37,7 +38,7 @@ namespace Avalonia.Controls.Primitives /// /// Defines the event. /// - [Obsolete("Use IsCheckedChangedEvent instead.")] + [Obsolete("Use IsCheckedChangedEvent instead."), EditorBrowsable(EditorBrowsableState.Never)] public static readonly RoutedEvent UncheckedEvent = RoutedEvent.Register( nameof(Unchecked), @@ -46,7 +47,7 @@ namespace Avalonia.Controls.Primitives /// /// Defines the event. /// - [Obsolete("Use IsCheckedChangedEvent instead.")] + [Obsolete("Use IsCheckedChangedEvent instead."), EditorBrowsable(EditorBrowsableState.Never)] public static readonly RoutedEvent IndeterminateEvent = RoutedEvent.Register( nameof(Indeterminate), @@ -72,7 +73,7 @@ namespace Avalonia.Controls.Primitives /// /// Raised when a is checked. /// - [Obsolete("Use IsCheckedChanged instead.")] + [Obsolete("Use IsCheckedChanged instead."), EditorBrowsable(EditorBrowsableState.Never)] public event EventHandler? Checked { add => AddHandler(CheckedEvent, value); @@ -82,7 +83,7 @@ namespace Avalonia.Controls.Primitives /// /// Raised when a is unchecked. /// - [Obsolete("Use IsCheckedChanged instead.")] + [Obsolete("Use IsCheckedChanged instead."), EditorBrowsable(EditorBrowsableState.Never)] public event EventHandler? Unchecked { add => AddHandler(UncheckedEvent, value); @@ -92,7 +93,7 @@ namespace Avalonia.Controls.Primitives /// /// Raised when a is neither checked nor unchecked. /// - [Obsolete("Use IsCheckedChanged instead.")] + [Obsolete("Use IsCheckedChanged instead."), EditorBrowsable(EditorBrowsableState.Never)] public event EventHandler? Indeterminate { add => AddHandler(IndeterminateEvent, value); @@ -168,7 +169,7 @@ namespace Avalonia.Controls.Primitives /// Called when becomes true. /// /// Event arguments for the routed event that is raised by the default implementation of this method. - [Obsolete("Use OnIsCheckedChanged instead.")] + [Obsolete("Use OnIsCheckedChanged instead."), EditorBrowsable(EditorBrowsableState.Never)] protected virtual void OnChecked(RoutedEventArgs e) { RaiseEvent(e); @@ -178,7 +179,7 @@ namespace Avalonia.Controls.Primitives /// Called when becomes false. /// /// Event arguments for the routed event that is raised by the default implementation of this method. - [Obsolete("Use OnIsCheckedChanged instead.")] + [Obsolete("Use OnIsCheckedChanged instead."), EditorBrowsable(EditorBrowsableState.Never)] protected virtual void OnUnchecked(RoutedEventArgs e) { RaiseEvent(e); @@ -188,7 +189,7 @@ namespace Avalonia.Controls.Primitives /// Called when becomes null. /// /// Event arguments for the routed event that is raised by the default implementation of this method. - [Obsolete("Use OnIsCheckedChanged instead.")] + [Obsolete("Use OnIsCheckedChanged instead."), EditorBrowsable(EditorBrowsableState.Never)] protected virtual void OnIndeterminate(RoutedEventArgs e) { RaiseEvent(e); diff --git a/src/Avalonia.Controls/Screens.cs b/src/Avalonia.Controls/Screens.cs index 60d5358c49..c65aaafa4b 100644 --- a/src/Avalonia.Controls/Screens.cs +++ b/src/Avalonia.Controls/Screens.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using Avalonia.Platform; @@ -68,7 +69,7 @@ namespace Avalonia.Controls /// /// The window impl for which to retrieve the Screen. /// The . - [Obsolete("Use ScreenFromWindow(WindowBase) overload.")] + [Obsolete("Use ScreenFromWindow(WindowBase) overload."), EditorBrowsable(EditorBrowsableState.Never)] public Screen? ScreenFromWindow(IWindowBaseImpl window) { return _iScreenImpl.ScreenFromWindow(window); diff --git a/src/Avalonia.Controls/SystemDialog.cs b/src/Avalonia.Controls/SystemDialog.cs index d2b893df37..8d4dab11d7 100644 --- a/src/Avalonia.Controls/SystemDialog.cs +++ b/src/Avalonia.Controls/SystemDialog.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using Avalonia.Controls.Platform; @@ -11,7 +12,7 @@ namespace Avalonia.Controls /// /// Base class for system file dialogs. /// - [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")] + [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API"), EditorBrowsable(EditorBrowsableState.Never)] public abstract class FileDialog : FileSystemDialog { /// @@ -29,7 +30,7 @@ namespace Avalonia.Controls /// /// Base class for system file and directory dialogs. /// - [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")] + [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API"), EditorBrowsable(EditorBrowsableState.Never)] public abstract class FileSystemDialog : SystemDialog { /// @@ -42,7 +43,7 @@ namespace Avalonia.Controls /// /// Represents a system dialog that prompts the user to select a location for saving a file. /// - [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")] + [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API"), EditorBrowsable(EditorBrowsableState.Never)] public class SaveFileDialog : FileDialog { /// @@ -91,7 +92,7 @@ namespace Avalonia.Controls /// /// Represents a system dialog that allows the user to select one or more files to open. /// - [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")] + [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API"), EditorBrowsable(EditorBrowsableState.Never)] public class OpenFileDialog : FileDialog { /// @@ -132,7 +133,7 @@ namespace Avalonia.Controls /// /// Represents a system dialog that allows the user to select a directory. /// - [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")] + [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API"), EditorBrowsable(EditorBrowsableState.Never)] public class OpenFolderDialog : FileSystemDialog { /// @@ -167,7 +168,7 @@ namespace Avalonia.Controls /// /// Base class for system dialogs. /// - [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")] + [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API"), EditorBrowsable(EditorBrowsableState.Never)] public abstract class SystemDialog { static SystemDialog() @@ -188,7 +189,7 @@ namespace Avalonia.Controls /// /// Represents a filter in an or an . /// - [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")] + [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API"), EditorBrowsable(EditorBrowsableState.Never)] public class FileDialogFilter { /// diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index e3a9a05951..5122b4aebd 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -3,6 +3,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using Avalonia.Collections; @@ -715,7 +716,7 @@ namespace Avalonia.Controls } } - [Obsolete] + [Obsolete, EditorBrowsable(EditorBrowsableState.Never)] private protected override ItemContainerGenerator CreateItemContainerGenerator() { return new TreeItemContainerGenerator(this); diff --git a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs index e9a75ab46a..86c0bfc588 100644 --- a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs +++ b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs @@ -1,6 +1,7 @@ #nullable enable using System; +using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; @@ -39,11 +40,11 @@ namespace Avalonia.Dialogs return builder; } - [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")] + [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API"), EditorBrowsable(EditorBrowsableState.Never)] public static Task ShowManagedAsync(this OpenFileDialog dialog, Window parent, ManagedFileDialogOptions? options = null) => ShowManagedAsync(dialog, parent, options); - [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")] + [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API"), EditorBrowsable(EditorBrowsableState.Never)] public static async Task ShowManagedAsync(this OpenFileDialog dialog, Window parent, ManagedFileDialogOptions? options = null) where TWindow : Window, new() { diff --git a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs index 7e73397743..83bf795b03 100644 --- a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs @@ -8,6 +8,8 @@ using Avalonia.Rendering; using Avalonia.Rendering.Composition; using Avalonia.VisualTree; using Avalonia.Platform; +using System.ComponentModel; + namespace Avalonia.OpenGL.Controls { public abstract class OpenGlControlBase : Control @@ -217,7 +219,7 @@ namespace Avalonia.OpenGL.Controls return true; } - [Obsolete("Use RequestNextFrameRendering()")] + [Obsolete("Use RequestNextFrameRendering()"), EditorBrowsable(EditorBrowsableState.Never)] // ReSharper disable once MemberCanBeProtected.Global public new void InvalidateVisual() => RequestNextFrameRendering(); From f0ea1f11161a6c390b7c5173069701b78af02302 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 3 May 2023 14:23:34 +0600 Subject: [PATCH 106/110] Fixed some potential threading issues --- .../Threading/DispatcherFrame.cs | 57 ++++++++++++------- .../Platform/ManagedDispatcherImpl.cs | 6 ++ 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/src/Avalonia.Base/Threading/DispatcherFrame.cs b/src/Avalonia.Base/Threading/DispatcherFrame.cs index 1f8974dfa3..e826432475 100644 --- a/src/Avalonia.Base/Threading/DispatcherFrame.cs +++ b/src/Avalonia.Base/Threading/DispatcherFrame.cs @@ -91,31 +91,44 @@ public class DispatcherFrame internal void Run(IControlledDispatcherImpl impl) { - // Since the actual platform run loop is controlled by a Cancellation token, we are restarting - // it if frame still needs to run - while (Continue) - RunCore(impl); - } - - private void RunCore(IControlledDispatcherImpl impl) - { - if (_isRunning) - throw new InvalidOperationException("This frame is already running"); - _isRunning = true; - try - { - _cancellationTokenSource = new CancellationTokenSource(); - // Wake up the dispatcher in case it has pending jobs - Dispatcher.RequestProcessing(); - impl.RunLoop(_cancellationTokenSource.Token); - } - finally + Dispatcher.VerifyAccess(); + + // Since the actual platform run loop is controlled by a Cancellation token, we have an + // outer loop that restarts the platform one in case Continue was set to true after being set to false + while (true) { - _isRunning = false; - _cancellationTokenSource?.Cancel(); - _cancellationTokenSource = null; + // Take the instance lock since `Continue` is changed from one too + lock (Dispatcher.InstanceLock) + { + if (!Continue) + return; + + if (_isRunning) + throw new InvalidOperationException("This frame is already running"); + + _cancellationTokenSource = new CancellationTokenSource(); + _isRunning = true; + } + + try + { + // Wake up the dispatcher in case it has pending jobs + Dispatcher.RequestProcessing(); + impl.RunLoop(_cancellationTokenSource.Token); + } + finally + { + lock (Dispatcher.InstanceLock) + { + _isRunning = false; + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = null; + } + } } } + internal void MaybeExitOnDispatcherRequest() { diff --git a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs index 20aa91c83e..fdc098777a 100644 --- a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs +++ b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs @@ -58,6 +58,10 @@ public class ManagedDispatcherImpl : IControlledDispatcherImpl public void RunLoop(CancellationToken token) { + CancellationTokenRegistration registration = default; + if (token.CanBeCanceled) + registration = token.Register(() => _wakeup.Set()); + while (!token.IsCancellationRequested) { bool signaled; @@ -105,5 +109,7 @@ public class ManagedDispatcherImpl : IControlledDispatcherImpl else _wakeup.WaitOne(); } + + registration.Dispose(); } } \ No newline at end of file From 2f7c518f1e12ac8bc28c261144db50683dbf187d Mon Sep 17 00:00:00 2001 From: Mike James Date: Wed, 3 May 2023 10:25:35 +0200 Subject: [PATCH 107/110] Update readme.md Refreshing the readme to match our current branding. --- readme.md | 65 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/readme.md b/readme.md index c8135080fe..339cce9ee1 100644 --- a/readme.md +++ b/readme.md @@ -1,26 +1,44 @@ -[![GH_Banner](https://user-images.githubusercontent.com/552074/218457976-92e76834-9e22-4e35-acfa-aa50281bc0f9.png)](https://avaloniaui.net/xpf) +![StarBanner3](https://user-images.githubusercontent.com/552074/235864283-691ac648-5113-4b5c-87e8-bed9ba192927.png) + +![Header](https://user-images.githubusercontent.com/552074/235865745-2a8e7274-4f66-4f77-8f05-feeb76e7d478.png) [![Telegram](https://raw.githubusercontent.com/Patrolavia/telegram-badge/master/chat.svg)](https://t.me/Avalonia) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) [![Discord](https://img.shields.io/badge/discord-join%20chat-46BC99)]( https://aka.ms/dotnet-discord) [![Build Status](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_apis/build/status/AvaloniaUI.Avalonia)](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [![Backers on Open Collective](https://opencollective.com/Avalonia/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/Avalonia/sponsors/badge.svg)](#sponsors) ![License](https://img.shields.io/github/license/avaloniaui/avalonia.svg)
[![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) [![downloads](https://img.shields.io/nuget/dt/avalonia)](https://www.nuget.org/packages/Avalonia) ![Size](https://img.shields.io/github/repo-size/avaloniaui/avalonia.svg) -# ⚠️ **v11 Update - Pausing community contributions** - -for more information see [this](https://github.com/AvaloniaUI/Avalonia/discussions/10599) discussion. +⚠️ **v11 Update - [Pausing community contributions](https://github.com/AvaloniaUI/Avalonia/discussions/10599)** ## 📖 About -Avalonia is a cross-platform UI framework for dotnet, providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS. Avalonia is mature and production ready. We also have in beta release support for iOS, Android and in early stages support for browser via WASM. +[Avalonia](https://avaloniaui.net) is a cross-platform UI framework for dotnet, providing a flexible styling system and supporting a wide range of platforms such as Windows, macOS, Linux, iOS, Android and WebAssembly. Avalonia is mature and production ready and is used by companies, including [Schneider Electric](https://avaloniaui.net/showcase#se), [Unity](https://avaloniaui.net/showcase#unity), [JetBrains](https://avaloniaui.net/showcase#rider) and [Github](https://avaloniaui.net/showcase#github). + +Considered by many to be the spiritual successor to WPF, Avalonia UI provides a familiar, modern development experience for XAML developers creating cross-platform applications. While Avalonia UI is [similar to WPF](https://docs.avaloniaui.net/misc/wpf), it isn't a 1:1 copy, and you'll find plenty of improvements. + +For those seeking a cross-platform WPF, we have created [Avalonia XPF](https://avaloniaui.net/xpf), enabling WPF applications to run on macOS and Linux with little to no code changes. Avalonia XPF is a commercial product and is licensed per-app, per-platform. + +#### Roadmap +To see the status of some of our features, please see our [Roadmap](https://github.com/AvaloniaUI/Avalonia/issues/2239). -![image](https://user-images.githubusercontent.com/4672627/152126443-932966cf-57e7-4e77-9be6-62463a66b9f8.png) +#### Breaking Changes +You can also see what [breaking changes](https://github.com/AvaloniaUI/Avalonia/issues/3538) we have planned and what our [past breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) have been. -To see the status of some of our features, please see our [Roadmap](https://github.com/AvaloniaUI/Avalonia/issues/2239). You can also see what [breaking changes](https://github.com/AvaloniaUI/Avalonia/issues/3538) we have planned and what our [past breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) have been. [Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is community-curated list of awesome Avalonia UI tools, libraries, projects and resources. Go and see what people are building with Avalonia! +#### Awesome Avalonia +[Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is community-curated list of awesome Avalonia UI tools, libraries, projects and resources. Go and see what people are building with Avalonia! ## 🚀 Getting Started +See our [Get Started](https://avaloniaui.net/GettingStarted) guide to begin developing apps with Avalonia UI. + +### Visual Studio The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starter guide see our [documentation](https://docs.avaloniaui.net/docs/getting-started). +### JetBrains Rider +[JetBrains Rider](https://www.jetbrains.com/rider/whatsnew/?mkt_tok=eyJpIjoiTURBNU1HSmhNV0kwTUdFMiIsInQiOiJtNnU2VEc1TlNLa1ZRVkROYmdZYVpYREJsaU1qdUhmS3dxSzRHczdYWHl0RVlTNDMwSFwvNUs3VENTNVM0bVcyNFdaRmVYZzVWTTF1N3VrQWNGTkJreEhlam1hMlB4UVVWcHBGM1dNOUxoXC95YnRQdGgyUXl1YmZCM3h3d3BVWWdBIn0%3D#avalonia-support) now has official support for Avalonia. + +Code completion, inspections and refactorings are supported out of the box, for XAML previewer add `https://plugins.jetbrains.com/plugins/dev/14839` to plugin repositories and install [AvaloniaRider](https://github.com/ForNeVeR/AvaloniaRider) plugin. + +### Avalonia Packages Avalonia is delivered via NuGet package manager. You can find the packages here: https://www.nuget.org/packages/Avalonia/ Use these commands in the Package Manager console to install Avalonia manually: @@ -30,31 +48,25 @@ Install-Package Avalonia.Desktop ``` ## Showcase +[![Showcase_Banner](https://user-images.githubusercontent.com/552074/235865504-f9acaf42-ea2d-456a-ab52-547c45614bd5.png)](https://avaloniaui.net/showcase) -Examples of UIs built with Avalonia - -([Lunacy](https://icons8.com/lunacy)) - -![image](https://user-images.githubusercontent.com/4672627/152325740-261c27a3-e6f0-4662-bff7-4796d4940e04.png) -([PlasticSCM](https://www.plasticscm.com/)) -![image](https://user-images.githubusercontent.com/4672627/152326453-14944c4d-33da-4d50-a268-b87f80927adb.png) -([WasabiWallet](https://www.wasabiwallet.io/)) - - -## JetBrains Rider - -[JetBrains Rider](https://www.jetbrains.com/rider/whatsnew/?mkt_tok=eyJpIjoiTURBNU1HSmhNV0kwTUdFMiIsInQiOiJtNnU2VEc1TlNLa1ZRVkROYmdZYVpYREJsaU1qdUhmS3dxSzRHczdYWHl0RVlTNDMwSFwvNUs3VENTNVM0bVcyNFdaRmVYZzVWTTF1N3VrQWNGTkJreEhlam1hMlB4UVVWcHBGM1dNOUxoXC95YnRQdGgyUXl1YmZCM3h3d3BVWWdBIn0%3D#avalonia-support) now has official support for Avalonia. - -Code completion, inspections and refactorings are supported out of the box, for XAML previewer add `https://plugins.jetbrains.com/plugins/dev/14839` to plugin repositories and install [AvaloniaRider](https://github.com/ForNeVeR/AvaloniaRider) plugin. +See what others have built with Avalonia UI on our [Showcase](https://avaloniaui.net/Showcase). We welcome submissions! ## Bleeding Edge Builds We also have a [nightly build](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed) which tracks the current state of master. Although these packages are less stable than the release on NuGet.org, you'll get all the latest features and bugfixes right away and many of our users actually prefer this feed! -## Documentation +## Learning -Documentation can be found at https://docs.avaloniaui.net. We also have a [tutorial](https://docs.avaloniaui.net/docs/getting-started/programming-with-avalonia) over there for newcomers. +### Documentation +Documentation can be found at https://docs.avaloniaui.net. + +### Tutorials +We also have a [tutorial](https://docs.avaloniaui.net/docs/getting-started/programming-with-avalonia) over there for newcomers. + +### Samples +We have a [range of samples](https://github.com/AvaloniaUI/Avalonia.Samples) to help you get started. ## Building and Using @@ -116,3 +128,8 @@ We have a range of [support plans available](https://avaloniaui.net/support) for ## .NET Foundation This project is supported by the [.NET Foundation](https://dotnetfoundation.org). + +## Avalonia XPF +Unleash the full potential of your existing WPF apps with our cross-platform UI framework, enabling WPF apps to run on macOS and Linux without requiring expensive and risky rewrites. + +[![GH_Banner](https://user-images.githubusercontent.com/552074/218457976-92e76834-9e22-4e35-acfa-aa50281bc0f9.png)](https://avaloniaui.net/xpf) From 015768c5bd29932372a5007a087e2d4f11eaa238 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 3 May 2023 14:26:02 +0600 Subject: [PATCH 108/110] Make sure that Dispatcher.InvokeAsync unwraps tasks and that correct overload is being chosen --- .../Threading/Dispatcher.Invoke.cs | 42 +++++++++++++++--- .../DispatcherTests.cs | 44 +++++++++++++++++++ 2 files changed, 80 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs index 699186868a..bb1663eac0 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs @@ -248,11 +248,11 @@ public partial class Dispatcher /// An operation representing the queued delegate to be invoked. /// /// - /// Note that the default priority is DispatcherPriority.Normal. + /// Note that the default priority is DispatcherPriority.Default. /// public DispatcherOperation InvokeAsync(Action callback) { - return InvokeAsync(callback, DispatcherPriority.Normal, CancellationToken.None); + return InvokeAsync(callback, default, CancellationToken.None); } /// @@ -326,11 +326,11 @@ public partial class Dispatcher /// An operation representing the queued delegate to be invoked. /// /// - /// Note that the default priority is DispatcherPriority.Normal. + /// Note that the default priority is DispatcherPriority.Default. /// public DispatcherOperation InvokeAsync(Func callback) { - return InvokeAsync(callback, DispatcherPriority.Normal, CancellationToken.None); + return InvokeAsync(callback, DispatcherPriority.Default, CancellationToken.None); } /// @@ -541,6 +541,18 @@ public partial class Dispatcher InvokeAsyncImpl(new DispatcherOperation(this, priority, action, true), CancellationToken.None); } + /// + /// Executes the specified Func<Task> asynchronously on the + /// thread that the Dispatcher was created on + /// + /// + /// A Func<Task> delegate to invoke through the dispatcher. + /// + /// + /// An task that completes after the task returned from callback finishes. + /// + public Task InvokeAsync(Func callback) => InvokeAsync(callback, DispatcherPriority.Default); + /// /// Executes the specified Func<Task> asynchronously on the /// thread that the Dispatcher was created on @@ -556,11 +568,29 @@ public partial class Dispatcher /// /// An task that completes after the task returned from callback finishes /// - public Task InvokeAsync(Func callback, DispatcherPriority priority = default) + public Task InvokeAsync(Func callback, DispatcherPriority priority) { _ = callback ?? throw new ArgumentNullException(nameof(callback)); return InvokeAsync(callback, priority).GetTask().Unwrap(); } + + /// + /// Executes the specified Func<Task<TResult>> asynchronously on the + /// thread that the Dispatcher was created on + /// + /// + /// A Func<Task<TResult>> delegate to invoke through the dispatcher. + /// + /// + /// The priority that determines in what order the specified + /// callback is invoked relative to the other pending operations + /// in the Dispatcher. + /// + /// + /// An task that completes after the task returned from callback finishes + /// + public Task InvokeAsync(Func> action) => + InvokeAsync(action, DispatcherPriority.Default); /// /// Executes the specified Func<Task<TResult>> asynchronously on the @@ -577,7 +607,7 @@ public partial class Dispatcher /// /// An task that completes after the task returned from callback finishes /// - public Task InvokeAsync(Func> action, DispatcherPriority priority = default) + public Task InvokeAsync(Func> action, DispatcherPriority priority) { _ = action ?? throw new ArgumentNullException(nameof(action)); return InvokeAsync>(action, priority).GetTask().Unwrap(); diff --git a/tests/Avalonia.Base.UnitTests/DispatcherTests.cs b/tests/Avalonia.Base.UnitTests/DispatcherTests.cs index 9ba3f3980d..7b401918ce 100644 --- a/tests/Avalonia.Base.UnitTests/DispatcherTests.cs +++ b/tests/Avalonia.Base.UnitTests/DispatcherTests.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; +using System.Threading.Tasks; +using Avalonia.Controls.Platform; using Avalonia.Threading; using Avalonia.Utilities; using Xunit; @@ -458,4 +460,46 @@ public class DispatcherTests } } + [Fact] + public void DispatcherInvokeAsyncUnwrapsTasks() + { + int asyncMethodStage = 0; + + async Task AsyncMethod() + { + asyncMethodStage = 1; + await Task.Delay(200); + asyncMethodStage = 2; + } + + async Task AsyncMethodWithResult() + { + await Task.Delay(100); + return 1; + } + + async Task Test() + { + await Dispatcher.UIThread.InvokeAsync(AsyncMethod); + Assert.Equal(2, asyncMethodStage); + Assert.Equal(1, await Dispatcher.UIThread.InvokeAsync(AsyncMethodWithResult)); + asyncMethodStage = 0; + + await Dispatcher.UIThread.InvokeAsync(AsyncMethod, DispatcherPriority.Default); + Assert.Equal(2, asyncMethodStage); + Assert.Equal(1, await Dispatcher.UIThread.InvokeAsync(AsyncMethodWithResult, DispatcherPriority.Default)); + + Dispatcher.UIThread.ExitAllFrames(); + } + + using (new DispatcherServices(new ManagedDispatcherImpl(null))) + { + var t = Test(); + var cts = new CancellationTokenSource(); + Task.Delay(3000).ContinueWith(_ => cts.Cancel()); + Dispatcher.UIThread.MainLoop(cts.Token); + Assert.True(t.IsCompletedSuccessfully); + t.GetAwaiter().GetResult(); + } + } } \ No newline at end of file From 1604dd701c7769791a3e7dc9931b7c8bdf6d817f Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 3 May 2023 13:19:17 +0200 Subject: [PATCH 109/110] The default cursor is called "default" and not "pointer" --- src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts index bbc59aba1c..77166e6f21 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts @@ -266,7 +266,7 @@ export class InputHelper { } public static setCursor(inputElement: HTMLInputElement, kind: string) { - if (kind === "pointer") { + if (kind === "default") { inputElement.style.removeProperty("cursor"); } else { inputElement.style.cursor = kind; From f956aa10e7005ee3c5dd9b9a52ce8221b672cc0b Mon Sep 17 00:00:00 2001 From: Mike James Date: Wed, 3 May 2023 16:28:01 +0200 Subject: [PATCH 110/110] Update readme.md Tweaked images to look slightly better in dark mode. --- readme.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 339cce9ee1..6dd556bd0d 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,4 @@ -![StarBanner3](https://user-images.githubusercontent.com/552074/235864283-691ac648-5113-4b5c-87e8-bed9ba192927.png) - +![Star our repo to show support](https://user-images.githubusercontent.com/552074/235945895-1b896994-a0b6-4e7c-a522-c5688c4ec1b9.png) ![Header](https://user-images.githubusercontent.com/552074/235865745-2a8e7274-4f66-4f77-8f05-feeb76e7d478.png) [![Telegram](https://raw.githubusercontent.com/Patrolavia/telegram-badge/master/chat.svg)](https://t.me/Avalonia) @@ -48,7 +47,8 @@ Install-Package Avalonia.Desktop ``` ## Showcase -[![Showcase_Banner](https://user-images.githubusercontent.com/552074/235865504-f9acaf42-ea2d-456a-ab52-547c45614bd5.png)](https://avaloniaui.net/showcase) +[![Showcase_Banner](https://user-images.githubusercontent.com/552074/235946124-bf6fda52-0c9f-4730-868b-0de957e5b97b.png)](https://avaloniaui.net/showcase) + See what others have built with Avalonia UI on our [Showcase](https://avaloniaui.net/Showcase). We welcome submissions!