Fix typos
[TortoiseGit.git] / src / TortoiseProc / RepositoryBrowser.cpp
blobe2147dc40631de08cb184de726ca528570be3dff
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2009-2024 - 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 [[fallthrough]];
96 case CRepositoryBrowser::eCol_Extension:
97 result = m_pList->GetItemText(static_cast<int>(lParam1), 1).CompareNoCase(m_pList->GetItemText(static_cast<int>(lParam2), 1));
98 if (result == 0) // if extensions are the same, use the filename to sort
99 result = SortStrCmp(pRight->m_sName, pRight->m_sName);
100 if (result != 0)
101 break;
102 [[fallthrough]];
103 case CRepositoryBrowser::eCol_FileSize:
104 if (pLeft->m_iSize > pRight->m_iSize)
105 result = 1;
106 else if (pLeft->m_iSize < pRight->m_iSize)
107 result = -1;
108 else // fallback
109 result = SortStrCmp(pLeft->m_sName, pRight->m_sName);
112 if (m_desc)
113 result = -result;
115 if (pLeft->m_bFolder != pRight->m_bFolder)
117 if (pRight->m_bFolder)
118 result = 1;
119 else
120 result = -1;
123 return result;
125 int SortStrCmp(const CString &left, const CString &right)
127 if (CRepositoryBrowser::s_bSortLogical)
128 return StrCmpLogicalW(left, right);
129 return StrCmpI(left, right);
132 int m_col = CRepositoryBrowser::eCol_Name;
133 bool m_desc = false;
134 CListCtrl* m_pList = nullptr;
137 // CRepositoryBrowser dialog
139 bool CRepositoryBrowser::s_bSortLogical = true;
141 IMPLEMENT_DYNAMIC(CRepositoryBrowser, CResizableStandAloneDialog)
143 CRepositoryBrowser::CRepositoryBrowser(CString rev, CWnd* pParent /*=nullptr*/)
144 : CResizableStandAloneDialog(CRepositoryBrowser::IDD, pParent)
145 , m_sRevision(rev)
146 , m_ColumnManager(&m_RepoList)
150 CRepositoryBrowser::~CRepositoryBrowser()
154 void CRepositoryBrowser::DoDataExchange(CDataExchange* pDX)
156 CDialog::DoDataExchange(pDX);
157 DDX_Control(pDX, IDC_REPOTREE, m_RepoTree);
158 DDX_Control(pDX, IDC_REPOLIST, m_RepoList);
162 BEGIN_MESSAGE_MAP(CRepositoryBrowser, CResizableStandAloneDialog)
163 ON_NOTIFY(TVN_SELCHANGED, IDC_REPOTREE, &CRepositoryBrowser::OnTvnSelchangedRepoTree)
164 ON_NOTIFY(TVN_ITEMEXPANDING, IDC_REPOTREE, &CRepositoryBrowser::OnTvnItemExpandingRepoTree)
165 ON_WM_CONTEXTMENU()
166 ON_NOTIFY(LVN_COLUMNCLICK, IDC_REPOLIST, &CRepositoryBrowser::OnLvnColumnclickRepoList)
167 ON_NOTIFY(LVN_ITEMCHANGED, IDC_REPOLIST, &CRepositoryBrowser::OnLvnItemchangedRepolist)
168 ON_NOTIFY(NM_DBLCLK, IDC_REPOLIST, &CRepositoryBrowser::OnNMDblclk_RepoList)
169 ON_BN_CLICKED(IDC_BUTTON_REVISION, &CRepositoryBrowser::OnBnClickedButtonRevision)
170 ON_WM_SETCURSOR()
171 ON_WM_DESTROY()
172 ON_WM_MOUSEMOVE()
173 ON_WM_LBUTTONDOWN()
174 ON_WM_LBUTTONUP()
175 ON_NOTIFY(LVN_BEGINDRAG, IDC_REPOLIST, &CRepositoryBrowser::OnLvnBegindragRepolist)
176 ON_NOTIFY(TVN_BEGINDRAG, IDC_REPOTREE, &CRepositoryBrowser::OnTvnBegindragRepotree)
177 END_MESSAGE_MAP()
180 // CRepositoryBrowser message handlers
182 BOOL CRepositoryBrowser::OnInitDialog()
184 CResizableStandAloneDialog::OnInitDialog();
185 CAppUtils::MarkWindowAsUnpinnable(m_hWnd);
187 AddAnchor(IDC_STATIC_REPOURL, TOP_LEFT);
188 AddAnchor(IDC_REPOBROWSER_URL, TOP_LEFT, TOP_RIGHT);
189 AddAnchor(IDC_STATIC_REF, TOP_RIGHT);
190 AddAnchor(IDC_BUTTON_REVISION, TOP_RIGHT);
191 AddAnchor(IDC_REPOTREE, TOP_LEFT, BOTTOM_LEFT);
192 AddAnchor(IDC_REPOLIST, TOP_LEFT, BOTTOM_RIGHT);
193 AddAnchor(IDHELP, BOTTOM_RIGHT);
194 AddAnchor(IDC_INFOLABEL, BOTTOM_LEFT, BOTTOM_RIGHT);
195 AddAnchor(IDOK, BOTTOM_RIGHT);
196 AddAnchor(IDCANCEL, BOTTOM_RIGHT);
198 CRepositoryBrowser::s_bSortLogical = !CRegDWORD(L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\Explorer\\NoStrCmpLogical", 0, false, HKEY_CURRENT_USER);
199 if (CRepositoryBrowser::s_bSortLogical)
200 CRepositoryBrowser::s_bSortLogical = !CRegDWORD(L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\Explorer\\NoStrCmpLogical", 0, false, HKEY_LOCAL_MACHINE);
202 static UINT columnNames[] = { IDS_STATUSLIST_COLFILENAME, IDS_STATUSLIST_COLEXT, IDS_LOG_SIZE };
203 static int columnWidths[] = { CDPIAware::Instance().ScaleX(GetSafeHwnd(), 150), CDPIAware::Instance().ScaleX(GetSafeHwnd(), 100), CDPIAware::Instance().ScaleX(GetSafeHwnd(), 100) };
204 static_assert(_countof(columnNames) == _countof(columnWidths));
205 DWORD dwDefaultColumns = (1 << eCol_Name) | (1 << eCol_Extension) | (1 << eCol_FileSize);
206 m_ColumnManager.SetNames(columnNames, _countof(columnNames));
207 constexpr int columnVersion = 6; // adjust when changing number/names/etc. of columns
208 m_ColumnManager.ReadSettings(dwDefaultColumns, 0, L"RepoBrowser", columnVersion, _countof(columnNames), columnWidths);
209 m_ColumnManager.SetRightAlign(m_ColumnManager.GetColumnByName(IDS_LOG_SIZE));
211 // set up the list control
212 // set the extended style of the list control
213 // 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.
214 CRegDWORD regFullRowSelect(L"Software\\TortoiseGit\\FullRowSelect", TRUE);
215 DWORD exStyle = LVS_EX_HEADERDRAGDROP | LVS_EX_DOUBLEBUFFER | LVS_EX_INFOTIP | LVS_EX_SUBITEMIMAGES;
216 if (DWORD(regFullRowSelect))
217 exStyle |= LVS_EX_FULLROWSELECT;
218 m_RepoList.SetExtendedStyle(exStyle);
219 m_RepoList.SetImageList(&SYS_IMAGE_LIST(), LVSIL_SMALL);
220 CAppUtils::SetListCtrlBackgroundImage(m_RepoList.GetSafeHwnd(), IDI_REPOBROWSER_BKG);
222 m_RepoTree.SetImageList(&SYS_IMAGE_LIST(), TVSIL_NORMAL);
223 exStyle = TVS_EX_FADEINOUTEXPANDOS | TVS_EX_AUTOHSCROLL | TVS_EX_DOUBLEBUFFER;
224 m_RepoTree.SetExtendedStyle(exStyle, exStyle);
226 m_nExternalOvl = SYS_IMAGE_LIST().AddIcon(CCommonAppUtils::LoadIconEx(IDI_EXTERNALOVL, 0, 0));
227 m_nExecutableOvl = SYS_IMAGE_LIST().AddIcon(CCommonAppUtils::LoadIconEx(IDI_EXECUTABLEOVL, 0, 0));
228 m_nSymlinkOvl = SYS_IMAGE_LIST().AddIcon(CCommonAppUtils::LoadIconEx(IDI_SYMLINKOVL, 0, 0));
229 // set externaloverlay in SYS_IMAGE_LIST() in Refresh method, so that it is updated after every launch of the logdialog
231 SetWindowTheme(m_RepoTree.GetSafeHwnd(), L"Explorer", nullptr);
232 SetWindowTheme(m_RepoList.GetSafeHwnd(), L"Explorer", nullptr);
234 int borderWidth = 0;
235 if (IsAppThemed())
237 CAutoThemeData hTheme = OpenThemeData(m_RepoTree, L"TREEVIEW");
238 GetThemeMetric(hTheme, NULL, TVP_TREEITEM, TREIS_NORMAL, TMT_BORDERSIZE, &borderWidth);
240 else
241 borderWidth = GetSystemMetrics(SM_CYBORDER);
242 m_RepoTree.SetItemHeight(static_cast<SHORT>(m_RepoTree.GetItemHeight() + 2 * borderWidth));
244 m_nIconFolder = SYS_IMAGE_LIST().GetDirIconIndex();
245 m_nOpenIconFolder = SYS_IMAGE_LIST().GetDirOpenIconIndex();
247 EnableSaveRestore(L"Reposbrowser");
249 DWORD xPos = CRegDWORD(L"Software\\TortoiseGit\\TortoiseProc\\ResizableState\\RepobrowserDivider", 0);
250 if (xPos == 0)
252 RECT rc;
253 GetDlgItem(IDC_REPOTREE)->GetClientRect(&rc);
254 xPos = rc.right - rc.left;
256 HandleDividerMove(CPoint(CDPIAware::Instance().ScaleX(GetSafeHwnd(), xPos + 20), CDPIAware::Instance().ScaleY(GetSafeHwnd(), 10)), false);
258 CAppUtils::SetWindowTitle(*this, g_Git.m_CurrentDir);
260 m_bHasWC = !GitAdminDir::IsBareRepo(g_Git.m_CurrentDir);
262 Refresh();
264 UpdateDiffWithFileFromReg();
266 m_RepoList.SetFocus();
268 return FALSE;
271 void CRepositoryBrowser::UpdateDiffWithFileFromReg()
273 static CString lastDiffLaterFile;
274 if (CString diffLaterFile = CRegString(L"Software\\TortoiseGit\\DiffLater", L""); !diffLaterFile.IsEmpty() && lastDiffLaterFile != diffLaterFile)
276 lastDiffLaterFile = diffLaterFile;
277 m_sMarkForDiffFilename = diffLaterFile;
278 m_sMarkForDiffVersion.Empty();
282 void CRepositoryBrowser::OnDestroy()
284 const int maxcol = m_ColumnManager.GetColumnCount();
285 for (int col = 0; col < maxcol; ++col)
286 if (m_ColumnManager.IsVisible(col))
287 m_ColumnManager.ColumnResized(col);
288 m_ColumnManager.WriteSettings();
290 CResizableStandAloneDialog::OnDestroy();
293 void CRepositoryBrowser::OnOK()
295 if (GetFocus() == &m_RepoList && (GetKeyState(VK_MENU) & 0x8000) == 0)
297 // list control has focus: 'enter' the folder
298 if (m_RepoList.GetSelectedCount() != 1)
299 return;
301 POSITION pos = m_RepoList.GetFirstSelectedItemPosition();
302 if (pos)
304 auto item = GetListEntry(m_RepoList.GetNextSelectedItem(pos));
305 if (item->m_bFolder)
307 FillListCtrlForShadowTree(item);
308 m_RepoTree.SelectItem(item->m_hTree);
310 else
311 OpenFile(item->GetFullName(), OPEN, item->m_bSubmodule, item->m_hash);
313 return;
316 SaveDividerPosition();
317 CResizableStandAloneDialog::OnOK();
320 void CRepositoryBrowser::OnCancel()
322 SaveDividerPosition();
323 CResizableStandAloneDialog::OnCancel();
326 void CRepositoryBrowser::OnNMDblclk_RepoList(NMHDR *pNMHDR, LRESULT *pResult)
328 *pResult = 0;
330 LPNMITEMACTIVATE pNmItemActivate = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);
331 if (pNmItemActivate->iItem < 0)
332 return;
334 auto pItem = GetListEntry(pNmItemActivate->iItem);
335 if (!pItem )
336 return;
338 if (!pItem->m_bFolder)
340 OpenFile(pItem->GetFullName(), OPEN, pItem->m_bSubmodule, pItem->m_hash);
341 return;
343 else
345 FillListCtrlForShadowTree(pItem);
346 m_RepoTree.SelectItem(pItem->m_hTree);
350 void CRepositoryBrowser::Refresh()
352 BeginWaitCursor();
353 if (m_nExternalOvl >= 0)
354 SYS_IMAGE_LIST().SetOverlayImage(m_nExternalOvl, OVERLAY_EXTERNAL);
355 if (m_nExecutableOvl >= 0)
356 SYS_IMAGE_LIST().SetOverlayImage(m_nExecutableOvl, OVERLAY_EXECUTABLE);
357 if (m_nSymlinkOvl >= 0)
358 SYS_IMAGE_LIST().SetOverlayImage(m_nSymlinkOvl, OVERLAY_SYMLINK);
360 m_RepoTree.DeleteAllItems();
361 m_RepoList.DeleteAllItems();
362 m_TreeRoot.m_ShadowTree.clear();
363 m_TreeRoot.m_sName.Empty();
364 m_TreeRoot.m_bFolder = true;
366 TVINSERTSTRUCT tvinsert = {0};
367 tvinsert.hParent = TVI_ROOT;
368 tvinsert.hInsertAfter = TVI_ROOT;
369 tvinsert.itemex.mask = TVIF_DI_SETITEM | TVIF_PARAM | TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_STATE;
370 tvinsert.itemex.pszText = const_cast<LPWSTR>(L"/");
371 tvinsert.itemex.lParam = reinterpret_cast<LPARAM>(&m_TreeRoot);
372 tvinsert.itemex.iImage = m_nIconFolder;
373 tvinsert.itemex.iSelectedImage = m_nOpenIconFolder;
374 m_TreeRoot.m_hTree= m_RepoTree.InsertItem(&tvinsert);
376 ReadTree(&m_TreeRoot);
377 m_RepoTree.Expand(m_TreeRoot.m_hTree, TVE_EXPAND);
378 FillListCtrlForShadowTree(&m_TreeRoot);
379 m_RepoTree.SelectItem(m_TreeRoot.m_hTree);
380 EndWaitCursor();
383 int CRepositoryBrowser::ReadTreeRecursive(git_repository& repo, const git_tree* tree, CShadowFilesTree* treeroot, bool recursive)
385 const size_t count = git_tree_entrycount(tree);
386 bool hasSubfolders = false;
387 treeroot->m_bLoaded = true;
389 for (size_t i = 0; i < count; ++i)
391 const git_tree_entry *entry = git_tree_entry_byindex(tree, i);
392 if (!entry)
393 continue;
395 const int mode = git_tree_entry_filemode(entry);
397 CString base = CUnicodeUtils::GetUnicode(git_tree_entry_name(entry));
399 const git_oid *oid = git_tree_entry_id(entry);
400 CShadowFilesTree * pNextTree = &treeroot->m_ShadowTree[base];
401 pNextTree->m_sName = base;
402 pNextTree->m_pParent = treeroot;
403 pNextTree->m_hash = oid;
405 if (mode == GIT_FILEMODE_COMMIT)
406 pNextTree->m_bSubmodule = true;
407 else if (mode & S_IFDIR)
409 hasSubfolders = true;
410 pNextTree->m_bFolder = true;
411 pNextTree->m_bLoaded = false;
413 TVINSERTSTRUCT tvinsert = {0};
414 tvinsert.hParent = treeroot->m_hTree;
415 tvinsert.hInsertAfter = TVI_SORT;
416 tvinsert.itemex.mask = TVIF_DI_SETITEM | TVIF_PARAM | TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_STATE | TVIF_CHILDREN;
417 tvinsert.itemex.pszText = base.GetBuffer(base.GetLength());
418 tvinsert.itemex.cChildren = 1;
419 tvinsert.itemex.lParam = reinterpret_cast<LPARAM>(pNextTree);
420 tvinsert.itemex.iImage = m_nIconFolder;
421 tvinsert.itemex.iSelectedImage = m_nOpenIconFolder;
422 pNextTree->m_hTree = m_RepoTree.InsertItem(&tvinsert);
423 base.ReleaseBuffer();
424 if (recursive)
426 CAutoTree subtree;
427 if (git_tree_lookup(subtree.GetPointer(), &repo, oid))
429 MessageBox(CGit::GetLibGit2LastErr(L"Could not lookup path."), L"TortoiseGit", MB_ICONERROR);
430 return -1;
433 ReadTreeRecursive(repo, subtree, pNextTree, recursive);
436 else
438 if (mode == GIT_FILEMODE_BLOB_EXECUTABLE)
439 pNextTree->m_bExecutable = true;
440 if (mode == GIT_FILEMODE_LINK)
441 pNextTree->m_bSymlink = true;
442 CAutoBlob blob;
443 git_blob_lookup(blob.GetPointer(), &repo, oid);
444 if (!blob)
445 continue;
447 pNextTree->m_iSize = git_blob_rawsize(blob);
451 if (!hasSubfolders)
453 TVITEM tvitem = { 0 };
454 tvitem.hItem = treeroot->m_hTree;
455 tvitem.mask = TVIF_CHILDREN;
456 tvitem.cChildren = 0;
457 m_RepoTree.SetItem(&tvitem);
460 return 0;
463 int CRepositoryBrowser::ReadTree(CShadowFilesTree* treeroot, const CString& root, bool recursive)
465 CWaitCursor wait;
466 CAutoRepository repository(g_Git.GetGitRepository());
467 if (!repository)
469 MessageBox(CGit::GetLibGit2LastErr(L"Could not open repository."), L"TortoiseGit", MB_ICONERROR);
470 return -1;
473 if (m_sRevision == L"HEAD")
475 int ret = git_repository_head_unborn(repository);
476 if (ret == 1) // is orphan
477 return ret;
478 else if (ret != 0)
480 MessageBox(g_Git.GetLibGit2LastErr(L"Could not check HEAD."), L"TortoiseGit", MB_ICONERROR);
481 return ret;
485 CGitHash hash;
486 if (CGit::GetHash(repository, hash, m_sRevision + L"^{}")) // add ^{} in order to dereference signed tags
488 MessageBox(CGit::GetLibGit2LastErr(L"Could not get hash of \"" + m_sRevision + L"^{}\"."), L"TortoiseGit", MB_ICONERROR);
489 return -1;
492 CAutoCommit commit;
493 if (git_commit_lookup(commit.GetPointer(), repository, hash))
495 MessageBox(CGit::GetLibGit2LastErr(L"Could not lookup commit."), L"TortoiseGit", MB_ICONERROR);
496 return -1;
499 CAutoTree tree;
500 if (git_commit_tree(tree.GetPointer(), commit))
502 MessageBox(CGit::GetLibGit2LastErr(L"Could not get tree of commit."), L"TortoiseGit", MB_ICONERROR);
503 return -1;
506 if (!root.IsEmpty())
508 CAutoTreeEntry treeEntry;
509 if (git_tree_entry_bypath(treeEntry.GetPointer(), tree, CUnicodeUtils::GetUTF8(root)))
511 MessageBox(CGit::GetLibGit2LastErr(L"Could not lookup path."), L"TortoiseGit", MB_ICONERROR);
512 return -1;
514 if (git_tree_entry_type(treeEntry) != GIT_OBJECT_TREE)
516 MessageBox(CGit::GetLibGit2LastErr(L"Could not lookup path."), L"TortoiseGit", MB_ICONERROR);
517 return -1;
520 CAutoObject object;
521 if (git_tree_entry_to_object(object.GetPointer(), repository, treeEntry))
523 MessageBox(CGit::GetLibGit2LastErr(L"Could not lookup path."), L"TortoiseGit", MB_ICONERROR);
524 return -1;
527 tree.ConvertFrom(std::move(object));
530 treeroot->m_hash = git_tree_id(tree);
531 ReadTreeRecursive(*repository, tree, treeroot, recursive);
533 // try to resolve hash to a branch name
534 if (m_sRevision == hash.ToString())
536 MAP_HASH_NAME map;
537 if (CGit::GetMapHashToFriendName(repository, map))
538 MessageBox(g_Git.GetLibGit2LastErr(L"Could not get all refs."), L"TortoiseGit", MB_ICONERROR);
539 if (!map[hash].empty())
540 m_sRevision = map[hash].at(0);
542 this->GetDlgItem(IDC_BUTTON_REVISION)->SetWindowText(CStringUtils::EscapeAccellerators(m_sRevision));
544 return 0;
547 void CRepositoryBrowser::OnTvnSelchangedRepoTree(NMHDR *pNMHDR, LRESULT *pResult)
549 LPNMTREEVIEW pNMTreeView = reinterpret_cast<LPNMTREEVIEW>(pNMHDR);
550 *pResult = 0;
552 FillListCtrlForTreeNode(pNMTreeView->itemNew.hItem);
555 void CRepositoryBrowser::OnTvnItemExpandingRepoTree(NMHDR *pNMHDR, LRESULT *pResult)
557 LPNMTREEVIEW pNMTreeView = reinterpret_cast<LPNMTREEVIEW>(pNMHDR);
558 *pResult = 0;
560 auto pTree = GetTreeEntry(pNMTreeView->itemNew.hItem);
561 if (!pTree)
562 return;
564 if (!pTree->m_bLoaded)
565 ReadTree(pTree, pTree->GetFullName());
568 void CRepositoryBrowser::FillListCtrlForTreeNode(HTREEITEM treeNode)
570 m_RepoList.DeleteAllItems();
572 auto pTree = GetTreeEntry(treeNode);
573 if (!pTree)
574 return;
576 CString url = L'/' + pTree->GetFullName();
577 GetDlgItem(IDC_REPOBROWSER_URL)->SetWindowText(url);
579 if (!pTree->m_bLoaded)
580 ReadTree(pTree, pTree->GetFullName());
582 FillListCtrlForShadowTree(pTree);
585 void CRepositoryBrowser::FillListCtrlForShadowTree(CShadowFilesTree* pTree)
587 for (auto itShadowTree = pTree->m_ShadowTree.cbegin(); itShadowTree != pTree->m_ShadowTree.cend(); ++itShadowTree)
589 int icon = m_nIconFolder;
590 if (!(*itShadowTree).second.m_bFolder && !(*itShadowTree).second.m_bSubmodule)
592 icon = SYS_IMAGE_LIST().GetPathIconIndex((*itShadowTree).second.m_sName);
595 const int indexItem = m_RepoList.InsertItem(m_RepoList.GetItemCount(), (*itShadowTree).second.m_sName, icon);
597 if ((*itShadowTree).second.m_bSubmodule)
599 m_RepoList.SetItemState(indexItem, INDEXTOOVERLAYMASK(OVERLAY_EXTERNAL), LVIS_OVERLAYMASK);
601 if ((*itShadowTree).second.m_bExecutable)
602 m_RepoList.SetItemState(indexItem, INDEXTOOVERLAYMASK(OVERLAY_EXECUTABLE), LVIS_OVERLAYMASK);
603 if ((*itShadowTree).second.m_bSymlink)
604 m_RepoList.SetItemState(indexItem, INDEXTOOVERLAYMASK(OVERLAY_SYMLINK), LVIS_OVERLAYMASK);
605 m_RepoList.SetItemData(indexItem, reinterpret_cast<DWORD_PTR>(&(*itShadowTree).second));
606 if (!(*itShadowTree).second.m_bFolder && !(*itShadowTree).second.m_bSubmodule)
608 CString temp;
610 temp = CPathUtils::GetFileExtFromPath((*itShadowTree).second.m_sName);
611 m_RepoList.SetItemText(indexItem, eCol_Extension, temp);
613 StrFormatByteSize64((*itShadowTree).second.m_iSize, CStrBuf(temp, 20), 20);
614 m_RepoList.SetItemText(indexItem, eCol_FileSize, temp);
618 CRepoListCompareFunc compareFunc(&m_RepoList, m_currSortCol, m_currSortDesc);
619 m_RepoList.SortItemsEx(&CRepoListCompareFunc::StaticCompare, reinterpret_cast<DWORD_PTR>(&compareFunc));
621 SetSortArrowA(&m_RepoList, m_currSortCol, !m_currSortDesc);
623 UpdateInfoLabel();
626 void CRepositoryBrowser::UpdateInfoLabel()
628 CString temp;
629 POSITION pos = m_RepoList.GetFirstSelectedItemPosition();
630 if (pos)
632 if (m_RepoList.GetSelectedCount() > 1)
634 temp.FormatMessage(IDS_REPOBROWSE_INFOMULTI, m_RepoList.GetSelectedCount());
636 else
638 int index = m_RepoList.GetNextSelectedItem(pos);
639 auto item = GetListEntry(index);
640 if (item->m_bSubmodule)
641 temp.FormatMessage(IDS_REPOBROWSE_INFOEXT, static_cast<LPCWSTR>(m_RepoList.GetItemText(index, eCol_Name)), static_cast<LPCWSTR>(item->m_hash.ToString()));
642 else if (item->m_bFolder)
643 temp = m_RepoList.GetItemText(index, eCol_Name);
644 else
645 temp.FormatMessage(IDS_REPOBROWSE_INFOFILE, static_cast<LPCWSTR>(m_RepoList.GetItemText(index, eCol_Name)), static_cast<LPCWSTR>(m_RepoList.GetItemText(index, eCol_FileSize)));
648 else
650 HTREEITEM hTreeItem = m_RepoTree.GetSelectedItem();
651 if (hTreeItem != nullptr)
653 auto pTree = GetTreeEntry(hTreeItem);
654 if (pTree != nullptr)
656 size_t files = 0, submodules = 0;
657 for (auto itShadowTree = pTree->m_ShadowTree.cbegin(); itShadowTree != pTree->m_ShadowTree.cend(); ++itShadowTree)
659 if (!(*itShadowTree).second.m_bFolder && !(*itShadowTree).second.m_bSubmodule)
660 ++files;
661 if ((*itShadowTree).second.m_bSubmodule)
662 ++submodules;
664 temp.FormatMessage(IDS_REPOBROWSE_INFO, static_cast<LPCWSTR>(pTree->m_sName), files, submodules, pTree->m_ShadowTree.size() - files - submodules, pTree->m_ShadowTree.size());
668 SetDlgItemText(IDC_INFOLABEL, temp);
671 void CRepositoryBrowser::OnLvnItemchangedRepolist(NMHDR * /* pNMHDR */, LRESULT *pResult)
673 *pResult = 0;
674 UpdateInfoLabel();
677 void CRepositoryBrowser::OnContextMenu(CWnd* pWndFrom, CPoint point)
679 if (pWndFrom == &m_RepoList)
681 CRect headerPosition;
682 m_RepoList.GetHeaderCtrl()->GetWindowRect(headerPosition);
683 if (!headerPosition.PtInRect(point))
684 OnContextMenu_RepoList(point);
686 else if (pWndFrom == &m_RepoTree)
687 OnContextMenu_RepoTree(point);
690 void CRepositoryBrowser::OnContextMenu_RepoTree(CPoint point)
692 CPoint clientPoint = point;
693 m_RepoTree.ScreenToClient(&clientPoint);
695 HTREEITEM hTreeItem = m_RepoTree.HitTest(clientPoint);
696 if (!hTreeItem)
697 return;
699 TShadowFilesTreeList selectedLeafs;
700 selectedLeafs.push_back(GetTreeEntry(hTreeItem));
702 ShowContextMenu(point, selectedLeafs, ONLY_FOLDERS);
705 void CRepositoryBrowser::OnContextMenu_RepoList(CPoint point)
707 TShadowFilesTreeList selectedLeafs;
708 selectedLeafs.reserve(m_RepoList.GetSelectedCount());
710 bool folderSelected = false;
711 bool filesSelected = false;
712 bool submodulesSelected = false;
714 POSITION pos = m_RepoList.GetFirstSelectedItemPosition();
715 while (pos)
717 auto item = GetListEntry(m_RepoList.GetNextSelectedItem(pos));
718 if (item->m_bSubmodule)
719 submodulesSelected = true;
720 if (item->m_bFolder)
721 folderSelected = true;
722 else
723 filesSelected = true;
724 selectedLeafs.push_back(item);
727 eSelectionType selType = ONLY_FILES;
728 if (folderSelected && filesSelected)
729 selType = MIXED_FOLDERS_FILES;
730 else if (folderSelected)
731 selType = ONLY_FOLDERS;
732 else if (submodulesSelected)
733 selType = ONLY_FILESSUBMODULES;
734 ShowContextMenu(point, selectedLeafs, selType);
737 void CRepositoryBrowser::ShowContextMenu(CPoint point, TShadowFilesTreeList &selectedLeafs, eSelectionType selType)
739 CIconMenu popupMenu;
740 popupMenu.CreatePopupMenu();
742 bool bAddSeparator = false;
744 if (selectedLeafs.size() == 1)
746 popupMenu.AppendMenuIcon(eCmd_Open, IDS_REPOBROWSE_OPEN, IDI_OPEN);
747 popupMenu.SetDefaultItem(eCmd_Open, FALSE);
748 if (selType == ONLY_FILES || selType == ONLY_FILESSUBMODULES)
750 popupMenu.AppendMenuIcon(eCmd_OpenWith, IDS_LOG_POPUP_OPENWITH, IDI_OPEN);
751 popupMenu.AppendMenuIcon(eCmd_OpenWithAlternativeEditor, IDS_LOG_POPUP_VIEWREV, IDI_NOTEPAD);
754 popupMenu.AppendMenu(MF_SEPARATOR);
756 if (m_bHasWC && (selType == ONLY_FILES || selType == ONLY_FILESSUBMODULES))
758 popupMenu.AppendMenuIcon(eCmd_CompareWC, IDS_LOG_POPUP_COMPARE, IDI_DIFF);
759 bAddSeparator = true;
762 if (bAddSeparator)
763 popupMenu.AppendMenu(MF_SEPARATOR);
764 bAddSeparator = false;
766 CString temp;
767 temp.LoadString(IDS_MENULOG);
768 popupMenu.AppendMenuIcon(eCmd_ViewLog, temp, IDI_LOG);
769 if (selectedLeafs[0]->m_bSubmodule)
771 temp.LoadString(IDS_LOG_SUBMODULE);
772 popupMenu.AppendMenuIcon(eCmd_ViewLogSubmodule, temp, IDI_LOG);
775 if (selType == ONLY_FILES)
777 if (m_bHasWC)
778 popupMenu.AppendMenuIcon(eCmd_Blame, IDS_LOG_POPUP_BLAME, IDI_BLAME);
780 popupMenu.AppendMenu(MF_SEPARATOR);
781 temp.LoadString(IDS_LOG_POPUP_SAVE);
782 popupMenu.AppendMenuIcon(eCmd_SaveAs, temp, IDI_SAVEAS);
785 bAddSeparator = true;
788 if (!selectedLeafs.empty() && selType == ONLY_FILES && m_bHasWC)
790 popupMenu.AppendMenuIcon(eCmd_Revert, IDS_LOG_POPUP_REVERTTOREV, IDI_REVERT);
791 bAddSeparator = true;
794 if (bAddSeparator)
795 popupMenu.AppendMenu(MF_SEPARATOR);
797 if (selectedLeafs.size() == 1 && selType == ONLY_FILES)
799 popupMenu.AppendMenuIcon(eCmd_PrepareDiff, IDS_PREPAREDIFF, IDI_DIFF);
800 UpdateDiffWithFileFromReg();
801 if (!m_sMarkForDiffFilename.IsEmpty())
803 CString diffWith;
804 if (selectedLeafs.at(0)->GetFullName() == m_sMarkForDiffFilename)
805 diffWith = m_sMarkForDiffVersion.ToString();
806 else
808 PathCompactPathEx(CStrBuf(diffWith, 2 * GIT_HASH_SIZE), m_sMarkForDiffFilename, 2 * GIT_HASH_SIZE, 0);
809 if (!m_sMarkForDiffVersion.IsEmpty() || PathIsRelative(m_sMarkForDiffFilename))
810 diffWith += L':' + m_sMarkForDiffVersion.ToString(g_Git.GetShortHASHLength());
812 CString menuEntry;
813 menuEntry.Format(IDS_MENUDIFFNOW, static_cast<LPCWSTR>(diffWith));
814 popupMenu.AppendMenuIcon(eCmd_PrepareDiff_Compare, menuEntry, IDI_DIFF);
816 popupMenu.AppendMenu(MF_SEPARATOR);
819 if (!selectedLeafs.empty())
821 popupMenu.AppendMenuIcon(eCmd_CopyPath, IDS_STATUSLIST_CONTEXT_COPY, IDI_COPYCLIP);
822 popupMenu.AppendMenuIcon(eCmd_CopyHash, IDS_COPY_COMMIT_HASH, IDI_COPYCLIP);
825 const eCmd cmd = static_cast<eCmd>(popupMenu.TrackPopupMenuEx(TPM_LEFTALIGN | TPM_RETURNCMD, point.x, point.y, this, nullptr));
826 switch(cmd)
828 case eCmd_ViewLog:
829 case eCmd_ViewLogSubmodule:
831 CString sCmd;
832 sCmd.Format(L"/command:log /path:\"%s\"", static_cast<LPCWSTR>(g_Git.CombinePath(selectedLeafs.at(0)->GetFullName())));
833 if (cmd == eCmd_ViewLog && selectedLeafs.at(0)->m_bSubmodule)
834 sCmd += L" /submodule";
835 if (cmd == eCmd_ViewLog)
836 sCmd += L" /endrev:" + m_sRevision;
837 CAppUtils::RunTortoiseGitProc(sCmd);
839 break;
840 case eCmd_Blame:
842 CAppUtils::LaunchTortoiseBlame(g_Git.CombinePath(selectedLeafs.at(0)->GetFullName()), m_sRevision);
844 break;
845 case eCmd_Open:
846 if (!selectedLeafs.at(0)->m_bSubmodule && selectedLeafs.at(0)->m_bFolder)
848 FillListCtrlForTreeNode(selectedLeafs.at(0)->m_hTree);
849 m_RepoTree.SelectItem(selectedLeafs.at(0)->m_hTree);
850 return;
852 OpenFile(selectedLeafs.at(0)->GetFullName(), OPEN, selectedLeafs.at(0)->m_bSubmodule, selectedLeafs.at(0)->m_hash);
853 break;
854 case eCmd_OpenWith:
855 OpenFile(selectedLeafs.at(0)->GetFullName(), OPEN_WITH, selectedLeafs.at(0)->m_bSubmodule, selectedLeafs.at(0)->m_hash);
856 break;
857 case eCmd_OpenWithAlternativeEditor:
858 OpenFile(selectedLeafs.at(0)->GetFullName(), ALTERNATIVEEDITOR, selectedLeafs.at(0)->m_bSubmodule, selectedLeafs.at(0)->m_hash);
859 break;
860 case eCmd_CompareWC:
862 CTGitPath file(selectedLeafs.at(0)->GetFullName());
863 CGitDiff::Diff(GetSafeHwnd(), &file, &file, GIT_REV_ZERO, m_sRevision);
865 break;
866 case eCmd_Revert:
868 int count = 0;
869 for (auto itShadowTree = selectedLeafs.cbegin(); itShadowTree != selectedLeafs.cend(); ++itShadowTree)
871 if (RevertItemToVersion((*itShadowTree)->GetFullName()))
872 ++count;
873 else
874 break;
876 CString msg;
877 msg.FormatMessage(IDS_STATUSLIST_FILESREVERTED, count, static_cast<LPCWSTR>(m_sRevision));
878 MessageBox(msg, L"TortoiseGit", MB_OK);
880 break;
881 case eCmd_SaveAs:
882 FileSaveAs(selectedLeafs.at(0)->GetFullName());
883 break;
884 case eCmd_CopyPath:
886 CString sClipboard;
887 for (auto itShadowTree = selectedLeafs.cbegin(); itShadowTree != selectedLeafs.cend(); ++itShadowTree)
889 sClipboard += (*itShadowTree)->m_sName + L"\r\n";
891 CStringUtils::WriteAsciiStringToClipboard(sClipboard);
893 break;
894 case eCmd_CopyHash:
896 CopyHashToClipboard(selectedLeafs);
898 break;
899 case eCmd_PrepareDiff:
900 m_sMarkForDiffFilename = selectedLeafs.at(0)->GetFullName();
901 if (g_Git.GetHash(m_sMarkForDiffVersion, m_sRevision + L"^{}")) // add ^{} in order to dereference signed tags
903 m_sMarkForDiffFilename.Empty();
904 MessageBox(g_Git.GetGitLastErr(L"Could not get SHA-1 for \"" + m_sRevision + L"^{}\"."), L"TortoiseGit", MB_ICONERROR);
906 break;
907 case eCmd_PrepareDiff_Compare:
909 if (auto reg = CRegString(L"Software\\TortoiseGit\\DiffLater", L""); m_sMarkForDiffFilename == reg)
910 reg.removeValue();
911 CTGitPath savedFile(m_sMarkForDiffFilename);
912 CTGitPath selectedFile(selectedLeafs.at(0)->GetFullName());
913 CGitHash currentHash;
914 if (g_Git.GetHash(currentHash, m_sRevision + L"^{}")) // add ^{} in order to dereference signed tags
916 MessageBox(g_Git.GetGitLastErr(L"Could not get SHA-1 for \"" + m_sRevision + L"^{}\"."), L"TortoiseGit", MB_ICONERROR);
917 return;
919 CGitDiff::Diff(GetSafeHwnd(), &selectedFile, &savedFile, currentHash.ToString(), m_sMarkForDiffVersion.ToString());
921 break;
925 BOOL CRepositoryBrowser::PreTranslateMessage(MSG* pMsg)
927 if (pMsg->message == WM_KEYDOWN)
929 switch (pMsg->wParam)
931 case VK_F5:
933 Refresh();
935 break;
936 case L'A':
937 if (pMsg->hwnd == m_RepoList.m_hWnd && (GetAsyncKeyState(VK_CONTROL) & 0x8000))
939 // select all entries
940 for (int i = 0; i < m_RepoList.GetItemCount(); ++i)
941 m_RepoList.SetItemState(i, LVIS_SELECTED, LVIS_SELECTED);
942 return TRUE;
944 break;
948 return CResizableStandAloneDialog::PreTranslateMessage(pMsg);
951 void CRepositoryBrowser::OnLvnColumnclickRepoList(NMHDR *pNMHDR, LRESULT *pResult)
953 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
954 if (pResult)
955 *pResult = 0;
957 if (m_currSortCol == pNMLV->iSubItem)
958 m_currSortDesc = !m_currSortDesc;
959 else
961 m_currSortCol = pNMLV->iSubItem;
962 m_currSortDesc = false;
965 CRepoListCompareFunc compareFunc(&m_RepoList, m_currSortCol, m_currSortDesc);
966 m_RepoList.SortItemsEx(&CRepoListCompareFunc::StaticCompare, reinterpret_cast<DWORD_PTR>(&compareFunc));
968 SetSortArrowA(&m_RepoList, m_currSortCol, !m_currSortDesc);
971 void CRepositoryBrowser::OnBnClickedButtonRevision()
973 // use the git log to allow selection of a version
974 CLogDlg dlg;
975 if (dlg.IsThreadRunning())
977 CMessageBox::Show(GetSafeHwnd(), IDS_PROC_LOG_ONLYONCE, IDS_APPNAME, MB_ICONEXCLAMATION);
978 return;
980 dlg.SetParams(CTGitPath(), CTGitPath(), m_sRevision, m_sRevision, 0);
981 // tell the dialog to use mode for selecting revisions
982 dlg.SetSelect(true);
983 dlg.ShowWorkingTreeChanges(false);
984 // only one revision must be selected however
985 dlg.SingleSelection(true);
986 if (dlg.DoModal() == IDOK && !dlg.GetSelectedHash().empty())
988 m_sRevision = dlg.GetSelectedHash().at(0).ToString();
989 Refresh();
991 BringWindowToTop(); /* cf. issue #3493 */
994 void CRepositoryBrowser::SaveDividerPosition()
996 RECT rc;
997 GetDlgItem(IDC_REPOTREE)->GetClientRect(&rc);
998 CRegDWORD xPos(L"Software\\TortoiseGit\\TortoiseProc\\ResizableState\\RepobrowserDivider");
999 xPos = CDPIAware::Instance().UnscaleX(GetSafeHwnd(), rc.right - rc.left);
1002 void CRepositoryBrowser::HandleDividerMove(CPoint point, bool bDraw)
1004 RECT rect, tree, list, treelist, treelistclient;
1006 // create an union of the tree and list control rectangle
1007 GetDlgItem(IDC_REPOLIST)->GetWindowRect(&list);
1008 GetDlgItem(IDC_REPOTREE)->GetWindowRect(&tree);
1009 UnionRect(&treelist, &tree, &list);
1010 treelistclient = treelist;
1011 ScreenToClient(&treelistclient);
1013 ClientToScreen(&point);
1014 GetClientRect(&rect);
1015 ClientToScreen(&rect);
1017 auto minWidth = CDPIAware::Instance().ScaleX(GetSafeHwnd(), REPOBROWSER_CTRL_MIN_WIDTH);
1018 CPoint point2 = point;
1019 if (point2.x < treelist.left + minWidth)
1020 point2.x = treelist.left + minWidth;
1021 if (point2.x > treelist.right - minWidth)
1022 point2.x = treelist.right - minWidth;
1024 point.x -= rect.left;
1025 point.y -= treelist.top;
1027 OffsetRect(&treelist, -treelist.left, -treelist.top);
1029 if (point.x < treelist.left + minWidth)
1030 point.x = treelist.left + minWidth;
1031 if (point.x > treelist.right - minWidth)
1032 point.x = treelist.right - minWidth;
1034 const auto divWidth = CDPIAware::Instance().ScaleX(GetSafeHwnd(), 2);
1036 if (bDraw)
1038 CDC * pDC = GetDC();
1039 DrawXorBar(pDC, m_oldSliderXPos - divWidth, treelistclient.top, 2 * divWidth, treelistclient.bottom - treelistclient.top - CDPIAware::Instance().ScaleY(GetSafeHwnd(), 2));
1040 ReleaseDC(pDC);
1043 m_oldSliderXPos = point.x;
1045 //position the child controls
1046 GetDlgItem(IDC_REPOTREE)->GetWindowRect(&treelist);
1047 treelist.right = point2.x - divWidth;
1048 ScreenToClient(&treelist);
1049 RemoveAnchor(IDC_REPOTREE);
1050 GetDlgItem(IDC_REPOTREE)->MoveWindow(&treelist);
1051 GetDlgItem(IDC_REPOLIST)->GetWindowRect(&treelist);
1052 treelist.left = point2.x + divWidth;
1053 ScreenToClient(&treelist);
1054 RemoveAnchor(IDC_REPOLIST);
1055 GetDlgItem(IDC_REPOLIST)->MoveWindow(&treelist);
1057 AddAnchor(IDC_REPOTREE, TOP_LEFT, BOTTOM_LEFT);
1058 AddAnchor(IDC_REPOLIST, TOP_LEFT, BOTTOM_RIGHT);
1061 void CRepositoryBrowser::OnMouseMove(UINT nFlags, CPoint point)
1063 if (bDragMode == FALSE)
1064 return;
1066 RECT rect, tree, list, treelist, treelistclient;
1067 // create an union of the tree and list control rectangle
1068 GetDlgItem(IDC_REPOLIST)->GetWindowRect(&list);
1069 GetDlgItem(IDC_REPOTREE)->GetWindowRect(&tree);
1070 UnionRect(&treelist, &tree, &list);
1071 treelistclient = treelist;
1072 ScreenToClient(&treelistclient);
1074 //convert the mouse coordinates relative to the top-left of
1075 //the window
1076 ClientToScreen(&point);
1077 GetClientRect(&rect);
1078 ClientToScreen(&rect);
1079 point.x -= rect.left;
1080 point.y -= treelist.top;
1082 //same for the window coordinates - make them relative to 0,0
1083 OffsetRect(&treelist, -treelist.left, -treelist.top);
1085 auto minWidth = CDPIAware::Instance().ScaleX(GetSafeHwnd(), REPOBROWSER_CTRL_MIN_WIDTH);
1086 if (point.x < treelist.left + minWidth)
1087 point.x = treelist.left + minWidth;
1088 if (point.x > treelist.right - minWidth)
1089 point.x = treelist.right - minWidth;
1091 if ((nFlags & MK_LBUTTON) && (point.x != m_oldSliderXPos))
1093 CDC * pDC = GetDC();
1095 if (pDC)
1097 const auto divWidth = CDPIAware::Instance().ScaleX(GetSafeHwnd(), 2);
1098 const auto divHeight = CDPIAware::Instance().ScaleY(GetSafeHwnd(), 2);
1099 DrawXorBar(pDC, m_oldSliderXPos - divWidth, treelistclient.top, 2 * divWidth, treelistclient.bottom - treelistclient.top - divHeight);
1100 DrawXorBar(pDC, point.x - divWidth, treelistclient.top, 2 * divWidth, treelistclient.bottom - treelistclient.top - divHeight);
1102 ReleaseDC(pDC);
1105 m_oldSliderXPos = point.x;
1108 CStandAloneDialogTmpl<CResizableDialog>::OnMouseMove(nFlags, point);
1111 void CRepositoryBrowser::OnLButtonDown(UINT nFlags, CPoint point)
1113 RECT rect, tree, list, treelist, treelistclient;
1115 // create an union of the tree and list control rectangle
1116 GetDlgItem(IDC_REPOLIST)->GetWindowRect(&list);
1117 GetDlgItem(IDC_REPOTREE)->GetWindowRect(&tree);
1118 UnionRect(&treelist, &tree, &list);
1119 treelistclient = treelist;
1120 ScreenToClient(&treelistclient);
1122 //convert the mouse coordinates relative to the top-left of
1123 //the window
1124 ClientToScreen(&point);
1125 GetClientRect(&rect);
1126 ClientToScreen(&rect);
1127 point.x -= rect.left;
1128 point.y -= treelist.top;
1130 //same for the window coordinates - make them relative to 0,0
1131 OffsetRect(&treelist, -treelist.left, -treelist.top);
1133 const auto minWidth = CDPIAware::Instance().ScaleX(GetSafeHwnd(), REPOBROWSER_CTRL_MIN_WIDTH);
1135 if (point.x < treelist.left + minWidth)
1136 return CStandAloneDialogTmpl < CResizableDialog>::OnLButtonDown(nFlags, point);
1137 if (point.x > treelist.right - CDPIAware::Instance().ScaleX(GetSafeHwnd(), 3))
1138 return CStandAloneDialogTmpl < CResizableDialog>::OnLButtonDown(nFlags, point);
1139 if (point.x > treelist.right - minWidth)
1140 point.x = treelist.right - minWidth;
1142 const auto divHeight = CDPIAware::Instance().ScaleY(GetSafeHwnd(), 3);
1143 if ((point.y < treelist.top + divHeight) || (point.y > treelist.bottom - divHeight))
1144 return CStandAloneDialogTmpl<CResizableDialog>::OnLButtonDown(nFlags, point);
1146 bDragMode = true;
1148 SetCapture();
1150 const auto divWidth = CDPIAware::Instance().ScaleX(GetSafeHwnd(), 2);
1151 CDC * pDC = GetDC();
1152 DrawXorBar(pDC, point.x - divWidth, treelistclient.top, 2 * divWidth, treelistclient.bottom - treelistclient.top - CDPIAware::Instance().ScaleY(GetSafeHwnd(), 2));
1153 ReleaseDC(pDC);
1155 m_oldSliderXPos = point.x;
1157 CStandAloneDialogTmpl<CResizableDialog>::OnLButtonDown(nFlags, point);
1160 void CRepositoryBrowser::OnLButtonUp(UINT nFlags, CPoint point)
1162 if (bDragMode == FALSE)
1163 return;
1165 HandleDividerMove(point, true);
1167 bDragMode = false;
1168 ReleaseCapture();
1170 CStandAloneDialogTmpl<CResizableDialog>::OnLButtonUp(nFlags, point);
1173 void CRepositoryBrowser::OnCaptureChanged(CWnd *pWnd)
1175 bDragMode = false;
1177 __super::OnCaptureChanged(pWnd);
1180 void CRepositoryBrowser::DrawXorBar(CDC * pDC, int x1, int y1, int width, int height)
1182 static WORD _dotPatternBmp[8] =
1184 0x0055, 0x00aa, 0x0055, 0x00aa,
1185 0x0055, 0x00aa, 0x0055, 0x00aa
1188 HBITMAP hbm;
1189 HBRUSH hbr, hbrushOld;
1191 hbm = CreateBitmap(8, 8, 1, 1, _dotPatternBmp);
1192 hbr = CreatePatternBrush(hbm);
1194 pDC->SetBrushOrg(x1, y1);
1195 hbrushOld = static_cast<HBRUSH>(pDC->SelectObject(hbr));
1197 PatBlt(pDC->GetSafeHdc(), x1, y1, width, height, PATINVERT);
1199 pDC->SelectObject(hbrushOld);
1201 DeleteObject(hbr);
1202 DeleteObject(hbm);
1205 BOOL CRepositoryBrowser::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
1207 if (pWnd == this)
1209 RECT rect;
1210 POINT pt;
1211 GetClientRect(&rect);
1212 GetCursorPos(&pt);
1213 ScreenToClient(&pt);
1214 if (PtInRect(&rect, pt))
1216 ClientToScreen(&pt);
1217 // are we right of the tree control?
1218 GetDlgItem(IDC_REPOTREE)->GetWindowRect(&rect);
1219 const auto divHeight = CDPIAware::Instance().ScaleY(GetSafeHwnd(), 3);
1220 if ((pt.x > rect.right) && (pt.y >= rect.top + divHeight) && (pt.y <= rect.bottom - divHeight))
1222 // but left of the list control?
1223 GetDlgItem(IDC_REPOLIST)->GetWindowRect(&rect);
1224 if (pt.x < rect.left)
1226 HCURSOR hCur = LoadCursor(nullptr, IDC_SIZEWE);
1227 SetCursor(hCur);
1228 return TRUE;
1233 return CStandAloneDialogTmpl<CResizableDialog>::OnSetCursor(pWnd, nHitTest, message);
1236 void CRepositoryBrowser::FileSaveAs(const CString path)
1238 CTGitPath gitPath(path);
1240 CGitHash hash;
1241 if (g_Git.GetHash(hash, m_sRevision + L"^{}")) // add ^{} in order to dereference signed tags
1243 MessageBox(g_Git.GetGitLastErr(L"Could not get hash of \"" + m_sRevision + L"^{}\"."), L"TortoiseGit", MB_ICONERROR);
1244 return;
1247 CString filename;
1248 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()));
1249 if (!CAppUtils::FileOpenSave(filename, nullptr, 0, 0, false, GetSafeHwnd()))
1250 return;
1252 if (g_Git.GetOneFile(m_sRevision, gitPath, filename))
1254 CString out;
1255 out.FormatMessage(IDS_STATUSLIST_CHECKOUTFILEFAILED, static_cast<LPCWSTR>(gitPath.GetGitPathString()), static_cast<LPCWSTR>(m_sRevision), static_cast<LPCWSTR>(filename));
1256 MessageBox(g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", MB_ICONERROR);
1260 void CRepositoryBrowser::OpenFile(const CString path, eOpenType mode, bool isSubmodule, const CGitHash& itemHash)
1262 CTGitPath gitPath(path);
1264 CGitHash hash;
1265 if (g_Git.GetHash(hash, m_sRevision + L"^{}")) // add ^{} in order to dereference signed tags
1267 MessageBox(g_Git.GetGitLastErr(L"Could not get hash of \"" + m_sRevision + L"^{}\"."), L"TortoiseGit", MB_ICONERROR);
1268 return;
1271 CString file = CTempFiles::Instance().GetTempFilePath(false, gitPath, hash).GetWinPathString();
1272 if (isSubmodule)
1274 if (mode == OPEN && !GitAdminDir::IsBareRepo(g_Git.m_CurrentDir))
1276 CTGitPath subPath = CTGitPath(g_Git.m_CurrentDir);
1277 subPath.AppendPathString(gitPath.GetWinPathString());
1278 CAutoRepository repo(subPath.GetGitPathString());
1279 CAutoCommit commit;
1280 if (!repo || git_commit_lookup(commit.GetPointer(), repo, itemHash))
1282 CString out;
1283 out.FormatMessage(IDS_REPOBROWSEASKSUBMODULEUPDATE, static_cast<LPCWSTR>(itemHash.ToString()), static_cast<LPCWSTR>(gitPath.GetGitPathString()));
1284 if (MessageBox(out, L"TortoiseGit", MB_YESNO | MB_ICONQUESTION) != IDYES)
1285 return;
1287 CString sCmd;
1288 sCmd.Format(L"/command:subupdate /bkpath:\"%s\" /selectedpath:\"%s\"", static_cast<LPCWSTR>(g_Git.m_CurrentDir), static_cast<LPCWSTR>(gitPath.GetGitPathString()));
1289 CAppUtils::RunTortoiseGitProc(sCmd);
1290 return;
1293 CString cmd;
1294 cmd.Format(L"/command:repobrowser /path:\"%s\" /rev:%s", static_cast<LPCWSTR>(g_Git.CombinePath(path)), static_cast<LPCWSTR>(itemHash.ToString()));
1295 CAppUtils::RunTortoiseGitProc(cmd);
1296 return;
1299 file += L".txt";
1300 CFile submoduleCommit(file, CFile::modeCreate | CFile::modeWrite);
1301 CStringA commitInfo = "Subproject commit " + CStringA(itemHash.ToString());
1302 submoduleCommit.Write(commitInfo, commitInfo.GetLength());
1304 else if (g_Git.GetOneFile(m_sRevision, gitPath, file))
1306 CString out;
1307 out.FormatMessage(IDS_STATUSLIST_CHECKOUTFILEFAILED, static_cast<LPCWSTR>(gitPath.GetGitPathString()), static_cast<LPCWSTR>(m_sRevision), static_cast<LPCWSTR>(file));
1308 MessageBox(g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", MB_ICONERROR);
1309 return;
1312 if (mode == ALTERNATIVEEDITOR)
1314 CAppUtils::LaunchAlternativeEditor(file);
1315 return;
1317 else if (mode == OPEN)
1319 CAppUtils::ShellOpen(file);
1320 return;
1323 CAppUtils::ShowOpenWithDialog(file);
1325 bool CRepositoryBrowser::RevertItemToVersion(const CString &path)
1327 CString endOfOptions;
1328 if (CGit::ms_LastMsysGitVersion >= ConvertVersionToInt(2, 43, 1))
1329 endOfOptions = L" --end-of-options";
1330 CString cmd, out;
1331 cmd.Format(L"git.exe checkout%s %s -- \"%s\"", static_cast<LPCWSTR>(endOfOptions), static_cast<LPCWSTR>(m_sRevision), static_cast<LPCWSTR>(path));
1332 if (g_Git.Run(cmd, &out, CP_UTF8))
1334 if (MessageBox(out, L"TortoiseGit", MB_ICONEXCLAMATION | MB_OKCANCEL) == IDCANCEL)
1335 return false;
1338 return true;
1341 void CRepositoryBrowser::CopyHashToClipboard(TShadowFilesTreeList &selectedLeafs)
1343 if (!selectedLeafs.empty())
1345 CString sClipdata;
1346 bool first = true;
1347 for (size_t i = 0; i < selectedLeafs.size(); ++i)
1349 if (!first)
1350 sClipdata += L"\r\n";
1351 sClipdata += selectedLeafs[i]->m_hash.ToString();
1352 first = false;
1354 CStringUtils::WriteAsciiStringToClipboard(sClipdata, GetSafeHwnd());
1358 void CRepositoryBrowser::OnLvnBegindragRepolist(NMHDR* pNMHDR, LRESULT* pResult)
1360 *pResult = 0;
1362 // get selected paths
1363 POSITION pos = m_RepoList.GetFirstSelectedItemPosition();
1364 if (!pos)
1365 return;
1367 HTREEITEM hTreeItem = m_RepoTree.GetSelectedItem();
1368 if (!hTreeItem)
1370 ASSERT(FALSE);
1371 return;
1373 auto pTree = GetTreeEntry(hTreeItem);
1374 if (!pTree)
1375 return;
1377 CTGitPathList toExport;
1378 int index = -1;
1379 while ((index = m_RepoList.GetNextSelectedItem(pos)) >= 0)
1381 auto item = GetListEntry(index);
1382 if (item->m_bFolder)
1384 RecursivelyAdd(toExport, item);
1385 continue;
1388 CTGitPath path;
1389 path.SetFromGit(item->GetFullName(), item->m_bSubmodule);
1390 toExport.AddPath(path);
1393 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
1394 BeginDrag(m_RepoList, toExport, pTree->GetFullName(), pNMLV->ptAction);
1397 void CRepositoryBrowser::RecursivelyAdd(CTGitPathList& toExport, CShadowFilesTree* pTree)
1399 if (!pTree->m_bLoaded)
1400 ReadTree(pTree, pTree->GetFullName(), true);
1402 for (auto itShadowTree = pTree->m_ShadowTree.begin(); itShadowTree != pTree->m_ShadowTree.end(); ++itShadowTree)
1404 if ((*itShadowTree).second.m_bFolder)
1406 RecursivelyAdd(toExport, &(*itShadowTree).second);
1407 continue;
1409 CTGitPath path;
1410 path.SetFromGit((*itShadowTree).second.GetFullName(), (*itShadowTree).second.m_bSubmodule);
1411 toExport.AddPath(path);
1415 void CRepositoryBrowser::OnTvnBegindragRepotree(NMHDR* pNMHDR, LRESULT* pResult)
1417 *pResult = 0;
1419 LPNMTREEVIEW pNMTreeView = reinterpret_cast<LPNMTREEVIEW>(pNMHDR);
1421 auto pTree = GetTreeEntry(pNMTreeView->itemNew.hItem);
1422 if (!pTree)
1423 return;
1425 CTGitPathList toExport;
1426 RecursivelyAdd(toExport, pTree);
1428 BeginDrag(m_RepoTree, toExport, pTree->m_pParent ? pTree->m_pParent->GetFullName() : CString(), pNMTreeView->ptDrag);
1431 void CRepositoryBrowser::BeginDrag(const CWnd& window, CTGitPathList& files, const CString& root, POINT& point)
1433 CGitHash hash;
1434 if (g_Git.GetHash(hash, m_sRevision + L"^{}")) // add ^{} in order to dereference signed tags
1436 MessageBox(g_Git.GetGitLastErr(L"Could not get hash of \"" + m_sRevision + L"^{}\"."), L"TortoiseGit", MB_ICONERROR);
1437 return;
1440 // build copy source / content
1441 auto pdsrc = std::make_unique<CIDropSource>();
1442 if (!pdsrc)
1443 return;
1445 pdsrc->AddRef();
1447 GitDataObject* pdobj = new GitDataObject(files, hash, root.GetLength());
1448 if (!pdobj)
1449 return;
1450 pdobj->AddRef();
1451 pdobj->SetAsyncMode(TRUE);
1452 CDragSourceHelper dragsrchelper;
1453 dragsrchelper.InitializeFromWindow(window.GetSafeHwnd(), point, pdobj);
1454 pdsrc->m_pIDataObj = pdobj;
1455 pdsrc->m_pIDataObj->AddRef();
1457 // Initiate the Drag & Drop
1458 DWORD dwEffect;
1459 ::DoDragDrop(pdobj, pdsrc.get(), DROPEFFECT_MOVE | DROPEFFECT_COPY, &dwEffect);
1460 pdsrc->Release();
1461 pdsrc.release();
1462 pdobj->Release();
1466 CShadowFilesTree* CRepositoryBrowser::GetListEntry(int index)
1468 auto entry = reinterpret_cast<CShadowFilesTree*>(m_RepoList.GetItemData(index));
1469 ASSERT(entry);
1470 return entry;
1473 CShadowFilesTree* CRepositoryBrowser::GetTreeEntry(HTREEITEM treeItem)
1475 auto entry = reinterpret_cast<CShadowFilesTree*>(m_RepoTree.GetItemData(treeItem));
1476 ASSERT(entry);
1477 return entry;