Browse Source

Merge branch 'master' into fixes/property-changed-thread-dispatch

pull/10362/head
Max Katz 3 years ago
committed by GitHub
parent
commit
e5f8f47cf2
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      samples/ControlCatalog.Browser.Blazor/App.razor.cs
  2. 12
      samples/ControlCatalog.Browser.Blazor/Program.cs
  3. 20
      samples/ControlCatalog.Browser/Program.cs
  4. 3
      samples/ControlCatalog.Browser/main.js
  5. 51
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  6. 3
      samples/IntegrationTestApp/MainWindow.axaml
  7. 6
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
  8. 6
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs
  9. 10
      src/Avalonia.Base/AvaloniaObject.cs
  10. 27
      src/Avalonia.Base/Layout/LayoutInformation.cs
  11. 3
      src/Avalonia.Base/Layout/Layoutable.cs
  12. 2
      src/Avalonia.Base/Platform/DefaultPlatformSettings.cs
  13. 6
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
  14. 6
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs
  15. 16
      src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs
  16. 12
      src/Avalonia.Base/Platform/Storage/IStorageFile.cs
  17. 6
      src/Avalonia.Base/Platform/Storage/IStorageProvider.cs
  18. 45
      src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs
  19. 5
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  20. 8
      src/Avalonia.Base/StyledElement.cs
  21. 2
      src/Avalonia.Base/Visual.cs
  22. 1
      src/Avalonia.Base/composition-schema.xml
  23. 22
      src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs
  24. 6
      src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs
  25. 5
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  26. 10
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  27. 39
      src/Avalonia.Controls/RelativePanel.AttachedProperties.cs
  28. 6
      src/Avalonia.Controls/Slider.cs
  29. 8
      src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs
  30. 2
      src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs
  31. 2
      src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs
  32. 2
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  33. 6
      src/Avalonia.Native/SystemDialogs.cs
  34. 12
      src/Avalonia.X11/NativeDialogs/CompositeStorageProvider.cs
  35. 2
      src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
  36. 14
      src/Browser/Avalonia.Browser.Blazor/AvaloniaView.cs
  37. 37
      src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs
  38. 10
      src/Browser/Avalonia.Browser/AvaloniaView.cs
  39. 83
      src/Browser/Avalonia.Browser/BrowserAppBuilder.cs
  40. 40
      src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs
  41. 51
      src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs
  42. 4
      src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs
  43. 12
      src/Browser/Avalonia.Browser/Interop/StorageHelper.cs
  44. 28
      src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs
  45. 2
      src/Browser/Avalonia.Browser/webapp/.eslintrc.json
  46. 2
      src/Browser/Avalonia.Browser/webapp/build.js
  47. 18
      src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts
  48. 12
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/caniuse.ts
  49. 3
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts
  50. 50
      src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts
  51. 29
      src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts
  52. 117
      src/Browser/Avalonia.Browser/webapp/package-lock.json
  53. 4
      src/Browser/Avalonia.Browser/webapp/package.json
  54. 6
      src/Browser/Avalonia.Browser/webapp/tsconfig.json
  55. 65
      src/Windows/Avalonia.Win32/Automation/AutomationNode.cs
  56. 35
      src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs
  57. 2
      src/Windows/Avalonia.Win32/Win32StorageProvider.cs
  58. 6
      src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs
  59. 10
      src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs
  60. 35
      tests/Avalonia.IntegrationTests.Appium/SliderTests.cs
  61. 81
      tests/Avalonia.RenderTests/Controls/AdornerTests.cs
  62. BIN
      tests/TestFiles/Direct2D1/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped_Clip_False.expected.png
  63. 0
      tests/TestFiles/Direct2D1/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped_Clip_True.expected.png
  64. BIN
      tests/TestFiles/Skia/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped_Clip_False.expected.png
  65. 0
      tests/TestFiles/Skia/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped_Clip_True.expected.png

11
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<ControlCatalog.App>()
.UseBlazor()
// .With(new SkiaOptions { CustomGpuFactory = null }) // uncomment to disable GPU/GL rendering
.SetupWithSingleViewLifetime();
base.OnParametersSet();
}
}

12
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<ControlCatalog.App>()
.StartBlazorAppAsync();
}
public static WebAssemblyHostBuilder CreateHostBuilder(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);

20
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<App>();
}

3
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!"]);

51
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@ -40,7 +40,7 @@ namespace ControlCatalog.Pages
if (Enum.TryParse<WellKnownFolder>(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<FilePickerFileType>
{
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<IStorageItem>? 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<char>.Shared.Rent(length);
try
{
var charsRead = await reader.ReadAsync(buffer, 0, length);
resultText += new string(buffer, 0, charsRead);
}
finally
{
ArrayPool<char>.Shared.Return(buffer);
}
// 4GB file test, shouldn't load more than 10000 chars into a memory.
const int length = 10000;
var buffer = ArrayPool<char>.Shared.Rent(length);
try
{
var charsRead = await reader.ReadAsync(buffer, 0, length);
resultText += new string(buffer, 0, charsRead);
}
finally
{
ArrayPool<char>.Shared.Return(buffer);
}
}

3
samples/IntegrationTestApp/MainWindow.axaml

@ -153,6 +153,9 @@
</StackPanel>
</Grid>
</TabItem>
<TabItem Header="SliderTab">
<Slider VerticalAlignment="Top" Name="Slider" Value="30"/>
</TabItem>
</TabControl>
</DockPanel>
</Window>

6
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<Stream> OpenReadAsync() => Task.FromResult(OpenContentStream(Activity, Uri, false)
?? throw new InvalidOperationException("Failed to open content stream"));

6
src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs

@ -37,7 +37,7 @@ internal class AndroidStorageProvider : IStorageProvider
return Task.FromResult<IStorageBookmarkFolder?>(new AndroidStorageFolder(_activity, uri, false));
}
public async Task<IStorageFile?> TryGetFileFromPath(Uri filePath)
public async Task<IStorageFile?> TryGetFileFromPathAsync(Uri filePath)
{
if (filePath is null)
{
@ -70,7 +70,7 @@ internal class AndroidStorageProvider : IStorageProvider
return new AndroidStorageFile(_activity, androidUri);
}
public async Task<IStorageFolder?> TryGetFolderFromPath(Uri folderPath)
public async Task<IStorageFolder?> TryGetFolderFromPathAsync(Uri folderPath)
{
if (folderPath is null)
{
@ -103,7 +103,7 @@ internal class AndroidStorageProvider : IStorageProvider
return new AndroidStorageFolder(_activity, androidUri, false);
}
public Task<IStorageFolder?> TryGetWellKnownFolder(WellKnownFolder wellKnownFolder)
public Task<IStorageFolder?> TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder)
{
var dirCode = wellKnownFolder switch
{

10
src/Avalonia.Base/AvaloniaObject.cs

@ -664,14 +664,12 @@ namespace Avalonia
/// <param name="property">The property that has changed.</param>
/// <param name="oldValue">The old property value.</param>
/// <param name="newValue">The new property value.</param>
/// <param name="priority">The priority of the binding that produced the value.</param>
protected void RaisePropertyChanged<T>(
DirectPropertyBase<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority = BindingPriority.LocalValue)
T oldValue,
T newValue)
{
RaisePropertyChanged(property, oldValue, newValue, priority, true);
RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue, true);
}
/// <summary>
@ -720,7 +718,7 @@ namespace Avalonia
/// <returns>
/// True if the value changed, otherwise false.
/// </returns>
protected bool SetAndRaise<T>(AvaloniaProperty<T> property, ref T field, T value)
protected bool SetAndRaise<T>(DirectPropertyBase<T> property, ref T field, T value)
{
VerifyAccess();

27
src/Avalonia.Base/Layout/LayoutInformation.cs

@ -0,0 +1,27 @@
namespace Avalonia.Layout;
/// <summary>
/// Provides access to layout information of a control.
/// </summary>
public static class LayoutInformation
{
/// <summary>
/// Gets the available size constraint passed in the previous layout pass.
/// </summary>
/// <param name="control">The control.</param>
/// <returns>Previous control measure constraint, if any.</returns>
public static Size? GetPreviousMeasureConstraint(Layoutable control)
{
return control.PreviousMeasure;
}
/// <summary>
/// Gets the control bounds used in the previous layout arrange pass.
/// </summary>
/// <param name="control">The control.</param>
/// <returns>Previous control arrange bounds, if any.</returns>
public static Rect? GetPreviousArrangeBounds(Layoutable control)
{
return control.PreviousArrange;
}
}

3
src/Avalonia.Base/Layout/Layoutable.cs

@ -323,6 +323,9 @@ namespace Avalonia.Layout
set { SetValue(UseLayoutRoundingProperty, value); }
}
/// <summary>
/// Gets the available size passed in the previous layout pass, if any.
/// </summary>
internal Size? PreviousMeasure => _previousMeasure;
/// <summary>

2
src/Avalonia.Base/Platform/DefaultPlatformSettings.cs

@ -37,7 +37,7 @@ namespace Avalonia.Platform
};
}
public event EventHandler<PlatformColorValues>? ColorValuesChanged;
public virtual event EventHandler<PlatformColorValues>? ColorValuesChanged;
protected void OnColorValuesChanged(PlatformColorValues colorValues)
{

6
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;

6
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs

@ -34,7 +34,7 @@ internal abstract class BclStorageProvider : IStorageProvider
: Task.FromResult<IStorageBookmarkFolder?>(null);
}
public virtual Task<IStorageFile?> TryGetFileFromPath(Uri filePath)
public virtual Task<IStorageFile?> TryGetFileFromPathAsync(Uri filePath)
{
if (filePath.IsAbsoluteUri)
{
@ -48,7 +48,7 @@ internal abstract class BclStorageProvider : IStorageProvider
return Task.FromResult<IStorageFile?>(null);
}
public virtual Task<IStorageFolder?> TryGetFolderFromPath(Uri folderPath)
public virtual Task<IStorageFolder?> TryGetFolderFromPathAsync(Uri folderPath)
{
if (folderPath.IsAbsoluteUri)
{
@ -62,7 +62,7 @@ internal abstract class BclStorageProvider : IStorageProvider
return Task.FromResult<IStorageFolder?>(null);
}
public virtual Task<IStorageFolder?> TryGetWellKnownFolder(WellKnownFolder wellKnownFolder)
public virtual Task<IStorageFolder?> TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder)
{
// Note, this BCL API returns different values depending on the .NET version.
// We should also document it.

16
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 "*.*".
/// </summary>
/// <remarks>
/// Used on Windows and Linux systems.
/// Used on Windows, Linux and Browser platforms.
/// </remarks>
public IReadOnlyList<string>? Patterns { get; set; }
@ -29,7 +31,7 @@ public sealed class FilePickerFileType
/// List of extensions in MIME format.
/// </summary>
/// <remarks>
/// Used on Android, Browser and Linux systems.
/// Used on Android, Linux and Browser platforms.
/// </remarks>
public IReadOnlyList<string>? MimeTypes { get; set; }
@ -41,4 +43,14 @@ public sealed class FilePickerFileType
/// See https://developer.apple.com/documentation/uniformtypeidentifiers/system_declared_uniform_type_identifiers.
/// </remarks>
public IReadOnlyList<string>? AppleUniformTypeIdentifiers { get; set; }
internal IReadOnlyList<string>? 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()!;
}
}

