1 /////////////////////////////////////////////////////////////////////////////
2 // WinMerge: an interactive diff/merge utility
3 // Copyright (C) 1997-2000 Thingamahoochie Software
5 // SPDX-License-Identifier: GPL-2.0-or-later
6 /////////////////////////////////////////////////////////////////////////////
10 * @brief Implementation of the COpenView class
17 #include "UnicodeString.h"
20 #include "ProjectFile.h"
22 #include "SelectUnpackerDlg.h"
23 #include "OptionsDef.h"
25 #include "OptionsMgr.h"
26 #include "FileOrFolderSelect.h"
28 #include "Constants.h"
30 #include "DropHandler.h"
31 #include "FileFilterHelper.h"
34 #include "LanguageSelect.h"
35 #include "Win_VersionHelper.h"
42 #define BCN_DROPDOWN (BCN_FIRST + 0x0002)
45 // Timer ID and timeout for delaying path validity check
46 const UINT IDT_CHECKFILES = 1;
47 const UINT IDT_RETRY = 2;
48 const UINT CHECKFILES_TIMEOUT = 1000; // milliseconds
49 const int RETRY_MAX = 3;
50 static const TCHAR EMPTY_EXTENSION[] = _T(".*");
52 /** @brief Location for Open-dialog specific help to open. */
53 static TCHAR OpenDlgHelpLocation[] = _T("::/htmlhelp/Open_paths.html");
57 IMPLEMENT_DYNCREATE(COpenView, CFormView)
59 BEGIN_MESSAGE_MAP(COpenView, CFormView)
60 //{{AFX_MSG_MAP(COpenView)
61 ON_CONTROL_RANGE(BN_CLICKED, IDC_PATH0_BUTTON, IDC_PATH2_BUTTON, OnPathButton)
62 ON_BN_CLICKED(IDC_SWAP01_BUTTON, (OnSwapButton<IDC_PATH0_COMBO, IDC_PATH1_COMBO>))
63 ON_BN_CLICKED(IDC_SWAP12_BUTTON, (OnSwapButton<IDC_PATH1_COMBO, IDC_PATH2_COMBO>))
64 ON_BN_CLICKED(IDC_SWAP02_BUTTON, (OnSwapButton<IDC_PATH0_COMBO, IDC_PATH2_COMBO>))
65 ON_CONTROL_RANGE(CBN_SELCHANGE, IDC_PATH0_COMBO, IDC_PATH2_COMBO, OnSelchangePathCombo)
66 ON_CONTROL_RANGE(CBN_EDITCHANGE, IDC_PATH0_COMBO, IDC_PATH2_COMBO, OnEditEvent)
67 ON_BN_CLICKED(IDC_SELECT_UNPACKER, OnSelectUnpacker)
68 ON_CBN_SELENDCANCEL(IDC_PATH0_COMBO, UpdateButtonStates)
69 ON_CBN_SELENDCANCEL(IDC_PATH1_COMBO, UpdateButtonStates)
70 ON_CBN_SELENDCANCEL(IDC_PATH2_COMBO, UpdateButtonStates)
71 ON_NOTIFY_RANGE(CBEN_BEGINEDIT, IDC_PATH0_COMBO, IDC_PATH2_COMBO, OnSetfocusPathCombo)
72 ON_NOTIFY_RANGE(CBEN_DRAGBEGIN, IDC_PATH0_COMBO, IDC_PATH2_COMBO, OnDragBeginPathCombo)
74 ON_BN_CLICKED(IDC_SELECT_FILTER, OnSelectFilter)
75 ON_BN_CLICKED(IDC_OPTIONS, OnOptions)
76 ON_NOTIFY(BCN_DROPDOWN, IDC_OPTIONS, OnDropDownOptions)
78 ON_COMMAND(ID_LOAD_PROJECT, OnLoadProject)
79 ON_COMMAND(ID_SAVE_PROJECT, OnSaveProject)
80 ON_COMMAND(ID_FILE_SAVE, OnSaveProject)
81 ON_NOTIFY(BCN_DROPDOWN, ID_SAVE_PROJECT, OnDropDownSaveProject)
82 ON_COMMAND(IDOK, OnOK)
83 ON_COMMAND(IDCANCEL, OnCancel)
84 ON_COMMAND(ID_HELP, OnHelp)
85 ON_COMMAND(ID_EDIT_COPY, OnEditAction<WM_COPY>)
86 ON_COMMAND(ID_EDIT_PASTE, OnEditAction<WM_PASTE>)
87 ON_COMMAND(ID_EDIT_CUT, OnEditAction<WM_CUT>)
88 ON_COMMAND(ID_EDIT_UNDO, OnEditAction<WM_UNDO>)
89 ON_COMMAND(ID_EDIT_SELECT_ALL, (OnEditAction<EM_SETSEL, 0, -1>))
90 ON_MESSAGE(WM_USER + 1, OnUpdateStatus)
94 ON_WM_WINDOWPOSCHANGING()
95 ON_WM_WINDOWPOSCHANGED()
101 // COpenView construction/destruction
103 COpenView::COpenView()
104 : CFormView(COpenView::IDD)
105 , m_pUpdateButtonStatusThread(nullptr)
107 , m_pDropHandler(nullptr)
109 , m_bAutoCompleteReady()
110 , m_bReadOnly {false, false, false}
111 , m_hIconRotate(theApp.LoadIcon(IDI_ROTATE2))
112 , m_hCursorNo(LoadCursor(nullptr, IDC_NO))
115 // CWnd::EnableScrollBarCtrl() called inside CScrollView::UpdateBars() is quite slow.
116 // Therefore, set m_bInsideUpdate = TRUE so that CScrollView::UpdateBars() does almost nothing.
117 m_bInsideUpdate = TRUE;
120 COpenView::~COpenView()
122 TerminateThreadIfRunning();
125 void COpenView::DoDataExchange(CDataExchange* pDX)
127 CFormView::DoDataExchange(pDX);
128 //{{AFX_DATA_MAP(COpenView)
129 DDX_Control(pDX, IDC_EXT_COMBO, m_ctlExt);
130 DDX_Control(pDX, IDC_PATH0_COMBO, m_ctlPath[0]);
131 DDX_Control(pDX, IDC_PATH1_COMBO, m_ctlPath[1]);
132 DDX_Control(pDX, IDC_PATH2_COMBO, m_ctlPath[2]);
133 DDX_CBStringExact(pDX, IDC_PATH0_COMBO, m_strPath[0]);
134 DDX_CBStringExact(pDX, IDC_PATH1_COMBO, m_strPath[1]);
135 DDX_CBStringExact(pDX, IDC_PATH2_COMBO, m_strPath[2]);
136 DDX_Check(pDX, IDC_PATH0_READONLY, m_bReadOnly[0]);
137 DDX_Check(pDX, IDC_PATH1_READONLY, m_bReadOnly[1]);
138 DDX_Check(pDX, IDC_PATH2_READONLY, m_bReadOnly[2]);
139 DDX_Check(pDX, IDC_RECURS_CHECK, m_bRecurse);
140 DDX_CBStringExact(pDX, IDC_EXT_COMBO, m_strExt);
141 DDX_Text(pDX, IDC_UNPACKER_EDIT, m_strUnpacker);
145 BOOL COpenView::PreCreateWindow(CREATESTRUCT& cs)
147 // TODO: Modify the Window class or styles here by modifying
148 // the CREATESTRUCT cs
149 cs.style &= ~WS_BORDER;
150 cs.dwExStyle &= ~WS_EX_CLIENTEDGE;
151 return CFormView::PreCreateWindow(cs);
154 void COpenView::OnInitialUpdate()
156 if (!IsVista_OrGreater())
159 SendDlgItemMessage(IDC_OPTIONS, BM_SETSTYLE, BS_PUSHBUTTON, TRUE);
160 SendDlgItemMessage(ID_SAVE_PROJECT, BM_SETSTYLE, BS_PUSHBUTTON, TRUE);
163 m_sizeOrig = GetTotalSize();
165 theApp.TranslateDialog(m_hWnd);
167 if (!LoadImageFromResource(m_image, MAKEINTRESOURCE(IDR_LOGO), _T("IMAGE")))
169 // FIXME: LoadImageFromResource() seems to fail when running on Wine 5.0.
170 m_image.Create(1, 1, 24, 0);
173 CFormView::OnInitialUpdate();
175 // set caption to "swap paths" button
177 GetDlgItem(IDC_SWAP01_BUTTON)->GetFont()->GetObject(sizeof(lf), &lf);
178 lf.lfCharSet = SYMBOL_CHARSET;
179 lstrcpy(lf.lfFaceName, _T("Wingdings"));
180 m_fontSwapButton.CreateFontIndirect(&lf);
181 const int ids[] = {IDC_SWAP01_BUTTON, IDC_SWAP12_BUTTON, IDC_SWAP02_BUTTON};
182 for (int i = 0; i < sizeof(ids)/sizeof(ids[0]); ++i)
184 GetDlgItem(ids[i])->SetFont(&m_fontSwapButton);
185 SetDlgItemText(ids[i], _T("\xf4"));
188 m_constraint.InitializeCurrentSize(this);
189 m_constraint.InitializeSpecificSize(this, m_sizeOrig.cx, m_sizeOrig.cy);
190 m_constraint.SetMaxSizePixels(-1, m_sizeOrig.cy);
191 m_constraint.SetScrollScale(this, 1.0, 1.0);
192 m_constraint.SetSizeGrip(prdlg::CMoveConstraint::SG_NONE);
193 m_constraint.DisallowHeightGrowth();
194 //m_constraint.SubclassWnd(); // install subclassing
196 m_constraint.LoadPosition(_T("ResizeableDialogs"), _T("OpenView"), false); // persist size via registry
197 m_constraint.UpdateSizes();
199 COpenDoc *pDoc = GetDocument();
202 GetWindowText(strTitle);
203 pDoc->SetTitle(strTitle);
205 m_files = pDoc->m_files;
206 m_bRecurse = pDoc->m_bRecurse;
207 m_strExt = pDoc->m_strExt;
208 m_strUnpacker = pDoc->m_strUnpacker;
209 m_infoHandler = pDoc->m_infoHandler;
210 m_dwFlags[0] = pDoc->m_dwFlags[0];
211 m_dwFlags[1] = pDoc->m_dwFlags[1];
212 m_dwFlags[2] = pDoc->m_dwFlags[2];
214 m_ctlPath[0].SetFileControlStates();
215 m_ctlPath[1].SetFileControlStates(true);
216 m_ctlPath[2].SetFileControlStates(true);
218 for (int file = 0; file < m_files.GetSize(); file++)
220 m_strPath[file] = m_files[file];
221 m_ctlPath[file].SetWindowText(m_files[file].c_str());
222 m_bReadOnly[file] = (m_dwFlags[file] & FFILEOPEN_READONLY) != 0;
225 m_ctlPath[0].AttachSystemImageList();
226 m_ctlPath[1].AttachSystemImageList();
227 m_ctlPath[2].AttachSystemImageList();
228 LoadComboboxStates();
230 bool bDoUpdateData = true;
231 for (auto& strPath: m_strPath)
233 if (!strPath.empty())
234 bDoUpdateData = false;
236 UpdateData(bDoUpdateData);
238 String filterNameOrMask = theApp.m_pGlobalFileFilter->GetFilterNameOrMask();
239 bool bMask = theApp.m_pGlobalFileFilter->IsUsingMask();
243 String filterPrefix = _("[F] ");
244 filterNameOrMask = filterPrefix + filterNameOrMask;
247 int ind = m_ctlExt.FindStringExact(0, filterNameOrMask.c_str());
249 m_ctlExt.SetCurSel(ind);
252 ind = m_ctlExt.InsertString(0, filterNameOrMask.c_str());
254 m_ctlExt.SetCurSel(ind);
256 LogErrorString(_T("Failed to add string to filters combo list!"));
259 if (!GetOptionsMgr()->GetBool(OPT_VERIFY_OPEN_PATHS))
261 EnableDlgItem(IDOK, true);
262 EnableDlgItem(IDC_UNPACKER_EDIT, true);
263 EnableDlgItem(IDC_SELECT_UNPACKER, true);
266 UpdateButtonStates();
268 bool bOverwriteRecursive = false;
269 if (m_dwFlags[0] & FFILEOPEN_PROJECT || m_dwFlags[1] & FFILEOPEN_PROJECT)
270 bOverwriteRecursive = true;
271 if (m_dwFlags[0] & FFILEOPEN_CMDLINE || m_dwFlags[1] & FFILEOPEN_CMDLINE)
272 bOverwriteRecursive = true;
273 if (!bOverwriteRecursive)
274 m_bRecurse = GetOptionsMgr()->GetBool(OPT_CMP_INCLUDE_SUBDIRS);
276 m_strUnpacker = m_infoHandler.m_PluginName;
278 SetStatus(IDS_OPEN_FILESDIRS);
279 SetUnpackerStatus(IDS_USERCHOICE_NONE);
281 m_pDropHandler = new DropHandler(std::bind(&COpenView::OnDropFiles, this, std::placeholders::_1));
282 RegisterDragDrop(m_hWnd, m_pDropHandler);
285 void COpenView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint)
287 m_bRecurse = GetDocument()->m_bRecurse;
291 // COpenView diagnostics
294 COpenDoc* COpenView::GetDocument() const // non-debug version is inline
296 ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(COpenDoc)));
297 return (COpenDoc*)m_pDocument;
301 /////////////////////////////////////////////////////////////////////////////
302 // COpenView message handlers
304 void COpenView::OnPaint()
310 // Draw the logo image
311 CSize size{ m_image.GetWidth(), m_image.GetHeight() };
312 CRect rcImage(0, 0, size.cx * GetSystemMetrics(SM_CXSMICON) / 16, size.cy * GetSystemMetrics(SM_CYSMICON) / 16);
313 m_image.Draw(dc.m_hDC, rcImage, Gdiplus::InterpolationModeBicubic);
314 // And extend it to the Right boundary
315 dc.PatBlt(rcImage.Width(), 0, rc.Width() - rcImage.Width(), rcImage.Height(), PATCOPY);
317 // Draw the resize gripper in the Lower Right corner.
319 rcGrip.left = rc.right - GetSystemMetrics(SM_CXVSCROLL);
320 rcGrip.top = rc.bottom - GetSystemMetrics(SM_CYHSCROLL);
321 dc.DrawFrameControl(&rcGrip, DFC_SCROLL, DFCS_SCROLLSIZEGRIP);
323 // Draw a line to separate the Status Line
324 CPen newPen(PS_SOLID, 1, RGB(208, 208, 208)); // a very light gray
325 CPen* oldpen = (CPen*)dc.SelectObject(&newPen);
328 GetDlgItem(IDC_OPEN_STATUS)->GetWindowRect(&rcStatus);
329 ScreenToClient(&rcStatus);
330 dc.MoveTo(0, rcStatus.top - 3);
331 dc.LineTo(rc.right, rcStatus.top - 3);
332 dc.SelectObject(oldpen);
334 CFormView::OnPaint();
337 void COpenView::OnLButtonUp(UINT nFlags, CPoint point)
339 if (::GetCapture() == m_hWnd)
341 if (CWnd *const pwndHit = ChildWindowFromPoint(point,
342 CWP_SKIPINVISIBLE | CWP_SKIPDISABLED | CWP_SKIPTRANSPARENT))
344 switch (int const id1 = pwndHit->GetDlgCtrlID())
346 case IDC_PATH0_COMBO:
347 case IDC_PATH1_COMBO:
348 case IDC_PATH2_COMBO:
350 CWnd *pwndChild = GetFocus();
351 if (IsChild(pwndChild) && !pwndHit->IsChild(pwndChild)) do
353 id2 = pwndChild->GetDlgCtrlID();
354 pwndChild = pwndChild->GetParent();
355 } while (pwndChild != this);
358 case IDC_PATH0_COMBO:
359 case IDC_PATH1_COMBO:
360 case IDC_PATH2_COMBO:
362 GetDlgItemText(id1, s1);
363 GetDlgItemText(id2, s2);
364 SetDlgItemText(id1, s2);
365 SetDlgItemText(id2, s1);
376 void COpenView::OnMouseMove(UINT nFlags, CPoint point)
378 if (::GetCapture() == m_hWnd)
380 if (CWnd *const pwndHit = ChildWindowFromPoint(point,
381 CWP_SKIPINVISIBLE | CWP_SKIPDISABLED | CWP_SKIPTRANSPARENT))
383 switch (pwndHit->GetDlgCtrlID())
385 case IDC_PATH0_COMBO:
386 case IDC_PATH1_COMBO:
387 case IDC_PATH2_COMBO:
388 if (!pwndHit->IsChild(GetFocus()))
390 SetCursor(m_hIconRotate);
395 SetCursor(m_hCursorNo);
402 void COpenView::OnWindowPosChanging(WINDOWPOS* lpwndpos)
404 if ((lpwndpos->flags & (SWP_NOMOVE | SWP_NOSIZE)) == 0)
406 CFrameWnd *const pFrameWnd = GetParentFrame();
407 if (pFrameWnd == GetTopLevelFrame()->GetActiveFrame())
410 pFrameWnd->GetClientRect(&rc);
411 lpwndpos->flags |= SWP_FRAMECHANGED | SWP_SHOWWINDOW;
412 lpwndpos->cy = m_sizeOrig.cy;
413 if (lpwndpos->flags & SWP_NOOWNERZORDER)
415 lpwndpos->x = rc.right - (lpwndpos->x + lpwndpos->cx);
416 lpwndpos->cx = rc.right - 2 * lpwndpos->x;
417 lpwndpos->y = (rc.bottom - lpwndpos->cy) / 2;
421 else if (pFrameWnd->IsZoomed())
423 lpwndpos->cx = m_totalLog.cx;
424 lpwndpos->y = (rc.bottom - lpwndpos->cy) / 2;
428 if (lpwndpos->cx > rc.Width())
429 lpwndpos->cx = rc.Width();
430 if (lpwndpos->cx < m_sizeOrig.cx)
431 lpwndpos->cx = m_sizeOrig.cx;
432 lpwndpos->x = (rc.right - lpwndpos->cx) / 2;
439 void COpenView::OnWindowPosChanged(WINDOWPOS* lpwndpos)
441 if (lpwndpos->flags & SWP_FRAMECHANGED)
443 m_constraint.UpdateSizes();
444 CFrameWnd *const pFrameWnd = GetParentFrame();
445 if (pFrameWnd == GetTopLevelFrame()->GetActiveFrame())
447 m_constraint.Persist(true, false);
448 WINDOWPLACEMENT wp = {};
449 wp.length = sizeof wp;
450 pFrameWnd->GetWindowPlacement(&wp);
453 pFrameWnd->CalcWindowRect(&rc, CWnd::adjustOutside);
454 wp.rcNormalPosition.right = wp.rcNormalPosition.left + rc.Width();
455 wp.rcNormalPosition.bottom = wp.rcNormalPosition.top + rc.Height();
456 pFrameWnd->SetWindowPlacement(&wp);
459 CFormView::OnWindowPosChanged(lpwndpos);
462 void COpenView::OnDestroy()
464 if (m_pDropHandler != nullptr)
465 RevokeDragDrop(m_hWnd);
467 CFormView::OnDestroy();
470 LRESULT COpenView::OnNcHitTest(CPoint point)
472 if (GetParentFrame()->IsZoomed())
476 rc.left = rc.right - GetSystemMetrics(SM_CXVSCROLL);
477 rc.top = rc.bottom - GetSystemMetrics(SM_CYHSCROLL);
478 if (PtInRect(&rc, point))
481 return CFormView::OnNcHitTest(point);
485 * @brief Called when "Browse..." button is selected for N path.
487 void COpenView::OnPathButton(UINT nId)
489 const int index = nId - IDC_PATH0_BUTTON;
494 paths::PATH_EXISTENCE existence = paths::DoesPathExist(m_strPath[index]);
497 case paths::IS_EXISTING_DIR:
498 sfolder = m_strPath[index];
500 case paths::IS_EXISTING_FILE:
501 sfolder = paths::GetPathOnly(m_strPath[index]);
503 case paths::DOES_NOT_EXIST:
504 if (!m_strPath[index].empty())
505 sfolder = paths::GetParentPath(m_strPath[index]);
508 _RPTF0(_CRT_ERROR, "Invalid return value from paths::DoesPathExist()");
512 if (SelectFileOrFolder(GetSafeHwnd(), s, sfolder.c_str()))
514 m_strPath[index] = s;
515 m_strBrowsePath[index] = s;
517 UpdateButtonStates();
521 void COpenView::OnSwapButton(int id1, int id2)
524 GetDlgItemText(id1, s1);
525 GetDlgItemText(id2, s2);
527 SetDlgItemText(id1, s1);
528 SetDlgItemText(id2, s2);
531 template<int id1, int id2>
532 void COpenView::OnSwapButton()
534 OnSwapButton(id1, id2);
538 * @brief Called when dialog is closed with "OK".
540 * Checks that paths are valid and sets filters.
542 void COpenView::OnOK()
544 int pathsType; // enum from paths::PATH_EXISTENCE in paths.h
545 const String filterPrefix = _("[F] ");
551 for (auto& strPath: m_strPath)
553 if (nFiles >= 1 && strPath.empty())
555 m_files.SetSize(nFiles + 1);
556 m_files[nFiles] = strPath;
557 m_dwFlags[nFiles] &= ~FFILEOPEN_READONLY;
558 m_dwFlags[nFiles] |= m_bReadOnly[nFiles] ? FFILEOPEN_READONLY : 0;
561 // If left path is a project-file, load it
563 paths::SplitFilename(m_strPath[0], nullptr, nullptr, &ext);
566 if (strutils::compare_nocase(ext, ProjectFile::PROJECTFILE_EXT) == 0)
567 LoadProjectFile(m_strPath[0]);
569 GetMainFrame()->DoSelfCompare(m_strPath[0], nullptr);
573 pathsType = paths::GetPairComparability(m_files, IsArchiveFile);
575 if (pathsType == paths::DOES_NOT_EXIST)
577 LangMessageBox(IDS_ERROR_INCOMPARABLE, MB_ICONSTOP);
581 for (int index = 0; index < nFiles; index++)
583 // If user has edited path by hand, expand environment variables
584 bool bExpand = false;
585 if (strutils::compare_nocase(m_strBrowsePath[index], m_files[index]) != 0)
588 if (!paths::IsURLorCLSID(m_files[index]))
590 m_files[index] = paths::GetLongPath(m_files[index], bExpand);
592 // Add trailing '\' for directories if its missing
593 if (paths::DoesPathExist(m_files[index]) == paths::IS_EXISTING_DIR)
594 m_files[index] = paths::AddTrailingSlash(m_files[index]);
595 m_strPath[index] = m_files[index];
600 KillTimer(IDT_CHECKFILES);
601 KillTimer(IDT_RETRY);
603 String filter(strutils::trim_ws(m_strExt));
605 // If prefix found from start..
606 if (filter.substr(0, filterPrefix.length()) == filterPrefix)
608 // Remove prefix + space
609 filter.erase(0, filterPrefix.length());
610 if (!theApp.m_pGlobalFileFilter->SetFilter(filter))
612 // If filtername is not found use default *.* mask
613 theApp.m_pGlobalFileFilter->SetFilter(_T("*.*"));
616 GetOptionsMgr()->SaveOption(OPT_FILEFILTER_CURRENT, filter);
620 bool bFilterSet = theApp.m_pGlobalFileFilter->SetFilter(filter);
622 m_strExt = theApp.m_pGlobalFileFilter->GetFilterNameOrMask();
623 GetOptionsMgr()->SaveOption(OPT_FILEFILTER_CURRENT, filter);
626 SaveComboboxStates();
627 GetOptionsMgr()->SaveOption(OPT_CMP_INCLUDE_SUBDIRS, m_bRecurse);
628 LoadComboboxStates();
630 m_constraint.Persist(true, false);
632 COpenDoc *pDoc = GetDocument();
633 pDoc->m_files = m_files;
634 pDoc->m_bRecurse = m_bRecurse;
635 pDoc->m_strExt = m_strExt;
636 pDoc->m_strUnpacker = m_strUnpacker;
637 pDoc->m_infoHandler = m_infoHandler;
638 pDoc->m_dwFlags[0] = m_dwFlags[0];
639 pDoc->m_dwFlags[1] = m_dwFlags[1];
640 pDoc->m_dwFlags[2] = m_dwFlags[2];
642 if (GetOptionsMgr()->GetBool(OPT_CLOSE_WITH_OK))
643 GetParentFrame()->PostMessage(WM_CLOSE);
645 PathContext tmpPathContext(pDoc->m_files);
646 PackingInfo tmpPackingInfo(pDoc->m_infoHandler);
647 GetMainFrame()->DoFileOpen(
648 &tmpPathContext, std::array<DWORD, 3>(pDoc->m_dwFlags).data(),
649 nullptr, _T(""), pDoc->m_bRecurse, nullptr, _T(""), &tmpPackingInfo);
653 * @brief Called when dialog is closed via Cancel.
655 * Open-dialog is closed when `Cancel` button is selected or the
656 * `Esc` key is pressed. Save combobox states, since user may have
657 * removed items from them (with `shift-del`) and doesn't want them
659 * This is *not* called when the program is terminated, even if the
660 * dialog is visible at the time.
662 void COpenView::OnCancel()
664 SaveComboboxStates();
665 AfxGetMainWnd()->PostMessage(WM_COMMAND, ID_FILE_CLOSE);
669 * @brief Callled when Open-button for project file is selected.
671 void COpenView::OnLoadProject()
673 String fileName = AskProjectFileName(true);
674 if (fileName.empty())
678 if (!theApp.LoadProjectFile(fileName, project))
680 if (project.Items().size() == 0)
683 ProjectFileItem& projItem = *project.Items().begin();
684 projItem.GetPaths(paths, m_bRecurse);
685 projItem.GetLeftReadOnly();
686 if (paths.GetSize() < 3)
688 m_strPath[0] = paths[0];
689 m_strPath[1] = paths[1];
690 m_strPath[2] = _T("");
691 m_bReadOnly[0] = projItem.GetLeftReadOnly();
692 m_bReadOnly[1] = projItem.GetRightReadOnly();
693 m_bReadOnly[2] = false;
697 m_strPath[0] = paths[0];
698 m_strPath[1] = paths[1];
699 m_strPath[2] = paths[2];
700 m_bReadOnly[0] = projItem.GetLeftReadOnly();
701 m_bReadOnly[1] = projItem.GetMiddleReadOnly();
702 m_bReadOnly[2] = projItem.GetRightReadOnly();
704 m_strExt = projItem.GetFilter();
707 LangMessageBox(IDS_PROJFILE_LOAD_SUCCESS, MB_ICONINFORMATION);
711 * @brief Called when Save-button for project file is selected.
713 void COpenView::OnSaveProject()
717 String fileName = AskProjectFileName(false);
718 if (fileName.empty())
722 ProjectFileItem projItem;
724 if (!m_strPath[0].empty())
725 projItem.SetLeft(m_strPath[0], &m_bReadOnly[0]);
726 if (m_strPath[2].empty())
728 if (!m_strPath[1].empty())
729 projItem.SetRight(m_strPath[1], &m_bReadOnly[1]);
733 if (!m_strPath[1].empty())
734 projItem.SetMiddle(m_strPath[1], &m_bReadOnly[1]);
735 if (!m_strPath[2].empty())
736 projItem.SetRight(m_strPath[2], &m_bReadOnly[2]);
738 if (!m_strExt.empty())
740 // Remove possbile prefix from the filter name
741 String prefix = _("[F] ");
742 String strExt = m_strExt;
743 size_t ind = strExt.find(prefix, 0);
746 strExt.erase(0, prefix.length());
748 strExt = strutils::trim_ws_begin(strExt);
749 projItem.SetFilter(strExt);
751 projItem.SetSubfolders(m_bRecurse);
752 project.Items().push_back(projItem);
754 if (!theApp.SaveProjectFile(fileName, project))
757 LangMessageBox(IDS_PROJFILE_SAVE_SUCCESS, MB_ICONINFORMATION);
760 void COpenView::OnDropDownSaveProject(NMHDR *pNMHDR, LRESULT *pResult)
762 CRect rcButton, rcView;
763 GetDlgItem(ID_SAVE_PROJECT)->GetWindowRect(&rcButton);
765 VERIFY(menu.LoadMenu(IDR_POPUP_PROJECT));
766 theApp.TranslateMenu(menu.m_hMenu);
767 CMenu* pPopup = menu.GetSubMenu(0);
768 if (pPopup != nullptr)
770 pPopup->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON,
771 rcButton.left, rcButton.bottom, GetMainFrame());
777 * @brief Allow user to select a file to open/save.
779 String COpenView::AskProjectFileName(bool bOpen)
781 // get the default projects path
782 String strProjectFileName;
783 String strProjectPath = GetOptionsMgr()->GetString(OPT_PROJECTS_PATH);
785 if (!::SelectFile(GetSafeHwnd(), strProjectFileName, bOpen, strProjectPath.c_str(),
786 _T(""), _("WinMerge Project Files (*.WinMerge)|*.WinMerge||"), _T(".WinMerge")))
789 if (strProjectFileName.empty())
792 // get the path part from the filename
793 strProjectPath = paths::GetParentPath(strProjectFileName);
794 // store this as the new project path
795 GetOptionsMgr()->SaveOption(OPT_PROJECTS_PATH, strProjectPath);
796 return strProjectFileName;
800 * @brief Load File- and filter-combobox states.
802 void COpenView::LoadComboboxStates()
804 m_ctlPath[0].LoadState(_T("Files\\Left"));
805 m_ctlPath[1].LoadState(_T("Files\\Right"));
806 m_ctlPath[2].LoadState(_T("Files\\Option"));
807 m_ctlExt.LoadState(_T("Files\\Ext"));
811 * @brief Save File- and filter-combobox states.
813 void COpenView::SaveComboboxStates()
815 m_ctlPath[0].SaveState(_T("Files\\Left"));
816 m_ctlPath[1].SaveState(_T("Files\\Right"));
817 m_ctlPath[2].SaveState(_T("Files\\Option"));
818 m_ctlExt.SaveState(_T("Files\\Ext"));
821 struct UpdateButtonStatesThreadParams
827 static UINT UpdateButtonStatesThread(LPVOID lpParam)
832 CoInitialize(nullptr);
833 CAssureScriptsForThread scriptsForRescan;
835 while( (bRet = GetMessage( &msg, nullptr, 0, 0 )) != 0)
839 if (msg.message != WM_USER + 2)
842 bool bIsaFolderCompare = true;
843 bool bIsaFileCompare = true;
844 bool bInvalid[3] = {false, false, false};
845 paths::PATH_EXISTENCE pathType[3] = {paths::DOES_NOT_EXIST, paths::DOES_NOT_EXIST, paths::DOES_NOT_EXIST};
846 int iStatusMsgId = IDS_OPEN_FILESDIRS;
848 UpdateButtonStatesThreadParams *pParams = reinterpret_cast<UpdateButtonStatesThreadParams *>(msg.wParam);
849 PathContext paths = pParams->m_paths;
850 HWND hWnd = pParams->m_hWnd;
853 // Check if we have project file as left side path
854 bool bProject = false;
856 paths::SplitFilename(paths[0], nullptr, nullptr, &ext);
857 if (paths[1].empty() && strutils::compare_nocase(ext, ProjectFile::PROJECTFILE_EXT) == 0)
862 for (int i = 0; i < paths.GetSize(); ++i)
864 pathType[i] = paths::DoesPathExist(paths[i], IsArchiveFile);
865 if (pathType[i] == paths::DOES_NOT_EXIST)
870 // Enable buttons as appropriate
871 if (GetOptionsMgr()->GetBool(OPT_VERIFY_OPEN_PATHS))
873 paths::PATH_EXISTENCE pathsType = pathType[0];
875 if (paths.GetSize() <= 2)
877 if (bInvalid[0] && bInvalid[1])
878 iStatusMsgId = IDS_OPEN_BOTHINVALID;
879 else if (bInvalid[0])
880 iStatusMsgId = IDS_OPEN_LEFTINVALID;
881 else if (bInvalid[1])
883 if (pathType[0] == paths::IS_EXISTING_FILE && (paths.GetSize() == 1 || paths[1].empty()))
884 iStatusMsgId = IDS_OPEN_FILESDIRS;
886 iStatusMsgId = IDS_OPEN_RIGHTINVALID2;
888 else if (!bInvalid[0] && !bInvalid[1])
890 if (pathType[0] != pathType[1])
891 iStatusMsgId = IDS_OPEN_MISMATCH;
893 iStatusMsgId = IDS_OPEN_FILESDIRS;
898 if (bInvalid[0] && bInvalid[1] && bInvalid[2])
899 iStatusMsgId = IDS_OPEN_ALLINVALID;
900 else if (!bInvalid[0] && bInvalid[1] && bInvalid[2])
901 iStatusMsgId = IDS_OPEN_MIDDLERIGHTINVALID;
902 else if (bInvalid[0] && !bInvalid[1] && bInvalid[2])
903 iStatusMsgId = IDS_OPEN_LEFTRIGHTINVALID;
904 else if (!bInvalid[0] && !bInvalid[1] && bInvalid[2])
905 iStatusMsgId = IDS_OPEN_RIGHTINVALID3;
906 else if (bInvalid[0] && bInvalid[1] && !bInvalid[2])
907 iStatusMsgId = IDS_OPEN_LEFTMIDDLEINVALID;
908 else if (!bInvalid[0] && bInvalid[1] && !bInvalid[2])
909 iStatusMsgId = IDS_OPEN_MIDDLEINVALID;
910 else if (bInvalid[0] && !bInvalid[1] && !bInvalid[2])
911 iStatusMsgId = IDS_OPEN_LEFTINVALID;
912 else if (!bInvalid[0] && !bInvalid[1] && !bInvalid[2])
914 if (pathType[0] != pathType[1] || pathType[0] != pathType[2])
915 iStatusMsgId = IDS_OPEN_MISMATCH;
917 iStatusMsgId = IDS_OPEN_FILESDIRS;
920 if (iStatusMsgId != IDS_OPEN_FILESDIRS)
921 pathsType = paths::DOES_NOT_EXIST;
922 bIsaFileCompare = (pathsType == paths::IS_EXISTING_FILE);
923 bIsaFolderCompare = (pathsType == paths::IS_EXISTING_DIR);
924 // Both will be `false` if incompatibilities or something is missing
925 // Both will end up `true` if file validity isn't being checked
928 PostMessage(hWnd, WM_USER + 1, MAKEWPARAM(bIsaFolderCompare, bIsaFileCompare), MAKELPARAM(iStatusMsgId, bProject));
937 * @brief Update any resources necessary after a GUI language change
939 void COpenView::UpdateResources()
941 theApp.m_pLangDlg->RetranslateDialog(m_hWnd, MAKEINTRESOURCE(IDD_OPEN));
942 if (m_strUnpacker != m_infoHandler.m_PluginName)
943 m_strUnpacker = theApp.LoadString(IDS_OPEN_UNPACKERDISABLED);
947 * @brief Enable/disable components based on validity of paths.
949 void COpenView::UpdateButtonStates()
951 UpdateData(TRUE); // load member variables from screen
952 KillTimer(IDT_CHECKFILES);
955 if (m_pUpdateButtonStatusThread == nullptr)
957 m_pUpdateButtonStatusThread = AfxBeginThread(
958 UpdateButtonStatesThread, nullptr, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED);
959 m_pUpdateButtonStatusThread->m_bAutoDelete = FALSE;
960 m_pUpdateButtonStatusThread->ResumeThread();
961 while (PostThreadMessage(m_pUpdateButtonStatusThread->m_nThreadID, WM_NULL, 0, 0) == FALSE)
965 UpdateButtonStatesThreadParams *pParams = new UpdateButtonStatesThreadParams;
966 pParams->m_hWnd = this->m_hWnd;
967 pParams->m_paths = PathContext(std::vector<String>(&m_strPath[0], &m_strPath[m_strPath[2].empty() ? 2 : 3]));
969 PostThreadMessage(m_pUpdateButtonStatusThread->m_nThreadID, WM_USER + 2, (WPARAM)pParams, 0);
972 void COpenView::TerminateThreadIfRunning()
974 if (m_pUpdateButtonStatusThread == nullptr)
977 PostThreadMessage(m_pUpdateButtonStatusThread->m_nThreadID, WM_QUIT, 0, 0);
978 DWORD dwResult = WaitForSingleObject(m_pUpdateButtonStatusThread->m_hThread, 100);
979 if (dwResult != WAIT_OBJECT_0)
981 m_pUpdateButtonStatusThread->SuspendThread();
982 TerminateThread(m_pUpdateButtonStatusThread->m_hThread, 0);
984 delete m_pUpdateButtonStatusThread;
985 m_pUpdateButtonStatusThread = nullptr;
989 * @brief Called when user changes selection in left/middle/right path's combo box.
991 void COpenView::OnSelchangePathCombo(UINT nId)
993 const int index = nId - IDC_PATH0_COMBO;
994 int sel = m_ctlPath[index].GetCurSel();
998 m_ctlPath[index].GetLBText(sel, cstrPath);
999 m_strPath[index] = cstrPath;
1000 m_ctlPath[index].SetWindowText(cstrPath);
1003 UpdateButtonStates();
1006 void COpenView::OnSetfocusPathCombo(UINT id, NMHDR *pNMHDR, LRESULT *pResult)
1008 if (!m_bAutoCompleteReady[id - IDC_PATH0_COMBO])
1010 int nSource = GetOptionsMgr()->GetInt(OPT_AUTO_COMPLETE_SOURCE);
1012 m_ctlPath[id - IDC_PATH0_COMBO].SetAutoComplete(nSource);
1013 m_bAutoCompleteReady[id - IDC_PATH0_COMBO] = true;
1018 void COpenView::OnDragBeginPathCombo(UINT id, NMHDR *pNMHDR, LRESULT *pResult)
1020 m_ctlPath[id - IDC_PATH0_COMBO].SetFocus();
1026 * @brief Called every time paths are edited.
1028 void COpenView::OnEditEvent(UINT nID)
1030 const int N = nID - IDC_PATH0_COMBO;
1031 if (CEdit *const edit = m_ctlPath[N].GetEditCtrl())
1033 int const len = edit->GetWindowTextLength();
1034 if (edit->GetSel() == MAKEWPARAM(len, len))
1037 edit->GetWindowText(text);
1038 // Remove any double quotes
1040 if (text.GetLength() != len)
1042 edit->SetSel(0, len);
1043 edit->ReplaceSel(text);
1047 // (Re)start timer to path validity check delay
1048 // If timer starting fails, update buttonstates immediately
1049 if (!SetTimer(IDT_CHECKFILES, CHECKFILES_TIMEOUT, nullptr))
1050 UpdateButtonStates();
1054 * @brief Handle timer events.
1055 * Checks if paths are valid and sets control states accordingly.
1056 * @param [in] nIDEvent Timer ID that fired.
1058 void COpenView::OnTimer(UINT_PTR nIDEvent)
1060 if (nIDEvent == IDT_CHECKFILES || nIDEvent == IDT_RETRY)
1061 UpdateButtonStates();
1063 CFormView::OnTimer(nIDEvent);
1067 * @brief Called when users selects plugin browse button.
1069 void COpenView::OnSelectUnpacker()
1071 paths::PATH_EXISTENCE pathsType;
1075 for (auto& strPath: m_strPath)
1077 if (nFiles == 2 && strPath.empty())
1079 m_files.SetSize(nFiles + 1);
1080 m_files[nFiles] = strPath;
1083 pathsType = paths::GetPairComparability(m_files);
1085 if (pathsType != paths::IS_EXISTING_FILE)
1088 // let the user select a handler
1089 CSelectUnpackerDlg dlg(m_files[0], this);
1090 PackingInfo infoUnpacker(PLUGIN_MODE::PLUGIN_AUTO);
1091 dlg.SetInitialInfoHandler(&infoUnpacker);
1093 if (dlg.DoModal() == IDOK)
1095 m_infoHandler = dlg.GetInfoHandler();
1097 m_strUnpacker = m_infoHandler.m_PluginName;
1103 LRESULT COpenView::OnUpdateStatus(WPARAM wParam, LPARAM lParam)
1105 const bool bIsaFolderCompare = LOWORD(wParam) != 0;
1106 const bool bIsaFileCompare = HIWORD(wParam) != 0;
1107 const bool bProject = HIWORD(lParam) != 0;
1108 const int iStatusMsgId = LOWORD(lParam);
1110 EnableDlgItem(IDOK, bIsaFolderCompare || bIsaFileCompare || bProject);
1112 EnableDlgItem(IDC_FILES_DIRS_GROUP4, bIsaFileCompare);
1113 EnableDlgItem(IDC_UNPACKER_EDIT, bIsaFileCompare);
1114 EnableDlgItem(IDC_SELECT_UNPACKER, bIsaFileCompare);
1116 EnableDlgItem(IDC_FILES_DIRS_GROUP3, bIsaFolderCompare);
1117 EnableDlgItem(IDC_EXT_COMBO, bIsaFolderCompare);
1118 EnableDlgItem(IDC_SELECT_FILTER, bIsaFolderCompare);
1119 EnableDlgItem(IDC_RECURS_CHECK, bIsaFolderCompare);
1121 SetStatus(iStatusMsgId);
1123 if (iStatusMsgId != IDS_OPEN_FILESDIRS && m_retryCount <= RETRY_MAX)
1125 if (m_retryCount == 0)
1126 SetTimer(IDT_RETRY, CHECKFILES_TIMEOUT, nullptr);
1131 KillTimer(IDT_RETRY);
1138 * @brief Sets the path status text.
1139 * The open dialog shows a status text of selected paths. This function
1140 * is used to set that status text.
1141 * @param [in] msgID Resource ID of status text to set.
1143 void COpenView::SetStatus(UINT msgID)
1145 String msg = theApp.LoadString(msgID);
1146 SetDlgItemText(IDC_OPEN_STATUS, msg);
1150 * @brief Set the plugin edit box text.
1151 * Plugin edit box is at the same time a plugin status view. This function
1152 * sets the status text.
1153 * @param [in] msgID Resource ID of status text to set.
1155 void COpenView::SetUnpackerStatus(UINT msgID)
1157 String msg = (msgID == 0 ? m_strUnpacker : theApp.LoadString(msgID));
1158 SetDlgItemText(IDC_UNPACKER_EDIT, msg);
1162 * @brief Called when "Select..." button for filters is selected.
1164 void COpenView::OnSelectFilter()
1166 String filterPrefix = _("[F] ");
1169 const bool bUseMask = theApp.m_pGlobalFileFilter->IsUsingMask();
1170 GetDlgItemText(IDC_EXT_COMBO, curFilter);
1171 curFilter = strutils::trim_ws(curFilter);
1173 GetMainFrame()->SelectFilter();
1175 String filterNameOrMask = theApp.m_pGlobalFileFilter->GetFilterNameOrMask();
1176 if (theApp.m_pGlobalFileFilter->IsUsingMask())
1178 // If we had filter chosen and now has mask we can overwrite filter
1179 if (!bUseMask || curFilter[0] != '*')
1181 SetDlgItemText(IDC_EXT_COMBO, filterNameOrMask);
1186 filterNameOrMask = filterPrefix + filterNameOrMask;
1187 SetDlgItemText(IDC_EXT_COMBO, filterNameOrMask);
1191 void COpenView::OnOptions()
1193 GetMainFrame()->PostMessage(WM_COMMAND, ID_OPTIONS);
1196 void COpenView::OnDropDownOptions(NMHDR *pNMHDR, LRESULT *pResult)
1198 NMTOOLBAR dropDown = { 0 };
1199 dropDown.hdr.code = TBN_DROPDOWN;
1200 dropDown.hdr.hwndFrom = GetMainFrame()->GetDescendantWindow(AFX_IDW_TOOLBAR)->GetSafeHwnd();
1201 dropDown.hdr.idFrom = AFX_IDW_TOOLBAR;
1202 GetDlgItem(IDC_OPTIONS)->GetWindowRect(&dropDown.rcButton);
1203 GetMainFrame()->ScreenToClient(&dropDown.rcButton);
1204 GetMainFrame()->SendMessage(WM_NOTIFY, dropDown.hdr.idFrom, reinterpret_cast<LPARAM>(&dropDown));
1209 * @brief Read paths and filter from project file.
1210 * Reads the given project file. After the file is read, found paths and
1211 * filter is updated to dialog GUI. Other possible settings found in the
1212 * project file are kept in memory and used later when loading paths
1214 * @param [in] path Path to the project file.
1215 * @return `true` if the project file was successfully loaded, `false` otherwise.
1217 bool COpenView::LoadProjectFile(const String &path)
1219 String filterPrefix = _("[F] ");
1222 if (!theApp.LoadProjectFile(path, prj))
1224 if (prj.Items().size() == 0)
1227 ProjectFileItem& projItem = *prj.Items().begin();
1228 projItem.GetPaths(m_files, recurse);
1229 m_bRecurse = recurse;
1230 m_dwFlags[0] &= ~FFILEOPEN_READONLY;
1231 m_dwFlags[0] |= projItem.GetLeftReadOnly() ? FFILEOPEN_READONLY : 0;
1232 if (m_files.GetSize() < 3)
1234 m_dwFlags[1] &= ~FFILEOPEN_READONLY;
1235 m_dwFlags[1] |= projItem.GetRightReadOnly() ? FFILEOPEN_READONLY : 0;
1239 m_dwFlags[1] &= ~FFILEOPEN_READONLY;
1240 m_dwFlags[1] |= projItem.GetMiddleReadOnly() ? FFILEOPEN_READONLY : 0;
1241 m_dwFlags[2] &= ~FFILEOPEN_READONLY;
1242 m_dwFlags[2] |= projItem.GetRightReadOnly() ? FFILEOPEN_READONLY : 0;
1244 if (projItem.HasFilter())
1246 m_strExt = strutils::trim_ws(projItem.GetFilter());
1247 if (m_strExt[0] != '*')
1248 m_strExt.insert(0, filterPrefix);
1254 * @brief Removes whitespaces from left and right paths
1255 * @note Assumes UpdateData(TRUE) is called before this function.
1257 void COpenView::TrimPaths()
1259 for (auto& strPath: m_strPath)
1260 strPath = strutils::trim_ws(strPath);
1264 * @brief Update control states when dialog is activated.
1266 * Update control states when user re-activates dialog. User might have
1267 * switched for other program to e.g. update files/folders and then
1268 * swiches back to WinMerge. Its nice to see WinMerge detects updated
1271 void COpenView::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized)
1273 CFormView::OnActivate(nState, pWndOther, bMinimized);
1275 if (nState == WA_ACTIVE || nState == WA_CLICKACTIVE)
1276 UpdateButtonStates();
1279 void COpenView::OnEditAction(int msg, WPARAM wParam, LPARAM lParam)
1281 CWnd *pCtl = GetFocus();
1282 if (pCtl != nullptr)
1283 pCtl->PostMessage(msg, wParam, lParam);
1286 template <int MSG, int WPARAM, int LPARAM>
1287 void COpenView::OnEditAction()
1289 OnEditAction(MSG, WPARAM, LPARAM);
1293 * @brief Open help from mainframe when user presses F1.
1295 void COpenView::OnHelp()
1297 theApp.ShowHelp(OpenDlgHelpLocation);
1300 /////////////////////////////////////////////////////////////////////////////
1302 // OnDropFiles code from CDropEdit
1303 // Copyright 1997 Chris Losinger
1305 // shortcut expansion code modified from :
1306 // CShortcut, 1996 Rob Warner
1310 * @brief Drop paths(s) to the dialog.
1311 * One or two paths can be dropped to the dialog. The behaviour is:
1313 * - drop to empty path edit box (check left first)
1314 * - if both boxes have a path, drop to left path
1316 * - overwrite both paths, empty or not
1317 * @param [in] dropInfo Dropped data, including paths.
1319 void COpenView::OnDropFiles(const std::vector<String>& files)
1321 const size_t fileCount = files.size();
1323 // Add dropped paths to the dialog
1327 m_strPath[0] = files[0];
1328 m_strPath[1] = files[1];
1329 m_strPath[2] = files[2];
1331 UpdateButtonStates();
1333 else if (fileCount == 2)
1335 m_strPath[0] = files[0];
1336 m_strPath[1] = files[1];
1338 UpdateButtonStates();
1340 else if (fileCount == 1)
1343 GetCursorPos(&point);
1344 ScreenToClient(&point);
1345 if (CWnd *const pwndHit = ChildWindowFromPoint(point,
1346 CWP_SKIPINVISIBLE | CWP_SKIPDISABLED | CWP_SKIPTRANSPARENT))
1348 switch (int const id = pwndHit->GetDlgCtrlID())
1350 case IDC_PATH0_COMBO:
1351 case IDC_PATH1_COMBO:
1352 case IDC_PATH2_COMBO:
1353 m_strPath[id - IDC_PATH0_COMBO] = files[0];
1356 if (m_strPath[0].empty())
1357 m_strPath[0] = files[0];
1358 else if (m_strPath[1].empty())
1359 m_strPath[1] = files[0];
1360 else if (m_strPath[2].empty())
1361 m_strPath[2] = files[0];
1363 m_strPath[0] = files[0];
1368 UpdateButtonStates();