Browse Source

Better platform window handle abstraction for DBusSystemDialog (#21379)

pull/21393/head
Nikita Tsukanov 5 months ago
committed by GitHub
parent
commit
a339c8d11f
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 14
      src/Avalonia.Base/Platform/Storage/FallbackStorageProvider.cs
  2. 31
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  3. 29
      src/Avalonia.FreeDesktop/IPortalParentLease.cs
  4. 3
      src/Avalonia.X11/X11Window.cs

14
src/Avalonia.Base/Platform/Storage/FallbackStorageProvider.cs

@ -8,7 +8,7 @@ namespace Avalonia.Platform.Storage;
internal class FallbackStorageProvider : IStorageProvider
{
private readonly Func<Task<IStorageProvider?>>[] _factories;
private Func<Task<IStorageProvider?>>[] _factories;
private readonly List<IStorageProvider> _providers = new();
private int _nextProviderFactory = 0;
@ -16,6 +16,18 @@ internal class FallbackStorageProvider : IStorageProvider
{
_factories = factories;
}
/// <summary>
/// Discards any cached providers and replaces the factory list. Useful when the
/// underlying platform state has changed (e.g. wayland compositor reconnect) and
/// previously-selected providers may no longer be appropriate.
/// </summary>
public void Reset(Func<Task<IStorageProvider?>>[] factories)
{
_factories = factories;
_providers.Clear();
_nextProviderFactory = 0;
}
async IAsyncEnumerable<IStorageProvider> GetProviders()
{

31
src/Avalonia.FreeDesktop/DBusSystemDialog.cs

@ -16,7 +16,16 @@ namespace Avalonia.FreeDesktop
{
internal class DBusSystemDialog : BclStorageProvider
{
internal static async Task<IStorageProvider?> TryCreateAsync(IPlatformHandle handle)
/// <summary>
/// Creates a portal-backed storage provider if xdg-desktop-portal FileChooser is available.
/// </summary>
/// <param name="parentLeaseProvider">
/// Callback invoked once per picker call to obtain the parent-window handle to pass to the
/// portal. The returned <see cref="IPortalParentLease"/> is held until the picker call
/// completes and then disposed. May return <c>null</c> to call the portal with an empty
/// parent_window string (per portal spec: "no parent").
/// </param>
internal static async Task<IStorageProvider?> TryCreateAsync(Func<Task<IPortalParentLease?>>? parentLeaseProvider)
{
if (DBusHelper.DefaultConnection is not { } conn)
return null;
@ -35,23 +44,26 @@ namespace Avalonia.FreeDesktop
return null;
}
return new DBusSystemDialog(conn, handle, dbusFileChooser, version);
return new DBusSystemDialog(conn, parentLeaseProvider, dbusFileChooser, version);
}
private readonly Connection _connection;
private readonly OrgFreedesktopPortalFileChooserProxy _fileChooser;
private readonly IPlatformHandle _handle;
private readonly Func<Task<IPortalParentLease?>>? _parentLeaseProvider;
private readonly uint _version;
private DBusSystemDialog(Connection connection, IPlatformHandle handle,
private DBusSystemDialog(Connection connection, Func<Task<IPortalParentLease?>>? parentLeaseProvider,
OrgFreedesktopPortalFileChooserProxy fileChooser, uint version)
{
_connection = connection;
_fileChooser = fileChooser;
_handle = handle;
_parentLeaseProvider = parentLeaseProvider;
_version = version;
}
private async Task<IPortalParentLease?> AcquireParentLeaseAsync()
=> _parentLeaseProvider is null ? null : await _parentLeaseProvider().ConfigureAwait(false);
public override bool CanOpen => true;
public override bool CanSave => true;
@ -60,7 +72,8 @@ namespace Avalonia.FreeDesktop
public override async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
var parentWindow = $"x11:{_handle.Handle:X}";
await using var parentLease = await AcquireParentLeaseAsync().ConfigureAwait(false);
var parentWindow = parentLease?.Handle ?? string.Empty;
ObjectPath objectPath;
var chooserOptions = new Dictionary<string, VariantValue>();
@ -109,7 +122,8 @@ namespace Avalonia.FreeDesktop
private async Task<(IStorageFile? file, FilePickerFileType? selectedType)> SaveFilePickerCoreAsync(
FilePickerSaveOptions options)
{
var parentWindow = $"x11:{_handle.Handle:X}";
await using var parentLease = await AcquireParentLeaseAsync().ConfigureAwait(false);
var parentWindow = parentLease?.Handle ?? string.Empty;
ObjectPath objectPath;
var chooserOptions = new Dictionary<string, VariantValue>();
if (TryParseFilters(options.FileTypeChoices, options.SuggestedFileType, out var filters,
@ -182,7 +196,8 @@ namespace Avalonia.FreeDesktop
if (_version < 3)
return [];
var parentWindow = $"x11:{_handle.Handle:X}";
await using var parentLease = await AcquireParentLeaseAsync().ConfigureAwait(false);
var parentWindow = parentLease?.Handle ?? string.Empty;
var chooserOptions = new Dictionary<string, VariantValue>
{
{ "directory", VariantValue.Bool(true) }, { "multiple", VariantValue.Bool(options.AllowMultiple) }

29
src/Avalonia.FreeDesktop/IPortalParentLease.cs

@ -0,0 +1,29 @@
using System;
using System.Threading.Tasks;
namespace Avalonia.FreeDesktop;
/// <summary>
/// Backend-provided lease on a <c>parent_window</c> handle string suitable for passing
/// to <c>org.freedesktop.portal.FileChooser</c> et al.
/// </summary>
/// <remarks>
/// <para>The handle string is in the platform-prefixed format defined by xdg-desktop-portal:
/// <c>x11:HEX</c> for X11, <c>wayland:HANDLE</c> for wayland.</para>
/// <para>Disposal frees any platform resources backing the handle. For wayland this destroys
/// the <c>zxdg_exported_v2</c> object that provided the handle string; the imported handle
/// on the portal side becomes invalid afterwards, so the lease MUST be held until the
/// portal call completes.</para>
/// </remarks>
internal interface IPortalParentLease : IAsyncDisposable
{
/// <summary>Prefixed parent-window handle string (e.g. <c>"x11:1A2B"</c>).</summary>
string Handle { get; }
}
internal sealed class TrivialPortalParentLease : IPortalParentLease
{
public TrivialPortalParentLease(string handle) => Handle = handle;
public string Handle { get; }
public ValueTask DisposeAsync() => default;
}

3
src/Avalonia.X11/X11Window.cs

@ -269,7 +269,8 @@ namespace Avalonia.X11
_storageProvider = new FallbackStorageProvider(new[]
{
() => _platform.Options.UseDBusFilePicker
? DBusSystemDialog.TryCreateAsync(Handle)
? DBusSystemDialog.TryCreateAsync(() => Task.FromResult<IPortalParentLease?>(
new TrivialPortalParentLease($"x11:{Handle.Handle:X}")))
: Task.FromResult<IStorageProvider?>(null),
() => GtkSystemDialog.TryCreate(this),
// TODO: This will be incompatible with "root element is not a TopLevel" scenarios,

Loading…
Cancel
Save