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"