File indexing completed on 2024-05-05 17:45:02
0001 /* 0002 SPDX-FileCopyrightText: 2006 Aaron Seigo <aseigo@kde.org> 0003 SPDX-FileCopyrightText: 2014 Vishesh Handa <vhanda@kde.org> 0004 SPDX-FileCopyrightText: 2016-2020 Harald Sitter <sitter@kde.org> 0005 SPDX-FileCopyrightText: 2022 Alexander Lohnau <alexander.lohnau@gmx.de> 0006 0007 SPDX-License-Identifier: LGPL-2.0-only 0008 */ 0009 0010 #include "servicerunner.h" 0011 0012 #include <algorithm> 0013 0014 #include <QMimeData> 0015 0016 #include <QDebug> 0017 #include <QDir> 0018 #include <QIcon> 0019 #include <QStandardPaths> 0020 #include <QUrl> 0021 #include <QUrlQuery> 0022 0023 #include <KActivities/ResourceInstance> 0024 #include <KApplicationTrader> 0025 #include <KLocalizedString> 0026 #include <KNotificationJobUiDelegate> 0027 #include <KServiceAction> 0028 #include <KShell> 0029 #include <KStringHandler> 0030 #include <KSycoca> 0031 0032 #include <KIO/ApplicationLauncherJob> 0033 #include <KIO/DesktopExecParser> 0034 0035 #include "debug.h" 0036 0037 namespace 0038 { 0039 int weightedLength(const QString &query) 0040 { 0041 return KStringHandler::logicalLength(query); 0042 } 0043 0044 inline bool contains(const QString &result, const QStringList &queryList) 0045 { 0046 return std::all_of(queryList.cbegin(), queryList.cend(), [&result](const QString &query) { 0047 return result.contains(query, Qt::CaseInsensitive); 0048 }); 0049 } 0050 0051 inline bool contains(const QStringList &results, const QStringList &queryList) 0052 { 0053 return std::all_of(queryList.cbegin(), queryList.cend(), [&results](const QString &query) { 0054 return std::any_of(results.cbegin(), results.cend(), [&query](const QString &result) { 0055 return result.contains(query, Qt::CaseInsensitive); 0056 }); 0057 }); 0058 } 0059 0060 } // namespace 0061 0062 /** 0063 * @brief Finds all KServices for a given runner query 0064 */ 0065 class ServiceFinder 0066 { 0067 public: 0068 ServiceFinder(ServiceRunner *runner, const QList<KService::Ptr> &list) 0069 : m_runner(runner) 0070 , m_services(list) 0071 { 0072 } 0073 0074 void match(Plasma::RunnerContext &context) 0075 { 0076 term = context.query(); 0077 // Splitting the query term to match using subsequences 0078 queryList = term.split(QLatin1Char(' ')); 0079 weightedTermLength = weightedLength(term); 0080 0081 matchNameKeywordAndGenericName(); 0082 matchCategories(); 0083 matchJumpListActions(); 0084 0085 context.addMatches(matches); 0086 } 0087 0088 private: 0089 void seen(const KService::Ptr &service) 0090 { 0091 m_seen.insert(service->storageId()); 0092 m_seen.insert(service->exec()); 0093 } 0094 0095 void seen(const KServiceAction &action) 0096 { 0097 m_seen.insert(action.exec()); 0098 } 0099 0100 bool hasSeen(const KService::Ptr &service) 0101 { 0102 return m_seen.contains(service->storageId()) && m_seen.contains(service->exec()); 0103 } 0104 0105 bool hasSeen(const KServiceAction &action) 0106 { 0107 return m_seen.contains(action.exec()); 0108 } 0109 0110 bool disqualify(const KService::Ptr &service) 0111 { 0112 auto ret = hasSeen(service) || service->noDisplay(); 0113 qCDebug(RUNNER_SERVICES) << service->name() << "disqualified?" << ret; 0114 seen(service); 0115 return ret; 0116 } 0117 0118 qreal increaseMatchRelavance(const KService::Ptr &service, const QStringList &strList, const QString &category) 0119 { 0120 // Increment the relevance based on all the words (other than the first) of the query list 0121 qreal relevanceIncrement = 0; 0122 0123 for (int i = 1; i < strList.size(); ++i) { 0124 const auto &str = strList.at(i); 0125 if (category == QLatin1String("Name")) { 0126 if (service->name().contains(str, Qt::CaseInsensitive)) { 0127 relevanceIncrement += 0.01; 0128 } 0129 } else if (category == QLatin1String("GenericName")) { 0130 if (service->genericName().contains(str, Qt::CaseInsensitive)) { 0131 relevanceIncrement += 0.01; 0132 } 0133 } else if (category == QLatin1String("Exec")) { 0134 if (service->exec().contains(str, Qt::CaseInsensitive)) { 0135 relevanceIncrement += 0.01; 0136 } 0137 } else if (category == QLatin1String("Comment")) { 0138 if (service->comment().contains(str, Qt::CaseInsensitive)) { 0139 relevanceIncrement += 0.01; 0140 } 0141 } 0142 } 0143 0144 return relevanceIncrement; 0145 } 0146 0147 void setupMatch(const KService::Ptr &service, Plasma::QueryMatch &match) 0148 { 0149 const QString name = service->name(); 0150 0151 match.setText(name); 0152 0153 QUrl url(service->storageId()); 0154 url.setScheme(QStringLiteral("applications")); 0155 match.setData(url); 0156 0157 QString path = service->entryPath(); 0158 if (!QDir::isAbsolutePath(path)) { 0159 path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kservices5/") + path); 0160 } 0161 0162 match.setUrls({QUrl::fromLocalFile(path)}); 0163 0164 QString exec = service->exec(); 0165 0166 QStringList resultingArgs = KIO::DesktopExecParser(KService(QString(), exec, QString()), {}).resultingArguments(); 0167 0168 // Remove any environment variables. 0169 if (KIO::DesktopExecParser::executableName(exec) == QLatin1String("env")) { 0170 resultingArgs.removeFirst(); // remove "env". 0171 0172 while (!resultingArgs.isEmpty() && resultingArgs.first().contains(QLatin1Char('='))) { 0173 resultingArgs.removeFirst(); 0174 } 0175 0176 // Now parse it again to resolve the path. 0177 resultingArgs = KIO::DesktopExecParser(KService(QString(), KShell::joinArgs(resultingArgs), QString()), {}).resultingArguments(); 0178 } 0179 0180 // Remove special arguments that have no real impact on the application. 0181 static const auto specialArgs = {QStringLiteral("-qwindowtitle"), QStringLiteral("-qwindowicon"), QStringLiteral("--started-from-file")}; 0182 0183 for (const auto &specialArg : specialArgs) { 0184 int index = resultingArgs.indexOf(specialArg); 0185 if (index > -1) { 0186 if (resultingArgs.count() > index) { 0187 resultingArgs.removeAt(index); 0188 } 0189 if (resultingArgs.count() > index) { 0190 resultingArgs.removeAt(index); // remove value, too, if any. 0191 } 0192 } 0193 } 0194 0195 match.setId(QStringLiteral("exec://") + resultingArgs.join(QLatin1Char(' '))); 0196 if (!service->genericName().isEmpty() && service->genericName() != name) { 0197 match.setSubtext(service->genericName()); 0198 } else if (!service->comment().isEmpty()) { 0199 match.setSubtext(service->comment()); 0200 } 0201 0202 if (!service->icon().isEmpty()) { 0203 match.setIconName(service->icon()); 0204 } 0205 } 0206 0207 void matchNameKeywordAndGenericName() 0208 { 0209 const auto nameKeywordAndGenericNameFilter = [this](const KService::Ptr &service) { 0210 // Name 0211 if (contains(service->name(), queryList)) { 0212 return true; 0213 } 0214 // If the term length is < 3, no real point searching the Keywords and GenericName 0215 if (weightedTermLength < 3) { 0216 return false; 0217 } 0218 // Keywords 0219 if (contains(service->keywords(), queryList)) { 0220 return true; 0221 } 0222 // GenericName 0223 if (contains(service->genericName(), queryList) || contains(service->untranslatedGenericName(), queryList)) { 0224 return true; 0225 } 0226 // Comment 0227 if (contains(service->comment(), queryList)) { 0228 return true; 0229 } 0230 0231 return false; 0232 }; 0233 0234 for (const KService::Ptr &service : m_services) { 0235 if (!nameKeywordAndGenericNameFilter(service) || disqualify(service)) { 0236 continue; 0237 } 0238 0239 const QString id = service->storageId(); 0240 const QString name = service->name(); 0241 const QString exec = service->exec(); 0242 0243 Plasma::QueryMatch match(m_runner); 0244 match.setType(Plasma::QueryMatch::PossibleMatch); 0245 setupMatch(service, match); 0246 qreal relevance(0.6); 0247 0248 // If the term was < 3 chars and NOT at the beginning of the App's name or Exec, then 0249 // chances are the user doesn't want that app. 0250 if (weightedTermLength < 3) { 0251 if (name.startsWith(term, Qt::CaseInsensitive) || exec.startsWith(term, Qt::CaseInsensitive)) { 0252 relevance = 0.9; 0253 } else { 0254 continue; 0255 } 0256 } else if (name.compare(term, Qt::CaseInsensitive) == 0) { 0257 relevance = 1; 0258 match.setType(Plasma::QueryMatch::ExactMatch); 0259 } else if (name.contains(queryList[0], Qt::CaseInsensitive)) { 0260 relevance = 0.8; 0261 relevance += increaseMatchRelavance(service, queryList, QStringLiteral("Name")); 0262 0263 if (name.startsWith(queryList[0], Qt::CaseInsensitive)) { 0264 relevance += 0.1; 0265 } 0266 } else if (service->genericName().contains(queryList[0], Qt::CaseInsensitive)) { 0267 relevance = 0.65; 0268 relevance += increaseMatchRelavance(service, queryList, QStringLiteral("GenericName")); 0269 0270 if (service->genericName().startsWith(queryList[0], Qt::CaseInsensitive)) { 0271 relevance += 0.05; 0272 } 0273 } else if (service->comment().contains(queryList[0], Qt::CaseInsensitive)) { 0274 relevance = 0.5; 0275 relevance += increaseMatchRelavance(service, queryList, QStringLiteral("Comment")); 0276 0277 if (service->comment().startsWith(queryList[0], Qt::CaseInsensitive)) { 0278 relevance += 0.05; 0279 } 0280 } 0281 0282 if (service->categories().contains(QLatin1String("KDE"))) { 0283 qCDebug(RUNNER_SERVICES) << "found a kde thing" << id << match.subtext() << relevance; 0284 relevance += .09; 0285 } 0286 0287 qCDebug(RUNNER_SERVICES) << name << "is this relevant:" << relevance; 0288 match.setRelevance(relevance); 0289 0290 matches << match; 0291 } 0292 } 0293 0294 void matchCategories() 0295 { 0296 // Do not match categories for short queries, BUG: 469769 0297 if (weightedTermLength < 5) { 0298 return; 0299 } 0300 const auto categoriesFilter = [this](const KService::Ptr &service) { 0301 return contains(service->categories(), queryList); 0302 }; 0303 0304 for (const KService::Ptr &service : m_services) { 0305 if (!categoriesFilter(service) || disqualify(service)) { 0306 continue; 0307 } 0308 qCDebug(RUNNER_SERVICES) << service->name() << "is an exact match!" << service->storageId() << service->exec(); 0309 0310 Plasma::QueryMatch match(m_runner); 0311 match.setType(Plasma::QueryMatch::PossibleMatch); 0312 setupMatch(service, match); 0313 0314 qreal relevance = 0.4; 0315 const QStringList categories = service->categories(); 0316 if (std::any_of(categories.begin(), categories.end(), [this](const QString &category) { 0317 return category.compare(term, Qt::CaseInsensitive) == 0; 0318 })) { 0319 relevance = 0.6; 0320 } else if (service->categories().contains(QLatin1String("X-KDE-More")) || !service->showInCurrentDesktop()) { 0321 relevance = 0.5; 0322 } 0323 0324 if (service->isApplication()) { 0325 relevance += .04; 0326 } 0327 0328 match.setRelevance(relevance); 0329 matches << match; 0330 } 0331 } 0332 0333 void matchJumpListActions() 0334 { 0335 if (weightedTermLength < 3) { 0336 return; 0337 } 0338 0339 const auto hasActionsFilter = [](const KService::Ptr &service) { 0340 return !service->actions().isEmpty(); 0341 }; 0342 for (const KService::Ptr &service : m_services) { 0343 if (!hasActionsFilter(service) || service->noDisplay()) { 0344 continue; 0345 } 0346 0347 // Skip SystemSettings as we find KCMs already 0348 if (service->storageId() == QLatin1String("systemsettings.desktop")) { 0349 continue; 0350 } 0351 0352 const auto actions = service->actions(); 0353 for (const KServiceAction &action : actions) { 0354 if (action.text().isEmpty() || action.exec().isEmpty() || hasSeen(action)) { 0355 continue; 0356 } 0357 seen(action); 0358 0359 const int matchIndex = action.text().indexOf(term, 0, Qt::CaseInsensitive); 0360 if (matchIndex < 0) { 0361 continue; 0362 } 0363 0364 Plasma::QueryMatch match(m_runner); 0365 match.setType(Plasma::QueryMatch::PossibleMatch); 0366 if (!action.icon().isEmpty()) { 0367 match.setIconName(action.icon()); 0368 } else { 0369 match.setIconName(service->icon()); 0370 } 0371 match.setText(i18nc("Jump list search result, %1 is action (eg. open new tab), %2 is application (eg. browser)", 0372 "%1 - %2", 0373 action.text(), 0374 service->name())); 0375 0376 QUrl url(service->storageId()); 0377 url.setScheme(QStringLiteral("applications")); 0378 0379 QUrlQuery query; 0380 query.addQueryItem(QStringLiteral("action"), action.name()); 0381 url.setQuery(query); 0382 0383 match.setData(url); 0384 0385 qreal relevance = 0.5; 0386 if (action.text().compare(term, Qt::CaseInsensitive) == 0) { 0387 relevance = 0.65; 0388 match.setType(Plasma::QueryMatch::HelperMatch); // Give it a higer match type to ensure it is shown, BUG: 455436 0389 } else if (matchIndex == 0) { 0390 relevance += 0.05; 0391 } 0392 0393 match.setRelevance(relevance); 0394 0395 matches << match; 0396 } 0397 } 0398 } 0399 0400 ServiceRunner *m_runner; 0401 QSet<QString> m_seen; 0402 const QList<KService::Ptr> m_services; 0403 0404 QList<Plasma::QueryMatch> matches; 0405 QString query; 0406 QString term; 0407 QStringList queryList; 0408 int weightedTermLength = -1; 0409 }; 0410 0411 ServiceRunner::ServiceRunner(QObject *parent, const KPluginMetaData &metaData, const QVariantList &args) 0412 : Plasma::AbstractRunner(parent, metaData, args) 0413 { 0414 setObjectName(QStringLiteral("Application")); 0415 setPriority(AbstractRunner::HighestPriority); 0416 0417 addSyntax(Plasma::RunnerSyntax(QStringLiteral(":q:"), i18n("Finds applications whose name or description match :q:"))); 0418 } 0419 0420 ServiceRunner::~ServiceRunner() = default; 0421 0422 void ServiceRunner::match(Plasma::RunnerContext &context) 0423 { 0424 KSycoca::disableAutoRebuild(); 0425 ServiceFinder finder(this, KApplicationTrader::query([](const KService::Ptr &) { 0426 return true; 0427 })); 0428 finder.match(context); 0429 } 0430 0431 void ServiceRunner::run(const Plasma::RunnerContext &context, const Plasma::QueryMatch &match) 0432 { 0433 Q_UNUSED(context) 0434 0435 const QUrl dataUrl = match.data().toUrl(); 0436 0437 KService::Ptr service = KService::serviceByStorageId(dataUrl.path()); 0438 if (!service) { 0439 return; 0440 } 0441 0442 KActivities::ResourceInstance::notifyAccessed(QUrl(QStringLiteral("applications:") + service->storageId()), QStringLiteral("org.kde.krunner")); 0443 0444 KIO::ApplicationLauncherJob *job = nullptr; 0445 0446 const QString actionName = QUrlQuery(dataUrl).queryItemValue(QStringLiteral("action")); 0447 if (actionName.isEmpty()) { 0448 job = new KIO::ApplicationLauncherJob(service); 0449 } else { 0450 const auto actions = service->actions(); 0451 auto it = std::find_if(actions.begin(), actions.end(), [&actionName](const KServiceAction &action) { 0452 return action.name() == actionName; 0453 }); 0454 Q_ASSERT(it != actions.end()); 0455 0456 job = new KIO::ApplicationLauncherJob(*it); 0457 } 0458 0459 auto *delegate = new KNotificationJobUiDelegate; 0460 delegate->setAutoErrorHandlingEnabled(true); 0461 job->setUiDelegate(delegate); 0462 job->start(); 0463 }