diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index 4b2aa63ef1..1751d863e6 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -121,6 +121,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + + CP0006 + M:Avalonia.Platform.Storage.IStorageProvider.OpenFilePickerWithResultAsync(Avalonia.Platform.Storage.FilePickerOpenOptions) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0006 M:Avalonia.Controls.Platform.IWin32OptionsTopLevelImpl.SetWindowCornerPreference(Avalonia.Controls.Win32Properties.WindowCornerPreference) @@ -133,6 +139,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0006 + M:Avalonia.Platform.Storage.IStorageProvider.OpenFilePickerWithResultAsync(Avalonia.Platform.Storage.FilePickerOpenOptions) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0006 M:Avalonia.Controls.Platform.IWin32OptionsTopLevelImpl.SetWindowCornerPreference(Avalonia.Controls.Win32Properties.WindowCornerPreference) diff --git a/native/Avalonia.Native/src/OSX/StorageProvider.mm b/native/Avalonia.Native/src/OSX/StorageProvider.mm index a608f6ffaf..7f9f622171 100644 --- a/native/Avalonia.Native/src/OSX/StorageProvider.mm +++ b/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) { diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs index c64431a034..e55689f93d 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs +++ b/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 OpenFilePickerWithResultAsync(FilePickerOpenOptions options) + { + var files = await OpenFilePickerAsync(options).ConfigureAwait(false); + return new OpenFilePickerResult { Files = files }; + } + public async Task SaveFilePickerAsync(FilePickerSaveOptions options) { var mimeTypes = options.FileTypeChoices?.Where(t => t != FilePickerFileTypes.All) diff --git a/src/Avalonia.Base/Platform/Storage/FallbackStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/FallbackStorageProvider.cs index 0852bbf329..d5760408a0 100644 --- a/src/Avalonia.Base/Platform/Storage/FallbackStorageProvider.cs +++ b/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 OpenFilePickerWithResultAsync(FilePickerOpenOptions options) + { + return await (await GetFor(p => p.CanOpen)).OpenFilePickerWithResultAsync(options); + } + public async Task SaveFilePickerAsync(FilePickerSaveOptions options) { return await (await GetFor(p => p.CanSave)).SaveFilePickerAsync(options); diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs index a471dba720..61a0674b2e 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs +++ b/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> OpenFilePickerAsync(FilePickerOpenOptions options); + + public async Task> OpenFilePickerAsync(FilePickerOpenOptions options) + { + var result = await OpenFilePickerWithResultAsync(options).ConfigureAwait(false); + return result.Files; + } + public abstract Task OpenFilePickerWithResultAsync(FilePickerOpenOptions options); public abstract bool CanSave { get; } - public abstract Task SaveFilePickerAsync(FilePickerSaveOptions options); + + public async Task SaveFilePickerAsync(FilePickerSaveOptions options) + { + var result = await SaveFilePickerWithResultAsync(options).ConfigureAwait(false); + return result.File; + } + public abstract Task SaveFilePickerWithResultAsync(FilePickerSaveOptions options); public abstract bool CanPickFolder { get; } + public abstract Task> OpenFolderPickerAsync(FolderPickerOpenOptions options); public virtual Task OpenFileBookmarkAsync(string bookmark) diff --git a/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs index 22f8d8bd0a..f4fe7ca5eb 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs @@ -10,40 +10,46 @@ namespace Avalonia.Platform.Storage; public interface IStorageProvider { /// - /// 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. /// bool CanOpen { get; } /// - /// Opens file picker dialog. + /// Opens the file picker dialog. /// /// Array of selected or empty collection if user canceled the dialog. Task> OpenFilePickerAsync(FilePickerOpenOptions options); /// - /// 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. + /// + /// with selected files and additional dialog information such as selected file type. + Task OpenFilePickerWithResultAsync(FilePickerOpenOptions options); + + /// + /// Returns true if it's possible to open the save file picker on the current platform. /// bool CanSave { get; } /// - /// Opens save file picker dialog. + /// Opens the save file picker dialog. /// /// Saved or null if user canceled the dialog. Task SaveFilePickerAsync(FilePickerSaveOptions options); /// - /// 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. /// /// with saved file and additional dialog information such as selected file type. Task SaveFilePickerWithResultAsync(FilePickerSaveOptions options); /// - /// 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. /// bool CanPickFolder { get; } /// - /// Opens folder picker dialog. + /// Opens the folder picker dialog. /// /// Array of selected or empty collection if user canceled the dialog. Task> OpenFolderPickerAsync(FolderPickerOpenOptions options); diff --git a/src/Avalonia.Base/Platform/Storage/NoopStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/NoopStorageProvider.cs index 4b056d104e..c2c984d43d 100644 --- a/src/Avalonia.Base/Platform/Storage/NoopStorageProvider.cs +++ b/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> OpenFilePickerAsync(FilePickerOpenOptions options) + + public override Task OpenFilePickerWithResultAsync(FilePickerOpenOptions options) { - return Task.FromResult>(Array.Empty()); + return Task.FromResult(new OpenFilePickerResult()); } public override bool CanSave => false; - public override Task SaveFilePickerAsync(FilePickerSaveOptions options) - { - return Task.FromResult(null); - } public override Task SaveFilePickerWithResultAsync(FilePickerSaveOptions options) { @@ -25,6 +22,7 @@ internal class NoopStorageProvider : BclStorageProvider } public override bool CanPickFolder => false; + public override Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) { return Task.FromResult>(Array.Empty()); diff --git a/src/Avalonia.Base/Platform/Storage/OpenFilePickerResult.cs b/src/Avalonia.Base/Platform/Storage/OpenFilePickerResult.cs new file mode 100644 index 0000000000..91e7c7eccf --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/OpenFilePickerResult.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace Avalonia.Platform.Storage; + +/// +/// Represents the result of the operation. +/// +public readonly record struct OpenFilePickerResult +{ + /// + /// Gets the list of files selected by the user, or empty if the user canceled the dialog. + /// + public IReadOnlyList Files + { + get => field ?? []; + init; + } + + /// + /// Gets the file type selected by the user, or null if the platform does not support this feature. + /// + public FilePickerFileType? SelectedFileType { get; init; } +} diff --git a/src/Avalonia.Base/Platform/Storage/SaveFilePickerResult.cs b/src/Avalonia.Base/Platform/Storage/SaveFilePickerResult.cs index 222b4a4dbc..c643011453 100644 --- a/src/Avalonia.Base/Platform/Storage/SaveFilePickerResult.cs +++ b/src/Avalonia.Base/Platform/Storage/SaveFilePickerResult.cs @@ -1,17 +1,17 @@ namespace Avalonia.Platform.Storage; /// -/// Extended result of the operation. +/// Represents the result of the operation. /// public readonly record struct SaveFilePickerResult { /// - /// Saved or null if user canceled the dialog. + /// Gets the file selected by the user, or null if the user canceled the dialog. /// public IStorageFile? File { get; init; } /// - /// 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. /// public FilePickerFileType? SelectedFileType { get; init; } } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index b53645c67b..97fda7d8d1 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/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> OpenFilePickerAsync(FilePickerOpenOptions options) - { - return Task.FromResult>(Array.Empty()); - } - - public override bool CanSave => false; - public override Task SaveFilePickerAsync(FilePickerSaveOptions options) - { - return Task.FromResult(null); - } - - public override Task SaveFilePickerWithResultAsync(FilePickerSaveOptions options) - { - return Task.FromResult(new SaveFilePickerResult()); - } - - public override bool CanPickFolder => false; - public override Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) - { - return Task.FromResult>(Array.Empty()); - } - } } diff --git a/src/Avalonia.Dialogs/ManagedStorageProvider.cs b/src/Avalonia.Dialogs/ManagedStorageProvider.cs index 58edc32224..30328fabeb 100644 --- a/src/Avalonia.Dialogs/ManagedStorageProvider.cs +++ b/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> 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 SaveFilePickerAsync(FilePickerSaveOptions options) + public override async Task 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(); + var fileType = TryGetSelectedFileType(options.FileTypeFilter, model.SelectedFilter); + + return new OpenFilePickerResult { Files = files, SelectedFileType = fileType }; } public override async Task 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> OpenFolderPickerAsync(FolderPickerOpenOptions options) @@ -88,7 +79,7 @@ internal class ManagedStorageProvider : BclStorageProvider return root; } - + private Task Show(ManagedFileChooserViewModel model) { var root = PrepareRoot(model); @@ -149,7 +140,7 @@ internal class ManagedStorageProvider : BclStorageProvider return result; } - + private async Task ShowAsPopup(ContentControl root, ManagedFileChooserViewModel model) { var tcs = new TaskCompletionSource(); @@ -236,4 +227,11 @@ internal class ManagedStorageProvider : BclStorageProvider return promptResult; } + + private static FilePickerFileType? TryGetSelectedFileType( + IReadOnlyList? fileTypes, + ManagedFileChooserFilterViewModel? selectedFileType) + => fileTypes is not null && selectedFileType?.Index is { } index && index >= 0 && index < fileTypes.Count ? + fileTypes[index] : + null; } diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs index 078a79ca90..43f7816449 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs +++ b/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> OpenFilePickerAsync(FilePickerOpenOptions options) + public override async Task 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(); - - 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(); - using var disposable = await request.WatchResponseAsync(notification => - { - if (notification.IsCompletion) - tsc.TrySetException(notification.Exception); - else - tsc.TrySetResult(notification.Value.Results["uris"].GetArray()); - }, 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 SaveFilePickerAsync(FilePickerSaveOptions options) - { - var (file, _) = await SaveFilePickerCoreAsync(options).ConfigureAwait(false); - return file; + return new OpenFilePickerResult { Files = files, SelectedFileType = selectedType }; } public override async Task 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? fileTypes, + string? title, + bool? allowMultiple, + Func, Task> showAsync) { await using var parentLease = await AcquireParentLeaseAsync().ConfigureAwait(false); var parentWindow = parentLease?.Handle ?? string.Empty; var (expectedPath, token) = CreateRequestToken(); var chooserOptions = new Dictionary(); - 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> OpenFolderPickerAsync(FolderPickerOpenOptions options) diff --git a/src/Avalonia.Native/StorageProviderApi.cs b/src/Avalonia.Native/StorageProviderApi.cs index 1a8fc12de4..305b43dc57 100644 --- a/src/Avalonia.Native/StorageProviderApi.cs +++ b/src/Avalonia.Native/StorageProviderApi.cs @@ -151,12 +151,12 @@ internal class StorageProviderApi(IAvnStorageProvider native, bool sandboxEnable _native.Dispose(); } - public async Task> OpenFileDialog(TopLevelImpl? topLevel, FilePickerOpenOptions options) + public async Task<(IReadOnlyList 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().ToArray(); + var files = items.OfType().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().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? fileTypes, int? nullableIndex) + => fileTypes is not null && nullableIndex is { } index && index >= 0 && index < fileTypes.Count ? + fileTypes[index] : + null; + internal class FilePickerFileTypesWrapper( IReadOnlyList? types, string? defaultExtension, diff --git a/src/Avalonia.Native/StorageProviderImpl.cs b/src/Avalonia.Native/StorageProviderImpl.cs index d3062715c8..b07d630b0f 100644 --- a/src/Avalonia.Native/StorageProviderImpl.cs +++ b/src/Avalonia.Native/StorageProviderImpl.cs @@ -14,15 +14,22 @@ internal sealed class StorageProviderImpl(TopLevelImpl topLevel, StorageProvider public bool CanPickFolder => true; - public Task> OpenFilePickerAsync(FilePickerOpenOptions options) + public async Task> OpenFilePickerAsync(FilePickerOpenOptions options) { - return native.OpenFileDialog(topLevel, options); + var result = await OpenFilePickerWithResultAsync(options).ConfigureAwait(false); + return result.Files; + } + + public async Task OpenFilePickerWithResultAsync(FilePickerOpenOptions options) + { + var (files, selectedType) = await native.OpenFileDialog(topLevel, options).ConfigureAwait(false); + return new OpenFilePickerResult { Files = files, SelectedFileType = selectedType }; } public async Task 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 SaveFilePickerWithResultAsync(FilePickerSaveOptions options) diff --git a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs index f70525b819..b30d8ad62b 100644 --- a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs +++ b/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> OpenFilePickerAsync(FilePickerOpenOptions options) + public override async Task 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(); + 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(); + + return new OpenFilePickerResult { Files = storageFiles, SelectedFileType = selectedFilter }; }); } @@ -59,20 +72,6 @@ namespace Avalonia.X11.NativeDialogs Array.Empty(); }); } - - public override async Task 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 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, diff --git a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs index 957642bfad..5bf34c9920 100644 --- a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs +++ b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs @@ -60,6 +60,12 @@ internal class BrowserStorageProvider : IStorageProvider } } + public async Task OpenFilePickerWithResultAsync(FilePickerOpenOptions options) + { + var files = await OpenFilePickerAsync(options).ConfigureAwait(false); + return new OpenFilePickerResult { Files = files }; + } + public async Task SaveFilePickerAsync(FilePickerSaveOptions options) { await AvaloniaModule.ImportStorage(); diff --git a/src/Windows/Avalonia.Win32/Win32StorageProvider.cs b/src/Windows/Avalonia.Win32/Win32StorageProvider.cs index 6519933de7..ce62c4bb82 100644 --- a/src/Windows/Avalonia.Win32/Win32StorageProvider.cs +++ b/src/Windows/Avalonia.Win32/Win32StorageProvider.cs @@ -32,58 +32,67 @@ namespace Avalonia.Win32 public override async Task> 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> OpenFilePickerAsync(FilePickerOpenOptions options) + public override async Task 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 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 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 items, int typeIndex)> ShowFilePicker( + private unsafe Task<(IReadOnlyList items, int typeIndex)> ShowFilePickerAsync( bool isOpenFile, bool openFolder, bool allowMultiple, @@ -237,6 +246,11 @@ namespace Avalonia.Win32 return tcs.Task; } + private static FilePickerFileType? TryGetSelectedFileType(IReadOnlyList? 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); diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs index a4d993d540..8fc11debdf 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs @@ -43,6 +43,12 @@ internal class IOSStorageProvider : IStorageProvider } } + public async Task OpenFilePickerWithResultAsync(FilePickerOpenOptions options) + { + var files = await OpenFilePickerAsync(options).ConfigureAwait(false); + return new OpenFilePickerResult { Files = files }; + } + private async Task> OpenImagePickerAsync(FilePickerOpenOptions options) { #pragma warning disable CA1422 // Validate platform compatibility - we can't use PHImagePicker here.