From c8743ab9527307e42e682da86ef57b7888b56d9a Mon Sep 17 00:00:00 2001 From: geezmolycos Date: Wed, 11 Feb 2026 00:10:39 +0800 Subject: [PATCH 1/7] experiment with context menu for third party file manager UI --- src/bin/shell.nss | 4 +++- src/dll/src/ContextMenu.cpp | 25 ++++++++++++++++++++++--- src/dll/src/Main.cpp | 5 +++++ src/dll/src/Selections.cpp | 21 +++++++++++++++++++++ src/shared/System/Log.h | 2 +- 5 files changed, 52 insertions(+), 5 deletions(-) diff --git a/src/bin/shell.nss b/src/bin/shell.nss index 8ddaaca..32d8a58 100644 --- a/src/bin/shell.nss +++ b/src/bin/shell.nss @@ -1,7 +1,7 @@ settings { priority=1 - exclude.where = !process.is_explorer + exclude.where = !process.is_explorer && (process.name != "XYplorer") && (process.name != "notepad") showdelay = 200 // Options to allow modification of system items modify.remove.duplicate=1 @@ -34,3 +34,5 @@ import 'imports/file-manage.nss' import 'imports/develop.nss' import 'imports/goto.nss' import 'imports/taskbar.nss' + +item(title = 'hello' cmd=msg('hello')) diff --git a/src/dll/src/ContextMenu.cpp b/src/dll/src/ContextMenu.cpp index e7239da..14decce 100644 --- a/src/dll/src/ContextMenu.cpp +++ b/src/dll/src/ContextMenu.cpp @@ -176,6 +176,8 @@ namespace Nilesoft //d2d.create_render(); //d2d.create_res(); + _log.info(L"ContextMenu::ContextMenu %d", 1); + Window window = hWnd; hwnd.owner = hWnd; @@ -212,6 +214,8 @@ namespace Nilesoft ThreadId = window.get_threadId(&ProcessId); + _log.info(L"ContextMenu::ContextMenu ProcessId: %d, ThreadId: %d", ProcessId, ThreadId); + GUITHREADINFO gti = { sizeof(GUITHREADINFO) }; if(::GetGUIThreadInfo(ThreadId, >i)) { @@ -4047,6 +4051,8 @@ namespace Nilesoft auto items = &menu->items; + __trace(L"In build_main_system_menuitems: "); + for(auto si : _cache->statics) { if(si->has_clsid) @@ -4081,6 +4087,7 @@ namespace Nilesoft _this.length = item->length; ;// mii->title.length(); _this.title = item->title; _this.title_normalize = item->name; + __trace(L"item(instruction) #%d, name=%ls", i, item->name.c_str()); } _context._this = &_this; @@ -4217,6 +4224,8 @@ namespace Nilesoft auto itmes_count = ::GetMenuItemCount(hMenu); + __trace(L"In build_system_menuitems: items count %d", itmes_count); + menu->items.reserve(itmes_count); for(int i = 0; i < itmes_count; i++) @@ -4267,6 +4276,8 @@ namespace Nilesoft if(mii.cch > 0) { item->title = title.release(mii.cch).move(); + + __trace(L" system menu item #%d, name=%ls", i, item->title.c_str()); item->hash = MenuItemInfo::normalize(item->title, &item->name, &item->tab, &item->length, &item->keys); item->ui = Initializer::get_muid(item->hash); @@ -4343,6 +4354,7 @@ namespace Nilesoft if(!Initializer::Inited()) return false; __trace(L"ContextMenu init"); + _log.info(L"ContextMenu init"); auto initializer = Initializer::instance; @@ -4353,9 +4365,12 @@ namespace Nilesoft Selected.Window.handle = hwnd.owner; Selected.Window.hInstance = _window.instance(); - if(!Selected.QueryShellWindow()) + bool has_shell_window = Selected.QueryShellWindow(); + __trace(L"Out QueryShellWindow: Window.has_IShellBrowser=%d, explorer=%d, Window.id=%d", Selected.Window.has_IShellBrowser, Selected.Window.explorer, Selected.Window.id); + + if(!has_shell_window) { - __trace(L"QueryShellWindow"); + __trace(L"No Shell window"); return false; } @@ -4467,7 +4482,11 @@ namespace Nilesoft __system_menu_tree->type = 10; __map_system_menu[0] = __system_menu_tree; - if(0 == ::GetPropW(hwnd.owner, UxSubclass)) + auto prop = ::GetPropW(hwnd.owner, UxSubclass); + + __trace(L"UxSubclass Prop: %d, modify items enabled: %d", prop, _settings.modify_items.enabled); + + if(0 == prop) build_system_menuitems(_hMenu_original, __system_menu_tree, true); if(_settings.modify_items.enabled) diff --git a/src/dll/src/Main.cpp b/src/dll/src/Main.cpp index 0c56bda..c8b461e 100644 --- a/src/dll/src/Main.cpp +++ b/src/dll/src/Main.cpp @@ -1150,6 +1150,11 @@ STDAPI DllGetClassObject(_In_ REFCLSID rclsid, [[maybe_unused]] _In_ REFIID riid //Guid iid2 = riid; if(ppv) *ppv = nullptr; + // __trace(L"In DllGetClassObject: iid={%08lX-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}\n", iid.Data1, iid.Data2, iid.Data3, + // iid.Data4[0], iid.Data4[1], + // iid.Data4[2], iid.Data4[3], iid.Data4[4], + // iid.Data4[5], iid.Data4[6], iid.Data4[7]); + __try { if(iid.equals(IID_FolderExtensions)) diff --git a/src/dll/src/Selections.cpp b/src/dll/src/Selections.cpp index 5dd1e11..7551fe0 100644 --- a/src/dll/src/Selections.cpp +++ b/src/dll/src/Selections.cpp @@ -375,6 +375,7 @@ namespace Nilesoft bool Selections::verify_types(const FileSystemObjects &fso) const { + return true; if(fso.any_types) return true; @@ -974,6 +975,20 @@ namespace Nilesoft */ HWND current_window{}; + + if (!Window.has_IShellBrowser) { + current_window = Window.handle; + while(current_window) + { + // Get IShellBrowser interface for current hWnd + ShellBrowser = GetIShellBrowser(current_window, true); + if(ShellBrowser) break; + current_window = ::GetParent(current_window); + } + __trace(L"In QuerySelected: Window has no IShellBrowser but try anyways"); + __trace(L"Ptr to ShellBrowser: %p", ShellBrowser); + + } if(!Window.has_IShellBrowser) return false; @@ -1185,6 +1200,7 @@ MultitaskingViewFrame // Multitask Button */ bool Selections::QueryShellWindow() { + __trace(L"In QueryShellWindow"); auto window = &Window.handle; try { @@ -1217,9 +1233,11 @@ Edit >> ComboBox > #32770 > Notepad auto hParent = window->parent(); Hash cur_Parent = hParent.class_hash(); + __trace(L"In QueryShellWindow: hParent class: %ls", hParent.class_name().c_str()); if(cur_hash.equals({ WC_SHELLDLL_DefView, WC_SysListView32, WC__STATIC, WC_ShellTabWindowClass/*, WC_CabinetWClass, WC_ExplorerWClass*//*, WC_DirectUIHWND*/ })) { + __trace(L"In QueryShellWindow: is explorer"); Window.id = WINDOW_EXPLORER; Window.explorer = true; @@ -1239,6 +1257,7 @@ Edit >> ComboBox > #32770 > Notepad } else if(cur_hash.equals(WC_SysTreeView32)) { + __trace(L"In QueryShellWindow: is WC_SysTreeView32 explorer_tree"); if(!cur_Parent.equals(WC_NamespaceTreeControl)) { auto active = window->ActiveWindow(); @@ -1260,6 +1279,7 @@ Edit >> ComboBox > #32770 > Notepad } else if(cur_hash.equals({ WC__EDIT, WC__SEARCHEDITBOXFAKEWINDOW })) { + __trace(L"In QueryShellWindow: is EDIT"); Window.id = WINDOW_EDIT; Types[FSO_EDIT] = 1; Types[FSO_COUNT]++; @@ -1267,6 +1287,7 @@ Edit >> ComboBox > #32770 > Notepad } else { + __trace(L"In QueryShellWindow: is taskbar or other"); Hash root_owner_hash; bool from_root_owner = false; bool from_taskbar = cur_hash.equals({ WC_Shell_TrayWnd, WC_Shell_SecondaryTrayWnd, diff --git a/src/shared/System/Log.h b/src/shared/System/Log.h index 6f92ff9..9b8ee93 100644 --- a/src/shared/System/Log.h +++ b/src/shared/System/Log.h @@ -1,6 +1,6 @@ #pragma once -//#define TRACE +#define TRACE namespace Nilesoft { From 7378ab27ef84f5aeba540d0b9ee7f0dbe64093f5 Mon Sep 17 00:00:00 2001 From: geezmolycos Date: Thu, 12 Feb 2026 03:21:10 +0800 Subject: [PATCH 2/7] add IShellExtInit, IContextMenu handler, first step to retrieve file info in third party file managers --- src/dll/Shell.vcxproj | 4 + src/dll/src/ClassFactory.cpp | 109 ++++++++ src/dll/src/ContextMenu.cpp | 8 +- src/dll/src/FileContextMenuExt.cpp | 398 +++++++++++++++++++++++++++++ src/dll/src/Main.cpp | 20 ++ 5 files changed, 535 insertions(+), 4 deletions(-) create mode 100644 src/dll/src/ClassFactory.cpp create mode 100644 src/dll/src/FileContextMenuExt.cpp diff --git a/src/dll/Shell.vcxproj b/src/dll/Shell.vcxproj index 76e5293..7f8e447 100644 --- a/src/dll/Shell.vcxproj +++ b/src/dll/Shell.vcxproj @@ -168,6 +168,8 @@ + + @@ -185,6 +187,8 @@ + + diff --git a/src/dll/src/ClassFactory.cpp b/src/dll/src/ClassFactory.cpp new file mode 100644 index 0000000..96af57b --- /dev/null +++ b/src/dll/src/ClassFactory.cpp @@ -0,0 +1,109 @@ +/****************************** Module Header ******************************\ +Module Name: ClassFactory.cpp +Project: CppShellExtContextMenuHandler +Copyright (c) Microsoft Corporation. + +The file implements the class factory for the FileContextMenuExt COM class. + +This source is subject to the Microsoft Public License. +See http://www.microsoft.com/opensource/licenses.mspx#Ms-PL. +All other rights reserved. + +THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, +EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. +\***************************************************************************/ + +#include +#include "Include/ClassFactory.h" +#include "Include/FileContextMenuExt.h" +#include +#include +#pragma comment(lib, "shlwapi.lib") + +extern Logger &_log; + +extern long g_cDllRef; + + +ClassFactory::ClassFactory() : m_cRef(1) +{ + InterlockedIncrement(&g_cDllRef); +} + +ClassFactory::~ClassFactory() +{ + InterlockedDecrement(&g_cDllRef); +} + + +// +// IUnknown +// + +IFACEMETHODIMP ClassFactory::QueryInterface(REFIID riid, void **ppv) +{ + __trace(L"In ClassFactory::QueryInterface"); + static const QITAB qit[] = + { + QITABENT(ClassFactory, IClassFactory), + { 0 }, + }; + return QISearch(this, qit, riid, ppv); +} + +IFACEMETHODIMP_(ULONG) ClassFactory::AddRef() +{ + return InterlockedIncrement(&m_cRef); +} + +IFACEMETHODIMP_(ULONG) ClassFactory::Release() +{ + ULONG cRef = InterlockedDecrement(&m_cRef); + if (0 == cRef) + { + delete this; + } + return cRef; +} + + +// +// IClassFactory +// + +IFACEMETHODIMP ClassFactory::CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppv) +{ + __trace(L"In ClassFactory::CreateInstance"); + HRESULT hr = CLASS_E_NOAGGREGATION; + + // pUnkOuter is used for aggregation. We do not support it in the sample. + if (pUnkOuter == NULL) + { + hr = E_OUTOFMEMORY; + + // Create the COM component. + FileContextMenuExt *pExt = new (std::nothrow) FileContextMenuExt(); + if (pExt) + { + // Query the specified interface. + hr = pExt->QueryInterface(riid, ppv); + pExt->Release(); + } + } + + return hr; +} + +IFACEMETHODIMP ClassFactory::LockServer(BOOL fLock) +{ + if (fLock) + { + InterlockedIncrement(&g_cDllRef); + } + else + { + InterlockedDecrement(&g_cDllRef); + } + return S_OK; +} \ No newline at end of file diff --git a/src/dll/src/ContextMenu.cpp b/src/dll/src/ContextMenu.cpp index 14decce..0ba75ec 100644 --- a/src/dll/src/ContextMenu.cpp +++ b/src/dll/src/ContextMenu.cpp @@ -4051,7 +4051,7 @@ namespace Nilesoft auto items = &menu->items; - __trace(L"In build_main_system_menuitems: "); + // __trace(L"In build_main_system_menuitems: "); for(auto si : _cache->statics) { @@ -4087,7 +4087,7 @@ namespace Nilesoft _this.length = item->length; ;// mii->title.length(); _this.title = item->title; _this.title_normalize = item->name; - __trace(L"item(instruction) #%d, name=%ls", i, item->name.c_str()); + // __trace(L"item(instruction) #%d, name=%ls", i, item->name.c_str()); } _context._this = &_this; @@ -4224,7 +4224,7 @@ namespace Nilesoft auto itmes_count = ::GetMenuItemCount(hMenu); - __trace(L"In build_system_menuitems: items count %d", itmes_count); + // __trace(L"In build_system_menuitems: items count %d", itmes_count); menu->items.reserve(itmes_count); @@ -4277,7 +4277,7 @@ namespace Nilesoft { item->title = title.release(mii.cch).move(); - __trace(L" system menu item #%d, name=%ls", i, item->title.c_str()); + // __trace(L" system menu item #%d, name=%ls", i, item->title.c_str()); item->hash = MenuItemInfo::normalize(item->title, &item->name, &item->tab, &item->length, &item->keys); item->ui = Initializer::get_muid(item->hash); diff --git a/src/dll/src/FileContextMenuExt.cpp b/src/dll/src/FileContextMenuExt.cpp new file mode 100644 index 0000000..1145646 --- /dev/null +++ b/src/dll/src/FileContextMenuExt.cpp @@ -0,0 +1,398 @@ +/****************************** Module Header ******************************\ +Module Name: FileContextMenuExt.cpp +Project: CppShellExtContextMenuHandler +Copyright (c) Microsoft Corporation. + +The code sample demonstrates creating a Shell context menu handler with C++. + +A context menu handler is a shell extension handler that adds commands to an +existing context menu. Context menu handlers are associated with a particular +file class and are called any time a context menu is displayed for a member +of the class. While you can add items to a file class context menu with the +registry, the items will be the same for all members of the class. By +implementing and registering such a handler, you can dynamically add items to +an object's context menu, customized for the particular object. + +The example context menu handler adds the menu item "Display File Name (C++)" +to the context menu when you right-click a .cpp file in the Windows Explorer. +Clicking the menu item brings up a message box that displays the full path +of the .cpp file. + +This source is subject to the Microsoft Public License. +See http://www.microsoft.com/opensource/licenses.mspx#Ms-PL. +All other rights reserved. + +THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, +EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. +\***************************************************************************/ + +#include +#include "Include/FileContextMenuExt.h" +#include +#include +#pragma comment(lib, "shlwapi.lib") + +extern Logger &_log; + +extern HINSTANCE g_hInst; +extern long g_cDllRef; + +#define IDM_DISPLAY 0 // The command's identifier offset + +wchar_t menutext[] = L"Menu text"; + +FileContextMenuExt::FileContextMenuExt(void) + : m_cRef(1), + m_pszMenuText(menutext), + m_pszVerb("verb"), + m_pwszVerb(L"verb"), + m_pszVerbCanonicalName("canocial name"), + m_pwszVerbCanonicalName(L"canocial name"), + m_pszVerbHelpText("help text"), + m_pwszVerbHelpText(L"help text") +{ + InterlockedIncrement(&g_cDllRef); + + // Load the bitmap for the menu item. + // If you want the menu item bitmap to be transparent, the color depth of + // the bitmap must not be greater than 8bpp. + // m_hMenuBmp = LoadImage(g_hInst, MAKEINTRESOURCE(IDB_OK), + // IMAGE_BITMAP, 0, 0, LR_DEFAULTSIZE | LR_LOADTRANSPARENT); +} + +FileContextMenuExt::~FileContextMenuExt(void) +{ + if (m_hMenuBmp) + { + DeleteObject(m_hMenuBmp); + m_hMenuBmp = NULL; + } + + InterlockedDecrement(&g_cDllRef); +} + + +void FileContextMenuExt::OnVerbDisplayFileName(HWND hWnd) +{ + for (size_t i = 0; i < m_vSelectedFiles.size() && i < 2; i++) + { + wchar_t szMessage[300]; + if (SUCCEEDED(StringCchPrintf(szMessage, ARRAYSIZE(szMessage), + L"The selected file is:\r\n\r\n%s", m_vSelectedFiles[i].c_str()))) + { + ::MessageBoxW(hWnd, szMessage, L"friendly name", MB_OK); + } + } +} + + +#pragma region IUnknown + +// Query to the interface the component supported. +IFACEMETHODIMP FileContextMenuExt::QueryInterface(REFIID riid, void **ppv) +{ + __trace(L"In FileContextMenuExt::QueryInterface"); + static const QITAB qit[] = + { + QITABENT(FileContextMenuExt, IContextMenu), + QITABENT(FileContextMenuExt, IShellExtInit), + { 0 }, + }; + return QISearch(this, qit, riid, ppv); +} + +// Increase the reference count for an interface on an object. +IFACEMETHODIMP_(ULONG) FileContextMenuExt::AddRef() +{ + return InterlockedIncrement(&m_cRef); +} + +// Decrease the reference count for an interface on an object. +IFACEMETHODIMP_(ULONG) FileContextMenuExt::Release() +{ + ULONG cRef = InterlockedDecrement(&m_cRef); + if (0 == cRef) + { + delete this; + } + + return cRef; +} + +#pragma endregion + + +#pragma region IShellExtInit + +// Initialize the context menu handler. +IFACEMETHODIMP FileContextMenuExt::Initialize( + LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hKeyProgID) +{ + __trace(L"In FileContextMenuExt::Initialize"); + if (NULL == pDataObj) + { + return E_INVALIDARG; + } + + HRESULT hr = E_FAIL; + + FORMATETC fe = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; + STGMEDIUM stm; + + // The pDataObj pointer contains the objects being acted upon. In this + // example, we get an HDROP handle for enumerating the selected files and + // folders. + if (SUCCEEDED(pDataObj->GetData(&fe, &stm))) + { + // Get an HDROP handle. + HDROP hDrop = static_cast(GlobalLock(stm.hGlobal)); + if (hDrop != NULL) + { + // Determine how many files are involved in this operation. This + // code sample displays the custom context menu item when only + // one file is selected. + UINT nFiles = DragQueryFile(hDrop, 0xFFFFFFFF, NULL, 0); + __trace(L"In FileContextMenuExt::Initialize: %d files selected", nFiles); + for (size_t i = 0; i < nFiles; i++) + { + wchar_t szSelectedFile[MAX_PATH] = { 0 }; + // Get the path of the file. + if (0 != ::DragQueryFileW(hDrop, i, szSelectedFile, static_cast(ARRAYSIZE(szSelectedFile)))) + { + m_vSelectedFiles.push_back(szSelectedFile); + __trace(L"In FileContextMenuExt::Initialize: %ls", szSelectedFile); + hr = S_OK; + continue; + } + hr = E_FAIL; + break; + } + + GlobalUnlock(stm.hGlobal); + } + + ReleaseStgMedium(&stm); + } + + if (S_OK == hr) + { + for (auto file = m_vSelectedFiles.cbegin(); file != m_vSelectedFiles.cend(); ++file) + { + const wchar_t *dot = wcsrchr(file->c_str(), L'.'); + __trace(L"In FileContextMenuExt::Initialize: ext: %ls", dot); + if (dot && 0 != _wcsicmp(dot, L".txt")) + { + hr = E_INVALIDARG; + break; + } + } + } + + __trace(L"Out FileContextMenuExt::Initialize: hr=%08X", hr); + + // If any value other than S_OK is returned from the method, the context + // menu item is not displayed. + return hr; +} + +#pragma endregion + + +#pragma region IContextMenu + +// +// FUNCTION: FileContextMenuExt::QueryContextMenu +// +// PURPOSE: The Shell calls IContextMenu::QueryContextMenu to allow the +// context menu handler to add its menu items to the menu. It +// passes in the HMENU handle in the hmenu parameter. The +// indexMenu parameter is set to the index to be used for the +// first menu item that is to be added. +// +IFACEMETHODIMP FileContextMenuExt::QueryContextMenu( + HMENU hMenu, UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags) +{ + __trace(L"In FileContextMenuExt::QueryContextMenu"); + // If uFlags include CMF_DEFAULTONLY then we should not do anything. + if (CMF_DEFAULTONLY & uFlags) + { + return MAKE_HRESULT(SEVERITY_SUCCESS, 0, USHORT(0)); + } + + // Use either InsertMenu or InsertMenuItem to add menu items. + // Learn how to add sub-menu from: + // http://www.codeproject.com/KB/shell/ctxextsubmenu.aspx + + MENUITEMINFOW mii = { sizeof(mii) }; + mii.fMask = MIIM_BITMAP | MIIM_STRING | MIIM_FTYPE | MIIM_ID | MIIM_STATE; + mii.wID = idCmdFirst + IDM_DISPLAY; + mii.fType = MFT_STRING; + mii.dwTypeData = m_pszMenuText; + mii.fState = MFS_ENABLED; + mii.hbmpItem = static_cast(m_hMenuBmp); + if (!::InsertMenuItemW(hMenu, indexMenu, TRUE, &mii)) + { + return HRESULT_FROM_WIN32(GetLastError()); + } + + // Add a separator. + MENUITEMINFOW sep = { sizeof(sep) }; + sep.fMask = MIIM_TYPE; + sep.fType = MFT_SEPARATOR; + if (!::InsertMenuItemW(hMenu, indexMenu + 1, TRUE, &sep)) + { + return HRESULT_FROM_WIN32(GetLastError()); + } + + // Return an HRESULT value with the severity set to SEVERITY_SUCCESS. + // Set the code value to the offset of the largest command identifier + // that was assigned, plus one (1). + return MAKE_HRESULT(SEVERITY_SUCCESS, 0, USHORT(IDM_DISPLAY + 1)); +} + + +// +// FUNCTION: FileContextMenuExt::InvokeCommand +// +// PURPOSE: This method is called when a user clicks a menu item to tell +// the handler to run the associated command. The lpcmi parameter +// points to a structure that contains the needed information. +// +IFACEMETHODIMP FileContextMenuExt::InvokeCommand(LPCMINVOKECOMMANDINFO pici) +{ + BOOL fUnicode = FALSE; + + // Determine which structure is being passed in, CMINVOKECOMMANDINFO or + // CMINVOKECOMMANDINFOEX based on the cbSize member of lpcmi. Although + // the lpcmi parameter is declared in Shlobj.h as a CMINVOKECOMMANDINFO + // structure, in practice it often points to a CMINVOKECOMMANDINFOEX + // structure. This struct is an extended version of CMINVOKECOMMANDINFO + // and has additional members that allow Unicode strings to be passed. + if (pici->cbSize == sizeof(CMINVOKECOMMANDINFOEX)) + { + if (pici->fMask & CMIC_MASK_UNICODE) + { + fUnicode = TRUE; + } + } + + // Determines whether the command is identified by its offset or verb. + // There are two ways to identify commands: + // + // 1) The command's verb string + // 2) The command's identifier offset + // + // If the high-order word of lpcmi->lpVerb (for the ANSI case) or + // lpcmi->lpVerbW (for the Unicode case) is nonzero, lpVerb or lpVerbW + // holds a verb string. If the high-order word is zero, the command + // offset is in the low-order word of lpcmi->lpVerb. + + // For the ANSI case, if the high-order word is not zero, the command's + // verb string is in lpcmi->lpVerb. + if (!fUnicode && HIWORD(pici->lpVerb)) + { + // Is the verb supported by this context menu extension? + if (StrCmpIA(pici->lpVerb, m_pszVerb) == 0) + { + OnVerbDisplayFileName(pici->hwnd); + } + else + { + // If the verb is not recognized by the context menu handler, it + // must return E_FAIL to allow it to be passed on to the other + // context menu handlers that might implement that verb. + return E_FAIL; + } + } + + // For the Unicode case, if the high-order word is not zero, the + // command's verb string is in lpcmi->lpVerbW. + else if (fUnicode && HIWORD(((CMINVOKECOMMANDINFOEX*)pici)->lpVerbW)) + { + // Is the verb supported by this context menu extension? + if (StrCmpIW(((CMINVOKECOMMANDINFOEX*)pici)->lpVerbW, m_pwszVerb) == 0) + { + OnVerbDisplayFileName(pici->hwnd); + } + else + { + // If the verb is not recognized by the context menu handler, it + // must return E_FAIL to allow it to be passed on to the other + // context menu handlers that might implement that verb. + return E_FAIL; + } + } + + // If the command cannot be identified through the verb string, then + // check the identifier offset. + else + { + // Is the command identifier offset supported by this context menu + // extension? + if (LOWORD(pici->lpVerb) == IDM_DISPLAY) + { + OnVerbDisplayFileName(pici->hwnd); + } + else + { + // If the verb is not recognized by the context menu handler, it + // must return E_FAIL to allow it to be passed on to the other + // context menu handlers that might implement that verb. + return E_FAIL; + } + } + + return S_OK; +} + + +// +// FUNCTION: CFileContextMenuExt::GetCommandString +// +// PURPOSE: If a user highlights one of the items added by a context menu +// handler, the handler's IContextMenu::GetCommandString method is +// called to request a Help text string that will be displayed on +// the Windows Explorer status bar. This method can also be called +// to request the verb string that is assigned to a command. +// Either ANSI or Unicode verb strings can be requested. This +// example only implements support for the Unicode values of +// uFlags, because only those have been used in Windows Explorer +// since Windows 2000. +// +IFACEMETHODIMP FileContextMenuExt::GetCommandString(UINT_PTR idCommand, + UINT uFlags, UINT *pwReserved, LPSTR pszName, UINT cchMax) +{ + HRESULT hr = E_INVALIDARG; + + if (idCommand == IDM_DISPLAY) + { + switch (uFlags) + { + case GCS_HELPTEXTW: + // Only useful for pre-Vista versions of Windows that have a + // Status bar. + hr = ::StringCchCopyW(reinterpret_cast(pszName), cchMax, + m_pwszVerbHelpText); + break; + + case GCS_VERBW: + // GCS_VERBW is an optional feature that enables a caller to + // discover the canonical name for the verb passed in through + // idCommand. + hr = ::StringCchCopyW(reinterpret_cast(pszName), cchMax, + m_pwszVerbCanonicalName); + break; + + default: + hr = S_OK; + } + } + + // If the command (idCommand) is not supported by this context menu + // extension handler, return E_INVALIDARG. + + return hr; +} + +#pragma endregion diff --git a/src/dll/src/Main.cpp b/src/dll/src/Main.cpp index c8b461e..4b8039f 100644 --- a/src/dll/src/Main.cpp +++ b/src/dll/src/Main.cpp @@ -3,6 +3,7 @@ #include "Library/detours.h" #include "RegistryConfig.h" #include +#include "Include/ClassFactory.h" using namespace Nilesoft::Shell; using namespace Diagnostics; @@ -158,6 +159,7 @@ std::unordered_map< uint32_t, const wchar_t *> msg_map0 = { #define GET_Y_LPARAM(lp) ((int)(short)HIWORD(lp)) // windowsx.h HINSTANCE _hInstance{}; +long g_cDllRef = 0; Initializer _initializer; extern Logger &_log = Logger::Instance(); const Windows::Version *ver = &Windows::Version::Instance(); @@ -1140,6 +1142,19 @@ BOOL APIENTRY DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID) return FALSE; } +HRESULT make_class_factory_query_interface(REFIID riid, LPVOID FAR *ppv) +{ + __trace(L"In make_class_factory_query_interface"); + HRESULT hr = E_OUTOFMEMORY; + ClassFactory *pClassFactory = new ClassFactory(); + if (pClassFactory) + { + hr = pClassFactory->QueryInterface(riid, ppv); + pClassFactory->Release(); + } + return hr; +} + //IID_FolderExtensions //#pragma comment(linker, "/export:DllGetClassObject=DllGetClassObject") _Check_return_ @@ -1176,6 +1191,8 @@ STDAPI DllGetClassObject(_In_ REFCLSID rclsid, [[maybe_unused]] _In_ REFIID riid { _loader.contextmenuhandler = true; Selections::point.GetCursorPos(); + // make class for retrieving selected objects in third-party file explorers + hr = make_class_factory_query_interface(riid, ppv); } if(!_initializer.Status.Loaded) @@ -1201,6 +1218,9 @@ STDAPI DllGetClassObject(_In_ REFCLSID rclsid, [[maybe_unused]] _In_ REFIID riid __control_entrypoint(DllExport) STDAPI DllCanUnloadNow(void) { + if (g_cDllRef > 0) { + return S_FALSE; + } if(!_loader.explorer || !is_registered()) return S_OK; return S_FALSE; From 8eb36fb8942e74e5fb7d7b82e1a3bea76dc5e339 Mon Sep 17 00:00:00 2001 From: geezmolycos Date: Sun, 15 Feb 2026 16:05:37 +0800 Subject: [PATCH 3/7] implement ShellExtSelectionRetriever to retrieve info of background and selected items --- src/dll/Shell.vcxproj | 4 +- src/dll/src/ClassFactory.cpp | 4 +- src/dll/src/FileContextMenuExt.cpp | 398 ------------------ src/dll/src/Include/ClassFactory.h | 42 ++ .../src/Include/ShellExtSelectionRetriever.h | 43 ++ src/dll/src/Selections.cpp | 23 +- src/dll/src/ShellExtSelectionRetriever.cpp | 217 ++++++++++ 7 files changed, 326 insertions(+), 405 deletions(-) delete mode 100644 src/dll/src/FileContextMenuExt.cpp create mode 100644 src/dll/src/Include/ClassFactory.h create mode 100644 src/dll/src/Include/ShellExtSelectionRetriever.h create mode 100644 src/dll/src/ShellExtSelectionRetriever.cpp diff --git a/src/dll/Shell.vcxproj b/src/dll/Shell.vcxproj index 7f8e447..fce39bb 100644 --- a/src/dll/Shell.vcxproj +++ b/src/dll/Shell.vcxproj @@ -169,7 +169,7 @@ - + @@ -188,7 +188,7 @@ - + diff --git a/src/dll/src/ClassFactory.cpp b/src/dll/src/ClassFactory.cpp index 96af57b..c01a34c 100644 --- a/src/dll/src/ClassFactory.cpp +++ b/src/dll/src/ClassFactory.cpp @@ -16,7 +16,7 @@ WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. #include #include "Include/ClassFactory.h" -#include "Include/FileContextMenuExt.h" +#include "Include/ShellExtSelectionRetriever.h" #include #include #pragma comment(lib, "shlwapi.lib") @@ -83,7 +83,7 @@ IFACEMETHODIMP ClassFactory::CreateInstance(IUnknown *pUnkOuter, REFIID riid, vo hr = E_OUTOFMEMORY; // Create the COM component. - FileContextMenuExt *pExt = new (std::nothrow) FileContextMenuExt(); + ShellExtSelectionRetriever *pExt = new (std::nothrow) ShellExtSelectionRetriever(); if (pExt) { // Query the specified interface. diff --git a/src/dll/src/FileContextMenuExt.cpp b/src/dll/src/FileContextMenuExt.cpp deleted file mode 100644 index 1145646..0000000 --- a/src/dll/src/FileContextMenuExt.cpp +++ /dev/null @@ -1,398 +0,0 @@ -/****************************** Module Header ******************************\ -Module Name: FileContextMenuExt.cpp -Project: CppShellExtContextMenuHandler -Copyright (c) Microsoft Corporation. - -The code sample demonstrates creating a Shell context menu handler with C++. - -A context menu handler is a shell extension handler that adds commands to an -existing context menu. Context menu handlers are associated with a particular -file class and are called any time a context menu is displayed for a member -of the class. While you can add items to a file class context menu with the -registry, the items will be the same for all members of the class. By -implementing and registering such a handler, you can dynamically add items to -an object's context menu, customized for the particular object. - -The example context menu handler adds the menu item "Display File Name (C++)" -to the context menu when you right-click a .cpp file in the Windows Explorer. -Clicking the menu item brings up a message box that displays the full path -of the .cpp file. - -This source is subject to the Microsoft Public License. -See http://www.microsoft.com/opensource/licenses.mspx#Ms-PL. -All other rights reserved. - -THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, -EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. -\***************************************************************************/ - -#include -#include "Include/FileContextMenuExt.h" -#include -#include -#pragma comment(lib, "shlwapi.lib") - -extern Logger &_log; - -extern HINSTANCE g_hInst; -extern long g_cDllRef; - -#define IDM_DISPLAY 0 // The command's identifier offset - -wchar_t menutext[] = L"Menu text"; - -FileContextMenuExt::FileContextMenuExt(void) - : m_cRef(1), - m_pszMenuText(menutext), - m_pszVerb("verb"), - m_pwszVerb(L"verb"), - m_pszVerbCanonicalName("canocial name"), - m_pwszVerbCanonicalName(L"canocial name"), - m_pszVerbHelpText("help text"), - m_pwszVerbHelpText(L"help text") -{ - InterlockedIncrement(&g_cDllRef); - - // Load the bitmap for the menu item. - // If you want the menu item bitmap to be transparent, the color depth of - // the bitmap must not be greater than 8bpp. - // m_hMenuBmp = LoadImage(g_hInst, MAKEINTRESOURCE(IDB_OK), - // IMAGE_BITMAP, 0, 0, LR_DEFAULTSIZE | LR_LOADTRANSPARENT); -} - -FileContextMenuExt::~FileContextMenuExt(void) -{ - if (m_hMenuBmp) - { - DeleteObject(m_hMenuBmp); - m_hMenuBmp = NULL; - } - - InterlockedDecrement(&g_cDllRef); -} - - -void FileContextMenuExt::OnVerbDisplayFileName(HWND hWnd) -{ - for (size_t i = 0; i < m_vSelectedFiles.size() && i < 2; i++) - { - wchar_t szMessage[300]; - if (SUCCEEDED(StringCchPrintf(szMessage, ARRAYSIZE(szMessage), - L"The selected file is:\r\n\r\n%s", m_vSelectedFiles[i].c_str()))) - { - ::MessageBoxW(hWnd, szMessage, L"friendly name", MB_OK); - } - } -} - - -#pragma region IUnknown - -// Query to the interface the component supported. -IFACEMETHODIMP FileContextMenuExt::QueryInterface(REFIID riid, void **ppv) -{ - __trace(L"In FileContextMenuExt::QueryInterface"); - static const QITAB qit[] = - { - QITABENT(FileContextMenuExt, IContextMenu), - QITABENT(FileContextMenuExt, IShellExtInit), - { 0 }, - }; - return QISearch(this, qit, riid, ppv); -} - -// Increase the reference count for an interface on an object. -IFACEMETHODIMP_(ULONG) FileContextMenuExt::AddRef() -{ - return InterlockedIncrement(&m_cRef); -} - -// Decrease the reference count for an interface on an object. -IFACEMETHODIMP_(ULONG) FileContextMenuExt::Release() -{ - ULONG cRef = InterlockedDecrement(&m_cRef); - if (0 == cRef) - { - delete this; - } - - return cRef; -} - -#pragma endregion - - -#pragma region IShellExtInit - -// Initialize the context menu handler. -IFACEMETHODIMP FileContextMenuExt::Initialize( - LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hKeyProgID) -{ - __trace(L"In FileContextMenuExt::Initialize"); - if (NULL == pDataObj) - { - return E_INVALIDARG; - } - - HRESULT hr = E_FAIL; - - FORMATETC fe = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; - STGMEDIUM stm; - - // The pDataObj pointer contains the objects being acted upon. In this - // example, we get an HDROP handle for enumerating the selected files and - // folders. - if (SUCCEEDED(pDataObj->GetData(&fe, &stm))) - { - // Get an HDROP handle. - HDROP hDrop = static_cast(GlobalLock(stm.hGlobal)); - if (hDrop != NULL) - { - // Determine how many files are involved in this operation. This - // code sample displays the custom context menu item when only - // one file is selected. - UINT nFiles = DragQueryFile(hDrop, 0xFFFFFFFF, NULL, 0); - __trace(L"In FileContextMenuExt::Initialize: %d files selected", nFiles); - for (size_t i = 0; i < nFiles; i++) - { - wchar_t szSelectedFile[MAX_PATH] = { 0 }; - // Get the path of the file. - if (0 != ::DragQueryFileW(hDrop, i, szSelectedFile, static_cast(ARRAYSIZE(szSelectedFile)))) - { - m_vSelectedFiles.push_back(szSelectedFile); - __trace(L"In FileContextMenuExt::Initialize: %ls", szSelectedFile); - hr = S_OK; - continue; - } - hr = E_FAIL; - break; - } - - GlobalUnlock(stm.hGlobal); - } - - ReleaseStgMedium(&stm); - } - - if (S_OK == hr) - { - for (auto file = m_vSelectedFiles.cbegin(); file != m_vSelectedFiles.cend(); ++file) - { - const wchar_t *dot = wcsrchr(file->c_str(), L'.'); - __trace(L"In FileContextMenuExt::Initialize: ext: %ls", dot); - if (dot && 0 != _wcsicmp(dot, L".txt")) - { - hr = E_INVALIDARG; - break; - } - } - } - - __trace(L"Out FileContextMenuExt::Initialize: hr=%08X", hr); - - // If any value other than S_OK is returned from the method, the context - // menu item is not displayed. - return hr; -} - -#pragma endregion - - -#pragma region IContextMenu - -// -// FUNCTION: FileContextMenuExt::QueryContextMenu -// -// PURPOSE: The Shell calls IContextMenu::QueryContextMenu to allow the -// context menu handler to add its menu items to the menu. It -// passes in the HMENU handle in the hmenu parameter. The -// indexMenu parameter is set to the index to be used for the -// first menu item that is to be added. -// -IFACEMETHODIMP FileContextMenuExt::QueryContextMenu( - HMENU hMenu, UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags) -{ - __trace(L"In FileContextMenuExt::QueryContextMenu"); - // If uFlags include CMF_DEFAULTONLY then we should not do anything. - if (CMF_DEFAULTONLY & uFlags) - { - return MAKE_HRESULT(SEVERITY_SUCCESS, 0, USHORT(0)); - } - - // Use either InsertMenu or InsertMenuItem to add menu items. - // Learn how to add sub-menu from: - // http://www.codeproject.com/KB/shell/ctxextsubmenu.aspx - - MENUITEMINFOW mii = { sizeof(mii) }; - mii.fMask = MIIM_BITMAP | MIIM_STRING | MIIM_FTYPE | MIIM_ID | MIIM_STATE; - mii.wID = idCmdFirst + IDM_DISPLAY; - mii.fType = MFT_STRING; - mii.dwTypeData = m_pszMenuText; - mii.fState = MFS_ENABLED; - mii.hbmpItem = static_cast(m_hMenuBmp); - if (!::InsertMenuItemW(hMenu, indexMenu, TRUE, &mii)) - { - return HRESULT_FROM_WIN32(GetLastError()); - } - - // Add a separator. - MENUITEMINFOW sep = { sizeof(sep) }; - sep.fMask = MIIM_TYPE; - sep.fType = MFT_SEPARATOR; - if (!::InsertMenuItemW(hMenu, indexMenu + 1, TRUE, &sep)) - { - return HRESULT_FROM_WIN32(GetLastError()); - } - - // Return an HRESULT value with the severity set to SEVERITY_SUCCESS. - // Set the code value to the offset of the largest command identifier - // that was assigned, plus one (1). - return MAKE_HRESULT(SEVERITY_SUCCESS, 0, USHORT(IDM_DISPLAY + 1)); -} - - -// -// FUNCTION: FileContextMenuExt::InvokeCommand -// -// PURPOSE: This method is called when a user clicks a menu item to tell -// the handler to run the associated command. The lpcmi parameter -// points to a structure that contains the needed information. -// -IFACEMETHODIMP FileContextMenuExt::InvokeCommand(LPCMINVOKECOMMANDINFO pici) -{ - BOOL fUnicode = FALSE; - - // Determine which structure is being passed in, CMINVOKECOMMANDINFO or - // CMINVOKECOMMANDINFOEX based on the cbSize member of lpcmi. Although - // the lpcmi parameter is declared in Shlobj.h as a CMINVOKECOMMANDINFO - // structure, in practice it often points to a CMINVOKECOMMANDINFOEX - // structure. This struct is an extended version of CMINVOKECOMMANDINFO - // and has additional members that allow Unicode strings to be passed. - if (pici->cbSize == sizeof(CMINVOKECOMMANDINFOEX)) - { - if (pici->fMask & CMIC_MASK_UNICODE) - { - fUnicode = TRUE; - } - } - - // Determines whether the command is identified by its offset or verb. - // There are two ways to identify commands: - // - // 1) The command's verb string - // 2) The command's identifier offset - // - // If the high-order word of lpcmi->lpVerb (for the ANSI case) or - // lpcmi->lpVerbW (for the Unicode case) is nonzero, lpVerb or lpVerbW - // holds a verb string. If the high-order word is zero, the command - // offset is in the low-order word of lpcmi->lpVerb. - - // For the ANSI case, if the high-order word is not zero, the command's - // verb string is in lpcmi->lpVerb. - if (!fUnicode && HIWORD(pici->lpVerb)) - { - // Is the verb supported by this context menu extension? - if (StrCmpIA(pici->lpVerb, m_pszVerb) == 0) - { - OnVerbDisplayFileName(pici->hwnd); - } - else - { - // If the verb is not recognized by the context menu handler, it - // must return E_FAIL to allow it to be passed on to the other - // context menu handlers that might implement that verb. - return E_FAIL; - } - } - - // For the Unicode case, if the high-order word is not zero, the - // command's verb string is in lpcmi->lpVerbW. - else if (fUnicode && HIWORD(((CMINVOKECOMMANDINFOEX*)pici)->lpVerbW)) - { - // Is the verb supported by this context menu extension? - if (StrCmpIW(((CMINVOKECOMMANDINFOEX*)pici)->lpVerbW, m_pwszVerb) == 0) - { - OnVerbDisplayFileName(pici->hwnd); - } - else - { - // If the verb is not recognized by the context menu handler, it - // must return E_FAIL to allow it to be passed on to the other - // context menu handlers that might implement that verb. - return E_FAIL; - } - } - - // If the command cannot be identified through the verb string, then - // check the identifier offset. - else - { - // Is the command identifier offset supported by this context menu - // extension? - if (LOWORD(pici->lpVerb) == IDM_DISPLAY) - { - OnVerbDisplayFileName(pici->hwnd); - } - else - { - // If the verb is not recognized by the context menu handler, it - // must return E_FAIL to allow it to be passed on to the other - // context menu handlers that might implement that verb. - return E_FAIL; - } - } - - return S_OK; -} - - -// -// FUNCTION: CFileContextMenuExt::GetCommandString -// -// PURPOSE: If a user highlights one of the items added by a context menu -// handler, the handler's IContextMenu::GetCommandString method is -// called to request a Help text string that will be displayed on -// the Windows Explorer status bar. This method can also be called -// to request the verb string that is assigned to a command. -// Either ANSI or Unicode verb strings can be requested. This -// example only implements support for the Unicode values of -// uFlags, because only those have been used in Windows Explorer -// since Windows 2000. -// -IFACEMETHODIMP FileContextMenuExt::GetCommandString(UINT_PTR idCommand, - UINT uFlags, UINT *pwReserved, LPSTR pszName, UINT cchMax) -{ - HRESULT hr = E_INVALIDARG; - - if (idCommand == IDM_DISPLAY) - { - switch (uFlags) - { - case GCS_HELPTEXTW: - // Only useful for pre-Vista versions of Windows that have a - // Status bar. - hr = ::StringCchCopyW(reinterpret_cast(pszName), cchMax, - m_pwszVerbHelpText); - break; - - case GCS_VERBW: - // GCS_VERBW is an optional feature that enables a caller to - // discover the canonical name for the verb passed in through - // idCommand. - hr = ::StringCchCopyW(reinterpret_cast(pszName), cchMax, - m_pwszVerbCanonicalName); - break; - - default: - hr = S_OK; - } - } - - // If the command (idCommand) is not supported by this context menu - // extension handler, return E_INVALIDARG. - - return hr; -} - -#pragma endregion diff --git a/src/dll/src/Include/ClassFactory.h b/src/dll/src/Include/ClassFactory.h new file mode 100644 index 0000000..0f5ee54 --- /dev/null +++ b/src/dll/src/Include/ClassFactory.h @@ -0,0 +1,42 @@ +/****************************** Module Header ******************************\ +Module Name: ClassFactory.h +Project: CppShellExtContextMenuHandler +Copyright (c) Microsoft Corporation. + +The file declares the class factory for the FileContextMenuExt COM class. + +This source is subject to the Microsoft Public License. +See http://www.microsoft.com/opensource/licenses.mspx#Ms-PL. +All other rights reserved. + +THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, +EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. +\***************************************************************************/ + +#pragma once + +#include // For IClassFactory +#include + + +class ClassFactory : public IClassFactory +{ +public: + // IUnknown + IFACEMETHODIMP QueryInterface(REFIID riid, void **ppv); + IFACEMETHODIMP_(ULONG) AddRef(); + IFACEMETHODIMP_(ULONG) Release(); + + // IClassFactory + IFACEMETHODIMP CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppv); + IFACEMETHODIMP LockServer(BOOL fLock); + + ClassFactory(); + +protected: + ~ClassFactory(); + +private: + long m_cRef; +}; \ No newline at end of file diff --git a/src/dll/src/Include/ShellExtSelectionRetriever.h b/src/dll/src/Include/ShellExtSelectionRetriever.h new file mode 100644 index 0000000..3df0305 --- /dev/null +++ b/src/dll/src/Include/ShellExtSelectionRetriever.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include + +using Microsoft::WRL::ComPtr; + +// Global structure to share data with existing Parse logic +struct GlobalSelectionContext { + bool IsBackground = false; + ComPtr ParentFolder; + ComPtr SelectionArray; +}; + +extern GlobalSelectionContext g_ShellContext; + +class ShellExtSelectionRetriever : public IShellExtInit, public IContextMenu +{ +public: + // IUnknown + IFACEMETHODIMP QueryInterface(REFIID riid, void **ppv); + IFACEMETHODIMP_(ULONG) AddRef(); + IFACEMETHODIMP_(ULONG) Release(); + + // IShellExtInit + IFACEMETHODIMP Initialize(LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hKeyProgID); + + // IContextMenu + IFACEMETHODIMP QueryContextMenu(HMENU hMenu, UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags); + IFACEMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO pici); + IFACEMETHODIMP GetCommandString(UINT_PTR idCommand, UINT uFlags, UINT *pwReserved, LPSTR pszName, UINT cchMax); + + ShellExtSelectionRetriever(void); + +protected: + ~ShellExtSelectionRetriever(void); + +private: + // Reference count of component. + long m_cRef; +}; diff --git a/src/dll/src/Selections.cpp b/src/dll/src/Selections.cpp index 7551fe0..81bca3f 100644 --- a/src/dll/src/Selections.cpp +++ b/src/dll/src/Selections.cpp @@ -608,6 +608,7 @@ namespace Nilesoft bool Selections::Parse(FileProperties *prop) { + __trace(L"Parse file %ls inserting to Types", prop->Path.c_str()); std::unique_ptr pathItem; try { @@ -1023,13 +1024,15 @@ namespace Nilesoft // ShellBrowser->AddRef(); - // Logger::Info(L"%x %s %x", current_window, Window::class_name(current_window).c_str(), sb); + Logger::Info(L"In QuerySelected: current_window=%x class_name=%s", current_window, Window::class_name(current_window).c_str()); if(ShellBrowser == nullptr) return false; + if(Window.id == WINDOW_EXPLORER) { + __trace(L"In QuerySelected: Window is explorer"); IComPtr sv; if(S_OK != ShellBrowser->QueryActiveShellView(sv)) @@ -1049,18 +1052,22 @@ namespace Nilesoft FileProperties folderProp; Selections::GetFileProperties(si, &folderProp); + __trace(L"In QuerySelected: get folder properties: path=%ls, rawpath=%ls", folderProp.Path.c_str(), folderProp.PathRaw.c_str()); + IComPtr sia; - if(S_OK == fv->GetSelection(FALSE, sia)) + if(S_OK == fv->GetSelection(FALSE, sia)) // Selected are foreground objects { DWORD sel_count = 0; if(S_OK != sia->GetCount(&sel_count)) return false; + __trace(L"In QuerySelected: get selection count = %d", sel_count); Items.reserve(sel_count); for(DWORD i = 0; i < sel_count; i++) { + __trace(L"Parse selection item #%d", i); IComPtr item; if(S_OK == sia->GetItemAt(i, item) && item) Parse(item); @@ -1072,8 +1079,9 @@ namespace Nilesoft return !Items.empty(); } - if(folderProp.Folder) + if(folderProp.Folder) // has background folder { + __trace(L"In QuerySelected: folder is Folder type"); if(folderProp.FileSystem || folderProp.FileSysAnceStor) folderProp.Background = TRUE; else @@ -1084,19 +1092,23 @@ namespace Nilesoft auto h = folderProp.PathRaw.hash(); if(h == GUID_HOME or h == GUID_QUICK_ACCESS or h == GUID_LIBRARIES) { + Window.id = (h == GUID_HOME) ? WINDOW_HOME : (h == GUID_QUICK_ACCESS) ? WINDOW_QUICK_ACCESS : WINDOW_LIBRARIES; folderProp.Background = TRUE; } } + __trace(L"In QuerySelected: folder is background? %d", folderProp.Background); } if(Window.desktop) { + __trace(L"In QuerySelected: Window is desktop"); Parent = Path::Parent(folderProp.Path).move(); ParentRaw = Path::Parent(folderProp.PathRaw).move(); } else { + __trace(L"In QuerySelected: Window is NOT desktop"); IComPtr sip; if(S_OK == si->GetParent(sip)) { @@ -1109,9 +1121,11 @@ namespace Nilesoft } } } + __trace(L"In QuerySelected: Folder Parent is %ls", Parent.c_str()); if(folderProp.Background) { + __trace(L"In QuerySelected: Selected is background"); this->Background = true; this->Parse(&folderProp); //return true; @@ -1120,6 +1134,7 @@ namespace Nilesoft } else if(Window.explorer_tree) { + __trace(L"In QuerySelected: Window is explorer tree"); IComPtr nstc; IComPtr sp; @@ -1159,10 +1174,12 @@ namespace Nilesoft }*/ } Window.id = WINDOW_UI; + __trace(L"In QuerySelected: Tree part is UI"); } if(si) { + __trace(L"In QuerySelected: Tree part is something"); auto ret = Parse(si); IComPtr sip; if(S_OK == si->GetParent(sip)) diff --git a/src/dll/src/ShellExtSelectionRetriever.cpp b/src/dll/src/ShellExtSelectionRetriever.cpp new file mode 100644 index 0000000..17cbd5a --- /dev/null +++ b/src/dll/src/ShellExtSelectionRetriever.cpp @@ -0,0 +1,217 @@ +#include +#include "Include/ShellExtSelectionRetriever.h" +#include +#include +#pragma comment(lib, "shlwapi.lib") +#include // 必须包含现代 Shell 接口头文件 +#include +#include // 推荐使用 Microsoft::WRL::ComPtr 管理引用计数 +#include + +using Microsoft::WRL::ComPtr; + +extern Logger &_log; + +extern HINSTANCE g_hInst; +extern long g_cDllRef; + +GlobalSelectionContext g_ShellContext; + +wchar_t menutext[] = L"Menu text"; + +HRESULT GetThisPCItem(ComPtr& spItem) { + return SHCreateItemFromParsingName(L"::{20D04FE0-3AEA-1069-A2D8-08002B30309D}", + NULL, IID_PPV_ARGS(&spItem)); +} + +ShellExtSelectionRetriever::ShellExtSelectionRetriever(void) + : m_cRef(1){ + InterlockedIncrement(&g_cDllRef); +} + +ShellExtSelectionRetriever::~ShellExtSelectionRetriever(void) +{ + + InterlockedDecrement(&g_cDllRef); +} + +#pragma region IUnknown + +// Query to the interface the component supported. +IFACEMETHODIMP ShellExtSelectionRetriever::QueryInterface(REFIID riid, void **ppv) +{ + __trace(L"In ShellExtSelectionRetriever::QueryInterface"); + static const QITAB qit[] = + { + QITABENT(ShellExtSelectionRetriever, IContextMenu), + QITABENT(ShellExtSelectionRetriever, IShellExtInit), + { 0 }, + }; + return QISearch(this, qit, riid, ppv); +} + +// Increase the reference count for an interface on an object. +IFACEMETHODIMP_(ULONG) ShellExtSelectionRetriever::AddRef() +{ + return InterlockedIncrement(&m_cRef); +} + +// Decrease the reference count for an interface on an object. +IFACEMETHODIMP_(ULONG) ShellExtSelectionRetriever::Release() +{ + ULONG cRef = InterlockedDecrement(&m_cRef); + if (0 == cRef) + { + delete this; + } + + return cRef; +} + +#pragma endregion + +#pragma region IShellExtInit +IFACEMETHODIMP ShellExtSelectionRetriever::Initialize(LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, [[maybe_unused]] HKEY hKeyProgID) +{ + // Reset global context for the current operation + g_ShellContext = GlobalSelectionContext(); + + // 1. Check for Background Context (Right-click on folder background) + if (pDataObj == NULL && pidlFolder != NULL) + { + g_ShellContext.IsBackground = true; + __trace(L"[ShellExt] Context: BACKGROUND"); + + ComPtr spItem; + if (SUCCEEDED(SHCreateItemFromIDList(pidlFolder, IID_PPV_ARGS(&spItem)))) { + spItem.As(&g_ShellContext.ParentFolder); + + // Log background folder path + PWSTR pszPath = nullptr; + if (SUCCEEDED(g_ShellContext.ParentFolder->GetDisplayName(SIGDN_FILESYSPATH, &pszPath))) { + __trace(L"[ShellExt] Background Folder Path: %ls", pszPath); + CoTaskMemFree(pszPath); + } + } + } + // 2. Check for Selection Context (Right-click on one or more files/folders) + else if (pDataObj != NULL) + { + g_ShellContext.IsBackground = false; + __trace(L"[ShellExt] Context: SELECTION"); + + if (SUCCEEDED(SHCreateShellItemArrayFromDataObject(pDataObj, IID_PPV_ARGS(&g_ShellContext.SelectionArray)))) + { + DWORD dwCount = 0; + g_ShellContext.SelectionArray->GetCount(&dwCount); + __trace(L"[ShellExt] Total Selected Items: %u", dwCount); + + if (dwCount > 0) { + ComPtr spFirstItem; + ComPtr spFirstParent; + bool allSameParent = true; + + // Loop through all selected items for logging and parent verification + for (DWORD i = 0; i < dwCount; ++i) { + ComPtr spCurrentItem; + if (SUCCEEDED(g_ShellContext.SelectionArray->GetItemAt(i, &spCurrentItem))) { + // Log individual item path + PWSTR pszItemPath = nullptr; + // Use SIGDN_FILESYSPATH for local paths, fallback to SIGDN_NORMALDISPLAY for virtual items + if (SUCCEEDED(spCurrentItem->GetDisplayName(SIGDN_FILESYSPATH, &pszItemPath))) { + __trace(L"[ShellExt] Item[%u]: %ls", i, pszItemPath); + CoTaskMemFree(pszItemPath); + } + + // Parent verification logic + if (i == 0) { + spCurrentItem->GetParent(&spFirstParent); + // Log individual item path + PWSTR pszItemPath = nullptr; + // Use SIGDN_FILESYSPATH for local paths, fallback to SIGDN_NORMALDISPLAY for virtual items + if (SUCCEEDED(spFirstParent->GetDisplayName(SIGDN_NORMALDISPLAY, &pszItemPath))) { + __trace(L"[ShellExt] Parent: %ls", pszItemPath); + CoTaskMemFree(pszItemPath); + } + } else if (allSameParent) { + ComPtr spCurrentParent; + int compareResult = 0; + if (FAILED(spCurrentItem->GetParent(&spCurrentParent)) || + FAILED(spFirstParent->Compare(spCurrentParent.Get(), SICHINT_CANONICAL, &compareResult)) || + compareResult != 0) + { + allSameParent = false; + } + } + } + } + + // Assign the final ParentFolder based on consistency + if (allSameParent && spFirstParent) { + spFirstParent.As(&g_ShellContext.ParentFolder); + __trace(L"[ShellExt] Parent Folder is consistent."); + } else { + __trace(L"[ShellExt] Mixed parents or virtual folder; setting Parent to 'This PC'"); + GetThisPCItem(g_ShellContext.ParentFolder); + } + } + } + } + + return S_OK; +} +#pragma endregion + +#pragma region IContextMenu +IFACEMETHODIMP ShellExtSelectionRetriever::QueryContextMenu(HMENU hmenu, UINT indexMenu, UINT idCmdFirst, [[maybe_unused]] UINT idCmdLast, UINT uFlags) +{ + // If CMF_DEFAULTONLY is set, we should not add any items + if (uFlags & CMF_DEFAULTONLY) return MAKE_HRESULT(SEVERITY_SUCCESS, 0, 0); + + // Insert the placeholder menu item + // idCmdFirst is the starting ID for your commands + InsertMenu(hmenu, indexMenu, MF_BYPOSITION | MF_STRING, idCmdFirst, L"ShellExtSelectionRetriever Placeholder"); + + // Return the number of items added (1) + return MAKE_HRESULT(SEVERITY_SUCCESS, 0, 1); +} + + +// +// FUNCTION: ShellExtSelectionRetriever::InvokeCommand +// +// PURPOSE: This method is called when a user clicks a menu item to tell +// the handler to run the associated command. The lpcmi parameter +// points to a structure that contains the needed information. +// +IFACEMETHODIMP ShellExtSelectionRetriever::InvokeCommand(LPCMINVOKECOMMANDINFO lpici) +{ + if (LOWORD(lpici->lpVerb) == 0) + { + __trace(L"[ShellExt] Placeholder clicked."); + return S_OK; + } + return E_FAIL; +} + + +// +// FUNCTION: CShellExtSelectionRetriever::GetCommandString +// +// PURPOSE: If a user highlights one of the items added by a context menu +// handler, the handler's IContextMenu::GetCommandString method is +// called to request a Help text string that will be displayed on +// the Windows Explorer status bar. This method can also be called +// to request the verb string that is assigned to a command. +// Either ANSI or Unicode verb strings can be requested. This +// example only implements support for the Unicode values of +// uFlags, because only those have been used in Windows Explorer +// since Windows 2000. +// +IFACEMETHODIMP ShellExtSelectionRetriever::GetCommandString([[maybe_unused]] UINT_PTR idCommand, + [[maybe_unused]] UINT uFlags, [[maybe_unused]] UINT *pwReserved, [[maybe_unused]] LPSTR pszName, [[maybe_unused]] UINT cchMax) +{ + return E_NOTIMPL; +} + +#pragma endregion From dfeaa0081939d608d944137b5715f98aefdbe82e Mon Sep 17 00:00:00 2001 From: geezmolycos Date: Sun, 15 Feb 2026 18:13:55 +0800 Subject: [PATCH 4/7] successfully tested under xyplorer, selection retriever working in third-party file managers --- src/dll/src/ContextMenu.cpp | 58 +++++++++-- src/dll/src/Include/ContextMenu.h | 3 +- src/dll/src/Include/Selections.h | 12 ++- .../src/Include/ShellExtSelectionRetriever.h | 4 +- src/dll/src/Selections.cpp | 98 ++++++++++++++++--- src/dll/src/ShellExtSelectionRetriever.cpp | 16 ++- 6 files changed, 148 insertions(+), 43 deletions(-) diff --git a/src/dll/src/ContextMenu.cpp b/src/dll/src/ContextMenu.cpp index 0ba75ec..f675113 100644 --- a/src/dll/src/ContextMenu.cpp +++ b/src/dll/src/ContextMenu.cpp @@ -2,6 +2,7 @@ #include "Include/Theme.h" #include "Include/ContextMenu.h" #include "Include/stb_image_write.h" +#include "Include/ShellExtSelectionRetriever.h" using namespace Nilesoft::Diagnostics; #include @@ -133,7 +134,7 @@ struct DWM inline HMENU GET_HMENU(HWND hWnd) { return SendMSG(hWnd, MN_GETHMENU, 0, 0); } -auto ver = &Windows::Version::Instance(); +auto ver = &Nilesoft::Windows::Version::Instance(); #pragma endregion @@ -4218,9 +4219,12 @@ namespace Nilesoft } } - void ContextMenu::build_system_menuitems(HMENU hMenu, menuitem_t *menu, bool is_root) + void ContextMenu::build_system_menuitems(HMENU hMenu, menuitem_t *menu, bool is_root, bool skip_init) { - ::SendMessageW(hwnd.owner, WM_INITMENUPOPUP, reinterpret_cast(hMenu), 0xFFFFFFFF); + if (!skip_init) + { + ::SendMessageW(hwnd.owner, WM_INITMENUPOPUP, reinterpret_cast(hMenu), 0xFFFFFFFF); + } auto itmes_count = ::GetMenuItemCount(hMenu); @@ -4347,6 +4351,30 @@ namespace Nilesoft } } + bool ContextMenu::check_shell_ext_selection_retriever_placeholder(HMENU hMenu) + { + auto itmes_count = ::GetMenuItemCount(hMenu); + + for(int i = 0; i < itmes_count; i++) + { + string title; + MENUITEMINFOW mii{ sizeof(MENUITEMINFOW) }; + mii.fMask = MenuItemInfo::FMASK; + mii.dwTypeData = title.buffer((1024)); + mii.cch = 1024; + if(::GetMenuItemInfoW(hMenu, i, true, &mii)) + { + title.release(mii.cch); + // __trace(L"In check_shell_ext_selection_retriever_placeholder: %ls", title.c_str()); + if (title.equals(shell_ext_selection_retriever_placeholder, false)) + { + return true; + } + } + } + return false; + } + bool ContextMenu::Initialize() { try @@ -4405,6 +4433,15 @@ namespace Nilesoft _context.variables.runtime = &_cache->variables.runtime; _context.variables.local = nullptr; + auto prop = ::GetPropW(hwnd.owner, UxSubclass); + + __trace(L"UxSubclass Prop: %d, modify items enabled: %d", prop, _settings.modify_items.enabled); + + // initialize popup menu to check whether to use ShellExtSelectionRetriever + if(0 == prop) { + ::SendMessageW(hwnd.owner, WM_INITMENUPOPUP, reinterpret_cast(_hMenu_original), 0xFFFFFFFF); + } + switch(Selected.Window.id) { case WINDOW_TASKBAR: @@ -4413,9 +4450,14 @@ namespace Nilesoft Selected.Directory = Path::GetKnownFolder(FOLDERID_Desktop).move(); break; default: - if(!Selected.QuerySelected()) + bool use_shell_ext_selection_retriever = check_shell_ext_selection_retriever_placeholder(_hMenu_original); + if (use_shell_ext_selection_retriever) + { + __trace(L"Menu has ShellExtSelectionRetriever placeholder"); + } + if(!Selected.QuerySelected(use_shell_ext_selection_retriever)) { - __trace(L"QuerySelected"); + __trace(L"QuerySelected"); //return false; } //::{2cc5ca98-6485-489a-920e-b3e88a6ccce3} @@ -4482,12 +4524,8 @@ namespace Nilesoft __system_menu_tree->type = 10; __map_system_menu[0] = __system_menu_tree; - auto prop = ::GetPropW(hwnd.owner, UxSubclass); - - __trace(L"UxSubclass Prop: %d, modify items enabled: %d", prop, _settings.modify_items.enabled); - if(0 == prop) - build_system_menuitems(_hMenu_original, __system_menu_tree, true); + build_system_menuitems(_hMenu_original, __system_menu_tree, true, true); if(_settings.modify_items.enabled) build_main_system_menuitems(__system_menu_tree, true); diff --git a/src/dll/src/Include/ContextMenu.h b/src/dll/src/Include/ContextMenu.h index 3a3743b..0227985 100644 --- a/src/dll/src/Include/ContextMenu.h +++ b/src/dll/src/Include/ContextMenu.h @@ -733,8 +733,9 @@ plutovg_move_to(pluto, start.x, start.y); bool Initialize(); int Uninitialize(); int InvokeCommand(int id); - void build_system_menuitems(HMENU hMenu, menuitem_t *menu, bool is_root = false); + void build_system_menuitems(HMENU hMenu, menuitem_t *menu, bool is_root = false, bool skip_init = false); void build_main_system_menuitems(menuitem_t *menu, bool is_root = false); + bool check_shell_ext_selection_retriever_placeholder(HMENU hMenu); void backup_native_items(HMENU hMenu, uint32_t id, bool check = false); diff --git a/src/dll/src/Include/Selections.h b/src/dll/src/Include/Selections.h index db93784..b2b2f32 100644 --- a/src/dll/src/Include/Selections.h +++ b/src/dll/src/Include/Selections.h @@ -16,10 +16,10 @@ namespace Nilesoft //::{031E4825-7B94-4DC3-B131-E946B44C8DD5}\CameraRoll.library-ms /* - wnd cls=ShellTabWindowClass title=title* - wnd aaname=Explorer Pane cls=DirectUIHWND - wnd aaname=Shell Folder View cls=SHELLDLL_DefView title=ShellView - wnd aaname=Items View cls=DirectUIHWND + wnd cls=�ShellTabWindowClass� title=�title*� + wnd aaname=�Explorer Pane� cls=�DirectUIHWND� + wnd aaname=�Shell Folder View� cls=�SHELLDLL_DefView� title=�ShellView� + wnd aaname=�Items View� cls=�DirectUIHWND� */ //Windows Explorer on XP, Vista, 7, 8 @@ -320,7 +320,9 @@ namespace Nilesoft void QuerySelectionMode(); bool QueryShellWindow(); - bool QuerySelected(); + bool QuerySelectedWithShellBrowser(); + bool QuerySelectedWithShellExtSelectionRetriever(); + bool QuerySelected(bool use_shell_ext_selection_retriever); bool Preparing(); bool Parse(IShellItem *shellItem); bool Parse(FileProperties *prop); diff --git a/src/dll/src/Include/ShellExtSelectionRetriever.h b/src/dll/src/Include/ShellExtSelectionRetriever.h index 3df0305..87dd5d0 100644 --- a/src/dll/src/Include/ShellExtSelectionRetriever.h +++ b/src/dll/src/Include/ShellExtSelectionRetriever.h @@ -1,9 +1,6 @@ #pragma once -#include -#include #include -#include using Microsoft::WRL::ComPtr; @@ -15,6 +12,7 @@ struct GlobalSelectionContext { }; extern GlobalSelectionContext g_ShellContext; +extern const wchar_t shell_ext_selection_retriever_placeholder[]; class ShellExtSelectionRetriever : public IShellExtInit, public IContextMenu { diff --git a/src/dll/src/Selections.cpp b/src/dll/src/Selections.cpp index 81bca3f..7ba292e 100644 --- a/src/dll/src/Selections.cpp +++ b/src/dll/src/Selections.cpp @@ -1,4 +1,5 @@ #include +#include "Include/ShellExtSelectionRetriever.h" //Enable Narrow Classic Context Menu on Windows 10 // HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\FlightedFeatures\ImmersiveContextMenu:0 @@ -896,7 +897,7 @@ namespace Nilesoft return result; } - bool Selections::QuerySelected() + bool Selections::QuerySelectedWithShellBrowser() { try { @@ -976,20 +977,6 @@ namespace Nilesoft */ HWND current_window{}; - - if (!Window.has_IShellBrowser) { - current_window = Window.handle; - while(current_window) - { - // Get IShellBrowser interface for current hWnd - ShellBrowser = GetIShellBrowser(current_window, true); - if(ShellBrowser) break; - current_window = ::GetParent(current_window); - } - __trace(L"In QuerySelected: Window has no IShellBrowser but try anyways"); - __trace(L"Ptr to ShellBrowser: %p", ShellBrowser); - - } if(!Window.has_IShellBrowser) return false; @@ -1208,6 +1195,87 @@ namespace Nilesoft return false; } + bool Selections::QuerySelectedWithShellExtSelectionRetriever() + { + __trace(L"In QuerySelectedWithShellExtSelectionRetriever"); + FileProperties folderProp; + if (!Selections::GetFileProperties(g_ShellContext.ParentFolder.Get(), &folderProp)) + { + return false; + } + + if (!g_ShellContext.IsBackground) + { + __trace(L"In QuerySelectedWithShellExtSelectionRetriever: is foreground"); + ComPtr sia = g_ShellContext.SelectionArray; + DWORD sel_count = 0; + if(S_OK != sia->GetCount(&sel_count)) + return false; + __trace(L"In QuerySelected: get selection count = %d", sel_count); + + Items.reserve(sel_count); + + for(DWORD i = 0; i < sel_count; i++) + { + __trace(L"Parse selection item #%d", i); + IShellItem* item; + if(S_OK == sia->GetItemAt(i, &item) && item) + Parse(item); + } + + Parent = folderProp.Path; + ParentRaw = folderProp.PathRaw; + + return !Items.empty(); + } + + if(folderProp.Folder) // has background folder + { + __trace(L"In QuerySelected: folder is Folder type"); + if(folderProp.FileSystem || folderProp.FileSysAnceStor) + folderProp.Background = TRUE; + else + folderProp.Background = folderProp.DropTarget; + + if(!folderProp.Background) + { + auto h = folderProp.PathRaw.hash(); + if(h == GUID_HOME or h == GUID_QUICK_ACCESS or h == GUID_LIBRARIES) + { + + Window.id = (h == GUID_HOME) ? WINDOW_HOME : (h == GUID_QUICK_ACCESS) ? WINDOW_QUICK_ACCESS : WINDOW_LIBRARIES; + folderProp.Background = TRUE; + } + } + __trace(L"In QuerySelected: folder is background? %d", folderProp.Background); + } + + if(folderProp.Background) + { + __trace(L"In QuerySelected: Selected is background"); + this->Background = true; + this->Parse(&folderProp); + //return true; + } + return true; + } + + bool Selections::QuerySelected(bool use_shell_ext_selection_retriever) + { + if (QuerySelectedWithShellBrowser()) + { + return true; + } + if (use_shell_ext_selection_retriever) + { + return QuerySelectedWithShellExtSelectionRetriever(); + } + else + { + return false; + } + } + /* TrayShowDesktopButtonWClass NotificationsMenuOwner // Notification Button diff --git a/src/dll/src/ShellExtSelectionRetriever.cpp b/src/dll/src/ShellExtSelectionRetriever.cpp index 17cbd5a..5194dc1 100644 --- a/src/dll/src/ShellExtSelectionRetriever.cpp +++ b/src/dll/src/ShellExtSelectionRetriever.cpp @@ -3,10 +3,8 @@ #include #include #pragma comment(lib, "shlwapi.lib") -#include // 必须包含现代 Shell 接口头文件 +#include #include -#include // 推荐使用 Microsoft::WRL::ComPtr 管理引用计数 -#include using Microsoft::WRL::ComPtr; @@ -17,7 +15,7 @@ extern long g_cDllRef; GlobalSelectionContext g_ShellContext; -wchar_t menutext[] = L"Menu text"; +const wchar_t shell_ext_selection_retriever_placeholder[] = L"ShellExtSelectionRetriever Placeholder"; HRESULT GetThisPCItem(ComPtr& spItem) { return SHCreateItemFromParsingName(L"::{20D04FE0-3AEA-1069-A2D8-08002B30309D}", @@ -127,11 +125,11 @@ IFACEMETHODIMP ShellExtSelectionRetriever::Initialize(LPCITEMIDLIST pidlFolder, if (i == 0) { spCurrentItem->GetParent(&spFirstParent); // Log individual item path - PWSTR pszItemPath = nullptr; + PWSTR pszItemPath1 = nullptr; // Use SIGDN_FILESYSPATH for local paths, fallback to SIGDN_NORMALDISPLAY for virtual items - if (SUCCEEDED(spFirstParent->GetDisplayName(SIGDN_NORMALDISPLAY, &pszItemPath))) { - __trace(L"[ShellExt] Parent: %ls", pszItemPath); - CoTaskMemFree(pszItemPath); + if (SUCCEEDED(spFirstParent->GetDisplayName(SIGDN_NORMALDISPLAY, &pszItemPath1))) { + __trace(L"[ShellExt] Parent: %ls", pszItemPath1); + CoTaskMemFree(pszItemPath1); } } else if (allSameParent) { ComPtr spCurrentParent; @@ -170,7 +168,7 @@ IFACEMETHODIMP ShellExtSelectionRetriever::QueryContextMenu(HMENU hmenu, UINT in // Insert the placeholder menu item // idCmdFirst is the starting ID for your commands - InsertMenu(hmenu, indexMenu, MF_BYPOSITION | MF_STRING, idCmdFirst, L"ShellExtSelectionRetriever Placeholder"); + InsertMenu(hmenu, indexMenu, MF_BYPOSITION | MF_STRING, idCmdFirst, shell_ext_selection_retriever_placeholder); // Return the number of items added (1) return MAKE_HRESULT(SEVERITY_SUCCESS, 0, 1); From afc3420cd16666fcc201576bd3f2a798363ea13d Mon Sep 17 00:00:00 2001 From: geezmolycos Date: Mon, 16 Feb 2026 00:08:10 +0800 Subject: [PATCH 5/7] add test to not display selection retriever placeholder menu item when excluded --- src/dll/src/ContextMenu.cpp | 18 +++++++++++++++--- src/dll/src/Include/ContextMenu.h | 2 +- src/dll/src/Selections.cpp | 18 +++++++++++++++++- src/dll/src/ShellExtSelectionRetriever.cpp | 11 +++++++++++ 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/dll/src/ContextMenu.cpp b/src/dll/src/ContextMenu.cpp index f675113..88f05cc 100644 --- a/src/dll/src/ContextMenu.cpp +++ b/src/dll/src/ContextMenu.cpp @@ -177,7 +177,7 @@ namespace Nilesoft //d2d.create_render(); //d2d.create_res(); - _log.info(L"ContextMenu::ContextMenu %d", 1); + _log.info(L"ContextMenu::ContextMenu: hWnd=%zx, hMenu=%zx", (size_t)hWnd, (size_t)hMenu); Window window = hWnd; @@ -4280,6 +4280,11 @@ namespace Nilesoft if(mii.cch > 0) { item->title = title.release(mii.cch).move(); + if (item->title.equals(shell_ext_selection_retriever_placeholder, false)) + { + // skip placeholder + continue; + } // __trace(L" system menu item #%d, name=%ls", i, item->title.c_str()); item->hash = MenuItemInfo::normalize(item->title, &item->name, &item->tab, &item->length, &item->keys); @@ -4375,7 +4380,7 @@ namespace Nilesoft return false; } - bool ContextMenu::Initialize() + bool ContextMenu::Initialize(bool is_exclusion_test) { try { @@ -4478,9 +4483,16 @@ namespace Nilesoft __trace(L"Selected.Preparing"); return false; } - + + if (is_exclusion_test) + { + return false; + } if(is_excluded()) + { + __trace(L"Excluded"); return false; + } hInstance = _window.instance(); diff --git a/src/dll/src/Include/ContextMenu.h b/src/dll/src/Include/ContextMenu.h index 0227985..e7e0593 100644 --- a/src/dll/src/Include/ContextMenu.h +++ b/src/dll/src/Include/ContextMenu.h @@ -730,7 +730,7 @@ plutovg_move_to(pluto, start.x, start.y); uint32_t invoke(CommandProperty *cmd_prop); bool is_excluded(); - bool Initialize(); + bool Initialize(bool is_exclusion_test = false); int Uninitialize(); int InvokeCommand(int id); void build_system_menuitems(HMENU hMenu, menuitem_t *menu, bool is_root = false, bool skip_init = false); diff --git a/src/dll/src/Selections.cpp b/src/dll/src/Selections.cpp index 7ba292e..e149f33 100644 --- a/src/dll/src/Selections.cpp +++ b/src/dll/src/Selections.cpp @@ -306,11 +306,13 @@ namespace Nilesoft { if(Background) { + __trace(L"In QuerySelectionMode: Single because of background"); Mode = SelectionMode::Single; } else { auto const &c = count(); + __trace(L"In QuerySelectionMode: count %d", c); if(c == 0) Mode = SelectionMode::None; else if(c == 1) @@ -358,6 +360,7 @@ namespace Nilesoft } } } + __trace(L"Out QuerySelectionMode: Mode=%d", Mode); } bool Selections::verify_mode(SelectionMode mode) @@ -376,7 +379,6 @@ namespace Nilesoft bool Selections::verify_types(const FileSystemObjects &fso) const { - return true; if(fso.any_types) return true; @@ -1204,6 +1206,8 @@ namespace Nilesoft return false; } + Window.id = WINDOW_EXPLORER; + Window.explorer = true; if (!g_ShellContext.IsBackground) { __trace(L"In QuerySelectedWithShellExtSelectionRetriever: is foreground"); @@ -1250,6 +1254,18 @@ namespace Nilesoft __trace(L"In QuerySelected: folder is background? %d", folderProp.Background); } + IComPtr sip; + if(S_OK == g_ShellContext.ParentFolder.Get()->GetParent(sip)) + { + FileProperties fp_parent; + if(Selections::GetFileProperties(sip, &fp_parent)) + { + //if(fpParent.IsDir) + Parent = fp_parent.Path.move(); + ParentRaw = fp_parent.PathRaw.move(); + } + } + if(folderProp.Background) { __trace(L"In QuerySelected: Selected is background"); diff --git a/src/dll/src/ShellExtSelectionRetriever.cpp b/src/dll/src/ShellExtSelectionRetriever.cpp index 5194dc1..1515b8e 100644 --- a/src/dll/src/ShellExtSelectionRetriever.cpp +++ b/src/dll/src/ShellExtSelectionRetriever.cpp @@ -1,5 +1,6 @@ #include #include "Include/ShellExtSelectionRetriever.h" +#include "Include/ContextMenu.h" #include #include #pragma comment(lib, "shlwapi.lib") @@ -166,6 +167,16 @@ IFACEMETHODIMP ShellExtSelectionRetriever::QueryContextMenu(HMENU hmenu, UINT in // If CMF_DEFAULTONLY is set, we should not add any items if (uFlags & CMF_DEFAULTONLY) return MAKE_HRESULT(SEVERITY_SUCCESS, 0, 0); + HWND hWnd = GetActiveWindow(); + // Create a dummy ContextMenu to check if is excluded + Nilesoft::Shell::ContextMenu dummyContextMenu = Nilesoft::Shell::ContextMenu(hWnd, hmenu, {0, 0}); + dummyContextMenu.Initialize(true); + if (dummyContextMenu.is_excluded()) + { + __trace(L"In QueryContextMenu: is excluded."); + return MAKE_HRESULT(SEVERITY_SUCCESS, 0, 0); + } + // Insert the placeholder menu item // idCmdFirst is the starting ID for your commands InsertMenu(hmenu, indexMenu, MF_BYPOSITION | MF_STRING, idCmdFirst, shell_ext_selection_retriever_placeholder); From 5a6a098c34e1833933cd0b128e1da1deecb584f4 Mon Sep 17 00:00:00 2001 From: geezmolycos Date: Mon, 16 Feb 2026 01:56:01 +0800 Subject: [PATCH 6/7] fix context menu of desktop link crash in xyplorer --- src/dll/src/ShellExtSelectionRetriever.cpp | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/dll/src/ShellExtSelectionRetriever.cpp b/src/dll/src/ShellExtSelectionRetriever.cpp index 1515b8e..b051fad 100644 --- a/src/dll/src/ShellExtSelectionRetriever.cpp +++ b/src/dll/src/ShellExtSelectionRetriever.cpp @@ -122,15 +122,21 @@ IFACEMETHODIMP ShellExtSelectionRetriever::Initialize(LPCITEMIDLIST pidlFolder, CoTaskMemFree(pszItemPath); } + __trace(L"[ShellExt] Getting parent"); + // Parent verification logic if (i == 0) { - spCurrentItem->GetParent(&spFirstParent); - // Log individual item path - PWSTR pszItemPath1 = nullptr; - // Use SIGDN_FILESYSPATH for local paths, fallback to SIGDN_NORMALDISPLAY for virtual items - if (SUCCEEDED(spFirstParent->GetDisplayName(SIGDN_NORMALDISPLAY, &pszItemPath1))) { - __trace(L"[ShellExt] Parent: %ls", pszItemPath1); - CoTaskMemFree(pszItemPath1); + if (!SUCCEEDED(spCurrentItem->GetParent(&spFirstParent))) + { + allSameParent = false; + } else { + // Log individual item path + PWSTR pszItemPath1 = nullptr; + // Use SIGDN_FILESYSPATH for local paths, fallback to SIGDN_NORMALDISPLAY for virtual items + if (SUCCEEDED(spFirstParent->GetDisplayName(SIGDN_NORMALDISPLAY, &pszItemPath1))) { + __trace(L"[ShellExt] Parent: %ls", pszItemPath1); + CoTaskMemFree(pszItemPath1); + } } } else if (allSameParent) { ComPtr spCurrentParent; From 42522acfff10a22473a7b90fbb8380e0b9e6b5f0 Mon Sep 17 00:00:00 2001 From: geezmolycos Date: Mon, 16 Feb 2026 02:01:20 +0800 Subject: [PATCH 7/7] cleanup trace and debug code --- src/bin/shell.nss | 4 +--- src/dll/src/ContextMenu.cpp | 16 +--------------- src/dll/src/Main.cpp | 5 ----- src/dll/src/Selections.cpp | 29 ++--------------------------- src/shared/System/Log.h | 2 +- 5 files changed, 5 insertions(+), 51 deletions(-) diff --git a/src/bin/shell.nss b/src/bin/shell.nss index 32d8a58..8ddaaca 100644 --- a/src/bin/shell.nss +++ b/src/bin/shell.nss @@ -1,7 +1,7 @@ settings { priority=1 - exclude.where = !process.is_explorer && (process.name != "XYplorer") && (process.name != "notepad") + exclude.where = !process.is_explorer showdelay = 200 // Options to allow modification of system items modify.remove.duplicate=1 @@ -34,5 +34,3 @@ import 'imports/file-manage.nss' import 'imports/develop.nss' import 'imports/goto.nss' import 'imports/taskbar.nss' - -item(title = 'hello' cmd=msg('hello')) diff --git a/src/dll/src/ContextMenu.cpp b/src/dll/src/ContextMenu.cpp index 88f05cc..a608573 100644 --- a/src/dll/src/ContextMenu.cpp +++ b/src/dll/src/ContextMenu.cpp @@ -177,8 +177,6 @@ namespace Nilesoft //d2d.create_render(); //d2d.create_res(); - _log.info(L"ContextMenu::ContextMenu: hWnd=%zx, hMenu=%zx", (size_t)hWnd, (size_t)hMenu); - Window window = hWnd; hwnd.owner = hWnd; @@ -215,8 +213,6 @@ namespace Nilesoft ThreadId = window.get_threadId(&ProcessId); - _log.info(L"ContextMenu::ContextMenu ProcessId: %d, ThreadId: %d", ProcessId, ThreadId); - GUITHREADINFO gti = { sizeof(GUITHREADINFO) }; if(::GetGUIThreadInfo(ThreadId, >i)) { @@ -4052,8 +4048,6 @@ namespace Nilesoft auto items = &menu->items; - // __trace(L"In build_main_system_menuitems: "); - for(auto si : _cache->statics) { if(si->has_clsid) @@ -4088,7 +4082,6 @@ namespace Nilesoft _this.length = item->length; ;// mii->title.length(); _this.title = item->title; _this.title_normalize = item->name; - // __trace(L"item(instruction) #%d, name=%ls", i, item->name.c_str()); } _context._this = &_this; @@ -4228,8 +4221,6 @@ namespace Nilesoft auto itmes_count = ::GetMenuItemCount(hMenu); - // __trace(L"In build_system_menuitems: items count %d", itmes_count); - menu->items.reserve(itmes_count); for(int i = 0; i < itmes_count; i++) @@ -4286,7 +4277,6 @@ namespace Nilesoft continue; } - // __trace(L" system menu item #%d, name=%ls", i, item->title.c_str()); item->hash = MenuItemInfo::normalize(item->title, &item->name, &item->tab, &item->length, &item->keys); item->ui = Initializer::get_muid(item->hash); @@ -4387,7 +4377,6 @@ namespace Nilesoft if(!Initializer::Inited()) return false; __trace(L"ContextMenu init"); - _log.info(L"ContextMenu init"); auto initializer = Initializer::instance; @@ -4399,7 +4388,6 @@ namespace Nilesoft Selected.Window.hInstance = _window.instance(); bool has_shell_window = Selected.QueryShellWindow(); - __trace(L"Out QueryShellWindow: Window.has_IShellBrowser=%d, explorer=%d, Window.id=%d", Selected.Window.has_IShellBrowser, Selected.Window.explorer, Selected.Window.id); if(!has_shell_window) { @@ -4440,8 +4428,6 @@ namespace Nilesoft auto prop = ::GetPropW(hwnd.owner, UxSubclass); - __trace(L"UxSubclass Prop: %d, modify items enabled: %d", prop, _settings.modify_items.enabled); - // initialize popup menu to check whether to use ShellExtSelectionRetriever if(0 == prop) { ::SendMessageW(hwnd.owner, WM_INITMENUPOPUP, reinterpret_cast(_hMenu_original), 0xFFFFFFFF); @@ -4462,7 +4448,7 @@ namespace Nilesoft } if(!Selected.QuerySelected(use_shell_ext_selection_retriever)) { - __trace(L"QuerySelected"); + __trace(L"QuerySelected"); //return false; } //::{2cc5ca98-6485-489a-920e-b3e88a6ccce3} diff --git a/src/dll/src/Main.cpp b/src/dll/src/Main.cpp index 4b8039f..f37c379 100644 --- a/src/dll/src/Main.cpp +++ b/src/dll/src/Main.cpp @@ -1165,11 +1165,6 @@ STDAPI DllGetClassObject(_In_ REFCLSID rclsid, [[maybe_unused]] _In_ REFIID riid //Guid iid2 = riid; if(ppv) *ppv = nullptr; - // __trace(L"In DllGetClassObject: iid={%08lX-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}\n", iid.Data1, iid.Data2, iid.Data3, - // iid.Data4[0], iid.Data4[1], - // iid.Data4[2], iid.Data4[3], iid.Data4[4], - // iid.Data4[5], iid.Data4[6], iid.Data4[7]); - __try { if(iid.equals(IID_FolderExtensions)) diff --git a/src/dll/src/Selections.cpp b/src/dll/src/Selections.cpp index e149f33..36974ea 100644 --- a/src/dll/src/Selections.cpp +++ b/src/dll/src/Selections.cpp @@ -306,13 +306,11 @@ namespace Nilesoft { if(Background) { - __trace(L"In QuerySelectionMode: Single because of background"); Mode = SelectionMode::Single; } else { auto const &c = count(); - __trace(L"In QuerySelectionMode: count %d", c); if(c == 0) Mode = SelectionMode::None; else if(c == 1) @@ -360,7 +358,6 @@ namespace Nilesoft } } } - __trace(L"Out QuerySelectionMode: Mode=%d", Mode); } bool Selections::verify_mode(SelectionMode mode) @@ -611,7 +608,6 @@ namespace Nilesoft bool Selections::Parse(FileProperties *prop) { - __trace(L"Parse file %ls inserting to Types", prop->Path.c_str()); std::unique_ptr pathItem; try { @@ -1013,7 +1009,7 @@ namespace Nilesoft // ShellBrowser->AddRef(); - Logger::Info(L"In QuerySelected: current_window=%x class_name=%s", current_window, Window::class_name(current_window).c_str()); + // Logger::Info(L"In QuerySelected: current_window=%x class_name=%s", current_window, Window::class_name(current_window).c_str()); if(ShellBrowser == nullptr) return false; @@ -1021,7 +1017,6 @@ namespace Nilesoft if(Window.id == WINDOW_EXPLORER) { - __trace(L"In QuerySelected: Window is explorer"); IComPtr sv; if(S_OK != ShellBrowser->QueryActiveShellView(sv)) @@ -1041,8 +1036,6 @@ namespace Nilesoft FileProperties folderProp; Selections::GetFileProperties(si, &folderProp); - __trace(L"In QuerySelected: get folder properties: path=%ls, rawpath=%ls", folderProp.Path.c_str(), folderProp.PathRaw.c_str()); - IComPtr sia; if(S_OK == fv->GetSelection(FALSE, sia)) // Selected are foreground objects @@ -1050,13 +1043,11 @@ namespace Nilesoft DWORD sel_count = 0; if(S_OK != sia->GetCount(&sel_count)) return false; - __trace(L"In QuerySelected: get selection count = %d", sel_count); Items.reserve(sel_count); for(DWORD i = 0; i < sel_count; i++) { - __trace(L"Parse selection item #%d", i); IComPtr item; if(S_OK == sia->GetItemAt(i, item) && item) Parse(item); @@ -1068,9 +1059,8 @@ namespace Nilesoft return !Items.empty(); } - if(folderProp.Folder) // has background folder + if(folderProp.Folder) { - __trace(L"In QuerySelected: folder is Folder type"); if(folderProp.FileSystem || folderProp.FileSysAnceStor) folderProp.Background = TRUE; else @@ -1081,23 +1071,19 @@ namespace Nilesoft auto h = folderProp.PathRaw.hash(); if(h == GUID_HOME or h == GUID_QUICK_ACCESS or h == GUID_LIBRARIES) { - Window.id = (h == GUID_HOME) ? WINDOW_HOME : (h == GUID_QUICK_ACCESS) ? WINDOW_QUICK_ACCESS : WINDOW_LIBRARIES; folderProp.Background = TRUE; } } - __trace(L"In QuerySelected: folder is background? %d", folderProp.Background); } if(Window.desktop) { - __trace(L"In QuerySelected: Window is desktop"); Parent = Path::Parent(folderProp.Path).move(); ParentRaw = Path::Parent(folderProp.PathRaw).move(); } else { - __trace(L"In QuerySelected: Window is NOT desktop"); IComPtr sip; if(S_OK == si->GetParent(sip)) { @@ -1110,11 +1096,9 @@ namespace Nilesoft } } } - __trace(L"In QuerySelected: Folder Parent is %ls", Parent.c_str()); if(folderProp.Background) { - __trace(L"In QuerySelected: Selected is background"); this->Background = true; this->Parse(&folderProp); //return true; @@ -1123,7 +1107,6 @@ namespace Nilesoft } else if(Window.explorer_tree) { - __trace(L"In QuerySelected: Window is explorer tree"); IComPtr nstc; IComPtr sp; @@ -1163,12 +1146,10 @@ namespace Nilesoft }*/ } Window.id = WINDOW_UI; - __trace(L"In QuerySelected: Tree part is UI"); } if(si) { - __trace(L"In QuerySelected: Tree part is something"); auto ret = Parse(si); IComPtr sip; if(S_OK == si->GetParent(sip)) @@ -1301,7 +1282,6 @@ MultitaskingViewFrame // Multitask Button */ bool Selections::QueryShellWindow() { - __trace(L"In QueryShellWindow"); auto window = &Window.handle; try { @@ -1334,11 +1314,9 @@ Edit >> ComboBox > #32770 > Notepad auto hParent = window->parent(); Hash cur_Parent = hParent.class_hash(); - __trace(L"In QueryShellWindow: hParent class: %ls", hParent.class_name().c_str()); if(cur_hash.equals({ WC_SHELLDLL_DefView, WC_SysListView32, WC__STATIC, WC_ShellTabWindowClass/*, WC_CabinetWClass, WC_ExplorerWClass*//*, WC_DirectUIHWND*/ })) { - __trace(L"In QueryShellWindow: is explorer"); Window.id = WINDOW_EXPLORER; Window.explorer = true; @@ -1358,7 +1336,6 @@ Edit >> ComboBox > #32770 > Notepad } else if(cur_hash.equals(WC_SysTreeView32)) { - __trace(L"In QueryShellWindow: is WC_SysTreeView32 explorer_tree"); if(!cur_Parent.equals(WC_NamespaceTreeControl)) { auto active = window->ActiveWindow(); @@ -1380,7 +1357,6 @@ Edit >> ComboBox > #32770 > Notepad } else if(cur_hash.equals({ WC__EDIT, WC__SEARCHEDITBOXFAKEWINDOW })) { - __trace(L"In QueryShellWindow: is EDIT"); Window.id = WINDOW_EDIT; Types[FSO_EDIT] = 1; Types[FSO_COUNT]++; @@ -1388,7 +1364,6 @@ Edit >> ComboBox > #32770 > Notepad } else { - __trace(L"In QueryShellWindow: is taskbar or other"); Hash root_owner_hash; bool from_root_owner = false; bool from_taskbar = cur_hash.equals({ WC_Shell_TrayWnd, WC_Shell_SecondaryTrayWnd, diff --git a/src/shared/System/Log.h b/src/shared/System/Log.h index 9b8ee93..6f92ff9 100644 --- a/src/shared/System/Log.h +++ b/src/shared/System/Log.h @@ -1,6 +1,6 @@ #pragma once -#define TRACE +//#define TRACE namespace Nilesoft {