Browse Source

Implement IStorageProvider.OpenFileWithResultAsync (#21418)

* Implement IStorageProvider.OpenFileWithResult

* Update API suppressions
pull/21711/head
Julien Lebosquain 3 months ago
committed by GitHub
parent
commit
fa3f4978ce
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 12
      api/Avalonia.nupkg.xml
  2. 41
      native/Avalonia.Native/src/OSX/StorageProvider.mm
  3. 6
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs
  4. 5
      src/Avalonia.Base/Platform/Storage/FallbackStorageProvider.cs
  5. 17
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs
  6. 20
      src/Avalonia.Base/Platform/Storage/IStorageProvider.cs
  7. 10
      src/Avalonia.Base/Platform/Storage/NoopStorageProvider.cs
  8. 23
      src/Avalonia.Base/Platform/Storage/OpenFilePickerResult.cs
  9. 6
      src/Avalonia.Base/Platform/Storage/SaveFilePickerResult.cs
  10. 26
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  11. 34
      src/Avalonia.Dialogs/ManagedStorageProvider.cs
  12. 111
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  13. 21
      src/Avalonia.Native/StorageProviderApi.cs
  14. 15
      src/Avalonia.Native/StorageProviderImpl.cs
  15. 44
      src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
  16. 6
      src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs
  17. 86
      src/Windows/Avalonia.Win32/Win32StorageProvider.cs
  18. 6
      src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs

12
api/Avalonia.nupkg.xml

@ -121,6 +121,12 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.Storage.IStorageProvider.OpenFilePickerWithResultAsync(Avalonia.Platform.Storage.FilePickerOpenOptions)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Controls.Platform.IWin32OptionsTopLevelImpl.SetWindowCornerPreference(Avalonia.Controls.Win32Properties.WindowCornerPreference)</Target>
@ -133,6 +139,12 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.Storage.IStorageProvider.OpenFilePickerWithResultAsync(Avalonia.Platform.Storage.FilePickerOpenOptions)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Controls.Platform.IWin32OptionsTopLevelImpl.SetWindowCornerPreference(Avalonia.Controls.Win32Properties.WindowCornerPreference)</Target>

41
native/Avalonia.Native/src/OSX/StorageProvider.mm

@ -165,6 +165,17 @@ public:
return nullptr;
}
static int GetSelectedFilterIndex(NSSavePanel* _Nonnull panel)
{
if (panel.accessoryView != nil)
{
auto popup = [panel.accessoryView viewWithTag:kFileTypePopupTag];
if ([popup isKindOfClass:[NSPopUpButton class]])
return (int)[(NSPopUpButton*)popup indexOfSelectedItem];
}
return -1;
}
virtual void SelectFolderDialog (IAvnTopLevel* parentTopLevel,
IAvnSystemDialogEvents* events,
@ -270,30 +281,32 @@ public:
auto parentWindow = GetEffectiveNSWindow(parentTopLevel);
auto handler = ^(NSModalResponse result) {
auto selectedIndex = GetSelectedFilterIndex(panel);
if(result == NSFileHandlingPanelOKButton)
{
auto urls = [panel URLs];
if(urls.count > 0)
{
auto uriStrings = CreateAvnStringArray(urls);
ownedEvents->OnCompleted(uriStrings);
ownedEvents->OnCompletedWithFilter(uriStrings, selectedIndex);
[panel orderOut:panel];
if (parentWindow != nullptr)
{
[parentWindow makeKeyAndOrderFront:parentWindow];
}
return;
}
}
ownedEvents->OnCompleted(nullptr);
ownedEvents->OnCompletedWithFilter(nullptr, selectedIndex);
};
if (parentWindow != nullptr)
{
[panel beginSheetModalForWindow:parentWindow completionHandler:handler];
@ -304,7 +317,7 @@ public:
}
}
}
virtual void SaveFileDialog (IAvnTopLevel* parentTopLevel,
IAvnSystemDialogEvents* events,
const char* title,
@ -340,15 +353,7 @@ public:
auto parentWindow = GetEffectiveNSWindow(parentTopLevel);
auto handler = ^(NSModalResponse result) {
int selectedIndex = -1;
if (panel.accessoryView != nil)
{
auto popup = [panel.accessoryView viewWithTag:kFileTypePopupTag];
if ([popup isKindOfClass:[NSPopUpButton class]])
{
selectedIndex = (int)[(NSPopUpButton*)popup indexOfSelectedItem];
}
}
auto selectedIndex = GetSelectedFilterIndex(panel);
if(result == NSFileHandlingPanelOKButton)
{

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

@ -165,6 +165,12 @@ internal class AndroidStorageProvider : IStorageProvider
return uris.Select(u => new AndroidStorageFile(_activity, u)).ToArray();
}
public async Task<OpenFilePickerResult> OpenFilePickerWithResultAsync(FilePickerOpenOptions options)
{
var files = await OpenFilePickerAsync(options).ConfigureAwait(false);
return new OpenFilePickerResult { Files = files };
}
public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
var mimeTypes = options.FileTypeChoices?.Where(t => t != FilePickerFileTypes.All)

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

@ -65,6 +65,11 @@ internal class FallbackStorageProvider : IStorageProvider
return await (await GetFor(p => p.CanOpen)).OpenFilePickerAsync(options);
}
public async Task<OpenFilePickerResult> OpenFilePickerWithResultAsync(FilePickerOpenOptions options)
{
return await (await GetFor(p => p.CanOpen)).OpenFilePickerWithResultAsync(options);
}
public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
return await (await GetFor(p => p.CanSave)).SaveFilePickerAsync(options);

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

@ -10,13 +10,26 @@ namespace Avalonia.Platform.Storage.FileIO;
internal abstract class BclStorageProvider : IStorageProvider
{
public abstract bool CanOpen { get; }
public abstract Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options);
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
var result = await OpenFilePickerWithResultAsync(options).ConfigureAwait(false);
return result.Files;
}
public abstract Task<OpenFilePickerResult> OpenFilePickerWithResultAsync(FilePickerOpenOptions options);
public abstract bool CanSave { get; }
public abstract Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options);
public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
var result = await SaveFilePickerWithResultAsync(options).ConfigureAwait(false);
return result.File;
}
public abstract Task<SaveFilePickerResult> SaveFilePickerWithResultAsync(FilePickerSaveOptions options);
public abstract bool CanPickFolder { get; }
public abstract Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options);
public virtual Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark)

