Skip to content

Commit 27a8089

Browse files
authored
Display the wsl node in FolderPicker and FileSavePicker dialog navigation (#6326)
## Problem The WSL navigation node (`\\wsl.localhost`) is hidden in the navigation pane of `FolderPicker` and `FileSavePicker` dialogs due to undocumented behavior in the Windows Common Item Dialog. `FileOpenPicker` is unaffected. ## Fix `PickerCommon::WslNodeRevealer` - a per-dialog `IFileDialogEvents` handler - is registered before `Show()`. On the first `OnFolderChange` it: 1. Walks `IServiceProvider` -> `SID_STopLevelBrowser` -> `IShellBrowser` -> `INameSpaceTreeControl`. 2. Resolves the WSL root shell item (`\\wsl.localhost`, falling back to `\\wsl$`). 3. Polls (10 ms, up to 1 s) for navigation-pane root nodes to load; when the WSL node is found among their children, calls `INameSpaceTreeControl::SetItemState` to reveal it. ### Changes - **PickerCommon.h / PickerCommon.cpp** : Added `WslNodeRevealer` with per-instance state. `PollTimerProc` is a `static` trampoline (required by `TIMERPROC`) that delegates to `RevealWslNodeWhenReady`; all other methods are non-static. - **FileSavePicker.cpp** : Register `WslNodeRevealer` in `PickSaveFileAsync`. - **FolderPicker.cpp** : Register `WslNodeRevealer` in `PickSingleFolderAsync` and `PickMultipleFoldersAsync`.
1 parent 2ef437a commit 27a8089

4 files changed

Lines changed: 233 additions & 0 deletions

File tree

dev/Interop/StoragePickers/FileSavePicker.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,19 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation
188188
check_hresult(dialog->GetOptions(&dialogOptions));
189189
check_hresult(dialog->SetOptions(dialogOptions | FOS_STRICTFILETYPES));
190190

191+
// Register event handler to show the WSL node in the navigation pane.
192+
// Use SUCCEEDED() instead of check_hresult() so the picker still opens if registration fails.
193+
auto wslRevealer = winrt::make_self<PickerCommon::WslNodeRevealer>();
194+
DWORD adviseCookie{};
195+
bool wslAdvised = SUCCEEDED(dialog->Advise(wslRevealer.as<IFileDialogEvents>().get(), &adviseCookie));
196+
auto unadvise = wil::scope_exit([&] {
197+
if (wslAdvised)
198+
{
199+
dialog->Unadvise(adviseCookie);
200+
}
201+
wslRevealer->CancelPendingReveal();
202+
});
203+
191204
if (FAILED(dialog->Show(parameters.HWnd)))
192205
{
193206
logTelemetry.Stop(m_telemetryHelper, false);

dev/Interop/StoragePickers/FolderPicker.cpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,19 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation
120120
parameters.ConfigureDialog(dialog);
121121
dialog->SetOptions(FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM);
122122

123+
// Register event handler to show the WSL node in the navigation pane.
124+
// Use SUCCEEDED() instead of check_hresult() so the picker still opens if registration fails.
125+
auto wslRevealer = winrt::make_self<PickerCommon::WslNodeRevealer>();
126+
DWORD adviseCookie{};
127+
bool wslAdvised = SUCCEEDED(dialog->Advise(wslRevealer.as<IFileDialogEvents>().get(), &adviseCookie));
128+
auto unadvise = wil::scope_exit([&] {
129+
if (wslAdvised)
130+
{
131+
dialog->Unadvise(adviseCookie);
132+
}
133+
wslRevealer->CancelPendingReveal();
134+
});
135+
123136
if (FAILED(dialog->Show(parameters.HWnd)) || cancellationToken())
124137
{
125138
logTelemetry.Stop(m_telemetryHelper, false);
@@ -170,6 +183,19 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation
170183
check_hresult(dialog->GetOptions(&dialogOptions));
171184
check_hresult(dialog->SetOptions(dialogOptions | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_ALLOWMULTISELECT));
172185

186+
// Register event handler to show the WSL node in the navigation pane.
187+
// Use SUCCEEDED() instead of check_hresult() so the picker still opens if registration fails.
188+
auto wslRevealer = winrt::make_self<PickerCommon::WslNodeRevealer>();
189+
DWORD adviseCookie{};
190+
bool wslAdvised = SUCCEEDED(dialog->Advise(wslRevealer.as<IFileDialogEvents>().get(), &adviseCookie));
191+
auto unadvise = wil::scope_exit([&] {
192+
if (wslAdvised)
193+
{
194+
dialog->Unadvise(adviseCookie);
195+
}
196+
wslRevealer->CancelPendingReveal();
197+
});
198+
173199
if (FAILED(dialog->Show(parameters.HWnd)) || cancellationToken())
174200
{
175201
logTelemetry.Stop(m_telemetryHelper, true, false);

dev/Interop/StoragePickers/PickerCommon.cpp

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include "ShObjIdl.h"
1111
#include "shobjidl_core.h"
1212
#include <KnownFolders.h>
13+
#include <shlguid.h>
1314
#include <filesystem>
1415
#include <format>
1516
#include <utility>
@@ -101,9 +102,170 @@ namespace {
101102

102103
}
103104

105+
namespace {
106+
107+
// Search immediate children of parentItem for the WSL item.
108+
// If found, make it visible via INameSpaceTreeControl::SetItemState.
109+
bool FindAndShowWslInChildren(winrt::com_ptr<IShellItem> const& wslItem, winrt::com_ptr<IShellItem> const& parentItem, winrt::com_ptr<INameSpaceTreeControl> const& nstc) noexcept
110+
{
111+
if (!parentItem || !wslItem || !nstc)
112+
{
113+
return false;
114+
}
115+
116+
winrt::com_ptr<IEnumShellItems> enumItems;
117+
if (FAILED(parentItem->BindToHandler(nullptr, BHID_EnumItems, IID_PPV_ARGS(enumItems.put()))) || !enumItems)
118+
{
119+
return false;
120+
}
121+
122+
winrt::com_ptr<IShellItem> childItem;
123+
ULONG fetched = 0;
124+
while (SUCCEEDED(enumItems->Next(1, childItem.put(), &fetched)) && fetched > 0)
125+
{
126+
if (childItem)
127+
{
128+
int order = 0;
129+
if (childItem->Compare(wslItem.get(), SICHINT_CANONICAL, &order) == S_OK && order == 0)
130+
{
131+
nstc->SetItemState(childItem.get(), NSTCIS_EXPANDED, NSTCIS_EXPANDED);
132+
return true;
133+
}
134+
childItem = nullptr;
135+
}
136+
fetched = 0;
137+
}
138+
139+
return false;
140+
}
141+
142+
}
143+
104144
namespace PickerCommon {
105145
using namespace winrt;
106146

147+
void CALLBACK WslNodeRevealer::PollTimerProc(HWND hwnd, UINT, UINT_PTR timerId, DWORD) noexcept
148+
{
149+
reinterpret_cast<WslNodeRevealer*>(timerId)->RevealWslNodeWhenReady(hwnd);
150+
}
151+
152+
void WslNodeRevealer::RevealWslNodeWhenReady(HWND hwnd) noexcept
153+
{
154+
if (!m_nstc || !m_wslItem || ++m_pollCount >= s_maxPollCount)
155+
{
156+
KillTimer(hwnd, reinterpret_cast<UINT_PTR>(this));
157+
m_timerPending = false;
158+
m_nstc = nullptr;
159+
m_wslItem = nullptr;
160+
return;
161+
}
162+
163+
winrt::com_ptr<IShellItemArray> roots;
164+
DWORD count = 0;
165+
if (SUCCEEDED(m_nstc->GetRootItems(roots.put())) && roots)
166+
{
167+
if (FAILED(roots->GetCount(&count)))
168+
{
169+
count = 0;
170+
}
171+
}
172+
173+
// Wait until at least one root node has loaded.
174+
if (count == 0)
175+
{
176+
return;
177+
}
178+
179+
KillTimer(hwnd, reinterpret_cast<UINT_PTR>(this));
180+
m_timerPending = false;
181+
182+
// Search for the WSL node in the immediate children of each root node.
183+
for (DWORD i = 0; i < count; i++)
184+
{
185+
winrt::com_ptr<IShellItem> rootItem;
186+
if (SUCCEEDED(roots->GetItemAt(i, rootItem.put())) && rootItem)
187+
{
188+
if (FindAndShowWslInChildren(m_wslItem, rootItem, m_nstc))
189+
{
190+
break;
191+
}
192+
}
193+
}
194+
195+
m_nstc = nullptr;
196+
m_wslItem = nullptr;
197+
m_pollCount = 0;
198+
}
199+
200+
void WslNodeRevealer::CancelPendingReveal() noexcept
201+
{
202+
if (m_timerPending)
203+
{
204+
KillTimer(m_timerHwnd, reinterpret_cast<UINT_PTR>(this));
205+
m_timerPending = false;
206+
m_timerHwnd = nullptr;
207+
}
208+
m_nstc = nullptr;
209+
m_wslItem = nullptr;
210+
m_pollCount = 0;
211+
}
212+
213+
// IFileDialogEvents::OnFolderChange is called when the dialog is opened.
214+
// https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ifiledialogevents-onfolderchange
215+
IFACEMETHODIMP WslNodeRevealer::OnFolderChange(IFileDialog* pfd) noexcept
216+
{
217+
if (!m_revealed)
218+
{
219+
m_revealed = true;
220+
TryStartReveal(pfd); // best-effort; ignore failure so the picker always opens
221+
}
222+
return S_OK;
223+
}
224+
225+
HRESULT WslNodeRevealer::TryStartReveal(IFileDialog* pfd) noexcept
226+
{
227+
winrt::com_ptr<IServiceProvider> sp;
228+
RETURN_IF_FAILED(pfd->QueryInterface(IID_PPV_ARGS(sp.put())));
229+
230+
winrt::com_ptr<IShellBrowser> sb;
231+
RETURN_IF_FAILED(sp->QueryService(SID_STopLevelBrowser, IID_PPV_ARGS(sb.put())));
232+
233+
winrt::com_ptr<IServiceProvider> sbSp;
234+
RETURN_IF_FAILED(sb->QueryInterface(IID_PPV_ARGS(sbSp.put())));
235+
236+
winrt::com_ptr<INameSpaceTreeControl> nstc;
237+
RETURN_IF_FAILED(sbSp->QueryService(IID_INameSpaceTreeControl, IID_PPV_ARGS(nstc.put())));
238+
239+
// Resolve the WSL root shell item, falling back from \\wsl.localhost to \\wsl$.
240+
winrt::com_ptr<IShellItem> wslItem;
241+
if (FAILED(SHCreateItemFromParsingName(L"\\\\wsl.localhost", nullptr, IID_PPV_ARGS(wslItem.put()))))
242+
{
243+
RETURN_IF_FAILED(SHCreateItemFromParsingName(L"\\\\wsl$", nullptr, IID_PPV_ARGS(wslItem.put())));
244+
}
245+
246+
// Obtain the dialog's HWND so we can attach the timer to it.
247+
// SetTimer with a non-NULL hWnd uses the supplied nIDEvent as-is (letting us
248+
// recover `this` in PollTimerProc) and synthesizes WM_TIMER via the window's
249+
// message loop rather than posting to the thread queue, so KillTimer is atomic.
250+
winrt::com_ptr<IOleWindow> oleWindow;
251+
RETURN_IF_FAILED(pfd->QueryInterface(IID_PPV_ARGS(oleWindow.put())));
252+
HWND dialogHwnd = nullptr;
253+
RETURN_IF_FAILED(oleWindow->GetWindow(&dialogHwnd));
254+
RETURN_HR_IF_NULL(E_FAIL, dialogHwnd);
255+
256+
m_nstc = nstc;
257+
m_wslItem = wslItem;
258+
m_pollCount = 0;
259+
m_timerHwnd = dialogHwnd;
260+
m_timerPending = SetTimer(m_timerHwnd, reinterpret_cast<UINT_PTR>(this), s_pollIntervalMs, PollTimerProc) != 0;
261+
if (!m_timerPending)
262+
{
263+
m_timerHwnd = nullptr;
264+
}
265+
266+
return S_OK;
267+
}
268+
107269
bool IsHStringNullOrEmpty(winrt::hstring value)
108270
{
109271
return value.empty();

dev/Interop/StoragePickers/PickerCommon.h

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,38 @@ namespace PickerCommon {
3232
void ValidateFolderPath(winrt::hstring const& path, std::string const& propertyName);
3333
void ValidateInitialFileTypeIndex(int const& value);
3434

35+
// Shows the WSL node in the navigation pane of COM file dialogs that hide it by default.
36+
// The WSL node was hidden in FileSavePicker (IFileSaveDialog) and FolderPicker (IFileOpenDialog + FOS_PICKFOLDERS + FOS_FORCEFILESYSTEM);
37+
// It takes time for the navigation pane to load nodes.
38+
// This handler checks the root nodes and looks for the WSL node in children nodes.
39+
// When finds the existing hidden WSL node, makes it visible.
40+
struct WslNodeRevealer : winrt::implements<WslNodeRevealer, IFileDialogEvents>
41+
{
42+
bool m_revealed{ false };
43+
bool m_timerPending{ false };
44+
int m_pollCount{ 0 };
45+
HWND m_timerHwnd{ nullptr };
46+
winrt::com_ptr<INameSpaceTreeControl> m_nstc;
47+
winrt::com_ptr<IShellItem> m_wslItem;
48+
49+
// looking for the navigation node for 1 second at most
50+
static constexpr UINT s_pollIntervalMs{ 10 };
51+
static constexpr int s_maxPollCount{ 100 };
52+
53+
static void CALLBACK PollTimerProc(HWND, UINT, UINT_PTR timerId, DWORD) noexcept;
54+
void RevealWslNodeWhenReady(HWND hwnd) noexcept;
55+
void CancelPendingReveal() noexcept;
56+
HRESULT TryStartReveal(IFileDialog* pfd) noexcept;
57+
58+
IFACEMETHODIMP OnFolderChange(IFileDialog* pfd) noexcept override;
59+
IFACEMETHODIMP OnFileOk(IFileDialog*) noexcept override { return S_OK; }
60+
IFACEMETHODIMP OnFolderChanging(IFileDialog*, IShellItem*) noexcept override { return S_OK; }
61+
IFACEMETHODIMP OnSelectionChange(IFileDialog*) noexcept override { return S_OK; }
62+
IFACEMETHODIMP OnShareViolation(IFileDialog*, IShellItem*, FDE_SHAREVIOLATION_RESPONSE*) noexcept override { return S_OK; }
63+
IFACEMETHODIMP OnTypeChange(IFileDialog*) noexcept override { return S_OK; }
64+
IFACEMETHODIMP OnOverwrite(IFileDialog*, IShellItem*, FDE_OVERWRITE_RESPONSE*) noexcept override { return S_OK; }
65+
};
66+
3567
struct PickerParameters {
3668
HWND HWnd{};
3769
winrt::hstring CommitButtonText;

0 commit comments

Comments
 (0)