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 // RCS ID line follows -- this is updated by CVS
43 #include "ShellExtension.h"
44 #include "WinMergeShell.h"
46 #include "coretools.h"
47 #include <sys/types.h>
51 * @brief Flags for enabling and other settings of context menu.
55 EXT_ENABLED = 0x01, /**< ShellExtension enabled/disabled. */
56 EXT_ADVANCED = 0x02, /**< Advanced menuitems enabled/disabled. */
57 EXT_SUBFOLDERS = 0x04, /**< Subfolders included by default? */
60 /// Max. filecount to select
61 static const int MaxFileCount = 2;
62 /// Registry path to WinMerge
63 #define REGDIR _T("Software\\Thingamahoochie\\WinMerge")
64 static const TCHAR f_RegDir[] = REGDIR;
65 static const TCHAR f_RegLocaleDir[] = REGDIR _T("\\Locale");
68 * @name Registry valuenames.
71 /** Shell context menuitem enabled/disabled */
72 static const TCHAR f_RegValueEnabled[] = _T("ContextMenuEnabled");
73 /** 'Saved' path in advanced mode */
74 static const TCHAR f_FirstSelection[] = _T("FirstSelection");
75 /** Path to WinMerge[U].exe */
76 static const TCHAR f_RegValuePath[] = _T("Executable");
77 /** Path to WinMerge[U].exe, overwrites f_RegValuePath if present. */
78 static const TCHAR f_RegValuePriPath[] = _T("PriExecutable");
80 static const TCHAR f_LanguageId[] = _T("LanguageId");
92 #define USES_WINMERGELOCALE CWinMergeTempLocale __wmtl__
94 class CWinMergeTempLocale
99 CWinMergeTempLocale() {
101 if (reg.Open(HKEY_CURRENT_USER, f_RegLocaleDir) != ERROR_SUCCESS)
104 m_lcidOld = GetThreadLocale();
106 int iLangId = reg.ReadDword(f_LanguageId, (DWORD)-1);
108 SetThreadLocale(MAKELCID(iLangId, SORT_DEFAULT));
110 ~CWinMergeTempLocale() {
111 SetThreadLocale(m_lcidOld);
115 /////////////////////////////////////////////////////////////////////////////
118 /// Default constructor, loads icon bitmap
119 CWinMergeShell::CWinMergeShell()
122 HBITMAP hMergeBmp = LoadBitmap(_Module.GetModuleInstance(),
123 MAKEINTRESOURCE(IDB_WINMERGE));
124 m_MergeBmp.Attach(hMergeBmp);
127 /// Reads selected paths
128 HRESULT CWinMergeShell::Initialize(LPCITEMIDLIST pidlFolder,
129 LPDATAOBJECT pDataObj, HKEY hProgID)
131 FORMATETC fmt = {CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
132 STGMEDIUM stg = {TYMED_HGLOBAL};
136 // Look for CF_HDROP data in the data object.
137 if (FAILED(pDataObj->GetData(&fmt, &stg)))
138 // Nope! Return an "invalid argument" error back to Explorer.
141 // Get a pointer to the actual data.
142 hDropInfo = (HDROP) GlobalLock(stg.hGlobal);
144 // Make sure it worked.
145 if (NULL == hDropInfo)
148 // Sanity check & make sure there is at least one filename.
149 UINT uNumFilesDropped = DragQueryFile (hDropInfo, 0xFFFFFFFF, NULL, 0);
150 m_nSelectedItems = uNumFilesDropped;
152 if (uNumFilesDropped == 0)
154 GlobalUnlock(stg.hGlobal);
155 ReleaseStgMedium(&stg);
161 // Get all file names.
162 for (WORD x = 0 ; x < uNumFilesDropped; x++)
164 // Get the number of bytes required by the file's full pathname
165 UINT wPathnameSize = DragQueryFile(hDropInfo, x, NULL, 0);
167 // Allocate memory to contain full pathname & zero byte
169 LPTSTR npszFile = (TCHAR *) new TCHAR[wPathnameSize];
171 // If not enough memory, skip this one
172 if (npszFile == NULL)
175 // Copy the pathname into the buffer
176 DragQueryFile(hDropInfo, x, npszFile, wPathnameSize);
178 if (x < MaxFileCount)
179 m_strPaths[x] = npszFile;
183 GlobalUnlock(stg.hGlobal);
184 ReleaseStgMedium(&stg);
189 /// Adds context menu item
190 HRESULT CWinMergeShell::QueryContextMenu(HMENU hmenu, UINT uMenuIndex,
191 UINT uidFirstCmd, UINT uidLastCmd, UINT uFlags)
193 AFX_MANAGE_STATE(AfxGetStaticModuleState())
197 // If the flags include CMF_DEFAULTONLY then we shouldn't do anything.
198 if (uFlags & CMF_DEFAULTONLY)
199 return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 0);
201 // Check if user wants to use context menu
203 if (reg.Open(HKEY_CURRENT_USER, f_RegDir) != ERROR_SUCCESS)
204 return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 0);
206 m_dwContextMenuEnabled = reg.ReadDword(f_RegValueEnabled, 0);
207 m_strPreviousPath = reg.ReadString(f_FirstSelection, _T(""));
209 if (m_dwContextMenuEnabled & EXT_ENABLED) // Context menu enabled
211 // Check if advanced menuitems enabled
212 if ((m_dwContextMenuEnabled & EXT_ADVANCED) == 0)
214 m_dwMenuState = MENU_SIMPLE;
215 nItemsAdded = DrawSimpleMenu(hmenu, uMenuIndex, uidFirstCmd);
219 if (m_nSelectedItems == 1 && m_strPreviousPath.IsEmpty())
220 m_dwMenuState = MENU_ONESEL_NOPREV;
221 else if (m_nSelectedItems == 1 && !m_strPreviousPath.IsEmpty())
222 m_dwMenuState = MENU_ONESEL_PREV;
223 else if (m_nSelectedItems == 2)
224 m_dwMenuState = MENU_TWOSEL;
226 nItemsAdded = DrawAdvancedMenu(hmenu, uMenuIndex, uidFirstCmd);
229 return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, nItemsAdded);
231 return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 0);
234 /// Gets string shown explorer's status bar when menuitem selected
235 HRESULT CWinMergeShell::GetCommandString(UINT idCmd, UINT uFlags,
236 UINT* pwReserved, LPSTR pszName, UINT cchMax)
238 AFX_MANAGE_STATE(AfxGetStaticModuleState())
242 // Check idCmd, it must be 0 in simple mode and 0 or 1 in advanced mode.
243 if ((m_dwMenuState & EXT_ADVANCED) == 0)
254 // If Explorer is asking for a help string, copy our string into the
256 if (uFlags & GCS_HELPTEXT)
260 strHelp = GetHelpText(idCmd);
262 if (uFlags & GCS_UNICODE)
263 // We need to cast pszName to a Unicode string, and then use the
264 // Unicode string copy API.
265 lstrcpynW((LPWSTR) pszName, T2CW(strHelp), cchMax);
267 // Use the ANSI string copy API to return the help string.
268 lstrcpynA(pszName, T2CA(strHelp), cchMax);
275 /// Runs WinMerge with given paths
276 HRESULT CWinMergeShell::InvokeCommand(LPCMINVOKECOMMANDINFO pCmdInfo)
278 AFX_MANAGE_STATE(AfxGetStaticModuleState())
280 CString strWinMergePath;
281 BOOL bCompare = FALSE;
282 BOOL bAlterSubFolders = FALSE;
285 // If lpVerb really points to a string, ignore this function call and bail out.
286 if (HIWORD(pCmdInfo->lpVerb) != 0)
289 // Read WinMerge location from registry
290 if (!GetWinMergeDir(strWinMergePath))
293 // Check that file we are trying to execute exists and is executable
294 if (!CheckExecutable(strWinMergePath))
297 if (LOWORD(pCmdInfo->lpVerb) == 0)
299 switch (m_dwMenuState)
305 case MENU_ONESEL_NOPREV:
306 m_strPreviousPath = m_strPaths[0];
307 if (reg.Open(HKEY_CURRENT_USER, f_RegDir) == ERROR_SUCCESS)
308 reg.WriteString(f_FirstSelection, m_strPreviousPath);
311 case MENU_ONESEL_PREV:
312 m_strPaths[1] = m_strPaths[0];
313 m_strPaths[0] = m_strPreviousPath;
316 // Forget previous selection
317 if (reg.Open(HKEY_CURRENT_USER, f_RegDir) == ERROR_SUCCESS)
318 reg.WriteString(f_FirstSelection, _T(""));
322 // "Compare" - compare paths
324 m_strPreviousPath.Empty();
328 else if (LOWORD(pCmdInfo->lpVerb) == 1)
330 switch (m_dwMenuState)
332 case MENU_ONESEL_PREV:
333 m_strPreviousPath = m_strPaths[0];
334 if (reg.Open(HKEY_CURRENT_USER, f_RegDir) == ERROR_SUCCESS)
335 reg.WriteString(f_FirstSelection, m_strPreviousPath);
339 // "Compare..." - user wants to compare this single item and open WinMerge
340 m_strPaths[1].Empty();
348 if (bCompare == FALSE)
351 if ((GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0)
352 bAlterSubFolders = TRUE;
354 CString strCommandLine = FormatCmdLine(strWinMergePath, m_strPaths[0],
355 m_strPaths[1], bAlterSubFolders);
357 // Finally start a new WinMerge process
359 STARTUPINFO stInfo = {0};
360 stInfo.cb = sizeof(STARTUPINFO);
361 PROCESS_INFORMATION processInfo = {0};
363 retVal = CreateProcess(NULL, (LPTSTR)(LPCTSTR)strCommandLine,
364 NULL, NULL, FALSE, CREATE_DEFAULT_ERROR_MODE, NULL, NULL,
365 &stInfo, &processInfo);
370 CloseHandle(processInfo.hThread);
371 CloseHandle(processInfo.hProcess);
375 /// Reads WinMerge path from registry
376 BOOL CWinMergeShell::GetWinMergeDir(CString &strDir)
379 if (!reg.QueryRegUser(f_RegDir))
382 // Try first reading debug/test value
383 strDir = reg.ReadString(f_RegValuePriPath, _T(""));
384 if (strDir.IsEmpty())
386 strDir = reg.ReadString(f_RegValuePath, _T(""));
387 if (strDir.IsEmpty())
394 /// Checks if given file exists and is executable
395 BOOL CWinMergeShell::CheckExecutable(CString path)
398 SplitFilename(path, NULL, NULL, &ext);
402 if (ext == _T("exe") || ext == _T("cmd") || ext == ("bat"))
404 // Check if file exists
405 struct _stati64 statBuffer;
406 int nRetVal = _tstati64(path, &statBuffer);
413 /// Create menu for simple mode
414 int CWinMergeShell::DrawSimpleMenu(HMENU hmenu, UINT uMenuIndex,
418 VERIFY(strMenu.LoadString(IDS_CONTEXT_MENU));
420 InsertMenu(hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd, strMenu);
423 if ((HBITMAP)m_MergeBmp != NULL)
424 SetMenuItemBitmaps(hmenu, uMenuIndex, MF_BYPOSITION, m_MergeBmp, NULL);
426 // Show menu item as grayed if more than two items selected
427 if (m_nSelectedItems > MaxFileCount)
428 EnableMenuItem(hmenu, uMenuIndex, MF_BYPOSITION | MF_GRAYED);
433 /// Create menu for advanced mode
434 int CWinMergeShell::DrawAdvancedMenu(HMENU hmenu, UINT uMenuIndex,
438 CString strCompareEllipsis;
439 CString strCompareTo;
443 VERIFY(strCompare.LoadString(IDS_COMPARE));
444 VERIFY(strCompareEllipsis.LoadString(IDS_COMPARE_ELLIPSIS));
445 VERIFY(strCompareTo.LoadString(IDS_COMPARE_TO));
446 VERIFY(strReselect.LoadString(IDS_RESELECT_FIRST));
448 switch (m_dwMenuState)
450 // No items selected earlier
451 // Select item as first item to compare
452 case MENU_ONESEL_NOPREV:
453 InsertMenu(hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd, strCompareTo);
456 InsertMenu(hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd, strCompareEllipsis);
460 // One item selected earlier:
461 // Allow re-selecting first item or selecting second item
462 case MENU_ONESEL_PREV:
463 InsertMenu(hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd, strCompare);
466 InsertMenu(hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd, strReselect);
470 // Two items selected
471 // Select both items for compare
473 InsertMenu(hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd, strCompare);
478 InsertMenu(hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd, strCompare);
484 if ((HBITMAP)m_MergeBmp != NULL)
486 if (nItemsAdded == 2)
487 SetMenuItemBitmaps(hmenu, uMenuIndex - 1, MF_BYPOSITION, m_MergeBmp, NULL);
488 SetMenuItemBitmaps(hmenu, uMenuIndex, MF_BYPOSITION, m_MergeBmp, NULL);
491 // Show menu item as grayed if more than two items selected
492 if (m_nSelectedItems > MaxFileCount)
494 if (nItemsAdded == 2)
495 EnableMenuItem(hmenu, uMenuIndex - 1, MF_BYPOSITION | MF_GRAYED);
496 EnableMenuItem(hmenu, uMenuIndex, MF_BYPOSITION | MF_GRAYED);
502 /// Determine help text shown in explorer's statusbar
503 CString CWinMergeShell::GetHelpText(int idCmd)
507 // More than two items selected, advice user
508 if (m_nSelectedItems > MaxFileCount)
510 VERIFY(strHelp.LoadString(IDS_CONTEXT_HELP_MANYITEMS));
516 switch (m_dwMenuState)
519 VERIFY(strHelp.LoadString(IDS_CONTEXT_HELP));
522 case MENU_ONESEL_NOPREV:
523 VERIFY(strHelp.LoadString(IDS_HELP_SAVETHIS));
526 case MENU_ONESEL_PREV:
527 AfxFormatString1(strHelp, IDS_HELP_COMPARESAVED, m_strPreviousPath);
531 VERIFY(strHelp.LoadString(IDS_CONTEXT_HELP));
537 switch (m_dwMenuState)
539 case MENU_ONESEL_PREV:
540 VERIFY(strHelp.LoadString(IDS_HELP_SAVETHIS));
543 VERIFY(strHelp.LoadString(IDS_CONTEXT_HELP));
550 /// Format commandline used to start WinMerge
551 CString CWinMergeShell::FormatCmdLine(const CString &winmergePath,
552 const CString &path1, const CString &path2, BOOL bAlterSubFolders)
554 CString strCommandline = winmergePath;
555 BOOL bOnlyFiles = FALSE;
557 if (!path1.IsEmpty() && !path2.IsEmpty())
561 if (CFile::GetStatus(path1, status) &&
562 CFile::GetStatus(path2, status2))
564 // Check if both paths are files
565 if ((status.m_attribute & CFile::Attribute::directory) == 0 &&
566 (status2.m_attribute & CFile::Attribute::directory) == 0)
573 // Check if user wants to use context menu
574 BOOL bSubfoldersByDefault = FALSE;
575 if (m_dwContextMenuEnabled & EXT_SUBFOLDERS) // User wants subfolders by def
576 bSubfoldersByDefault = TRUE;
578 if (bAlterSubFolders && !bSubfoldersByDefault)
579 strCommandline += _T(" /r");
580 else if (!bAlterSubFolders && bSubfoldersByDefault)
581 strCommandline += _T(" /r");
583 strCommandline += _T(" \"") + path1 + _T("\"");
585 if (!m_strPaths[1].IsEmpty())
586 strCommandline += _T(" \"") + path2 + _T("\"");
588 return strCommandline;