diff --git a/src/Avalonia.Base/Platform/Storage/FallbackStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/FallbackStorageProvider.cs index 0192c6d38c..0852bbf329 100644 --- a/src/Avalonia.Base/Platform/Storage/FallbackStorageProvider.cs +++ b/src/Avalonia.Base/Platform/Storage/FallbackStorageProvider.cs @@ -8,7 +8,7 @@ namespace Avalonia.Platform.Storage; internal class FallbackStorageProvider : IStorageProvider { - private readonly Func>[] _factories; + private Func>[] _factories; private readonly List _providers = new(); private int _nextProviderFactory = 0; @@ -16,6 +16,18 @@ internal class FallbackStorageProvider : IStorageProvider { _factories = factories; } + + /// + /// 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. + /// + public void Reset(Func>[] factories) + { + _factories = factories; + _providers.Clear(); + _nextProviderFactory = 0; + } async IAsyncEnumerable GetProviders() { diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs index 1def2a4659..4434400475 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs @@ -16,7 +16,16 @@ namespace Avalonia.FreeDesktop { internal class DBusSystemDialog : BclStorageProvider { - internal static async Task TryCreateAsync(IPlatformHandle handle) + /// + /// Creates a portal-backed storage provider if xdg-desktop-portal FileChooser is available. + /// + /// + /// Callback invoked once per picker call to obtain the parent-window handle to pass to the + /// portal. The returned is held until the picker call + /// completes and then disposed. May return null to call the portal with an empty + /// parent_window string (per portal spec: "no parent"). + /// + internal static async Task TryCreateAsync(Func>? 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>? _parentLeaseProvider; private readonly uint _version; - private DBusSystemDialog(Connection connection, IPlatformHandle handle, + private DBusSystemDialog(Connection connection, Func>? parentLeaseProvider, OrgFreedesktopPortalFileChooserProxy fileChooser, uint version) { _connection = connection; _fileChooser = fileChooser; - _handle = handle; + _parentLeaseProvider = parentLeaseProvider; _version = version; } + private async Task 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> 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(); @@ -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(); 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 { { "directory", VariantValue.Bool(true) }, { "multiple", VariantValue.Bool(options.AllowMultiple) } diff --git a/src/Avalonia.FreeDesktop/IPortalParentLease.cs b/src/Avalonia.FreeDesktop/IPortalParentLease.cs new file mode 100644 index 0000000000..190070bd40 --- /dev/null +++ b/src/Avalonia.FreeDesktop/IPortalParentLease.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading.Tasks; + +namespace Avalonia.FreeDesktop; + +/// +/// Backend-provided lease on a parent_window handle string suitable for passing +/// to org.freedesktop.portal.FileChooser et al. +/// +/// +/// The handle string is in the platform-prefixed format defined by xdg-desktop-portal: +/// x11:HEX for X11, wayland:HANDLE for wayland. +/// Disposal frees any platform resources backing the handle. For wayland this destroys +/// the zxdg_exported_v2 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. +/// +internal interface IPortalParentLease : IAsyncDisposable +{ + /// Prefixed parent-window handle string (e.g. "x11:1A2B"). + string Handle { get; } +} + +internal sealed class TrivialPortalParentLease : IPortalParentLease +{ + public TrivialPortalParentLease(string handle) => Handle = handle; + public string Handle { get; } + public ValueTask DisposeAsync() => default; +} diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 661c720f40..91fbfb93d2 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/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( + new TrivialPortalParentLease($"x11:{Handle.Handle:X}"))) : Task.FromResult(null), () => GtkSystemDialog.TryCreate(this), // TODO: This will be incompatible with "root element is not a TopLevel" scenarios,