diff --git a/specs/Storage.Pickers/FileOpenPicker.md b/specs/Storage.Pickers/FileOpenPicker.md index 525145381b..fe8b3230d1 100644 --- a/specs/Storage.Pickers/FileOpenPicker.md +++ b/specs/Storage.Pickers/FileOpenPicker.md @@ -19,9 +19,12 @@ runtimeclass FileOpenPicker FileOpenPicker(Microsoft.UI.WindowId windowId); string CommitButtonText; + string Title; + string SettingsIdentifier; IMap> FileTypeChoices{ get; }; IVector FileTypeFilter{ get; }; + Int32 DefaultFileTypeFilterIndex; string SuggestedFolder; String SuggestedStartFolder; @@ -65,6 +68,14 @@ var openPicker = new FileOpenPicker(this.AppWindow.Id) // If not specified, the system uses a default label of "Open" (suitably translated). CommitButtonText = "Choose selected files", + // (Optional) specify the title of the picker. + // If not specified, the system uses a default title. + Title = "Open File", + + // (Optional) specify the settings identifier of the picker. + // It allows the picker to remember its state (e.g. size, location, etc) across sessions. + SettingsIdentifier = "MySettingsIdentifier", + // (Optional) group file types into labeled choices // FileTypeChoices takes precedence over FileTypeFilter when both defined. FileTypeChoices = { @@ -75,6 +86,13 @@ var openPicker = new FileOpenPicker(this.AppWindow.Id) // (Optional) specify file extension filters. If not specified, defaults to all files (*.*). FileTypeFilter = { ".txt", ".pdf", ".doc", ".docx" }, + // (Optional) specify the index of the file type filter to be selected by default. + // The index is 0-based. + // When not specified, its value is -1 and the filter follows API's behavior. That is: + // When FileTypeFilter is in effect, auto-select the last one (All Files). + // Otherwise, auto-select the first one. + DefaultFileTypeFilterIndex = 1, // auto select Pictures + // (Optional) specify the view mode of the picker dialog. If not specified, defaults to List. ViewMode = PickerViewMode.List, }; @@ -109,6 +127,14 @@ openPicker.SuggestedStartLocation(PickerLocationId::DocumentsLibrary); // If not specified, the system uses a default label of "Open" (suitably translated). openPicker.CommitButtonText(L"Choose selected files"); +// (Optional) specify the title of the picker. +// If not specified, the system uses a default title. +openPicker.Title(L"Open File"); + +// (Optional) specify the settings identifier of the picker. +// It allows the picker to remember its state (e.g. size, location, etc) across sessions. +openPicker.SettingsIdentifier(L"MySettingsIdentifier"); + // (Optional) group file types into labeled choices // FileTypeChoices takes precedence over FileTypeFilter when both defined. auto choices = openPicker.FileTypeChoices(); @@ -118,6 +144,13 @@ choices.Insert(L"Pictures", winrt::single_threaded_vector({ L".p // (Optional) specify file extension filters. If not specified, defaults to all files (*.*). openPicker.FileTypeFilter().ReplaceAll({ L".txt", L".pdf", L".doc", L".docx" }); +// (Optional) specify the index of the file type filter to be selected by default. +// The index is 0-based. +// When not specified, its value is -1 and the filter follows API's behavior. That is: +// When FileTypeFilter is in effect, auto-select the last one (All Files). +// Otherwise, auto-select the first one. +openPicker.DefaultFileTypeFilterIndex(1); // auto select Pictures + // (Optional) specify the view mode of the picker dialog. If not specified, defaults to List. openPicker.ViewMode(PickerViewMode::List); ``` diff --git a/specs/Storage.Pickers/FileSavePicker.md b/specs/Storage.Pickers/FileSavePicker.md index 4d28aeab5f..6a3f1b88e9 100644 --- a/specs/Storage.Pickers/FileSavePicker.md +++ b/specs/Storage.Pickers/FileSavePicker.md @@ -17,10 +17,17 @@ runtimeclass FileSavePicker FileSavePicker(Microsoft.UI.WindowId windowId); string CommitButtonText; + string Title; + string SettingsIdentifier; + string DefaultFileExtension; string SuggestedFileName; IMap> FileTypeChoices{ get; }; + Int32 DefaultFileTypeFilterIndex; + + Boolean ShowOverwritePrompt; + Boolean CreateNewFileIfNotExists; string SuggestedFolder; String SuggestedStartFolder; @@ -64,14 +71,38 @@ var savePicker = new FileSavePicker(this.AppWindow.Id) // If not specified, the system uses a default label of "Save" (suitably translated). CommitButtonText = "Save Document", + // (Optional) specify the title of the picker. + // If not specified, the system uses a default title. + Title = "Save File", + + // (Optional) specify the settings identifier of the picker. + // It allows the picker to remember its state (e.g. size, location, etc) across sessions. + SettingsIdentifier = "MySettingsIdentifier", + // (Optional) categorized extension types. If not specified, "All Files (*.*)" is allowed. // Note that when "All Files (*.*)" is allowed, end users can save a file without an extension. FileTypeChoices = { - { "Documents", new List { ".txt", ".doc", ".docx" } } + { "Text", new List {".txt"} }, + { "Documents", new List { ".doc", ".docx" } } }, + // (Optional) specify the index of the file type filter to be selected by default. + // The index is 0-based. + // When not specified, its value is -1. + DefaultFileTypeFilterIndex = 1, // this will auto-select Documents + + // (Optional) Show a warning prompt of file overwrite when user tries to pick an existing file. + // set to true by default. + ShowOverwritePrompt = true, + + // (Optional) create an empty file when the picked file does not yet exist. + // set to true by default. + CreateNewFileIfNotExists = true, + // (Optional) specify the default file extension (will be appended to SuggestedFileName). - // If not specified, no extension will be appended. + // Note: the default extension applies when the active filter is "All Files (*)" + // or includes multiple extensions, and the default extension is one of them. + // If not applied, no extension will be appended. DefaultFileExtension = ".txt", }; ``` @@ -108,12 +139,36 @@ savePicker.SuggestedFileName(L"NewDocument"); // If not specified, the system uses a default label of "Save" (suitably translated). savePicker.CommitButtonText(L"Save Document"); +// (Optional) specify the title of the picker. +// If not specified, the system uses a default title. +savePicker.Title(L"Save File"); + +// (Optional) specify the settings identifier of the picker. +// It allows the picker to remember its state (e.g. size, location, etc) across sessions. +savePicker.SettingsIdentifier(L"MySettingsIdentifier"); + // (Optional) categorized extension types. If not specified, "All Files (*.*)" is allowed. // Note that when "All Files (*.*)" is allowed, end users can save a file without an extension. -savePicker.FileTypeChoices().Insert(L"Text", winrt::single_threaded_vector({ L".txt" })); +savePicker.FileTypeChoices().Insert(L"Texts", winrt::single_threaded_vector({ L".txt" })); +savePicker.FileTypeChoices().Insert(L"Documents", winrt::single_threaded_vector({ L".doc", L".docx" })); + +// (Optional) specify the index of the file type filter to be selected by default. +// The index is 0-based. +// When not specified, its value is -1. +savePicker.DefaultFileTypeFilterIndex(1); // this will auto-select Documents + +// (Optional) Show a warning prompt of file overwrite when user tries to pick an existing file. +// set to true by default. +savePicker.ShowOverwritePrompt(true); + +// (Optional) create an empty file when the picked file does not yet exist. +// set to true by default. +savePicker.CreateNewFileIfNotExists(true); // (Optional) specify the default file extension (will be appended to SuggestedFileName). -// If not specified, no extension will be appended. +// Note: the default extension applies when the selected filter is "All Files (*)" +// or includes multiple extensions, and the default extension is one of them. +// If not applied, no extension will be appended. savePicker.DefaultFileExtension(L".txt"); ``` diff --git a/specs/Storage.Pickers/FolderPicker.md b/specs/Storage.Pickers/FolderPicker.md index 6b0592cc76..5571b9068d 100644 --- a/specs/Storage.Pickers/FolderPicker.md +++ b/specs/Storage.Pickers/FolderPicker.md @@ -19,6 +19,8 @@ runtimeclass FolderPicker FolderPicker(Microsoft.UI.WindowId windowId); string CommitButtonText; + string Title; + string SettingsIdentifier; string SuggestedFolder; String SuggestedStartFolder; @@ -27,6 +29,7 @@ runtimeclass FolderPicker PickerViewMode ViewMode; Windows.Foundation.IAsyncOperation PickSingleFolderAsync(); + Windows.Foundation.IAsyncOperation> PickMultipleFoldersAsync(); } ``` @@ -66,6 +69,14 @@ var folderPicker = new FolderPicker(this.AppWindow.Id) // If not specified, the system uses a default label of "Open" (suitably translated). CommitButtonText = "Select Folder", + // (Optional) specify the title of the picker. + // If not specified, the system uses a default title. + Title = "Select Folder", + + // (Optional) specify the settings identifier of the picker. + // It allows the picker to remember its state (e.g. size, location, etc) across sessions. + SettingsIdentifier = "MySettingsIdentifier", + // (Optional) specify the view mode of the picker dialog. If not specified, default to List. ViewMode = PickerViewMode.List, }; @@ -100,6 +111,14 @@ folderPicker.SuggestedStartLocation(PickerLocationId::DocumentsLibrary); // If not specified, the system uses a default label of "Open" (suitably translated). folderPicker.CommitButtonText(L"Select Folder"); +// (Optional) specify the title of the picker. +// If not specified, the system uses a default title. +folderPicker.Title(L"Select Folder"); + +// (Optional) specify the settings identifier of the picker. +// It allows the picker to remember its state (e.g. size, location, etc) across sessions. +folderPicker.SettingsIdentifier(L"MySettingsIdentifier"); + // (Optional) specify the view mode of the picker dialog. If not specified, default to List. folderPicker.ViewMode(PickerViewMode::List); ``` @@ -148,6 +167,58 @@ else } ``` +## FolderPicker.PickMultipleFoldersAsync + +Displays a UI element that allows the user to choose multiple folders. + +Returns a collection of lightweight objects that have the path of the picked folders. + +Returns an empty list (`Count` = 0) if the folder dialog was cancelled or closed without a selection. + +### Examples + +C# + +```C# +using Microsoft.Windows.Storage.Pickers; + +var folderPicker = new FolderPicker(this.AppWindow.Id); + +var results = await folderPicker.PickMultipleFoldersAsync(); +if (results.Count > 0) +{ + var pickedFolderPaths = results.Select(f => f.Path); + foreach (var path in pickedFolderPaths) + { + // Do something with the folder path + } +} +else +{ + // error handling. +} +``` + +C++ +```C++ +#include +using namespace winrt::Microsoft::Windows::Storage::Pickers; + +FolderPicker folderPicker(AppWindow().Id()); +auto results{ co_await folderPicker.PickMultipleFoldersAsync() }; +if (results.Size() > 0) +{ + for (auto const& result : results) + { + auto path{ result.Path() }; + } +} +else +{ + // error handling. +} +``` + # See Also [PickFolderResult](./PickFolderResult.md) diff --git a/specs/Storage.Pickers/Microsoft.Windows.Storage.Pickers.md b/specs/Storage.Pickers/Microsoft.Windows.Storage.Pickers.md index 2036ab3ff6..3d1b786dc6 100644 --- a/specs/Storage.Pickers/Microsoft.Windows.Storage.Pickers.md +++ b/specs/Storage.Pickers/Microsoft.Windows.Storage.Pickers.md @@ -57,6 +57,28 @@ to `SuggestedStartLocation`, then to the system default. catagorized filter types. When both `FileTypeChoices` and `FileTypeFilter` are provided, `FileTypeChoices` is used and `FileTypeFilter` is ignored. +1. Adding `DefaultFileTypeFilterIndex` for `FileOpenPicker` and `FileSavePicker`. This allows +setting the default selected file type filter index. Note this index is 0-based. When it is +-1 (the default value), the selected filter might be override by the API's default behavior. + +1. The property `SettingsIdentifier` for all 3 pickers will be available in the new Storage.Pickers +APIs from WindowsAppSDK2.0. `SettingsIdentifier` allows the picker to remember its state (e.g. size, +location, etc) across sessions. When two different apps use the same string for SettingsIdentifier +property, they will have their respective independent states (Read more in Note 2). + +1. Adding `ShowOverwritePrompt` for `FileSavePicker`. This Boolean properties default to `true` and +control whether the picker warns about overwriting when user picked an existing file via +FileSavePicker. + +1. Adding `CreateNewFileIfNotExists` for `FileSavePicker`. This Boolean properties default to `true` +and control whether to auto-create the picked file when it doesn't exist. + +1. Adding `Title` for all 3 pickers. `Title` allows setting the title of the picker dialog. + +1. Adding `PickMultipleFoldersAsync` for `FolderPicker`. This allows selecting multiple folders in +the folder picker dialog. + + # Conceptual pages # API @@ -119,9 +141,12 @@ namespace Microsoft.Windows.Storage.Pickers FileOpenPicker(Microsoft.UI.WindowId windowId); string CommitButtonText; + string Title; + string SettingsIdentifier; IMap> FileTypeChoices{ get; }; IVector FileTypeFilter{ get; }; + int DefaultFileTypeFilterIndex; string SuggestedFolder; string SuggestedStartFolder; @@ -138,10 +163,17 @@ namespace Microsoft.Windows.Storage.Pickers FileSavePicker(Microsoft.UI.WindowId windowId); string CommitButtonText; + string Title; + string SettingsIdentifier; + string DefaultFileExtension; string SuggestedFileName; IMap> FileTypeChoices{ get; }; + int DefaultFileTypeFilterIndex; + + bool ShowOverwritePrompt; + bool CreateNewFileIfNotExists; string SuggestedFolder; string SuggestedStartFolder; @@ -155,6 +187,8 @@ namespace Microsoft.Windows.Storage.Pickers FolderPicker(Microsoft.UI.WindowId windowId); string CommitButtonText; + string Title; + string SettingsIdentifier; string SuggestedFolder; string SuggestedStartFolder; @@ -163,11 +197,14 @@ namespace Microsoft.Windows.Storage.Pickers PickerViewMode ViewMode; Windows.Foundation.IAsyncOperation PickSingleFolderAsync(); + Windows.Foundation.IAsyncOperation> PickMultipleFoldersAsync(); } } ``` -Note: **Understanding SuggestedStartFolder/SuggestedStartLocation vs SuggestedFolder:** +# Notes + +Note 1: **Understanding SuggestedStartFolder/SuggestedStartLocation vs SuggestedFolder:** These two kinds of properties have fundamentally different behaviors in terms of when and how they affect the picker: @@ -187,3 +224,22 @@ affect the picker: navigation history. `SuggestedStartFolder` takes precedence over `SuggestedStartLocation` when both specified. + +Note 2: **The implementation of SettingsIdentifier** + +When an app sets `SettingsIdentifier`, the picker persists its window placement and navigation +history through the underlying [`IFileDialog::SetClientGuid`](https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ifiledialog-setclientguid) +API. The implementation derives that `ClientGuid` as follows: + +- First it tries to obtain the packaged app's Identity via [`GetCurrentApplicationUserModelId`](https://learn.microsoft.com/en-us/windows/win32/api/appmodel/nf-appmodel-getcurrentapplicationusermodelid). +- If the process has no package identity found (typical for unpackaged apps, win32 apps, etc.), + it falls back to the full path to the running executable retrieved from `wil::GetModuleFileNameW`. +- The chosen identifier is concatenated with the caller-provided `SettingsIdentifier` value using a + `"|"` format. That string is then hashed with MD5 + and coerced into an GUID. +- The calculated GUID will be passed to set the `ClientGuid`. + +This means the feature works for both packaged and unpackaged apps. Packaged apps remain distinct by +their package identity, while unpackaged apps are differentiated by the absolute path of their +executable. As long as an app uses a stable combination of package identity (or executable path) and +`SettingsIdentifier`, the picker will reopen with the same saved settings across sessions.