OSDN Git Service

Allow NUL and \\.\NUL in paths specified as command line arguments (#2056)
[winmerge-jp/winmerge-jp.git] / Src / FilepathEdit.cpp
1 /////////////////////////////////////////////////////////////////////////////
2 //    WinMerge:  an interactive diff/merge utility
3 //    Copyright (C) 1997-2000  Thingamahoochie Software
4 //    Author: Dean Grimm
5 //    SPDX-License-Identifier: GPL-2.0-or-later
6 /////////////////////////////////////////////////////////////////////////////
7 /** 
8  * @file  FilePathEdit.cpp
9  *
10  * @brief Implementation of the CFilepathEdit class.
11  */
12
13 #include "stdafx.h"
14 #include <Shlwapi.h>
15 #include "FilepathEdit.h"
16 #include "Merge.h"
17 #include "ClipBoard.h"
18 #include "FileOrFolderSelect.h"
19 #include "Win_VersionHelper.h"
20 #include "paths.h"
21 #include "cecolor.h"
22
23 #ifdef _DEBUG
24 #define new DEBUG_NEW
25 #endif
26
27 static int FormatFilePathForDisplayWidth(CDC * pDC, int maxWidth, String & sFilepath);
28
29 BEGIN_MESSAGE_MAP(CFilepathEdit, CEdit)
30         ON_WM_CONTEXTMENU()
31         ON_WM_CTLCOLOR_REFLECT()
32         ON_WM_NCPAINT()
33         ON_WM_PAINT()
34         ON_WM_SETCURSOR()
35         ON_WM_KILLFOCUS()
36         ON_WM_LBUTTONDOWN()
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)
43 END_MESSAGE_MAP()
44
45
46 /** 
47  * @brief Format the path for display in header control. 
48  *
49  * Formats path so it fits to given length, tries to end lines after
50  * slash characters.
51  *
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.
58  */
59 static int FormatFilePathForDisplayWidth(CDC * pDC, int maxWidth, String & sFilepath)
60 {
61         size_t iBegin = 0;
62         int nLines = 1;
63         
64         while (true)
65         {
66                 String line;
67
68                 // find the next truncation point
69                 size_t iEndMin = 0;
70                 size_t iEndMax = sFilepath.length() - iBegin + 1;
71                 while(1)
72                 {
73                         size_t iEnd = (iEndMin + iEndMax) / 2;
74                         if (iEnd == iEndMin)
75                                 break;
76                         line = sFilepath.substr(iBegin, iEnd);
77                         int width = (pDC->GetTextExtent(line.c_str())).cx;
78                         if (width > maxWidth)
79                                 iEndMax = iEnd;
80                         else
81                                 iEndMin = iEnd;
82                 };
83                 ASSERT(iEndMax == iEndMin+1);
84
85                 // here iEndMin is the last character displayed in maxWidth
86
87                 // exit the loop if we can display the remaining characters with no truncation
88                 if (iBegin + iEndMin == sFilepath.length())
89                         break;
90
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;
96
97                 sFilepath.insert(iBegin + iEndMin, _T("\n"));
98                 iBegin += iEndMin + 2;
99                 nLines ++;
100         }
101
102         return nLines;
103 }
104
105 /**
106  * @brief Constructor.
107  * Set text color to black and background white by default.
108  */
109 CFilepathEdit::CFilepathEdit()
110  : m_crBackGnd(RGB(255, 255, 255))
111  , m_crText(RGB(0,0,0))
112  , m_bActive(false)
113  , m_bInEditing(false)
114  , m_bEnabledFileSelection(false)
115  , m_bEnabledFolderSelection(false)
116 {
117 }
118
119 /**
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.
124  */
125 bool CFilepathEdit::SubClassEdit(UINT nID, CWnd* pParent)
126 {
127         m_bActive = false;
128         return SubclassDlgItem(nID, pParent);
129 };
130
131 /**
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.
135  */
136 void CFilepathEdit::SetOriginalText(const String& sString)
137 {
138         if (m_sOriginalText.compare(sString) == 0)
139                 return;
140
141         m_sOriginalText = sString;
142
143         RefreshDisplayText();
144 }
145
146 /**
147  * @brief Re-format the displayed text and update GUI.
148  * This method formats the visible text from original text.
149  */
150 void CFilepathEdit::RefreshDisplayText()
151 {
152         String line = m_sOriginalText;
153
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)
161         {
162                 String textToReverse = line.substr(iEndIntro + 1, iBeginLast -
163                                 (iEndIntro + 1));
164                 std::reverse(textToReverse.begin(), textToReverse.end());
165                 line = line.substr(0, iEndIntro + 1) + textToReverse + line.substr(iBeginLast);
166         }
167
168         // get a device context object
169         CClientDC lDC(this);
170         // and use the correct font
171         CFont *pFontOld = lDC.SelectObject(GetFont());  
172
173         // compact the path
174         CRect rect;
175         GetRect(rect);
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());
179         line = &tmp[0];
180         
181         // set old font back
182         lDC.SelectObject(pFontOld);
183
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)
189         {
190                 String textToReverse = line.substr(iEndIntro + 1, iBeginLast -
191                                 (iEndIntro+1));
192                 std::reverse(textToReverse.begin(), textToReverse.end());
193                 line = line.substr(0, iEndIntro + 1) + textToReverse + line.substr(iBeginLast);
194         }
195
196         SetWindowText(line.c_str());
197 }
198
199 /**
200  * @brief Updates and returns the tooltip for this edit box
201  */
202 const String& CFilepathEdit::GetUpdatedTipText(CDC * pDC, int maxWidth)
203 {
204         GetOriginalText(m_sToolTipString);
205         FormatFilePathForDisplayWidth(pDC, maxWidth, m_sToolTipString);
206         return m_sToolTipString;
207 }
208
209 /**
210  * @brief retrieve text from the OriginalText
211  *
212  * @note The standard Copy function works with the (compacted) windowText 
213  */
214 void CFilepathEdit::CustomCopy(size_t iBegin, size_t iEnd /*=-1*/)
215 {
216         if (iEnd == String::npos)
217                 iEnd = m_sOriginalText.length();
218
219         PutToClipboard(m_sOriginalText.substr(iBegin, iEnd - iBegin), m_hWnd);
220 }
221
222 /**
223  * @brief Format the context menu.
224  */
225 void CFilepathEdit::OnContextMenu(CWnd* pWnd, CPoint point)
226 {
227         if (m_bInEditing)
228         {
229                 __super::OnContextMenu(pWnd, point);
230         }
231         else
232         {
233                 if (!m_bActive)
234                         SetFocus();
235
236                 if (point.x == -1 && point.y == -1){
237                         //keystroke invocation
238                         CRect rect;
239                         GetClientRect(rect);
240                         ClientToScreen(rect);
241
242                         point = rect.TopLeft();
243                         point.Offset(5, 5);
244                 }
245
246                 CMenu menu;
247                 VERIFY(menu.LoadMenu(IDR_POPUP_EDITOR_HEADERBAR));
248                 theApp.TranslateMenu(menu.m_hMenu);
249
250                 CMenu* pPopup = menu.GetSubMenu(0);
251                 ASSERT(pPopup != nullptr);
252
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);
261
262                 // invoke context menu
263                 pPopup->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, this);
264         }
265 }
266
267 static COLORREF MakeBackColor(bool bActive, bool bInEditing)
268 {
269         if (bActive)
270                 return CEColor::GetIntermediateColor(::GetSysColor(bInEditing ? COLOR_WINDOW : COLOR_ACTIVECAPTION), ::GetSysColor(COLOR_3DFACE), 0.5f);
271         else
272                 return CEColor::GetIntermediateColor(::GetSysColor(bInEditing ? COLOR_WINDOW : COLOR_INACTIVECAPTION), ::GetSysColor(COLOR_3DFACE), 0.5f);
273 }
274
275 void CFilepathEdit::OnNcPaint()
276 {
277         COLORREF crBackGnd = m_bInEditing ? ::GetSysColor(COLOR_ACTIVEBORDER) : m_crBackGnd;
278         CWindowDC dc(this);
279         CRect rect;
280         const int lpx = dc.GetDeviceCaps(LOGPIXELSX);
281         auto pointToPixel = [lpx](int point) { return MulDiv(point, lpx, 72); };
282         const int margin = pointToPixel(3);
283
284         GetWindowRect(rect);
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);
292 }
293
294 void CFilepathEdit::OnPaint()
295 {
296         __super::OnPaint();
297         if (!m_bInEditing)
298         {
299                 CClientDC dc(this);
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);
309         }
310 }
311
312 void CFilepathEdit::OnKillFocus(CWnd* pNewWnd)
313 {
314         __super::OnKillFocus(pNewWnd);
315         if (m_bInEditing)
316         {
317                 m_bInEditing = false;
318                 SetTextColor(::GetSysColor(COLOR_WINDOWTEXT));
319                 SetBackColor(MakeBackColor(false, false));
320                 RedrawWindow(nullptr, nullptr, RDW_FRAME | RDW_INVALIDATE);
321                 SetReadOnly(true);
322                 SetWindowText(m_sOriginalText.c_str());
323         }
324 }
325
326 CRect CFilepathEdit::GetMenuCharRect(CDC* pDC)
327 {
328         CRect rc;
329         GetClientRect(rc);
330         int charWidth;
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);
335         return rc;
336 }
337
338 void CFilepathEdit::OnLButtonDown(UINT nFlags, CPoint point)
339 {
340         CClientDC dc(this);
341         CRect rc = GetMenuCharRect(&dc);
342         if (PtInRect(&rc, point))
343         {
344                 ClientToScreen(&point);
345                 OnContextMenu(this, point);
346         }
347         else
348         {
349                 __super::OnLButtonDown(nFlags, point);
350         }
351 }
352
353 BOOL CFilepathEdit::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
354 {
355         CPoint pt;
356         GetCursorPos(&pt);
357         ScreenToClient(&pt);
358         CClientDC dc(this);
359         CRect rc = GetMenuCharRect(&dc);
360         if (PtInRect(&rc, pt))
361         {
362                 SetCursor(LoadCursor(nullptr, IDC_ARROW));
363                 return TRUE;
364         }
365         return __super::OnSetCursor(pWnd, nHitTest, message);
366 }
367
368 void CFilepathEdit::OnEditCopy()
369 {
370         int nStartChar, nEndChar;
371         GetSel(nStartChar, nEndChar);
372         if (nStartChar == nEndChar)
373                 SetSel(0, -1);
374         Copy();
375         if (nStartChar == nEndChar)
376                 SetSel(nStartChar, nEndChar);
377 }
378
379 void CFilepathEdit::OnEditPaste()
380 {
381         Paste();
382 }
383
384 void CFilepathEdit::OnEditCut()
385 {
386         Cut();
387 }
388
389 void CFilepathEdit::OnEditUndo()
390 {
391         Undo();
392 }
393
394 void CFilepathEdit::OnEditSelectAll()
395 {
396         SetSel(0, -1);
397 }
398
399 void CFilepathEdit::OnContextMenuSelected(UINT nID)
400 {
401         // compute the beginning of the text to copy (in OriginalText)
402         size_t iBegin = 0;
403         switch (nID)
404         {
405         case ID_EDITOR_COPY:
406                 Copy();
407                 return;
408         case ID_EDITOR_COPY_FILENAME:
409                 {
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;
415                 else
416                         iBegin = 0;
417                 }
418                 break;
419         case ID_EDITOR_COPY_PATH:
420                 // pass the heading "*" for modified files
421                 if (m_sOriginalText.at(0) == '*')
422                         iBegin = 2;
423                 else
424                         iBegin = 0;
425                 break;
426         case ID_EDITOR_EDIT_CAPTION:
427                 m_bInEditing = true;
428                 SetReadOnly(false);
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());
433                 SetSel(0, -1);
434                 SetFocus();
435                 return;
436         case ID_EDITOR_SELECT_FILE:
437         {
438                 CString text;
439                 GetWindowText(text);
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());
446                 else
447                         selected = SelectFolder(m_sFilepath, dir.c_str(), _T(""), GetSafeHwnd());
448                 if (selected)
449                         GetParent()->PostMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), EN_USER_FILE_SELECTED), (LPARAM)m_hWnd);
450                 return;
451         }
452         default:
453                 return;
454         }
455         
456         CustomCopy(iBegin);
457 }
458
459 BOOL CFilepathEdit::PreTranslateMessage(MSG *pMsg)
460 {
461         if (pMsg->message >= WM_KEYFIRST && pMsg->message <= WM_KEYLAST)
462         {
463                 if (::TranslateAccelerator (m_hWnd, static_cast<CFrameWnd *>(AfxGetMainWnd())->GetDefaultAccelerator(), pMsg))
464                         return TRUE;
465         }
466         if (pMsg->message == WM_KEYDOWN)
467         {
468                 if (pMsg->wParam == VK_RETURN)
469                 {
470                         m_bInEditing = false;
471                         SetTextColor(::GetSysColor(COLOR_CAPTIONTEXT));
472                         SetBackColor(MakeBackColor(true, false));
473                         RedrawWindow(nullptr, nullptr, RDW_FRAME | RDW_INVALIDATE);
474                         SetReadOnly();
475                         CString sText;
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);
481                         return TRUE;
482                 }
483                 if (pMsg->wParam == VK_ESCAPE)
484                 {
485                         m_bInEditing = false;
486                         SetTextColor(GetSysColor(COLOR_CAPTIONTEXT));
487                         SetBackColor(GetSysColor(COLOR_INACTIVECAPTION));
488                         RedrawWindow(nullptr, nullptr, RDW_FRAME | RDW_INVALIDATE);
489                         SetReadOnly();
490                         SetWindowText(m_sOriginalText.c_str());
491                 }
492         }
493         return CEdit::PreTranslateMessage(pMsg);
494 }
495
496 /**
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.
502  */
503 void CFilepathEdit::SetActive(bool bActive)
504 {
505         m_bActive = bActive;
506
507         if (m_hWnd == nullptr)
508                 return;
509
510         CRect rcWnd;
511         GetWindowRect(&rcWnd);
512
513         if (bActive)
514         {
515                 SetTextColor(::GetSysColor(m_bInEditing ? COLOR_WINDOWTEXT : COLOR_CAPTIONTEXT));
516         }
517         else
518         {
519                 SetTextColor(::GetSysColor(m_bInEditing ? COLOR_WINDOWTEXT : COLOR_INACTIVECAPTIONTEXT));
520         }
521         SetBackColor(MakeBackColor(bActive, m_bInEditing));
522         RedrawWindow(nullptr, nullptr, RDW_FRAME | RDW_INVALIDATE);
523 }
524
525 /**
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.
532  */
533 HBRUSH CFilepathEdit::CtlColor(CDC* pDC, UINT nCtlColor) 
534 {
535         UNUSED_ALWAYS(nCtlColor);
536         // Return a non-`nullptr` brush if the parent's 
537         //handler should not be called
538
539         //set text color
540         pDC->SetTextColor(m_crText);
541
542         //set the text's background color
543         pDC->SetBkColor(m_crBackGnd);
544
545         //return the brush used for background this sets control background
546         return m_brBackGnd;
547 }
548
549 /**
550  * @brief Set control's bacground color.
551  * @param [in] rgb Color to set as background color.
552  */
553 void CFilepathEdit::SetBackColor(COLORREF rgb)
554 {
555         //set background color ref (used for text's background)
556         m_crBackGnd = rgb;
557         
558         //free brush
559         if (m_brBackGnd.GetSafeHandle())
560                 m_brBackGnd.DeleteObject();
561         //set brush to new color
562         m_brBackGnd.CreateSolidBrush(rgb);
563         
564         //redraw
565         Invalidate(TRUE);
566 }
567
568 /**
569  * @brief Set control's text color.
570  * @param [in] Color to set as text color.
571  */
572 void CFilepathEdit::SetTextColor(COLORREF rgb)
573 {
574         //set text color ref
575         m_crText = rgb;
576
577         //redraw
578         Invalidate(TRUE);
579 }