1 /////////////////////////////////////////////////////////////////////////////
2 // WinMerge: an interactive diff/merge utility
3 // Copyright (C) 1997-2000 Thingamahoochie Software
5 // SPDX-License-Identifier: GPL-2.0-or-later
6 /////////////////////////////////////////////////////////////////////////////
8 * @file FilePathEdit.cpp
10 * @brief Implementation of the CFilepathEdit class.
15 #include "FilepathEdit.h"
17 #include "ClipBoard.h"
18 #include "FileOrFolderSelect.h"
19 #include "Win_VersionHelper.h"
27 static int FormatFilePathForDisplayWidth(CDC * pDC, int maxWidth, String & sFilepath);
29 BEGIN_MESSAGE_MAP(CFilepathEdit, CEdit)
31 ON_WM_CTLCOLOR_REFLECT()
37 ON_COMMAND(ID_EDIT_COPY, OnEditCopy)
38 ON_COMMAND(ID_EDIT_PASTE, OnEditPaste)
39 ON_COMMAND(ID_EDIT_CUT, OnEditCut)
40 ON_COMMAND(ID_EDIT_UNDO, OnEditUndo)
41 ON_COMMAND(ID_EDIT_SELECT_ALL, OnEditSelectAll)
42 ON_COMMAND_RANGE(ID_EDITOR_COPY_PATH, ID_EDITOR_SELECT_FILE, OnContextMenuSelected)
47 * @brief Format the path for display in header control.
49 * Formats path so it fits to given length, tries to end lines after
52 * @param [in] pDC Pointer to draw context.
53 * @param [in] maxWidth Maximum width of the string in the GUI.
54 * @param [in,out] sFilepath:
55 * - in: string to format
56 * - out: formatted string
57 * @return Number of lines path is splitted to.
59 static int FormatFilePathForDisplayWidth(CDC * pDC, int maxWidth, String & sFilepath)
68 // find the next truncation point
70 size_t iEndMax = sFilepath.length() - iBegin + 1;
73 size_t iEnd = (iEndMin + iEndMax) / 2;
76 line = sFilepath.substr(iBegin, iEnd);
77 int width = (pDC->GetTextExtent(line.c_str())).cx;
83 ASSERT(iEndMax == iEndMin+1);
85 // here iEndMin is the last character displayed in maxWidth
87 // exit the loop if we can display the remaining characters with no truncation
88 if (iBegin + iEndMin == sFilepath.length())
91 // truncate the text to the previous "\" if possible
92 line = sFilepath.substr(iBegin, iEndMin);
93 size_t lastSlash = line.rfind('\\');
94 if (lastSlash != String::npos)
95 iEndMin = lastSlash + 1;
97 sFilepath.insert(iBegin + iEndMin, _T("\n"));
98 iBegin += iEndMin + 2;
106 * @brief Constructor.
107 * Set text color to black and background white by default.
109 CFilepathEdit::CFilepathEdit()
110 : m_crBackGnd(RGB(255, 255, 255))
111 , m_crText(RGB(0,0,0))
113 , m_bInEditing(false)
114 , m_bEnabledFileSelection(false)
115 , m_bEnabledFolderSelection(false)
120 * @brief Subclass the control.
121 * @param [in] nID ID of the control to subclass.
122 * @param [in] pParent Parent control of the control to subclass.
123 * @return `true` if succeeded, `false` otherwise.
125 bool CFilepathEdit::SubClassEdit(UINT nID, CWnd* pParent)
128 return SubclassDlgItem(nID, pParent);
132 * @brief Set the text to show in the control.
133 * This function sets the text (original text) to show in the control.
134 * The control may modify the text for displaying in the GUI.
136 void CFilepathEdit::SetOriginalText(const String& sString)
138 if (m_sOriginalText.compare(sString) == 0)
141 m_sOriginalText = sString;
143 RefreshDisplayText();
147 * @brief Re-format the displayed text and update GUI.
148 * This method formats the visible text from original text.
150 void CFilepathEdit::RefreshDisplayText()
152 String line = m_sOriginalText;
154 // we want to keep the first and the last path component, and in between,
155 // as much characters as possible from the right
156 // PathCompactPath keeps, in between, as much characters as possible from the left
157 // so we reverse everything between the first and the last component before calling PathCompactPath
158 size_t iBeginLast = line.rfind('\\');
159 size_t iEndIntro = line.find('\\');
160 if (iBeginLast != String::npos && iEndIntro != iBeginLast)
162 String textToReverse = line.substr(iEndIntro + 1, iBeginLast -
164 std::reverse(textToReverse.begin(), textToReverse.end());
165 line = line.substr(0, iEndIntro + 1) + textToReverse + line.substr(iBeginLast);
168 // get a device context object
170 // and use the correct font
171 CFont *pFontOld = lDC.SelectObject(GetFont());
176 std::vector<tchar_t> tmp((std::max)(MAX_PATH, line.length() + 1));
177 std::copy(line.begin(), line.end(), tmp.begin());
178 PathCompactPath(lDC.GetSafeHdc(), &tmp[0], rect.Width());
182 lDC.SelectObject(pFontOld);
184 // we reverse back everything between the first and the last component
185 // it works OK as "..." reversed = "..." again
186 iBeginLast = line.rfind('\\');
187 iEndIntro = line.find('\\');
188 if (iBeginLast != String::npos && iEndIntro != iBeginLast)
190 String textToReverse = line.substr(iEndIntro + 1, iBeginLast -
192 std::reverse(textToReverse.begin(), textToReverse.end());
193 line = line.substr(0, iEndIntro + 1) + textToReverse + line.substr(iBeginLast);
196 SetWindowText(line.c_str());
200 * @brief Updates and returns the tooltip for this edit box
202 const String& CFilepathEdit::GetUpdatedTipText(CDC * pDC, int maxWidth)
204 GetOriginalText(m_sToolTipString);
205 FormatFilePathForDisplayWidth(pDC, maxWidth, m_sToolTipString);
206 return m_sToolTipString;
210 * @brief retrieve text from the OriginalText
212 * @note The standard Copy function works with the (compacted) windowText
214 void CFilepathEdit::CustomCopy(size_t iBegin, size_t iEnd /*=-1*/)
216 if (iEnd == String::npos)
217 iEnd = m_sOriginalText.length();
219 PutToClipboard(m_sOriginalText.substr(iBegin, iEnd - iBegin), m_hWnd);
223 * @brief Format the context menu.
225 void CFilepathEdit::OnContextMenu(CWnd* pWnd, CPoint point)
229 __super::OnContextMenu(pWnd, point);
236 if (point.x == -1 && point.y == -1){
237 //keystroke invocation
240 ClientToScreen(rect);
242 point = rect.TopLeft();
247 VERIFY(menu.LoadMenu(IDR_POPUP_EDITOR_HEADERBAR));
248 theApp.TranslateMenu(menu.m_hMenu);
250 CMenu* pPopup = menu.GetSubMenu(0);
251 ASSERT(pPopup != nullptr);
253 DWORD sel = GetSel();
254 if (HIWORD(sel) == LOWORD(sel))
255 pPopup->EnableMenuItem(ID_EDITOR_COPY, MF_GRAYED);
256 if (paths::EndsWithSlash(m_sOriginalText))
257 // no filename, we have to disable the unwanted menu entry
258 pPopup->EnableMenuItem(ID_EDITOR_COPY_FILENAME, MF_GRAYED);
259 if (!m_bEnabledFileSelection && !m_bEnabledFolderSelection)
260 pPopup->EnableMenuItem(ID_EDITOR_SELECT_FILE, MF_GRAYED);
262 // invoke context menu
263 pPopup->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, this);
267 static COLORREF MakeBackColor(bool bActive, bool bInEditing)
270 return CEColor::GetIntermediateColor(::GetSysColor(bInEditing ? COLOR_WINDOW : COLOR_ACTIVECAPTION), ::GetSysColor(COLOR_3DFACE), 0.5f);
272 return CEColor::GetIntermediateColor(::GetSysColor(bInEditing ? COLOR_WINDOW : COLOR_INACTIVECAPTION), ::GetSysColor(COLOR_3DFACE), 0.5f);
275 void CFilepathEdit::OnNcPaint()
277 COLORREF crBackGnd = m_bInEditing ? ::GetSysColor(COLOR_ACTIVEBORDER) : m_crBackGnd;
280 const int lpx = dc.GetDeviceCaps(LOGPIXELSX);
281 auto pointToPixel = [lpx](int point) { return MulDiv(point, lpx, 72); };
282 const int margin = pointToPixel(3);
285 rect.OffsetRect(-rect.TopLeft());
286 dc.FillSolidRect(CRect(rect.left, rect.top, rect.left + margin, rect.bottom), CEColor::GetDarkenColor(crBackGnd, 0.98f));
287 dc.FillSolidRect(CRect(rect.left, rect.top, rect.left + 1, rect.bottom), CEColor::GetDarkenColor(crBackGnd, 0.96f));
288 dc.FillSolidRect(CRect(rect.right - margin, rect.top, rect.right, rect.bottom), crBackGnd);
289 dc.FillSolidRect(CRect(rect.left + 1, rect.top, rect.right, rect.top + margin), CEColor::GetDarkenColor(crBackGnd, 0.98f));
290 dc.FillSolidRect(CRect(rect.left, rect.top, rect.right, rect.top + 1), CEColor::GetDarkenColor(crBackGnd, 0.96f));
291 dc.FillSolidRect(CRect(rect.left + margin, rect.bottom - margin, rect.right, rect.bottom), crBackGnd);
294 void CFilepathEdit::OnPaint()
300 CFont *pFontOld = dc.SelectObject(GetFont());
301 int oldBkMode = dc.SetBkMode(TRANSPARENT);
302 CRect rc = GetMenuCharRect(&dc);
303 const int lpx = dc.GetDeviceCaps(LOGPIXELSX);
304 auto pointToPixel = [lpx](int point) { return MulDiv(point, lpx, 72); };
305 const int margin = pointToPixel(3);
306 dc.TextOutW(rc.left + margin, 0, IsWin7_OrGreater() ? _T("\u2261") : _T("="));
307 dc.SetBkMode(oldBkMode);
308 dc.SelectObject(pFontOld);
312 void CFilepathEdit::OnKillFocus(CWnd* pNewWnd)
314 __super::OnKillFocus(pNewWnd);
317 m_bInEditing = false;
318 SetTextColor(::GetSysColor(COLOR_WINDOWTEXT));
319 SetBackColor(MakeBackColor(false, false));
320 RedrawWindow(nullptr, nullptr, RDW_FRAME | RDW_INVALIDATE);
322 SetWindowText(m_sOriginalText.c_str());
326 CRect CFilepathEdit::GetMenuCharRect(CDC* pDC)
331 pDC->GetCharWidth('=', '=', &charWidth);
332 const int lpx = pDC->GetDeviceCaps(LOGPIXELSX);
333 auto pointToPixel = [lpx](int point) { return MulDiv(point, lpx, 72); };
334 rc.left = rc.right - charWidth - pointToPixel(3 * 2);
338 void CFilepathEdit::OnLButtonDown(UINT nFlags, CPoint point)
341 CRect rc = GetMenuCharRect(&dc);
342 if (PtInRect(&rc, point))
344 ClientToScreen(&point);
345 OnContextMenu(this, point);
349 __super::OnLButtonDown(nFlags, point);
353 BOOL CFilepathEdit::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
359 CRect rc = GetMenuCharRect(&dc);
360 if (PtInRect(&rc, pt))
362 SetCursor(LoadCursor(nullptr, IDC_ARROW));
365 return __super::OnSetCursor(pWnd, nHitTest, message);
368 void CFilepathEdit::OnEditCopy()
370 int nStartChar, nEndChar;
371 GetSel(nStartChar, nEndChar);
372 if (nStartChar == nEndChar)
375 if (nStartChar == nEndChar)
376 SetSel(nStartChar, nEndChar);
379 void CFilepathEdit::OnEditPaste()
384 void CFilepathEdit::OnEditCut()
389 void CFilepathEdit::OnEditUndo()
394 void CFilepathEdit::OnEditSelectAll()
399 void CFilepathEdit::OnContextMenuSelected(UINT nID)
401 // compute the beginning of the text to copy (in OriginalText)
408 case ID_EDITOR_COPY_FILENAME:
410 size_t lastSlash = m_sOriginalText.rfind('\\');
411 if (lastSlash == String::npos)
412 lastSlash = m_sOriginalText.rfind('/');
413 if (lastSlash != String::npos)
414 iBegin = lastSlash+1;
419 case ID_EDITOR_COPY_PATH:
420 // pass the heading "*" for modified files
421 if (m_sOriginalText.at(0) == '*')
426 case ID_EDITOR_EDIT_CAPTION:
429 SetTextColor(::GetSysColor(COLOR_WINDOWTEXT));
430 SetBackColor(::GetSysColor(COLOR_WINDOW));
431 RedrawWindow(nullptr, nullptr, RDW_FRAME | RDW_INVALIDATE);
432 SetWindowText((m_sOriginalText.at(0) == '*' ? m_sOriginalText.substr(2) : m_sOriginalText).c_str());
436 case ID_EDITOR_SELECT_FILE:
440 if (!text.IsEmpty() && text[0] == '*')
441 text = text.Right(text.GetLength() - 2);
442 String dir = paths::GetParentPath(static_cast<const tchar_t*>(text));
443 bool selected = false;
444 if (m_bEnabledFileSelection)
445 selected = SelectFile(m_hWnd, m_sFilepath, true, dir.c_str());
447 selected = SelectFolder(m_sFilepath, dir.c_str(), _T(""), GetSafeHwnd());
449 GetParent()->PostMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), EN_USER_FILE_SELECTED), (LPARAM)m_hWnd);
459 BOOL CFilepathEdit::PreTranslateMessage(MSG *pMsg)
461 if (pMsg->message >= WM_KEYFIRST && pMsg->message <= WM_KEYLAST)
463 if (::TranslateAccelerator (m_hWnd, static_cast<CFrameWnd *>(AfxGetMainWnd())->GetDefaultAccelerator(), pMsg))
466 if (pMsg->message == WM_KEYDOWN)
468 if (pMsg->wParam == VK_RETURN)
470 m_bInEditing = false;
471 SetTextColor(::GetSysColor(COLOR_CAPTIONTEXT));
472 SetBackColor(MakeBackColor(true, false));
473 RedrawWindow(nullptr, nullptr, RDW_FRAME | RDW_INVALIDATE);
476 GetWindowText(sText);
477 if (m_sOriginalText.at(2) == '*')
478 sText = _T("* ") + sText;
479 SetWindowText(sText);
480 GetParent()->PostMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), EN_USER_CAPTION_CHANGED), (LPARAM)m_hWnd);
483 if (pMsg->wParam == VK_ESCAPE)
485 m_bInEditing = false;
486 SetTextColor(GetSysColor(COLOR_CAPTIONTEXT));
487 SetBackColor(GetSysColor(COLOR_INACTIVECAPTION));
488 RedrawWindow(nullptr, nullptr, RDW_FRAME | RDW_INVALIDATE);
490 SetWindowText(m_sOriginalText.c_str());
493 return CEdit::PreTranslateMessage(pMsg);
497 * @brief Set the control to look active/inactive.
498 * This function sets control to look like an active control. We don't
499 * have real focus on this control, but editor pane below it. However
500 * for user this active look informs which editor pane is active.
501 * @param [in] bActive If `true` set control look like active control.
503 void CFilepathEdit::SetActive(bool bActive)
507 if (m_hWnd == nullptr)
511 GetWindowRect(&rcWnd);
515 SetTextColor(::GetSysColor(m_bInEditing ? COLOR_WINDOWTEXT : COLOR_CAPTIONTEXT));
519 SetTextColor(::GetSysColor(m_bInEditing ? COLOR_WINDOWTEXT : COLOR_INACTIVECAPTIONTEXT));
521 SetBackColor(MakeBackColor(bActive, m_bInEditing));
522 RedrawWindow(nullptr, nullptr, RDW_FRAME | RDW_INVALIDATE);
526 * @brief Set control's colors.
527 * @param [in] pDC pointer to device context.
528 * @param [in] nCtlColor Control color to set.
529 * @note Parameter @p nCtlColor is not used but must be present as this method
530 * is called by framework.
531 * @return Brush for background.
533 HBRUSH CFilepathEdit::CtlColor(CDC* pDC, UINT nCtlColor)
535 UNUSED_ALWAYS(nCtlColor);
536 // Return a non-`nullptr` brush if the parent's
537 //handler should not be called
540 pDC->SetTextColor(m_crText);
542 //set the text's background color
543 pDC->SetBkColor(m_crBackGnd);
545 //return the brush used for background this sets control background
550 * @brief Set control's bacground color.
551 * @param [in] rgb Color to set as background color.
553 void CFilepathEdit::SetBackColor(COLORREF rgb)
555 //set background color ref (used for text's background)
559 if (m_brBackGnd.GetSafeHandle())
560 m_brBackGnd.DeleteObject();
561 //set brush to new color
562 m_brBackGnd.CreateSolidBrush(rgb);
569 * @brief Set control's text color.
570 * @param [in] Color to set as text color.
572 void CFilepathEdit::SetTextColor(COLORREF rgb)