This allows filtering a hierarchy of folders with path patterns like "parent/sub" which matches all folders matching "sub" with parent folders matching "parent". Funded by: Intevation GmbH BUG: 443791 FIXED-IN: 5.19.0wilder-portage
parent
a37cf79cc0
commit
dc60839a85
7 changed files with 267 additions and 9 deletions
@ -0,0 +1,103 @@ |
|||||||
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2021 Intevation GmbH |
||||||
|
SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de> |
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later |
||||||
|
*/ |
||||||
|
|
||||||
|
#include "hierarchicalfoldermatcher_p.h" |
||||||
|
|
||||||
|
#include <QAbstractItemModel> |
||||||
|
#include <QModelIndex> |
||||||
|
#include <QRegularExpression> |
||||||
|
|
||||||
|
namespace MailCommon |
||||||
|
{ |
||||||
|
|
||||||
|
HierarchicalFolderMatcher::HierarchicalFolderMatcher() |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
bool HierarchicalFolderMatcher::isNull() |
||||||
|
{ |
||||||
|
return filterRegExps.empty(); |
||||||
|
} |
||||||
|
|
||||||
|
void HierarchicalFolderMatcher::setFilter(const QString &filter, Qt::CaseSensitivity caseSensitivity) |
||||||
|
{ |
||||||
|
filterRegExps.clear(); |
||||||
|
if (filter.isEmpty()) { |
||||||
|
return; |
||||||
|
} |
||||||
|
const auto patternOptions = caseSensitivity == Qt::CaseInsensitive ? |
||||||
|
QRegularExpression::CaseInsensitiveOption : |
||||||
|
QRegularExpression::NoPatternOption; |
||||||
|
const auto parts = filter.split(QLatin1Char('/')); |
||||||
|
std::transform(std::begin(parts), std::end(parts), |
||||||
|
std::back_inserter(filterRegExps), |
||||||
|
[patternOptions](const auto &part) { |
||||||
|
// QRegularExpression::wildcardToRegularExpression() returns a fully anchored
|
||||||
|
// regular expression, but we want to check for substring matches; wrap
|
||||||
|
// the user's filter part into '*' to fix this
|
||||||
|
return QRegularExpression{QRegularExpression::wildcardToRegularExpression( |
||||||
|
QLatin1Char('*') + part + QLatin1Char('*')), patternOptions}; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
bool HierarchicalFolderMatcher::matches(const QAbstractItemModel *model, const QModelIndex &start, int role) |
||||||
|
{ |
||||||
|
if (!start.isValid()) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
const auto filterKeyColumn = start.column(); |
||||||
|
QModelIndex idx = start; |
||||||
|
for (auto it = filterRegExps.crbegin(); it != filterRegExps.crend(); ++it) { |
||||||
|
if (!idx.isValid()) { |
||||||
|
// we have exceeded the model root or the column does not exist
|
||||||
|
return false; |
||||||
|
} |
||||||
|
const QString key = model->data(idx, role).toString(); |
||||||
|
if (!it->match(key).hasMatch()) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
idx = idx.parent().siblingAtColumn(filterKeyColumn); |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
QModelIndex HierarchicalFolderMatcher::findFirstMatch(const QAbstractItemModel *model, const QModelIndex &start, int role) |
||||||
|
{ |
||||||
|
// inspired by QAbstractItemModel::match(), but using our own matching
|
||||||
|
QModelIndex result; |
||||||
|
|
||||||
|
const int filterKeyColumn = start.column(); |
||||||
|
const QModelIndex p = model->parent(start); |
||||||
|
int from = start.row(); |
||||||
|
int to = model->rowCount(p); |
||||||
|
|
||||||
|
// iterate twice (first from start row to last row; then from first row to before start row)
|
||||||
|
for (int i = 0; (i < 2) && !result.isValid(); ++i) { |
||||||
|
for (int row = from; (row < to) && !result.isValid(); ++row) { |
||||||
|
QModelIndex idx = model->index(row, filterKeyColumn, p); |
||||||
|
if (!idx.isValid()) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
if (matches(model, idx, role)) { |
||||||
|
result = idx; |
||||||
|
break; |
||||||
|
} |
||||||
|
const auto idxAsParent = filterKeyColumn != 0 ? idx.siblingAtColumn(0) : idx; |
||||||
|
if (model->hasChildren(idxAsParent)) { |
||||||
|
result = findFirstMatch(model, model->index(0, filterKeyColumn, idxAsParent), role); |
||||||
|
} |
||||||
|
} |
||||||
|
// prepare for the next iteration
|
||||||
|
from = 0; |
||||||
|
to = start.row(); |
||||||
|
} |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,37 @@ |
|||||||
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2021 Intevation GmbH |
||||||
|
SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de> |
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later |
||||||
|
*/ |
||||||
|
|
||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <Qt> |
||||||
|
|
||||||
|
#include <vector> |
||||||
|
|
||||||
|
class QAbstractItemModel; |
||||||
|
class QModelIndex; |
||||||
|
class QRegularExpression; |
||||||
|
class QString; |
||||||
|
|
||||||
|
namespace MailCommon |
||||||
|
{ |
||||||
|
class HierarchicalFolderMatcher |
||||||
|
{ |
||||||
|
public: |
||||||
|
HierarchicalFolderMatcher(); |
||||||
|
|
||||||
|
bool isNull(); |
||||||
|
|
||||||
|
void setFilter(const QString &filter, Qt::CaseSensitivity caseSensitivity); |
||||||
|
|
||||||
|
bool matches(const QAbstractItemModel *model, const QModelIndex &start, int role = Qt::DisplayRole); |
||||||
|
|
||||||
|
QModelIndex findFirstMatch(const QAbstractItemModel *model, const QModelIndex &start, int role = Qt::DisplayRole); |
||||||
|
|
||||||
|
private: |
||||||
|
std::vector<QRegularExpression> filterRegExps; |
||||||
|
}; |
||||||
|
} |
||||||
Loading…
Reference in new issue