OSDN Git Service

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