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.
14 #include "FilepathEdit.h"
17 #include "ClipBoard.h"
25 static int FormatFilePathForDisplayWidth(CDC * pDC, int maxWidth, String & sFilepath);
27 BEGIN_MESSAGE_MAP(CFilepathEdit, CEdit)
29 ON_WM_CTLCOLOR_REFLECT()
31 ON_COMMAND(ID_EDIT_COPY, OnEditCopy)
36 * @brief Format the path for display in header control.
38 * Formats path so it fits to given length, tries to end lines after
41 * @param [in] pDC Pointer to draw context.
42 * @param [in] maxWidth Maximum width of the string in the GUI.
43 * @param [in,out] sFilepath:
44 * - in: string to format
45 * - out: formatted string
46 * @return Number of lines path is splitted to.
48 static int FormatFilePathForDisplayWidth(CDC * pDC, int maxWidth, String & sFilepath)
57 // find the next truncation point
59 size_t iEndMax = sFilepath.length() - iBegin + 1;
62 size_t iEnd = (iEndMin + iEndMax) / 2;
65 line = sFilepath.substr(iBegin, iEnd);
66 int width = (pDC->GetTextExtent(line.c_str())).cx;
72 ASSERT(iEndMax == iEndMin+1);
74 // here iEndMin is the last character displayed in maxWidth
76 // exit the loop if we can display the remaining characters with no truncation
77 if (iBegin + iEndMin == sFilepath.length())
80 // truncate the text to the previous "\" if possible
81 line = sFilepath.substr(iBegin, iEndMin);
82 size_t lastSlash = line.rfind('\\');
83 if (lastSlash != String::npos)
84 iEndMin = lastSlash + 1;
86 sFilepath.insert(iBegin + iEndMin, _T("\n"));
87 iBegin += iEndMin + 2;
96 * Set text color to black and background white by default.
98 CFilepathEdit::CFilepathEdit()
99 : m_crBackGnd(RGB(255, 255, 255))
100 , m_crText(RGB(0,0,0))
106 * @brief Subclass the control.
107 * @param [in] nID ID of the control to subclass.
108 * @param [in] pParent Parent control of the control to subclass.
109 * @return `true` if succeeded, `false` otherwise.
111 bool CFilepathEdit::SubClassEdit(UINT nID, CWnd* pParent)
114 return SubclassDlgItem(nID, pParent);
118 * @brief Set the text to show in the control.
119 * This function sets the text (original text) to show in the control.
120 * The control may modify the text for displaying in the GUI.
122 void CFilepathEdit::SetOriginalText(const String& sString)
124 if (m_sOriginalText.compare(sString) == 0)
127 m_sOriginalText = sString;
129 RefreshDisplayText();
133 * @brief Re-format the displayed text and update GUI.
134 * This method formats the visible text from original text.
136 void CFilepathEdit::RefreshDisplayText()
138 String line = m_sOriginalText;
140 // we want to keep the first and the last path component, and in between,
141 // as much characters as possible from the right
142 // PathCompactPath keeps, in between, as much characters as possible from the left
143 // so we reverse everything between the first and the last component before calling PathCompactPath
144 size_t iBeginLast = line.rfind('\\');
145 size_t iEndIntro = line.find('\\');
146 if (iBeginLast != String::npos && iEndIntro != iBeginLast)
148 String textToReverse = line.substr(iEndIntro + 1, iBeginLast -
150 std::reverse(textToReverse.begin(), textToReverse.end());
151 line = line.substr(0, iEndIntro + 1) + textToReverse + line.substr(iBeginLast);
154 // get a device context object
156 // and use the correct font
157 CFont *pFontOld = lDC.SelectObject(GetFont());
162 // take GetBuffer (lenght +3) to count for ellipsis
163 std::vector<TCHAR> tmp(line.length() + 4);
164 std::copy(line.begin(), line.end(), tmp.begin());
165 PathCompactPath(lDC.GetSafeHdc(), &tmp[0], rect.Width());
169 lDC.SelectObject(pFontOld);
171 // we reverse back everything between the first and the last component
172 // it works OK as "..." reversed = "..." again
173 iBeginLast = line.rfind('\\');
174 iEndIntro = line.find('\\');
175 if (iBeginLast != String::npos && iEndIntro != iBeginLast)
177 String textToReverse = line.substr(iEndIntro + 1, iBeginLast -
179 std::reverse(textToReverse.begin(), textToReverse.end());
180 line = line.substr(0, iEndIntro + 1) + textToReverse + line.substr(iBeginLast);
183 SetWindowText(line.c_str());
187 * @brief Updates and returns the tooltip for this edit box
189 const String& CFilepathEdit::GetUpdatedTipText(CDC * pDC, int maxWidth)
191 GetOriginalText(m_sToolTipString);
192 FormatFilePathForDisplayWidth(pDC, maxWidth, m_sToolTipString);
193 return m_sToolTipString;
197 * @brief retrieve text from the OriginalText
199 * @note The standard Copy function works with the (compacted) windowText
201 void CFilepathEdit::CustomCopy(size_t iBegin, size_t iEnd /*=-1*/)
203 if (iEnd == String::npos)
204 iEnd = m_sOriginalText.length();
206 PutToClipboard(m_sOriginalText.substr(iBegin, iEnd - iBegin), m_hWnd);
210 * @brief Format the context menu.
212 void CFilepathEdit::OnContextMenu(CWnd*, CPoint point)
215 if (point.x == -1 && point.y == -1){
216 //keystroke invocation
219 ClientToScreen(rect);
221 point = rect.TopLeft();
226 VERIFY(menu.LoadMenu(IDR_POPUP_EDITOR_HEADERBAR));
227 theApp.TranslateMenu(menu.m_hMenu);
229 BCMenu* pPopup = static_cast<BCMenu *>(menu.GetSubMenu(0));
230 ASSERT(pPopup != nullptr);
232 DWORD sel = GetSel();
233 if (HIWORD(sel) == LOWORD(sel))
234 pPopup->EnableMenuItem(ID_EDITOR_COPY, MF_GRAYED);
235 if (paths::EndsWithSlash(m_sOriginalText))
236 // no filename, we have to disable the unwanted menu entry
237 pPopup->EnableMenuItem(ID_EDITOR_COPY_FILENAME, MF_GRAYED);
239 // invoke context menu
240 // we don't want to use the main application handlers, so we
241 // use flags TPM_NONOTIFY | TPM_RETURNCMD
242 // and handle the command after TrackPopupMenu
243 int command = pPopup->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON |
244 TPM_NONOTIFY | TPM_RETURNCMD, point.x, point.y, AfxGetMainWnd());
246 // compute the beginning of the text to copy (in OriginalText)
253 case ID_EDITOR_COPY_FILENAME:
255 size_t lastSlash = m_sOriginalText.rfind('\\');
256 if (lastSlash == String::npos)
257 lastSlash = m_sOriginalText.rfind('/');
258 if (lastSlash != String::npos)
259 iBegin = lastSlash+1;
264 case ID_EDITOR_COPY_PATH:
265 // pass the heading "*" for modified files
266 if (m_sOriginalText.at(0) == '*')
279 static COLORREF GetDarkenColor(COLORREF a, double r)
281 const int R = static_cast<int>(GetRValue(a) * r);
282 const int G = static_cast<int>(GetGValue(a) * r);
283 const int B = static_cast<int>(GetBValue(a) * r);
287 void CFilepathEdit::OnNcPaint()
291 const int margin = 4;
293 rect.OffsetRect(-rect.TopLeft());
294 dc.FillSolidRect(CRect(rect.left, rect.top, rect.left + margin, rect.bottom), GetDarkenColor(m_crBackGnd, 0.98));
295 dc.FillSolidRect(CRect(rect.left, rect.top, rect.left + 1, rect.bottom), GetDarkenColor(m_crBackGnd, 0.96));
296 dc.FillSolidRect(CRect(rect.right - margin, rect.top, rect.right, rect.bottom), m_crBackGnd);
297 dc.FillSolidRect(CRect(rect.left + 1, rect.top, rect.right, rect.top + margin), GetDarkenColor(m_crBackGnd, 0.98));
298 dc.FillSolidRect(CRect(rect.left, rect.top, rect.right, rect.top + 1), GetDarkenColor(m_crBackGnd, 0.96));
299 dc.FillSolidRect(CRect(rect.left + margin, rect.bottom - margin, rect.right, rect.bottom), m_crBackGnd);
302 void CFilepathEdit::OnEditCopy()
304 int nStartChar, nEndChar;
305 GetSel(nStartChar, nEndChar);
306 if (nStartChar == nEndChar)
309 if (nStartChar == nEndChar)
310 SetSel(nStartChar, nEndChar);
313 BOOL CFilepathEdit::PreTranslateMessage(MSG *pMsg)
315 if (pMsg->message >= WM_KEYFIRST && pMsg->message <= WM_KEYLAST)
317 if (::TranslateAccelerator (m_hWnd, static_cast<CFrameWnd *>(AfxGetMainWnd())->GetDefaultAccelerator(), pMsg))
320 return CEdit::PreTranslateMessage(pMsg);
324 * @brief Set the control to look active/inactive.
325 * This function sets control to look like an active control. We don't
326 * have real focus on this control, but editor pane below it. However
327 * for user this active look informs which editor pane is active.
328 * @param [in] bActive If `true` set control look like active control.
330 void CFilepathEdit::SetActive(bool bActive)
334 if (m_hWnd == nullptr)
338 GetWindowRect(&rcWnd);
342 SetTextColor(::GetSysColor(COLOR_CAPTIONTEXT));
343 SetBackColor(::GetSysColor(COLOR_ACTIVECAPTION));
347 SetTextColor(::GetSysColor(COLOR_INACTIVECAPTIONTEXT));
348 SetBackColor(::GetSysColor(COLOR_INACTIVECAPTION));
350 RedrawWindow(nullptr, nullptr, RDW_FRAME | RDW_INVALIDATE);
354 * @brief Set control's colors.
355 * @param [in] pDC pointer to device context.
356 * @param [in] nCtlColor Control color to set.
357 * @note Parameter @p nCtlColor is not used but must be present as this method
358 * is called by framework.
359 * @return Brush for background.
361 HBRUSH CFilepathEdit::CtlColor(CDC* pDC, UINT nCtlColor)
363 UNUSED_ALWAYS(nCtlColor);
364 // Return a non-`nullptr` brush if the parent's
365 //handler should not be called
368 pDC->SetTextColor(m_crText);
370 //set the text's background color
371 pDC->SetBkColor(m_crBackGnd);
373 //return the brush used for background this sets control background
378 * @brief Set control's bacground color.
379 * @param [in] rgb Color to set as background color.
381 void CFilepathEdit::SetBackColor(COLORREF rgb)
383 //set background color ref (used for text's background)
387 if (m_brBackGnd.GetSafeHandle())
388 m_brBackGnd.DeleteObject();
389 //set brush to new color
390 m_brBackGnd.CreateSolidBrush(rgb);
397 * @brief Set control's text color.
398 * @param [in] Color to set as text color.
400 void CFilepathEdit::SetTextColor(COLORREF rgb)