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