1 /////////////////////////////////////////////////////////////////////////////
3 // This program is free software; you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation; either version 2 of the License, or (at
6 // your option) any later version.
8 // This program is distributed in the hope that it will be useful, but
9 // WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 // GNU General Public License for more details.
13 // You should have received a copy of the GNU General Public License
14 // along with this program; if not, write to the Free Software
15 // Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
16 /////////////////////////////////////////////////////////////////////////////
17 // Look at http://www.codeproject.com/shell/ for excellent guide
18 // to Windows Shell programming by Michael Dunn.
20 // This extension needs two registry values to be defined:
21 // HKEY_CURRENT_USER\Software\Thingamahoochie\WinMerge\ContextMenuEnabled
22 // defines if context menu is shown (extension enabled) and if
23 // we show simple or advanced menu
24 // HKEY_CURRENT_USER\Software\Thingamahoochie\WinMerge\Executable
25 // contains path to program to run (can be batch file too)
27 // HKEY_CURRENT_USER\Software\Thingamahoochie\WinMerge\FirstSelection
28 // is used to store path for first selection in advanced mode
30 // HKEY_CURRENT_USER\Software\Thingamahoochie\WinMerge\PriExecutable
31 // overwrites 'Executable' if defined. Useful to overwrite
32 // option set from UI when debugging/testing.
33 /////////////////////////////////////////////////////////////////////////////
35 * @file WinMergeShell.cpp
37 * @brief Implementation of the ShellExtension class
39 // ID line follows -- this is updated by SVN
40 // $Id: WinMergeShell.cpp 6933 2009-07-26 14:07:03Z kimmov $
43 #include "ShellExtension.h"
44 #include "WinMergeShell.h"
45 #include "UnicodeString.h"
47 #include <sys/types.h>
50 OBJECT_ENTRY_AUTO(CLSID_WinMergeShell, CWinMergeShell)
53 * @brief Flags for enabling and other settings of context menu.
57 EXT_ENABLED = 0x01, /**< ShellExtension enabled/disabled. */
58 EXT_ADVANCED = 0x02, /**< Advanced menuitems enabled/disabled. */
61 /// Max. filecount to select
62 static const int MaxFileCount = 3;
63 /// Registry path to WinMerge
64 #define REGDIR _T("Software\\Thingamahoochie\\WinMerge")
65 static const TCHAR f_RegDir[] = REGDIR;
66 static const TCHAR f_RegLocaleDir[] = REGDIR _T("\\Locale");
67 static const TCHAR f_RegSettingsDir[] = REGDIR _T("\\Settings");
70 * @name Registry valuenames.
73 /** Shell context menuitem enabled/disabled */
74 static const TCHAR f_RegValueEnabled[] = _T("ContextMenuEnabled");
75 /** 'Saved' path in advanced mode */
76 static const TCHAR f_FirstSelection[] = _T("FirstSelection");
77 /** Path to WinMergeU.exe */
78 static const TCHAR f_RegValuePath[] = _T("Executable");
79 /** Path to WinMergeU.exe, overwrites f_RegValuePath if present. */
80 static const TCHAR f_RegValuePriPath[] = _T("PriExecutable");
82 static const TCHAR f_LanguageId[] = _T("LanguageId");
84 static const TCHAR f_Recurse[] = _T("Recurse");
88 * @brief The states in which the menu can be.
89 * These states define what items are added to the menu and how those
94 MENU_SIMPLE = 0, /**< Simple menu, only "Compare item" is shown. */
95 MENU_ONESEL_NOPREV, /**< One item selected, no previous selections. */
96 MENU_ONESEL_PREV, /**< One item selected, previous selection exists. */
97 MENU_TWOSEL, /**< Two items are selected. */
101 static String GetResourceString(UINT resourceID);
103 // GreyMerlin (03 Sept 2017) - The following Version Info checking code is a
104 // short extract from the Microsoft <versionhelpers.h> file. Unfortunatly,
105 // that file is not available for WinXP-compatible Platform Toolsets (e.g.
106 // v141_xp for VS2017). Fortunatly, all the actual API interfaces do exist
107 // in WinXP (actually, in all Windows products since Win2000). Use of this
108 // <versionhelpers.h> code avoids the unpleasant deprecation of the GetVersionEx()
109 // API begining with Win 8.1. This Version Info checking code is also fully
110 // compatible with all non-XP-compatible Toolsets as well (e.g. v141).
112 #ifndef _WIN32_WINNT_VISTA
113 #define _WIN32_WINNT_VISTA 0x0600
115 #ifndef _WIN32_WINNT_WIN8
116 #define _WIN32_WINNT_WIN8 0x0602
119 #ifndef VERSIONHELPERAPI
120 #define VERSIONHELPERAPI inline bool
123 IsWindowsVersionOrGreater(WORD wMajorVersion, WORD wMinorVersion, WORD wServicePackMajor)
125 OSVERSIONINFOEXW osvi = { sizeof(osvi), 0, 0, 0, 0, {0}, 0, 0 };
126 DWORDLONG const dwlConditionMask = VerSetConditionMask(
129 0, VER_MAJORVERSION, VER_GREATER_EQUAL),
130 VER_MINORVERSION, VER_GREATER_EQUAL),
131 VER_SERVICEPACKMAJOR, VER_GREATER_EQUAL);
133 osvi.dwMajorVersion = wMajorVersion;
134 osvi.dwMinorVersion = wMinorVersion;
135 osvi.wServicePackMajor = wServicePackMajor;
137 return VerifyVersionInfoW(&osvi, VER_MAJORVERSION | VER_MINORVERSION | VER_SERVICEPACKMAJOR, dwlConditionMask) != FALSE;
142 IsWindows8OrGreater()
144 return IsWindowsVersionOrGreater(HIBYTE(_WIN32_WINNT_WIN8), LOBYTE(_WIN32_WINNT_WIN8), 0);
146 #endif // VERSIONHELPERAPI
149 * @brief Load a string from resource.
150 * @param [in] Resource string ID.
151 * @return String loaded from resource.
153 static String GetResourceString(UINT resourceID)
155 TCHAR resStr[1024] = {0};
156 int res = LoadString(_AtlComModule.m_hInstTypeLib, resourceID, resStr, 1024);
158 String strResource = resStr;
162 HBITMAP ConvertHICONtoHBITMAP(HICON hIcon, int cx, int cy)
165 BITMAPINFO bmi = { { sizeof(BITMAPINFOHEADER), cx, cy, 1, IsWindows8OrGreater() ? 32u : 24u } };
166 HBITMAP hbmp = CreateDIBSection(NULL, (BITMAPINFO*)&bmi, DIB_RGB_COLORS, &lpBits, NULL, 0);
167 HDC hdcMem = CreateCompatibleDC(NULL);
168 HBITMAP hbmpPrev = (HBITMAP)SelectObject(hdcMem, hbmp);
169 RECT rc = { 0, 0, cx, cy };
170 if (bmi.bmiHeader.biBitCount <= 24)
172 SetBkColor(hdcMem, GetSysColor(COLOR_MENU));
173 ExtTextOut(hdcMem, 0, 0, ETO_OPAQUE, &rc, NULL, 0, NULL);
175 DrawIconEx(hdcMem, 0, 0, hIcon, cx, cy, 0, NULL, DI_NORMAL);
176 SelectObject(hdcMem, hbmpPrev);
181 /////////////////////////////////////////////////////////////////////////////
184 /// Default constructor, loads icon bitmap
185 CWinMergeShell::CWinMergeShell()
188 int cx = GetSystemMetrics(SM_CXMENUCHECK);
189 int cy = GetSystemMetrics(SM_CYMENUCHECK);
191 // compress or stretch icon bitmap according to menu item height
192 HICON hMergeIcon = (HICON)LoadImage(_AtlComModule.m_hInstTypeLib, MAKEINTRESOURCE(IDI_WINMERGE), IMAGE_ICON,
193 cx, cy, LR_DEFAULTCOLOR);
194 HICON hMergeDirIcon = (HICON)LoadImage(_AtlComModule.m_hInstTypeLib, MAKEINTRESOURCE(IDI_WINMERGEDIR), IMAGE_ICON,
195 cx, cy, LR_DEFAULTCOLOR);
197 m_MergeBmp = ConvertHICONtoHBITMAP(hMergeIcon, cx, cy);
198 m_MergeDirBmp = ConvertHICONtoHBITMAP(hMergeDirIcon, cx, cy);
200 DestroyIcon(hMergeIcon);
201 DestroyIcon(hMergeDirIcon);
205 /// Default destructor, unloads bitmap
206 CWinMergeShell::~CWinMergeShell()
208 DeleteObject(m_MergeDirBmp);
209 DeleteObject(m_MergeBmp);
212 /// Reads selected paths
213 HRESULT CWinMergeShell::Initialize(LPCITEMIDLIST pidlFolder,
214 LPDATAOBJECT pDataObj, HKEY hProgID)
216 HRESULT hr = E_INVALIDARG;
218 for (auto& path: m_strPaths)
221 // Files/folders selected normally from the explorer
224 FORMATETC fmt = {CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
225 STGMEDIUM stg = {TYMED_HGLOBAL};
228 // Look for CF_HDROP data in the data object.
229 if (FAILED(pDataObj->GetData(&fmt, &stg)))
230 // Nope! Return an "invalid argument" error back to Explorer.
233 // Get a pointer to the actual data.
234 hDropInfo = (HDROP) GlobalLock(stg.hGlobal);
236 // Make sure it worked.
237 if (NULL == hDropInfo)
240 // Sanity check & make sure there is at least one filename.
241 UINT uNumFilesDropped = DragQueryFile(hDropInfo, 0xFFFFFFFF, NULL, 0);
242 m_nSelectedItems = uNumFilesDropped;
244 if (uNumFilesDropped == 0)
246 GlobalUnlock(stg.hGlobal);
247 ReleaseStgMedium(&stg);
253 // Get all file names.
254 for (WORD x = 0 ; x < uNumFilesDropped; x++)
256 // Get the number of bytes required by the file's full pathname
257 UINT wPathnameSize = DragQueryFile(hDropInfo, x, NULL, 0);
259 // Allocate memory to contain full pathname & zero byte
261 LPTSTR npszFile = (TCHAR *) new TCHAR[wPathnameSize];
263 // If not enough memory, skip this one
264 if (npszFile == NULL)
267 // Copy the pathname into the buffer
268 DragQueryFile(hDropInfo, x, npszFile, wPathnameSize);
270 if (x < MaxFileCount)
271 m_strPaths[x] = npszFile;
275 GlobalUnlock(stg.hGlobal);
276 ReleaseStgMedium(&stg);
280 m_nSelectedItems = 0;
283 // No item selected - selection is the folder background
286 TCHAR szPath[MAX_PATH] = {0};
288 if (SHGetPathFromIDList(pidlFolder, szPath))
290 if (m_nSelectedItems < MaxFileCount)
291 m_strPaths[m_nSelectedItems++] = szPath;
302 /// Adds context menu item
303 HRESULT CWinMergeShell::QueryContextMenu(HMENU hmenu, UINT uMenuIndex,
304 UINT uidFirstCmd, UINT uidLastCmd, UINT uFlags)
308 // If the flags include CMF_DEFAULTONLY then we shouldn't do anything.
309 if (uFlags & CMF_DEFAULTONLY)
310 return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 0);
312 // Check if user wants to use context menu
314 if (reg.Open(HKEY_CURRENT_USER, f_RegDir) != ERROR_SUCCESS)
315 return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 0);
317 m_dwContextMenuEnabled = reg.ReadDword(f_RegValueEnabled, 0);
318 m_strPreviousPath = reg.ReadString(f_FirstSelection, _T("")).c_str();
320 if (m_dwContextMenuEnabled & EXT_ENABLED) // Context menu enabled
322 // Check if advanced menuitems enabled
323 if ((m_dwContextMenuEnabled & EXT_ADVANCED) == 0)
325 m_dwMenuState = MENU_SIMPLE;
326 nItemsAdded = DrawSimpleMenu(hmenu, uMenuIndex, uidFirstCmd);
330 if (m_nSelectedItems == 1 && m_strPreviousPath.empty())
331 m_dwMenuState = MENU_ONESEL_NOPREV;
332 else if (m_nSelectedItems == 1 && !m_strPreviousPath.empty())
333 m_dwMenuState = MENU_ONESEL_PREV;
334 else if (m_nSelectedItems == 2)
335 m_dwMenuState = MENU_TWOSEL;
336 else if (m_nSelectedItems == 3)
337 m_dwMenuState = MENU_THREESEL;
339 nItemsAdded = DrawAdvancedMenu(hmenu, uMenuIndex, uidFirstCmd);
342 return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, nItemsAdded);
344 return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 0);
347 /// Gets string shown explorer's status bar when menuitem selected
348 HRESULT CWinMergeShell::GetCommandString(UINT_PTR idCmd, UINT uFlags,
349 UINT* pwReserved, LPSTR pszName, UINT cchMax)
353 // Check idCmd, it must be 0 in simple mode and 0 or 1 in advanced mode.
354 if ((m_dwMenuState & EXT_ADVANCED) == 0)
365 // If Explorer is asking for a help string, copy our string into the
367 if (uFlags & GCS_HELPTEXT)
371 strHelp = GetHelpText(idCmd);
373 if (uFlags & GCS_UNICODE)
374 // We need to cast pszName to a Unicode string, and then use the
375 // Unicode string copy API.
376 lstrcpynW((LPWSTR) pszName, T2CW(strHelp.c_str()), cchMax);
378 // Use the ANSI string copy API to return the help string.
379 lstrcpynA(pszName, T2CA(strHelp.c_str()), cchMax);
386 /// Runs WinMerge with given paths
387 HRESULT CWinMergeShell::InvokeCommand(LPCMINVOKECOMMANDINFO pCmdInfo)
390 String strWinMergePath;
391 BOOL bCompare = FALSE;
392 BOOL bAlterSubFolders = FALSE;
394 // If lpVerb really points to a string, ignore this function call and bail out.
395 if (HIWORD(pCmdInfo->lpVerb) != 0)
398 // Read WinMerge location from registry
399 if (!GetWinMergeDir(strWinMergePath))
402 // Check that file we are trying to execute exists
403 if (!PathFileExists(strWinMergePath.c_str()))
406 if (LOWORD(pCmdInfo->lpVerb) == 0)
408 switch (m_dwMenuState)
414 case MENU_ONESEL_NOPREV:
415 m_strPreviousPath = m_strPaths[0];
416 if (reg.Open(HKEY_CURRENT_USER, f_RegDir) == ERROR_SUCCESS)
417 reg.WriteString(f_FirstSelection, m_strPreviousPath.c_str());
420 case MENU_ONESEL_PREV:
421 m_strPaths[1] = m_strPaths[0];
422 m_strPaths[0] = m_strPreviousPath;
425 // Forget previous selection
426 if (reg.Open(HKEY_CURRENT_USER, f_RegDir) == ERROR_SUCCESS)
427 reg.WriteString(f_FirstSelection, _T(""));
432 // "Compare" - compare paths
434 m_strPreviousPath.erase();
438 else if (LOWORD(pCmdInfo->lpVerb) == 1)
440 switch (m_dwMenuState)
442 case MENU_ONESEL_PREV:
443 m_strPreviousPath = m_strPaths[0];
444 if (reg.Open(HKEY_CURRENT_USER, f_RegDir) == ERROR_SUCCESS)
445 reg.WriteString(f_FirstSelection, m_strPreviousPath.c_str());
449 // "Compare..." - user wants to compare this single item and open WinMerge
450 m_strPaths[1].erase();
451 m_strPaths[2].erase();
459 if (bCompare == FALSE)
462 if ((GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0)
463 bAlterSubFolders = TRUE;
465 String strCommandLine = FormatCmdLine(strWinMergePath,
466 m_strPaths[0], m_strPaths[1], m_strPaths[2], bAlterSubFolders);
468 // Finally start a new WinMerge process
470 STARTUPINFO stInfo = {0};
471 stInfo.cb = sizeof(STARTUPINFO);
472 PROCESS_INFORMATION processInfo = {0};
474 retVal = CreateProcess(NULL, (LPTSTR)strCommandLine.c_str(),
475 NULL, NULL, FALSE, CREATE_DEFAULT_ERROR_MODE, NULL, NULL,
476 &stInfo, &processInfo);
480 CloseHandle(processInfo.hThread);
481 CloseHandle(processInfo.hProcess);
483 else if (GetLastError() == ERROR_ELEVATION_REQUIRED)
485 String strCommandLine = FormatCmdLine(_T(""),
486 m_strPaths[0], m_strPaths[1], m_strPaths[2], bAlterSubFolders);
487 HINSTANCE hInstance = ShellExecute(nullptr, _T("runas"), strWinMergePath.c_str(), strCommandLine.c_str(), 0, SW_SHOWNORMAL);
488 if (reinterpret_cast<intptr_t>(hInstance) < 32)
499 /// Reads WinMerge path from registry
500 BOOL CWinMergeShell::GetWinMergeDir(String &strDir)
503 if (!reg.QueryRegUser(f_RegDir))
506 // Try first reading debug/test value
507 strDir = reg.ReadString(f_RegValuePriPath, _T(""));
510 strDir = reg.ReadString(f_RegValuePath, _T(""));
518 /// Create menu for simple mode
519 int CWinMergeShell::DrawSimpleMenu(HMENU hmenu, UINT uMenuIndex,
522 String strMenu = GetResourceString(IDS_CONTEXT_MENU);
523 InsertMenu(hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd, strMenu.c_str());
526 HBITMAP hBitmap = PathIsDirectory(m_strPaths[0].c_str()) ? m_MergeDirBmp : m_MergeBmp;
528 SetMenuItemBitmaps(hmenu, uMenuIndex, MF_BYPOSITION, hBitmap, NULL);
530 // Show menu item as grayed if more than two items selected
531 if (m_nSelectedItems > MaxFileCount)
532 EnableMenuItem(hmenu, uMenuIndex, MF_BYPOSITION | MF_GRAYED);
537 /// Create menu for advanced mode
538 int CWinMergeShell::DrawAdvancedMenu(HMENU hmenu, UINT uMenuIndex,
541 String strCompare = GetResourceString(IDS_COMPARE);
542 String strCompareEllipsis = GetResourceString(IDS_COMPARE_ELLIPSIS);
543 String strCompareTo = GetResourceString(IDS_COMPARE_TO);
544 String strReselect = GetResourceString(IDS_RESELECT_FIRST);
547 switch (m_dwMenuState)
549 // No items selected earlier
550 // Select item as first item to compare
551 case MENU_ONESEL_NOPREV:
552 InsertMenu(hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd,
553 strCompareTo.c_str());
556 InsertMenu(hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd,
557 strCompareEllipsis.c_str());
561 // One item selected earlier:
562 // Allow re-selecting first item or selecting second item
563 case MENU_ONESEL_PREV:
564 InsertMenu(hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd,
568 InsertMenu(hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd,
569 strReselect.c_str());
573 // Two items selected
574 // Select both items for compare
577 InsertMenu(hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd,
583 InsertMenu(hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd,
590 HBITMAP hBitmap = PathIsDirectory(m_strPaths[0].c_str()) ? m_MergeDirBmp : m_MergeBmp;
593 if (nItemsAdded == 2)
594 SetMenuItemBitmaps(hmenu, uMenuIndex - 1, MF_BYPOSITION, hBitmap, NULL);
595 SetMenuItemBitmaps(hmenu, uMenuIndex, MF_BYPOSITION, hBitmap, NULL);
598 // Show menu item as grayed if more than two items selected
599 if (m_nSelectedItems > MaxFileCount)
601 if (nItemsAdded == 2)
602 EnableMenuItem(hmenu, uMenuIndex - 1, MF_BYPOSITION | MF_GRAYED);
603 EnableMenuItem(hmenu, uMenuIndex, MF_BYPOSITION | MF_GRAYED);
609 /// Determine help text shown in explorer's statusbar
610 String CWinMergeShell::GetHelpText(UINT_PTR idCmd)
614 // More than two items selected, advice user
615 if (m_nSelectedItems > MaxFileCount)
617 strHelp = GetResourceString(IDS_CONTEXT_HELP_MANYITEMS);
623 switch (m_dwMenuState)
626 strHelp = GetResourceString(IDS_CONTEXT_HELP);;
629 case MENU_ONESEL_NOPREV:
630 strHelp = GetResourceString(IDS_HELP_SAVETHIS);
633 case MENU_ONESEL_PREV:
634 strHelp = GetResourceString(IDS_HELP_COMPARESAVED);
635 strutils::replace(strHelp, _T("%1"), m_strPreviousPath);
640 strHelp = GetResourceString(IDS_CONTEXT_HELP);
646 switch (m_dwMenuState)
648 case MENU_ONESEL_PREV:
649 strHelp = GetResourceString(IDS_HELP_SAVETHIS);
652 strHelp = GetResourceString(IDS_CONTEXT_HELP);
659 /// Format commandline used to start WinMerge
660 String CWinMergeShell::FormatCmdLine(const String &winmergePath,
661 const String &path1, const String &path2, const String &path3, BOOL bAlterSubFolders)
663 String strCommandline = winmergePath.empty() ? _T("") : _T("\"") + winmergePath + _T("\"");
665 // Check if user wants to use context menu
666 BOOL bSubfoldersByDefault = FALSE;
668 if (reg.Open(HKEY_CURRENT_USER, f_RegSettingsDir) == ERROR_SUCCESS)
669 bSubfoldersByDefault = reg.ReadBool(f_Recurse, FALSE);
671 if (bAlterSubFolders && !bSubfoldersByDefault)
672 strCommandline += _T(" /r");
673 else if (!bAlterSubFolders && bSubfoldersByDefault)
674 strCommandline += _T(" /r");
676 strCommandline += _T(" \"") + path1 + _T("\"");
678 if (!m_strPaths[1].empty())
679 strCommandline += _T(" \"") + path2 + _T("\"");
681 if (!m_strPaths[2].empty())
682 strCommandline += _T(" \"") + path3 + _T("\"");
684 return strCommandline;