OSDN Git Service

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