File indexing completed on 2024-05-05 05:38:37
0001 /* 0002 SPDX-FileCopyrightText: 2016 Eike Hein <hein@kde.org> 0003 SPDX-FileCopyrightText: 2023 Harald Sitter <sitter@kde.org> 0004 0005 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 0006 */ 0007 0008 #include "tasktools.h" 0009 #include "abstracttasksmodel.h" 0010 0011 #include <KApplicationTrader> 0012 #include <KConfigGroup> 0013 #include <KDesktopFile> 0014 #include <KFileItem> 0015 #include <KNotificationJobUiDelegate> 0016 #include <KProcessList> 0017 #include <KWindowSystem> 0018 #include <PlasmaActivities/ResourceInstance> 0019 #include <kemailsettings.h> 0020 0021 #include <KIO/ApplicationLauncherJob> 0022 #include <KIO/OpenUrlJob> 0023 0024 #include <QDir> 0025 #include <QGuiApplication> 0026 #include <QRegularExpression> 0027 #include <QScreen> 0028 #include <QUrlQuery> 0029 #include <qnamespace.h> 0030 0031 #include <defaultservice.h> 0032 0033 using namespace Qt::StringLiterals; 0034 0035 namespace TaskManager 0036 { 0037 AppData appDataFromUrl(const QUrl &url, const QIcon &fallbackIcon) 0038 { 0039 AppData data; 0040 data.url = url; 0041 0042 if (url.hasQuery()) { 0043 QUrlQuery uQuery(url); 0044 0045 if (uQuery.hasQueryItem(QLatin1String("iconData"))) { 0046 QString iconData(uQuery.queryItemValue(QLatin1String("iconData"))); 0047 QPixmap pixmap; 0048 QByteArray bytes = QByteArray::fromBase64(iconData.toLocal8Bit(), QByteArray::Base64UrlEncoding); 0049 pixmap.loadFromData(bytes); 0050 data.icon.addPixmap(pixmap); 0051 } 0052 0053 if (uQuery.hasQueryItem(QLatin1String("skipTaskbar"))) { 0054 QString skipTaskbar(uQuery.queryItemValue(QLatin1String("skipTaskbar"))); 0055 data.skipTaskbar = (skipTaskbar == QLatin1String("true")); 0056 } 0057 } 0058 0059 // applications: URLs are used to refer to applications by their KService::menuId 0060 // (i.e. .desktop file name) rather than the absolute path to a .desktop file. 0061 if (url.scheme() == QLatin1String("applications")) { 0062 const KService::Ptr service = KService::serviceByMenuId(url.path()); 0063 0064 if (service && url.path() == service->menuId()) { 0065 data.name = service->name(); 0066 data.genericName = service->genericName(); 0067 data.id = service->storageId(); 0068 0069 if (data.icon.isNull()) { 0070 data.icon = QIcon::fromTheme(service->icon()); 0071 } 0072 } 0073 } 0074 0075 if (url.isLocalFile()) { 0076 if (KDesktopFile::isDesktopFile(url.toLocalFile())) { 0077 const KService::Ptr service = KService::serviceByStorageId(url.fileName()); 0078 0079 // Resolve to non-absolute menuId-based URL if possible. 0080 if (service) { 0081 const QString &menuId = service->menuId(); 0082 0083 if (!menuId.isEmpty()) { 0084 data.url = QUrl(QLatin1String("applications:") + menuId); 0085 } 0086 } 0087 0088 if (service && QUrl::fromLocalFile(service->entryPath()) == url) { 0089 data.name = service->name(); 0090 data.genericName = service->genericName(); 0091 data.id = service->storageId(); 0092 0093 if (data.icon.isNull()) { 0094 data.icon = QIcon::fromTheme(service->icon()); 0095 } 0096 } else { 0097 KDesktopFile f(url.toLocalFile()); 0098 if (f.tryExec()) { 0099 data.name = f.readName(); 0100 data.genericName = f.readGenericName(); 0101 data.id = QUrl::fromLocalFile(f.fileName()).fileName(); 0102 0103 if (data.icon.isNull()) { 0104 const QString iconValue = f.readIcon(); 0105 if (QIcon::hasThemeIcon(iconValue)) { 0106 data.icon = QIcon::fromTheme(iconValue); 0107 } else if (!iconValue.startsWith(QDir::separator())) { 0108 const int lastIndexOfPeriod = iconValue.lastIndexOf(QLatin1Char('.')); 0109 const QString iconValueWithoutSuffix = lastIndexOfPeriod < 0 ? iconValue : iconValue.left(lastIndexOfPeriod); 0110 // Find an icon in the same folder 0111 const QDir sameDir = QFileInfo(url.toLocalFile()).absoluteDir(); 0112 const auto iconList = sameDir.entryInfoList( 0113 { 0114 QStringLiteral("*.png").arg(iconValueWithoutSuffix), 0115 QStringLiteral("*.svg").arg(iconValueWithoutSuffix), 0116 }, 0117 QDir::Files); 0118 if (!iconList.empty()) { 0119 data.icon = QIcon(iconList[0].absoluteFilePath()); 0120 } 0121 } else { 0122 data.icon = QIcon(iconValue); 0123 } 0124 } 0125 } 0126 } 0127 0128 if (data.id.endsWith(".desktop")) { 0129 data.id = data.id.left(data.id.length() - 8); 0130 } 0131 } else { 0132 data.id = url.fileName(); 0133 } 0134 0135 } else if (url.scheme() == QLatin1String("preferred")) { 0136 data.id = defaultApplication(url); 0137 0138 const KService::Ptr service = KService::serviceByStorageId(data.id); 0139 0140 if (service) { 0141 const QString &menuId = service->menuId(); 0142 const QString &desktopFile = service->entryPath(); 0143 0144 data.name = service->name(); 0145 data.genericName = service->genericName(); 0146 data.id = service->storageId(); 0147 0148 if (data.icon.isNull()) { 0149 data.icon = QIcon::fromTheme(service->icon()); 0150 } 0151 0152 // Update with resolved URL. 0153 if (!menuId.isEmpty()) { 0154 data.url = QUrl(QLatin1String("applications:") + menuId); 0155 } else { 0156 data.url = QUrl::fromLocalFile(desktopFile); 0157 } 0158 } 0159 } 0160 0161 if (data.name.isEmpty()) { 0162 data.name = url.fileName(); 0163 } 0164 0165 if (data.icon.isNull()) { 0166 data.icon = fallbackIcon; 0167 } 0168 0169 return data; 0170 } 0171 0172 QUrl windowUrlFromMetadata(const QString &appId, quint32 pid, KSharedConfig::Ptr rulesConfig, const QString &xWindowsWMClassName) 0173 { 0174 if (!rulesConfig) { 0175 return QUrl(); 0176 } 0177 0178 QUrl url; 0179 KService::List services; 0180 bool triedPid = false; 0181 0182 // The code below this function goes on a hunt for services based on the metadata 0183 // that has been passed in. Occasionally, it will find more than one matching 0184 // service. In some scenarios (e.g. multiple identically-named .desktop files) 0185 // there's a need to pick the most useful one. The function below promises to "sort" 0186 // a list of services by how closely their KService::menuId() relates to the key that 0187 // has been passed in. The current naive implementation simply looks for a menuId 0188 // that starts with the key, prepends it to the list and returns it. In practice, 0189 // that means a KService with a menuId matching the appId will win over one with a 0190 // menuId that encodes a subfolder hierarchy. 0191 // A concrete example: Valve's Steam client is sometimes installed two times, once 0192 // natively as a Linux application, once via Wine. Both have .desktop files named 0193 // (S|)steam.desktop. The Linux native version is located in the menu by means of 0194 // categorization ("Games") and just has a menuId() matching the .desktop file name, 0195 // but the Wine version is placed in a folder hierarchy by Wine and gets a menuId() 0196 // of wine-Programs-Steam-Steam.desktop. The weighing done by this function makes 0197 // sure the Linux native version gets mapped to the former, while other heuristics 0198 // map the Wine version reliably to the latter. 0199 // In lieu of this weighing we just used whatever KApplicationTrader returned first, 0200 // so what we do here can be no worse. 0201 auto sortServicesByMenuId = [](KService::List &services, const QString &key) { 0202 if (services.count() == 1) { 0203 return; 0204 } 0205 0206 for (const auto &service : services) { 0207 if (service->menuId().startsWith(key, Qt::CaseInsensitive)) { 0208 services.prepend(service); 0209 return; 0210 } 0211 } 0212 }; 0213 0214 if (!(appId.isEmpty() && xWindowsWMClassName.isEmpty())) { 0215 // Check to see if this wmClass matched a saved one ... 0216 KConfigGroup grp(rulesConfig, u"Mapping"_s); 0217 KConfigGroup set(rulesConfig, u"Settings"_s); 0218 0219 // Evaluate MatchCommandLineFirst directives from config first. 0220 // Some apps have different launchers depending upon command line ... 0221 QStringList matchCommandLineFirst = set.readEntry("MatchCommandLineFirst", QStringList()); 0222 0223 if (!appId.isEmpty() && matchCommandLineFirst.contains(appId)) { 0224 triedPid = true; 0225 services = servicesFromPid(pid, rulesConfig); 0226 } 0227 0228 // Try to match using xWindowsWMClassName also. 0229 if (!xWindowsWMClassName.isEmpty() && matchCommandLineFirst.contains("::" + xWindowsWMClassName)) { 0230 triedPid = true; 0231 services = servicesFromPid(pid, rulesConfig); 0232 } 0233 0234 if (!appId.isEmpty()) { 0235 // Evaluate any mapping rules that map to a specific .desktop file. 0236 QString mapped(grp.readEntry(appId + "::" + xWindowsWMClassName, QString())); 0237 0238 if (mapped.endsWith(QLatin1String(".desktop"))) { 0239 url = QUrl(mapped); 0240 return url; 0241 } 0242 0243 if (mapped.isEmpty()) { 0244 mapped = grp.readEntry(appId, QString()); 0245 0246 if (mapped.endsWith(QLatin1String(".desktop"))) { 0247 url = QUrl(mapped); 0248 return url; 0249 } 0250 } 0251 0252 // Some apps, such as Wine, cannot use xWindowsWMClassName to map to launcher name - as Wine itself is not a GUI app 0253 // So, Settings/ManualOnly lists window classes where the user will always have to manualy set the launcher ... 0254 QStringList manualOnly = set.readEntry("ManualOnly", QStringList()); 0255 0256 if (!appId.isEmpty() && manualOnly.contains(appId)) { 0257 return url; 0258 } 0259 0260 // Try matching both appId and xWindowsWMClassName against StartupWMClass. 0261 // We do this before evaluating the mapping rules further, because StartupWMClass 0262 // is essentially a mapping rule, and we expect it to be set deliberately and 0263 // sensibly to instruct us what to do. Also, mapping rules 0264 // 0265 // StartupWMClass=STRING 0266 // 0267 // If true, it is KNOWN that the application will map at least one 0268 // window with the given string as its WM class or WM name hint. 0269 // 0270 // Source: https://specifications.freedesktop.org/startup-notification-spec/startup-notification-0.1.txt 0271 if (services.isEmpty() && !xWindowsWMClassName.isEmpty()) { 0272 services = KApplicationTrader::query([&xWindowsWMClassName](const KService::Ptr &service) { 0273 return service->property<QString>(QStringLiteral("StartupWMClass")).compare(xWindowsWMClassName, Qt::CaseInsensitive) == 0; 0274 }); 0275 sortServicesByMenuId(services, xWindowsWMClassName); 0276 } 0277 0278 if (services.isEmpty()) { 0279 services = KApplicationTrader::query([&appId](const KService::Ptr &service) { 0280 return service->property<QString>(QStringLiteral("StartupWMClass")).compare(appId, Qt::CaseInsensitive) == 0; 0281 }); 0282 sortServicesByMenuId(services, appId); 0283 } 0284 0285 // Evaluate rewrite rules from config. 0286 if (services.isEmpty()) { 0287 KConfigGroup rewriteRulesGroup(rulesConfig, QStringLiteral("Rewrite Rules")); 0288 if (rewriteRulesGroup.hasGroup(appId)) { 0289 KConfigGroup rewriteGroup(&rewriteRulesGroup, appId); 0290 0291 const QStringList &rules = rewriteGroup.groupList(); 0292 for (const QString &rule : rules) { 0293 KConfigGroup ruleGroup(&rewriteGroup, rule); 0294 0295 const QString propertyConfig = ruleGroup.readEntry(QStringLiteral("Property"), QString()); 0296 0297 QString matchProperty; 0298 if (propertyConfig == QLatin1String("ClassClass")) { 0299 matchProperty = appId; 0300 } else if (propertyConfig == QLatin1String("ClassName")) { 0301 matchProperty = xWindowsWMClassName; 0302 } 0303 0304 if (matchProperty.isEmpty()) { 0305 continue; 0306 } 0307 0308 const QString serviceSearchIdentifier = ruleGroup.readEntry(QStringLiteral("Identifier"), QString()); 0309 if (serviceSearchIdentifier.isEmpty()) { 0310 continue; 0311 } 0312 0313 QRegularExpression regExp(ruleGroup.readEntry(QStringLiteral("Match"))); 0314 const auto match = regExp.match(matchProperty); 0315 0316 if (match.hasMatch()) { 0317 const QString actualMatch = match.captured(QStringLiteral("match")); 0318 if (actualMatch.isEmpty()) { 0319 continue; 0320 } 0321 0322 QString rewrittenString = ruleGroup.readEntry(QStringLiteral("Target")).arg(actualMatch); 0323 // If no "Target" is provided, instead assume the matched property (appId/xWindowsWMClassName). 0324 if (rewrittenString.isEmpty()) { 0325 rewrittenString = matchProperty; 0326 } 0327 0328 services = KApplicationTrader::query([&rewrittenString, &serviceSearchIdentifier](const KService::Ptr &service) { 0329 return service->property<QString>(serviceSearchIdentifier).compare(rewrittenString, Qt::CaseInsensitive) == 0; 0330 }); 0331 sortServicesByMenuId(services, serviceSearchIdentifier); 0332 0333 if (!services.isEmpty()) { 0334 break; 0335 } 0336 } 0337 } 0338 } 0339 } 0340 0341 // The appId looks like a path. 0342 if (services.isEmpty() && appId.startsWith(QLatin1String("/"))) { 0343 // Check if it's a path to a .desktop file. 0344 if (KDesktopFile::isDesktopFile(appId) && QFile::exists(appId)) { 0345 return QUrl::fromLocalFile(appId); 0346 } 0347 0348 // Check if the appId passes as a .desktop file path if we add the extension. 0349 const QString appIdPlusExtension(appId + QStringLiteral(".desktop")); 0350 0351 if (KDesktopFile::isDesktopFile(appIdPlusExtension) && QFile::exists(appIdPlusExtension)) { 0352 return QUrl::fromLocalFile(appIdPlusExtension); 0353 } 0354 } 0355 0356 // Try matching mapped name against DesktopEntryName. 0357 if (!mapped.isEmpty() && services.isEmpty()) { 0358 services = KApplicationTrader::query([&mapped](const KService::Ptr &service) { 0359 return !service->noDisplay() && service->desktopEntryName().compare(mapped, Qt::CaseInsensitive) == 0; 0360 }); 0361 sortServicesByMenuId(services, mapped); 0362 } 0363 0364 // Try matching mapped name against 'Name'. 0365 if (!mapped.isEmpty() && services.isEmpty()) { 0366 services = KApplicationTrader::query([&mapped](const KService::Ptr &service) { 0367 return !service->noDisplay() && service->name().compare(mapped, Qt::CaseInsensitive) == 0; 0368 }); 0369 sortServicesByMenuId(services, mapped); 0370 } 0371 0372 // Try matching appId against DesktopEntryName. 0373 if (services.isEmpty()) { 0374 services = KApplicationTrader::query([&appId](const KService::Ptr &service) { 0375 return service->desktopEntryName().compare(appId, Qt::CaseInsensitive) == 0; 0376 }); 0377 sortServicesByMenuId(services, appId); 0378 } 0379 0380 // Try matching appId against 'Name'. 0381 // This has a shaky chance of success as appId is untranslated, but 'Name' may be localized. 0382 if (services.isEmpty()) { 0383 services = KApplicationTrader::query([&appId](const KService::Ptr &service) { 0384 return !service->noDisplay() && service->name().compare(appId, Qt::CaseInsensitive) == 0; 0385 }); 0386 sortServicesByMenuId(services, appId); 0387 } 0388 0389 // Check rules configuration for whether we want to hide this task. 0390 // Some window tasks update from bogus to useful metadata early during startup. 0391 // This config key allows listing the bogus metadata, and the matching window 0392 // tasks are hidden until they perform a metadate update that stops them from 0393 // matching. 0394 QStringList skipTaskbar = set.readEntry("SkipTaskbar", QStringList()); 0395 0396 if (skipTaskbar.contains(appId)) { 0397 QUrlQuery query(url); 0398 query.addQueryItem(QStringLiteral("skipTaskbar"), QStringLiteral("true")); 0399 url.setQuery(query); 0400 } else if (skipTaskbar.contains(mapped)) { 0401 QUrlQuery query(url); 0402 query.addQueryItem(QStringLiteral("skipTaskbar"), QStringLiteral("true")); 0403 url.setQuery(query); 0404 } 0405 } 0406 0407 // Ok, absolute *last* chance, try matching via pid (but only if we have not already tried this!) ... 0408 if (services.isEmpty() && !triedPid) { 0409 services = servicesFromPid(pid, rulesConfig); 0410 } 0411 } 0412 0413 // Try to improve on a possible from-binary fallback. 0414 // If no services were found or we got a fake-service back from getServicesViaPid() 0415 // we attempt to improve on this by adding a loosely matched reverse-domain-name 0416 // DesktopEntryName. Namely anything that is '*.appId.desktop' would qualify here. 0417 // 0418 // Illustrative example of a case where the above heuristics would fail to produce 0419 // a reasonable result: 0420 // - org.kde.dragonplayer.desktop 0421 // - binary is 'dragon' 0422 // - qapp appname and thus appId is 'dragonplayer' 0423 // - appId cannot directly match the desktop file because of RDN 0424 // - appId also cannot match the binary because of name mismatch 0425 // - in the following code *.appId can match org.kde.dragonplayer though 0426 if (!appId.isEmpty() /* BUG 472576 */ && (services.isEmpty() || services.at(0)->desktopEntryName().isEmpty())) { 0427 auto matchingServices = KApplicationTrader::query([&appId](const KService::Ptr &service) { 0428 return !service->noDisplay() && service->desktopEntryName().contains(appId, Qt::CaseInsensitive); 0429 }); 0430 0431 QMutableListIterator<KService::Ptr> it(matchingServices); 0432 while (it.hasNext()) { 0433 auto service = it.next(); 0434 if (!service->desktopEntryName().endsWith("." + appId)) { 0435 it.remove(); 0436 } 0437 } 0438 // Exactly one match is expected, otherwise we discard the results as to reduce 0439 // the likelihood of false-positive mappings. Since we essentially eliminate the 0440 // uniqueness that RDN is meant to bring to the table we could potentially end 0441 // up with more than one match here. 0442 if (matchingServices.length() == 1) { 0443 services = matchingServices; 0444 } 0445 } 0446 0447 if (!services.isEmpty()) { 0448 const QString &menuId = services.at(0)->menuId(); 0449 0450 // applications: URLs are used to refer to applications by their KService::menuId 0451 // (i.e. .desktop file name) rather than the absolute path to a .desktop file. 0452 if (!menuId.isEmpty()) { 0453 url.setUrl(QStringLiteral("applications:") + menuId); 0454 return url; 0455 } 0456 0457 QString path = services.at(0)->entryPath(); 0458 0459 if (path.isEmpty()) { 0460 path = services.at(0)->exec(); 0461 } 0462 0463 if (!path.isEmpty()) { 0464 QString query = url.query(); 0465 url = QUrl::fromLocalFile(path); 0466 url.setQuery(query); 0467 return url; 0468 } 0469 } 0470 0471 return url; 0472 } 0473 0474 KService::List servicesFromPid(quint32 pid, KSharedConfig::Ptr rulesConfig) 0475 { 0476 if (pid == 0) { 0477 return KService::List(); 0478 } 0479 0480 if (!rulesConfig) { 0481 return KService::List(); 0482 } 0483 0484 // Read the BAMF_DESKTOP_FILE_HINT environment variable which contains the actual desktop file path for Snaps. 0485 QFile environFile(QStringLiteral("/proc/%1/environ").arg(QString::number(pid))); 0486 if (environFile.open(QIODevice::ReadOnly | QIODevice::Text)) { 0487 constexpr QByteArrayView bamfDesktopFileHint{"BAMF_DESKTOP_FILE_HINT"}; 0488 constexpr QByteArrayView appDir{"APPDIR"}; 0489 const auto lines = environFile.readAll().split('\0'); 0490 for (const QByteArrayView line : lines) { 0491 const int equalsIdx = line.indexOf('='); 0492 if (equalsIdx <= 0) { 0493 continue; 0494 } 0495 0496 const QByteArrayView key = line.sliced(0, equalsIdx); 0497 if (key == bamfDesktopFileHint) { 0498 const QByteArrayView value = line.sliced(equalsIdx + 1); 0499 0500 KService::Ptr service = KService::serviceByDesktopPath(QString::fromUtf8(value)); 0501 if (service) { 0502 return {service}; 0503 } 0504 break; 0505 } else if (key == appDir) { 0506 // For AppImage 0507 const QByteArrayView value = line.sliced(equalsIdx + 1); 0508 const auto desktopFileList = QDir(QString::fromUtf8(value)).entryInfoList(QStringList{QStringLiteral("*.desktop")}, QDir::Files); 0509 if (!desktopFileList.empty()) { 0510 return {QExplicitlySharedDataPointer<KService>(new KService(desktopFileList[0].absoluteFilePath()))}; 0511 } 0512 break; 0513 } 0514 } 0515 } 0516 0517 auto proc = KProcessList::processInfo(pid); 0518 if (!proc.isValid()) { 0519 return KService::List(); 0520 } 0521 0522 const QString cmdLine = proc.command(); 0523 0524 if (cmdLine.isEmpty()) { 0525 return KService::List(); 0526 } 0527 0528 return servicesFromCmdLine(cmdLine, proc.name(), rulesConfig); 0529 } 0530 0531 KService::List servicesFromCmdLine(const QString &_cmdLine, const QString &processName, KSharedConfig::Ptr rulesConfig) 0532 { 0533 QString cmdLine = _cmdLine; 0534 KService::List services; 0535 0536 if (!rulesConfig) { 0537 return services; 0538 } 0539 0540 const int firstSpace = cmdLine.indexOf(' '); 0541 int slash = 0; 0542 0543 services = KApplicationTrader::query([&cmdLine](const KService::Ptr &service) { 0544 return service->exec() == cmdLine; 0545 }); 0546 0547 if (services.isEmpty()) { 0548 // Could not find with complete command line, so strip out the path part ... 0549 slash = cmdLine.lastIndexOf('/', firstSpace); 0550 0551 if (slash > 0) { 0552 const QStringView midCmd = QStringView(cmdLine).mid(slash + 1); 0553 services = services = KApplicationTrader::query([&midCmd](const KService::Ptr &service) { 0554 return service->exec() == midCmd; 0555 }); 0556 } 0557 } 0558 0559 if (services.isEmpty() && firstSpace > 0) { 0560 // Could not find with arguments, so try without ... 0561 cmdLine.truncate(firstSpace); 0562 0563 services = KApplicationTrader::query([&cmdLine](const KService::Ptr &service) { 0564 return service->exec() == cmdLine; 0565 }); 0566 0567 if (services.isEmpty()) { 0568 slash = cmdLine.lastIndexOf('/'); 0569 0570 if (slash > 0) { 0571 const QStringView midCmd = QStringView(cmdLine).mid(slash + 1); 0572 services = KApplicationTrader::query([&midCmd](const KService::Ptr &service) { 0573 return service->exec() == midCmd; 0574 }); 0575 } 0576 } 0577 } 0578 0579 if (services.isEmpty()) { 0580 KConfigGroup set(rulesConfig, u"Settings"_s); 0581 const QStringList &runtimes = set.readEntry("TryIgnoreRuntimes", QStringList()); 0582 0583 bool ignore = runtimes.contains(cmdLine); 0584 0585 if (!ignore && slash > 0) { 0586 ignore = runtimes.contains(cmdLine.mid(slash + 1)); 0587 } 0588 0589 if (ignore) { 0590 return servicesFromCmdLine(_cmdLine.mid(firstSpace + 1), processName, rulesConfig); 0591 } 0592 } 0593 0594 if (services.isEmpty() && !processName.isEmpty() && !QStandardPaths::findExecutable(cmdLine).isEmpty()) { 0595 // cmdLine now exists without arguments if there were any. 0596 services << QExplicitlySharedDataPointer<KService>(new KService(processName, cmdLine, QString())); 0597 } 0598 0599 return services; 0600 } 0601 0602 QString defaultApplication(const QUrl &url) 0603 { 0604 if (url.scheme() != QLatin1String("preferred")) { 0605 return QString(); 0606 } 0607 0608 const QString &application = url.host(); 0609 0610 if (application.isEmpty()) { 0611 return QString(); 0612 } 0613 0614 if (application.compare(QLatin1String("mailer"), Qt::CaseInsensitive) == 0) { 0615 KEMailSettings settings; 0616 0617 // In KToolInvocation, the default is kmail; but let's be friendlier. 0618 QString command = settings.getSetting(KEMailSettings::ClientProgram); 0619 0620 if (command.isEmpty()) { 0621 if (KService::Ptr kontact = KService::serviceByStorageId(QStringLiteral("kontact"))) { 0622 return kontact->storageId(); 0623 } else if (KService::Ptr kmail = KService::serviceByStorageId(QStringLiteral("kmail"))) { 0624 return kmail->storageId(); 0625 } 0626 } 0627 0628 if (!command.isEmpty()) { 0629 if (settings.getSetting(KEMailSettings::ClientTerminal) == QLatin1String("true")) { 0630 KConfigGroup confGroup(KSharedConfig::openConfig(), u"General"_s); 0631 const QString preferredTerminal = confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole")); 0632 command = preferredTerminal + QLatin1String(" -e ") + command; 0633 } 0634 0635 return command; 0636 } 0637 } else if (application.compare(QLatin1String("browser"), Qt::CaseInsensitive) == 0) { 0638 const auto service = DefaultService::browser(); 0639 return service ? service->storageId() : DefaultService::legacyBrowserExec(); 0640 } else if (application.compare(QLatin1String("terminal"), Qt::CaseInsensitive) == 0) { 0641 KConfigGroup confGroup(KSharedConfig::openConfig(), u"General"_s); 0642 0643 return confGroup.readPathEntry("TerminalApplication", KService::serviceByStorageId(QStringLiteral("konsole")) ? QStringLiteral("konsole") : QString()); 0644 } else if (application.compare(QLatin1String("filemanager"), Qt::CaseInsensitive) == 0) { 0645 KService::Ptr service = KApplicationTrader::preferredService(QStringLiteral("inode/directory")); 0646 0647 if (service) { 0648 return service->storageId(); 0649 } 0650 } else if (KService::Ptr service = KApplicationTrader::preferredService(application)) { 0651 return service->storageId(); 0652 } 0653 0654 return QLatin1String(""); 0655 } 0656 0657 bool launcherUrlsMatch(const QUrl &a, const QUrl &b, UrlComparisonMode mode) 0658 { 0659 QUrl sanitizedA = a; 0660 QUrl sanitizedB = b; 0661 0662 if (mode == IgnoreQueryItems) { 0663 sanitizedA = a.adjusted(QUrl::RemoveQuery); 0664 sanitizedB = b.adjusted(QUrl::RemoveQuery); 0665 } 0666 0667 auto tryResolveToApplicationsUrl = [](const QUrl &url) -> QUrl { 0668 QUrl resolvedUrl = url; 0669 0670 if (url.isLocalFile() && KDesktopFile::isDesktopFile(url.toLocalFile())) { 0671 KDesktopFile f(url.toLocalFile()); 0672 0673 const KService::Ptr service = KService::serviceByStorageId(f.fileName()); 0674 0675 // Resolve to non-absolute menuId-based URL if possible. 0676 if (service) { 0677 const QString &menuId = service->menuId(); 0678 0679 if (!menuId.isEmpty()) { 0680 resolvedUrl = QUrl(QLatin1String("applications:") + menuId); 0681 resolvedUrl.setQuery(url.query()); 0682 } 0683 } 0684 } 0685 0686 return resolvedUrl; 0687 }; 0688 0689 sanitizedA = tryResolveToApplicationsUrl(sanitizedA); 0690 sanitizedB = tryResolveToApplicationsUrl(sanitizedB); 0691 0692 return (sanitizedA == sanitizedB); 0693 } 0694 0695 bool appsMatch(const QModelIndex &a, const QModelIndex &b) 0696 { 0697 const QString &aAppId = a.data(AbstractTasksModel::AppId).toString(); 0698 const QString &bAppId = b.data(AbstractTasksModel::AppId).toString(); 0699 0700 if (!aAppId.isEmpty() && aAppId == bAppId) { 0701 return true; 0702 } 0703 0704 const QUrl &aUrl = a.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl(); 0705 const QUrl &bUrl = b.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl(); 0706 0707 if (aUrl.isValid() && aUrl == bUrl) { 0708 return true; 0709 } 0710 0711 return false; 0712 } 0713 0714 QRect screenGeometry(const QPoint &pos) 0715 { 0716 if (pos.isNull()) { 0717 return QRect(); 0718 } 0719 0720 const QList<QScreen *> &screens = QGuiApplication::screens(); 0721 QRect screenGeometry; 0722 int shortestDistance = INT_MAX; 0723 0724 for (int i = 0; i < screens.count(); ++i) { 0725 const QRect &geometry = screens.at(i)->geometry(); 0726 0727 if (geometry.contains(pos)) { 0728 return geometry; 0729 } 0730 0731 int distance = QPoint(geometry.topLeft() - pos).manhattanLength(); 0732 distance = qMin(distance, QPoint(geometry.topRight() - pos).manhattanLength()); 0733 distance = qMin(distance, QPoint(geometry.bottomRight() - pos).manhattanLength()); 0734 distance = qMin(distance, QPoint(geometry.bottomLeft() - pos).manhattanLength()); 0735 0736 if (distance < shortestDistance) { 0737 shortestDistance = distance; 0738 screenGeometry = geometry; 0739 } 0740 } 0741 0742 return screenGeometry; 0743 } 0744 0745 void runApp(const AppData &appData, const QList<QUrl> &urls) 0746 { 0747 if (appData.url.isValid()) { 0748 KService::Ptr service; 0749 0750 // applications: URLs are used to refer to applications by their KService::menuId 0751 // (i.e. .desktop file name) rather than the absolute path to a .desktop file. 0752 if (appData.url.scheme() == QLatin1String("applications")) { 0753 service = KService::serviceByMenuId(appData.url.path()); 0754 } else if (appData.url.scheme() == QLatin1String("preferred")) { 0755 service = KService::serviceByStorageId(defaultApplication(appData.url)); 0756 } else { 0757 service = KService::serviceByDesktopPath(appData.url.toLocalFile()); 0758 } 0759 0760 if (service && service->isApplication()) { 0761 auto *job = new KIO::ApplicationLauncherJob(service); 0762 job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled)); 0763 job->setUrls(urls); 0764 job->start(); 0765 0766 KActivities::ResourceInstance::notifyAccessed(QUrl(QStringLiteral("applications:") + service->storageId()), 0767 QStringLiteral("org.kde.libtaskmanager")); 0768 } else { 0769 auto *job = new KIO::OpenUrlJob(appData.url); 0770 job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled)); 0771 job->setRunExecutables(true); 0772 job->start(); 0773 0774 if (!appData.id.isEmpty()) { 0775 KActivities::ResourceInstance::notifyAccessed(QUrl(QStringLiteral("applications:") + appData.id), QStringLiteral("org.kde.libtaskmanager")); 0776 } 0777 } 0778 } 0779 } 0780 0781 bool canLauchNewInstance(const AppData &appData) 0782 { 0783 if (appData.url.isEmpty()) { 0784 return false; 0785 } 0786 0787 QString desktopEntry = appData.id; 0788 0789 // Remove suffix if necessary 0790 if (desktopEntry.endsWith(QLatin1String(".desktop"))) { 0791 desktopEntry.chop(8); 0792 } 0793 0794 const KService::Ptr service = KService::serviceByDesktopName(desktopEntry); 0795 0796 if (service) { 0797 if (service->noDisplay()) { 0798 return false; 0799 } 0800 0801 if (service->property<bool>(QStringLiteral("SingleMainWindow"))) { 0802 return false; 0803 } 0804 0805 // GNOME-specific key, for backwards compatibility with apps that haven't 0806 // started using the XDG "SingleMainWindow" key yet 0807 if (service->property<bool>(QStringLiteral("X-GNOME-SingleWindow"))) { 0808 return false; 0809 } 0810 0811 // Hide our own action if there's already a "New Window" action 0812 const auto actions = service->actions(); 0813 for (const KServiceAction &action : actions) { 0814 if (action.name().startsWith("new", Qt::CaseInsensitive) && action.name().endsWith("window", Qt::CaseInsensitive)) { 0815 return false; 0816 } 0817 0818 if (action.name() == QLatin1String("WindowNew")) { 0819 return false; 0820 } 0821 } 0822 } 0823 0824 return true; 0825 } 0826 }