20
src/Avalonia.Base/Platform/Storage/IStorageProvider.cs

@ -10,40 +10,46 @@ namespace Avalonia.Platform.Storage;
public interface IStorageProvider
{
/// <summary>
/// Returns true if it's possible to open file picker on the current platform.
/// Returns true if it's possible to open the file picker on the current platform.
/// </summary>
bool CanOpen { get; }
/// <summary>
/// Opens file picker dialog.
/// Opens the file picker dialog.
/// </summary>
/// <returns>Array of selected <see cref="IStorageFile"/> or empty collection if user canceled the dialog.</returns>
Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options);
/// <summary>
/// Returns true if it's possible to open save file picker on the current platform.
/// Opens the file picker dialog and returns additional information about the result.
/// </summary>
/// <returns><see cref="OpenFilePickerResult"/> with selected files and additional dialog information such as selected file type.</returns>
Task<OpenFilePickerResult> OpenFilePickerWithResultAsync(FilePickerOpenOptions options);
/// <summary>
/// Returns true if it's possible to open the save file picker on the current platform.
/// </summary>
bool CanSave { get; }
/// <summary>
/// Opens save file picker dialog.
/// Opens the save file picker dialog.
/// </summary>
/// <returns>Saved <see cref="IStorageFile"/> or null if user canceled the dialog.</returns>
Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options);
/// <summary>
/// Opens save file picker dialog and returns additional information about the result.
/// Opens the save file picker dialog and returns additional information about the result.
/// </summary>
/// <returns><see cref="SaveFilePickerResult"/> with saved file and additional dialog information such as selected file type.</returns>
Task<SaveFilePickerResult> SaveFilePickerWithResultAsync(FilePickerSaveOptions options);
/// <summary>
/// Returns true if it's possible to open folder picker on the current platform.
/// Returns true if it's possible to open the folder picker on the current platform.
/// </summary>
bool CanPickFolder { get; }
/// <summary>
/// Opens folder picker dialog.
/// Opens the folder picker dialog.
/// </summary>
/// <returns>Array of selected <see cref="IStorageFolder"/> or empty collection if user canceled the dialog.</returns>
Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options);

