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;