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.