diff --git a/dev/Interop/StoragePickers/FileOpenPicker.cpp b/dev/Interop/StoragePickers/FileOpenPicker.cpp index 77a4ae29c8..f71c2e39a6 100644 --- a/dev/Interop/StoragePickers/FileOpenPicker.cpp +++ b/dev/Interop/StoragePickers/FileOpenPicker.cpp @@ -128,6 +128,9 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation CaptureParameters(parameters); + // Capture focus on the UI thread so it can be restored after the dialog closes (issue #6505). + PickerCommon::DialogFocusRestorer focusRestorer{}; + auto cancellationToken = co_await winrt::get_cancellation_token(); cancellationToken.enable_propagation(true); co_await winrt::resume_background(); @@ -176,6 +179,9 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation CaptureParameters(parameters); + // Capture focus on the UI thread so it can be restored after the dialog closes (issue #6505). + PickerCommon::DialogFocusRestorer focusRestorer{}; + auto cancellationToken = co_await winrt::get_cancellation_token(); cancellationToken.enable_propagation(true); co_await winrt::resume_background(); diff --git a/dev/Interop/StoragePickers/FileSavePicker.cpp b/dev/Interop/StoragePickers/FileSavePicker.cpp index 33e43d248e..9b91358ced 100644 --- a/dev/Interop/StoragePickers/FileSavePicker.cpp +++ b/dev/Interop/StoragePickers/FileSavePicker.cpp @@ -154,6 +154,9 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation CaptureParameters(parameters); + // Capture focus on the UI thread so it can be restored after the dialog closes (issue #6505). + PickerCommon::DialogFocusRestorer focusRestorer{}; + auto defaultFileExtension = m_defaultFileExtension; auto suggestedFolder = m_suggestedFolder; auto suggestedFileName = m_suggestedFileName; diff --git a/dev/Interop/StoragePickers/FolderPicker.cpp b/dev/Interop/StoragePickers/FolderPicker.cpp index 3b5e156ac7..e66b9b797f 100644 --- a/dev/Interop/StoragePickers/FolderPicker.cpp +++ b/dev/Interop/StoragePickers/FolderPicker.cpp @@ -104,7 +104,10 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation PickerCommon::PickerParameters parameters{}; CaptureParameters(parameters); - + + // Capture focus on the UI thread so it can be restored after the dialog closes (issue #6505). + PickerCommon::DialogFocusRestorer focusRestorer{}; + auto cancellationToken = co_await winrt::get_cancellation_token(); cancellationToken.enable_propagation(true); co_await winrt::resume_background(); @@ -164,7 +167,10 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation PickerCommon::PickerParameters parameters{}; CaptureParameters(parameters); - + + // Capture focus on the UI thread so it can be restored after the dialog closes (issue #6505). + PickerCommon::DialogFocusRestorer focusRestorer{}; + auto cancellationToken = co_await winrt::get_cancellation_token(); cancellationToken.enable_propagation(true); co_await winrt::resume_background(); diff --git a/dev/Interop/StoragePickers/ManualTests/README.md b/dev/Interop/StoragePickers/ManualTests/README.md index 5321b582dc..547ee28ea3 100644 --- a/dev/Interop/StoragePickers/ManualTests/README.md +++ b/dev/Interop/StoragePickers/ManualTests/README.md @@ -77,3 +77,22 @@ else } ``` Expect to print out all selected folders. + +### Keyboard focus is restored after the dialog closes +Run from a WinUI 3 app with a button whose Click handler opens a picker. This applies to every pick +method: `FileOpenPicker.PickSingleFileAsync` / `PickMultipleFilesAsync`, `FileSavePicker.PickSaveFileAsync`, +and `FolderPicker.PickSingleFolderAsync` / `PickMultipleFoldersAsync`. + +Test code (C++), in the button's Click handler: +```C++ +winrt::Microsoft::Windows::Storage::Pickers::FileOpenPicker picker{ AppWindow().Id() }; +co_await picker.PickSingleFileAsync(); +``` + +1. Click the button. The file dialog opens. +2. Pick a file and confirm. The dialog closes. +3. Without touching the mouse, press Enter. + + **Expected behavior - with the fix:** Enter re-invokes the button and the dialog reopens. + + **Example of unexpected behavior - without the fix:** Enter does nothing. diff --git a/dev/Interop/StoragePickers/PickerCommon.cpp b/dev/Interop/StoragePickers/PickerCommon.cpp index 9aff9355c3..1a6f9cb46b 100644 --- a/dev/Interop/StoragePickers/PickerCommon.cpp +++ b/dev/Interop/StoragePickers/PickerCommon.cpp @@ -144,6 +144,27 @@ namespace { namespace PickerCommon { using namespace winrt; + DialogFocusRestorer::DialogFocusRestorer() + { + // Capture on the UI thread: GetFocus returns the focused window of the calling + // thread's message queue, which is the WinUI content island hosting the focused element. + m_focusedWindow = ::GetFocus(); + m_dispatcherQueue = winrt::Microsoft::UI::Dispatching::DispatcherQueue::GetForCurrentThread(); + } + + DialogFocusRestorer::~DialogFocusRestorer() + { + if (m_focusedWindow && m_dispatcherQueue) + { + HWND focusedWindow = m_focusedWindow; + // Marshal back to the UI thread so SetFocus runs on the thread that owns the window. + m_dispatcherQueue.TryEnqueue([focusedWindow]() + { + ::SetFocus(focusedWindow); + }); + } + } + void CALLBACK WslNodeRevealer::PollTimerProc(HWND hwnd, UINT, UINT_PTR timerId, DWORD) noexcept { reinterpret_cast(timerId)->RevealWslNodeWhenReady(hwnd); diff --git a/dev/Interop/StoragePickers/PickerCommon.h b/dev/Interop/StoragePickers/PickerCommon.h index 2d23e4df32..149b8915c1 100644 --- a/dev/Interop/StoragePickers/PickerCommon.h +++ b/dev/Interop/StoragePickers/PickerCommon.h @@ -8,6 +8,7 @@ #include #include #include +#include namespace PickerCommon { winrt::hstring GetPathFromShellItem(winrt::com_ptr shellItem); @@ -32,6 +33,27 @@ namespace PickerCommon { void ValidateFolderPath(winrt::hstring const& path, std::string const& propertyName); void ValidateInitialFileTypeIndex(int const& value); + // Restores keyboard focus to the WinUI window after a COM file dialog closes. + // + // These dialogs run on a background thread (winrt::resume_background), so the SetFocus they + // perform on close comes from a thread that does not own the window and is ignored, leaving the + // window unable to receive key presses until the user clicks it again (issue #6505). + // + // Construct this RAII helper on the UI thread before switching to the background thread: it + // captures the focused window at construction and, on destruction, marshals SetFocus back to + // the UI thread via its DispatcherQueue, restoring focus once the dialog has closed. + struct DialogFocusRestorer + { + DialogFocusRestorer(); + ~DialogFocusRestorer(); + DialogFocusRestorer(DialogFocusRestorer const&) = delete; + DialogFocusRestorer& operator=(DialogFocusRestorer const&) = delete; + + private: + HWND m_focusedWindow{ nullptr }; + winrt::Microsoft::UI::Dispatching::DispatcherQueue m_dispatcherQueue{ nullptr }; + }; + // Shows the WSL node in the navigation pane of COM file dialogs that hide it by default. // The WSL node was hidden in FileSavePicker (IFileSaveDialog) and FolderPicker (IFileOpenDialog + FOS_PICKFOLDERS + FOS_FORCEFILESYSTEM); // It takes time for the navigation pane to load nodes.