OSDN Git Service

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