Warning, file /plasma/milou/lib/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 * 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"