10
src/Avalonia.Base/Platform/Storage/NoopStorageProvider.cs

@ -8,16 +8,13 @@ namespace Avalonia.Platform.Storage;
internal class NoopStorageProvider : BclStorageProvider
{
public override bool CanOpen => false;
public override Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
public override Task<OpenFilePickerResult> OpenFilePickerWithResultAsync(FilePickerOpenOptions options)
{
return Task.FromResult<IReadOnlyList<IStorageFile>>(Array.Empty<IStorageFile>());
return Task.FromResult(new OpenFilePickerResult());
}
public override bool CanSave => false;
public override Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
return Task.FromResult<IStorageFile?>(null);
}
public override Task<SaveFilePickerResult> SaveFilePickerWithResultAsync(FilePickerSaveOptions options)
{
@ -25,6 +22,7 @@ internal class NoopStorageProvider : BclStorageProvider
}
public override bool CanPickFolder => false;
public override Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
return Task.FromResult<IReadOnlyList<IStorageFolder>>(Array.Empty<IStorageFolder>());

23
src/Avalonia.Base/Platform/Storage/OpenFilePickerResult.cs

@ -0,0 +1,23 @@
using System.Collections.Generic;
namespace Avalonia.Platform.Storage;
/// <summary>
/// Represents the result of the <see cref="IStorageProvider.OpenFilePickerWithResultAsync"/> operation.
/// </summary>
public readonly record struct OpenFilePickerResult
{
/// <summary>
/// Gets the list of files selected by the user, or empty if the user canceled the dialog.
/// </summary>
public IReadOnlyList<IStorageFile> Files
{
get => field ?? [];
init;
}
/// <summary>
/// Gets the file type selected by the user, or null if the platform does not support this feature.
/// </summary>
public FilePickerFileType? SelectedFileType { get; init; }
}

6
src/Avalonia.Base/Platform/Storage/SaveFilePickerResult.cs

@ -1,17 +1,17 @@
namespace Avalonia.Platform.Storage;
/// <summary>
/// Extended result of the <see cref="IStorageProvider.SaveFilePickerWithResultAsync(FilePickerSaveOptions)"/> operation.
/// Represents the result of the <see cref="IStorageProvider.SaveFilePickerWithResultAsync"/> operation.
/// </summary>
public readonly record struct SaveFilePickerResult
{
/// <summary>
/// Saved <see cref="IStorageFile"/> or null if user canceled the dialog.
/// Gets the file selected by the user, or null if the user canceled the dialog.
/// </summary>
public IStorageFile? File { get; init; }
/// <summary>
/// Selected file type or null if not supported.
/// Gets the file type selected by the user, or null if the platform does not support this feature.
/// </summary>
public FilePickerFileType? SelectedFileType { get; init; }
}

26
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@ -274,30 +274,4 @@ namespace Avalonia.DesignerSupport.Remote
}
}
}
internal class NoopStorageProvider : BclStorageProvider
{
public override bool CanOpen => false;
public override Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
return Task.FromResult<IReadOnlyList<IStorageFile>>(Array.Empty<IStorageFile>());
}
public override bool CanSave => false;
public override Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
return Task.FromResult<IStorageFile?>(null);
}
public override Task<SaveFilePickerResult> SaveFilePickerWithResultAsync(FilePickerSaveOptions options)
{
return Task.FromResult(new SaveFilePickerResult());
}
public override bool CanPickFolder => false;
public override Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
return Task.FromResult<IReadOnlyList<IStorageFolder>>(Array.Empty<IStorageFolder>());
}
}
}

34
src/Avalonia.Dialogs/ManagedStorageProvider.cs

