diff --git a/dev/Interop/StoragePickers/FileSavePicker.cpp b/dev/Interop/StoragePickers/FileSavePicker.cpp index ae26a58bfa..33e43d248e 100644 --- a/dev/Interop/StoragePickers/FileSavePicker.cpp +++ b/dev/Interop/StoragePickers/FileSavePicker.cpp @@ -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(); + DWORD adviseCookie{}; + bool wslAdvised = SUCCEEDED(dialog->Advise(wslRevealer.as().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); diff --git a/dev/Interop/StoragePickers/FolderPicker.cpp b/dev/Interop/StoragePickers/FolderPicker.cpp index fd74d2bc0b..3b5e156ac7 100644 --- a/dev/Interop/StoragePickers/FolderPicker.cpp +++ b/dev/Interop/StoragePickers/FolderPicker.cpp @@ -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(); + DWORD adviseCookie{}; + bool wslAdvised = SUCCEEDED(dialog->Advise(wslRevealer.as().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); @@ -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(); + DWORD adviseCookie{}; + bool wslAdvised = SUCCEEDED(dialog->Advise(wslRevealer.as().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); diff --git a/dev/Interop/StoragePickers/PickerCommon.cpp b/dev/Interop/StoragePickers/PickerCommon.cpp index d20a90d365..9aff9355c3 100644 --- a/dev/Interop/StoragePickers/PickerCommon.cpp +++ b/dev/Interop/StoragePickers/PickerCommon.cpp @@ -10,6 +10,7 @@ #include "ShObjIdl.h" #include "shobjidl_core.h" #include +#include #include #include #include @@ -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 const& wslItem, winrt::com_ptr const& parentItem, winrt::com_ptr const& nstc) noexcept + { + if (!parentItem || !wslItem || !nstc) + { + return false; + } + + winrt::com_ptr enumItems; + if (FAILED(parentItem->BindToHandler(nullptr, BHID_EnumItems, IID_PPV_ARGS(enumItems.put()))) || !enumItems) + { + return false; + } + + winrt::com_ptr 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 + { + reinterpret_cast(timerId)->RevealWslNodeWhenReady(hwnd); + } + + void WslNodeRevealer::RevealWslNodeWhenReady(HWND hwnd) noexcept + { + if (!m_nstc || !m_wslItem || ++m_pollCount >= s_maxPollCount) + { + KillTimer(hwnd, reinterpret_cast(this)); + m_timerPending = false; + m_nstc = nullptr; + m_wslItem = nullptr; + return; + } + + winrt::com_ptr 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(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 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(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 + { + 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 sp; + RETURN_IF_FAILED(pfd->QueryInterface(IID_PPV_ARGS(sp.put()))); + + winrt::com_ptr sb; + RETURN_IF_FAILED(sp->QueryService(SID_STopLevelBrowser, IID_PPV_ARGS(sb.put()))); + + winrt::com_ptr sbSp; + RETURN_IF_FAILED(sb->QueryInterface(IID_PPV_ARGS(sbSp.put()))); + + winrt::com_ptr 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 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 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(this), s_pollIntervalMs, PollTimerProc) != 0; + if (!m_timerPending) + { + m_timerHwnd = nullptr; + } + + return S_OK; + } + bool IsHStringNullOrEmpty(winrt::hstring value) { return value.empty(); diff --git a/dev/Interop/StoragePickers/PickerCommon.h b/dev/Interop/StoragePickers/PickerCommon.h index 94d0b5f1c5..2d23e4df32 100644 --- a/dev/Interop/StoragePickers/PickerCommon.h +++ b/dev/Interop/StoragePickers/PickerCommon.h @@ -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 + { + bool m_revealed{ false }; + bool m_timerPending{ false }; + int m_pollCount{ 0 }; + HWND m_timerHwnd{ nullptr }; + winrt::com_ptr m_nstc; + winrt::com_ptr 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;