Browse Source

Impl `SuggestedFileType` for Save/OpenFilePicker (#20026)

* new SuggestedFileType API

* Add impl for Win32 SuggestedFileType

* Impl suggestedFileType for GTK

* DBus impl

* Impl API for mac

* Add sample for suggested file filter

---------

Co-authored-by: Emmanuel Hansen <emmausssss@gmail.com>
# Conflicts:
#	samples/ControlCatalog/Pages/DialogsPage.xaml.cs
pull/20084/head
Jumar Macato 3 months ago
committed by Max Katz
parent
commit
310d876e7b
  1. 9
      src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs
  2. 9
      src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs
  3. 31
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  4. 20
      src/Avalonia.Native/StorageProviderApi.cs
  5. 2
      src/Avalonia.X11/NativeDialogs/Gtk.cs
  6. 16
      src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
  7. 17
      src/Windows/Avalonia.Win32/Win32StorageProvider.cs

9
src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs

@ -7,6 +7,15 @@ namespace Avalonia.Platform.Storage;
/// </summary>
public class FilePickerOpenOptions : PickerOptions
{
/// <summary>
/// Gets or sets the file type that should be preselected when the dialog is opened.
/// </summary>
/// <remarks>
/// This value should reference one of the items in <see cref="FileTypeChoices"/>.
/// If not set, the first file type in <see cref="FileTypeChoices"/> may be selected by default.
/// </remarks>
public FilePickerFileType? SuggestedFileType { get; set; }
/// <summary>
/// Gets or sets an option indicating whether open picker allows users to select multiple files.
/// </summary>

9
src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs

@ -7,6 +7,15 @@ namespace Avalonia.Platform.Storage;
/// </summary>
public class FilePickerSaveOptions : PickerOptions
{
/// <summary>
/// Gets or sets the file type that should be preselected when the dialog is opened.
/// </summary>
/// <remarks>
/// This value should reference one of the items in <see cref="FileTypeChoices"/>.
/// If not set, the first file type in <see cref="FileTypeChoices"/> may be selected by default.
/// </remarks>
public FilePickerFileType? SuggestedFileType { get; set; }
/// <summary>
/// Gets or sets the default extension to be used to save the file.
/// </summary>

31
src/Avalonia.FreeDesktop/DBusSystemDialog.cs

@ -63,8 +63,13 @@ namespace Avalonia.FreeDesktop
ObjectPath objectPath;
var chooserOptions = new Dictionary<string, VariantValue>();
if (TryParseFilters(options.FileTypeFilter, out var filters))
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")));
@ -106,8 +111,13 @@ namespace Avalonia.FreeDesktop
var parentWindow = $"x11:{_handle.Handle:X}";
ObjectPath objectPath;
var chooserOptions = new Dictionary<string, VariantValue>();
if (TryParseFilters(options.FileTypeChoices, out var filters))
if (TryParseFilters(options.FileTypeChoices, options.SuggestedFileType, out var filters,
out var currentFilter))
{
chooserOptions.Add("filters", filters);
if (currentFilter is { } filter)
chooserOptions.Add("current_filter", filter);
}
if (options.SuggestedFileName is { } currentName)
chooserOptions.Add("current_name", VariantValue.String(currentName));
@ -203,7 +213,10 @@ namespace Avalonia.FreeDesktop
.Select(static path => new BclStorageFolder(new DirectoryInfo(path))).ToList();
}
private static bool TryParseFilters(IReadOnlyList<FilePickerFileType>? fileTypes, out VariantValue result)
private static bool TryParseFilters(IReadOnlyList<FilePickerFileType>? fileTypes,
FilePickerFileType? suggestedFileType,
out VariantValue result,
out VariantValue? currentFilter)
{
const uint GlobStyle = 0u;
const uint MimeStyle = 1u;
@ -212,10 +225,12 @@ namespace Avalonia.FreeDesktop
if (fileTypes is null)
{
result = default;
currentFilter = null;
return false;
}
var filters = new Array<Struct<string, Array<Struct<uint, string>>>>();
currentFilter = null;
foreach (var fileType in fileTypes)
{
@ -228,7 +243,15 @@ namespace Avalonia.FreeDesktop
else
continue;
filters.Add(Struct.Create(fileType.Name, new Array<Struct<uint, string>>(extensions)));
var filterStruct = Struct.Create(fileType.Name, new Array<Struct<uint, string>>(extensions));
filters.Add(filterStruct);
if (suggestedFileType is not null && ReferenceEquals(fileType, suggestedFileType))
{
currentFilter = VariantValue.Struct(
VariantValue.String(filterStruct.Item1),
filterStruct.Item2.AsVariantValue());
}
}
result = filters.AsVariantValue();

20
src/Avalonia.Native/StorageProviderApi.cs

@ -155,7 +155,7 @@ internal class StorageProviderApi(IAvnStorageProvider native, bool sandboxEnable
public async Task<IReadOnlyList<IStorageFile>> OpenFileDialog(TopLevelImpl? topLevel, FilePickerOpenOptions options)
{
using var fileTypes = new FilePickerFileTypesWrapper(options.FileTypeFilter, null);
using var fileTypes = new FilePickerFileTypesWrapper(options.FileTypeFilter, null, options.SuggestedFileType);
var suggestedDirectory = options.SuggestedStartLocation?.Path.AbsoluteUri ?? string.Empty;
var (items, _) = await OpenDialogAsync(events =>
@ -174,7 +174,7 @@ internal class StorageProviderApi(IAvnStorageProvider native, bool sandboxEnable
public async Task<(IStorageFile? file, FilePickerFileType? selectedType)> SaveFileDialog(TopLevelImpl? topLevel, FilePickerSaveOptions options)
{
using var fileTypes = new FilePickerFileTypesWrapper(options.FileTypeChoices, options.DefaultExtension);
using var fileTypes = new FilePickerFileTypesWrapper(options.FileTypeChoices, options.DefaultExtension, options.SuggestedFileType);
var suggestedDirectory = options.SuggestedStartLocation?.Path.AbsoluteUri ?? string.Empty;
var (items, selectedFilterIndex) = await OpenDialogAsync(events =>
@ -237,15 +237,25 @@ internal class StorageProviderApi(IAvnStorageProvider native, bool sandboxEnable
internal class FilePickerFileTypesWrapper(
IReadOnlyList<FilePickerFileType>? types,
string? defaultExtension)
string? defaultExtension,
FilePickerFileType? suggestedType)
: NativeCallbackBase, IAvnFilePickerFileTypes
{
private readonly List<IDisposable> _disposables = new();
public int Count => types?.Count ?? 0;
public int IsDefaultType(int index) => (defaultExtension is not null &&
types![index].TryGetExtensions()?.Any(defaultExtension.EndsWith) == true).AsComBool();
public int IsDefaultType(int index)
{
if (types is null)
return false.AsComBool();
if (suggestedType is not null && ReferenceEquals(types[index], suggestedType))
return true.AsComBool();
return (defaultExtension is not null &&
types[index].TryGetExtensions()?.Any(defaultExtension.EndsWith) == true).AsComBool();
}
public int IsAnyType(int index) =>
(types![index].Patterns?.Contains("*.*") == true || types[index].MimeTypes?.Contains("*.*") == true)

2
src/Avalonia.X11/NativeDialogs/Gtk.cs

@ -101,6 +101,8 @@ namespace Avalonia.X11.NativeDialogs
[DllImport(GtkName)]
public static extern IntPtr gtk_file_chooser_get_filter(IntPtr chooser);
[DllImport(GtkName)]
public static extern void gtk_file_chooser_set_filter(IntPtr chooser, IntPtr filter);
[DllImport(GtkName)]
public static extern void gtk_widget_realize(IntPtr gtkWidget);

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

@ -40,7 +40,7 @@ namespace Avalonia.X11.NativeDialogs
return await await RunOnGlibThread(async () =>
{
var (files, _) = await ShowDialog(options.Title, _window, GtkFileChooserAction.Open,
options.AllowMultiple, options.SuggestedStartLocation, null, options.FileTypeFilter, null, false)
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>();
@ -53,7 +53,7 @@ namespace Avalonia.X11.NativeDialogs
{
var (folders, _) = await ShowDialog(options.Title, _window, GtkFileChooserAction.SelectFolder,
options.AllowMultiple, options.SuggestedStartLocation, null,
null, null, false)
null, null, null, false)
.ConfigureAwait(false);
return folders?.Select(f => new BclStorageFolder(new DirectoryInfo(f))).ToArray() ??
Array.Empty<IStorageFolder>();
@ -65,7 +65,7 @@ namespace Avalonia.X11.NativeDialogs
return await await RunOnGlibThread(async () =>
{
var (files, _) = await ShowDialog(options.Title, _window, GtkFileChooserAction.Save,
false, options.SuggestedStartLocation, options.SuggestedFileName, options.FileTypeChoices,
false, options.SuggestedStartLocation, options.SuggestedFileName,options.SuggestedFileType, options.FileTypeChoices,
options.DefaultExtension, options.ShowOverwritePrompt ?? false)
.ConfigureAwait(false);
return files?.FirstOrDefault() is { } file
@ -79,7 +79,7 @@ namespace Avalonia.X11.NativeDialogs
return await await RunOnGlibThread(async () =>
{
var (files, selectedFilter) = await ShowDialog(options.Title, _window, GtkFileChooserAction.Save,
false, options.SuggestedStartLocation, options.SuggestedFileName, options.FileTypeChoices,
false, options.SuggestedStartLocation, options.SuggestedFileName, options.SuggestedFileType, options.FileTypeChoices,
options.DefaultExtension, options.ShowOverwritePrompt ?? false)
.ConfigureAwait(false);
var file = files?.FirstOrDefault() is { } path
@ -92,7 +92,7 @@ namespace Avalonia.X11.NativeDialogs
private unsafe Task<(string[]? files, FilePickerFileType? selectedFilter)> ShowDialog(string? title,
IWindowImpl parent, GtkFileChooserAction action,
bool multiSelect, IStorageFolder? initialFolder, string? initialFileName,
bool multiSelect, IStorageFolder? initialFolder, string? initialFileName, FilePickerFileType? suggestedFileType,
IEnumerable<FilePickerFileType>? filters, string? defaultExtension, bool overwritePrompt)
{
IntPtr dlg;
@ -165,8 +165,14 @@ namespace Avalonia.X11.NativeDialogs
}
gtk_file_chooser_add_filter(dlg, filter);
if (suggestedFileType != null && suggestedFileType == f)
{
gtk_file_chooser_set_filter(dlg, filter);
}
}
}
}
disposables = new List<IDisposable>

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

@ -4,6 +4,7 @@ using System.IO;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia.Controls.Utils;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
using Avalonia.Win32.Interop;
@ -33,7 +34,7 @@ namespace Avalonia.Win32
var (folders, _) = await ShowFilePicker(
true, true,
options.AllowMultiple, false,
options.Title, options.SuggestedFileName, options.SuggestedStartLocation, null, null,
options.Title, options.SuggestedFileName, null, options.SuggestedStartLocation, null, null,
f => new BclStorageFolder(new DirectoryInfo(f)))
.ConfigureAwait(false);
return folders;
@ -44,7 +45,7 @@ namespace Avalonia.Win32
var (files, _) = await ShowFilePicker(
true, false,
options.AllowMultiple, false,
options.Title, options.SuggestedFileName, options.SuggestedStartLocation,
options.Title, options.SuggestedFileName, options.SuggestedFileType, options.SuggestedStartLocation,
null, options.FileTypeFilter,
f => new BclStorageFile(new FileInfo(f)))
.ConfigureAwait(false);
@ -56,7 +57,7 @@ namespace Avalonia.Win32
var (files, _) = await ShowFilePicker(
false, false,
false, options.ShowOverwritePrompt,
options.Title, options.SuggestedFileName, options.SuggestedStartLocation,
options.Title, options.SuggestedFileName, options.SuggestedFileType, options.SuggestedStartLocation,
options.DefaultExtension, options.FileTypeChoices,
f => new BclStorageFile(new FileInfo(f)))
.ConfigureAwait(false);
@ -68,7 +69,7 @@ namespace Avalonia.Win32
var (files, index) = await ShowFilePicker(
false, false,
false, options.ShowOverwritePrompt,
options.Title, options.SuggestedFileName, options.SuggestedStartLocation,
options.Title, options.SuggestedFileName, options.SuggestedFileType, options.SuggestedStartLocation,
options.DefaultExtension, options.FileTypeChoices,
f => new BclStorageFile(new FileInfo(f)))
.ConfigureAwait(false);
@ -88,6 +89,7 @@ namespace Avalonia.Win32
bool? showOverwritePrompt,
string? title,
string? suggestedFileName,
FilePickerFileType? suggestedFileType,
IStorageFolder? folder,
string? defaultExtension,
IReadOnlyList<FilePickerFileType>? filters,
@ -118,6 +120,7 @@ namespace Avalonia.Win32
{
options &= ~FILEOPENDIALOGOPTIONS.FOS_OVERWRITEPROMPT;
}
frm.SetOptions(options);
defaultExtension ??= string.Empty;
@ -152,6 +155,12 @@ namespace Avalonia.Win32
}
}
if (suggestedFileType != null &&
filters?.IndexOf(suggestedFileType) is { } fi and > -1)
{
frm.SetFileTypeIndex((uint)(fi + 1));
}
if (folder?.TryGetLocalPath() is { } folderPath)
{
var riid = UnmanagedMethods.ShellIds.IShellItem;

Loading…
Cancel
Save