@ -27,23 +27,16 @@ internal class ManagedStorageProvider : BclStorageProvider
public override bool CanSave => true;
public override bool CanOpen => true;
public override bool CanPickFolder => true;
public override async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
var model = new ManagedFileChooserViewModel(options, _managedOptions);
var results = await Show(model);
return results.Select(f => new BclStorageFile(new FileInfo(f))).ToArray();
}
public override async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
public override async Task<OpenFilePickerResult> OpenFilePickerWithResultAsync(FilePickerOpenOptions options)
{
var model = new ManagedFileChooserViewModel(options, _managedOptions);
var results = await Show(model);
return results.FirstOrDefault() is { } result
? new BclStorageFile(new FileInfo(result))
: null;
var files = results.Select(f => new BclStorageFile(new FileInfo(f))).ToArray<IStorageFile>();
var fileType = TryGetSelectedFileType(options.FileTypeFilter, model.SelectedFilter);
return new OpenFilePickerResult { Files = files, SelectedFileType = fileType };
}
public override async Task<SaveFilePickerResult> SaveFilePickerWithResultAsync(FilePickerSaveOptions options)
@ -52,11 +45,9 @@ internal class ManagedStorageProvider : BclStorageProvider
var results = await Show(model);
var file = results.FirstOrDefault() is { } result ? new BclStorageFile(new FileInfo(result)) : null;
var filterType = model.SelectedFilter?.Index is { } index && index < options.FileTypeChoices?.Count ?
options.FileTypeChoices[index] :
null;
var fileType = TryGetSelectedFileType(options.FileTypeChoices, model.SelectedFilter);
return new SaveFilePickerResult { File = file, SelectedFileType = filterType };
return new SaveFilePickerResult { File = file, SelectedFileType = fileType };
}
public override async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
@ -88,7 +79,7 @@ internal class ManagedStorageProvider : BclStorageProvider
return root;
}
private Task<string[]> Show(ManagedFileChooserViewModel model)
{
var root = PrepareRoot(model);
@ -149,7 +140,7 @@ internal class ManagedStorageProvider : BclStorageProvider
return result;
}
private async Task<string[]> ShowAsPopup(ContentControl root, ManagedFileChooserViewModel model)
{
var tcs = new TaskCompletionSource<bool>();
@ -236,4 +227,11 @@ internal class ManagedStorageProvider : BclStorageProvider
return promptResult;
}
private static FilePickerFileType? TryGetSelectedFileType(
IReadOnlyList<FilePickerFileType>? fileTypes,
ManagedFileChooserFilterViewModel? selectedFileType)
=> fileTypes is not null && selectedFileType?.Index is { } index && index >= 0 && index < fileTypes.Count ?
fileTypes[index] :
null;
}

111
src/Avalonia.FreeDesktop/DBusSystemDialog.cs

