[SHLWAPI][SHLWAPI_APITEST][SDK] Support SHInvokeCommandsOnContextMenu (#8919)

Implementing missing features...
JIRA issue: CORE-19278
- Implement
  SHInvokeCommandsOnContextMenu
  function.
- Modify shlwapi.spec.
- Add prototype to <shlwapi_undoc.h>.
- Add
  SHInvokeCommandsOnContextMenu
  testcase.
This commit is contained in:
Katayama Hirofumi MZ
2026-05-07 21:18:24 +09:00
committed by GitHub
parent 054c83ffda
commit c6bcf2ac10
6 changed files with 432 additions and 1 deletions
+1 -1
View File
@@ -538,7 +538,7 @@
538 stdcall -noname IUnknown_QueryServiceForWebBrowserApp(ptr ptr ptr)
539 stub -noname IUnknown_ShowBrowserBar
540 stdcall -noname SHInvokeCommandOnContextMenu(ptr ptr ptr long str)
541 stub -noname SHInvokeCommandsOnContextMenu
541 stdcall -noname SHInvokeCommandsOnContextMenu(ptr ptr ptr long ptr long)
542 stdcall -noname GetUIVersion()
543 stdcall -noname CreateColorSpaceWrapW(ptr) gdi32.CreateColorSpaceW
544 stub -noname QuerySourceCreateFromKey
+101
View File
@@ -215,6 +215,107 @@ SHInvokeCommandOnContextMenu(
return SHInvokeCommandOnContextMenuEx(hWnd, pUnk, pCM, fCMIC, CMF_EXTENDEDVERBS, pszVerb, NULL);
}
static inline BOOL
IsTextAsciiOnly(PCSTR psz)
{
for (const signed char *pch = (const signed char *)psz; *pch; ++pch)
{
if (*pch < 0)
return FALSE;
}
return TRUE;
}
/*************************************************************************
* SHInvokeCommandsOnContextMenu [SHLWAPI.541]
*/
EXTERN_C
HRESULT WINAPI
SHInvokeCommandsOnContextMenu(
_In_opt_ HWND hwnd,
_In_opt_ IUnknown *punkSite,
_In_ IContextMenu *pCM,
_In_ DWORD fMask,
_In_reads_opt_(cVerbs) PCSTR *pVerbs,
_In_ UINT cVerbs)
{
HRESULT hr;
CMINVOKECOMMANDINFOEX ici;
WCHAR szVerbW[MAX_PATH];
HMENU hMenu = NULL;
UINT iVerb, idDefault = (UINT)-1;
PCSTR pszVerbA = NULL;
if (!pCM)
return E_INVALIDARG;
hMenu = CreatePopupMenu();
if (!hMenu)
return E_OUTOFMEMORY;
if (punkSite)
IUnknown_SetSite(pCM, punkSite);
hr = pCM->QueryContextMenu(hMenu, 0, 1, MAXSHORT, (cVerbs ? 0 : CMF_DEFAULTONLY));
if (FAILED(hr))
goto Cleanup;
if (!cVerbs)
{
idDefault = GetMenuDefaultItem(hMenu, FALSE, 0);
if (idDefault != (UINT)-1)
pszVerbA = MAKEINTRESOURCEA(idDefault - 1);
}
ZeroMemory(&ici, sizeof(ici));
ici.cbSize = sizeof(ici);
ici.hwnd = hwnd;
ici.nShow = SW_SHOWNORMAL;
iVerb = 0;
do
{
if (cVerbs)
pszVerbA = pVerbs[iVerb];
if (!pszVerbA && idDefault == (UINT)-1)
{
hr = E_FAIL;
break;
}
ici.fMask = fMask;
ici.lpVerb = pszVerbA;
ici.lpVerbW = NULL;
if (idDefault == (UINT)-1 && !IS_INTRESOURCE(pszVerbA) && IsTextAsciiOnly(pszVerbA))
{
size_t ich;
for (ich = 0; pszVerbA[ich] && ich + 1 < _countof(szVerbW); ++ich)
{
szVerbW[ich] = (BYTE)pszVerbA[ich];
}
szVerbW[ich] = UNICODE_NULL;
ici.lpVerbW = szVerbW;
ici.fMask |= CMIC_MASK_UNICODE;
}
hr = pCM->InvokeCommand((LPCMINVOKECOMMANDINFO)&ici);
if (SUCCEEDED(hr) || hr == HRESULT_FROM_WIN32(ERROR_CANCELLED))
break;
++iVerb;
} while (iVerb < cVerbs);
Cleanup:
if (punkSite)
IUnknown_SetSite(pCM, NULL);
DestroyMenu(hMenu);
return hr;
}
/*************************************************************************
* SHInvokeCommandWithFlagsAndSite [SHLWAPI.571]
*/
@@ -19,6 +19,7 @@ list(APPEND SOURCE
PathUnExpandEnvStringsForUser.c
SHAreIconsEqual.c
SHGetRestriction.c
SHInvokeCommandsOnContextMenu.cpp
SHLoadIndirectString.c
SHLoadRegUIString.c
SHPropertyBag.cpp
@@ -0,0 +1,318 @@
/*
* PROJECT: ReactOS api tests
* LICENSE: GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later)
* PURPOSE: Tests for SHInvokeCommandsOnContextMenu
* COPYRIGHT: Copyright 2026 Katayama Hirofumi MZ <[email protected]>
*/
#include <apitest.h>
#include <windows.h>
#include <shlwapi.h>
#include <objbase.h>
#include <shellapi.h>
#include <shobjidl.h>
typedef HRESULT (WINAPI *FN_SHInvokeCommandsOnContextMenu)(HWND, IUnknown *, IContextMenu *,
DWORD, PCSTR *, UINT);
static FN_SHInvokeCommandsOnContextMenu g_fnSHInvokeCommandsOnContextMenu = NULL;
struct MockContextMenu : public IContextMenu
{
HRESULT queryResult = S_OK;
HRESULT invokeResult = S_OK;
UINT defaultItemId = (UINT)-1;
INT queryCalled = 0;
INT invokeCalled = 0;
DWORD lastQueryFlags = 0;
CHAR lastVerb[MAX_PATH] = {};
WCHAR lastVerbW[MAX_PATH] = {};
DWORD lastFMask = 0;
BOOL lastHadUnicode = FALSE;
STDMETHODIMP_(ULONG) AddRef() override { return 1; }
STDMETHODIMP_(ULONG) Release() override { return 1; }
STDMETHODIMP QueryInterface(REFIID, void**) override
{
return E_NOINTERFACE;
}
STDMETHODIMP
QueryContextMenu(HMENU hmenu, UINT, UINT idCmdFirst, UINT, UINT uFlags) override
{
++queryCalled;
lastQueryFlags = uFlags;
if (FAILED(queryResult))
return queryResult;
if (defaultItemId != (UINT)-1)
{
AppendMenuA(hmenu, MF_STRING, idCmdFirst + defaultItemId - 1, "Item");
SetMenuDefaultItem(hmenu, idCmdFirst + defaultItemId - 1, FALSE);
}
return queryResult;
}
STDMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO pici) override
{
++invokeCalled;
const CMINVOKECOMMANDINFOEX* iciex = reinterpret_cast<const CMINVOKECOMMANDINFOEX*>(pici);
lastFMask = iciex->fMask;
lastHadUnicode = !!(iciex->fMask & CMIC_MASK_UNICODE);
if (HIWORD(iciex->lpVerb))
lstrcpyA(lastVerb, iciex->lpVerb);
else
lastVerb[0] = ANSI_NULL;
if (lastHadUnicode && iciex->lpVerbW && HIWORD(iciex->lpVerbW))
lstrcpyW(lastVerbW, iciex->lpVerbW);
else
lastVerbW[0] = UNICODE_NULL;
return invokeResult;
}
STDMETHODIMP GetCommandString(UINT_PTR, UINT, UINT*, CHAR*, UINT) override
{
return E_NOTIMPL;
}
};
struct MockSite : public IUnknown
{
STDMETHODIMP_(ULONG) AddRef() override { return 1; }
STDMETHODIMP_(ULONG) Release() override { return 1; }
STDMETHODIMP QueryInterface(REFIID, void**) override
{
return E_NOINTERFACE;
}
};
class SHInvokeCommandsOnContextMenuTest : public MockContextMenu
{
protected:
MockSite site;
public:
SHInvokeCommandsOnContextMenuTest()
{
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
}
~SHInvokeCommandsOnContextMenuTest()
{
CoUninitialize();
}
};
static void ZeroVerbs_NoDefaultItem_ReturnsEFail(void)
{
SHInvokeCommandsOnContextMenuTest cm;
HRESULT hr = g_fnSHInvokeCommandsOnContextMenu(NULL, NULL, &cm, 0, NULL, 0);
ok_hr(hr, E_FAIL);
ok_int(cm.queryCalled, 1);
ok_int(cm.invokeCalled, 0);
}
static void ZeroVerbs_WithDefaultItem_InvokesOnce(void)
{
SHInvokeCommandsOnContextMenuTest cm;
cm.defaultItemId = 1;
cm.invokeResult = S_OK;
HRESULT hr = g_fnSHInvokeCommandsOnContextMenu(NULL, NULL, &cm, 0, NULL, 0);
ok_hr(hr, S_OK);
ok_int(cm.queryCalled, 1);
ok_int(cm.invokeCalled, 1);
}
static void ZeroVerbs_QueryReceivesCMF_DEFAULTONLY(void)
{
SHInvokeCommandsOnContextMenuTest cm;
cm.defaultItemId = 1;
g_fnSHInvokeCommandsOnContextMenu(NULL, NULL, &cm, 0, NULL, 0);
ok_int(!!(cm.lastQueryFlags & CMF_DEFAULTONLY), TRUE);
}
static void NonZeroVerbs_QueryDoesNotReceiveCMF_DEFAULTONLY(void)
{
const char* verbs[] = { "open" };
SHInvokeCommandsOnContextMenuTest cm;
g_fnSHInvokeCommandsOnContextMenu(NULL, NULL, &cm, 0, verbs, 1);
ok_int(!!(cm.lastQueryFlags & CMF_DEFAULTONLY), FALSE);
}
static void SingleVerb_Success(void)
{
const char* verbs[] = { "open" };
SHInvokeCommandsOnContextMenuTest cm;
cm.invokeResult = S_OK;
HRESULT hr = g_fnSHInvokeCommandsOnContextMenu(NULL, NULL, &cm, 0, verbs, 1);
ok_hr(hr, S_OK);
ok_int(cm.invokeCalled, 1);
ok_str(cm.lastVerb, "open");
}
static void MultipleVerbs_StopsOnFirstSuccess(void)
{
const char* verbs[] = { "open", "print" };
SHInvokeCommandsOnContextMenuTest cm;
cm.invokeResult = S_OK;
HRESULT hr = g_fnSHInvokeCommandsOnContextMenu(NULL, NULL, &cm, 0, verbs, 2);
ok_hr(hr, S_OK);
ok_int(cm.invokeCalled, 1);
ok_str(cm.lastVerb, "open");
}
static void Cancelled_BreaksLoop(void)
{
const char* verbs[] = { "open", "print" };
SHInvokeCommandsOnContextMenuTest cm;
cm.invokeResult = HRESULT_FROM_WIN32(ERROR_CANCELLED);
HRESULT hr = g_fnSHInvokeCommandsOnContextMenu(NULL, NULL, &cm, 0, verbs, 2);
ok_hr(hr, HRESULT_FROM_WIN32(ERROR_CANCELLED));
ok_int(cm.invokeCalled, 1);
}
static void AsciiVerb_SetsUnicodeMaskAndVerbW(void)
{
const char* verbs[] = { "open" };
SHInvokeCommandsOnContextMenuTest cm;
cm.invokeResult = S_OK;
g_fnSHInvokeCommandsOnContextMenu(NULL, NULL, &cm, 0, verbs, 1);
ok_int(!!cm.lastHadUnicode, TRUE);
ok_wstr(cm.lastVerbW, L"open");
}
static void FMask_IsPassedToInvokeCommand(void)
{
const DWORD kMask = SEE_MASK_NOASYNC | SEE_MASK_FLAG_NO_UI;
const char* verbs[] = { "open" };
SHInvokeCommandsOnContextMenuTest cm;
cm.invokeResult = S_OK;
g_fnSHInvokeCommandsOnContextMenu(NULL, NULL, &cm, kMask, verbs, 1);
ok_int(!!(cm.lastFMask & kMask), TRUE);
}
struct MockSiteTarget : public IContextMenu, public IObjectWithSite
{
IUnknown* site = NULL;
BOOL siteSet = FALSE;
BOOL siteClear= FALSE;
INT queryCnt = 0;
// IUnknown
STDMETHODIMP_(ULONG) AddRef() override { return 1; }
STDMETHODIMP_(ULONG) Release() override { return 1; }
STDMETHODIMP QueryInterface(REFIID riid, void** ppv) override
{
if (riid == IID_IObjectWithSite) { *ppv = static_cast<IObjectWithSite*>(this); return S_OK; }
return E_NOINTERFACE;
}
// IObjectWithSite
STDMETHODIMP SetSite(IUnknown* pSite) override
{
if (pSite) siteSet = true;
else siteClear= true;
site = pSite;
return S_OK;
}
STDMETHODIMP GetSite(REFIID, void**) override { return E_NOTIMPL; }
// IContextMenu
STDMETHODIMP QueryContextMenu(HMENU, UINT, UINT, UINT, UINT) override
{
++queryCnt;
return S_OK;
}
STDMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO) override { return E_FAIL; }
STDMETHODIMP GetCommandString(UINT_PTR, UINT, UINT*, CHAR*, UINT) override { return E_NOTIMPL; }
};
static void PunkSite_SetAndClearedAroundCall(void)
{
MockSiteTarget target;
MockSite mySite;
const char* verbs[] = { "open" };
g_fnSHInvokeCommandsOnContextMenu(NULL, &mySite, &target, 0, verbs, 1);
ok_int(target.siteSet, TRUE);
ok_int(target.siteClear, TRUE);
}
static void QueryFails_InvokeNotCalled(void)
{
SHInvokeCommandsOnContextMenuTest cm;
cm.queryResult = E_FAIL;
cm.invokeResult = S_OK;
const char* verbs[] = { "open" };
HRESULT hr = g_fnSHInvokeCommandsOnContextMenu(NULL, NULL, &cm, 0, verbs, 1);
ok_int(FAILED(hr), TRUE);
ok_int(cm.invokeCalled, 0);
}
struct HwndCaptureMock : public MockContextMenu
{
HWND capturedHwnd = NULL;
STDMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO pici) override
{
capturedHwnd = pici->hwnd;
return S_OK;
}
};
static void Hwnd_IsPassedToInvokeCommand(void)
{
HwndCaptureMock mock;
const char* verbs[] = { "open" };
HWND fakeHwnd = reinterpret_cast<HWND>(static_cast<ULONG_PTR>(0xDEADBEEF));
g_fnSHInvokeCommandsOnContextMenu(fakeHwnd, NULL, &mock, 0, verbs, 1);
ok_ptr(mock.capturedHwnd, fakeHwnd);
}
START_TEST(SHInvokeCommandsOnContextMenu)
{
HINSTANCE hSHLWAPI = LoadLibraryW(L"shlwapi");
if (!hSHLWAPI)
{
skip("shlwapi not found\n");
return;
}
g_fnSHInvokeCommandsOnContextMenu =
(FN_SHInvokeCommandsOnContextMenu)GetProcAddress(hSHLWAPI, MAKEINTRESOURCEA(541));
if (!g_fnSHInvokeCommandsOnContextMenu)
{
skip("SHInvokeCommandsOnContextMenu not found\n");
FreeLibrary(hSHLWAPI);
return;
}
ZeroVerbs_NoDefaultItem_ReturnsEFail();
ZeroVerbs_WithDefaultItem_InvokesOnce();
ZeroVerbs_QueryReceivesCMF_DEFAULTONLY();
NonZeroVerbs_QueryDoesNotReceiveCMF_DEFAULTONLY();
SingleVerb_Success();
MultipleVerbs_StopsOnFirstSuccess();
Cancelled_BreaksLoop();
AsciiVerb_SetsUnicodeMaskAndVerbW();
FMask_IsPassedToInvokeCommand();
PunkSite_SetAndClearedAroundCall();
QueryFails_InvokeNotCalled();
Hwnd_IsPassedToInvokeCommand();
FreeLibrary(hSHLWAPI);
}
@@ -15,6 +15,7 @@ extern void func_PathUnExpandEnvStrings(void);
extern void func_PathUnExpandEnvStringsForUser(void);
extern void func_SHAreIconsEqual(void);
extern void func_SHGetRestriction(void);
extern void func_SHInvokeCommandsOnContextMenu(void);
extern void func_SHLoadIndirectString(void);
extern void func_SHLoadRegUIString(void);
extern void func_SHPropertyBag(void);
@@ -38,6 +39,7 @@ const struct test winetest_testlist[] =
{ "PathUnExpandEnvStringsForUser", func_PathUnExpandEnvStringsForUser },
{ "SHAreIconsEqual", func_SHAreIconsEqual },
{ "SHGetRestriction", func_SHGetRestriction },
{ "SHInvokeCommandsOnContextMenu", func_SHInvokeCommandsOnContextMenu },
{ "SHLoadIndirectString", func_SHLoadIndirectString },
{ "SHLoadRegUIString", func_SHLoadRegUIString },
{ "SHPropertyBag", func_SHPropertyBag },
+9
View File
@@ -110,6 +110,15 @@ HRESULT WINAPI SHIsExpandableFolder(LPSHELLFOLDER lpFolder, LPCITEMIDLIST pidl);
DWORD WINAPI SHFillRectClr(HDC hDC, LPCRECT pRect, COLORREF cRef);
int WINAPI SHSearchMapInt(const int *lpKeys, const int *lpValues, int iLen, int iKey);
HRESULT WINAPI
SHInvokeCommandsOnContextMenu(
_In_opt_ HWND hwnd,
_In_opt_ IUnknown *punkSite,
_In_ IContextMenu *pCM,
_In_ DWORD fMask,
_In_reads_opt_(cVerbs) PCSTR *pVerbs,
_In_ UINT cVerbs);
HRESULT WINAPI
MayQSForward(
_In_ IUnknown *lpUnknown,