OSDN Git Service

PATCH: [ 1535175 ] Shell ext: Add flag for subfolder inclusion
[winmerge-jp/winmerge-jp.git] / ShellExtension / WinMergeShell.cpp
1 /////////////////////////////////////////////////////////////////////////////
2 //    License (GPLv2+):
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.
7 //    
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.
12 //
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.
19 // 
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)
26 //
27 // HKEY_CURRENT_USER\Software\Thingamahoochie\WinMerge\FirstSelection
28 //  is used to store path for first selection in advanced mode
29 //
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 /////////////////////////////////////////////////////////////////////////////
34 /** 
35  * @file  WinMergeShell.cpp
36  *
37  * @brief Implementation of the ShellExtension class
38  */
39 // RCS ID line follows -- this is updated by CVS
40 // $Id$
41
42 #include "stdafx.h"
43 #include "ShellExtension.h"
44 #include "WinMergeShell.h"
45 #include "RegKey.h"
46 #include "coretools.h"
47 #include <sys/types.h>
48 #include <sys/stat.h>
49
50 /** 
51  * @brief Flags for enabling and other settings of context menu.
52  */
53 enum ExtensionFlags
54 {
55         EXT_ENABLED = 0x01, /**< ShellExtension enabled/disabled. */
56         EXT_ADVANCED = 0x02, /**< Advanced menuitems enabled/disabled. */
57         EXT_SUBFOLDERS = 0x04, /**< Subfolders included by default? */
58 };
59
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");
66
67 /**
68  * @name Registry valuenames.
69  */
70 /*@{*/ 
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");
79 /** LanguageId */
80 static const TCHAR f_LanguageId[] = _T("LanguageId");
81 /*@}*/
82
83 /// Shown menustate
84 enum
85 {
86         MENU_SIMPLE = 0,
87         MENU_ONESEL_NOPREV,
88         MENU_ONESEL_PREV,
89         MENU_TWOSEL,
90 };
91
92 #define USES_WINMERGELOCALE CWinMergeTempLocale __wmtl__
93
94 class CWinMergeTempLocale
95 {
96 private:
97         LCID m_lcidOld;
98 public:
99         CWinMergeTempLocale() {
100                 CRegKeyEx reg;
101                 if (reg.Open(HKEY_CURRENT_USER, f_RegLocaleDir) != ERROR_SUCCESS)
102                         return;
103
104                 m_lcidOld = GetThreadLocale();
105
106                 int iLangId = reg.ReadDword(f_LanguageId, (DWORD)-1);
107                 if (iLangId != -1)
108                         SetThreadLocale(MAKELCID(iLangId, SORT_DEFAULT));
109         }
110         ~CWinMergeTempLocale() {
111                 SetThreadLocale(m_lcidOld);
112         }
113 };
114
115 /////////////////////////////////////////////////////////////////////////////
116 // CWinMergeShell
117
118 /// Default constructor, loads icon bitmap
119 CWinMergeShell::CWinMergeShell()
120 {
121         m_dwMenuState = 0;
122         HBITMAP hMergeBmp = LoadBitmap(_Module.GetModuleInstance(),
123                         MAKEINTRESOURCE(IDB_WINMERGE));
124         m_MergeBmp.Attach(hMergeBmp);
125 }
126
127 /// Reads selected paths
128 HRESULT CWinMergeShell::Initialize(LPCITEMIDLIST pidlFolder,
129                 LPDATAOBJECT pDataObj, HKEY hProgID)
130 {
131         FORMATETC fmt = {CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
132         STGMEDIUM stg = {TYMED_HGLOBAL};
133         HDROP hDropInfo;
134         USES_WINMERGELOCALE;
135
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.
139                 return E_INVALIDARG;
140
141         // Get a pointer to the actual data.
142         hDropInfo = (HDROP) GlobalLock(stg.hGlobal);
143
144         // Make sure it worked.
145         if (NULL == hDropInfo)
146                 return E_INVALIDARG;
147
148         // Sanity check & make sure there is at least one filename.
149         UINT uNumFilesDropped = DragQueryFile (hDropInfo, 0xFFFFFFFF, NULL, 0);
150         m_nSelectedItems = uNumFilesDropped;
151
152         if (uNumFilesDropped == 0)
153         {
154                 GlobalUnlock(stg.hGlobal);
155                 ReleaseStgMedium(&stg);
156                 return E_INVALIDARG;
157         }
158
159         HRESULT hr = S_OK;
160
161         // Get all file names.
162         for (WORD x = 0 ; x < uNumFilesDropped; x++)
163         {
164                 // Get the number of bytes required by the file's full pathname
165                 UINT wPathnameSize = DragQueryFile(hDropInfo, x, NULL, 0);
166
167                 // Allocate memory to contain full pathname & zero byte
168                 wPathnameSize += 1;
169                 LPTSTR npszFile = (TCHAR *) new TCHAR[wPathnameSize];
170
171                 // If not enough memory, skip this one
172                 if (npszFile == NULL)
173                         continue;
174
175                 // Copy the pathname into the buffer
176                 DragQueryFile(hDropInfo, x, npszFile, wPathnameSize);
177
178                 if (x < MaxFileCount)
179                         m_strPaths[x] = npszFile;
180
181                 delete[] npszFile;
182         }
183         GlobalUnlock(stg.hGlobal);
184         ReleaseStgMedium(&stg);
185
186     return hr;
187 }
188
189 /// Adds context menu item
190 HRESULT CWinMergeShell::QueryContextMenu(HMENU hmenu, UINT uMenuIndex,
191                 UINT uidFirstCmd, UINT uidLastCmd, UINT uFlags)
192 {
193         AFX_MANAGE_STATE(AfxGetStaticModuleState())
194         int nItemsAdded = 0;
195         USES_WINMERGELOCALE;
196
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);
200
201         // Check if user wants to use context menu
202         CRegKeyEx reg;
203         if (reg.Open(HKEY_CURRENT_USER, f_RegDir) != ERROR_SUCCESS)
204                 return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 0);
205
206         m_dwContextMenuEnabled = reg.ReadDword(f_RegValueEnabled, 0);
207         m_strPreviousPath = reg.ReadString(f_FirstSelection, _T(""));
208
209         if (m_dwContextMenuEnabled & EXT_ENABLED) // Context menu enabled
210         {
211                 // Check if advanced menuitems enabled
212                 if ((m_dwContextMenuEnabled & EXT_ADVANCED) == 0)
213                 {
214                         m_dwMenuState = MENU_SIMPLE;
215                         nItemsAdded = DrawSimpleMenu(hmenu, uMenuIndex, uidFirstCmd);
216                 }
217                 else
218                 {
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;
225
226                         nItemsAdded = DrawAdvancedMenu(hmenu, uMenuIndex, uidFirstCmd);
227                 }
228
229                 return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, nItemsAdded);
230         }
231         return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 0);
232 }
233
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)
237 {
238         AFX_MANAGE_STATE(AfxGetStaticModuleState())
239         USES_CONVERSION;
240         USES_WINMERGELOCALE;
241
242         // Check idCmd, it must be 0 in simple mode and 0 or 1 in advanced mode.
243         if ((m_dwMenuState & EXT_ADVANCED) == 0)
244         {
245                 if (idCmd > 0)
246                         return E_INVALIDARG;
247         }
248         else
249         {
250                 if (idCmd > 1)
251                         return E_INVALIDARG;
252         }
253
254         // If Explorer is asking for a help string, copy our string into the
255         // supplied buffer.
256         if (uFlags & GCS_HELPTEXT)
257         {
258                 CString strHelp;
259
260                 strHelp = GetHelpText(idCmd);
261
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);
266                 else
267                         // Use the ANSI string copy API to return the help string.
268                         lstrcpynA(pszName, T2CA(strHelp), cchMax);
269
270                 return S_OK;
271         }
272         return E_INVALIDARG;
273 }
274
275 /// Runs WinMerge with given paths
276 HRESULT CWinMergeShell::InvokeCommand(LPCMINVOKECOMMANDINFO pCmdInfo)
277 {
278         AFX_MANAGE_STATE(AfxGetStaticModuleState())
279         CRegKeyEx reg;
280         CString strWinMergePath;
281         BOOL bCompare = FALSE;
282         BOOL bAlterSubFolders = FALSE;
283         USES_WINMERGELOCALE;
284
285         // If lpVerb really points to a string, ignore this function call and bail out.
286         if (HIWORD(pCmdInfo->lpVerb) != 0)
287                 return E_INVALIDARG;
288
289         // Read WinMerge location from registry
290         if (!GetWinMergeDir(strWinMergePath))
291                 return S_FALSE;
292
293         // Check that file we are trying to execute exists and is executable
294         if (!CheckExecutable(strWinMergePath))
295                 return S_FALSE;
296
297         if (LOWORD(pCmdInfo->lpVerb) == 0)
298         {
299                 switch (m_dwMenuState)
300                 {
301                 case MENU_SIMPLE:
302                         bCompare = TRUE;
303                         break;
304
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);
309                         break;
310
311                 case MENU_ONESEL_PREV:
312                         m_strPaths[1] = m_strPaths[0];
313                         m_strPaths[0] = m_strPreviousPath;
314                         bCompare = TRUE;
315                         
316                         // Forget previous selection
317                         if (reg.Open(HKEY_CURRENT_USER, f_RegDir) == ERROR_SUCCESS)
318                                 reg.WriteString(f_FirstSelection, _T(""));
319                         break;
320
321                 case MENU_TWOSEL:
322                         // "Compare" - compare paths
323                         bCompare = TRUE;
324                         m_strPreviousPath.Empty();
325                         break;
326                 }
327         }
328         else if (LOWORD(pCmdInfo->lpVerb) == 1)
329         {
330                 switch (m_dwMenuState)
331                 {
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);
336                         bCompare = FALSE;
337                         break;
338                 default:
339                         // "Compare..." - user wants to compare this single item and open WinMerge
340                         m_strPaths[1].Empty();
341                         bCompare = TRUE;
342                         break;
343                 }
344         }
345         else
346                 return E_INVALIDARG;
347
348         if (bCompare == FALSE)
349                 return S_FALSE;
350
351         if ((GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0)
352                 bAlterSubFolders = TRUE;
353
354         CString strCommandLine = FormatCmdLine(strWinMergePath, m_strPaths[0],
355                 m_strPaths[1], bAlterSubFolders);
356
357         // Finally start a new WinMerge process
358         BOOL retVal = FALSE;
359         STARTUPINFO stInfo = {0};
360         stInfo.cb = sizeof(STARTUPINFO);
361         PROCESS_INFORMATION processInfo = {0};
362         
363         retVal = CreateProcess(NULL, (LPTSTR)(LPCTSTR)strCommandLine,
364                 NULL, NULL, FALSE, CREATE_DEFAULT_ERROR_MODE, NULL, NULL,
365                 &stInfo, &processInfo);
366
367         if (!retVal)
368                 return S_FALSE;
369
370         CloseHandle(processInfo.hThread);
371         CloseHandle(processInfo.hProcess);
372         return S_OK;
373 }
374
375 /// Reads WinMerge path from registry
376 BOOL CWinMergeShell::GetWinMergeDir(CString &strDir)
377 {
378         CRegKeyEx reg;
379         if (!reg.QueryRegUser(f_RegDir))
380                 return FALSE;
381         
382         // Try first reading debug/test value
383         strDir = reg.ReadString(f_RegValuePriPath, _T(""));
384         if (strDir.IsEmpty())
385         {
386                 strDir = reg.ReadString(f_RegValuePath, _T(""));
387                 if (strDir.IsEmpty())
388                         return FALSE;
389         }       
390
391         return TRUE;
392 }
393
394 /// Checks if given file exists and is executable
395 BOOL CWinMergeShell::CheckExecutable(CString path)
396 {
397         CString ext;
398         SplitFilename(path, NULL, NULL, &ext);
399
400         // Check extension
401         ext.MakeLower();
402         if (ext == _T("exe") || ext == _T("cmd") || ext == ("bat"))
403         {
404                 // Check if file exists
405                 struct _stati64 statBuffer;
406                 int nRetVal = _tstati64(path, &statBuffer);
407                 if (nRetVal > -1)
408                         return TRUE;
409         }
410         return FALSE;
411 }
412
413 /// Create menu for simple mode
414 int CWinMergeShell::DrawSimpleMenu(HMENU hmenu, UINT uMenuIndex,
415                 UINT uidFirstCmd)
416 {
417         CString strMenu;
418         VERIFY(strMenu.LoadString(IDS_CONTEXT_MENU));
419
420         InsertMenu(hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd, strMenu);
421         
422         // Add bitmap
423         if ((HBITMAP)m_MergeBmp != NULL)
424                 SetMenuItemBitmaps(hmenu, uMenuIndex, MF_BYPOSITION, m_MergeBmp, NULL);
425         
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);
429         
430         return 1;
431 }
432
433 /// Create menu for advanced mode
434 int CWinMergeShell::DrawAdvancedMenu(HMENU hmenu, UINT uMenuIndex,
435                 UINT uidFirstCmd)
436 {
437         CString strCompare;
438         CString strCompareEllipsis;
439         CString strCompareTo;
440         CString strReselect;
441         int nItemsAdded = 0;
442
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));
447
448         switch (m_dwMenuState)
449         {
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);
454                 uMenuIndex++;
455                 uidFirstCmd++;
456                 InsertMenu(hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd, strCompareEllipsis);
457                 nItemsAdded = 2;
458                 break;
459
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);
464                 uMenuIndex++;
465                 uidFirstCmd++;
466                 InsertMenu(hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd, strReselect);
467                 nItemsAdded = 2;
468                 break;
469
470         // Two items selected
471         // Select both items for compare
472         case MENU_TWOSEL:
473                 InsertMenu(hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd, strCompare);
474                 nItemsAdded = 1;
475                 break;
476
477         default:
478                 InsertMenu(hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd, strCompare);
479                 nItemsAdded = 1;
480                 break;
481         }
482         
483         // Add bitmap
484         if ((HBITMAP)m_MergeBmp != NULL)
485         {
486                 if (nItemsAdded == 2)
487                         SetMenuItemBitmaps(hmenu, uMenuIndex - 1, MF_BYPOSITION, m_MergeBmp, NULL);
488                 SetMenuItemBitmaps(hmenu, uMenuIndex, MF_BYPOSITION, m_MergeBmp, NULL);
489         }
490         
491         // Show menu item as grayed if more than two items selected
492         if (m_nSelectedItems > MaxFileCount)
493         {
494                 if (nItemsAdded == 2)
495                         EnableMenuItem(hmenu, uMenuIndex - 1, MF_BYPOSITION | MF_GRAYED);
496                 EnableMenuItem(hmenu, uMenuIndex, MF_BYPOSITION | MF_GRAYED);
497         }
498
499         return nItemsAdded;
500 }
501
502 /// Determine help text shown in explorer's statusbar
503 CString CWinMergeShell::GetHelpText(int idCmd)
504 {
505         CString strHelp;
506
507         // More than two items selected, advice user
508         if (m_nSelectedItems > MaxFileCount)
509         {
510                 VERIFY(strHelp.LoadString(IDS_CONTEXT_HELP_MANYITEMS));
511                 return strHelp;
512         }
513
514         if (idCmd == 0)
515         {
516                 switch (m_dwMenuState)
517                 {
518                 case MENU_SIMPLE:
519                         VERIFY(strHelp.LoadString(IDS_CONTEXT_HELP));
520                         break;
521
522                 case MENU_ONESEL_NOPREV:
523                         VERIFY(strHelp.LoadString(IDS_HELP_SAVETHIS));
524                         break;
525                 
526                 case MENU_ONESEL_PREV:
527                         AfxFormatString1(strHelp, IDS_HELP_COMPARESAVED, m_strPreviousPath);
528                         break;
529                 
530                 case MENU_TWOSEL:
531                         VERIFY(strHelp.LoadString(IDS_CONTEXT_HELP));
532                         break;
533                 }
534         }
535         else if (idCmd == 1)
536         {
537                 switch (m_dwMenuState)
538                 {
539                 case MENU_ONESEL_PREV:
540                         VERIFY(strHelp.LoadString(IDS_HELP_SAVETHIS));
541                         break;
542                 default:
543                         VERIFY(strHelp.LoadString(IDS_CONTEXT_HELP));
544                         break;
545                 }
546         }
547         return strHelp;
548 }
549
550 /// Format commandline used to start WinMerge
551 CString CWinMergeShell::FormatCmdLine(const CString &winmergePath,
552         const CString &path1, const CString &path2, BOOL bAlterSubFolders)
553 {
554         CString strCommandline = winmergePath;
555         BOOL bOnlyFiles = FALSE;
556         
557         if (!path1.IsEmpty() && !path2.IsEmpty())
558         {
559                 CFileStatus status;
560                 CFileStatus status2;
561                 if (CFile::GetStatus(path1, status) &&
562                         CFile::GetStatus(path2, status2))
563                 {
564                         // Check if both paths are files
565                         if ((status.m_attribute & CFile::Attribute::directory) == 0 &&
566                                 (status2.m_attribute & CFile::Attribute::directory) == 0)
567                         {
568                                 bOnlyFiles = TRUE;
569                         }
570                 }
571         }
572
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;
577
578         if (bAlterSubFolders && !bSubfoldersByDefault)
579                 strCommandline += _T(" /r");
580         else if (!bAlterSubFolders && bSubfoldersByDefault)
581                 strCommandline += _T(" /r");
582         
583         strCommandline += _T(" \"") + path1 + _T("\"");
584         
585         if (!m_strPaths[1].IsEmpty())
586                 strCommandline += _T(" \"") + path2 + _T("\"");
587
588         return strCommandline;
589 }