@ -5,7 +5,6 @@ using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
using Avalonia.Threading;
@ -70,64 +69,64 @@ namespace Avalonia.FreeDesktop
public override bool CanPickFolder => _version >= 3;
public override async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
public override async Task<OpenFilePickerResult> OpenFilePickerWithResultAsync(FilePickerOpenOptions options)
{
await using var parentLease = await AcquireParentLeaseAsync().ConfigureAwait(false);
var parentWindow = parentLease?.Handle ?? string.Empty;
var (expectedPath, token) = CreateRequestToken();
var chooserOptions = new Dictionary<string, VariantValue>();
if (TryParseFilters(options.FileTypeFilter, options.SuggestedFileType, out var filters,
out var currentFilter))
{
chooserOptions.Add("filters", filters);
if (currentFilter is { } filter)
chooserOptions.Add("current_filter", filter);
}
if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath)
chooserOptions.Add("current_folder", VariantValue.Array(Encoding.UTF8.GetBytes(folderPath + "\0")));
chooserOptions.Add("multiple", VariantValue.Bool(options.AllowMultiple));
chooserOptions.Add("handle_token", token);
var request = new Request(_connection, "org.freedesktop.portal.Desktop", expectedPath);
var tsc = new TaskCompletionSource<string[]?>();
using var disposable = await request.WatchResponseAsync(notification =>
{
if (notification.IsCompletion)
tsc.TrySetException(notification.Exception);
else
tsc.TrySetResult(notification.Value.Results["uris"].GetArray<string>());
}, ObserverFlags.EmitAll);
var actualPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions);
VerifyRequestPath(tsc, expectedPath, actualPath);
var (paths, selectedType) = await ShowFilePickerAsync(
options.SuggestedFileType,
options.SuggestedFileName,
options.SuggestedStartLocation,
options.FileTypeFilter,
options.Title,
options.AllowMultiple,
_fileChooser.OpenFileAsync)
.ConfigureAwait(false);
var uris = await tsc.Task ?? [];
return uris.Select(static path => new BclStorageFile(new FileInfo(new Uri(path).LocalPath))).ToList();
}
var files = paths.Select(path => new BclStorageFile(new FileInfo(path))).ToArray();
public override async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
var (file, _) = await SaveFilePickerCoreAsync(options).ConfigureAwait(false);
return file;
return new OpenFilePickerResult { Files = files, SelectedFileType = selectedType };
}
public override async Task<SaveFilePickerResult> SaveFilePickerWithResultAsync(FilePickerSaveOptions options)
{
var (file, selectedType) = await SaveFilePickerCoreAsync(options).ConfigureAwait(false);
var (paths, selectedType) = await ShowFilePickerAsync(
options.SuggestedFileType,
options.SuggestedFileName,
options.SuggestedStartLocation,
options.FileTypeChoices,
options.Title,
null,
_fileChooser.SaveFileAsync)
.ConfigureAwait(false);
var path = paths.FirstOrDefault();
BclStorageFile? file;
if (path is null)
file = null;
else
{
// WSL2 freedesktop automatically adds extension from selected file type, but we can't pass "default ext". So apply it manually.
path = StorageProviderHelpers.NameWithExtension(path, options.DefaultExtension, selectedType);
file = new BclStorageFile(new FileInfo(path));
}
return new SaveFilePickerResult { File = file, SelectedFileType = selectedType };
}
private async Task<(IStorageFile? file, FilePickerFileType? selectedType)> SaveFilePickerCoreAsync(
FilePickerSaveOptions options)
private async Task<(string[] paths, FilePickerFileType? selectedType)> ShowFilePickerAsync(
FilePickerFileType? suggestedFileType,
string? suggestedFileName,
IStorageFolder? suggestedStartLocation,
IReadOnlyList<FilePickerFileType>? fileTypes,
string? title,
bool? allowMultiple,
Func<string, string, Dictionary<string, VariantValue>, Task<ObjectPath>> showAsync)
{
await using var parentLease = await AcquireParentLeaseAsync().ConfigureAwait(false);
var parentWindow = parentLease?.Handle ?? string.Empty;
var (expectedPath, token) = CreateRequestToken();
var chooserOptions = new Dictionary<string, VariantValue>();
if (TryParseFilters(options.FileTypeChoices, options.SuggestedFileType, out var filters,
if (TryParseFilters(fileTypes,suggestedFileType, out var filters,
out var currentFilter))
{
chooserOptions.Add("filters", filters);
@ -135,10 +134,12 @@ namespace Avalonia.FreeDesktop
chooserOptions.Add("current_filter", filter);
}
if (options.SuggestedFileName is { } currentName)
chooserOptions.Add("current_name", VariantValue.String(currentName));
if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath)
if (suggestedFileName is not null)
chooserOptions.Add("current_name", VariantValue.String(suggestedFileName));
if (suggestedStartLocation?.TryGetLocalPath() is { } folderPath)
chooserOptions.Add("current_folder", VariantValue.Array(Encoding.UTF8.GetBytes(folderPath + "\0")));
if (allowMultiple.HasValue)
chooserOptions.Add("multiple", VariantValue.Bool(allowMultiple.Value));
chooserOptions.Add("handle_token", token);
var request = new Request(_connection, "org.freedesktop.portal.Desktop", expectedPath);
@ -170,7 +171,7 @@ namespace Avalonia.FreeDesktop
// Reuse the file type objects from options
// so the consuming code can match exactly the
// file type selected instead of spawning one.
selectedType = options.FileTypeChoices?.FirstOrDefault(type => type.Name == name && (
selectedType = fileTypes?.FirstOrDefault(type => type.Name == name && (
(type.MimeTypes?.All(y => mimeTypes.Contains(y)) ?? false) ||
(type.Patterns?.All(y => patterns.Contains(y)) ?? false)))
?? new FilePickerFileType(name) { MimeTypes = mimeTypes, Patterns = patterns };
@ -180,19 +181,13 @@ namespace Avalonia.FreeDesktop
}
}, ObserverFlags.EmitAll).ConfigureAwait(false);
var actualPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions)
var actualPath = await showAsync(parentWindow, title ?? string.Empty, chooserOptions)
.ConfigureAwait(false);
VerifyRequestPath(tsc, expectedPath, actualPath);
var uris = await tsc.Task.ConfigureAwait(false);
var path = uris?.FirstOrDefault() is { } filePath ? new Uri(filePath).LocalPath : null;
if (path is null)
return (null, selectedType);
// WSL2 freedesktop automatically adds extension from selected file type, but we can't pass "default ext". So apply it manually.
path = StorageProviderHelpers.NameWithExtension(path, options.DefaultExtension, selectedType);
return (new BclStorageFile(new FileInfo(path)), selectedType);
var uris = await tsc.Task.ConfigureAwait(false) ?? [];
var paths = uris.Select(uri => new Uri(uri).LocalPath).ToArray();
return (paths, selectedType);
}
public override async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)

