File indexing completed on 2024-04-28 05:34:17

0001 // SPDX-FileCopyrightText: 2018 Daniel Vrátil <dvratil@kde.org>
0002 //
0003 // SPDX-License-Identifier: LGPL-2.1-or-later
0004 
0005 #include "passwordfiltermodel.h"
0006 #include "abbreviations.h"
0007 #include "passwordsmodel.h"
0008 
0009 #include <KDescendantsProxyModel>
0010 
0011 #include <QFutureWatcher>
0012 #include <QtConcurrent>
0013 
0014 #include <chrono>
0015 #include <iterator>
0016 
0017 using namespace PlasmaPass;
0018 
0019 namespace
0020 {
0021 constexpr const auto invalidateDelay = std::chrono::milliseconds(100);
0022 constexpr const char *newFilterProperty = "newFilter";
0023 
0024 class ModelIterator
0025 {
0026 public:
0027     using reference = const QModelIndex &;
0028     using pointer = QModelIndex *;
0029     using value_type = QModelIndex;
0030     using difference_type = int;
0031     using iterator_category = std::forward_iterator_tag;
0032 
0033     static ModelIterator begin(QAbstractItemModel *model)
0034     {
0035         return ModelIterator{model, model->index(0, 0)};
0036     }
0037 
0038     static ModelIterator end(QAbstractItemModel *model)
0039     {
0040         return ModelIterator{model, {}};
0041     }
0042 
0043     bool operator==(const ModelIterator &other) const
0044     {
0045         return mModel == other.mModel && mIndex == other.mIndex;
0046     }
0047 
0048     bool operator!=(const ModelIterator &other) const
0049     {
0050         return !(*this == other);
0051     }
0052 
0053     QModelIndex operator*() const
0054     {
0055         return mIndex;
0056     }
0057 
0058     const QModelIndex *operator->() const
0059     {
0060         return &mIndex;
0061     }
0062 
0063     ModelIterator &operator++()
0064     {
0065         if (mIndex.row() < mModel->rowCount() - 1) {
0066             mIndex = mModel->index(mIndex.row() + 1, mIndex.column());
0067         } else {
0068             mIndex = {};
0069         }
0070         return *this;
0071     }
0072 
0073     ModelIterator operator++(int)
0074     {
0075         ModelIterator it = *this;
0076         ++*this;
0077         return it;
0078     }
0079 
0080 private:
0081     ModelIterator(QAbstractItemModel *model, const QModelIndex &index)
0082         : mModel(model)
0083         , mIndex(index)
0084     {
0085     }
0086 
0087 private:
0088     QAbstractItemModel *mModel = nullptr;
0089     QModelIndex mIndex;
0090 };
0091 
0092 } // namespace
0093 
0094 PasswordFilterModel::PathFilter::PathFilter(QString filter)
0095     : filter(std::move(filter))
0096 {
0097 }
0098 
0099 PasswordFilterModel::PathFilter::PathFilter(const PathFilter &other)
0100     : filter(other.filter)
0101 {
0102     updateParts();
0103 }
0104 
0105 PasswordFilterModel::PathFilter &PasswordFilterModel::PathFilter::operator=(const PathFilter &other)
0106 {
0107     filter = other.filter;
0108     updateParts();
0109     return *this;
0110 }
0111 
0112 PasswordFilterModel::PathFilter::PathFilter(PathFilter &&other) noexcept
0113     : filter(std::move(other.filter))
0114 {
0115     updateParts();
0116 }
0117 
0118 PasswordFilterModel::PathFilter &PasswordFilterModel::PathFilter::operator=(PathFilter &&other) noexcept
0119 {
0120     filter = std::move(other.filter);
0121     updateParts();
0122     return *this;
0123 }
0124 
0125 void PasswordFilterModel::PathFilter::updateParts()
0126 {
0127 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0128     mParts = filter.splitRef(QLatin1Char('/'), Qt::SkipEmptyParts);
0129 #else
0130     mParts = QStringView(filter).split(QLatin1Char('/'), Qt::SkipEmptyParts);
0131 #endif
0132 }
0133 
0134 PasswordFilterModel::PathFilter::result_type PasswordFilterModel::PathFilter::operator()(const QModelIndex &index) const
0135 {
0136     const auto path = index.model()->data(index, PasswordsModel::FullNameRole).toString();
0137 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0138     const auto weight = matchPathFilter(path.splitRef(QLatin1Char('/')), mParts);
0139 #else
0140     const auto weight = matchPathFilter(QStringView(path).split(QLatin1Char('/')), mParts);
0141 #endif
0142     return std::make_pair(index, weight);
0143 }
0144 
0145 PasswordFilterModel::PasswordFilterModel(QObject *parent)
0146     : QSortFilterProxyModel(parent)
0147     , mFlatModel(new KDescendantsProxyModel(this))
0148 {
0149     mFlatModel->setDisplayAncestorData(false);
0150     sort(0); // enable sorting
0151 
0152     mUpdateTimer.setSingleShot(true);
0153     connect(&mUpdateTimer, &QTimer::timeout, this, &PasswordFilterModel::delayedUpdateFilter);
0154     connect(&mUpdateTimer, &QTimer::timeout, this, []() {
0155         qDebug() << "Update timer timeout, will calculate results lazily.";
0156     });
0157 }
0158 
0159 void PasswordFilterModel::setSourceModel(QAbstractItemModel *sourceModel)
0160 {
0161     mFlatModel->setSourceModel(sourceModel);
0162 
0163     if (this->sourceModel() == nullptr) {
0164         QSortFilterProxyModel::setSourceModel(mFlatModel);
0165     }
0166 }
0167 
0168 QString PasswordFilterModel::passwordFilter() const
0169 {
0170     return mFilter.filter;
0171 }
0172 
0173 void PasswordFilterModel::setPasswordFilter(const QString &filter)
0174 {
0175     if (mFilter.filter != filter) {
0176         if (mUpdateTimer.isActive()) {
0177             mUpdateTimer.stop();
0178         }
0179 
0180         mUpdateTimer.setProperty(newFilterProperty, filter);
0181         mUpdateTimer.start(invalidateDelay);
0182 
0183         if (mFuture.isRunning()) {
0184             mFuture.cancel();
0185         }
0186         if (!filter.isEmpty()) {
0187             mFuture = QtConcurrent::mappedReduced<QHash<QModelIndex, int>>(ModelIterator::begin(sourceModel()),
0188                                                                            ModelIterator::end(sourceModel()),
0189                                                                            PathFilter{filter},
0190                                                                            [](QHash<QModelIndex, int> &result, const std::pair<QModelIndex, int> &value) {
0191                                                                                result.insert(value.first, value.second);
0192                                                                            });
0193             auto watcher = new QFutureWatcher<QHash<QModelIndex, int>>();
0194             connect(watcher, &QFutureWatcherBase::finished, this, [this, watcher]() {
0195                 mSortingLookup = mFuture.result();
0196                 watcher->deleteLater();
0197                 // If the timer is not active it means we were to slow, so don't invoke
0198                 // delayedUpdateFilter() again, just update mSortingLookup with our
0199                 // results.
0200                 if (mUpdateTimer.isActive()) {
0201                     mUpdateTimer.stop();
0202                     delayedUpdateFilter();
0203                 }
0204             });
0205             connect(watcher, &QFutureWatcherBase::canceled, watcher, &QObject::deleteLater);
0206             watcher->setFuture(mFuture);
0207         }
0208     }
0209 }
0210 
0211 void PasswordFilterModel::delayedUpdateFilter()
0212 {
0213     mFilter = PathFilter(mUpdateTimer.property(newFilterProperty).toString());
0214     Q_EMIT passwordFilterChanged();
0215     if (mFuture.isRunning()) {
0216         // If the future is still running, clear up the lookup table, we will have to
0217         // calculate the intermediate results ourselves
0218         mSortingLookup.clear();
0219     }
0220     invalidate();
0221 }
0222 
0223 QVariant PasswordFilterModel::data(const QModelIndex &index, int role) const
0224 {
0225     if (role == Qt::DisplayRole) {
0226         return data(index, PasswordsModel::FullNameRole);
0227     }
0228 
0229     return QSortFilterProxyModel::data(index, role);
0230 }
0231 
0232 bool PasswordFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
0233 {
0234     const auto src_index = sourceModel()->index(source_row, 0, source_parent);
0235     const auto type = static_cast<PasswordsModel::EntryType>(sourceModel()->data(src_index, PasswordsModel::EntryTypeRole).toInt());
0236     if (type == PasswordsModel::FolderEntry) {
0237         return false;
0238     }
0239 
0240     if (mFilter.filter.isEmpty()) {
0241         return true;
0242     }
0243 
0244     // Try to lookup the weight in the lookup table, the worker thread may have put it in there
0245     // while the updateTimer was ticking
0246     auto weight = mSortingLookup.find(src_index);
0247     if (weight == mSortingLookup.end()) {
0248         // It's not there, let's calculate the value now
0249         const auto result = mFilter(src_index);
0250         weight = mSortingLookup.insert(result.first, result.second);
0251     }
0252 
0253     return *weight > -1;
0254 }
0255 
0256 bool PasswordFilterModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
0257 {
0258     const auto weightLeft = mSortingLookup.value(source_left, -1);
0259     const auto weightRight = mSortingLookup.value(source_right, -1);
0260 
0261     if (weightLeft == weightRight) {
0262         const auto nameLeft = source_left.data(PasswordsModel::FullNameRole).toString();
0263         const auto nameRight = source_right.data(PasswordsModel::FullNameRole).toString();
0264         return QString::localeAwareCompare(nameLeft, nameRight) < 0;
0265     }
0266 
0267     return weightLeft < weightRight;
0268 }
0269 
0270 #include "moc_passwordfiltermodel.cpp"