File indexing completed on 2024-07-21 13:10:33

0001 /*
0002  * This file is part of the KDE Milou Project
0003  * SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@broulik.de>
0004  *
0005  * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0006  *
0007  */
0008 
0009 #include "resultsmodel.h"
0010 
0011 #include "runnerresultsmodel.h"
0012 
0013 #include <QIdentityProxyModel>
0014 
0015 #include <KDescendantsProxyModel>
0016 #include <KModelIndexProxyMapper>
0017 
0018 #include <KRunner/AbstractRunner>
0019 #include <cmath>
0020 
0021 using namespace Milou;
0022 
0023 /**
0024  * Sorts the matches and categories by their type and relevance
0025  *
0026  * A category gets type and relevance of the highest
0027  * scoring match within.
0028  */
0029 class SortProxyModel : public QSortFilterProxyModel
0030 {
0031     Q_OBJECT
0032 
0033 public:
0034     SortProxyModel(QObject *parent)
0035         : QSortFilterProxyModel(parent)
0036     {
0037         setDynamicSortFilter(true);
0038         sort(0, Qt::DescendingOrder);
0039     }
0040     ~SortProxyModel() override = default;
0041 
0042     void setQueryString(const QString &queryString)
0043     {
0044         const QStringList words = queryString.split(QLatin1Char(' '), Qt::SkipEmptyParts);
0045         if (m_words != words) {
0046             m_words = words;
0047             invalidate();
0048         }
0049     }
0050 
0051 protected:
0052     bool lessThan(const QModelIndex &sourceA, const QModelIndex &sourceB) const override
0053     {
0054         const int typeA = sourceA.data(ResultsModel::TypeRole).toInt();
0055         const int typeB = sourceB.data(ResultsModel::TypeRole).toInt();
0056 
0057         if (typeA != typeB) {
0058             return typeA < typeB;
0059         }
0060 
0061         const qreal relevanceA = sourceA.data(ResultsModel::RelevanceRole).toReal();
0062         const qreal relevanceB = sourceB.data(ResultsModel::RelevanceRole).toReal();
0063 
0064         if (!qFuzzyCompare(relevanceA, relevanceB)) {
0065             return relevanceA < relevanceB;
0066         }
0067 
0068         return QSortFilterProxyModel::lessThan(sourceA, sourceB);
0069     }
0070 
0071 private:
0072     QStringList m_words;
0073 };
0074 
0075 /**
0076  * Distributes the number of matches shown per category
0077  *
0078  * Each category may occupy a maximum of 1/(n+1) of the given @c limit,
0079  * this means the further down you get, the less matches there are.
0080  * There is at least one match shown per category.
0081  *
0082  * This model assumes the results to already be sorted
0083  * descending by their relevance/score.
0084  */
0085 class CategoryDistributionProxyModel : public QSortFilterProxyModel
0086 {
0087     Q_OBJECT
0088 
0089 public:
0090     CategoryDistributionProxyModel(QObject *parent)
0091         : QSortFilterProxyModel(parent)
0092     {
0093     }
0094     ~CategoryDistributionProxyModel() override = default;
0095 
0096     void setSourceModel(QAbstractItemModel *sourceModel) override
0097     {
0098         if (this->sourceModel()) {
0099             disconnect(this->sourceModel(), nullptr, this, nullptr);
0100         }
0101 
0102         QSortFilterProxyModel::setSourceModel(sourceModel);
0103 
0104         if (sourceModel) {
0105             connect(sourceModel, &QAbstractItemModel::rowsInserted, this, &CategoryDistributionProxyModel::invalidateFilter);
0106             connect(sourceModel, &QAbstractItemModel::rowsMoved, this, &CategoryDistributionProxyModel::invalidateFilter);
0107             connect(sourceModel, &QAbstractItemModel::rowsRemoved, this, &CategoryDistributionProxyModel::invalidateFilter);
0108         }
0109     }
0110 
0111     int limit() const
0112     {
0113         return m_limit;
0114     }
0115 
0116     void setLimit(int limit)
0117     {
0118         if (m_limit == limit) {
0119             return;
0120         }
0121         m_limit = limit;
0122         invalidateFilter();
0123         Q_EMIT limitChanged();
0124     }
0125 
0126 Q_SIGNALS:
0127     void limitChanged();
0128 
0129 protected:
0130     bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
0131     {
0132         if (m_limit <= 0) {
0133             return true;
0134         }
0135 
0136         if (!sourceParent.isValid()) {
0137             return true;
0138         }
0139 
0140         const int categoryCount = sourceModel()->rowCount();
0141 
0142         int maxItemsInCategory = m_limit;
0143 
0144         if (categoryCount > 1) {
0145             int itemsBefore = 0;
0146             for (int i = 0; i <= sourceParent.row(); ++i) {
0147                 const int itemsInCategory = sourceModel()->rowCount(sourceModel()->index(i, 0));
0148 
0149                 // Take into account that every category gets at least one item shown
0150                 const int availableSpace = m_limit - itemsBefore - std::ceil(m_limit / qreal(categoryCount));
0151 
0152                 // The further down the category is the less relevant it is and the less space it my occupy
0153                 // First category gets max half the total limit, second category a third, etc
0154                 maxItemsInCategory = std::min(availableSpace, int(std::ceil(m_limit / qreal(i + 2))));
0155 
0156                 // At least show one item per category
0157                 maxItemsInCategory = std::max(1, maxItemsInCategory);
0158 
0159                 itemsBefore += std::min(itemsInCategory, maxItemsInCategory);
0160             }
0161         }
0162 
0163         if (sourceRow >= maxItemsInCategory) {
0164             return false;
0165         }
0166 
0167         return true;
0168     }
0169 
0170 private:
0171     // if you change this, update the default in resetLimit()
0172     int m_limit = 0;
0173 };
0174 
0175 /**
0176  * This model hides the root items of data originally in a tree structure
0177  *
0178  * KDescendantsProxyModel collapses the items but keeps all items in tact.
0179  * The root items of the RunnerMatchesModel represent the individual cateories
0180  * which we don't want in the resulting flat list.
0181  * This model maps the items back to the given @c treeModel and filters
0182  * out any item with an invalid parent, i.e. "on the root level"
0183  */
0184 class HideRootLevelProxyModel : public QSortFilterProxyModel
0185 {
0186     Q_OBJECT
0187 
0188 public:
0189     HideRootLevelProxyModel(QObject *parent)
0190         : QSortFilterProxyModel(parent)
0191     {
0192     }
0193     ~HideRootLevelProxyModel() override = default;
0194 
0195     QAbstractItemModel *treeModel() const
0196     {
0197         return m_treeModel;
0198     }
0199     void setTreeModel(QAbstractItemModel *treeModel)
0200     {
0201         m_treeModel = treeModel;
0202         invalidateFilter();
0203     }
0204 
0205 protected:
0206     bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
0207     {
0208         KModelIndexProxyMapper mapper(sourceModel(), m_treeModel);
0209         const QModelIndex treeIdx = mapper.mapLeftToRight(sourceModel()->index(sourceRow, 0, sourceParent));
0210         return treeIdx.parent().isValid();
0211     }
0212 
0213 private:
0214     QAbstractItemModel *m_treeModel = nullptr;
0215 };
0216 
0217 /**
0218  * Populates the IsDuplicateRole of an item
0219  *
0220  * The IsDuplicateRole returns true for each item if there is two or more
0221  * elements in the model with the same DisplayRole as the item.
0222  */
0223 class DuplicateDetectorProxyModel : public QIdentityProxyModel
0224 {
0225     Q_OBJECT
0226 
0227 public:
0228     DuplicateDetectorProxyModel(QObject *parent)
0229         : QIdentityProxyModel(parent)
0230     {
0231     }
0232     ~DuplicateDetectorProxyModel() override = default;
0233 
0234     QVariant data(const QModelIndex &index, int role) const override
0235     {
0236         if (role != ResultsModel::DuplicateRole) {
0237             return QIdentityProxyModel::data(index, role);
0238         }
0239 
0240         int duplicatesCount = 0;
0241         const QString display = index.data(Qt::DisplayRole).toString();
0242 
0243         for (int i = 0; i < sourceModel()->rowCount(); ++i) {
0244             if (sourceModel()->index(i, 0).data(Qt::DisplayRole) == display) {
0245                 ++duplicatesCount;
0246 
0247                 if (duplicatesCount == 2) {
0248                     return true;
0249                 }
0250             }
0251         }
0252 
0253         return false;
0254     }
0255 };
0256 
0257 class Q_DECL_HIDDEN ResultsModel::Private
0258 {
0259 public:
0260     Private(ResultsModel *q);
0261 
0262     ResultsModel *q;
0263 
0264     QPointer<Plasma::AbstractRunner> runner = nullptr;
0265 
0266     RunnerResultsModel *resultsModel;
0267     SortProxyModel *sortModel;
0268     CategoryDistributionProxyModel *distributionModel;
0269     KDescendantsProxyModel *flattenModel;
0270     HideRootLevelProxyModel *hideRootModel;
0271     DuplicateDetectorProxyModel *duplicateDetectorModel;
0272 };
0273 
0274 ResultsModel::Private::Private(ResultsModel *q)
0275     : q(q)
0276     , resultsModel(new RunnerResultsModel(q))
0277     , sortModel(new SortProxyModel(q))
0278     , distributionModel(new CategoryDistributionProxyModel(q))
0279     , flattenModel(new KDescendantsProxyModel(q))
0280     , hideRootModel(new HideRootLevelProxyModel(q))
0281     , duplicateDetectorModel(new DuplicateDetectorProxyModel(q))
0282 {
0283 }
0284 
0285 ResultsModel::ResultsModel(QObject *parent)
0286     : QSortFilterProxyModel(parent)
0287     , d(new Private(this))
0288 {
0289     connect(d->resultsModel, &RunnerResultsModel::queryStringChanged, this, &ResultsModel::queryStringChanged);
0290     connect(d->resultsModel, &RunnerResultsModel::queryingChanged, this, &ResultsModel::queryingChanged);
0291     connect(d->resultsModel, &RunnerResultsModel::queryStringChangeRequested, this, &ResultsModel::queryStringChangeRequested);
0292 
0293     // The matches for the old query string remain on display until the first set of matches arrive for the new query string.
0294     // Therefore we must not update the query string inside RunnerResultsModel exactly when the query string changes, otherwise it would
0295     // re-sort the old query string matches based on the new query string.
0296     // So we only make it aware of the query string change at the time when we receive the first set of matches for the new query string.
0297     connect(d->resultsModel, &RunnerResultsModel::matchesChanged, this, [this]() {
0298         d->sortModel->setQueryString(queryString());
0299     });
0300 
0301     connect(d->distributionModel, &CategoryDistributionProxyModel::limitChanged, this, &ResultsModel::limitChanged);
0302 
0303     // The data flows as follows:
0304     // - RunnerResultsModel
0305     //   - SortProxyModel
0306     //     - CategoryDistributionProxyModel
0307     //       - KDescendantsProxyModel
0308     //         - HideRootLevelProxyModel
0309     //           - DuplicateDetectorProxyModel
0310 
0311     d->sortModel->setSourceModel(d->resultsModel);
0312 
0313     d->distributionModel->setSourceModel(d->sortModel);
0314 
0315     d->flattenModel->setSourceModel(d->distributionModel);
0316 
0317     d->hideRootModel->setSourceModel(d->flattenModel);
0318     d->hideRootModel->setTreeModel(d->resultsModel);
0319 
0320     d->duplicateDetectorModel->setSourceModel(d->hideRootModel);
0321 
0322     setSourceModel(d->duplicateDetectorModel);
0323 }
0324 
0325 ResultsModel::~ResultsModel() = default;
0326 
0327 QString ResultsModel::queryString() const
0328 {
0329     return d->resultsModel->queryString();
0330 }
0331 
0332 void ResultsModel::setQueryString(const QString &queryString)
0333 {
0334     d->resultsModel->setQueryString(queryString, runner());
0335 }
0336 
0337 int ResultsModel::limit() const
0338 {
0339     return d->distributionModel->limit();
0340 }
0341 
0342 void ResultsModel::setLimit(int limit)
0343 {
0344     d->distributionModel->setLimit(limit);
0345 }
0346 
0347 void ResultsModel::resetLimit()
0348 {
0349     setLimit(0);
0350 }
0351 
0352 bool ResultsModel::querying() const
0353 {
0354     return d->resultsModel->querying();
0355 }
0356 
0357 QString ResultsModel::runner() const
0358 {
0359     return d->runner ? d->runner->id() : QString();
0360 }
0361 
0362 void ResultsModel::setRunner(const QString &runnerId)
0363 {
0364     if (runnerId == runner()) {
0365         return;
0366     }
0367     if (runnerId.isEmpty()) {
0368         d->runner = nullptr;
0369     } else {
0370         d->runner = runnerManager()->runner(runnerId);
0371     }
0372     Q_EMIT runnerChanged();
0373 }
0374 
0375 QString ResultsModel::runnerName() const
0376 {
0377     return d->runner ? d->runner->name() : QString();
0378 }
0379 
0380 QIcon ResultsModel::runnerIcon() const
0381 {
0382     return d->runner ? d->runner->icon() : QIcon();
0383 }
0384 
0385 QHash<int, QByteArray> ResultsModel::roleNames() const
0386 {
0387     auto names = QAbstractItemModel::roleNames();
0388     names[IdRole] = QByteArrayLiteral("matchId"); // "id" is QML-reserved
0389     names[EnabledRole] = QByteArrayLiteral("enabled");
0390     names[TypeRole] = QByteArrayLiteral("type");
0391     names[RelevanceRole] = QByteArrayLiteral("relevance");
0392     names[CategoryRole] = QByteArrayLiteral("category");
0393     names[SubtextRole] = QByteArrayLiteral("subtext");
0394     names[DuplicateRole] = QByteArrayLiteral("isDuplicate");
0395     names[ActionsRole] = QByteArrayLiteral("actions");
0396     names[MultiLineRole] = QByteArrayLiteral("multiLine");
0397     return names;
0398 }
0399 
0400 void ResultsModel::clear()
0401 {
0402     d->resultsModel->clear();
0403 }
0404 
0405 bool ResultsModel::run(const QModelIndex &idx)
0406 {
0407     KModelIndexProxyMapper mapper(this, d->resultsModel);
0408     const QModelIndex resultsIdx = mapper.mapLeftToRight(idx);
0409     if (!resultsIdx.isValid()) {
0410         return false;
0411     }
0412     return d->resultsModel->run(resultsIdx);
0413 }
0414 
0415 bool ResultsModel::runAction(const QModelIndex &idx, int actionNumber)
0416 {
0417     KModelIndexProxyMapper mapper(this, d->resultsModel);
0418     const QModelIndex resultsIdx = mapper.mapLeftToRight(idx);
0419     if (!resultsIdx.isValid()) {
0420         return false;
0421     }
0422     return d->resultsModel->runAction(resultsIdx, actionNumber);
0423 }
0424 
0425 QMimeData *ResultsModel::getMimeData(const QModelIndex &idx) const
0426 {
0427     KModelIndexProxyMapper mapper(this, d->resultsModel);
0428     const QModelIndex resultsIdx = mapper.mapLeftToRight(idx);
0429     if (!resultsIdx.isValid()) {
0430         return nullptr;
0431     }
0432     return d->resultsModel->mimeData({resultsIdx});
0433 }
0434 
0435 Plasma::RunnerManager *Milou::ResultsModel::runnerManager() const
0436 {
0437     return d->resultsModel->runnerManager();
0438 }
0439 
0440 #include "resultsmodel.moc"