Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions dev/Interop/StoragePickers/FileOpenPicker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
3 changes: 3 additions & 0 deletions dev/Interop/StoragePickers/FileSavePicker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 8 additions & 2 deletions dev/Interop/StoragePickers/FolderPicker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
19 changes: 19 additions & 0 deletions dev/Interop/StoragePickers/ManualTests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
21 changes: 21 additions & 0 deletions dev/Interop/StoragePickers/PickerCommon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<WslNodeRevealer*>(timerId)->RevealWslNodeWhenReady(hwnd);
Expand Down
22 changes: 22 additions & 0 deletions dev/Interop/StoragePickers/PickerCommon.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <winrt/Windows.Security.Cryptography.h>
#include <winrt/Windows.Security.Cryptography.Core.h>
#include <winrt/Microsoft.UI.Windowing.h>
#include <winrt/Microsoft.UI.Dispatching.h>

namespace PickerCommon {
winrt::hstring GetPathFromShellItem(winrt::com_ptr<IShellItem> shellItem);
Expand All @@ -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.
Expand Down