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) <bozhang@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 41b6b39b-b620-4e02-b7ca-8ae470a9c1e2
This commit is contained in:
Christian Gaarden Gaardmark
2026-08-11 23:23:48 -07:00
committed by GitHub
parent c4431304c4
commit b605fd35c5
21 changed files with 498 additions and 134 deletions

View File

@@ -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

View File

@@ -14,7 +14,6 @@
</PropertyGroup>
<PropertyGroup Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
@@ -85,6 +84,7 @@
<ClInclude Include="..\NewShellExtensionContextMenu\constants.h" />
<ClInclude Include="..\NewShellExtensionContextMenu\helpers_filesystem.h" />
<ClInclude Include="..\NewShellExtensionContextMenu\helpers_variables.h" />
<ClInclude Include="..\NewShellExtensionContextMenu\newplus_icon_utilities.h" />
<ClInclude Include="..\NewShellExtensionContextMenu\new_utilities.h" />
<ClInclude Include="..\NewShellExtensionContextMenu\settings.h" />
<ClInclude Include="..\NewShellExtensionContextMenu\shell_context_sub_menu.h" />
@@ -101,6 +101,7 @@
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\NewShellExtensionContextMenu\Helpers.cpp" />
<ClCompile Include="..\NewShellExtensionContextMenu\newplus_icon_utilities.cpp" />
<ClCompile Include="..\NewShellExtensionContextMenu\new_utilities.cpp" />
<ClCompile Include="..\NewShellExtensionContextMenu\powertoys_module.cpp" />
<ClCompile Include="..\NewShellExtensionContextMenu\settings.cpp" />

View File

@@ -63,6 +63,12 @@
<ClInclude Include="..\NewShellExtensionContextMenu\helpers_variables.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\NewShellExtensionContextMenu\Helpers.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\NewShellExtensionContextMenu\newplus_icon_utilities.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
@@ -98,7 +104,10 @@
<ClCompile Include="..\NewShellExtensionContextMenu\new_utilities.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\powerrename\lib\Helpers.cpp">
<ClCompile Include="..\NewShellExtensionContextMenu\Helpers.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\NewShellExtensionContextMenu\newplus_icon_utilities.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
@@ -121,5 +130,6 @@
</ItemGroup>
<ItemGroup>
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natstepfilter" />
</ItemGroup>
</Project>

View File

@@ -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<InProc>::GetModule().GetObjectCount() == 0 ? S_OK : S_FALSE;
return Module<InProc>::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)

View File

@@ -1,6 +1,8 @@
#pragma once
#include <atomic>
#include <common/Telemetry/EtwTrace/EtwTrace.h>
extern HMODULE module_instance_handle;
extern Shared::Trace::ETWTrace trace;
extern std::atomic_uint32_t active_rename_workers;

View File

@@ -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
{

View File

@@ -42,4 +42,5 @@ protected:
ComPtr<IUnknown> site_of_folder;
newplus::template_folder* templates = nullptr;
std::vector<HBITMAP> bitmap_handles;
POINT mouse_position_at_time_of_invoke = {-1, -1};
};

View File

@@ -16,13 +16,11 @@
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
@@ -124,6 +122,7 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv</Command>
<ClInclude Include="settings.h" />
<ClInclude Include="trace.h" />
<ClInclude Include="new_utilities.h" />
<ClInclude Include="newplus_icon_utilities.h" />
<ClInclude Include="RuntimeRegistration.h" />
<ClInclude Include="resource.base.h" />
<ClInclude Include="template_folder.h" />
@@ -134,6 +133,7 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv</Command>
<ItemGroup>
<ClCompile Include="Helpers.cpp" />
<ClCompile Include="new_utilities.cpp" />
<ClCompile Include="newplus_icon_utilities.cpp" />
<ClCompile Include="shell_context_menu.cpp" />
<ClCompile Include="shell_context_sub_menu.cpp" />
<ClCompile Include="shell_context_sub_menu_item.cpp" />

View File

@@ -34,7 +34,10 @@
<ClCompile Include="new_utilities.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\powerrename\lib\Helpers.cpp">
<ClCompile Include="newplus_icon_utilities.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Helpers.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
@@ -87,6 +90,12 @@
<ClInclude Include="RuntimeRegistration.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="newplus_icon_utilities.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Helpers.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
@@ -202,8 +211,14 @@
</ResourceCompile>
</ItemGroup>
<ItemGroup>
<CopyFileToFolders Include="TemplateExamples\Any files or folders placed in the template folder are available via New+.txt" />
<CopyFileToFolders Include="TemplateExamples\Example folder\Example txt file.txt" />
<CopyFileToFolders Include="TemplateExamples\Example folder\Another example txt file.txt" />
<CopyFileToFolders Include="TemplateExamples\Any files or folders placed in the template folder are available via New+.txt">
<Filter>Template Examples</Filter>
</CopyFileToFolders>
<CopyFileToFolders Include="TemplateExamples\Example folder\Example txt file.txt">
<Filter>Template Examples\Example folder</Filter>
</CopyFileToFolders>
<CopyFileToFolders Include="TemplateExamples\Example folder\Another example txt file.txt">
<Filter>Template Examples\Example folder</Filter>
</CopyFileToFolders>
</ItemGroup>
</Project>

View File

@@ -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<InProc>::GetModule().GetObjectCount() == 0 ? S_OK : S_FALSE;
return Module<InProc>::GetModule().GetObjectCount() == 0 && active_rename_workers.load() == 0 ? S_OK : S_FALSE;
}
STDAPI DllGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Outptr_ LPVOID FAR* ppv)

View File

