OSDN Git Service

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