OSDN Git Service

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