@@ -1,6 +1,8 @@
#pragma once
#include <atomic>
#include <common/Telemetry/EtwTrace/EtwTrace.h>
extern HMODULE module_instance_handle;
extern Shared::Trace::ETWTrace trace;
extern Shared::Trace::ETWTrace trace;
extern std::atomic_uint32_t active_rename_workers;

View File

@@ -10,7 +10,9 @@
#include "template_item.h"
#include "trace.h"
#include "helpers_variables.h"
#include <shellscalingapi.h>
#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<HICON>(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<IShellWindows> shell_windows;
CComPtr<IShellWindows> shell_windows;
HRESULT hr;
if (FAILED(CoCreateInstance(CLSID_ShellWindows, NULL, CLSCTX_ALL, IID_PPV_ARGS(&shell_windows))))
{
return;
return false;
}
long window_handle;
ComPtr<IDispatch> shell_window;
long desktop_window_handle = 0;
CComPtr<IDispatch> 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<IWebBrowserApp> web_browser_app;
CComPtr<IWebBrowserApp> 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<IServiceProvider> service_provider;
shell_window.As(&service_provider);
ComPtr<IShellBrowser> shell_browser;
service_provider->QueryService(SID_STopLevelBrowser, IID_PPV_ARGS(&shell_browser));
ComPtr<IShellView> shell_view;
shell_browser->QueryActiveShellView(&shell_view);
ComPtr<IFolderView> folder_view;
shell_view.As(&folder_view);
CComPtr<IServiceProvider> service_provider;
if (FAILED(shell_window->QueryInterface(IID_PPV_ARGS(&service_provider))) || service_provider == nullptr)
{
return false;
}
CComPtr<IShellBrowser> shell_browser;
if (FAILED(service_provider->QueryService(SID_STopLevelBrowser, IID_PPV_ARGS(&shell_browser))) || shell_browser == nullptr)
{
return false;
}
CComPtr<IShellView> shell_view;
if (FAILED(shell_browser->QueryActiveShellView(&shell_view)) || shell_view == nullptr)
{
return false;
}
CComPtr<IFolderView> 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<IShellFolder> 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<int>(invoke_dpi_x), 96);
screen_point.x = std::max<LONG>(screen_point.x, min_margin);
screen_point.y = std::max<LONG>(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<IUnknown> site_of_folder)
inline HRESULT copy_template(const template_item* template_entry, const ComPtr<IUnknown> 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)
{

View File

@@ -0,0 +1,161 @@
#include "pch.h"
// pch.h first
#include "newplus_icon_utilities.h"
#include <mutex>
#include <unordered_map>
#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<UINT>(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<std::wstring, std::wstring> s_icon_cache;
const std::wstring key = path.wstring();
{
std::lock_guard<std::mutex> 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<std::mutex> 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());
}
}

View File

@@ -0,0 +1,12 @@
#pragma once
#include <filesystem>
#include <string>
#include <windows.h>
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);
}

View File

@@ -71,7 +71,19 @@ IFACEMETHODIMP shell_context_menu::EnumSubCommands(_COM_Outptr_ IEnumExplorerCom
{
try
{
auto e = Make<shell_context_sub_menu>(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<shell_context_sub_menu>(site_of_folder, cursor_position);
return e->QueryInterface(IID_PPV_ARGS(returned_enum_commands));
}
catch (const std::exception& ex)

View File

@@ -6,10 +6,13 @@
using namespace Microsoft::WRL;
// // Sub context menu command enumerator
shell_context_sub_menu::shell_context_sub_menu(const ComPtr<IUnknown> site_of_folder)
shell_context_sub_menu::shell_context_sub_menu(const ComPtr<IUnknown> 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<IUnknown> site_of_fo
int index = 0;
for (int i = 0; i < number_of_templates; i++)
{
explorer_menu_item_commands.push_back(Make<shell_context_sub_menu_item>(templates->get_template_item(i), site_of_folder));
explorer_menu_item_commands.push_back(Make<shell_context_sub_menu_item>(templates->get_template_item(i), site_of_folder, mouse_position_at_time_of_invoke));
}
// Add separator to context menu

View File

@@ -13,7 +13,7 @@ using namespace newplus;
class shell_context_sub_menu final : public RuntimeClass<RuntimeClassFlags<ClassicCom>, IEnumExplorerCommand>
{
public:
shell_context_sub_menu(const ComPtr<IUnknown> site_of_folder);
shell_context_sub_menu(const ComPtr<IUnknown> 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<ComPtr<IExplorerCommand>>::const_iterator current_command;
template_folder* templates;
ComPtr<IUnknown> site_of_folder;
POINT mouse_position_at_time_of_invoke{ -1, -1 };
};

View File

@@ -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<IUnknown> site_of_folder)
shell_context_sub_menu_item::shell_context_sub_menu_item(const template_item* template_entry, const ComPtr<IUnknown> 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)

View File

@@ -12,7 +12,7 @@ using namespace newplus;
class shell_context_sub_menu_item : public RuntimeClass<RuntimeClassFlags<ClassicCom>, IExplorerCommand>
{
public:
shell_context_sub_menu_item(const template_item* template_entry, const ComPtr<IUnknown> site_of_folder);
shell_context_sub_menu_item(const template_item* template_entry, const ComPtr<IUnknown> 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<IUnknown> 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

View File

@@ -1,14 +1,24 @@
#include "pch.h"
#include "template_item.h"
#include <shellapi.h>
#include "newplus_icon_utilities.h"
#include "new_utilities.h"
#include <cassert>
#include <chrono>
#include <thread>
#include <shlobj_core.h>
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<LPCWSTR>(&module_instance_handle),
&module_reference))
{
return;
}
std::unique_ptr<rename_worker_context> context;
try
{
context = std::make_unique<rename_worker_context>(
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<rename_worker_context> context(static_cast<rename_worker_context*>(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 (...)
{
}
}

View File

@@ -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;
};