diff --git a/libtaskmanager/tasktools.cpp b/libtaskmanager/tasktools.cpp index 342d3c462..de806efc2 100644 --- a/libtaskmanager/tasktools.cpp +++ b/libtaskmanager/tasktools.cpp @@ -1,715 +1,744 @@ /******************************************************************** Copyright 2016 Eike Hein This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . *********************************************************************/ #include "tasktools.h" #include "abstracttasksmodel.h" #include "tasktools.h" #include "abstracttasksmodel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if HAVE_X11 #include #endif namespace TaskManager { AppData appDataFromUrl(const QUrl &url, const QIcon &fallbackIcon) { AppData data; data.url = url; if (url.hasQuery()) { QUrlQuery uQuery(url); if (uQuery.hasQueryItem(QLatin1String("iconData"))) { QString iconData(uQuery.queryItemValue(QLatin1String("iconData"))); QPixmap pixmap; QByteArray bytes = QByteArray::fromBase64(iconData.toLocal8Bit(), QByteArray::Base64UrlEncoding); pixmap.loadFromData(bytes); data.icon.addPixmap(pixmap); } } // applications: URLs are used to refer to applications by their KService::menuId // (i.e. .desktop file name) rather than the absolute path to a .desktop file. if (url.scheme() == QStringLiteral("applications")) { const KService::Ptr service = KService::serviceByMenuId(url.path()); if (service && url.path() == service->menuId()) { data.name = service->name(); data.genericName = service->genericName(); data.id = service->storageId(); if (data.icon.isNull()) { data.icon = QIcon::fromTheme(service->icon()); } } } if (url.isLocalFile() && KDesktopFile::isDesktopFile(url.toLocalFile())) { KDesktopFile f(url.toLocalFile()); const KService::Ptr service = KService::serviceByStorageId(f.fileName()); // Resolve to non-absolute menuId-based URL if possible. if (service) { const QString &menuId = service->menuId(); if (!menuId.isEmpty()) { data.url = QUrl(QStringLiteral("applications:") + menuId); } } if (service && QUrl::fromLocalFile(service->entryPath()) == url) { data.name = service->name(); data.genericName = service->genericName(); data.id = service->storageId(); if (data.icon.isNull()) { data.icon = QIcon::fromTheme(service->icon()); } } else if (f.tryExec()) { data.name = f.readName(); data.genericName = f.readGenericName(); data.id = QUrl::fromLocalFile(f.fileName()).fileName(); if (data.icon.isNull()) { data.icon = QIcon::fromTheme(f.readIcon()); } } if (data.id.endsWith(".desktop")) { data.id = data.id.left(data.id.length() - 8); } } else if (url.scheme() == QLatin1String("preferred")) { data.id = defaultApplication(url); const KService::Ptr service = KService::serviceByStorageId(data.id); if (service) { const QString &menuId = service->menuId(); const QString &desktopFile = service->entryPath(); data.name = service->name(); data.genericName = service->genericName(); data.id = service->storageId(); if (data.icon.isNull()) { data.icon = QIcon::fromTheme(service->icon()); } // Update with resolved URL. if (!menuId.isEmpty()) { data.url = QUrl(QStringLiteral("applications:") + menuId); } else { data.url = QUrl::fromLocalFile(desktopFile); } } } if (data.name.isEmpty()) { data.name = url.fileName(); } if (data.icon.isNull()) { data.icon = fallbackIcon; } return data; } AppData appDataFromAppId(const QString &appId) { AppData data; KService::Ptr service = KService::serviceByStorageId(appId); if (service) { data.id = service->storageId(); data.name = service->name(); data.genericName = service->genericName(); const QString &menuId = service->menuId(); // applications: URLs are used to refer to applications by their KService::menuId // (i.e. .desktop file name) rather than the absolute path to a .desktop file. if (!menuId.isEmpty()) { data.url = QUrl(QStringLiteral("applications:") + menuId); } else { data.url = QUrl::fromLocalFile(service->entryPath()); } return data; } QString desktopFile = appId; if (!desktopFile.endsWith(QLatin1String(".desktop"))) { desktopFile.append(QLatin1String(".desktop")); } if (KDesktopFile::isDesktopFile(desktopFile) && QFile::exists(desktopFile)) { KDesktopFile f(desktopFile); data.id = QUrl::fromLocalFile(f.fileName()).fileName(); if (data.id.endsWith(QLatin1String(".desktop"))) { data.id = data.id.left(data.id.length() - 8); } data.name = f.readName(); data.genericName = f.readGenericName(); data.url = QUrl::fromLocalFile(desktopFile); } return data; } QUrl windowUrlFromMetadata(const QString &appId, quint32 pid, KSharedConfig::Ptr rulesConfig, const QString &xWindowsWMClassName) { if (!rulesConfig) { return QUrl(); } QUrl url; KService::List services; bool triedPid = false; if (!(appId.isEmpty() && xWindowsWMClassName.isEmpty())) { // Check to see if this wmClass matched a saved one ... KConfigGroup grp(rulesConfig, "Mapping"); KConfigGroup set(rulesConfig, "Settings"); // Evaluate MatchCommandLineFirst directives from config first. // Some apps have different launchers depending upon command line ... QStringList matchCommandLineFirst = set.readEntry("MatchCommandLineFirst", QStringList()); if (!appId.isEmpty() && matchCommandLineFirst.contains(appId)) { triedPid = true; services = servicesFromPid(pid, rulesConfig); } // Try to match using xWindowsWMClassName also. if (!xWindowsWMClassName.isEmpty() && matchCommandLineFirst.contains("::"+xWindowsWMClassName)) { triedPid = true; services = servicesFromPid(pid, rulesConfig); } if (!appId.isEmpty()) { // Evaluate any mapping rules that map to a specific .desktop file. QString mapped(grp.readEntry(appId + "::" + xWindowsWMClassName, QString())); if (mapped.endsWith(QLatin1String(".desktop"))) { url = QUrl(mapped); return url; } if (mapped.isEmpty()) { mapped = grp.readEntry(appId, QString()); if (mapped.endsWith(QLatin1String(".desktop"))) { url = QUrl(mapped); return url; } } // Some apps, such as Wine, cannot use xWindowsWMClassName to map to launcher name - as Wine itself is not a GUI app // So, Settings/ManualOnly lists window classes where the user will always have to manualy set the launcher ... QStringList manualOnly = set.readEntry("ManualOnly", QStringList()); if (!appId.isEmpty() && manualOnly.contains(appId)) { return url; } // Try matching both appId and xWindowsWMClassName against StartupWMClass. // We do this before evaluating the mapping rules further, because StartupWMClass // is essentially a mapping rule, and we expect it to be set deliberately and // sensibly to instruct us what to do. Also, mapping rules // // StartupWMClass=STRING // // If true, it is KNOWN that the application will map at least one // window with the given string as its WM class or WM name hint. // // Source: https://specifications.freedesktop.org/startup-notification-spec/startup-notification-0.1.txt if (services.empty()) { services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ StartupWMClass)").arg(appId)); } if (services.empty()) { services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ StartupWMClass)").arg(xWindowsWMClassName)); } // Evaluate rewrite rules from config. if (services.empty()) { KConfigGroup rewriteRulesGroup(rulesConfig, QStringLiteral("Rewrite Rules")); if (rewriteRulesGroup.hasGroup(appId)) { KConfigGroup rewriteGroup(&rewriteRulesGroup, appId); const QStringList &rules = rewriteGroup.groupList(); for (const QString &rule : rules) { KConfigGroup ruleGroup(&rewriteGroup, rule); const QString propertyConfig = ruleGroup.readEntry(QStringLiteral("Property"), QString()); QString matchProperty; if (propertyConfig == QLatin1String("ClassClass")) { matchProperty = appId; } else if (propertyConfig == QLatin1String("ClassName")) { matchProperty = xWindowsWMClassName; } if (matchProperty.isEmpty()) { continue; } const QString serviceSearchIdentifier = ruleGroup.readEntry(QStringLiteral("Identifier"), QString()); if (serviceSearchIdentifier.isEmpty()) { continue; } QRegularExpression regExp(ruleGroup.readEntry(QStringLiteral("Match"))); const auto match = regExp.match(matchProperty); if (match.hasMatch()) { const QString actualMatch = match.captured(QStringLiteral("match")); if (actualMatch.isEmpty()) { continue; } QString rewrittenString = ruleGroup.readEntry(QStringLiteral("Target")).arg(actualMatch); // If no "Target" is provided, instead assume the matched property (appId/xWindowsWMClassName). if (rewrittenString.isEmpty()) { rewrittenString = matchProperty; } services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ %2)").arg(rewrittenString, serviceSearchIdentifier)); if (!services.isEmpty()) { break; } } } } } // The appId looks like a path. if (appId.startsWith(QStringLiteral("/"))) { // Check if it's a path to a .desktop file. if (KDesktopFile::isDesktopFile(appId) && QFile::exists(appId)) { return QUrl::fromLocalFile(appId); } // Check if the appId passes as a .desktop file path if we add the extension. const QString appIdPlusExtension(appId + QStringLiteral(".desktop")); if (KDesktopFile::isDesktopFile(appIdPlusExtension) && QFile::exists(appIdPlusExtension)) { return QUrl::fromLocalFile(appIdPlusExtension); } } // Try matching mapped name against DesktopEntryName. if (!mapped.isEmpty() && services.empty()) { services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ DesktopEntryName)").arg(mapped)); } // Try matching mapped name against 'Name'. if (!mapped.isEmpty() && services.empty()) { services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Name) and (not exist NoDisplay or not NoDisplay)").arg(mapped)); } // Try matching appId against DesktopEntryName. if (services.empty()) { services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ DesktopEntryName)").arg(appId)); } // Try matching appId against 'Name'. // This has a shaky chance of success as appId is untranslated, but 'Name' may be localized. if (services.empty()) { services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Name) and (not exist NoDisplay or not NoDisplay)").arg(appId)); } } // Ok, absolute *last* chance, try matching via pid (but only if we have not already tried this!) ... if (services.empty() && !triedPid) { services = servicesFromPid(pid, rulesConfig); } } // Try to improve on a possible from-binary fallback. // If no services were found or we got a fake-service back from getServicesViaPid() // we attempt to improve on this by adding a loosely matched reverse-domain-name // DesktopEntryName. Namely anything that is '*.appId.desktop' would qualify here. // // Illustrative example of a case where the above heuristics would fail to produce // a reasonable result: // - org.kde.dragonplayer.desktop // - binary is 'dragon' // - qapp appname and thus appId is 'dragonplayer' // - appId cannot directly match the desktop file because of RDN // - appId also cannot match the binary because of name mismatch // - in the following code *.appId can match org.kde.dragonplayer though if (services.empty() || services.at(0)->desktopEntryName().isEmpty()) { auto matchingServices = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' ~~ DesktopEntryName)").arg(appId)); QMutableListIterator it(matchingServices); while (it.hasNext()) { auto service = it.next(); if (!service->desktopEntryName().endsWith("." + appId)) { it.remove(); } } // Exactly one match is expected, otherwise we discard the results as to reduce // the likelihood of false-positive mappings. Since we essentially eliminate the // uniqueness that RDN is meant to bring to the table we could potentially end // up with more than one match here. if (matchingServices.length() == 1) { services = matchingServices; } } if (!services.empty()) { const QString &menuId = services.at(0)->menuId(); // applications: URLs are used to refer to applications by their KService::menuId // (i.e. .desktop file name) rather than the absolute path to a .desktop file. if (!menuId.isEmpty()) { return QUrl(QStringLiteral("applications:") + menuId); } QString path = services.at(0)->entryPath(); if (path.isEmpty()) { path = services.at(0)->exec(); } if (!path.isEmpty()) { url = QUrl::fromLocalFile(path); } } return url; } KService::List servicesFromPid(quint32 pid, KSharedConfig::Ptr rulesConfig) { if (pid == 0) { return KService::List(); } if (!rulesConfig) { return KService::List(); } KSysGuard::Processes procs; procs.updateOrAddProcess(pid); KSysGuard::Process *proc = procs.getProcess(pid); const QString &cmdLine = proc ? proc->command().simplified() : QString(); // proc->command has a trailing space??? if (cmdLine.isEmpty()) { return KService::List(); } return servicesFromCmdLine(cmdLine, proc->name(), rulesConfig); } KService::List servicesFromCmdLine(const QString &_cmdLine, const QString &processName, KSharedConfig::Ptr rulesConfig) { QString cmdLine = _cmdLine; KService::List services; if (!rulesConfig) { return services; } const int firstSpace = cmdLine.indexOf(' '); int slash = 0; services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdLine)); if (services.empty()) { // Could not find with complete command line, so strip out the path part ... slash = cmdLine.lastIndexOf('/', firstSpace); if (slash > 0) { services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdLine.mid(slash + 1))); } } if (services.empty() && firstSpace > 0) { // Could not find with arguments, so try without ... cmdLine = cmdLine.left(firstSpace); services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdLine)); if (services.empty()) { slash = cmdLine.lastIndexOf('/'); if (slash > 0) { services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdLine.mid(slash + 1))); } } } if (services.empty()) { KConfigGroup set(rulesConfig, "Settings"); const QStringList &runtimes = set.readEntry("TryIgnoreRuntimes", QStringList()); bool ignore = runtimes.contains(cmdLine); if (!ignore && slash > 0) { ignore = runtimes.contains(cmdLine.mid(slash + 1)); } if (ignore) { return servicesFromCmdLine(_cmdLine.mid(firstSpace + 1), processName, rulesConfig); } } if (services.empty() && !processName.isEmpty() && !QStandardPaths::findExecutable(cmdLine).isEmpty()) { // cmdLine now exists without arguments if there were any. services << QExplicitlySharedDataPointer(new KService(processName, cmdLine, QString())); } return services; } QString defaultApplication(const QUrl &url) { if (url.scheme() != QLatin1String("preferred")) { return QString(); } const QString &application = url.host(); if (application.isEmpty()) { return QString(); } if (application.compare(QLatin1String("mailer"), Qt::CaseInsensitive) == 0) { KEMailSettings settings; // In KToolInvocation, the default is kmail; but let's be friendlier. QString command = settings.getSetting(KEMailSettings::ClientProgram); if (command.isEmpty()) { if (KService::Ptr kontact = KService::serviceByStorageId(QStringLiteral("kontact"))) { return kontact->storageId(); } else if (KService::Ptr kmail = KService::serviceByStorageId(QStringLiteral("kmail"))) { return kmail->storageId(); } } if (!command.isEmpty()) { if (settings.getSetting(KEMailSettings::ClientTerminal) == QLatin1String("true")) { KConfigGroup confGroup(KSharedConfig::openConfig(), "General"); const QString preferredTerminal = confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole")); command = preferredTerminal + QLatin1String(" -e ") + command; } return command; } } else if (application.compare(QLatin1String("browser"), Qt::CaseInsensitive) == 0) { KConfigGroup config(KSharedConfig::openConfig(), "General"); QString browserApp = config.readPathEntry("BrowserApplication", QString()); if (browserApp.isEmpty()) { const KService::Ptr htmlApp = KMimeTypeTrader::self()->preferredService(QStringLiteral("text/html")); if (htmlApp) { browserApp = htmlApp->storageId(); } } else if (browserApp.startsWith('!')) { browserApp = browserApp.mid(1); } return browserApp; } else if (application.compare(QLatin1String("terminal"), Qt::CaseInsensitive) == 0) { KConfigGroup confGroup(KSharedConfig::openConfig(), "General"); return confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole")); } else if (application.compare(QLatin1String("filemanager"), Qt::CaseInsensitive) == 0) { KService::Ptr service = KMimeTypeTrader::self()->preferredService(QStringLiteral("inode/directory")); if (service) { return service->storageId(); } } else if (KService::Ptr service = KMimeTypeTrader::self()->preferredService(application)) { return service->storageId(); } else { // Try the files in share/apps/kcm_componentchooser/*.desktop. QStringList directories = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kcm_componentchooser"), QStandardPaths::LocateDirectory); QStringList services; foreach(const QString& directory, directories) { QDir dir(directory); foreach(const QString& f, dir.entryList(QStringList("*.desktop"))) services += dir.absoluteFilePath(f); } foreach (const QString & service, services) { KConfig config(service, KConfig::SimpleConfig); KConfigGroup cg = config.group(QByteArray()); const QString type = cg.readEntry("valueName", QString()); if (type.compare(application, Qt::CaseInsensitive) == 0) { KConfig store(cg.readPathEntry("storeInFile", QStringLiteral("null"))); KConfigGroup storeCg(&store, cg.readEntry("valueSection", QString())); const QString exec = storeCg.readPathEntry(cg.readEntry("valueName", "kcm_componenchooser_null"), cg.readEntry("defaultImplementation", QString())); if (!exec.isEmpty()) { return exec; } break; } } } return QString(""); } bool launcherUrlsMatch(const QUrl &a, const QUrl &b, UrlComparisonMode mode) { + QUrl sanitizedA = a; + QUrl sanitizedB = b; + if (mode == IgnoreQueryItems) { - return (a.adjusted(QUrl::RemoveQuery) == b.adjusted(QUrl::RemoveQuery)); + sanitizedA = a.adjusted(QUrl::RemoveQuery); + sanitizedB = b.adjusted(QUrl::RemoveQuery); } - return (a == b); + auto tryResolveToApplicationsUrl = [](const QUrl &url) -> QUrl { + QUrl resolvedUrl = url; + + if (url.isLocalFile() && KDesktopFile::isDesktopFile(url.toLocalFile())) { + KDesktopFile f(url.toLocalFile()); + + const KService::Ptr service = KService::serviceByStorageId(f.fileName()); + + // Resolve to non-absolute menuId-based URL if possible. + if (service) { + const QString &menuId = service->menuId(); + + if (!menuId.isEmpty()) { + resolvedUrl = QUrl(QStringLiteral("applications:") + menuId); + resolvedUrl.setQuery(url.query()); + } + } + } + + return resolvedUrl; + }; + + sanitizedA = tryResolveToApplicationsUrl(sanitizedA); + sanitizedB = tryResolveToApplicationsUrl(sanitizedB); + + return (sanitizedA == sanitizedB); } bool appsMatch(const QModelIndex &a, const QModelIndex &b) { const QString &aAppId = a.data(AbstractTasksModel::AppId).toString(); const QString &bAppId = b.data(AbstractTasksModel::AppId).toString(); if (!aAppId.isEmpty() && aAppId == bAppId) { return true; } const QUrl &aUrl = a.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl(); const QUrl &bUrl = b.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl(); if (aUrl.isValid() && aUrl == bUrl) { return true; } return false; } QRect screenGeometry(const QPoint &pos) { if (pos.isNull()) { return QRect(); } const QList &screens = QGuiApplication::screens(); QRect screenGeometry; int shortestDistance = INT_MAX; for (int i = 0; i < screens.count(); ++i) { const QRect &geometry = screens.at(i)->geometry(); if (geometry.contains(pos)) { return geometry; } int distance = QPoint(geometry.topLeft() - pos).manhattanLength(); distance = qMin(distance, QPoint(geometry.topRight() - pos).manhattanLength()); distance = qMin(distance, QPoint(geometry.bottomRight() - pos).manhattanLength()); distance = qMin(distance, QPoint(geometry.bottomLeft() - pos).manhattanLength()); if (distance < shortestDistance) { shortestDistance = distance; screenGeometry = geometry; } } return screenGeometry; } void runApp(const AppData &appData, const QList &urls) { if (appData.url.isValid()) { quint32 timeStamp = 0; #if HAVE_X11 if (KWindowSystem::isPlatformX11()) { timeStamp = QX11Info::appUserTime(); } #endif KService::Ptr service; // applications: URLs are used to refer to applications by their KService::menuId // (i.e. .desktop file name) rather than the absolute path to a .desktop file. if (appData.url.scheme() == QStringLiteral("applications")) { service = KService::serviceByMenuId(appData.url.path()); } else if (appData.url.scheme() == QLatin1String("preferred")) { const KService::Ptr service = KService::serviceByStorageId(defaultApplication(appData.url)); } else { service = KService::serviceByDesktopPath(appData.url.toLocalFile()); } if (service && service->isApplication()) { KRun::runApplication(*service, urls, nullptr, 0, {}, KStartupInfo::createNewStartupIdForTimestamp(timeStamp)); KActivities::ResourceInstance::notifyAccessed(QUrl(QStringLiteral("applications:") + service->storageId()), QStringLiteral("org.kde.libtaskmanager")); } else { new KRun(appData.url, 0, false, KStartupInfo::createNewStartupIdForTimestamp(timeStamp)); if (!appData.id.isEmpty()) { KActivities::ResourceInstance::notifyAccessed(QUrl(QStringLiteral("applications:") + appData.id), QStringLiteral("org.kde.libtaskmanager")); } } } } }