12
src/Avalonia.Base/Platform/Storage/IStorageFile.cs

@ -10,22 +10,12 @@ namespace Avalonia.Platform.Storage;
[NotClientImplementable]
public interface IStorageFile : IStorageItem
{
/// <summary>
/// Returns true, if file is readable.
/// </summary>
bool CanOpenRead { get; }
/// <summary>
/// Opens a stream for read access.
/// </summary>
/// <exception cref="System.UnauthorizedAccessException" />
Task<Stream> OpenReadAsync();
/// <summary>
/// Returns true, if file is writeable.
/// </summary>
bool CanOpenWrite { get; }
/// <summary>
/// Opens stream for writing to the file.
/// </summary>

6
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.
/// </remarks>
/// <returns>File or null if it doesn't exist.</returns>
Task<IStorageFile?> TryGetFileFromPath(Uri filePath);
Task<IStorageFile?> TryGetFileFromPathAsync(Uri filePath);
/// <summary>
/// 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.
/// </remarks>
/// <returns>Folder or null if it doesn't exist.</returns>
Task<IStorageFolder?> TryGetFolderFromPath(Uri folderPath);
Task<IStorageFolder?> TryGetFolderFromPathAsync(Uri folderPath);
/// <summary>
/// Attempts to read folder from the file-system by its path
/// </summary>
/// <param name="wellKnownFolder">Well known folder identifier.</param>
/// <returns>Folder or null if it doesn't exist.</returns>
Task<IStorageFolder?> TryGetWellKnownFolder(WellKnownFolder wellKnownFolder);
Task<IStorageFolder?> TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder);
}

45
src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs

@ -8,48 +8,47 @@ namespace Avalonia.Platform.Storage;
/// </summary>
public static class StorageProviderExtensions
{
/// <inheritdoc cref="IStorageProvider.TryGetFileFromPath"/>
public static Task<IStorageFile?> TryGetFileFromPath(this IStorageProvider provider, string filePath)
/// <inheritdoc cref="IStorageProvider.TryGetFileFromPathAsync"/>
public static Task<IStorageFile?> TryGetFileFromPathAsync(this IStorageProvider provider, string filePath)
{
return provider.TryGetFileFromPath(StorageProviderHelpers.FilePathToUri(filePath));
return provider.TryGetFileFromPathAsync(StorageProviderHelpers.FilePathToUri(filePath));
}
/// <inheritdoc cref="IStorageProvider.TryGetFolderFromPath"/>
public static Task<IStorageFolder?> TryGetFolderFromPath(this IStorageProvider provider, string folderPath)
/// <inheritdoc cref="IStorageProvider.TryGetFolderFromPathAsync"/>
public static Task<IStorageFolder?> 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)
/// <summary>
/// Gets the local file system path of the item as a string.
/// </summary>
/// <param name="item">Storage folder or file.</param>
/// <returns>Full local path to the folder or file if possible, otherwise null.</returns>
/// <remarks>
/// 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.
/// </remarks>
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;
}
}

5
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@ -48,7 +48,8 @@ namespace Avalonia.Rendering.Composition.Server
{
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);
@ -74,7 +75,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();

8
src/Avalonia.Base/StyledElement.cs

@ -524,13 +524,7 @@ namespace Avalonia
NotifyResourcesChanged();
}
#nullable disable
RaisePropertyChanged(
ParentProperty,
new Optional<StyledElement>(old),
new BindingValue<StyledElement>(Parent),
BindingPriority.LocalValue);
#nullable enable
RaisePropertyChanged(ParentProperty, old, Parent);
}
}

2
src/Avalonia.Base/Visual.cs

@ -573,7 +573,7 @@ namespace Avalonia
/// <param name="newParent">The new visual parent.</param>
protected virtual void OnVisualParentChanged(Visual? oldParent, Visual? newParent)
{
RaisePropertyChanged(VisualParentProperty, oldParent, newParent, BindingPriority.LocalValue);
RaisePropertyChanged(VisualParentProperty, oldParent, newParent);
}
internal override ParametrizedLogger? GetBindingWarningLogger(

1
src/Avalonia.Base/composition-schema.xml

@ -26,6 +26,7 @@
<Property Name="Scale" Type="Vector3" DefaultValue="new Vector3(1, 1, 1)" Animated="true"/>
<Property Name="TransformMatrix" Type="Matrix4x4" DefaultValue="Matrix4x4.Identity" Animated="true"/>
<Property Name="AdornedVisual" Type="CompositionVisual?" Internal="true" />
<Property Name="AdornerIsClipped" Type="bool" Internal="true" />
<Property Name="OpacityMaskBrush" Type="Avalonia.Media.IBrush?" Internal="true" />
</Object>
<Object Name="CompositionContainerVisual" Inherits="CompositionVisual"/>

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

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

5
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)

