diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 741570061b..4a7a329fc6 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -42,6 +42,7 @@ "src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj", "src\\tools\\DevAnalyzers\\DevAnalyzers.csproj", "src\\tools\\DevGenerators\\DevGenerators.csproj", + "src\\tools\\PublicAnalyzers\\Avalonia.Analyzers.csproj", "tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj", "tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj", "tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj", @@ -61,4 +62,4 @@ "tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj" ] } -} \ No newline at end of file +} diff --git a/Avalonia.sln b/Avalonia.sln index 525e01c891..56847bae31 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -231,7 +231,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Browser.Blaz EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}" +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}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{176582E8-46AF-416A-85C1-13A5C6744497}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater", "src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj", "{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}" EndProject @@ -548,7 +555,6 @@ Global {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.Build.0 = Release|Any CPU - {C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.Build.0 = Debug|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.ActiveCfg = Release|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.Build.0 = Release|Any CPU @@ -560,6 +566,10 @@ Global {F4E36AA8-814E-4704-BC07-291F70F45193}.Debug|Any CPU.Build.0 = Debug|Any CPU {F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.ActiveCfg = Release|Any CPU {F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.Build.0 = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.ActiveCfg = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.Build.0 = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -626,6 +636,7 @@ Global {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098} {C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/build/DevAnalyzers.props b/build/DevAnalyzers.props index 14e4f6a563..7d021d051f 100644 --- a/build/DevAnalyzers.props +++ b/build/DevAnalyzers.props @@ -5,5 +5,10 @@ ReferenceOutputAssembly="false" OutputItemType="Analyzer" SetTargetFramework="TargetFramework=netstandard2.0"/> + diff --git a/samples/ControlCatalog.Browser.Blazor/App.razor.cs b/samples/ControlCatalog.Browser.Blazor/App.razor.cs index f38db2b055..c331625664 100644 --- a/samples/ControlCatalog.Browser.Blazor/App.razor.cs +++ b/samples/ControlCatalog.Browser.Blazor/App.razor.cs @@ -1,3 +1,5 @@ +using System; +using System.Threading.Tasks; using Avalonia; using Avalonia.Browser.Blazor; @@ -5,13 +7,4 @@ namespace ControlCatalog.Browser.Blazor; public partial class App { - protected override void OnParametersSet() - { - AppBuilder.Configure() - .UseBlazor() - // .With(new SkiaOptions { CustomGpuFactory = null }) // uncomment to disable GPU/GL rendering - .SetupWithSingleViewLifetime(); - - base.OnParametersSet(); - } } diff --git a/samples/ControlCatalog.Browser.Blazor/Program.cs b/samples/ControlCatalog.Browser.Blazor/Program.cs index eb99ca518e..e68e9b14d9 100644 --- a/samples/ControlCatalog.Browser.Blazor/Program.cs +++ b/samples/ControlCatalog.Browser.Blazor/Program.cs @@ -1,6 +1,8 @@ using System; using System.Net.Http; using System.Threading.Tasks; +using Avalonia; +using Avalonia.Browser.Blazor; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.Extensions.DependencyInjection; using ControlCatalog.Browser.Blazor; @@ -9,9 +11,17 @@ public class Program { public static async Task Main(string[] args) { - await CreateHostBuilder(args).Build().RunAsync(); + var host = CreateHostBuilder(args).Build(); + await StartAvaloniaApp(); + await host.RunAsync(); } + public static async Task StartAvaloniaApp() + { + await AppBuilder.Configure() + .StartBlazorAppAsync(); + } + public static WebAssemblyHostBuilder CreateHostBuilder(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); diff --git a/samples/ControlCatalog.Browser/Program.cs b/samples/ControlCatalog.Browser/Program.cs index 53b7c60a6f..e1a4500173 100644 --- a/samples/ControlCatalog.Browser/Program.cs +++ b/samples/ControlCatalog.Browser/Program.cs @@ -1,6 +1,8 @@ using System.Runtime.Versioning; +using System.Threading.Tasks; using Avalonia; using Avalonia.Browser; +using Avalonia.Controls; using ControlCatalog; using ControlCatalog.Browser; @@ -8,15 +10,27 @@ using ControlCatalog.Browser; internal partial class Program { - private static void Main(string[] args) + public static async Task Main(string[] args) { - BuildAvaloniaApp() + await BuildAvaloniaApp() .AfterSetup(_ => { ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb(); - }).SetupBrowserApp("out"); + }) + .StartBrowserAppAsync("out"); } + // Example without a ISingleViewApplicationLifetime + // private static AvaloniaView _avaloniaView; + // public static async Task Main(string[] args) + // { + // await BuildAvaloniaApp() + // .SetupBrowserApp(); + // + // _avaloniaView = new AvaloniaView("out"); + // _avaloniaView.Content = new TextBlock { Text = "Hello world" }; + // } + public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure(); } diff --git a/samples/ControlCatalog.Browser/main.js b/samples/ControlCatalog.Browser/main.js index 87f8a4f943..9d90db8bd2 100644 --- a/samples/ControlCatalog.Browser/main.js +++ b/samples/ControlCatalog.Browser/main.js @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. import { dotnet } from './dotnet.js' -import { registerAvaloniaModule } from './avalonia.js'; const is_browser = typeof window != "undefined"; if (!is_browser) throw new Error(`Expected to be running in a browser`); @@ -12,8 +11,6 @@ const dotnetRuntime = await dotnet .withApplicationArgumentsFromQuery() .create(); -await registerAvaloniaModule(dotnetRuntime); - const config = dotnetRuntime.getConfig(); await dotnetRuntime.runMainAndExit(config.mainAssemblyName, ["dotnet", "is", "great!"]); diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index f7b020678d..e24860e3e1 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -40,7 +40,7 @@ namespace ControlCatalog.Pages if (Enum.TryParse(currentFolderBox.Text, true, out var folderEnum)) { - lastSelectedDirectory = await GetStorageProvider().TryGetWellKnownFolder(folderEnum); + lastSelectedDirectory = await GetStorageProvider().TryGetWellKnownFolderAsync(folderEnum); } else { @@ -51,7 +51,7 @@ namespace ControlCatalog.Pages if (folderLink is not null) { - lastSelectedDirectory = await GetStorageProvider().TryGetFolderFromPath(folderLink); + lastSelectedDirectory = await GetStorageProvider().TryGetFolderFromPathAsync(folderLink); } } }; @@ -82,7 +82,13 @@ namespace ControlCatalog.Pages return new List { FilePickerFileTypes.All, - FilePickerFileTypes.TextPlain + FilePickerFileTypes.TextPlain, + new("Binary Log") + { + Patterns = new[] { "*.binlog", "*.buildlog" }, + MimeTypes = new[] { "application/binlog", "application/buildlog" }, + AppleUniformTypeIdentifiers = new []{ "public.data" } + } }; } @@ -142,7 +148,7 @@ namespace ControlCatalog.Pages } else { - SetFolder(await GetStorageProvider().TryGetFolderFromPath(result)); + SetFolder(await GetStorageProvider().TryGetFolderFromPathAsync(result)); results.Items = new[] { result }; resultsVisible.IsVisible = true; } @@ -223,7 +229,7 @@ namespace ControlCatalog.Pages ShowOverwritePrompt = false }); - if (file is not null && file.CanOpenWrite) + if (file is not null) { // Sync disposal of StreamWriter is not supported on WASM #if NET6_0_OR_GREATER @@ -275,7 +281,7 @@ namespace ControlCatalog.Pages { ignoreTextChanged = true; lastSelectedDirectory = folder; - currentFolderBox.Text = folder?.Path.LocalPath; + currentFolderBox.Text = folder?.Path is { IsAbsoluteUri: true } abs ? abs.LocalPath : folder?.Path?.ToString(); ignoreTextChanged = false; } async Task SetPickerResult(IReadOnlyCollection? items) @@ -298,31 +304,26 @@ namespace ControlCatalog.Pages if (item is IStorageFile file) { resultText += @$" - CanOpenRead: {file.CanOpenRead} - CanOpenWrite: {file.CanOpenWrite} Content: "; - if (file.CanOpenRead) - { #if NET6_0_OR_GREATER - await using var stream = await file.OpenReadAsync(); + await using var stream = await file.OpenReadAsync(); #else - using var stream = await file.OpenReadAsync(); + using var stream = await file.OpenReadAsync(); #endif - using var reader = new System.IO.StreamReader(stream); + using var reader = new System.IO.StreamReader(stream); - // 4GB file test, shouldn't load more than 10000 chars into a memory. - const int length = 10000; - var buffer = ArrayPool.Shared.Rent(length); - try - { - var charsRead = await reader.ReadAsync(buffer, 0, length); - resultText += new string(buffer, 0, charsRead); - } - finally - { - ArrayPool.Shared.Return(buffer); - } + // 4GB file test, shouldn't load more than 10000 chars into a memory. + const int length = 10000; + var buffer = ArrayPool.Shared.Rent(length); + try + { + var charsRead = await reader.ReadAsync(buffer, 0, length); + resultText += new string(buffer, 0, charsRead); + } + finally + { + ArrayPool.Shared.Return(buffer); } } diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props index 3b14f0ce12..ac78d9c739 100644 --- a/samples/Directory.Build.props +++ b/samples/Directory.Build.props @@ -6,4 +6,5 @@ 11 + diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index a6f463fdcb..353e01dca7 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -153,6 +153,9 @@ + + + diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs index 9838bb06c8..9d6dd46d0e 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs @@ -177,11 +177,7 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF public AndroidStorageFile(Activity activity, AndroidUri uri) : base(activity, uri, false) { } - - public bool CanOpenRead => true; - - public bool CanOpenWrite => true; - + public Task OpenReadAsync() => Task.FromResult(OpenContentStream(Activity, Uri, false) ?? throw new InvalidOperationException("Failed to open content stream")); diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs index f611f50164..e35bde0acd 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs @@ -37,7 +37,7 @@ internal class AndroidStorageProvider : IStorageProvider return Task.FromResult(new AndroidStorageFolder(_activity, uri, false)); } - public async Task TryGetFileFromPath(Uri filePath) + public async Task TryGetFileFromPathAsync(Uri filePath) { if (filePath is null) { @@ -70,7 +70,7 @@ internal class AndroidStorageProvider : IStorageProvider return new AndroidStorageFile(_activity, androidUri); } - public async Task TryGetFolderFromPath(Uri folderPath) + public async Task TryGetFolderFromPathAsync(Uri folderPath) { if (folderPath is null) { @@ -103,7 +103,7 @@ internal class AndroidStorageProvider : IStorageProvider return new AndroidStorageFolder(_activity, androidUri, false); } - public Task TryGetWellKnownFolder(WellKnownFolder wellKnownFolder) + public Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder) { var dirCode = wellKnownFolder switch { diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 2c9efc7767..d89d6f3690 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -664,14 +664,12 @@ namespace Avalonia /// The property that has changed. /// The old property value. /// The new property value. - /// The priority of the binding that produced the value. protected void RaisePropertyChanged( DirectPropertyBase property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority = BindingPriority.LocalValue) + T oldValue, + T newValue) { - RaisePropertyChanged(property, oldValue, newValue, priority, true); + RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue, true); } /// @@ -720,7 +718,7 @@ namespace Avalonia /// /// True if the value changed, otherwise false. /// - protected bool SetAndRaise(AvaloniaProperty property, ref T field, T value) + protected bool SetAndRaise(DirectPropertyBase property, ref T field, T value) { VerifyAccess(); diff --git a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs index 9ec256225b..57e4fa4a8e 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs +++ b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs @@ -22,7 +22,7 @@ namespace Avalonia.Data.Core if (target is INotifyPropertyChanged inpc) { - WeakEvents.PropertyChanged.Subscribe(inpc, this); + WeakEvents.ThreadSafePropertyChanged.Subscribe(inpc, this); } ValueChanged(GetValue(target)); @@ -39,7 +39,7 @@ namespace Avalonia.Data.Core if (target is INotifyPropertyChanged inpc) { - WeakEvents.PropertyChanged.Unsubscribe(inpc, this); + WeakEvents.ThreadSafePropertyChanged.Unsubscribe(inpc, this); } } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index 7c2caf02b4..e8e3e6d509 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -160,7 +160,7 @@ namespace Avalonia.Data.Core.Plugins var inpc = GetReferenceTarget() as INotifyPropertyChanged; if (inpc != null) - WeakEvents.PropertyChanged.Unsubscribe(inpc, this); + WeakEvents.ThreadSafePropertyChanged.Unsubscribe(inpc, this); } private object? GetReferenceTarget() @@ -185,7 +185,7 @@ namespace Avalonia.Data.Core.Plugins var inpc = GetReferenceTarget() as INotifyPropertyChanged; if (inpc != null) - WeakEvents.PropertyChanged.Subscribe(inpc, this); + WeakEvents.ThreadSafePropertyChanged.Subscribe(inpc, this); } } } diff --git a/src/Avalonia.Base/Layout/LayoutInformation.cs b/src/Avalonia.Base/Layout/LayoutInformation.cs new file mode 100644 index 0000000000..9b821053a2 --- /dev/null +++ b/src/Avalonia.Base/Layout/LayoutInformation.cs @@ -0,0 +1,27 @@ +namespace Avalonia.Layout; + +/// +/// Provides access to layout information of a control. +/// +public static class LayoutInformation +{ + /// + /// Gets the available size constraint passed in the previous layout pass. + /// + /// The control. + /// Previous control measure constraint, if any. + public static Size? GetPreviousMeasureConstraint(Layoutable control) + { + return control.PreviousMeasure; + } + + /// + /// Gets the control bounds used in the previous layout arrange pass. + /// + /// The control. + /// Previous control arrange bounds, if any. + public static Rect? GetPreviousArrangeBounds(Layoutable control) + { + return control.PreviousArrange; + } +} diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs index 775b8adddd..4a273b0291 100644 --- a/src/Avalonia.Base/Layout/Layoutable.cs +++ b/src/Avalonia.Base/Layout/Layoutable.cs @@ -323,6 +323,9 @@ namespace Avalonia.Layout set { SetValue(UseLayoutRoundingProperty, value); } } + /// + /// Gets the available size passed in the previous layout pass, if any. + /// internal Size? PreviousMeasure => _previousMeasure; /// diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs index 622181dba0..a37fa6fd32 100644 --- a/src/Avalonia.Base/Media/DrawingContext.cs +++ b/src/Avalonia.Base/Media/DrawingContext.cs @@ -361,11 +361,12 @@ namespace Avalonia.Media /// Pushes an opacity value. /// /// The opacity. + /// The bounds. /// A disposable used to undo the opacity. - public PushedState PushOpacity(double opacity) + public PushedState PushOpacity(double opacity, Rect bounds) //TODO: Eliminate platform-specific push opacity call { - PlatformImpl.PushOpacity(opacity); + PlatformImpl.PushOpacity(opacity, bounds); return new PushedState(this, PushedState.PushedStateType.Opacity); } diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index b7abda2c61..7b02649b6c 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -74,10 +74,12 @@ namespace Avalonia.Media public override void Draw(DrawingContext context) { + var bounds = GetBounds(); + using (context.PushPreTransform(Transform?.Value ?? Matrix.Identity)) - using (context.PushOpacity(Opacity)) + using (context.PushOpacity(Opacity, bounds)) using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default) - using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, GetBounds()) : default) + using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, bounds) : default) { foreach (var drawing in Children) { @@ -284,7 +286,7 @@ namespace Avalonia.Media throw new NotImplementedException(); } - public void PushOpacity(double opacity) + public void PushOpacity(double opacity, Rect bounds) { throw new NotImplementedException(); } diff --git a/src/Avalonia.Base/Media/IImageBrush.cs b/src/Avalonia.Base/Media/IImageBrush.cs index 732f1957d0..07fd2d56fa 100644 --- a/src/Avalonia.Base/Media/IImageBrush.cs +++ b/src/Avalonia.Base/Media/IImageBrush.cs @@ -12,6 +12,6 @@ namespace Avalonia.Media /// /// Gets the image to draw. /// - IBitmap Source { get; } + IBitmap? Source { get; } } } diff --git a/src/Avalonia.Base/Media/ImageBrush.cs b/src/Avalonia.Base/Media/ImageBrush.cs index 2f2a0fb627..718ebf1686 100644 --- a/src/Avalonia.Base/Media/ImageBrush.cs +++ b/src/Avalonia.Base/Media/ImageBrush.cs @@ -11,8 +11,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty SourceProperty = - AvaloniaProperty.Register(nameof(Source)); + public static readonly StyledProperty SourceProperty = + AvaloniaProperty.Register(nameof(Source)); static ImageBrush() { @@ -30,7 +30,7 @@ namespace Avalonia.Media /// Initializes a new instance of the class. /// /// The image to draw. - public ImageBrush(IBitmap source) + public ImageBrush(IBitmap? source) { Source = source; } @@ -38,7 +38,7 @@ namespace Avalonia.Media /// /// Gets or sets the image to draw. /// - public IBitmap Source + public IBitmap? Source { get { return GetValue(SourceProperty); } set { SetValue(SourceProperty, value); } diff --git a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs index 7d9534c414..2564d89bac 100644 --- a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs +++ b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs @@ -281,11 +281,12 @@ namespace Avalonia.Media /// Pushes an opacity value. /// /// The opacity. + /// The bounds. /// A disposable used to undo the opacity. - public PushedState PushOpacity(double opacity) + public PushedState PushOpacity(double opacity, Rect bounds) //TODO: Eliminate platform-specific push opacity call { - PlatformImpl.PushOpacity(opacity); + PlatformImpl.PushOpacity(opacity, bounds); return new PushedState(this, PushedState.PushedStateType.Opacity); } diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs index f9892bf60c..668a907fdf 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs @@ -24,13 +24,13 @@ namespace Avalonia.Media.Immutable /// The tile mode. /// The bitmap interpolation mode. public ImmutableImageBrush( - IBitmap source, + IBitmap? source, AlignmentX alignmentX = AlignmentX.Center, AlignmentY alignmentY = AlignmentY.Center, RelativeRect? destinationRect = null, double opacity = 1, ImmutableTransform? transform = null, - RelativePoint transformOrigin = new RelativePoint(), + RelativePoint transformOrigin = default, RelativeRect? sourceRect = null, Stretch stretch = Stretch.Uniform, TileMode tileMode = TileMode.None, @@ -61,6 +61,6 @@ namespace Avalonia.Media.Immutable } /// - public IBitmap Source { get; } + public IBitmap? Source { get; } } } diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs index 0b625080e3..e9086eee37 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs @@ -24,7 +24,7 @@ namespace Avalonia.Media.Immutable /// The tile mode. /// Controls the quality of interpolation. public ImmutableVisualBrush( - Visual visual, + Visual? visual, AlignmentX alignmentX = AlignmentX.Center, AlignmentY alignmentY = AlignmentY.Center, RelativeRect? destinationRect = null, diff --git a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs index b5e7298b7e..08fcdb50aa 100644 --- a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs +++ b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs @@ -37,7 +37,7 @@ namespace Avalonia.Platform }; } - public event EventHandler? ColorValuesChanged; + public virtual event EventHandler? ColorValuesChanged; protected void OnColorValuesChanged(PlatformColorValues colorValues) { diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index 8509067cd0..8962bc1586 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs @@ -128,7 +128,7 @@ namespace Avalonia.Platform /// Pushes an opacity value. /// /// The opacity. - void PushOpacity(double opacity); + void PushOpacity(double opacity, Rect bounds); /// /// Pops the latest pushed opacity value. diff --git a/src/Avalonia.Base/Platform/IGeometryImpl.cs b/src/Avalonia.Base/Platform/IGeometryImpl.cs index 5826cfb2ff..d1964bf07e 100644 --- a/src/Avalonia.Base/Platform/IGeometryImpl.cs +++ b/src/Avalonia.Base/Platform/IGeometryImpl.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Avalonia.Media; using Avalonia.Metadata; @@ -47,7 +48,7 @@ namespace Avalonia.Platform /// The stroke to use. /// The point. /// true if the geometry contains the point; otherwise, false. - bool StrokeContains(IPen pen, Point point); + bool StrokeContains(IPen? pen, Point point); /// /// Makes a clone of the geometry with the specified transform. @@ -87,6 +88,7 @@ namespace Avalonia.Platform /// If ture, the resulting snipped path will start with a BeginFigure call. /// The resulting snipped path. /// If the snipping operation is successful. - bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, out IGeometryImpl segmentGeometry); + bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, + [NotNullWhen(true)] out IGeometryImpl? segmentGeometry); } } diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs index a4005d4f5f..5bf9ff9d9a 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs @@ -18,11 +18,7 @@ internal class BclStorageFile : IStorageBookmarkFile } public FileInfo FileInfo { get; } - - public bool CanOpenRead => true; - - public bool CanOpenWrite => true; - + public string Name => FileInfo.Name; public virtual bool CanBookmark => true; diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs index ee169d62a5..34409f5fda 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs @@ -34,7 +34,7 @@ internal abstract class BclStorageProvider : IStorageProvider : Task.FromResult(null); } - public virtual Task TryGetFileFromPath(Uri filePath) + public virtual Task TryGetFileFromPathAsync(Uri filePath) { if (filePath.IsAbsoluteUri) { @@ -48,7 +48,7 @@ internal abstract class BclStorageProvider : IStorageProvider return Task.FromResult(null); } - public virtual Task TryGetFolderFromPath(Uri folderPath) + public virtual Task TryGetFolderFromPathAsync(Uri folderPath) { if (folderPath.IsAbsoluteUri) { @@ -62,7 +62,7 @@ internal abstract class BclStorageProvider : IStorageProvider return Task.FromResult(null); } - public virtual Task TryGetWellKnownFolder(WellKnownFolder wellKnownFolder) + public virtual Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder) { // Note, this BCL API returns different values depending on the .NET version. // We should also document it. diff --git a/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs b/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs index f9c7f9685d..7b0446e224 100644 --- a/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs +++ b/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.IO; +using System.Linq; namespace Avalonia.Platform.Storage; @@ -21,7 +23,7 @@ public sealed class FilePickerFileType /// List of extensions in GLOB format. I.e. "*.png" or "*.*". /// /// - /// Used on Windows and Linux systems. + /// Used on Windows, Linux and Browser platforms. /// public IReadOnlyList? Patterns { get; set; } @@ -29,7 +31,7 @@ public sealed class FilePickerFileType /// List of extensions in MIME format. /// /// - /// Used on Android, Browser and Linux systems. + /// Used on Android, Linux and Browser platforms. /// public IReadOnlyList? MimeTypes { get; set; } @@ -41,4 +43,14 @@ public sealed class FilePickerFileType /// See https://developer.apple.com/documentation/uniformtypeidentifiers/system_declared_uniform_type_identifiers. /// public IReadOnlyList? AppleUniformTypeIdentifiers { get; set; } + + internal IReadOnlyList? TryGetExtensions() + { + // Converts random glob pattern to a simple extension name. + // GetExtension should be sufficient here. + // Only exception is "*.*proj" patterns that should be filtered as well. + return Patterns?.Select(Path.GetExtension) + .Where(e => !string.IsNullOrEmpty(e) && !e.Contains('*') && e.StartsWith(".")) + .ToArray()!; + } } diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs index 4aa84e3ec4..2a0ce15279 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs @@ -10,22 +10,12 @@ namespace Avalonia.Platform.Storage; [NotClientImplementable] public interface IStorageFile : IStorageItem { - /// - /// Returns true, if file is readable. - /// - bool CanOpenRead { get; } - /// /// Opens a stream for read access. /// /// Task OpenReadAsync(); - - /// - /// Returns true, if file is writeable. - /// - bool CanOpenWrite { get; } - + /// /// Opens stream for writing to the file. /// diff --git a/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs index 6922151e02..9d3c961e51 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs @@ -66,7 +66,7 @@ public interface IStorageProvider /// It also might ask user for the permission, and throw an exception if it was denied. /// /// File or null if it doesn't exist. - Task TryGetFileFromPath(Uri filePath); + Task TryGetFileFromPathAsync(Uri filePath); /// /// Attempts to read folder from the file-system by its path. @@ -78,12 +78,12 @@ public interface IStorageProvider /// It also might ask user for the permission, and throw an exception if it was denied. /// /// Folder or null if it doesn't exist. - Task TryGetFolderFromPath(Uri folderPath); + Task TryGetFolderFromPathAsync(Uri folderPath); /// /// Attempts to read folder from the file-system by its path /// /// Well known folder identifier. /// Folder or null if it doesn't exist. - Task TryGetWellKnownFolder(WellKnownFolder wellKnownFolder); + Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder); } diff --git a/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs b/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs index c7772d1196..6f8b945cd6 100644 --- a/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs +++ b/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs @@ -8,48 +8,47 @@ namespace Avalonia.Platform.Storage; /// public static class StorageProviderExtensions { - /// - public static Task TryGetFileFromPath(this IStorageProvider provider, string filePath) + /// + public static Task TryGetFileFromPathAsync(this IStorageProvider provider, string filePath) { - return provider.TryGetFileFromPath(StorageProviderHelpers.FilePathToUri(filePath)); + return provider.TryGetFileFromPathAsync(StorageProviderHelpers.FilePathToUri(filePath)); } - /// - public static Task TryGetFolderFromPath(this IStorageProvider provider, string folderPath) + /// + public static Task TryGetFolderFromPathAsync(this IStorageProvider provider, string folderPath) { - return provider.TryGetFolderFromPath(StorageProviderHelpers.FilePathToUri(folderPath)); + return provider.TryGetFolderFromPathAsync(StorageProviderHelpers.FilePathToUri(folderPath)); } - internal static string? TryGetFullPath(this IStorageFolder folder) + /// + /// Gets the local file system path of the item as a string. + /// + /// Storage folder or file. + /// Full local path to the folder or file if possible, otherwise null. + /// + /// Android platform usually uses "content:" virtual file paths + /// and Browser platform has isolated access without full paths, + /// so on these platforms this method will return null. + /// + public static string? TryGetLocalPath(this IStorageItem item) { // We can avoid double escaping of the path by checking for BclStorageFolder. // Ideally, `folder.Path.LocalPath` should also work, as that's only available way for the users. - if (folder is BclStorageFolder storageFolder) + if (item is BclStorageFolder storageFolder) { return storageFolder.DirectoryInfo.FullName; } - - if (folder.Path is { IsAbsoluteUri: true, Scheme: "file" } absolutePath) - { - return absolutePath.LocalPath; - } - - // android "content:", browser and ios relative links go here. - return null; - } - - internal static string? TryGetFullPath(this IStorageFile file) - { - if (file is BclStorageFile storageFolder) + if (item is BclStorageFile storageFile) { - return storageFolder.FileInfo.FullName; + return storageFile.FileInfo.FullName; } - if (file.Path is { IsAbsoluteUri: true, Scheme: "file" } absolutePath) + if (item.Path is { IsAbsoluteUri: true, Scheme: "file" } absolutePath) { return absolutePath.LocalPath; } + // android "content:", browser and ios relative links go here. return null; } } diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs index b75d080cfd..6b380608fe 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -313,13 +313,13 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } /// - public void PushOpacity(double opacity) + public void PushOpacity(double opacity, Rect bounds) { var next = NextDrawAs(); - if (next == null || !next.Item.Equals(opacity)) + if (next == null || !next.Item.Equals(opacity, bounds)) { - Add(new OpacityNode(opacity)); + Add(new OpacityNode(opacity, bounds)); } else { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 50df8bd32b..08e506536f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -111,9 +111,9 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont _impl.PopClip(); } - public void PushOpacity(double opacity) + public void PushOpacity(double opacity, Rect bounds) { - _impl.PushOpacity(opacity); + _impl.PushOpacity(opacity, bounds); } public void PopOpacity() diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index e33dc999dc..f9492d0015 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs @@ -41,27 +41,29 @@ namespace Avalonia.Rendering.Composition.Server return; Root!.RenderedVisuals++; - - if (Opacity != 1) - canvas.PushOpacity(Opacity); + + var boundsRect = new Rect(new Size(Size.X, Size.Y)); + if (AdornedVisual != null) { canvas.PostTransform = Matrix.Identity; canvas.Transform = Matrix.Identity; - canvas.PushClip(AdornedVisual._combinedTransformedClipBounds); + if (AdornerIsClipped) + canvas.PushClip(AdornedVisual._combinedTransformedClipBounds); } var transform = GlobalTransformMatrix; canvas.PostTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; - - var boundsRect = new Rect(new Size(Size.X, Size.Y)); + + if (Opacity != 1) + canvas.PushOpacity(Opacity, boundsRect); if (ClipToBounds && !HandlesClipToBounds) canvas.PushClip(Root!.SnapToDevicePixels(boundsRect)); if (Clip != null) canvas.PushGeometryClip(Clip); if(OpacityMaskBrush != null) canvas.PushOpacityMask(OpacityMaskBrush, boundsRect); - + RenderCore(canvas, currentTransformedClip); // Hack to force invalidation of SKMatrix @@ -74,7 +76,7 @@ namespace Avalonia.Rendering.Composition.Server canvas.PopGeometryClip(); if (ClipToBounds && !HandlesClipToBounds) canvas.PopClip(); - if (AdornedVisual != null) + if (AdornedVisual != null && AdornerIsClipped) canvas.PopClip(); if(Opacity != 1) canvas.PopOpacity(); diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs index 8e5dc38317..09d2d55ce3 100644 --- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs @@ -117,7 +117,7 @@ namespace Avalonia.Rendering } using (context.PushPostTransform(m)) - using (context.PushOpacity(opacity)) + using (context.PushOpacity(opacity, bounds)) using (clipToBounds #pragma warning disable CS0618 // Type or member is obsolete ? visual is IVisualWithRoundRectClip roundClipVisual diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs index e41e639067..f76a055934 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs @@ -12,9 +12,11 @@ namespace Avalonia.Rendering.SceneGraph /// opacity push. /// /// The opacity to push. - public OpacityNode(double opacity) + /// The bounds. + public OpacityNode(double opacity, Rect bounds) { Opacity = opacity; + Bounds = bounds; } /// @@ -26,7 +28,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public Rect Bounds => default; + public Rect Bounds { get; } /// /// Gets the opacity to be pushed or null if the operation represents a pop. @@ -40,19 +42,20 @@ namespace Avalonia.Rendering.SceneGraph /// Determines if this draw operation equals another. /// /// The opacity of the other draw operation. + /// The bounds of the other draw operation. /// True if the draw operations are the same, otherwise false. /// /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(double? opacity) => Opacity == opacity; + public bool Equals(double? opacity, Rect bounds) => Opacity == opacity && Bounds == bounds; /// public void Render(IDrawingContextImpl context) { if (Opacity.HasValue) { - context.PushOpacity(Opacity.Value); + context.PushOpacity(Opacity.Value, Bounds); } else { diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index a80351e654..405b874fed 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -498,13 +498,7 @@ namespace Avalonia NotifyResourcesChanged(); } -#nullable disable - RaisePropertyChanged( - ParentProperty, - new Optional(old), - new BindingValue(Parent), - BindingPriority.LocalValue); -#nullable enable + RaisePropertyChanged(ParentProperty, old, Parent); } } diff --git a/src/Avalonia.Base/Utilities/WeakEvents.cs b/src/Avalonia.Base/Utilities/WeakEvents.cs index 6da899bab2..2f62564e0e 100644 --- a/src/Avalonia.Base/Utilities/WeakEvents.cs +++ b/src/Avalonia.Base/Utilities/WeakEvents.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Specialized; using System.ComponentModel; using System.Windows.Input; +using Avalonia.Threading; namespace Avalonia.Utilities; @@ -20,15 +21,30 @@ public class WeakEvents }); /// - /// Represents PropertyChanged event from + /// Represents PropertyChanged event from with auto-dispatching to the UI thread /// public static readonly WeakEvent - PropertyChanged = WeakEvent.Register( + ThreadSafePropertyChanged = WeakEvent.Register( (s, h) => { - PropertyChangedEventHandler handler = (_, e) => h(s, e); + bool unsubscribed = false; + PropertyChangedEventHandler handler = (_, e) => + { + if (Dispatcher.UIThread.CheckAccess()) + h(s, e); + else + Dispatcher.UIThread.Post(() => + { + if (!unsubscribed) + h(s, e); + }); + }; s.PropertyChanged += handler; - return () => s.PropertyChanged -= handler; + return () => + { + unsubscribed = true; + s.PropertyChanged -= handler; + }; }); diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 87bb1d3790..8b0cc06136 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -573,7 +573,7 @@ namespace Avalonia /// The new visual parent. protected virtual void OnVisualParentChanged(Visual? oldParent, Visual? newParent) { - RaisePropertyChanged(VisualParentProperty, oldParent, newParent, BindingPriority.LocalValue); + RaisePropertyChanged(VisualParentProperty, oldParent, newParent); } internal override ParametrizedLogger? GetBindingWarningLogger( diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index 36fd9fe709..31722974ee 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -26,6 +26,7 @@ + diff --git a/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs new file mode 100644 index 0000000000..42b15eec96 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs @@ -0,0 +1,22 @@ +using Avalonia.Automation.Peers; + +namespace Avalonia.Controls.Automation.Peers +{ + public class SliderAutomationPeer : RangeBaseAutomationPeer + { + public SliderAutomationPeer(Slider owner) : base(owner) + { + } + + override protected string GetClassNameCore() + { + return "Slider"; + } + + override protected AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Slider; + } + + } +} diff --git a/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs b/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs index a8a266e378..20bfb440e3 100644 --- a/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs +++ b/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs @@ -27,7 +27,7 @@ namespace Avalonia.Controls.Platform var files = await filePicker.OpenFilePickerAsync(options); return files - .Select(file => file.TryGetFullPath() ?? file.Name) + .Select(file => file.TryGetLocalPath() ?? file.Name) .ToArray(); } else if (dialog is SaveFileDialog saveDialog) @@ -46,7 +46,7 @@ namespace Avalonia.Controls.Platform return null; } - var filePath = file.TryGetFullPath() ?? file.Name; + var filePath = file.TryGetLocalPath() ?? file.Name; return new[] { filePath }; } return null; @@ -64,7 +64,7 @@ namespace Avalonia.Controls.Platform var folders = await filePicker.OpenFolderPickerAsync(options); return folders - .Select(folder => folder.TryGetFullPath() ?? folder.Name) + .Select(folder => folder.TryGetLocalPath() ?? folder.Name) .FirstOrDefault(u => u is not null); } } diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 454f7eac9d..bc86558ab3 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -427,6 +427,7 @@ namespace Avalonia.Controls.Presenters Viewport = finalSize; Extent = Child!.Bounds.Size.Inflate(Child.Margin); + Offset = ScrollViewer.CoerceOffset(Extent, finalSize, Offset); _isAnchorElementDirty = true; return finalSize; diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 79719912ea..611d57a980 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -279,8 +279,11 @@ namespace Avalonia.Controls.Primitives private void UpdateAdornedElement(Visual adorner, Visual? adorned) { if (adorner.CompositionVisual != null) + { adorner.CompositionVisual.AdornedVisual = adorned?.CompositionVisual; - + adorner.CompositionVisual.AdornerIsClipped = GetIsClipEnabled(adorner); + } + var info = adorner.GetValue(s_adornedElementInfoProperty); if (info != null) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 065c4ff2e5..2ee32b0dda 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -345,10 +345,7 @@ namespace Avalonia.Controls.Primitives if (_oldSelectedItems != SelectedItems) { - RaisePropertyChanged( - SelectedItemsProperty, - new Optional(_oldSelectedItems), - new BindingValue(SelectedItems)); + RaisePropertyChanged(SelectedItemsProperty, _oldSelectedItems, SelectedItems); _oldSelectedItems = SelectedItems; } } @@ -909,10 +906,7 @@ namespace Avalonia.Controls.Primitives else if (e.PropertyName == nameof(InternalSelectionModel.WritableSelectedItems) && _oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems) { - RaisePropertyChanged( - SelectedItemsProperty, - new Optional(_oldSelectedItems), - new BindingValue(SelectedItems)); + RaisePropertyChanged(SelectedItemsProperty, _oldSelectedItems, SelectedItems); _oldSelectedItems = SelectedItems; } else if (e.PropertyName == nameof(ISelectionModel.Source)) diff --git a/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs b/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs index 18cf96ddca..d2b91def7d 100644 --- a/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs +++ b/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs @@ -1,37 +1,30 @@ using Avalonia.Layout; +using Avalonia.Threading; namespace Avalonia.Controls { public partial class RelativePanel { - private static void OnAlignPropertiesChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) - { - if (d is Layoutable layoutable && layoutable.Parent is Layoutable layoutableParent) - { - layoutableParent.InvalidateArrange(); - } - } static RelativePanel() { ClipToBoundsProperty.OverrideDefaultValue(true); - AboveProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignBottomWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignBottomWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignHorizontalCenterWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignHorizontalCenterWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignLeftWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignLeftWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignRightWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignRightWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignTopWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignTopWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignVerticalCenterWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignVerticalCenterWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - BelowProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - LeftOfProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - RightOfProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AffectsParentArrange( + AlignLeftWithPanelProperty, AlignLeftWithProperty, LeftOfProperty, + AlignRightWithPanelProperty, AlignRightWithProperty, RightOfProperty, + AlignTopWithPanelProperty, AlignTopWithProperty, AboveProperty, + AlignBottomWithPanelProperty, AlignBottomWithProperty, BelowProperty, + AlignHorizontalCenterWithPanelProperty, AlignHorizontalCenterWithProperty, + AlignVerticalCenterWithPanelProperty, AlignVerticalCenterWithProperty); + + AffectsParentMeasure( + AlignLeftWithPanelProperty, AlignLeftWithProperty, LeftOfProperty, + AlignRightWithPanelProperty, AlignRightWithProperty, RightOfProperty, + AlignTopWithPanelProperty, AlignTopWithProperty, AboveProperty, + AlignBottomWithPanelProperty, AlignBottomWithProperty, BelowProperty, + AlignHorizontalCenterWithPanelProperty, AlignHorizontalCenterWithProperty, + AlignVerticalCenterWithPanelProperty, AlignVerticalCenterWithProperty); } /// diff --git a/src/Avalonia.Controls/SelectableTextBlock.cs b/src/Avalonia.Controls/SelectableTextBlock.cs index f8ce5d23f6..6603e20a2a 100644 --- a/src/Avalonia.Controls/SelectableTextBlock.cs +++ b/src/Avalonia.Controls/SelectableTextBlock.cs @@ -336,7 +336,7 @@ namespace Avalonia.Controls point = new Point( MathUtilities.Clamp(point.X, 0, Math.Max(TextLayout.Bounds.Width, 0)), - MathUtilities.Clamp(point.Y, 0, Math.Max(TextLayout.Bounds.Width, 0))); + MathUtilities.Clamp(point.Y, 0, Math.Max(TextLayout.Bounds.Height, 0))); var hit = TextLayout.HitTestPoint(point); var textPosition = hit.TextPosition; diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 828bf2a1fb..7de726a932 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -10,6 +10,7 @@ using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Utilities; using Avalonia.Automation; +using Avalonia.Controls.Automation.Peers; namespace Avalonia.Controls { @@ -380,6 +381,11 @@ namespace Avalonia.Controls } } + protected override AutomationPeer OnCreateAutomationPeer() + { + return new SliderAutomationPeer(this); + } + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 90a28e3c2b..fdcb8cc537 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -413,7 +413,31 @@ namespace Avalonia.Controls { return visual?.VisualRoot as TopLevel; } - + + /// + /// Requests a to be inhibited. + /// The behavior remains inhibited until the return value is disposed. + /// The available set of s depends on the platform. + /// If a behavior is inhibited on a platform where this type is not supported the request will have no effect. + /// + public async Task RequestPlatformInhibition(PlatformInhibitionType type, string reason) + { + var platformBehaviorInhibition = PlatformImpl?.TryGetFeature(); + if (platformBehaviorInhibition == null) + { + return Disposable.Create(() => { }); + } + + switch (type) + { + case PlatformInhibitionType.AppSleep: + await platformBehaviorInhibition.SetInhibitAppSleep(true, reason); + return Disposable.Create(() => platformBehaviorInhibition.SetInhibitAppSleep(false, reason).Wait()); + default: + return Disposable.Create(() => { }); + } + } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -579,30 +603,6 @@ namespace Avalonia.Controls /// The event args. protected virtual void OnClosed(EventArgs e) => Closed?.Invoke(this, e); - /// - /// Requests a to be inhibited. - /// The behavior remains inhibited until the return value is disposed. - /// The available set of s depends on the platform. - /// If a behavior is inhibited on a platform where this type is not supported the request will have no effect. - /// - protected async Task RequestPlatformInhibition(PlatformInhibitionType type, string reason) - { - var platformBehaviorInhibition = PlatformImpl?.TryGetFeature(); - if (platformBehaviorInhibition == null) - { - return Disposable.Create(() => { }); - } - - switch (type) - { - case PlatformInhibitionType.AppSleep: - await platformBehaviorInhibition.SetInhibitAppSleep(true, reason); - return Disposable.Create(() => platformBehaviorInhibition.SetInhibitAppSleep(false, reason).Wait()); - default: - return Disposable.Create(() => { }); - } - } - /// /// Tries to get a service from an , logging a /// warning if not found. diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 9f8e3e38c0..e9abfef673 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -190,7 +190,7 @@ namespace Avalonia.Controls { if (treeViewItem.ItemCount > 0 && !treeViewItem.IsExpanded) { - treeViewItem.IsExpanded = true; + treeViewItem.SetCurrentValue(IsExpandedProperty, true); return true; } @@ -201,7 +201,7 @@ namespace Avalonia.Controls { if (treeViewItem.ItemCount > 0 && treeViewItem.IsExpanded) { - treeViewItem.IsExpanded = false; + treeViewItem.SetCurrentValue(IsExpandedProperty, false); return true; } @@ -214,7 +214,7 @@ namespace Avalonia.Controls { if (treeViewItem.IsFocused) { - treeViewItem.IsExpanded = false; + treeViewItem.SetCurrentValue(IsExpandedProperty, false); } else { @@ -265,7 +265,7 @@ namespace Avalonia.Controls { if (ItemCount > 0) { - IsExpanded = !IsExpanded; + SetCurrentValue(IsExpandedProperty, !IsExpanded); e.Handled = true; } } diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 634efbd699..8e7690aa6c 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -403,7 +403,7 @@ namespace Avalonia.Controls if (firstIndex == -1) { estimatedElementSize = EstimateElementSizeU(); - firstIndex = (int)(viewportStart / estimatedElementSize); + firstIndex = Math.Min((int)(viewportStart / estimatedElementSize), maxIndex); firstIndexU = firstIndex * estimatedElementSize; } @@ -411,13 +411,13 @@ namespace Avalonia.Controls { if (estimatedElementSize == -1) estimatedElementSize = EstimateElementSizeU(); - lastIndex = (int)(viewportEnd / estimatedElementSize); + lastIndex = Math.Min((int)(viewportEnd / estimatedElementSize), maxIndex); } return new MeasureViewport { - firstIndex = MathUtilities.Clamp(firstIndex, 0, maxIndex), - lastIndex = MathUtilities.Clamp(lastIndex, 0, maxIndex), + firstIndex = firstIndex, + lastIndex = lastIndex, viewportUStart = viewportStart, viewportUEnd = viewportEnd, startU = firstIndexU, @@ -1131,6 +1131,7 @@ namespace Avalonia.Controls // The removed range was before the realized elements. Update the first index and // the indexes of the realized elements. _firstIndex -= count; + _startUUnstable = true; var newIndex = _firstIndex; for (var i = 0; i < _elements.Count; ++i) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml index 3bfe511fbc..1b5f431f36 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml @@ -16,6 +16,8 @@