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