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"
29 /** @brief Template file used when creating new filefilter. */
30 static const TCHAR FILE_FILTER_TEMPLATE[] = _T("FileFilter.tmpl");
32 /** @brief Location for filters specific help to open. */
33 static const TCHAR FilterHelpLocation[] = _T("::/htmlhelp/Filters.html");
35 /////////////////////////////////////////////////////////////////////////////
37 IMPLEMENT_DYNCREATE(FileFiltersDlg, CTrPropertyPage)
42 FileFiltersDlg::FileFiltersDlg() : CTrPropertyPage(FileFiltersDlg::IDD)
44 m_strCaption = theApp.LoadDialogCaption(m_lpszTemplateName).c_str();
45 m_psp.pszTitle = m_strCaption;
46 m_psp.dwFlags |= PSP_USETITLE;
47 m_psp.hIcon = AfxGetApp()->LoadIcon(IDI_FILEFILTER);
48 m_psp.dwFlags |= PSP_USEHICON;
51 void FileFiltersDlg::DoDataExchange(CDataExchange* pDX)
53 CDialog::DoDataExchange(pDX);
54 //{{AFX_DATA_MAP(FileFiltersDlg)
55 DDX_Control(pDX, IDC_FILTERFILE_LIST, m_listFilters);
60 BEGIN_MESSAGE_MAP(FileFiltersDlg, CTrPropertyPage)
61 //{{AFX_MSG_MAP(FileFiltersDlg)
62 ON_BN_CLICKED(IDC_FILTERFILE_EDITBTN, OnFiltersEditbtn)
63 ON_NOTIFY(NM_DBLCLK, IDC_FILTERFILE_LIST, OnDblclkFiltersList)
65 ON_BN_CLICKED(IDC_FILTERFILE_TEST_BTN, OnBnClickedFilterfileTestButton)
66 ON_BN_CLICKED(IDC_FILTERFILE_NEWBTN, OnBnClickedFilterfileNewbutton)
67 ON_BN_CLICKED(IDC_FILTERFILE_DELETEBTN, OnBnClickedFilterfileDelete)
68 ON_COMMAND(ID_HELP, OnHelp)
70 ON_NOTIFY(LVN_ITEMCHANGED, IDC_FILTERFILE_LIST, OnLvnItemchangedFilterfileList)
71 ON_NOTIFY(LVN_GETINFOTIP, IDC_FILTERFILE_LIST, OnInfoTip)
72 ON_BN_CLICKED(IDC_FILTERFILE_INSTALL, OnBnClickedFilterfileInstall)
75 /////////////////////////////////////////////////////////////////////////////
76 // CFiltersDlg message handlers
79 * @brief Set array of filters.
80 * @param [in] fileFilters Array of filters to show in the dialog.
81 * @note Call this before actually showing the dialog.
83 void FileFiltersDlg::SetFilterArray(const vector<FileFilterInfo>& fileFilters)
85 m_Filters = fileFilters;
89 * @brief Returns path (cont. filename) of selected filter
90 * @return Full path to selected filter file.
92 String FileFiltersDlg::GetSelected()
94 return m_sFileFilterPath;
98 * @brief Set path of selected filter.
99 * @param [in] Path for selected filter.
100 * @note Call this before actually showing the dialog.
102 void FileFiltersDlg::SetSelected(const String & selected)
104 m_sFileFilterPath = selected;
108 * @brief Initialise listcontrol containing filters.
110 void FileFiltersDlg::InitList()
112 // Show selection across entire row.
113 // Also enable infotips.
114 m_listFilters.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_INFOTIP);
116 const int lpx = CClientDC(this).GetDeviceCaps(LOGPIXELSX);
117 auto pointToPixel = [lpx](int point) { return MulDiv(point, lpx, 72); };
119 String title = _("Name");
120 m_listFilters.InsertColumn(0, title.c_str(), LVCFMT_LEFT, pointToPixel(112));
121 title = _("Description");
122 m_listFilters.InsertColumn(1, title.c_str(), LVCFMT_LEFT, pointToPixel(262));
123 title = _("Location");
124 m_listFilters.InsertColumn(2, title.c_str(), LVCFMT_LEFT, pointToPixel(262));
127 m_listFilters.InsertItem(1, title.c_str());
128 m_listFilters.SetItemText(0, 1, title.c_str());
129 m_listFilters.SetItemText(0, 2, title.c_str());
131 const int count = (int) m_Filters.size();
133 for (int i = 0; i < count; i++)
140 * @brief Select filter by index in the listview.
141 * @param [in] index Index of filter to select.
143 void FileFiltersDlg::SelectFilterByIndex(int index)
145 m_listFilters.SetItemState(index, LVIS_SELECTED, LVIS_SELECTED);
146 bool bPartialOk = false;
147 m_listFilters.EnsureVisible(index, bPartialOk);
151 * @brief Select filter by file path in the listview.
152 * @param [in] path file path
154 void FileFiltersDlg::SelectFilterByFilePath(const String& path)
156 for (size_t i = 0; i < m_Filters.size(); ++i)
158 if (m_Filters[i].fullpath == path)
160 SelectFilterByIndex(static_cast<int>(i + 1));
167 * @brief Called before dialog is shown.
168 * @return Always TRUE.
170 BOOL FileFiltersDlg::OnInitDialog()
172 CTrPropertyPage::OnInitDialog();
176 if (m_sFileFilterPath.empty())
178 SelectFilterByIndex(0);
182 int count = m_listFilters.GetItemCount();
183 for (int i = 0; i < count; i++)
185 String desc = m_listFilters.GetItemText(i, 2);
186 if (strutils::compare_nocase(desc, m_sFileFilterPath) == 0)
188 SelectFilterByIndex(i);
192 return TRUE; // return TRUE unless you set the focus to a control
193 // EXCEPTION: OCX Property Pages should return FALSE
197 * @brief Add filter from filter-list index to dialog.
198 * @param [in] filterIndex Index of filter to add.
200 void FileFiltersDlg::AddToGrid(int filterIndex)
202 const FileFilterInfo & filterinfo = m_Filters.at(filterIndex);
203 const int item = filterIndex + 1;
205 m_listFilters.InsertItem(item, filterinfo.name.c_str());
206 m_listFilters.SetItemText(item, 1, filterinfo.description.c_str());
207 m_listFilters.SetItemText(item, 2, filterinfo.fullpath.c_str());
211 * @brief Called when dialog is closed with "OK" button.
213 void FileFiltersDlg::OnOK()
215 int sel = m_listFilters.GetNextItem(-1, LVNI_SELECTED);
216 m_sFileFilterPath = m_listFilters.GetItemText(sel, 2);
218 AfxGetApp()->WriteProfileInt(_T("Settings"), _T("FilterStartPage"), GetParentSheet()->GetActiveIndex());
224 * @brief Open selected filter for editing.
226 * This opens selected file filter file for user to edit. Other WinMerge UI is
227 * not (anymore) blocked during editing. We let user continue working with
228 * WinMerge while editing filter(s). Before opening this dialog and before
229 * doing directory compare we re-load changed filter files from disk. So we
230 * always compare with latest saved filters.
231 * @sa CMainFrame::OnToolsFilters()
232 * @sa CDirDoc::Rescan()
233 * @sa FileFilterHelper::ReloadUpdatedFilters()
235 void FileFiltersDlg::OnFiltersEditbtn()
239 sel = m_listFilters.GetNextItem(sel, LVNI_SELECTED);
241 // Can't edit first "None"
244 String path = m_listFilters.GetItemText(sel, 2);
245 EditFileFilter(path);
250 * @brief Edit file filter in external editor.
251 * @param [in] path Full path to file filter to edit.
253 void FileFiltersDlg::EditFileFilter(const String& path)
255 CMergeApp::OpenFileToExternalEditor(path);
259 * @brief Edit selected filter when its double-clicked.
260 * @param [in] pNMHDR List control item data.
261 * @param [out] pResult Result of the action is returned in here.
263 void FileFiltersDlg::OnDblclkFiltersList(NMHDR* pNMHDR, LRESULT* pResult)
265 UNREFERENCED_PARAMETER(pNMHDR);
272 * @brief Is item in list the <None> item?
273 * @param [in] item Item to test.
274 * @return true if item is <None> item.
276 bool FileFiltersDlg::IsFilterItemNone(int item) const
278 String txtNone = _("<None>");
279 String txt = m_listFilters.GetItemText(item, 0);
281 return (strutils::compare_nocase(txt, txtNone) == 0);
285 * @brief Called when item state is changed.
287 * Disable Edit-button when "None" filter is selected.
288 * @param [in] pNMHDR Listview item data.
289 * @param [out] pResult Result of the action is returned in here.
291 void FileFiltersDlg::OnLvnItemchangedFilterfileList(NMHDR *pNMHDR, LRESULT *pResult)
293 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
295 // If item got selected
296 if (pNMLV->uNewState & LVIS_SELECTED)
298 String txtNone = _("<None>");
299 String txt = m_listFilters.GetItemText(pNMLV->iItem, 0);
301 bool isNone = strutils::compare_nocase(txt, txtNone) == 0;
303 EnableDlgItem(IDC_FILTERFILE_TEST_BTN, !isNone);
304 EnableDlgItem(IDC_FILTERFILE_EDITBTN, !isNone);
305 EnableDlgItem(IDC_FILTERFILE_DELETEBTN, !isNone);
311 * @brief Called before infotip is shown to get infotip text.
312 * @param [in] pNMHDR Listview item data.
313 * @param [out] pResult Result of the action is returned in here.
315 void FileFiltersDlg::OnInfoTip(NMHDR * pNMHDR, LRESULT * pResult)
317 LVHITTESTINFO lvhti = {0};
318 NMLVGETINFOTIP * pInfoTip = reinterpret_cast<NMLVGETINFOTIP*>(pNMHDR);
319 ASSERT(pInfoTip != nullptr);
321 // Get subitem under mouse cursor
322 lvhti.pt = m_ptLastMousePos;
323 m_listFilters.SubItemHitTest(&lvhti);
325 if (lvhti.iSubItem > 1)
327 // Check that we are over icon or label
328 if ((lvhti.flags & LVHT_ONITEMICON) || (lvhti.flags & LVHT_ONITEMLABEL))
330 // Set item text to tooltip
331 String strText = m_listFilters.GetItemText(lvhti.iItem, lvhti.iSubItem);
332 _tcscpy_s(pInfoTip->pszText, pInfoTip->cchTextMax, strText.c_str());
338 * @brief Track mouse position for showing tooltips.
339 * @param [in] nFlags Mouse movement flags.
340 * @param [in] point Current mouse position.
342 void FileFiltersDlg::OnMouseMove(UINT nFlags, CPoint point)
344 m_ptLastMousePos = point;
345 CDialog::OnMouseMove(nFlags, point);
349 * @brief Called when user presses "Test" button.
351 * Asks filename for new filter from user (using standard
352 * file picker dialog) and copies template file to that
353 * name. Opens new filterfile for editing.
354 * @todo (At least) Warn if user puts filter to outside
355 * filter directories?
357 void FileFiltersDlg::OnBnClickedFilterfileTestButton()
361 int sel = m_listFilters.GetNextItem(-1, LVNI_SELECTED);
364 if (IsFilterItemNone(sel))
367 m_sFileFilterPath = m_listFilters.GetItemText(sel, 2);
369 // Ensure filter is up-to-date (user probably just edited it)
370 auto* pGlobalFileFilter = theApp.GetGlobalFileFilter();
371 pGlobalFileFilter->ReloadUpdatedFilters();
373 FileFilterMgr *pMgr = pGlobalFileFilter->GetManager();
374 FileFilter * pFileFilter = pMgr->GetFilterByPath(m_sFileFilterPath);
375 if (pFileFilter == nullptr)
378 CTestFilterDlg dlg(this, pFileFilter, pMgr);
383 * @brief Called when user presses "New..." button.
385 * Asks filename for new filter from user (using standard
386 * file picker dialog) and copies template file to that
387 * name. Opens new filterfile for editing.
388 * @todo (At least) Warn if user puts filter to outside
389 * filter directories?
390 * @todo Can global filter path be empty (I think not - Kimmo).
392 void FileFiltersDlg::OnBnClickedFilterfileNewbutton()
394 auto* pGlobalFileFilter = theApp.GetGlobalFileFilter();
395 String globalPath = pGlobalFileFilter->GetGlobalFilterPathWithCreate();
396 String userPath = pGlobalFileFilter->GetUserFilterPathWithCreate();
398 if (globalPath.empty() && userPath.empty())
401 _("User's filter file folder is not defined!\n\nPlease select filter folder in Options/System.").c_str(), MB_ICONSTOP);
405 // Format path to template file
406 String templatePath = paths::ConcatPath(globalPath, FILE_FILTER_TEMPLATE);
408 if (paths::DoesPathExist(templatePath) != paths::IS_EXISTING_FILE)
410 String msg = strutils::format_string2(
411 _("Cannot find file filter template file!\n\nPlease copy file %1 to WinMerge/Filters -folder:\n%2."),
412 FILE_FILTER_TEMPLATE, templatePath);
413 AfxMessageBox(msg.c_str(), MB_ICONERROR);
417 String path = globalPath.empty() ? userPath : globalPath;
419 if (!globalPath.empty() && !userPath.empty())
421 CSharedFilterDlg dlg(
422 GetOptionsMgr()->GetBool(OPT_FILEFILTER_SHARED) ?
423 CSharedFilterDlg::SHARED : CSharedFilterDlg::PRIVATE);
424 if (dlg.DoModal() != IDOK)
426 GetOptionsMgr()->SaveOption(OPT_FILEFILTER_SHARED, (dlg.GetSelectedFilterType() == CSharedFilterDlg::SHARED));
427 path = dlg.GetSelectedFilterType() == CSharedFilterDlg::SHARED ? globalPath : userPath;
431 path = paths::AddTrailingSlash(path);
434 if (SelectFile(GetSafeHwnd(), s, false, path.c_str(), _("Select filename for new filter"),
435 _("File Filters (*.flt)|*.flt|All Files (*.*)|*.*||")))
437 // Fix file extension
438 TCHAR file[_MAX_FNAME] = {0};
439 TCHAR ext[_MAX_EXT] = {0};
440 TCHAR dir[_MAX_DIR] = {0};
441 TCHAR drive[_MAX_DRIVE] = {0};
442 _tsplitpath_s(s.c_str(), drive, _MAX_DRIVE, dir, _MAX_DIR, file, _MAX_FNAME, ext, _MAX_EXT);
447 else if (_tcsicmp(ext, FileFilterExt) != 0)
455 // Open-dialog asks about overwriting, so we can overwrite filter file
456 // user has already allowed it.
458 UniStdioFile fileOut;
459 if (!fileIn.OpenReadOnly(templatePath) || !fileOut.OpenCreate(s))
461 String msg = strutils::format_string1(
462 _( "Cannot copy filter template file to filter folder:\n%1\n\nPlease make sure the folder exists and is writable."),
464 AfxMessageBox(msg.c_str(), MB_ICONERROR);
468 fileIn.ReadStringAll(lines);
469 strutils::replace(lines, _T("${name}"), file);
470 fileOut.WriteString(lines);
475 FileFilterMgr *pMgr = pGlobalFileFilter->GetManager();
476 int retval = pMgr->AddFilter(s);
477 if (retval == FILTER_OK)
479 // Remove all from filterslist and re-add so we can update UI
481 pGlobalFileFilter->LoadAllFileFilters();
482 m_Filters = pGlobalFileFilter->GetFileFilters(selected);
485 SelectFilterByFilePath(s);
491 * @brief Delete selected filter.
493 void FileFiltersDlg::OnBnClickedFilterfileDelete()
498 sel = m_listFilters.GetNextItem(sel, LVNI_SELECTED);
500 // Can't delete first "None"
503 path = m_listFilters.GetItemText(sel, 2);
505 String sConfirm = strutils::format_string1(_("Are you sure you want to delete\n\n%1 ?"), path);
506 int res = AfxMessageBox(sConfirm.c_str(), MB_ICONWARNING | MB_YESNO);
509 if (DeleteFile(path.c_str()))
511 auto* pGlobalFileFilter = theApp.GetGlobalFileFilter();
512 FileFilterMgr *pMgr = pGlobalFileFilter->GetManager();
513 pMgr->RemoveFilter(path);
515 // Remove all from filterslist and re-add so we can update UI
517 m_Filters = pGlobalFileFilter->GetFileFilters(selected);
523 String msg = strutils::format_string1(
524 _("Failed to delete the filter file:\n%1\n\nMaybe the file is read-only?"),
526 AfxMessageBox(msg.c_str(), MB_ICONSTOP);
533 * @brief Update filters to list.
535 void FileFiltersDlg::UpdateFiltersList()
537 int count = (int) m_Filters.size();
539 m_listFilters.DeleteAllItems();
541 String title = _("<None>");
542 m_listFilters.InsertItem(1, title.c_str());
543 m_listFilters.SetItemText(0, 1, title.c_str());
544 m_listFilters.SetItemText(0, 2, title.c_str());
546 for (int i = 0; i < count; i++)
553 * @brief Open help from mainframe when user presses F1
555 void FileFiltersDlg::OnHelp()
557 theApp.ShowHelp(FilterHelpLocation);
561 * @brief Install new filter.
562 * This function is called when user selects "Install" button from GUI.
563 * Function allows easy installation of new filters for user. For example
564 * when user has downloaded filter file from net. First we ask user to
565 * select filter to install. Then we copy selected filter to private
568 void FileFiltersDlg::OnBnClickedFilterfileInstall()
570 auto* pGlobalFileFilter = theApp.GetGlobalFileFilter();
574 if (SelectFile(GetSafeHwnd(), s, true, path.c_str(),_("Locate filter file to install"),
575 _("File Filters (*.flt)|*.flt|All Files (*.*)|*.*||")))
577 String userPath = pGlobalFileFilter->GetUserFilterPathWithCreate();
578 userPath = paths::ConcatPath(userPath, paths::FindFileName(s));
579 if (!CopyFile(s.c_str(), userPath.c_str(), TRUE))
581 // If file already exists, ask from user
582 // If user wants to, overwrite existing filter
583 if (paths::DoesPathExist(userPath) == paths::IS_EXISTING_FILE)
585 int res = LangMessageBox(IDS_FILEFILTER_OVERWRITE, MB_YESNO |
589 if (!CopyFile(s.c_str(), userPath.c_str(), FALSE))
591 LangMessageBox(IDS_FILEFILTER_INSTALLFAIL, MB_ICONSTOP);
597 LangMessageBox(IDS_FILEFILTER_INSTALLFAIL, MB_ICONSTOP);
602 FileFilterMgr *pMgr = pGlobalFileFilter->GetManager();
603 pMgr->AddFilter(userPath);
605 // Remove all from filterslist and re-add so we can update UI
607 m_Filters = pGlobalFileFilter->GetFileFilters(selected);
610 SelectFilterByFilePath(userPath);