OSDN Git Service

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