Fixed issue #4126: Capitalize the first letter in the Push dialog
[TortoiseGit.git] / src / TortoiseProc / GitProgressList.cpp
blob8a421e9f63e42999692a81d7aa439970b296fcb8
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2008-2024 - TortoiseGit
4 // Copyright (C) 2003-2008 - 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 #include "stdafx.h"
21 #include "GitProgressList.h"
22 #include "TortoiseProc.h"
23 #include "Git.h"
24 #include "registry.h"
25 #include "AppUtils.h"
26 #include "StringUtils.h"
27 #include "LogFile.h"
28 #include "LoglistUtils.h"
29 #include "Theme.h"
30 #include "TempFile.h"
31 #include "git2/sys/errors.h"
33 BOOL CGitProgressList::m_bAscending = FALSE;
34 int CGitProgressList::m_nSortedColumn = -1;
36 #define TRANSFERTIMER 100
37 #define VISIBLETIMER 101
38 // CGitProgressList
40 IMPLEMENT_DYNAMIC(CGitProgressList, CListCtrl)
42 CGitProgressList::CGitProgressList():CListCtrl()
44 m_columnbuf[0] = L'\0';
47 CGitProgressList::~CGitProgressList()
49 for (size_t i = 0; i < m_arData.size(); ++i)
50 delete m_arData[i];
51 delete m_pThread;
55 BEGIN_MESSAGE_MAP(CGitProgressList, CListCtrl)
56 ON_NOTIFY_REFLECT(NM_CUSTOMDRAW, OnNMCustomdrawSvnprogress)
57 ON_NOTIFY_REFLECT(NM_DBLCLK, OnNMDblclkSvnprogress)
58 ON_NOTIFY_REFLECT(HDN_ITEMCLICK, OnHdnItemclickSvnprogress)
59 ON_NOTIFY_REFLECT(LVN_BEGINDRAG, OnLvnBegindragSvnprogress)
60 ON_NOTIFY_REFLECT(LVN_GETDISPINFO, OnLvnGetdispinfoSvnprogress)
61 ON_MESSAGE(WM_SHOWCONFLICTRESOLVER, OnShowConflictResolver)
62 ON_WM_SIZE()
63 ON_WM_TIMER()
64 ON_WM_CONTEXTMENU()
65 ON_WM_CLOSE()
66 END_MESSAGE_MAP()
68 void CGitProgressList::Cancel()
70 m_bCancelled = TRUE;
75 // CGitProgressList message handlers
78 LRESULT CGitProgressList::OnShowConflictResolver(WPARAM /*wParam*/, LPARAM /*lParam*/)
80 #if 0
81 CConflictResolveDlg dlg(this);
82 const svn_wc_conflict_description_t *description = (svn_wc_conflict_description_t *)lParam;
83 if (description)
85 dlg.SetConflictDescription(description);
86 if (m_pTaskbarList)
88 m_pTaskbarList->SetProgressState(m_hWnd, TBPF_PAUSED);
90 if (dlg.DoModal() == IDOK)
92 if (dlg.GetResult() == svn_wc_conflict_choose_postpone)
94 // if the result is conflicted and the dialog returned IDOK,
95 // that means we should not ask again in case of a conflict
96 m_AlwaysConflicted = true;
97 ::SendMessage(GetDlgItem(IDC_NONINTERACTIVE)->GetSafeHwnd(), BM_SETCHECK, BST_CHECKED, 0);
100 m_mergedfile = dlg.GetMergedFile();
101 m_bCancelled = dlg.IsCancelled();
102 if (m_pTaskbarList)
103 m_pTaskbarList->SetProgressState(m_hWnd, TBPF_INDETERMINATE);
104 return dlg.GetResult();
107 return svn_wc_conflict_choose_postpone;
108 #endif
109 return 0;
111 #if 0
112 svn_wc_conflict_choice_t CGitProgressList::ConflictResolveCallback(const svn_wc_conflict_description_t *description, CString& mergedfile)
114 // we only bother the user when merging
115 if (((m_Command == GitProgress_Merge)||(m_Command == GitProgress_MergeAll)||(m_Command == GitProgress_MergeReintegrate))&&(!m_AlwaysConflicted)&&(description))
117 // we're in a worker thread here. That means we must not show a dialog from the thread
118 // but let the UI thread do it.
119 // To do that, we send a message to the UI thread and let it show the conflict resolver dialog.
120 LRESULT dlgResult = ::SendMessage(GetSafeHwnd(), WM_SHOWCONFLICTRESOLVER, 0, reinterpret_cast<LPARAM>(description));
121 mergedfile = m_mergedfile;
122 return (svn_wc_conflict_choice_t)dlgResult;
125 return svn_wc_conflict_choose_postpone;
127 #endif
128 void CGitProgressList::AddItemToList()
130 const int totalcount = GetItemCount();
132 SetItemCountEx(totalcount+1, LVSICF_NOSCROLL|LVSICF_NOINVALIDATEALL);
133 // make columns width fit
134 if (iFirstResized < 30)
136 // only resize the columns for the first 30 or so entries.
137 // after that, don't resize them anymore because that's an
138 // expensive function call and the columns will be sized
139 // close enough already.
140 ResizeColumns();
141 ++iFirstResized;
144 // Make sure the item is *entirely* visible even if the horizontal
145 // scroll bar is visible.
146 const int count = GetCountPerPage();
147 if (totalcount <= (GetTopIndex() + count + nEnsureVisibleCount + 2))
149 ++nEnsureVisibleCount;
150 m_bLastVisible = true;
152 else
154 nEnsureVisibleCount = 0;
155 if (IsIconic() == 0)
156 m_bLastVisible = false;
160 CString CGitProgressList::BuildInfoString()
162 CString infotext;
163 m_Command->ShowInfo(infotext);
165 #if 0
167 int added = 0;
168 int copied = 0;
169 int deleted = 0;
170 int restored = 0;
171 int reverted = 0;
172 int resolved = 0;
173 int conflicted = 0;
174 int updated = 0;
175 int merged = 0;
176 int modified = 0;
177 int skipped = 0;
178 int replaced = 0;
180 for (size_t i=0; i<m_arData.size(); ++i)
182 const NotificationData * dat = m_arData[i];
183 switch (dat->action)
185 case svn_wc_notify_add:
186 case svn_wc_notify_update_add:
187 case svn_wc_notify_commit_added:
188 if (dat->bConflictedActionItem)
189 ++conflicted;
190 else
191 ++added;
192 break;
193 case svn_wc_notify_copy:
194 ++copied;
195 break;
196 case svn_wc_notify_delete:
197 case svn_wc_notify_update_delete:
198 case svn_wc_notify_commit_deleted:
199 ++deleted;
200 break;
201 case svn_wc_notify_restore:
202 ++restored;
203 break;
204 case svn_wc_notify_revert:
205 ++reverted;
206 break;
207 case svn_wc_notify_resolved:
208 ++resolved;
209 break;
210 case svn_wc_notify_update_update:
211 if (dat->bConflictedActionItem)
212 ++conflicted;
213 else if ((dat->content_state == svn_wc_notify_state_merged) || (dat->prop_state == svn_wc_notify_state_merged))
214 ++merged;
215 else
216 ++updated;
217 break;
218 case svn_wc_notify_commit_modified:
219 ++modified;
220 break;
221 case svn_wc_notify_skip:
222 ++skipped;
223 break;
224 case svn_wc_notify_commit_replaced:
225 ++replaced;
226 break;
229 if (conflicted)
231 temp.LoadString(IDS_SVNACTION_CONFLICTED);
232 infotext += temp;
233 temp.Format(L":%d ", conflicted);
234 infotext += temp;
236 if (skipped)
238 temp.LoadString(IDS_SVNACTION_SKIP);
239 infotext += temp;
240 infotext.AppendFormat(L":%d ", skipped);
242 if (merged)
244 temp.LoadString(IDS_SVNACTION_MERGED);
245 infotext += temp;
246 infotext.AppendFormat(L":%d ", merged);
248 if (added)
250 temp.LoadString(IDS_SVNACTION_ADD);
251 infotext += temp;
252 infotext.AppendFormat(L":%d ", added);
254 if (deleted)
256 temp.LoadString(IDS_SVNACTION_DELETE);
257 infotext += temp;
258 infotext.AppendFormat(L":%d ", deleted);
260 if (modified)
262 temp.LoadString(IDS_SVNACTION_MODIFIED);
263 infotext += temp;
264 infotext.AppendFormat(L":%d ", modified);
266 if (copied)
268 temp.LoadString(IDS_SVNACTION_COPY);
269 infotext += temp;
270 infotext.AppendFormat(L":%d ", copied);
272 if (replaced)
274 temp.LoadString(IDS_SVNACTION_REPLACED);
275 infotext += temp;
276 infotext.AppendFormat(L":%d ", replaced);
278 if (updated)
280 temp.LoadString(IDS_SVNACTION_UPDATE);
281 infotext += temp;
282 infotext.AppendFormat(L":%d ", updated);
284 if (restored)
286 temp.LoadString(IDS_SVNACTION_RESTORE);
287 infotext += temp;
288 infotext.AppendFormat(L":%d ", restored);
290 if (reverted)
292 temp.LoadString(IDS_SVNACTION_REVERT);
293 infotext += temp;
294 infotext.AppendFormat(L":%d ", reverted);
296 if (resolved)
298 temp.LoadString(IDS_SVNACTION_RESOLVE);
299 infotext += temp;
300 infotext.AppendFormat(L":%d ", resolved);
302 #endif
303 return infotext;
306 void CGitProgressList::ResizeColumns()
308 SetRedraw(FALSE);
310 wchar_t textbuf[MAX_PATH] = { 0 };
312 auto pHeaderCtrl = GetHeaderCtrl();
313 if (pHeaderCtrl)
315 const int maxcol = pHeaderCtrl->GetItemCount()-1;
316 for (int col = 0; col <= maxcol; ++col)
318 // find the longest width of all items
319 const int count = min(static_cast<int>(m_arData.size()), GetItemCount());
320 HDITEM hdi = {0};
321 hdi.mask = HDI_TEXT;
322 hdi.pszText = textbuf;
323 hdi.cchTextMax = _countof(textbuf);
324 pHeaderCtrl->GetItem(col, &hdi);
325 int cx = GetStringWidth(hdi.pszText) + CDPIAware::Instance().ScaleX(GetSafeHwnd(), 20); // 20 pixels for col separator and margin
327 for (int index = 0; index<count; ++index)
329 // get the width of the string and add 12 pixels for the column separator and margins
330 int linewidth = cx;
331 switch (col)
333 case 0:
334 linewidth = GetStringWidth(m_arData[index]->sActionColumnText) + CDPIAware::Instance().ScaleX(GetSafeHwnd(), 12);
335 break;
336 case 1:
337 linewidth = GetStringWidth(m_arData[index]->sPathColumnText) + CDPIAware::Instance().ScaleX(GetSafeHwnd(), 12);
338 break;
340 if (cx < linewidth)
341 cx = linewidth;
343 SetColumnWidth(col, cx);
347 SetRedraw(TRUE);
350 bool CGitProgressList::SetBackgroundImage(UINT nID)
352 return CAppUtils::SetListCtrlBackgroundImage(GetSafeHwnd(), nID);
355 void CGitProgressList::ReportGitError()
357 ReportError(CGit::GetLibGit2LastErr());
360 void CGitProgressList::ReportUserCanceled()
362 ReportError(CString(MAKEINTRESOURCE(IDS_USERCANCELLED)));
365 void CGitProgressList::ReportError(const CString& sError)
367 if (CRegDWORD(L"Software\\TortoiseGit\\NoSounds", FALSE) == FALSE)
368 PlaySound(reinterpret_cast<LPCWSTR>(SND_ALIAS_SYSTEMEXCLAMATION), nullptr, SND_ALIAS_ID | SND_ASYNC);
369 ReportString(sError, CString(MAKEINTRESOURCE(IDS_ERR_ERROR)), true, m_Colors.GetColor(CColors::Conflict));
370 m_bErrorsOccurred = true;
373 void CGitProgressList::ReportWarning(const CString& sWarning)
375 if (CRegDWORD(L"Software\\TortoiseGit\\NoSounds", FALSE) == FALSE)
376 PlaySound(reinterpret_cast<LPCWSTR>(SND_ALIAS_SYSTEMDEFAULT), nullptr, SND_ALIAS_ID | SND_ASYNC);
377 ReportString(sWarning, CString(MAKEINTRESOURCE(IDS_WARN_WARNING)), true, m_Colors.GetColor(CColors::Conflict));
380 void CGitProgressList::ReportNotification(const CString& sNotification)
382 if (CRegDWORD(L"Software\\TortoiseGit\\NoSounds", FALSE) == FALSE)
383 PlaySound(reinterpret_cast<LPCWSTR>(SND_ALIAS_SYSTEMDEFAULT), nullptr, SND_ALIAS_ID | SND_ASYNC);
384 ReportString(sNotification, CString(MAKEINTRESOURCE(IDS_WARN_NOTE)), false);
387 void CGitProgressList::ReportCmd(const CString& sCmd)
389 ReportString(sCmd, CString(MAKEINTRESOURCE(IDS_PROGRS_CMDINFO)), true, m_Colors.GetColor(CColors::Cmd));
392 void CGitProgressList::ReportString(CString sMessage, const CString& sMsgKind, bool colorIsDirect, COLORREF color)
394 // instead of showing a dialog box with the error message or notification,
395 // just insert the error text into the list control.
396 // that way the user isn't 'interrupted' by a dialog box popping up!
398 // the message may be split up into different lines
399 // so add a new entry for each line of the message
400 while (!sMessage.IsEmpty())
402 NotificationData * data = new NotificationData();
403 data->bAuxItem = true;
404 data->sActionColumnText = sMsgKind;
405 if (sMessage.Find('\n')>=0)
406 data->sPathColumnText = sMessage.Left(sMessage.Find('\n'));
407 else
408 data->sPathColumnText = sMessage;
409 data->sPathColumnText.Trim(L"\n\r");
410 data->color = color;
411 data->colorIsDirect = colorIsDirect;
412 if (sMessage.Find('\n')>=0)
414 sMessage = sMessage.Mid(sMessage.Find('\n'));
415 sMessage.Trim(L"\n\r");
417 else
418 sMessage.Empty();
419 AddNotify(data);
423 UINT CGitProgressList::ProgressThreadEntry(LPVOID pVoid)
425 return static_cast<CGitProgressList*>(pVoid)->ProgressThread();
428 UINT CGitProgressList::ProgressThread()
430 // The SetParams function should have loaded something for us
432 CString temp;
433 CString sWindowTitle;
434 bool bSuccess = false;
436 if(m_pPostWnd)
437 m_pPostWnd->PostMessage(WM_PROG_CMD_START, reinterpret_cast<WPARAM>(m_Command));
439 if(m_pProgressLabelCtrl)
441 m_pProgressLabelCtrl->ShowWindow(SW_SHOW);
442 m_pProgressLabelCtrl->SetWindowText(L"");
445 // SetAndClearProgressInfo(m_hWnd);
446 m_itemCount = m_itemCountTotal;
448 InterlockedExchange(&m_bThreadRunning, TRUE);
449 iFirstResized = 0;
450 bSecondResized = FALSE;
451 m_bFinishedItemAdded = false;
452 auto startTime = GetTickCount64();
454 if (m_pTaskbarList && m_pPostWnd)
455 m_pTaskbarList->SetProgressState(m_pPostWnd->GetSafeHwnd(), TBPF_INDETERMINATE);
457 m_TotalBytesTransferred = 0;
458 if (m_Command)
459 bSuccess = m_Command->Run(this, sWindowTitle, m_itemCountTotal, m_itemCount);
460 else
461 bSuccess = false;
463 if (!bSuccess)
464 temp.LoadString(IDS_PROGRS_TITLEFAILED);
465 else
466 temp.LoadString(IDS_PROGRS_TITLEFIN);
467 sWindowTitle = sWindowTitle + L' ' + temp;
468 if (m_bSetTitle && m_pPostWnd)
469 ::SetWindowText(m_pPostWnd->GetSafeHwnd(), sWindowTitle);
471 KillTimer(TRANSFERTIMER);
472 KillTimer(VISIBLETIMER);
474 if (m_pTaskbarList && m_pPostWnd)
476 if (DidErrorsOccur())
478 m_pTaskbarList->SetProgressState(m_pPostWnd->GetSafeHwnd(), TBPF_ERROR);
479 m_pTaskbarList->SetProgressValue(m_pPostWnd->GetSafeHwnd(), 100, 100);
481 else
482 m_pTaskbarList->SetProgressState(m_pPostWnd->GetSafeHwnd(), TBPF_NOPROGRESS);
485 if (m_pInfoCtrl)
487 CString info;
488 if (!bSuccess)
489 info.LoadString(IDS_PROGRS_INFOFAILED);
490 else // this implies that command is not nullptr
491 info = BuildInfoString();
492 m_pInfoCtrl->SetWindowText(info);
495 ResizeColumns();
497 auto time = GetTickCount64() - startTime;
499 CString sFinalInfo;
500 if (!m_sTotalBytesTransferred.IsEmpty())
502 temp.FormatMessage(IDS_PROGRS_TIME, static_cast<DWORD>(time / 1000) / 60, static_cast<DWORD>(time / 1000) % 60);
503 sFinalInfo.FormatMessage(IDS_PROGRS_FINALINFO, static_cast<LPCWSTR>(m_sTotalBytesTransferred), static_cast<LPCWSTR>(temp));
504 if (m_pProgressLabelCtrl)
505 m_pProgressLabelCtrl->SetWindowText(sFinalInfo);
507 else
509 if (m_pProgressLabelCtrl)
510 m_pProgressLabelCtrl->ShowWindow(SW_HIDE);
513 if (m_pProgControl)
514 m_pProgControl->ShowWindow(SW_HIDE);
516 if (!m_bFinishedItemAdded)
518 CString log, str;
519 COLORREF color;
520 if (bSuccess)
522 str.LoadString(IDS_SUCCESS);
523 if (CTheme::Instance().IsHighContrastMode())
524 color = ::GetSysColor(COLOR_WINDOWTEXT);
525 else
526 color = CTheme::Instance().IsDarkTheme() ? RGB(0, 178, 255) : RGB(0, 0, 255);
528 else
530 str.LoadString(IDS_FAIL);
531 color = CTheme::Instance().IsDarkTheme() ? RGB(207, 47, 47) : RGB(255, 0, 0);
533 log.Format(L"%s (%I64u ms @ %s)", static_cast<LPCWSTR>(str), time, static_cast<LPCWSTR>(CLoglistUtils::FormatDateAndTime(CTime::GetCurrentTime(), DATE_SHORTDATE, true, false)));
535 // there's no "finished: xxx" line at the end. We add one here to make
536 // sure the user sees that the command is actually finished.
537 ReportString(log, CString(MAKEINTRESOURCE(IDS_PROGRS_FINISHED)), !(CTheme::Instance().IsHighContrastMode() && bSuccess), color);
540 const int count = GetItemCount();
541 if ((count > 0)&&(m_bLastVisible))
542 EnsureVisible(count-1, FALSE);
544 CLogFile logfile(g_Git.m_CurrentDir);
545 if (logfile.Open())
547 logfile.AddTimeLine();
548 for (size_t i = 0; i < m_arData.size(); ++i)
550 NotificationData * data = m_arData[i];
551 temp.Format(L"%-20s : %s", static_cast<LPCWSTR>(data->sActionColumnText), static_cast<LPCWSTR>(data->sPathColumnText));
552 logfile.AddLine(temp);
554 if (!sFinalInfo.IsEmpty())
555 logfile.AddLine(sFinalInfo);
556 logfile.Close();
559 m_bCancelled = TRUE;
560 InterlockedExchange(&m_bThreadRunning, FALSE);
562 if (m_pPostWnd)
563 m_pPostWnd->PostMessage(WM_PROG_CMD_FINISH, reinterpret_cast<WPARAM>(m_Command), 0L);
565 //Don't do anything here which might cause messages to be sent to the window
566 //The window thread is probably now blocked in OnOK if we've done an auto close
567 return 0;
570 void CGitProgressList::OnLvnGetdispinfoSvnprogress(NMHDR *pNMHDR, LRESULT *pResult)
572 NMLVDISPINFO *pDispInfo = reinterpret_cast<NMLVDISPINFO*>(pNMHDR);
574 if (pDispInfo)
576 if (pDispInfo->item.mask & LVIF_TEXT)
578 if (pDispInfo->item.iItem < static_cast<int>(m_arData.size()))
580 const NotificationData * data = m_arData[pDispInfo->item.iItem];
581 switch (pDispInfo->item.iSubItem)
583 case 0:
584 lstrcpyn(m_columnbuf, data->sActionColumnText, MAX_PATH);
585 break;
586 case 1:
587 lstrcpyn(m_columnbuf, data->sPathColumnText, pDispInfo->item.cchTextMax - 1);
588 if (!data->bAuxItem)
590 int cWidth = GetColumnWidth(1);
591 cWidth = max(12, cWidth-12);
592 CDC * pDC = GetDC();
593 if (pDC)
595 CFont * pFont = pDC->SelectObject(GetFont());
596 PathCompactPath(pDC->GetSafeHdc(), m_columnbuf, cWidth);
597 CPathUtils::ConvertToSlash(m_columnbuf);
598 pDC->SelectObject(pFont);
599 ReleaseDC(pDC);
602 break;
603 default:
604 m_columnbuf[0] = L'\0';
606 pDispInfo->item.pszText = m_columnbuf;
610 *pResult = 0;
613 void CGitProgressList::OnNMCustomdrawSvnprogress(NMHDR *pNMHDR, LRESULT *pResult)
615 NMLVCUSTOMDRAW* pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>( pNMHDR );
617 // Take the default processing unless we set this to something else below.
618 *pResult = CDRF_DODEFAULT;
620 // First thing - check the draw stage. If it's the control's prepaint
621 // stage, then tell Windows we want messages for every item.
623 if ( CDDS_PREPAINT == pLVCD->nmcd.dwDrawStage )
625 *pResult = CDRF_NOTIFYITEMDRAW;
627 else if ( CDDS_ITEMPREPAINT == pLVCD->nmcd.dwDrawStage )
629 // This is the prepaint stage for an item. Here's where we set the
630 // item's text color. Our return value will tell Windows to draw the
631 // item itself, but it will use the new color we set here.
633 // Tell Windows to paint the control itself.
634 *pResult = CDRF_DODEFAULT;
636 ASSERT(pLVCD->nmcd.dwItemSpec < m_arData.size());
637 if(pLVCD->nmcd.dwItemSpec >= m_arData.size())
639 return;
641 const NotificationData * data = m_arData[pLVCD->nmcd.dwItemSpec];
642 ASSERT(data);
643 if (!data)
644 return;
646 // Store the color back in the NMLVCUSTOMDRAW struct.
647 pLVCD->clrText = CTheme::Instance().GetThemeColor(data->color, data->colorIsDirect);
651 void CGitProgressList::OnNMDblclkSvnprogress(NMHDR* pNMHDR, LRESULT* pResult)
653 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
654 *pResult = 0;
655 if (pNMLV->iItem < 0)
656 return;
657 if (m_options & ProgOptDryRun || m_bThreadRunning)
658 return; //don't do anything in a dry-run.
660 const NotificationData* data = m_arData[pNMLV->iItem];
661 if (!data)
662 return;
664 data->HandleDblClick();
667 void CGitProgressList::OnHdnItemclickSvnprogress(NMHDR *pNMHDR, LRESULT *pResult)
669 LPNMHEADER phdr = reinterpret_cast<LPNMHEADER>(pNMHDR);
670 if (m_bThreadRunning)
671 return;
672 if (m_nSortedColumn == phdr->iItem)
673 m_bAscending = !m_bAscending;
674 else
675 m_bAscending = TRUE;
676 m_nSortedColumn = phdr->iItem;
677 Sort();
679 CString temp;
680 SetRedraw(FALSE);
681 DeleteAllItems();
682 SetItemCountEx (static_cast<int>(m_arData.size()));
684 SetRedraw(TRUE);
686 *pResult = 0;
689 bool CGitProgressList::NotificationDataIsAux(const NotificationData* pData)
691 return pData->bAuxItem;
694 void CGitProgressList::AddNotify(NotificationData* data, CColors::Colors color)
696 if (color != CColors::COLOR_END)
698 data->color = m_Colors.GetColor(color);
699 data->colorIsDirect = true;
701 else
702 data->SetColorCode(m_Colors);
704 m_arData.push_back(data);
705 AddItemToList();
707 if ((!data->bAuxItem) && (m_itemCount > 0))
709 if (m_pProgControl)
711 m_pProgControl->ShowWindow(SW_SHOW);
712 m_pProgControl->SetPos(m_itemCount);
713 m_pProgControl->SetRange32(0, m_itemCountTotal);
715 if (m_pTaskbarList && m_pPostWnd)
717 m_pTaskbarList->SetProgressState(m_pPostWnd->GetSafeHwnd(), TBPF_NORMAL);
718 m_pTaskbarList->SetProgressValue(m_pPostWnd->GetSafeHwnd(), m_itemCount, m_itemCountTotal);
722 // needed as long as RemoteProgressCommand::RemoteCompletionCallback never gets called by libgit2
723 if (m_pAnimate)
724 m_pAnimate->ShowWindow(SW_HIDE);
727 int CGitProgressList::UpdateProgress(const git_indexer_progress* stat)
729 static ULONGLONG start = 0;
730 auto dt = GetTickCount64() - start;
731 double speed = 0;
733 if (m_bCancelled)
735 git_error_set_str(GIT_ERROR_NONE, "User cancelled.");
736 return GIT_EUSER;
739 if (dt > 100)
741 start = GetTickCount64();
742 const size_t ds = stat->received_bytes - m_TotalBytesTransferred;
743 speed = ds * 1000.0/dt;
744 m_TotalBytesTransferred = stat->received_bytes;
746 else
747 return 0;
749 const int progress = stat->received_objects + stat->indexed_objects;
751 if ((stat->total_objects > 1000) && m_pProgControl && (!m_pProgControl->IsWindowVisible()))
752 m_pProgControl->ShowWindow(SW_SHOW);
754 if (m_pProgressLabelCtrl && m_pProgressLabelCtrl->IsWindowVisible())
755 m_pProgressLabelCtrl->ShowWindow(SW_SHOW);
757 if (m_pProgControl)
759 m_pProgControl->SetPos(progress);
760 m_pProgControl->SetRange32(0, 2 * stat->total_objects);
762 if (m_pTaskbarList && m_pPostWnd)
764 m_pTaskbarList->SetProgressState(m_pPostWnd->GetSafeHwnd(), TBPF_NORMAL);
765 m_pTaskbarList->SetProgressValue(m_pPostWnd->GetSafeHwnd(), progress, 2 * stat->total_objects);
768 CString progText;
769 if (stat->received_bytes < 1024)
770 m_sTotalBytesTransferred.Format(IDS_SVN_PROGRESS_TOTALBYTESTRANSFERRED, static_cast<int64_t>(stat->received_bytes));
771 else if (stat->received_bytes < 1200000)
772 m_sTotalBytesTransferred.Format(IDS_SVN_PROGRESS_TOTALTRANSFERRED, static_cast<int64_t>(stat->received_bytes) / 1024);
773 else
774 m_sTotalBytesTransferred.Format(IDS_SVN_PROGRESS_TOTALMBTRANSFERRED, static_cast<double>(stat->received_bytes) / 1048576.0);
776 CString str;
777 if(speed < 1024)
778 str.Format(L"%.0f B/s", speed);
779 else if(speed < 1024 * 1024)
780 str.Format(L"%.2f KiB/s", speed / 1024);
781 else
782 str.Format(L"%.2f MiB/s", speed / 1048576.0);
784 progText.FormatMessage(IDS_SVN_PROGRESS_TOTALANDSPEED, static_cast<LPCWSTR>(m_sTotalBytesTransferred), static_cast<LPCWSTR>(str));
785 if (m_pProgressLabelCtrl)
786 m_pProgressLabelCtrl->SetWindowText(progText);
788 SetTimer(TRANSFERTIMER, 2000, nullptr);
790 return 0;
793 void CGitProgressList::OnTimer(UINT_PTR nIDEvent)
795 if (nIDEvent == TRANSFERTIMER)
797 CString progText;
798 CString progSpeed = L"0 B/s";
799 progText.FormatMessage(IDS_SVN_PROGRESS_TOTALANDSPEED, static_cast<LPCWSTR>(m_sTotalBytesTransferred), static_cast<LPCWSTR>(progSpeed));
800 if (m_pProgressLabelCtrl)
801 m_pProgressLabelCtrl->SetWindowText(progText);
803 KillTimer(TRANSFERTIMER);
805 if (nIDEvent == VISIBLETIMER)
807 if (nEnsureVisibleCount)
808 EnsureVisible(GetItemCount()-1, false);
809 nEnsureVisibleCount = 0;
813 void CGitProgressList::Sort()
815 if(m_arData.size() < 2)
816 return;
818 // We need to sort the blocks which lie between the auxiliary entries
819 // This is so that any aux data stays where it was
820 NotificationDataVect::iterator actionBlockBegin;
821 NotificationDataVect::iterator actionBlockEnd = m_arData.begin(); // We start searching from here
823 for(;;)
825 // Search to the start of the non-aux entry in the next block
826 actionBlockBegin = std::find_if(actionBlockEnd, m_arData.end(), [](const auto& pData) { return !CGitProgressList::NotificationDataIsAux(pData); });
827 if(actionBlockBegin == m_arData.end())
829 // There are no more actions
830 break;
832 // Now search to find the end of the block
833 actionBlockEnd = std::find_if(actionBlockBegin + 1, m_arData.end(), [](const auto& pData) { return CGitProgressList::NotificationDataIsAux(pData); });
834 // Now sort the block
835 std::sort(actionBlockBegin, actionBlockEnd, &CGitProgressList::SortCompare);
839 bool CGitProgressList::SortCompare(const NotificationData * pData1, const NotificationData * pData2)
841 int result = 0;
842 switch (m_nSortedColumn)
844 case 0: //action column
845 result = pData1->sActionColumnText.Compare(pData2->sActionColumnText);
846 break;
847 case 1: //path column
848 // Compare happens after switch()
849 break;
850 default:
851 break;
854 // Sort by path if everything else is equal
855 if (result == 0)
856 result = CTGitPath::Compare(pData1->path, pData2->path);
858 if (!m_bAscending)
859 result = -result;
860 return result < 0;
863 void CGitProgressList::OnContextMenu(CWnd* pWnd, CPoint point)
865 if (m_options & ProgOptDryRun)
866 return; // don't do anything in a dry-run.
868 if (pWnd != this)
869 return;
871 const int selIndex = GetSelectionMark();
872 if ((point.x == -1) && (point.y == -1))
874 // Menu was invoked from the keyboard rather than by right-clicking
875 CRect rect;
876 GetItemRect(selIndex, &rect, LVIR_LABEL);
877 ClientToScreen(&rect);
878 point = rect.CenterPoint();
881 if ((selIndex < 0) || m_bThreadRunning || GetSelectedCount() == 0)
882 return;
884 // entry is selected, thread has finished with updating so show the popup menu
885 CIconMenu popup;
886 if (!popup.CreatePopupMenu())
887 return;
889 ContextMenuActionList actions;
890 NotificationData* data = m_arData[selIndex];
891 if (data && GetSelectedCount() == 1)
892 data->GetContextMenu(popup, actions);
894 if (!actions.empty())
895 popup.AppendMenu(MF_SEPARATOR, NULL);
896 actions.push_back([&]()
898 CString sLines;
899 POSITION pos = GetFirstSelectedItemPosition();
900 while (pos)
902 const int nItem = GetNextSelectedItem(pos);
903 NotificationData* data = m_arData[nItem];
904 if (data)
906 sLines += data->sPathColumnText;
907 sLines += L"\r\n";
910 sLines.TrimRight();
911 if (!sLines.IsEmpty())
912 CStringUtils::WriteAsciiStringToClipboard(sLines, GetSafeHwnd());
914 popup.AppendMenuIcon(actions.size(), IDS_LOG_POPUP_COPYTOCLIPBOARD, IDI_COPYCLIP);
916 if (actions.empty())
917 return;
919 const int cmd = popup.TrackPopupMenu(TPM_RETURNCMD | TPM_LEFTALIGN | TPM_NONOTIFY, point.x, point.y, this);
921 if (cmd <= 0 || static_cast<size_t>(cmd) > actions.size())
922 return;
924 theApp.DoWaitCursor(1);
925 actions.at(cmd - 1)();
926 theApp.DoWaitCursor(-1);
929 void CGitProgressList::OnLvnBegindragSvnprogress(NMHDR* , LRESULT *pResult)
931 //LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
932 #if 0
933 int selIndex = GetSelectionMark();
934 if (selIndex < 0)
935 return;
937 CDropFiles dropFiles; // class for creating DROPFILES struct
939 int index;
940 POSITION pos = GetFirstSelectedItemPosition();
941 while ( (index = GetNextSelectedItem(pos)) >= 0 )
943 NotificationData * data = m_arData[index];
945 if ( data->kind==svn_node_file || data->kind==svn_node_dir )
947 CString sPath = GetPathFromColumnText(data->sPathColumnText);
949 dropFiles.AddFile( sPath );
953 if (!dropFiles.IsEmpty())
955 dropFiles.CreateStructure();
957 #endif
958 *pResult = 0;
961 void CGitProgressList::OnSize(UINT nType, int cx, int cy)
963 CListCtrl::OnSize(nType, cx, cy);
964 if ((nType == SIZE_RESTORED)&&(m_bLastVisible))
966 if(!m_hWnd)
967 return;
969 const int count = GetItemCount();
970 if (count > 0)
971 EnsureVisible(count-1, false);
975 void CGitProgressList::Init()
977 SetExtendedStyle((CRegDWORD(L"Software\\TortoiseGit\\FullRowSelect", TRUE) ? LVS_EX_FULLROWSELECT : 0) | LVS_EX_DOUBLEBUFFER);
979 DeleteAllItems();
980 int c = GetHeaderCtrl()->GetItemCount()-1;
981 while (c>=0)
982 DeleteColumn(c--);
984 CString temp;
985 temp.LoadString(IDS_PROGRS_ACTION);
986 InsertColumn(0, temp);
987 temp.LoadString(IDS_PROGRS_PATH);
988 InsertColumn(1, temp);
990 m_pThread = AfxBeginThread(ProgressThreadEntry, this, THREAD_PRIORITY_NORMAL,0,CREATE_SUSPENDED);
991 if (!m_pThread)
992 ReportError(CString(MAKEINTRESOURCE(IDS_ERR_THREADSTARTFAILED)));
993 else
995 m_pThread->m_bAutoDelete = FALSE;
996 m_pThread->ResumeThread();
999 // Call this early so that the column headings aren't hidden before any
1000 // text gets added.
1001 ResizeColumns();
1003 SetTimer(VISIBLETIMER, 300, nullptr);
1007 void CGitProgressList::OnClose()
1009 if (m_bCancelled)
1011 g_Git.KillRelatedThreads(m_pThread);
1012 InterlockedExchange(&m_bThreadRunning, FALSE);
1014 else
1016 m_bCancelled = TRUE;
1017 return;
1019 CListCtrl::OnClose();
1022 BOOL CGitProgressList::PreTranslateMessage(MSG* pMsg)
1024 if (pMsg->message == WM_KEYDOWN)
1026 if (pMsg->wParam == 'A')
1028 if (GetKeyState(VK_CONTROL)&0x8000)
1030 // Ctrl-A -> select all
1031 SetSelectionMark(0);
1032 for (int i=0; i<GetItemCount(); ++i)
1033 SetItemState(i, LVIS_SELECTED, LVIS_SELECTED);
1036 if ((pMsg->wParam == 'C')||(pMsg->wParam == VK_INSERT))
1038 int selIndex = GetSelectionMark();
1039 if (selIndex >= 0)
1041 if (GetKeyState(VK_CONTROL)&0x8000)
1043 //Ctrl-C -> copy to clipboard
1044 CString sClipdata;
1045 POSITION pos = GetFirstSelectedItemPosition();
1046 if (pos)
1048 while (pos)
1050 int nItem = GetNextSelectedItem(pos);
1051 CString sAction = GetItemText(nItem, 0);
1052 CString sPath = GetItemText(nItem, 1);
1053 CString sMime = GetItemText(nItem, 2);
1054 sClipdata.AppendFormat(L"%s: %s %s\r\n", static_cast<LPCWSTR>(sAction), static_cast<LPCWSTR>(sPath), static_cast<LPCWSTR>(sMime));
1056 CStringUtils::WriteAsciiStringToClipboard(sClipdata);
1061 } // if (pMsg->message == WM_KEYDOWN)
1062 return CListCtrl::PreTranslateMessage(pMsg);
1065 void CGitProgressList::SetWindowTitle(UINT id, const CString& urlorpath, CString& dialogname)
1067 if (!m_bSetTitle || !m_pPostWnd)
1068 return;
1070 dialogname.LoadString(id);
1071 CAppUtils::SetWindowTitle(m_pPostWnd->GetSafeHwnd(), urlorpath, dialogname);
1074 void CGitProgressList::ShowProgressBar()
1076 if (m_pProgControl)
1078 m_pProgControl->ShowWindow(SW_SHOW);
1079 m_pProgControl->SetPos(0);
1080 m_pProgControl->SetRange32(0, 1);
1084 void CGitProgressList::SetProgressLabelText(const CString& str)
1086 if (m_pProgressLabelCtrl)
1087 m_pProgressLabelCtrl->SetWindowText(str);
1090 CGitProgressList::WC_File_NotificationData::WC_File_NotificationData(const CTGitPath& path, Git_WC_Notify_Action action)
1091 : NotificationData()
1092 , action(action)
1094 this->path = path;
1095 sPathColumnText = path.GetGitPathString();
1097 switch (action)
1099 case Git_WC_Notify_Action::Skip:
1100 sActionColumnText.LoadString(IDS_SVNACTION_SKIP);
1101 break;
1102 case Git_WC_Notify_Action::Add:
1103 sActionColumnText.LoadString(IDS_SVNACTION_ADD);
1104 break;
1105 case Git_WC_Notify_Action::Resolved:
1106 sActionColumnText.LoadString(IDS_SVNACTION_RESOLVE);
1107 break;
1108 case Git_WC_Notify_Action::Revert:
1109 sActionColumnText.LoadString(IDS_SVNACTION_REVERT);
1110 break;
1111 case Git_WC_Notify_Action::Checkout:
1112 sActionColumnText.LoadString(IDS_PROGRS_CMD_CHECKOUT);
1113 break;
1114 case Git_WC_Notify_Action::LFS_Lock:
1115 sActionColumnText.LoadString(IDS_PROGRS_CMD_LFS_LOCK);
1116 break;
1117 case Git_WC_Notify_Action::LFS_Unlock:
1118 sActionColumnText.LoadString(IDS_PROGRS_CMD_LFS_UNLOCK);
1119 break;
1120 default:
1121 break;
1125 void CGitProgressList::WC_File_NotificationData::SetColorCode(CColors& colors)
1127 switch (action)
1129 case Git_WC_Notify_Action::Checkout:
1130 [[fallthrough]];
1131 case Git_WC_Notify_Action::Add:
1132 color = colors.GetColor(CColors::Added);
1133 colorIsDirect = true;
1134 break;
1136 default:
1137 break;
1141 void CGitProgressList::WC_File_NotificationData::GetContextMenu(CIconMenu& popup, ContextMenuActionList& actions)
1143 if ((action == Git_WC_Notify_Action::Add) ||
1144 (action == Git_WC_Notify_Action::Revert) ||
1145 (action == Git_WC_Notify_Action::Resolved) ||
1146 (action == Git_WC_Notify_Action::Checkout) ||
1147 (action == Git_WC_Notify_Action::LFS_Lock) ||
1148 (action == Git_WC_Notify_Action::LFS_Unlock))
1150 actions.push_back([&]()
1152 CString cmd = L"/command:log";
1153 CString sPath = g_Git.CombinePath(path);
1154 cmd += L" /path:\"" + sPath + L'"';
1155 CAppUtils::RunTortoiseGitProc(cmd);
1157 popup.AppendMenuIcon(actions.size(), IDS_MENULOG, IDI_LOG);
1159 popup.AppendMenu(MF_SEPARATOR, NULL);
1160 if (!PathIsDirectory(g_Git.CombinePath(path)))
1162 actions.push_back([&]{ CAppUtils::ShellOpen(g_Git.CombinePath(path)); });
1163 popup.AppendMenuIcon(actions.size(), IDS_LOG_POPUP_OPEN, IDI_OPEN);
1164 actions.push_back([&]{ CAppUtils::ShowOpenWithDialog(g_Git.CombinePath(path)); });
1165 popup.AppendMenuIcon(actions.size(), IDS_LOG_POPUP_OPENWITH, IDI_OPEN);
1168 actions.push_back([&]{ CAppUtils::ExploreTo(nullptr, g_Git.CombinePath(path)); });
1169 popup.AppendMenuIcon(actions.size(), IDS_STATUSLIST_CONTEXT_EXPLORE, IDI_EXPLORER);
1171 if ((action == Git_WC_Notify_Action::LFS_Lock) ||
1172 (action == Git_WC_Notify_Action::LFS_Unlock))
1174 popup.AppendMenu(MF_SEPARATOR, NULL);
1176 CTGitPathList pathList(path);
1178 actions.push_back([&] {
1179 CString tempfilename = CTempFiles::Instance().GetTempFilePath(false).GetWinPathString();
1180 VERIFY(pathList.WriteToFile(tempfilename));
1181 CString sCmd;
1182 sCmd.Format(L"/command:lfslock /pathfile:\"%s\" /deletepathfile", static_cast<LPCWSTR>(tempfilename));
1183 CAppUtils::RunTortoiseGitProc(sCmd);
1185 popup.AppendMenuIcon(actions.size(), IDS_PROGRS_TITLE_LFS_LOCK, IDI_LFSLOCK);
1187 actions.push_back([&] {
1188 CString tempfilename = CTempFiles::Instance().GetTempFilePath(false).GetWinPathString();
1189 VERIFY(pathList.WriteToFile(tempfilename));
1190 CString sCmd;
1191 sCmd.Format(L"/command:lfsunlock /pathfile:\"%s\" /deletepathfile", static_cast<LPCWSTR>(tempfilename));
1192 CAppUtils::RunTortoiseGitProc(sCmd);
1194 popup.AppendMenuIcon(actions.size(), IDS_PROGRS_TITLE_LFS_UNLOCK, IDI_LFSUNLOCK);
1199 void CGitProgressList::WC_File_NotificationData::HandleDblClick() const
1201 CString sWinPath = g_Git.CombinePath(path);
1202 if (PathIsDirectory(sWinPath))
1204 CAppUtils::ExploreTo(nullptr, sWinPath);
1205 return;
1207 CAppUtils::ShellOpen(sWinPath);
1210 void CGitProgressList::OnSysColorChange()
1212 __super::OnSysColorChange();
1213 CTheme::Instance().OnSysColorChanged();
1216 ULONG CGitProgressList::GetGestureStatus(CPoint /*ptTouch*/)
1218 return 0;