10
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -345,10 +345,7 @@ namespace Avalonia.Controls.Primitives
if (_oldSelectedItems != SelectedItems)
{
RaisePropertyChanged(
SelectedItemsProperty,
new Optional<IList?>(_oldSelectedItems),
new BindingValue<IList?>(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<IList?>(_oldSelectedItems),
new BindingValue<IList?>(SelectedItems));
RaisePropertyChanged(SelectedItemsProperty, _oldSelectedItems, SelectedItems);
_oldSelectedItems = SelectedItems;
}
else if (e.PropertyName == nameof(ISelectionModel.Source))

39
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<RelativePanel>(true);
AboveProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged);
AlignBottomWithPanelProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged);
AlignBottomWithProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged);
AlignHorizontalCenterWithPanelProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged);
AlignHorizontalCenterWithProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged);
AlignLeftWithPanelProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged);
AlignLeftWithProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged);
AlignRightWithPanelProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged);
AlignRightWithProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged);
AlignTopWithPanelProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged);
AlignTopWithProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged);
AlignVerticalCenterWithPanelProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged);
AlignVerticalCenterWithProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged);
BelowProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged);
LeftOfProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged);
RightOfProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged);
AffectsParentArrange<RelativePanel>(
AlignLeftWithPanelProperty, AlignLeftWithProperty, LeftOfProperty,
AlignRightWithPanelProperty, AlignRightWithProperty, RightOfProperty,
AlignTopWithPanelProperty, AlignTopWithProperty, AboveProperty,
AlignBottomWithPanelProperty, AlignBottomWithProperty, BelowProperty,
AlignHorizontalCenterWithPanelProperty, AlignHorizontalCenterWithProperty,
AlignVerticalCenterWithPanelProperty, AlignVerticalCenterWithProperty);
AffectsParentMeasure<RelativePanel>(
AlignLeftWithPanelProperty, AlignLeftWithProperty, LeftOfProperty,
AlignRightWithPanelProperty, AlignRightWithProperty, RightOfProperty,
AlignTopWithPanelProperty, AlignTopWithProperty, AboveProperty,
AlignBottomWithPanelProperty, AlignBottomWithProperty, BelowProperty,
AlignHorizontalCenterWithPanelProperty, AlignHorizontalCenterWithProperty,
AlignVerticalCenterWithPanelProperty, AlignVerticalCenterWithProperty);
}
/// <summary>

6
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);
}
/// <inheritdoc />
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{

8
src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs

@ -54,8 +54,8 @@ namespace Avalonia.Diagnostics.Screenshots
protected override async Task<Stream?> GetStream(Control control)
{
var storageProvider = GetTopLevel(control).StorageProvider;
var defaultFolder = await storageProvider.TryGetFolderFromPath(_screenshotRoot)
?? await storageProvider.TryGetWellKnownFolder(WellKnownFolder.Pictures);
var defaultFolder = await storageProvider.TryGetFolderFromPathAsync(_screenshotRoot)
?? await storageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Pictures);
var result = await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
{
@ -68,10 +68,6 @@ namespace Avalonia.Diagnostics.Screenshots
{
return null;
}
if (!result.CanOpenWrite)
{
throw new InvalidOperationException("Read-only file was selected.");
}
return await result.OpenWriteAsync();
}

2
src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs

@ -260,7 +260,7 @@ namespace Avalonia.Dialogs.Internal
public void Navigate(IStorageFolder path, string initialSelectionName = null)
{
var fullDirectoryPath = path?.TryGetFullPath() ?? Directory.GetCurrentDirectory();
var fullDirectoryPath = path?.TryGetLocalPath() ?? Directory.GetCurrentDirectory();
Navigate(fullDirectoryPath, initialSelectionName);
}

2
src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs

@ -51,7 +51,7 @@ namespace Avalonia.Dialogs
var files = await impl.OpenFilePickerAsync(dialog.ToFilePickerOpenOptions());
return files
.Select(file => file.TryGetFullPath() ?? file.Name)
.Select(file => file.TryGetLocalPath() ?? file.Name)
.ToArray();
}
}

2
src/Avalonia.FreeDesktop/DBusSystemDialog.cs

@ -88,7 +88,7 @@ namespace Avalonia.FreeDesktop
if (options.SuggestedFileName is { } currentName)
chooserOptions.Add("current_name", currentName);
if (options.SuggestedStartLocation?.TryGetFullPath() is { } folderPath)
if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath)
chooserOptions.Add("current_folder", Encoding.UTF8.GetBytes(folderPath));
objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions);

6
src/Avalonia.Native/SystemDialogs.cs

@ -33,7 +33,7 @@ namespace Avalonia.Native
{
using var events = new SystemDialogEvents();
var suggestedDirectory = options.SuggestedStartLocation?.TryGetFullPath() ?? string.Empty;
var suggestedDirectory = options.SuggestedStartLocation?.TryGetLocalPath() ?? string.Empty;
_native.OpenFileDialog((IAvnWindow)_window.Native,
events,
@ -53,7 +53,7 @@ namespace Avalonia.Native
{
using var events = new SystemDialogEvents();
var suggestedDirectory = options.SuggestedStartLocation?.TryGetFullPath() ?? string.Empty;
var suggestedDirectory = options.SuggestedStartLocation?.TryGetLocalPath() ?? string.Empty;
_native.SaveFileDialog((IAvnWindow)_window.Native,
events,
@ -72,7 +72,7 @@ namespace Avalonia.Native
{
using var events = new SystemDialogEvents();
var suggestedDirectory = options.SuggestedStartLocation?.TryGetFullPath() ?? string.Empty;
var suggestedDirectory = options.SuggestedStartLocation?.TryGetLocalPath() ?? string.Empty;
_native.SelectFolderDialog((IAvnWindow)_window.Native, events, options.AllowMultiple.AsComBool(), options.Title ?? "", suggestedDirectory);

12
src/Avalonia.X11/NativeDialogs/CompositeStorageProvider.cs

@ -62,21 +62,21 @@ internal class CompositeStorageProvider : IStorageProvider
return await provider.OpenFolderBookmarkAsync(bookmark).ConfigureAwait(false);
}
public async Task<IStorageFile?> TryGetFileFromPath(Uri filePath)
public async Task<IStorageFile?> TryGetFileFromPathAsync(Uri filePath)
{
var provider = await EnsureStorageProvider().ConfigureAwait(false);
return await provider.TryGetFileFromPath(filePath).ConfigureAwait(false);
return await provider.TryGetFileFromPathAsync(filePath).ConfigureAwait(false);
}
public async Task<IStorageFolder?> TryGetFolderFromPath(Uri folderPath)
public async Task<IStorageFolder?> TryGetFolderFromPathAsync(Uri folderPath)
{
var provider = await EnsureStorageProvider().ConfigureAwait(false);
return await provider.TryGetFolderFromPath(folderPath).ConfigureAwait(false);
return await provider.TryGetFolderFromPathAsync(folderPath).ConfigureAwait(false);
}
public async Task<IStorageFolder?> TryGetWellKnownFolder(WellKnownFolder wellKnownFolder)
public async Task<IStorageFolder?> TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder)
{
var provider = await EnsureStorageProvider().ConfigureAwait(false);
return await provider.TryGetWellKnownFolder(wellKnownFolder).ConfigureAwait(false);
return await provider.TryGetWellKnownFolderAsync(wellKnownFolder).ConfigureAwait(false);
}
}

2
src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs

@ -196,7 +196,7 @@ namespace Avalonia.X11.NativeDialogs
gtk_dialog_add_button(dlg, open, GtkResponseType.Cancel);
}
var folderLocalPath = initialFolder?.TryGetFullPath();
var folderLocalPath = initialFolder?.TryGetLocalPath();
if (folderLocalPath is not null)
{
using var dir = new Utf8Buffer(folderLocalPath);

14
src/Browser/Avalonia.Browser.Blazor/AvaloniaView.cs

@ -30,12 +30,10 @@ public class AvaloniaView : ComponentBase
builder.CloseElement();
}
protected override async Task OnInitializedAsync()
protected override void OnAfterRender(bool firstRender)
{
if (OperatingSystem.IsBrowser())
if (firstRender)
{
await AvaloniaModule.ImportMain();
_browserView = new Browser.AvaloniaView(_containerId);
if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime lifetime)
{
@ -43,4 +41,12 @@ public class AvaloniaView : ComponentBase
}
}
}
protected override void OnInitialized()
{
if (!OperatingSystem.IsBrowser())
{
throw new NotSupportedException("Avalonia doesn't support server-side Blazor");
}
}
}

