74 changes: 64 additions & 10 deletions libtaskmanager/tasktools.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
#include <QModelIndex>
#include <QUrl>

#include <KService>
#include <KSharedConfig>

namespace TaskManager
{

Expand Down Expand Up @@ -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.
Expand Down
102 changes: 94 additions & 8 deletions libtaskmanager/waylandtasksmodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
#include "tasktools.h"

#include <KActivities/ResourceInstance>
#include <KDirWatch>
#include <KRun>
#include <KService>
#include <KSharedConfig>
#include <KWayland/Client/connection_thread.h>
#include <KWayland/Client/plasmawindowmanagement.h>
#include <KWayland/Client/registry.h>
Expand All @@ -47,12 +49,17 @@ class WaylandTasksModel::Private
QList<KWayland::Client::PlasmaWindow*> windows;
QHash<KWayland::Client::PlasmaWindow*, AppData> 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<int> &roles);

Expand All @@ -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<int>{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()) {
Expand Down Expand Up @@ -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<int>{AppId, AppName, GenericName,
// Refresh roles satisfied from the app data cache.
this->dataChanged(window, QVector<int>{AppId, AppName, GenericName,
LauncherUrl, LauncherUrlWithoutIcon});
}
);
Expand Down Expand Up @@ -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<int>{AppId, AppName, GenericName,
LauncherUrl, LauncherUrlWithoutIcon});
}
);
}

Expand All @@ -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));
Expand All @@ -263,7 +343,7 @@ WaylandTasksModel::WaylandTasksModel(QObject *parent)
: AbstractWindowTasksModel(parent)
, d(new Private(this))
{
d->initWayland();
d->init();
}

WaylandTasksModel::~WaylandTasksModel() = default;
Expand All @@ -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) {
Expand Down
403 changes: 32 additions & 371 deletions libtaskmanager/xwindowtasksmodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,22 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
#include "tasktools.h"

#include <KActivities/ResourceInstance>
#include <KConfigGroup>
#include <KDesktopFile>
#include <KDirWatch>
#include <KIconLoader>
#include <KRun>
#include <KService>
#include <KServiceTypeTrader>
#include <KSharedConfig>
#include <KStartupInfo>
#include <KSycoca>
#include <KWindowInfo>
#include <KWindowSystem>
#include <processcore/processes.h>
#include <processcore/process.h>

#include <QBuffer>
#include <QDir>
#include <QIcon>
#include <QFile>
#include <QRegularExpression>
#include <QSet>
#include <QStandardPaths>
#include <QTimer>
#include <QX11Info>

Expand Down Expand Up @@ -88,9 +82,6 @@ class XWindowTasksModel::Private
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:
Expand All @@ -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<int>{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<int>{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,
Expand All @@ -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);
Expand Down Expand Up @@ -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());
Expand All @@ -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<KService::Ptr> 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)
Expand Down Expand Up @@ -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<KService>(new KService(processName, cmdLine, QString()));
}

return services;
}

bool XWindowTasksModel::Private::demandsAttention(WId window)
{
if (windows.contains(window)) {
Expand Down