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 }