OSDN Git Service

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