1 /////////////////////////////////////////////////////////////////////////////
2 // WinMerge: an interactive diff/merge utility
3 // Copyright (C) 1997-2000 Thingamahoochie Software
6 // This program is free software; you can redistribute it and/or modify
7 // it under the terms of the GNU General Public License as published by
8 // the Free Software Foundation; either version 2 of the License, or
9 // (at your option) any later version.
11 // This program is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
16 // You should have received a copy of the GNU General Public License
17 // along with this program; if not, write to the Free Software
18 // Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20 /////////////////////////////////////////////////////////////////////////////
24 * @brief Implementation of the COpenView class
31 #include "UnicodeString.h"
34 #include "ProjectFile.h"
36 #include "SelectUnpackerDlg.h"
37 #include "OptionsDef.h"
39 #include "OptionsMgr.h"
40 #include "FileOrFolderSelect.h"
42 #include "Constants.h"
44 #include "DropHandler.h"
45 #include "FileFilterHelper.h"
48 #include "LanguageSelect.h"
49 #include "Win_VersionHelper.h"
56 #define BCN_DROPDOWN (BCN_FIRST + 0x0002)
59 // Timer ID and timeout for delaying path validity check
60 const UINT IDT_CHECKFILES = 1;
61 const UINT CHECKFILES_TIMEOUT = 1000; // milliseconds
62 static const TCHAR EMPTY_EXTENSION[] = _T(".*");
64 /** @brief Location for Open-dialog specific help to open. */
65 static TCHAR OpenDlgHelpLocation[] = _T("::/htmlhelp/Open_paths.html");
69 IMPLEMENT_DYNCREATE(COpenView, CFormView)
71 BEGIN_MESSAGE_MAP(COpenView, CFormView)
72 //{{AFX_MSG_MAP(COpenView)
73 ON_BN_CLICKED(IDC_PATH0_BUTTON, OnPathButton<0>)
74 ON_BN_CLICKED(IDC_PATH1_BUTTON, OnPathButton<1>)
75 ON_BN_CLICKED(IDC_PATH2_BUTTON, OnPathButton<2>)
76 ON_BN_CLICKED(IDC_SWAP01_BUTTON, (OnSwapButton<IDC_PATH0_COMBO, IDC_PATH1_COMBO>))
77 ON_BN_CLICKED(IDC_SWAP12_BUTTON, (OnSwapButton<IDC_PATH1_COMBO, IDC_PATH2_COMBO>))
78 ON_BN_CLICKED(IDC_SWAP02_BUTTON, (OnSwapButton<IDC_PATH0_COMBO, IDC_PATH2_COMBO>))
79 ON_CBN_SELCHANGE(IDC_PATH0_COMBO, OnSelchangePathCombo<0>)
80 ON_CBN_SELCHANGE(IDC_PATH1_COMBO, OnSelchangePathCombo<1>)
81 ON_CBN_SELCHANGE(IDC_PATH2_COMBO, OnSelchangePathCombo<2>)
82 ON_CBN_EDITCHANGE(IDC_PATH0_COMBO, OnEditEvent<0>)
83 ON_CBN_EDITCHANGE(IDC_PATH1_COMBO, OnEditEvent<1>)
84 ON_CBN_EDITCHANGE(IDC_PATH2_COMBO, OnEditEvent<2>)
85 ON_BN_CLICKED(IDC_SELECT_UNPACKER, OnSelectUnpacker)
86 ON_CBN_SELENDCANCEL(IDC_PATH0_COMBO, UpdateButtonStates)
87 ON_CBN_SELENDCANCEL(IDC_PATH1_COMBO, UpdateButtonStates)
88 ON_CBN_SELENDCANCEL(IDC_PATH2_COMBO, UpdateButtonStates)
89 ON_NOTIFY_RANGE(CBEN_BEGINEDIT, IDC_PATH0_COMBO, IDC_PATH2_COMBO, OnSetfocusPathCombo)
90 ON_NOTIFY_RANGE(CBEN_DRAGBEGIN, IDC_PATH0_COMBO, IDC_PATH2_COMBO, OnDragBeginPathCombo)
92 ON_BN_CLICKED(IDC_SELECT_FILTER, OnSelectFilter)
93 ON_BN_CLICKED(IDC_OPTIONS, OnOptions)
94 ON_NOTIFY(BCN_DROPDOWN, IDC_OPTIONS, OnDropDownOptions)
96 ON_COMMAND(ID_LOAD_PROJECT, OnLoadProject)
97 ON_COMMAND(ID_SAVE_PROJECT, OnSaveProject)
98 ON_NOTIFY(BCN_DROPDOWN, ID_SAVE_PROJECT, OnDropDownSaveProject)
99 ON_COMMAND(IDOK, OnOK)
100 ON_COMMAND(IDCANCEL, OnCancel)
101 ON_COMMAND(ID_HELP, OnHelp)
102 ON_COMMAND(ID_EDIT_COPY, OnEditAction<WM_COPY>)
103 ON_COMMAND(ID_EDIT_PASTE, OnEditAction<WM_PASTE>)
104 ON_COMMAND(ID_EDIT_CUT, OnEditAction<WM_CUT>)
105 ON_COMMAND(ID_EDIT_UNDO, OnEditAction<WM_UNDO>)
106 ON_COMMAND(ID_EDIT_SELECT_ALL, (OnEditAction<EM_SETSEL, 0, -1>))
107 ON_MESSAGE(WM_USER + 1, OnUpdateStatus)
111 ON_WM_WINDOWPOSCHANGING()
112 ON_WM_WINDOWPOSCHANGED()
118 // COpenView construction/destruction
120 COpenView::COpenView()
121 : CFormView(COpenView::IDD)
122 , m_pUpdateButtonStatusThread(nullptr)
124 , m_pDropHandler(nullptr)
126 , m_bAutoCompleteReady()
127 , m_bReadOnly {false, false, false}
128 , m_hIconRotate(theApp.LoadIcon(IDI_ROTATE2))
129 , m_hCursorNo(LoadCursor(nullptr, IDC_NO))
131 // CWnd::EnableScrollBarCtrl() called inside CScrollView::UpdateBars() is quite slow.
132 // Therefore, set m_bInsideUpdate = TRUE so that CScrollView::UpdateBars() does almost nothing.
133 m_bInsideUpdate = TRUE;
136 COpenView::~COpenView()
138 TerminateThreadIfRunning();
141 void COpenView::DoDataExchange(CDataExchange* pDX)
143 CFormView::DoDataExchange(pDX);
144 //{{AFX_DATA_MAP(COpenView)
145 DDX_Control(pDX, IDC_EXT_COMBO, m_ctlExt);
146 DDX_Control(pDX, IDC_PATH0_COMBO, m_ctlPath[0]);
147 DDX_Control(pDX, IDC_PATH1_COMBO, m_ctlPath[1]);
148 DDX_Control(pDX, IDC_PATH2_COMBO, m_ctlPath[2]);
149 DDX_CBStringExact(pDX, IDC_PATH0_COMBO, m_strPath[0]);
150 DDX_CBStringExact(pDX, IDC_PATH1_COMBO, m_strPath[1]);
151 DDX_CBStringExact(pDX, IDC_PATH2_COMBO, m_strPath[2]);
152 DDX_Check(pDX, IDC_PATH0_READONLY, m_bReadOnly[0]);
153 DDX_Check(pDX, IDC_PATH1_READONLY, m_bReadOnly[1]);
154 DDX_Check(pDX, IDC_PATH2_READONLY, m_bReadOnly[2]);
155 DDX_Check(pDX, IDC_RECURS_CHECK, m_bRecurse);
156 DDX_CBStringExact(pDX, IDC_EXT_COMBO, m_strExt);
157 DDX_Text(pDX, IDC_UNPACKER_EDIT, m_strUnpacker);
161 BOOL COpenView::PreCreateWindow(CREATESTRUCT& cs)
163 // TODO: Modify the Window class or styles here by modifying
164 // the CREATESTRUCT cs
165 cs.style &= ~WS_BORDER;
166 cs.dwExStyle &= ~WS_EX_CLIENTEDGE;
167 return CFormView::PreCreateWindow(cs);
170 void COpenView::OnInitialUpdate()
172 if (!IsVista_OrGreater())
175 SendDlgItemMessage(IDC_OPTIONS, BM_SETSTYLE, BS_PUSHBUTTON, TRUE);
176 SendDlgItemMessage(ID_SAVE_PROJECT, BM_SETSTYLE, BS_PUSHBUTTON, TRUE);
179 m_sizeOrig = GetTotalSize();
181 theApp.TranslateDialog(m_hWnd);
183 if (!LoadImageFromResource(m_image, MAKEINTRESOURCE(IDR_LOGO), _T("IMAGE")))
186 CFormView::OnInitialUpdate();
188 // set caption to "swap paths" button
190 GetDlgItem(IDC_SWAP01_BUTTON)->GetFont()->GetObject(sizeof(lf), &lf);
191 lf.lfCharSet = SYMBOL_CHARSET;
192 lstrcpy(lf.lfFaceName, _T("Wingdings"));
193 m_fontSwapButton.CreateFontIndirect(&lf);
194 const int ids[] = {IDC_SWAP01_BUTTON, IDC_SWAP12_BUTTON, IDC_SWAP02_BUTTON};
195 for (int i = 0; i < sizeof(ids)/sizeof(ids[0]); ++i)
197 GetDlgItem(ids[i])->SetFont(&m_fontSwapButton);
198 SetDlgItemText(ids[i], _T("\xf4"));
201 m_constraint.InitializeCurrentSize(this);
202 m_constraint.InitializeSpecificSize(this, m_sizeOrig.cx, m_sizeOrig.cy);
203 m_constraint.SetMaxSizePixels(-1, m_sizeOrig.cy);
204 m_constraint.SetScrollScale(this, 1.0, 1.0);
205 m_constraint.SetSizeGrip(prdlg::CMoveConstraint::SG_NONE);
206 m_constraint.DisallowHeightGrowth();
207 //m_constraint.SubclassWnd(); // install subclassing
209 m_constraint.LoadPosition(_T("ResizeableDialogs"), _T("OpenView"), false); // persist size via registry
210 m_constraint.UpdateSizes();
212 COpenDoc *pDoc = GetDocument();
215 GetWindowText(strTitle);
216 pDoc->SetTitle(strTitle);
218 m_files = pDoc->m_files;
219 m_bRecurse = pDoc->m_bRecurse;
220 m_strExt = pDoc->m_strExt;
221 m_strUnpacker = pDoc->m_strUnpacker;
222 m_infoHandler = pDoc->m_infoHandler;
223 m_dwFlags[0] = pDoc->m_dwFlags[0];
224 m_dwFlags[1] = pDoc->m_dwFlags[1];
225 m_dwFlags[2] = pDoc->m_dwFlags[2];
227 m_ctlPath[0].SetFileControlStates();
228 m_ctlPath[1].SetFileControlStates();
229 m_ctlPath[2].SetFileControlStates(true);
231 for (int file = 0; file < m_files.GetSize(); file++)
233 m_strPath[file] = m_files[file];
234 m_ctlPath[file].SetWindowText(m_files[file].c_str());
235 m_bReadOnly[file] = (m_dwFlags[file] & FFILEOPEN_READONLY) != 0;
238 m_ctlPath[0].AttachSystemImageList();
239 m_ctlPath[1].AttachSystemImageList();
240 m_ctlPath[2].AttachSystemImageList();
241 LoadComboboxStates();
243 bool bDoUpdateData = true;
244 for (auto& strPath: m_strPath)
246 if (!strPath.empty())
247 bDoUpdateData = false;
249 UpdateData(bDoUpdateData);
251 String filterNameOrMask = theApp.m_pGlobalFileFilter->GetFilterNameOrMask();
252 bool bMask = theApp.m_pGlobalFileFilter->IsUsingMask();
256 String filterPrefix = _("[F] ");
257 filterNameOrMask = filterPrefix + filterNameOrMask;
260 int ind = m_ctlExt.FindStringExact(0, filterNameOrMask.c_str());
262 m_ctlExt.SetCurSel(ind);
265 ind = m_ctlExt.InsertString(0, filterNameOrMask.c_str());
267 m_ctlExt.SetCurSel(ind);
269 LogErrorString(_T("Failed to add string to filters combo list!"));
272 if (!GetOptionsMgr()->GetBool(OPT_VERIFY_OPEN_PATHS))
274 EnableDlgItem(IDOK, true);
275 EnableDlgItem(IDC_UNPACKER_EDIT, true);
276 EnableDlgItem(IDC_SELECT_UNPACKER, true);
279 UpdateButtonStates();
281 bool bOverwriteRecursive = false;
282 if (m_dwFlags[0] & FFILEOPEN_PROJECT || m_dwFlags[1] & FFILEOPEN_PROJECT)
283 bOverwriteRecursive = true;
284 if (m_dwFlags[0] & FFILEOPEN_CMDLINE || m_dwFlags[1] & FFILEOPEN_CMDLINE)
285 bOverwriteRecursive = true;
286 if (!bOverwriteRecursive)
287 m_bRecurse = GetOptionsMgr()->GetBool(OPT_CMP_INCLUDE_SUBDIRS);
289 m_strUnpacker = m_infoHandler.m_PluginName;
291 SetStatus(IDS_OPEN_FILESDIRS);
292 SetUnpackerStatus(IDS_USERCHOICE_NONE);
294 m_pDropHandler = new DropHandler(std::bind(&COpenView::OnDropFiles, this, std::placeholders::_1));
295 RegisterDragDrop(m_hWnd, m_pDropHandler);
298 void COpenView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint)
300 m_bRecurse = GetDocument()->m_bRecurse;
304 // COpenView diagnostics
307 COpenDoc* COpenView::GetDocument() const // non-debug version is inline
309 ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(COpenDoc)));
310 return (COpenDoc*)m_pDocument;
314 /////////////////////////////////////////////////////////////////////////////
315 // COpenView message handlers
317 void COpenView::OnPaint()
323 // Draw the logo image
324 CSize size{ m_image.GetWidth(), m_image.GetHeight() };
325 CRect rcImage(0, 0, size.cx * GetSystemMetrics(SM_CXSMICON) / 16, size.cy * GetSystemMetrics(SM_CYSMICON) / 16);
326 m_image.Draw(dc.m_hDC, rcImage, Gdiplus::InterpolationModeBicubic);
327 // And extend it to the Right boundary
328 dc.PatBlt(rcImage.Width(), 0, rc.Width() - rcImage.Width(), rcImage.Height(), PATCOPY);
330 // Draw the resize gripper in the Lower Right corner.
332 rcGrip.left = rc.right - GetSystemMetrics(SM_CXVSCROLL);
333 rcGrip.top = rc.bottom - GetSystemMetrics(SM_CYHSCROLL);
334 dc.DrawFrameControl(&rcGrip, DFC_SCROLL, DFCS_SCROLLSIZEGRIP);
336 // Draw a line to separate the Status Line
337 CPen newPen(PS_SOLID, 1, RGB(208, 208, 208)); // a very light gray
338 CPen* oldpen = (CPen*)dc.SelectObject(&newPen);
341 GetDlgItem(IDC_OPEN_STATUS)->GetWindowRect(&rcStatus);
342 ScreenToClient(&rcStatus);
343 dc.MoveTo(0, rcStatus.top - 3);
344 dc.LineTo(rc.right, rcStatus.top - 3);
345 dc.SelectObject(oldpen);
347 CFormView::OnPaint();
350 void COpenView::OnLButtonUp(UINT nFlags, CPoint point)
352 if (::GetCapture() == m_hWnd)
354 if (CWnd *const pwndHit = ChildWindowFromPoint(point,
355 CWP_SKIPINVISIBLE | CWP_SKIPDISABLED | CWP_SKIPTRANSPARENT))
357 switch (int const id1 = pwndHit->GetDlgCtrlID())
359 case IDC_PATH0_COMBO:
360 case IDC_PATH1_COMBO:
361 case IDC_PATH2_COMBO:
363 CWnd *pwndChild = GetFocus();
364 if (IsChild(pwndChild) && !pwndHit->IsChild(pwndChild)) do
366 id2 = pwndChild->GetDlgCtrlID();
367 pwndChild = pwndChild->GetParent();
368 } while (pwndChild != this);
371 case IDC_PATH0_COMBO:
372 case IDC_PATH1_COMBO:
373 case IDC_PATH2_COMBO:
375 GetDlgItemText(id1, s1);
376 GetDlgItemText(id2, s2);
377 SetDlgItemText(id1, s2);
378 SetDlgItemText(id2, s1);
389 void COpenView::OnMouseMove(UINT nFlags, CPoint point)
391 if (::GetCapture() == m_hWnd)
393 if (CWnd *const pwndHit = ChildWindowFromPoint(point,
394 CWP_SKIPINVISIBLE | CWP_SKIPDISABLED | CWP_SKIPTRANSPARENT))
396 switch (pwndHit->GetDlgCtrlID())
398 case IDC_PATH0_COMBO:
399 case IDC_PATH1_COMBO:
400 case IDC_PATH2_COMBO:
401 if (!pwndHit->IsChild(GetFocus()))
403 SetCursor(m_hIconRotate);
408 SetCursor(m_hCursorNo);
415 void COpenView::OnWindowPosChanging(WINDOWPOS* lpwndpos)
417 if ((lpwndpos->flags & (SWP_NOMOVE | SWP_NOSIZE)) == 0)
419 CFrameWnd *const pFrameWnd = GetParentFrame();
420 if (pFrameWnd == GetTopLevelFrame()->GetActiveFrame())
423 pFrameWnd->GetClientRect(&rc);
424 lpwndpos->flags |= SWP_FRAMECHANGED | SWP_SHOWWINDOW;
425 lpwndpos->cy = m_sizeOrig.cy;
426 if (lpwndpos->flags & SWP_NOOWNERZORDER)
428 lpwndpos->x = rc.right - (lpwndpos->x + lpwndpos->cx);
429 lpwndpos->cx = rc.right - 2 * lpwndpos->x;
430 lpwndpos->y = (rc.bottom - lpwndpos->cy) / 2;
434 else if (pFrameWnd->IsZoomed())
436 lpwndpos->cx = m_totalLog.cx;
437 lpwndpos->y = (rc.bottom - lpwndpos->cy) / 2;
441 if (lpwndpos->cx > rc.Width())
442 lpwndpos->cx = rc.Width();
443 if (lpwndpos->cx < m_sizeOrig.cx)
444 lpwndpos->cx = m_sizeOrig.cx;
445 lpwndpos->x = (rc.right - lpwndpos->cx) / 2;
452 void COpenView::OnWindowPosChanged(WINDOWPOS* lpwndpos)
454 if (lpwndpos->flags & SWP_FRAMECHANGED)
456 m_constraint.UpdateSizes();
457 CFrameWnd *const pFrameWnd = GetParentFrame();
458 if (pFrameWnd == GetTopLevelFrame()->GetActiveFrame())
460 m_constraint.Persist(true, false);
462 wp.length = sizeof wp;
463 pFrameWnd->GetWindowPlacement(&wp);
466 pFrameWnd->CalcWindowRect(&rc, CWnd::adjustOutside);
467 wp.rcNormalPosition.right = wp.rcNormalPosition.left + rc.Width();
468 wp.rcNormalPosition.bottom = wp.rcNormalPosition.top + rc.Height();
469 pFrameWnd->SetWindowPlacement(&wp);
472 CFormView::OnWindowPosChanged(lpwndpos);
475 void COpenView::OnDestroy()
477 if (m_pDropHandler != nullptr)
478 RevokeDragDrop(m_hWnd);
480 CFormView::OnDestroy();
483 LRESULT COpenView::OnNcHitTest(CPoint point)
485 if (GetParentFrame()->IsZoomed())
489 rc.left = rc.right - GetSystemMetrics(SM_CXVSCROLL);
490 rc.top = rc.bottom - GetSystemMetrics(SM_CYHSCROLL);
491 if (PtInRect(&rc, point))
494 return CFormView::OnNcHitTest(point);
497 void COpenView::OnButton(int index)
503 paths::PATH_EXISTENCE existence = paths::DoesPathExist(m_strPath[index]);
506 case paths::IS_EXISTING_DIR:
507 sfolder = m_strPath[index];
509 case paths::IS_EXISTING_FILE:
510 sfolder = paths::GetPathOnly(m_strPath[index]);
512 case paths::DOES_NOT_EXIST:
513 // Do nothing, empty foldername will be passed to dialog
516 _RPTF0(_CRT_ERROR, "Invalid return value from paths::DoesPathExist()");
520 if (SelectFileOrFolder(GetSafeHwnd(), s, sfolder.c_str()))
522 m_strPath[index] = s;
523 m_strBrowsePath[index] = s;
525 UpdateButtonStates();
530 * @brief Called when "Browse..." button is selected for N path.
533 void COpenView::OnPathButton()
538 template<int id1, int id2>
539 void COpenView::OnSwapButton()
542 GetDlgItemText(id1, s1);
543 GetDlgItemText(id2, s2);
545 SetDlgItemText(id1, s1);
546 SetDlgItemText(id2, s2);
550 * @brief Called when dialog is closed with "OK".
552 * Checks that paths are valid and sets filters.
554 void COpenView::OnOK()
556 int pathsType; // enum from paths::PATH_EXISTENCE in paths.h
557 const String filterPrefix = _("[F] ");
563 for (auto& strPath: m_strPath)
565 if (nFiles == 2 && strPath.empty())
567 m_files.SetSize(nFiles + 1);
568 m_files[nFiles] = strPath;
569 m_dwFlags[nFiles] &= ~FFILEOPEN_READONLY;
570 m_dwFlags[nFiles] |= m_bReadOnly[nFiles] ? FFILEOPEN_READONLY : 0;
573 // If left path is a project-file, load it
575 paths::SplitFilename(m_strPath[0], nullptr, nullptr, &ext);
576 if (m_strPath[1].empty() && strutils::compare_nocase(ext, ProjectFile::PROJECTFILE_EXT) == 0)
577 LoadProjectFile(m_strPath[0]);
579 pathsType = paths::GetPairComparability(m_files, IsArchiveFile);
581 if (pathsType == paths::DOES_NOT_EXIST)
583 LangMessageBox(IDS_ERROR_INCOMPARABLE, MB_ICONSTOP);
587 for (int index = 0; index < nFiles; index++)
589 // If user has edited path by hand, expand environment variables
590 bool bExpand = false;
591 if (strutils::compare_nocase(m_strBrowsePath[index], m_files[index]) != 0)
594 if (!paths::IsURLorCLSID(m_files[index]))
596 m_files[index] = paths::GetLongPath(m_files[index], bExpand);
598 // Add trailing '\' for directories if its missing
599 if (paths::DoesPathExist(m_files[index]) == paths::IS_EXISTING_DIR)
600 m_files[index] = paths::AddTrailingSlash(m_files[index]);
601 m_strPath[index] = m_files[index];
606 KillTimer(IDT_CHECKFILES);
608 String filter(strutils::trim_ws(m_strExt));
610 // If prefix found from start..
611 if (filter.substr(0, filterPrefix.length()) == filterPrefix)
613 // Remove prefix + space
614 filter.erase(0, filterPrefix.length());
615 if (!theApp.m_pGlobalFileFilter->SetFilter(filter))
617 // If filtername is not found use default *.* mask
618 theApp.m_pGlobalFileFilter->SetFilter(_T("*.*"));
621 GetOptionsMgr()->SaveOption(OPT_FILEFILTER_CURRENT, filter);
625 bool bFilterSet = theApp.m_pGlobalFileFilter->SetFilter(filter);
627 m_strExt = theApp.m_pGlobalFileFilter->GetFilterNameOrMask();
628 GetOptionsMgr()->SaveOption(OPT_FILEFILTER_CURRENT, filter);
631 SaveComboboxStates();
632 GetOptionsMgr()->SaveOption(OPT_CMP_INCLUDE_SUBDIRS, m_bRecurse);
633 LoadComboboxStates();
635 m_constraint.Persist(true, false);
637 COpenDoc *pDoc = GetDocument();
638 pDoc->m_files = m_files;
639 pDoc->m_bRecurse = m_bRecurse;
640 pDoc->m_strExt = m_strExt;
641 pDoc->m_strUnpacker = m_strUnpacker;
642 pDoc->m_infoHandler = m_infoHandler;
643 pDoc->m_dwFlags[0] = m_dwFlags[0];
644 pDoc->m_dwFlags[1] = m_dwFlags[1];
645 pDoc->m_dwFlags[2] = m_dwFlags[2];
647 if (GetOptionsMgr()->GetBool(OPT_CLOSE_WITH_OK))
648 GetParentFrame()->PostMessage(WM_CLOSE);
650 PathContext tmpPathContext(pDoc->m_files);
651 PackingInfo tmpPackingInfo(pDoc->m_infoHandler);
652 GetMainFrame()->DoFileOpen(
653 &tmpPathContext, std::array<DWORD, 3>(pDoc->m_dwFlags).data(),
654 nullptr, _T(""), pDoc->m_bRecurse, nullptr, _T(""), &tmpPackingInfo);
658 * @brief Called when dialog is closed via Cancel.
660 * Open-dialog is closed when `Cancel` button is selected or the
661 * `Esc` key is pressed. Save combobox states, since user may have
662 * removed items from them (with `shift-del`) and doesn't want them
664 * This is *not* called when the program is terminated, even if the
665 * dialog is visible at the time.
667 void COpenView::OnCancel()
669 SaveComboboxStates();
670 AfxGetMainWnd()->PostMessage(WM_COMMAND, ID_FILE_CLOSE);
674 * @brief Callled when Open-button for project file is selected.
676 void COpenView::OnLoadProject()
678 String fileName = AskProjectFileName(true);
679 if (fileName.empty())
683 if (!theApp.LoadProjectFile(fileName, project))
685 if (project.Items().size() == 0)
688 ProjectFileItem& projItem = *project.Items().begin();
689 projItem.GetPaths(paths, m_bRecurse);
690 projItem.GetLeftReadOnly();
691 if (paths.GetSize() < 3)
693 m_strPath[0] = paths[0];
694 m_strPath[1] = paths[1];
695 m_strPath[2] = _T("");
696 m_bReadOnly[0] = projItem.GetLeftReadOnly();
697 m_bReadOnly[1] = projItem.GetRightReadOnly();
698 m_bReadOnly[2] = false;
702 m_strPath[0] = paths[0];
703 m_strPath[1] = paths[1];
704 m_strPath[2] = paths[2];
705 m_bReadOnly[0] = projItem.GetLeftReadOnly();
706 m_bReadOnly[1] = projItem.GetMiddleReadOnly();
707 m_bReadOnly[2] = projItem.GetRightReadOnly();
709 m_strExt = projItem.GetFilter();
712 LangMessageBox(IDS_PROJFILE_LOAD_SUCCESS, MB_ICONINFORMATION);
716 * @brief Called when Save-button for project file is selected.
718 void COpenView::OnSaveProject()
722 String fileName = AskProjectFileName(false);
723 if (fileName.empty())
727 ProjectFileItem projItem;
729 if (!m_strPath[0].empty())
730 projItem.SetLeft(m_strPath[0], &m_bReadOnly[0]);
731 if (m_strPath[2].empty())
733 if (!m_strPath[1].empty())
734 projItem.SetRight(m_strPath[1], &m_bReadOnly[1]);
738 if (!m_strPath[1].empty())
739 projItem.SetMiddle(m_strPath[1], &m_bReadOnly[1]);
740 if (!m_strPath[2].empty())
741 projItem.SetRight(m_strPath[2], &m_bReadOnly[2]);
743 if (!m_strExt.empty())
745 // Remove possbile prefix from the filter name
746 String prefix = _("[F] ");
747 String strExt = m_strExt;
748 size_t ind = strExt.find(prefix, 0);
751 strExt.erase(0, prefix.length());
753 strExt = strutils::trim_ws_begin(strExt);
754 projItem.SetFilter(strExt);
756 projItem.SetSubfolders(m_bRecurse);
757 project.Items().push_back(projItem);
759 if (!theApp.SaveProjectFile(fileName, project))
762 LangMessageBox(IDS_PROJFILE_SAVE_SUCCESS, MB_ICONINFORMATION);
765 void COpenView::OnDropDownSaveProject(NMHDR *pNMHDR, LRESULT *pResult)
767 CRect rcButton, rcView;
768 GetDlgItem(ID_SAVE_PROJECT)->GetWindowRect(&rcButton);
770 VERIFY(menu.LoadMenu(IDR_POPUP_PROJECT));
771 theApp.TranslateMenu(menu.m_hMenu);
772 CMenu* pPopup = menu.GetSubMenu(0);
773 if (pPopup != nullptr)
775 pPopup->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON,
776 rcButton.left, rcButton.bottom, GetMainFrame());
782 * @brief Allow user to select a file to open/save.
784 String COpenView::AskProjectFileName(bool bOpen)
786 // get the default projects path
787 String strProjectFileName;
788 String strProjectPath = GetOptionsMgr()->GetString(OPT_PROJECTS_PATH);
790 if (!::SelectFile(GetSafeHwnd(), strProjectFileName, bOpen, strProjectPath.c_str(),
791 _T(""), _("WinMerge Project Files (*.WinMerge)|*.WinMerge||"), _T(".WinMerge")))
794 if (strProjectFileName.empty())
797 // get the path part from the filename
798 strProjectPath = paths::GetParentPath(strProjectFileName);
799 // store this as the new project path
800 GetOptionsMgr()->SaveOption(OPT_PROJECTS_PATH, strProjectPath);
801 return strProjectFileName;
805 * @brief Load File- and filter-combobox states.
807 void COpenView::LoadComboboxStates()
809 m_ctlPath[0].LoadState(_T("Files\\Left"));
810 m_ctlPath[1].LoadState(_T("Files\\Right"));
811 m_ctlPath[2].LoadState(_T("Files\\Option"));
812 m_ctlExt.LoadState(_T("Files\\Ext"));
816 * @brief Save File- and filter-combobox states.
818 void COpenView::SaveComboboxStates()
820 m_ctlPath[0].SaveState(_T("Files\\Left"));
821 m_ctlPath[1].SaveState(_T("Files\\Right"));
822 m_ctlPath[2].SaveState(_T("Files\\Option"));
823 m_ctlExt.SaveState(_T("Files\\Ext"));
826 struct UpdateButtonStatesThreadParams
832 static UINT UpdateButtonStatesThread(LPVOID lpParam)
837 CoInitialize(nullptr);
838 CAssureScriptsForThread scriptsForRescan;
840 while( (bRet = GetMessage( &msg, nullptr, 0, 0 )) != 0)
844 if (msg.message != WM_USER + 2)
847 bool bIsaFolderCompare = true;
848 bool bIsaFileCompare = true;
849 bool bInvalid[3] = {false, false, false};
850 int iStatusMsgId = 0;
851 int iUnpackerStatusMsgId = 0;
853 UpdateButtonStatesThreadParams *pParams = reinterpret_cast<UpdateButtonStatesThreadParams *>(msg.wParam);
854 PathContext paths = pParams->m_paths;
855 HWND hWnd = pParams->m_hWnd;
858 // Check if we have project file as left side path
859 bool bProject = false;
861 paths::SplitFilename(paths[0], nullptr, nullptr, &ext);
862 if (paths[1].empty() && strutils::compare_nocase(ext, ProjectFile::PROJECTFILE_EXT) == 0)
867 if (paths::DoesPathExist(paths[0], IsArchiveFile) == paths::DOES_NOT_EXIST)
869 if (paths::DoesPathExist(paths[1], IsArchiveFile) == paths::DOES_NOT_EXIST)
871 if (paths.GetSize() > 2 && paths::DoesPathExist(paths[2], IsArchiveFile) == paths::DOES_NOT_EXIST)
875 // Enable buttons as appropriate
876 if (GetOptionsMgr()->GetBool(OPT_VERIFY_OPEN_PATHS))
878 paths::PATH_EXISTENCE pathsType = paths::DOES_NOT_EXIST;
880 if (paths.GetSize() <= 2)
882 if (bInvalid[0] && bInvalid[1])
883 iStatusMsgId = IDS_OPEN_BOTHINVALID;
884 else if (bInvalid[0])
885 iStatusMsgId = IDS_OPEN_LEFTINVALID;
886 else if (bInvalid[1])
887 iStatusMsgId = IDS_OPEN_RIGHTINVALID2;
888 else if (!bInvalid[0] && !bInvalid[1])
890 pathsType = paths::GetPairComparability(paths, IsArchiveFile);
891 if (pathsType == paths::DOES_NOT_EXIST)
892 iStatusMsgId = IDS_OPEN_MISMATCH;
894 iStatusMsgId = IDS_OPEN_FILESDIRS;
899 if (bInvalid[0] && bInvalid[1] && bInvalid[2])
900 iStatusMsgId = IDS_OPEN_ALLINVALID;
901 else if (!bInvalid[0] && bInvalid[1] && bInvalid[2])
902 iStatusMsgId = IDS_OPEN_MIDDLERIGHTINVALID;
903 else if (bInvalid[0] && !bInvalid[1] && bInvalid[2])
904 iStatusMsgId = IDS_OPEN_LEFTRIGHTINVALID;
905 else if (!bInvalid[0] && !bInvalid[1] && bInvalid[2])
906 iStatusMsgId = IDS_OPEN_RIGHTINVALID3;
907 else if (bInvalid[0] && bInvalid[1] && !bInvalid[2])
908 iStatusMsgId = IDS_OPEN_LEFTMIDDLEINVALID;
909 else if (!bInvalid[0] && bInvalid[1] && !bInvalid[2])
910 iStatusMsgId = IDS_OPEN_MIDDLEINVALID;
911 else if (bInvalid[0] && !bInvalid[1] && !bInvalid[2])
912 iStatusMsgId = IDS_OPEN_LEFTINVALID;
913 else if (!bInvalid[0] && !bInvalid[1] && !bInvalid[2])
915 pathsType = paths::GetPairComparability(paths, IsArchiveFile);
916 if (pathsType == paths::DOES_NOT_EXIST)
917 iStatusMsgId = IDS_OPEN_MISMATCH;
919 iStatusMsgId = IDS_OPEN_FILESDIRS;
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 if (m_strPath[2].empty())
968 pParams->m_paths = PathContext(m_strPath[0], m_strPath[1]);
970 pParams->m_paths = PathContext(m_strPath[0], m_strPath[1], m_strPath[2]);
972 PostThreadMessage(m_pUpdateButtonStatusThread->m_nThreadID, WM_USER + 2, (WPARAM)pParams, 0);
975 void COpenView::TerminateThreadIfRunning()
977 if (m_pUpdateButtonStatusThread == nullptr)
980 PostThreadMessage(m_pUpdateButtonStatusThread->m_nThreadID, WM_QUIT, 0, 0);
981 DWORD dwResult = WaitForSingleObject(m_pUpdateButtonStatusThread->m_hThread, 100);
982 if (dwResult != WAIT_OBJECT_0)
984 m_pUpdateButtonStatusThread->SuspendThread();
985 TerminateThread(m_pUpdateButtonStatusThread->m_hThread, 0);
987 delete m_pUpdateButtonStatusThread;
988 m_pUpdateButtonStatusThread = nullptr;
992 * @brief Called when user changes selection in left/middle/right path's combo box.
994 void COpenView::OnSelchangeCombo(int index)
996 int sel = m_ctlPath[index].GetCurSel();
1000 m_ctlPath[index].GetLBText(sel, cstrPath);
1001 m_strPath[index] = cstrPath;
1002 m_ctlPath[index].SetWindowText(cstrPath);
1005 UpdateButtonStates();
1009 void COpenView::OnSelchangePathCombo()
1011 OnSelchangeCombo(N);
1014 void COpenView::OnSetfocusPathCombo(UINT id, NMHDR *pNMHDR, LRESULT *pResult)
1016 if (!m_bAutoCompleteReady[id - IDC_PATH0_COMBO])
1018 int nSource = GetOptionsMgr()->GetInt(OPT_AUTO_COMPLETE_SOURCE);
1020 m_ctlPath[id - IDC_PATH0_COMBO].SetAutoComplete(nSource);
1021 m_bAutoCompleteReady[id - IDC_PATH0_COMBO] = true;
1026 void COpenView::OnDragBeginPathCombo(UINT id, NMHDR *pNMHDR, LRESULT *pResult)
1028 m_ctlPath[id - IDC_PATH0_COMBO].SetFocus();
1034 * @brief Called every time paths are edited.
1037 void COpenView::OnEditEvent()
1039 if (CEdit *const edit = m_ctlPath[N].GetEditCtrl())
1041 int const len = edit->GetWindowTextLength();
1042 if (edit->GetSel() == MAKEWPARAM(len, len))
1045 edit->GetWindowText(text);
1046 // Remove any double quotes
1048 if (text.GetLength() != len)
1050 edit->SetSel(0, len);
1051 edit->ReplaceSel(text);
1055 // (Re)start timer to path validity check delay
1056 // If timer starting fails, update buttonstates immediately
1057 if (!SetTimer(IDT_CHECKFILES, CHECKFILES_TIMEOUT, nullptr))
1058 UpdateButtonStates();
1062 * @brief Handle timer events.
1063 * Checks if paths are valid and sets control states accordingly.
1064 * @param [in] nIDEvent Timer ID that fired.
1066 void COpenView::OnTimer(UINT_PTR nIDEvent)
1068 if (nIDEvent == IDT_CHECKFILES)
1069 UpdateButtonStates();
1071 CFormView::OnTimer(nIDEvent);
1075 * @brief Called when users selects plugin browse button.
1077 void COpenView::OnSelectUnpacker()
1079 paths::PATH_EXISTENCE pathsType;
1083 for (auto& strPath: m_strPath)
1085 if (nFiles == 2 && strPath.empty())
1087 m_files.SetSize(nFiles + 1);
1088 m_files[nFiles] = strPath;
1091 pathsType = paths::GetPairComparability(m_files);
1093 if (pathsType != paths::IS_EXISTING_FILE)
1096 // let the user select a handler
1097 CSelectUnpackerDlg dlg(m_files[0], this);
1098 PackingInfo infoUnpacker(PLUGIN_AUTO);
1099 dlg.SetInitialInfoHandler(&infoUnpacker);
1101 if (dlg.DoModal() == IDOK)
1103 m_infoHandler = dlg.GetInfoHandler();
1105 m_strUnpacker = m_infoHandler.m_PluginName;
1111 LRESULT COpenView::OnUpdateStatus(WPARAM wParam, LPARAM lParam)
1113 bool bIsaFolderCompare = LOWORD(wParam) != 0;
1114 bool bIsaFileCompare = HIWORD(wParam) != 0;
1115 bool bProject = HIWORD(lParam) != 0;
1117 EnableDlgItem(IDOK, bIsaFolderCompare || bIsaFileCompare || bProject);
1119 EnableDlgItem(IDC_FILES_DIRS_GROUP4, bIsaFileCompare);
1120 EnableDlgItem(IDC_UNPACKER_EDIT, bIsaFileCompare);
1121 EnableDlgItem(IDC_SELECT_UNPACKER, bIsaFileCompare);
1124 EnableDlgItem(IDC_FILES_DIRS_GROUP3, bIsaFolderCompare);
1125 EnableDlgItem(IDC_EXT_COMBO, bIsaFolderCompare);
1126 EnableDlgItem(IDC_SELECT_FILTER, bIsaFolderCompare);
1127 EnableDlgItem(IDC_RECURS_CHECK, bIsaFolderCompare);
1129 SetStatus(LOWORD(lParam));
1135 * @brief Sets the path status text.
1136 * The open dialog shows a status text of selected paths. This function
1137 * is used to set that status text.
1138 * @param [in] msgID Resource ID of status text to set.
1140 void COpenView::SetStatus(UINT msgID)
1142 String msg = theApp.LoadString(msgID);
1143 SetDlgItemText(IDC_OPEN_STATUS, msg);
1147 * @brief Set the plugin edit box text.
1148 * Plugin edit box is at the same time a plugin status view. This function
1149 * sets the status text.
1150 * @param [in] msgID Resource ID of status text to set.
1152 void COpenView::SetUnpackerStatus(UINT msgID)
1154 String msg = (msgID == 0 ? m_strUnpacker : theApp.LoadString(msgID));
1155 SetDlgItemText(IDC_UNPACKER_EDIT, msg);
1159 * @brief Called when "Select..." button for filters is selected.
1161 void COpenView::OnSelectFilter()
1163 String filterPrefix = _("[F] ");
1166 const bool bUseMask = theApp.m_pGlobalFileFilter->IsUsingMask();
1167 GetDlgItemText(IDC_EXT_COMBO, curFilter);
1168 curFilter = strutils::trim_ws(curFilter);
1170 GetMainFrame()->SelectFilter();
1172 String filterNameOrMask = theApp.m_pGlobalFileFilter->GetFilterNameOrMask();
1173 if (theApp.m_pGlobalFileFilter->IsUsingMask())
1175 // If we had filter chosen and now has mask we can overwrite filter
1176 if (!bUseMask || curFilter[0] != '*')
1178 SetDlgItemText(IDC_EXT_COMBO, filterNameOrMask);
1183 filterNameOrMask = filterPrefix + filterNameOrMask;
1184 SetDlgItemText(IDC_EXT_COMBO, filterNameOrMask);
1188 void COpenView::OnOptions()
1190 GetMainFrame()->PostMessage(WM_COMMAND, ID_OPTIONS);
1193 void COpenView::OnDropDownOptions(NMHDR *pNMHDR, LRESULT *pResult)
1195 NMTOOLBAR dropDown = { 0 };
1196 dropDown.hdr.code = TBN_DROPDOWN;
1197 dropDown.hdr.hwndFrom = GetMainFrame()->GetDescendantWindow(AFX_IDW_TOOLBAR)->GetSafeHwnd();
1198 dropDown.hdr.idFrom = AFX_IDW_TOOLBAR;
1199 GetDlgItem(IDC_OPTIONS)->GetWindowRect(&dropDown.rcButton);
1200 GetMainFrame()->ScreenToClient(&dropDown.rcButton);
1201 GetMainFrame()->SendMessage(WM_NOTIFY, dropDown.hdr.idFrom, reinterpret_cast<LPARAM>(&dropDown));
1206 * @brief Read paths and filter from project file.
1207 * Reads the given project file. After the file is read, found paths and
1208 * filter is updated to dialog GUI. Other possible settings found in the
1209 * project file are kept in memory and used later when loading paths
1211 * @param [in] path Path to the project file.
1212 * @return `true` if the project file was successfully loaded, `false` otherwise.
1214 bool COpenView::LoadProjectFile(const String &path)
1216 String filterPrefix = _("[F] ");
1219 if (!theApp.LoadProjectFile(path, prj))
1221 if (prj.Items().size() == 0)
1224 ProjectFileItem& projItem = *prj.Items().begin();
1225 projItem.GetPaths(m_files, recurse);
1226 m_bRecurse = recurse;
1227 m_dwFlags[0] &= ~FFILEOPEN_READONLY;
1228 m_dwFlags[0] |= projItem.GetLeftReadOnly() ? FFILEOPEN_READONLY : 0;
1229 if (m_files.GetSize() < 3)
1231 m_dwFlags[1] &= ~FFILEOPEN_READONLY;
1232 m_dwFlags[1] |= projItem.GetRightReadOnly() ? FFILEOPEN_READONLY : 0;
1236 m_dwFlags[1] &= ~FFILEOPEN_READONLY;
1237 m_dwFlags[1] |= projItem.GetMiddleReadOnly() ? FFILEOPEN_READONLY : 0;
1238 m_dwFlags[2] &= ~FFILEOPEN_READONLY;
1239 m_dwFlags[2] |= projItem.GetRightReadOnly() ? FFILEOPEN_READONLY : 0;
1241 if (projItem.HasFilter())
1243 m_strExt = strutils::trim_ws(projItem.GetFilter());
1244 if (m_strExt[0] != '*')
1245 m_strExt.insert(0, filterPrefix);
1251 * @brief Removes whitespaces from left and right paths
1252 * @note Assumes UpdateData(TRUE) is called before this function.
1254 void COpenView::TrimPaths()
1256 for (auto& strPath: m_strPath)
1257 strPath = strutils::trim_ws(strPath);
1261 * @brief Update control states when dialog is activated.
1263 * Update control states when user re-activates dialog. User might have
1264 * switched for other program to e.g. update files/folders and then
1265 * swiches back to WinMerge. Its nice to see WinMerge detects updated
1268 void COpenView::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized)
1270 CFormView::OnActivate(nState, pWndOther, bMinimized);
1272 if (nState == WA_ACTIVE || nState == WA_CLICKACTIVE)
1273 UpdateButtonStates();
1276 template <int MSG, int WPARAM, int LPARAM>
1277 void COpenView::OnEditAction()
1279 CWnd *pCtl = GetFocus();
1280 if (pCtl != nullptr)
1281 pCtl->PostMessage(MSG, WPARAM, LPARAM);
1285 * @brief Open help from mainframe when user presses F1.
1287 void COpenView::OnHelp()
1289 theApp.ShowHelp(OpenDlgHelpLocation);
1292 /////////////////////////////////////////////////////////////////////////////
1294 // OnDropFiles code from CDropEdit
1295 // Copyright 1997 Chris Losinger
1297 // shortcut expansion code modified from :
1298 // CShortcut, 1996 Rob Warner
1302 * @brief Drop paths(s) to the dialog.
1303 * One or two paths can be dropped to the dialog. The behaviour is:
1305 * - drop to empty path edit box (check left first)
1306 * - if both boxes have a path, drop to left path
1308 * - overwrite both paths, empty or not
1309 * @param [in] dropInfo Dropped data, including paths.
1311 void COpenView::OnDropFiles(const std::vector<String>& files)
1313 const size_t fileCount = files.size();
1315 // Add dropped paths to the dialog
1319 m_strPath[0] = files[0];
1320 m_strPath[1] = files[1];
1321 m_strPath[2] = files[2];
1323 UpdateButtonStates();
1325 else if (fileCount == 2)
1327 m_strPath[0] = files[0];
1328 m_strPath[1] = files[1];
1330 UpdateButtonStates();
1332 else if (fileCount == 1)
1335 GetCursorPos(&point);
1336 ScreenToClient(&point);
1337 if (CWnd *const pwndHit = ChildWindowFromPoint(point,
1338 CWP_SKIPINVISIBLE | CWP_SKIPDISABLED | CWP_SKIPTRANSPARENT))
1340 switch (int const id = pwndHit->GetDlgCtrlID())
1342 case IDC_PATH0_COMBO:
1343 case IDC_PATH1_COMBO:
1344 case IDC_PATH2_COMBO:
1345 m_strPath[id - IDC_PATH0_COMBO] = files[0];
1348 if (m_strPath[0].empty())
1349 m_strPath[0] = files[0];
1350 else if (m_strPath[1].empty())
1351 m_strPath[1] = files[0];
1352 else if (m_strPath[2].empty())
1353 m_strPath[2] = files[0];
1355 m_strPath[0] = files[0];
1360 UpdateButtonStates();