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 }