OSDN Git Service

Modify "Tools - Generate Report" (Add column width definition to html output) (#1333...
[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 #include "UniFile.h"
22
23 using std::vector;
24
25 #ifdef _DEBUG
26 #define new DEBUG_NEW
27 #endif
28
29 /** @brief Template file used when creating new filefilter. */
30 static const TCHAR FILE_FILTER_TEMPLATE[] = _T("FileFilter.tmpl");
31
32 /** @brief Location for filters specific help to open. */
33 static const TCHAR FilterHelpLocation[] = _T("::/htmlhelp/Filters.html");
34
35 /////////////////////////////////////////////////////////////////////////////
36 // CFiltersDlg dialog
37 IMPLEMENT_DYNCREATE(FileFiltersDlg, CTrPropertyPage)
38
39 /**
40  * @brief Constructor.
41  */
42 FileFiltersDlg::FileFiltersDlg() : CTrPropertyPage(FileFiltersDlg::IDD)
43 {
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;
49 }
50
51 void FileFiltersDlg::DoDataExchange(CDataExchange* pDX)
52 {
53         CDialog::DoDataExchange(pDX);
54         //{{AFX_DATA_MAP(FileFiltersDlg)
55         DDX_Control(pDX, IDC_FILTERFILE_LIST, m_listFilters);
56         //}}AFX_DATA_MAP
57 }
58
59
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)
64         ON_WM_MOUSEMOVE()
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)
69         //}}AFX_MSG_MAP
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)
73 END_MESSAGE_MAP()
74
75 /////////////////////////////////////////////////////////////////////////////
76 // CFiltersDlg message handlers
77
78 /**
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.
82  */
83 void FileFiltersDlg::SetFilterArray(const vector<FileFilterInfo>& fileFilters)
84 {
85         m_Filters = fileFilters;
86 }
87
88 /**
89  * @brief Returns path (cont. filename) of selected filter
90  * @return Full path to selected filter file.
91  */
92 String FileFiltersDlg::GetSelected()
93 {
94         return m_sFileFilterPath;
95 }
96
97 /**
98  * @brief Set path of selected filter.
99  * @param [in] Path for selected filter.
100  * @note Call this before actually showing the dialog.
101  */
102 void FileFiltersDlg::SetSelected(const String & selected)
103 {
104         m_sFileFilterPath = selected;
105 }
106
107 /**
108  * @brief Initialise listcontrol containing filters.
109  */
110 void FileFiltersDlg::InitList()
111 {
112         // Show selection across entire row.
113         // Also enable infotips.
114         m_listFilters.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_INFOTIP);
115
116         const int lpx = CClientDC(this).GetDeviceCaps(LOGPIXELSX);
117         auto pointToPixel = [lpx](int point) { return MulDiv(point, lpx, 72); };
118
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));
125
126         title = _("<None>");
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());
130
131         const int count = (int) m_Filters.size();
132
133         for (int i = 0; i < count; i++)
134         {
135                 AddToGrid(i);
136         }
137 }
138
139 /**
140  * @brief Select filter by index in the listview.
141  * @param [in] index Index of filter to select.
142  */
143 void FileFiltersDlg::SelectFilterByIndex(int index)
144 {
145         m_listFilters.SetItemState(index, LVIS_SELECTED, LVIS_SELECTED);
146         bool bPartialOk = false;
147         m_listFilters.EnsureVisible(index, bPartialOk);
148 }
149
150 /**
151  * @brief Select filter by file path in the listview.
152  * @param [in] path file path
153  */
154 void FileFiltersDlg::SelectFilterByFilePath(const String& path)
155 {
156         for (size_t i = 0; i < m_Filters.size(); ++i)
157         {
158                 if (m_Filters[i].fullpath == path)
159                 {
160                         SelectFilterByIndex(static_cast<int>(i + 1));
161                         break;
162                 }
163         }
164 }
165
166 /**
167  * @brief Called before dialog is shown.
168  * @return Always TRUE.
169  */
170 BOOL FileFiltersDlg::OnInitDialog()
171 {
172         CTrPropertyPage::OnInitDialog();
173
174         InitList();
175
176         if (m_sFileFilterPath.empty())
177         {
178                 SelectFilterByIndex(0);
179                 return TRUE;
180         }
181
182         int count = m_listFilters.GetItemCount();
183         for (int i = 0; i < count; i++)
184         {
185                 String desc = m_listFilters.GetItemText(i, 2);
186                 if (strutils::compare_nocase(desc, m_sFileFilterPath) == 0)
187                 {
188                         SelectFilterByIndex(i);
189                 }
190         }
191
192         return TRUE;  // return TRUE unless you set the focus to a control
193                       // EXCEPTION: OCX Property Pages should return FALSE
194 }
195
196 /**
197  * @brief Add filter from filter-list index to dialog.
198  * @param [in] filterIndex Index of filter to add.
199  */
200 void FileFiltersDlg::AddToGrid(int filterIndex)
201 {
202         const FileFilterInfo & filterinfo = m_Filters.at(filterIndex);
203         const int item = filterIndex + 1;
204
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());
208 }
209
210 /**
211  * @brief Called when dialog is closed with "OK" button.
212  */
213 void FileFiltersDlg::OnOK()
214 {
215         int sel = m_listFilters.GetNextItem(-1, LVNI_SELECTED);
216         m_sFileFilterPath = m_listFilters.GetItemText(sel, 2);
217
218         AfxGetApp()->WriteProfileInt(_T("Settings"), _T("FilterStartPage"), GetParentSheet()->GetActiveIndex());
219
220         CDialog::OnOK();
221 }
222
223 /**
224  * @brief Open selected filter for editing.
225  *
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()
234  */
235 void FileFiltersDlg::OnFiltersEditbtn()
236 {
237         int sel =- 1;
238
239         sel = m_listFilters.GetNextItem(sel, LVNI_SELECTED);
240
241         // Can't edit first "None"
242         if (sel > 0)
243         {
244                 String path = m_listFilters.GetItemText(sel, 2);
245                 EditFileFilter(path);
246         }
247 }
248
249 /**
250  * @brief Edit file filter in external editor.
251  * @param [in] path Full path to file filter to edit.
252  */
253 void FileFiltersDlg::EditFileFilter(const String& path)
254 {
255         CMergeApp::OpenFileToExternalEditor(path);
256 }
257
258 /**
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.
262  */
263 void FileFiltersDlg::OnDblclkFiltersList(NMHDR* pNMHDR, LRESULT* pResult)
264 {
265         UNREFERENCED_PARAMETER(pNMHDR);
266
267         OnFiltersEditbtn();
268         *pResult = 0;
269 }
270
271 /**
272  * @brief Is item in list the <None> item?
273  * @param [in] item Item to test.
274  * @return true if item is <None> item.
275  */
276 bool FileFiltersDlg::IsFilterItemNone(int item) const
277 {
278         String txtNone = _("<None>");
279         String txt = m_listFilters.GetItemText(item, 0);
280
281         return (strutils::compare_nocase(txt, txtNone) == 0);
282 }
283
284 /**
285  * @brief Called when item state is changed.
286  *
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.
290  */
291 void FileFiltersDlg::OnLvnItemchangedFilterfileList(NMHDR *pNMHDR, LRESULT *pResult)
292 {
293         LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
294
295         // If item got selected
296         if (pNMLV->uNewState & LVIS_SELECTED)
297         {
298                 String txtNone = _("<None>");
299                 String txt = m_listFilters.GetItemText(pNMLV->iItem, 0);
300
301                 bool isNone = strutils::compare_nocase(txt, txtNone) == 0;
302
303                 EnableDlgItem(IDC_FILTERFILE_TEST_BTN, !isNone);
304                 EnableDlgItem(IDC_FILTERFILE_EDITBTN, !isNone);
305                 EnableDlgItem(IDC_FILTERFILE_DELETEBTN, !isNone);
306         }
307         *pResult = 0;
308 }
309
310 /**
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.
314  */
315 void FileFiltersDlg::OnInfoTip(NMHDR * pNMHDR, LRESULT * pResult)
316 {
317         LVHITTESTINFO lvhti = {0};
318         NMLVGETINFOTIP * pInfoTip = reinterpret_cast<NMLVGETINFOTIP*>(pNMHDR);
319         ASSERT(pInfoTip != nullptr);
320
321         // Get subitem under mouse cursor
322         lvhti.pt = m_ptLastMousePos;
323         m_listFilters.SubItemHitTest(&lvhti);
324
325         if (lvhti.iSubItem > 1)
326         {
327                 // Check that we are over icon or label
328                 if ((lvhti.flags & LVHT_ONITEMICON) || (lvhti.flags & LVHT_ONITEMLABEL))
329                 {
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());
333                 }
334         }
335 }
336
337 /**
338  * @brief Track mouse position for showing tooltips.
339  * @param [in] nFlags Mouse movement flags.
340  * @param [in] point Current mouse position.
341  */
342 void FileFiltersDlg::OnMouseMove(UINT nFlags, CPoint point) 
343 {
344         m_ptLastMousePos = point;
345         CDialog::OnMouseMove(nFlags, point);
346 }
347
348 /**
349  * @brief Called when user presses "Test" button.
350  *
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?
356  */
357 void FileFiltersDlg::OnBnClickedFilterfileTestButton()
358 {
359         UpdateData(TRUE);
360
361         int sel = m_listFilters.GetNextItem(-1, LVNI_SELECTED);
362         if (sel == -1)
363                 return;
364         if (IsFilterItemNone(sel))
365                 return;
366         
367         m_sFileFilterPath = m_listFilters.GetItemText(sel, 2);
368
369         // Ensure filter is up-to-date (user probably just edited it)
370         auto* pGlobalFileFilter = theApp.GetGlobalFileFilter();
371         pGlobalFileFilter->ReloadUpdatedFilters();
372
373         FileFilterMgr *pMgr = pGlobalFileFilter->GetManager();
374         FileFilter * pFileFilter = pMgr->GetFilterByPath(m_sFileFilterPath);
375         if (pFileFilter == nullptr)
376                 return;
377
378         CTestFilterDlg dlg(this, pFileFilter, pMgr);
379         dlg.DoModal();
380 }
381
382 /**
383  * @brief Called when user presses "New..." button.
384  *
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).
391  */
392 void FileFiltersDlg::OnBnClickedFilterfileNewbutton()
393 {
394         auto* pGlobalFileFilter = theApp.GetGlobalFileFilter();
395         String globalPath = pGlobalFileFilter->GetGlobalFilterPathWithCreate();
396         String userPath = pGlobalFileFilter->GetUserFilterPathWithCreate();
397
398         if (globalPath.empty() && userPath.empty())
399         {
400                 AfxMessageBox(
401                         _("User's filter file folder is not defined!\n\nPlease select filter folder in Options/System.").c_str(), MB_ICONSTOP);
402                 return;
403         }
404
405         // Format path to template file
406         String templatePath = paths::ConcatPath(globalPath, FILE_FILTER_TEMPLATE);
407
408         if (paths::DoesPathExist(templatePath) != paths::IS_EXISTING_FILE)
409         {
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);
414                 return;
415         }
416
417         String path = globalPath.empty() ? userPath : globalPath;
418
419         if (!globalPath.empty() && !userPath.empty())
420         {
421                 CSharedFilterDlg dlg(
422                         GetOptionsMgr()->GetBool(OPT_FILEFILTER_SHARED) ? 
423                                 CSharedFilterDlg::SHARED : CSharedFilterDlg::PRIVATE);
424                 if (dlg.DoModal() != IDOK)
425                         return;
426                 GetOptionsMgr()->SaveOption(OPT_FILEFILTER_SHARED, (dlg.GetSelectedFilterType() == CSharedFilterDlg::SHARED));
427                 path = dlg.GetSelectedFilterType() == CSharedFilterDlg::SHARED ? globalPath : userPath;
428         }
429
430         if (path.length())
431                 path = paths::AddTrailingSlash(path);
432         
433         String s;
434         if (SelectFile(GetSafeHwnd(), s, false, path.c_str(), _("Select filename for new filter"),
435                 _("File Filters (*.flt)|*.flt|All Files (*.*)|*.*||")))
436         {
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);
443                 if (ext[0] == '\0')
444                 {
445                         s += FileFilterExt;
446                 }
447                 else if (_tcsicmp(ext, FileFilterExt) != 0)
448                 {
449                         s = drive;
450                         s += dir;
451                         s += file;
452                         s += FileFilterExt;
453                 }
454
455                 // Open-dialog asks about overwriting, so we can overwrite filter file
456                 // user has already allowed it.
457                 UniMemFile fileIn;
458                 UniStdioFile fileOut;
459                 if (!fileIn.OpenReadOnly(templatePath) || !fileOut.OpenCreate(s))
460                 {
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."),
463                                 templatePath);
464                         AfxMessageBox(msg.c_str(), MB_ICONERROR);
465                         return;
466                 }
467                 String lines;
468                 fileIn.ReadStringAll(lines);
469                 strutils::replace(lines, _T("${name}"), file);
470                 fileOut.WriteString(lines);
471                 fileIn.Close();
472                 fileOut.Close();
473
474                 EditFileFilter(s);
475                 FileFilterMgr *pMgr = pGlobalFileFilter->GetManager();
476                 int retval = pMgr->AddFilter(s);
477                 if (retval == FILTER_OK)
478                 {
479                         // Remove all from filterslist and re-add so we can update UI
480                         String selected;
481                         pGlobalFileFilter->LoadAllFileFilters();
482                         m_Filters = pGlobalFileFilter->GetFileFilters(selected);
483
484                         UpdateFiltersList();
485                         SelectFilterByFilePath(s);
486                 }
487         }
488 }
489
490 /**
491  * @brief Delete selected filter.
492  */
493 void FileFiltersDlg::OnBnClickedFilterfileDelete()
494 {
495         String path;
496         int sel =- 1;
497
498         sel = m_listFilters.GetNextItem(sel, LVNI_SELECTED);
499
500         // Can't delete first "None"
501         if (sel > 0)
502         {
503                 path = m_listFilters.GetItemText(sel, 2);
504
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);
507                 if (res == IDYES)
508                 {
509                         if (DeleteFile(path.c_str()))
510                         {
511                                 auto* pGlobalFileFilter = theApp.GetGlobalFileFilter();
512                                 FileFilterMgr *pMgr = pGlobalFileFilter->GetManager();
513                                 pMgr->RemoveFilter(path);
514                                 
515                                 // Remove all from filterslist and re-add so we can update UI
516                                 String selected;
517                                 m_Filters = pGlobalFileFilter->GetFileFilters(selected);
518
519                                 UpdateFiltersList();
520                         }
521                         else
522                         {
523                                 String msg = strutils::format_string1(
524                                         _("Failed to delete the filter file:\n%1\n\nMaybe the file is read-only?"),
525                                         path);
526                                 AfxMessageBox(msg.c_str(), MB_ICONSTOP);
527                         }
528                 }
529         }
530 }
531
532 /**
533  * @brief Update filters to list.
534  */
535 void FileFiltersDlg::UpdateFiltersList()
536 {
537         int count = (int) m_Filters.size();
538
539         m_listFilters.DeleteAllItems();
540
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());
545
546         for (int i = 0; i < count; i++)
547         {
548                 AddToGrid(i);
549         }
550 }
551
552 /**
553  * @brief Open help from mainframe when user presses F1
554  */
555 void FileFiltersDlg::OnHelp()
556 {
557         theApp.ShowHelp(FilterHelpLocation);
558 }
559
560 /**
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
566  * filters folder.
567  */
568 void FileFiltersDlg::OnBnClickedFilterfileInstall()
569 {
570         auto* pGlobalFileFilter = theApp.GetGlobalFileFilter();
571         String s;
572         String path;
573
574         if (SelectFile(GetSafeHwnd(), s, true, path.c_str(),_("Locate filter file to install"),
575                 _("File Filters (*.flt)|*.flt|All Files (*.*)|*.*||")))
576         {
577                 String userPath = pGlobalFileFilter->GetUserFilterPathWithCreate();
578                 userPath = paths::ConcatPath(userPath, paths::FindFileName(s));
579                 if (!CopyFile(s.c_str(), userPath.c_str(), TRUE))
580                 {
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)
584                         {
585                                 int res = LangMessageBox(IDS_FILEFILTER_OVERWRITE, MB_YESNO |
586                                         MB_ICONWARNING);
587                                 if (res == IDYES)
588                                 {
589                                         if (!CopyFile(s.c_str(), userPath.c_str(), FALSE))
590                                         {
591                                                 LangMessageBox(IDS_FILEFILTER_INSTALLFAIL, MB_ICONSTOP);
592                                         }
593                                 }
594                         }
595                         else
596                         {
597                                 LangMessageBox(IDS_FILEFILTER_INSTALLFAIL, MB_ICONSTOP);
598                         }
599                 }
600                 else
601                 {
602                         FileFilterMgr *pMgr = pGlobalFileFilter->GetManager();
603                         pMgr->AddFilter(userPath);
604
605                         // Remove all from filterslist and re-add so we can update UI
606                         String selected;
607                         m_Filters = pGlobalFileFilter->GetFileFilters(selected);
608
609                         UpdateFiltersList();
610                         SelectFilterByFilePath(userPath);
611                 }
612         }
613 }