37
src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs

@ -1,33 +1,28 @@
using System.Runtime.Versioning;
using System;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Browser.Interop;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
namespace Avalonia.Browser.Blazor;
public static class WebAppBuilder
public static class BlazorAppBuilder
{
public static AppBuilder SetupWithSingleViewLifetime(
this AppBuilder builder)
/// <summary>
/// Configures blazor backend, loads avalonia javascript modules and creates a single view lifetime.
/// </summary>
/// <param name="builder">Application builder.</param>
/// <param name="options">Browser backend specific options.</param>
public static async Task StartBlazorAppAsync(this AppBuilder builder, BrowserPlatformOptions? options = null)
{
return builder.SetupWithLifetime(new BlazorSingleViewLifetime());
}
options ??= new BrowserPlatformOptions();
options.FrameworkAssetPathResolver ??= filePath => $"/_content/Avalonia.Browser.Blazor/{filePath}";
public static AppBuilder UseBlazor(this AppBuilder builder)
{
return builder
.UseBrowser()
.With(new BrowserPlatformOptions
{
FrameworkAssetPathResolver = new(filePath => $"/_content/Avalonia.Browser.Blazor/{filePath}")
});
}
builder = await BrowserAppBuilder.PreSetupBrowser(builder, options);
public static AppBuilder Configure<TApp>()
where TApp : Application, new()
{
return AppBuilder.Configure<TApp>()
.UseBlazor();
builder.SetupWithLifetime(new BlazorSingleViewLifetime());
}
internal class BlazorSingleViewLifetime : ISingleViewApplicationLifetime

10
src/Browser/Avalonia.Browser/AvaloniaView.cs

@ -20,7 +20,7 @@ using static System.Runtime.CompilerServices.RuntimeHelpers;
namespace Avalonia.Browser
{
public partial class AvaloniaView : ITextInputMethodImpl
public class AvaloniaView : ITextInputMethodImpl
{
private static readonly PooledList<RawPointerPoint> s_intermediatePointsPooledList = new(ClearMode.Never);
private readonly BrowserTopLevelImpl _topLevelImpl;
@ -43,8 +43,9 @@ namespace Avalonia.Browser
private bool _useGL;
private ITextInputMethodClient? _client;
/// <param name="divId">ID of the html element where avalonia content should be rendered.</param>
public AvaloniaView(string divId)
: this(DomHelper.GetElementById(divId) ?? throw new Exception($"Element with id {divId} was not found in the html document."))
: this(DomHelper.GetElementById(divId) ?? throw new Exception($"Element with id '{divId}' was not found in the html document."))
{
}
@ -380,12 +381,10 @@ namespace Avalonia.Browser
{
if (_useGL && (_jsGlInfo == null))
{
Console.WriteLine("nothing to render");
return;
}
if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0)
{
Console.WriteLine("nothing to render");
return;
}
@ -458,7 +457,6 @@ namespace Avalonia.Browser
void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client)
{
Console.WriteLine("Set Client");
if (_client != null)
{
_client.SurroundingTextChanged -= SurroundingTextChanged;
@ -481,8 +479,6 @@ namespace Avalonia.Browser
var surroundingText = _client.SurroundingText;
InputHelper.SetSurroundingText(_inputElement, surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset);
Console.WriteLine("Shown, focused and surrounded.");
}
else
{

83
src/Browser/Avalonia.Browser/BrowserAppBuilder.cs

@ -0,0 +1,83 @@
using System;
using System.Threading.Tasks;
using Avalonia.Browser.Interop;
namespace Avalonia.Browser;
public class BrowserPlatformOptions
{
/// <summary>
/// Defines paths where avalonia modules and service locator should be resolved.
/// If null, default path resolved depending on the backend (browser or blazor) is used.
/// </summary>
public Func<string, string>? FrameworkAssetPathResolver { get; set; }
}
public static class BrowserAppBuilder
{
/// <summary>
/// Configures browser backend, loads avalonia javascript modules and creates a single view lifetime from the passed <see cref="mainDivId"/> parameter.
/// </summary>
/// <param name="builder">Application builder.</param>
/// <param name="mainDivId">ID of the html element where avalonia content should be rendered.</param>
/// <param name="options">Browser backend specific options.</param>
public static async Task StartBrowserAppAsync(this AppBuilder builder, string mainDivId, BrowserPlatformOptions? options = null)
{
if (mainDivId is null)
{
throw new ArgumentNullException(nameof(mainDivId));
}
builder = await PreSetupBrowser(builder, options);
var lifetime = new BrowserSingleViewLifetime();
builder
.AfterSetup(_ =>
{
lifetime.View = new AvaloniaView(mainDivId);
})
.SetupWithLifetime(lifetime);
}
/// <summary>
/// Loads avalonia javascript modules and configures browser backend.
/// </summary>
/// <param name="builder">Application builder.</param>
/// <param name="options">Browser backend specific options.</param>
/// <remarks>
/// This method doesn't creates any avalonia views to be rendered. To do so create an <see cref="AvaloniaView"/> object.
/// Alternatively, you can call <see cref="StartBrowserAppAsync"/> method instead of <see cref="SetupBrowserAppAsync"/>.
/// </remarks>
public static async Task SetupBrowserAppAsync(this AppBuilder builder, BrowserPlatformOptions? options = null)
{
builder = await PreSetupBrowser(builder, options);
builder
.SetupWithoutStarting();
}
internal static async Task<AppBuilder> PreSetupBrowser(AppBuilder builder, BrowserPlatformOptions? options)
{
options ??= new BrowserPlatformOptions();
options.FrameworkAssetPathResolver ??= fileName => $"./{fileName}";
AvaloniaLocator.CurrentMutable.Bind<BrowserPlatformOptions>().ToConstant(options);
await AvaloniaModule.ImportMain();
if (builder.WindowingSubsystemInitializer is null)
{
builder = builder.UseBrowser();
}
return builder;
}
public static AppBuilder UseBrowser(
this AppBuilder builder)
{
return builder
.UseWindowingSubsystem(BrowserWindowingPlatform.Register)
.UseSkia();
}
}

40
src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs

@ -1,4 +1,5 @@
using Avalonia.Browser.Interop;
using System;
using Avalonia.Browser.Interop;
using Avalonia.Platform;
namespace Avalonia.Browser;
@ -7,25 +8,44 @@ internal class BrowserPlatformSettings : DefaultPlatformSettings
{
private bool _isDarkMode;
private bool _isHighContrast;
public BrowserPlatformSettings()
private bool _isInitialized;
public override event EventHandler<PlatformColorValues>? ColorValuesChanged
{
var obj = DomHelper.ObserveDarkMode((isDarkMode, isHighContrast) =>
add
{
_isDarkMode = isDarkMode;
_isHighContrast = isHighContrast;
OnColorValuesChanged(GetColorValues());
});
_isDarkMode = obj.GetPropertyAsBoolean("isDarkMode");
_isHighContrast = obj.GetPropertyAsBoolean("isHighContrast");
EnsureBackend();
base.ColorValuesChanged += value;
}
remove => base.ColorValuesChanged -= value;
}
public override PlatformColorValues GetColorValues()
{
EnsureBackend();
return base.GetColorValues() with
{
ThemeVariant = _isDarkMode ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light,
ContrastPreference = _isHighContrast ? ColorContrastPreference.High : ColorContrastPreference.NoPreference
};
}
private void EnsureBackend()
{
if (!_isInitialized)
{
// WASM module has async nature of initialization. We can't native code right away during components registration.
_isInitialized = true;
var obj = DomHelper.ObserveDarkMode((isDarkMode, isHighContrast) =>
{
_isDarkMode = isDarkMode;
_isHighContrast = isHighContrast;
OnColorValuesChanged(GetColorValues());
});
_isDarkMode = obj.GetPropertyAsBoolean("isDarkMode");
_isHighContrast = obj.GetPropertyAsBoolean("isHighContrast");
}
}
}

51
src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs

@ -1,47 +1,36 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using System.Runtime.Versioning;
using Avalonia.Browser;
namespace Avalonia.Browser;
namespace Avalonia;
public class BrowserSingleViewLifetime : ISingleViewApplicationLifetime
internal class BrowserSingleViewLifetime : ISingleViewApplicationLifetime
{
public AvaloniaView? View;
public Control? MainView
{
get => View!.Content;
set => View!.Content = value;
}
}
public class BrowserPlatformOptions
{
public Func<string, string> FrameworkAssetPathResolver { get; set; } = new(fileName => $"./{fileName}");
}
public static class WebAppBuilder
{
public static AppBuilder SetupBrowserApp(
this AppBuilder builder, string mainDivId)
{
var lifetime = new BrowserSingleViewLifetime();
return builder
.UseBrowser()
.AfterSetup(b =>
{
lifetime.View = new AvaloniaView(mainDivId);
})
.SetupWithLifetime(lifetime);
get
{
EnsureView();
return View.Content;
}
set
{
EnsureView();
View.Content = value;
}
}
public static AppBuilder UseBrowser(
this AppBuilder builder)
[MemberNotNull(nameof(View))]
private void EnsureView()
{
return builder
.UseWindowingSubsystem(BrowserWindowingPlatform.Register)
.UseSkia();
if (View is null)
{
throw new InvalidOperationException("Browser lifetime was not initialized. Make sure AppBuilder.StartBrowserApp was called.");
}
}
}

4
src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs

@ -11,13 +11,13 @@ internal static partial class AvaloniaModule
public static Task ImportMain()
{
var options = AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() ?? new BrowserPlatformOptions();
return JSHost.ImportAsync(MainModuleName, options.FrameworkAssetPathResolver("avalonia.js"));
return JSHost.ImportAsync(MainModuleName, options.FrameworkAssetPathResolver!("avalonia.js"));
}
public static Task ImportStorage()
{
var options = AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() ?? new BrowserPlatformOptions();
return JSHost.ImportAsync(StorageModuleName, options.FrameworkAssetPathResolver("storage.js"));
return JSHost.ImportAsync(StorageModuleName, options.FrameworkAssetPathResolver!("storage.js"));
}
[JSImport("Caniuse.isMobile", AvaloniaModule.MainModuleName)]

12
src/Browser/Avalonia.Browser/Interop/StorageHelper.cs

@ -5,14 +5,8 @@ namespace Avalonia.Browser.Interop;
internal static partial class StorageHelper
{
[JSImport("Caniuse.canShowOpenFilePicker", AvaloniaModule.MainModuleName)]
public static partial bool CanShowOpenFilePicker();
[JSImport("Caniuse.canShowSaveFilePicker", AvaloniaModule.MainModuleName)]
public static partial bool CanShowSaveFilePicker();
[JSImport("Caniuse.canShowDirectoryPicker", AvaloniaModule.MainModuleName)]
public static partial bool CanShowDirectoryPicker();
[JSImport("Caniuse.hasNativeFilePicker", AvaloniaModule.MainModuleName)]
public static partial bool HasNativeFilePicker();
[JSImport("StorageProvider.selectFolderDialog", AvaloniaModule.StorageModuleName)]
public static partial Task<JSObject?> SelectFolderDialog(JSObject? startIn);
@ -54,5 +48,5 @@ internal static partial class StorageHelper
public static partial JSObject[] ItemsArray(JSObject item);
[JSImport("StorageProvider.createAcceptType", AvaloniaModule.StorageModuleName)]
public static partial JSObject CreateAcceptType(string description, string[] mimeTypes);
public static partial JSObject CreateAcceptType(string description, string[] mimeTypes, string[]? extensions);
}

28
src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs

@ -20,9 +20,9 @@ internal class BrowserStorageProvider : IStorageProvider
private readonly Lazy<Task> _lazyModule = new(() => AvaloniaModule.ImportStorage());
public bool CanOpen => StorageHelper.CanShowOpenFilePicker();
public bool CanSave => StorageHelper.CanShowSaveFilePicker();
public bool CanPickFolder => StorageHelper.CanShowDirectoryPicker();
public bool CanOpen => true;
public bool CanSave => StorageHelper.HasNativeFilePicker();
public bool CanPickFolder => true;
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
@ -116,17 +116,17 @@ internal class BrowserStorageProvider : IStorageProvider
return item is not null ? new JSStorageFolder(item) : null;
}
public Task<IStorageFile?> TryGetFileFromPath(Uri filePath)
public Task<IStorageFile?> TryGetFileFromPathAsync(Uri filePath)
{
return Task.FromResult<IStorageFile?>(null);
}
public Task<IStorageFolder?> TryGetFolderFromPath(Uri folderPath)
public Task<IStorageFolder?> TryGetFolderFromPathAsync(Uri folderPath)
{
return Task.FromResult<IStorageFolder?>(null);
}
public async Task<IStorageFolder?> TryGetWellKnownFolder(WellKnownFolder wellKnownFolder)
public async Task<IStorageFolder?> TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder)
{
await _lazyModule.Value;
var directory = StorageHelper.CreateWellKnownDirectory(wellKnownFolder switch
@ -147,7 +147,7 @@ internal class BrowserStorageProvider : IStorageProvider
{
var types = input?
.Where(t => t.MimeTypes?.Any() == true && t != FilePickerFileTypes.All)
.Select(t => StorageHelper.CreateAcceptType(t.Name, t.MimeTypes!.ToArray()))
.Select(t => StorageHelper.CreateAcceptType(t.Name, t.MimeTypes!.ToArray(), t.TryGetExtensions()?.ToArray()))
.ToArray();
if (types?.Length == 0)
{
@ -186,10 +186,15 @@ internal abstract class JSStorageItem : IStorageBookmarkItem
dateModified: lastModified > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(lastModified.Value) : null);
}
public bool CanBookmark => true;
public bool CanBookmark => StorageHelper.HasNativeFilePicker();
public Task<string?> SaveBookmarkAsync()
{
if (!CanBookmark)
{
return Task.FromResult<string?>(null);
}
return StorageHelper.SaveBookmark(FileHandle);
}
@ -200,6 +205,11 @@ internal abstract class JSStorageItem : IStorageBookmarkItem
public Task ReleaseBookmarkAsync()
{
if (!CanBookmark)
{
return Task.CompletedTask;
}
return StorageHelper.DeleteBookmark(FileHandle);
}
@ -216,7 +226,6 @@ internal class JSStorageFile : JSStorageItem, IStorageBookmarkFile
{
}
public bool CanOpenRead => true;
public async Task<Stream> OpenReadAsync()
{
try
@ -230,7 +239,6 @@ internal class JSStorageFile : JSStorageItem, IStorageBookmarkFile
}
}
public bool CanOpenWrite => true;
public async Task<Stream> OpenWriteAsync()
{
try

2
src/Browser/Avalonia.Browser/webapp/.eslintrc.json

@ -43,5 +43,5 @@
}
]
},
"ignorePatterns": ["types/*"]
"ignorePatterns": ["types/*","node_modules/*"]
}

