1 // SPDX-License-Identifier: GPL-2.0-or-later
3 * @file FileFiltersDlg.cpp
5 * @brief Implementation of FileFilters -dialog
9 #include "FileFiltersDlg.h"
11 #include "UnicodeString.h"
13 #include "OptionsMgr.h"
14 #include "OptionsDef.h"
15 #include "FileFilterMgr.h"
16 #include "FileFilterHelper.h"
18 #include "SharedFilterDlg.h"
19 #include "TestFilterDlg.h"
20 #include "FileOrFolderSelect.h"
28 /** @brief Template file used when creating new filefilter. */
29 static const TCHAR FILE_FILTER_TEMPLATE[] = _T("FileFilter.tmpl");
31 /** @brief Location for filters specific help to open. */
32 static const TCHAR FilterHelpLocation[] = _T("::/htmlhelp/Filters.html");
34 /////////////////////////////////////////////////////////////////////////////
36 IMPLEMENT_DYNCREATE(FileFiltersDlg, CTrPropertyPage)
41 FileFiltersDlg::FileFiltersDlg() : CTrPropertyPage(FileFiltersDlg::IDD)
43 m_strCaption = theApp.LoadDialogCaption(m_lpszTemplateName).c_str();
44 m_psp.pszTitle = m_strCaption;
45 m_psp.dwFlags |= PSP_USETITLE;
46 m_psp.hIcon = AfxGetApp()->LoadIcon(IDI_FILEFILTER);
47 m_psp.dwFlags |= PSP_USEHICON;
50 void FileFiltersDlg::DoDataExchange(CDataExchange* pDX)
52 CDialog::DoDataExchange(pDX);
53 //{{AFX_DATA_MAP(FileFiltersDlg)
54 DDX_Control(pDX, IDC_FILTERFILE_LIST, m_listFilters);
59 BEGIN_MESSAGE_MAP(FileFiltersDlg, CTrPropertyPage)
60 //{{AFX_MSG_MAP(FileFiltersDlg)
61 ON_BN_CLICKED(IDC_FILTERFILE_EDITBTN, OnFiltersEditbtn)
62 ON_NOTIFY(NM_DBLCLK, IDC_FILTERFILE_LIST, OnDblclkFiltersList)
64 ON_BN_CLICKED(IDC_FILTERFILE_TEST_BTN, OnBnClickedFilterfileTestButton)
65 ON_BN_CLICKED(IDC_FILTERFILE_NEWBTN, OnBnClickedFilterfileNewbutton)
66 ON_BN_CLICKED(IDC_FILTERFILE_DELETEBTN, OnBnClickedFilterfileDelete)
67 ON_COMMAND(ID_HELP, OnHelp)
69 ON_NOTIFY(LVN_ITEMCHANGED, IDC_FILTERFILE_LIST, OnLvnItemchangedFilterfileList)
70 ON_NOTIFY(LVN_GETINFOTIP, IDC_FILTERFILE_LIST, OnInfoTip)
71 ON_BN_CLICKED(IDC_FILTERFILE_INSTALL, OnBnClickedFilterfileInstall)
74 /////////////////////////////////////////////////////////////////////////////
75 // CFiltersDlg message handlers
78 * @brief Set array of filters.
79 * @param [in] fileFilters Array of filters to show in the dialog.
80 * @note Call this before actually showing the dialog.
82 void FileFiltersDlg::SetFilterArray(const vector<FileFilterInfo>& fileFilters)
84 m_Filters = fileFilters;
88 * @brief Returns path (cont. filename) of selected filter
89 * @return Full path to selected filter file.
91 String FileFiltersDlg::GetSelected()
93 return m_sFileFilterPath;
97 * @brief Set path of selected filter.
98 * @param [in] Path for selected filter.
99 * @note Call this before actually showing the dialog.
101 void FileFiltersDlg::SetSelected(const String & selected)
103 m_sFileFilterPath = selected;
107 * @brief Initialise listcontrol containing filters.
109 void FileFiltersDlg::InitList()
111 // Show selection across entire row.
112 // Also enable infotips.
113 m_listFilters.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_INFOTIP);
115 const int lpx = CClientDC(this).GetDeviceCaps(LOGPIXELSX);
116 auto pointToPixel = [lpx](int point) { return MulDiv(point, lpx, 72); };
118 String title = _("Name");
119 m_listFilters.InsertColumn(0, title.c_str(), LVCFMT_LEFT, pointToPixel(112));
120 title = _("Description");
121 m_listFilters.InsertColumn(1, title.c_str(), LVCFMT_LEFT, pointToPixel(262));
122 title = _("Location");
123 m_listFilters.InsertColumn(2, title.c_str(), LVCFMT_LEFT, pointToPixel(262));
126 m_listFilters.InsertItem(1, title.c_str());
127 m_listFilters.SetItemText(0, 1, title.c_str());
128 m_listFilters.SetItemText(0, 2, title.c_str());
130 const int count = (int) m_Filters.size();
132 for (int i = 0; i < count; i++)
139 * @brief Select filter by index in the listview.
140 * @param [in] index Index of filter to select.
142 void FileFiltersDlg::SelectFilterByIndex(int index)
144 m_listFilters.SetItemState(index, LVIS_SELECTED, LVIS_SELECTED);
145 bool bPartialOk = false;
146 m_listFilters.EnsureVisible(index, bPartialOk);
150 * @brief Called before dialog is shown.
151 * @return Always TRUE.
153 BOOL FileFiltersDlg::OnInitDialog()
155 CTrPropertyPage::OnInitDialog();
159 if (m_sFileFilterPath.empty())
161 SelectFilterByIndex(0);
165 int count = m_listFilters.GetItemCount();
166 for (int i = 0; i < count; i++)
168 String desc = m_listFilters.GetItemText(i, 2);
169 if (strutils::compare_nocase(desc, m_sFileFilterPath) == 0)
171 SelectFilterByIndex(i);
175 return TRUE; // return TRUE unless you set the focus to a control
176 // EXCEPTION: OCX Property Pages should return FALSE
180 * @brief Add filter from filter-list index to dialog.
181 * @param [in] filterIndex Index of filter to add.
183 void FileFiltersDlg::AddToGrid(int filterIndex)
185 const FileFilterInfo & filterinfo = m_Filters.at(filterIndex);
186 const int item = filterIndex + 1;
188 m_listFilters.InsertItem(item, filterinfo.name.c_str());
189 m_listFilters.SetItemText(item, 1, filterinfo.description.c_str());
190 m_listFilters.SetItemText(item, 2, filterinfo.fullpath.c_str());
194 * @brief Called when dialog is closed with "OK" button.
196 void FileFiltersDlg::OnOK()
198 int sel = m_listFilters.GetNextItem(-1, LVNI_SELECTED);
199 m_sFileFilterPath = m_listFilters.GetItemText(sel, 2);
201 AfxGetApp()->WriteProfileInt(_T("Settings"), _T("FilterStartPage"), GetParentSheet()->GetActiveIndex());
207 * @brief Open selected filter for editing.
209 * This opens selected file filter file for user to edit. Other WinMerge UI is
210 * not (anymore) blocked during editing. We let user continue working with
211 * WinMerge while editing filter(s). Before opening this dialog and before
212 * doing directory compare we re-load changed filter files from disk. So we
213 * always compare with latest saved filters.
214 * @sa CMainFrame::OnToolsFilters()
215 * @sa CDirDoc::Rescan()
216 * @sa FileFilterHelper::ReloadUpdatedFilters()
218 void FileFiltersDlg::OnFiltersEditbtn()
222 sel = m_listFilters.GetNextItem(sel, LVNI_SELECTED);
224 // Can't edit first "None"
227 String path = m_listFilters.GetItemText(sel, 2);
228 EditFileFilter(path);
233 * @brief Edit file filter in external editor.
234 * @param [in] path Full path to file filter to edit.
236 void FileFiltersDlg::EditFileFilter(const String& path)
238 CMergeApp::OpenFileToExternalEditor(path);
242 * @brief Edit selected filter when its double-clicked.
243 * @param [in] pNMHDR List control item data.
244 * @param [out] pResult Result of the action is returned in here.
246 void FileFiltersDlg::OnDblclkFiltersList(NMHDR* pNMHDR, LRESULT* pResult)
248 UNREFERENCED_PARAMETER(pNMHDR);
255 * @brief Is item in list the <None> item?
256 * @param [in] item Item to test.
257 * @return true if item is <None> item.
259 bool FileFiltersDlg::IsFilterItemNone(int item) const
261 String txtNone = _("<None>");
262 String txt = m_listFilters.GetItemText(item, 0);
264 return (strutils::compare_nocase(txt, txtNone) == 0);
268 * @brief Called when item state is changed.
270 * Disable Edit-button when "None" filter is selected.
271 * @param [in] pNMHDR Listview item data.
272 * @param [out] pResult Result of the action is returned in here.
274 void FileFiltersDlg::OnLvnItemchangedFilterfileList(NMHDR *pNMHDR, LRESULT *pResult)
276 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
278 // If item got selected
279 if (pNMLV->uNewState & LVIS_SELECTED)
281 String txtNone = _("<None>");
282 String txt = m_listFilters.GetItemText(pNMLV->iItem, 0);
284 bool isNone = strutils::compare_nocase(txt, txtNone) == 0;
286 EnableDlgItem(IDC_FILTERFILE_TEST_BTN, !isNone);
287 EnableDlgItem(IDC_FILTERFILE_EDITBTN, !isNone);
288 EnableDlgItem(IDC_FILTERFILE_DELETEBTN, !isNone);
294 * @brief Called before infotip is shown to get infotip text.
295 * @param [in] pNMHDR Listview item data.
296 * @param [out] pResult Result of the action is returned in here.
298 void FileFiltersDlg::OnInfoTip(NMHDR * pNMHDR, LRESULT * pResult)
300 LVHITTESTINFO lvhti = {0};
301 NMLVGETINFOTIP * pInfoTip = reinterpret_cast<NMLVGETINFOTIP*>(pNMHDR);
302 ASSERT(pInfoTip != nullptr);
304 // Get subitem under mouse cursor
305 lvhti.pt = m_ptLastMousePos;
306 m_listFilters.SubItemHitTest(&lvhti);
308 if (lvhti.iSubItem > 1)
310 // Check that we are over icon or label
311 if ((lvhti.flags & LVHT_ONITEMICON) || (lvhti.flags & LVHT_ONITEMLABEL))
313 // Set item text to tooltip
314 String strText = m_listFilters.GetItemText(lvhti.iItem, lvhti.iSubItem);
315 _tcscpy_s(pInfoTip->pszText, pInfoTip->cchTextMax, strText.c_str());
321 * @brief Track mouse position for showing tooltips.
322 * @param [in] nFlags Mouse movement flags.
323 * @param [in] point Current mouse position.
325 void FileFiltersDlg::OnMouseMove(UINT nFlags, CPoint point)
327 m_ptLastMousePos = point;
328 CDialog::OnMouseMove(nFlags, point);
332 * @brief Called when user presses "Test" button.
334 * Asks filename for new filter from user (using standard
335 * file picker dialog) and copies template file to that
336 * name. Opens new filterfile for editing.
337 * @todo (At least) Warn if user puts filter to outside
338 * filter directories?
340 void FileFiltersDlg::OnBnClickedFilterfileTestButton()
344 int sel = m_listFilters.GetNextItem(-1, LVNI_SELECTED);
347 if (IsFilterItemNone(sel))
350 m_sFileFilterPath = m_listFilters.GetItemText(sel, 2);
352 // Ensure filter is up-to-date (user probably just edited it)
353 theApp.m_pGlobalFileFilter->ReloadUpdatedFilters();
355 FileFilterMgr *pMgr = theApp.m_pGlobalFileFilter->GetManager();
356 FileFilter * pFileFilter = pMgr->GetFilterByPath(m_sFileFilterPath);
357 if (pFileFilter == nullptr)
360 CTestFilterDlg dlg(this, pFileFilter, pMgr);
365 * @brief Called when user presses "New..." button.
367 * Asks filename for new filter from user (using standard
368 * file picker dialog) and copies template file to that
369 * name. Opens new filterfile for editing.
370 * @todo (At least) Warn if user puts filter to outside
371 * filter directories?
372 * @todo Can global filter path be empty (I think not - Kimmo).
374 void FileFiltersDlg::OnBnClickedFilterfileNewbutton()
376 String globalPath = theApp.m_pGlobalFileFilter->GetGlobalFilterPathWithCreate();
377 String userPath = theApp.m_pGlobalFileFilter->GetUserFilterPathWithCreate();
379 if (globalPath.empty() && userPath.empty())
382 _("User's filter file folder is not defined!\n\nPlease select filter folder in Options/System.").c_str(), MB_ICONSTOP);
386 // Format path to template file
387 String templatePath = paths::ConcatPath(globalPath, FILE_FILTER_TEMPLATE);
389 if (paths::DoesPathExist(templatePath) != paths::IS_EXISTING_FILE)
391 String msg = strutils::format_string2(
392 _("Cannot find file filter template file!\n\nPlease copy file %1 to WinMerge/Filters -folder:\n%2."),
393 FILE_FILTER_TEMPLATE, templatePath);
394 AfxMessageBox(msg.c_str(), MB_ICONERROR);
398 String path = globalPath.empty() ? userPath : globalPath;
400 if (!globalPath.empty() && !userPath.empty())
402 CSharedFilterDlg dlg(
403 GetOptionsMgr()->GetBool(OPT_FILEFILTER_SHARED) ?
404 CSharedFilterDlg::SHARED : CSharedFilterDlg::PRIVATE);
405 if (dlg.DoModal() != IDOK)
407 GetOptionsMgr()->SaveOption(OPT_FILEFILTER_SHARED, (dlg.GetSelectedFilterType() == CSharedFilterDlg::SHARED));
408 path = dlg.GetSelectedFilterType() == CSharedFilterDlg::SHARED ? globalPath : userPath;
412 path = paths::AddTrailingSlash(path);
415 if (SelectFile(GetSafeHwnd(), s, false, path.c_str(), _("Select filename for new filter"),
416 _("File Filters (*.flt)|*.flt|All Files (*.*)|*.*||")))
418 // Fix file extension
419 TCHAR file[_MAX_FNAME] = {0};
420 TCHAR ext[_MAX_EXT] = {0};
421 TCHAR dir[_MAX_DIR] = {0};
422 TCHAR drive[_MAX_DRIVE] = {0};
423 _tsplitpath_s(s.c_str(), drive, _MAX_DRIVE, dir, _MAX_DIR, file, _MAX_FNAME, ext, _MAX_EXT);
424 if (_tcslen(ext) == 0)
428 else if (_tcsicmp(ext, FileFilterExt) != 0)
436 // Open-dialog asks about overwriting, so we can overwrite filter file
437 // user has already allowed it.
438 if (!CopyFile(templatePath.c_str(), s.c_str(), FALSE))
440 String msg = strutils::format_string1(
441 _( "Cannot copy filter template file to filter folder:\n%1\n\nPlease make sure the folder exists and is writable."),
443 AfxMessageBox(msg.c_str(), MB_ICONERROR);
447 FileFilterMgr *pMgr = theApp.m_pGlobalFileFilter->GetManager();
448 int retval = pMgr->AddFilter(s);
449 if (retval == FILTER_OK)
451 // Remove all from filterslist and re-add so we can update UI
453 theApp.m_pGlobalFileFilter->LoadAllFileFilters();
454 m_Filters = theApp.m_pGlobalFileFilter->GetFileFilters(selected);
462 * @brief Delete selected filter.
464 void FileFiltersDlg::OnBnClickedFilterfileDelete()
469 sel = m_listFilters.GetNextItem(sel, LVNI_SELECTED);
471 // Can't delete first "None"
474 path = m_listFilters.GetItemText(sel, 2);
476 String sConfirm = strutils::format_string1(_("Are you sure you want to delete\n\n%1 ?"), path);
477 int res = AfxMessageBox(sConfirm.c_str(), MB_ICONWARNING | MB_YESNO);
480 if (DeleteFile(path.c_str()))
482 FileFilterMgr *pMgr = theApp.m_pGlobalFileFilter->GetManager();
483 pMgr->RemoveFilter(path);
485 // Remove all from filterslist and re-add so we can update UI
487 m_Filters = theApp.m_pGlobalFileFilter->GetFileFilters(selected);
493 String msg = strutils::format_string1(
494 _("Failed to delete the filter file:\n%1\n\nMaybe the file is read-only?"),
496 AfxMessageBox(msg.c_str(), MB_ICONSTOP);
503 * @brief Update filters to list.
505 void FileFiltersDlg::UpdateFiltersList()
507 int count = (int) m_Filters.size();
509 m_listFilters.DeleteAllItems();
511 String title = _("<None>");
512 m_listFilters.InsertItem(1, title.c_str());
513 m_listFilters.SetItemText(0, 1, title.c_str());
514 m_listFilters.SetItemText(0, 2, title.c_str());
516 for (int i = 0; i < count; i++)
523 * @brief Open help from mainframe when user presses F1
525 void FileFiltersDlg::OnHelp()
527 theApp.ShowHelp(FilterHelpLocation);
531 * @brief Install new filter.
532 * This function is called when user selects "Install" button from GUI.
533 * Function allows easy installation of new filters for user. For example
534 * when user has downloaded filter file from net. First we ask user to
535 * select filter to install. Then we copy selected filter to private
538 void FileFiltersDlg::OnBnClickedFilterfileInstall()
542 String userPath = theApp.m_pGlobalFileFilter->GetUserFilterPathWithCreate();
544 if (SelectFile(GetSafeHwnd(), s, true, path.c_str(),_("Locate filter file to install"),
545 _("File Filters (*.flt)|*.flt|All Files (*.*)|*.*||")))
547 userPath = paths::ConcatPath(userPath, paths::FindFileName(s));
548 if (!CopyFile(s.c_str(), userPath.c_str(), TRUE))
550 // If file already exists, ask from user
551 // If user wants to, overwrite existing filter
552 if (paths::DoesPathExist(userPath) == paths::IS_EXISTING_FILE)
554 int res = LangMessageBox(IDS_FILEFILTER_OVERWRITE, MB_YESNO |
558 if (!CopyFile(s.c_str(), userPath.c_str(), FALSE))
560 LangMessageBox(IDS_FILEFILTER_INSTALLFAIL, MB_ICONSTOP);
566 LangMessageBox(IDS_FILEFILTER_INSTALLFAIL, MB_ICONSTOP);
571 FileFilterMgr *pMgr = theApp.m_pGlobalFileFilter->GetManager();
572 pMgr->AddFilter(userPath);
574 // Remove all from filterslist and re-add so we can update UI
576 m_Filters = theApp.m_pGlobalFileFilter->GetFileFilters(selected);