Fix piping to TortoiseGitUDiff by checking for ERROR_BROKEN_PIPE and treating that...
[TortoiseGit.git] / src / TortoiseShell / ContextMenu.cpp
blob7f68c53d71c3ee565123cb6b9e9e05a5b5c35c54
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2003-2012, 2014-2016, 2018 - TortoiseSVN
4 // Copyright (C) 2008-2023 - TortoiseGit
6 // This program is free software; you can redistribute it and/or
7 // modify it under the terms of the GNU General Public License
8 // as published by the Free Software Foundation; either version 2
9 // of the License, or (at your option) any later version.
11 // This program is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
16 // You should have received a copy of the GNU General Public License
17 // along with this program; if not, write to the Free Software Foundation,
18 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 #include "stdafx.h"
21 #include <comutil.h>
22 #include <winrt/base.h>
23 #include <wrl/client.h>
24 #include <windowsx.h>
25 #include "ShellExt.h"
26 #include "ItemIDList.h"
27 #include "PreserveChdir.h"
28 #include "UnicodeUtils.h"
29 #include "Git.h"
30 #include "GitStatus.h"
31 #include "TGitPath.h"
32 #include "PathUtils.h"
33 #include "CreateProcessHelper.h"
34 #include "FormatMessageWrapper.h"
35 #include "../TGitCache/CacheInterface.h"
36 #include "resource.h"
37 #include "LoadIconEx.h"
38 #include "ClipboardHelper.h"
40 #pragma comment(lib, "comsupp.lib")
42 #define GetPIDLFolder(pida) reinterpret_cast<LPCITEMIDLIST>(reinterpret_cast<LPBYTE>(pida) + (pida)->aoffset[0])
43 #define GetPIDLItem(pida, i) reinterpret_cast<LPCITEMIDLIST>(reinterpret_cast<LPBYTE>(pida) + (pida)->aoffset[i + 1])
45 int g_shellidlist=RegisterClipboardFormat(CFSTR_SHELLIDLIST);
47 extern MenuInfo menuInfo[];
48 static int g_syncSeq = 0;
50 STDMETHODIMP CShellExt::Initialize(LPCITEMIDLIST pIDFolder, LPDATAOBJECT pDataObj, HKEY /* hRegKey */)
52 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": Shell :: Initialize\n");
53 PreserveChdir preserveChdir;
54 files_.clear();
55 folder_.clear();
56 uuidSource.clear();
57 uuidTarget.clear();
58 itemStates = 0;
59 itemStatesFolder = 0;
60 std::wstring statuspath;
61 git_wc_status_kind fetchedstatus = git_wc_status_none;
62 // get selected files/folders
63 if (pDataObj)
65 STGMEDIUM medium;
66 FORMATETC fmte = { static_cast<CLIPFORMAT>(g_shellidlist),
67 nullptr,
68 DVASPECT_CONTENT,
69 -1,
70 TYMED_HGLOBAL};
71 HRESULT hres = pDataObj->GetData(&fmte, &medium);
73 if (SUCCEEDED(hres) && medium.hGlobal)
75 if (m_State == FileStateDropHandler)
77 if (!CRegStdDWORD(L"Software\\TortoiseGit\\EnableDragContextMenu", TRUE, false, HKEY_CURRENT_USER, KEY_WOW64_64KEY))
79 ReleaseStgMedium(&medium);
80 return S_OK;
83 FORMATETC etc = { CF_HDROP, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
84 STGMEDIUM stg = { TYMED_HGLOBAL };
85 if ( FAILED( pDataObj->GetData ( &etc, &stg )))
87 ReleaseStgMedium ( &medium );
88 return E_INVALIDARG;
92 auto drop = static_cast<HDROP>(GlobalLock(stg.hGlobal));
93 if (!drop)
95 ReleaseStgMedium ( &stg );
96 ReleaseStgMedium ( &medium );
97 return E_INVALIDARG;
100 const int count = DragQueryFile(drop, UINT(-1), nullptr, 0);
101 if (count == 1)
102 itemStates |= ITEMIS_ONLYONE;
103 for (int i = 0; i < count; i++)
105 // find the path length in chars
106 const UINT len = DragQueryFile(drop, i, nullptr, 0);
107 if (len == 0 || len >= INT_MAX)
108 continue;
109 auto szFileName = std::make_unique<wchar_t[]>(len + 1);
110 if (0 == DragQueryFile(drop, i, szFileName.get(), len + 1))
111 continue;
112 auto str = std::wstring(szFileName.get());
113 if ((!str.empty()) && (g_ShellCache.IsContextPathAllowed(szFileName.get())))
116 CTGitPath strpath;
117 strpath.SetFromWin(str.c_str());
118 itemStates |= (strpath.GetFileExtension().CompareNoCase(L".diff") == 0) ? ITEMIS_PATCHFILE : 0;
119 itemStates |= (strpath.GetFileExtension().CompareNoCase(L".patch") == 0) ? ITEMIS_PATCHFILE : 0;
121 files_.push_back(str);
122 if (i == 0)
124 //get the git status of the item
125 git_wc_status_kind status = git_wc_status_none;
126 CTGitPath askedpath;
127 askedpath.SetFromWin(str.c_str());
128 CString workTreePath;
129 if (!askedpath.HasAdminDir(&workTreePath) && GitAdminDir::IsBareRepo(str.c_str()))
130 itemStates |= ITEMIS_BAREREPO; // TODO: optimize
131 uuidSource = workTreePath;
134 if (g_ShellCache.GetCacheType() == ShellCache::exe && g_ShellCache.IsPathAllowed(str.c_str()))
136 CTGitPath tpath(str.c_str());
137 if (!tpath.HasAdminDir())
139 status = git_wc_status_none;
140 continue;
142 if (tpath.IsAdminDir())
144 status = git_wc_status_none;
145 continue;
147 TGITCacheResponse itemStatus = { 0 };
148 if (m_remoteCacheLink.GetStatusFromRemoteCache(tpath, &itemStatus, true))
150 fetchedstatus = status = static_cast<git_wc_status_kind>(itemStatus.m_status);
151 if (askedpath.IsDirectory())//if ((stat.status->entry)&&(stat.status->entry->kind == git_node_dir))
153 itemStates |= ITEMIS_FOLDER;
154 if ((status != git_wc_status_unversioned) && (status != git_wc_status_ignored) && (status != git_wc_status_none))
155 itemStates |= ITEMIS_FOLDERINGIT;
159 else
161 GitStatus stat;
162 stat.GetStatus(CTGitPath(str.c_str()), false, false, true);
163 if (stat.status)
165 statuspath = str;
166 status = stat.status->status;
167 fetchedstatus = status;
168 if ( askedpath.IsDirectory() )//if ((stat.status->entry)&&(stat.status->entry->kind == git_node_dir))
170 itemStates |= ITEMIS_FOLDER;
171 if ((status != git_wc_status_unversioned)&&(status != git_wc_status_ignored)&&(status != git_wc_status_none))
172 itemStates |= ITEMIS_FOLDERINGIT;
174 //if ((stat.status->entry)&&(stat.status->entry->uuid))
175 // uuidSource = CUnicodeUtils::StdGetUnicode(stat.status->entry->uuid);
177 else
179 // sometimes, git_client_status() returns with an error.
180 // in that case, we have to check if the working copy is versioned
181 // anyway to show the 'correct' context menu
182 if (askedpath.HasAdminDir())
183 status = git_wc_status_normal;
187 catch ( ... )
189 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": Exception in GitStatus::GetStatus()\n");
192 // TODO: should we really assume any sub-directory to be versioned
193 // or only if it contains versioned files
194 itemStates |= askedpath.GetAdminDirMask();
196 if ((status == git_wc_status_unversioned) || (status == git_wc_status_ignored) || (status == git_wc_status_none))
197 itemStates &= ~ITEMIS_INGIT;
199 if (status == git_wc_status_ignored)
200 itemStates |= ITEMIS_IGNORED;
201 if (status == git_wc_status_normal)
202 itemStates |= ITEMIS_NORMAL;
203 if (status == git_wc_status_conflicted)
204 itemStates |= ITEMIS_CONFLICTED;
205 if (status == git_wc_status_added)
206 itemStates |= ITEMIS_ADDED;
207 if (status == git_wc_status_deleted)
208 itemStates |= ITEMIS_DELETED;
211 } // for (int i = 0; i < count; i++)
212 GlobalUnlock ( drop );
213 ReleaseStgMedium ( &stg );
215 } // if (m_State == FileStateDropHandler)
216 else
218 //Enumerate PIDLs which the user has selected
219 auto cida = static_cast<CIDA*>(GlobalLock(medium.hGlobal));
220 ItemIDList parent( GetPIDLFolder (cida));
222 const int count = cida->cidl;
223 BOOL statfetched = FALSE;
224 for (int i = 0; i < count; ++i)
226 ItemIDList child (GetPIDLItem (cida, i), &parent);
227 std::wstring str = child.toString();
228 if ((str.empty() == false)&&(g_ShellCache.IsContextPathAllowed(str.c_str())))
230 //check if our menu is requested for a git admin directory
231 if (GitAdminDir::IsAdminDirPath(str.c_str()))
232 continue;
234 files_.push_back(str);
235 CTGitPath strpath;
236 strpath.SetFromWin(str.c_str());
237 itemStates |= (strpath.GetFileExtension().CompareNoCase(L".diff") == 0) ? ITEMIS_PATCHFILE : 0;
238 itemStates |= (strpath.GetFileExtension().CompareNoCase(L".patch") == 0) ? ITEMIS_PATCHFILE : 0;
239 if (!statfetched)
241 //get the git status of the item
242 git_wc_status_kind status = git_wc_status_none;
243 if ((g_ShellCache.IsSimpleContext())&&(strpath.IsDirectory()))
245 if (strpath.HasAdminDir())
246 status = git_wc_status_normal;
248 else
252 if (g_ShellCache.GetCacheType() == ShellCache::exe && g_ShellCache.IsPathAllowed(str.c_str()))
254 CTGitPath tpath(str.c_str());
255 if(!tpath.HasAdminDir())
257 status = git_wc_status_none;
258 continue;
260 if(tpath.IsAdminDir())
262 status = git_wc_status_none;
263 continue;
265 TGITCacheResponse itemStatus = { 0 };
266 if (m_remoteCacheLink.GetStatusFromRemoteCache(tpath, &itemStatus, true))
268 fetchedstatus = status = static_cast<git_wc_status_kind>(itemStatus.m_status);
269 if (strpath.IsDirectory())//if ((stat.status->entry)&&(stat.status->entry->kind == git_node_dir))
271 itemStates |= ITEMIS_FOLDER;
272 if ((status != git_wc_status_unversioned) && (status != git_wc_status_ignored) && (status != git_wc_status_none))
273 itemStates |= ITEMIS_FOLDERINGIT;
275 if (status == git_wc_status_conflicted)//if ((stat.status->entry)&&(stat.status->entry->conflict_wrk))
276 itemStates |= ITEMIS_CONFLICTED;
279 else
281 GitStatus stat;
282 if (strpath.HasAdminDir())
283 stat.GetStatus(strpath, false, false, true);
284 statuspath = str;
285 if (stat.status)
287 status = stat.status->status;
288 fetchedstatus = status;
289 if ( strpath.IsDirectory() )//if ((stat.status->entry)&&(stat.status->entry->kind == git_node_dir))
291 itemStates |= ITEMIS_FOLDER;
292 if ((status != git_wc_status_unversioned)&&(status != git_wc_status_ignored)&&(status != git_wc_status_none))
293 itemStates |= ITEMIS_FOLDERINGIT;
295 // TODO: do we need to check that it's not a dir? does conflict options makes sense for dir in git?
296 if (status == git_wc_status_conflicted)//if ((stat.status->entry)&&(stat.status->entry->conflict_wrk))
297 itemStates |= ITEMIS_CONFLICTED;
298 //if ((stat.status->entry)&&(stat.status->entry->uuid))
299 // uuidSource = CUnicodeUtils::StdGetUnicode(stat.status->entry->uuid);
301 else
303 // sometimes, git_client_status() returns with an error.
304 // in that case, we have to check if the working copy is versioned
305 // anyway to show the 'correct' context menu
306 if (strpath.HasAdminDir())
308 status = git_wc_status_normal;
309 fetchedstatus = status;
313 statfetched = TRUE;
315 catch ( ... )
317 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": Exception in GitStatus::GetStatus()\n");
321 itemStates |= strpath.GetAdminDirMask();
323 if ((status == git_wc_status_unversioned)||(status == git_wc_status_ignored)||(status == git_wc_status_none))
324 itemStates &= ~ITEMIS_INGIT;
325 if (status == git_wc_status_ignored)
327 itemStates |= ITEMIS_IGNORED;
330 if (status == git_wc_status_normal)
331 itemStates |= ITEMIS_NORMAL;
332 if (status == git_wc_status_conflicted)
333 itemStates |= ITEMIS_CONFLICTED;
334 if (status == git_wc_status_added)
335 itemStates |= ITEMIS_ADDED;
336 if (status == git_wc_status_deleted)
337 itemStates |= ITEMIS_DELETED;
340 } // for (int i = 0; i < count; ++i)
341 ItemIDList child (GetPIDLItem (cida, 0), &parent);
342 if (g_ShellCache.HasGITAdminDir(child.toString().c_str(), FALSE))
343 itemStates |= ITEMIS_INVERSIONEDFOLDER;
345 if (GitAdminDir::IsBareRepo(child.toString().c_str()))
346 itemStates = ITEMIS_BAREREPO;
348 GlobalUnlock(medium.hGlobal);
350 // if the item is a versioned folder, check if there's a patch file
351 // in the clipboard to be used in "Apply Patch"
352 const UINT cFormatDiff = RegisterClipboardFormat(L"TGIT_UNIFIEDDIFF");
353 if (cFormatDiff)
355 if (IsClipboardFormatAvailable(cFormatDiff))
356 itemStates |= ITEMIS_PATCHINCLIPBOARD;
358 if (IsClipboardFormatAvailable(CF_HDROP))
359 itemStates |= ITEMIS_PATHINCLIPBOARD;
363 ReleaseStgMedium ( &medium );
364 if (medium.pUnkForRelease)
366 IUnknown* relInterface = medium.pUnkForRelease;
367 relInterface->Release();
372 // get folder background
373 if (pIDFolder)
375 ItemIDList list(pIDFolder);
376 folder_ = list.toString();
377 git_wc_status_kind status = git_wc_status_none;
378 if (IsClipboardFormatAvailable(CF_HDROP))
379 itemStatesFolder |= ITEMIS_PATHINCLIPBOARD;
381 CTGitPath askedpath;
382 askedpath.SetFromWin(folder_.c_str());
384 if (g_ShellCache.IsContextPathAllowed(folder_.c_str()))
386 if (folder_.compare(statuspath)!=0)
388 CString worktreePath;
389 askedpath.HasAdminDir(&worktreePath);
390 uuidTarget = worktreePath;
393 if (g_ShellCache.GetCacheType() == ShellCache::exe && g_ShellCache.IsPathAllowed(folder_.c_str()))
395 CTGitPath tpath(folder_.c_str());
396 if(!tpath.HasAdminDir())
397 status = git_wc_status_none;
398 else if(tpath.IsAdminDir())
399 status = git_wc_status_none;
400 else
402 TGITCacheResponse itemStatus = { 0 };
403 if (m_remoteCacheLink.GetStatusFromRemoteCache(tpath, &itemStatus, true))
404 status = static_cast<git_wc_status_kind>(itemStatus.m_status);
407 else
409 GitStatus stat;
410 stat.GetStatus(CTGitPath(folder_.c_str()), false, false, true);
411 if (stat.status)
412 status = stat.status->status;
413 else
415 // sometimes, git_client_status() returns with an error.
416 // in that case, we have to check if the working copy is versioned
417 // anyway to show the 'correct' context menu
418 if (askedpath.HasAdminDir())
419 status = git_wc_status_normal;
423 //if ((status != git_wc_status_unversioned)&&(status != git_wc_status_ignored)&&(status != git_wc_status_none))
424 itemStatesFolder |= askedpath.GetAdminDirMask();
426 if ((status == git_wc_status_unversioned)||(status == git_wc_status_ignored)||(status == git_wc_status_none))
427 itemStates &= ~ITEMIS_INGIT;
429 if (status == git_wc_status_normal)
430 itemStatesFolder |= ITEMIS_NORMAL;
431 if (status == git_wc_status_conflicted)
432 itemStatesFolder |= ITEMIS_CONFLICTED;
433 if (status == git_wc_status_added)
434 itemStatesFolder |= ITEMIS_ADDED;
435 if (status == git_wc_status_deleted)
436 itemStatesFolder |= ITEMIS_DELETED;
438 if (GitAdminDir::IsBareRepo(askedpath.GetWinPath()))
439 itemStatesFolder = ITEMIS_BAREREPO;
441 catch ( ... )
443 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": Exception in GitStatus::GetStatus()\n");
446 else
448 status = fetchedstatus;
450 //if ((status != git_wc_status_unversioned)&&(status != git_wc_status_ignored)&&(status != git_wc_status_none))
451 itemStatesFolder |= askedpath.GetAdminDirMask();
453 if (status == git_wc_status_ignored)
454 itemStatesFolder |= ITEMIS_IGNORED;
455 itemStatesFolder |= ITEMIS_FOLDER;
456 if (files_.empty())
457 itemStates |= ITEMIS_ONLYONE;
458 if (m_State != FileStateDropHandler)
459 itemStates |= itemStatesFolder;
461 else
463 folder_.clear();
464 status = fetchedstatus;
467 if (files_.size() == 2)
468 itemStates |= ITEMIS_TWO;
469 if ((files_.size() == 1)&&(g_ShellCache.IsContextPathAllowed(files_.front().c_str())))
471 itemStates |= ITEMIS_ONLYONE;
472 if (m_State != FileStateDropHandler)
474 if (PathIsDirectory(files_.front().c_str()))
476 folder_ = files_.front();
477 git_wc_status_kind status = git_wc_status_none;
478 CTGitPath askedpath;
479 askedpath.SetFromWin(folder_.c_str());
481 if (folder_.compare(statuspath)!=0)
485 GitStatus stat;
486 stat.GetStatus(CTGitPath(folder_.c_str()), false, false, true);
487 if (stat.status)
488 status = stat.status->status;
490 catch ( ... )
492 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": Exception in GitStatus::GetStatus()\n");
495 else
497 status = fetchedstatus;
499 //if ((status != git_wc_status_unversioned)&&(status != git_wc_status_ignored)&&(status != git_wc_status_none))
500 itemStates |= askedpath.GetAdminDirMask();
502 if ((status == git_wc_status_unversioned)||(status == git_wc_status_ignored)||(status == git_wc_status_none))
503 itemStates &= ~ITEMIS_INGIT;
505 if (status == git_wc_status_ignored)
506 itemStates |= ITEMIS_IGNORED;
507 itemStates |= ITEMIS_FOLDER;
508 if (status == git_wc_status_added)
509 itemStates |= ITEMIS_ADDED;
510 if (status == git_wc_status_deleted)
511 itemStates |= ITEMIS_DELETED;
518 return S_OK;
521 void CShellExt::InsertGitMenu(BOOL istop, HMENU menu, UINT pos, UINT_PTR id, UINT stringid, UINT icon, UINT idCmdFirst, GitCommands com, UINT /*uFlags*/)
523 wchar_t menutextbuffer[512] = { 0 };
524 wchar_t verbsbuffer[255] = { 0 };
525 MAKESTRING(stringid);
527 if (istop && menu)
529 //menu entry for the top context menu, so append an "Git " before
530 //the menu text to indicate where the entry comes from
531 wcscpy_s(menutextbuffer, L"Git ");
532 if (!g_ShellCache.HasShellMenuAccelerators())
534 // remove the accelerators
535 std::wstring temp = stringtablebuffer;
536 temp.erase(std::remove(temp.begin(), temp.end(), '&'), temp.end());
537 wcscpy_s(stringtablebuffer, temp.c_str());
540 wcscat_s(menutextbuffer, stringtablebuffer);
542 // insert branch name into "Git Commit..." entry, so it looks like "Git Commit "master"..."
543 // so we have an easy and fast way to check the current branch
544 // (the other alternative is using a separate disabled menu entry, the code is already done but commented out)
545 if (com == ShellMenuCommit)
547 // get branch name
548 CTGitPath path(folder_.empty() ? files_.front().c_str() : folder_.c_str());
549 CString sProjectRoot;
550 CString sBranchName;
552 if (path.GetAdminDirMask() & ITEMIS_SUBMODULE)
554 if (istop)
555 wcscpy_s(menutextbuffer, L"Git ");
556 else
557 menutextbuffer[0] = L'\0';
558 MAKESTRING(IDS_MENUCOMMITSUBMODULE);
559 wcscat_s(menutextbuffer, stringtablebuffer);
562 if (path.HasAdminDir(&sProjectRoot) && !CGit::GetCurrentBranchFromFile(sProjectRoot, sBranchName))
564 if (sBranchName.GetLength() == 2 * GIT_HASH_SIZE)
566 // if SHA1 only show 4 first bytes
567 BOOL bIsSha1 = TRUE;
568 for (int i = 0; i < 2 * GIT_HASH_SIZE; ++i)
569 if ( !iswxdigit(sBranchName[i]) )
571 bIsSha1 = FALSE;
572 break;
574 if (bIsSha1)
575 sBranchName = sBranchName.Left(g_Git.GetShortHASHLength()) + L"...";
578 // sanity check
579 if (sBranchName.GetLength() > 64)
580 sBranchName = sBranchName.Left(64) + L"...";
582 // scan to before "..."
583 LPWSTR s = menutextbuffer + wcslen(menutextbuffer)-1;
584 if (s > menutextbuffer)
586 while (s > menutextbuffer)
588 if (*s != L'.')
590 s++;
591 break;
593 s--;
596 else
598 s = menutextbuffer;
601 // append branch name and end with ...
602 wcscpy_s(s, 255 - wcslen(menutextbuffer) - 1, L" -> \"" + CStringUtils::EscapeAccellerators(sBranchName) + L"\"...");
606 if (com == ShellMenuDiffLater)
608 std::wstring sPath = regDiffLater;
609 if (!sPath.empty())
611 // add the path of the saved file
612 wchar_t compact[2 * GIT_HASH_SIZE] = { 0 };
613 PathCompactPathEx(compact, sPath.c_str(), _countof(compact) - 1, 0);
614 MAKESTRING(IDS_MENUDIFFNOW);
615 CString sMenu;
616 sMenu.Format(CString(stringtablebuffer), compact);
617 wcscpy_s(menutextbuffer, sMenu);
621 MENUITEMINFO menuiteminfo = { 0 };
622 menuiteminfo.cbSize = sizeof(menuiteminfo);
623 menuiteminfo.fMask = MIIM_FTYPE | MIIM_ID | MIIM_STRING;
624 menuiteminfo.fType = MFT_STRING;
625 menuiteminfo.dwTypeData = menutextbuffer;
626 if (icon)
628 menuiteminfo.fMask |= MIIM_BITMAP;
629 menuiteminfo.hbmpItem = m_iconBitmapUtils.IconToBitmapPARGB32(g_hResInst, icon);
631 menuiteminfo.wID = static_cast<UINT>(id);
632 if (menu)
633 InsertMenuItem(menu, pos, TRUE, &menuiteminfo);
634 else
635 m_explorerCommands.push_back(Microsoft::WRL::Make<CExplorerCommand>(menutextbuffer, icon, com, static_cast<LPCWSTR>(CPathUtils::GetAppDirectory(g_hmodThisDll)), uuidSource, itemStates, itemStatesFolder, files_, std::vector<Microsoft::WRL::ComPtr<CExplorerCommand>>(), m_site));
636 if (istop)
638 //menu entry for the top context menu, so append an "Git " before
639 //the menu text to indicate where the entry comes from
640 wcscpy_s(menutextbuffer, L"Git ");
642 LoadString(g_hResInst, stringid, verbsbuffer, _countof(verbsbuffer));
643 wcscat_s(menutextbuffer, verbsbuffer);
644 auto verb = std::wstring(menutextbuffer);
645 if (verb.find('&') != -1)
647 verb.erase(verb.find('&'),1);
649 myVerbsMap[verb] = id - idCmdFirst;
650 myVerbsMap[verb] = id;
651 myVerbsIDMap[id - idCmdFirst] = verb;
652 myVerbsIDMap[id] = verb;
653 // We store the relative and absolute diameter
654 // (drawitem callback uses absolute, others relative)
655 myIDMap[id - idCmdFirst] = com;
656 myIDMap[id] = com;
657 if (!istop)
658 mySubMenuMap[pos] = com;
661 bool CShellExt::WriteClipboardPathsToTempFile(std::wstring& tempfile)
663 tempfile = std::wstring();
664 //write all selected files and paths to a temporary file
665 //for TortoiseGitProc.exe to read out again.
666 DWORD written = 0;
667 DWORD pathlength = GetTortoiseGitTempPath(0, nullptr);
668 auto path = std::make_unique<wchar_t[]>(pathlength + 1);
669 auto tempFile = std::make_unique<wchar_t[]>(pathlength + 100);
670 GetTortoiseGitTempPath(pathlength+1, path.get());
671 GetTempFileName(path.get(), L"git", 0, tempFile.get());
672 tempfile = std::wstring(tempFile.get());
674 CAutoFile file = ::CreateFile(tempFile.get(),
675 GENERIC_WRITE,
676 FILE_SHARE_READ,
677 nullptr,
678 CREATE_ALWAYS,
679 FILE_ATTRIBUTE_TEMPORARY,
680 nullptr);
682 if (!file)
683 return false;
685 if (!IsClipboardFormatAvailable(CF_HDROP))
686 return false;
687 CClipboardHelper clipboardHelper;
688 if (!clipboardHelper.Open(nullptr))
689 return false;
691 HGLOBAL hglb = GetClipboardData(CF_HDROP);
692 SCOPE_EXIT
694 GlobalUnlock(hglb);
696 auto hDrop = static_cast<HDROP>(GlobalLock(hglb));
697 if (!hDrop)
698 return false;
699 SCOPE_EXIT { GlobalUnlock(hDrop); };
701 wchar_t szFileName[MAX_PATH] = { 0 };
702 const UINT cFiles = DragQueryFile(hDrop, 0xFFFFFFFF, nullptr, 0);
703 for(UINT i = 0; i < cFiles; ++i)
705 DragQueryFile(hDrop, i, szFileName, _countof(szFileName));
706 std::wstring filename = szFileName;
707 ::WriteFile (file, filename.c_str(), static_cast<DWORD>(filename.size()) * sizeof(wchar_t), &written, nullptr);
708 ::WriteFile(file, L"\n", 2, &written, nullptr);
711 return true;
714 std::wstring CShellExt::WriteFileListToTempFile(bool bFoldersOnly, const std::vector<std::wstring>& files, const std::wstring folder)
716 //write all selected files and paths to a temporary file
717 //for TortoiseGitProc.exe to read out again.
718 DWORD pathlength = GetTortoiseGitTempPath(0, nullptr);
719 auto path = std::make_unique<wchar_t[]>(pathlength + 1);
720 auto tempFile = std::make_unique<wchar_t[]>(pathlength + 100);
721 GetTortoiseGitTempPath(pathlength + 1, path.get());
722 GetTempFileName(path.get(), L"git", 0, tempFile.get());
723 auto retFilePath = std::wstring(tempFile.get());
725 CAutoFile file = ::CreateFile (tempFile.get(),
726 GENERIC_WRITE,
727 FILE_SHARE_READ,
728 nullptr,
729 CREATE_ALWAYS,
730 FILE_ATTRIBUTE_TEMPORARY,
731 nullptr);
733 if (!file)
735 MessageBox(nullptr, L"Could not create temporary file. Please check (the permissions of) your temp-folder: " + CString(tempFile.get()), L"TortoiseGit", MB_ICONERROR);
736 return std::wstring();
739 DWORD written = 0;
740 if (files.empty())
742 ::WriteFile (file, folder.c_str(), static_cast<DWORD>(folder.size()) * sizeof(wchar_t), &written, 0);
743 ::WriteFile(file, L"\n", 2, &written, 0);
746 for (const auto& file_ : files)
748 if (bFoldersOnly && !PathIsDirectory(file_.c_str()))
749 continue;
751 ::WriteFile(file, file_.c_str(), static_cast<DWORD>(file_.size()) * sizeof(wchar_t), &written, 0);
752 ::WriteFile(file, L"\n", 2, &written, 0);
754 return retFilePath;
757 STDMETHODIMP CShellExt::QueryDropContext(UINT uFlags, UINT idCmdFirst, HMENU hMenu, UINT &indexMenu)
759 if (!CRegStdDWORD(L"Software\\TortoiseGit\\EnableDragContextMenu", TRUE, false, HKEY_CURRENT_USER, KEY_WOW64_64KEY))
760 return S_OK;
762 PreserveChdir preserveChdir;
763 LoadLangDll();
765 if ((uFlags & CMF_DEFAULTONLY)!=0)
766 return S_OK; //we don't change the default action
768 if (files_.empty() || folder_.empty())
769 return S_OK;
771 if (((uFlags & 0x000f)!=CMF_NORMAL)&&(!(uFlags & CMF_EXPLORE))&&(!(uFlags & CMF_VERBSONLY)))
772 return S_OK;
774 bool bSourceAndTargetFromSameRepository = ((uuidSource.size() == uuidTarget.size() && _wcsnicmp(uuidSource.c_str(), uuidTarget.c_str(), uuidSource.size()) == 0)) || uuidSource.empty() || uuidTarget.empty();
776 //the drop handler only has eight commands, but not all are visible at the same time:
777 //if the source file(s) are under version control then those files can be moved
778 //to the new location or they can be moved with a rename,
779 //if they are unversioned then they can be added to the working copy
780 //if they are versioned, they also can be exported to an unversioned location
781 UINT idCmd = idCmdFirst;
783 bool moveAvailable = false;
784 // Git move here
785 // available if source is versioned but not added, target is versioned, source and target from same repository or target folder is added
786 if ((bSourceAndTargetFromSameRepository || (itemStatesFolder & ITEMIS_ADDED)) && (itemStatesFolder & ITEMIS_FOLDERINGIT) && ((itemStates & (ITEMIS_NORMAL | ITEMIS_INGIT | ITEMIS_FOLDERINGIT))))
788 InsertGitMenu(FALSE, hMenu, indexMenu++, idCmd++, IDS_DROPMOVEMENU, 0, idCmdFirst, ShellMenuDropMove, uFlags);
789 moveAvailable = true;
792 // Git move and rename here
793 // available if source is a single, versioned but not added item, target is versioned, source and target from same repository or target folder is added
794 if ((bSourceAndTargetFromSameRepository || (itemStatesFolder & ITEMIS_ADDED)) && (itemStatesFolder & ITEMIS_FOLDERINGIT) && (itemStates & (ITEMIS_NORMAL | ITEMIS_INGIT | ITEMIS_FOLDERINGIT)) && (itemStates & ITEMIS_ONLYONE))
796 InsertGitMenu(FALSE, hMenu, indexMenu++, idCmd++, IDS_DROPMOVERENAMEMENU, 0, idCmdFirst, ShellMenuDropMoveRename, uFlags);
797 moveAvailable = true;
800 // Git copy here
801 // available if source is versioned but not added, target is versioned, source and target from same repository or target folder is added
802 //if ((bSourceAndTargetFromSameRepository||(itemStatesFolder & ITEMIS_ADDED))&&(itemStatesFolder & ITEMIS_FOLDERINGIT)&&(itemStates & ITEMIS_INGIT)&&((~itemStates) & ITEMIS_ADDED))
803 // InsertGitMenu(FALSE, hMenu, indexMenu++, idCmd++, IDS_DROPCOPYMENU, 0, idCmdFirst, ShellMenuDropCopy, uFlags);
805 // Git copy and rename here, source and target from same repository
806 // available if source is a single, versioned but not added item, target is versioned or target folder is added
807 //if ((bSourceAndTargetFromSameRepository||(itemStatesFolder & ITEMIS_ADDED))&&(itemStatesFolder & ITEMIS_FOLDERINGIT)&&(itemStates & ITEMIS_INGIT)&&(itemStates & ITEMIS_ONLYONE)&&((~itemStates) & ITEMIS_ADDED))
808 // InsertGitMenu(FALSE, hMenu, indexMenu++, idCmd++, IDS_DROPCOPYRENAMEMENU, 0, idCmdFirst, ShellMenuDropCopyRename, uFlags);
810 // Git add here
811 // available if target is versioned and source is either unversioned or from another repository
812 if ((itemStatesFolder & ITEMIS_FOLDERINGIT) && (((~itemStates) & ITEMIS_INGIT) || !bSourceAndTargetFromSameRepository) && !moveAvailable)
813 InsertGitMenu(FALSE, hMenu, indexMenu++, idCmd++, IDS_DROPCOPYADDMENU, 0, idCmdFirst, ShellMenuDropCopyAdd, uFlags);
815 // Git export here
816 // available if source is versioned and a folder
817 //if ((itemStates & ITEMIS_INGIT)&&(itemStates & ITEMIS_FOLDER))
818 // InsertGitMenu(FALSE, hMenu, indexMenu++, idCmd++, IDS_DROPEXPORTMENU, 0, idCmdFirst, ShellMenuDropExport, uFlags);
820 // Git export all here
821 // available if source is versioned and a folder
822 //if ((itemStates & ITEMIS_INGIT)&&(itemStates & ITEMIS_FOLDER))
823 // InsertGitMenu(FALSE, hMenu, indexMenu++, idCmd++, IDS_DROPEXPORTEXTENDEDMENU, 0, idCmdFirst, ShellMenuDropExportExtended, uFlags);
825 // apply patch
826 // available if source is a patchfile
827 if (itemStates & ITEMIS_PATCHFILE)
829 if (itemStates & ITEMIS_ONLYONE)
830 InsertGitMenu(FALSE, hMenu, indexMenu++, idCmd++, IDS_MENUAPPLYPATCH, 0, idCmdFirst, ShellMenuApplyPatch, uFlags);
831 InsertGitMenu(FALSE, hMenu, indexMenu++, idCmd++, IDS_MENUIMPORTPATCH, 0, idCmdFirst, ShellMenuImportPatchDrop, uFlags);
834 if ((itemStates & ITEMIS_ONLYONE) && (itemStates & (ITEMIS_WCROOT | ITEMIS_BAREREPO)) && !(itemStatesFolder & ITEMIS_INVERSIONEDFOLDER))
835 InsertGitMenu(FALSE, hMenu, indexMenu++, idCmd++, IDS_DROPNEWWORKTREE, 0, idCmdFirst, ShellMenuDropNewWorktree, uFlags);
837 // separator
838 if (idCmd != idCmdFirst)
839 InsertMenu(hMenu, indexMenu++, MF_SEPARATOR | MF_BYPOSITION, 0, nullptr);
841 TweakMenu(hMenu);
843 return ResultFromScode(MAKE_SCODE(SEVERITY_SUCCESS, 0, static_cast<USHORT>(idCmd - idCmdFirst)));
846 STDMETHODIMP CShellExt::QueryContextMenu(HMENU hMenu, UINT indexMenu, UINT idCmdFirst, UINT /*idCmdLast*/, UINT uFlags)
848 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": Shell :: QueryContextMenu itemStates=%ld\n", itemStates);
849 PreserveChdir preserveChdir;
851 //first check if our drop handler is called
852 //and then (if true) provide the context menu for the
853 //drop handler
854 if (m_State == FileStateDropHandler)
856 return QueryDropContext(uFlags, idCmdFirst, hMenu, indexMenu);
859 if ((uFlags & CMF_DEFAULTONLY)!=0)
860 return S_OK; //we don't change the default action
862 if (files_.empty() && folder_.empty())
863 return S_OK;
865 if (((uFlags & 0x000f)!=CMF_NORMAL)&&(!(uFlags & CMF_EXPLORE))&&(!(uFlags & CMF_VERBSONLY)))
866 return S_OK;
868 if (IsIllegalFolder(folder_))
869 return S_OK;
871 if (folder_.empty())
873 // folder is empty, but maybe files are selected
874 if (files_.empty())
875 return S_OK; // nothing selected - we don't have a menu to show
876 // check whether a selected entry is an UID - those are namespace extensions
877 // which we can't handle
878 if (std::any_of(files_.cbegin(), files_.cend(), [](auto& file) { return CStringUtils::StartsWith(file.c_str(), L"::{"); }))
879 return S_OK;
881 else
883 if (CStringUtils::StartsWith(folder_.c_str(), L"::{"))
884 return S_OK;
887 if (((uFlags & CMF_EXTENDEDVERBS) == 0) && g_ShellCache.HideMenusForUnversionedItems())
889 if ((itemStates & (ITEMIS_INGIT | ITEMIS_INVERSIONEDFOLDER | ITEMIS_FOLDERINGIT | ITEMIS_BAREREPO)) == 0)
890 return S_OK;
893 //check if our menu is requested for a git admin directory
894 if (GitAdminDir::IsAdminDirPath(folder_.c_str()))
895 return S_OK;
897 if (uFlags & CMF_EXTENDEDVERBS)
898 itemStates |= ITEMIS_EXTENDED;
900 regDiffLater.read();
901 if (!std::wstring(regDiffLater).empty())
902 itemStates |= ITEMIS_HASDIFFLATER;
904 const BOOL bShortcut = !!(uFlags & CMF_VERBSONLY);
905 if ( bShortcut && (files_.size()==1))
907 // Don't show the context menu for a link if the
908 // destination is not part of a working copy.
909 // It would only show the standard menu items
910 // which are already shown for the lnk-file.
911 CString path = files_.front().c_str();
912 if (!GitAdminDir::HasAdminDir(path))
914 return S_OK;
918 if (hMenu)
920 //check if we already added our menu entry for a folder.
921 //we check that by iterating through all menu entries and check if
922 //the dwItemData member points to our global ID string. That string is set
923 //by our shell extension when the folder menu is inserted.
924 wchar_t menubuf[MAX_PATH] = { 0 };
925 const int count = GetMenuItemCount(hMenu);
926 for (int i=0; i<count; ++i)
928 MENUITEMINFO miif = { 0 };
929 miif.cbSize = sizeof(MENUITEMINFO);
930 miif.fMask = MIIM_DATA;
931 miif.dwTypeData = menubuf;
932 miif.cch = _countof(menubuf);
933 GetMenuItemInfo(hMenu, i, TRUE, &miif);
934 if (miif.dwItemData == reinterpret_cast<ULONG_PTR>(g_MenuIDString))
935 return S_OK;
939 LoadLangDll();
940 UINT idCmd = idCmdFirst;
942 //create the sub menu
943 HMENU subMenu = hMenu ? CreateMenu() : nullptr;
944 int indexSubMenu = 0;
946 unsigned __int64 topMenu11 = g_ShellCache.GetMenuLayout11();
947 unsigned __int64 topmenu = g_ShellCache.GetMenuLayout();
948 unsigned __int64 menumask = g_ShellCache.GetMenuMask();
949 unsigned __int64 menuex = g_ShellCache.GetMenuExt();
951 int menuIndex = 0;
952 bool bAddSeparator = false;
953 bool bMenuEntryAdded = false;
954 bool bMenuEmpty = true;
955 if (hMenu)
957 // insert separator at start
958 InsertMenu(hMenu, indexMenu++, MF_SEPARATOR | MF_BYPOSITION, 0, nullptr); idCmd++;
960 bool bShowIcons = !!DWORD(CRegStdDWORD(L"Software\\TortoiseGit\\ShowContextMenuIcons", TRUE, false, HKEY_CURRENT_USER, KEY_WOW64_64KEY));
962 while (menuInfo[menuIndex].command != ShellMenuLastEntry)
964 if (menuInfo[menuIndex].command == ShellSeparator)
966 // we don't add a separator immediately. Because there might not be
967 // another 'normal' menu entry after we insert a separator.
968 // we simply set a flag here, indicating that before the next
969 // 'normal' menu entry, a separator should be added.
970 if (!bMenuEmpty)
971 bAddSeparator = true;
972 if (bMenuEntryAdded)
973 bAddSeparator = true;
975 else
977 // check the conditions whether to show the menu entry or not
978 bool bInsertMenu = ShouldInsertItem(menuInfo[menuIndex]);
979 if (menuInfo[menuIndex].menuID & menuex)
981 if( !(itemStates & ITEMIS_EXTENDED) )
982 bInsertMenu = false;
985 if (menuInfo[menuIndex].menuID & (~menumask))
987 if (bInsertMenu)
989 bool bIsTop = ((topmenu & menuInfo[menuIndex].menuID) != 0);
990 // insert a separator
991 if ((bMenuEntryAdded)&&(bAddSeparator)&&(!bIsTop))
993 bAddSeparator = false;
994 bMenuEntryAdded = false;
995 if (subMenu)
996 InsertMenu(subMenu, indexSubMenu++, MF_SEPARATOR | MF_BYPOSITION, 0, nullptr);
997 else
998 m_explorerCommands.push_back(Microsoft::WRL::Make<CExplorerCommand>(L"", 0, 0, static_cast<LPCWSTR>(CPathUtils::GetAppDirectory(g_hmodThisDll)), uuidSource, itemStates, itemStatesFolder, files_, std::vector<Microsoft::WRL::ComPtr<CExplorerCommand>>(), m_site));
999 idCmd++;
1002 const bool isMenu11 = ((topMenu11 & menuInfo[menuIndex].menuID) != 0);
1003 // handle special cases (sub menus)
1004 if ((menuInfo[menuIndex].command == ShellMenuIgnoreSub)||(menuInfo[menuIndex].command == ShellMenuUnIgnoreSub)||(menuInfo[menuIndex].command == ShellMenuDeleteIgnoreSub))
1006 if (hMenu || isMenu11)
1008 if (InsertIgnoreSubmenus(idCmd, idCmdFirst, hMenu, subMenu, indexMenu, indexSubMenu, topmenu, bShowIcons, uFlags))
1009 bMenuEntryAdded = true;
1012 else if (menuInfo[menuIndex].command == ShellMenuLFSMenu)
1014 if (InsertLFSSubmenu(idCmd, idCmdFirst, hMenu, subMenu, indexMenu, indexSubMenu, topmenu, bShowIcons, uFlags))
1015 bMenuEntryAdded = true;
1017 else
1019 bIsTop = ((topmenu & menuInfo[menuIndex].menuID) != 0);
1021 if (hMenu || isMenu11)
1023 // insert the menu entry
1024 InsertGitMenu(bIsTop,
1025 bIsTop ? hMenu : subMenu,
1026 bIsTop ? indexMenu++ : indexSubMenu++,
1027 idCmd++,
1028 menuInfo[menuIndex].menuTextID,
1029 bShowIcons ? menuInfo[menuIndex].iconID : 0,
1030 idCmdFirst,
1031 menuInfo[menuIndex].command,
1032 uFlags);
1033 if (!bIsTop)
1035 bMenuEntryAdded = true;
1036 bMenuEmpty = false;
1037 bAddSeparator = false;
1044 menuIndex++;
1047 // do not show TortoiseGit menu if it's empty
1048 if (bMenuEmpty)
1050 if (idCmd - idCmdFirst > 0)
1052 if (hMenu)
1054 //separator after
1055 InsertMenu(hMenu, indexMenu++, MF_SEPARATOR | MF_BYPOSITION, 0, nullptr); idCmd++;
1056 TweakMenu(hMenu);
1060 //return number of menu items added
1061 return ResultFromScode(MAKE_SCODE(SEVERITY_SUCCESS, 0, static_cast<USHORT>(idCmd - idCmdFirst)));
1064 //add sub menu to main context menu
1065 //don't use InsertMenu because this will lead to multiple menu entries in the explorer file menu.
1066 //see https://web.archive.org/web/20090728090357/http://support.microsoft.com/kb/214477 for details of that.
1067 MAKESTRING(IDS_MENUSUBMENU);
1068 if (!g_ShellCache.HasShellMenuAccelerators())
1070 // remove the accelerators
1071 std::wstring temp = stringtablebuffer;
1072 temp.erase(std::remove(temp.begin(), temp.end(), '&'), temp.end());
1073 wcscpy_s(stringtablebuffer, temp.c_str());
1075 MENUITEMINFO menuiteminfo = { 0 };
1076 menuiteminfo.cbSize = sizeof(menuiteminfo);
1077 menuiteminfo.fType = MFT_STRING;
1078 menuiteminfo.dwTypeData = stringtablebuffer;
1080 UINT uIcon = bShowIcons ? IDI_APP : 0;
1081 if (!folder_.empty())
1083 uIcon = bShowIcons ? IDI_MENUFOLDER : 0;
1084 myIDMap[idCmd - idCmdFirst] = ShellSubMenuFolder;
1085 myIDMap[idCmd] = ShellSubMenuFolder;
1086 menuiteminfo.dwItemData = reinterpret_cast<ULONG_PTR>(g_MenuIDString);
1088 else if (!bShortcut && (files_.size()==1))
1090 uIcon = bShowIcons ? IDI_MENUFILE : 0;
1091 myIDMap[idCmd - idCmdFirst] = ShellSubMenuFile;
1092 myIDMap[idCmd] = ShellSubMenuFile;
1094 else if (bShortcut && (files_.size()==1))
1096 uIcon = bShowIcons ? IDI_MENULINK : 0;
1097 myIDMap[idCmd - idCmdFirst] = ShellSubMenuLink;
1098 myIDMap[idCmd] = ShellSubMenuLink;
1100 else if (!files_.empty())
1102 uIcon = bShowIcons ? IDI_MENUMULTIPLE : 0;
1103 myIDMap[idCmd - idCmdFirst] = ShellSubMenuMultiple;
1104 myIDMap[idCmd] = ShellSubMenuMultiple;
1106 else
1108 myIDMap[idCmd - idCmdFirst] = ShellSubMenu;
1109 myIDMap[idCmd] = ShellSubMenu;
1111 menuiteminfo.fMask = MIIM_FTYPE | MIIM_ID | MIIM_SUBMENU | MIIM_DATA | MIIM_STRING;
1112 if (uIcon)
1114 menuiteminfo.fMask |= MIIM_BITMAP;
1115 menuiteminfo.hbmpItem = m_iconBitmapUtils.IconToBitmapPARGB32(g_hResInst, uIcon);
1117 menuiteminfo.hSubMenu = subMenu;
1118 menuiteminfo.wID = idCmd++;
1119 InsertMenuItem(hMenu, indexMenu++, TRUE, &menuiteminfo);
1121 //separator after
1122 InsertMenu(hMenu, indexMenu++, MF_SEPARATOR | MF_BYPOSITION, 0, nullptr); idCmd++;
1124 TweakMenu(hMenu);
1126 //return number of menu items added
1127 return ResultFromScode(MAKE_SCODE(SEVERITY_SUCCESS, 0, static_cast<USHORT>(idCmd - idCmdFirst)));
1130 void CShellExt::TweakMenu(HMENU hMenu)
1132 MENUINFO MenuInfo = {};
1133 MenuInfo.cbSize = sizeof(MenuInfo);
1134 MenuInfo.fMask = MIM_STYLE | MIM_APPLYTOSUBMENUS;
1135 MenuInfo.dwStyle = MNS_CHECKORBMP;
1136 SetMenuInfo(hMenu, &MenuInfo);
1139 void CShellExt::AddPathCommand(std::wstring& gitCmd, LPCWSTR command, bool bFilesAllowed, const std::vector<std::wstring>& files, const std::wstring folder)
1141 gitCmd += command;
1142 gitCmd += L" /path:\"";
1143 if ((bFilesAllowed) && !files.empty())
1144 gitCmd += files.front();
1145 else
1146 gitCmd += folder;
1147 gitCmd += L'"';
1150 void CShellExt::AddPathFileCommand(std::wstring& gitCmd, LPCWSTR command, const std::vector<std::wstring>& files, const std::wstring folder, bool bFoldersOnly = false)
1152 std::wstring tempfile = WriteFileListToTempFile(bFoldersOnly, files, folder);
1153 gitCmd += command;
1154 gitCmd += L" /pathfile:\"";
1155 gitCmd += tempfile;
1156 gitCmd += L'"';
1157 gitCmd += L" /deletepathfile";
1160 void CShellExt::AddPathFileDropCommand(std::wstring& gitCmd, LPCWSTR command, const std::vector<std::wstring>& files, const std::wstring folder)
1162 std::wstring tempfile = WriteFileListToTempFile(false, files, folder);
1163 gitCmd += command;
1164 gitCmd += L" /pathfile:\"";
1165 gitCmd += tempfile;
1166 gitCmd += L'"';
1167 gitCmd += L" /deletepathfile";
1168 gitCmd += L" /droptarget:\"";
1169 gitCmd += folder;
1170 gitCmd += L'"';
1173 // This is called when you invoke a command on the menu:
1174 STDMETHODIMP CShellExt::InvokeCommand(LPCMINVOKECOMMANDINFO lpcmi)
1176 PreserveChdir preserveChdir;
1177 HRESULT hr = E_INVALIDARG;
1178 if (!lpcmi)
1179 return hr;
1181 if (!files_.empty() || !folder_.empty())
1183 UINT_PTR idCmd = LOWORD(lpcmi->lpVerb);
1185 if (HIWORD(lpcmi->lpVerb))
1187 auto verb = MultibyteToWide(lpcmi->lpVerb);
1188 const auto verb_it = myVerbsMap.lower_bound(verb);
1189 if (verb_it != myVerbsMap.end() && verb_it->first == verb)
1190 idCmd = verb_it->second;
1191 else
1192 return hr;
1195 // See if we have a handler interface for this id
1196 std::map<UINT_PTR, UINT_PTR>::const_iterator id_it = myIDMap.lower_bound(idCmd);
1197 if (id_it != myIDMap.end() && id_it->first == idCmd)
1199 InvokeCommand(static_cast<int>(id_it->second),
1200 static_cast<LPCWSTR>(CPathUtils::GetAppDirectory(g_hmodThisDll)),
1201 uuidSource,
1202 lpcmi->hwnd,
1203 itemStates,
1204 itemStatesFolder,
1205 files_,
1206 folder_,
1207 regDiffLater,
1208 nullptr);
1209 myIDMap.clear();
1210 myVerbsIDMap.clear();
1211 myVerbsMap.clear();
1213 hr = S_OK;
1214 } // if (id_it != myIDMap.end() && id_it->first == idCmd)
1215 } // if (files_.empty() || folder_.empty())
1216 return hr;
1219 void CShellExt::InvokeCommand(int cmd, const std::wstring& appDir, const std::wstring uuidSource, HWND hParent, DWORD itemStates, DWORD itemStatesFolder, const std::vector<std::wstring>& paths, const std::wstring& folder, CRegStdString& regDiffLater, Microsoft::WRL::ComPtr<IUnknown> site)
1221 // TortoiseGitProc expects a command line of the form:
1222 //"/command:<commandname> /pathfile:<path> [/deletepathfile] ...
1223 // or
1224 //"/command:<commandname> /path:<path> ...
1226 //* path is a path to a single file/directory for commands which only act on single items (log, sync, ...)
1227 //* pathfile is a path to a temporary file which contains a list of file paths
1228 CTraceToOutputDebugString::Instance()(__FUNCTION__);
1229 std::wstring gitCmd = L" /command:";
1230 switch (cmd)
1232 case ShellMenuSync:
1234 wchar_t syncSeq[12] = { 0 };
1235 swprintf_s(syncSeq, L"%d", g_syncSeq++);
1236 AddPathCommand(gitCmd, L"sync", false, paths, folder);
1237 gitCmd += L" /seq:";
1238 gitCmd += syncSeq;
1240 break;
1241 case ShellMenuSubSync:
1242 AddPathFileCommand(gitCmd, L"subsync", paths, folder);
1243 if (itemStatesFolder & ITEMIS_SUBMODULECONTAINER || (itemStates & ITEMIS_SUBMODULECONTAINER && itemStates & ITEMIS_WCROOT && itemStates & ITEMIS_ONLYONE))
1245 gitCmd += L" /bkpath:\"";
1246 gitCmd += folder;
1247 gitCmd += L'"';
1249 break;
1250 case ShellMenuUpdateExt:
1251 AddPathFileCommand(gitCmd, L"subupdate", paths, folder);
1252 if (itemStatesFolder & ITEMIS_SUBMODULECONTAINER || (itemStates & ITEMIS_SUBMODULECONTAINER && itemStates & ITEMIS_WCROOT && itemStates & ITEMIS_ONLYONE))
1254 gitCmd += L" /bkpath:\"";
1255 gitCmd += folder;
1256 gitCmd += L'"';
1258 break;
1259 case ShellMenuCommit:
1260 AddPathFileCommand(gitCmd, L"commit", paths, folder);
1261 break;
1262 case ShellMenuAdd:
1263 AddPathFileCommand(gitCmd, L"add", paths, folder);
1264 break;
1265 case ShellMenuIgnore:
1266 AddPathFileCommand(gitCmd, L"ignore", paths, folder);
1267 break;
1268 case ShellMenuIgnoreCaseSensitive:
1269 AddPathFileCommand(gitCmd, L"ignore", paths, folder);
1270 gitCmd += L" /onlymask";
1271 break;
1272 case ShellMenuDeleteIgnore:
1273 AddPathFileCommand(gitCmd, L"ignore", paths, folder);
1274 gitCmd += L" /delete";
1275 break;
1276 case ShellMenuDeleteIgnoreCaseSensitive:
1277 AddPathFileCommand(gitCmd, L"ignore", paths, folder);
1278 gitCmd += L" /delete /onlymask";
1279 break;
1280 case ShellMenuUnIgnore:
1281 AddPathFileCommand(gitCmd, L"unignore", paths, folder);
1282 break;
1283 case ShellMenuUnIgnoreCaseSensitive:
1284 AddPathFileCommand(gitCmd, L"unignore", paths, folder);
1285 gitCmd += L" /onlymask";
1286 break;
1287 case ShellMenuMergeAbort:
1288 AddPathCommand(gitCmd, L"merge", true, paths, folder);
1289 gitCmd += L" /abort";
1290 break;
1291 case ShellMenuRevert:
1292 AddPathFileCommand(gitCmd, L"revert", paths, folder);
1293 break;
1294 case ShellMenuCleanup:
1295 AddPathFileCommand(gitCmd, L"cleanup", paths, folder);
1296 break;
1297 case ShellMenuSendMail:
1298 AddPathFileCommand(gitCmd, L"sendmail", paths, folder);
1299 break;
1300 case ShellMenuResolve:
1301 AddPathFileCommand(gitCmd, L"resolve", paths, folder);
1302 break;
1303 case ShellMenuSwitch:
1304 AddPathCommand(gitCmd, L"switch", false, paths, folder);
1305 break;
1306 case ShellMenuExport:
1307 AddPathCommand(gitCmd, L"export", false, paths, folder);
1308 break;
1309 case ShellMenuAbout:
1310 gitCmd += L"about";
1311 break;
1312 case ShellMenuCreateRepos:
1313 AddPathCommand(gitCmd, L"repocreate", false, paths, folder);
1314 break;
1315 case ShellMenuMerge:
1316 AddPathCommand(gitCmd, L"merge", false, paths, folder);
1317 break;
1318 case ShellMenuCopy:
1319 AddPathCommand(gitCmd, L"copy", true, paths, folder);
1320 break;
1321 case ShellMenuSettings:
1322 AddPathCommand(gitCmd, L"settings", true, paths, folder);
1323 break;
1324 case ShellMenuHelp:
1325 gitCmd += L"help";
1326 break;
1327 case ShellMenuRename:
1328 AddPathCommand(gitCmd, L"rename", true, paths, folder);
1329 if (itemStates & ITEMIS_SUBMODULE)
1330 gitCmd += L" /submodule";
1331 break;
1332 case ShellMenuRemove:
1333 AddPathFileCommand(gitCmd, L"remove", paths, folder);
1334 if (itemStates & ITEMIS_SUBMODULE)
1335 gitCmd += L" /submodule";
1336 break;
1337 case ShellMenuRemoveKeep:
1338 AddPathFileCommand(gitCmd, L"remove", paths, folder);
1339 gitCmd += L" /keep";
1340 break;
1341 case ShellMenuDiff:
1342 gitCmd += L"diff /path:\"";
1343 if (paths.size() == 1)
1344 gitCmd += paths.front();
1345 else if (paths.size() == 2)
1347 auto I = paths.cbegin();
1348 gitCmd += *I;
1349 ++I;
1350 gitCmd += L"\" /path2:\"";
1351 gitCmd += *I;
1353 else
1354 gitCmd += folder;
1355 gitCmd += L'"';
1356 if (GetAsyncKeyState(VK_SHIFT) & 0x8000)
1357 gitCmd += L" /alternative";
1358 break;
1359 case ShellMenuDiffLater:
1360 if (GetAsyncKeyState(VK_CONTROL) & 0x8000)
1362 gitCmd.clear();
1363 regDiffLater.removeValue();
1365 else if (paths.size() == 1)
1367 if (std::wstring(regDiffLater).empty())
1369 gitCmd.clear();
1370 regDiffLater = paths[0];
1372 else
1374 AddPathCommand(gitCmd, L"diff", true, paths, folder);
1375 gitCmd += L" /path2:\"";
1376 gitCmd += std::wstring(regDiffLater);
1377 gitCmd += L'"';
1378 if (GetAsyncKeyState(VK_SHIFT) & 0x8000)
1379 gitCmd += L" /alternative";
1380 regDiffLater.removeValue();
1383 else
1384 gitCmd.clear();
1385 break;
1386 case ShellMenuPrevDiff:
1387 AddPathCommand(gitCmd, L"prevdiff", true, paths, folder);
1388 if (GetAsyncKeyState(VK_SHIFT) & 0x8000)
1389 gitCmd += L" /alternative";
1390 break;
1391 case ShellMenuDiffTwo:
1392 AddPathCommand(gitCmd, L"diffcommits", true, paths, folder);
1393 break;
1394 case ShellMenuDropCopyAdd:
1395 AddPathFileDropCommand(gitCmd, L"dropcopyadd", paths, folder);
1396 break;
1397 case ShellMenuDropCopy:
1398 AddPathFileDropCommand(gitCmd, L"dropcopy", paths, folder);
1399 break;
1400 case ShellMenuDropCopyRename:
1401 AddPathFileDropCommand(gitCmd, L"dropcopy", paths, folder);
1402 gitCmd += L" /rename";
1403 break;
1404 case ShellMenuDropMove:
1405 AddPathFileDropCommand(gitCmd, L"dropmove", paths, folder);
1406 break;
1407 case ShellMenuDropMoveRename:
1408 AddPathFileDropCommand(gitCmd, L"dropmove", paths, folder);
1409 gitCmd += L" /rename";
1410 break;
1411 case ShellMenuDropExport:
1412 AddPathFileDropCommand(gitCmd, L"dropexport", paths, folder);
1413 break;
1414 case ShellMenuDropExportExtended:
1415 AddPathFileDropCommand(gitCmd, L"dropexport", paths, folder);
1416 gitCmd += L" /extended";
1417 break;
1418 case ShellMenuLog:
1419 case ShellMenuLogSubmoduleFolder:
1420 AddPathCommand(gitCmd, L"log", true, paths, folder);
1421 if (cmd == ShellMenuLogSubmoduleFolder)
1422 gitCmd += L" /submodule";
1423 break;
1424 case ShellMenuDaemon:
1425 AddPathCommand(gitCmd, L"daemon", true, paths, folder);
1426 break;
1427 case ShellMenuRevisionGraph:
1428 AddPathCommand(gitCmd, L"revisiongraph", true, paths, folder);
1429 break;
1430 case ShellMenuConflictEditor:
1431 AddPathCommand(gitCmd, L"conflicteditor", true, paths, folder);
1432 if (GetAsyncKeyState(VK_SHIFT) & 0x8000)
1433 gitCmd += L" /alternative";
1434 break;
1435 case ShellMenuGitSVNRebase:
1436 AddPathCommand(gitCmd, L"svnrebase", false, paths, folder);
1437 break;
1438 case ShellMenuGitSVNDCommit:
1439 AddPathCommand(gitCmd, L"svndcommit", true, paths, folder);
1440 break;
1441 case ShellMenuGitSVNDFetch:
1442 AddPathCommand(gitCmd, L"svnfetch", false, paths, folder);
1443 break;
1444 case ShellMenuGitSVNIgnore:
1445 AddPathCommand(gitCmd, L"svnignore", false, paths, folder);
1446 break;
1447 case ShellMenuRebase:
1448 AddPathCommand(gitCmd, L"rebase", false, paths, folder);
1449 break;
1450 case ShellMenuShowChanged:
1451 if (paths.size() > 1)
1452 AddPathFileCommand(gitCmd, L"repostatus", paths, folder);
1453 else
1454 AddPathCommand(gitCmd, L"repostatus", true, paths, folder);
1455 break;
1456 case ShellMenuRepoBrowse:
1457 AddPathCommand(gitCmd, L"repobrowser", false, paths, folder);
1458 break;
1459 case ShellMenuRefBrowse:
1460 AddPathCommand(gitCmd, L"refbrowse", false, paths, folder);
1461 break;
1462 case ShellMenuRefLog:
1463 AddPathCommand(gitCmd, L"reflog", false, paths, folder);
1464 break;
1465 case ShellMenuStashSave:
1466 AddPathCommand(gitCmd, L"stashsave", true, paths, folder);
1467 break;
1468 case ShellMenuStashApply:
1469 AddPathCommand(gitCmd, L"stashapply", false, paths, folder);
1470 break;
1471 case ShellMenuStashPop:
1472 AddPathCommand(gitCmd, L"stashpop", false, paths, folder);
1473 break;
1474 case ShellMenuStashList:
1475 AddPathCommand(gitCmd, L"reflog", false, paths, folder);
1476 gitCmd += L" /ref:refs/stash";
1477 break;
1478 case ShellMenuBisectStart:
1479 AddPathCommand(gitCmd, L"bisect", false, paths, folder);
1480 gitCmd += L" /start";
1481 break;
1482 case ShellMenuBisectGood:
1483 AddPathCommand(gitCmd, L"bisect", false, paths, folder);
1484 gitCmd += L" /good";
1485 break;
1486 case ShellMenuBisectBad:
1487 AddPathCommand(gitCmd, L"bisect", false, paths, folder);
1488 gitCmd += L" /bad";
1489 break;
1490 case ShellMenuBisectSkip:
1491 AddPathCommand(gitCmd, L"bisect", false, paths, folder);
1492 gitCmd += L" /skip";
1493 break;
1494 case ShellMenuBisectReset:
1495 AddPathCommand(gitCmd, L"bisect", false, paths, folder);
1496 gitCmd += L" /reset";
1497 break;
1498 case ShellMenuSubAdd:
1499 AddPathCommand(gitCmd, L"subadd", false, paths, folder);
1500 break;
1501 case ShellMenuBlame:
1502 AddPathCommand(gitCmd, L"blame", true, paths, folder);
1503 break;
1504 case ShellMenuApplyPatch:
1506 auto localPaths = paths;
1507 if ((itemStates & ITEMIS_PATCHINCLIPBOARD) && ((~itemStates) & ITEMIS_PATCHFILE))
1509 // if there's a patch file in the clipboard, we save it
1510 // to a temporary file and tell TortoiseGitMerge to use that one
1511 const UINT cFormat = RegisterClipboardFormat(L"TGIT_UNIFIEDDIFF");
1512 CClipboardHelper clipboardHelper;
1513 if (cFormat && clipboardHelper.Open(nullptr))
1515 HGLOBAL hglb = GetClipboardData(cFormat);
1516 auto lpstr = static_cast<LPCSTR>(GlobalLock(hglb));
1518 DWORD len = GetTortoiseGitTempPath(0, nullptr);
1519 auto path = std::make_unique<wchar_t[]>(len + 1);
1520 auto tempF = std::make_unique<wchar_t[]>(len + 100);
1521 GetTortoiseGitTempPath(len + 1, path.get());
1522 GetTempFileName(path.get(), TEXT("git"), 0, tempF.get());
1523 std::wstring sTempFile = std::wstring(tempF.get());
1525 FILE* outFile;
1526 size_t patchlen = strlen(lpstr);
1527 _wfopen_s(&outFile, sTempFile.c_str(), L"wb");
1528 if (outFile)
1530 size_t size = fwrite(lpstr, sizeof(char), patchlen, outFile);
1531 if (size == patchlen)
1533 itemStates |= ITEMIS_PATCHFILE;
1534 localPaths.clear();
1535 localPaths.push_back(sTempFile);
1537 fclose(outFile);
1539 GlobalUnlock(hglb);
1542 if (itemStates & ITEMIS_PATCHFILE)
1544 gitCmd = L" /diff:\"";
1545 if (!localPaths.empty())
1547 gitCmd += localPaths.front();
1548 if (itemStatesFolder & ITEMIS_FOLDERINGIT)
1550 gitCmd += L"\" /patchpath:\"";
1551 gitCmd += folder;
1554 else
1555 gitCmd += folder;
1556 if (itemStates & ITEMIS_INVERSIONEDFOLDER)
1557 gitCmd += L"\" /wc";
1558 else
1559 gitCmd += L'"';
1561 else
1563 gitCmd = L" /patchpath:\"";
1564 if (!localPaths.empty())
1565 gitCmd += localPaths.front();
1566 else
1567 gitCmd += folder;
1568 gitCmd += L'"';
1570 RunCommand(appDir + L"TortoiseGitMerge.exe", gitCmd, L"TortoiseGitMerge launch failed", site);
1572 return;
1573 case ShellMenuClipPaste:
1575 std::wstring tempfile;
1576 if (WriteClipboardPathsToTempFile(tempfile))
1578 bool bCopy = true;
1579 const UINT cPrefDropFormat = RegisterClipboardFormat(L"Preferred DropEffect");
1580 if (cPrefDropFormat)
1582 CClipboardHelper clipboardHelper;
1583 if (clipboardHelper.Open(hParent))
1585 HGLOBAL hglb = GetClipboardData(cPrefDropFormat);
1586 if (hglb)
1588 auto effect = static_cast<DWORD*>(GlobalLock(hglb));
1589 if (*effect == DROPEFFECT_MOVE)
1590 bCopy = false;
1591 GlobalUnlock(hglb);
1596 if (bCopy)
1597 gitCmd += L"pastecopy /pathfile:\"";
1598 else
1599 gitCmd += L"pastemove /pathfile:\"";
1600 gitCmd += tempfile;
1601 gitCmd += L'"';
1602 gitCmd += L" /deletepathfile";
1603 gitCmd += L" /droptarget:\"";
1604 gitCmd += folder;
1605 gitCmd += L'"';
1607 else
1608 return;
1609 break;
1611 case ShellMenuClone:
1612 AddPathCommand(gitCmd, L"clone", false, paths, folder);
1613 break;
1614 case ShellMenuPull:
1615 AddPathFileCommand(gitCmd, L"pull", paths, folder, true);
1616 break;
1617 case ShellMenuPush:
1618 AddPathFileCommand(gitCmd, L"push", paths, folder, true);
1619 break;
1620 case ShellMenuBranch:
1621 AddPathCommand(gitCmd, L"branch", false, paths, folder);
1622 break;
1623 case ShellMenuTag:
1624 AddPathCommand(gitCmd, L"tag", false, paths, folder);
1625 break;
1626 case ShellMenuFormatPatch:
1627 AddPathCommand(gitCmd, L"formatpatch", false, paths, folder);
1628 break;
1629 case ShellMenuImportPatch:
1630 AddPathFileCommand(gitCmd, L"importpatch", paths, folder);
1631 break;
1632 case ShellMenuImportPatchDrop:
1633 AddPathFileDropCommand(gitCmd, L"importpatch", paths, folder);
1634 break;
1635 case ShellMenuFetch:
1636 AddPathFileCommand(gitCmd, L"fetch", paths, folder, true);
1637 break;
1638 case ShellMenuLFSLocks:
1639 AddPathFileCommand(gitCmd, L"lfslocks", paths, folder);
1640 break;
1641 case ShellMenuLFSLock:
1642 AddPathFileCommand(gitCmd, L"lfslock", paths, folder);
1643 break;
1644 case ShellMenuLFSUnlock:
1645 AddPathFileCommand(gitCmd, L"lfsunlock", paths, folder);
1646 break;
1647 case ShellMenuWorktree:
1648 AddPathCommand(gitCmd, L"worktreelist", false, paths, folder);
1649 break;
1650 case ShellMenuDropNewWorktree:
1651 AddPathFileDropCommand(gitCmd, L"dropnewworktree", paths, folder);
1652 break;
1654 default:
1655 break;
1656 } // switch (id_it->second)
1657 if (!gitCmd.empty())
1659 gitCmd += L" /hwnd:";
1660 wchar_t buf[30] = { 0 };
1661 swprintf_s(buf, L"%p", static_cast<void*>(hParent));
1662 gitCmd += buf;
1663 RunCommand(appDir + L"TortoiseGitProc.exe", gitCmd, L"TortoiseGitProc launch failed", site);
1667 // This is for the status bar and things like that:
1668 STDMETHODIMP CShellExt::GetCommandString(UINT_PTR idCmd, UINT uFlags, UINT FAR * /*reserved*/, LPSTR pszName, UINT cchMax)
1670 PreserveChdir preserveChdir;
1671 //do we know the id?
1672 std::map<UINT_PTR, UINT_PTR>::const_iterator id_it = myIDMap.lower_bound(idCmd);
1673 if (id_it == myIDMap.end() || id_it->first != idCmd)
1675 return E_INVALIDARG; //no, we don't
1678 LoadLangDll();
1679 HRESULT hr = E_INVALIDARG;
1681 MAKESTRING(IDS_MENUDESCDEFAULT);
1682 int menuIndex = 0;
1683 while (menuInfo[menuIndex].command != ShellMenuLastEntry)
1685 if (menuInfo[menuIndex].command == static_cast<GitCommands>(id_it->second))
1687 MAKESTRING(menuInfo[menuIndex].menuDescID);
1688 break;
1690 menuIndex++;
1693 const wchar_t* desc = stringtablebuffer;
1694 switch(uFlags)
1696 case GCS_HELPTEXTA:
1698 std::string help = WideToMultibyte(desc);
1699 lstrcpynA(pszName, help.c_str(), cchMax - 1);
1700 hr = S_OK;
1701 break;
1703 case GCS_HELPTEXTW:
1705 std::wstring help = desc;
1706 lstrcpynW(reinterpret_cast<LPWSTR>(pszName), help.c_str(), cchMax - 1);
1707 hr = S_OK;
1708 break;
1710 case GCS_VERBA:
1712 const auto verb_id_it = myVerbsIDMap.lower_bound(idCmd);
1713 if (verb_id_it != myVerbsIDMap.end() && verb_id_it->first == idCmd)
1715 std::string help = WideToMultibyte(verb_id_it->second);
1716 lstrcpynA(pszName, help.c_str(), cchMax - 1);
1717 hr = S_OK;
1720 break;
1721 case GCS_VERBW:
1723 const auto verb_id_it = myVerbsIDMap.lower_bound(idCmd);
1724 if (verb_id_it != myVerbsIDMap.end() && verb_id_it->first == idCmd)
1726 std::wstring help = verb_id_it->second;
1727 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": verb : %ws\n", help.c_str());
1728 lstrcpynW(reinterpret_cast<LPWSTR>(pszName), help.c_str(), cchMax - 1);
1729 hr = S_OK;
1732 break;
1734 return hr;
1737 STDMETHODIMP CShellExt::HandleMenuMsg(UINT uMsg, WPARAM wParam, LPARAM lParam)
1739 LRESULT res;
1740 return HandleMenuMsg2(uMsg, wParam, lParam, &res);
1743 STDMETHODIMP CShellExt::HandleMenuMsg2(UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT *pResult)
1745 PreserveChdir preserveChdir;
1747 LRESULT res;
1748 if (!pResult)
1749 pResult = &res;
1750 *pResult = FALSE;
1752 LoadLangDll();
1753 switch (uMsg)
1755 case WM_MEASUREITEM:
1757 auto lpmis = reinterpret_cast<MEASUREITEMSTRUCT*>(lParam);
1758 if (!lpmis)
1759 break;
1760 lpmis->itemWidth = 16;
1761 lpmis->itemHeight = 16;
1762 *pResult = TRUE;
1764 break;
1765 case WM_DRAWITEM:
1767 LPCWSTR resource;
1768 auto lpdis = reinterpret_cast<DRAWITEMSTRUCT*>(lParam);
1769 if (!lpdis || lpdis->CtlType != ODT_MENU)
1770 return S_OK; //not for a menu
1771 resource = GetMenuTextFromResource(static_cast<int>(myIDMap[lpdis->itemID]));
1772 if (!resource)
1773 return S_OK;
1774 int iconWidth = GetSystemMetrics(SM_CXSMICON);
1775 int iconHeight = GetSystemMetrics(SM_CYSMICON);
1776 CAutoIcon hIcon = LoadIconEx(g_hResInst, resource, iconWidth, iconHeight);
1777 if (!hIcon)
1778 return S_OK;
1779 DrawIconEx(lpdis->hDC,
1780 lpdis->rcItem.left,
1781 lpdis->rcItem.top + (lpdis->rcItem.bottom - lpdis->rcItem.top - iconHeight) / 2,
1782 hIcon, iconWidth, iconHeight,
1783 0, nullptr, DI_NORMAL);
1784 *pResult = TRUE;
1786 break;
1787 case WM_MENUCHAR:
1789 wchar_t* szItem;
1790 if (HIWORD(wParam) != MF_POPUP)
1791 return S_OK;
1792 int nChar = LOWORD(wParam);
1793 if (_istascii(static_cast<wint_t>(nChar)) && _istupper(static_cast<wint_t>(nChar)))
1794 nChar = tolower(nChar);
1795 // we have the char the user pressed, now search that char in all our
1796 // menu items
1797 std::vector<UINT_PTR> accmenus;
1798 for (auto It = mySubMenuMap.cbegin(); It != mySubMenuMap.cend(); ++It)
1800 LPCWSTR resource = GetMenuTextFromResource(static_cast<int>(mySubMenuMap[It->first]));
1801 if (!resource)
1802 continue;
1803 szItem = stringtablebuffer;
1804 wchar_t* amp = wcschr(szItem, L'&');
1805 if (!amp)
1806 continue;
1807 amp++;
1808 int ampChar = LOWORD(*amp);
1809 if (_istascii(static_cast<wint_t>(ampChar)) && _istupper(static_cast<wint_t>(ampChar)))
1810 ampChar = tolower(ampChar);
1811 if (ampChar == nChar)
1813 // yep, we found a menu which has the pressed key
1814 // as an accelerator. Add that menu to the list to
1815 // process later.
1816 accmenus.push_back(It->first);
1819 if (accmenus.empty())
1821 // no menu with that accelerator key.
1822 *pResult = MAKELONG(0, MNC_IGNORE);
1823 return S_OK;
1825 if (accmenus.size() == 1)
1827 // Only one menu with that accelerator key. We're lucky!
1828 // So just execute that menu entry.
1829 *pResult = MAKELONG(accmenus[0], MNC_EXECUTE);
1830 return S_OK;
1832 if (accmenus.size() > 1)
1834 // we have more than one menu item with this accelerator key!
1835 MENUITEMINFO mif;
1836 mif.cbSize = sizeof(MENUITEMINFO);
1837 mif.fMask = MIIM_STATE;
1838 for (auto it = accmenus.cbegin(); it != accmenus.cend(); ++it)
1840 GetMenuItemInfo(reinterpret_cast<HMENU>(lParam), static_cast<UINT>(*it), TRUE, &mif);
1841 if (mif.fState == MFS_HILITE)
1843 // this is the selected item, so select the next one
1844 ++it;
1845 if (it == accmenus.end())
1846 *pResult = MAKELONG(accmenus[0], MNC_SELECT);
1847 else
1848 *pResult = MAKELONG(*it, MNC_SELECT);
1849 return S_OK;
1852 *pResult = MAKELONG(accmenus[0], MNC_SELECT);
1855 break;
1856 default:
1857 return S_OK;
1860 return S_OK;
1863 LPCWSTR CShellExt::GetMenuTextFromResource(int id)
1865 wchar_t textbuf[255] = { 0 };
1866 LPCWSTR resource = nullptr;
1867 unsigned __int64 layout = g_ShellCache.GetMenuLayout();
1868 space = 6;
1870 int menuIndex = 0;
1871 while (menuInfo[menuIndex].command != ShellMenuLastEntry)
1873 if (menuInfo[menuIndex].command == id)
1875 MAKESTRING(menuInfo[menuIndex].menuTextID);
1876 resource = MAKEINTRESOURCE(menuInfo[menuIndex].iconID);
1877 switch (id)
1879 case ShellSubMenuMultiple:
1880 case ShellSubMenuLink:
1881 case ShellSubMenuFolder:
1882 case ShellSubMenuFile:
1883 case ShellSubMenu:
1884 space = 0;
1885 break;
1886 default:
1887 space = (layout & menuInfo[menuIndex].menuID) ? 0 : 6;
1888 if (layout & menuInfo[menuIndex].menuID)
1890 wcscpy_s(textbuf, L"Git ");
1891 wcscat_s(textbuf, stringtablebuffer);
1892 wcscpy_s(stringtablebuffer, textbuf);
1894 break;
1896 return resource;
1898 menuIndex++;
1900 return nullptr;
1903 bool CShellExt::IsIllegalFolder(const std::wstring& folder)
1905 static const GUID code[] = {
1906 FOLDERID_RecycleBinFolder,
1907 FOLDERID_CDBurning,
1908 FOLDERID_Favorites,
1909 FOLDERID_CommonStartMenu,
1910 FOLDERID_NetworkFolder,
1911 FOLDERID_ConnectionsFolder,
1912 FOLDERID_ControlPanelFolder,
1913 FOLDERID_Cookies,
1914 FOLDERID_Favorites,
1915 FOLDERID_Fonts,
1916 FOLDERID_History,
1917 FOLDERID_InternetFolder,
1918 FOLDERID_InternetCache,
1919 FOLDERID_NetHood,
1920 FOLDERID_NetworkFolder,
1921 FOLDERID_PrintersFolder,
1922 FOLDERID_PrintHood,
1923 FOLDERID_Recent,
1924 FOLDERID_SendTo,
1925 FOLDERID_StartMenu,
1927 for (int i = 0; i < _countof(code); i++)
1929 CComHeapPtr<WCHAR> pszPath;
1930 if (SHGetKnownFolderPath(code[i], 0, nullptr, &pszPath) != S_OK)
1931 continue;
1932 if (!pszPath[0])
1933 continue;
1934 if (wcscmp(pszPath, folder.c_str()) == 0)
1935 return true;
1937 return false;
1940 bool CShellExt::InsertLFSSubmenu(UINT& idCmd, UINT idCmdFirst, HMENU hMenu, HMENU subMenu, UINT& indexMenu, int& indexSubMenu, unsigned __int64 topmenu, bool bShowIcons, UINT uFlags)
1942 static MenuInfo infos[] = {
1943 { ShellMenuLFSLocks, 0, IDI_REPOBROWSE, IDS_MENULFSLOCKS, IDS_MENUDESCLFSLOCKS, { ITEMIS_FOLDERINGIT | ITEMIS_ONLYONE, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 } },
1944 { ShellMenuLFSLock, 0, IDI_LFSLOCK, IDS_MENULFSLOCK, IDS_MENUDESCLFSLOCK, { ITEMIS_INGIT | ITEMIS_INVERSIONEDFOLDER, ITEMIS_FOLDER }, { 0, 0 }, { 0, 0 }, { 0, 0 } },
1945 { ShellMenuLFSUnlock, 0, IDI_LFSUNLOCK, IDS_MENULFSUNLOCK, IDS_MENUDESCLFSUNLOCK, { ITEMIS_INGIT | ITEMIS_INVERSIONEDFOLDER, ITEMIS_FOLDER }, { 0, 0 }, { 0, 0 }, { 0, 0 } },
1948 if (folder_.empty() && files_.empty())
1949 return false;
1951 CTGitPath askedpath;
1952 askedpath.SetFromWin(folder_.empty() ? files_.front().c_str() : folder_.c_str());
1953 if (!askedpath.HasLFS())
1954 return false;
1956 HMENU lfssubmenu = hMenu ? CreateMenu() : nullptr;
1957 int indexlfssub = 0;
1958 bool anyMenu = false;
1960 for (const auto& info : infos)
1962 if (ShouldInsertItem(info))
1964 InsertGitMenu(false, lfssubmenu, indexlfssub++, idCmd++, info.menuTextID, bShowIcons ? info.iconID : 0, idCmdFirst, info.command, uFlags);
1965 anyMenu = true;
1969 if (!anyMenu)
1970 return false;
1972 MENUITEMINFO menuiteminfo = { 0 };
1973 menuiteminfo.cbSize = sizeof(menuiteminfo);
1974 menuiteminfo.fMask = MIIM_FTYPE | MIIM_ID | MIIM_SUBMENU | MIIM_DATA | MIIM_STRING | MIIM_BITMAP;
1975 menuiteminfo.fType = MFT_STRING;
1976 menuiteminfo.hbmpItem = m_iconBitmapUtils.IconToBitmapPARGB32(g_hResInst, IDI_LFS);
1977 menuiteminfo.hSubMenu = lfssubmenu;
1978 menuiteminfo.wID = idCmd;
1980 SecureZeroMemory(stringtablebuffer, sizeof(stringtablebuffer));
1981 GetMenuTextFromResource(ShellMenuLFSMenu);
1982 menuiteminfo.dwTypeData = stringtablebuffer;
1983 menuiteminfo.cch = static_cast<UINT>(min(wcslen(menuiteminfo.dwTypeData), static_cast<size_t>(UINT_MAX)));
1985 if (hMenu)
1986 InsertMenuItem((topmenu & MENULFS) ? hMenu : subMenu, (topmenu & MENULFS) ? indexMenu++ : indexSubMenu++, TRUE, &menuiteminfo);
1987 myIDMap[idCmd - idCmdFirst] = ShellMenuLFSMenu;
1988 myIDMap[idCmd++] = ShellMenuLFSMenu;
1990 return true;
1993 bool CShellExt::InsertIgnoreSubmenus(UINT &idCmd, UINT idCmdFirst, HMENU hMenu, HMENU subMenu, UINT &indexMenu, int &indexSubMenu, unsigned __int64 topmenu, bool bShowIcons, UINT /*uFlags*/)
1995 HMENU ignoresubmenu = nullptr;
1996 int indexignoresub = 0;
1997 bool bShowIgnoreMenu = false;
1998 wchar_t maskbuf[MAX_PATH] = {0}; // MAX_PATH is ok, since this only holds a filename
1999 wchar_t ignorepath[MAX_PATH] = {0}; // MAX_PATH is ok, since this only holds a filename
2000 std::vector<Microsoft::WRL::ComPtr<CExplorerCommand>> exCmds;
2001 if (files_.empty())
2002 return false;
2003 const UINT icon = bShowIcons ? IDI_IGNORE : 0;
2005 auto I = files_.cbegin();
2006 if (wcsrchr(I->c_str(), L'\\'))
2007 wcscpy_s(ignorepath, wcsrchr(I->c_str(), L'\\') + 1);
2008 else
2009 wcscpy_s(ignorepath, I->c_str());
2010 if ((itemStates & ITEMIS_IGNORED) && (!ignoredprops.empty()))
2012 // check if the item name is ignored or the mask
2013 size_t p = 0;
2014 const size_t pathLength = wcslen(ignorepath);
2015 while ( (p=ignoredprops.find( ignorepath,p )) != -1 )
2017 if ((p == 0 || ignoredprops[p - 1] == wchar_t('\n'))
2018 && (p + pathLength == ignoredprops.length() || ignoredprops[p + pathLength + 1] == wchar_t('\n')))
2020 break;
2022 p++;
2024 if (p!=-1)
2026 ignoresubmenu = hMenu ? CreateMenu() : nullptr;
2027 if (hMenu)
2028 InsertMenu(ignoresubmenu, indexignoresub++, MF_BYPOSITION | MF_STRING , idCmd, ignorepath);
2029 else
2030 exCmds.push_back(Microsoft::WRL::Make<CExplorerCommand>(ignorepath, 0, ShellMenuUnIgnore, static_cast<LPCWSTR>(CPathUtils::GetAppDirectory(g_hmodThisDll)), uuidSource, itemStates, itemStatesFolder, files_, std::vector<Microsoft::WRL::ComPtr<CExplorerCommand>>(), m_site));
2031 auto verb = std::wstring(ignorepath);
2032 myVerbsMap[verb] = idCmd - idCmdFirst;
2033 myVerbsMap[verb] = idCmd;
2034 myVerbsIDMap[idCmd - idCmdFirst] = verb;
2035 myVerbsIDMap[idCmd] = verb;
2036 myIDMap[idCmd - idCmdFirst] = ShellMenuUnIgnore;
2037 myIDMap[idCmd++] = ShellMenuUnIgnore;
2038 bShowIgnoreMenu = true;
2040 wcscpy_s(maskbuf, L"*");
2041 if (wcsrchr(ignorepath, L'.'))
2043 wcscat_s(maskbuf, wcsrchr(ignorepath, L'.'));
2044 p = ignoredprops.find(maskbuf);
2045 if ((p!=-1) &&
2046 ((ignoredprops.compare(maskbuf) == 0) || (ignoredprops.find(L'\n', p) == p + wcslen(maskbuf) + 1) || (ignoredprops.rfind(L'\n', p) == p - 1)))
2048 if (!ignoresubmenu)
2049 ignoresubmenu = CreateMenu();
2050 if (hMenu)
2051 InsertMenu(ignoresubmenu, indexignoresub++, MF_BYPOSITION | MF_STRING , idCmd, maskbuf);
2052 else
2053 exCmds.push_back(Microsoft::WRL::Make<CExplorerCommand>(maskbuf, 0, ShellMenuUnIgnoreCaseSensitive, static_cast<LPCWSTR>(CPathUtils::GetAppDirectory(g_hmodThisDll)), uuidSource, itemStates, itemStatesFolder, files_, std::vector<Microsoft::WRL::ComPtr<CExplorerCommand>>(), m_site));
2054 auto verb = std::wstring(maskbuf);
2055 myVerbsMap[verb] = idCmd - idCmdFirst;
2056 myVerbsMap[verb] = idCmd;
2057 myVerbsIDMap[idCmd - idCmdFirst] = verb;
2058 myVerbsIDMap[idCmd] = verb;
2059 myIDMap[idCmd - idCmdFirst] = ShellMenuUnIgnoreCaseSensitive;
2060 myIDMap[idCmd++] = ShellMenuUnIgnoreCaseSensitive;
2061 bShowIgnoreMenu = true;
2065 else if ((itemStates & ITEMIS_IGNORED) == 0)
2067 bShowIgnoreMenu = true;
2068 ignoresubmenu = CreateMenu();
2069 if (itemStates & ITEMIS_ONLYONE)
2071 if (itemStates & ITEMIS_INGIT)
2073 if (hMenu)
2074 InsertMenu(ignoresubmenu, indexignoresub++, MF_BYPOSITION | MF_STRING , idCmd, ignorepath);
2075 else
2076 exCmds.push_back(Microsoft::WRL::Make<CExplorerCommand>(ignorepath, 0, ShellMenuDeleteIgnore, static_cast<LPCWSTR>(CPathUtils::GetAppDirectory(g_hmodThisDll)), uuidSource, itemStates, itemStatesFolder, files_, std::vector<Microsoft::WRL::ComPtr<CExplorerCommand>>(), m_site));
2077 myIDMap[idCmd - idCmdFirst] = ShellMenuDeleteIgnore;
2078 myIDMap[idCmd++] = ShellMenuDeleteIgnore;
2080 wcscpy_s(maskbuf, L"*");
2081 if (!(itemStates & ITEMIS_FOLDER) && wcsrchr(ignorepath, L'.'))
2083 wcscat_s(maskbuf, wcsrchr(ignorepath, L'.'));
2084 if (hMenu)
2085 InsertMenu(ignoresubmenu, indexignoresub++, MF_BYPOSITION | MF_STRING , idCmd, maskbuf);
2086 else
2087 exCmds.push_back(Microsoft::WRL::Make<CExplorerCommand>(maskbuf, 0, ShellMenuDeleteIgnoreCaseSensitive, static_cast<LPCWSTR>(CPathUtils::GetAppDirectory(g_hmodThisDll)), uuidSource, itemStates, itemStatesFolder, files_, std::vector<Microsoft::WRL::ComPtr<CExplorerCommand>>(), m_site));
2088 auto verb = std::wstring(maskbuf);
2089 myVerbsMap[verb] = idCmd - idCmdFirst;
2090 myVerbsMap[verb] = idCmd;
2091 myVerbsIDMap[idCmd - idCmdFirst] = verb;
2092 myVerbsIDMap[idCmd] = verb;
2093 myIDMap[idCmd - idCmdFirst] = ShellMenuDeleteIgnoreCaseSensitive;
2094 myIDMap[idCmd++] = ShellMenuDeleteIgnoreCaseSensitive;
2097 else
2099 if (hMenu)
2100 InsertMenu(ignoresubmenu, indexignoresub++, MF_BYPOSITION | MF_STRING , idCmd, ignorepath);
2101 else
2102 exCmds.push_back(Microsoft::WRL::Make<CExplorerCommand>(ignorepath, 0, ShellMenuIgnore, static_cast<LPCWSTR>(CPathUtils::GetAppDirectory(g_hmodThisDll)), uuidSource, itemStates, itemStatesFolder, files_, std::vector<Microsoft::WRL::ComPtr<CExplorerCommand>>(), m_site));
2103 myIDMap[idCmd - idCmdFirst] = ShellMenuIgnore;
2104 myIDMap[idCmd++] = ShellMenuIgnore;
2106 wcscpy_s(maskbuf, L"*");
2107 if (!(itemStates & ITEMIS_FOLDER) && wcsrchr(ignorepath, L'.'))
2109 wcscat_s(maskbuf, wcsrchr(ignorepath, L'.'));
2110 if (hMenu)
2111 InsertMenu(ignoresubmenu, indexignoresub++, MF_BYPOSITION | MF_STRING, idCmd, maskbuf);
2112 else
2113 exCmds.push_back(Microsoft::WRL::Make<CExplorerCommand>(maskbuf, 0, ShellMenuIgnoreCaseSensitive, static_cast<LPCWSTR>(CPathUtils::GetAppDirectory(g_hmodThisDll)), uuidSource, itemStates, itemStatesFolder, files_, std::vector<Microsoft::WRL::ComPtr<CExplorerCommand>>(), m_site));
2114 auto verb = std::wstring(maskbuf);
2115 myVerbsMap[verb] = idCmd - idCmdFirst;
2116 myVerbsMap[verb] = idCmd;
2117 myVerbsIDMap[idCmd - idCmdFirst] = verb;
2118 myVerbsIDMap[idCmd] = verb;
2119 myIDMap[idCmd - idCmdFirst] = ShellMenuIgnoreCaseSensitive;
2120 myIDMap[idCmd++] = ShellMenuIgnoreCaseSensitive;
2124 else
2126 // note: as of Windows 7, the shell does not pass more than 16 items from a multiselection
2127 // in the Initialize() call before the QueryContextMenu() call. Which means even if the user
2128 // has selected more than 16 files, we won't know about that here.
2129 // Note: after QueryContextMenu() exits, Initialize() is called again with all selected files.
2130 if (itemStates & ITEMIS_INGIT)
2132 if (files_.size() >= 16)
2134 MAKESTRING(IDS_MENUDELETEIGNOREMULTIPLE2);
2135 wcscpy_s(ignorepath, stringtablebuffer);
2137 else
2139 MAKESTRING(IDS_MENUDELETEIGNOREMULTIPLE);
2140 swprintf_s(ignorepath, stringtablebuffer, files_.size());
2142 if (hMenu)
2143 InsertMenu(ignoresubmenu, indexignoresub++, MF_BYPOSITION | MF_STRING , idCmd, ignorepath);
2144 else
2145 exCmds.push_back(Microsoft::WRL::Make<CExplorerCommand>(ignorepath, 0, ShellMenuDeleteIgnore, static_cast<LPCWSTR>(CPathUtils::GetAppDirectory(g_hmodThisDll)), uuidSource, itemStates, itemStatesFolder, files_, std::vector<Microsoft::WRL::ComPtr<CExplorerCommand>>(), m_site));
2146 auto verb = std::wstring(ignorepath);
2147 myVerbsMap[verb] = idCmd - idCmdFirst;
2148 myVerbsMap[verb] = idCmd;
2149 myVerbsIDMap[idCmd - idCmdFirst] = verb;
2150 myVerbsIDMap[idCmd] = verb;
2151 myIDMap[idCmd - idCmdFirst] = ShellMenuDeleteIgnore;
2152 myIDMap[idCmd++] = ShellMenuDeleteIgnore;
2154 if (files_.size() >= 16)
2156 MAKESTRING(IDS_MENUDELETEIGNOREMULTIPLEMASK2);
2157 wcscpy_s(ignorepath, stringtablebuffer);
2159 else
2161 MAKESTRING(IDS_MENUDELETEIGNOREMULTIPLEMASK);
2162 swprintf_s(ignorepath, stringtablebuffer, files_.size());
2164 if (hMenu)
2165 InsertMenu(ignoresubmenu, indexignoresub++, MF_BYPOSITION | MF_STRING , idCmd, ignorepath);
2166 else
2167 exCmds.push_back(Microsoft::WRL::Make<CExplorerCommand>(ignorepath, 0, ShellMenuDeleteIgnoreCaseSensitive, static_cast<LPCWSTR>(CPathUtils::GetAppDirectory(g_hmodThisDll)), uuidSource, itemStates, itemStatesFolder, files_, std::vector<Microsoft::WRL::ComPtr<CExplorerCommand>>(), m_site));
2168 verb = std::wstring(ignorepath);
2169 myVerbsMap[verb] = idCmd - idCmdFirst;
2170 myVerbsMap[verb] = idCmd;
2171 myVerbsIDMap[idCmd - idCmdFirst] = verb;
2172 myVerbsIDMap[idCmd] = verb;
2173 myIDMap[idCmd - idCmdFirst] = ShellMenuDeleteIgnoreCaseSensitive;
2174 myIDMap[idCmd++] = ShellMenuDeleteIgnoreCaseSensitive;
2176 else
2178 if (files_.size() >= 16)
2180 MAKESTRING(IDS_MENUIGNOREMULTIPLE2);
2181 wcscpy_s(ignorepath, stringtablebuffer);
2183 else
2185 MAKESTRING(IDS_MENUIGNOREMULTIPLE);
2186 swprintf_s(ignorepath, stringtablebuffer, files_.size());
2188 if (hMenu)
2189 InsertMenu(ignoresubmenu, indexignoresub++, MF_BYPOSITION | MF_STRING , idCmd, ignorepath);
2190 else
2191 exCmds.push_back(Microsoft::WRL::Make<CExplorerCommand>(ignorepath, 0, ShellMenuIgnore, static_cast<LPCWSTR>(CPathUtils::GetAppDirectory(g_hmodThisDll)), uuidSource, itemStates, itemStatesFolder, files_, std::vector<Microsoft::WRL::ComPtr<CExplorerCommand>>(), m_site));
2192 auto verb = std::wstring(ignorepath);
2193 myVerbsMap[verb] = idCmd - idCmdFirst;
2194 myVerbsMap[verb] = idCmd;
2195 myVerbsIDMap[idCmd - idCmdFirst] = verb;
2196 myVerbsIDMap[idCmd] = verb;
2197 myIDMap[idCmd - idCmdFirst] = ShellMenuIgnore;
2198 myIDMap[idCmd++] = ShellMenuIgnore;
2200 if (files_.size() >= 16)
2202 MAKESTRING(IDS_MENUIGNOREMULTIPLEMASK2);
2203 wcscpy_s(ignorepath, stringtablebuffer);
2205 else
2207 MAKESTRING(IDS_MENUIGNOREMULTIPLEMASK);
2208 swprintf_s(ignorepath, stringtablebuffer, files_.size());
2210 if (hMenu)
2211 InsertMenu(ignoresubmenu, indexignoresub++, MF_BYPOSITION | MF_STRING , idCmd, ignorepath);
2212 else
2213 exCmds.push_back(Microsoft::WRL::Make<CExplorerCommand>(ignorepath, 0, ShellMenuIgnoreCaseSensitive, static_cast<LPCWSTR>(CPathUtils::GetAppDirectory(g_hmodThisDll)), uuidSource, itemStates, itemStatesFolder, files_, std::vector<Microsoft::WRL::ComPtr<CExplorerCommand>>(), m_site));
2214 verb = std::wstring(ignorepath);
2215 myVerbsMap[verb] = idCmd - idCmdFirst;
2216 myVerbsMap[verb] = idCmd;
2217 myVerbsIDMap[idCmd - idCmdFirst] = verb;
2218 myVerbsIDMap[idCmd] = verb;
2219 myIDMap[idCmd - idCmdFirst] = ShellMenuIgnoreCaseSensitive;
2220 myIDMap[idCmd++] = ShellMenuIgnoreCaseSensitive;
2225 if (bShowIgnoreMenu)
2227 MENUITEMINFO menuiteminfo = { 0 };
2228 menuiteminfo.cbSize = sizeof(menuiteminfo);
2229 menuiteminfo.fMask = MIIM_FTYPE | MIIM_ID | MIIM_SUBMENU | MIIM_DATA | MIIM_STRING;
2230 if (icon)
2232 menuiteminfo.fMask |= MIIM_BITMAP;
2233 menuiteminfo.hbmpItem = m_iconBitmapUtils.IconToBitmapPARGB32(g_hResInst, icon);
2235 menuiteminfo.fType = MFT_STRING;
2236 menuiteminfo.hSubMenu = ignoresubmenu;
2237 menuiteminfo.wID = idCmd;
2238 SecureZeroMemory(stringtablebuffer, sizeof(stringtablebuffer));
2239 if (itemStates & ITEMIS_IGNORED)
2240 GetMenuTextFromResource(ShellMenuUnIgnoreSub);
2241 else if (itemStates & ITEMIS_INGIT)
2242 GetMenuTextFromResource(ShellMenuDeleteIgnoreSub);
2243 else
2244 GetMenuTextFromResource(ShellMenuIgnoreSub);
2245 menuiteminfo.dwTypeData = stringtablebuffer;
2246 menuiteminfo.cch = static_cast<UINT>(min(wcslen(menuiteminfo.dwTypeData), static_cast<size_t>(UINT_MAX)));
2248 if (hMenu)
2249 InsertMenuItem((topmenu & MENUIGNORE) ? hMenu : subMenu, (topmenu & MENUIGNORE) ? indexMenu++ : indexSubMenu++, TRUE, &menuiteminfo);
2250 else
2252 m_explorerCommands.push_back(Microsoft::WRL::Make<CExplorerCommand>(L"", 0, ShellSeparator, static_cast<LPCWSTR>(CPathUtils::GetAppDirectory(g_hmodThisDll)), uuidSource, itemStates, itemStatesFolder, files_, std::vector<Microsoft::WRL::ComPtr<CExplorerCommand>>(), m_site));
2253 for (const auto& cmd : exCmds)
2255 m_explorerCommands.push_back(cmd);
2256 std::wstring prep = stringtablebuffer;
2257 prep += L": ";
2258 m_explorerCommands.back()->PrependTitleWith(prep);
2260 // currently, explorer does not support subcommands which their own subcommands. Once it does,
2261 // use the line below instead of the ones above
2262 //m_explorerCommands.push_back(CExplorerCommand(stringTableBuffer, icon, ShellMenuUnIgnoreSub, GetAppDirectory(), uuidSource, itemStates, itemStatesFolder, m_files, exCmds));
2264 if (itemStates & ITEMIS_IGNORED)
2266 myIDMap[idCmd - idCmdFirst] = ShellMenuUnIgnoreSub;
2267 myIDMap[idCmd++] = ShellMenuUnIgnoreSub;
2269 else
2271 myIDMap[idCmd - idCmdFirst] = ShellMenuIgnoreSub;
2272 myIDMap[idCmd++] = ShellMenuIgnoreSub;
2275 return bShowIgnoreMenu;
2278 void CShellExt::RunCommand(const std::wstring& path, const std::wstring& command, LPCWSTR errorMessage, Microsoft::WRL::ComPtr<IUnknown> site)
2280 if (site)
2282 Microsoft::WRL::ComPtr<IServiceProvider> serviceProvider;
2283 if (SUCCEEDED(site.As(&serviceProvider)))
2285 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": got IServiceProvider\n");
2286 Microsoft::WRL::ComPtr<IShellBrowser> shellBrowser;
2287 if (SUCCEEDED(serviceProvider->QueryService(SID_SShellBrowser, IID_IShellBrowser, &shellBrowser)))
2289 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": got IShellBrowser\n");
2290 Microsoft::WRL::ComPtr<IShellView> shellView;
2291 if (SUCCEEDED(shellBrowser->QueryActiveShellView(&shellView)))
2293 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": got IShellView\n");
2294 Microsoft::WRL::ComPtr<IDispatch> spdispView;
2295 if (SUCCEEDED(shellView->GetItemObject(SVGIO_BACKGROUND, IID_PPV_ARGS(&spdispView))))
2297 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": got IDispatch\n");
2298 Microsoft::WRL::ComPtr<IShellFolderViewDual> spFolderView;
2299 if (SUCCEEDED(spdispView.As(&spFolderView)))
2301 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": got IShellFolderViewDual\n");
2302 Microsoft::WRL::ComPtr<IDispatch> spdispShell;
2303 if (SUCCEEDED(spFolderView->get_Application(&spdispShell)))
2305 Microsoft::WRL::ComPtr<IShellDispatch2> spdispShell2;
2306 if (SUCCEEDED(spdispShell.As(&spdispShell2)))
2308 // without this, the launched app is not moved to the foreground
2309 AllowSetForegroundWindow(ASFW_ANY);
2311 if (SUCCEEDED(spdispShell2->ShellExecute(_bstr_t{ path.c_str() },
2312 _variant_t{ command.c_str() },
2313 _variant_t{ L"" },
2314 _variant_t{ L"open" },
2315 _variant_t{ SW_NORMAL })))
2317 return;
2327 if (CCreateProcessHelper::CreateProcessDetached(path.c_str(), command.c_str()))
2329 // process started - exit
2330 return;
2333 MessageBox(nullptr, CFormatMessageWrapper(), errorMessage, MB_OK | MB_ICONERROR);
2336 bool CShellExt::ShouldInsertItem(const MenuInfo& item) const
2338 return ShouldEnableMenu(item.first) || ShouldEnableMenu(item.second) ||
2339 ShouldEnableMenu(item.third) || ShouldEnableMenu(item.fourth);
2342 bool CShellExt::ShouldEnableMenu(const YesNoPair& pair) const
2344 if (pair.yes && pair.no)
2346 if (((pair.yes & itemStates) == pair.yes) && ((pair.no & (~itemStates)) == pair.no))
2347 return true;
2349 else if ((pair.yes) && ((pair.yes & itemStates) == pair.yes))
2350 return true;
2351 else if ((pair.no) && ((pair.no & (~itemStates)) == pair.no))
2352 return true;
2353 return false;
2356 // IExplorerCommand
2357 HRESULT __stdcall CShellExt::GetTitle(IShellItemArray* /*psiItemArray*/, LPWSTR* ppszName)
2359 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": Shell :: GetTitle\n");
2360 SHStrDupW(L"TortoiseGit", ppszName);
2361 return S_OK;
2364 HRESULT __stdcall CShellExt::GetIcon(IShellItemArray* /*psiItemArray*/, LPWSTR* ppszIcon)
2366 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": Shell :: GetIcon\n");
2367 std::wstring iconPath = static_cast<LPCWSTR>(CPathUtils::GetAppDirectory(g_hmodThisDll));
2368 iconPath += L"TortoiseGitProc.exe,-";
2369 iconPath += std::to_wstring(IDI_APP);
2370 SHStrDupW(iconPath.c_str(), ppszIcon);
2371 return S_OK;
2374 HRESULT __stdcall CShellExt::GetToolTip(IShellItemArray* /*psiItemArray*/, LPWSTR* ppszInfotip)
2376 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": Shell :: GetToolTip\n");
2377 *ppszInfotip = nullptr;
2378 return E_NOTIMPL;
2381 HRESULT __stdcall CShellExt::GetCanonicalName(GUID* pguidCommandName)
2383 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": Shell :: GetCanonicalName\n");
2384 *pguidCommandName = GUID_NULL;
2385 return S_OK;
2388 HRESULT __stdcall CShellExt::GetState(IShellItemArray* psiItemArray, BOOL fOkToBeSlow, EXPCMDSTATE* pCmdState)
2390 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": Shell :: GetState\n");
2391 *pCmdState = ECS_ENABLED;
2392 Microsoft::WRL::ComPtr<IShellItemArray> ownItemArray;
2393 if (m_site)
2395 Microsoft::WRL::ComPtr<IOleWindow> oleWindow;
2396 m_site.As(&oleWindow);
2397 if (oleWindow)
2399 // We don't want to show the menu on the classic context menu.
2400 // The classic menu provides an IOleWindow, but the main context
2401 // menu in Win11 does not, except on the left tree view.
2402 // So we check the window class name: if it's "NamespaceTreeControl",
2403 // then we're dealing with the main context menu of the tree view.
2404 // If it's not, then we're dealing with the classic context menu
2405 // and there we hide this menu entry.
2406 HWND hWnd = nullptr;
2407 oleWindow->GetWindow(&hWnd);
2408 wchar_t szWndClassName[MAX_PATH] = { 0 };
2409 GetClassName(hWnd, szWndClassName, _countof(szWndClassName));
2410 if (wcscmp(szWndClassName, L"NamespaceTreeControl"))
2412 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": Shell :: GetState - hidden\n");
2413 *pCmdState = ECS_HIDDEN;
2414 return S_OK;
2416 else
2418 // tree view
2419 if (!psiItemArray)
2421 using SetThreadDpiAwarenessContextProc = DPI_AWARENESS_CONTEXT(WINAPI*)(DPI_AWARENESS_CONTEXT);
2422 SetThreadDpiAwarenessContextProc SetThreadDpiAwarenessContext = reinterpret_cast<SetThreadDpiAwarenessContextProc>(GetProcAddress(GetModuleHandle(L"user32"), "SetThreadDpiAwarenessContext"));
2423 DPI_AWARENESS_CONTEXT context = DPI_AWARENESS_CONTEXT_UNAWARE;
2424 // the shell disables dpi awareness for extensions, so enable them explicitly
2425 if (SetThreadDpiAwarenessContext)
2426 context = SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE);
2427 Microsoft::WRL::ComPtr<INameSpaceTreeControl> nameSpaceTreeCtrl;
2428 oleWindow.As(&nameSpaceTreeCtrl);
2429 if (nameSpaceTreeCtrl)
2431 // we do a hit test on the tree view to get the right-clicked item.
2432 // however this only works if the menu shows up due to a right-click.
2433 // if the menu is shown because of a key press (right windows key),
2434 // then this will get the wrong item if the mouse pointer is somewhere
2435 // over the tree view while the key is clicked!
2436 // if the mouse pointer is NOT over the tree view when the menu is brought up
2437 // via keyboard, then this works fine.
2438 auto msgPos = GetMessagePos();
2439 POINT msgPoint{};
2440 msgPoint.x = GET_X_LPARAM(msgPos);
2441 msgPoint.y = GET_Y_LPARAM(msgPos);
2442 POINT pt = msgPoint;
2443 ScreenToClient(hWnd, &pt);
2444 Microsoft::WRL::ComPtr<IShellItem> shellItem;
2446 nameSpaceTreeCtrl->HitTest(&pt, &shellItem);
2447 if (shellItem)
2448 SHCreateShellItemArrayFromShellItem(shellItem.Get(), IID_IShellItemArray, &ownItemArray);
2449 else
2450 nameSpaceTreeCtrl->GetSelectedItems(&ownItemArray);
2452 if (SetThreadDpiAwarenessContext)
2453 SetThreadDpiAwarenessContext(context);
2454 if (!ownItemArray)
2456 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": Shell :: GetState - hidden\n");
2457 *pCmdState = ECS_HIDDEN;
2458 return S_OK;
2460 else
2461 psiItemArray = ownItemArray.Get();
2467 if (!fOkToBeSlow)
2468 return E_PENDING;
2470 Initialize(nullptr, nullptr, nullptr);
2471 Microsoft::WRL::ComPtr<IShellItemArray> itemArray;
2472 if (!psiItemArray)
2474 // context menu for a folder background (no selection),
2475 // so try to get the current path of the explorer window instead
2476 auto path = ExplorerViewPath(m_site);
2477 if (path.empty())
2479 *pCmdState = ECS_HIDDEN;
2480 return S_OK;
2482 PIDLIST_ABSOLUTE pidl{};
2483 if (SUCCEEDED(SHParseDisplayName(path.c_str(), nullptr, &pidl, 0, nullptr)))
2485 if (SUCCEEDED(SHCreateShellItemArrayFromIDLists(1, const_cast<LPCITEMIDLIST*>(&pidl), itemArray.GetAddressOf())))
2487 if (itemArray)
2489 psiItemArray = itemArray.Get();
2494 if (psiItemArray)
2496 IDataObject* pDataObj = nullptr;
2497 if (SUCCEEDED(psiItemArray->BindToHandler(nullptr, BHID_DataObject, IID_IDataObject, reinterpret_cast<void**>(&pDataObj))))
2499 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": Shell :: Initialize from GetState()\n");
2501 Initialize(nullptr, pDataObj, nullptr);
2502 pDataObj->Release();
2503 pDataObj = nullptr;
2505 else
2506 *pCmdState = ECS_HIDDEN;
2509 if (g_ShellCache.HideMenusForUnversionedItems() && (GetKeyState(VK_SHIFT) & 0x8000) == 0)
2511 if ((itemStates & (ITEMIS_INGIT | ITEMIS_INVERSIONEDFOLDER | ITEMIS_FOLDERINGIT)) == 0)
2512 *pCmdState = ECS_HIDDEN;
2514 if (*pCmdState != ECS_HIDDEN)
2516 m_explorerCommands.clear();
2517 QueryContextMenu(nullptr, 0, 0, 0, CMF_EXTENDEDVERBS | CMF_NORMAL);
2518 if (m_explorerCommands.empty())
2519 *pCmdState = ECS_HIDDEN;
2522 return S_OK;
2525 HRESULT __stdcall CShellExt::Invoke(IShellItemArray* /*psiItemArray*/, IBindCtx* /*pbc*/)
2527 return E_NOTIMPL;
2530 HRESULT __stdcall CShellExt::GetFlags(EXPCMDFLAGS* pFlags)
2532 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": Shell :: GetFlags\n");
2533 *pFlags = ECF_HASSUBCOMMANDS;
2534 return S_OK;
2537 HRESULT __stdcall CShellExt::EnumSubCommands(IEnumExplorerCommand** ppEnum)
2539 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": Shell :: EnumSubCommands %lld\n", m_explorerCommands.size());
2540 m_explorerCommands.clear();
2541 QueryContextMenu(nullptr, 0, 0, 0, CMF_EXTENDEDVERBS | CMF_NORMAL);
2542 *ppEnum = Microsoft::WRL::Make<CExplorerCommandEnum>(m_explorerCommands).Detach();
2543 return S_OK;
2546 std::wstring CShellExt::ExplorerViewPath(const Microsoft::WRL::ComPtr<IUnknown>& site)
2548 CTraceToOutputDebugString::Instance()(__FUNCTION__ "\n");
2549 std::wstring path;
2550 if (site)
2552 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": got site\n");
2553 Microsoft::WRL::ComPtr<IServiceProvider> serviceProvider;
2554 if (SUCCEEDED(site.As(&serviceProvider)))
2556 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": got IServiceProvider\n");
2557 Microsoft::WRL::ComPtr<IShellBrowser> shellBrowser;
2558 if (SUCCEEDED(serviceProvider->QueryService(SID_SShellBrowser, IID_IShellBrowser, &shellBrowser)))
2560 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": got IShellBrowser\n");
2561 Microsoft::WRL::ComPtr<IShellView> shellView;
2562 if (SUCCEEDED(shellBrowser->QueryActiveShellView(&shellView)))
2564 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": got IShellView\n");
2565 Microsoft::WRL::ComPtr<IFolderView> folderView;
2566 if (SUCCEEDED(shellView.As(&folderView)))
2568 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": got IFolderView\n");
2569 Microsoft::WRL::ComPtr<IPersistFolder2> persistFolder;
2570 if (SUCCEEDED(folderView->GetFolder(IID_IPersistFolder2, (LPVOID*)&persistFolder)))
2572 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": got IPersistFolder2\n");
2573 PIDLIST_ABSOLUTE curFolder;
2574 if (SUCCEEDED(persistFolder->GetCurFolder(&curFolder)))
2576 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": got GetCurFolder\n");
2577 wchar_t buf[MAX_PATH] = { 0 };
2578 // find the path of the folder
2579 if (SHGetPathFromIDList(curFolder, buf))
2581 CTraceToOutputDebugString::Instance()(__FUNCTION__ L": got SHGetPathFromIDList : %s\n", buf);
2582 path = buf;
2584 CoTaskMemFree(curFolder);
2593 return path;