File indexing completed on 2024-10-13 13:15:59
0001 /* 0002 * This file is part of the KDE Milou Project 0003 * SPDX-FileCopyrightText: 2013 Vishesh Handa <me@vhanda.in> 0004 * 0005 * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 0006 * 0007 */ 0008 0009 #include "sourcesmodel.h" 0010 0011 #include <KConfig> 0012 #include <KDirWatch> 0013 #include <KSharedConfig> 0014 0015 #include <QAction> 0016 #include <QMimeData> 0017 #include <QModelIndex> 0018 #include <QSet> 0019 0020 using namespace Milou; 0021 0022 SourcesModel::SourcesModel(QObject *parent) 0023 : QAbstractListModel(parent) 0024 , m_size(0) 0025 { 0026 m_manager = new Plasma::RunnerManager(this); 0027 connect(m_manager, &Plasma::RunnerManager::matchesChanged, this, &SourcesModel::slotMatchesChanged); 0028 0029 KDirWatch *watch = KDirWatch::self(); 0030 connect(watch, &KDirWatch::created, this, &SourcesModel::slotSettingsFileChanged); 0031 connect(watch, &KDirWatch::dirty, this, &SourcesModel::slotSettingsFileChanged); 0032 watch->addFile(QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("krunnerrc"))); 0033 0034 m_resetTimer.setSingleShot(true); 0035 m_resetTimer.setInterval(500); 0036 connect(&m_resetTimer, &QTimer::timeout, this, &SourcesModel::slotResetTimeout); 0037 } 0038 0039 SourcesModel::~SourcesModel() 0040 { 0041 } 0042 0043 QHash<int, QByteArray> SourcesModel::roleNames() const 0044 { 0045 QHash<int, QByteArray> roles = QAbstractListModel::roleNames(); 0046 roles.insert(TypeRole, "type"); 0047 roles.insert(SubtextRole, "subtext"); 0048 roles.insert(ActionsRole, "actions"); 0049 roles.insert(DuplicateRole, "isDuplicate"); 0050 roles.insert(PreviewTypeRole, "previewType"); 0051 roles.insert(PreviewUrlRole, "previewUrl"); 0052 roles.insert(PreviewLabelRole, "previewLabel"); 0053 0054 return roles; 0055 } 0056 0057 Plasma::QueryMatch SourcesModel::fetchMatch(int row) const 0058 { 0059 for (const QString &type : qAsConst(m_types)) { 0060 const TypeData data = m_matches.value(type); 0061 if (row < data.shown.size()) { 0062 return data.shown[row]; 0063 } else { 0064 row -= data.shown.size(); 0065 if (row < 0) { 0066 break; 0067 } 0068 } 0069 } 0070 0071 return Plasma::QueryMatch(nullptr); 0072 } 0073 0074 QVariant SourcesModel::data(const QModelIndex &index, int role) const 0075 { 0076 if (!index.isValid()) { 0077 return QVariant(); 0078 } 0079 0080 if (index.row() >= m_size) { 0081 return QVariant(); 0082 } 0083 0084 Plasma::QueryMatch m = fetchMatch(index.row()); 0085 Q_ASSERT(m.runner()); 0086 0087 switch (role) { 0088 case Qt::DisplayRole: 0089 return m.text(); 0090 0091 case Qt::DecorationRole: 0092 if (!m.iconName().isEmpty()) { 0093 return m.iconName(); 0094 } 0095 0096 return m.icon(); 0097 0098 case TypeRole: 0099 return m.matchCategory(); 0100 0101 case SubtextRole: 0102 return m.subtext(); 0103 0104 case ActionsRole: { 0105 const auto &actions = m_manager->actionsForMatch(m); 0106 if (actions.isEmpty()) { 0107 return QVariantList(); 0108 } 0109 0110 QVariantList actionsList; 0111 actionsList.reserve(actions.size()); 0112 0113 for (QAction *action : actions) { 0114 actionsList.append(QVariant::fromValue(action)); 0115 } 0116 0117 return actionsList; 0118 } 0119 case DuplicateRole: 0120 return m_duplicates.value(m.text()); 0121 0122 /* 0123 case PreviewTypeRole: 0124 return m.previewType(); 0125 0126 case PreviewUrlRole: 0127 return m.previewUrl(); 0128 0129 case PreviewLabelRole: 0130 return m.previewLabel(); 0131 */ 0132 } 0133 0134 return QVariant(); 0135 } 0136 0137 int SourcesModel::rowCount(const QModelIndex &parent) const 0138 { 0139 if (parent.isValid()) { 0140 return 0; 0141 } 0142 0143 return m_size; 0144 } 0145 0146 QString SourcesModel::queryString() const 0147 { 0148 return m_queryString; 0149 } 0150 0151 int SourcesModel::queryLimit() const 0152 { 0153 return m_queryLimit; 0154 } 0155 0156 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 81) 0157 QString SourcesModel::runner() const 0158 { 0159 return m_runner; 0160 } 0161 0162 void SourcesModel::setRunner(const QString &runner) 0163 { 0164 if (m_runner != runner) { 0165 m_runner = runner; 0166 0167 m_manager->setSingleModeRunnerId(m_runner); 0168 m_manager->setSingleMode(!m_runner.isEmpty()); 0169 0170 Q_EMIT runnerChanged(); 0171 } 0172 } 0173 0174 QString SourcesModel::runnerName() const 0175 { 0176 auto *singleRunner = m_manager->singleModeRunner(); 0177 if (!singleRunner) { 0178 return QString(); 0179 } 0180 0181 return singleRunner->name(); 0182 } 0183 0184 QIcon SourcesModel::runnerIcon() const 0185 { 0186 auto *singleRunner = m_manager->singleModeRunner(); 0187 if (!singleRunner) { 0188 return QIcon(); 0189 } 0190 0191 return singleRunner->icon(); 0192 } 0193 #endif 0194 0195 void SourcesModel::setQueryLimit(int limit) 0196 { 0197 m_queryLimit = limit; 0198 /* 0199 foreach (AbstractSource* source, m_sources) 0200 source->setQueryLimit(limit); 0201 */ 0202 } 0203 0204 void SourcesModel::setQueryString(const QString &str) 0205 { 0206 if (str.trimmed() == m_queryString.trimmed()) { 0207 return; 0208 } 0209 0210 m_queryString = str; 0211 if (m_queryString.isEmpty()) { 0212 clear(); 0213 return; 0214 } 0215 0216 // We avoid clearing the model instantly, and instead wait for the results 0217 // to show up, and only then do we clear the model. In the event 0218 // where there are no results, we wait for a predefined time before 0219 // clearing the model 0220 m_resetTimer.start(); 0221 0222 m_modelPopulated = false; 0223 m_manager->launchQuery(m_queryString, m_runner); 0224 } 0225 0226 void SourcesModel::slotResetTimeout() 0227 { 0228 if (!m_modelPopulated) { 0229 // The old items are still shown, get rid of them 0230 beginResetModel(); 0231 m_matches.clear(); 0232 m_size = 0; 0233 m_duplicates.clear(); 0234 endResetModel(); 0235 } 0236 } 0237 0238 void SourcesModel::slotMatchesChanged(const QList<Plasma::QueryMatch> &l) 0239 { 0240 // We do reset handling ourselves, so ignore clears if the reset timer 0241 // is supposed to handle them (see setQueryString) 0242 if (l.length() == 0 && m_resetTimer.isActive() && !m_modelPopulated) { 0243 return; 0244 } 0245 0246 beginResetModel(); 0247 m_matches.clear(); 0248 m_size = 0; 0249 m_types.clear(); 0250 m_duplicates.clear(); 0251 0252 QList<Plasma::QueryMatch> list(l); 0253 std::sort(list.begin(), list.end()); 0254 0255 for (auto it = list.crbegin(), end = list.crend(); it != end; ++it) { 0256 slotMatchAdded(*it); 0257 } 0258 0259 // Sort the result types. We give the results which contain the query 0260 // text in the user visible string a higher preference than the ones 0261 // that do not 0262 // The rest are given the same preference as given by the runners. 0263 const QString simplifiedQuery = m_queryString.simplified(); 0264 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 0265 const auto words = simplifiedQuery.splitRef(QLatin1Char(' '), Qt::SkipEmptyParts); 0266 #else 0267 const auto words = QStringView(simplifiedQuery).split(QLatin1Char(' '), Qt::SkipEmptyParts); 0268 #endif 0269 QSet<QString> higherTypes; 0270 for (const QString &type : qAsConst(m_types)) { 0271 const TypeData td = m_matches.value(type); 0272 0273 for (const Plasma::QueryMatch &match : td.shown) { 0274 const QString text = match.text().simplified(); 0275 bool containsAll = true; 0276 0277 for (const auto &word : words) { 0278 if (!text.contains(word, Qt::CaseInsensitive)) { 0279 containsAll = false; 0280 break; 0281 } 0282 } 0283 0284 // Maybe we should be giving it a higher type based on the number of matched 0285 // words in the text? 0286 if (containsAll) { 0287 higherTypes << match.matchCategory(); 0288 } 0289 } 0290 } 0291 0292 auto sortFunc = [&](const QString &l, const QString &r) { 0293 bool lHigher = higherTypes.contains(l); 0294 bool rHigher = higherTypes.contains(r); 0295 0296 if (lHigher == rHigher) { 0297 return false; 0298 } else { 0299 return lHigher; 0300 } 0301 }; 0302 std::stable_sort(m_types.begin(), m_types.end(), sortFunc); 0303 0304 m_modelPopulated = true; 0305 endResetModel(); 0306 } 0307 0308 // 0309 // Tries to make sure that all the types have the same number 0310 // of visible items 0311 // 0312 void SourcesModel::slotMatchAdded(const Plasma::QueryMatch &m) 0313 { 0314 if (m_queryString.isEmpty()) { 0315 return; 0316 } 0317 0318 QString matchType = m.matchCategory(); 0319 0320 if (!m_types.contains(matchType)) { 0321 m_types << matchType; 0322 } 0323 0324 if (m_size == m_queryLimit) { 0325 int maxShownItems = 0; 0326 QString maxShownType; 0327 for (const QString &type : qAsConst(m_types)) { 0328 TypeData data = m_matches.value(type); 0329 if (data.shown.size() >= maxShownItems) { 0330 maxShownItems = data.shown.size(); 0331 maxShownType = type; 0332 } 0333 } 0334 0335 if (maxShownType == matchType) { 0336 m_matches[matchType].hidden.append(m); 0337 return; 0338 } 0339 0340 // Remove the last shown row from maxShownType 0341 // and add it to matchType 0342 Plasma::QueryMatch transferMatch = m_matches[maxShownType].shown.takeLast(); 0343 m_matches[maxShownType].hidden.append(transferMatch); 0344 m_size--; 0345 m_duplicates[transferMatch.text()]--; 0346 } 0347 0348 m_matches[matchType].shown.append(m); 0349 m_size++; 0350 m_duplicates[m.text()]++; 0351 } 0352 0353 void SourcesModel::slotSettingsFileChanged(const QString &path) 0354 { 0355 if (!path.endsWith(QLatin1String("krunnerrc"))) { 0356 return; 0357 } 0358 0359 reloadConfiguration(); 0360 } 0361 0362 void SourcesModel::clear() 0363 { 0364 beginResetModel(); 0365 m_matches.clear(); 0366 m_size = 0; 0367 m_duplicates.clear(); 0368 m_queryString.clear(); 0369 m_manager->reset(); 0370 m_manager->matchSessionComplete(); 0371 endResetModel(); 0372 } 0373 0374 bool SourcesModel::run(int index) 0375 { 0376 Plasma::QueryMatch match = fetchMatch(index); 0377 Q_ASSERT(match.runner()); 0378 0379 #if KRUNNER_ENABLE_DEPRECATED_SINCE(5, 99) 0380 if (match.type() == Plasma::QueryMatch::InformationalMatch) { 0381 QString info = match.data().toString(); 0382 int editPos = info.length(); 0383 0384 if (!info.isEmpty()) { 0385 // FIXME: pretty lame way to decide if this is a query prototype 0386 // Copied from kde4 krunner interface.cpp 0387 if (match.runner() == nullptr) { 0388 // lame way of checking to see if this is a Help Button generated match! 0389 int index = info.indexOf(QStringLiteral(":q:")); 0390 0391 if (index != -1) { 0392 editPos = index; 0393 info.replace(QStringLiteral(":q:"), QString()); 0394 } 0395 } 0396 0397 Q_EMIT updateSearchTerm(info, editPos); 0398 return false; 0399 } 0400 } 0401 #endif 0402 0403 m_manager->run(match); 0404 return true; 0405 } 0406 0407 bool SourcesModel::runAction(int index, int actionIndex) 0408 { 0409 Plasma::QueryMatch match = fetchMatch(index); 0410 Q_ASSERT(match.runner()); 0411 0412 const auto &actions = m_manager->actionsForMatch(match); 0413 if (actionIndex < 0 || actionIndex >= actions.count()) { 0414 return false; 0415 } 0416 0417 QAction *action = actions.at(actionIndex); 0418 match.setSelectedAction(action); 0419 m_manager->run(match); 0420 return true; 0421 } 0422 0423 void SourcesModel::reloadConfiguration() 0424 { 0425 KSharedConfig::openConfig(QStringLiteral("krunnerrc"))->reparseConfiguration(); 0426 m_manager->reloadConfiguration(); 0427 } 0428 0429 QMimeData *SourcesModel::getMimeData(int index) const 0430 { 0431 Plasma::QueryMatch match = fetchMatch(index); 0432 Q_ASSERT(match.runner()); 0433 0434 // we're returning a parent-less QObject from a Q_INVOKABLE 0435 // which means the QML engine will take care of deleting it eventually 0436 QMimeData *mimeData = m_manager->mimeDataForMatch(match); 0437 0438 return mimeData; 0439 }