Fixed issue #3017: Spaces at the beginning of the URL breaks pushes
[TortoiseGit.git] / src / Utils / MiscUI / HistoryCombo.cpp
blob91659888c00a931607b55980b96619c20dce14a8
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2003-2012 - TortoiseSVN
4 // Copyright (C) 2013-2017 - TortoiseGit
6 // This program is free software; you can redistribute it and/or
7 // modify it under the terms of the GNU General Public License
8 // as published by the Free Software Foundation; either version 2
9 // of the License, or (at your option) any later version.
11 // This program is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
16 // You should have received a copy of the GNU General Public License
17 // along with this program; if not, write to the Free Software Foundation,
18 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 #include "stdafx.h"
21 #include "HistoryCombo.h"
22 #include "../registry.h"
24 #ifdef HISTORYCOMBO_WITH_SYSIMAGELIST
25 #include "../SysImageList.h"
26 #endif
28 #define MAX_HISTORY_ITEMS 25
30 int CHistoryCombo::m_nGitIconIndex = 0;
32 CHistoryCombo::CHistoryCombo(BOOL bAllowSortStyle /*=FALSE*/ )
33 : m_nMaxHistoryItems ( MAX_HISTORY_ITEMS)
34 , m_bAllowSortStyle(bAllowSortStyle)
35 , m_bURLHistory(FALSE)
36 , m_bPathHistory(FALSE)
37 , m_hWndToolTip(nullptr)
38 , m_ttShown(FALSE)
39 , m_bDyn(FALSE)
40 , m_bWantReturn(FALSE)
41 , m_bTrim(TRUE)
42 , m_bCaseSensitive(FALSE)
44 SecureZeroMemory(&m_ToolInfo, sizeof(m_ToolInfo));
47 CHistoryCombo::~CHistoryCombo()
51 BOOL CHistoryCombo::PreCreateWindow(CREATESTRUCT& cs)
53 if (!m_bAllowSortStyle) //turn off CBS_SORT style
54 cs.style &= ~CBS_SORT;
55 cs.style |= CBS_AUTOHSCROLL;
56 m_bDyn = TRUE;
57 return CComboBoxEx::PreCreateWindow(cs);
60 BOOL CHistoryCombo::PreTranslateMessage(MSG* pMsg)
62 if (pMsg->message == WM_KEYDOWN)
64 bool bShift = !!(GetKeyState(VK_SHIFT) & 0x8000);
65 int nVirtKey = (int) pMsg->wParam;
67 if (nVirtKey == VK_RETURN)
68 return OnReturnKeyPressed();
69 else if (nVirtKey == VK_DELETE && bShift && GetDroppedState() )
71 RemoveSelectedItem();
72 return TRUE;
75 if (nVirtKey == 'A' && (GetKeyState(VK_CONTROL) & 0x8000 ) )
77 CEdit *edit = GetEditCtrl();
78 if (edit)
79 edit->SetSel(0, -1);
80 return TRUE;
83 else if (pMsg->message == WM_MOUSEMOVE && this->m_bDyn )
85 if ((pMsg->wParam & MK_LBUTTON) == 0)
87 CPoint pt;
88 pt.x = LOWORD(pMsg->lParam);
89 pt.y = HIWORD(pMsg->lParam);
90 OnMouseMove((UINT)pMsg->wParam, pt);
91 return TRUE;
94 else if ((pMsg->message == WM_MOUSEWHEEL || pMsg->message == WM_MOUSEHWHEEL) && !GetDroppedState())
96 return TRUE;
99 return CComboBoxEx::PreTranslateMessage(pMsg);
102 BEGIN_MESSAGE_MAP(CHistoryCombo, CComboBoxEx)
103 ON_WM_MOUSEMOVE()
104 ON_WM_TIMER()
105 ON_WM_CREATE()
106 END_MESSAGE_MAP()
108 int CHistoryCombo::AddString(const CString& str, INT_PTR pos /* = -1*/, BOOL isSel /* = TRUE */)
110 if (str.IsEmpty())
111 return -1;
113 if (pos < 0)
114 pos = GetCount();
116 CString combostring = str;
117 combostring.Replace('\r', ' ');
118 combostring.Replace('\n', ' ');
119 if (m_bTrim)
120 combostring.Trim();
121 if (combostring.IsEmpty())
122 return -1;
124 //search the Combo for another string like this
125 //and do not insert if found
126 int nIndex = m_bCaseSensitive ? FindStringExactCaseSensitive(-1, combostring) : FindStringExact(-1, combostring);
127 if (nIndex != -1)
129 if (nIndex > pos)
131 DeleteItem(nIndex);
132 m_arEntries.RemoveAt(nIndex);
134 else
136 if(isSel)
137 SetCurSel(nIndex);
138 return nIndex;
142 //truncate list to m_nMaxHistoryItems
143 int nNumItems = GetCount();
144 for (int n = m_nMaxHistoryItems; n < nNumItems; n++)
146 DeleteItem(m_nMaxHistoryItems);
147 m_arEntries.RemoveAt(m_nMaxHistoryItems);
150 int nRet = InsertEntry(combostring, pos);
152 if (isSel)
153 SetCurSel(nRet);
154 return nRet;
157 int CHistoryCombo::InsertEntry(const CString& combostring, INT_PTR pos)
159 COMBOBOXEXITEM cbei = { 0 };
160 cbei.mask = CBEIF_TEXT;
161 cbei.iItem = pos;
163 cbei.pszText = const_cast<LPTSTR>(combostring.GetString());
165 #ifdef HISTORYCOMBO_WITH_SYSIMAGELIST
166 if (m_bURLHistory)
168 cbei.iImage = SYS_IMAGE_LIST().GetFileIconIndex(combostring);
169 if (cbei.iImage == 0 || cbei.iImage == SYS_IMAGE_LIST().GetDefaultIconIndex())
171 if (CStringUtils::StartsWith(combostring, L"http:"))
172 cbei.iImage = SYS_IMAGE_LIST().GetFileIconIndex(L".html");
173 else if (CStringUtils::StartsWith(combostring, L"https:"))
174 cbei.iImage = SYS_IMAGE_LIST().GetFileIconIndex(L".html");
175 else if (CStringUtils::StartsWith(combostring, L"file:"))
176 cbei.iImage = SYS_IMAGE_LIST().GetDirIconIndex();
177 else if (CStringUtils::StartsWith(combostring, L"git:"))
178 cbei.iImage = m_nGitIconIndex;
179 else if (CStringUtils::StartsWith(combostring, L"ssh:"))
180 cbei.iImage = m_nGitIconIndex;
181 else
182 cbei.iImage = SYS_IMAGE_LIST().GetDirIconIndex();
184 cbei.iSelectedImage = cbei.iImage;
185 cbei.mask |= CBEIF_IMAGE | CBEIF_SELECTEDIMAGE;
187 if (m_bPathHistory)
189 cbei.iImage = SYS_IMAGE_LIST().GetFileIconIndex(combostring);
190 if (cbei.iImage == SYS_IMAGE_LIST().GetDefaultIconIndex())
192 cbei.iImage = SYS_IMAGE_LIST().GetDirIconIndex();
194 cbei.iSelectedImage = cbei.iImage;
195 cbei.mask |= CBEIF_IMAGE | CBEIF_SELECTEDIMAGE;
197 #endif
199 int nRet = InsertItem(&cbei);
200 if (nRet >= 0)
201 m_arEntries.InsertAt(nRet, combostring);
203 return nRet;
206 void CHistoryCombo::SetList(const STRING_VECTOR& list)
208 Reset();
209 for (size_t i = 0; i < list.size(); ++i)
211 CString combostring = list[i];
212 combostring.Replace('\r', ' ');
213 combostring.Replace('\n', ' ');
214 if (m_bTrim)
215 combostring.Trim();
216 if (combostring.IsEmpty())
217 continue;
219 InsertEntry(combostring, i);
223 CString CHistoryCombo::LoadHistory(LPCTSTR lpszSection, LPCTSTR lpszKeyPrefix)
225 if (!lpszSection || !lpszKeyPrefix || *lpszSection == '\0')
226 return L"";
228 m_sSection = lpszSection;
229 m_sKeyPrefix = lpszKeyPrefix;
231 int n = 0;
232 CString sText;
235 //keys are of form <lpszKeyPrefix><entrynumber>
236 CString sKey;
237 sKey.Format(L"%s\\%s%d", (LPCTSTR)m_sSection, (LPCTSTR)m_sKeyPrefix, n++);
238 sText = CRegString(sKey);
239 if (!sText.IsEmpty())
240 AddString(sText);
241 } while (!sText.IsEmpty() && n < m_nMaxHistoryItems);
243 SetCurSel(-1);
245 ModifyStyleEx(WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE, 0);
247 // need to resize the control for correct display
248 CRect rect;
249 GetWindowRect(rect);
250 GetParent()->ScreenToClient(rect);
251 MoveWindow(rect.left, rect.top, rect.Width(),100);
253 return sText;
256 void CHistoryCombo::SaveHistory()
258 if (m_sSection.IsEmpty())
259 return;
261 //add the current item to the history
262 CString sCurItem;
263 GetWindowText(sCurItem);
264 if (m_bTrim)
265 sCurItem.Trim();
266 if (!sCurItem.IsEmpty())
267 AddString(sCurItem, 0);
268 //save history to registry/inifile
269 int nMax = min(GetCount(), m_nMaxHistoryItems + 1);
270 for (int n = 0; n < nMax; n++)
272 CString sKey;
273 sKey.Format(L"%s\\%s%d", (LPCTSTR)m_sSection, (LPCTSTR)m_sKeyPrefix, n);
274 CRegString regkey(sKey);
275 regkey = m_arEntries.GetAt(n);
277 //remove items exceeding the max number of history items
278 for (int n = nMax; ; n++)
280 CString sKey;
281 sKey.Format(L"%s\\%s%d", (LPCTSTR)m_sSection, (LPCTSTR)m_sKeyPrefix, n);
282 CRegString regkey(sKey);
283 CString sText = regkey;
284 if (sText.IsEmpty())
285 break;
286 regkey.removeValue(); // remove entry
290 void CHistoryCombo::ClearHistory(BOOL bDeleteRegistryEntries/*=TRUE*/)
292 ResetContent();
293 if (! m_sSection.IsEmpty() && bDeleteRegistryEntries)
295 //remove profile entries
296 CString sKey;
297 for (int n = 0; ; n++)
299 sKey.Format(L"%s\\%s%d", (LPCTSTR)m_sSection, (LPCTSTR)m_sKeyPrefix, n);
300 CRegString regkey(sKey);
301 CString sText = regkey;
302 if (sText.IsEmpty())
303 break;
304 regkey.removeValue(); // remove entry
309 void CHistoryCombo::RemoveEntryFromHistory(LPCTSTR lpszSection, LPCTSTR lpszKeyPrefix, const CString& entryToRemove)
311 if (entryToRemove.IsEmpty())
312 return;
313 CString sText;
314 bool found = false;
315 int n = -1;
318 CString sKey;
319 sKey.Format(L"%s\\%s%d", lpszSection, lpszKeyPrefix, ++n);
320 CRegString regkey(sKey);
321 sText = regkey;
322 if (sText == entryToRemove)
324 regkey.removeValue();
325 found = true;
326 ++n;
327 break;
329 } while (!sText.IsEmpty());
330 if (!found)
331 return;
332 for (;; ++n)
334 CString sKey;
335 sKey.Format(L"%s\\%s%d", lpszSection, lpszKeyPrefix, n);
336 CRegString regkey(sKey);
337 sText = regkey;
338 if (!sText.IsEmpty())
340 CString sKeyNew;
341 sKeyNew.Format(L"%s\\%s%d", lpszSection, lpszKeyPrefix, n - 1);
342 CRegString regkeyNew(sKeyNew);
343 regkeyNew = sText;
344 regkey.removeValue();
345 continue;
347 else
348 break;
352 void CHistoryCombo::SetURLHistory(BOOL bURLHistory)
354 m_bURLHistory = bURLHistory;
356 if (m_bURLHistory)
358 HWND hwndEdit;
359 // use for ComboEx
360 hwndEdit = (HWND)::SendMessage(this->m_hWnd, CBEM_GETEDITCONTROL, 0, 0);
361 if (!hwndEdit)
363 // Try the unofficial way of getting the edit control CWnd*
364 CWnd* pWnd = this->GetDlgItem(1001);
365 if(pWnd)
367 hwndEdit = pWnd->GetSafeHwnd();
370 if (hwndEdit)
371 SHAutoComplete(hwndEdit, SHACF_URLALL);
374 #ifdef HISTORYCOMBO_WITH_SYSIMAGELIST
375 SetImageList(&SYS_IMAGE_LIST());
376 #endif
379 void CHistoryCombo::SetPathHistory(BOOL bPathHistory)
381 m_bPathHistory = bPathHistory;
383 if (m_bPathHistory)
385 HWND hwndEdit;
386 // use for ComboEx
387 hwndEdit = (HWND)::SendMessage(this->m_hWnd, CBEM_GETEDITCONTROL, 0, 0);
388 if (!hwndEdit)
390 CWnd* pWnd = this->GetDlgItem(1001);
391 if (pWnd)
392 hwndEdit = pWnd->GetSafeHwnd();
394 if (hwndEdit)
395 SHAutoComplete(hwndEdit, SHACF_FILESYSTEM);
398 #ifdef HISTORYCOMBO_WITH_SYSIMAGELIST
399 SetImageList(&SYS_IMAGE_LIST());
400 #endif
403 void CHistoryCombo::SetMaxHistoryItems(int nMaxItems)
405 m_nMaxHistoryItems = nMaxItems;
407 //truncate list to nMaxItems
408 int nNumItems = GetCount();
409 for (int n = m_nMaxHistoryItems; n < nNumItems; n++)
410 DeleteString(m_nMaxHistoryItems);
413 CString CHistoryCombo::GetString() const
415 CString str;
416 int sel;
417 sel = GetCurSel();
418 DWORD style=GetStyle();
420 if (sel == CB_ERR ||(m_bURLHistory)||(m_bPathHistory) || (!(style&CBS_SIMPLE)))
422 GetWindowText(str);
423 if (m_bTrim)
424 str.Trim();
425 return str;
428 return m_arEntries.GetAt(sel);
431 BOOL CHistoryCombo::RemoveSelectedItem()
433 int nIndex = GetCurSel();
434 if (nIndex == CB_ERR)
436 return FALSE;
439 DeleteItem(nIndex);
440 m_arEntries.RemoveAt(nIndex);
442 if ( nIndex < GetCount() )
444 // index stays the same to select the
445 // next item after the item which has
446 // just been deleted
448 else
450 // the end of the list has been reached
451 // so we select the previous item
452 nIndex--;
455 if ( nIndex == -1 )
457 // The one and only item has just been
458 // deleted -> reset window text since
459 // there is no item to select
460 SetWindowText(L"");
462 else
464 SetCurSel(nIndex);
467 // Since the dialog might be canceled we
468 // should now save the history. Before that
469 // set the selection to the first item so that
470 // the items will not be reordered and restore
471 // the selection after saving.
472 SetCurSel(0);
473 SaveHistory();
474 if ( nIndex != -1 )
476 SetCurSel(nIndex);
479 return TRUE;
482 void CHistoryCombo::PreSubclassWindow()
484 CComboBoxEx::PreSubclassWindow();
486 if (!m_bDyn)
487 CreateToolTip();
490 void CHistoryCombo::OnMouseMove(UINT nFlags, CPoint point)
492 CRect rectClient;
493 GetClientRect(&rectClient);
494 int nComboButtonWidth = ::GetSystemMetrics(SM_CXHTHUMB) + 2;
495 rectClient.right = rectClient.right - nComboButtonWidth;
497 if (rectClient.PtInRect(point))
499 ClientToScreen(&rectClient);
501 m_ToolText = GetString();
502 m_ToolInfo.lpszText = (LPTSTR)(LPCTSTR)m_ToolText;
504 HDC hDC = ::GetDC(m_hWnd);
506 CFont *pFont = GetFont();
507 HFONT hOldFont = (HFONT) ::SelectObject(hDC, (HFONT) *pFont);
509 SIZE size;
510 ::GetTextExtentPoint32(hDC, m_ToolText, m_ToolText.GetLength(), &size);
511 ::SelectObject(hDC, hOldFont);
512 ::ReleaseDC(m_hWnd, hDC);
514 if (size.cx > (rectClient.Width() - 6))
516 rectClient.left += 1;
517 rectClient.top += 3;
519 COLORREF rgbText = ::GetSysColor(COLOR_WINDOWTEXT);
520 COLORREF rgbBackground = ::GetSysColor(COLOR_WINDOW);
522 CWnd *pWnd = GetFocus();
523 if (pWnd)
525 if (pWnd->m_hWnd == m_hWnd)
527 rgbText = ::GetSysColor(COLOR_HIGHLIGHTTEXT);
528 rgbBackground = ::GetSysColor(COLOR_HIGHLIGHT);
532 if (!m_ttShown)
534 ::SendMessage(m_hWndToolTip, TTM_SETTIPBKCOLOR, rgbBackground, 0);
535 ::SendMessage(m_hWndToolTip, TTM_SETTIPTEXTCOLOR, rgbText, 0);
536 ::SendMessage(m_hWndToolTip, TTM_UPDATETIPTEXT, 0, (LPARAM) &m_ToolInfo);
537 ::SendMessage(m_hWndToolTip, TTM_TRACKPOSITION, 0, (LPARAM)MAKELONG(rectClient.left, rectClient.top));
538 ::SendMessage(m_hWndToolTip, TTM_TRACKACTIVATE, TRUE, (LPARAM)(LPTOOLINFO) &m_ToolInfo);
539 SetTimer(1, 80, nullptr);
540 SetTimer(2, 2000, nullptr);
541 m_ttShown = TRUE;
544 else
546 ::SendMessage(m_hWndToolTip, TTM_TRACKACTIVATE, FALSE, (LPARAM)(LPTOOLINFO) &m_ToolInfo);
547 m_ttShown = FALSE;
550 else
552 ::SendMessage(m_hWndToolTip, TTM_TRACKACTIVATE, FALSE, (LPARAM)(LPTOOLINFO) &m_ToolInfo);
553 m_ttShown = FALSE;
556 CComboBoxEx::OnMouseMove(nFlags, point);
559 void CHistoryCombo::OnTimer(UINT_PTR nIDEvent)
561 CPoint point;
562 DWORD ptW = GetMessagePos();
563 point.x = GET_X_LPARAM(ptW);
564 point.y = GET_Y_LPARAM(ptW);
565 ScreenToClient(&point);
567 CRect rectClient;
568 GetClientRect(&rectClient);
569 int nComboButtonWidth = ::GetSystemMetrics(SM_CXHTHUMB) + 2;
571 rectClient.right = rectClient.right - nComboButtonWidth;
573 if (!rectClient.PtInRect(point))
575 KillTimer(nIDEvent);
576 ::SendMessage(m_hWndToolTip, TTM_TRACKACTIVATE, FALSE, (LPARAM)(LPTOOLINFO) &m_ToolInfo);
577 m_ttShown = FALSE;
579 if (nIDEvent == 2)
581 // tooltip timeout, just deactivate it
582 ::SendMessage(m_hWndToolTip, TTM_TRACKACTIVATE, FALSE, (LPARAM)(LPTOOLINFO) &m_ToolInfo);
583 // don't set m_ttShown to FALSE, because we don't want the tooltip to show up again
584 // without the mouse pointer first leaving the control and entering it again
587 CComboBoxEx::OnTimer(nIDEvent);
590 void CHistoryCombo::CreateToolTip()
592 // create tooltip
593 m_hWndToolTip = ::CreateWindowEx(NULL,
594 TOOLTIPS_CLASS,
595 nullptr,
596 TTS_NOPREFIX | TTS_ALWAYSTIP,
597 CW_USEDEFAULT,
598 CW_USEDEFAULT,
599 CW_USEDEFAULT,
600 CW_USEDEFAULT,
601 m_hWnd,
602 nullptr,
603 nullptr,
604 nullptr);
606 // initialize tool info struct
607 memset(&m_ToolInfo, 0, sizeof(m_ToolInfo));
608 m_ToolInfo.cbSize = sizeof(m_ToolInfo);
609 m_ToolInfo.uFlags = TTF_TRANSPARENT;
610 m_ToolInfo.hwnd = m_hWnd;
612 ::SendMessage(m_hWndToolTip, TTM_SETMAXTIPWIDTH, 0, SHRT_MAX);
613 ::SendMessage(m_hWndToolTip, TTM_ADDTOOL, 0, (LPARAM) (LPTOOLINFO) &m_ToolInfo);
614 ::SendMessage(m_hWndToolTip, TTM_SETTIPBKCOLOR, ::GetSysColor(COLOR_HIGHLIGHT), 0);
615 ::SendMessage(m_hWndToolTip, TTM_SETTIPTEXTCOLOR, ::GetSysColor(COLOR_HIGHLIGHTTEXT), 0);
617 CRect rectMargins(0,-1,0,-1);
618 ::SendMessage(m_hWndToolTip, TTM_SETMARGIN, 0, (LPARAM)&rectMargins);
620 CFont *pFont = GetFont();
621 ::SendMessage(m_hWndToolTip, WM_SETFONT, (WPARAM)(HFONT)*pFont, FALSE);
624 int CHistoryCombo::OnCreate(LPCREATESTRUCT lpCreateStruct)
626 if (CComboBoxEx::OnCreate(lpCreateStruct) == -1)
627 return -1;
629 if (m_bDyn)
630 CreateToolTip();
632 return 0;
635 int CHistoryCombo::FindStringExactCaseSensitive(int nIndexStart, LPCTSTR lpszFind)
637 nIndexStart = max(0, nIndexStart);
638 for (int i = nIndexStart; i < GetCount(); i++)
640 CString exactString;
641 GetLBText(i, exactString);
642 if (exactString == lpszFind)
644 return i;
647 return -1;