diff --git a/libtaskmanager/tasktools.cpp b/libtaskmanager/tasktools.cpp
index 5ebbb733c..602e55e53 100644
--- a/libtaskmanager/tasktools.cpp
+++ b/libtaskmanager/tasktools.cpp
@@ -27,10 +27,13 @@ License along with this library. If not, see .
#include
#include
#include
-#include
+#include
+#include
+#include
#include
#include
+#include
#include
namespace TaskManager
@@ -154,6 +157,309 @@ AppData appDataFromAppId(const QString &appId)
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/tasktools.h b/libtaskmanager/tasktools.h
index f577eda80..464ba0a01 100644
--- a/libtaskmanager/tasktools.h
+++ b/libtaskmanager/tasktools.h
@@ -27,6 +27,9 @@ License along with this library. If not, see .
#include
#include
+#include
+#include
+
namespace TaskManager
{
@@ -65,20 +68,71 @@ enum UrlComparisonMode {
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/waylandtasksmodel.cpp b/libtaskmanager/waylandtasksmodel.cpp
index 85734fe98..35d92ac08 100644
--- a/libtaskmanager/waylandtasksmodel.cpp
+++ b/libtaskmanager/waylandtasksmodel.cpp
@@ -22,8 +22,10 @@ License along with this library. If not, see .
#include "tasktools.h"
#include
+#include
#include
#include
+#include
#include
#include
#include
@@ -47,12 +49,17 @@ public:
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 @@ WaylandTasksModel::Private::Private(WaylandTasksModel *q)
{
}
+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 @@ void WaylandTasksModel::Private::addWindow(KWayland::Client::PlasmaWindow *windo
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 @@ void WaylandTasksModel::Private::addWindow(KWayland::Client::PlasmaWindow *windo
[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 @@ AppData WaylandTasksModel::Private::appData(KWayland::Client::PlasmaWindow *wind
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 @@ WaylandTasksModel::WaylandTasksModel(QObject *parent)
: AbstractWindowTasksModel(parent)
, d(new Private(this))
{
- d->initWayland();
+ d->init();
}
WaylandTasksModel::~WaylandTasksModel() = default;
@@ -279,9 +359,15 @@ QVariant WaylandTasksModel::data(const QModelIndex &index, int role) const
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
index 47a64bdba..38e2dbf94 100644
--- a/libtaskmanager/xwindowtasksmodel.cpp
+++ b/libtaskmanager/xwindowtasksmodel.cpp
@@ -23,28 +23,22 @@ License along with this library. If not, see .
#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 @@ public:
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 @@ XWindowTasksModel::Private::~Private()
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 @@ void XWindowTasksModel::Private::init()
}
);
+ 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 @@ QString XWindowTasksModel::Private::groupMimeType()
QUrl XWindowTasksModel::Private::windowUrl(WId window)
{
- QUrl url;
-
const KWindowInfo *info = windowInfo(window);
QString desktopFile = QString::fromUtf8(info->desktopFileName());
@@ -493,216 +488,9 @@ QUrl XWindowTasksModel::Private::windowUrl(WId window)
}
}
- 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 @@ QUrl XWindowTasksModel::Private::launcherUrl(WId window, bool encodeFallbackIcon
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)) {