|
// Copyright (C) 2011 Fog Creek Software
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "stdafx.h"
#include "THgShell_h.h"
#include "TortoiseUtils.h"
#include "StringUtils.h"
#include "THgStatus.h"
#include "Winstat.h"
#include "SysInfo.h"
#include "RegistryConfig.h"
#include "TortoiseIconBitmap.h"
#include "THgVersion.h"
#include "TortoiseHgCmenu.h"
#include "RunDialog.h"
#include "Kiln.h"
#include <msi.h>
void CTortoiseHgCmenuBase::AddMenuList(UINT idCmd, const CString& strName)
{
ATLTRACE("AddMenuList: idCmd = %d, name = '%s'\n", idCmd, strName);
m_mapMenuId[idCmd] = m_mapMenuDesc[strName];
}
void CTortoiseHgCmenuBase::InitMenuMaps(const CMenuDescription *pMenuDescs, int nCount)
{
if (m_mapMenuDesc.IsEmpty())
{
CString strLang;
GetRegistryConfig("CMenuLang", strLang);
for (int i = 0; i < nCount; i++)
{
CMenuDescription md = pMenuDescs[i];
if (md.strName.IsEmpty())
{
ATLTRACE("**** InitMenuMaps: ignoring entry with empty name\n");
break;
}
ATLTRACE("InitMenuMaps: adding '%s'\n", (LPCTSTR)md.strName);
// Look for translation of menu and help text
if (!strLang.IsEmpty())
{
GetCMenuTranslation(strLang, md.strName, md.strMenuText, md.strHelpText);
}
m_mapMenuDesc[md.strName] = md;
}
}
m_mapMenuId.RemoveAll();
}
void InsertMenuItemWithIcon1(HMENU hMenu, UINT indexMenu, UINT idCmd,
const CStringW& strMenuText, const CString& strIconName)
{
// MFT_STRING is obsolete and should not be used (replaced by MIIM_STRING
// from Win2K onward)
MENUITEMINFOW mii;
memset(&mii, 0, sizeof(MENUITEMINFOW));
mii.cbSize = sizeof(MENUITEMINFOW);
mii.fMask = MIIM_ID | MIIM_STRING;
mii.dwTypeData = (LPWSTR)(LPCWSTR)strMenuText;
mii.cch = strMenuText.GetLength();
mii.wID = idCmd;
if (!strIconName.IsEmpty())
{
if (CSysInfo::IsVistaOrLater())
{
HBITMAP hBitmap = GetTortoiseIconBitmap(strIconName);
if (hBitmap)
{
mii.fMask |= MIIM_BITMAP;
mii.hbmpItem = hBitmap;
}
else
{
ATLTRACE(" ***** InsertMenuItemWithIcon1: can't find '%s'\n",
(LPCTSTR)strIconName);
}
}
else
{
HICON hIcon = GetTortoiseIcon(strIconName);
if (hIcon)
{
mii.fMask |= MIIM_BITMAP | MIIM_DATA;
mii.dwItemData = (ULONG_PTR)hIcon;
mii.hbmpItem = HBMMENU_CALLBACK;
}
else
{
ATLTRACE(" ***** InsertMenuItemWithIcon1: can't find '%s'\n",
(LPCTSTR)strIconName);
}
}
}
::InsertMenuItemW(hMenu, indexMenu, TRUE, &mii);
ATLTRACE(L"InsertMenuItemWithIcon1('%s') finished\n", (LPCWSTR)strMenuText);
}
void InsertSubMenuItemWithIcon2(HMENU hMenu, HMENU hSubMenu, UINT indexMenu, UINT idCmd,
const CStringW& strMenuText, const CString& strIconName)
{
// MFT_STRING is obsolete and should not be used (replaced by MIIM_STRING
// from Win2K onward)
MENUITEMINFOW mii;
memset(&mii, 0, sizeof(MENUITEMINFOW));
mii.cbSize = sizeof(MENUITEMINFOW);
mii.fMask = MIIM_SUBMENU | MIIM_ID | MIIM_STRING;
mii.dwTypeData = (LPWSTR)(LPCWSTR)strMenuText;
mii.cch = strMenuText.GetLength();
mii.wID = idCmd;
mii.hSubMenu = hSubMenu;
if (CSysInfo::IsVistaOrLater())
{
HBITMAP hBitmap = GetTortoiseIconBitmap(strIconName);
if (hBitmap)
{
mii.fMask |= MIIM_BITMAP;
mii.hbmpItem = hBitmap;
}
else
{
ATLTRACE(" ***** InsertSubMenuItemWithIcon2: can't find '%s'\n",
(LPCTSTR)strIconName);
}
}
else
{
HICON hIcon = GetTortoiseIcon(strIconName);
if (hIcon)
{
mii.fMask |= MIIM_BITMAP | MIIM_DATA;
mii.dwItemData = (ULONG_PTR)hIcon;
mii.hbmpItem = HBMMENU_CALLBACK;
}
else
{
ATLTRACE(" ***** InsertSubMenuItemWithIcon2: can't find '%s'\n",
(LPCTSTR)strIconName);
}
}
::InsertMenuItemW(hMenu, indexMenu, TRUE, &mii);
ATLTRACE(L"InsertMenuItemWithIcon2('%s') finished\n", (LPCWSTR)strMenuText);
}
void CTortoiseHgCmenuBase::InsertMenuItemByName(HMENU hMenu, const CString& strName,
UINT indexMenu, UINT idCmd, UINT idCmdFirst, const CStringW& strPrefix)
{
const CMenuDescriptionMap::CPair* pPair = m_mapMenuDesc.Lookup(strName);
if (pPair == NULL)
{
ATLTRACE("***** InsertMenuItemByName: can't find menu info for '%s'\n",
(LPCTSTR)strName);
return;
}
const CMenuDescription& md = pPair->m_value;
AddMenuList(idCmd - idCmdFirst, strName);
InsertMenuItemWithIcon1(hMenu, indexMenu, idCmd, strPrefix + md.strMenuText,
md.strIconName);
}
const LPCWSTR TortoiseHgMenuEntryString = L"TortoiseHg";
// returns -1 on error, 0 otherwise
int HasTortoiseMenu(HMENU hMenu, bool& bHasMenu)
{
bHasMenu = false;
int nCount = ::GetMenuItemCount(hMenu);
if (nCount == -1)
{
ATLTRACE("***** HasTortoiseMenu: GetMenuItemCount returned -1\n");
return -1;
}
MENUITEMINFOW mii;
for (int i = 0; i < nCount; ++i)
{
memset(&mii, 0, sizeof(MENUITEMINFOW));
mii.cbSize = sizeof(MENUITEMINFOW);
// first GetMenuItemInfoW call: get size of menu item string
mii.fMask = MIIM_STRING;
BOOL bRes = ::GetMenuItemInfoW(hMenu, i, true, &mii);
if (bRes == 0)
{
ATLTRACE("HasTortoiseMenu: first GetMenuItemInfo returned 0\n");
continue;
}
if (mii.dwTypeData != MFT_STRING)
{
// not a string
continue;
}
// allocate buffer for the string
CStringW strMenuItemText;
LPWSTR lpszBuf = strMenuItemText.GetBuffer(mii.cch + 1);
// second GetMenuItemInfoW call: get string into buffer
mii.dwTypeData = lpszBuf;
++mii.cch; // size of buffer is one more than length of string
bRes = ::GetMenuItemInfoW(hMenu, i, true, &mii);
strMenuItemText.ReleaseBuffer();
if (bRes == 0)
{
ATLTRACE("HasTortoiseMenu: second GetMenuItemInfo returned 0\n");
continue;
}
if (strMenuItemText == TortoiseHgMenuEntryString)
{
ATLTRACE("HasTortoiseMenu: FOUND TortoiseHg menu entry\n");
bHasMenu = true;
return 0;
}
}
ATLTRACE("HasTortoiseMenu: TortoiseHg menu entry NOT found\n");
return 0;
}
void CTortoiseHgCmenuBase::TweakMenuForVista(HMENU hMenu)
{
if (!CSysInfo::IsVistaOrLater()) return;
MENUINFO mi;
memset(&mi, 0, sizeof(MENUINFO));
mi.cbSize = sizeof(MENUINFO);
mi.fMask = MIM_STYLE | MIM_APPLYTOSUBMENUS;
mi.dwStyle = MNS_CHECKORBMP;
::SetMenuInfo(hMenu, &mi);
}
#define ResultFromShort(i) ResultFromScode(MAKE_SCODE(SEVERITY_SUCCESS, 0, (USHORT)(i)))
// IContextMenu
STDMETHODIMP CTortoiseHgCmenuBase::QueryContextMenu(HMENU hMenu, UINT indexMenu,
UINT idCmdFirst, UINT idCmdLast, UINT uFlags)
{
ATLTRACE("CTortoiseHgCmenuBase::QueryContextMenu\n");
UINT idCmd = idCmdFirst;
BOOL bAppendItems = TRUE;
if ((uFlags & 0x000F) == CMF_NORMAL)
bAppendItems = TRUE;
else if (uFlags & CMF_VERBSONLY)
bAppendItems = TRUE;
else if (uFlags & CMF_EXPLORE)
bAppendItems = TRUE;
else
bAppendItems = FALSE;
if (!bAppendItems)
return S_OK;
bool bHasTHgMenu = false;
if (HasTortoiseMenu(hMenu, bHasTHgMenu) == 0 && bHasTHgMenu)
{
ATLTRACE("CTortoiseHgCmenuBase::QueryContextMenu: "
"TortoiseHg menu entry already in menu -> skipping");
return S_OK;
}
InitMenuMaps(MenuDescList, MenuDescListCount);
CString promoted_string = DefaultPromotedString; // default value if key not found
GetRegistryConfig("PromotedItems", promoted_string);
CAtlList<CString> listPromoted;
Tokenize(promoted_string, listPromoted, ",");
// Select menu to show
bool bFileMenu = !m_listFiles.IsEmpty();
bool bIsHgRepo = false;
CString strCwd;
if (!m_strFolder.IsEmpty())
{
strCwd = m_strFolder;
}
else if (bFileMenu)
{
strCwd = m_listFiles.GetHead();
if (!::PathIsDirectory(strCwd)) strCwd = DirName(strCwd);
}
if (!strCwd.IsEmpty())
{
// check if target directory is a Mercurial repository
CString strRoot = GetHgRepoRoot(strCwd);
bIsHgRepo = !strRoot.IsEmpty();
if (m_listFiles.GetCount() == 1 && strRoot == m_listFiles.GetHead())
{
bFileMenu = false;
m_strFolder = strCwd;
m_listFiles.RemoveAll();
}
}
if ((uFlags & CMF_EXTENDEDVERBS) == 0)
{
// shift key is not down
if (!bIsHgRepo)
{
// we are not inside a repo
CString strCval;
if (GetRegistryConfig("HideMenuOutsideRepo", strCval) != 0 && strCval == "1")
{
return S_OK; // don't show thg cmenu entries
}
}
}
ATLTRACE("CTortoiseHgCmenuBase::QueryContextMenu: bIsHgRepo = %d, bFileMenu = %d\n",
bIsHgRepo, bFileMenu);
/* We have three menu types: files-selected, no-files-selected, no-repo */
const LPCTSTR* ppszEntries = NULL;
int nCount;
if (bIsHgRepo)
{
if (bFileMenu)
{
ppszEntries = RepoFilesMenu;
nCount = RepoFilesMenuCount;
}
else
{
ppszEntries = RepoNoFilesMenu;
nCount = RepoNoFilesMenuCount;
}
}
else
{
ppszEntries = NoRepoMenu;
nCount = NoRepoMenuCount;
}
// start building TortoiseHg menus and submenus
::InsertMenu(hMenu, indexMenu++, MF_SEPARATOR | MF_BYPOSITION, 0, NULL);
for (int i = 0; i < nCount; i++)
{
CString strName = ppszEntries[i];
if (listPromoted.Find(strName) != NULL)
{
if ((strName == "kiln" || strName == "kilnfiles") && !KilnGetUrl(strCwd))
{
continue;
}
InsertMenuItemByName(hMenu, strName, indexMenu++, idCmd++, idCmdFirst, L"Hg ");
}
}
const HMENU hSubMenu = ::CreatePopupMenu();
if (hSubMenu)
{
UINT indexSubMenu = 0;
bool bIsSeparator = true;
for (int i = 0; i < nCount; i++)
{
if (ppszEntries[i] == NULL)
{
if (!bIsSeparator)
{
::InsertMenu(hSubMenu, indexSubMenu++,
MF_SEPARATOR | MF_BYPOSITION, 0, NULL);
bIsSeparator = true;
}
}
else
{
CString strName = ppszEntries[i];
if (listPromoted.Find(strName) == NULL)
{
if ((strName == "kiln" || strName == "kilnfiles") &&
!KilnGetUrl(strCwd))
{
continue;
}
InsertMenuItemByName(hSubMenu, strName, indexSubMenu++, idCmd++,
idCmdFirst, L"");
bIsSeparator = false;
}
}
}
if (bIsSeparator && indexSubMenu > 0)
::RemoveMenu(hSubMenu, indexSubMenu - 1, MF_BYPOSITION);
TweakMenuForVista(hSubMenu);
}
ATLTRACE(" CTortoiseHgCmenuBase::QueryContextMenu: adding main THG menu\n");
InsertSubMenuItemWithIcon2(hMenu, hSubMenu, indexMenu++, idCmd++,
TortoiseHgMenuEntryString, "hg.ico");
::InsertMenu(hMenu, indexMenu++, MF_SEPARATOR | MF_BYPOSITION, 0, NULL);
TweakMenuForVista(hMenu);
return ResultFromShort(idCmd - idCmdFirst);
}
STDMETHODIMP CTortoiseHgCmenuBase::InvokeCommand(LPCMINVOKECOMMANDINFO lpcmi)
{
ATLTRACE("CTortoiseHgCmenuBase::InvokeCommand");
HRESULT hr = E_INVALIDARG;
if (!HIWORD(lpcmi->lpVerb))
{
UINT idCmd = LOWORD(lpcmi->lpVerb);
ATLTRACE("CTortoiseHgCmenuBase::InvokeCommand: idCmd = %d\n", idCmd);
const CMenuIdCmdMap::CPair* p = m_mapMenuId.Lookup(idCmd);
if (p != NULL)
{
RunDialog(lpcmi->hwnd, p->m_value.strName, m_listFiles, m_strFolder);
hr = S_OK;
}
else
{
ATLTRACE("***** CTortoiseHgCmenuBase::InvokeCommand: action not found "
"for idCmd %d\n", idCmd);
}
}
return hr;
}
STDMETHODIMP CTortoiseHgCmenuBase::GetCommandString(UINT_PTR idCmd, UINT uFlags,
UINT FAR *reserved, LPSTR pszName, UINT cchMax)
{
// see http://msdn.microsoft.com/en-us/library/bb776094%28VS.85%29.aspx
HRESULT hr = S_FALSE;
const char* psz = "";
const wchar_t* pszw = 0;
#ifdef _DEBUG
CString strFlags = "?";
switch (uFlags)
{
case GCS_HELPTEXTW:
strFlags = "GCS_HELPTEXTW";
break;
case GCS_HELPTEXTA:
strFlags = "GCS_HELPTEXTA";
break;
case GCS_VALIDATEW:
strFlags = "GCS_VALIDATEW";
break;
case GCS_VALIDATEA:
strFlags = "GCS_VALIDATEA";
break;
case GCS_VERBW:
strFlags = "GCS_VERBW";
break;
case GCS_VERBA:
strFlags = "GCS_VERBA";
break;
}
ATLTRACE("CTortoiseHgCmenuBase::GetCommandString: idCmd = %d, uFlags = %d (%s), "
"cchMax = %d\n", idCmd, uFlags, (LPCTSTR)strFlags, cchMax);
#endif
const CMenuIdCmdMap::CPair* p = m_mapMenuId.Lookup((UINT)idCmd);
if (p == NULL)
{
ATLTRACE("***** CTortoiseHgCmenuBase::GetCommandString: idCmd not found\n");
}
else
{
ATLTRACE("CTortoiseHgCmenuBase::GetCommandString: name = '%s'\n",
(LPCTSTR)p->m_value.strName);
if (uFlags == GCS_HELPTEXTW)
{
pszw = p->m_value.strHelpText;
hr = S_OK;
int nSize = p->m_value.strHelpText.GetLength();
if (nSize >= 40)
{
ATLTRACE("***** CTortoiseHgCmenuBase::GetCommandString: warning: "
"length of help text is %d, which is not reasonably "
"short (<40)\n", nSize);
}
}
else if (uFlags == GCS_HELPTEXTA)
{
// we don't provide ansi help texts
psz = "";
hr = S_OK;
}
else if (uFlags == GCS_VERBW || uFlags == GCS_VERBA)
{
#if 0
psz = p->m_value.name;
#else
// bugfix: don't provide verbs ("rename" conflicted with rename of explorer)
psz = "";
#endif
hr = S_OK;
}
else if (uFlags == GCS_VALIDATEW || uFlags == GCS_VALIDATEA)
{
hr = S_OK;
}
}
if (cchMax < 1)
{
ATLTRACE("CTortoiseHgCmenuBase::GetCommandString: cchMax = %d (is <1)\n", cchMax);
return hr;
}
size_t nSize = 0;
if (uFlags & GCS_UNICODE)
{
LPWSTR lpszDest = (LPWSTR)pszName;
CStringW strUnicode = psz;
LPCWSTR lpszSrc = pszw ? pszw : strUnicode;
wcsncpy_s(lpszDest, cchMax, lpszSrc, cchMax-1);
*(lpszDest + cchMax-1) = 0;
nSize = wcslen(lpszSrc);
ATLTRACE(L"CTortoiseHgCmenuBase::GetCommandString: res = %d, "
L"pszName (wide) = '%s'\n", (int)hr, lpszDest);
}
else
{
strncpy_s(pszName, cchMax, psz, cchMax-1);
*(pszName + cchMax-1) = 0;
nSize = strlen(psz);
ATLTRACE("CShellExtCMenu::GetCommandString: res = %d, pszName = '%s'\n",
(int)hr, psz);
}
if (nSize > cchMax-1)
{
ATLTRACE("***** CShellExtCMenu::GetCommandString: string was truncated: "
"size = %d, cchMax = %d\n", nSize, cchMax);
}
return hr;
}
STDMETHODIMP CTortoiseHgCmenuBase::HandleMenuMsg(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
LRESULT lResult;
return HandleMenuMsg2(uMsg, wParam, lParam, &lResult);
}
STDMETHODIMP CTortoiseHgCmenuBase::HandleMenuMsg2(UINT uMsg, WPARAM wParam,
LPARAM lParam, LRESULT* pResult)
{
// A great tutorial on owner drawn menus in shell extension can be found
// here: http://www.codeproject.com/shell/shellextguide7.asp
LRESULT lResult;
if (!pResult)
pResult = &lResult;
*pResult = FALSE;
switch (uMsg)
{
case WM_MEASUREITEM:
{
MEASUREITEMSTRUCT* lpmis = (MEASUREITEMSTRUCT*)lParam;
if (lpmis == NULL) break;
lpmis->itemWidth += 2;
if(lpmis->itemHeight < 16)
lpmis->itemHeight = 16;
*pResult = TRUE;
}
break;
case WM_DRAWITEM:
{
DRAWITEMSTRUCT* lpdis = (DRAWITEMSTRUCT*)lParam;
if (!lpdis || (lpdis->CtlType != ODT_MENU) || !lpdis->itemData) break;
::DrawIconEx(lpdis->hDC, lpdis->rcItem.left - 16,
lpdis->rcItem.top + (lpdis->rcItem.bottom - lpdis->rcItem.top - 16) / 2,
(HICON)lpdis->itemData, 16, 16, 0, 0, DI_NORMAL);
*pResult = TRUE;
}
break;
default:
return S_OK;
}
return S_OK;
}
#ifdef _DEBUG
void CTortoiseHgCmenuBase::PrintDebugHeader(LPCITEMIDLIST pIDFolder, LPDATAOBJECT pDataObj)
{
ATLTRACE("CTortoiseHgCmenuBase::Initialize\n");
// get installed MSI product id (for debugging purposes for now)
#ifdef _M_X64
LPCTSTR lpszShellExId = "{D5D1E532-CDAD-4FFD-9695-757B8A29B4BA}";
#else
LPCTSTR lpszShellExId = "{728E8840-5878-4EA7-918F-281C2697ABB1}";
#endif
char szProductId[50];
UINT msires = ::MsiGetProductCode(lpszShellExId, szProductId);
ATLTRACE("MSI shellexid: %s\n", lpszShellExId);
ATLTRACE("MSI msires: %d", msires);
ATLTRACE("MSI installed product id: %s\n", szProductId);
DWORD dwBufSize = 300;
CAutoVectorPtr<char> buf(new char[dwBufSize]);
msires = ::MsiGetProductInfo(szProductId, INSTALLPROPERTY_INSTALLLOCATION,
buf, &dwBufSize);
if (msires == ERROR_SUCCESS)
{
ATLTRACE("MSI install location: %s\n", &buf[0]);
}
else
{
ATLTRACE("MSI install location: error %d\n", msires);
}
ATLTRACE(L"---- TortoiseHg shell extension version %s ----",
(LPCTSTR)THgVersion);
ATLTRACE(" pIDFolder: %p\n", pIDFolder);
ATLTRACE(" pDataObj: %p\n", pDataObj);
}
#endif
STDMETHODIMP CTortoiseHgCmenuBase::Initialize(LPCITEMIDLIST pIDFolder,
LPDATAOBJECT pDataObj, HKEY hRegKey)
{
TCHAR szName[MAX_PATH + 1];
#ifdef _DEBUG
PrintDebugHeader(pIDFolder, pDataObj);
#endif
m_strFolder.Empty();
m_listFiles.RemoveAll();
if (pDataObj)
{
FORMATETC fmt = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
STGMEDIUM stg = { TYMED_HGLOBAL };
if (SUCCEEDED(pDataObj->GetData(&fmt, &stg)) && stg.hGlobal)
{
HDROP hDrop = (HDROP)::GlobalLock(stg.hGlobal);
if (hDrop)
{
UINT uNumFiles = ::DragQueryFile(hDrop, 0xFFFFFFFF, NULL, 0);
ATLTRACE(" hDrop uNumFiles = %d\n", uNumFiles);
for (UINT i = 0; i < uNumFiles; ++i) {
if (::DragQueryFile(hDrop, i, szName, MAX_PATH) > 0)
{
ATLTRACE(" DragQueryFile [%d] = '%s'\n", i, szName);
m_listFiles.AddTail(szName);
}
}
}
else
{
ATLTRACE(" hDrop is NULL\n");
}
::GlobalUnlock(stg.hGlobal);
if (stg.pUnkForRelease)
{
IUnknown* relInterface = (IUnknown*) stg.pUnkForRelease;
relInterface->Release();
}
}
else
{
ATLTRACE(" pDataObj->GetData failed\n");
}
}
// if a directory background
if (pIDFolder)
{
::SHGetPathFromIDList(pIDFolder, szName);
ATLTRACE(" Folder '%s'\n", szName);
m_strFolder = szName;
}
// disable context menu if neither the folder nor the files
// have been found
if (m_strFolder.IsEmpty() && m_listFiles.IsEmpty())
{
ATLTRACE(" shell extension not available on this object\n");
return E_FAIL;
}
else
{
return S_OK;
}
}
CTortoiseHgCmenuBase::CTortoiseHgCmenuBase()
{
}
CTortoiseHgCmenu::CTortoiseHgCmenu()
{
}
|
Loading...