OSDN Git Service

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