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"
22 #include "Constants.h"
30 /** @brief Template file used when creating new filefilter. */
31 static const TCHAR FILE_FILTER_TEMPLATE[] = _T("FileFilter.tmpl");
33 /////////////////////////////////////////////////////////////////////////////
35 IMPLEMENT_DYNCREATE(FileFiltersDlg, CTrPropertyPage)
40 FileFiltersDlg::FileFiltersDlg() : CTrPropertyPage(FileFiltersDlg::IDD)
42 m_strCaption = theApp.LoadDialogCaption(m_lpszTemplateName).c_str();
43 m_psp.pszTitle = m_strCaption;
44 m_psp.dwFlags |= PSP_USETITLE;
45 m_psp.hIcon = AfxGetApp()->LoadIcon(IDI_FILEFILTER);
46 m_psp.dwFlags |= PSP_USEHICON;
49 void FileFiltersDlg::DoDataExchange(CDataExchange* pDX)
51 CDialog::DoDataExchange(pDX);
52 //{{AFX_DATA_MAP(FileFiltersDlg)
53 DDX_Control(pDX, IDC_FILTERFILE_LIST, m_listFilters);
58 BEGIN_MESSAGE_MAP(FileFiltersDlg, CTrPropertyPage)
59 //{{AFX_MSG_MAP(FileFiltersDlg)
60 ON_BN_CLICKED(IDC_FILTERFILE_EDITBTN, OnFiltersEditbtn)
61 ON_NOTIFY(NM_DBLCLK, IDC_FILTERFILE_LIST, OnDblclkFiltersList)
63 ON_BN_CLICKED(IDC_FILTERFILE_TEST_BTN, OnBnClickedFilterfileTestButton)
64 ON_BN_CLICKED(IDC_FILTERFILE_NEWBTN, OnBnClickedFilterfileNewbutton)
65 ON_BN_CLICKED(IDC_FILTERFILE_DELETEBTN, OnBnClickedFilterfileDelete)
66 ON_COMMAND(ID_HELP, OnHelp)
68 ON_NOTIFY(LVN_ITEMCHANGED, IDC_FILTERFILE_LIST, OnLvnItemchangedFilterfileList)
69 ON_NOTIFY(LVN_GETINFOTIP, IDC_FILTERFILE_LIST, OnInfoTip)
70 ON_BN_CLICKED(IDC_FILTERFILE_INSTALL, OnBnClickedFilterfileInstall)
73 /////////////////////////////////////////////////////////////////////////////
74 // CFiltersDlg message handlers
77 * @brief Set array of filters.
78 * @param [in] fileFilters Array of filters to show in the dialog.
79 * @note Call this before actually showing the dialog.
81 void FileFiltersDlg::SetFilterArray(const vector<FileFilterInfo>& fileFilters)
83 m_Filters = fileFilters;
87 * @brief Returns path (cont. filename) of selected filter
88 * @return Full path to selected filter file.
90 String FileFiltersDlg::GetSelected()
92 return m_sFileFilterPath;
96 * @brief Set path of selected filter.
97 * @param [in] Path for selected filter.
98 * @note Call this before actually showing the dialog.
100 void FileFiltersDlg::SetSelected(const String & selected)
102 m_sFileFilterPath = selected;
106 * @brief Initialise listcontrol containing filters.
108 void FileFiltersDlg::InitList()
110 // Show selection across entire row.
111 // Also enable infotips.
112 m_listFilters.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_INFOTIP);
114 const int lpx = CClientDC(this).GetDeviceCaps(LOGPIXELSX);
115 auto pointToPixel = [lpx](int point) { return MulDiv(point, lpx, 72); };
117 String title = _("Name");
118 m_listFilters.InsertColumn(0, title.c_str(), LVCFMT_LEFT, pointToPixel(112));
119 title = _("Description");
120 m_listFilters.InsertColumn(1, title.c_str(), LVCFMT_LEFT, pointToPixel(262));
121 title = _("Location");
122 m_listFilters.InsertColumn(2, title.c_str(), LVCFMT_LEFT, pointToPixel(262));
125 m_listFilters.InsertItem(1, title.c_str());
126 m_listFilters.SetItemText(0, 1, title.c_str());
127 m_listFilters.SetItemText(0, 2, title.c_str());
129 const int count = (int) m_Filters.size();
131 for (int i = 0; i < count; i++)
138 * @brief Select filter by index in the listview.
139 * @param [in] index Index of filter to select.
141 void FileFiltersDlg::SelectFilterByIndex(int index)
143 m_listFilters.SetItemState(index, LVIS_SELECTED, LVIS_SELECTED);
144 bool bPartialOk = false;
145 m_listFilters.EnsureVisible(index, bPartialOk);
149 * @brief Select filter by file path in the listview.
150 * @param [in] path file path
152 void FileFiltersDlg::SelectFilterByFilePath(const String& path)
154 for (size_t i = 0; i < m_Filters.size(); ++i)
156 if (m_Filters[i].fullpath == path)
158 SelectFilterByIndex(static_cast<int>(i + 1));
165 * @brief Called before dialog is shown.
166 * @return Always TRUE.
168 BOOL FileFiltersDlg::OnInitDialog()
170 CTrPropertyPage::OnInitDialog();
174 if (m_sFileFilterPath.empty())
176 SelectFilterByIndex(0);
180 int count = m_listFilters.GetItemCount();
181 for (int i = 0; i < count; i++)
183 String desc = m_listFilters.GetItemText(i, 2);
184 if (strutils::compare_nocase(desc, m_sFileFilterPath) == 0)
186 SelectFilterByIndex(i);
190 return TRUE; // return TRUE unless you set the focus to a control
191 // EXCEPTION: OCX Property Pages should return FALSE
195 * @brief Add filter from filter-list index to dialog.
196 * @param [in] filterIndex Index of filter to add.
198 void FileFiltersDlg::AddToGrid(int filterIndex)
200 const FileFilterInfo & filterinfo = m_Filters.at(filterIndex);
201 const int item = filterIndex + 1;
203 m_listFilters.InsertItem(item, filterinfo.name.c_str());
204 m_listFilters.SetItemText(item, 1, filterinfo.description.c_str());
205 m_listFilters.SetItemText(item, 2, filterinfo.fullpath.c_str());
209 * @brief Called when dialog is closed with "OK" button.
211 void FileFiltersDlg::OnOK()
213 int sel = m_listFilters.GetNextItem(-1, LVNI_SELECTED);
214 m_sFileFilterPath = m_listFilters.GetItemText(sel, 2);
216 AfxGetApp()->WriteProfileInt(_T("Settings"), _T("FilterStartPage"), GetParentSheet()->GetActiveIndex());
222 * @brief Open selected filter for editing.
224 * This opens selected file filter file for user to edit. Other WinMerge UI is
225 * not (anymore) blocked during editing. We let user continue working with
226 * WinMerge while editing filter(s). Before opening this dialog and before
227 * doing directory compare we re-load changed filter files from disk. So we
228 * always compare with latest saved filters.
229 * @sa CMainFrame::OnToolsFilters()
230 * @sa CDirDoc::Rescan()
231 * @sa FileFilterHelper::ReloadUpdatedFilters()
233 void FileFiltersDlg::OnFiltersEditbtn()
237 sel = m_listFilters.GetNextItem(sel, LVNI_SELECTED);
239 // Can't edit first "None"
242 String path = m_listFilters.GetItemText(sel, 2);
243 EditFileFilter(path);
248 * @brief Edit file filter in external editor.
249 * @param [in] path Full path to file filter to edit.
251 void FileFiltersDlg::EditFileFilter(const String& path)
253 CMergeApp::OpenFileToExternalEditor(path);
257 * @brief Edit selected filter when its double-clicked.
258 * @param [in] pNMHDR List control item data.
259 * @param [out] pResult Result of the action is returned in here.
261 void FileFiltersDlg::OnDblclkFiltersList(NMHDR* pNMHDR, LRESULT* pResult)
263 UNREFERENCED_PARAMETER(pNMHDR);
270 * @brief Is item in list the <None> item?
271 * @param [in] item Item to test.
272 * @return true if item is <None> item.
274 bool FileFiltersDlg::IsFilterItemNone(int item) const
276 String txtNone = _("<None>");
277 String txt = m_listFilters.GetItemText(item, 0);
279 return (strutils::compare_nocase(txt, txtNone) == 0);
283 * @brief Called when item state is changed.
285 * Disable Edit-button when "None" filter is selected.
286 * @param [in] pNMHDR Listview item data.
287 * @param [out] pResult Result of the action is returned in here.
289 void FileFiltersDlg::OnLvnItemchangedFilterfileList(NMHDR *pNMHDR, LRESULT *pResult)
291 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
293 // If item got selected
294 if (pNMLV->uNewState & LVIS_SELECTED)
296 String txtNone = _("<None>");
297 String txt = m_listFilters.GetItemText(pNMLV->iItem, 0);
299 bool isNone = strutils::compare_nocase(txt, txtNone) == 0;
301 EnableDlgItem(IDC_FILTERFILE_TEST_BTN, !isNone);
302 EnableDlgItem(IDC_FILTERFILE_EDITBTN, !isNone);
303 EnableDlgItem(IDC_FILTERFILE_DELETEBTN, !isNone);
309 * @brief Called before infotip is shown to get infotip text.
310 * @param [in] pNMHDR Listview item data.
311 * @param [out] pResult Result of the action is returned in here.
313 void FileFiltersDlg::OnInfoTip(NMHDR * pNMHDR, LRESULT * pResult)
315 LVHITTESTINFO lvhti = {0};
316 NMLVGETINFOTIP * pInfoTip = reinterpret_cast<NMLVGETINFOTIP*>(pNMHDR);
317 ASSERT(pInfoTip != nullptr);
319 // Get subitem under mouse cursor
320 lvhti.pt = m_ptLastMousePos;
321 m_listFilters.SubItemHitTest(&lvhti);
323 if (lvhti.iSubItem > 1)
325 // Check that we are over icon or label
326 if ((lvhti.flags & LVHT_ONITEMICON) || (lvhti.flags & LVHT_ONITEMLABEL))
328 // Set item text to tooltip
329 String strText = m_listFilters.GetItemText(lvhti.iItem, lvhti.iSubItem);
330 _tcscpy_s(pInfoTip->pszText, pInfoTip->cchTextMax, strText.c_str());
336 * @brief Track mouse position for showing tooltips.
337 * @param [in] nFlags Mouse movement flags.
338 * @param [in] point Current mouse position.
340 void FileFiltersDlg::OnMouseMove(UINT nFlags, CPoint point)
342 m_ptLastMousePos = point;
343 CDialog::OnMouseMove(nFlags, point);
347 * @brief Called when user presses "Test" button.
349 * Asks filename for new filter from user (using standard
350 * file picker dialog) and copies template file to that
351 * name. Opens new filterfile for editing.
352 * @todo (At least) Warn if user puts filter to outside
353 * filter directories?
355 void FileFiltersDlg::OnBnClickedFilterfileTestButton()
359 int sel = m_listFilters.GetNextItem(-1, LVNI_SELECTED);
362 if (IsFilterItemNone(sel))
365 m_sFileFilterPath = m_listFilters.GetItemText(sel, 2);
367 // Ensure filter is up-to-date (user probably just edited it)
368 auto* pGlobalFileFilter = theApp.GetGlobalFileFilter();
369 pGlobalFileFilter->ReloadUpdatedFilters();
371 FileFilterMgr *pMgr = pGlobalFileFilter->GetManager();
372 FileFilter * pFileFilter = pMgr->GetFilterByPath(m_sFileFilterPath);
373 if (pFileFilter == nullptr)
376 CTestFilterDlg dlg(this, pFileFilter, pMgr);
381 * @brief Called when user presses "New..." button.
383 * Asks filename for new filter from user (using standard
384 * file picker dialog) and copies template file to that
385 * name. Opens new filterfile for editing.
386 * @todo (At least) Warn if user puts filter to outside
387 * filter directories?
388 * @todo Can global filter path be empty (I think not - Kimmo).
390 void FileFiltersDlg::OnBnClickedFilterfileNewbutton()
392 auto* pGlobalFileFilter = theApp.GetGlobalFileFilter();
393 String globalPath = pGlobalFileFilter->GetGlobalFilterPathWithCreate();
394 String userPath = pGlobalFileFilter->GetUserFilterPathWithCreate();
396 if (globalPath.empty() && userPath.empty())
399 _("User's filter file folder is not defined!\n\nPlease select filter folder in Options/System.").c_str(), MB_ICONSTOP);
403 // Format path to template file
404 String templatePath = paths::ConcatPath(globalPath, FILE_FILTER_TEMPLATE);
406 if (paths::DoesPathExist(templatePath) != paths::IS_EXISTING_FILE)
408 String msg = strutils::format_string2(
409 _("Cannot find file filter template file!\n\nPlease copy file %1 to WinMerge/Filters folder:\n%2."),
410 FILE_FILTER_TEMPLATE, templatePath);
411 AfxMessageBox(msg.c_str(), MB_ICONERROR);
415 String path = globalPath.empty() ? userPath : globalPath;
417 if (!globalPath.empty() && !userPath.empty())
419 CSharedFilterDlg dlg(
420 GetOptionsMgr()->GetBool(OPT_FILEFILTER_SHARED) ?
421 CSharedFilterDlg::SHARED : CSharedFilterDlg::PRIVATE);
422 if (dlg.DoModal() != IDOK)
424 GetOptionsMgr()->SaveOption(OPT_FILEFILTER_SHARED, (dlg.GetSelectedFilterType() == CSharedFilterDlg::SHARED));
425 path = dlg.GetSelectedFilterType() == CSharedFilterDlg::SHARED ? globalPath : userPath;
429 path = paths::AddTrailingSlash(path);
432 if (SelectFile(GetSafeHwnd(), s, false, path.c_str(), _("Select filename for new filter"),
433 _("File Filters (*.flt)|*.flt|All Files (*.*)|*.*||")))
435 // Fix file extension
436 TCHAR file[_MAX_FNAME] = {0};
437 TCHAR ext[_MAX_EXT] = {0};
438 TCHAR dir[_MAX_DIR] = {0};
439 TCHAR drive[_MAX_DRIVE] = {0};
440 _tsplitpath_s(s.c_str(), drive, _MAX_DRIVE, dir, _MAX_DIR, file, _MAX_FNAME, ext, _MAX_EXT);
445 else if (_tcsicmp(ext, FileFilterExt) != 0)
453 // Open-dialog asks about overwriting, so we can overwrite filter file
454 // user has already allowed it.
456 UniStdioFile fileOut;
457 if (!fileIn.OpenReadOnly(templatePath) || !fileOut.OpenCreate(s))
459 String msg = strutils::format_string1(
460 _( "Cannot copy filter template file to filter folder:\n%1\n\nPlease make sure the folder exists and is writable."),
462 AfxMessageBox(msg.c_str(), MB_ICONERROR);
466 fileIn.ReadStringAll(lines);
467 strutils::replace(lines, _T("${name}"), file);
468 fileOut.WriteString(lines);
473 FileFilterMgr *pMgr = pGlobalFileFilter->GetManager();
474 int retval = pMgr->AddFilter(s);
475 if (retval == FILTER_OK)
477 // Remove all from filterslist and re-add so we can update UI
479 pGlobalFileFilter->LoadAllFileFilters();
480 m_Filters = pGlobalFileFilter->GetFileFilters(selected);
483 SelectFilterByFilePath(s);
489 * @brief Delete selected filter.
491 void FileFiltersDlg::OnBnClickedFilterfileDelete()
496 sel = m_listFilters.GetNextItem(sel, LVNI_SELECTED);
498 // Can't delete first "None"
501 path = m_listFilters.GetItemText(sel, 2);
503 String sConfirm = strutils::format_string1(_("Are you sure you want to delete\n\n%1 ?"), path);
504 int res = AfxMessageBox(sConfirm.c_str(), MB_ICONWARNING | MB_YESNO);
507 if (DeleteFile(path.c_str()))
509 auto* pGlobalFileFilter = theApp.GetGlobalFileFilter();
510 FileFilterMgr *pMgr = pGlobalFileFilter->GetManager();
511 pMgr->RemoveFilter(path);
513 // Remove all from filterslist and re-add so we can update UI
515 m_Filters = pGlobalFileFilter->GetFileFilters(selected);
521 String msg = strutils::format_string1(
522 _("Failed to delete the filter file:\n%1\n\nMaybe the file is read-only?"),
524 AfxMessageBox(msg.c_str(), MB_ICONSTOP);
531 * @brief Update filters to list.
533 void FileFiltersDlg::UpdateFiltersList()
535 int count = (int) m_Filters.size();
537 m_listFilters.DeleteAllItems();
539 String title = _("<None>");
540 m_listFilters.InsertItem(1, title.c_str());
541 m_listFilters.SetItemText(0, 1, title.c_str());
542 m_listFilters.SetItemText(0, 2, title.c_str());
544 for (int i = 0; i < count; i++)
551 * @brief Open help from mainframe when user presses F1
553 void FileFiltersDlg::OnHelp()
555 theApp.ShowHelp(FilterHelpLocation);
559 * @brief Install new filter.
560 * This function is called when user selects "Install" button from GUI.
561 * Function allows easy installation of new filters for user. For example
562 * when user has downloaded filter file from net. First we ask user to
563 * select filter to install. Then we copy selected filter to private
566 void FileFiltersDlg::OnBnClickedFilterfileInstall()
568 auto* pGlobalFileFilter = theApp.GetGlobalFileFilter();
572 if (SelectFile(GetSafeHwnd(), s, true, path.c_str(),_("Locate filter file to install"),
573 _("File Filters (*.flt)|*.flt|All Files (*.*)|*.*||")))
575 String userPath = pGlobalFileFilter->GetUserFilterPathWithCreate();
576 userPath = paths::ConcatPath(userPath, paths::FindFileName(s));
577 if (!CopyFile(s.c_str(), userPath.c_str(), TRUE))
579 // If file already exists, ask from user
580 // If user wants to, overwrite existing filter
581 if (paths::DoesPathExist(userPath) == paths::IS_EXISTING_FILE)
583 int res = LangMessageBox(IDS_FILEFILTER_OVERWRITE, MB_YESNO |
587 if (!CopyFile(s.c_str(), userPath.c_str(), FALSE))
589 LangMessageBox(IDS_FILEFILTER_INSTALLFAIL, MB_ICONSTOP);
595 LangMessageBox(IDS_FILEFILTER_INSTALLFAIL, MB_ICONSTOP);
600 FileFilterMgr *pMgr = pGlobalFileFilter->GetManager();
601 pMgr->AddFilter(userPath);
603 // Remove all from filterslist and re-add so we can update UI
605 m_Filters = pGlobalFileFilter->GetFileFilters(selected);
608 SelectFilterByFilePath(userPath);