From b605fd35c51f458d55da9ccdb82959c95d31c8d3 Mon Sep 17 00:00:00 2001 From: Christian Gaarden Gaardmark Date: Tue, 11 Aug 2026 23:23:48 -0700 Subject: [PATCH] New+ Rename and Desktop icon positioning improvements (#48083) New+ Rename and Desktop icon positioning improvements. Manually tested on Windows 11. Windows 10 updates have NOT been tested. ## Summary of the Pull Request * Obtain cursor position early in the lifecycle of the context menu * Busy wait until copy is complete and shell is aware of icon * If context menu is on desktop, reposition the icon using the cursor position obtained scaled using monitor appropriate DPI * Slight refactor to help port code from New++ to New+ ## PR Checklist - [x] Closes: #36440 - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [n/a] **Localization:** All end-user-facing strings can be localized - [n/a] **Dev docs:** Added/updated - [n/a] **New binaries:** Added on the required places - [n/a] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [n/a] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [n/a] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [n/a] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [n/a] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments * n/a ## Validation Steps Performed **Windows 11** * x64: Manually tested * ARM64: Not tested **Windows 10** * Not tested --------- Co-authored-by: Boliang Zhang (from Dev Box) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 41b6b39b-b620-4e02-b7ca-8ae470a9c1e2 --- .github/actions/spell-check/allow/code.txt | 5 + .../NewPlus.ShellExtension.win10.vcxproj | 3 +- ...wPlus.ShellExtension.win10.vcxproj.filters | 12 +- .../dll_main.cpp | 3 +- .../dll_main.h | 2 + .../shell_context_menu_win10.cpp | 15 +- .../shell_context_menu_win10.h | 1 + .../NewShellExtensionContextMenu.vcxproj | 4 +- ...wShellExtensionContextMenu.vcxproj.filters | 23 +- .../NewShellExtensionContextMenu/dll_main.cpp | 3 +- .../NewShellExtensionContextMenu/dll_main.h | 4 +- .../new_utilities.h | 218 ++++++++++-------- .../newplus_icon_utilities.cpp | 161 +++++++++++++ .../newplus_icon_utilities.h | 12 + .../shell_context_menu.cpp | 14 +- .../shell_context_sub_menu.cpp | 7 +- .../shell_context_sub_menu.h | 3 +- .../shell_context_sub_menu_item.cpp | 9 +- .../shell_context_sub_menu_item.h | 3 +- .../template_item.cpp | 125 ++++++++-- .../template_item.h | 5 +- 21 files changed, 498 insertions(+), 134 deletions(-) create mode 100644 src/modules/NewPlus/NewShellExtensionContextMenu/newplus_icon_utilities.cpp create mode 100644 src/modules/NewPlus/NewShellExtensionContextMenu/newplus_icon_utilities.h diff --git a/.github/actions/spell-check/allow/code.txt b/.github/actions/spell-check/allow/code.txt index e13c75d982..311299ba53 100644 --- a/.github/actions/spell-check/allow/code.txt +++ b/.github/actions/spell-check/allow/code.txt @@ -171,6 +171,7 @@ thmutil uriutil VKTAB wcautil +wcsnlen winkey wininet WMKEYDOWN @@ -452,3 +453,7 @@ WIDGETBOARD # URIs actioncenter + +# New+ shell PIDL type names +PCUITEMID +PITEMID diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj index fbdda07ddd..27e64a374f 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj +++ b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj @@ -14,7 +14,6 @@ DynamicLibrary - Unicode @@ -85,6 +84,7 @@ + @@ -101,6 +101,7 @@ + diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj.filters b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj.filters index a883117179..f8be919465 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj.filters +++ b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj.filters @@ -63,6 +63,12 @@ Header Files + + Header Files + + + Header Files + @@ -98,7 +104,10 @@ Source Files - + + Source Files + + Source Files @@ -121,5 +130,6 @@ + \ No newline at end of file diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/dll_main.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/dll_main.cpp index b036eb2ed0..fc8c80aced 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/dll_main.cpp +++ b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/dll_main.cpp @@ -8,6 +8,7 @@ HMODULE module_instance_handle = 0; Shared::Trace::ETWTrace trace(L"NewPlusShellExtension_Win10"); +std::atomic_uint32_t active_rename_workers = 0; BOOL APIENTRY DllMain(HMODULE module_handle, DWORD ul_reason_for_call, LPVOID reserved) { @@ -33,7 +34,7 @@ STDAPI DllGetActivationFactory(_In_ HSTRING activatableClassId, _COM_Outptr_ IAc STDAPI DllCanUnloadNow() { - return Module::GetModule().GetObjectCount() == 0 ? S_OK : S_FALSE; + return Module::GetModule().GetObjectCount() == 0 && active_rename_workers.load() == 0 ? S_OK : S_FALSE; } STDAPI DllGetClassObject(_In_ REFCLSID ref_class_id, _In_ REFIID ref_interface_id, _Outptr_ LPVOID FAR* object) diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/dll_main.h b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/dll_main.h index c2d69eebfd..444ee8c724 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/dll_main.h +++ b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/dll_main.h @@ -1,6 +1,8 @@ #pragma once +#include #include extern HMODULE module_instance_handle; extern Shared::Trace::ETWTrace trace; +extern std::atomic_uint32_t active_rename_workers; diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/shell_context_menu_win10.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/shell_context_menu_win10.cpp index 8801ec6c48..c7f9b0f148 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/shell_context_menu_win10.cpp +++ b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/shell_context_menu_win10.cpp @@ -45,6 +45,18 @@ IFACEMETHODIMP shell_context_menu_win10::QueryContextMenu(HMENU menu_handle, UIN try { + // Capture mouse position now (at menu-open time) for more accurate desktop icon placement later. + // Use {-1,-1} as the "not captured" sentinel (matching the Win11 path) because (0,0) is a valid + // screen coordinate; only treat the position as real when GetCursorPos succeeds. + mouse_position_at_time_of_invoke = { -1, -1 }; + const DPI_AWARENESS_CONTEXT prev_dpi_ctx = SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + if (!GetCursorPos(&mouse_position_at_time_of_invoke)) + { + mouse_position_at_time_of_invoke = { -1, -1 }; + } + + SetThreadDpiAwarenessContext(prev_dpi_ctx); + // Create the initial context popup menu containing the list of templates and open templates action int menu_id = menu_first_cmd_id; MENUITEMINFO newplus_main_context_menu_item = { 0 }; @@ -245,8 +257,7 @@ IFACEMETHODIMP shell_context_menu_win10::InvokeCommand(CMINVOKECOMMANDINFO* para { // It's a template menu item const auto template_entry = templates->get_template_item(selected_menu_item_index); - - return newplus::utilities::copy_template(template_entry, site_of_folder); + return newplus::utilities::copy_template(template_entry, site_of_folder, mouse_position_at_time_of_invoke); } else { diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/shell_context_menu_win10.h b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/shell_context_menu_win10.h index f69355cccb..f5c37a117e 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/shell_context_menu_win10.h +++ b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/shell_context_menu_win10.h @@ -42,4 +42,5 @@ protected: ComPtr site_of_folder; newplus::template_folder* templates = nullptr; std::vector bitmap_handles; + POINT mouse_position_at_time_of_invoke = {-1, -1}; }; diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj b/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj index fd2574476b..132b7c885f 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj @@ -16,13 +16,11 @@ DynamicLibrary true - Unicode DynamicLibrary false - true Unicode @@ -124,6 +122,7 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv + @@ -134,6 +133,7 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv + diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj.filters b/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj.filters index d8b2eaf9ea..59d6f10da6 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj.filters +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj.filters @@ -34,7 +34,10 @@ Source Files - + + Source Files + + Source Files @@ -87,6 +90,12 @@ Header Files + + Header Files + + + Header Files + @@ -202,8 +211,14 @@ - - - + + Template Examples + + + Template Examples\Example folder + + + Template Examples\Example folder + \ No newline at end of file diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/dll_main.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/dll_main.cpp index 878673826b..ffa63444f6 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/dll_main.cpp +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/dll_main.cpp @@ -8,6 +8,7 @@ HMODULE module_instance_handle = 0; Shared::Trace::ETWTrace trace(L"NewPlusShellExtension"); +std::atomic_uint32_t active_rename_workers = 0; BOOL APIENTRY DllMain(HMODULE module_handle, DWORD ul_reason_for_call, LPVOID reserved) { @@ -33,7 +34,7 @@ STDAPI DllGetActivationFactory(_In_ HSTRING activatableClassId, _COM_Outptr_ IAc STDAPI DllCanUnloadNow() { - return Module::GetModule().GetObjectCount() == 0 ? S_OK : S_FALSE; + return Module::GetModule().GetObjectCount() == 0 && active_rename_workers.load() == 0 ? S_OK : S_FALSE; } STDAPI DllGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Outptr_ LPVOID FAR* ppv) diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/dll_main.h b/src/modules/NewPlus/NewShellExtensionContextMenu/dll_main.h index 9e980caa0d..1b4be4a1a5 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/dll_main.h +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/dll_main.h @@ -1,6 +1,8 @@ #pragma once +#include #include extern HMODULE module_instance_handle; -extern Shared::Trace::ETWTrace trace; \ No newline at end of file +extern Shared::Trace::ETWTrace trace; +extern std::atomic_uint32_t active_rename_workers; \ No newline at end of file diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h b/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h index a2e6d7e036..fc96041a18 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h @@ -10,7 +10,9 @@ #include "template_item.h" #include "trace.h" #include "helpers_variables.h" +#include +#pragma comment(lib, "Shcore.lib") #pragma comment(lib, "Shlwapi.lib") using namespace newplus; @@ -20,59 +22,6 @@ namespace newplus::utilities size_t get_saved_number_of_templates(); void set_saved_number_of_templates(size_t templates); - inline std::wstring get_explorer_icon(std::filesystem::path path) - { - SHFILEINFO shell_file_info = { 0 }; - const std::wstring filepath = path.wstring(); - DWORD_PTR result = SHGetFileInfo(filepath.c_str(), 0, &shell_file_info, sizeof(shell_file_info), SHGFI_ICONLOCATION); - std::wstring icon_path = shell_file_info.szDisplayName; - if (icon_path != L"") - { - const int icon_index = shell_file_info.iIcon; - std::wstring icon_resource = icon_path + std::wstring(L",") + std::to_wstring(icon_index); - return icon_resource; - } - - WCHAR icon_resource_specifier[MAX_PATH] = { 0 }; - DWORD buffer_length = MAX_PATH; - const std::wstring extension = path.extension().wstring(); - const HRESULT hr = AssocQueryString(ASSOCF_INIT_IGNOREUNKNOWN, - ASSOCSTR_DEFAULTICON, - extension.c_str(), - NULL, - icon_resource_specifier, - &buffer_length); - const std::wstring icon_resource = icon_resource_specifier; - return icon_resource; - } - - inline HICON get_explorer_icon_handle(std::filesystem::path path) - { - SHFILEINFO shell_file_info = { 0 }; - const std::wstring filepath = path.wstring(); - DWORD_PTR result = SHGetFileInfo(filepath.c_str(), 0, &shell_file_info, sizeof(shell_file_info), SHGFI_ICON); - if (shell_file_info.hIcon) - { - return shell_file_info.hIcon; - } - - WCHAR icon_resource_specifier[MAX_PATH] = { 0 }; - DWORD buffer_length = MAX_PATH; - const std::wstring extension = path.extension().wstring(); - const HRESULT hr = AssocQueryString(ASSOCF_INIT_IGNOREUNKNOWN, - ASSOCSTR_DEFAULTICON, - extension.c_str(), - NULL, - icon_resource_specifier, - &buffer_length); - const std::wstring icon_resource = icon_resource_specifier; - - const auto icon_x = GetSystemMetrics(SM_CXSMICON); - const auto icon_y = GetSystemMetrics(SM_CYSMICON); - HICON hIcon = static_cast(LoadImage(NULL, icon_resource.c_str(), IMAGE_ICON, icon_x, icon_y, LR_LOADFROMFILE)); - return hIcon; - } - inline bool wstring_same_when_comparing_ignore_case(std::wstring stringA, std::wstring stringB) { transform(stringA.begin(), stringA.end(), stringA.begin(), towupper); @@ -198,21 +147,21 @@ namespace newplus::utilities return false; } - inline void explorer_enter_rename_mode(const std::filesystem::path target_fullpath_of_new_instance) + inline bool explorer_enter_rename_mode_and_reposition(const std::filesystem::path target_fullpath_of_new_instance, const POINT mouse_position_at_time_of_invoke, const bool enter_rename_mode = true) { const std::filesystem::path path_without_new_file_or_dir = target_fullpath_of_new_instance.parent_path(); const std::filesystem::path new_file_or_dir_without_path = target_fullpath_of_new_instance.filename(); - ComPtr shell_windows; + CComPtr shell_windows; HRESULT hr; if (FAILED(CoCreateInstance(CLSID_ShellWindows, NULL, CLSCTX_ALL, IID_PPV_ARGS(&shell_windows)))) { - return; + return false; } - long window_handle; - ComPtr shell_window; + long desktop_window_handle = 0; + CComPtr shell_window; const bool object_created_on_desktop = is_desktop_folder(path_without_new_file_or_dir.c_str()); if (object_created_on_desktop) { @@ -220,9 +169,9 @@ namespace newplus::utilities VARIANT empty_yet_needed_incl_init; VariantInit(&empty_yet_needed_incl_init); - if (FAILED(shell_windows->FindWindowSW(&empty_yet_needed_incl_init, &empty_yet_needed_incl_init, SWC_DESKTOP, &window_handle, SWFO_NEEDDISPATCH, &shell_window))) + if (FAILED(shell_windows->FindWindowSW(&empty_yet_needed_incl_init, &empty_yet_needed_incl_init, SWC_DESKTOP, &desktop_window_handle, SWFO_NEEDDISPATCH, &shell_window))) { - return; + return false; } } else @@ -232,7 +181,7 @@ namespace newplus::utilities for (long i = 0; i < count_of_shell_windows; ++i) { - ComPtr web_browser_app; + CComPtr web_browser_app; VARIANT v; VariantInit(&v); V_VT(&v) = VT_I4; @@ -240,14 +189,14 @@ namespace newplus::utilities hr = shell_windows->Item(v, &shell_window); if (SUCCEEDED(hr) && shell_window) { - hr = shell_window.As(&web_browser_app); + hr = shell_window->QueryInterface(IID_PPV_ARGS(&web_browser_app)); if (SUCCEEDED(hr)) { BSTR folder_view_location; hr = web_browser_app->get_LocationURL(&folder_view_location); if (SUCCEEDED(hr) && folder_view_location) { - wchar_t path[MAX_PATH]; + wchar_t path[MAX_PATH * 2]; DWORD pathLength = ARRAYSIZE(path); hr = PathCreateFromUrl(folder_view_location, path, &pathLength, 0); SysFreeString(folder_view_location); @@ -264,17 +213,36 @@ namespace newplus::utilities if (!shell_window) { - return; + return false; } - ComPtr service_provider; - shell_window.As(&service_provider); - ComPtr shell_browser; - service_provider->QueryService(SID_STopLevelBrowser, IID_PPV_ARGS(&shell_browser)); - ComPtr shell_view; - shell_browser->QueryActiveShellView(&shell_view); - ComPtr folder_view; - shell_view.As(&folder_view); + CComPtr service_provider; + if (FAILED(shell_window->QueryInterface(IID_PPV_ARGS(&service_provider))) || service_provider == nullptr) + { + return false; + } + + CComPtr shell_browser; + if (FAILED(service_provider->QueryService(SID_STopLevelBrowser, IID_PPV_ARGS(&shell_browser))) || shell_browser == nullptr) + { + return false; + } + + CComPtr shell_view; + if (FAILED(shell_browser->QueryActiveShellView(&shell_view)) || shell_view == nullptr) + { + return false; + } + + CComPtr folder_view; + if (FAILED(shell_view->QueryInterface(IID_PPV_ARGS(&folder_view))) || folder_view == nullptr) + { + return false; + } + + // The folder backing the active view; used to resolve child PIDLs to their names below. + CComPtr view_shell_folder; + folder_view->GetFolder(IID_PPV_ARGS(&view_shell_folder)); // Find the newly created object (file or folder) // And put object into edit mode (SVSI_EDIT) and if desktop also reposition @@ -283,40 +251,98 @@ namespace newplus::utilities folder_view->ItemCount(SVGIO_ALLVIEW, &number_of_objects_in_view); for (int i = 0; i < number_of_objects_in_view && !done; ++i) { - std::wstring path_of_item(MAX_PATH, 0); - LPITEMIDLIST shell_item_ids; + PITEMID_CHILD shell_item_id = nullptr; - folder_view->Item(i, &shell_item_ids); - SHGetPathFromIDList(shell_item_ids, &path_of_item[0]); - - const std::wstring current_filename = std::filesystem::path(path_of_item.c_str()).filename(); - - if (utilities::wstring_same_when_comparing_ignore_case(new_file_or_dir_without_path, current_filename)) + if (FAILED(folder_view->Item(i, &shell_item_id)) || shell_item_id == nullptr) { - const DWORD common_select_flags = SVSI_EDIT | SVSI_SELECT | SVSI_DESELECTOTHERS | SVSI_ENSUREVISIBLE | SVSI_FOCUSED; + continue; + } + + wchar_t path_buffer[MAX_PATH * 2] = { 0 }; + + // IFolderView::Item returns a child (folder-relative) PIDL. SHGetPathFromIDList expects an + // absolute PIDL, so ask the parent IShellFolder for the item's in-folder parsing name instead + // of reinterpret-casting the child PIDL to absolute (which fails outside the desktop and + // would leave the new item never matched, so rename/reposition would silently time out). + if (view_shell_folder != nullptr) + { + STRRET str_ret; + if (SUCCEEDED(view_shell_folder->GetDisplayNameOf(shell_item_id, SHGDN_INFOLDER | SHGDN_FORPARSING, &str_ret))) + { + StrRetToBufW(&str_ret, shell_item_id, path_buffer, ARRAYSIZE(path_buffer)); + } + } + + if (path_buffer[0] == L'\0') + { + CoTaskMemFree(shell_item_id); + continue; + } + + const std::wstring current_filename = std::filesystem::path(path_buffer).filename(); + + if (newplus::utilities::wstring_same_when_comparing_ignore_case(new_file_or_dir_without_path, current_filename)) + { + const DWORD common_select_flags = (enter_rename_mode ? SVSI_EDIT : 0) | SVSI_SELECT | SVSI_DESELECTOTHERS | SVSI_ENSUREVISIBLE | SVSI_FOCUSED; if (object_created_on_desktop) { - // Newly created object is on the desktop -- reposition under mouse and enter rename mode - LPCITEMIDLIST shell_item_to_select_and_position[] = { shell_item_ids }; - POINT mouse_position; - GetCursorPos(&mouse_position); - mouse_position.x -= GetSystemMetrics(SM_CXMENUSIZE); - mouse_position.x = (std::max)(mouse_position.x, 20L); - mouse_position.y -= GetSystemMetrics(SM_CXMENUSIZE)/2; - mouse_position.y = (std::max)(mouse_position.y, 20L); - POINT position[] = { mouse_position }; - folder_view->SelectAndPositionItems(1, shell_item_to_select_and_position, position, common_select_flags | SVSI_POSITIONITEM); + // All coordinate work is done under per-monitor-DPI-aware context so that + // GetCursorPos, MonitorFromPoint, ScreenToClient, and GetDpiForMonitor all + // operate in physical screen pixels — correctly handling mixed-DPI setups + // where the invoke monitor differs from the primary monitor. + POINT screen_point; + const DPI_AWARENESS_CONTEXT prev_ctx = SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + + if (mouse_position_at_time_of_invoke.x != -1) + { + screen_point = mouse_position_at_time_of_invoke; + } + else + { + if (!GetCursorPos(&screen_point)) + screen_point = { 100, 100 }; + } + + // Resolve effective DPI for the monitor the right-click was on. + UINT invoke_dpi_x = 96; + const HMONITOR h_monitor = MonitorFromPoint(screen_point, MONITOR_DEFAULTTONEAREST); + if (h_monitor) + { + UINT invoke_dpi_y = 0; + GetDpiForMonitor(h_monitor, MDT_EFFECTIVE_DPI, &invoke_dpi_x, &invoke_dpi_y); + } + + // IFolderView expects client coordinates for its view window. + HWND folder_view_window = nullptr; + if (SUCCEEDED(shell_view->GetWindow(&folder_view_window)) && folder_view_window != nullptr) + { + ::ScreenToClient(folder_view_window, &screen_point); + } + + if (prev_ctx != nullptr) + { + SetThreadDpiAwarenessContext(prev_ctx); + } + + // Keep icon clear of the screen edge: ~30 logical pixels scaled to the invoke monitor's DPI. + const LONG min_margin = ::MulDiv(30, static_cast(invoke_dpi_x), 96); + screen_point.x = std::max(screen_point.x, min_margin); + screen_point.y = std::max(screen_point.y, min_margin); + + POINT position[] = { screen_point }; + PCUITEMID_CHILD shell_item_to_select_and_position[] = { shell_item_id }; + done = SUCCEEDED(folder_view->SelectAndPositionItems(1, shell_item_to_select_and_position, position, common_select_flags | SVSI_POSITIONITEM)); } else { // Enter rename mode - folder_view->SelectItem(i, common_select_flags); + done = SUCCEEDED(folder_view->SelectItem(i, common_select_flags)); } - done = true; } - CoTaskMemFree(shell_item_ids); + CoTaskMemFree(shell_item_id); } + return done; } inline void update_last_write_time(const std::filesystem::path path) @@ -334,7 +360,7 @@ namespace newplus::utilities } } - inline HRESULT copy_template(const template_item* template_entry, const ComPtr site_of_folder) + inline HRESULT copy_template(const template_item* template_entry, const ComPtr site_of_folder, const POINT mouse_position_at_invoke) { HRESULT hr = S_OK; @@ -396,7 +422,7 @@ namespace newplus::utilities template_entry->refresh_target(target_final_fullpath); // Enter rename mode - template_entry->enter_rename_mode(target_final_fullpath); + template_entry->enter_rename_mode(target_final_fullpath, mouse_position_at_invoke); } catch (const std::exception& ex) { diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/newplus_icon_utilities.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/newplus_icon_utilities.cpp new file mode 100644 index 0000000000..b42cbbc0d8 --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/newplus_icon_utilities.cpp @@ -0,0 +1,161 @@ +#include "pch.h" +// pch.h first +#include "newplus_icon_utilities.h" +#include +#include + +#pragma comment(lib, "Shlwapi.lib") + +namespace newplus::icon_utilities +{ + +namespace +{ + std::wstring query_default_icon(const wchar_t* association) + { + DWORD buffer_length = 0; + const HRESULT size_result = AssocQueryString( + ASSOCF_INIT_IGNOREUNKNOWN, + ASSOCSTR_DEFAULTICON, + association, + nullptr, + nullptr, + &buffer_length); + if (size_result != S_FALSE || buffer_length == 0) + { + return {}; + } + + std::wstring icon_resource(buffer_length, L'\0'); + const HRESULT query_result = AssocQueryString( + ASSOCF_INIT_IGNOREUNKNOWN, + ASSOCSTR_DEFAULTICON, + association, + nullptr, + icon_resource.data(), + &buffer_length); + if (FAILED(query_result)) + { + return {}; + } + + icon_resource.resize(wcsnlen_s(icon_resource.c_str(), icon_resource.size())); + return icon_resource; + } + + HICON extract_default_icon(const wchar_t* association) + { + const std::wstring icon_resource = query_default_icon(association); + if (icon_resource.empty()) + { + return nullptr; + } + + const DWORD expanded_length = ExpandEnvironmentStrings(icon_resource.c_str(), nullptr, 0); + if (expanded_length == 0) + { + return nullptr; + } + + std::wstring icon_path(expanded_length, L'\0'); + const DWORD expand_result = ExpandEnvironmentStrings(icon_resource.c_str(), icon_path.data(), expanded_length); + if (expand_result == 0 || expand_result > expanded_length) + { + return nullptr; + } + + const int icon_index = PathParseIconLocation(icon_path.data()); + PathUnquoteSpaces(icon_path.data()); + icon_path.resize(wcsnlen_s(icon_path.c_str(), icon_path.size())); + + HICON icon = nullptr; + const UINT icon_size = static_cast(GetSystemMetrics(SM_CXSMICON)); + if (FAILED(SHDefExtractIcon(icon_path.c_str(), icon_index, 0, nullptr, &icon, MAKELONG(0, icon_size)))) + { + return nullptr; + } + + return icon; + } +} + +std::wstring get_explorer_icon(const std::filesystem::path& path, bool is_directory) +{ + // Cache by full path — directories are excluded because their icon can change via desktop.ini + // without a DLL reload. Extension is intentionally NOT used as the key: icons for types like .exe + // and .lnk are per-file (the icon comes from the binary/shortcut itself), so an extension key would + // return the first-seen file's icon for every template of that type. + if (!is_directory) + { + // Explorer can call into the shell extension on multiple threads concurrently, so the + // process-wide cache must be synchronized to avoid a data race on the unordered_map. + // The lock is only ever held around the map lookup/insert and never while calling into the + // shell (SHGetFileInfo/AssocQueryString), because those calls can be reentrant and would + // otherwise risk deadlocking this non-recursive mutex on the same thread. + static std::mutex s_icon_cache_mutex; + static std::unordered_map s_icon_cache; + const std::wstring key = path.wstring(); + + { + std::lock_guard cache_lock(s_icon_cache_mutex); + const auto it = s_icon_cache.find(key); + if (it != s_icon_cache.end()) + return it->second; + } + + std::wstring icon_resource; + + SHFILEINFO shell_file_info = { 0 }; + SHGetFileInfo(key.c_str(), 0, &shell_file_info, sizeof(shell_file_info), SHGFI_ICONLOCATION); + const std::wstring icon_path = shell_file_info.szDisplayName; + if (!icon_path.empty()) + { + icon_resource = icon_path + L"," + std::to_wstring(shell_file_info.iIcon); + } + else + { + const std::wstring extension = path.extension().wstring(); + icon_resource = query_default_icon(extension.c_str()); + } + + { + std::lock_guard cache_lock(s_icon_cache_mutex); + // Only cache successful (non-empty) lookups so a transient SHGetFileInfo/AssocQueryString + // failure cannot permanently poison the cache with an empty icon for that path. + if (!icon_resource.empty()) + { + s_icon_cache[key] = icon_resource; + } + } + + return icon_resource; + } + + // Directories: always read fresh from the shell + SHFILEINFO shell_file_info = { 0 }; + const std::wstring filepath = path.wstring(); + SHGetFileInfo(filepath.c_str(), 0, &shell_file_info, sizeof(shell_file_info), SHGFI_ICONLOCATION); + const std::wstring icon_path = shell_file_info.szDisplayName; + if (!icon_path.empty()) + { + return icon_path + L"," + std::to_wstring(shell_file_info.iIcon); + } + + return query_default_icon(L""); +} + +HICON get_explorer_icon_handle(const std::filesystem::path& path) +{ + SHFILEINFO shell_file_info = { 0 }; + const std::wstring filepath = path.wstring(); + SHGetFileInfo(filepath.c_str(), 0, &shell_file_info, sizeof(shell_file_info), SHGFI_ICON); + if (shell_file_info.hIcon) + { + return shell_file_info.hIcon; + } + + const std::wstring extension = path.extension().wstring(); + return extract_default_icon(extension.c_str()); +} + +} diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/newplus_icon_utilities.h b/src/modules/NewPlus/NewShellExtensionContextMenu/newplus_icon_utilities.h new file mode 100644 index 0000000000..aaebf7355f --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/newplus_icon_utilities.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include +#include + +namespace newplus::icon_utilities +{ + // is_directory=true skips the per-file icon cache (directory icons can change via desktop.ini) + std::wstring get_explorer_icon(const std::filesystem::path& path, bool is_directory = false); + HICON get_explorer_icon_handle(const std::filesystem::path& path); +} diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_menu.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_menu.cpp index 887701b0ac..9ab26d7483 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_menu.cpp +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_menu.cpp @@ -71,7 +71,19 @@ IFACEMETHODIMP shell_context_menu::EnumSubCommands(_COM_Outptr_ IEnumExplorerCom { try { - auto e = Make(site_of_folder); + // Get the cursor position as early as possible to get as close to the point where the context menu was + // invoked for Desktop icon placement. + // Capture in per-monitor-DPI-aware context so the stored position is always in physical screen pixels. + POINT cursor_position = { -1, -1 }; + const DPI_AWARENESS_CONTEXT prev_dpi_ctx = SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + if (!GetCursorPos(&cursor_position)) + { + cursor_position = { -1, -1 }; + } + + SetThreadDpiAwarenessContext(prev_dpi_ctx); + + auto e = Make(site_of_folder, cursor_position); return e->QueryInterface(IID_PPV_ARGS(returned_enum_commands)); } catch (const std::exception& ex) diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu.cpp index 921130baa4..47bceb4883 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu.cpp +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu.cpp @@ -6,10 +6,13 @@ using namespace Microsoft::WRL; // // Sub context menu command enumerator -shell_context_sub_menu::shell_context_sub_menu(const ComPtr site_of_folder) +shell_context_sub_menu::shell_context_sub_menu(const ComPtr site_of_folder, const POINT mouse_position_at_time_of_context_menu) { this->site_of_folder = site_of_folder; + // Capture mouse position now (at menu-open time) for more accurate desktop icon placement later + mouse_position_at_time_of_invoke = mouse_position_at_time_of_context_menu; + // Determine the New+ Template folder location const std::filesystem::path root = utilities::get_new_template_folder_location(); @@ -25,7 +28,7 @@ shell_context_sub_menu::shell_context_sub_menu(const ComPtr site_of_fo int index = 0; for (int i = 0; i < number_of_templates; i++) { - explorer_menu_item_commands.push_back(Make(templates->get_template_item(i), site_of_folder)); + explorer_menu_item_commands.push_back(Make(templates->get_template_item(i), site_of_folder, mouse_position_at_time_of_invoke)); } // Add separator to context menu diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu.h b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu.h index 2fef78ed63..58043d920f 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu.h +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu.h @@ -13,7 +13,7 @@ using namespace newplus; class shell_context_sub_menu final : public RuntimeClass, IEnumExplorerCommand> { public: - shell_context_sub_menu(const ComPtr site_of_folder); + shell_context_sub_menu(const ComPtr site_of_folder, const POINT mouse_position_at_time_of_context_menu); // IEnumExplorerCommand IFACEMETHODIMP Next(ULONG celt, __out_ecount_part(celt, *pceltFetched) IExplorerCommand** apUICommand, __out_opt ULONG* pceltFetched); @@ -26,4 +26,5 @@ protected: std::vector>::const_iterator current_command; template_folder* templates; ComPtr site_of_folder; + POINT mouse_position_at_time_of_invoke{ -1, -1 }; }; diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu_item.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu_item.cpp index a4c4e092b4..0b6fe15734 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu_item.cpp +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu_item.cpp @@ -8,14 +8,13 @@ using namespace Microsoft::WRL; // Sub context menu containing the actual list of templates shell_context_sub_menu_item::shell_context_sub_menu_item() + : template_entry(nullptr), site_of_folder(nullptr), mouse_position_at_time_of_invoke{ -1, -1 } { - this->template_entry = nullptr; } -shell_context_sub_menu_item::shell_context_sub_menu_item(const template_item* template_entry, const ComPtr site_of_folder) +shell_context_sub_menu_item::shell_context_sub_menu_item(const template_item* template_entry, const ComPtr site_of_folder, const POINT mouse_position_at_invoke) + : template_entry(template_entry), site_of_folder(site_of_folder), mouse_position_at_time_of_invoke(mouse_position_at_invoke) { - this->template_entry = template_entry; - this->site_of_folder = site_of_folder; } IFACEMETHODIMP shell_context_sub_menu_item::GetTitle(_In_opt_ IShellItemArray* items, _Outptr_result_nullonfailure_ PWSTR* title) @@ -64,7 +63,7 @@ IFACEMETHODIMP shell_context_sub_menu_item::GetState(_In_opt_ IShellItemArray* s IFACEMETHODIMP shell_context_sub_menu_item::Invoke(_In_opt_ IShellItemArray*, _In_opt_ IBindCtx*) noexcept { - return newplus::utilities::copy_template(template_entry, site_of_folder); + return newplus::utilities::copy_template(template_entry, site_of_folder, mouse_position_at_time_of_invoke); } IFACEMETHODIMP shell_context_sub_menu_item::GetFlags(_Out_ EXPCMDFLAGS* returned_flags) diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu_item.h b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu_item.h index 38d24973ed..eb0544a452 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu_item.h +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu_item.h @@ -12,7 +12,7 @@ using namespace newplus; class shell_context_sub_menu_item : public RuntimeClass, IExplorerCommand> { public: - shell_context_sub_menu_item(const template_item* template_entry, const ComPtr site_of_folder); + shell_context_sub_menu_item(const template_item* template_entry, const ComPtr site_of_folder, const POINT mouse_position_at_invoke); // IExplorerCommand IFACEMETHODIMP GetTitle(_In_opt_ IShellItemArray* items, _Outptr_result_nullonfailure_ PWSTR* title); @@ -35,6 +35,7 @@ protected: shell_context_sub_menu_item(); const template_item* template_entry; ComPtr site_of_folder; + POINT mouse_position_at_time_of_invoke; }; // Sub-context-menu separator between the list of templates menu-items and "Open templates" menu-item diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp index e52bd7234d..56f4e2197d 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp @@ -1,14 +1,24 @@ #include "pch.h" #include "template_item.h" -#include +#include "newplus_icon_utilities.h" #include "new_utilities.h" -#include +#include #include #include using namespace Microsoft::WRL; using namespace newplus; +namespace +{ + struct rename_worker_context + { + std::filesystem::path target_fullpath; + POINT mouse_position_at_invoke; + HMODULE module_reference; + }; +} + template_item::template_item(const std::filesystem::path entry) { path = entry; @@ -147,12 +157,16 @@ std::wstring template_item::remove_starting_digits_from_filename(std::wstring fi std::wstring template_item::get_explorer_icon() const { - return utilities::get_explorer_icon(path); + // Use the non-throwing filesystem query: this runs while Explorer builds the context menu, so a + // throwing directory check here could take down the shell extension. On error, treat as a file. + std::error_code ec; + const bool is_dir = std::filesystem::is_directory(path, ec) && !ec; + return icon_utilities::get_explorer_icon(path, is_dir); } HICON template_item::get_explorer_icon_handle() const { - return utilities::get_explorer_icon_handle(path); + return icon_utilities::get_explorer_icon_handle(path); } std::filesystem::path template_item::copy_object_to(const HWND window_handle, const std::filesystem::path destination) const @@ -188,18 +202,103 @@ void template_item::refresh_target(const std::filesystem::path target_final_full SHChangeNotify(SHCNE_CREATE, SHCNF_PATH | SHCNF_FLUSH, target_final_fullpath.wstring().c_str(), NULL); } -void template_item::enter_rename_mode(const std::filesystem::path target_fullpath) const +void template_item::enter_rename_mode(const std::filesystem::path target_fullpath, const POINT mouse_position_at_invoke) const { - std::thread thread_for_renaming_workaround(rename_on_other_thread_workaround, target_fullpath); - thread_for_renaming_workaround.detach(); + HMODULE module_reference = nullptr; + if (!GetModuleHandleExW( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, + reinterpret_cast(&module_instance_handle), + &module_reference)) + { + return; + } + + std::unique_ptr context; + try + { + context = std::make_unique( + target_fullpath, + mouse_position_at_invoke, + module_reference); + } + catch (...) + { + FreeLibrary(module_reference); + return; + } + + active_rename_workers.fetch_add(1); + const HANDLE thread = CreateThread(nullptr, 0, rename_worker_thread_proc, context.get(), 0, nullptr); + if (thread == nullptr) + { + active_rename_workers.fetch_sub(1); + FreeLibrary(module_reference); + return; + } + + context.release(); + CloseHandle(thread); } -void template_item::rename_on_other_thread_workaround(const std::filesystem::path target_fullpath) +DWORD WINAPI template_item::rename_worker_thread_proc(void* parameter) { - // Have been unable to have Windows Explorer Shell enter rename mode from the main thread - // Sleep for a bit to only enter rename mode when icon has been drawn. - const std::chrono::milliseconds approx_wait_for_icon_redraw_not_needed{ 50 }; - std::this_thread::sleep_for(std::chrono::milliseconds(approx_wait_for_icon_redraw_not_needed)); + std::unique_ptr context(static_cast(parameter)); + const HMODULE module_reference = context->module_reference; - newplus::utilities::explorer_enter_rename_mode(target_fullpath); + rename_on_other_thread_workaround(context->target_fullpath, context->mouse_position_at_invoke); + context.reset(); + active_rename_workers.fetch_sub(1); + FreeLibraryAndExitThread(module_reference, 0); +} + +void template_item::rename_on_other_thread_workaround(const std::filesystem::path& target_fullpath, const POINT mouse_position_at_invoke) +{ + struct worker_cleanup + { + bool com_initialized = false; + + ~worker_cleanup() + { + if (com_initialized) + { + CoUninitialize(); + } + } + } cleanup; + + const HRESULT com_result = CoInitializeEx(nullptr, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE); + if (FAILED(com_result)) + { + return; + } + cleanup.com_initialized = true; + + // Have been unable to have Windows Explorer Shell enter rename mode from the main thread. + // Poll until the item appears in the folder view so icon is positioned and rename mode is entered + // without a jump in the positioning + constexpr std::chrono::milliseconds initial_poll_interval{ 30 }; + constexpr std::chrono::milliseconds maximum_poll_interval{ 240 }; + constexpr std::chrono::milliseconds poll_timeout{ 2000 }; + const auto deadline = std::chrono::steady_clock::now() + poll_timeout; + auto poll_interval = initial_poll_interval; + + try + { + while (std::chrono::steady_clock::now() < deadline) + { + if (newplus::utilities::explorer_enter_rename_mode_and_reposition(target_fullpath, mouse_position_at_invoke)) + { + return; + } + std::this_thread::sleep_for(poll_interval); + poll_interval = std::min(poll_interval * 2, maximum_poll_interval); + } + + // Final attempt: the item may have appeared during the last sleep interval (after the previous + // attempt but before the deadline), so try once more so a just-in-time item still enters rename mode. + newplus::utilities::explorer_enter_rename_mode_and_reposition(target_fullpath, mouse_position_at_invoke); + } + catch (...) + { + } } diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.h b/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.h index 813d7aa1a7..9bf0427ffe 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.h +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.h @@ -27,12 +27,13 @@ namespace newplus void refresh_target(const std::filesystem::path target_final_fullpath) const; - void enter_rename_mode(const std::filesystem::path target_fullpath) const; + void enter_rename_mode(const std::filesystem::path target_fullpath, const POINT mouse_position_at_invoke) const; std::filesystem::path path; private: - static void rename_on_other_thread_workaround(const std::filesystem::path target_fullpath); + static DWORD WINAPI rename_worker_thread_proc(void* parameter); + static void rename_on_other_thread_workaround(const std::filesystem::path& target_fullpath, const POINT mouse_position_at_invoke); std::wstring remove_starting_digits_from_filename(std::wstring filename) const; };