OSDN Git Service

Fix an issue where items with different case are not displayed correctly in the folde...
[winmerge-jp/winmerge-jp.git] / Src / FileFilterHelper.cpp
1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /** 
3  * @file  FileFilterHelper.cpp
4  *
5  * @brief Implementation file for FileFilterHelper class
6  */
7
8 #include "pch.h"
9 #include "FileFilterHelper.h"
10 #include "UnicodeString.h"
11 #include "FilterList.h"
12 #include "DirItem.h"
13 #include "FileFilterMgr.h"
14 #include "paths.h"
15 #include "Environment.h"
16 #include "unicoder.h"
17
18 using std::vector;
19
20 /** 
21  * @brief Constructor, creates new filtermanager.
22  */
23 FileFilterHelper::FileFilterHelper()
24 : m_pMaskFileFilter(nullptr)
25 , m_pMaskDirFilter(nullptr)
26 , m_bUseMask(true)
27 , m_fileFilterMgr(new FileFilterMgr)
28 , m_currentFilter(nullptr)
29 {
30 }
31
32 /** 
33  * @brief Destructor, deletes filtermanager.
34  */
35 FileFilterHelper::~FileFilterHelper() = default;
36
37 /**
38  * @brief Store current filter path.
39  *
40  * Select filter based on filepath. If filter with that path
41  * is found select it. Otherwise set path to empty (default).
42  * @param [in] szFileFilterPath Full path to filter to select.
43  */
44 void FileFilterHelper::SetFileFilterPath(const String& szFileFilterPath)
45 {
46         // Use none as default path
47         m_sFileFilterPath.clear();
48
49         if (m_fileFilterMgr == nullptr)
50                 return;
51
52         // Don't bother to lookup empty path
53         if (!szFileFilterPath.empty())
54         {
55                 m_currentFilter = m_fileFilterMgr->GetFilterByPath(szFileFilterPath);
56                 if (m_currentFilter != nullptr)
57                         m_sFileFilterPath = szFileFilterPath;
58         }
59 }
60
61 /**
62  * @brief Get list of filters currently available.
63  *
64  * @param [out] selected Filepath of currently selected filter.
65  * @return Filter list to receive found filters.
66  */
67 std::vector<FileFilterInfo> FileFilterHelper::GetFileFilters(String & selected) const
68 {
69         std::vector<FileFilterInfo> filters;
70         if (m_fileFilterMgr != nullptr)
71         {
72                 const int count = m_fileFilterMgr->GetFilterCount();
73                 filters.reserve(count);
74                 for (int i = 0; i < count; ++i)
75                 {
76                         FileFilterInfo filter;
77                         filter.fullpath = m_fileFilterMgr->GetFilterPath(i);
78                         filter.name = m_fileFilterMgr->GetFilterName(i);
79                         filter.description = m_fileFilterMgr->GetFilterDesc(i);
80                         filters.push_back(filter);
81                 }
82         }
83         selected = m_sFileFilterPath;
84         return filters;
85 }
86
87 /**
88  * @brief Return name of filter in given file.
89  * If no filter cannot be found, return empty string.
90  * @param [in] filterPath Path to filterfile.
91  * @sa FileFilterHelper::GetFileFilterPath()
92  */
93 String FileFilterHelper::GetFileFilterName(const String& filterPath) const
94 {
95         String selected;
96         String name;
97         vector<FileFilterInfo> filters = GetFileFilters(selected);
98         vector<FileFilterInfo>::const_iterator iter = filters.begin();
99         while (iter != filters.end())
100         {
101                 if ((*iter).fullpath == filterPath)
102                 {
103                         name = (*iter).name;
104                         break;
105                 }
106                 ++iter;
107         }
108         return name;
109 }
110
111 /** 
112  * @brief Return path to filter with given name.
113  * @param [in] filterName Name of filter.
114  * @sa FileFilterHelper::GetFileFilterName()
115  */
116 String FileFilterHelper::GetFileFilterPath(const String& filterName) const
117 {
118         String selected;
119         String path;
120         vector<FileFilterInfo> filters = GetFileFilters(selected);
121         vector<FileFilterInfo>::const_iterator iter = filters.begin();
122         while (iter != filters.end())
123         {
124                 if ((*iter).name == filterName)
125                 {
126                         path = (*iter).fullpath;
127                         break;
128                 }
129                 ++iter;
130         }
131         return path;
132 }
133
134 /** 
135  * @brief Set User's filter folder.
136  * @param [in] filterPath Location of User's filters.
137  */
138 void FileFilterHelper::SetUserFilterPath(const String & filterPath)
139 {
140         m_sUserSelFilterPath = filterPath;
141         paths::normalize(m_sUserSelFilterPath);
142 }
143
144 /** 
145  * @brief Select between mask and filterfile.
146  * @param [in] bUseMask If true we use mask instead of filter files.
147  */
148 void FileFilterHelper::UseMask(bool bUseMask)
149 {
150         m_bUseMask = bUseMask;
151         if (m_bUseMask)
152         {
153                 if (m_pMaskFileFilter == nullptr)
154                 {
155                         m_pMaskFileFilter.reset(new FilterList);
156                 }
157                 if (m_pMaskDirFilter == nullptr)
158                 {
159                         m_pMaskDirFilter.reset(new FilterList);
160                 }
161         }
162         else
163         {
164                 m_pMaskFileFilter.reset();
165                 m_pMaskDirFilter.reset();
166         }
167 }
168
169 /** 
170  * @brief Set filemask for filtering.
171  * @param [in] strMask Mask to set (e.g. *.cpp;*.h).
172  */
173 void FileFilterHelper::SetMask(const String& strMask)
174 {
175         if (!m_bUseMask)
176         {
177                 throw "Filter mask tried to set when masks disabled!";
178         }
179         m_sMask = strMask;
180         auto [regExpFile, regExpFileExclude, regExpDir, regExpDirExclude] = ParseExtensions(strMask);
181
182         std::string regexp_str_file = ucr::toUTF8(regExpFile);
183         std::string regexp_str_file_excluded = ucr::toUTF8(regExpFileExclude);
184         std::string regexp_str_dir = ucr::toUTF8(regExpDir);
185         std::string regexp_str_dir_excluded = ucr::toUTF8(regExpDirExclude);
186
187         m_pMaskFileFilter->RemoveAllFilters();
188         m_pMaskFileFilter->AddRegExp(regexp_str_file, false);
189         if (!regexp_str_file_excluded.empty())
190                 m_pMaskFileFilter->AddRegExp(regexp_str_file_excluded, true);
191         m_pMaskDirFilter->RemoveAllFilters();
192         m_pMaskDirFilter->AddRegExp(regexp_str_dir, false);
193         if (!regexp_str_dir_excluded.empty())
194                 m_pMaskDirFilter->AddRegExp(regexp_str_dir_excluded, true);
195 }
196
197 /**
198  * @brief Check if any of filefilter rules match to filename.
199  *
200  * @param [in] szFileName Filename to test.
201  * @return true unless we're suppressing this file by filter
202  */
203 bool FileFilterHelper::includeFile(const String& szFileName) const
204 {
205         if (m_bUseMask)
206         {
207                 if (m_pMaskFileFilter == nullptr)
208                 {
209                         throw "Use mask set, but no filter rules for mask!";
210                 }
211
212                 // preprend a backslash if there is none
213                 String strFileName = strutils::makelower(szFileName);
214                 if (strFileName.empty() || strFileName[0] != '\\')
215                         strFileName = _T("\\") + strFileName;
216                 // append a point if there is no extension
217                 if (strFileName.find('.') == String::npos)
218                         strFileName = strFileName + _T(".");
219
220                 return m_pMaskFileFilter->Match(ucr::toUTF8(strFileName));
221         }
222         else
223         {
224                 if (m_fileFilterMgr == nullptr || m_currentFilter ==nullptr)
225                         return true;
226                 return m_fileFilterMgr->TestFileNameAgainstFilter(m_currentFilter, szFileName);
227         }
228 }
229
230 /**
231  * @brief Check if any of filefilter rules match to directoryname.
232  *
233  * @param [in] szFileName Directoryname to test.
234  * @return true unless we're suppressing this directory by filter
235  */
236 bool FileFilterHelper::includeDir(const String& szDirName) const
237 {
238         if (m_bUseMask)
239         {
240                 if (m_pMaskDirFilter == nullptr)
241                 {
242                         throw "Use mask set, but no filter rules for mask!";
243                 }
244
245                 // preprend a backslash if there is none
246                 String strDirName = strutils::makelower(szDirName);
247                 if (strDirName.empty() || strDirName[0] != '\\')
248                         strDirName = _T("\\") + strDirName;
249                 // append a point if there is no extension
250                 if (strDirName.find('.') == String::npos)
251                         strDirName = strDirName + _T(".");
252
253                 return m_pMaskDirFilter->Match(ucr::toUTF8(strDirName));
254         }
255         else
256         {
257                 if (m_fileFilterMgr == nullptr || m_currentFilter == nullptr)
258                         return true;
259
260                 // Add a backslash
261                 String strDirName(_T("\\"));
262                 strDirName += szDirName;
263
264                 return m_fileFilterMgr->TestDirNameAgainstFilter(m_currentFilter, strDirName);
265         }
266 }
267
268 /**
269  * @brief Load in all filters in a folder.
270  * @param [in] dir Folder from where to load filters.
271  * @param [in] sPattern Wildcard defining files to add to map as filter files.
272  *   It is filemask, for example, "*.flt"
273  */
274 void FileFilterHelper::LoadFileFilterDirPattern(const String& dir, const String& szPattern)
275 {
276         m_fileFilterMgr->LoadFromDirectory(dir, szPattern, FileFilterExt);
277 }
278
279 static String ConvertWildcardPatternToRegexp(const String& pattern)
280 {
281         String strRegex;
282         for (const TCHAR *p = pattern.c_str(); *p; ++p)
283         {
284                 switch (*p)
285                 {
286                 case '\\': strRegex += _T("\\\\");     break;
287                 case '.':  strRegex += _T("\\.");      break;
288                 case '[':  strRegex += _T("\\[");      break;
289                 case ']':  strRegex += _T("\\]");      break;
290                 case '(':  strRegex += _T("\\(");      break;
291                 case ')':  strRegex += _T("\\)");      break;
292                 case '$':  strRegex += _T("\\$");      break;
293                 case '^':  strRegex += _T("\\^");      break;
294                 case '+':  strRegex += _T("\\+");      break;
295                 case '?':  strRegex += _T("[^\\\\]");  break;
296                 case '*':  strRegex += _T("[^\\\\]*"); break;
297                 default:   strRegex += *p;             break;
298                 }
299         }
300         strRegex += _T("$");
301         return _T("(^|\\\\)") + strRegex;
302 }
303
304 /** 
305  * @brief Convert user-given extension list to valid regular expression.
306  * @param [in] Extension list/mask to convert to regular expression.
307  * @return Regular expression that matches extension list.
308  */
309 std::tuple<String, String, String, String> FileFilterHelper::ParseExtensions(const String &extensions) const
310 {
311         String strFileParsed;
312         String strDirParsed;
313         std::vector<String> filePatterns;
314         std::vector<String> filePatternsExclude;
315         std::vector<String> dirPatterns;
316         std::vector<String> dirPatternsExclude;
317         String ext(extensions);
318         static const TCHAR pszSeps[] = _T(" ;|,:");
319
320         ext += _T(";"); // Add one separator char to end
321         size_t pos = ext.find_first_of(pszSeps);
322         
323         while (pos != String::npos)
324         {
325                 String token = ext.substr(0, pos); // Get first extension
326                 ext = ext.substr(pos + 1); // Remove extension + separator
327                 
328                 // Only "*." or "*.something" allowed, other ignored
329                 if (token.length() >= 1)
330                 {
331                         bool exclude = token[0] == '!';
332                         if (exclude)
333                                 token = token.substr(1);
334                         bool isdir = token.back() == '\\';
335                         if (isdir)
336                                 token = token.substr(0, token.size() - 1);
337                         if (token.find('.') == String::npos && !token.empty() && token.back() != '*')
338                                 token += _T(".");
339                         String strRegex = strutils::makelower(ConvertWildcardPatternToRegexp(token));
340                         if (exclude)
341                         {
342                                 if (isdir)
343                                         dirPatternsExclude.push_back(strRegex);
344                                 else
345                                         filePatternsExclude.push_back(strRegex);
346                         }
347                         else
348                         {
349                                 if (isdir)
350                                         dirPatterns.push_back(strRegex);
351                                 else
352                                         filePatterns.push_back(strRegex);
353                         }
354                 }
355
356                 pos = ext.find_first_of(pszSeps); 
357         }
358
359         if (filePatterns.empty())
360                 strFileParsed = _T(".*"); // Match everything
361         else
362                 strFileParsed = strutils::join(filePatterns.begin(), filePatterns.end(), _T("|"));
363         if (dirPatterns.empty())
364                 strDirParsed = _T(".*"); // Match everything
365         else
366                 strDirParsed = strutils::join(dirPatterns.begin(), dirPatterns.end(), _T("|"));
367         String strFileParsedExclude = strutils::join(filePatternsExclude.begin(), filePatternsExclude.end(), _T("|"));
368         String strDirParsedExclude = strutils::join(dirPatternsExclude.begin(), dirPatternsExclude.end(), _T("|"));
369         return { strFileParsed, strFileParsedExclude, strDirParsed, strDirParsedExclude };
370 }
371
372 /** 
373  * @brief Returns active filter (or mask string)
374  * @return The active filter.
375  */
376 String FileFilterHelper::GetFilterNameOrMask() const
377 {
378         String sFilter;
379
380         if (!IsUsingMask())
381                 sFilter = GetFileFilterName(m_sFileFilterPath);
382         else
383                 sFilter = m_sMask;
384
385         return sFilter;
386 }
387
388 /** 
389  * @brief Set filter.
390  *
391  * Simple-to-use function to select filter. This function determines
392  * filter type so caller doesn't need to care about it.
393  *
394  * @param [in] filter File mask or filter name.
395  * @return true if given filter was set, false if default filter was set.
396  * @note If function returns false, you should ask filter set with
397  * GetFilterNameOrMask().
398  */
399 bool FileFilterHelper::SetFilter(const String &filter)
400 {
401         // If filter is empty string set default filter
402         if (filter.empty())
403         {
404                 UseMask(true);
405                 SetMask(_T("*.*"));
406                 SetFileFilterPath(_T(""));
407                 return false;
408         }
409
410         // Remove leading and trailing whitespace characters from the string.
411         String flt = strutils::trim_ws(filter);
412
413         String path = GetFileFilterPath(flt);
414         if (!path.empty())
415         {
416                 UseMask(false);
417                 SetFileFilterPath(path);
418         }
419         else
420         {
421                 UseMask(true);
422                 SetMask(flt);
423                 SetFileFilterPath(_T(""));
424                 return false;
425         }
426         return true;
427 }
428
429 /** 
430  * @brief Reloads changed filter files
431  *
432  * Checks if filter file has been modified since it was last time
433  * loaded/reloaded. If file has been modified we reload it.
434  * @todo How to handle an error in reloading filter?
435  */
436 void FileFilterHelper::ReloadUpdatedFilters()
437 {
438         DirItem fileInfo;
439         String selected;
440         vector<FileFilterInfo> filters = GetFileFilters(selected);
441         vector<FileFilterInfo>::const_iterator iter = filters.begin();
442         while (iter != filters.end())
443         {
444                 String path = (*iter).fullpath;
445
446                 fileInfo.Update(path);
447                 if (fileInfo.mtime != (*iter).fileinfo.mtime ||
448                         fileInfo.size != (*iter).fileinfo.size)
449                 {
450                         // Reload filter after changing it
451                         int retval = m_fileFilterMgr->ReloadFilterFromDisk(path);
452                         
453                         if (retval == FILTER_OK)
454                         {
455                                 // If it was active filter we have to re-set it
456                                 if (path == selected)
457                                         SetFileFilterPath(path);
458                         }
459                 }
460                 ++iter;
461         }
462 }
463
464 /**
465  * @brief Load any known file filters
466  * @todo Preserve filter selection? How?
467  */
468 void FileFilterHelper::LoadAllFileFilters()
469 {
470         // First delete existing filters
471         m_fileFilterMgr->DeleteAllFilters();
472
473         // Program application directory
474         m_sGlobalFilterPath = paths::ConcatPath(env::GetProgPath(), _T("Filters"));
475         paths::normalize(m_sGlobalFilterPath);
476         String pattern(_T("*"));
477         pattern += FileFilterExt;
478         LoadFileFilterDirPattern(m_sGlobalFilterPath, pattern);
479         if (strutils::compare_nocase(m_sGlobalFilterPath, m_sUserSelFilterPath) != 0)
480                 LoadFileFilterDirPattern(m_sUserSelFilterPath, pattern);
481 }
482
483 /**
484  * @brief Return path to global filters (& create if needed), or empty if cannot create
485  */
486 String FileFilterHelper::GetGlobalFilterPathWithCreate() const
487 {
488         return paths::EnsurePathExist(m_sGlobalFilterPath);
489 }
490
491 /**
492  * @brief Return path to user filters (& create if needed), or empty if cannot create
493  */
494 String FileFilterHelper::GetUserFilterPathWithCreate() const
495 {
496         return paths::EnsurePathExist(m_sUserSelFilterPath);
497 }
498
499 /**
500  * @brief Clone file filter helper from another file filter helper.
501  * This function clones file filter helper from another file filter helper.
502  * Current contents in the file filter helper are removed and new contents added from the given file filter helper.
503  * @param [in] pHelper File filter helper to clone.
504  */
505 void FileFilterHelper::CloneFrom(const FileFilterHelper* pHelper)
506 {
507         if (!pHelper)
508                 return;
509
510         if (pHelper->m_pMaskFileFilter)
511         {
512                 std::unique_ptr<FilterList> filterList(new FilterList());
513                 m_pMaskFileFilter = std::move(filterList);
514                 m_pMaskFileFilter->CloneFrom(pHelper->m_pMaskFileFilter.get());
515         }
516
517         if (pHelper->m_pMaskDirFilter)
518         {
519                 std::unique_ptr<FilterList> filterList(new FilterList());
520                 m_pMaskDirFilter = std::move(filterList);
521                 m_pMaskDirFilter->CloneFrom(pHelper->m_pMaskDirFilter.get());
522         }
523
524         if (pHelper->m_fileFilterMgr)
525         {
526                 std::unique_ptr<FileFilterMgr> fileFilterMgr(new FileFilterMgr());
527                 m_fileFilterMgr = std::move(fileFilterMgr);
528                 m_fileFilterMgr->CloneFrom(pHelper->m_fileFilterMgr.get());
529         }
530
531         m_currentFilter = nullptr;
532         if (pHelper->m_currentFilter && pHelper->m_fileFilterMgr)
533         {
534                 int count = pHelper->m_fileFilterMgr->GetFilterCount();
535                 for (int i = 0; i < count; i++)
536                         if (pHelper->m_fileFilterMgr->GetFilterByIndex(i) == pHelper->m_currentFilter)
537                         {
538                                 m_currentFilter = m_fileFilterMgr->GetFilterByIndex(i);
539                                 break;
540                         }
541         }
542
543         m_sFileFilterPath = pHelper->m_sFileFilterPath;
544         m_sMask = pHelper->m_sMask;
545         m_bUseMask = pHelper->m_bUseMask;
546         m_sGlobalFilterPath = pHelper->m_sGlobalFilterPath;
547         m_sUserSelFilterPath = pHelper->m_sUserSelFilterPath;
548 }