File indexing completed on 2024-05-26 05:25:13

0001 /*
0002   SPDX-FileCopyrightText: 2021 Intevation GmbH
0003   SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
0004 
0005   SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "hierarchicalfoldermatcher_p.h"
0009 
0010 #include <QAbstractItemModel>
0011 #include <QModelIndex>
0012 #include <QRegularExpression>
0013 
0014 using namespace MailCommon;
0015 HierarchicalFolderMatcher::HierarchicalFolderMatcher() = default;
0016 
0017 bool HierarchicalFolderMatcher::isNull() const
0018 {
0019     return filterRegExps.empty();
0020 }
0021 
0022 void HierarchicalFolderMatcher::setFilter(const QString &filter, Qt::CaseSensitivity caseSensitivity)
0023 {
0024     filterRegExps.clear();
0025     if (filter.isEmpty()) {
0026         return;
0027     }
0028     const auto patternOptions = caseSensitivity == Qt::CaseInsensitive ? QRegularExpression::CaseInsensitiveOption : QRegularExpression::NoPatternOption;
0029     const auto parts = filter.split(QLatin1Char('/'));
0030     std::transform(std::begin(parts), std::end(parts), std::back_inserter(filterRegExps), [patternOptions](const auto &part) {
0031         // QRegularExpression::wildcardToRegularExpression() returns a fully anchored
0032         // regular expression, but we want to check for substring matches; wrap
0033         // the user's filter part into '*' to fix this
0034         return QRegularExpression{QRegularExpression::wildcardToRegularExpression(QLatin1Char('*') + part + QLatin1Char('*')), patternOptions};
0035     });
0036 }
0037 
0038 bool HierarchicalFolderMatcher::matches(const QAbstractItemModel *model, const QModelIndex &start, int role)
0039 {
0040     if (!start.isValid()) {
0041         return false;
0042     }
0043 
0044     const auto filterKeyColumn = start.column();
0045     QModelIndex idx = start;
0046     for (auto it = filterRegExps.crbegin(); it != filterRegExps.crend(); ++it) {
0047         if (!idx.isValid()) {
0048             // we have exceeded the model root or the column does not exist
0049             return false;
0050         }
0051         const QString key = model->data(idx, role).toString();
0052         if (!it->match(key).hasMatch()) {
0053             return false;
0054         }
0055         idx = idx.parent().siblingAtColumn(filterKeyColumn);
0056     }
0057     return true;
0058 }
0059 
0060 QModelIndex HierarchicalFolderMatcher::findFirstMatch(const QAbstractItemModel *model, const QModelIndex &start, int role)
0061 {
0062     // inspired by QAbstractItemModel::match(), but using our own matching
0063     QModelIndex result;
0064 
0065     const int filterKeyColumn = start.column();
0066     const QModelIndex p = model->parent(start);
0067     int from = start.row();
0068     int to = model->rowCount(p);
0069 
0070     // iterate twice (first from start row to last row; then from first row to before start row)
0071     for (int i = 0; (i < 2) && !result.isValid(); ++i) {
0072         for (int row = from; (row < to) && !result.isValid(); ++row) {
0073             QModelIndex idx = model->index(row, filterKeyColumn, p);
0074             if (!idx.isValid()) {
0075                 continue;
0076             }
0077             if (matches(model, idx, role)) {
0078                 result = idx;
0079                 break;
0080             }
0081             const auto idxAsParent = filterKeyColumn != 0 ? idx.siblingAtColumn(0) : idx;
0082             if (model->hasChildren(idxAsParent)) {
0083                 result = findFirstMatch(model, model->index(0, filterKeyColumn, idxAsParent), role);
0084             }
0085         }
0086         // prepare for the next iteration
0087         from = 0;
0088         to = start.row();
0089     }
0090 
0091     return result;
0092 }