Warning, file /frameworks/krunner/src/model/resultsmodel.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

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