mirror of
https://github.com/ApfelTeeSaft/reactos.git
synced 2026-08-26 19:33:31 +00:00
[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:
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user