diff --git a/libtaskmanager/tasktools.h b/libtaskmanager/tasktools.h --- a/libtaskmanager/tasktools.h +++ b/libtaskmanager/tasktools.h @@ -27,6 +27,9 @@ #include #include +#include +#include + namespace TaskManager { @@ -65,20 +68,71 @@ TASKMANAGER_EXPORT AppData appDataFromUrl(const QUrl &url, const QIcon &fallbackIcon = QIcon()); /** - * Fills in and returns an AppData struct based on the given application - * id. + * Takes several bits of window metadata as input and tries to find + * the .desktop file for the application owning this window, or, + * failing that, the path to its executable. + * + * The source for the metadata is generally the the window's appId on + * Wayland, or the window class part of the WM_CLASS window property + * on X Windows. + * + * TODO: The supplied config object can contain various mapping and + * mangling rules that affect the behavior of this function, allowing + * to map bits of metadata to different values and other things. This + * config file format still needs to be documented fully; in the + * meantime the bundled default rules in taskmanagerrulesrc (the + * config file opened by various models in this library) can be used + * for reference. + * + * @param appId A string uniquely identifying the application owning + * the window, ideally matching a .desktop file name. + * @param pid The process id for the process owning the window. + * @param rulesConfig A KConfig object parameterizing the matching + * behavior. + * @param xWindowsWMClassName The instance name part of X Windows' + * WM_CLASS window property. + * @returns A .desktop file or executable path for the application + * owning the window. + */ +TASKMANAGER_EXPORT QUrl windowUrlFromMetadata(const QString &appId, quint32 pid = 0, + KSharedConfig::Ptr config = KSharedConfig::Ptr(), const QString &xWindowsWMClassName = QString()); + +/** + * Returns a list of (usually application) KService instances for the + * given process id, by examining the process and querying the service + * database for process metadata. * - * Application ids are .desktop file names sans extension or an absolute - * path to a .desktop file. + * @param pid A process id. + * @param rulesConfig A KConfig object parameterizing the matching + * behavior. + * @returns A list of KService instances. + */ +TASKMANAGER_EXPORT KService::List servicesFromPid(quint32 pid, + KSharedConfig::Ptr rulesConfig = KSharedConfig::Ptr()); + +/** + * Returns a list of (usually application) KService instances for the + * given process command line and process name, by mangling the command + * line in various ways and checking the data against the Exec keys in + * the service database. Mangling is done e.g. to check for executable + * names with and without paths leading to them and to ignore arguments. + * if needed. * - * NOTE: Unlike appDataFromUrl(), this makes no attempt to procure icon - * data at this time. + * The [Settings]TryIgnoreRuntimes key in the supplied config object can + * hold a comma-separated list of runtime executables that this code will + * try to ignore in the process command line. This is useful in cases where + * the command line has the contents of a .desktop Exec key prefixed with + * a runtime executable. The code tries to strip the path to the runtime + * executable if needed. * - * @see appDataFromUrl - * @param appId An application id. - * @returns @c AppData filled in based on the given application id. + * @param cmdLine A process command line. + * @param processName The process name. + * @param rulesConfig A KConfig object parameterizing the matching + * behavior. + * @returns A list of KService instances. */ -TASKMANAGER_EXPORT AppData appDataFromAppId(const QString &appId); +TASKMANAGER_EXPORT KService::List servicesFromCmdLine(const QString &cmdLine, const QString &processName, + KSharedConfig::Ptr rulesConfig = KSharedConfig::Ptr()); /** * Returns an application id for an URL using the preferred:// scheme. diff --git a/libtaskmanager/tasktools.cpp b/libtaskmanager/tasktools.cpp --- a/libtaskmanager/tasktools.cpp +++ b/libtaskmanager/tasktools.cpp @@ -27,10 +27,13 @@ #include #include #include -#include +#include +#include +#include #include #include +#include #include namespace TaskManager @@ -154,6 +157,309 @@ 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()) { + QString path = services[0]->entryPath(); + if (path.isEmpty()) { + path = services[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")) { diff --git a/libtaskmanager/waylandtasksmodel.cpp b/libtaskmanager/waylandtasksmodel.cpp --- a/libtaskmanager/waylandtasksmodel.cpp +++ b/libtaskmanager/waylandtasksmodel.cpp @@ -22,8 +22,10 @@ #include "tasktools.h" #include +#include #include #include +#include #include #include #include @@ -47,12 +49,17 @@ QList windows; QHash appDataCache; KWayland::Client::PlasmaWindowManagement *windowManagement = nullptr; + KSharedConfig::Ptr rulesConfig; + KDirWatch *configWatcher = nullptr; + void init(); void initWayland(); void addWindow(KWayland::Client::PlasmaWindow *window); AppData appData(KWayland::Client::PlasmaWindow *window); + QIcon icon(KWayland::Client::PlasmaWindow *window); + void dataChanged(KWayland::Client::PlasmaWindow *window, int role); void dataChanged(KWayland::Client::PlasmaWindow *window, const QVector &roles); @@ -65,6 +72,42 @@ { } +void WaylandTasksModel::Private::init() +{ + auto clearCacheAndRefresh = [this] { + if (!windows.count()) { + return; + } + + appDataCache.clear(); + + // Emit changes of all roles satisfied from app data cache. + q->dataChanged(q->index(0, 0), q->index(windows.count() - 1, 0), + QVector{Qt::DecorationRole, AbstractTasksModel::AppId, + AbstractTasksModel::AppName, AbstractTasksModel::GenericName, + AbstractTasksModel::LauncherUrl, + AbstractTasksModel::LauncherUrlWithoutIcon}); + }; + + rulesConfig = KSharedConfig::openConfig(QStringLiteral("taskmanagerrulesrc")); + configWatcher = new KDirWatch(q); + + foreach (const QString &location, QStandardPaths::standardLocations(QStandardPaths::ConfigLocation)) { + configWatcher->addFile(location + QLatin1String("/taskmanagerrulesrc")); + } + + auto rulesConfigChange = [this, &clearCacheAndRefresh] { + rulesConfig->reparseConfiguration(); + clearCacheAndRefresh(); + }; + + QObject::connect(configWatcher, &KDirWatch::dirty, rulesConfigChange); + QObject::connect(configWatcher, &KDirWatch::created, rulesConfigChange); + QObject::connect(configWatcher, &KDirWatch::deleted, rulesConfigChange); + + initWayland(); +} + void WaylandTasksModel::Private::initWayland() { if (!KWindowSystem::isPlatformWayland()) { @@ -135,18 +178,31 @@ QObject::connect(window, &QObject::destroyed, q, removeWindow); QObject::connect(window, &KWayland::Client::PlasmaWindow::titleChanged, q, - [window, this] { dataChanged(window, Qt::DisplayRole); } + [window, this] { this->dataChanged(window, Qt::DisplayRole); } ); QObject::connect(window, &KWayland::Client::PlasmaWindow::iconChanged, q, - [window, this] { dataChanged(window, Qt::DecorationRole); } + [window, this] { + // The icon in the AppData struct might come from PlasmaWindow if it wasn't + // filled in by windowUrlFromMetadata+appDataFromUrl. + // TODO: Don't evict the cache unnecessarily if this isn't the case. As icons + // are currently very static on Wayland, this eviction is unlikely to happen + // frequently as of now. + appDataCache.remove(window); + + this->dataChanged(window, Qt::DecorationRole); + } ); QObject::connect(window, &KWayland::Client::PlasmaWindow::appIdChanged, q, [window, this] { + // The AppData struct in the cache is derived from this and needs + // to be evicted in favor of a fresh struct based on the changed + // window metadata. appDataCache.remove(window); - dataChanged(window, QVector{AppId, AppName, GenericName, + // Refresh roles satisfied from the app data cache. + this->dataChanged(window, QVector{AppId, AppName, GenericName, LauncherUrl, LauncherUrlWithoutIcon}); } ); @@ -227,8 +283,18 @@ [window, this] { this->dataChanged(window, SkipTaskbar); } ); + // NOTE: The pid will never actually change on a real system. But if it ever did ... QObject::connect(window, &KWayland::Client::PlasmaWindow::pidChanged, q, - [window, this] { this->dataChanged(window, AppPid); } + [window, this] { + // The AppData struct in the cache is derived from this and needs + // to be evicted in favor of a fresh struct based on the changed + // window metadata. + appDataCache.remove(window); + + // Refresh roles satisfied from the app data cache. + this->dataChanged(window, QVector{AppId, AppName, GenericName, + LauncherUrl, LauncherUrlWithoutIcon}); + } ); } @@ -240,13 +306,27 @@ return *it; } - const AppData &data = appDataFromAppId(window->appId()); + const AppData &data = appDataFromUrl(windowUrlFromMetadata(window->appId(), + window->pid(), rulesConfig)); appDataCache.insert(window, data); return data; } +QIcon WaylandTasksModel::Private::icon(KWayland::Client::PlasmaWindow *window) +{ + const AppData &app = appData(window); + + if (!app.icon.isNull()) { + return app.icon; + } + + appDataCache[window].icon = window->icon(); + + return window->icon(); +} + void WaylandTasksModel::Private::dataChanged(KWayland::Client::PlasmaWindow *window, int role) { QModelIndex idx = q->index(windows.indexOf(window)); @@ -263,7 +343,7 @@ : AbstractWindowTasksModel(parent) , d(new Private(this)) { - d->initWayland(); + d->init(); } WaylandTasksModel::~WaylandTasksModel() = default; @@ -279,9 +359,15 @@ if (role == Qt::DisplayRole) { return window->title(); } else if (role == Qt::DecorationRole) { - return window->icon(); + return d->icon(window); } else if (role == AppId) { - return window->appId(); + const QString &id = d->appData(window).id; + + if (id.isEmpty()) { + return window->appId(); + } else { + return id; + } } else if (role == AppName) { return d->appData(window).name; } else if (role == GenericName) { diff --git a/libtaskmanager/xwindowtasksmodel.cpp b/libtaskmanager/xwindowtasksmodel.cpp --- a/libtaskmanager/xwindowtasksmodel.cpp +++ b/libtaskmanager/xwindowtasksmodel.cpp @@ -23,28 +23,22 @@ #include "tasktools.h" #include -#include #include #include #include #include #include -#include #include #include #include #include #include -#include -#include #include #include #include #include -#include #include -#include #include #include @@ -88,9 +82,6 @@ static QString groupMimeType(); QUrl windowUrl(WId window); QUrl launcherUrl(WId window, bool encodeFallbackIcon = true); - QUrl serviceUrl(int pid, const QString &type, const QStringList &cmdRemovals); - KService::List servicesFromPid(int pid); - KService::List servicesFromCmdLine(const QString &cmdLine, const QString &processName); bool demandsAttention(WId window); private: @@ -110,35 +101,25 @@ void XWindowTasksModel::Private::init() { - rulesConfig = KSharedConfig::openConfig(QStringLiteral("taskmanagerrulesrc")); - configWatcher = new KDirWatch(q); + auto clearCacheAndRefresh = [this] { + if (!windows.count()) { + return; + } - foreach (const QString &location, QStandardPaths::standardLocations(QStandardPaths::ConfigLocation)) { - configWatcher->addFile(location + QLatin1String("/taskmanagerrulesrc")); - } + appDataCache.clear(); - QObject::connect(configWatcher, &KDirWatch::dirty, [this] { rulesConfig->reparseConfiguration(); }); - QObject::connect(configWatcher, &KDirWatch::created, [this] { rulesConfig->reparseConfiguration(); }); - QObject::connect(configWatcher, &KDirWatch::deleted, [this] { rulesConfig->reparseConfiguration(); }); + // Emit changes of all roles satisfied from app data cache. + q->dataChanged(q->index(0, 0), q->index(windows.count() - 1, 0), + QVector{Qt::DecorationRole, AbstractTasksModel::AppId, + AbstractTasksModel::AppName, AbstractTasksModel::GenericName, + AbstractTasksModel::LauncherUrl, + AbstractTasksModel::LauncherUrlWithoutIcon}); + }; sycocaChangeTimer.setSingleShot(true); sycocaChangeTimer.setInterval(100); - QObject::connect(&sycocaChangeTimer, &QTimer::timeout, q, - [this]() { - if (!windows.count()) { - return; - } - - appDataCache.clear(); - - // Emit changes of all roles satisfied from app data cache. - q->dataChanged(q->index(0, 0), q->index(windows.count() - 1, 0), - QVector{Qt::DecorationRole, AbstractTasksModel::AppId, - AbstractTasksModel::AppName, AbstractTasksModel::GenericName, - AbstractTasksModel::LauncherUrl}); - } - ); + QObject::connect(&sycocaChangeTimer, &QTimer::timeout, q, clearCacheAndRefresh); void (KSycoca::*myDatabaseChangeSignal)(const QStringList &) = &KSycoca::databaseChanged; QObject::connect(KSycoca::self(), myDatabaseChangeSignal, q, @@ -151,6 +132,22 @@ } ); + rulesConfig = KSharedConfig::openConfig(QStringLiteral("taskmanagerrulesrc")); + configWatcher = new KDirWatch(q); + + foreach (const QString &location, QStandardPaths::standardLocations(QStandardPaths::ConfigLocation)) { + configWatcher->addFile(location + QLatin1String("/taskmanagerrulesrc")); + } + + auto rulesConfigChange = [this, &clearCacheAndRefresh] { + rulesConfig->reparseConfiguration(); + clearCacheAndRefresh(); + }; + + QObject::connect(configWatcher, &KDirWatch::dirty, rulesConfigChange); + QObject::connect(configWatcher, &KDirWatch::created, rulesConfigChange); + QObject::connect(configWatcher, &KDirWatch::deleted, rulesConfigChange); + QObject::connect(KWindowSystem::self(), &KWindowSystem::windowAdded, q, [this](WId window) { addWindow(window); @@ -471,8 +468,6 @@ QUrl XWindowTasksModel::Private::windowUrl(WId window) { - QUrl url; - const KWindowInfo *info = windowInfo(window); QString desktopFile = QString::fromUtf8(info->desktopFileName()); @@ -493,216 +488,9 @@ } } - const QString &classClass = info->windowClassClass(); - const QString &className = info->windowClassName(); - - KService::List services; - bool triedPid = false; - - if (!(classClass.isEmpty() && className.isEmpty())) { - int pid = NETWinInfo(QX11Info::connection(), window, QX11Info::appRootWindow(), NET::WMPid, 0).pid(); - - // For KCModules, if we matched on window class, etc, we would end up matching - // to kcmshell5 itself - but we are more than likely interested in the actual - // control module. Therefore we obtain this via the commandline. This commandline - // may contain "kdeinit4:" or "[kdeinit]", so we remove these first. - if (classClass == "kcmshell5") { - url = serviceUrl(pid, QStringLiteral("KCModule"), QStringList() << QStringLiteral("kdeinit5:") << QStringLiteral("[kdeinit]")); - - if (!url.isEmpty()) { - return url; - } - } - - // 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 (!classClass.isEmpty() && matchCommandLineFirst.contains(classClass)) { - triedPid = true; - services = servicesFromPid(pid); - } - - // Try to match using className also. - if (!className.isEmpty() && matchCommandLineFirst.contains("::"+className)) { - triedPid = true; - services = servicesFromPid(pid); - } - - if (!classClass.isEmpty()) { - // Evaluate any mapping rules that map to a specific .desktop file. - QString mapped(grp.readEntry(classClass + "::" + className, QString())); - - if (mapped.endsWith(QLatin1String(".desktop"))) { - url = QUrl(mapped); - return url; - } - - if (mapped.isEmpty()) { - mapped = grp.readEntry(classClass, QString()); - - if (mapped.endsWith(QLatin1String(".desktop"))) { - url = QUrl(mapped); - return url; - } - } - - // Some apps, such as Wine, cannot use className 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 (!classClass.isEmpty() && manualOnly.contains(classClass)) { - return url; - } - - // Try matching both WM_CLASS instance and general class 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(classClass)); - } - - if (services.empty()) { - services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ StartupWMClass)").arg(className)); - } - - // Evaluate rewrite rules from config. - if (services.empty()) { - KConfigGroup rewriteRulesGroup(rulesConfig, QStringLiteral("Rewrite Rules")); - if (rewriteRulesGroup.hasGroup(classClass)) { - KConfigGroup rewriteGroup(&rewriteRulesGroup, classClass); - - 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 = classClass; - } else if (propertyConfig == QLatin1String("ClassName")) { - matchProperty = className; - } - - 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 (ClassClass/ClassName). - if (rewrittenString.isEmpty()) { - rewrittenString = matchProperty; - } - - services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ %2)").arg(rewrittenString, serviceSearchIdentifier)); - - if (!services.isEmpty()) { - break; - } - } - } - } - } - - // 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 WM_CLASS general class against DesktopEntryName. - if (services.empty()) { - services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ DesktopEntryName)").arg(classClass)); - } - - // Try matching WM_CLASS general class against 'Name'. - // This has a shaky chance of success as WM_CLASS 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(classClass)); - } - } - - // Ok, absolute *last* chance, try matching via pid (but only if we have not already tried this!) ... - if (services.empty() && !triedPid) { - services = servicesFromPid(pid); - } - } - - // 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 '*.classClass.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 classClass is 'dragonplayer' - // - classClass cannot directly match the desktop file because of RDN - // - classClass also cannot match the binary because of name mismatch - // - in the following code *.classClass 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(classClass)); - QMutableListIterator it(matchingServices); - while (it.hasNext()) { - auto service = it.next(); - if (!service->desktopEntryName().endsWith("." + classClass)) { - 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()) { - QString path = services[0]->entryPath(); - if (path.isEmpty()) { - path = services[0]->exec(); - } - - if (!path.isEmpty()) { - url = QUrl::fromLocalFile(path); - } - } - - return url; + return windowUrlFromMetadata(info->windowClassClass(), + NETWinInfo(QX11Info::connection(), window, QX11Info::appRootWindow(), NET::WMPid, 0).pid(), + rulesConfig, info->windowClassName()); } QUrl XWindowTasksModel::Private::launcherUrl(WId window, bool encodeFallbackIcon) @@ -736,133 +524,6 @@ return url; } -QUrl XWindowTasksModel::Private::serviceUrl(int pid, const QString &type, const QStringList &cmdRemovals = QStringList()) -{ - if (pid == 0) { - return QUrl(); - } - - KSysGuard::Processes procs; - procs.updateOrAddProcess(pid); - - KSysGuard::Process *proc = procs.getProcess(pid); - QString cmdline = proc ? proc->command().simplified() : QString(); // proc->command has a trailing space??? - - if (cmdline.isEmpty()) { - return QUrl(); - } - - foreach (const QString & r, cmdRemovals) { - cmdline.replace(r, QLatin1String("")); - } - - KService::List services = KServiceTypeTrader::self()->query(type, QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdline)); - - if (services.empty()) { - // Could not find with complete command line, so strip out path part ... - int slash = cmdline.lastIndexOf('/', cmdline.indexOf(' ')); - if (slash > 0) { - services = KServiceTypeTrader::self()->query(type, QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdline.mid(slash + 1))); - } - - if (services.empty()) { - return QUrl(); - } - } - - if (!services.isEmpty()) { - QString path = services[0]->entryPath(); - - if (!QDir::isAbsolutePath(path)) { - QString absolutePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "kservices5/"+path); - if (!absolutePath.isEmpty()) - path = absolutePath; - } - - if (QFile::exists(path)) { - return QUrl::fromLocalFile(path); - } - } - - return QUrl(); -} - -KService::List XWindowTasksModel::Private::servicesFromPid(int pid) -{ - if (pid == 0) { - 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()); -} - -KService::List XWindowTasksModel::Private::servicesFromCmdLine(const QString &_cmdLine, const QString &processName) -{ - QString cmdLine = _cmdLine; - KService::List 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); - } - } - - 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; -} - bool XWindowTasksModel::Private::demandsAttention(WId window) { if (windows.contains(window)) {