21
src/Avalonia.Native/StorageProviderApi.cs

@ -151,12 +151,12 @@ internal class StorageProviderApi(IAvnStorageProvider native, bool sandboxEnable
_native.Dispose();
}
public async Task<IReadOnlyList<IStorageFile>> OpenFileDialog(TopLevelImpl? topLevel, FilePickerOpenOptions options)
public async Task<(IReadOnlyList<IStorageFile> files, FilePickerFileType? selectedType)> OpenFileDialog(TopLevelImpl? topLevel, FilePickerOpenOptions options)
{
using var fileTypes = new FilePickerFileTypesWrapper(options.FileTypeFilter, null, options.SuggestedFileType);
var suggestedDirectory = options.SuggestedStartLocation?.Path.AbsoluteUri ?? string.Empty;
var (items, _) = await OpenDialogAsync(events =>
var (items, selectedFilterIndex) = await OpenDialogAsync(events =>
{
_native.OpenFileDialog(topLevel?.Native,
events,
@ -167,7 +167,10 @@ internal class StorageProviderApi(IAvnStorageProvider native, bool sandboxEnable
fileTypes);
}).ConfigureAwait(false);
return items.OfType<IStorageFile>().ToArray();
var files = items.OfType<IStorageFile>().ToArray();
var selectedType = TryGetSelectedFileType(options.FileTypeFilter, selectedFilterIndex);
return (files, selectedType);
}
public async Task<(IStorageFile? file, FilePickerFileType? selectedType)> SaveFileDialog(TopLevelImpl? topLevel, FilePickerSaveOptions options)
@ -186,11 +189,7 @@ internal class StorageProviderApi(IAvnStorageProvider native, bool sandboxEnable
}, create: true).ConfigureAwait(false);
var file = items.OfType<IStorageFile>().FirstOrDefault();
FilePickerFileType? selectedType = null;
if (selectedFilterIndex is { } index && index >= 0 && options.FileTypeChoices is { Count: > 0 } choices && index < choices.Count)
{
selectedType = choices[index];
}
var selectedType = TryGetSelectedFileType(options.FileTypeChoices, selectedFilterIndex);
return (file, selectedType);
}
@ -233,6 +232,12 @@ internal class StorageProviderApi(IAvnStorageProvider native, bool sandboxEnable
return Uri.TryCreate(resultString?.String, UriKind.Absolute, out var resultUri) ? resultUri : null;
}
private static FilePickerFileType? TryGetSelectedFileType(
IReadOnlyList<FilePickerFileType>? fileTypes, int? nullableIndex)
=> fileTypes is not null && nullableIndex is { } index && index >= 0 && index < fileTypes.Count ?
fileTypes[index] :
null;
internal class FilePickerFileTypesWrapper(
IReadOnlyList<FilePickerFileType>? types,
string? defaultExtension,

15
src/Avalonia.Native/StorageProviderImpl.cs

@ -14,15 +14,22 @@ internal sealed class StorageProviderImpl(TopLevelImpl topLevel, StorageProvider
public bool CanPickFolder => true;
public Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
return native.OpenFileDialog(topLevel, options);
var result = await OpenFilePickerWithResultAsync(options).ConfigureAwait(false);
return result.Files;
}
public async Task<OpenFilePickerResult> OpenFilePickerWithResultAsync(FilePickerOpenOptions options)
{
var (files, selectedType) = await native.OpenFileDialog(topLevel, options).ConfigureAwait(false);
return new OpenFilePickerResult { Files = files, SelectedFileType = selectedType };
}
public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
var (file, _) = await native.SaveFileDialog(topLevel, options).ConfigureAwait(false);
return file;
var result = await SaveFilePickerWithResultAsync(options).ConfigureAwait(false);
return result.File;
}
public async Task<SaveFilePickerResult> SaveFilePickerWithResultAsync(FilePickerSaveOptions options)

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

