1 /////////////////////////////////////////////////////////////////////////////
3 // This program is free software; you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation; either version 2 of the License, or (at
6 // your option) any later version.
8 // This program is distributed in the hope that it will be useful, but
9 // WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 // GNU General Public License for more details.
13 // You should have received a copy of the GNU General Public License
14 // along with this program; if not, write to the Free Software
15 // Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
16 /////////////////////////////////////////////////////////////////////////////
18 * @file FileFiltersDlg.cpp
20 * @brief Implementation of FileFilters -dialog
24 #include "FileFiltersDlg.h"
26 #include "UnicodeString.h"
28 #include "OptionsMgr.h"
29 #include "OptionsDef.h"
30 #include "FileFilterMgr.h"
31 #include "FileFilterHelper.h"
33 #include "SharedFilterDlg.h"
34 #include "TestFilterDlg.h"
35 #include "FileOrFolderSelect.h"
43 /** @brief Template file used when creating new filefilter. */
44 static const TCHAR FILE_FILTER_TEMPLATE[] = _T("FileFilter.tmpl");
46 /** @brief Location for filters specific help to open. */
47 static const TCHAR FilterHelpLocation[] = _T("::/htmlhelp/Filters.html");
49 /////////////////////////////////////////////////////////////////////////////
51 IMPLEMENT_DYNCREATE(FileFiltersDlg, CPropertyPage)
56 FileFiltersDlg::FileFiltersDlg() : CTrPropertyPage(FileFiltersDlg::IDD)
59 m_strCaption = theApp.LoadDialogCaption(m_lpszTemplateName).c_str();
60 m_psp.pszTitle = m_strCaption;
61 m_psp.dwFlags |= PSP_USETITLE;
62 m_psp.hIcon = AfxGetApp()->LoadIcon(IDI_FILEFILTER);
63 m_psp.dwFlags |= PSP_USEHICON;
66 void FileFiltersDlg::DoDataExchange(CDataExchange* pDX)
68 CDialog::DoDataExchange(pDX);
69 //{{AFX_DATA_MAP(FileFiltersDlg)
70 DDX_Control(pDX, IDC_FILTERFILE_LIST, m_listFilters);
75 BEGIN_MESSAGE_MAP(FileFiltersDlg, CDialog)
76 //{{AFX_MSG_MAP(FileFiltersDlg)
77 ON_BN_CLICKED(IDC_FILTERFILE_EDITBTN, OnFiltersEditbtn)
78 ON_NOTIFY(NM_DBLCLK, IDC_FILTERFILE_LIST, OnDblclkFiltersList)
80 ON_BN_CLICKED(IDC_FILTERFILE_TEST_BTN, OnBnClickedFilterfileTestButton)
81 ON_BN_CLICKED(IDC_FILTERFILE_NEWBTN, OnBnClickedFilterfileNewbutton)
82 ON_BN_CLICKED(IDC_FILTERFILE_DELETEBTN, OnBnClickedFilterfileDelete)
83 ON_COMMAND(ID_HELP, OnHelp)
85 ON_NOTIFY(LVN_ITEMCHANGED, IDC_FILTERFILE_LIST, OnLvnItemchangedFilterfileList)
86 ON_NOTIFY(LVN_GETINFOTIP, IDC_FILTERFILE_LIST, OnInfoTip)
87 ON_BN_CLICKED(IDC_FILTERFILE_INSTALL, OnBnClickedFilterfileInstall)
90 /////////////////////////////////////////////////////////////////////////////
91 // CFiltersDlg message handlers
94 * @brief Set array of filters.
95 * @param [in] fileFilters Array of filters to show in the dialog.
96 * @note Call this before actually showing the dialog.
98 void FileFiltersDlg::SetFilterArray(vector<FileFilterInfo> * fileFilters)
100 m_Filters = fileFilters;
104 * @brief Returns path (cont. filename) of selected filter
105 * @return Full path to selected filter file.
107 String FileFiltersDlg::GetSelected()
109 return m_sFileFilterPath;
113 * @brief Set path of selected filter.
114 * @param [in] Path for selected filter.
115 * @note Call this before actually showing the dialog.
117 void FileFiltersDlg::SetSelected(const String & selected)
119 m_sFileFilterPath = selected;
123 * @brief Initialise listcontrol containing filters.
125 void FileFiltersDlg::InitList()
127 // Show selection across entire row.
128 // Also enable infotips.
129 m_listFilters.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_INFOTIP);
131 const int lpx = CClientDC(this).GetDeviceCaps(LOGPIXELSX);
132 auto pointToPixel = [lpx](int point) { return MulDiv(point, lpx, 72); };
134 String title = _("Name");
135 m_listFilters.InsertColumn(0, title.c_str(), LVCFMT_LEFT, pointToPixel(112));
136 title = _("Description");
137 m_listFilters.InsertColumn(1, title.c_str(), LVCFMT_LEFT, pointToPixel(262));
138 title = _("Location");
139 m_listFilters.InsertColumn(2, title.c_str(), LVCFMT_LEFT, pointToPixel(262));
142 m_listFilters.InsertItem(1, title.c_str());
143 m_listFilters.SetItemText(0, 1, title.c_str());
144 m_listFilters.SetItemText(0, 2, title.c_str());
146 const int count = (int) m_Filters->size();
148 for (int i = 0; i < count; i++)
155 * @brief Select filter by index in the listview.
156 * @param [in] index Index of filter to select.
158 void FileFiltersDlg::SelectFilterByIndex(int index)
160 m_listFilters.SetItemState(index, LVIS_SELECTED, LVIS_SELECTED);
161 bool bPartialOk = false;
162 m_listFilters.EnsureVisible(index, bPartialOk);
166 * @brief Called before dialog is shown.
167 * @return Always TRUE.
169 BOOL FileFiltersDlg::OnInitDialog()
171 CTrPropertyPage::OnInitDialog();
175 if (m_sFileFilterPath.empty())
177 SelectFilterByIndex(0);
181 int count = m_listFilters.GetItemCount();
182 for (int i = 0; i < count; i++)
184 String desc = m_listFilters.GetItemText(i, 2);
185 if (strutils::compare_nocase(desc, m_sFileFilterPath) == 0)
187 SelectFilterByIndex(i);
191 return TRUE; // return TRUE unless you set the focus to a control
192 // EXCEPTION: OCX Property Pages should return FALSE
196 * @brief Add filter from filter-list index to dialog.
197 * @param [in] filterIndex Index of filter to add.
199 void FileFiltersDlg::AddToGrid(int filterIndex)
201 const FileFilterInfo & filterinfo = m_Filters->at(filterIndex);
202 const int item = filterIndex + 1;
204 m_listFilters.InsertItem(item, filterinfo.name.c_str());
205 m_listFilters.SetItemText(item, 1, filterinfo.description.c_str());
206 m_listFilters.SetItemText(item, 2, filterinfo.fullpath.c_str());
210 * @brief Called when dialog is closed with "OK" button.
212 void FileFiltersDlg::OnOK()
214 int sel = m_listFilters.GetNextItem(-1, LVNI_SELECTED);
215 m_sFileFilterPath = m_listFilters.GetItemText(sel, 2);
221 * @brief Open selected filter for editing.
223 * This opens selected file filter file for user to edit. Other WinMerge UI is
224 * not (anymore) blocked during editing. We let user continue working with
225 * WinMerge while editing filter(s). Before opening this dialog and before
226 * doing directory compare we re-load changed filter files from disk. So we
227 * always compare with latest saved filters.
228 * @sa CMainFrame::OnToolsFilters()
229 * @sa CDirDoc::Rescan()
230 * @sa FileFilterHelper::ReloadUpdatedFilters()
232 void FileFiltersDlg::OnFiltersEditbtn()
236 sel = m_listFilters.GetNextItem(sel, LVNI_SELECTED);
238 // Can't edit first "None"
241 String path = m_listFilters.GetItemText(sel, 2);
242 EditFileFilter(path);
247 * @brief Edit file filter in external editor.
248 * @param [in] path Full path to file filter to edit.
250 void FileFiltersDlg::EditFileFilter(const String& path)
252 theApp.OpenFileToExternalEditor(path);
256 * @brief Edit selected filter when its double-clicked.
257 * @param [in] pNMHDR List control item data.
258 * @param [out] pResult Result of the action is returned in here.
260 void FileFiltersDlg::OnDblclkFiltersList(NMHDR* pNMHDR, LRESULT* pResult)
262 UNREFERENCED_PARAMETER(pNMHDR);
269 * @brief Is item in list the <None> item?
270 * @param [in] item Item to test.
271 * @return true if item is <None> item.
273 bool FileFiltersDlg::IsFilterItemNone(int item) const
275 String txtNone = _("<None>");
276 String txt = m_listFilters.GetItemText(item, 0);
278 return (strutils::compare_nocase(txt, txtNone) == 0);
282 * @brief Called when item state is changed.
284 * Disable Edit-button when "None" filter is selected.
285 * @param [in] pNMHDR Listview item data.
286 * @param [out] pResult Result of the action is returned in here.
288 void FileFiltersDlg::OnLvnItemchangedFilterfileList(NMHDR *pNMHDR, LRESULT *pResult)
290 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
292 // If item got selected
293 if (pNMLV->uNewState & LVIS_SELECTED)
295 String txtNone = _("<None>");
296 String txt = m_listFilters.GetItemText(pNMLV->iItem, 0);
298 bool isNone = strutils::compare_nocase(txt, txtNone) == 0;
300 EnableDlgItem(IDC_FILTERFILE_TEST_BTN, !isNone);
301 EnableDlgItem(IDC_FILTERFILE_EDITBTN, !isNone);
302 EnableDlgItem(IDC_FILTERFILE_DELETEBTN, !isNone);
308 * @brief Called before infotip is shown to get infotip text.
309 * @param [in] pNMHDR Listview item data.
310 * @param [out] pResult Result of the action is returned in here.
312 void FileFiltersDlg::OnInfoTip(NMHDR * pNMHDR, LRESULT * pResult)
314 LVHITTESTINFO lvhti = {0};
315 NMLVGETINFOTIP * pInfoTip = reinterpret_cast<NMLVGETINFOTIP*>(pNMHDR);
318 // Get subitem under mouse cursor
319 lvhti.pt = m_ptLastMousePos;
320 m_listFilters.SubItemHitTest(&lvhti);
322 if (lvhti.iSubItem > 1)
324 // Check that we are over icon or label
325 if ((lvhti.flags & LVHT_ONITEMICON) || (lvhti.flags & LVHT_ONITEMLABEL))
327 // Set item text to tooltip
328 String strText = m_listFilters.GetItemText(lvhti.iItem, lvhti.iSubItem);
329 _tcscpy_s(pInfoTip->pszText, pInfoTip->cchTextMax, strText.c_str());
335 * @brief Track mouse position for showing tooltips.
336 * @param [in] nFlags Mouse movement flags.
337 * @param [in] point Current mouse position.
339 void FileFiltersDlg::OnMouseMove(UINT nFlags, CPoint point)
341 m_ptLastMousePos = point;
342 CDialog::OnMouseMove(nFlags, point);
346 * @brief Called when user presses "Test" button.
348 * Asks filename for new filter from user (using standard
349 * file picker dialog) and copies template file to that
350 * name. Opens new filterfile for editing.
351 * @todo (At least) Warn if user puts filter to outside
352 * filter directories?
354 void FileFiltersDlg::OnBnClickedFilterfileTestButton()
358 int sel = m_listFilters.GetNextItem(-1, LVNI_SELECTED);
361 if (IsFilterItemNone(sel))
364 m_sFileFilterPath = m_listFilters.GetItemText(sel, 2);
366 // Ensure filter is up-to-date (user probably just edited it)
367 theApp.m_pGlobalFileFilter->ReloadUpdatedFilters();
369 FileFilterMgr *pMgr = theApp.m_pGlobalFileFilter->GetManager();
370 FileFilter * pFileFilter = pMgr->GetFilterByPath(m_sFileFilterPath);
374 CTestFilterDlg dlg(this, pFileFilter, pMgr);
379 * @brief Called when user presses "New..." button.
381 * Asks filename for new filter from user (using standard
382 * file picker dialog) and copies template file to that
383 * name. Opens new filterfile for editing.
384 * @todo (At least) Warn if user puts filter to outside
385 * filter directories?
386 * @todo Can global filter path be empty (I think not - Kimmo).
388 void FileFiltersDlg::OnBnClickedFilterfileNewbutton()
390 String globalPath = theApp.m_pGlobalFileFilter->GetGlobalFilterPathWithCreate();
391 String userPath = theApp.m_pGlobalFileFilter->GetUserFilterPathWithCreate();
393 if (globalPath.empty() && userPath.empty())
396 _("User's filter file folder is not defined!\n\nPlease select filter folder in Options/System.").c_str(), MB_ICONSTOP);
400 // Format path to template file
401 String templatePath = paths::ConcatPath(globalPath, FILE_FILTER_TEMPLATE);
403 if (paths::DoesPathExist(templatePath) != paths::IS_EXISTING_FILE)
405 String msg = strutils::format_string2(
406 _("Cannot find file filter template file!\n\nPlease copy file %1 to WinMerge/Filters -folder:\n%2."),
407 FILE_FILTER_TEMPLATE, templatePath);
408 AfxMessageBox(msg.c_str(), MB_ICONERROR);
412 String path = globalPath.empty() ? userPath : globalPath;
414 if (!globalPath.empty() && !userPath.empty())
416 CSharedFilterDlg dlg(
417 GetOptionsMgr()->GetBool(OPT_FILEFILTER_SHARED) ?
418 CSharedFilterDlg::SHARED : CSharedFilterDlg::PRIVATE);
419 if (dlg.DoModal() != IDOK)
421 GetOptionsMgr()->SaveOption(OPT_FILEFILTER_SHARED, (dlg.GetSelectedFilterType() == CSharedFilterDlg::SHARED));
422 path = dlg.GetSelectedFilterType() == CSharedFilterDlg::SHARED ? globalPath : userPath;
426 path = paths::AddTrailingSlash(path);
429 if (SelectFile(GetSafeHwnd(), s, path.c_str(), _("Select filename for new filter"), _("File Filters (*.flt)|*.flt|All Files (*.*)|*.*||"),
432 // Fix file extension
433 TCHAR file[_MAX_FNAME] = {0};
434 TCHAR ext[_MAX_EXT] = {0};
435 TCHAR dir[_MAX_DIR] = {0};
436 TCHAR drive[_MAX_DRIVE] = {0};
437 _tsplitpath_s(s.c_str(), drive, _MAX_DRIVE, dir, _MAX_DIR, file, _MAX_FNAME, ext, _MAX_EXT);
438 if (_tcslen(ext) == 0)
442 else if (_tcsicmp(ext, FileFilterExt) != 0)
450 // Open-dialog asks about overwriting, so we can overwrite filter file
451 // user has already allowed it.
452 if (!CopyFile(templatePath.c_str(), s.c_str(), FALSE))
454 String msg = strutils::format_string1(
455 _( "Cannot copy filter template file to filter folder:\n%1\n\nPlease make sure the folder exists and is writable."),
457 AfxMessageBox(msg.c_str(), MB_ICONERROR);
461 FileFilterMgr *pMgr = theApp.m_pGlobalFileFilter->GetManager();
462 int retval = pMgr->AddFilter(s);
463 if (retval == FILTER_OK)
465 // Remove all from filterslist and re-add so we can update UI
468 theApp.m_pGlobalFileFilter->LoadAllFileFilters();
469 theApp.m_pGlobalFileFilter->GetFileFilters(m_Filters, selected);
477 * @brief Delete selected filter.
479 void FileFiltersDlg::OnBnClickedFilterfileDelete()
484 sel = m_listFilters.GetNextItem(sel, LVNI_SELECTED);
486 // Can't delete first "None"
489 path = m_listFilters.GetItemText(sel, 2);
491 String sConfirm = strutils::format_string1(_("Are you sure you want to delete\n\n%1 ?"), path);
492 int res = AfxMessageBox(sConfirm.c_str(), MB_ICONWARNING | MB_YESNO);
495 if (DeleteFile(path.c_str()))
497 FileFilterMgr *pMgr = theApp.m_pGlobalFileFilter->GetManager();
498 pMgr->RemoveFilter(path);
500 // Remove all from filterslist and re-add so we can update UI
503 theApp.m_pGlobalFileFilter->GetFileFilters(m_Filters, selected);
509 String msg = strutils::format_string1(
510 _("Failed to delete the filter file:\n%1\n\nMaybe the file is read-only?"),
512 AfxMessageBox(msg.c_str(), MB_ICONSTOP);
519 * @brief Update filters to list.
521 void FileFiltersDlg::UpdateFiltersList()
523 int count = (int) m_Filters->size();
525 m_listFilters.DeleteAllItems();
527 String title = _("<None>");
528 m_listFilters.InsertItem(1, title.c_str());
529 m_listFilters.SetItemText(0, 1, title.c_str());
530 m_listFilters.SetItemText(0, 2, title.c_str());
532 for (int i = 0; i < count; i++)
539 * @brief Open help from mainframe when user presses F1
541 void FileFiltersDlg::OnHelp()
543 theApp.ShowHelp(FilterHelpLocation);
547 * @brief Install new filter.
548 * This function is called when user selects "Install" button from GUI.
549 * Function allows easy installation of new filters for user. For example
550 * when user has downloaded filter file from net. First we ask user to
551 * select filter to install. Then we copy selected filter to private
554 void FileFiltersDlg::OnBnClickedFilterfileInstall()
558 String userPath = theApp.m_pGlobalFileFilter->GetUserFilterPathWithCreate();
560 if (SelectFile(GetSafeHwnd(), s, path.c_str(), _("Locate filter file to install"), _("File Filters (*.flt)|*.flt|All Files (*.*)|*.*||"),
563 userPath = paths::ConcatPath(userPath, paths::FindFileName(s));
564 if (!CopyFile(s.c_str(), userPath.c_str(), TRUE))
566 // If file already exists, ask from user
567 // If user wants to, overwrite existing filter
568 if (paths::DoesPathExist(userPath) == paths::IS_EXISTING_FILE)
570 int res = LangMessageBox(IDS_FILEFILTER_OVERWRITE, MB_YESNO |
574 if (!CopyFile(s.c_str(), userPath.c_str(), FALSE))
576 LangMessageBox(IDS_FILEFILTER_INSTALLFAIL, MB_ICONSTOP);
582 LangMessageBox(IDS_FILEFILTER_INSTALLFAIL, MB_ICONSTOP);
587 FileFilterMgr *pMgr = theApp.m_pGlobalFileFilter->GetManager();
588 pMgr->AddFilter(userPath);
590 // Remove all from filterslist and re-add so we can update UI
593 theApp.m_pGlobalFileFilter->GetFileFilters(m_Filters, selected);