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
13 changes: 13 additions & 0 deletions dev/Interop/StoragePickers/FileSavePicker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,19 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation
check_hresult(dialog->GetOptions(&dialogOptions));
check_hresult(dialog->SetOptions(dialogOptions | FOS_STRICTFILETYPES));

// Register event handler to show the WSL node in the navigation pane.
// Use SUCCEEDED() instead of check_hresult() so the picker still opens if registration fails.
auto wslRevealer = winrt::make_self<PickerCommon::WslNodeRevealer>();
DWORD adviseCookie{};
bool wslAdvised = SUCCEEDED(dialog->Advise(wslRevealer.as<IFileDialogEvents>().get(), &adviseCookie));
auto unadvise = wil::scope_exit([&] {
if (wslAdvised)
{
dialog->Unadvise(adviseCookie);
}
wslRevealer->CancelPendingReveal();
});

if (FAILED(dialog->Show(parameters.HWnd)))
{
logTelemetry.Stop(m_telemetryHelper, false);
Expand Down
26 changes: 26 additions & 0 deletions dev/Interop/StoragePickers/FolderPicker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,19 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation
parameters.ConfigureDialog(dialog);
dialog->SetOptions(FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM);

// Register event handler to show the WSL node in the navigation pane.
// Use SUCCEEDED() instead of check_hresult() so the picker still opens if registration fails.
auto wslRevealer = winrt::make_self<PickerCommon::WslNodeRevealer>();
DWORD adviseCookie{};
bool wslAdvised = SUCCEEDED(dialog->Advise(wslRevealer.as<IFileDialogEvents>().get(), &adviseCookie));
auto unadvise = wil::scope_exit([&] {
if (wslAdvised)
{
dialog->Unadvise(adviseCookie);
}
wslRevealer->CancelPendingReveal();
});

if (FAILED(dialog->Show(parameters.HWnd)) || cancellationToken())
{
logTelemetry.Stop(m_telemetryHelper, false);
Expand Down Expand Up @@ -170,6 +183,19 @@ namespace winrt::Microsoft::Windows::Storage::Pickers::implementation
check_hresult(dialog->GetOptions(&dialogOptions));
check_hresult(dialog->SetOptions(dialogOptions | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_ALLOWMULTISELECT));

// Register event handler to show the WSL node in the navigation pane.
// Use SUCCEEDED() instead of check_hresult() so the picker still opens if registration fails.
auto wslRevealer = winrt::make_self<PickerCommon::WslNodeRevealer>();
DWORD adviseCookie{};
bool wslAdvised = SUCCEEDED(dialog->Advise(wslRevealer.as<IFileDialogEvents>().get(), &adviseCookie));
auto unadvise = wil::scope_exit([&] {
if (wslAdvised)
{
dialog->Unadvise(adviseCookie);
}
wslRevealer->CancelPendingReveal();
});

if (FAILED(dialog->Show(parameters.HWnd)) || cancellationToken())
{
logTelemetry.Stop(m_telemetryHelper, true, false);
Expand Down
162 changes: 162 additions & 0 deletions dev/Interop/StoragePickers/PickerCommon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "ShObjIdl.h"
#include "shobjidl_core.h"
#include <KnownFolders.h>
#include <shlguid.h>
#include <filesystem>
#include <format>
#include <utility>
Expand Down Expand Up @@ -101,9 +102,170 @@ namespace {

}

namespace {

// Search immediate children of parentItem for the WSL item.
// If found, make it visible via INameSpaceTreeControl::SetItemState.
bool FindAndShowWslInChildren(winrt::com_ptr<IShellItem> const& wslItem, winrt::com_ptr<IShellItem> const& parentItem, winrt::com_ptr<INameSpaceTreeControl> const& nstc) noexcept
{
if (!parentItem || !wslItem || !nstc)
{
return false;
}

winrt::com_ptr<IEnumShellItems> enumItems;
if (FAILED(parentItem->BindToHandler(nullptr, BHID_EnumItems, IID_PPV_ARGS(enumItems.put()))) || !enumItems)
{
return false;
}

winrt::com_ptr<IShellItem> childItem;
ULONG fetched = 0;
while (SUCCEEDED(enumItems->Next(1, childItem.put(), &fetched)) && fetched > 0)
{
if (childItem)
{
int order = 0;
if (childItem->Compare(wslItem.get(), SICHINT_CANONICAL, &order) == S_OK && order == 0)
{
nstc->SetItemState(childItem.get(), NSTCIS_EXPANDED, NSTCIS_EXPANDED);
return true;
}
childItem = nullptr;
}
fetched = 0;
}

return false;
}

}

namespace PickerCommon {
using namespace winrt;

void CALLBACK WslNodeRevealer::PollTimerProc(HWND hwnd, UINT, UINT_PTR timerId, DWORD) noexcept
Comment thread
DinahK-2SO marked this conversation as resolved.
{
reinterpret_cast<WslNodeRevealer*>(timerId)->RevealWslNodeWhenReady(hwnd);
}

void WslNodeRevealer::RevealWslNodeWhenReady(HWND hwnd) noexcept
{
if (!m_nstc || !m_wslItem || ++m_pollCount >= s_maxPollCount)
{
KillTimer(hwnd, reinterpret_cast<UINT_PTR>(this));
m_timerPending = false;
m_nstc = nullptr;
m_wslItem = nullptr;
return;
}

winrt::com_ptr<IShellItemArray> roots;
DWORD count = 0;
if (SUCCEEDED(m_nstc->GetRootItems(roots.put())) && roots)
{
if (FAILED(roots->GetCount(&count)))
{
count = 0;
}
}

// Wait until at least one root node has loaded.
if (count == 0)
{
return;
}

KillTimer(hwnd, reinterpret_cast<UINT_PTR>(this));
m_timerPending = false;

// Search for the WSL node in the immediate children of each root node.
for (DWORD i = 0; i < count; i++)
{
winrt::com_ptr<IShellItem> rootItem;
if (SUCCEEDED(roots->GetItemAt(i, rootItem.put())) && rootItem)
{
if (FindAndShowWslInChildren(m_wslItem, rootItem, m_nstc))
{
break;
}
}
}

m_nstc = nullptr;
m_wslItem = nullptr;
m_pollCount = 0;
}

void WslNodeRevealer::CancelPendingReveal() noexcept
{
if (m_timerPending)
{
KillTimer(m_timerHwnd, reinterpret_cast<UINT_PTR>(this));
m_timerPending = false;
m_timerHwnd = nullptr;
}
m_nstc = nullptr;
m_wslItem = nullptr;
m_pollCount = 0;
}

// IFileDialogEvents::OnFolderChange is called when the dialog is opened.
// https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ifiledialogevents-onfolderchange
IFACEMETHODIMP WslNodeRevealer::OnFolderChange(IFileDialog* pfd) noexcept
Comment thread
DinahK-2SO marked this conversation as resolved.
{
if (!m_revealed)
{
m_revealed = true;
TryStartReveal(pfd); // best-effort; ignore failure so the picker always opens
}
return S_OK;
}

HRESULT WslNodeRevealer::TryStartReveal(IFileDialog* pfd) noexcept
{
winrt::com_ptr<IServiceProvider> sp;
RETURN_IF_FAILED(pfd->QueryInterface(IID_PPV_ARGS(sp.put())));

winrt::com_ptr<IShellBrowser> sb;
RETURN_IF_FAILED(sp->QueryService(SID_STopLevelBrowser, IID_PPV_ARGS(sb.put())));

winrt::com_ptr<IServiceProvider> sbSp;
RETURN_IF_FAILED(sb->QueryInterface(IID_PPV_ARGS(sbSp.put())));

winrt::com_ptr<INameSpaceTreeControl> nstc;
RETURN_IF_FAILED(sbSp->QueryService(IID_INameSpaceTreeControl, IID_PPV_ARGS(nstc.put())));

// Resolve the WSL root shell item, falling back from \\wsl.localhost to \\wsl$.
winrt::com_ptr<IShellItem> wslItem;
if (FAILED(SHCreateItemFromParsingName(L"\\\\wsl.localhost", nullptr, IID_PPV_ARGS(wslItem.put()))))
{
RETURN_IF_FAILED(SHCreateItemFromParsingName(L"\\\\wsl$", nullptr, IID_PPV_ARGS(wslItem.put())));
}

// Obtain the dialog's HWND so we can attach the timer to it.
// SetTimer with a non-NULL hWnd uses the supplied nIDEvent as-is (letting us
// recover `this` in PollTimerProc) and synthesizes WM_TIMER via the window's
// message loop rather than posting to the thread queue, so KillTimer is atomic.
winrt::com_ptr<IOleWindow> oleWindow;
RETURN_IF_FAILED(pfd->QueryInterface(IID_PPV_ARGS(oleWindow.put())));
HWND dialogHwnd = nullptr;
RETURN_IF_FAILED(oleWindow->GetWindow(&dialogHwnd));
RETURN_HR_IF_NULL(E_FAIL, dialogHwnd);

m_nstc = nstc;
m_wslItem = wslItem;
m_pollCount = 0;
m_timerHwnd = dialogHwnd;
m_timerPending = SetTimer(m_timerHwnd, reinterpret_cast<UINT_PTR>(this), s_pollIntervalMs, PollTimerProc) != 0;
if (!m_timerPending)
{
m_timerHwnd = nullptr;
}

return S_OK;
}

bool IsHStringNullOrEmpty(winrt::hstring value)
{
return value.empty();
Expand Down
32 changes: 32 additions & 0 deletions dev/Interop/StoragePickers/PickerCommon.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,38 @@ namespace PickerCommon {
void ValidateFolderPath(winrt::hstring const& path, std::string const& propertyName);
void ValidateInitialFileTypeIndex(int const& value);

// 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.
// This handler checks the root nodes and looks for the WSL node in children nodes.
// When finds the existing hidden WSL node, makes it visible.
struct WslNodeRevealer : winrt::implements<WslNodeRevealer, IFileDialogEvents>
{
bool m_revealed{ false };
bool m_timerPending{ false };
int m_pollCount{ 0 };
HWND m_timerHwnd{ nullptr };
winrt::com_ptr<INameSpaceTreeControl> m_nstc;
winrt::com_ptr<IShellItem> m_wslItem;

// looking for the navigation node for 1 second at most
static constexpr UINT s_pollIntervalMs{ 10 };
static constexpr int s_maxPollCount{ 100 };

static void CALLBACK PollTimerProc(HWND, UINT, UINT_PTR timerId, DWORD) noexcept;
void RevealWslNodeWhenReady(HWND hwnd) noexcept;
void CancelPendingReveal() noexcept;
HRESULT TryStartReveal(IFileDialog* pfd) noexcept;

IFACEMETHODIMP OnFolderChange(IFileDialog* pfd) noexcept override;
IFACEMETHODIMP OnFileOk(IFileDialog*) noexcept override { return S_OK; }
IFACEMETHODIMP OnFolderChanging(IFileDialog*, IShellItem*) noexcept override { return S_OK; }
IFACEMETHODIMP OnSelectionChange(IFileDialog*) noexcept override { return S_OK; }
IFACEMETHODIMP OnShareViolation(IFileDialog*, IShellItem*, FDE_SHAREVIOLATION_RESPONSE*) noexcept override { return S_OK; }
IFACEMETHODIMP OnTypeChange(IFileDialog*) noexcept override { return S_OK; }
IFACEMETHODIMP OnOverwrite(IFileDialog*, IShellItem*, FDE_OVERWRITE_RESPONSE*) noexcept override { return S_OK; }
};

struct PickerParameters {
HWND HWnd{};
winrt::hstring CommitButtonText;
Expand Down