@ -35,15 +35,28 @@ namespace Avalonia.X11.NativeDialogs
return await _initialized ? new GtkSystemDialog(window) : null;
}
public override async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
public override async Task<OpenFilePickerResult> OpenFilePickerWithResultAsync(FilePickerOpenOptions options)
{
return await await RunOnGlibThread(async () =>
{
var (files, _) = await ShowDialog(options.Title, _window, GtkFileChooserAction.Open,
options.AllowMultiple, options.SuggestedStartLocation, null, options.SuggestedFileType, options.FileTypeFilter, null, false)
.ConfigureAwait(false);
return files?.Where(f => File.Exists(f)).Select(f => new BclStorageFile(new FileInfo(f))).ToArray() ??
Array.Empty<IStorageFile>();
var (files, selectedFilter) = await ShowDialog(
options.Title,
_window,
GtkFileChooserAction.Open,
options.AllowMultiple,
options.SuggestedStartLocation,
null,
options.SuggestedFileType,
options.FileTypeFilter,
null,
false)
.ConfigureAwait(false);
var storageFiles =
files?.Where(File.Exists).Select(f => new BclStorageFile(new FileInfo(f))).ToArray() ??
Array.Empty<IStorageFile>();
return new OpenFilePickerResult { Files = storageFiles, SelectedFileType = selectedFilter };
});
}
@ -59,20 +72,6 @@ namespace Avalonia.X11.NativeDialogs
Array.Empty<IStorageFolder>();
});
}
public override async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
return await await RunOnGlibThread(async () =>
{
var (files, _) = await ShowDialog(options.Title, _window, GtkFileChooserAction.Save,
false, options.SuggestedStartLocation, options.SuggestedFileName,options.SuggestedFileType, options.FileTypeChoices,
options.DefaultExtension, options.ShowOverwritePrompt ?? false)
.ConfigureAwait(false);
return files?.FirstOrDefault() is { } file
? new BclStorageFile(new FileInfo(file))
: null;
});
}
public override async Task<SaveFilePickerResult> SaveFilePickerWithResultAsync(FilePickerSaveOptions options)
{
@ -199,11 +198,12 @@ namespace Avalonia.X11.NativeDialogs
g_slist_free(gs);
result = resultList.ToArray();
var currentFilter = gtk_file_chooser_get_filter(dlg);
filtersDic.TryGetValue(currentFilter, out selectedFilter);
// GTK doesn't auto-append the extension, so we need to do that manually
if (action == GtkFileChooserAction.Save)
{
var currentFilter = gtk_file_chooser_get_filter(dlg);
filtersDic.TryGetValue(currentFilter, out selectedFilter);
for (var c = 0; c < result.Length; c++)
{
result[c] = StorageProviderHelpers.NameWithExtension(result[c], defaultExtension,

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

@ -60,6 +60,12 @@ internal class BrowserStorageProvider : IStorageProvider
}
}
public async Task<OpenFilePickerResult> OpenFilePickerWithResultAsync(FilePickerOpenOptions options)
{
var files = await OpenFilePickerAsync(options).ConfigureAwait(false);
return new OpenFilePickerResult { Files = files };
}
public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
await AvaloniaModule.ImportStorage();

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

@ -32,58 +32,67 @@ namespace Avalonia.Win32
public override async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
var (folders, _) = await ShowFilePicker(
true, true,
options.AllowMultiple, false,
options.Title, options.SuggestedFileName, null, options.SuggestedStartLocation, null, null,
var (folders, _) = await ShowFilePickerAsync(
true,
true,
options.AllowMultiple,
false,
options.Title,
options.SuggestedFileName,
null,
options.SuggestedStartLocation,
null,
null,
f => new BclStorageFolder(new DirectoryInfo(f)))
.ConfigureAwait(false);
.ConfigureAwait(false);
return folders;
}
public override async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
public override async Task<OpenFilePickerResult> OpenFilePickerWithResultAsync(FilePickerOpenOptions options)
{
var (files, _) = await ShowFilePicker(
true, false,
options.AllowMultiple, false,
options.Title, options.SuggestedFileName, options.SuggestedFileType, options.SuggestedStartLocation,
null, options.FileTypeFilter,
var (files, typeIndex) = await ShowFilePickerAsync(
true,
false,
options.AllowMultiple,
false,
options.Title,
options.SuggestedFileName,
options.SuggestedFileType,
options.SuggestedStartLocation,
null,
options.FileTypeFilter,
f => new BclStorageFile(new FileInfo(f)))
.ConfigureAwait(false);
return files;
}
.ConfigureAwait(false);
public override async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
var (files, _) = await ShowFilePicker(
false, false,
false, options.ShowOverwritePrompt,
options.Title, options.SuggestedFileName, options.SuggestedFileType, options.SuggestedStartLocation,
options.DefaultExtension, options.FileTypeChoices,
f => new BclStorageFile(new FileInfo(f)))
.ConfigureAwait(false);
return files.Count > 0 ? files[0] : null;
var selectedFileType = TryGetSelectedFileType(options.FileTypeFilter, typeIndex);
return new OpenFilePickerResult { Files = files, SelectedFileType = selectedFileType };
}
public override async Task<SaveFilePickerResult> SaveFilePickerWithResultAsync(FilePickerSaveOptions options)
{
var (files, index) = await ShowFilePicker(
false, false,
false, options.ShowOverwritePrompt,
options.Title, options.SuggestedFileName, options.SuggestedFileType, options.SuggestedStartLocation,
options.DefaultExtension, options.FileTypeChoices,
f => new BclStorageFile(new FileInfo(f)))
.ConfigureAwait(false);
var (files, typeIndex) = await ShowFilePickerAsync(
false,
false,
false,
options.ShowOverwritePrompt,
options.Title,
options.SuggestedFileName,
options.SuggestedFileType,
options.SuggestedStartLocation,
options.DefaultExtension,
options.FileTypeChoices,
f => new BclStorageFile(new FileInfo(f)))
.ConfigureAwait(false);
var file = files.Count > 0 ? files[0] : null;
var selectedFileType = options.FileTypeChoices?.Count > 0
&& (index > 0 && index <= options.FileTypeChoices.Count) ?
options.FileTypeChoices[index - 1] :
null;
var selectedFileType = TryGetSelectedFileType(options.FileTypeChoices, typeIndex);
return new SaveFilePickerResult { File = file, SelectedFileType = selectedFileType };
}
private unsafe Task<(IReadOnlyList<TStorageItem> items, int typeIndex)> ShowFilePicker<TStorageItem>(
private unsafe Task<(IReadOnlyList<TStorageItem> items, int typeIndex)> ShowFilePickerAsync<TStorageItem>(
bool isOpenFile,
bool openFolder,
bool allowMultiple,
@ -237,6 +246,11 @@ namespace Avalonia.Win32
return tcs.Task;
}
private static FilePickerFileType? TryGetSelectedFileType(IReadOnlyList<FilePickerFileType>? fileTypes, int index)
=> fileTypes is not null && index >= 1 && index <= fileTypes.Count ?
fileTypes[index - 1] :
null;
private static string? GetParsingName(IShellItem shellItem)
{
return GetDisplayName(shellItem, SIGDN_DESKTOPABSOLUTEPARSING);

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

@ -43,6 +43,12 @@ internal class IOSStorageProvider : IStorageProvider
}
}
public async Task<OpenFilePickerResult> OpenFilePickerWithResultAsync(FilePickerOpenOptions options)
{
var files = await OpenFilePickerAsync(options).ConfigureAwait(false);
return new OpenFilePickerResult { Files = files };
}
private async Task<IReadOnlyList<IStorageFile>> OpenImagePickerAsync(FilePickerOpenOptions options)
{
#pragma warning disable CA1422 // Validate platform compatibility - we can't use PHImagePicker here.

Loading…
Cancel
Save