diff --git a/dev/Interop/StoragePickers/FileOpenPicker.cpp b/dev/Interop/StoragePickers/FileOpenPicker.cpp index 775e6411ab..74cd25d930 100644 --- a/dev/Interop/StoragePickers/FileOpenPicker.cpp +++ b/dev/Interop/StoragePickers/FileOpenPicker.cpp @@ -13,6 +13,10 @@ #include "PickerCommon.h" #include "PickFileResult.h" #include "PickerLocalization.h" +#include + +// Bug 63006043: [1.8 servicing] Fix focus not restored after Storage Pickers dialog closes +#define WINAPPSDK_CHANGEID_63006043 63006043, WinAppSDK_1_8_11 namespace winrt::Microsoft::Windows::Storage::Pickers::implementation { @@ -74,6 +78,13 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation CaptureParameters(parameters); + std::optional focusRestorer; + if (WinAppSdk::Containment::IsChangeEnabled()) + { + // Capture focus on the UI thread so it can be restored after the dialog closes (issue #6505). + focusRestorer.emplace(); + } + auto cancellationToken = co_await winrt::get_cancellation_token(); cancellationToken.enable_propagation(true); co_await winrt::resume_background(); @@ -127,6 +138,13 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation CaptureParameters(parameters); + std::optional focusRestorer; + if (WinAppSdk::Containment::IsChangeEnabled()) + { + // Capture focus on the UI thread so it can be restored after the dialog closes (issue #6505). + focusRestorer.emplace(); + } + 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 dad4ef48f6..d89260990c 100644 --- a/dev/Interop/StoragePickers/FileSavePicker.cpp +++ b/dev/Interop/StoragePickers/FileSavePicker.cpp @@ -23,6 +23,9 @@ // Bug 60257559: [1.8 servicing] Bugfix: FileSavePicker.PickSaveFileAsync() should not truncate file when the picked file exists. #define WINAPPSDK_CHANGEID_60257559 60257559, WinAppSDK_1_8_4 +// Bug 63006043: [1.8 servicing] Fix focus not restored after Storage Pickers dialog closes +#define WINAPPSDK_CHANGEID_63006043 63006043, WinAppSDK_1_8_11 + namespace winrt::Microsoft::Windows::Storage::Pickers::implementation { @@ -104,6 +107,13 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation CaptureParameters(parameters); + std::optional focusRestorer; + if (WinAppSdk::Containment::IsChangeEnabled()) + { + // Capture focus on the UI thread so it can be restored after the dialog closes (issue #6505). + focusRestorer.emplace(); + } + 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 c95a581607..fa1dd5f0e4 100644 --- a/dev/Interop/StoragePickers/FolderPicker.cpp +++ b/dev/Interop/StoragePickers/FolderPicker.cpp @@ -11,7 +11,11 @@ #include "TerminalVelocityFeatures-StoragePickers.h" #include "PickerCommon.h" #include "PickFolderResult.h" -#include "PickerLocalization.h" +#include "PickerLocalization.h" +#include + +// Bug 63006043: [1.8 servicing] Fix focus not restored after Storage Pickers dialog closes +#define WINAPPSDK_CHANGEID_63006043 63006043, WinAppSDK_1_8_11 namespace winrt::Microsoft::Windows::Storage::Pickers::implementation { @@ -67,7 +71,14 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation parameters.AllFilesText = PickerLocalization::GetStoragePickersLocalizationText(PickerCommon::AllFilesLocalizationKey); CaptureParameters(parameters); - + + std::optional focusRestorer; + if (WinAppSdk::Containment::IsChangeEnabled()) + { + // Capture focus on the UI thread so it can be restored after the dialog closes (issue #6505). + focusRestorer.emplace(); + } + 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 b89e6aaade..26ffa30120 100644 --- a/dev/Interop/StoragePickers/ManualTests/README.md +++ b/dev/Interop/StoragePickers/ManualTests/README.md @@ -56,5 +56,21 @@ auto& file = co_await picker.PickSaveFileAsync(); **Example of unexpected behavior - without the fix:** ![alter-extension-with-choices-beforefix](./media/alter-extension-with-choices-beforefix.gif) +### 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`. +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 c0811a0edf..08ca2249f9 100644 --- a/dev/Interop/StoragePickers/PickerCommon.cpp +++ b/dev/Interop/StoragePickers/PickerCommon.cpp @@ -105,6 +105,28 @@ 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); + }); + } + } + bool IsHStringNullOrEmpty(winrt::hstring value) { return value.empty(); diff --git a/dev/Interop/StoragePickers/PickerCommon.h b/dev/Interop/StoragePickers/PickerCommon.h index 124a9eb261..3f29156584 100644 --- a/dev/Interop/StoragePickers/PickerCommon.h +++ b/dev/Interop/StoragePickers/PickerCommon.h @@ -8,6 +8,8 @@ #include #include #include +#include +#include namespace PickerCommon { winrt::hstring GetPathFromShellItem(winrt::com_ptr shellItem); @@ -28,6 +30,27 @@ namespace PickerCommon { void ValidateSuggestedFileName(winrt::hstring const& suggestedFileName); void ValidateSuggestedFolder(winrt::hstring const& path); + // 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 }; + }; + struct PickerParameters { HWND HWnd{}; winrt::hstring CommitButtonText; diff --git a/dev/RuntimeCompatibilityOptions/RuntimeCompatibilityOptions.idl b/dev/RuntimeCompatibilityOptions/RuntimeCompatibilityOptions.idl index debf9a1634..186fc1434d 100644 --- a/dev/RuntimeCompatibilityOptions/RuntimeCompatibilityOptions.idl +++ b/dev/RuntimeCompatibilityOptions/RuntimeCompatibilityOptions.idl @@ -60,6 +60,9 @@ namespace Microsoft.Windows.ApplicationModel.WindowsAppRuntime // 1.8.10 ThemeSettings_OffThreadDestructorFix = 62451730, + + // 1.8.11 + StoragePickers_RestoreFocusAfterDialogCloses = 63006043, }; /// Represents a version of the Windows App Runtime.