OSDN Git Service

6d922d07ace98665259f764550b77d150a400bdd
[winmerge-jp/winmerge-jp.git] / Src / FileFiltersDlg.cpp
1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /**
3  * @file  FileFiltersDlg.cpp
4  *
5  * @brief Implementation of FileFilters -dialog
6  */
7
8 #include "stdafx.h"
9 #include "FileFiltersDlg.h"
10 #include <vector>
11 #include "UnicodeString.h"
12 #include "Merge.h"
13 #include "OptionsMgr.h"
14 #include "OptionsDef.h"
15 #include "FileFilterMgr.h"
16 #include "FileFilterHelper.h"
17 #include "paths.h"
18 #include "SharedFilterDlg.h"
19 #include "TestFilterDlg.h"
20 #include "FileOrFolderSelect.h"
21
22 using std::vector;
23
24 #ifdef _DEBUG
25 #define new DEBUG_NEW
26 #endif
27
28 /** @brief Template file used when creating new filefilter. */
29 static const TCHAR FILE_FILTER_TEMPLATE[] = _T("FileFilter.tmpl");
30
31 /** @brief Location for filters specific help to open. */
32 static const TCHAR FilterHelpLocation[] = _T("::/htmlhelp/Filters.html");
33
34 /////////////////////////////////////////////////////////////////////////////
35 // CFiltersDlg dialog
36 IMPLEMENT_DYNCREATE(FileFiltersDlg, CTrPropertyPage)
37
38 /**
39  * @brief Constructor.
40  */
41 FileFiltersDlg::FileFiltersDlg() : CTrPropertyPage(FileFiltersDlg::IDD)
42 {
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;
48 }
49
50 void FileFiltersDlg::DoDataExchange(CDataExchange* pDX)
51 {
52         CDialog::DoDataExchange(pDX);
53         //{{AFX_DATA_MAP(FileFiltersDlg)
54         DDX_Control(pDX, IDC_FILTERFILE_LIST, m_listFilters);
55         //}}AFX_DATA_MAP
56 }
57
58
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)
63         ON_WM_MOUSEMOVE()
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)
68         //}}AFX_MSG_MAP
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)
72 END_MESSAGE_MAP()
73
74 /////////////////////////////////////////////////////////////////////////////
75 // CFiltersDlg message handlers
76
77 /**
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.
81  */
82 void FileFiltersDlg::SetFilterArray(const vector<FileFilterInfo>& fileFilters)
83 {
84         m_Filters = fileFilters;
85 }
86
87 /**
88  * @brief Returns path (cont. filename) of selected filter
89  * @return Full path to selected filter file.
90  */
91 String FileFiltersDlg::GetSelected()
92 {
93         return m_sFileFilterPath;
94 }
95
96 /**
97  * @brief Set path of selected filter.
98  * @param [in] Path for selected filter.
99  * @note Call this before actually showing the dialog.
100  */
101 void FileFiltersDlg::SetSelected(const String & selected)
102 {
103         m_sFileFilterPath = selected;
104 }
105
106 /**
107  * @brief Initialise listcontrol containing filters.
108  */
109 void FileFiltersDlg::InitList()
110 {
111         // Show selection across entire row.
112         // Also enable infotips.
113         m_listFilters.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_INFOTIP);
114
115         const int lpx = CClientDC(this).GetDeviceCaps(LOGPIXELSX);
116         auto pointToPixel = [lpx](int point) { return MulDiv(point, lpx, 72); };
117
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));
124
125         title = _("<None>");
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());
129
130         const int count = (int) m_Filters.size();
131
132         for (int i = 0; i < count; i++)
133         {
134                 AddToGrid(i);
135         }
136 }
137
138 /**
139  * @brief Select filter by index in the listview.
140  * @param [in] index Index of filter to select.
141  */
142 void FileFiltersDlg::SelectFilterByIndex(int index)
143 {
144         m_listFilters.SetItemState(index, LVIS_SELECTED, LVIS_SELECTED);
145         bool bPartialOk = false;
146         m_listFilters.EnsureVisible(index, bPartialOk);
147 }
148
149 /**
150  * @brief Called before dialog is shown.
151  * @return Always TRUE.
152  */
153 BOOL FileFiltersDlg::OnInitDialog()
154 {
155         CTrPropertyPage::OnInitDialog();
156
157         InitList();
158
159         if (m_sFileFilterPath.empty())
160         {
161                 SelectFilterByIndex(0);
162                 return TRUE;
163         }
164
165         int count = m_listFilters.GetItemCount();
166         for (int i = 0; i < count; i++)
167         {
168                 String desc = m_listFilters.GetItemText(i, 2);
169                 if (strutils::compare_nocase(desc, m_sFileFilterPath) == 0)
170                 {
171                         SelectFilterByIndex(i);
172                 }
173         }
174
175         return TRUE;  // return TRUE unless you set the focus to a control
176                       // EXCEPTION: OCX Property Pages should return FALSE
177 }
178
179 /**
180  * @brief Add filter from filter-list index to dialog.
181  * @param [in] filterIndex Index of filter to add.
182  */
183 void FileFiltersDlg::AddToGrid(int filterIndex)
184 {
185         const FileFilterInfo & filterinfo = m_Filters.at(filterIndex);
186         const int item = filterIndex + 1;
187
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());
191 }
192
193 /**
194  * @brief Called when dialog is closed with "OK" button.
195  */
196 void FileFiltersDlg::OnOK()
197 {
198         int sel = m_listFilters.GetNextItem(-1, LVNI_SELECTED);
199         m_sFileFilterPath = m_listFilters.GetItemText(sel, 2);
200
201         AfxGetApp()->WriteProfileInt(_T("Settings"), _T("FilterStartPage"), GetParentSheet()->GetActiveIndex());
202
203         CDialog::OnOK();
204 }
205
206 /**
207  * @brief Open selected filter for editing.
208  *
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()
217  */
218 void FileFiltersDlg::OnFiltersEditbtn()
219 {
220         int sel =- 1;
221
222         sel = m_listFilters.GetNextItem(sel, LVNI_SELECTED);
223
224         // Can't edit first "None"
225         if (sel > 0)
226         {
227                 String path = m_listFilters.GetItemText(sel, 2);
228                 EditFileFilter(path);
229         }
230 }
231
232 /**
233  * @brief Edit file filter in external editor.
234  * @param [in] path Full path to file filter to edit.
235  */
236 void FileFiltersDlg::EditFileFilter(const String& path)
237 {
238         theApp.OpenFileToExternalEditor(path);
239 }
240
241 /**
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.
245  */
246 void FileFiltersDlg::OnDblclkFiltersList(NMHDR* pNMHDR, LRESULT* pResult)
247 {
248         UNREFERENCED_PARAMETER(pNMHDR);
249
250         OnFiltersEditbtn();
251         *pResult = 0;
252 }
253
254 /**
255  * @brief Is item in list the <None> item?
256  * @param [in] item Item to test.
257  * @return true if item is <None> item.
258  */
259 bool FileFiltersDlg::IsFilterItemNone(int item) const
260 {
261         String txtNone = _("<None>");
262         String txt = m_listFilters.GetItemText(item, 0);
263
264         return (strutils::compare_nocase(txt, txtNone) == 0);
265 }
266
267 /**
268  * @brief Called when item state is changed.
269  *
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.
273  */
274 void FileFiltersDlg::OnLvnItemchangedFilterfileList(NMHDR *pNMHDR, LRESULT *pResult)
275 {
276         LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
277
278         // If item got selected
279         if (pNMLV->uNewState & LVIS_SELECTED)
280         {
281                 String txtNone = _("<None>");
282                 String txt = m_listFilters.GetItemText(pNMLV->iItem, 0);
283
284                 bool isNone = strutils::compare_nocase(txt, txtNone) == 0;
285
286                 EnableDlgItem(IDC_FILTERFILE_TEST_BTN, !isNone);
287                 EnableDlgItem(IDC_FILTERFILE_EDITBTN, !isNone);
288                 EnableDlgItem(IDC_FILTERFILE_DELETEBTN, !isNone);
289         }
290         *pResult = 0;
291 }
292
293 /**
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.
297  */
298 void FileFiltersDlg::OnInfoTip(NMHDR * pNMHDR, LRESULT * pResult)
299 {
300         LVHITTESTINFO lvhti = {0};
301         NMLVGETINFOTIP * pInfoTip = reinterpret_cast<NMLVGETINFOTIP*>(pNMHDR);
302         ASSERT(pInfoTip != nullptr);
303
304         // Get subitem under mouse cursor
305         lvhti.pt = m_ptLastMousePos;
306         m_listFilters.SubItemHitTest(&lvhti);
307
308         if (lvhti.iSubItem > 1)
309         {
310                 // Check that we are over icon or label
311                 if ((lvhti.flags & LVHT_ONITEMICON) || (lvhti.flags & LVHT_ONITEMLABEL))
312                 {
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());
316                 }
317         }
318 }
319
320 /**
321  * @brief Track mouse position for showing tooltips.
322  * @param [in] nFlags Mouse movement flags.
323  * @param [in] point Current mouse position.
324  */
325 void FileFiltersDlg::OnMouseMove(UINT nFlags, CPoint point) 
326 {
327         m_ptLastMousePos = point;
328         CDialog::OnMouseMove(nFlags, point);
329 }
330
331 /**
332  * @brief Called when user presses "Test" button.
333  *
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?
339  */
340 void FileFiltersDlg::OnBnClickedFilterfileTestButton()
341 {
342         UpdateData(TRUE);
343
344         int sel = m_listFilters.GetNextItem(-1, LVNI_SELECTED);
345         if (sel == -1)
346                 return;
347         if (IsFilterItemNone(sel))
348                 return;
349         
350         m_sFileFilterPath = m_listFilters.GetItemText(sel, 2);
351
352         // Ensure filter is up-to-date (user probably just edited it)
353         theApp.m_pGlobalFileFilter->ReloadUpdatedFilters();
354
355         FileFilterMgr *pMgr = theApp.m_pGlobalFileFilter->GetManager();
356         FileFilter * pFileFilter = pMgr->GetFilterByPath(m_sFileFilterPath);
357         if (pFileFilter == nullptr)
358                 return;
359
360         CTestFilterDlg dlg(this, pFileFilter, pMgr);
361         dlg.DoModal();
362 }
363
364 /**
365  * @brief Called when user presses "New..." button.
366  *
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).
373  */
374 void FileFiltersDlg::OnBnClickedFilterfileNewbutton()
375 {
376         String globalPath = theApp.m_pGlobalFileFilter->GetGlobalFilterPathWithCreate();
377         String userPath = theApp.m_pGlobalFileFilter->GetUserFilterPathWithCreate();
378
379         if (globalPath.empty() && userPath.empty())
380         {
381                 AfxMessageBox(
382                         _("User's filter file folder is not defined!\n\nPlease select filter folder in Options/System.").c_str(), MB_ICONSTOP);
383                 return;
384         }
385
386         // Format path to template file
387         String templatePath = paths::ConcatPath(globalPath, FILE_FILTER_TEMPLATE);
388
389         if (paths::DoesPathExist(templatePath) != paths::IS_EXISTING_FILE)
390         {
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);
395                 return;
396         }
397
398         String path = globalPath.empty() ? userPath : globalPath;
399
400         if (!globalPath.empty() && !userPath.empty())
401         {
402                 CSharedFilterDlg dlg(
403                         GetOptionsMgr()->GetBool(OPT_FILEFILTER_SHARED) ? 
404                                 CSharedFilterDlg::SHARED : CSharedFilterDlg::PRIVATE);
405                 if (dlg.DoModal() != IDOK)
406                         return;
407                 GetOptionsMgr()->SaveOption(OPT_FILEFILTER_SHARED, (dlg.GetSelectedFilterType() == CSharedFilterDlg::SHARED));
408                 path = dlg.GetSelectedFilterType() == CSharedFilterDlg::SHARED ? globalPath : userPath;
409         }
410
411         if (path.length())
412                 path = paths::AddTrailingSlash(path);
413         
414         String s;
415         if (SelectFile(GetSafeHwnd(), s, false, path.c_str(), _("Select filename for new filter"),
416                 _("File Filters (*.flt)|*.flt|All Files (*.*)|*.*||")))
417         {
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)
425                 {
426                         s += FileFilterExt;
427                 }
428                 else if (_tcsicmp(ext, FileFilterExt) != 0)
429                 {
430                         s = drive;
431                         s += dir;
432                         s += file;
433                         s += FileFilterExt;
434                 }
435
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))
439                 {
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."),
442                                 templatePath);
443                         AfxMessageBox(msg.c_str(), MB_ICONERROR);
444                         return;
445                 }
446                 EditFileFilter(s);
447                 FileFilterMgr *pMgr = theApp.m_pGlobalFileFilter->GetManager();
448                 int retval = pMgr->AddFilter(s);
449                 if (retval == FILTER_OK)
450                 {
451                         // Remove all from filterslist and re-add so we can update UI
452                         String selected;
453                         theApp.m_pGlobalFileFilter->LoadAllFileFilters();
454                         m_Filters = theApp.m_pGlobalFileFilter->GetFileFilters(selected);
455
456                         UpdateFiltersList();
457                 }
458         }
459 }
460
461 /**
462  * @brief Delete selected filter.
463  */
464 void FileFiltersDlg::OnBnClickedFilterfileDelete()
465 {
466         String path;
467         int sel =- 1;
468
469         sel = m_listFilters.GetNextItem(sel, LVNI_SELECTED);
470
471         // Can't delete first "None"
472         if (sel > 0)
473         {
474                 path = m_listFilters.GetItemText(sel, 2);
475
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);
478                 if (res == IDYES)
479                 {
480                         if (DeleteFile(path.c_str()))
481                         {
482                                 FileFilterMgr *pMgr = theApp.m_pGlobalFileFilter->GetManager();
483                                 pMgr->RemoveFilter(path);
484                                 
485                                 // Remove all from filterslist and re-add so we can update UI
486                                 String selected;
487                                 m_Filters = theApp.m_pGlobalFileFilter->GetFileFilters(selected);
488
489                                 UpdateFiltersList();
490                         }
491                         else
492                         {
493                                 String msg = strutils::format_string1(
494                                         _("Failed to delete the filter file:\n%1\n\nMaybe the file is read-only?"),
495                                         path);
496                                 AfxMessageBox(msg.c_str(), MB_ICONSTOP);
497                         }
498                 }
499         }
500 }
501
502 /**
503  * @brief Update filters to list.
504  */
505 void FileFiltersDlg::UpdateFiltersList()
506 {
507         int count = (int) m_Filters.size();
508
509         m_listFilters.DeleteAllItems();
510
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());
515
516         for (int i = 0; i < count; i++)
517         {
518                 AddToGrid(i);
519         }
520 }
521
522 /**
523  * @brief Open help from mainframe when user presses F1
524  */
525 void FileFiltersDlg::OnHelp()
526 {
527         theApp.ShowHelp(FilterHelpLocation);
528 }
529
530 /**
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
536  * filters folder.
537  */
538 void FileFiltersDlg::OnBnClickedFilterfileInstall()
539 {
540         String s;
541         String path;
542         String userPath = theApp.m_pGlobalFileFilter->GetUserFilterPathWithCreate();
543
544         if (SelectFile(GetSafeHwnd(), s, true, path.c_str(),_("Locate filter file to install"),
545                 _("File Filters (*.flt)|*.flt|All Files (*.*)|*.*||")))
546         {
547                 userPath = paths::ConcatPath(userPath, paths::FindFileName(s));
548                 if (!CopyFile(s.c_str(), userPath.c_str(), TRUE))
549                 {
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)
553                         {
554                                 int res = LangMessageBox(IDS_FILEFILTER_OVERWRITE, MB_YESNO |
555                                         MB_ICONWARNING);
556                                 if (res == IDYES)
557                                 {
558                                         if (!CopyFile(s.c_str(), userPath.c_str(), FALSE))
559                                         {
560                                                 LangMessageBox(IDS_FILEFILTER_INSTALLFAIL, MB_ICONSTOP);
561                                         }
562                                 }
563                         }
564                         else
565                         {
566                                 LangMessageBox(IDS_FILEFILTER_INSTALLFAIL, MB_ICONSTOP);
567                         }
568                 }
569                 else
570                 {
571                         FileFilterMgr *pMgr = theApp.m_pGlobalFileFilter->GetManager();
572                         pMgr->AddFilter(userPath);
573
574                         // Remove all from filterslist and re-add so we can update UI
575                         String selected;
576                         m_Filters = theApp.m_pGlobalFileFilter->GetFileFilters(selected);
577
578                         UpdateFiltersList();
579                 }
580         }
581 }