OSDN Git Service

Add /self-compare command line option
[winmerge-jp/winmerge-jp.git] / Src / OpenView.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  OpenView.cpp
9  *
10  * @brief Implementation of the COpenView class
11  */
12
13 #include "stdafx.h"
14 #include "OpenView.h"
15 #include <vector>
16 #include <sys/stat.h>
17 #include "UnicodeString.h"
18 #include "Merge.h"
19 #include "OpenDoc.h"
20 #include "ProjectFile.h"
21 #include "paths.h"
22 #include "SelectUnpackerDlg.h"
23 #include "OptionsDef.h"
24 #include "MainFrm.h"
25 #include "OptionsMgr.h"
26 #include "FileOrFolderSelect.h"
27 #include "7zCommon.h"
28 #include "Constants.h"
29 #include "Bitmap.h"
30 #include "DropHandler.h"
31 #include "FileFilterHelper.h"
32 #include "Plugins.h"
33 #include "BCMenu.h"
34 #include "LanguageSelect.h"
35 #include "Win_VersionHelper.h"
36
37 #ifdef _DEBUG
38 #define new DEBUG_NEW
39 #endif
40
41 #ifndef BCN_DROPDOWN
42 #define BCN_DROPDOWN            (BCN_FIRST + 0x0002)
43 #endif
44
45 // Timer ID and timeout for delaying path validity check
46 const UINT IDT_CHECKFILES = 1;
47 const UINT IDT_RETRY = 2;
48 const UINT CHECKFILES_TIMEOUT = 1000; // milliseconds
49 const int RETRY_MAX = 3;
50 static const TCHAR EMPTY_EXTENSION[] = _T(".*");
51
52 /** @brief Location for Open-dialog specific help to open. */
53 static TCHAR OpenDlgHelpLocation[] = _T("::/htmlhelp/Open_paths.html");
54
55 // COpenView
56
57 IMPLEMENT_DYNCREATE(COpenView, CFormView)
58
59 BEGIN_MESSAGE_MAP(COpenView, CFormView)
60         //{{AFX_MSG_MAP(COpenView)
61         ON_CONTROL_RANGE(BN_CLICKED, IDC_PATH0_BUTTON, IDC_PATH2_BUTTON, OnPathButton)
62         ON_BN_CLICKED(IDC_SWAP01_BUTTON, (OnSwapButton<IDC_PATH0_COMBO, IDC_PATH1_COMBO>))
63         ON_BN_CLICKED(IDC_SWAP12_BUTTON, (OnSwapButton<IDC_PATH1_COMBO, IDC_PATH2_COMBO>))
64         ON_BN_CLICKED(IDC_SWAP02_BUTTON, (OnSwapButton<IDC_PATH0_COMBO, IDC_PATH2_COMBO>))
65         ON_CONTROL_RANGE(CBN_SELCHANGE, IDC_PATH0_COMBO, IDC_PATH2_COMBO, OnSelchangePathCombo)
66         ON_CONTROL_RANGE(CBN_EDITCHANGE, IDC_PATH0_COMBO, IDC_PATH2_COMBO, OnEditEvent)
67         ON_BN_CLICKED(IDC_SELECT_UNPACKER, OnSelectUnpacker)
68         ON_CBN_SELENDCANCEL(IDC_PATH0_COMBO, UpdateButtonStates)
69         ON_CBN_SELENDCANCEL(IDC_PATH1_COMBO, UpdateButtonStates)
70         ON_CBN_SELENDCANCEL(IDC_PATH2_COMBO, UpdateButtonStates)
71         ON_NOTIFY_RANGE(CBEN_BEGINEDIT, IDC_PATH0_COMBO, IDC_PATH2_COMBO, OnSetfocusPathCombo)
72         ON_NOTIFY_RANGE(CBEN_DRAGBEGIN, IDC_PATH0_COMBO, IDC_PATH2_COMBO, OnDragBeginPathCombo)
73         ON_WM_TIMER()
74         ON_BN_CLICKED(IDC_SELECT_FILTER, OnSelectFilter)
75         ON_BN_CLICKED(IDC_OPTIONS, OnOptions)
76         ON_NOTIFY(BCN_DROPDOWN, IDC_OPTIONS, OnDropDownOptions)
77         ON_WM_ACTIVATE()
78         ON_COMMAND(ID_LOAD_PROJECT, OnLoadProject)
79         ON_COMMAND(ID_SAVE_PROJECT, OnSaveProject)
80         ON_COMMAND(ID_FILE_SAVE, OnSaveProject)
81         ON_NOTIFY(BCN_DROPDOWN, ID_SAVE_PROJECT, OnDropDownSaveProject)
82         ON_COMMAND(IDOK, OnOK)
83         ON_COMMAND(IDCANCEL, OnCancel)
84         ON_COMMAND(ID_HELP, OnHelp)
85         ON_COMMAND(ID_EDIT_COPY, OnEditAction<WM_COPY>)
86         ON_COMMAND(ID_EDIT_PASTE, OnEditAction<WM_PASTE>)
87         ON_COMMAND(ID_EDIT_CUT, OnEditAction<WM_CUT>)
88         ON_COMMAND(ID_EDIT_UNDO, OnEditAction<WM_UNDO>)
89         ON_COMMAND(ID_EDIT_SELECT_ALL, (OnEditAction<EM_SETSEL, 0, -1>))
90         ON_MESSAGE(WM_USER + 1, OnUpdateStatus)
91         ON_WM_PAINT()
92         ON_WM_LBUTTONUP()
93         ON_WM_MOUSEMOVE()
94         ON_WM_WINDOWPOSCHANGING()
95         ON_WM_WINDOWPOSCHANGED()
96         ON_WM_NCHITTEST()
97         ON_WM_DESTROY()
98         //}}AFX_MSG_MAP
99 END_MESSAGE_MAP()
100
101 // COpenView construction/destruction
102
103 COpenView::COpenView()
104         : CFormView(COpenView::IDD)
105         , m_pUpdateButtonStatusThread(nullptr)
106         , m_bRecurse(false)
107         , m_pDropHandler(nullptr)
108         , m_dwFlags()
109         , m_bAutoCompleteReady()
110         , m_bReadOnly {false, false, false}
111         , m_hIconRotate(theApp.LoadIcon(IDI_ROTATE2))
112         , m_hCursorNo(LoadCursor(nullptr, IDC_NO))
113         , m_retryCount(0)
114 {
115         // CWnd::EnableScrollBarCtrl() called inside CScrollView::UpdateBars() is quite slow.
116         // Therefore, set m_bInsideUpdate = TRUE so that CScrollView::UpdateBars() does almost nothing.
117         m_bInsideUpdate = TRUE;
118 }
119
120 COpenView::~COpenView()
121 {
122         TerminateThreadIfRunning();
123 }
124
125 void COpenView::DoDataExchange(CDataExchange* pDX)
126 {
127         CFormView::DoDataExchange(pDX);
128         //{{AFX_DATA_MAP(COpenView)
129         DDX_Control(pDX, IDC_EXT_COMBO, m_ctlExt);
130         DDX_Control(pDX, IDC_PATH0_COMBO, m_ctlPath[0]);
131         DDX_Control(pDX, IDC_PATH1_COMBO, m_ctlPath[1]);
132         DDX_Control(pDX, IDC_PATH2_COMBO, m_ctlPath[2]);
133         DDX_CBStringExact(pDX, IDC_PATH0_COMBO, m_strPath[0]);
134         DDX_CBStringExact(pDX, IDC_PATH1_COMBO, m_strPath[1]);
135         DDX_CBStringExact(pDX, IDC_PATH2_COMBO, m_strPath[2]);
136         DDX_Check(pDX, IDC_PATH0_READONLY, m_bReadOnly[0]);
137         DDX_Check(pDX, IDC_PATH1_READONLY, m_bReadOnly[1]);
138         DDX_Check(pDX, IDC_PATH2_READONLY, m_bReadOnly[2]);
139         DDX_Check(pDX, IDC_RECURS_CHECK, m_bRecurse);
140         DDX_CBStringExact(pDX, IDC_EXT_COMBO, m_strExt);
141         DDX_Text(pDX, IDC_UNPACKER_EDIT, m_strUnpacker);
142         //}}AFX_DATA_MAP
143 }
144
145 BOOL COpenView::PreCreateWindow(CREATESTRUCT& cs)
146 {
147         // TODO: Modify the Window class or styles here by modifying
148         //  the CREATESTRUCT cs
149         cs.style &= ~WS_BORDER;
150         cs.dwExStyle &= ~WS_EX_CLIENTEDGE;
151         return CFormView::PreCreateWindow(cs);
152 }
153
154 void COpenView::OnInitialUpdate()
155 {
156         if (!IsVista_OrGreater())
157         {
158                 // fallback for XP 
159                 SendDlgItemMessage(IDC_OPTIONS, BM_SETSTYLE, BS_PUSHBUTTON, TRUE);
160                 SendDlgItemMessage(ID_SAVE_PROJECT, BM_SETSTYLE, BS_PUSHBUTTON, TRUE);
161         }
162
163         m_sizeOrig = GetTotalSize();
164
165         theApp.TranslateDialog(m_hWnd);
166
167         if (!LoadImageFromResource(m_image, MAKEINTRESOURCE(IDR_LOGO), _T("IMAGE")))
168         {
169                 // FIXME: LoadImageFromResource() seems to fail when running on Wine 5.0.
170                 m_image.Create(1, 1, 24, 0);
171         }
172
173         CFormView::OnInitialUpdate();
174
175         // set caption to "swap paths" button
176         LOGFONT lf;
177         GetDlgItem(IDC_SWAP01_BUTTON)->GetFont()->GetObject(sizeof(lf), &lf);
178         lf.lfCharSet = SYMBOL_CHARSET;
179         lstrcpy(lf.lfFaceName, _T("Wingdings"));
180         m_fontSwapButton.CreateFontIndirect(&lf);
181         const int ids[] = {IDC_SWAP01_BUTTON, IDC_SWAP12_BUTTON, IDC_SWAP02_BUTTON};
182         for (int i = 0; i < sizeof(ids)/sizeof(ids[0]); ++i)
183         {
184                 GetDlgItem(ids[i])->SetFont(&m_fontSwapButton);
185                 SetDlgItemText(ids[i], _T("\xf4"));
186         }
187
188         m_constraint.InitializeCurrentSize(this);
189         m_constraint.InitializeSpecificSize(this, m_sizeOrig.cx, m_sizeOrig.cy);
190         m_constraint.SetMaxSizePixels(-1, m_sizeOrig.cy);
191         m_constraint.SetScrollScale(this, 1.0, 1.0);
192         m_constraint.SetSizeGrip(prdlg::CMoveConstraint::SG_NONE);
193         m_constraint.DisallowHeightGrowth();
194         //m_constraint.SubclassWnd(); // install subclassing
195
196         m_constraint.LoadPosition(_T("ResizeableDialogs"), _T("OpenView"), false); // persist size via registry
197         m_constraint.UpdateSizes();
198
199         COpenDoc *pDoc = GetDocument();
200
201         CString strTitle;
202         GetWindowText(strTitle);
203         pDoc->SetTitle(strTitle);
204
205         m_files = pDoc->m_files;
206         m_bRecurse = pDoc->m_bRecurse;
207         m_strExt = pDoc->m_strExt;
208         m_strUnpacker = pDoc->m_strUnpacker;
209         m_infoHandler = pDoc->m_infoHandler;
210         m_dwFlags[0] = pDoc->m_dwFlags[0];
211         m_dwFlags[1] = pDoc->m_dwFlags[1];
212         m_dwFlags[2] = pDoc->m_dwFlags[2];
213
214         m_ctlPath[0].SetFileControlStates();
215         m_ctlPath[1].SetFileControlStates(true);
216         m_ctlPath[2].SetFileControlStates(true);
217
218         for (int file = 0; file < m_files.GetSize(); file++)
219         {
220                 m_strPath[file] = m_files[file];
221                 m_ctlPath[file].SetWindowText(m_files[file].c_str());
222                 m_bReadOnly[file] = (m_dwFlags[file] & FFILEOPEN_READONLY) != 0;
223         }
224
225         m_ctlPath[0].AttachSystemImageList();
226         m_ctlPath[1].AttachSystemImageList();
227         m_ctlPath[2].AttachSystemImageList();
228         LoadComboboxStates();
229
230         bool bDoUpdateData = true;
231         for (auto& strPath: m_strPath)
232         {
233                 if (!strPath.empty())
234                         bDoUpdateData = false;
235         }
236         UpdateData(bDoUpdateData);
237
238         String filterNameOrMask = theApp.m_pGlobalFileFilter->GetFilterNameOrMask();
239         bool bMask = theApp.m_pGlobalFileFilter->IsUsingMask();
240
241         if (!bMask)
242         {
243                 String filterPrefix = _("[F] ");
244                 filterNameOrMask = filterPrefix + filterNameOrMask;
245         }
246
247         int ind = m_ctlExt.FindStringExact(0, filterNameOrMask.c_str());
248         if (ind != CB_ERR)
249                 m_ctlExt.SetCurSel(ind);
250         else
251         {
252                 ind = m_ctlExt.InsertString(0, filterNameOrMask.c_str());
253                 if (ind != CB_ERR)
254                         m_ctlExt.SetCurSel(ind);
255                 else
256                         LogErrorString(_T("Failed to add string to filters combo list!"));
257         }
258
259         if (!GetOptionsMgr()->GetBool(OPT_VERIFY_OPEN_PATHS))
260         {
261                 EnableDlgItem(IDOK, true);
262                 EnableDlgItem(IDC_UNPACKER_EDIT, true);
263                 EnableDlgItem(IDC_SELECT_UNPACKER, true);
264         }
265
266         UpdateButtonStates();
267
268         bool bOverwriteRecursive = false;
269         if (m_dwFlags[0] & FFILEOPEN_PROJECT || m_dwFlags[1] & FFILEOPEN_PROJECT)
270                 bOverwriteRecursive = true;
271         if (m_dwFlags[0] & FFILEOPEN_CMDLINE || m_dwFlags[1] & FFILEOPEN_CMDLINE)
272                 bOverwriteRecursive = true;
273         if (!bOverwriteRecursive)
274                 m_bRecurse = GetOptionsMgr()->GetBool(OPT_CMP_INCLUDE_SUBDIRS);
275
276         m_strUnpacker = m_infoHandler.m_PluginName;
277         UpdateData(FALSE);
278         SetStatus(IDS_OPEN_FILESDIRS);
279         SetUnpackerStatus(IDS_USERCHOICE_NONE); 
280
281         m_pDropHandler = new DropHandler(std::bind(&COpenView::OnDropFiles, this, std::placeholders::_1));
282         RegisterDragDrop(m_hWnd, m_pDropHandler);
283 }
284
285 void COpenView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint)
286 {
287         m_bRecurse = GetDocument()->m_bRecurse;
288         UpdateData(FALSE);
289 }
290
291 // COpenView diagnostics
292
293 #ifdef _DEBUG
294 COpenDoc* COpenView::GetDocument() const // non-debug version is inline
295 {
296         ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(COpenDoc)));
297         return (COpenDoc*)m_pDocument;
298 }
299 #endif //_DEBUG
300
301 /////////////////////////////////////////////////////////////////////////////
302 // COpenView message handlers
303
304 void COpenView::OnPaint()
305 {
306         CPaintDC dc(this);
307         CRect rc;
308         GetClientRect(&rc);
309
310         // Draw the logo image
311         CSize size{ m_image.GetWidth(), m_image.GetHeight() };
312         CRect rcImage(0, 0, size.cx * GetSystemMetrics(SM_CXSMICON) / 16, size.cy * GetSystemMetrics(SM_CYSMICON) / 16);
313         m_image.Draw(dc.m_hDC, rcImage, Gdiplus::InterpolationModeBicubic);
314         // And extend it to the Right boundary
315         dc.PatBlt(rcImage.Width(), 0, rc.Width() - rcImage.Width(), rcImage.Height(), PATCOPY);
316
317         // Draw the resize gripper in the Lower Right corner.
318         CRect rcGrip = rc;
319         rcGrip.left = rc.right - GetSystemMetrics(SM_CXVSCROLL);
320         rcGrip.top = rc.bottom - GetSystemMetrics(SM_CYHSCROLL);
321         dc.DrawFrameControl(&rcGrip, DFC_SCROLL, DFCS_SCROLLSIZEGRIP);
322
323         // Draw a line to separate the Status Line
324         CPen newPen(PS_SOLID, 1, RGB(208, 208, 208));   // a very light gray
325         CPen* oldpen = (CPen*)dc.SelectObject(&newPen);
326
327         CRect rcStatus;
328         GetDlgItem(IDC_OPEN_STATUS)->GetWindowRect(&rcStatus);
329         ScreenToClient(&rcStatus);
330         dc.MoveTo(0, rcStatus.top - 3);
331         dc.LineTo(rc.right, rcStatus.top - 3);
332         dc.SelectObject(oldpen);
333
334         CFormView::OnPaint();
335 }
336
337 void COpenView::OnLButtonUp(UINT nFlags, CPoint point)
338 {
339         if (::GetCapture() == m_hWnd)
340         {
341                 if (CWnd *const pwndHit = ChildWindowFromPoint(point,
342                         CWP_SKIPINVISIBLE | CWP_SKIPDISABLED | CWP_SKIPTRANSPARENT))
343                 {
344                         switch (int const id1 = pwndHit->GetDlgCtrlID())
345                         {
346                         case IDC_PATH0_COMBO:
347                         case IDC_PATH1_COMBO:
348                         case IDC_PATH2_COMBO:
349                                 int id2 = 0;
350                                 CWnd *pwndChild = GetFocus();
351                                 if (IsChild(pwndChild) && !pwndHit->IsChild(pwndChild)) do
352                                 {
353                                         id2 = pwndChild->GetDlgCtrlID();
354                                         pwndChild = pwndChild->GetParent();
355                                 } while (pwndChild != this);
356                                 switch (id2)
357                                 {
358                                 case IDC_PATH0_COMBO:
359                                 case IDC_PATH1_COMBO:
360                                 case IDC_PATH2_COMBO:
361                                         String s1, s2;
362                                         GetDlgItemText(id1, s1);
363                                         GetDlgItemText(id2, s2);
364                                         SetDlgItemText(id1, s2);
365                                         SetDlgItemText(id2, s1);
366                                         pwndHit->SetFocus();
367                                         break;
368                                 }
369                                 break;
370                         }
371                 }
372                 ReleaseCapture();
373         }
374 }
375
376 void COpenView::OnMouseMove(UINT nFlags, CPoint point)
377 {
378         if (::GetCapture() == m_hWnd)
379         {
380                 if (CWnd *const pwndHit = ChildWindowFromPoint(point,
381                         CWP_SKIPINVISIBLE | CWP_SKIPDISABLED | CWP_SKIPTRANSPARENT))
382                 {
383                         switch (pwndHit->GetDlgCtrlID())
384                         {
385                         case IDC_PATH0_COMBO:
386                         case IDC_PATH1_COMBO:
387                         case IDC_PATH2_COMBO:
388                                 if (!pwndHit->IsChild(GetFocus()))
389                                 {
390                                         SetCursor(m_hIconRotate);
391                                         break;
392                                 }
393                                 [[fallthrough]];
394                         default:
395                                 SetCursor(m_hCursorNo);
396                                 break;
397                         }
398                 }
399         }
400 }
401
402 void COpenView::OnWindowPosChanging(WINDOWPOS* lpwndpos)
403 {
404         if ((lpwndpos->flags & (SWP_NOMOVE | SWP_NOSIZE)) == 0)
405         {
406                 CFrameWnd *const pFrameWnd = GetParentFrame();
407                 if (pFrameWnd == GetTopLevelFrame()->GetActiveFrame())
408                 {
409                         CRect rc;
410                         pFrameWnd->GetClientRect(&rc);
411                         lpwndpos->flags |= SWP_FRAMECHANGED | SWP_SHOWWINDOW;
412                         lpwndpos->cy = m_sizeOrig.cy;
413                         if (lpwndpos->flags & SWP_NOOWNERZORDER)
414                         {
415                                 lpwndpos->x = rc.right - (lpwndpos->x + lpwndpos->cx);
416                                 lpwndpos->cx = rc.right - 2 * lpwndpos->x;
417                                 lpwndpos->y = (rc.bottom - lpwndpos->cy) / 2;
418                                 if (lpwndpos->y < 0)
419                                         lpwndpos->y = 0;
420                         }
421                         else if (pFrameWnd->IsZoomed())
422                         {
423                                 lpwndpos->cx = m_totalLog.cx;
424                                 lpwndpos->y = (rc.bottom - lpwndpos->cy) / 2;
425                                 if (lpwndpos->y < 0)
426                                         lpwndpos->y = 0;
427                         }
428                         if (lpwndpos->cx > rc.Width())
429                                 lpwndpos->cx = rc.Width();
430                         if (lpwndpos->cx < m_sizeOrig.cx)
431                                 lpwndpos->cx = m_sizeOrig.cx;
432                         lpwndpos->x = (rc.right - lpwndpos->cx) / 2;
433                         if (lpwndpos->x < 0)
434                                 lpwndpos->x = 0;
435                 }
436         }
437 }
438
439 void COpenView::OnWindowPosChanged(WINDOWPOS* lpwndpos)
440 {
441         if (lpwndpos->flags & SWP_FRAMECHANGED)
442         {
443                 m_constraint.UpdateSizes();
444                 CFrameWnd *const pFrameWnd = GetParentFrame();
445                 if (pFrameWnd == GetTopLevelFrame()->GetActiveFrame())
446                 {
447                         m_constraint.Persist(true, false);
448                         WINDOWPLACEMENT wp = {};
449                         wp.length = sizeof wp;
450                         pFrameWnd->GetWindowPlacement(&wp);
451                         CRect rc;
452                         GetWindowRect(&rc);
453                         pFrameWnd->CalcWindowRect(&rc, CWnd::adjustOutside);
454                         wp.rcNormalPosition.right = wp.rcNormalPosition.left + rc.Width();
455                         wp.rcNormalPosition.bottom = wp.rcNormalPosition.top + rc.Height();
456                         pFrameWnd->SetWindowPlacement(&wp);
457                 }
458         }
459         CFormView::OnWindowPosChanged(lpwndpos);
460 }
461
462 void COpenView::OnDestroy()
463 {
464         if (m_pDropHandler != nullptr)
465                 RevokeDragDrop(m_hWnd);
466
467         CFormView::OnDestroy();
468 }
469
470 LRESULT COpenView::OnNcHitTest(CPoint point)
471 {
472         if (GetParentFrame()->IsZoomed())
473         {
474                 CRect rc;
475                 GetWindowRect(&rc);
476                 rc.left = rc.right - GetSystemMetrics(SM_CXVSCROLL);
477                 rc.top = rc.bottom - GetSystemMetrics(SM_CYHSCROLL);
478                 if (PtInRect(&rc, point))
479                         return HTRIGHT;
480         }
481         return CFormView::OnNcHitTest(point);
482 }
483
484 /** 
485  * @brief Called when "Browse..." button is selected for N path.
486  */
487 void COpenView::OnPathButton(UINT nId)
488 {
489         const int index = nId - IDC_PATH0_BUTTON;
490         String s;
491         String sfolder;
492         UpdateData(TRUE); 
493
494         paths::PATH_EXISTENCE existence = paths::DoesPathExist(m_strPath[index]);
495         switch (existence)
496         {
497         case paths::IS_EXISTING_DIR:
498                 sfolder = m_strPath[index];
499                 break;
500         case paths::IS_EXISTING_FILE:
501                 sfolder = paths::GetPathOnly(m_strPath[index]);
502                 break;
503         case paths::DOES_NOT_EXIST:
504                 if (!m_strPath[index].empty())
505                         sfolder = paths::GetParentPath(m_strPath[index]);
506                 break;
507         default:
508                 _RPTF0(_CRT_ERROR, "Invalid return value from paths::DoesPathExist()");
509                 break;
510         }
511
512         if (SelectFileOrFolder(GetSafeHwnd(), s, sfolder.c_str()))
513         {
514                 m_strPath[index] = s;
515                 m_strBrowsePath[index] = s;
516                 UpdateData(FALSE);
517                 UpdateButtonStates();
518         }       
519 }
520
521 void COpenView::OnSwapButton(int id1, int id2)
522 {
523         String s1, s2;
524         GetDlgItemText(id1, s1);
525         GetDlgItemText(id2, s2);
526         std::swap(s1, s2);
527         SetDlgItemText(id1, s1);
528         SetDlgItemText(id2, s2);
529 }
530
531 template<int id1, int id2>
532 void COpenView::OnSwapButton() 
533 {
534         OnSwapButton(id1, id2);
535 }
536
537 /** 
538  * @brief Called when dialog is closed with "OK".
539  *
540  * Checks that paths are valid and sets filters.
541  */
542 void COpenView::OnOK() 
543 {
544         int pathsType; // enum from paths::PATH_EXISTENCE in paths.h
545         const String filterPrefix = _("[F] ");
546
547         UpdateData(TRUE);
548         TrimPaths();
549
550         int nFiles = 0;
551         for (auto& strPath: m_strPath)
552         {
553                 if (nFiles >= 1 && strPath.empty())
554                         break;
555                 m_files.SetSize(nFiles + 1);
556                 m_files[nFiles] = strPath;
557                 m_dwFlags[nFiles] &= ~FFILEOPEN_READONLY;
558                 m_dwFlags[nFiles] |= m_bReadOnly[nFiles] ? FFILEOPEN_READONLY : 0;
559                 nFiles++;
560         }
561         // If left path is a project-file, load it
562         String ext;
563         paths::SplitFilename(m_strPath[0], nullptr, nullptr, &ext);
564         if (nFiles == 1)
565         {
566                 if (strutils::compare_nocase(ext, ProjectFile::PROJECTFILE_EXT) == 0)
567                         LoadProjectFile(m_strPath[0]);
568                 else
569                         GetMainFrame()->DoSelfCompare(m_strPath[0], nullptr);
570                 return;
571         }
572
573         pathsType = paths::GetPairComparability(m_files, IsArchiveFile);
574
575         if (pathsType == paths::DOES_NOT_EXIST)
576         {
577                 LangMessageBox(IDS_ERROR_INCOMPARABLE, MB_ICONSTOP);
578                 return;
579         }
580
581         for (int index = 0; index < nFiles; index++)
582         {
583                 // If user has edited path by hand, expand environment variables
584                 bool bExpand = false;
585                 if (strutils::compare_nocase(m_strBrowsePath[index], m_files[index]) != 0)
586                         bExpand = true;
587
588                 if (!paths::IsURLorCLSID(m_files[index]))
589                 {
590                         m_files[index] = paths::GetLongPath(m_files[index], bExpand);
591         
592                         // Add trailing '\' for directories if its missing
593                         if (paths::DoesPathExist(m_files[index]) == paths::IS_EXISTING_DIR)
594                                 m_files[index] = paths::AddTrailingSlash(m_files[index]);
595                         m_strPath[index] = m_files[index];
596                 }
597         }
598
599         UpdateData(FALSE);
600         KillTimer(IDT_CHECKFILES);
601         KillTimer(IDT_RETRY);
602
603         String filter(strutils::trim_ws(m_strExt));
604
605         // If prefix found from start..
606         if (filter.substr(0, filterPrefix.length()) == filterPrefix)
607         {
608                 // Remove prefix + space
609                 filter.erase(0, filterPrefix.length());
610                 if (!theApp.m_pGlobalFileFilter->SetFilter(filter))
611                 {
612                         // If filtername is not found use default *.* mask
613                         theApp.m_pGlobalFileFilter->SetFilter(_T("*.*"));
614                         filter = _T("*.*");
615                 }
616                 GetOptionsMgr()->SaveOption(OPT_FILEFILTER_CURRENT, filter);
617         }
618         else
619         {
620                 bool bFilterSet = theApp.m_pGlobalFileFilter->SetFilter(filter);
621                 if (!bFilterSet)
622                         m_strExt = theApp.m_pGlobalFileFilter->GetFilterNameOrMask();
623                 GetOptionsMgr()->SaveOption(OPT_FILEFILTER_CURRENT, filter);
624         }
625
626         SaveComboboxStates();
627         GetOptionsMgr()->SaveOption(OPT_CMP_INCLUDE_SUBDIRS, m_bRecurse);
628         LoadComboboxStates();
629
630         m_constraint.Persist(true, false);
631
632         COpenDoc *pDoc = GetDocument();
633         pDoc->m_files = m_files;
634         pDoc->m_bRecurse = m_bRecurse;
635         pDoc->m_strExt = m_strExt;
636         pDoc->m_strUnpacker = m_strUnpacker;
637         pDoc->m_infoHandler = m_infoHandler;
638         pDoc->m_dwFlags[0] = m_dwFlags[0];
639         pDoc->m_dwFlags[1] = m_dwFlags[1];
640         pDoc->m_dwFlags[2] = m_dwFlags[2];
641
642         if (GetOptionsMgr()->GetBool(OPT_CLOSE_WITH_OK))
643                 GetParentFrame()->PostMessage(WM_CLOSE);
644
645         PathContext tmpPathContext(pDoc->m_files);
646         PackingInfo tmpPackingInfo(pDoc->m_infoHandler);
647         GetMainFrame()->DoFileOpen(
648                 &tmpPathContext, std::array<DWORD, 3>(pDoc->m_dwFlags).data(), 
649                 nullptr, _T(""), pDoc->m_bRecurse, nullptr, _T(""), &tmpPackingInfo);
650 }
651
652 /** 
653  * @brief Called when dialog is closed via Cancel.
654  *
655  * Open-dialog is closed when `Cancel` button is selected or the
656  * `Esc` key is pressed.  Save combobox states, since user may have
657  * removed items from them (with `shift-del`) and doesn't want them 
658  * to re-appear.
659  * This is *not* called when the program is terminated, even if the 
660  * dialog is visible at the time.
661  */
662 void COpenView::OnCancel()
663 {
664         SaveComboboxStates();
665         AfxGetMainWnd()->PostMessage(WM_COMMAND, ID_FILE_CLOSE);
666 }
667
668 /** 
669  * @brief Callled when Open-button for project file is selected.
670  */
671 void COpenView::OnLoadProject()
672 {
673         String fileName = AskProjectFileName(true);
674         if (fileName.empty())
675                 return;
676
677         ProjectFile project;
678         if (!theApp.LoadProjectFile(fileName, project))
679                 return;
680         if (project.Items().size() == 0)
681                 return;
682         PathContext paths;
683         ProjectFileItem& projItem = *project.Items().begin();
684         projItem.GetPaths(paths, m_bRecurse);
685         projItem.GetLeftReadOnly();
686         if (paths.GetSize() < 3)
687         {
688                 m_strPath[0] = paths[0];
689                 m_strPath[1] = paths[1];
690                 m_strPath[2] = _T("");
691                 m_bReadOnly[0] = projItem.GetLeftReadOnly();
692                 m_bReadOnly[1] = projItem.GetRightReadOnly();
693                 m_bReadOnly[2] = false;
694         }
695         else
696         {
697                 m_strPath[0] = paths[0];
698                 m_strPath[1] = paths[1];
699                 m_strPath[2] = paths[2];
700                 m_bReadOnly[0] = projItem.GetLeftReadOnly();
701                 m_bReadOnly[1] = projItem.GetMiddleReadOnly();
702                 m_bReadOnly[2] = projItem.GetRightReadOnly();
703         }
704         m_strExt = projItem.GetFilter();
705
706         UpdateData(FALSE);
707         LangMessageBox(IDS_PROJFILE_LOAD_SUCCESS, MB_ICONINFORMATION);
708 }
709
710 /** 
711  * @brief Called when Save-button for project file is selected.
712  */
713 void COpenView::OnSaveProject()
714 {
715         UpdateData(TRUE);
716
717         String fileName = AskProjectFileName(false);
718         if (fileName.empty())
719                 return;
720
721         ProjectFile project;
722         ProjectFileItem projItem;
723
724         if (!m_strPath[0].empty())
725                 projItem.SetLeft(m_strPath[0], &m_bReadOnly[0]);
726         if (m_strPath[2].empty())
727         {
728                 if (!m_strPath[1].empty())
729                         projItem.SetRight(m_strPath[1], &m_bReadOnly[1]);
730         }
731         else
732         {
733                 if (!m_strPath[1].empty())
734                         projItem.SetMiddle(m_strPath[1], &m_bReadOnly[1]);
735                 if (!m_strPath[2].empty())
736                         projItem.SetRight(m_strPath[2], &m_bReadOnly[2]);
737         }
738         if (!m_strExt.empty())
739         {
740                 // Remove possbile prefix from the filter name
741                 String prefix = _("[F] ");
742                 String strExt = m_strExt;
743                 size_t ind = strExt.find(prefix, 0);
744                 if (ind == 0)
745                 {
746                         strExt.erase(0, prefix.length());
747                 }
748                 strExt = strutils::trim_ws_begin(strExt);
749                 projItem.SetFilter(strExt);
750         }
751         projItem.SetSubfolders(m_bRecurse);
752         project.Items().push_back(projItem);
753
754         if (!theApp.SaveProjectFile(fileName, project))
755                 return;
756
757         LangMessageBox(IDS_PROJFILE_SAVE_SUCCESS, MB_ICONINFORMATION);
758 }
759
760 void COpenView::OnDropDownSaveProject(NMHDR *pNMHDR, LRESULT *pResult)
761 {
762         CRect rcButton, rcView;
763         GetDlgItem(ID_SAVE_PROJECT)->GetWindowRect(&rcButton);
764         BCMenu menu;
765         VERIFY(menu.LoadMenu(IDR_POPUP_PROJECT));
766         theApp.TranslateMenu(menu.m_hMenu);
767         CMenu* pPopup = menu.GetSubMenu(0);
768         if (pPopup != nullptr)
769         {
770                 pPopup->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, 
771                         rcButton.left, rcButton.bottom, GetMainFrame());
772         }
773         *pResult = 0;
774 }
775
776 /** 
777  * @brief Allow user to select a file to open/save.
778  */
779 String COpenView::AskProjectFileName(bool bOpen)
780 {
781         // get the default projects path
782         String strProjectFileName;
783         String strProjectPath = GetOptionsMgr()->GetString(OPT_PROJECTS_PATH);
784
785         if (!::SelectFile(GetSafeHwnd(), strProjectFileName, bOpen, strProjectPath.c_str(),
786                         _T(""), _("WinMerge Project Files (*.WinMerge)|*.WinMerge||"), _T(".WinMerge")))
787                 return _T("");
788
789         if (strProjectFileName.empty())
790                 return _T("");
791
792         // get the path part from the filename
793         strProjectPath = paths::GetParentPath(strProjectFileName);
794         // store this as the new project path
795         GetOptionsMgr()->SaveOption(OPT_PROJECTS_PATH, strProjectPath);
796         return strProjectFileName;
797 }
798
799 /** 
800  * @brief Load File- and filter-combobox states.
801  */
802 void COpenView::LoadComboboxStates()
803 {
804         m_ctlPath[0].LoadState(_T("Files\\Left"));
805         m_ctlPath[1].LoadState(_T("Files\\Right"));
806         m_ctlPath[2].LoadState(_T("Files\\Option"));
807         m_ctlExt.LoadState(_T("Files\\Ext"));
808 }
809
810 /** 
811  * @brief Save File- and filter-combobox states.
812  */
813 void COpenView::SaveComboboxStates()
814 {
815         m_ctlPath[0].SaveState(_T("Files\\Left"));
816         m_ctlPath[1].SaveState(_T("Files\\Right"));
817         m_ctlPath[2].SaveState(_T("Files\\Option"));
818         m_ctlExt.SaveState(_T("Files\\Ext"));
819 }
820
821 struct UpdateButtonStatesThreadParams
822 {
823         HWND m_hWnd;
824         PathContext m_paths;
825 };
826
827 static UINT UpdateButtonStatesThread(LPVOID lpParam)
828 {
829         MSG msg;
830         BOOL bRet;
831
832         CoInitialize(nullptr);
833         CAssureScriptsForThread scriptsForRescan;
834
835         while( (bRet = GetMessage( &msg, nullptr, 0, 0 )) != 0)
836         { 
837                 if (bRet == -1)
838                         break;
839                 if (msg.message != WM_USER + 2)
840                         continue;
841
842                 bool bIsaFolderCompare = true;
843                 bool bIsaFileCompare = true;
844                 bool bInvalid[3] = {false, false, false};
845                 paths::PATH_EXISTENCE pathType[3] = {paths::DOES_NOT_EXIST, paths::DOES_NOT_EXIST, paths::DOES_NOT_EXIST};
846                 int iStatusMsgId = IDS_OPEN_FILESDIRS;
847
848                 UpdateButtonStatesThreadParams *pParams = reinterpret_cast<UpdateButtonStatesThreadParams *>(msg.wParam);
849                 PathContext paths = pParams->m_paths;
850                 HWND hWnd = pParams->m_hWnd;
851                 delete pParams;
852
853                 // Check if we have project file as left side path
854                 bool bProject = false;
855                 String ext;
856                 paths::SplitFilename(paths[0], nullptr, nullptr, &ext);
857                 if (paths[1].empty() && strutils::compare_nocase(ext, ProjectFile::PROJECTFILE_EXT) == 0)
858                         bProject = true;
859
860                 if (!bProject)
861                 {
862                         for (int i = 0; i < paths.GetSize(); ++i)
863                         {
864                                 pathType[i] = paths::DoesPathExist(paths[i], IsArchiveFile);
865                                 if (pathType[i] == paths::DOES_NOT_EXIST)
866                                         bInvalid[i] = true;
867                         }
868                 }
869
870                 // Enable buttons as appropriate
871                 if (GetOptionsMgr()->GetBool(OPT_VERIFY_OPEN_PATHS))
872                 {
873                         paths::PATH_EXISTENCE pathsType = pathType[0];
874
875                         if (paths.GetSize() <= 2)
876                         {
877                                 if (bInvalid[0] && bInvalid[1])
878                                         iStatusMsgId = IDS_OPEN_BOTHINVALID;
879                                 else if (bInvalid[0])
880                                         iStatusMsgId = IDS_OPEN_LEFTINVALID;
881                                 else if (bInvalid[1])
882                                 {
883                                         if (pathType[0] == paths::IS_EXISTING_FILE && (paths.GetSize() == 1 || paths[1].empty()))
884                                                 iStatusMsgId = IDS_OPEN_FILESDIRS;
885                                         else
886                                                 iStatusMsgId = IDS_OPEN_RIGHTINVALID2;
887                                 }
888                                 else if (!bInvalid[0] && !bInvalid[1])
889                                 {
890                                         if (pathType[0] != pathType[1])
891                                                 iStatusMsgId = IDS_OPEN_MISMATCH;
892                                         else
893                                                 iStatusMsgId = IDS_OPEN_FILESDIRS;
894                                 }
895                         }
896                         else
897                         {
898                                 if (bInvalid[0] && bInvalid[1] && bInvalid[2])
899                                         iStatusMsgId = IDS_OPEN_ALLINVALID;
900                                 else if (!bInvalid[0] && bInvalid[1] && bInvalid[2])
901                                         iStatusMsgId = IDS_OPEN_MIDDLERIGHTINVALID;
902                                 else if (bInvalid[0] && !bInvalid[1] && bInvalid[2])
903                                         iStatusMsgId = IDS_OPEN_LEFTRIGHTINVALID;
904                                 else if (!bInvalid[0] && !bInvalid[1] && bInvalid[2])
905                                         iStatusMsgId = IDS_OPEN_RIGHTINVALID3;
906                                 else if (bInvalid[0] && bInvalid[1] && !bInvalid[2])
907                                         iStatusMsgId = IDS_OPEN_LEFTMIDDLEINVALID;
908                                 else if (!bInvalid[0] && bInvalid[1] && !bInvalid[2])
909                                         iStatusMsgId = IDS_OPEN_MIDDLEINVALID;
910                                 else if (bInvalid[0] && !bInvalid[1] && !bInvalid[2])
911                                         iStatusMsgId = IDS_OPEN_LEFTINVALID;
912                                 else if (!bInvalid[0] && !bInvalid[1] && !bInvalid[2])
913                                 {
914                                         if (pathType[0] != pathType[1] || pathType[0] != pathType[2])
915                                                 iStatusMsgId = IDS_OPEN_MISMATCH;
916                                         else
917                                                 iStatusMsgId = IDS_OPEN_FILESDIRS;
918                                 }
919                         }
920                         if (iStatusMsgId != IDS_OPEN_FILESDIRS)
921                                 pathsType = paths::DOES_NOT_EXIST;
922                         bIsaFileCompare = (pathsType == paths::IS_EXISTING_FILE);
923                         bIsaFolderCompare = (pathsType == paths::IS_EXISTING_DIR);
924                         // Both will be `false` if incompatibilities or something is missing
925                         // Both will end up `true` if file validity isn't being checked
926                 }
927
928                 PostMessage(hWnd, WM_USER + 1, MAKEWPARAM(bIsaFolderCompare, bIsaFileCompare), MAKELPARAM(iStatusMsgId, bProject)); 
929         }
930
931         CoUninitialize();
932
933         return 0;
934 }
935
936 /**
937  * @brief Update any resources necessary after a GUI language change
938  */
939 void COpenView::UpdateResources()
940 {
941         theApp.m_pLangDlg->RetranslateDialog(m_hWnd, MAKEINTRESOURCE(IDD_OPEN));
942         if (m_strUnpacker != m_infoHandler.m_PluginName)
943                 m_strUnpacker = theApp.LoadString(IDS_OPEN_UNPACKERDISABLED);
944 }
945
946 /** 
947  * @brief Enable/disable components based on validity of paths.
948  */
949 void COpenView::UpdateButtonStates()
950 {
951         UpdateData(TRUE); // load member variables from screen
952         KillTimer(IDT_CHECKFILES);
953         TrimPaths();
954         
955         if (m_pUpdateButtonStatusThread == nullptr)
956         {
957                 m_pUpdateButtonStatusThread = AfxBeginThread(
958                         UpdateButtonStatesThread, nullptr, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED);
959                 m_pUpdateButtonStatusThread->m_bAutoDelete = FALSE;
960                 m_pUpdateButtonStatusThread->ResumeThread();
961                 while (PostThreadMessage(m_pUpdateButtonStatusThread->m_nThreadID, WM_NULL, 0, 0) == FALSE)
962                         Sleep(1);
963         }
964
965         UpdateButtonStatesThreadParams *pParams = new UpdateButtonStatesThreadParams;
966         pParams->m_hWnd = this->m_hWnd;
967         pParams->m_paths = PathContext(std::vector<String>(&m_strPath[0], &m_strPath[m_strPath[2].empty() ? 2 : 3]));
968
969         PostThreadMessage(m_pUpdateButtonStatusThread->m_nThreadID, WM_USER + 2, (WPARAM)pParams, 0);
970 }
971
972 void COpenView::TerminateThreadIfRunning()
973 {
974         if (m_pUpdateButtonStatusThread == nullptr)
975                 return;
976
977         PostThreadMessage(m_pUpdateButtonStatusThread->m_nThreadID, WM_QUIT, 0, 0);
978         DWORD dwResult = WaitForSingleObject(m_pUpdateButtonStatusThread->m_hThread, 100);
979         if (dwResult != WAIT_OBJECT_0)
980         {
981                 m_pUpdateButtonStatusThread->SuspendThread();
982                 TerminateThread(m_pUpdateButtonStatusThread->m_hThread, 0);
983         }
984         delete m_pUpdateButtonStatusThread;
985         m_pUpdateButtonStatusThread = nullptr;
986 }
987
988 /**
989  * @brief Called when user changes selection in left/middle/right path's combo box.
990  */
991 void COpenView::OnSelchangePathCombo(UINT nId) 
992 {
993         const int index = nId - IDC_PATH0_COMBO;
994         int sel = m_ctlPath[index].GetCurSel();
995         if (sel != CB_ERR)
996         {
997                 CString cstrPath;
998                 m_ctlPath[index].GetLBText(sel, cstrPath);
999                 m_strPath[index] = cstrPath;
1000                 m_ctlPath[index].SetWindowText(cstrPath);
1001                 UpdateData(TRUE);
1002         }
1003         UpdateButtonStates();
1004 }
1005
1006 void COpenView::OnSetfocusPathCombo(UINT id, NMHDR *pNMHDR, LRESULT *pResult) 
1007 {
1008         if (!m_bAutoCompleteReady[id - IDC_PATH0_COMBO])
1009         {
1010                 int nSource = GetOptionsMgr()->GetInt(OPT_AUTO_COMPLETE_SOURCE);
1011                 if (nSource > 0)
1012                         m_ctlPath[id - IDC_PATH0_COMBO].SetAutoComplete(nSource);
1013                 m_bAutoCompleteReady[id - IDC_PATH0_COMBO] = true;
1014         }
1015         *pResult = 0;
1016 }
1017
1018 void COpenView::OnDragBeginPathCombo(UINT id, NMHDR *pNMHDR, LRESULT *pResult)
1019 {
1020         m_ctlPath[id - IDC_PATH0_COMBO].SetFocus();
1021         SetCapture();
1022         *pResult = 0;
1023 }
1024
1025 /**
1026  * @brief Called every time paths are edited.
1027  */
1028 void COpenView::OnEditEvent(UINT nID)
1029 {
1030         const int N = nID - IDC_PATH0_COMBO;
1031         if (CEdit *const edit = m_ctlPath[N].GetEditCtrl())
1032         {
1033                 int const len = edit->GetWindowTextLength();
1034                 if (edit->GetSel() == MAKEWPARAM(len, len))
1035                 {
1036                         CString text;
1037                         edit->GetWindowText(text);
1038                         // Remove any double quotes
1039                         text.Remove('"');
1040                         if (text.GetLength() != len)
1041                         {
1042                                 edit->SetSel(0, len);
1043                                 edit->ReplaceSel(text);
1044                         }
1045                 }
1046         }
1047         // (Re)start timer to path validity check delay
1048         // If timer starting fails, update buttonstates immediately
1049         if (!SetTimer(IDT_CHECKFILES, CHECKFILES_TIMEOUT, nullptr))
1050                 UpdateButtonStates();
1051 }
1052
1053 /**
1054  * @brief Handle timer events.
1055  * Checks if paths are valid and sets control states accordingly.
1056  * @param [in] nIDEvent Timer ID that fired.
1057  */
1058 void COpenView::OnTimer(UINT_PTR nIDEvent)
1059 {
1060         if (nIDEvent == IDT_CHECKFILES || nIDEvent == IDT_RETRY)
1061                 UpdateButtonStates();
1062
1063         CFormView::OnTimer(nIDEvent);
1064 }
1065
1066 /**
1067  * @brief Called when users selects plugin browse button.
1068  */
1069 void COpenView::OnSelectUnpacker()
1070 {
1071         paths::PATH_EXISTENCE pathsType;
1072         UpdateData(TRUE);
1073
1074         int nFiles = 0;
1075         for (auto& strPath: m_strPath)
1076         {
1077                 if (nFiles == 2 && strPath.empty())
1078                         break;
1079                 m_files.SetSize(nFiles + 1);
1080                 m_files[nFiles] = strPath;
1081                 nFiles++;
1082         }
1083         pathsType = paths::GetPairComparability(m_files);
1084
1085         if (pathsType != paths::IS_EXISTING_FILE) 
1086                 return;
1087
1088         // let the user select a handler
1089         CSelectUnpackerDlg dlg(m_files[0], this);
1090         PackingInfo infoUnpacker(PLUGIN_MODE::PLUGIN_AUTO);
1091         dlg.SetInitialInfoHandler(&infoUnpacker);
1092
1093         if (dlg.DoModal() == IDOK)
1094         {
1095                 m_infoHandler = dlg.GetInfoHandler();
1096
1097                 m_strUnpacker = m_infoHandler.m_PluginName;
1098
1099                 UpdateData(FALSE);
1100         }
1101 }
1102
1103 LRESULT COpenView::OnUpdateStatus(WPARAM wParam, LPARAM lParam)
1104 {
1105         const bool bIsaFolderCompare = LOWORD(wParam) != 0;
1106         const bool bIsaFileCompare = HIWORD(wParam) != 0;
1107         const bool bProject = HIWORD(lParam) != 0;
1108         const int iStatusMsgId = LOWORD(lParam);
1109
1110         EnableDlgItem(IDOK, bIsaFolderCompare || bIsaFileCompare || bProject);
1111
1112         EnableDlgItem(IDC_FILES_DIRS_GROUP4, bIsaFileCompare);
1113         EnableDlgItem(IDC_UNPACKER_EDIT, bIsaFileCompare);
1114         EnableDlgItem(IDC_SELECT_UNPACKER, bIsaFileCompare);
1115
1116         EnableDlgItem(IDC_FILES_DIRS_GROUP3,  bIsaFolderCompare);
1117         EnableDlgItem(IDC_EXT_COMBO, bIsaFolderCompare);
1118         EnableDlgItem(IDC_SELECT_FILTER, bIsaFolderCompare);
1119         EnableDlgItem(IDC_RECURS_CHECK, bIsaFolderCompare);
1120         
1121         SetStatus(iStatusMsgId);
1122
1123         if (iStatusMsgId != IDS_OPEN_FILESDIRS && m_retryCount <= RETRY_MAX)
1124         {
1125                 if (m_retryCount == 0)
1126                         SetTimer(IDT_RETRY, CHECKFILES_TIMEOUT, nullptr);
1127                 m_retryCount++;
1128         }
1129         else
1130         {
1131                 KillTimer(IDT_RETRY);
1132                 m_retryCount = 0;
1133         }
1134         return 0;
1135 }
1136
1137 /**
1138  * @brief Sets the path status text.
1139  * The open dialog shows a status text of selected paths. This function
1140  * is used to set that status text.
1141  * @param [in] msgID Resource ID of status text to set.
1142  */
1143 void COpenView::SetStatus(UINT msgID)
1144 {
1145         String msg = theApp.LoadString(msgID);
1146         SetDlgItemText(IDC_OPEN_STATUS, msg);
1147 }
1148
1149 /**
1150  * @brief Set the plugin edit box text.
1151  * Plugin edit box is at the same time a plugin status view. This function
1152  * sets the status text.
1153  * @param [in] msgID Resource ID of status text to set.
1154  */
1155 void COpenView::SetUnpackerStatus(UINT msgID)
1156 {
1157         String msg = (msgID == 0 ? m_strUnpacker : theApp.LoadString(msgID));
1158         SetDlgItemText(IDC_UNPACKER_EDIT, msg);
1159 }
1160
1161 /** 
1162  * @brief Called when "Select..." button for filters is selected.
1163  */
1164 void COpenView::OnSelectFilter()
1165 {
1166         String filterPrefix = _("[F] ");
1167         String curFilter;
1168
1169         const bool bUseMask = theApp.m_pGlobalFileFilter->IsUsingMask();
1170         GetDlgItemText(IDC_EXT_COMBO, curFilter);
1171         curFilter = strutils::trim_ws(curFilter);
1172
1173         GetMainFrame()->SelectFilter();
1174         
1175         String filterNameOrMask = theApp.m_pGlobalFileFilter->GetFilterNameOrMask();
1176         if (theApp.m_pGlobalFileFilter->IsUsingMask())
1177         {
1178                 // If we had filter chosen and now has mask we can overwrite filter
1179                 if (!bUseMask || curFilter[0] != '*')
1180                 {
1181                         SetDlgItemText(IDC_EXT_COMBO, filterNameOrMask);
1182                 }
1183         }
1184         else
1185         {
1186                 filterNameOrMask = filterPrefix + filterNameOrMask;
1187                 SetDlgItemText(IDC_EXT_COMBO, filterNameOrMask);
1188         }
1189 }
1190
1191 void COpenView::OnOptions()
1192 {
1193         GetMainFrame()->PostMessage(WM_COMMAND, ID_OPTIONS);
1194 }
1195
1196 void COpenView::OnDropDownOptions(NMHDR *pNMHDR, LRESULT *pResult)
1197 {
1198         NMTOOLBAR dropDown = { 0 };
1199         dropDown.hdr.code = TBN_DROPDOWN;
1200         dropDown.hdr.hwndFrom = GetMainFrame()->GetDescendantWindow(AFX_IDW_TOOLBAR)->GetSafeHwnd();
1201         dropDown.hdr.idFrom = AFX_IDW_TOOLBAR;
1202         GetDlgItem(IDC_OPTIONS)->GetWindowRect(&dropDown.rcButton);
1203         GetMainFrame()->ScreenToClient(&dropDown.rcButton);
1204         GetMainFrame()->SendMessage(WM_NOTIFY, dropDown.hdr.idFrom, reinterpret_cast<LPARAM>(&dropDown));
1205         *pResult = 0;
1206 }
1207
1208 /** 
1209  * @brief Read paths and filter from project file.
1210  * Reads the given project file. After the file is read, found paths and
1211  * filter is updated to dialog GUI. Other possible settings found in the
1212  * project file are kept in memory and used later when loading paths
1213  * selected.
1214  * @param [in] path Path to the project file.
1215  * @return `true` if the project file was successfully loaded, `false` otherwise.
1216  */
1217 bool COpenView::LoadProjectFile(const String &path)
1218 {
1219         String filterPrefix = _("[F] ");
1220         ProjectFile prj;
1221
1222         if (!theApp.LoadProjectFile(path, prj))
1223                 return false;
1224         if (prj.Items().size() == 0)
1225                 return false;
1226         bool recurse;
1227         ProjectFileItem& projItem = *prj.Items().begin();
1228         projItem.GetPaths(m_files, recurse);
1229         m_bRecurse = recurse;
1230         m_dwFlags[0] &= ~FFILEOPEN_READONLY;
1231         m_dwFlags[0] |= projItem.GetLeftReadOnly() ?    FFILEOPEN_READONLY : 0;
1232         if (m_files.GetSize() < 3)
1233         {
1234                 m_dwFlags[1] &= ~FFILEOPEN_READONLY;
1235                 m_dwFlags[1] |= projItem.GetRightReadOnly() ? FFILEOPEN_READONLY : 0;
1236         }
1237         else
1238         {
1239                 m_dwFlags[1] &= ~FFILEOPEN_READONLY;
1240                 m_dwFlags[1] |= projItem.GetMiddleReadOnly() ? FFILEOPEN_READONLY : 0;
1241                 m_dwFlags[2] &= ~FFILEOPEN_READONLY;
1242                 m_dwFlags[2] |= projItem.GetRightReadOnly() ? FFILEOPEN_READONLY : 0;
1243         }
1244         if (projItem.HasFilter())
1245         {
1246                 m_strExt = strutils::trim_ws(projItem.GetFilter());
1247                 if (m_strExt[0] != '*')
1248                         m_strExt.insert(0, filterPrefix);
1249         }
1250         return true;
1251 }
1252
1253 /** 
1254  * @brief Removes whitespaces from left and right paths
1255  * @note Assumes UpdateData(TRUE) is called before this function.
1256  */
1257 void COpenView::TrimPaths()
1258 {
1259         for (auto& strPath: m_strPath)
1260                 strPath = strutils::trim_ws(strPath);
1261 }
1262
1263 /** 
1264  * @brief Update control states when dialog is activated.
1265  *
1266  * Update control states when user re-activates dialog. User might have
1267  * switched for other program to e.g. update files/folders and then
1268  * swiches back to WinMerge. Its nice to see WinMerge detects updated
1269  * files/folders.
1270  */
1271 void COpenView::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized)
1272 {
1273         CFormView::OnActivate(nState, pWndOther, bMinimized);
1274
1275         if (nState == WA_ACTIVE || nState == WA_CLICKACTIVE)
1276                 UpdateButtonStates();
1277 }
1278
1279 void COpenView::OnEditAction(int msg, WPARAM wParam, LPARAM lParam)
1280 {
1281         CWnd *pCtl = GetFocus();
1282         if (pCtl != nullptr)
1283                 pCtl->PostMessage(msg, wParam, lParam);
1284 }
1285
1286 template <int MSG, int WPARAM, int LPARAM>
1287 void COpenView::OnEditAction()
1288 {
1289         OnEditAction(MSG, WPARAM, LPARAM);
1290 }
1291
1292 /**
1293  * @brief Open help from mainframe when user presses F1.
1294  */
1295 void COpenView::OnHelp()
1296 {
1297         theApp.ShowHelp(OpenDlgHelpLocation);
1298 }
1299
1300 /////////////////////////////////////////////////////////////////////////////
1301 //
1302 //      OnDropFiles code from CDropEdit
1303 //      Copyright 1997 Chris Losinger
1304 //
1305 //      shortcut expansion code modified from :
1306 //      CShortcut, 1996 Rob Warner
1307 //
1308
1309 /**
1310  * @brief Drop paths(s) to the dialog.
1311  * One or two paths can be dropped to the dialog. The behaviour is:
1312  *   If 1 file:
1313  *     - drop to empty path edit box (check left first)
1314  *     - if both boxes have a path, drop to left path
1315  *   If two files:
1316  *    - overwrite both paths, empty or not
1317  * @param [in] dropInfo Dropped data, including paths.
1318  */
1319 void COpenView::OnDropFiles(const std::vector<String>& files)
1320 {
1321         const size_t fileCount = files.size();
1322
1323         // Add dropped paths to the dialog
1324         UpdateData(TRUE);
1325         if (fileCount == 3)
1326         {
1327                 m_strPath[0] = files[0];
1328                 m_strPath[1] = files[1];
1329                 m_strPath[2] = files[2];
1330                 UpdateData(FALSE);
1331                 UpdateButtonStates();
1332         }
1333         else if (fileCount == 2)
1334         {
1335                 m_strPath[0] = files[0];
1336                 m_strPath[1] = files[1];
1337                 UpdateData(FALSE);
1338                 UpdateButtonStates();
1339         }
1340         else if (fileCount == 1)
1341         {
1342                 CPoint point;
1343                 GetCursorPos(&point);
1344                 ScreenToClient(&point);
1345                 if (CWnd *const pwndHit = ChildWindowFromPoint(point,
1346                         CWP_SKIPINVISIBLE | CWP_SKIPDISABLED | CWP_SKIPTRANSPARENT))
1347                 {
1348                         switch (int const id = pwndHit->GetDlgCtrlID())
1349                         {
1350                         case IDC_PATH0_COMBO:
1351                         case IDC_PATH1_COMBO:
1352                         case IDC_PATH2_COMBO:
1353                                 m_strPath[id - IDC_PATH0_COMBO] = files[0];
1354                                 break;
1355                         default:
1356                                 if (m_strPath[0].empty())
1357                                         m_strPath[0] = files[0];
1358                                 else if (m_strPath[1].empty())
1359                                         m_strPath[1] = files[0];
1360                                 else if (m_strPath[2].empty())
1361                                         m_strPath[2] = files[0];
1362                                 else
1363                                         m_strPath[0] = files[0];
1364                                 break;
1365                         }
1366                 }
1367                 UpdateData(FALSE);
1368                 UpdateButtonStates();
1369         }
1370 }