2
src/Browser/Avalonia.Browser/webapp/build.js

@ -7,7 +7,7 @@ require("esbuild").build({
bundle: true,
minify: true,
format: "esm",
target: "es2016",
target: "es2018",
platform: "browser",
sourcemap: "linked",
loader: { ".ts": "ts" }

18
src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts

@ -1,4 +1,3 @@
import { RuntimeAPI } from "../types/dotnet";
import { SizeWatcher, DpiWatcher, Canvas } from "./avalonia/canvas";
import { InputHelper } from "./avalonia/input";
import { AvaloniaDOM } from "./avalonia/dom";
@ -7,19 +6,6 @@ import { StreamHelper } from "./avalonia/stream";
import { NativeControlHost } from "./avalonia/nativeControlHost";
import { NavigationHelper } from "./avalonia/navigationHelper";
async function registerAvaloniaModule(api: RuntimeAPI): Promise<void> {
api.setModuleImports("avalonia", {
Caniuse,
Canvas,
InputHelper,
SizeWatcher,
DpiWatcher,
AvaloniaDOM,
StreamHelper,
NativeControlHost,
NavigationHelper
});
}
export {
Caniuse,
Canvas,
@ -29,7 +15,5 @@ export {
AvaloniaDOM,
StreamHelper,
NativeControlHost,
NavigationHelper,
registerAvaloniaModule
NavigationHelper
};

12
src/Browser/Avalonia.Browser/webapp/modules/avalonia/caniuse.ts

@ -1,14 +1,6 @@
export class Caniuse {
public static canShowOpenFilePicker(): boolean {
return typeof globalThis.showOpenFilePicker !== "undefined";
}
public static canShowSaveFilePicker(): boolean {
return typeof globalThis.showSaveFilePicker !== "undefined";
}
public static canShowDirectoryPicker(): boolean {
return typeof globalThis.showDirectoryPicker !== "undefined";
public static hasNativeFilePicker(): boolean {
return "showSaveFilePicker" in globalThis;
}
public static isMobile(): boolean {

3
src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts

@ -1,3 +1,4 @@
import FileSystemWritableFileStream from "native-file-system-adapter/types/src/FileSystemWritableFileStream";
import { IMemoryView } from "../../types/dotnet";
export class StreamHelper {
@ -17,7 +18,7 @@ export class StreamHelper {
const array = new Uint8Array(span.byteLength);
span.copyTo(array);
const data: WriteParams = {
const data = {
type: "write",
data: array
};

50
src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts

@ -1,8 +1,10 @@
import { avaloniaDb, fileBookmarksStore } from "./indexedDb";
import { FileSystemFileHandle, FileSystemDirectoryHandle, FileSystemWritableFileStream } from "native-file-system-adapter";
import { Caniuse } from "../avalonia";
export class StorageItem {
constructor(
public handle?: FileSystemHandle,
public handle?: FileSystemFileHandle | FileSystemDirectoryHandle,
private readonly bookmarkId?: string,
public wellKnownType?: WellKnownDirectory
) {
@ -27,39 +29,44 @@ export class StorageItem {
}
public static async openRead(item: StorageItem): Promise<Blob> {
if (!(item.handle instanceof FileSystemFileHandle)) {
if (!item.handle || item.kind !== "file") {
throw new Error("StorageItem is not a file");
}
await item.verityPermissions("read");
const file = await item.handle.getFile();
const file = await (item.handle as FileSystemFileHandle).getFile();
return file;
}
public static async openWrite(item: StorageItem): Promise<FileSystemWritableFileStream> {
if (!(item.handle instanceof FileSystemFileHandle)) {
if (!item.handle || item.kind !== "file") {
throw new Error("StorageItem is not a file");
}
await item.verityPermissions("readwrite");
return await item.handle.createWritable({ keepExistingData: true });
return await (item.handle as FileSystemFileHandle).createWritable({ keepExistingData: true });
}
public static async getProperties(item: StorageItem): Promise<{ Size: number; LastModified: number; Type: string } | null> {
const file = item.handle instanceof FileSystemFileHandle &&
await item.handle.getFile();
if (!file) {
// getFile can fail with an exception depending if we use polyfill with a save file dialog or not.
try {
const file = item.handle instanceof FileSystemFileHandle &&
await item.handle.getFile();
if (!file) {
return null;
}
return {
Size: file.size,
LastModified: file.lastModified,
Type: file.type
};
} catch {
return null;
}
return {
Size: file.size,
LastModified: file.lastModified,
Type: file.type
};
}
public static async getItems(item: StorageItem): Promise<StorageItems> {
@ -74,11 +81,16 @@ export class StorageItem {
return new StorageItems(items);
}
private async verityPermissions(mode: FileSystemPermissionMode): Promise<void | never> {
private async verityPermissions(mode: "read" | "readwrite"): Promise<void | never> {
if (!this.handle) {
return;
}
// If we are using polyfill, let it decide permissions by itself, we can't request anything in this case.
if (!Caniuse.hasNativeFilePicker()) {
return;
}
if (await this.handle.queryPermission({ mode }) === "granted") {
return;
}
@ -93,7 +105,9 @@ export class StorageItem {
if (item.bookmarkId) {
return item.bookmarkId;
}
if (!item.handle) {
// Bookmarks are not supported with polyfill.
if (!item.handle || !Caniuse.hasNativeFilePicker()) {
return null;
}
@ -107,7 +121,7 @@ export class StorageItem {
}
public static async deleteBookmark(item: StorageItem): Promise<void> {
if (!item.bookmarkId) {
if (!item.bookmarkId || !Caniuse.hasNativeFilePicker()) {
return;
}

29
src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts

@ -1,14 +1,12 @@
import { avaloniaDb, fileBookmarksStore } from "./indexedDb";
import { StorageItem, StorageItems } from "./storageItem";
import { showOpenFilePicker, showDirectoryPicker, FileSystemFileHandle } from "native-file-system-adapter";
declare global {
type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos";
type StartInDirectory = WellKnownDirectory | FileSystemHandle;
interface OpenFilePickerOptions {
startIn?: StartInDirectory;
}
interface SaveFilePickerOptions {
startIn?: StartInDirectory;
interface FilePickerAcceptType {
description?: string | undefined;
accept: Record<string, string | string[]>;
}
}
@ -16,39 +14,40 @@ export class StorageProvider {
public static async selectFolderDialog(
startIn: StorageItem | null): Promise<StorageItem> {
// 'Picker' API doesn't accept "null" as a parameter, so it should be set to undefined.
const options: DirectoryPickerOptions = {
const options = {
startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined)
};
const handle = await window.showDirectoryPicker(options);
const handle = await showDirectoryPicker(options as any);
return new StorageItem(handle);
}
public static async openFileDialog(
startIn: StorageItem | null, multiple: boolean,
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise<StorageItems> {
const options: OpenFilePickerOptions = {
const options = {
startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined),
multiple,
excludeAcceptAllOption,
types: (types ?? undefined)
};
const handles = await window.showOpenFilePicker(options);
return new StorageItems(handles.map((handle: FileSystemHandle) => new StorageItem(handle)));
const handles = await showOpenFilePicker(options);
return new StorageItems(handles.map((handle: FileSystemFileHandle) => new StorageItem(handle)));
}
public static async saveFileDialog(
startIn: StorageItem | null, suggestedName: string | null,
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise<StorageItem> {
const options: SaveFilePickerOptions = {
const options = {
startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined),
suggestedName: (suggestedName ?? undefined),
excludeAcceptAllOption,
types: (types ?? undefined)
};
const handle = await window.showSaveFilePicker(options);
// Always prefer native save file picker, as polyfill solutions are not reliable.
const handle = await (globalThis as any).showSaveFilePicker(options);
return new StorageItem(handle);
}
@ -62,9 +61,9 @@ export class StorageProvider {
}
}
public static createAcceptType(description: string, mimeTypes: string[]): FilePickerAcceptType {
public static createAcceptType(description: string, mimeTypes: string[], extensions: string[] | undefined): FilePickerAcceptType {
const accept: Record<string, string[]> = {};
mimeTypes.forEach(a => { accept[a] = []; });
mimeTypes.forEach(a => { accept[a] = extensions ?? []; });
return { description, accept };
}
}

117
src/Browser/Avalonia.Browser/webapp/package-lock.json

@ -5,9 +5,11 @@
"packages": {
"": {
"name": "avalonia.browser",
"dependencies": {
"native-file-system-adapter": "github:jimmywarting/native-file-system-adapter#d43ad841581c2cc3ce47bbd1e8f11950ebdff027"
},
"devDependencies": {
"@types/emscripten": "^1.39.6",
"@types/wicg-file-system-access": "^2020.9.5",
"@typescript-eslint/eslint-plugin": "^5.38.1",
"esbuild": "^0.15.7",
"eslint": "^8.24.0",
@ -170,12 +172,6 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true
},
"node_modules/@types/wicg-file-system-access": {
"version": "2020.9.5",
"resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.5.tgz",
"integrity": "sha512-UYK244awtmcUYQfs7FR8710MJcefL2WvkyHMjA8yJzxd1mo0Gfn88sRZ1Bls7hiUhA2w7ne1gpJ9T5g3G0wOyA==",
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.38.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.1.tgz",
@ -1573,6 +1569,29 @@
"reusify": "^1.0.4"
}
},
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"optional": true,
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@ -2289,6 +2308,27 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"node_modules/native-file-system-adapter": {
"version": "3.0.0",
"resolved": "git+ssh://git@github.com/jimmywarting/native-file-system-adapter.git#d43ad841581c2cc3ce47bbd1e8f11950ebdff027",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"engines": {
"node": ">=14.8.0"
},
"optionalDependencies": {
"fetch-blob": "^3.2.0"
}
},
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@ -2301,6 +2341,25 @@
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"optional": true,
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/normalize-package-data": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
@ -3196,6 +3255,15 @@
"spdx-expression-parse": "^3.0.0"
}
},
"node_modules/web-streams-polyfill": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
"integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==",
"optional": true,
"engines": {
"node": ">= 8"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -3366,12 +3434,6 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true
},
"@types/wicg-file-system-access": {
"version": "2020.9.5",
"resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.5.tgz",
"integrity": "sha512-UYK244awtmcUYQfs7FR8710MJcefL2WvkyHMjA8yJzxd1mo0Gfn88sRZ1Bls7hiUhA2w7ne1gpJ9T5g3G0wOyA==",
"dev": true
},
"@typescript-eslint/eslint-plugin": {
"version": "5.38.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.1.tgz",
@ -4275,6 +4337,16 @@
"reusify": "^1.0.4"
}
},
"fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"optional": true,
"requires": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
}
},
"file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@ -4796,6 +4868,13 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"native-file-system-adapter": {
"version": "git+ssh://git@github.com/jimmywarting/native-file-system-adapter.git#d43ad841581c2cc3ce47bbd1e8f11950ebdff027",
"from": "native-file-system-adapter@github:jimmywarting/native-file-system-adapter#d43ad841581c2cc3ce47bbd1e8f11950ebdff027",
"requires": {
"fetch-blob": "^3.2.0"
}
},
"natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@ -4808,6 +4887,12 @@
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true
},
"node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"optional": true
},
"normalize-package-data": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
@ -5446,6 +5531,12 @@
"spdx-expression-parse": "^3.0.0"
}
},
"web-streams-polyfill": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
"integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==",
"optional": true
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

4
src/Browser/Avalonia.Browser/webapp/package.json

@ -8,7 +8,6 @@
},
"devDependencies": {
"@types/emscripten": "^1.39.6",
"@types/wicg-file-system-access": "^2020.9.5",
"@typescript-eslint/eslint-plugin": "^5.38.1",
"esbuild": "^0.15.7",
"eslint": "^8.24.0",
@ -18,5 +17,8 @@
"eslint-plugin-promise": "^6.0.1",
"npm-run-all": "^4.1.5",
"typescript": "^4.8.3"
},
"dependencies": {
"native-file-system-adapter": "github:jimmywarting/native-file-system-adapter#d43ad841581c2cc3ce47bbd1e8f11950ebdff027"
}
}

6
src/Browser/Avalonia.Browser/webapp/tsconfig.json

@ -1,14 +1,16 @@
{
"compilerOptions": {
"target": "es2016",
"target": "es2018",
"module": "es2020",
"strict": true,
"sourceMap": true,
"noEmitOnError": true,
"moduleResolution": "node",
"skipLibCheck": true,
"isolatedModules": true, // we need it for esbuild
"lib": [
"dom",
"es2016",
"es2018",
"esnext.asynciterable"
]
},

65
src/Windows/Avalonia.Win32/Automation/AutomationNode.cs

@ -20,7 +20,6 @@ namespace Avalonia.Win32.Automation
IRawElementProviderSimple,
IRawElementProviderSimple2,
IRawElementProviderFragment,
IRawElementProviderAdviseEvents,
IInvokeProvider
{
private static Dictionary<AutomationProperty, UiaPropertyId> s_propertyMap = new()
@ -47,14 +46,31 @@ namespace Avalonia.Win32.Automation
private static ConditionalWeakTable<AutomationPeer, AutomationNode> s_nodes = new();
private readonly int[] _runtimeId;
private int _raiseFocusChanged;
private int _raisePropertyChanged;
public AutomationNode(AutomationPeer peer)
{
_runtimeId = new int[] { 3, GetHashCode() };
Peer = peer;
s_nodes.Add(peer, this);
peer.ChildrenChanged += Peer_ChildrenChanged;
peer.PropertyChanged += Peer_PropertyChanged;
}
private void Peer_ChildrenChanged(object? sender, EventArgs e)
{
ChildrenChanged();
}
private void Peer_PropertyChanged(object? sender, AutomationPropertyChangedEventArgs e)
{
if (s_propertyMap.TryGetValue(e.Property, out var id))
{
UiaCoreProviderApi.UiaRaiseAutomationPropertyChangedEvent(
this,
(int)id,
e.OldValue as IConvertible,
e.NewValue as IConvertible);
}
}
public AutomationPeer Peer { get; protected set; }
@ -86,14 +102,6 @@ namespace Avalonia.Win32.Automation
0);
}
public void PropertyChanged(AutomationProperty property, object? oldValue, object? newValue)
{
if (_raisePropertyChanged > 0 && s_propertyMap.TryGetValue(property, out var id))
{
UiaCoreProviderApi.UiaRaiseAutomationPropertyChangedEvent(this, (int)id, oldValue, newValue);
}
}
[return: MarshalAs(UnmanagedType.IUnknown)]
public virtual object? GetPatternProvider(int patternId)
{
@ -188,32 +196,6 @@ namespace Avalonia.Win32.Automation
void IRawElementProviderSimple2.ShowContextMenu() => InvokeSync(() => Peer.ShowContextMenu());
void IInvokeProvider.Invoke() => InvokeSync((AAP.IInvokeProvider x) => x.Invoke());
void IRawElementProviderAdviseEvents.AdviseEventAdded(int eventId, int[] properties)
{
switch ((UiaEventId)eventId)
{
case UiaEventId.AutomationPropertyChanged:
++_raisePropertyChanged;
break;
case UiaEventId.AutomationFocusChanged:
++_raiseFocusChanged;
break;
}
}
void IRawElementProviderAdviseEvents.AdviseEventRemoved(int eventId, int[] properties)
{
switch ((UiaEventId)eventId)
{
case UiaEventId.AutomationPropertyChanged:
--_raisePropertyChanged;
break;
case UiaEventId.AutomationFocusChanged:
--_raiseFocusChanged;
break;
}
}
protected void InvokeSync(Action action)
{
if (Dispatcher.UIThread.CheckAccess())
@ -266,15 +248,6 @@ namespace Avalonia.Win32.Automation
throw new NotSupportedException();
}
protected void RaiseFocusChanged(AutomationNode? focused)
{
if (_raiseFocusChanged > 0)
{
UiaCoreProviderApi.UiaRaiseAutomationEvent(
focused,
(int)UiaEventId.AutomationFocusChanged);
}
}
private AutomationNode? GetRoot()
{

35
src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs

@ -10,8 +10,11 @@ namespace Avalonia.Win32.Automation
{
[RequiresUnreferencedCode("Requires .NET COM interop")]
internal class RootAutomationNode : AutomationNode,
IRawElementProviderFragmentRoot
IRawElementProviderFragmentRoot,
IRawElementProviderAdviseEvents
{
private int _raiseFocusChanged;
public RootAutomationNode(AutomationPeer peer)
: base(peer)
{
@ -42,6 +45,36 @@ namespace Avalonia.Win32.Automation
return GetOrCreate(focus);
}
void IRawElementProviderAdviseEvents.AdviseEventAdded(int eventId, int[] properties)
{
switch ((UiaEventId)eventId)
{
case UiaEventId.AutomationFocusChanged:
++_raiseFocusChanged;
break;
}
}
void IRawElementProviderAdviseEvents.AdviseEventRemoved(int eventId, int[] properties)
{
switch ((UiaEventId)eventId)
{
case UiaEventId.AutomationFocusChanged:
--_raiseFocusChanged;
break;
}
}
protected void RaiseFocusChanged(AutomationNode? focused)
{
if (_raiseFocusChanged > 0)
{
UiaCoreProviderApi.UiaRaiseAutomationEvent(
focused,
(int)UiaEventId.AutomationFocusChanged);
}
}
public void FocusChanged(object? sender, EventArgs e)
{
RaiseFocusChanged(GetOrCreate(Peer.GetFocus()));

2
src/Windows/Avalonia.Win32/Win32StorageProvider.cs

@ -131,7 +131,7 @@ namespace Avalonia.Win32
}
}
if (folder?.TryGetFullPath() is { } folderPath)
if (folder?.TryGetLocalPath() is { } folderPath)
{
var riid = UnmanagedMethods.ShellIds.IShellItem;
if (UnmanagedMethods.SHCreateItemFromParsingName(folderPath, IntPtr.Zero, ref riid, out var directoryShellItem)

6
src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs

@ -94,11 +94,7 @@ internal sealed class IOSStorageFile : IOSStorageItem, IStorageBookmarkFile
public IOSStorageFile(NSUrl url) : base(url)
{
}
public bool CanOpenRead => true;
public bool CanOpenWrite => true;
public Task<Stream> OpenReadAsync()
{
return Task.FromResult<Stream>(new IOSSecurityScopedStream(Url, FileAccess.Read));

10
src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs

@ -43,6 +43,10 @@ internal class IOSStorageProvider : IStorageProvider
{
return f.AppleUniformTypeIdentifiers.Select(id => UTType.CreateFromIdentifier(id));
}
if (f.TryGetExtensions() is { } extensions && extensions.Any())
{
return extensions.Select(id => UTType.CreateFromExtension(id.TrimStart('.')));
}
if (f.MimeTypes?.Any() == true)
{
return f.MimeTypes.Select(id => UTType.CreateFromMimeType(id));
@ -100,19 +104,19 @@ internal class IOSStorageProvider : IStorageProvider
? new IOSStorageFolder(url) : null);
}
public Task<IStorageFile?> TryGetFileFromPath(Uri filePath)
public Task<IStorageFile?> TryGetFileFromPathAsync(Uri filePath)
{
// TODO: research if it's possible, maybe with additional permissions.
return Task.FromResult<IStorageFile?>(null);
}
public Task<IStorageFolder?> TryGetFolderFromPath(Uri folderPath)
public Task<IStorageFolder?> TryGetFolderFromPathAsync(Uri folderPath)
{
// TODO: research if it's possible, maybe with additional permissions.
return Task.FromResult<IStorageFolder?>(null);
}
public Task<IStorageFolder?> TryGetWellKnownFolder(WellKnownFolder wellKnownFolder)
public Task<IStorageFolder?> TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder)
{
var directoryType = wellKnownFolder switch
{

35
tests/Avalonia.IntegrationTests.Appium/SliderTests.cs

@ -0,0 +1,35 @@
using System;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Interactions;
using Xunit;
namespace Avalonia.IntegrationTests.Appium
{
[Collection("Default")]
public class SliderTests
{
private readonly AppiumDriver<AppiumWebElement> _session;
public SliderTests(TestAppFixture fixture)
{
_session = fixture.Session;
var tabs = _session.FindElementByAccessibilityId("MainTabs");
var tab = tabs.FindElementByName("SliderTab");
tab.Click();
}
[Fact]
public void Changes_Value_When_Clicking_Increase_Button()
{
var slider = _session.FindElementByAccessibilityId("Slider");
// slider.Text gets the Slider value
Assert.True(double.Parse(slider.Text) == 30);
new Actions(_session).Click(slider).Perform();
Assert.Equal(50, Math.Round(double.Parse(slider.Text)));
}
}
}

81
tests/Avalonia.RenderTests/Controls/AdornerTests.cs

@ -1,3 +1,4 @@
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
@ -18,56 +19,70 @@ public class AdornerTests : TestBase
{
}
[Fact]
public async Task Focus_Adorner_Is_Properly_Clipped()
async Task CheckAdornedContent(Control content, Control adorned, Control adorner, int width = 200, int height = 200,
[CallerMemberName] string testName = "")
{
Border adorned;
var tree = new Decorator
{
Child = new VisualLayerManager
{
Child = new Border
{
Background = Brushes.Red,
Padding = new Thickness(10, 50, 10,10),
Child = new Border()
{
Background = Brushes.White,
ClipToBounds = true,
Padding = new Thickness(0, -30, 0, 0),
Child = adorned = new Border
{
Background = Brushes.Green,
VerticalAlignment = VerticalAlignment.Top,
Height = 100,
Width = 50
}
}
}
Child = content
},
Width = 200,
Height = 200
};
var adorner = new Border
{
BorderThickness = new Thickness(2),
BorderBrush = Brushes.Black
Width = width,
Height = height
};
var size = new Size(tree.Width, tree.Height);
tree.Measure(size);
tree.Arrange(new Rect(size));
adorned.AttachedToVisualTree += delegate
{
AdornerLayer.SetAdornedElement(adorner, adorned);
AdornerLayer.GetAdornerLayer(adorned)!.Children.Add(adorner);
};
tree.Measure(size);
tree.Arrange(new Rect(size));
await RenderToFile(tree);
CompareImages(skipImmediate: true);
await RenderToFile(tree, testName: testName);
CompareImages(skipImmediate: true, testName: testName);
}
[Theory,
InlineData(true),
InlineData(false)
]
public async Task Focus_Adorner_Is_Properly_Clipped(bool clip)
{
Border adorned;
var content = new Border
{
Background = Brushes.Red,
Padding = new Thickness(10, 50, 10, 10),
Child = new Border()
{
Background = Brushes.White,
ClipToBounds = true,
Padding = new Thickness(0, -30, 0, 0),
Child = adorned = new Border
{
Background = Brushes.Green,
VerticalAlignment = VerticalAlignment.Top,
Height = 100,
Width = 50
}
}
};
var adorner = new Border
{
BorderThickness = new Thickness(2),
BorderBrush = Brushes.Black
};
if (!clip)
AdornerLayer.SetIsClipEnabled(adorner, false);
await CheckAdornedContent(content, adorned, adorner,
testName: "Focus_Adorner_Is_Properly_Clipped_Clip_" + clip);
}
}

BIN
tests/TestFiles/Direct2D1/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped_Clip_False.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 B

0
tests/TestFiles/Direct2D1/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped.expected.png → tests/TestFiles/Direct2D1/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped_Clip_True.expected.png

Before

Width:  |  Height:  |  Size: 673 B

After

Width:  |  Height:  |  Size: 673 B

BIN
tests/TestFiles/Skia/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped_Clip_False.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 B

0
tests/TestFiles/Skia/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped.expected.png → tests/TestFiles/Skia/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped_Clip_True.expected.png

Before

Width:  |  Height:  |  Size: 673 B

After

Width:  |  Height:  |  Size: 673 B

Loading…
Cancel
Save