1142 lines
33 KiB
C++
1142 lines
33 KiB
C++
#include "shellprv.h"
|
|
#pragma hdrstop
|
|
|
|
#include <dpa.h>
|
|
#include "ids.h"
|
|
#include "idlcomm.h"
|
|
#include "recdocs.h"
|
|
#include "datautil.h"
|
|
#include "mtpt.h"
|
|
#include <cowsite.h>
|
|
|
|
typedef struct _DKAITEM { // dkai
|
|
HKEY hk;
|
|
TCHAR sz[CCH_KEYMAX];
|
|
} DKAITEM, *PDKAITEM;
|
|
typedef const DKAITEM * PCDKAITEM;
|
|
|
|
class CDKA : public CDSA<DKAITEM>
|
|
{
|
|
public:
|
|
~CDKA();
|
|
UINT AddKeys(HKEY hk, LPCTSTR pszSubKey, PCTSTR pszDefaultOrder);
|
|
|
|
PCTSTR ExposeName(int id)
|
|
{ return GetItemPtr(id)->sz; }
|
|
|
|
HKEY ExposeKey(int id)
|
|
{ return GetItemPtr(id)->hk; }
|
|
|
|
HRESULT GetValue(int id,
|
|
PCTSTR pszSubKey,
|
|
PCTSTR pszValue,
|
|
DWORD *pdwType,
|
|
void *pvData,
|
|
DWORD *pcbData);
|
|
|
|
BOOL DeleteItem(int id);
|
|
BOOL DeleteAllItems();
|
|
void Reset()
|
|
{ if ((HDSA)this) DestroyCallback(_ReleaseItem, NULL); }
|
|
BOOL HasDefault(HKEY hkProgid);
|
|
|
|
protected:
|
|
BOOL _AppendItem(HKEY hk, PDKAITEM pdkai);
|
|
void _AddOrderedKeys(HKEY hk, PCTSTR pszDefOrder);
|
|
void _AddEnumKeys(HKEY hk);
|
|
static int CALLBACK _ReleaseItem(PDKAITEM pdkai, void *pv);
|
|
|
|
protected:
|
|
TRIBIT _tbHasDefault;
|
|
};
|
|
|
|
BOOL CDKA::_AppendItem(HKEY hk, PDKAITEM pdkai)
|
|
{
|
|
BOOL fRet = FALSE;
|
|
// Verify that the key exists before adding it to the list
|
|
if (RegOpenKeyEx(hk, pdkai->sz, 0L, KEY_READ, &pdkai->hk) == ERROR_SUCCESS)
|
|
{
|
|
fRet = (AppendItem(pdkai) >= 0);
|
|
|
|
if (!fRet)
|
|
RegCloseKey(pdkai->hk);
|
|
}
|
|
return fRet;
|
|
}
|
|
|
|
void CDKA::_AddOrderedKeys(HKEY hk, PCTSTR pszDefOrder)
|
|
{
|
|
// First, add the subkeys from the value of the specified key
|
|
// This should never fail, since we just opened this key
|
|
DKAITEM dkai;
|
|
TCHAR szOrder[CCH_KEYMAX * 5];
|
|
LONG cbOrder = CbFromCch(ARRAYSIZE(szOrder));
|
|
*szOrder = 0;
|
|
RegQueryValue(hk, NULL, szOrder, &cbOrder);
|
|
if (*szOrder)
|
|
{
|
|
// now we must find something in this string in order to have a default
|
|
_tbHasDefault = TRIBIT_FALSE;
|
|
}
|
|
else if (pszDefOrder)
|
|
{
|
|
// If there is no value, use the order requested
|
|
// typically "Open" or "Explore Open" in explorer mode
|
|
StrCpyN(szOrder, pszDefOrder, ARRAYSIZE(szOrder));
|
|
}
|
|
|
|
PTSTR psz = szOrder;
|
|
while (psz && *psz)
|
|
{
|
|
// skip the space or comma characters
|
|
while(*psz==TEXT(' ') || *psz==TEXT(','))
|
|
psz++; // NLS Notes: OK to ++
|
|
|
|
if (*psz)
|
|
{
|
|
// Search for the space or comma character
|
|
LPTSTR pszNext = psz + StrCSpn(psz, TEXT(" ,"));
|
|
if (*pszNext) {
|
|
*pszNext++=0; // NLS Notes: OK to ++
|
|
}
|
|
StrCpyN(dkai.sz, psz, ARRAYSIZE(dkai.sz));
|
|
|
|
if (_AppendItem(hk, &dkai))
|
|
_tbHasDefault = TRIBIT_TRUE;
|
|
|
|
psz = pszNext;
|
|
}
|
|
}
|
|
}
|
|
|
|
void CDKA::_AddEnumKeys(HKEY hk)
|
|
{
|
|
DKAITEM dkai;
|
|
// Then, append the rest if they are not in the list yet.
|
|
for (int i = 0; RegEnumKey(hk, i, dkai.sz, ARRAYSIZE(dkai.sz)) == ERROR_SUCCESS; i++)
|
|
{
|
|
// Check if the key is already in the list.
|
|
for (int idsa = 0; idsa < GetItemCount(); idsa++)
|
|
{
|
|
PDKAITEM pdkai = GetItemPtr(idsa);
|
|
if (lstrcmpi(dkai.sz, pdkai->sz)==0)
|
|
break;
|
|
}
|
|
|
|
// we made it throug our array
|
|
// so this isnt in there
|
|
if (idsa == GetItemCount())
|
|
_AppendItem(hk, &dkai);
|
|
}
|
|
}
|
|
|
|
UINT CDKA::AddKeys(HKEY hkRoot, LPCTSTR pszSubKey, PCTSTR pszDefaultOrder)
|
|
{
|
|
UINT cKeys = GetItemCount();
|
|
HKEY hk;
|
|
if (ERROR_SUCCESS == RegOpenKeyEx(hkRoot, pszSubKey, 0L, KEY_READ, &hk))
|
|
{
|
|
_AddOrderedKeys(hk, pszDefaultOrder);
|
|
_AddEnumKeys(hk);
|
|
RegCloseKey(hk);
|
|
}
|
|
return GetItemCount() - cKeys;
|
|
}
|
|
|
|
int CALLBACK CDKA::_ReleaseItem(PDKAITEM pdkai, void *pv)
|
|
{
|
|
if (pdkai->hk)
|
|
{
|
|
RegCloseKey(pdkai->hk);
|
|
pdkai->hk = NULL;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
CDKA::~CDKA()
|
|
{
|
|
Reset();
|
|
}
|
|
|
|
// override this DSA methods to get release
|
|
BOOL CDKA::DeleteItem(int id)
|
|
{
|
|
PDKAITEM p = GetItemPtr(id);
|
|
if (p)
|
|
{
|
|
_ReleaseItem(p, NULL);
|
|
return CDSA<DKAITEM>::DeleteItem(id);
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
// override this DSA methods to get release
|
|
BOOL CDKA::DeleteAllItems()
|
|
{
|
|
EnumCallback(_ReleaseItem, NULL);
|
|
return CDSA<DKAITEM>::DeleteAllItems();
|
|
}
|
|
|
|
HRESULT CDKA::GetValue(int id,
|
|
PCTSTR pszSubKey,
|
|
PCTSTR pszValue,
|
|
DWORD *pdwType,
|
|
void *pvData,
|
|
DWORD *pcbData)
|
|
{
|
|
DWORD err = SHGetValue(GetItemPtr(id)->hk, pszSubKey, pszValue, pdwType, pvData, pcbData);
|
|
return HRESULT_FROM_WIN32(err);
|
|
}
|
|
|
|
BOOL CDKA::HasDefault(HKEY hkProgid)
|
|
{
|
|
if (_tbHasDefault == TRIBIT_UNDEFINED)
|
|
{
|
|
HKEY hk;
|
|
if (ERROR_SUCCESS== RegOpenKeyEx(hkProgid, L"ShellFolder", 0, MAXIMUM_ALLOWED, &hk))
|
|
{
|
|
// APPCOMPAT - regitems need to have the open verb - ZekeL - 30-JAN-2001
|
|
// so that the IQA and ICM will behave the same,
|
|
// and regitem folders will always default to
|
|
// folder\shell\open unless they implement open
|
|
// or specify default verbs.
|
|
//
|
|
_tbHasDefault = TRIBIT_FALSE;
|
|
RegCloseKey(hk);
|
|
}
|
|
else
|
|
{
|
|
_tbHasDefault = TRIBIT_TRUE;
|
|
}
|
|
}
|
|
return _tbHasDefault == TRIBIT_TRUE;
|
|
}
|
|
|
|
|
|
typedef HRESULT (__stdcall *LPFNADDPAGES)(IDataObject *, LPFNADDPROPSHEETPAGE, LPARAM);
|
|
|
|
class CShellExecMenu : public IShellExtInit, public IContextMenu, public IShellPropSheetExt, CObjectWithSite
|
|
{
|
|
public:
|
|
// IUnknown
|
|
STDMETHODIMP QueryInterface(REFIID riid, void **ppv);
|
|
STDMETHODIMP_(ULONG) AddRef(void);
|
|
STDMETHODIMP_(ULONG) Release(void);
|
|
|
|
// IShellExtInit
|
|
STDMETHODIMP Initialize(LPCITEMIDLIST pidlFolder, IDataObject *pdtobj, HKEY hkeyProgID);
|
|
|
|
// IContextMenu
|
|
STDMETHODIMP QueryContextMenu(HMENU hmenu, UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags);
|
|
STDMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO pici);
|
|
STDMETHODIMP GetCommandString(UINT_PTR idCmd, UINT wFlags, UINT *pwRes, LPSTR pszName, UINT cchMax);
|
|
|
|
// IShellPropSheetExt
|
|
STDMETHODIMP AddPages(LPFNADDPROPSHEETPAGE, LPARAM);
|
|
STDMETHODIMP ReplacePage(UINT, LPFNADDPROPSHEETPAGE, LPARAM);
|
|
|
|
CShellExecMenu(LPFNADDPAGES pfnAddPages);
|
|
|
|
protected: // methods
|
|
~CShellExecMenu();
|
|
void _Cleanup();
|
|
HRESULT _InsureVerbs(UINT idVerb = 0);
|
|
UINT _VerbCount();
|
|
LPCTSTR _GetVerb(UINT id);
|
|
UINT _FindIndex(LPCTSTR pszVerb);
|
|
DWORD _BrowseFlagsFromVerb(UINT idVerb);
|
|
BOOL _GetMenuString(UINT id, BOOL fExtended, LPTSTR pszMenu, UINT cchMax);
|
|
BOOL _IsExplorerMode();
|
|
BOOL _SupportsType(UINT idVerb);
|
|
BOOL _IsRestricted(UINT idVerb);
|
|
BOOL _IsVisible(BOOL fExtended, UINT idVerb);
|
|
BOOL _RemoveVerb(UINT idVerb);
|
|
BOOL _VerbCanDrop(UINT idVerb, CLSID *pclsid);
|
|
HRESULT _DoDrop(REFCLSID clsid, UINT idVerb, LPCMINVOKECOMMANDINFOEX pici);
|
|
|
|
HRESULT _MapVerbForInvoke(CMINVOKECOMMANDINFOEX *pici, UINT *pidVerb);
|
|
HRESULT _TryBrowseObject(LPCITEMIDLIST pidl, DWORD uFlags);
|
|
void _DoRecentStuff(LPCITEMIDLIST pidl, LPCTSTR pszPath);
|
|
HRESULT _InvokeOne(CMINVOKECOMMANDINFOEX *pici, UINT idVerb, LPCITEMIDLIST pidl);
|
|
HRESULT _InvokeMany(CMINVOKECOMMANDINFOEX *pici, UINT idVerb, LPIDA pida);
|
|
HRESULT _InvokeEach(LPCITEMIDLIST pidl, CMINVOKECOMMANDINFOEX *pici);
|
|
HRESULT _PromptUser(CMINVOKECOMMANDINFOEX *pici, UINT idVerb, LPIDA pida);
|
|
|
|
HRESULT _MapVerbForGCS(UINT_PTR idCmd, UINT uType, UINT *pidVerb);
|
|
HRESULT _GetHelpText(UINT idVerb, UINT uType, LPSTR pszName, UINT cchMax);
|
|
|
|
private: // members
|
|
LONG _cRef;
|
|
IDataObject *_pdtobj;
|
|
HKEY _hkeyProgID;
|
|
CDKA _dka;
|
|
LPFNADDPAGES _pfnAddPages;
|
|
UINT _uFlags;
|
|
};
|
|
|
|
CShellExecMenu::CShellExecMenu(LPFNADDPAGES pfnAddPages) : _pfnAddPages(pfnAddPages), _cRef(1)
|
|
{
|
|
}
|
|
|
|
CShellExecMenu::~CShellExecMenu()
|
|
{
|
|
_Cleanup();
|
|
}
|
|
|
|
void CShellExecMenu::_Cleanup()
|
|
{
|
|
_dka.Reset();
|
|
|
|
if (_hkeyProgID)
|
|
{
|
|
RegCloseKey(_hkeyProgID);
|
|
_hkeyProgID = NULL;
|
|
}
|
|
|
|
ATOMICRELEASE(_pdtobj);
|
|
}
|
|
|
|
STDMETHODIMP CShellExecMenu::QueryInterface(REFIID riid, void **ppvObj)
|
|
{
|
|
static const QITAB qit[] = {
|
|
QITABENT(CShellExecMenu, IShellExtInit),
|
|
QITABENT(CShellExecMenu, IContextMenu),
|
|
QITABENT(CShellExecMenu, IShellPropSheetExt),
|
|
QITABENT(CShellExecMenu, IObjectWithSite),
|
|
{ 0 },
|
|
};
|
|
return QISearch(this, qit, riid, ppvObj);
|
|
}
|
|
|
|
STDMETHODIMP_(ULONG) CShellExecMenu::AddRef()
|
|
{
|
|
return InterlockedIncrement(&_cRef);
|
|
}
|
|
|
|
STDMETHODIMP_(ULONG) CShellExecMenu::Release()
|
|
{
|
|
if (InterlockedDecrement(&_cRef))
|
|
return _cRef;
|
|
|
|
delete this;
|
|
return 0;
|
|
}
|
|
|
|
HRESULT CShellExecMenu::_InsureVerbs(UINT idVerb)
|
|
{
|
|
// the idVerb is the minimum verb that we need to succeed
|
|
if (!(HDSA)_dka && _hkeyProgID)
|
|
{
|
|
// create either "open" or "explore open"
|
|
if (_dka.Create(4))
|
|
{
|
|
_dka.AddKeys(_hkeyProgID, c_szShell, _IsExplorerMode() ? TEXT("Explore open") : TEXT("open"));
|
|
|
|
// WARNING - some verbs are not valid and need to be removed
|
|
for (int id = 0; id < _dka.GetItemCount(); id++)
|
|
{
|
|
if (_RemoveVerb(id))
|
|
_dka.DeleteItem(id);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
return ((HDSA)_dka && idVerb < (UINT)_dka.GetItemCount()) ? S_OK : E_FAIL;
|
|
}
|
|
|
|
// Descriptions:
|
|
// This function generates appropriate menu string from the given
|
|
// verb key string. This function is called if the verb key does
|
|
// not have the value.
|
|
|
|
BOOL _MenuString(LPCTSTR pszVerbKey, LPTSTR pszMenuString, UINT cchMax)
|
|
{
|
|
// Table look-up (verb key -> menu string mapping)
|
|
const static struct
|
|
{
|
|
LPCTSTR pszVerb;
|
|
UINT id;
|
|
} sVerbTrans[] = {
|
|
c_szOpen, IDS_MENUOPEN,
|
|
c_szExplore, IDS_MENUEXPLORE,
|
|
TEXT("edit"),IDS_MENUEDIT,
|
|
c_szFind, IDS_MENUFIND,
|
|
c_szPrint, IDS_MENUPRINT,
|
|
c_szOpenAs, IDS_MENUOPEN,
|
|
TEXT("runas"),IDS_MENURUNAS
|
|
};
|
|
|
|
for (int i = 0; i < ARRAYSIZE(sVerbTrans); i++)
|
|
{
|
|
if (lstrcmpi(pszVerbKey, sVerbTrans[i].pszVerb) == 0)
|
|
{
|
|
if (LoadString(HINST_THISDLL, sVerbTrans[i].id, pszMenuString, cchMax))
|
|
return TRUE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Worst case: Just put '&' on the top.
|
|
pszMenuString[0] = TEXT('&');
|
|
pszMenuString++;
|
|
cchMax--;
|
|
lstrcpyn(pszMenuString, pszVerbKey, cchMax);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
// Checks to see if there is a user policy in place that disables this key,
|
|
//
|
|
// For example, in the registry:
|
|
//
|
|
// CLSID_MyComputer
|
|
// +---Shell
|
|
// +---Manage
|
|
// (Default) = "Mana&ge"
|
|
// SuppressionPolicy = REST_NOMANAGEMYCOMPUTERVERB
|
|
//
|
|
// (Where REST_NOMANAGEMYCOMPUTERVERB is the DWORD value of that particular policy)
|
|
|
|
BOOL CShellExecMenu::_IsRestricted(UINT idVerb)
|
|
{
|
|
RESTRICTIONS rest;
|
|
BOOL fRestrict = FALSE;
|
|
if (0 == lstrcmpi(TEXT("runas"), _dka.ExposeName(idVerb)))
|
|
{
|
|
rest = REST_HIDERUNASVERB;
|
|
fRestrict = TRUE;
|
|
}
|
|
else
|
|
{
|
|
DWORD cb = sizeof(rest);
|
|
fRestrict = SUCCEEDED(_dka.GetValue(idVerb, NULL, TEXT("SuppressionPolicy"), NULL, &rest, &cb));
|
|
}
|
|
return fRestrict && SHRestricted(rest);
|
|
}
|
|
|
|
HRESULT _GetAppSource(HKEY hk, PCWSTR pszVerb, IQuerySource **ppqs)
|
|
{
|
|
CComPtr<IAssociationElement> spae;
|
|
HRESULT hr = AssocElemCreateForKey(&CLSID_AssocShellElement, hk, &spae);
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
CComPtr<IObjectWithQuerySource> spowqsApp;
|
|
hr = spae->QueryObject(AQVO_APPLICATION_DELEGATE, pszVerb, IID_PPV_ARG(IObjectWithQuerySource, &spowqsApp));
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
hr = spowqsApp->GetSource(IID_PPV_ARG(IQuerySource, ppqs));
|
|
}
|
|
}
|
|
return hr;
|
|
}
|
|
|
|
BOOL CShellExecMenu::_SupportsType(UINT idVerb)
|
|
{
|
|
BOOL fRet = TRUE;
|
|
if (SUCCEEDED(_dka.GetValue(idVerb, NULL, TEXT("CheckSupportedTypes"), NULL, NULL, NULL)))
|
|
{
|
|
// need to check the supported types for this application
|
|
// get the first item and then check it against SupportedFileExtensions
|
|
CComPtr<IShellItem> spsi;
|
|
if (SUCCEEDED(DataObj_GetIShellItem(_pdtobj, &spsi)))
|
|
{
|
|
SFGAOF sfgao;
|
|
if (S_OK == spsi->GetAttributes(SFGAO_STREAM, &sfgao))
|
|
{
|
|
CSmartCoTaskMem<OLECHAR> spszName;
|
|
if (SUCCEEDED(spsi->GetDisplayName(SIGDN_PARENTRELATIVEPARSING, &spszName)))
|
|
{
|
|
PWSTR pszExt = PathFindExtension(spszName);
|
|
if (*pszExt)
|
|
{
|
|
CComPtr<IQuerySource> spqs;
|
|
if (SUCCEEDED(_GetAppSource(_hkeyProgID, _dka.ExposeName(idVerb), &spqs)))
|
|
{
|
|
fRet = SUCCEEDED(spqs->QueryValueExists(L"SupportedTypes", pszExt));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return fRet;
|
|
}
|
|
|
|
//
|
|
// LegacyDisable
|
|
// LegacyDisable is set, then the verb exists only for legacy reasons, and
|
|
// is actually superceded by a context menu extension or some other behavior
|
|
// it there only to retain legacy behavior for external clients that require
|
|
// the existence of a verb.
|
|
//
|
|
BOOL CShellExecMenu::_RemoveVerb(UINT idVerb)
|
|
{
|
|
if (SUCCEEDED(_dka.GetValue(idVerb, NULL, TEXT("LegacyDisable"), NULL, NULL, NULL)))
|
|
return TRUE;
|
|
|
|
if (!_SupportsType(idVerb))
|
|
return TRUE;
|
|
|
|
return (_IsRestricted(idVerb));
|
|
}
|
|
|
|
BOOL CShellExecMenu::_IsVisible(BOOL fExtended, UINT idVerb)
|
|
{
|
|
// this is not an extended verb, or
|
|
// the request includes extended verbs
|
|
if (!fExtended && SUCCEEDED(_dka.GetValue(idVerb, NULL, TEXT("Extended"), NULL, NULL, NULL)))
|
|
return FALSE;
|
|
|
|
static const struct {
|
|
LPCTSTR pszVerb;
|
|
} sVerbIgnore[] = {
|
|
c_szPrintTo
|
|
};
|
|
|
|
for (int i = 0; i < ARRAYSIZE(sVerbIgnore); i++)
|
|
{
|
|
if (lstrcmpi(_dka.ExposeName(idVerb), sVerbIgnore[i].pszVerb) == 0)
|
|
{
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
BOOL CShellExecMenu::_GetMenuString(UINT id, BOOL fExtended, LPTSTR pszMenu, UINT cchMax)
|
|
{
|
|
BOOL bRet = FALSE;
|
|
// other verbs are hidden and just shouldnt be shown.
|
|
if (SUCCEEDED(_InsureVerbs(id)) && _IsVisible(fExtended, id))
|
|
{
|
|
DWORD cbVerb = CbFromCch(cchMax);
|
|
*pszMenu = 0;
|
|
// try the MUIVerb value first
|
|
// if that fails use the default value
|
|
// either of these can actually have an MUI string
|
|
if (FAILED(_dka.GetValue(id, NULL, TEXT("MUIVerb"), NULL, pszMenu, &cbVerb)))
|
|
{
|
|
cbVerb = CbFromCch(cchMax);
|
|
_dka.GetValue(id, NULL, NULL, NULL, pszMenu, &cbVerb);
|
|
}
|
|
|
|
if (!*pszMenu || FAILED(SHLoadIndirectString(pszMenu, pszMenu, cchMax, NULL)))
|
|
{
|
|
// If it does not have the value, generate it.
|
|
bRet = _MenuString(_dka.ExposeName(id), pszMenu, cchMax);
|
|
}
|
|
else
|
|
{
|
|
// use the value
|
|
bRet = TRUE;
|
|
}
|
|
ASSERT(!bRet || *pszMenu);
|
|
}
|
|
return bRet;
|
|
}
|
|
|
|
STDMETHODIMP CShellExecMenu::Initialize(LPCITEMIDLIST pidlFolder, IDataObject *pdtobj, HKEY hkeyProgID)
|
|
{
|
|
// new behavior: good context menus should interpret a NULL pidlFolder/hkeyProgID on a re-init
|
|
// as meaning they should use the ones they already have.
|
|
if (hkeyProgID)
|
|
{
|
|
_Cleanup(); // cleans up hkey and hdka, pdtobj too but that's ok
|
|
_hkeyProgID = SHRegDuplicateHKey(hkeyProgID); // make a copy
|
|
}
|
|
IUnknown_Set((IUnknown **)&_pdtobj, pdtobj);
|
|
return S_OK;
|
|
}
|
|
|
|
UINT CShellExecMenu::_VerbCount()
|
|
{
|
|
return SUCCEEDED(_InsureVerbs()) ? _dka.GetItemCount() : 0;
|
|
}
|
|
|
|
UINT CShellExecMenu::_FindIndex(LPCTSTR pszVerb)
|
|
{
|
|
for (UINT i = 0; i < _VerbCount(); i++)
|
|
{
|
|
if (!lstrcmpi(pszVerb, _dka.ExposeName(i)))
|
|
return i; // found it!
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
STDMETHODIMP CShellExecMenu::QueryContextMenu(HMENU hmenu, UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags)
|
|
{
|
|
UINT cVerbs = 0;
|
|
_uFlags = uFlags; // caller may force explorer mode (CMF_EXPLORE) here
|
|
|
|
TCHAR szMenu[CCH_MENUMAX];
|
|
for (UINT idCmd = idCmdFirst;
|
|
idCmd <= idCmdLast && (idCmd - idCmdFirst) < _VerbCount(); idCmd++)
|
|
{
|
|
UINT uMenuFlags = MF_BYPOSITION | MF_STRING;
|
|
if (_GetMenuString(idCmd - idCmdFirst, uFlags & CMF_EXTENDEDVERBS, szMenu, ARRAYSIZE(szMenu)))
|
|
{
|
|
InsertMenu(hmenu, indexMenu + cVerbs, uMenuFlags, idCmd, szMenu);
|
|
cVerbs++;
|
|
}
|
|
}
|
|
|
|
if (cVerbs && (GetMenuDefaultItem(hmenu, MF_BYPOSITION, 0) == -1))
|
|
{
|
|
if (_dka.HasDefault(_hkeyProgID))
|
|
{
|
|
// if there is a default verb on this key,
|
|
// trust that it was the first one that the CDKA added
|
|
SetMenuDefaultItem(hmenu, indexMenu, MF_BYPOSITION);
|
|
}
|
|
}
|
|
|
|
return ResultFromShort(_VerbCount());
|
|
}
|
|
|
|
LPCTSTR CShellExecMenu::_GetVerb(UINT id)
|
|
{
|
|
return SUCCEEDED(_InsureVerbs()) ? _dka.ExposeName(id) : NULL;
|
|
}
|
|
|
|
STATIC BOOL s_fAbortInvoke = FALSE;
|
|
|
|
// This private export allows the folder code a way to cause the main invoke
|
|
// loops processing several different files to abort.
|
|
|
|
STDAPI_(void) SHAbortInvokeCommand()
|
|
{
|
|
DebugMsg(DM_TRACE, TEXT("AbortInvokeCommand was called"));
|
|
s_fAbortInvoke = TRUE;
|
|
}
|
|
|
|
// Call shell exec (for the folder class) using the given file and the
|
|
// given pidl. The file will be passed as %1 in the dde command and the pidl
|
|
// will be passed as %2.
|
|
|
|
STDAPI _InvokePidl(LPCMINVOKECOMMANDINFOEX pici, DWORD dwAttribs, LPCTSTR pszPath, LPCITEMIDLIST pidl, HKEY hkClass)
|
|
{
|
|
SHELLEXECUTEINFO ei;
|
|
HRESULT hr = ICIX2SEI(pici, &ei);
|
|
pszPath = (dwAttribs & SFGAO_FILESYSTEM) ? pszPath : NULL;
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
ei.fMask |= SEE_MASK_IDLIST;
|
|
|
|
ei.lpFile = pszPath;
|
|
ei.lpIDList = (void *)pidl;
|
|
|
|
// if a directory is specifed use that, else make the current
|
|
// directory be the folder it self. UNLESS it is a AUDIO CDRom, it
|
|
// should never be the current directory (causes CreateProcess errors)
|
|
if (!ei.lpDirectory && (dwAttribs & SFGAO_FOLDER))
|
|
ei.lpDirectory = pszPath;
|
|
|
|
if (pszPath && ei.lpDirectory)
|
|
{
|
|
INT iDrive = PathGetDriveNumber(ei.lpDirectory);
|
|
|
|
CMountPoint* pmtpt = CMountPoint::GetMountPoint(iDrive);
|
|
|
|
if (pmtpt)
|
|
{
|
|
if (pmtpt->IsAudioCDNoData())
|
|
{
|
|
ei.lpDirectory = NULL;
|
|
}
|
|
|
|
pmtpt->Release();
|
|
}
|
|
}
|
|
|
|
if (hkClass)
|
|
{
|
|
ei.hkeyClass = hkClass;
|
|
ei.fMask |= SEE_MASK_CLASSKEY;
|
|
}
|
|
else
|
|
ei.fMask |= SEE_MASK_INVOKEIDLIST;
|
|
|
|
if (ShellExecuteEx(&ei))
|
|
hr = S_OK;
|
|
else
|
|
hr = HRESULT_FROM_WIN32(GetLastError());
|
|
}
|
|
return hr;
|
|
}
|
|
|
|
BOOL _QuitInvokeLoop()
|
|
{
|
|
MSG msg;
|
|
|
|
// Try to give the user a way to escape out of this
|
|
if (s_fAbortInvoke || GetAsyncKeyState(VK_ESCAPE) < 0)
|
|
return TRUE;
|
|
|
|
// And the next big mondo hack to handle CAD of our window
|
|
// because the user thinks it is hung.
|
|
if (PeekMessage(&msg, NULL, WM_CLOSE, WM_CLOSE, PM_NOREMOVE))
|
|
return TRUE; // Lets also bail..
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
#define CMINVOKE_VERBT(pici) (pici)->lpVerbW
|
|
|
|
HRESULT CShellExecMenu::_MapVerbForInvoke(CMINVOKECOMMANDINFOEX *pici, UINT *pidVerb)
|
|
{
|
|
LPCTSTR pszVerbKey;
|
|
// is pici->lpVerb specifying the verb index (0-based).
|
|
if (IS_INTRESOURCE(pici->lpVerb))
|
|
{
|
|
// find it in the CDKA
|
|
*pidVerb = LOWORD((ULONG_PTR)pici->lpVerb);
|
|
pszVerbKey = _GetVerb(*pidVerb);
|
|
CMINVOKE_VERBT(pici) = pszVerbKey; // alias into the CDKA
|
|
RIPMSG(pszVerbKey != NULL, "CShellExecMenu::InvokeCommand() passed an invalid verb id");
|
|
}
|
|
else
|
|
{
|
|
pszVerbKey = CMINVOKE_VERBT(pici);
|
|
if (pszVerbKey)
|
|
{
|
|
*pidVerb = _FindIndex(pszVerbKey);
|
|
if (-1 == *pidVerb)
|
|
pszVerbKey = NULL; // not in our list
|
|
}
|
|
}
|
|
|
|
ASSERT(!pszVerbKey || *pidVerb != -1);
|
|
return pszVerbKey ? S_OK : E_INVALIDARG;
|
|
}
|
|
|
|
BOOL CShellExecMenu::_IsExplorerMode()
|
|
{
|
|
BOOL bRet = (_uFlags & CMF_EXPLORE);
|
|
if (!bRet)
|
|
{
|
|
bRet = IsExplorerModeBrowser(_punkSite);
|
|
if (bRet)
|
|
_uFlags |= CMF_EXPLORE;
|
|
}
|
|
return bRet;
|
|
}
|
|
|
|
DWORD CShellExecMenu::_BrowseFlagsFromVerb(UINT idVerb)
|
|
{
|
|
DWORD dwFlags = 0;
|
|
DWORD cbFlags = sizeof(dwFlags);
|
|
_dka.GetValue(idVerb, NULL, _IsExplorerMode() ? TEXT("ExplorerFlags") : TEXT("BrowserFlags"), NULL, &dwFlags, &cbFlags);
|
|
return dwFlags;
|
|
}
|
|
|
|
HRESULT CShellExecMenu::_TryBrowseObject(LPCITEMIDLIST pidl, DWORD uFlags)
|
|
{
|
|
HRESULT hr = S_FALSE;
|
|
|
|
IShellBrowser *psb;
|
|
if (SUCCEEDED(IUnknown_QueryService(_punkSite, SID_SShellBrowser, IID_PPV_ARG(IShellBrowser, &psb))))
|
|
{
|
|
hr = psb->BrowseObject(pidl, (UINT) uFlags);
|
|
psb->Release();
|
|
}
|
|
return hr;
|
|
}
|
|
|
|
HRESULT _CanTryBrowseObject(DWORD dwAttribs, CMINVOKECOMMANDINFOEX* pici)
|
|
{
|
|
HRESULT hr = S_FALSE;
|
|
|
|
if (dwAttribs & SFGAO_FOLDER)
|
|
{
|
|
// we need to sniff the iciex here to see if there is anything special in it
|
|
// that cannot be conveyed to IShellBrowser::BrowseObject() (eg the nShow parameter)
|
|
if ((pici->nShow == SW_SHOWNORMAL) ||
|
|
(pici->nShow == SW_SHOW))
|
|
{
|
|
// nothing special in the ICIEX, should be safe to discard it and use
|
|
// IShellBrowser::BrowseObject() instead of ShellExecuteEx
|
|
hr = S_OK;
|
|
}
|
|
}
|
|
|
|
return hr;
|
|
}
|
|
|
|
BOOL CShellExecMenu::_VerbCanDrop(UINT idVerb, CLSID *pclsid)
|
|
{
|
|
TCHAR sz[GUIDSTR_MAX];
|
|
DWORD cb = sizeof(sz);
|
|
return (SUCCEEDED(_dka.GetValue(idVerb, L"DropTarget", L"Clsid", NULL, sz, &cb))
|
|
&& GUIDFromString(sz, pclsid));
|
|
}
|
|
|
|
HRESULT CShellExecMenu::_DoDrop(REFCLSID clsid, UINT idVerb, LPCMINVOKECOMMANDINFOEX pici)
|
|
{
|
|
// i think i need to persist the pici into the _pdtobj
|
|
// and probably add some values under the pqs
|
|
// we assume that the app will do something appropriate
|
|
// QueryService(_punkSite, clsid) might be useful
|
|
return SHSimulateDropOnClsid(clsid, _punkSite, _pdtobj);
|
|
}
|
|
|
|
STDMETHODIMP CShellExecMenu::InvokeCommand(LPCMINVOKECOMMANDINFO pici)
|
|
{
|
|
CMINVOKECOMMANDINFOEX ici;
|
|
void *pvFree;
|
|
HRESULT hr = ICI2ICIX(pici, &ici, &pvFree); // thunk incomming params
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
UINT idVerb;
|
|
hr = _MapVerbForInvoke(&ici, &idVerb);
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
CLSID clsid;
|
|
if (_VerbCanDrop(idVerb, &clsid))
|
|
{
|
|
hr = _DoDrop(clsid, idVerb, &ici);
|
|
}
|
|
else
|
|
{
|
|
STGMEDIUM medium;
|
|
LPIDA pida = DataObj_GetHIDA(_pdtobj, &medium);
|
|
if (pida)
|
|
{
|
|
if (pida->cidl == 1)
|
|
{
|
|
LPITEMIDLIST pidl = IDA_FullIDList(pida, 0);
|
|
if (pidl)
|
|
{
|
|
hr = _InvokeOne(&ici, idVerb, pidl);
|
|
ILFree(pidl);
|
|
}
|
|
else
|
|
hr = E_OUTOFMEMORY;
|
|
}
|
|
else
|
|
{
|
|
hr = _InvokeMany(&ici, idVerb, pida);
|
|
}
|
|
|
|
HIDA_ReleaseStgMedium(pida, &medium);
|
|
}
|
|
else
|
|
hr = E_OUTOFMEMORY;
|
|
}
|
|
}
|
|
|
|
if (pvFree)
|
|
LocalFree(pvFree);
|
|
}
|
|
|
|
return hr;
|
|
}
|
|
|
|
HRESULT CShellExecMenu::_InvokeOne(CMINVOKECOMMANDINFOEX *pici, UINT idVerb, LPCITEMIDLIST pidl)
|
|
{
|
|
HRESULT hr = S_FALSE;
|
|
TCHAR szPath[MAX_PATH];
|
|
DWORD dwAttrib = SFGAO_FILESYSTEM | SFGAO_FOLDER | SFGAO_LINK;
|
|
SHGetNameAndFlags(pidl, SHGDN_FORPARSING, szPath, ARRAYSIZE(szPath), &dwAttrib);
|
|
|
|
if (S_OK == _CanTryBrowseObject(dwAttrib, pici))
|
|
{
|
|
DWORD uFlags = _BrowseFlagsFromVerb(idVerb);
|
|
if (uFlags)
|
|
{
|
|
// if we did the site based navigation, we are done
|
|
hr = _TryBrowseObject(pidl, uFlags);
|
|
}
|
|
}
|
|
|
|
if (hr != S_OK)
|
|
{
|
|
hr = _InvokePidl(pici, dwAttrib, szPath, pidl, _hkeyProgID);
|
|
|
|
// only set recent on non-folders (SFGAO_STREAM?)
|
|
// and non-link since we know those should never be added
|
|
if (SUCCEEDED(hr) && !(dwAttrib & (SFGAO_FOLDER | SFGAO_LINK)))
|
|
{
|
|
AddToRecentDocs(pidl, szPath);
|
|
}
|
|
}
|
|
|
|
return hr;
|
|
}
|
|
|
|
BOOL _ShouldPrompt(DWORD cItems)
|
|
{
|
|
DWORD dwMin, cb = sizeof(dwMin);
|
|
if (SHRegGetUSValue(REGSTR_PATH_EXPLORER, TEXT("MultipleInvokePromptMinimum"), NULL, &dwMin, &cb, FALSE, NULL, 0) != ERROR_SUCCESS)
|
|
dwMin = 15;
|
|
|
|
return cItems > dwMin;
|
|
}
|
|
|
|
HRESULT CShellExecMenu::_PromptUser(CMINVOKECOMMANDINFOEX *pici, UINT idVerb, LPIDA pida)
|
|
{
|
|
HRESULT hr = S_FALSE;
|
|
if (pici->hwnd && !(pici->fMask & CMIC_MASK_FLAG_NO_UI)
|
|
&& _ShouldPrompt(pida->cidl))
|
|
{
|
|
// prompt the user with the verb and count
|
|
// we make a better experience if we keyed off
|
|
// homo/hetero types and had different behaviors
|
|
// but its not worth it. instead we should
|
|
// switch to using AutoPlay sniffing and dialog.
|
|
TCHAR szVerb[64];
|
|
TCHAR szNum[10];
|
|
wnsprintf(szNum, ARRAYSIZE(szNum), TEXT("%d"), pida->cidl);
|
|
hr = _GetHelpText(idVerb, GCS_HELPTEXT, (PSTR)szVerb, ARRAYSIZE(szVerb));
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
hr = E_OUTOFMEMORY;
|
|
PTSTR pszTitle = ShellConstructMessageString(HINST_THISDLL, MAKEINTRESOURCE(IDS_MULTIINVOKEPROMPT_TITLE), szVerb);
|
|
if (pszTitle)
|
|
{
|
|
PTSTR pszMsg = ShellConstructMessageString(HINST_THISDLL, MAKEINTRESOURCE(IDS_MULTIINVOKEPROMPT_MESSAGE), szVerb, szNum);
|
|
if (pszMsg)
|
|
{
|
|
int iRet = SHMessageBoxCheck(pici->hwnd, pszMsg, pszTitle, (MB_OKCANCEL | MB_ICONEXCLAMATION), IDOK, TEXT("MultipleInvokePrompt"));
|
|
hr = iRet == IDOK ? S_OK : HRESULT_FROM_WIN32(ERROR_CANCELLED);
|
|
|
|
LocalFree(pszMsg);
|
|
}
|
|
LocalFree(pszTitle);
|
|
}
|
|
}
|
|
}
|
|
return hr;
|
|
}
|
|
|
|
HRESULT CShellExecMenu::_InvokeEach(LPCITEMIDLIST pidl, CMINVOKECOMMANDINFOEX *pici)
|
|
{
|
|
HRESULT hr = E_OUTOFMEMORY;
|
|
HMENU hmenu = CreatePopupMenu();
|
|
if (hmenu)
|
|
{
|
|
CComPtr<IContextMenu> spcm;
|
|
hr = SHGetUIObjectOf(pidl, NULL, IID_PPV_ARG(IContextMenu, &spcm));
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
if (_punkSite)
|
|
IUnknown_SetSite(spcm, _punkSite);
|
|
|
|
hr = spcm->QueryContextMenu(hmenu, 0, CONTEXTMENU_IDCMD_FIRST, CONTEXTMENU_IDCMD_LAST, _uFlags);
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
hr = spcm->InvokeCommand((CMINVOKECOMMANDINFO *)pici);
|
|
}
|
|
|
|
if (_punkSite)
|
|
IUnknown_SetSite(spcm, NULL);
|
|
}
|
|
|
|
DestroyMenu(hmenu);
|
|
}
|
|
return hr;
|
|
}
|
|
|
|
HRESULT CShellExecMenu::_InvokeMany(CMINVOKECOMMANDINFOEX *pici, UINT idVerb, LPIDA pida)
|
|
{
|
|
HRESULT hr = _PromptUser(pici, idVerb, pida);
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
USES_CONVERSION;
|
|
s_fAbortInvoke = FALSE; // reset this global for this run...
|
|
// we want to alter the pici
|
|
// so that each item is handled individually
|
|
pici->hwnd = NULL;
|
|
pici->fMask |= CMIC_MASK_FLAG_NO_UI;
|
|
|
|
// NTBUG #502223 - MSI apps with DDE start multiple copies - ZekeL 2001-DEC-07
|
|
// ShellExec() will create a new thread for MSI apps to
|
|
// avoid a deadlock with the MSI APIs calling SHChangeNotify().
|
|
// this is described in NTBUG #200961
|
|
// however in the multiple invoke case we create one thread
|
|
// for each item in the invoke, which results in several processes
|
|
// contending for the DDE conversation.
|
|
//
|
|
// this is a half fix. we prefer to have the buggy behavior in 502223
|
|
// over the deadlock behavior in 200961 (a definite PSS call).
|
|
// since the deadlock case should only occur for the desktop,
|
|
// the rest of the time we will force a synchronous invoke.
|
|
IBindCtx *pbcRelease = NULL;
|
|
if (!IsDesktopBrowser(_punkSite))
|
|
{
|
|
TBCRegisterObjectParam(TBCDIDASYNC, SAFECAST(this, IContextMenu *), &pbcRelease);
|
|
}
|
|
|
|
pici->lpVerb = T2A(_dka.ExposeName(idVerb));
|
|
pici->lpVerbW = _dka.ExposeName(idVerb);
|
|
|
|
for (UINT iItem = 0; !_QuitInvokeLoop() && (iItem < pida->cidl); iItem++)
|
|
{
|
|
LPITEMIDLIST pidl = IDA_FullIDList(pida, iItem);
|
|
if (pidl)
|
|
{
|
|
hr = _InvokeEach(pidl, pici);
|
|
ILFree(pidl);
|
|
}
|
|
else
|
|
hr = E_OUTOFMEMORY;
|
|
|
|
if (hr == E_OUTOFMEMORY)
|
|
break;
|
|
|
|
}
|
|
|
|
ATOMICRELEASE(pbcRelease);
|
|
}
|
|
return hr;
|
|
}
|
|
|
|
HRESULT CShellExecMenu::_GetHelpText(UINT idVerb, UINT uType, LPSTR pszName, UINT cchMax)
|
|
{
|
|
// TODO - shouldnt we let the registry override?
|
|
HRESULT hr = E_OUTOFMEMORY;
|
|
TCHAR szMenuString[CCH_MENUMAX];
|
|
if (_GetMenuString(idVerb, TRUE, szMenuString, ARRAYSIZE(szMenuString)))
|
|
{
|
|
SHStripMneumonic(szMenuString);
|
|
// NOTE on US, IDS_VERBHELP is the same as "%s"
|
|
// do we want some better description?
|
|
LPTSTR pszHelp = ShellConstructMessageString(HINST_THISDLL, MAKEINTRESOURCE(IDS_VERBHELP), szMenuString);
|
|
if (pszHelp)
|
|
{
|
|
if (uType == GCS_HELPTEXTA)
|
|
SHTCharToAnsi(pszHelp, pszName, cchMax);
|
|
else
|
|
SHTCharToUnicode(pszHelp, (LPWSTR)pszName, cchMax);
|
|
LocalFree(pszHelp);
|
|
hr = S_OK;
|
|
}
|
|
}
|
|
return hr;
|
|
}
|
|
|
|
HRESULT CShellExecMenu::_MapVerbForGCS(UINT_PTR idCmd, UINT uType, UINT *pidVerb)
|
|
{
|
|
HRESULT hr = _InsureVerbs();
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
if (IS_INTRESOURCE(idCmd))
|
|
*pidVerb = (UINT)idCmd;
|
|
else
|
|
{
|
|
*pidVerb = -1;
|
|
if (!(uType & GCS_UNICODE))
|
|
{
|
|
USES_CONVERSION;
|
|
*pidVerb = _FindIndex(A2W((LPCSTR)idCmd));
|
|
}
|
|
|
|
// we fall back to the TCHAR version regardless
|
|
// of what the caller passed in uType
|
|
if (*pidVerb == -1)
|
|
{
|
|
if (!IsBadStringPtrW((LPCWSTR)idCmd, (UINT)-1))
|
|
*pidVerb = _FindIndex((LPCWSTR)idCmd);
|
|
}
|
|
}
|
|
hr = *pidVerb < _VerbCount() ? S_OK : E_INVALIDARG;
|
|
}
|
|
|
|
// VALIDATE returns S_FALSE for bad verbs
|
|
if (FAILED(hr) && (uType == GCS_VALIDATEA || uType == GCS_VALIDATEW))
|
|
hr = S_FALSE;
|
|
|
|
return hr;
|
|
}
|
|
|
|
STDMETHODIMP CShellExecMenu::GetCommandString(UINT_PTR idCmd, UINT uType, UINT *pwRes, LPSTR pszName, UINT cchMax)
|
|
{
|
|
UINT idVerb;
|
|
HRESULT hr = _MapVerbForGCS(idCmd, uType, &idVerb);
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
// the verb is good!
|
|
switch (uType)
|
|
{
|
|
case GCS_HELPTEXTA:
|
|
case GCS_HELPTEXTW:
|
|
hr = _GetHelpText(idVerb, uType, pszName, cchMax);
|
|
break;
|
|
|
|
case GCS_VERBA:
|
|
case GCS_VERBW:
|
|
{
|
|
if (uType == GCS_VERBA)
|
|
SHTCharToAnsi(_dka.ExposeName(idVerb), pszName, cchMax);
|
|
else
|
|
SHTCharToUnicode(_dka.ExposeName(idVerb), (LPWSTR)pszName, cchMax);
|
|
hr = S_OK;
|
|
}
|
|
break;
|
|
|
|
case GCS_VALIDATEA:
|
|
case GCS_VALIDATEW:
|
|
// the hr from MapVerb is good enough
|
|
break;
|
|
|
|
default:
|
|
hr = E_NOTIMPL;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return hr;
|
|
}
|
|
|
|
STDMETHODIMP CShellExecMenu::AddPages(LPFNADDPROPSHEETPAGE pfnAddPage, LPARAM lParam)
|
|
{
|
|
return _pfnAddPages(_pdtobj, pfnAddPage, lParam);
|
|
}
|
|
|
|
STDMETHODIMP CShellExecMenu::ReplacePage(UINT uPageID, LPFNADDPROPSHEETPAGE pfnReplaceWith, LPARAM lParam)
|
|
{
|
|
return E_NOTIMPL;
|
|
}
|
|
|
|
STDAPI CShellExecMenu_CreateInstance(LPFNADDPAGES pfnAddPages, REFIID riid, void **ppv)
|
|
{
|
|
HRESULT hr;
|
|
CShellExecMenu *pdext = new CShellExecMenu(pfnAddPages);
|
|
if (pdext)
|
|
{
|
|
hr = pdext->QueryInterface(riid, ppv);
|
|
pdext->Release();
|
|
}
|
|
else
|
|
{
|
|
*ppv = NULL;
|
|
hr = E_OUTOFMEMORY;
|
|
}
|
|
return hr;
|
|
}
|
|
|
|
// these handlers slime off of CShellExecMenu's IShellPropSheetExt implementation
|
|
|
|
STDAPI FileSystem_AddPages(IDataObject *pdtobj, LPFNADDPROPSHEETPAGE pfnAddPage, LPARAM lParam);
|
|
|
|
STDAPI CShellFileDefExt_CreateInstance(IUnknown *punkOuter, REFIID riid, void **ppv)
|
|
{
|
|
return CShellExecMenu_CreateInstance(FileSystem_AddPages, riid, ppv);
|
|
}
|
|
|
|
STDAPI CDrives_AddPages(IDataObject *pdtobj, LPFNADDPROPSHEETPAGE pfnAddPage, LPARAM lParam);
|
|
|
|
STDAPI CShellDrvDefExt_CreateInstance(IUnknown *punkOuter, REFIID riid, void **ppv)
|
|
{
|
|
return CShellExecMenu_CreateInstance(CDrives_AddPages, riid, ppv);
|
|
}
|
|
|
|
STDAPI PIF_AddPages(IDataObject *pdtobj, LPFNADDPROPSHEETPAGE pfnAddPage, LPARAM lParam);
|
|
|
|
STDAPI CProxyPage_CreateInstance(IUnknown *punkOuter, REFIID riid, void **ppv)
|
|
{
|
|
return CShellExecMenu_CreateInstance(PIF_AddPages, riid, ppv);
|
|
}
|