1073 lines
32 KiB
C++
1073 lines
32 KiB
C++
#include "shellprv.h"
|
|
#include "fsmenu.h"
|
|
#include "ids.h"
|
|
#include <limits.h>
|
|
#include "filetbl.h"
|
|
#include <oleacc.h> // MSAAMENUINFO stuff
|
|
|
|
#define CXIMAGEGAP 6
|
|
|
|
typedef enum
|
|
{
|
|
FMII_DEFAULT = 0x0000,
|
|
FMII_BREAK = 0x0001
|
|
} FMIIFLAGS;
|
|
|
|
#define FMI_MARKER 0x00000001
|
|
#define FMI_EXPAND 0x00000004
|
|
#define FMI_EMPTY 0x00000008
|
|
#define FMI_ON_MENU 0x00000040
|
|
|
|
// One of these per file menu.
|
|
typedef struct
|
|
{
|
|
HMENU hmenu; // Menu.
|
|
HDPA hdpa; // List of items (see below).
|
|
const struct _FILEMENUITEM *pfmiLastSel;
|
|
UINT idCmd; // Command.
|
|
UINT grfFlags; // enum filter
|
|
DWORD dwMask; // FMC_ flags
|
|
PFNFMCALLBACK pfnCallback; // Callback function.
|
|
LPARAM lParam; // Parameter passed for callback handler
|
|
int cyMenuSizeSinceLastBreak; // Size of menu (cy)
|
|
} FILEMENUHEADER;
|
|
|
|
// One of these for each file menu item.
|
|
//
|
|
// !!! Note: the testers have a test utility which grabs
|
|
// the first 7 fields of this structure. If you change
|
|
// the order or meaning of these fields, make sure they
|
|
// are notified so they can update their automated tests.
|
|
//
|
|
typedef struct _FILEMENUITEM
|
|
{
|
|
MSAAMENUINFO msaa; // accessibility must be first.
|
|
FILEMENUHEADER *pfmh; // The header.
|
|
IShellFolder *psf; // Shell Folder.
|
|
LPITEMIDLIST pidl; // IDlist for item.
|
|
int iImage; // Image index to use.
|
|
DWORD dwFlags; // Misc flags above.
|
|
DWORD dwAttributes; // GetAttributesOf(), SFGAO_ bits (only some)
|
|
LPTSTR psz; // Text when not using pidls.
|
|
LPARAM lParam; // Application data
|
|
} FILEMENUITEM;
|
|
|
|
#if defined(DEBUG)
|
|
|
|
BOOL IsValidPFILEMENUHEADER(FILEMENUHEADER *pfmh)
|
|
{
|
|
return (IS_VALID_WRITE_PTR(pfmh, FILEMENUHEADER) &&
|
|
IS_VALID_HANDLE(pfmh->hmenu, MENU) &&
|
|
IS_VALID_HANDLE(pfmh->hdpa, DPA));
|
|
}
|
|
|
|
BOOL IsValidPFILEMENUITEM(FILEMENUITEM *pfmi)
|
|
{
|
|
return (IS_VALID_WRITE_PTR(pfmi, FILEMENUITEM) &&
|
|
IS_VALID_STRUCT_PTR(pfmi->pfmh, FILEMENUHEADER) &&
|
|
(NULL == pfmi->pidl || IS_VALID_PIDL(pfmi->pidl)) &&
|
|
(NULL == pfmi->psz || IS_VALID_STRING_PTR(pfmi->psz, -1)));
|
|
}
|
|
#endif
|
|
|
|
|
|
DWORD GetItemTextExtent(HDC hdc, LPCTSTR lpsz)
|
|
{
|
|
SIZE sz;
|
|
|
|
GetTextExtentPoint(hdc, lpsz, lstrlen(lpsz), &sz);
|
|
// NB This is OK as long as an item's extend doesn't get very big.
|
|
return MAKELONG((WORD)sz.cx, (WORD)sz.cy);
|
|
}
|
|
|
|
void FileMenuItem_GetDisplayName(FILEMENUITEM *pfmi, LPTSTR pszName, UINT cchName)
|
|
{
|
|
ASSERT(IS_VALID_STRUCT_PTR(pfmi, FILEMENUITEM));
|
|
|
|
// Is this a special empty item?
|
|
if (pfmi->dwFlags & FMI_EMPTY)
|
|
{
|
|
// Yep, load the string from a resource.
|
|
LoadString(HINST_THISDLL, IDS_NONE, pszName, cchName);
|
|
}
|
|
else
|
|
{
|
|
*pszName = 0;
|
|
// If it's got a pidl use that, else just use the normal menu string.
|
|
if (pfmi->psz)
|
|
{
|
|
lstrcpyn(pszName, pfmi->psz, cchName);
|
|
}
|
|
else if (pfmi->pidl && pfmi->psf && SUCCEEDED(DisplayNameOf(pfmi->psf, pfmi->pidl, SHGDN_NORMAL, pszName, cchName)))
|
|
{
|
|
pfmi->psz = StrDup(pszName);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create a menu item structure to be stored in the hdpa
|
|
|
|
BOOL FileMenuItem_Create(FILEMENUHEADER *pfmh, IShellFolder *psf, LPCITEMIDLIST pidl, DWORD dwFlags, FILEMENUITEM **ppfmi)
|
|
{
|
|
FILEMENUITEM *pfmi = (FILEMENUITEM *)LocalAlloc(LPTR, sizeof(*pfmi));
|
|
if (pfmi)
|
|
{
|
|
pfmi->pfmh = pfmh;
|
|
pfmi->iImage = -1;
|
|
pfmi->dwFlags = dwFlags;
|
|
pfmi->pidl = pidl ? ILClone(pidl) : NULL;
|
|
pfmi->psf = psf;
|
|
if (pfmi->psf)
|
|
pfmi->psf->AddRef();
|
|
|
|
if (pfmi->psf && pfmi->pidl)
|
|
{
|
|
pfmi->dwAttributes = SFGAO_FOLDER;
|
|
pfmi->psf->GetAttributesOf(1, &pidl, &pfmi->dwAttributes);
|
|
}
|
|
|
|
// fill in msaa stuff
|
|
pfmi->msaa.dwMSAASignature = MSAA_MENU_SIG;
|
|
|
|
// prep the pfmi->psz cached displayname
|
|
WCHAR sz[MAX_PATH];
|
|
FileMenuItem_GetDisplayName(pfmi, sz, ARRAYSIZE(sz));
|
|
|
|
// just use the same string ref, so we dont dupe the allocation.
|
|
pfmi->msaa.pszWText = pfmi->psz;
|
|
pfmi->msaa.cchWText = pfmi->msaa.pszWText ? lstrlenW(pfmi->msaa.pszWText) : 0;
|
|
}
|
|
|
|
*ppfmi = pfmi;
|
|
|
|
return (NULL != pfmi);
|
|
}
|
|
|
|
BOOL FileMenuItem_Destroy(FILEMENUITEM *pfmi)
|
|
{
|
|
BOOL fRet = FALSE;
|
|
if (pfmi)
|
|
{
|
|
ILFree(pfmi->pidl);
|
|
LocalFree(pfmi->psz);
|
|
ATOMICRELEASE(pfmi->psf);
|
|
LocalFree(pfmi);
|
|
fRet = TRUE;
|
|
}
|
|
return fRet;
|
|
}
|
|
|
|
// Enumerates the folder and adds the files to the DPA.
|
|
// Returns: count of items in the list
|
|
int FileList_Build(FILEMENUHEADER *pfmh, IShellFolder *psf, int cItems)
|
|
{
|
|
ASSERT(IS_VALID_STRUCT_PTR(pfmh, FILEMENUHEADER));
|
|
|
|
if (pfmh->hdpa)
|
|
{
|
|
// special case the single empty item, and remove it.
|
|
// this is because we expect to get called multiple times in FileList_Build on a single menu.
|
|
if ((1 == cItems) && (1 == DPA_GetPtrCount(pfmh->hdpa)))
|
|
{
|
|
FILEMENUITEM *pfmiEmpty = (FILEMENUITEM*)DPA_GetPtr(pfmh->hdpa, 0);
|
|
if (pfmiEmpty->dwFlags & FMI_EMPTY)
|
|
{
|
|
DeleteMenu(pfmh->hmenu, 0, MF_BYPOSITION);
|
|
FileMenuItem_Destroy(pfmiEmpty);
|
|
DPA_DeletePtr(pfmh->hdpa, 0);
|
|
|
|
cItems = 0;
|
|
}
|
|
}
|
|
|
|
// We now need to iterate over the children under this guy...
|
|
IEnumIDList *penum;
|
|
if (S_OK == psf->EnumObjects(NULL, pfmh->grfFlags, &penum))
|
|
{
|
|
LPITEMIDLIST pidl;
|
|
while (S_OK == penum->Next(1, &pidl, NULL))
|
|
{
|
|
FILEMENUITEM *pfmi;
|
|
|
|
if (FileMenuItem_Create(pfmh, psf, pidl, 0, &pfmi))
|
|
{
|
|
int idpa = DPA_AppendPtr(pfmh->hdpa, pfmi);
|
|
ASSERTMSG(idpa != -1, "DPA_AppendPtr failed when adding file menu item");
|
|
|
|
if (idpa != -1)
|
|
{
|
|
// if the caller returns S_FALSE then we will remove the item from the
|
|
// menu, otherwise we behave as before.
|
|
if (pfmh->pfnCallback(FMM_ADD, pfmh->lParam, psf, pidl) == S_FALSE)
|
|
{
|
|
FileMenuItem_Destroy(pfmi);
|
|
DPA_DeletePtr(pfmh->hdpa, idpa);
|
|
}
|
|
else
|
|
{
|
|
cItems++;
|
|
}
|
|
}
|
|
}
|
|
ILFree(pidl);
|
|
}
|
|
penum->Release();
|
|
}
|
|
}
|
|
|
|
// Insert a special Empty item
|
|
if (!cItems && pfmh->hdpa)
|
|
{
|
|
FILEMENUITEM *pfmi;
|
|
|
|
if (FileMenuItem_Create(pfmh, NULL, NULL, FMI_EMPTY, &pfmi))
|
|
{
|
|
DPA_SetPtr(pfmh->hdpa, cItems, pfmi);
|
|
cItems++;
|
|
}
|
|
}
|
|
return cItems;
|
|
}
|
|
|
|
// Use the text extent of the given item and the size of the image to work
|
|
// what the full extent of the item will be.
|
|
DWORD GetItemExtent(HDC hdc, FILEMENUITEM *pfmi)
|
|
{
|
|
TCHAR szName[MAX_PATH];
|
|
|
|
szName[0] = 0;
|
|
|
|
ASSERT(IS_VALID_STRUCT_PTR(pfmi, FILEMENUITEM));
|
|
|
|
FileMenuItem_GetDisplayName(pfmi, szName, ARRAYSIZE(szName));
|
|
|
|
FILEMENUHEADER *pfmh = pfmi->pfmh;
|
|
ASSERT(pfmh);
|
|
|
|
DWORD dwExtent = GetItemTextExtent(hdc, szName);
|
|
|
|
UINT uHeight = HIWORD(dwExtent);
|
|
|
|
// If no custom height - calc it.
|
|
uHeight = max(uHeight, ((WORD)g_cySmIcon)) + 6;
|
|
|
|
ASSERT(pfmi->pfmh);
|
|
|
|
// string, image, gap on either side of image, popup triangle
|
|
// and background bitmap if there is one.
|
|
// FEATURE: popup triangle size needs to be real
|
|
UINT uWidth = LOWORD(dwExtent) + GetSystemMetrics(SM_CXMENUCHECK);
|
|
|
|
// Space for image if there is one.
|
|
// NB We currently always allow room for the image even if there
|
|
// isn't one so that imageless items line up properly.
|
|
uWidth += g_cxSmIcon + (2 * CXIMAGEGAP);
|
|
|
|
return MAKELONG(uWidth, uHeight);
|
|
}
|
|
|
|
|
|
// Get the FILEMENUITEM *of this menu item
|
|
FILEMENUITEM *FileMenu_GetItemData(HMENU hmenu, UINT iItem, BOOL bByPos)
|
|
{
|
|
MENUITEMINFO mii = {0};
|
|
|
|
mii.cbSize = sizeof(MENUITEMINFO);
|
|
mii.fMask = MIIM_DATA | MIIM_STATE;
|
|
|
|
return GetMenuItemInfo(hmenu, iItem, bByPos, &mii) ? (FILEMENUITEM *)mii.dwItemData : NULL;
|
|
}
|
|
|
|
FILEMENUHEADER *FileMenu_GetHeader(HMENU hmenu)
|
|
{
|
|
FILEMENUITEM *pfmi = FileMenu_GetItemData(hmenu, 0, TRUE);
|
|
|
|
if (pfmi &&
|
|
EVAL(IS_VALID_STRUCT_PTR(pfmi, FILEMENUITEM)) &&
|
|
EVAL(IS_VALID_STRUCT_PTR(pfmi->pfmh, FILEMENUHEADER)))
|
|
{
|
|
return pfmi->pfmh;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
// Create a file menu header. This header is to be associated
|
|
// with the given menu handle.
|
|
// If the menu handle already has header, simply return the
|
|
// existing header.
|
|
|
|
FILEMENUHEADER *FileMenuHeader_Create(HMENU hmenu, const FMCOMPOSE *pfmc)
|
|
{
|
|
FILEMENUHEADER *pfmh;
|
|
FILEMENUITEM *pfmi = FileMenu_GetItemData(hmenu, 0, TRUE);
|
|
// Does this guy already have a header?
|
|
if (pfmi)
|
|
{
|
|
// Yes; use it
|
|
pfmh = pfmi->pfmh;
|
|
ASSERT(IS_VALID_STRUCT_PTR(pfmh, FILEMENUHEADER));
|
|
}
|
|
else
|
|
{
|
|
// Nope, create one now.
|
|
pfmh = (FILEMENUHEADER *)LocalAlloc(LPTR, sizeof(*pfmh));
|
|
if (pfmh)
|
|
{
|
|
pfmh->hdpa = DPA_Create(0);
|
|
if (pfmh->hdpa == NULL)
|
|
{
|
|
LocalFree((HLOCAL)pfmh);
|
|
pfmh = NULL;
|
|
}
|
|
else
|
|
{
|
|
pfmh->hmenu = hmenu;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pfmc && pfmh)
|
|
{
|
|
pfmh->idCmd = pfmc->idCmd;
|
|
pfmh->grfFlags = pfmc->grfFlags;
|
|
pfmh->dwMask = pfmc->dwMask;
|
|
pfmh->pfnCallback = pfmc->pfnCallback;
|
|
pfmh->lParam = pfmc->lParam;
|
|
}
|
|
return pfmh;
|
|
}
|
|
|
|
BOOL FileMenuHeader_InsertMarkerItem(FILEMENUHEADER *pfmh, IShellFolder *psf);
|
|
|
|
// This functions adds the given item (index into DPA) into the actual menu.
|
|
BOOL FileMenuHeader_InsertItem(FILEMENUHEADER *pfmh, UINT iItem, FMIIFLAGS fFlags)
|
|
{
|
|
ASSERT(IS_VALID_STRUCT_PTR(pfmh, FILEMENUHEADER));
|
|
|
|
// Normal item.
|
|
FILEMENUITEM *pfmi = (FILEMENUITEM *)DPA_GetPtr(pfmh->hdpa, iItem);
|
|
if (!pfmi || (pfmi->dwFlags & FMI_ON_MENU))
|
|
return FALSE;
|
|
|
|
pfmi->dwFlags |= FMI_ON_MENU;
|
|
|
|
// The normal stuff.
|
|
UINT fMenu = MF_BYPOSITION | MF_OWNERDRAW;
|
|
// Keep track of where it's going in the menu.
|
|
|
|
// The special stuff...
|
|
if (fFlags & FMII_BREAK)
|
|
{
|
|
fMenu |= MF_MENUBARBREAK;
|
|
}
|
|
|
|
// Is it a folder (that's not open yet)?
|
|
if ((pfmi->dwAttributes & SFGAO_FOLDER) && !(pfmh->dwMask & FMC_NOEXPAND))
|
|
{
|
|
// Yep. Create a submenu item.
|
|
HMENU hmenuSub = CreatePopupMenu();
|
|
if (hmenuSub)
|
|
{
|
|
FMCOMPOSE fmc = {0};
|
|
|
|
// Set the callback now so it can be called when adding items
|
|
fmc.lParam = pfmh->lParam;
|
|
fmc.pfnCallback = pfmh->pfnCallback;
|
|
fmc.dwMask = pfmh->dwMask;
|
|
fmc.idCmd = pfmh->idCmd;
|
|
fmc.grfFlags = pfmh->grfFlags;
|
|
|
|
// Insert it into the parent menu.
|
|
InsertMenu(pfmh->hmenu, iItem, fMenu | MF_POPUP, (UINT_PTR)hmenuSub, (LPTSTR)pfmi);
|
|
|
|
// Set it's ID.
|
|
MENUITEMINFO mii = {0};
|
|
mii.cbSize = sizeof(mii);
|
|
mii.fMask = MIIM_ID;
|
|
mii.wID = pfmh->idCmd;
|
|
SetMenuItemInfo(pfmh->hmenu, iItem, TRUE, &mii);
|
|
|
|
IShellFolder *psf;
|
|
if (SUCCEEDED(pfmi->psf->BindToObject(pfmi->pidl, NULL, IID_PPV_ARG(IShellFolder, &psf))))
|
|
{
|
|
FILEMENUHEADER *pfmhSub = FileMenuHeader_Create(hmenuSub, &fmc);
|
|
if (pfmhSub)
|
|
{
|
|
// Build it a bit at a time.
|
|
FileMenuHeader_InsertMarkerItem(pfmhSub, psf);
|
|
}
|
|
psf->Release();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Nope.
|
|
if (pfmi->dwFlags & FMI_EMPTY)
|
|
fMenu |= MF_DISABLED | MF_GRAYED;
|
|
|
|
InsertMenu(pfmh->hmenu, iItem, fMenu, pfmh->idCmd, (LPTSTR)pfmi);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
// Give the submenu a marker item so we can check it's a filemenu item
|
|
// at initpopupmenu time.
|
|
BOOL FileMenuHeader_InsertMarkerItem(FILEMENUHEADER *pfmh, IShellFolder *psf)
|
|
{
|
|
ASSERT(IS_VALID_STRUCT_PTR(pfmh, FILEMENUHEADER));
|
|
|
|
FILEMENUITEM *pfmi;
|
|
if (FileMenuItem_Create(pfmh, psf, NULL, FMI_MARKER | FMI_EXPAND, &pfmi))
|
|
{
|
|
DPA_SetPtr(pfmh->hdpa, 0, pfmi);
|
|
FileMenuHeader_InsertItem(pfmh, 0, FMII_DEFAULT);
|
|
return TRUE;
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
// Enumerates the DPA and adds each item into the
|
|
// menu. Inserts vertical breaks if the menu becomes too long.
|
|
// Returns: count of items added to menu
|
|
int FileList_AddToMenu(FILEMENUHEADER *pfmh)
|
|
{
|
|
int cItemMac = 0;
|
|
|
|
ASSERT(IS_VALID_STRUCT_PTR(pfmh, FILEMENUHEADER));
|
|
|
|
if (pfmh->hdpa)
|
|
{
|
|
int cyItem = 0;
|
|
int cyMenu = pfmh->cyMenuSizeSinceLastBreak;
|
|
|
|
int cyMenuMax = GetSystemMetrics(SM_CYSCREEN);
|
|
|
|
// Get the rough height of an item so we can work out when to break the
|
|
// menu. User should really do this for us but that would be useful.
|
|
HDC hdc = GetDC(NULL);
|
|
if (hdc)
|
|
{
|
|
NONCLIENTMETRICS ncm;
|
|
ncm.cbSize = sizeof(ncm);
|
|
if (SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(ncm), &ncm, FALSE))
|
|
{
|
|
HFONT hfont = CreateFontIndirect(&ncm.lfMenuFont);
|
|
if (hfont)
|
|
{
|
|
HFONT hfontOld = SelectFont(hdc, hfont);
|
|
cyItem = HIWORD(GetItemExtent(hdc, (FILEMENUITEM *)DPA_GetPtr(pfmh->hdpa, 0)));
|
|
SelectObject(hdc, hfontOld);
|
|
DeleteObject(hfont);
|
|
}
|
|
}
|
|
ReleaseDC(NULL, hdc);
|
|
}
|
|
|
|
UINT cItems = DPA_GetPtrCount(pfmh->hdpa);
|
|
|
|
for (UINT i = 0; i < cItems; i++)
|
|
{
|
|
// Keep a rough count of the height of the menu.
|
|
cyMenu += cyItem;
|
|
if (cyMenu > cyMenuMax)
|
|
{
|
|
// Add a vertical break?
|
|
FileMenuHeader_InsertItem(pfmh, i, FMII_BREAK);
|
|
cyMenu = cyItem;
|
|
}
|
|
else
|
|
{
|
|
FileMenuHeader_InsertItem(pfmh, i, FMII_DEFAULT);
|
|
cItemMac++;
|
|
}
|
|
}
|
|
|
|
// Save the current cy size so we can use this again
|
|
// if more items are appended to this menu.
|
|
|
|
pfmh->cyMenuSizeSinceLastBreak = cyMenu;
|
|
}
|
|
|
|
return cItemMac;
|
|
}
|
|
|
|
|
|
BOOL FileList_AddImages(FILEMENUHEADER *pfmh)
|
|
{
|
|
ASSERT(IS_VALID_STRUCT_PTR(pfmh, FILEMENUHEADER));
|
|
|
|
int cItems = DPA_GetPtrCount(pfmh->hdpa);
|
|
for (int i = 0; i < cItems; i++)
|
|
{
|
|
FILEMENUITEM *pfmi = (FILEMENUITEM *)DPA_GetPtr(pfmh->hdpa, i);
|
|
if (pfmi && pfmi->pidl && (pfmi->iImage == -1))
|
|
{
|
|
pfmi->iImage = SHMapPIDLToSystemImageListIndex(pfmi->psf, pfmi->pidl, NULL);
|
|
}
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
// We create subemnu's with one marker item so we can check it's a file menu
|
|
// at init popup time but we need to delete it before adding new items.
|
|
BOOL FileMenuHeader_DeleteMarkerItem(FILEMENUHEADER *pfmh, IShellFolder **ppsf)
|
|
{
|
|
ASSERT(IS_VALID_STRUCT_PTR(pfmh, FILEMENUHEADER));
|
|
|
|
if (GetMenuItemCount(pfmh->hmenu) == 1)
|
|
{
|
|
if (GetMenuItemID(pfmh->hmenu, 0) == pfmh->idCmd)
|
|
{
|
|
FILEMENUITEM *pfmi = FileMenu_GetItemData(pfmh->hmenu, 0, TRUE);
|
|
if (pfmi && (pfmi->dwFlags & FMI_MARKER))
|
|
{
|
|
// Delete it.
|
|
ASSERT(pfmh->hdpa);
|
|
ASSERT(DPA_GetPtrCount(pfmh->hdpa) == 1);
|
|
|
|
if (ppsf)
|
|
{
|
|
*ppsf = pfmi->psf; // transfer the ref
|
|
pfmi->psf = NULL;
|
|
}
|
|
ASSERT(NULL == pfmi->psf);
|
|
// NB The marker shouldn't have a pidl.
|
|
ASSERT(NULL == pfmi->pidl);
|
|
|
|
LocalFree((HLOCAL)pfmi);
|
|
|
|
DPA_DeletePtr(pfmh->hdpa, 0);
|
|
DeleteMenu(pfmh->hmenu, 0, MF_BYPOSITION);
|
|
// Cleanup OK.
|
|
return TRUE;
|
|
}
|
|
}
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
// Add files to a file menu header. This function goes thru
|
|
// the following steps:
|
|
// - enumerates the folder and fills the hdpa list with items
|
|
// (files and subfolders)
|
|
// - sorts the list
|
|
// - gets the images for the items in the list
|
|
// - adds the items from list into actual menu
|
|
// The last step also (optionally) caps the length of the
|
|
// menu to the specified height. Ideally, this should
|
|
// happen at the enumeration time, except the required sort
|
|
// prevents this from happening. So we end up adding a
|
|
// bunch of items to the list and then removing them if
|
|
// there are too many.
|
|
// returns: count of items added
|
|
|
|
HRESULT FileMenuHeader_AddFiles(FILEMENUHEADER *pfmh, IShellFolder *psf, int iPos, int *pcItems)
|
|
{
|
|
HRESULT hr;
|
|
ASSERT(IS_VALID_STRUCT_PTR(pfmh, FILEMENUHEADER));
|
|
|
|
int cItems = FileList_Build(pfmh, psf, iPos);
|
|
|
|
// If the build was aborted cleanup and early out.
|
|
*pcItems = cItems;
|
|
|
|
if (cItems != 0)
|
|
{
|
|
// Add the images *after* adding to the menu, since the menu
|
|
// may be capped to a maximum height, and we can then prevent
|
|
// adding images we won't need.
|
|
*pcItems = FileList_AddToMenu(pfmh);
|
|
FileList_AddImages(pfmh);
|
|
}
|
|
|
|
hr = (*pcItems < cItems) ? S_FALSE : S_OK;
|
|
|
|
TraceMsg(TF_MENU, "FileMenuHeader_AddFiles: Added %d filemenu items.", cItems);
|
|
return hr;
|
|
}
|
|
|
|
// Add files to this menu.
|
|
// Returns: number of items added
|
|
HRESULT FileMenu_AddFiles(HMENU hmenu, UINT iPos, FMCOMPOSE *pfmc)
|
|
{
|
|
HRESULT hr = E_OUTOFMEMORY;
|
|
BOOL fMarker = FALSE;
|
|
|
|
// (FileMenuHeader_Create might return an existing header)
|
|
FILEMENUHEADER *pfmh = FileMenuHeader_Create(hmenu, pfmc);
|
|
if (pfmh)
|
|
{
|
|
FILEMENUITEM *pfmi = FileMenu_GetItemData(hmenu, 0, TRUE);
|
|
if (pfmi)
|
|
{
|
|
// Clean up marker item if there is one.
|
|
if ((FMI_MARKER | FMI_EXPAND) == (pfmi->dwFlags & (FMI_MARKER | FMI_EXPAND)))
|
|
{
|
|
// Nope, do it now.
|
|
FileMenuHeader_DeleteMarkerItem(pfmh, NULL);
|
|
fMarker = TRUE;
|
|
if (iPos)
|
|
iPos--;
|
|
}
|
|
}
|
|
|
|
hr = FileMenuHeader_AddFiles(pfmh, pfmc->psf, iPos, &pfmc->cItems);
|
|
|
|
if ((0 == pfmc->cItems) && fMarker)
|
|
{
|
|
// Aborted or no items. Put the marker back (if there used
|
|
// to be one).
|
|
FileMenuHeader_InsertMarkerItem(pfmh, NULL);
|
|
}
|
|
}
|
|
|
|
return hr;
|
|
}
|
|
|
|
// creator of the filemenu has to explicitly call to free
|
|
// up FileMenu items because USER doesn't send WM_DELETEITEM for ownerdraw
|
|
// menu. Great eh?
|
|
// Returns the number of items deleted.
|
|
|
|
void FileMenu_DeleteAllItems(HMENU hmenu)
|
|
{
|
|
FILEMENUHEADER *pfmh = FileMenu_GetHeader(hmenu);
|
|
if (pfmh)
|
|
{
|
|
// Clean up the items.
|
|
UINT cItems = DPA_GetPtrCount(pfmh->hdpa);
|
|
// backwards stop things dont move as we delete
|
|
for (int i = cItems - 1; i >= 0; i--)
|
|
{
|
|
FILEMENUITEM *pfmi = (FILEMENUITEM *)DPA_GetPtr(pfmh->hdpa, i);
|
|
if (pfmi)
|
|
{
|
|
HMENU hmenuSub = GetSubMenu(pfmh->hmenu, i); // cascade item?
|
|
if (hmenuSub)
|
|
{
|
|
// Yep. Get the submenu for this item, Delete all items.
|
|
FileMenu_DeleteAllItems(hmenuSub);
|
|
}
|
|
// Delete the item itself.
|
|
DeleteMenu(pfmh->hmenu, i, MF_BYPOSITION);
|
|
FileMenuItem_Destroy(pfmi);
|
|
DPA_DeletePtr(pfmh->hdpa, i);
|
|
}
|
|
}
|
|
|
|
// Clean up the header.
|
|
DPA_Destroy(pfmh->hdpa);
|
|
LocalFree((HLOCAL)pfmh);
|
|
}
|
|
}
|
|
|
|
STDAPI FileMenu_Compose(HMENU hmenu, UINT nMethod, FMCOMPOSE *pfmc)
|
|
{
|
|
HRESULT hr = E_INVALIDARG;
|
|
|
|
switch (nMethod)
|
|
{
|
|
case FMCM_INSERT:
|
|
hr = FileMenu_AddFiles(hmenu, 0, pfmc);
|
|
break;
|
|
|
|
case FMCM_APPEND:
|
|
hr = FileMenu_AddFiles(hmenu, GetMenuItemCount(hmenu), pfmc);
|
|
break;
|
|
|
|
case FMCM_REPLACE:
|
|
FileMenu_DeleteAllItems(hmenu);
|
|
hr = FileMenu_AddFiles(hmenu, 0, pfmc);
|
|
break;
|
|
}
|
|
return hr;
|
|
}
|
|
|
|
LPITEMIDLIST FileMenuItem_FullIDList(const FILEMENUITEM *pfmi)
|
|
{
|
|
LPITEMIDLIST pidlFolder, pidl = NULL;
|
|
if (SUCCEEDED(SHGetIDListFromUnk(pfmi->psf, &pidlFolder)))
|
|
{
|
|
pidl = ILCombine(pidlFolder, pfmi->pidl);
|
|
ILFree(pidlFolder);
|
|
}
|
|
return pidl;
|
|
}
|
|
|
|
void FileMenuItem_SetItem(const FILEMENUITEM *pfmi, BOOL bClear)
|
|
{
|
|
if (bClear)
|
|
{
|
|
pfmi->pfmh->pfmiLastSel = NULL;
|
|
pfmi->pfmh->pfnCallback(FMM_SETLASTPIDL, pfmi->pfmh->lParam, NULL, NULL);
|
|
}
|
|
else
|
|
{
|
|
pfmi->pfmh->pfmiLastSel = pfmi;
|
|
|
|
LPITEMIDLIST pidl = FileMenuItem_FullIDList(pfmi);
|
|
if (pidl)
|
|
{
|
|
pfmi->pfmh->pfnCallback(FMM_SETLASTPIDL, pfmi->pfmh->lParam, NULL, pidl);
|
|
ILFree(pidl);
|
|
}
|
|
}
|
|
}
|
|
|
|
LRESULT FileMenu_DrawItem(HWND hwnd, DRAWITEMSTRUCT *pdi)
|
|
{
|
|
BOOL fFlatMenu = FALSE;
|
|
BOOL fFrameRect = FALSE;
|
|
|
|
SystemParametersInfo(SPI_GETFLATMENU, 0, (void *)&fFlatMenu, 0);
|
|
|
|
if ((pdi->itemAction & ODA_SELECT) || (pdi->itemAction & ODA_DRAWENTIRE))
|
|
{
|
|
HBRUSH hbrOld = NULL;
|
|
FILEMENUITEM *pfmi = (FILEMENUITEM *)pdi->itemData;
|
|
|
|
ASSERT(IS_VALID_STRUCT_PTR(pfmi, FILEMENUITEM));
|
|
|
|
if (!pfmi)
|
|
{
|
|
TraceMsg(TF_ERROR, "FileMenu_DrawItem: Filemenu is invalid (no item data).");
|
|
return FALSE;
|
|
}
|
|
|
|
FILEMENUHEADER *pfmh = pfmi->pfmh;
|
|
ASSERT(IS_VALID_STRUCT_PTR(pfmh, FILEMENUHEADER));
|
|
|
|
// Adjust for large/small icons.
|
|
int cxIcon = g_cxSmIcon;
|
|
int cyIcon = g_cxSmIcon;
|
|
|
|
// Is the menu just starting to get drawn?
|
|
if (pdi->itemAction & ODA_DRAWENTIRE)
|
|
{
|
|
if (pfmi == DPA_GetPtr(pfmh->hdpa, 0))
|
|
{
|
|
// Yes; reset the last selection item
|
|
FileMenuItem_SetItem(pfmi, TRUE);
|
|
}
|
|
}
|
|
|
|
if (pdi->itemState & ODS_SELECTED)
|
|
{
|
|
// Determine the selection colors
|
|
//
|
|
// Normal menu colors apply until we are in edit mode, in which
|
|
// case the menu item is drawn unselected and an insertion caret
|
|
// is drawn above or below the current item. The exception is
|
|
// if the item is a cascaded menu item, then we draw it
|
|
// normally, but also show the insertion caret. (We do this
|
|
// because Office does this, and also, USER draws the arrow
|
|
// in the selected color always, so it looks kind of funny
|
|
// if we don't select the menu item.)
|
|
//
|
|
if (fFlatMenu)
|
|
{
|
|
SetBkColor(pdi->hDC, GetSysColor(COLOR_MENUHILIGHT));
|
|
hbrOld = SelectBrush(pdi->hDC, GetSysColorBrush(COLOR_MENUHILIGHT));
|
|
fFrameRect = TRUE;
|
|
}
|
|
else
|
|
{
|
|
// No
|
|
SetBkColor(pdi->hDC, GetSysColor(COLOR_HIGHLIGHT));
|
|
SetTextColor(pdi->hDC, GetSysColor(COLOR_HIGHLIGHTTEXT));
|
|
hbrOld = SelectBrush(pdi->hDC, GetSysColorBrush(COLOR_HIGHLIGHTTEXT));
|
|
}
|
|
|
|
// inform callback of last item
|
|
FileMenuItem_SetItem(pfmi, FALSE);
|
|
}
|
|
else
|
|
{
|
|
// dwRop = SRCAND;
|
|
hbrOld = SelectBrush(pdi->hDC, GetSysColorBrush(COLOR_MENUTEXT));
|
|
}
|
|
|
|
// Initial start pos.
|
|
int x = pdi->rcItem.left + CXIMAGEGAP;
|
|
|
|
// Get the name.
|
|
TCHAR szName[MAX_PATH];
|
|
FileMenuItem_GetDisplayName(pfmi, szName, ARRAYSIZE(szName));
|
|
|
|
// NB Keep a plain copy of the name for testing and accessibility.
|
|
if (!pfmi->psz)
|
|
pfmi->psz = StrDup(szName);
|
|
|
|
DWORD dwExtent = GetItemTextExtent(pdi->hDC, szName);
|
|
int y = (pdi->rcItem.bottom+pdi->rcItem.top - HIWORD(dwExtent)) / 2;
|
|
|
|
// Shrink the selection rect for small icons a bit.
|
|
pdi->rcItem.top += 1;
|
|
pdi->rcItem.bottom -= 1;
|
|
|
|
// Draw the text.
|
|
int fDSFlags;
|
|
|
|
if ((pfmi->dwFlags & FMI_ON_MENU) == 0)
|
|
{
|
|
// Norton Desktop Navigator 95 replaces the Start->&Run
|
|
// menu item with a &Run pidl. Even though the text is
|
|
// from a pidl, we still want to format the "&R" correctly.
|
|
fDSFlags = DST_PREFIXTEXT;
|
|
}
|
|
else
|
|
{
|
|
// All other strings coming from pidls are displayed
|
|
// as is to preserve any & in their display name.
|
|
fDSFlags = DST_TEXT;
|
|
}
|
|
|
|
if (pfmi->dwFlags & FMI_EMPTY)
|
|
{
|
|
if (pdi->itemState & ODS_SELECTED)
|
|
{
|
|
if (GetSysColor(COLOR_GRAYTEXT) == GetSysColor(COLOR_HIGHLIGHTTEXT))
|
|
{
|
|
fDSFlags |= DSS_UNION;
|
|
}
|
|
else
|
|
{
|
|
SetTextColor(pdi->hDC, GetSysColor(COLOR_GRAYTEXT));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
fDSFlags |= DSS_DISABLED;
|
|
}
|
|
|
|
ExtTextOut(pdi->hDC, 0, 0, ETO_OPAQUE, &pdi->rcItem, NULL, 0, NULL);
|
|
DrawState(pdi->hDC, NULL, NULL, (LONG_PTR)szName, lstrlen(szName), x + cxIcon + CXIMAGEGAP, y, 0, 0, fDSFlags);
|
|
}
|
|
else
|
|
{
|
|
ExtTextOut(pdi->hDC, x + cxIcon + CXIMAGEGAP, y, ETO_OPAQUE, &pdi->rcItem, NULL, 0, NULL);
|
|
DrawState(pdi->hDC, NULL, NULL, (LONG_PTR)szName, lstrlen(szName), x + cxIcon + CXIMAGEGAP, y, 0, 0, fDSFlags);
|
|
}
|
|
|
|
if (fFrameRect)
|
|
{
|
|
HBRUSH hbrFill = (HBRUSH)GetSysColorBrush(COLOR_HIGHLIGHT);
|
|
HBRUSH hbrSave = (HBRUSH)SelectObject(pdi->hDC, hbrFill);
|
|
int x = pdi->rcItem.left;
|
|
int y = pdi->rcItem.top;
|
|
int cx = pdi->rcItem.right - x - 1;
|
|
int cy = pdi->rcItem.bottom - y - 1;
|
|
|
|
PatBlt(pdi->hDC, x, y, 1, cy, PATCOPY);
|
|
PatBlt(pdi->hDC, x + 1, y, cx, 1, PATCOPY);
|
|
PatBlt(pdi->hDC, x, y + cy, cx, 1, PATCOPY);
|
|
PatBlt(pdi->hDC, x + cx, y + 1, 1, cy, PATCOPY);
|
|
|
|
SelectObject(pdi->hDC, hbrSave);
|
|
}
|
|
|
|
// Get the image if it needs it,
|
|
if ((pfmi->iImage == -1) && pfmi->pidl && pfmi->psf)
|
|
{
|
|
pfmi->iImage = SHMapPIDLToSystemImageListIndex(pfmi->psf, pfmi->pidl, NULL);
|
|
}
|
|
|
|
// Draw the image (if there is one).
|
|
if (pfmi->iImage != -1)
|
|
{
|
|
// Try to center image.
|
|
y = (pdi->rcItem.bottom + pdi->rcItem.top - cyIcon) / 2;
|
|
|
|
HIMAGELIST himl;
|
|
Shell_GetImageLists(NULL, &himl);
|
|
|
|
ImageList_DrawEx(himl, pfmi->iImage, pdi->hDC, x, y, 0, 0,
|
|
GetBkColor(pdi->hDC), CLR_NONE, ILD_NORMAL);
|
|
}
|
|
if (hbrOld)
|
|
SelectObject(pdi->hDC, hbrOld);
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
DWORD FileMenuItem_GetExtent(FILEMENUITEM *pfmi)
|
|
{
|
|
DWORD dwExtent = 0;
|
|
|
|
if (pfmi)
|
|
{
|
|
FILEMENUHEADER *pfmh = pfmi->pfmh;
|
|
|
|
ASSERT(IS_VALID_STRUCT_PTR(pfmh, FILEMENUHEADER));
|
|
|
|
HDC hdcMem = CreateCompatibleDC(NULL);
|
|
if (hdcMem)
|
|
{
|
|
// Get the rough height of an item so we can work out when to break the
|
|
// menu. User should really do this for us but that would be useful.
|
|
NONCLIENTMETRICS ncm = {0};
|
|
ncm.cbSize = sizeof(ncm);
|
|
if (SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(ncm), &ncm, FALSE))
|
|
{
|
|
HFONT hfont = CreateFontIndirect(&ncm.lfMenuFont);
|
|
if (hfont)
|
|
{
|
|
HFONT hfontOld = SelectFont(hdcMem, hfont);
|
|
dwExtent = GetItemExtent(hdcMem, pfmi);
|
|
SelectFont(hdcMem, hfontOld);
|
|
DeleteObject(hfont);
|
|
}
|
|
}
|
|
DeleteDC(hdcMem);
|
|
}
|
|
}
|
|
return dwExtent;
|
|
}
|
|
|
|
LRESULT FileMenu_MeasureItem(HWND hwnd, MEASUREITEMSTRUCT *pmi)
|
|
{
|
|
DWORD dwExtent = FileMenuItem_GetExtent((FILEMENUITEM *)pmi->itemData);
|
|
pmi->itemHeight = HIWORD(dwExtent);
|
|
pmi->itemWidth = LOWORD(dwExtent);
|
|
return TRUE;
|
|
}
|
|
|
|
// Fills the given filemenu with contents of the appropriate folder
|
|
//
|
|
// Returns: S_OK if all the files were added
|
|
// error on something bad
|
|
|
|
STDAPI FileMenu_InitMenuPopup(HMENU hmenu)
|
|
{
|
|
HRESULT hr = E_FAIL;
|
|
|
|
FILEMENUITEM *pfmi = FileMenu_GetItemData(hmenu, 0, TRUE);
|
|
if (pfmi)
|
|
{
|
|
FILEMENUHEADER *pfmh = pfmi->pfmh;
|
|
if (pfmh)
|
|
{
|
|
hr = S_OK;
|
|
|
|
// Have we already filled this thing out?
|
|
if ((FMI_MARKER | FMI_EXPAND) == (pfmi->dwFlags & (FMI_MARKER | FMI_EXPAND)))
|
|
{
|
|
// No, do it now. Get the previously init'ed header.
|
|
IShellFolder *psf;
|
|
if (FileMenuHeader_DeleteMarkerItem(pfmh, &psf))
|
|
{
|
|
// Fill it full of stuff.
|
|
int cItems;
|
|
hr = FileMenuHeader_AddFiles(pfmh, psf, 0, &cItems);
|
|
psf->Release();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return hr;
|
|
}
|
|
|
|
int FileMenuHeader_LastSelIndex(FILEMENUHEADER *pfmh)
|
|
{
|
|
for (int i = GetMenuItemCount(pfmh->hmenu) - 1; i >= 0; i--)
|
|
{
|
|
FILEMENUITEM *pfmi = FileMenu_GetItemData(pfmh->hmenu, i, TRUE);
|
|
if (pfmi && (pfmi == pfmh->pfmiLastSel))
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// If the string contains &ch or begins with ch then return TRUE.
|
|
BOOL _MenuCharMatch(LPCTSTR lpsz, TCHAR ch, BOOL fIgnoreAmpersand)
|
|
{
|
|
// Find the first ampersand.
|
|
LPTSTR pchAS = StrChr(lpsz, TEXT('&'));
|
|
if (pchAS && !fIgnoreAmpersand)
|
|
{
|
|
// Yep, is the next char the one we want.
|
|
if (CharUpperChar(*CharNext(pchAS)) == CharUpperChar(ch))
|
|
{
|
|
// Yep.
|
|
return TRUE;
|
|
}
|
|
}
|
|
else if (CharUpperChar(*lpsz) == CharUpperChar(ch))
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
STDAPI_(LRESULT) FileMenu_HandleMenuChar(HMENU hmenu, TCHAR ch)
|
|
{
|
|
FILEMENUITEM *pfmi;
|
|
TCHAR szName[MAX_PATH];
|
|
|
|
int iFoundOne = -1;
|
|
UINT iStep = 0;
|
|
UINT iItem = 0;
|
|
UINT cItems = GetMenuItemCount(hmenu);
|
|
|
|
// Start from the last place we looked from.
|
|
FILEMENUHEADER *pfmh = FileMenu_GetHeader(hmenu);
|
|
if (pfmh)
|
|
{
|
|
iItem = FileMenuHeader_LastSelIndex(pfmh) + 1;
|
|
if (iItem >= cItems)
|
|
iItem = 0;
|
|
}
|
|
|
|
while (iStep < cItems)
|
|
{
|
|
pfmi = FileMenu_GetItemData(hmenu, iItem, TRUE);
|
|
if (pfmi)
|
|
{
|
|
FileMenuItem_GetDisplayName(pfmi, szName, ARRAYSIZE(szName));
|
|
if (_MenuCharMatch(szName, ch, pfmi->pidl ? TRUE : FALSE))
|
|
{
|
|
// Found (another) match.
|
|
if (iFoundOne != -1)
|
|
{
|
|
// More than one, select the first.
|
|
return MAKELRESULT(iFoundOne, MNC_SELECT);
|
|
}
|
|
else
|
|
{
|
|
// Found at least one.
|
|
iFoundOne = iItem;
|
|
}
|
|
}
|
|
|
|
}
|
|
iItem++;
|
|
iStep++;
|
|
// Wrap.
|
|
if (iItem >= cItems)
|
|
iItem = 0;
|
|
}
|
|
|
|
// Did we find one?
|
|
if (iFoundOne != -1)
|
|
{
|
|
// Just in case the user types ahead without the selection being drawn.
|
|
pfmi = FileMenu_GetItemData(hmenu, iFoundOne, TRUE);
|
|
FileMenuItem_SetItem(pfmi, FALSE);
|
|
|
|
return MAKELRESULT(iFoundOne, MNC_EXECUTE);
|
|
}
|
|
else
|
|
{
|
|
// Didn't find it.
|
|
return MAKELRESULT(0, MNC_IGNORE);
|
|
}
|
|
}
|
|
|
|
|