Add support for Ctrl+A in BrowseRefsDlg and RepositoryBrowser
[TortoiseGit.git] / src / TortoiseProc / RepositoryBrowser.cpp
blob9490be502780aa8204e7105de714df0d392acb12
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2009-2021 - TortoiseGit
4 // Copyright (C) 2003-2013 - TortoiseSVN
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 // RepositoryBrowser.cpp : implementation file
23 #include "stdafx.h"
24 #include "TortoiseProc.h"
25 #include "RepositoryBrowser.h"
26 #include "LogDlg.h"
27 #include "AppUtils.h"
28 #include "IconMenu.h"
29 #include "UnicodeUtils.h"
30 #include "SysImageList.h"
31 #include <sys/stat.h>
32 #include "registry.h"
33 #include "PathUtils.h"
34 #include "StringUtils.h"
35 #include "GitDiff.h"
36 #include "DragDropImpl.h"
37 #include "GitDataObject.h"
38 #include "TempFile.h"
39 #include "DPIAware.h"
40 #include "MessageBox.h"
42 #define OVERLAY_EXTERNAL 1
43 #define OVERLAY_EXECUTABLE 2
44 #define OVERLAY_SYMLINK 3
46 void SetSortArrowA(CListCtrl * control, int nColumn, bool bAscending)
48 if (!control)
49 return;
51 // set the sort arrow
52 CHeaderCtrl * pHeader = control->GetHeaderCtrl();
53 HDITEM HeaderItem = {0};
54 HeaderItem.mask = HDI_FORMAT;
55 for (int i = 0; i < pHeader->GetItemCount(); ++i)
57 pHeader->GetItem(i, &HeaderItem);
58 HeaderItem.fmt &= ~(HDF_SORTDOWN | HDF_SORTUP);
59 pHeader->SetItem(i, &HeaderItem);
61 if (nColumn >= 0)
63 pHeader->GetItem(nColumn, &HeaderItem);
64 HeaderItem.fmt |= (bAscending ? HDF_SORTUP : HDF_SORTDOWN);
65 pHeader->SetItem(nColumn, &HeaderItem);
69 class CRepoListCompareFunc
71 public:
72 CRepoListCompareFunc(CListCtrl* pList, int col, bool desc)
73 : m_col(col)
74 , m_desc(desc)
75 , m_pList(pList)
78 static int CALLBACK StaticCompare(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort)
80 return reinterpret_cast<CRepoListCompareFunc*>(lParamSort)->Compare(lParam1, lParam2);
83 int Compare(LPARAM lParam1, LPARAM lParam2)
85 auto pLeft = reinterpret_cast<CShadowFilesTree*>(m_pList->GetItemData(static_cast<int>(lParam1)));
86 auto pRight = reinterpret_cast<CShadowFilesTree*>(m_pList->GetItemData(static_cast<int>(lParam2)));
88 int result = 0;
89 switch(m_col)
91 case CRepositoryBrowser::eCol_Name:
92 result = SortStrCmp(pLeft->m_sName, pRight->m_sName);
93 if (result != 0)
94 break;
95 case CRepositoryBrowser::eCol_Extension:
96 result = m_pList->GetItemText(static_cast<int>(lParam1), 1).CompareNoCase(m_pList->GetItemText(static_cast<int>(lParam2), 1));
97 if (result == 0) // if extensions are the same, use the filename to sort
98 result = SortStrCmp(pRight->m_sName, pRight->m_sName);
99 if (result != 0)
100 break;
101 case CRepositoryBrowser::eCol_FileSize:
102 if (pLeft->m_iSize > pRight->m_iSize)
103 result = 1;
104 else if (pLeft->m_iSize < pRight->m_iSize)
105 result = -1;
106 else // fallback
107 result = SortStrCmp(pLeft->m_sName, pRight->m_sName);
110 if (m_desc)
111 result = -result;
113 if (pLeft->m_bFolder != pRight->m_bFolder)
115 if (pRight->m_bFolder)
116 result = 1;
117 else
118 result = -1;
121 return result;
123 int SortStrCmp(const CString &left, const CString &right)
125 if (CRepositoryBrowser::s_bSortLogical)
126 return StrCmpLogicalW(left, right);
127 return StrCmpI(left, right);
130 int m_col;
131 bool m_desc;
132 CListCtrl* m_pList;
135 // CRepositoryBrowser dialog
137 bool CRepositoryBrowser::s_bSortLogical = true;
139 IMPLEMENT_DYNAMIC(CRepositoryBrowser, CResizableStandAloneDialog)
141 CRepositoryBrowser::CRepositoryBrowser(CString rev, CWnd* pParent /*=nullptr*/)
142 : CResizableStandAloneDialog(CRepositoryBrowser::IDD, pParent)
143 , m_currSortCol(0)
144 , m_currSortDesc(false)
145 , m_sRevision(rev)
146 , m_bHasWC(true)
147 , m_ColumnManager(&m_RepoList)
148 , m_nIconFolder(0)
149 , m_nOpenIconFolder(0)
150 , m_nExternalOvl(0)
151 , m_nExecutableOvl(0)
152 , m_nSymlinkOvl(0)
153 , bDragMode(false)
154 , oldy(0)
155 , oldx(0)
159 CRepositoryBrowser::~CRepositoryBrowser()
163 void CRepositoryBrowser::DoDataExchange(CDataExchange* pDX)
165 CDialog::DoDataExchange(pDX);
166 DDX_Control(pDX, IDC_REPOTREE, m_RepoTree);
167 DDX_Control(pDX, IDC_REPOLIST, m_RepoList);
171 BEGIN_MESSAGE_MAP(CRepositoryBrowser, CResizableStandAloneDialog)
172 ON_NOTIFY(TVN_SELCHANGED, IDC_REPOTREE, &CRepositoryBrowser::OnTvnSelchangedRepoTree)
173 ON_NOTIFY(TVN_ITEMEXPANDING, IDC_REPOTREE, &CRepositoryBrowser::OnTvnItemExpandingRepoTree)
174 ON_WM_CONTEXTMENU()
175 ON_NOTIFY(LVN_COLUMNCLICK, IDC_REPOLIST, &CRepositoryBrowser::OnLvnColumnclickRepoList)
176 ON_NOTIFY(LVN_ITEMCHANGED, IDC_REPOLIST, &CRepositoryBrowser::OnLvnItemchangedRepolist)
177 ON_NOTIFY(NM_DBLCLK, IDC_REPOLIST, &CRepositoryBrowser::OnNMDblclk_RepoList)
178 ON_BN_CLICKED(IDC_BUTTON_REVISION, &CRepositoryBrowser::OnBnClickedButtonRevision)
179 ON_WM_SETCURSOR()
180 ON_WM_DESTROY()
181 ON_WM_MOUSEMOVE()
182 ON_WM_LBUTTONDOWN()
183 ON_WM_LBUTTONUP()
184 ON_NOTIFY(LVN_BEGINDRAG, IDC_REPOLIST, &CRepositoryBrowser::OnLvnBegindragRepolist)
185 ON_NOTIFY(TVN_BEGINDRAG, IDC_REPOTREE, &CRepositoryBrowser::OnTvnBegindragRepotree)
186 END_MESSAGE_MAP()
189 // CRepositoryBrowser message handlers
191 BOOL CRepositoryBrowser::OnInitDialog()
193 CResizableStandAloneDialog::OnInitDialog();
194 CAppUtils::MarkWindowAsUnpinnable(m_hWnd);
196 AddAnchor(IDC_STATIC_REPOURL, TOP_LEFT);
197 AddAnchor(IDC_REPOBROWSER_URL, TOP_LEFT, TOP_RIGHT);
198 AddAnchor(IDC_STATIC_REF, TOP_RIGHT);
199 AddAnchor(IDC_BUTTON_REVISION, TOP_RIGHT);
200 AddAnchor(IDC_REPOTREE, TOP_LEFT, BOTTOM_LEFT);
201 AddAnchor(IDC_REPOLIST, TOP_LEFT, BOTTOM_RIGHT);
202 AddAnchor(IDHELP, BOTTOM_RIGHT);
203 AddAnchor(IDC_INFOLABEL, BOTTOM_LEFT, BOTTOM_RIGHT);
204 AddAnchor(IDOK, BOTTOM_RIGHT);
205 AddAnchor(IDCANCEL, BOTTOM_RIGHT);
207 CRepositoryBrowser::s_bSortLogical = !CRegDWORD(L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\Explorer\\NoStrCmpLogical", 0, false, HKEY_CURRENT_USER);
208 if (CRepositoryBrowser::s_bSortLogical)
209 CRepositoryBrowser::s_bSortLogical = !CRegDWORD(L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\Explorer\\NoStrCmpLogical", 0, false, HKEY_LOCAL_MACHINE);
211 static UINT columnNames[] = { IDS_STATUSLIST_COLFILENAME, IDS_STATUSLIST_COLEXT, IDS_LOG_SIZE };
212 static int columnWidths[] = { CDPIAware::Instance().ScaleX(150), CDPIAware::Instance().ScaleX(100), CDPIAware::Instance().ScaleX(100) };
213 DWORD dwDefaultColumns = (1 << eCol_Name) | (1 << eCol_Extension) | (1 << eCol_FileSize);
214 m_ColumnManager.SetNames(columnNames, _countof(columnNames));
215 constexpr int columnVersion = 6; // adjust when changing number/names/etc. of columns
216 m_ColumnManager.ReadSettings(dwDefaultColumns, 0, L"RepoBrowser", columnVersion, _countof(columnNames), columnWidths);
217 m_ColumnManager.SetRightAlign(m_ColumnManager.GetColumnByName(IDS_LOG_SIZE));
219 // set up the list control
220 // set the extended style of the list control
221 // the style LVS_EX_FULLROWSELECT interferes with the background watermark image but it's more important to be able to select in the whole row.
222 CRegDWORD regFullRowSelect(L"Software\\TortoiseGit\\FullRowSelect", TRUE);
223 DWORD exStyle = LVS_EX_HEADERDRAGDROP | LVS_EX_DOUBLEBUFFER | LVS_EX_INFOTIP | LVS_EX_SUBITEMIMAGES;
224 if (DWORD(regFullRowSelect))
225 exStyle |= LVS_EX_FULLROWSELECT;
226 m_RepoList.SetExtendedStyle(exStyle);
227 m_RepoList.SetImageList(&SYS_IMAGE_LIST(), LVSIL_SMALL);
228 CAppUtils::SetListCtrlBackgroundImage(m_RepoList.GetSafeHwnd(), IDI_REPOBROWSER_BKG);
230 m_RepoTree.SetImageList(&SYS_IMAGE_LIST(), TVSIL_NORMAL);
231 exStyle = TVS_EX_FADEINOUTEXPANDOS | TVS_EX_AUTOHSCROLL | TVS_EX_DOUBLEBUFFER;
232 m_RepoTree.SetExtendedStyle(exStyle, exStyle);
234 m_nExternalOvl = SYS_IMAGE_LIST().AddIcon(CCommonAppUtils::LoadIconEx(IDI_EXTERNALOVL, 0, 0));
235 m_nExecutableOvl = SYS_IMAGE_LIST().AddIcon(CCommonAppUtils::LoadIconEx(IDI_EXECUTABLEOVL, 0, 0));
236 m_nSymlinkOvl = SYS_IMAGE_LIST().AddIcon(CCommonAppUtils::LoadIconEx(IDI_SYMLINKOVL, 0, 0));
237 // set externaloverlay in SYS_IMAGE_LIST() in Refresh method, so that it is updated after every launch of the logdialog
239 SetWindowTheme(m_RepoTree.GetSafeHwnd(), L"Explorer", nullptr);
240 SetWindowTheme(m_RepoList.GetSafeHwnd(), L"Explorer", nullptr);
242 int borderWidth = 0;
243 if (IsAppThemed())
245 CAutoThemeData hTheme = OpenThemeData(m_RepoTree, L"TREEVIEW");
246 GetThemeMetric(hTheme, NULL, TVP_TREEITEM, TREIS_NORMAL, TMT_BORDERSIZE, &borderWidth);
248 else
249 borderWidth = GetSystemMetrics(SM_CYBORDER);
250 m_RepoTree.SetItemHeight(static_cast<SHORT>(m_RepoTree.GetItemHeight() + 2 * borderWidth));
252 m_nIconFolder = SYS_IMAGE_LIST().GetDirIconIndex();
253 m_nOpenIconFolder = SYS_IMAGE_LIST().GetDirOpenIconIndex();
255 EnableSaveRestore(L"Reposbrowser");
257 DWORD xPos = CRegDWORD(L"Software\\TortoiseGit\\TortoiseProc\\ResizableState\\RepobrowserDivider", 0);
258 if (xPos == 0)
260 RECT rc;
261 GetDlgItem(IDC_REPOTREE)->GetClientRect(&rc);
262 xPos = rc.right - rc.left;
264 HandleDividerMove(CPoint(CDPIAware::Instance().ScaleX(xPos + 20), CDPIAware::Instance().ScaleY(10)), false);
266 CString sWindowTitle;
267 GetWindowText(sWindowTitle);
268 CAppUtils::SetWindowTitle(m_hWnd, g_Git.m_CurrentDir, sWindowTitle);
270 m_bHasWC = !GitAdminDir::IsBareRepo(g_Git.m_CurrentDir);
272 Refresh();
274 UpdateDiffWithFileFromReg();
276 m_RepoList.SetFocus();
278 return FALSE;
281 void CRepositoryBrowser::UpdateDiffWithFileFromReg()
283 static CString lastDiffLaterFile;
284 if (CString diffLaterFile = CRegString(L"Software\\TortoiseGit\\DiffLater", L""); !diffLaterFile.IsEmpty() && lastDiffLaterFile != diffLaterFile)
286 lastDiffLaterFile = diffLaterFile;
287 m_sMarkForDiffFilename = diffLaterFile;
288 m_sMarkForDiffVersion.Empty();
292 void CRepositoryBrowser::OnDestroy()
294 int maxcol = m_ColumnManager.GetColumnCount();
295 for (int col = 0; col < maxcol; ++col)
296 if (m_ColumnManager.IsVisible(col))
297 m_ColumnManager.ColumnResized(col);
298 m_ColumnManager.WriteSettings();
300 CResizableStandAloneDialog::OnDestroy();
303 void CRepositoryBrowser::OnOK()
305 if (GetFocus() == &m_RepoList && (GetKeyState(VK_MENU) & 0x8000) == 0)
307 // list control has focus: 'enter' the folder
308 if (m_RepoList.GetSelectedCount() != 1)
309 return;
311 POSITION pos = m_RepoList.GetFirstSelectedItemPosition();
312 if (pos)
314 auto item = GetListEntry(m_RepoList.GetNextSelectedItem(pos));
315 if (item->m_bFolder)
317 FillListCtrlForShadowTree(item);
318 m_RepoTree.SelectItem(item->m_hTree);
320 else
321 OpenFile(item->GetFullName(), OPEN, item->m_bSubmodule, item->m_hash);
323 return;
326 SaveDividerPosition();
327 CResizableStandAloneDialog::OnOK();
330 void CRepositoryBrowser::OnCancel()
332 SaveDividerPosition();
333 CResizableStandAloneDialog::OnCancel();
336 void CRepositoryBrowser::OnNMDblclk_RepoList(NMHDR *pNMHDR, LRESULT *pResult)
338 *pResult = 0;
340 LPNMITEMACTIVATE pNmItemActivate = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);
341 if (pNmItemActivate->iItem < 0)
342 return;
344 auto pItem = GetListEntry(pNmItemActivate->iItem);
345 if (!pItem )
346 return;
348 if (!pItem->m_bFolder)
350 OpenFile(pItem->GetFullName(), OPEN, pItem->m_bSubmodule, pItem->m_hash);
351 return;
353 else
355 FillListCtrlForShadowTree(pItem);
356 m_RepoTree.SelectItem(pItem->m_hTree);
360 void CRepositoryBrowser::Refresh()
362 BeginWaitCursor();
363 if (m_nExternalOvl >= 0)
364 SYS_IMAGE_LIST().SetOverlayImage(m_nExternalOvl, OVERLAY_EXTERNAL);
365 if (m_nExecutableOvl >= 0)
366 SYS_IMAGE_LIST().SetOverlayImage(m_nExecutableOvl, OVERLAY_EXECUTABLE);
367 if (m_nSymlinkOvl >= 0)
368 SYS_IMAGE_LIST().SetOverlayImage(m_nSymlinkOvl, OVERLAY_SYMLINK);
370 m_RepoTree.DeleteAllItems();
371 m_RepoList.DeleteAllItems();
372 m_TreeRoot.m_ShadowTree.clear();
373 m_TreeRoot.m_sName.Empty();
374 m_TreeRoot.m_bFolder = true;
376 TVINSERTSTRUCT tvinsert = {0};
377 tvinsert.hParent = TVI_ROOT;
378 tvinsert.hInsertAfter = TVI_ROOT;
379 tvinsert.itemex.mask = TVIF_DI_SETITEM | TVIF_PARAM | TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_STATE;
380 tvinsert.itemex.pszText = const_cast<LPWSTR>(L"/");
381 tvinsert.itemex.lParam = reinterpret_cast<LPARAM>(&m_TreeRoot);
382 tvinsert.itemex.iImage = m_nIconFolder;
383 tvinsert.itemex.iSelectedImage = m_nOpenIconFolder;
384 m_TreeRoot.m_hTree= m_RepoTree.InsertItem(&tvinsert);
386 ReadTree(&m_TreeRoot);
387 m_RepoTree.Expand(m_TreeRoot.m_hTree, TVE_EXPAND);
388 FillListCtrlForShadowTree(&m_TreeRoot);
389 m_RepoTree.SelectItem(m_TreeRoot.m_hTree);
390 EndWaitCursor();
393 int CRepositoryBrowser::ReadTreeRecursive(git_repository& repo, const git_tree* tree, CShadowFilesTree* treeroot, bool recursive)
395 size_t count = git_tree_entrycount(tree);
396 bool hasSubfolders = false;
397 treeroot->m_bLoaded = true;
399 for (size_t i = 0; i < count; ++i)
401 const git_tree_entry *entry = git_tree_entry_byindex(tree, i);
402 if (!entry)
403 continue;
405 const int mode = git_tree_entry_filemode(entry);
407 CString base = CUnicodeUtils::GetUnicode(git_tree_entry_name(entry));
409 const git_oid *oid = git_tree_entry_id(entry);
410 CShadowFilesTree * pNextTree = &treeroot->m_ShadowTree[base];
411 pNextTree->m_sName = base;
412 pNextTree->m_pParent = treeroot;
413 pNextTree->m_hash = oid;
415 if (mode == GIT_FILEMODE_COMMIT)
416 pNextTree->m_bSubmodule = true;
417 else if (mode & S_IFDIR)
419 hasSubfolders = true;
420 pNextTree->m_bFolder = true;
421 pNextTree->m_bLoaded = false;
423 TVINSERTSTRUCT tvinsert = {0};
424 tvinsert.hParent = treeroot->m_hTree;
425 tvinsert.hInsertAfter = TVI_SORT;
426 tvinsert.itemex.mask = TVIF_DI_SETITEM | TVIF_PARAM | TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_STATE | TVIF_CHILDREN;
427 tvinsert.itemex.pszText = base.GetBuffer(base.GetLength());
428 tvinsert.itemex.cChildren = 1;
429 tvinsert.itemex.lParam = reinterpret_cast<LPARAM>(pNextTree);
430 tvinsert.itemex.iImage = m_nIconFolder;
431 tvinsert.itemex.iSelectedImage = m_nOpenIconFolder;
432 pNextTree->m_hTree = m_RepoTree.InsertItem(&tvinsert);
433 base.ReleaseBuffer();
434 if (recursive)
436 CAutoTree subtree;
437 if (git_tree_lookup(subtree.GetPointer(), &repo, oid))
439 MessageBox(CGit::GetLibGit2LastErr(L"Could not lookup path."), L"TortoiseGit", MB_ICONERROR);
440 return -1;
443 ReadTreeRecursive(repo, subtree, pNextTree, recursive);
446 else
448 if (mode == GIT_FILEMODE_BLOB_EXECUTABLE)
449 pNextTree->m_bExecutable = true;
450 if (mode == GIT_FILEMODE_LINK)
451 pNextTree->m_bSymlink = true;
452 CAutoBlob blob;
453 git_blob_lookup(blob.GetPointer(), &repo, oid);
454 if (!blob)
455 continue;
457 pNextTree->m_iSize = git_blob_rawsize(blob);
461 if (!hasSubfolders)
463 TVITEM tvitem = { 0 };
464 tvitem.hItem = treeroot->m_hTree;
465 tvitem.mask = TVIF_CHILDREN;
466 tvitem.cChildren = 0;
467 m_RepoTree.SetItem(&tvitem);
470 return 0;
473 int CRepositoryBrowser::ReadTree(CShadowFilesTree* treeroot, const CString& root, bool recursive)
475 CWaitCursor wait;
476 CAutoRepository repository(g_Git.GetGitRepository());
477 if (!repository)
479 MessageBox(CGit::GetLibGit2LastErr(L"Could not open repository."), L"TortoiseGit", MB_ICONERROR);
480 return -1;
483 if (m_sRevision == L"HEAD")
485 int ret = git_repository_head_unborn(repository);
486 if (ret == 1) // is orphan
487 return ret;
488 else if (ret != 0)
490 MessageBox(g_Git.GetLibGit2LastErr(L"Could not check HEAD."), L"TortoiseGit", MB_ICONERROR);
491 return ret;
495 CGitHash hash;
496 if (CGit::GetHash(repository, hash, m_sRevision + L"^{}")) // add ^{} in order to dereference signed tags
498 MessageBox(CGit::GetLibGit2LastErr(L"Could not get hash of \"" + m_sRevision + L"^{}\"."), L"TortoiseGit", MB_ICONERROR);
499 return -1;
502 CAutoCommit commit;
503 if (git_commit_lookup(commit.GetPointer(), repository, hash))
505 MessageBox(CGit::GetLibGit2LastErr(L"Could not lookup commit."), L"TortoiseGit", MB_ICONERROR);
506 return -1;
509 CAutoTree tree;
510 if (git_commit_tree(tree.GetPointer(), commit))
512 MessageBox(CGit::GetLibGit2LastErr(L"Could not get tree of commit."), L"TortoiseGit", MB_ICONERROR);
513 return -1;
516 if (!root.IsEmpty())
518 CAutoTreeEntry treeEntry;
519 if (git_tree_entry_bypath(treeEntry.GetPointer(), tree, CUnicodeUtils::GetUTF8(root)))
521 MessageBox(CGit::GetLibGit2LastErr(L"Could not lookup path."), L"TortoiseGit", MB_ICONERROR);
522 return -1;
524 if (git_tree_entry_type(treeEntry) != GIT_OBJECT_TREE)
526 MessageBox(CGit::GetLibGit2LastErr(L"Could not lookup path."), L"TortoiseGit", MB_ICONERROR);
527 return -1;
530 CAutoObject object;
531 if (git_tree_entry_to_object(object.GetPointer(), repository, treeEntry))
533 MessageBox(CGit::GetLibGit2LastErr(L"Could not lookup path."), L"TortoiseGit", MB_ICONERROR);
534 return -1;
537 tree.ConvertFrom(std::move(object));
540 treeroot->m_hash = git_tree_id(tree);
541 ReadTreeRecursive(*repository, tree, treeroot, recursive);
543 // try to resolve hash to a branch name
544 if (m_sRevision == hash.ToString())
546 MAP_HASH_NAME map;
547 if (CGit::GetMapHashToFriendName(repository, map))
548 MessageBox(g_Git.GetLibGit2LastErr(L"Could not get all refs."), L"TortoiseGit", MB_ICONERROR);
549 if (!map[hash].empty())
550 m_sRevision = map[hash].at(0);
552 this->GetDlgItem(IDC_BUTTON_REVISION)->SetWindowText(m_sRevision);
554 return 0;
557 void CRepositoryBrowser::OnTvnSelchangedRepoTree(NMHDR *pNMHDR, LRESULT *pResult)
559 LPNMTREEVIEW pNMTreeView = reinterpret_cast<LPNMTREEVIEW>(pNMHDR);
560 *pResult = 0;
562 FillListCtrlForTreeNode(pNMTreeView->itemNew.hItem);
565 void CRepositoryBrowser::OnTvnItemExpandingRepoTree(NMHDR *pNMHDR, LRESULT *pResult)
567 LPNMTREEVIEW pNMTreeView = reinterpret_cast<LPNMTREEVIEW>(pNMHDR);
568 *pResult = 0;
570 auto pTree = GetTreeEntry(pNMTreeView->itemNew.hItem);
571 if (!pTree)
572 return;
574 if (!pTree->m_bLoaded)
575 ReadTree(pTree, pTree->GetFullName());
578 void CRepositoryBrowser::FillListCtrlForTreeNode(HTREEITEM treeNode)
580 m_RepoList.DeleteAllItems();
582 auto pTree = GetTreeEntry(treeNode);
583 if (!pTree)
584 return;
586 CString url = L'/' + pTree->GetFullName();
587 GetDlgItem(IDC_REPOBROWSER_URL)->SetWindowText(url);
589 if (!pTree->m_bLoaded)
590 ReadTree(pTree, pTree->GetFullName());
592 FillListCtrlForShadowTree(pTree);
595 void CRepositoryBrowser::FillListCtrlForShadowTree(CShadowFilesTree* pTree)
597 for (auto itShadowTree = pTree->m_ShadowTree.cbegin(); itShadowTree != pTree->m_ShadowTree.cend(); ++itShadowTree)
599 int icon = m_nIconFolder;
600 if (!(*itShadowTree).second.m_bFolder && !(*itShadowTree).second.m_bSubmodule)
602 icon = SYS_IMAGE_LIST().GetPathIconIndex((*itShadowTree).second.m_sName);
605 int indexItem = m_RepoList.InsertItem(m_RepoList.GetItemCount(), (*itShadowTree).second.m_sName, icon);
607 if ((*itShadowTree).second.m_bSubmodule)
609 m_RepoList.SetItemState(indexItem, INDEXTOOVERLAYMASK(OVERLAY_EXTERNAL), LVIS_OVERLAYMASK);
611 if ((*itShadowTree).second.m_bExecutable)
612 m_RepoList.SetItemState(indexItem, INDEXTOOVERLAYMASK(OVERLAY_EXECUTABLE), LVIS_OVERLAYMASK);
613 if ((*itShadowTree).second.m_bSymlink)
614 m_RepoList.SetItemState(indexItem, INDEXTOOVERLAYMASK(OVERLAY_SYMLINK), LVIS_OVERLAYMASK);
615 m_RepoList.SetItemData(indexItem, reinterpret_cast<DWORD_PTR>(&(*itShadowTree).second));
616 if (!(*itShadowTree).second.m_bFolder && !(*itShadowTree).second.m_bSubmodule)
618 CString temp;
620 temp = CPathUtils::GetFileExtFromPath((*itShadowTree).second.m_sName);
621 m_RepoList.SetItemText(indexItem, eCol_Extension, temp);
623 StrFormatByteSize64((*itShadowTree).second.m_iSize, CStrBuf(temp, 20), 20);
624 m_RepoList.SetItemText(indexItem, eCol_FileSize, temp);
628 CRepoListCompareFunc compareFunc(&m_RepoList, m_currSortCol, m_currSortDesc);
629 m_RepoList.SortItemsEx(&CRepoListCompareFunc::StaticCompare, reinterpret_cast<DWORD_PTR>(&compareFunc));
631 SetSortArrowA(&m_RepoList, m_currSortCol, !m_currSortDesc);
633 UpdateInfoLabel();
636 void CRepositoryBrowser::UpdateInfoLabel()
638 CString temp;
639 POSITION pos = m_RepoList.GetFirstSelectedItemPosition();
640 if (pos)
642 if (m_RepoList.GetSelectedCount() > 1)
644 temp.FormatMessage(IDS_REPOBROWSE_INFOMULTI, m_RepoList.GetSelectedCount());
646 else
648 int index = m_RepoList.GetNextSelectedItem(pos);
649 auto item = GetListEntry(index);
650 if (item->m_bSubmodule)
651 temp.FormatMessage(IDS_REPOBROWSE_INFOEXT, static_cast<LPCWSTR>(m_RepoList.GetItemText(index, eCol_Name)), static_cast<LPCWSTR>(item->m_hash.ToString()));
652 else if (item->m_bFolder)
653 temp = m_RepoList.GetItemText(index, eCol_Name);
654 else
655 temp.FormatMessage(IDS_REPOBROWSE_INFOFILE, static_cast<LPCWSTR>(m_RepoList.GetItemText(index, eCol_Name)), static_cast<LPCWSTR>(m_RepoList.GetItemText(index, eCol_FileSize)));
658 else
660 HTREEITEM hTreeItem = m_RepoTree.GetSelectedItem();
661 if (hTreeItem != nullptr)
663 auto pTree = GetTreeEntry(hTreeItem);
664 if (pTree != nullptr)
666 size_t files = 0, submodules = 0;
667 for (auto itShadowTree = pTree->m_ShadowTree.cbegin(); itShadowTree != pTree->m_ShadowTree.cend(); ++itShadowTree)
669 if (!(*itShadowTree).second.m_bFolder && !(*itShadowTree).second.m_bSubmodule)
670 ++files;
671 if ((*itShadowTree).second.m_bSubmodule)
672 ++submodules;
674 temp.FormatMessage(IDS_REPOBROWSE_INFO, static_cast<LPCWSTR>(pTree->m_sName), files, submodules, pTree->m_ShadowTree.size() - files - submodules, pTree->m_ShadowTree.size());
678 SetDlgItemText(IDC_INFOLABEL, temp);
681 void CRepositoryBrowser::OnLvnItemchangedRepolist(NMHDR * /* pNMHDR */, LRESULT *pResult)
683 *pResult = 0;
684 UpdateInfoLabel();
687 void CRepositoryBrowser::OnContextMenu(CWnd* pWndFrom, CPoint point)
689 if (pWndFrom == &m_RepoList)
691 CRect headerPosition;
692 m_RepoList.GetHeaderCtrl()->GetWindowRect(headerPosition);
693 if (!headerPosition.PtInRect(point))
694 OnContextMenu_RepoList(point);
696 else if (pWndFrom == &m_RepoTree)
697 OnContextMenu_RepoTree(point);
700 void CRepositoryBrowser::OnContextMenu_RepoTree(CPoint point)
702 CPoint clientPoint = point;
703 m_RepoTree.ScreenToClient(&clientPoint);
705 HTREEITEM hTreeItem = m_RepoTree.HitTest(clientPoint);
706 if (!hTreeItem)
707 return;
709 TShadowFilesTreeList selectedLeafs;
710 selectedLeafs.push_back(GetTreeEntry(hTreeItem));
712 ShowContextMenu(point, selectedLeafs, ONLY_FOLDERS);
715 void CRepositoryBrowser::OnContextMenu_RepoList(CPoint point)
717 TShadowFilesTreeList selectedLeafs;
718 selectedLeafs.reserve(m_RepoList.GetSelectedCount());
720 bool folderSelected = false;
721 bool filesSelected = false;
722 bool submodulesSelected = false;
724 POSITION pos = m_RepoList.GetFirstSelectedItemPosition();
725 while (pos)
727 auto item = GetListEntry(m_RepoList.GetNextSelectedItem(pos));
728 if (item->m_bSubmodule)
729 submodulesSelected = true;
730 if (item->m_bFolder)
731 folderSelected = true;
732 else
733 filesSelected = true;
734 selectedLeafs.push_back(item);
737 eSelectionType selType = ONLY_FILES;
738 if (folderSelected && filesSelected)
739 selType = MIXED_FOLDERS_FILES;
740 else if (folderSelected)
741 selType = ONLY_FOLDERS;
742 else if (submodulesSelected)
743 selType = ONLY_FILESSUBMODULES;
744 ShowContextMenu(point, selectedLeafs, selType);
747 void CRepositoryBrowser::ShowContextMenu(CPoint point, TShadowFilesTreeList &selectedLeafs, eSelectionType selType)
749 CIconMenu popupMenu;
750 popupMenu.CreatePopupMenu();
752 bool bAddSeparator = false;
754 if (selectedLeafs.size() == 1)
756 popupMenu.AppendMenuIcon(eCmd_Open, IDS_REPOBROWSE_OPEN, IDI_OPEN);
757 popupMenu.SetDefaultItem(eCmd_Open, FALSE);
758 if (selType == ONLY_FILES || selType == ONLY_FILESSUBMODULES)
760 popupMenu.AppendMenuIcon(eCmd_OpenWith, IDS_LOG_POPUP_OPENWITH, IDI_OPEN);
761 popupMenu.AppendMenuIcon(eCmd_OpenWithAlternativeEditor, IDS_LOG_POPUP_VIEWREV, IDI_NOTEPAD);
764 popupMenu.AppendMenu(MF_SEPARATOR);
766 if (m_bHasWC && (selType == ONLY_FILES || selType == ONLY_FILESSUBMODULES))
768 popupMenu.AppendMenuIcon(eCmd_CompareWC, IDS_LOG_POPUP_COMPARE, IDI_DIFF);
769 bAddSeparator = true;
772 if (bAddSeparator)
773 popupMenu.AppendMenu(MF_SEPARATOR);
774 bAddSeparator = false;
776 CString temp;
777 temp.LoadString(IDS_MENULOG);
778 popupMenu.AppendMenuIcon(eCmd_ViewLog, temp, IDI_LOG);
779 if (selectedLeafs[0]->m_bSubmodule)
781 temp.LoadString(IDS_MENULOGSUBMODULE);
782 popupMenu.AppendMenuIcon(eCmd_ViewLogSubmodule, temp, IDI_LOG);
785 if (selType == ONLY_FILES)
787 if (m_bHasWC)
788 popupMenu.AppendMenuIcon(eCmd_Blame, IDS_LOG_POPUP_BLAME, IDI_BLAME);
790 popupMenu.AppendMenu(MF_SEPARATOR);
791 temp.LoadString(IDS_LOG_POPUP_SAVE);
792 popupMenu.AppendMenuIcon(eCmd_SaveAs, temp, IDI_SAVEAS);
795 bAddSeparator = true;
798 if (!selectedLeafs.empty() && selType == ONLY_FILES && m_bHasWC)
800 popupMenu.AppendMenuIcon(eCmd_Revert, IDS_LOG_POPUP_REVERTTOREV, IDI_REVERT);
801 bAddSeparator = true;
804 if (bAddSeparator)
805 popupMenu.AppendMenu(MF_SEPARATOR);
807 if (selectedLeafs.size() == 1 && selType == ONLY_FILES)
809 popupMenu.AppendMenuIcon(eCmd_PrepareDiff, IDS_PREPAREDIFF, IDI_DIFF);
810 UpdateDiffWithFileFromReg();
811 if (!m_sMarkForDiffFilename.IsEmpty())
813 CString diffWith;
814 if (selectedLeafs.at(0)->GetFullName() == m_sMarkForDiffFilename)
815 diffWith = m_sMarkForDiffVersion.ToString();
816 else
818 PathCompactPathEx(CStrBuf(diffWith, 2 * GIT_HASH_SIZE), m_sMarkForDiffFilename, 2 * GIT_HASH_SIZE, 0);
819 if (!m_sMarkForDiffVersion.IsEmpty() || PathIsRelative(m_sMarkForDiffFilename))
820 diffWith += L':' + m_sMarkForDiffVersion.ToString(g_Git.GetShortHASHLength());
822 CString menuEntry;
823 menuEntry.Format(IDS_MENUDIFFNOW, static_cast<LPCWSTR>(diffWith));
824 popupMenu.AppendMenuIcon(eCmd_PrepareDiff_Compare, menuEntry, IDI_DIFF);
826 popupMenu.AppendMenu(MF_SEPARATOR);
829 if (!selectedLeafs.empty())
831 popupMenu.AppendMenuIcon(eCmd_CopyPath, IDS_STATUSLIST_CONTEXT_COPY, IDI_COPYCLIP);
832 popupMenu.AppendMenuIcon(eCmd_CopyHash, IDS_COPY_COMMIT_HASH, IDI_COPYCLIP);
835 eCmd cmd = static_cast<eCmd>(popupMenu.TrackPopupMenuEx(TPM_LEFTALIGN | TPM_RETURNCMD, point.x, point.y, this, nullptr));
836 switch(cmd)
838 case eCmd_ViewLog:
839 case eCmd_ViewLogSubmodule:
841 CString sCmd;
842 sCmd.Format(L"/command:log /path:\"%s\"", static_cast<LPCWSTR>(g_Git.CombinePath(selectedLeafs.at(0)->GetFullName())));
843 if (cmd == eCmd_ViewLog && selectedLeafs.at(0)->m_bSubmodule)
844 sCmd += L" /submodule";
845 if (cmd == eCmd_ViewLog)
846 sCmd += L" /endrev:" + m_sRevision;
847 CAppUtils::RunTortoiseGitProc(sCmd);
849 break;
850 case eCmd_Blame:
852 CAppUtils::LaunchTortoiseBlame(g_Git.CombinePath(selectedLeafs.at(0)->GetFullName()), m_sRevision);
854 break;
855 case eCmd_Open:
856 if (!selectedLeafs.at(0)->m_bSubmodule && selectedLeafs.at(0)->m_bFolder)
858 FillListCtrlForTreeNode(selectedLeafs.at(0)->m_hTree);
859 m_RepoTree.SelectItem(selectedLeafs.at(0)->m_hTree);
860 return;
862 OpenFile(selectedLeafs.at(0)->GetFullName(), OPEN, selectedLeafs.at(0)->m_bSubmodule, selectedLeafs.at(0)->m_hash);
863 break;
864 case eCmd_OpenWith:
865 OpenFile(selectedLeafs.at(0)->GetFullName(), OPEN_WITH, selectedLeafs.at(0)->m_bSubmodule, selectedLeafs.at(0)->m_hash);
866 break;
867 case eCmd_OpenWithAlternativeEditor:
868 OpenFile(selectedLeafs.at(0)->GetFullName(), ALTERNATIVEEDITOR, selectedLeafs.at(0)->m_bSubmodule, selectedLeafs.at(0)->m_hash);
869 break;
870 case eCmd_CompareWC:
872 CTGitPath file(selectedLeafs.at(0)->GetFullName());
873 CGitDiff::Diff(GetSafeHwnd(), &file, &file, GIT_REV_ZERO, m_sRevision);
875 break;
876 case eCmd_Revert:
878 int count = 0;
879 for (auto itShadowTree = selectedLeafs.cbegin(); itShadowTree != selectedLeafs.cend(); ++itShadowTree)
881 if (RevertItemToVersion((*itShadowTree)->GetFullName()))
882 ++count;
883 else
884 break;
886 CString msg;
887 msg.FormatMessage(IDS_STATUSLIST_FILESREVERTED, count, static_cast<LPCWSTR>(m_sRevision));
888 MessageBox(msg, L"TortoiseGit", MB_OK);
890 break;
891 case eCmd_SaveAs:
892 FileSaveAs(selectedLeafs.at(0)->GetFullName());
893 break;
894 case eCmd_CopyPath:
896 CString sClipboard;
897 for (auto itShadowTree = selectedLeafs.cbegin(); itShadowTree != selectedLeafs.cend(); ++itShadowTree)
899 sClipboard += (*itShadowTree)->m_sName + L"\r\n";
901 CStringUtils::WriteAsciiStringToClipboard(sClipboard);
903 break;
904 case eCmd_CopyHash:
906 CopyHashToClipboard(selectedLeafs);
908 break;
909 case eCmd_PrepareDiff:
910 m_sMarkForDiffFilename = selectedLeafs.at(0)->GetFullName();
911 if (g_Git.GetHash(m_sMarkForDiffVersion, m_sRevision + L"^{}")) // add ^{} in order to dereference signed tags
913 m_sMarkForDiffFilename.Empty();
914 MessageBox(g_Git.GetGitLastErr(L"Could not get SHA-1 for \"" + m_sRevision + L"^{}\"."), L"TortoiseGit", MB_ICONERROR);
916 break;
917 case eCmd_PrepareDiff_Compare:
919 if (auto reg = CRegString(L"Software\\TortoiseGit\\DiffLater", L""); m_sMarkForDiffFilename == reg)
920 reg.removeValue();
921 CTGitPath savedFile(m_sMarkForDiffFilename);
922 CTGitPath selectedFile(selectedLeafs.at(0)->GetFullName());
923 CGitHash currentHash;
924 if (g_Git.GetHash(currentHash, m_sRevision + L"^{}")) // add ^{} in order to dereference signed tags
926 MessageBox(g_Git.GetGitLastErr(L"Could not get SHA-1 for \"" + m_sRevision + L"^{}\"."), L"TortoiseGit", MB_ICONERROR);
927 return;
929 CGitDiff::Diff(GetSafeHwnd(), &selectedFile, &savedFile, currentHash.ToString(), m_sMarkForDiffVersion.ToString());
931 break;
935 BOOL CRepositoryBrowser::PreTranslateMessage(MSG* pMsg)
937 if (pMsg->message == WM_KEYDOWN)
939 switch (pMsg->wParam)
941 case VK_F5:
943 Refresh();
945 break;
946 case L'A':
947 if (pMsg->hwnd == m_RepoList.m_hWnd && (GetAsyncKeyState(VK_CONTROL) & 0x8000))
949 // select all entries
950 for (int i = 0; i < m_RepoList.GetItemCount(); ++i)
951 m_RepoList.SetItemState(i, LVIS_SELECTED, LVIS_SELECTED);
952 return TRUE;
954 break;
958 return CResizableStandAloneDialog::PreTranslateMessage(pMsg);
961 void CRepositoryBrowser::OnLvnColumnclickRepoList(NMHDR *pNMHDR, LRESULT *pResult)
963 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
964 if (pResult)
965 *pResult = 0;
967 if (m_currSortCol == pNMLV->iSubItem)
968 m_currSortDesc = !m_currSortDesc;
969 else
971 m_currSortCol = pNMLV->iSubItem;
972 m_currSortDesc = false;
975 CRepoListCompareFunc compareFunc(&m_RepoList, m_currSortCol, m_currSortDesc);
976 m_RepoList.SortItemsEx(&CRepoListCompareFunc::StaticCompare, reinterpret_cast<DWORD_PTR>(&compareFunc));
978 SetSortArrowA(&m_RepoList, m_currSortCol, !m_currSortDesc);
981 void CRepositoryBrowser::OnBnClickedButtonRevision()
983 // use the git log to allow selection of a version
984 CLogDlg dlg;
985 if (dlg.IsThreadRunning())
987 CMessageBox::Show(GetSafeHwnd(), IDS_PROC_LOG_ONLYONCE, IDS_APPNAME, MB_ICONEXCLAMATION);
988 return;
990 dlg.SetParams(CTGitPath(), CTGitPath(), m_sRevision, m_sRevision, 0);
991 // tell the dialog to use mode for selecting revisions
992 dlg.SetSelect(true);
993 dlg.ShowWorkingTreeChanges(false);
994 // only one revision must be selected however
995 dlg.SingleSelection(true);
996 if (dlg.DoModal() == IDOK && !dlg.GetSelectedHash().empty())
998 m_sRevision = dlg.GetSelectedHash().at(0).ToString();
999 Refresh();
1001 BringWindowToTop(); /* cf. issue #3493 */
1004 void CRepositoryBrowser::SaveDividerPosition()
1006 RECT rc;
1007 GetDlgItem(IDC_REPOTREE)->GetClientRect(&rc);
1008 CRegDWORD xPos(L"Software\\TortoiseGit\\TortoiseProc\\ResizableState\\RepobrowserDivider");
1009 xPos = CDPIAware::Instance().UnscaleX(rc.right - rc.left);
1012 void CRepositoryBrowser::HandleDividerMove(CPoint point, bool bDraw)
1014 RECT rect, tree, list, treelist, treelistclient;
1016 // create an union of the tree and list control rectangle
1017 GetDlgItem(IDC_REPOLIST)->GetWindowRect(&list);
1018 GetDlgItem(IDC_REPOTREE)->GetWindowRect(&tree);
1019 UnionRect(&treelist, &tree, &list);
1020 treelistclient = treelist;
1021 ScreenToClient(&treelistclient);
1023 ClientToScreen(&point);
1024 GetClientRect(&rect);
1025 ClientToScreen(&rect);
1027 auto minWidth = CDPIAware::Instance().ScaleX(REPOBROWSER_CTRL_MIN_WIDTH);
1028 CPoint point2 = point;
1029 if (point2.x < treelist.left + minWidth)
1030 point2.x = treelist.left + minWidth;
1031 if (point2.x > treelist.right - minWidth)
1032 point2.x = treelist.right - minWidth;
1034 point.x -= rect.left;
1035 point.y -= treelist.top;
1037 OffsetRect(&treelist, -treelist.left, -treelist.top);
1039 if (point.x < treelist.left + minWidth)
1040 point.x = treelist.left + minWidth;
1041 if (point.x > treelist.right - minWidth)
1042 point.x = treelist.right - minWidth;
1044 auto divWidth = CDPIAware::Instance().ScaleX(2);
1046 if (bDraw)
1048 CDC * pDC = GetDC();
1049 DrawXorBar(pDC, oldx - divWidth, treelistclient.top, 2 * divWidth, treelistclient.bottom - treelistclient.top - CDPIAware::Instance().ScaleY(2));
1050 ReleaseDC(pDC);
1053 oldx = point.x;
1054 oldy = point.y;
1056 //position the child controls
1057 GetDlgItem(IDC_REPOTREE)->GetWindowRect(&treelist);
1058 treelist.right = point2.x - divWidth;
1059 ScreenToClient(&treelist);
1060 RemoveAnchor(IDC_REPOTREE);
1061 GetDlgItem(IDC_REPOTREE)->MoveWindow(&treelist);
1062 GetDlgItem(IDC_REPOLIST)->GetWindowRect(&treelist);
1063 treelist.left = point2.x + divWidth;
1064 ScreenToClient(&treelist);
1065 RemoveAnchor(IDC_REPOLIST);
1066 GetDlgItem(IDC_REPOLIST)->MoveWindow(&treelist);
1068 AddAnchor(IDC_REPOTREE, TOP_LEFT, BOTTOM_LEFT);
1069 AddAnchor(IDC_REPOLIST, TOP_LEFT, BOTTOM_RIGHT);
1072 void CRepositoryBrowser::OnMouseMove(UINT nFlags, CPoint point)
1074 if (bDragMode == FALSE)
1075 return;
1077 RECT rect, tree, list, treelist, treelistclient;
1078 // create an union of the tree and list control rectangle
1079 GetDlgItem(IDC_REPOLIST)->GetWindowRect(&list);
1080 GetDlgItem(IDC_REPOTREE)->GetWindowRect(&tree);
1081 UnionRect(&treelist, &tree, &list);
1082 treelistclient = treelist;
1083 ScreenToClient(&treelistclient);
1085 //convert the mouse coordinates relative to the top-left of
1086 //the window
1087 ClientToScreen(&point);
1088 GetClientRect(&rect);
1089 ClientToScreen(&rect);
1090 point.x -= rect.left;
1091 point.y -= treelist.top;
1093 //same for the window coordinates - make them relative to 0,0
1094 OffsetRect(&treelist, -treelist.left, -treelist.top);
1096 auto minWidth = CDPIAware::Instance().ScaleX(REPOBROWSER_CTRL_MIN_WIDTH);
1097 if (point.x < treelist.left + minWidth)
1098 point.x = treelist.left + minWidth;
1099 if (point.x > treelist.right - minWidth)
1100 point.x = treelist.right - minWidth;
1102 if ((nFlags & MK_LBUTTON) && (point.x != oldx))
1104 CDC * pDC = GetDC();
1106 if (pDC)
1108 auto divWidth = CDPIAware::Instance().ScaleX(2);
1109 auto divHeight = CDPIAware::Instance().ScaleY(2);
1110 DrawXorBar(pDC, oldx - divWidth, treelistclient.top, 2 * divWidth, treelistclient.bottom - treelistclient.top - divHeight);
1111 DrawXorBar(pDC, point.x - divWidth, treelistclient.top, 2 * divWidth, treelistclient.bottom - treelistclient.top - divHeight);
1113 ReleaseDC(pDC);
1116 oldx = point.x;
1117 oldy = point.y;
1120 CStandAloneDialogTmpl<CResizableDialog>::OnMouseMove(nFlags, point);
1123 void CRepositoryBrowser::OnLButtonDown(UINT nFlags, CPoint point)
1125 RECT rect, tree, list, treelist, treelistclient;
1127 // create an union of the tree and list control rectangle
1128 GetDlgItem(IDC_REPOLIST)->GetWindowRect(&list);
1129 GetDlgItem(IDC_REPOTREE)->GetWindowRect(&tree);
1130 UnionRect(&treelist, &tree, &list);
1131 treelistclient = treelist;
1132 ScreenToClient(&treelistclient);
1134 //convert the mouse coordinates relative to the top-left of
1135 //the window
1136 ClientToScreen(&point);
1137 GetClientRect(&rect);
1138 ClientToScreen(&rect);
1139 point.x -= rect.left;
1140 point.y -= treelist.top;
1142 //same for the window coordinates - make them relative to 0,0
1143 OffsetRect(&treelist, -treelist.left, -treelist.top);
1145 auto minWidth = CDPIAware::Instance().ScaleX(REPOBROWSER_CTRL_MIN_WIDTH);
1147 if (point.x < treelist.left + minWidth)
1148 return CStandAloneDialogTmpl < CResizableDialog>::OnLButtonDown(nFlags, point);
1149 if (point.x > treelist.right - CDPIAware::Instance().ScaleX(3))
1150 return CStandAloneDialogTmpl < CResizableDialog>::OnLButtonDown(nFlags, point);
1151 if (point.x > treelist.right - minWidth)
1152 point.x = treelist.right - minWidth;
1154 auto divHeight = CDPIAware::Instance().ScaleY(3);
1155 if ((point.y < treelist.top + divHeight) || (point.y > treelist.bottom - divHeight))
1156 return CStandAloneDialogTmpl<CResizableDialog>::OnLButtonDown(nFlags, point);
1158 bDragMode = true;
1160 SetCapture();
1162 auto divWidth = CDPIAware::Instance().ScaleX(2);
1163 CDC * pDC = GetDC();
1164 DrawXorBar(pDC, point.x - divWidth, treelistclient.top, 2 * divWidth, treelistclient.bottom - treelistclient.top - CDPIAware::Instance().ScaleY(2));
1165 ReleaseDC(pDC);
1167 oldx = point.x;
1168 oldy = point.y;
1170 CStandAloneDialogTmpl<CResizableDialog>::OnLButtonDown(nFlags, point);
1173 void CRepositoryBrowser::OnLButtonUp(UINT nFlags, CPoint point)
1175 if (bDragMode == FALSE)
1176 return;
1178 HandleDividerMove(point, true);
1180 bDragMode = false;
1181 ReleaseCapture();
1183 CStandAloneDialogTmpl<CResizableDialog>::OnLButtonUp(nFlags, point);
1186 void CRepositoryBrowser::OnCaptureChanged(CWnd *pWnd)
1188 bDragMode = false;
1190 __super::OnCaptureChanged(pWnd);
1193 void CRepositoryBrowser::DrawXorBar(CDC * pDC, int x1, int y1, int width, int height)
1195 static WORD _dotPatternBmp[8] =
1197 0x0055, 0x00aa, 0x0055, 0x00aa,
1198 0x0055, 0x00aa, 0x0055, 0x00aa
1201 HBITMAP hbm;
1202 HBRUSH hbr, hbrushOld;
1204 hbm = CreateBitmap(8, 8, 1, 1, _dotPatternBmp);
1205 hbr = CreatePatternBrush(hbm);
1207 pDC->SetBrushOrg(x1, y1);
1208 hbrushOld = static_cast<HBRUSH>(pDC->SelectObject(hbr));
1210 PatBlt(pDC->GetSafeHdc(), x1, y1, width, height, PATINVERT);
1212 pDC->SelectObject(hbrushOld);
1214 DeleteObject(hbr);
1215 DeleteObject(hbm);
1218 BOOL CRepositoryBrowser::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
1220 if (pWnd == this)
1222 RECT rect;
1223 POINT pt;
1224 GetClientRect(&rect);
1225 GetCursorPos(&pt);
1226 ScreenToClient(&pt);
1227 if (PtInRect(&rect, pt))
1229 ClientToScreen(&pt);
1230 // are we right of the tree control?
1231 GetDlgItem(IDC_REPOTREE)->GetWindowRect(&rect);
1232 auto divHeight = CDPIAware::Instance().ScaleY(3);
1233 if ((pt.x > rect.right) && (pt.y >= rect.top + divHeight) && (pt.y <= rect.bottom - divHeight))
1235 // but left of the list control?
1236 GetDlgItem(IDC_REPOLIST)->GetWindowRect(&rect);
1237 if (pt.x < rect.left)
1239 HCURSOR hCur = LoadCursor(nullptr, IDC_SIZEWE);
1240 SetCursor(hCur);
1241 return TRUE;
1246 return CStandAloneDialogTmpl<CResizableDialog>::OnSetCursor(pWnd, nHitTest, message);
1249 void CRepositoryBrowser::FileSaveAs(const CString path)
1251 CTGitPath gitPath(path);
1253 CGitHash hash;
1254 if (g_Git.GetHash(hash, m_sRevision + L"^{}")) // add ^{} in order to dereference signed tags
1256 MessageBox(g_Git.GetGitLastErr(L"Could not get hash of \"" + m_sRevision + L"^{}\"."), L"TortoiseGit", MB_ICONERROR);
1257 return;
1260 CString filename;
1261 filename.Format(L"%s\\%s-%s%s", static_cast<LPCWSTR>(g_Git.CombinePath(gitPath.GetContainingDirectory())), static_cast<LPCWSTR>(gitPath.GetBaseFilename()), static_cast<LPCWSTR>(hash.ToString(g_Git.GetShortHASHLength())), static_cast<LPCWSTR>(gitPath.GetFileExtension()));
1262 if (!CAppUtils::FileOpenSave(filename, nullptr, 0, 0, false, GetSafeHwnd()))
1263 return;
1265 if (g_Git.GetOneFile(m_sRevision, gitPath, filename))
1267 CString out;
1268 out.FormatMessage(IDS_STATUSLIST_CHECKOUTFILEFAILED, static_cast<LPCWSTR>(gitPath.GetGitPathString()), static_cast<LPCWSTR>(m_sRevision), static_cast<LPCWSTR>(filename));
1269 MessageBox(g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", MB_ICONERROR);
1273 void CRepositoryBrowser::OpenFile(const CString path, eOpenType mode, bool isSubmodule, const CGitHash& itemHash)
1275 CTGitPath gitPath(path);
1277 CGitHash hash;
1278 if (g_Git.GetHash(hash, m_sRevision + L"^{}")) // add ^{} in order to dereference signed tags
1280 MessageBox(g_Git.GetGitLastErr(L"Could not get hash of \"" + m_sRevision + L"^{}\"."), L"TortoiseGit", MB_ICONERROR);
1281 return;
1284 CString file = CTempFiles::Instance().GetTempFilePath(false, gitPath, hash).GetWinPathString();
1285 if (isSubmodule)
1287 if (mode == OPEN && !GitAdminDir::IsBareRepo(g_Git.m_CurrentDir))
1289 CTGitPath subPath = CTGitPath(g_Git.m_CurrentDir);
1290 subPath.AppendPathString(gitPath.GetWinPathString());
1291 CAutoRepository repo(subPath.GetGitPathString());
1292 CAutoCommit commit;
1293 if (!repo || git_commit_lookup(commit.GetPointer(), repo, itemHash))
1295 CString out;
1296 out.FormatMessage(IDS_REPOBROWSEASKSUBMODULEUPDATE, static_cast<LPCWSTR>(itemHash.ToString()), static_cast<LPCWSTR>(gitPath.GetGitPathString()));
1297 if (MessageBox(out, L"TortoiseGit", MB_YESNO | MB_ICONQUESTION) != IDYES)
1298 return;
1300 CString sCmd;
1301 sCmd.Format(L"/command:subupdate /bkpath:\"%s\" /selectedpath:\"%s\"", static_cast<LPCWSTR>(g_Git.m_CurrentDir), static_cast<LPCWSTR>(gitPath.GetGitPathString()));
1302 CAppUtils::RunTortoiseGitProc(sCmd);
1303 return;
1306 CString cmd;
1307 cmd.Format(L"/command:repobrowser /path:\"%s\" /rev:%s", static_cast<LPCWSTR>(g_Git.CombinePath(path)), static_cast<LPCWSTR>(itemHash.ToString()));
1308 CAppUtils::RunTortoiseGitProc(cmd);
1309 return;
1312 file += L".txt";
1313 CFile submoduleCommit(file, CFile::modeCreate | CFile::modeWrite);
1314 CStringA commitInfo = "Subproject commit " + CStringA(itemHash.ToString());
1315 submoduleCommit.Write(commitInfo, commitInfo.GetLength());
1317 else if (g_Git.GetOneFile(m_sRevision, gitPath, file))
1319 CString out;
1320 out.FormatMessage(IDS_STATUSLIST_CHECKOUTFILEFAILED, static_cast<LPCWSTR>(gitPath.GetGitPathString()), static_cast<LPCWSTR>(m_sRevision), static_cast<LPCWSTR>(file));
1321 MessageBox(g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", MB_ICONERROR);
1322 return;
1325 if (mode == ALTERNATIVEEDITOR)
1327 CAppUtils::LaunchAlternativeEditor(file);
1328 return;
1330 else if (mode == OPEN)
1332 CAppUtils::ShellOpen(file);
1333 return;
1336 CAppUtils::ShowOpenWithDialog(file);
1338 bool CRepositoryBrowser::RevertItemToVersion(const CString &path)
1340 CString cmd, out;
1341 cmd.Format(L"git.exe checkout %s -- \"%s\"", static_cast<LPCWSTR>(m_sRevision), static_cast<LPCWSTR>(path));
1342 if (g_Git.Run(cmd, &out, CP_UTF8))
1344 if (MessageBox(out, L"TortoiseGit", MB_ICONEXCLAMATION | MB_OKCANCEL) == IDCANCEL)
1345 return false;
1348 return true;
1351 void CRepositoryBrowser::CopyHashToClipboard(TShadowFilesTreeList &selectedLeafs)
1353 if (!selectedLeafs.empty())
1355 CString sClipdata;
1356 bool first = true;
1357 for (size_t i = 0; i < selectedLeafs.size(); ++i)
1359 if (!first)
1360 sClipdata += L"\r\n";
1361 sClipdata += selectedLeafs[i]->m_hash.ToString();
1362 first = false;
1364 CStringUtils::WriteAsciiStringToClipboard(sClipdata, GetSafeHwnd());
1368 void CRepositoryBrowser::OnLvnBegindragRepolist(NMHDR* pNMHDR, LRESULT* pResult)
1370 *pResult = 0;
1372 // get selected paths
1373 POSITION pos = m_RepoList.GetFirstSelectedItemPosition();
1374 if (!pos)
1375 return;
1377 HTREEITEM hTreeItem = m_RepoTree.GetSelectedItem();
1378 if (!hTreeItem)
1380 ASSERT(FALSE);
1381 return;
1383 auto pTree = GetTreeEntry(hTreeItem);
1384 if (!pTree)
1385 return;
1387 CTGitPathList toExport;
1388 int index = -1;
1389 while ((index = m_RepoList.GetNextSelectedItem(pos)) >= 0)
1391 auto item = GetListEntry(index);
1392 if (item->m_bFolder)
1394 RecursivelyAdd(toExport, item);
1395 continue;
1398 CTGitPath path;
1399 path.SetFromGit(item->GetFullName(), item->m_bSubmodule);
1400 toExport.AddPath(path);
1403 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
1404 BeginDrag(m_RepoList, toExport, pTree->GetFullName(), pNMLV->ptAction);
1407 void CRepositoryBrowser::RecursivelyAdd(CTGitPathList& toExport, CShadowFilesTree* pTree)
1409 if (!pTree->m_bLoaded)
1410 ReadTree(pTree, pTree->GetFullName(), true);
1412 for (auto itShadowTree = pTree->m_ShadowTree.begin(); itShadowTree != pTree->m_ShadowTree.end(); ++itShadowTree)
1414 if ((*itShadowTree).second.m_bFolder)
1416 RecursivelyAdd(toExport, &(*itShadowTree).second);
1417 continue;
1419 CTGitPath path;
1420 path.SetFromGit((*itShadowTree).second.GetFullName(), (*itShadowTree).second.m_bSubmodule);
1421 toExport.AddPath(path);
1425 void CRepositoryBrowser::OnTvnBegindragRepotree(NMHDR* pNMHDR, LRESULT* pResult)
1427 *pResult = 0;
1429 LPNMTREEVIEW pNMTreeView = reinterpret_cast<LPNMTREEVIEW>(pNMHDR);
1431 auto pTree = GetTreeEntry(pNMTreeView->itemNew.hItem);
1432 if (!pTree)
1433 return;
1435 CTGitPathList toExport;
1436 RecursivelyAdd(toExport, pTree);
1438 BeginDrag(m_RepoTree, toExport, pTree->m_pParent ? pTree->m_pParent->GetFullName() : CString(), pNMTreeView->ptDrag);
1441 void CRepositoryBrowser::BeginDrag(const CWnd& window, CTGitPathList& files, const CString& root, POINT& point)
1443 CGitHash hash;
1444 if (g_Git.GetHash(hash, m_sRevision + L"^{}")) // add ^{} in order to dereference signed tags
1446 MessageBox(g_Git.GetGitLastErr(L"Could not get hash of \"" + m_sRevision + L"^{}\"."), L"TortoiseGit", MB_ICONERROR);
1447 return;
1450 // build copy source / content
1451 auto pdsrc = std::make_unique<CIDropSource>();
1452 if (!pdsrc)
1453 return;
1455 pdsrc->AddRef();
1457 GitDataObject* pdobj = new GitDataObject(files, hash, root.GetLength());
1458 if (!pdobj)
1459 return;
1460 pdobj->AddRef();
1461 pdobj->SetAsyncMode(TRUE);
1462 CDragSourceHelper dragsrchelper;
1463 dragsrchelper.InitializeFromWindow(window.GetSafeHwnd(), point, pdobj);
1464 pdsrc->m_pIDataObj = pdobj;
1465 pdsrc->m_pIDataObj->AddRef();
1467 // Initiate the Drag & Drop
1468 DWORD dwEffect;
1469 ::DoDragDrop(pdobj, pdsrc.get(), DROPEFFECT_MOVE | DROPEFFECT_COPY, &dwEffect);
1470 pdsrc->Release();
1471 pdsrc.release();
1472 pdobj->Release();
1476 CShadowFilesTree* CRepositoryBrowser::GetListEntry(int index)
1478 auto entry = reinterpret_cast<CShadowFilesTree*>(m_RepoList.GetItemData(index));
1479 ASSERT(entry);
1480 return entry;
1483 CShadowFilesTree* CRepositoryBrowser::GetTreeEntry(HTREEITEM treeItem)
1485 auto entry = reinterpret_cast<CShadowFilesTree*>(m_RepoTree.GetItemData(treeItem));
1486 ASSERT(entry);
1487 return entry;