diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml b/samples/ControlCatalog/Pages/DialogsPage.xaml
index 75d98827c1..7320c1f3d7 100644
--- a/samples/ControlCatalog/Pages/DialogsPage.xaml
+++ b/samples/ControlCatalog/Pages/DialogsPage.xaml
@@ -31,6 +31,12 @@
TXT mime only
TXT apple type id only
+
+ Use SuggestedFileType
+
+ First filter
+
+
Force managed dialog
diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs
index d755a1b14d..0e2ad7d2b9 100644
--- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs
@@ -37,6 +37,8 @@ namespace ControlCatalog.Pages
var openedFileContent = OpenedFileContent;
var openMultiple = OpenMultiple;
var currentFolderBox = CurrentFolderBox;
+ var useSuggestedFilter = UseSuggestedFilter;
+ var suggestedFilterSelector = SuggestedFilterSelector;
currentFolderBox.TextChanged += async (sender, args) =>
{
@@ -76,7 +78,7 @@ namespace ControlCatalog.Pages
}).ToList() ?? new List();
}
- List? GetFileTypes()
+ List? BuildFileTypes()
{
var selectedItem = (FilterSelector.SelectedItem as ComboBoxItem)?.Content
?? "None";
@@ -115,6 +117,64 @@ namespace ControlCatalog.Pages
};
}
+ List? GetFileTypes()
+ {
+ var types = BuildFileTypes();
+ UpdateSuggestedFilterSelector(types);
+ return types;
+ }
+
+ void UpdateSuggestedFilterSelector(IReadOnlyList? types)
+ {
+ var previouslySelected = (suggestedFilterSelector.SelectedItem as ComboBoxItem)?.Tag as FilePickerFileType;
+ suggestedFilterSelector.Items.Clear();
+ suggestedFilterSelector.Items.Add(new ComboBoxItem { Content = "First filter", Tag = null });
+
+ var desiredIndex = 0;
+ if (types is { Count: > 0 })
+ {
+ for (var i = 0; i < types.Count; i++)
+ {
+ var type = types[i];
+ var item = new ComboBoxItem { Content = type.Name, Tag = type };
+ suggestedFilterSelector.Items.Add(item);
+
+ if (previouslySelected is not null && ReferenceEquals(previouslySelected, type))
+ {
+ desiredIndex = i + 1;
+ }
+ }
+ }
+
+ suggestedFilterSelector.SelectedIndex = desiredIndex;
+ }
+
+ FilePickerFileType? GetSuggestedFileType(IReadOnlyList? types)
+ {
+ if (useSuggestedFilter.IsChecked == true && types is { Count: > 0 })
+ {
+ if (suggestedFilterSelector.SelectedItem is ComboBoxItem { Tag: FilePickerFileType selectedType }
+ && types.Any(t => ReferenceEquals(t, selectedType)))
+ {
+ return selectedType;
+ }
+
+ return types.FirstOrDefault();
+ }
+
+ return null;
+ }
+
+ void UpdateSuggestedFilterSelectorState() =>
+ suggestedFilterSelector.IsEnabled = useSuggestedFilter.IsChecked == true;
+
+ useSuggestedFilter.Checked += (_, _) => UpdateSuggestedFilterSelectorState();
+ useSuggestedFilter.Unchecked += (_, _) => UpdateSuggestedFilterSelectorState();
+ UpdateSuggestedFilterSelectorState();
+
+ FilterSelector.SelectionChanged += (_, _) => UpdateSuggestedFilterSelector(BuildFileTypes());
+ UpdateSuggestedFilterSelector(BuildFileTypes());
+
OpenFile.Click += async delegate
{
// Almost guaranteed to exist
@@ -229,10 +289,12 @@ namespace ControlCatalog.Pages
OpenFilePicker.Click += async delegate
{
+ var fileTypes = GetFileTypes();
var result = await GetStorageProvider().OpenFilePickerAsync(new FilePickerOpenOptions()
{
Title = "Open file",
- FileTypeFilter = GetFileTypes(),
+ FileTypeFilter = fileTypes,
+ SuggestedFileType = GetSuggestedFileType(fileTypes),
SuggestedFileName = "FileName",
SuggestedStartLocation = lastSelectedDirectory,
AllowMultiple = openMultiple.IsChecked == true
@@ -243,10 +305,12 @@ namespace ControlCatalog.Pages
SaveFilePicker.Click += async delegate
{
var fileTypes = GetFileTypes();
+ var suggestedType = GetSuggestedFileType(fileTypes);
var file = await GetStorageProvider().SaveFilePickerAsync(new FilePickerSaveOptions()
{
Title = "Save file",
FileTypeChoices = fileTypes,
+ SuggestedFileType = suggestedType,
SuggestedStartLocation = lastSelectedDirectory,
SuggestedFileName = "FileName",
ShowOverwritePrompt = true
@@ -278,10 +342,12 @@ namespace ControlCatalog.Pages
};
SaveFilePickerWithResult.Click += async delegate
{
+ var saveFileTypes = new[] { FilePickerFileTypes.Json, FilePickerFileTypes.Xml };
var result = await GetStorageProvider().SaveFilePickerWithResultAsync(new FilePickerSaveOptions()
{
Title = "Save file",
- FileTypeChoices = [FilePickerFileTypes.Json, FilePickerFileTypes.Xml],
+ FileTypeChoices = saveFileTypes,
+ SuggestedFileType = GetSuggestedFileType(saveFileTypes),
SuggestedStartLocation = lastSelectedDirectory,
SuggestedFileName = "FileName",
ShowOverwritePrompt = true
diff --git a/src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs b/src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs
index 1674ec11c8..cbbd68d2d5 100644
--- a/src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs
+++ b/src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs
@@ -7,6 +7,15 @@ namespace Avalonia.Platform.Storage;
///
public class FilePickerOpenOptions : PickerOptions
{
+ ///
+ /// Gets or sets the file type that should be preselected when the dialog is opened.
+ ///
+ ///
+ /// This value should reference one of the items in .
+ /// If not set, the first file type in may be selected by default.
+ ///
+ public FilePickerFileType? SuggestedFileType { get; set; }
+
///
/// Gets or sets an option indicating whether open picker allows users to select multiple files.
///
diff --git a/src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs b/src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs
index 267ba59c71..63b32829f2 100644
--- a/src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs
+++ b/src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs
@@ -7,6 +7,15 @@ namespace Avalonia.Platform.Storage;
///
public class FilePickerSaveOptions : PickerOptions
{
+ ///
+ /// Gets or sets the file type that should be preselected when the dialog is opened.
+ ///
+ ///
+ /// This value should reference one of the items in .
+ /// If not set, the first file type in may be selected by default.
+ ///
+ public FilePickerFileType? SuggestedFileType { get; set; }
+
///
/// Gets or sets the default extension to be used to save the file.
///
diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs
index 500eef9250..fd4f351302 100644
--- a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs
+++ b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs
@@ -63,8 +63,13 @@ namespace Avalonia.FreeDesktop
ObjectPath objectPath;
var chooserOptions = new Dictionary();
- 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();
- 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? fileTypes, out VariantValue result)
+ private static bool TryParseFilters(IReadOnlyList? 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>>>();
+ currentFilter = null;
foreach (var fileType in fileTypes)
{
@@ -228,7 +243,15 @@ namespace Avalonia.FreeDesktop
else
continue;
- filters.Add(Struct.Create(fileType.Name, new Array>(extensions)));
+ var filterStruct = Struct.Create(fileType.Name, new Array>(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();
diff --git a/src/Avalonia.Native/StorageProviderApi.cs b/src/Avalonia.Native/StorageProviderApi.cs
index 298a9d914d..8c737336b2 100644
--- a/src/Avalonia.Native/StorageProviderApi.cs
+++ b/src/Avalonia.Native/StorageProviderApi.cs
@@ -155,7 +155,7 @@ internal class StorageProviderApi(IAvnStorageProvider native, bool sandboxEnable
public async Task> 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? types,
- string? defaultExtension)
+ string? defaultExtension,
+ FilePickerFileType? suggestedType)
: NativeCallbackBase, IAvnFilePickerFileTypes
{
private readonly List _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)
diff --git a/src/Avalonia.X11/NativeDialogs/Gtk.cs b/src/Avalonia.X11/NativeDialogs/Gtk.cs
index 3138bdb22f..a8def2c2b0 100644
--- a/src/Avalonia.X11/NativeDialogs/Gtk.cs
+++ b/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);
diff --git a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
index 75aff32b13..05a3c52c54 100644
--- a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
+++ b/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();
@@ -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();
@@ -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? 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
diff --git a/src/Windows/Avalonia.Win32/Win32StorageProvider.cs b/src/Windows/Avalonia.Win32/Win32StorageProvider.cs
index 7fa782f6fd..ba5ffeab8b 100644
--- a/src/Windows/Avalonia.Win32/Win32StorageProvider.cs
+++ b/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? 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;