diff --git a/dataengines/dict/dictengine.cpp b/dataengines/dict/dictengine.cpp index 02eee25ee..4315dc68e 100644 --- a/dataengines/dict/dictengine.cpp +++ b/dataengines/dict/dictengine.cpp @@ -1,241 +1,243 @@ /* * Copyright (C) 2007 Thomas Georgiou and Jeff Cooper * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License version 2 as * published by the Free Software Foundation * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "dictengine.h" #include #include #include #include DictEngine::DictEngine(QObject* parent, const QVariantList& args) : Plasma::DataEngine(parent, args) , m_tcpSocket(0) { Q_UNUSED(args) m_serverName = QLatin1String("dict.org"); //In case we need to switch it later m_dictName = QLatin1String("wn"); //Default, good dictionary } DictEngine::~DictEngine() { } void DictEngine::setDict(const QString &dict) { m_dictName = dict; } void DictEngine::setServer(const QString &server) { m_serverName = server; } static QString wnToHtml(const QString &word, QByteArray &text) { Q_UNUSED(word) QList splitText = text.split('\n'); QString def; def += QLatin1String("
\n"); QRegExp linkRx(QStringLiteral("\\{(.*)\\}")); linkRx.setMinimal(true); bool isFirst=true; while (!splitText.empty()) { //150 n definitions retrieved - definitions follow //151 word database name - text follows //250 ok (optional timing information here) //552 No match QString currentLine = splitText.takeFirst(); if (currentLine.startsWith(QLatin1String("151"))) { isFirst = true; continue; } if (currentLine.startsWith('.')) { def += QLatin1String(""); continue; } if (!(currentLine.startsWith(QLatin1String("150")) || currentLine.startsWith(QLatin1String("151")) || currentLine.startsWith(QLatin1String("250")) || currentLine.startsWith(QLatin1String("552")))) { currentLine.replace(linkRx,QLatin1String("\\1")); if (isFirst) { def += "
" + currentLine + "
\n
"; isFirst = false; continue; } else { if (currentLine.contains(QRegExp(QStringLiteral("([1-9]{1,2}:)")))) { def += QLatin1String("\n
\n"); } currentLine.replace(QRegExp(QStringLiteral("^([\\s\\S]*[1-9]{1,2}:)")), QLatin1String("\\1")); def += currentLine; continue; } } } def += QLatin1String("
"); return def; } void DictEngine::getDefinition() { m_tcpSocket->readAll(); QByteArray ret; m_tcpSocket->write(QByteArray("DEFINE ")); m_tcpSocket->write(m_dictName.toLatin1()); m_tcpSocket->write(QByteArray(" \"")); m_tcpSocket->write(m_currentWord.toUtf8()); m_tcpSocket->write(QByteArray("\"\n")); m_tcpSocket->flush(); while (!ret.contains("250") && !ret.contains("552") && !ret.contains("550")) { m_tcpSocket->waitForReadyRead(); ret += m_tcpSocket->readAll(); } connect(m_tcpSocket, &QTcpSocket::disconnected, this, &DictEngine::socketClosed); m_tcpSocket->disconnectFromHost(); // setData(m_currentWord, m_dictName, ret); // qWarning()< theHash; m_tcpSocket->readAll(); QByteArray ret; m_tcpSocket->write(QByteArray("SHOW DB\n"));; m_tcpSocket->flush(); m_tcpSocket->waitForReadyRead(); while (!ret.contains("250")) { m_tcpSocket->waitForReadyRead(); ret += m_tcpSocket->readAll(); } QList retLines = ret.split('\n'); QString tmp1, tmp2; while (!retLines.empty()) { QString curr(retLines.takeFirst()); if (curr.startsWith(QLatin1String("554"))) { //TODO: What happens if no DB available? //TODO: Eventually there will be functionality to change the server... break; } // ignore status code and empty lines if (curr.startsWith(QLatin1String("250")) || curr.startsWith(QLatin1String("110")) || curr.isEmpty()) { continue; } if (!curr.startsWith('-') && !curr.startsWith('.')) { curr = curr.trimmed(); tmp1 = curr.section(' ', 0, 0); tmp2 = curr.section(' ', 1); // theHash.insert(tmp1, tmp2); //qDebug() << tmp1 + " " + tmp2; setData(QStringLiteral("list-dictionaries"), tmp1, tmp2); } } m_tcpSocket->disconnectFromHost(); // setData("list-dictionaries", "dictionaries", QByteArray(theHash); } void DictEngine::socketClosed() { - m_tcpSocket->deleteLater(); + if (m_tcpSocket) { + m_tcpSocket->deleteLater(); + } m_tcpSocket = 0; } bool DictEngine::sourceRequestEvent(const QString &query) { // FIXME: this is COMPLETELY broken .. it can only look up one query at a time! // a DataContainer subclass that does the look up should probably be made if (m_currentQuery == query) { return false; } if (m_tcpSocket) { m_tcpSocket->abort(); //stop if lookup is in progress and new query is requested m_tcpSocket->deleteLater(); m_tcpSocket = 0; } QStringList queryParts = query.split(':', QString::SkipEmptyParts); if (queryParts.isEmpty()) { return false; } m_currentWord = queryParts.last(); m_currentQuery = query; //asked for a dictionary? if (queryParts.count() > 1) { setDict(queryParts[queryParts.count()-2]); //default to wordnet } else { setDict(QStringLiteral("wn")); } //asked for a server? if (queryParts.count() > 2) { setServer(queryParts[queryParts.count()-3]); //default to wordnet } else { setServer(QStringLiteral("dict.org")); } if (m_currentWord.simplified().isEmpty()) { setData(m_currentWord, m_dictName, QString()); } else { setData(m_currentWord, m_dictName, QString()); m_tcpSocket = new QTcpSocket(this); m_tcpSocket->abort(); connect(m_tcpSocket, &QTcpSocket::disconnected, this, &DictEngine::socketClosed); if (m_currentWord == QLatin1String("list-dictionaries")) { connect(m_tcpSocket, &QTcpSocket::readyRead, this, &DictEngine::getDicts); } else { connect(m_tcpSocket, &QTcpSocket::readyRead, this, &DictEngine::getDefinition); } m_tcpSocket->connectToHost(m_serverName, 2628); } return true; } K_EXPORT_PLASMA_DATAENGINE_WITH_JSON(dict, DictEngine , "plasma-dataengine-dict.json") #include "dictengine.moc" diff --git a/libtaskmanager/launchertasksmodel.cpp b/libtaskmanager/launchertasksmodel.cpp index bc4ae726b..e216fb194 100644 --- a/libtaskmanager/launchertasksmodel.cpp +++ b/libtaskmanager/launchertasksmodel.cpp @@ -1,586 +1,602 @@ /******************************************************************** Copyright 2016 Eike Hein This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . *********************************************************************/ #include "launchertasksmodel.h" #include "tasktools.h" +#include #include #include #include #include #include #include #include #include #include #include #include #include #if HAVE_X11 #include #endif #include "launchertasksmodel_p.h" namespace TaskManager { typedef QSet ActivitiesSet; template inline bool isOnAllActivities(const ActivitiesCollection &activities) { return activities.isEmpty() || activities.contains(NULL_UUID); } class LauncherTasksModel::Private { public: Private(LauncherTasksModel *q); KActivities::Consumer activitiesConsumer; QList launchersOrder; QHash activitiesForLauncher; inline void setActivitiesForLauncher(const QUrl &url, const ActivitiesSet &activities) { if (activities.size() == activitiesConsumer.activities().size()) { activitiesForLauncher[url] = { NULL_UUID }; } else { activitiesForLauncher[url] = activities; } } QHash appDataCache; QTimer sycocaChangeTimer; void init(); AppData appData(const QUrl &url); bool requestAddLauncherToActivities(const QUrl &_url, const QStringList &activities); bool requestRemoveLauncherFromActivities(const QUrl &_url, const QStringList &activities); private: LauncherTasksModel *q; }; LauncherTasksModel::Private::Private(LauncherTasksModel *q) : q(q) { } void LauncherTasksModel::Private::init() { sycocaChangeTimer.setSingleShot(true); sycocaChangeTimer.setInterval(100); QObject::connect(&sycocaChangeTimer, &QTimer::timeout, q, [this]() { if (!launchersOrder.count()) { return; } appDataCache.clear(); // Emit changes of all roles satisfied from app data cache. q->dataChanged(q->index(0, 0), q->index(launchersOrder.count() - 1, 0), QVector{Qt::DisplayRole, Qt::DecorationRole, AbstractTasksModel::AppId, AbstractTasksModel::AppName, AbstractTasksModel::GenericName, AbstractTasksModel::LauncherUrl, AbstractTasksModel::LauncherUrlWithoutIcon}); } ); void (KSycoca::*myDatabaseChangeSignal)(const QStringList &) = &KSycoca::databaseChanged; QObject::connect(KSycoca::self(), myDatabaseChangeSignal, q, [this](const QStringList &changedResources) { if (changedResources.contains(QLatin1String("services")) || changedResources.contains(QLatin1String("apps")) || changedResources.contains(QLatin1String("xdgdata-apps"))) { sycocaChangeTimer.start(); } } ); } AppData LauncherTasksModel::Private::appData(const QUrl &url) { const auto &it = appDataCache.constFind(url); if (it != appDataCache.constEnd()) { return *it; } const AppData &data = appDataFromUrl(url, QIcon::fromTheme(QLatin1String("unknown"))); appDataCache.insert(url, data); return data; } bool LauncherTasksModel::Private::requestAddLauncherToActivities(const QUrl &_url, const QStringList &_activities) { // isValid() for the passed-in URL might return true if it was // constructed in TolerantMode, but we want to reject invalid URLs. QUrl url(_url.toString(), QUrl::StrictMode); const auto activities = ActivitiesSet::fromList(_activities); if (url.isEmpty() || !url.isValid()) { return false; } + if (url.isLocalFile() && KDesktopFile::isDesktopFile(url.toLocalFile())) { + KDesktopFile f(url.toLocalFile()); + + const KService::Ptr service = KService::serviceByStorageId(f.fileName()); + + // Resolve to non-absolute menuId-based URL if possible. + if (service) { + const QString &menuId = service->menuId(); + + if (!menuId.isEmpty()) { + url = QUrl(QStringLiteral("applications:") + menuId); + } + } + } + // Merge duplicates int row = -1; foreach(const QUrl &launcher, launchersOrder) { ++row; if (launcherUrlsMatch(url, launcher, IgnoreQueryItems)) { ActivitiesSet newActivities; if (!activitiesForLauncher.contains(url)) { // If we don't have the activities assigned to this url // for some reason newActivities = activities; } else { if (isOnAllActivities(activities)) { // If the new list is empty, or has a null uuid, this // launcher should be on all activities newActivities = ActivitiesSet { NULL_UUID }; } else if (isOnAllActivities(activitiesForLauncher[url])) { // If we have been on all activities before, and we have // been asked to be on a specific one, lets make an // exception - we will set the activities to exactly // what we have been asked newActivities = activities; } else { newActivities += activities; newActivities += activitiesForLauncher[url]; } } if (newActivities != activitiesForLauncher[url]) { setActivitiesForLauncher(url, newActivities); emit q->dataChanged( q->index(row, 0), q->index(row, 0)); emit q->launcherListChanged(); return true; } return false; } } // This is a new one const auto count = launchersOrder.count(); q->beginInsertRows(QModelIndex(), count, count); setActivitiesForLauncher(url, activities); launchersOrder.append(url); q->endInsertRows(); emit q->launcherListChanged(); return true; } bool LauncherTasksModel::Private::requestRemoveLauncherFromActivities(const QUrl &url, const QStringList &activities) { for (int row = 0; row < launchersOrder.count(); ++row) { const QUrl &launcher = launchersOrder.at(row); if (launcherUrlsMatch(url, launcher, IgnoreQueryItems) || launcherUrlsMatch(url, appData(launcher).url, IgnoreQueryItems)) { const auto currentActivities = activitiesForLauncher[url]; ActivitiesSet newActivities; bool remove = false; bool update = false; if (isOnAllActivities(currentActivities)) { // We are currently on all activities. // Should we go away, or just remove from the current one? if (isOnAllActivities(activities)) { remove = true; } else { for (const auto& activity: activitiesConsumer.activities()) { if (!activities.contains(activity)) { newActivities << activity; } else { update = true; } } } } else if (isOnAllActivities(activities)) { remove = true; } else { // We weren't on all activities, just remove those that // we were on for (const auto& activity: currentActivities) { if (!activities.contains(activity)) { newActivities << activity; } } if (newActivities.isEmpty()) { remove = true; } else { update = true; } } if (remove) { q->beginRemoveRows(QModelIndex(), row, row); launchersOrder.removeAt(row); activitiesForLauncher.remove(url); appDataCache.remove(launcher); q->endRemoveRows(); } else if (update) { setActivitiesForLauncher(url, newActivities); emit q->dataChanged( q->index(row, 0), q->index(row, 0)); } if (remove || update) { emit q->launcherListChanged(); return true; } } } return false; } LauncherTasksModel::LauncherTasksModel(QObject *parent) : AbstractTasksModel(parent) , d(new Private(this)) { d->init(); } LauncherTasksModel::~LauncherTasksModel() { } QVariant LauncherTasksModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() >= d->launchersOrder.count()) { return QVariant(); } const QUrl &url = d->launchersOrder.at(index.row()); const AppData &data = d->appData(url); if (role == Qt::DisplayRole) { return data.name; } else if (role == Qt::DecorationRole) { return data.icon; } else if (role == AppId) { return data.id; } else if (role == AppName) { return data.name; } else if (role == GenericName) { return data.genericName; } else if (role == LauncherUrl) { // Take resolved URL from cache. return data.url; } else if (role == LauncherUrlWithoutIcon) { // Take resolved URL from cache. QUrl url = data.url; if (url.hasQuery()) { QUrlQuery query(url); query.removeQueryItem(QLatin1String("iconData")); url.setQuery(query); } return url; } else if (role == IsLauncher) { return true; } else if (role == IsOnAllVirtualDesktops) { return true; } else if (role == Activities) { return QStringList(d->activitiesForLauncher[url].toList()); } return QVariant(); } int LauncherTasksModel::rowCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : d->launchersOrder.count(); } QStringList LauncherTasksModel::launcherList() const { // Serializing the launchers QStringList result; for (const auto &launcher: d->launchersOrder) { const auto &activities = d->activitiesForLauncher[launcher]; QString serializedLauncher; if (isOnAllActivities(activities)) { serializedLauncher = launcher.toString(); } else { serializedLauncher = "[" + d->activitiesForLauncher[launcher].toList().join(",") + "]\n" + launcher.toString(); } result << serializedLauncher; } return result; } void LauncherTasksModel::setLauncherList(const QStringList &serializedLaunchers) { // Clearing everything QList newLaunchersOrder; QHash newActivitiesForLauncher; // Loading the activity to launchers map QHash> launchersForActivitiesCandidates; for (const auto& serializedLauncher: serializedLaunchers) { QStringList _activities; QUrl url; std::tie(url, _activities) = deserializeLauncher(serializedLauncher); auto activities = ActivitiesSet::fromList(_activities); // Is url is not valid, ignore it if (!url.isValid()) continue; // If we have a null uuid, it means we are on all activities // and we should contain only the null uuid if (isOnAllActivities(activities)) { activities = { NULL_UUID }; } else { // Filter out invalid activities const auto allActivities = d->activitiesConsumer.activities(); ActivitiesSet validActivities; for (const auto& activity: activities) { if (allActivities.contains(activity)) { validActivities << activity; } } if (validActivities.isEmpty()) { // If all activities that had this launcher are // removed, we are killing the launcher as well continue; } activities = validActivities; } // Is the url a duplicate? const auto location = std::find_if(newLaunchersOrder.begin(), newLaunchersOrder.end(), [&url] (const QUrl &item) { return launcherUrlsMatch(url, item, IgnoreQueryItems); }); if (location != newLaunchersOrder.end()) { // It is a duplicate url = *location; } else { // It is not a duplicate, we need to add it // to the list of registered launchers newLaunchersOrder << url; } if (!newActivitiesForLauncher.contains(url)) { // This is the first time we got this url newActivitiesForLauncher[url] = activities; } else if (newActivitiesForLauncher[url].contains(NULL_UUID)) { // Do nothing, we are already on all activities } else if (activities.contains(NULL_UUID)) { newActivitiesForLauncher[url] = { NULL_UUID }; } else { // We are not on all activities, append the new ones newActivitiesForLauncher[url] += activities; } } if (newLaunchersOrder != d->launchersOrder || newActivitiesForLauncher != d->activitiesForLauncher) { // Common case optimization: If the list changed but its size // did not (e.g. due to reordering by a user of this model), // just clear the caches and announce new data instead of // resetting. if (newLaunchersOrder.count() == d->launchersOrder.count()) { d->appDataCache.clear(); std::swap(newLaunchersOrder, d->launchersOrder); std::swap(newActivitiesForLauncher, d->activitiesForLauncher); emit dataChanged( index(0, 0), index(d->launchersOrder.count() - 1, 0)); } else { beginResetModel(); std::swap(newLaunchersOrder, d->launchersOrder); std::swap(newActivitiesForLauncher, d->activitiesForLauncher); d->appDataCache.clear(); endResetModel(); } emit launcherListChanged(); } } bool LauncherTasksModel::requestAddLauncher(const QUrl &url) { return d->requestAddLauncherToActivities(url, { NULL_UUID }); } bool LauncherTasksModel::requestRemoveLauncher(const QUrl &url) { return d->requestRemoveLauncherFromActivities(url, { NULL_UUID }); } bool LauncherTasksModel::requestAddLauncherToActivity(const QUrl &url, const QString &activity) { return d->requestAddLauncherToActivities(url, { activity }); } bool LauncherTasksModel::requestRemoveLauncherFromActivity(const QUrl &url, const QString &activity) { return d->requestRemoveLauncherFromActivities(url, { activity }); } QStringList LauncherTasksModel::launcherActivities(const QUrl &_url) const { const auto position = launcherPosition(_url); if (position == -1) { // If we do not have this launcher, return an empty list return {}; } else { const auto url = d->launchersOrder.at(position); // If the launcher is on all activities, return a null uuid return d->activitiesForLauncher.contains(url) ? d->activitiesForLauncher[url].toList() : QStringList { NULL_UUID }; } } int LauncherTasksModel::launcherPosition(const QUrl &url) const { for (int i = 0; i < d->launchersOrder.count(); ++i) { if (launcherUrlsMatch(url, d->launchersOrder.at(i), IgnoreQueryItems)) { return i; } } return -1; } void LauncherTasksModel::requestActivate(const QModelIndex &index) { requestNewInstance(index); } void LauncherTasksModel::requestNewInstance(const QModelIndex &index) { if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->launchersOrder.count()) { return; } runApp(d->appData(d->launchersOrder.at(index.row()))); } void LauncherTasksModel::requestOpenUrls(const QModelIndex &index, const QList &urls) { if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->launchersOrder.count() || urls.isEmpty()) { return; } const QUrl &url = d->launchersOrder.at(index.row()); quint32 timeStamp = 0; #if HAVE_X11 if (KWindowSystem::isPlatformX11()) { timeStamp = QX11Info::appUserTime(); } #endif KService::Ptr service; if (url.scheme() == QLatin1String("preferred")) { service = KService::serviceByStorageId(defaultApplication(url)); } else { service = KService::serviceByDesktopPath(url.toLocalFile()); } if (!service && !service->isApplication()) { return; } KRun::runApplication(*service, urls, nullptr, 0, {}, KStartupInfo::createNewStartupIdForTimestamp(timeStamp)); KActivities::ResourceInstance::notifyAccessed(QUrl(QStringLiteral("applications:") + service->storageId()), QStringLiteral("org.kde.libtaskmanager")); } } diff --git a/libtaskmanager/startuptasksmodel.cpp b/libtaskmanager/startuptasksmodel.cpp index 13977b356..f81be245d 100644 --- a/libtaskmanager/startuptasksmodel.cpp +++ b/libtaskmanager/startuptasksmodel.cpp @@ -1,282 +1,289 @@ /******************************************************************** Copyright 2016 Eike Hein This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . *********************************************************************/ #include "startuptasksmodel.h" #include #include #include #include #include #include #include #include #include namespace TaskManager { class StartupTasksModel::Private { public: Private(StartupTasksModel *q); KDirWatch* configWatcher = nullptr; KStartupInfo *startupInfo = nullptr; QVector startups; QHash startupData; QHash launcherUrls; void init(); void loadConfig(); QUrl launcherUrl(const KStartupInfoData &data); private: StartupTasksModel *q; }; StartupTasksModel::Private::Private(StartupTasksModel *q) : q(q) { } void StartupTasksModel::Private::init() { configWatcher = new KDirWatch(q); configWatcher->addFile(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1String("/klaunchrc")); QObject::connect(configWatcher, &KDirWatch::dirty, [this] { loadConfig(); }); QObject::connect(configWatcher, &KDirWatch::created, [this] { loadConfig(); }); QObject::connect(configWatcher, &KDirWatch::deleted, [this] { loadConfig(); }); loadConfig(); } void StartupTasksModel::Private::loadConfig() { const KConfig _c("klaunchrc"); KConfigGroup c(&_c, "FeedbackStyle"); if (!c.readEntry("TaskbarButton", true)) { delete startupInfo; startupInfo = nullptr; q->beginResetModel(); startups.clear(); startupData.clear(); q->endResetModel(); return; } if (!startupInfo) { startupInfo = new KStartupInfo(KStartupInfo::CleanOnCantDetect, q); QObject::connect(startupInfo, &KStartupInfo::gotNewStartup, q, [this](const KStartupInfoId &id, const KStartupInfoData &data) { if (startups.contains(id)) { return; } const QString appId = data.applicationId(); const QString bin = data.bin(); foreach(const KStartupInfoData &known, startupData) { // Reject if we already have a startup notification for this app. if (known.applicationId() == appId && known.bin() == bin) { return; } } const int count = startups.count(); q->beginInsertRows(QModelIndex(), count, count); startups.append(id); startupData.insert(id.id(), data); launcherUrls.insert(id.id(), launcherUrl(data)); q->endInsertRows(); } ); QObject::connect(startupInfo, &KStartupInfo::gotRemoveStartup, q, [this](const KStartupInfoId &id) { // The order in which startups are cancelled and corresponding // windows appear is not reliable. Add some grace time to make // an overlap more likely, giving a proxy some time to arbitrate // between the two. QTimer::singleShot(500, [this, id]() { const int row = startups.indexOf(id); if (row != -1) { q->beginRemoveRows(QModelIndex(), row, row); startups.removeAt(row); startupData.remove(id.id()); launcherUrls.remove(id.id()); q->endRemoveRows(); } } ); } ); QObject::connect(startupInfo, &KStartupInfo::gotStartupChange, q, [this](const KStartupInfoId &id, const KStartupInfoData &data) { const int row = startups.indexOf(id); if (row != -1) { startupData.insert(id.id(), data); launcherUrls.insert(id.id(), launcherUrl(data)); QModelIndex idx = q->index(row); emit q->dataChanged(idx, idx); } } ); } c = KConfigGroup(&_c, "TaskbarButtonSettings"); startupInfo->setTimeout(c.readEntry("Timeout", 5)); } QUrl StartupTasksModel::Private::launcherUrl(const KStartupInfoData &data) { QUrl launcherUrl; KService::List services; QString appId = data.applicationId(); // Try to match via desktop filename ... if (!appId.isEmpty() && appId.endsWith(QLatin1String(".desktop"))) { if (appId.startsWith(QLatin1String("/"))) { - launcherUrl = QUrl::fromLocalFile(appId); - return launcherUrl; + // Even if we have an absolute path, try resolving to a service first (Bug 385594) + KService::Ptr service = KService::serviceByDesktopPath(appId); + if (!service) { // No luck, just return it verbatim + launcherUrl = QUrl::fromLocalFile(appId); + return launcherUrl; + } + + // Fall-through to menuId() handling below + services = {service}; } else { if (appId.endsWith(QLatin1String(".desktop"))) { appId = appId.mid(appId.length() - 8); } services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ DesktopEntryName)").arg(appId)); } } const QString wmClass = data.WMClass(); // Try StartupWMClass. if (services.empty() && !wmClass.isEmpty()) { services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ StartupWMClass)").arg(wmClass)); } const QString name = data.findName(); // Try via name ... if (services.empty() && !name.isEmpty()) { services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Name)").arg(name)); } if (!services.empty()) { const QString &menuId = services.at(0)->menuId(); // applications: URLs are used to refer to applications by their KService::menuId // (i.e. .desktop file name) rather than the absolute path to a .desktop file. if (!menuId.isEmpty()) { return QUrl(QStringLiteral("applications:") + menuId); } QString path = services.at(0)->entryPath(); if (path.isEmpty()) { path = services.at(0)->exec(); } if (!path.isEmpty()) { launcherUrl = QUrl::fromLocalFile(path); } } return launcherUrl; } StartupTasksModel::StartupTasksModel(QObject *parent) : AbstractTasksModel(parent) , d(new Private(this)) { d->init(); } StartupTasksModel::~StartupTasksModel() { } QVariant StartupTasksModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() >= d->startups.count()) { return QVariant(); } const QByteArray &id = d->startups.at(index.row()).id(); if (!d->startupData.contains(id)) { return QVariant(); } const KStartupInfoData &data = d->startupData.value(id); if (role == Qt::DisplayRole) { return data.findName(); } else if (role == Qt::DecorationRole) { return QIcon::fromTheme(data.findIcon(), QIcon::fromTheme(QLatin1String("unknown"))); } else if (role == AppId) { QString idFromPath = QUrl::fromLocalFile(data.applicationId()).fileName(); if (idFromPath.endsWith(QLatin1String(".desktop"))) { idFromPath = idFromPath.left(idFromPath.length() - 8); } return idFromPath; } else if (role == AppName) { return data.findName(); } else if (role == LauncherUrl || role == LauncherUrlWithoutIcon) { return d->launcherUrls.value(id); } else if (role == IsStartup) { return true; } else if (role == VirtualDesktop) { return data.desktop(); } else if (role == IsOnAllVirtualDesktops) { return (data.desktop() == 0); } return QVariant(); } int StartupTasksModel::rowCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : d->startups.count(); } void StartupTasksModel::requestNewInstance(const QModelIndex &index) { Q_UNUSED(index) // FIXME Implement. } } diff --git a/libtaskmanager/tasktools.cpp b/libtaskmanager/tasktools.cpp index 342d3c462..de806efc2 100644 --- a/libtaskmanager/tasktools.cpp +++ b/libtaskmanager/tasktools.cpp @@ -1,715 +1,744 @@ /******************************************************************** Copyright 2016 Eike Hein This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . *********************************************************************/ #include "tasktools.h" #include "abstracttasksmodel.h" #include "tasktools.h" #include "abstracttasksmodel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if HAVE_X11 #include #endif namespace TaskManager { AppData appDataFromUrl(const QUrl &url, const QIcon &fallbackIcon) { AppData data; data.url = url; if (url.hasQuery()) { QUrlQuery uQuery(url); if (uQuery.hasQueryItem(QLatin1String("iconData"))) { QString iconData(uQuery.queryItemValue(QLatin1String("iconData"))); QPixmap pixmap; QByteArray bytes = QByteArray::fromBase64(iconData.toLocal8Bit(), QByteArray::Base64UrlEncoding); pixmap.loadFromData(bytes); data.icon.addPixmap(pixmap); } } // applications: URLs are used to refer to applications by their KService::menuId // (i.e. .desktop file name) rather than the absolute path to a .desktop file. if (url.scheme() == QStringLiteral("applications")) { const KService::Ptr service = KService::serviceByMenuId(url.path()); if (service && url.path() == service->menuId()) { data.name = service->name(); data.genericName = service->genericName(); data.id = service->storageId(); if (data.icon.isNull()) { data.icon = QIcon::fromTheme(service->icon()); } } } if (url.isLocalFile() && KDesktopFile::isDesktopFile(url.toLocalFile())) { KDesktopFile f(url.toLocalFile()); const KService::Ptr service = KService::serviceByStorageId(f.fileName()); // Resolve to non-absolute menuId-based URL if possible. if (service) { const QString &menuId = service->menuId(); if (!menuId.isEmpty()) { data.url = QUrl(QStringLiteral("applications:") + menuId); } } if (service && QUrl::fromLocalFile(service->entryPath()) == url) { data.name = service->name(); data.genericName = service->genericName(); data.id = service->storageId(); if (data.icon.isNull()) { data.icon = QIcon::fromTheme(service->icon()); } } else if (f.tryExec()) { data.name = f.readName(); data.genericName = f.readGenericName(); data.id = QUrl::fromLocalFile(f.fileName()).fileName(); if (data.icon.isNull()) { data.icon = QIcon::fromTheme(f.readIcon()); } } if (data.id.endsWith(".desktop")) { data.id = data.id.left(data.id.length() - 8); } } else if (url.scheme() == QLatin1String("preferred")) { data.id = defaultApplication(url); const KService::Ptr service = KService::serviceByStorageId(data.id); if (service) { const QString &menuId = service->menuId(); const QString &desktopFile = service->entryPath(); data.name = service->name(); data.genericName = service->genericName(); data.id = service->storageId(); if (data.icon.isNull()) { data.icon = QIcon::fromTheme(service->icon()); } // Update with resolved URL. if (!menuId.isEmpty()) { data.url = QUrl(QStringLiteral("applications:") + menuId); } else { data.url = QUrl::fromLocalFile(desktopFile); } } } if (data.name.isEmpty()) { data.name = url.fileName(); } if (data.icon.isNull()) { data.icon = fallbackIcon; } return data; } AppData appDataFromAppId(const QString &appId) { AppData data; KService::Ptr service = KService::serviceByStorageId(appId); if (service) { data.id = service->storageId(); data.name = service->name(); data.genericName = service->genericName(); const QString &menuId = service->menuId(); // applications: URLs are used to refer to applications by their KService::menuId // (i.e. .desktop file name) rather than the absolute path to a .desktop file. if (!menuId.isEmpty()) { data.url = QUrl(QStringLiteral("applications:") + menuId); } else { data.url = QUrl::fromLocalFile(service->entryPath()); } return data; } QString desktopFile = appId; if (!desktopFile.endsWith(QLatin1String(".desktop"))) { desktopFile.append(QLatin1String(".desktop")); } if (KDesktopFile::isDesktopFile(desktopFile) && QFile::exists(desktopFile)) { KDesktopFile f(desktopFile); data.id = QUrl::fromLocalFile(f.fileName()).fileName(); if (data.id.endsWith(QLatin1String(".desktop"))) { data.id = data.id.left(data.id.length() - 8); } data.name = f.readName(); data.genericName = f.readGenericName(); data.url = QUrl::fromLocalFile(desktopFile); } return data; } QUrl windowUrlFromMetadata(const QString &appId, quint32 pid, KSharedConfig::Ptr rulesConfig, const QString &xWindowsWMClassName) { if (!rulesConfig) { return QUrl(); } QUrl url; KService::List services; bool triedPid = false; if (!(appId.isEmpty() && xWindowsWMClassName.isEmpty())) { // Check to see if this wmClass matched a saved one ... KConfigGroup grp(rulesConfig, "Mapping"); KConfigGroup set(rulesConfig, "Settings"); // Evaluate MatchCommandLineFirst directives from config first. // Some apps have different launchers depending upon command line ... QStringList matchCommandLineFirst = set.readEntry("MatchCommandLineFirst", QStringList()); if (!appId.isEmpty() && matchCommandLineFirst.contains(appId)) { triedPid = true; services = servicesFromPid(pid, rulesConfig); } // Try to match using xWindowsWMClassName also. if (!xWindowsWMClassName.isEmpty() && matchCommandLineFirst.contains("::"+xWindowsWMClassName)) { triedPid = true; services = servicesFromPid(pid, rulesConfig); } if (!appId.isEmpty()) { // Evaluate any mapping rules that map to a specific .desktop file. QString mapped(grp.readEntry(appId + "::" + xWindowsWMClassName, QString())); if (mapped.endsWith(QLatin1String(".desktop"))) { url = QUrl(mapped); return url; } if (mapped.isEmpty()) { mapped = grp.readEntry(appId, QString()); if (mapped.endsWith(QLatin1String(".desktop"))) { url = QUrl(mapped); return url; } } // Some apps, such as Wine, cannot use xWindowsWMClassName to map to launcher name - as Wine itself is not a GUI app // So, Settings/ManualOnly lists window classes where the user will always have to manualy set the launcher ... QStringList manualOnly = set.readEntry("ManualOnly", QStringList()); if (!appId.isEmpty() && manualOnly.contains(appId)) { return url; } // Try matching both appId and xWindowsWMClassName against StartupWMClass. // We do this before evaluating the mapping rules further, because StartupWMClass // is essentially a mapping rule, and we expect it to be set deliberately and // sensibly to instruct us what to do. Also, mapping rules // // StartupWMClass=STRING // // If true, it is KNOWN that the application will map at least one // window with the given string as its WM class or WM name hint. // // Source: https://specifications.freedesktop.org/startup-notification-spec/startup-notification-0.1.txt if (services.empty()) { services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ StartupWMClass)").arg(appId)); } if (services.empty()) { services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ StartupWMClass)").arg(xWindowsWMClassName)); } // Evaluate rewrite rules from config. if (services.empty()) { KConfigGroup rewriteRulesGroup(rulesConfig, QStringLiteral("Rewrite Rules")); if (rewriteRulesGroup.hasGroup(appId)) { KConfigGroup rewriteGroup(&rewriteRulesGroup, appId); const QStringList &rules = rewriteGroup.groupList(); for (const QString &rule : rules) { KConfigGroup ruleGroup(&rewriteGroup, rule); const QString propertyConfig = ruleGroup.readEntry(QStringLiteral("Property"), QString()); QString matchProperty; if (propertyConfig == QLatin1String("ClassClass")) { matchProperty = appId; } else if (propertyConfig == QLatin1String("ClassName")) { matchProperty = xWindowsWMClassName; } if (matchProperty.isEmpty()) { continue; } const QString serviceSearchIdentifier = ruleGroup.readEntry(QStringLiteral("Identifier"), QString()); if (serviceSearchIdentifier.isEmpty()) { continue; } QRegularExpression regExp(ruleGroup.readEntry(QStringLiteral("Match"))); const auto match = regExp.match(matchProperty); if (match.hasMatch()) { const QString actualMatch = match.captured(QStringLiteral("match")); if (actualMatch.isEmpty()) { continue; } QString rewrittenString = ruleGroup.readEntry(QStringLiteral("Target")).arg(actualMatch); // If no "Target" is provided, instead assume the matched property (appId/xWindowsWMClassName). if (rewrittenString.isEmpty()) { rewrittenString = matchProperty; } services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ %2)").arg(rewrittenString, serviceSearchIdentifier)); if (!services.isEmpty()) { break; } } } } } // The appId looks like a path. if (appId.startsWith(QStringLiteral("/"))) { // Check if it's a path to a .desktop file. if (KDesktopFile::isDesktopFile(appId) && QFile::exists(appId)) { return QUrl::fromLocalFile(appId); } // Check if the appId passes as a .desktop file path if we add the extension. const QString appIdPlusExtension(appId + QStringLiteral(".desktop")); if (KDesktopFile::isDesktopFile(appIdPlusExtension) && QFile::exists(appIdPlusExtension)) { return QUrl::fromLocalFile(appIdPlusExtension); } } // Try matching mapped name against DesktopEntryName. if (!mapped.isEmpty() && services.empty()) { services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ DesktopEntryName)").arg(mapped)); } // Try matching mapped name against 'Name'. if (!mapped.isEmpty() && services.empty()) { services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Name) and (not exist NoDisplay or not NoDisplay)").arg(mapped)); } // Try matching appId against DesktopEntryName. if (services.empty()) { services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ DesktopEntryName)").arg(appId)); } // Try matching appId against 'Name'. // This has a shaky chance of success as appId is untranslated, but 'Name' may be localized. if (services.empty()) { services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Name) and (not exist NoDisplay or not NoDisplay)").arg(appId)); } } // Ok, absolute *last* chance, try matching via pid (but only if we have not already tried this!) ... if (services.empty() && !triedPid) { services = servicesFromPid(pid, rulesConfig); } } // Try to improve on a possible from-binary fallback. // If no services were found or we got a fake-service back from getServicesViaPid() // we attempt to improve on this by adding a loosely matched reverse-domain-name // DesktopEntryName. Namely anything that is '*.appId.desktop' would qualify here. // // Illustrative example of a case where the above heuristics would fail to produce // a reasonable result: // - org.kde.dragonplayer.desktop // - binary is 'dragon' // - qapp appname and thus appId is 'dragonplayer' // - appId cannot directly match the desktop file because of RDN // - appId also cannot match the binary because of name mismatch // - in the following code *.appId can match org.kde.dragonplayer though if (services.empty() || services.at(0)->desktopEntryName().isEmpty()) { auto matchingServices = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' ~~ DesktopEntryName)").arg(appId)); QMutableListIterator it(matchingServices); while (it.hasNext()) { auto service = it.next(); if (!service->desktopEntryName().endsWith("." + appId)) { it.remove(); } } // Exactly one match is expected, otherwise we discard the results as to reduce // the likelihood of false-positive mappings. Since we essentially eliminate the // uniqueness that RDN is meant to bring to the table we could potentially end // up with more than one match here. if (matchingServices.length() == 1) { services = matchingServices; } } if (!services.empty()) { const QString &menuId = services.at(0)->menuId(); // applications: URLs are used to refer to applications by their KService::menuId // (i.e. .desktop file name) rather than the absolute path to a .desktop file. if (!menuId.isEmpty()) { return QUrl(QStringLiteral("applications:") + menuId); } QString path = services.at(0)->entryPath(); if (path.isEmpty()) { path = services.at(0)->exec(); } if (!path.isEmpty()) { url = QUrl::fromLocalFile(path); } } return url; } KService::List servicesFromPid(quint32 pid, KSharedConfig::Ptr rulesConfig) { if (pid == 0) { return KService::List(); } if (!rulesConfig) { return KService::List(); } KSysGuard::Processes procs; procs.updateOrAddProcess(pid); KSysGuard::Process *proc = procs.getProcess(pid); const QString &cmdLine = proc ? proc->command().simplified() : QString(); // proc->command has a trailing space??? if (cmdLine.isEmpty()) { return KService::List(); } return servicesFromCmdLine(cmdLine, proc->name(), rulesConfig); } KService::List servicesFromCmdLine(const QString &_cmdLine, const QString &processName, KSharedConfig::Ptr rulesConfig) { QString cmdLine = _cmdLine; KService::List services; if (!rulesConfig) { return services; } const int firstSpace = cmdLine.indexOf(' '); int slash = 0; services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdLine)); if (services.empty()) { // Could not find with complete command line, so strip out the path part ... slash = cmdLine.lastIndexOf('/', firstSpace); if (slash > 0) { services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdLine.mid(slash + 1))); } } if (services.empty() && firstSpace > 0) { // Could not find with arguments, so try without ... cmdLine = cmdLine.left(firstSpace); services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdLine)); if (services.empty()) { slash = cmdLine.lastIndexOf('/'); if (slash > 0) { services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdLine.mid(slash + 1))); } } } if (services.empty()) { KConfigGroup set(rulesConfig, "Settings"); const QStringList &runtimes = set.readEntry("TryIgnoreRuntimes", QStringList()); bool ignore = runtimes.contains(cmdLine); if (!ignore && slash > 0) { ignore = runtimes.contains(cmdLine.mid(slash + 1)); } if (ignore) { return servicesFromCmdLine(_cmdLine.mid(firstSpace + 1), processName, rulesConfig); } } if (services.empty() && !processName.isEmpty() && !QStandardPaths::findExecutable(cmdLine).isEmpty()) { // cmdLine now exists without arguments if there were any. services << QExplicitlySharedDataPointer(new KService(processName, cmdLine, QString())); } return services; } QString defaultApplication(const QUrl &url) { if (url.scheme() != QLatin1String("preferred")) { return QString(); } const QString &application = url.host(); if (application.isEmpty()) { return QString(); } if (application.compare(QLatin1String("mailer"), Qt::CaseInsensitive) == 0) { KEMailSettings settings; // In KToolInvocation, the default is kmail; but let's be friendlier. QString command = settings.getSetting(KEMailSettings::ClientProgram); if (command.isEmpty()) { if (KService::Ptr kontact = KService::serviceByStorageId(QStringLiteral("kontact"))) { return kontact->storageId(); } else if (KService::Ptr kmail = KService::serviceByStorageId(QStringLiteral("kmail"))) { return kmail->storageId(); } } if (!command.isEmpty()) { if (settings.getSetting(KEMailSettings::ClientTerminal) == QLatin1String("true")) { KConfigGroup confGroup(KSharedConfig::openConfig(), "General"); const QString preferredTerminal = confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole")); command = preferredTerminal + QLatin1String(" -e ") + command; } return command; } } else if (application.compare(QLatin1String("browser"), Qt::CaseInsensitive) == 0) { KConfigGroup config(KSharedConfig::openConfig(), "General"); QString browserApp = config.readPathEntry("BrowserApplication", QString()); if (browserApp.isEmpty()) { const KService::Ptr htmlApp = KMimeTypeTrader::self()->preferredService(QStringLiteral("text/html")); if (htmlApp) { browserApp = htmlApp->storageId(); } } else if (browserApp.startsWith('!')) { browserApp = browserApp.mid(1); } return browserApp; } else if (application.compare(QLatin1String("terminal"), Qt::CaseInsensitive) == 0) { KConfigGroup confGroup(KSharedConfig::openConfig(), "General"); return confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole")); } else if (application.compare(QLatin1String("filemanager"), Qt::CaseInsensitive) == 0) { KService::Ptr service = KMimeTypeTrader::self()->preferredService(QStringLiteral("inode/directory")); if (service) { return service->storageId(); } } else if (KService::Ptr service = KMimeTypeTrader::self()->preferredService(application)) { return service->storageId(); } else { // Try the files in share/apps/kcm_componentchooser/*.desktop. QStringList directories = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kcm_componentchooser"), QStandardPaths::LocateDirectory); QStringList services; foreach(const QString& directory, directories) { QDir dir(directory); foreach(const QString& f, dir.entryList(QStringList("*.desktop"))) services += dir.absoluteFilePath(f); } foreach (const QString & service, services) { KConfig config(service, KConfig::SimpleConfig); KConfigGroup cg = config.group(QByteArray()); const QString type = cg.readEntry("valueName", QString()); if (type.compare(application, Qt::CaseInsensitive) == 0) { KConfig store(cg.readPathEntry("storeInFile", QStringLiteral("null"))); KConfigGroup storeCg(&store, cg.readEntry("valueSection", QString())); const QString exec = storeCg.readPathEntry(cg.readEntry("valueName", "kcm_componenchooser_null"), cg.readEntry("defaultImplementation", QString())); if (!exec.isEmpty()) { return exec; } break; } } } return QString(""); } bool launcherUrlsMatch(const QUrl &a, const QUrl &b, UrlComparisonMode mode) { + QUrl sanitizedA = a; + QUrl sanitizedB = b; + if (mode == IgnoreQueryItems) { - return (a.adjusted(QUrl::RemoveQuery) == b.adjusted(QUrl::RemoveQuery)); + sanitizedA = a.adjusted(QUrl::RemoveQuery); + sanitizedB = b.adjusted(QUrl::RemoveQuery); } - return (a == b); + auto tryResolveToApplicationsUrl = [](const QUrl &url) -> QUrl { + QUrl resolvedUrl = url; + + if (url.isLocalFile() && KDesktopFile::isDesktopFile(url.toLocalFile())) { + KDesktopFile f(url.toLocalFile()); + + const KService::Ptr service = KService::serviceByStorageId(f.fileName()); + + // Resolve to non-absolute menuId-based URL if possible. + if (service) { + const QString &menuId = service->menuId(); + + if (!menuId.isEmpty()) { + resolvedUrl = QUrl(QStringLiteral("applications:") + menuId); + resolvedUrl.setQuery(url.query()); + } + } + } + + return resolvedUrl; + }; + + sanitizedA = tryResolveToApplicationsUrl(sanitizedA); + sanitizedB = tryResolveToApplicationsUrl(sanitizedB); + + return (sanitizedA == sanitizedB); } bool appsMatch(const QModelIndex &a, const QModelIndex &b) { const QString &aAppId = a.data(AbstractTasksModel::AppId).toString(); const QString &bAppId = b.data(AbstractTasksModel::AppId).toString(); if (!aAppId.isEmpty() && aAppId == bAppId) { return true; } const QUrl &aUrl = a.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl(); const QUrl &bUrl = b.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl(); if (aUrl.isValid() && aUrl == bUrl) { return true; } return false; } QRect screenGeometry(const QPoint &pos) { if (pos.isNull()) { return QRect(); } const QList &screens = QGuiApplication::screens(); QRect screenGeometry; int shortestDistance = INT_MAX; for (int i = 0; i < screens.count(); ++i) { const QRect &geometry = screens.at(i)->geometry(); if (geometry.contains(pos)) { return geometry; } int distance = QPoint(geometry.topLeft() - pos).manhattanLength(); distance = qMin(distance, QPoint(geometry.topRight() - pos).manhattanLength()); distance = qMin(distance, QPoint(geometry.bottomRight() - pos).manhattanLength()); distance = qMin(distance, QPoint(geometry.bottomLeft() - pos).manhattanLength()); if (distance < shortestDistance) { shortestDistance = distance; screenGeometry = geometry; } } return screenGeometry; } void runApp(const AppData &appData, const QList &urls) { if (appData.url.isValid()) { quint32 timeStamp = 0; #if HAVE_X11 if (KWindowSystem::isPlatformX11()) { timeStamp = QX11Info::appUserTime(); } #endif KService::Ptr service; // applications: URLs are used to refer to applications by their KService::menuId // (i.e. .desktop file name) rather than the absolute path to a .desktop file. if (appData.url.scheme() == QStringLiteral("applications")) { service = KService::serviceByMenuId(appData.url.path()); } else if (appData.url.scheme() == QLatin1String("preferred")) { const KService::Ptr service = KService::serviceByStorageId(defaultApplication(appData.url)); } else { service = KService::serviceByDesktopPath(appData.url.toLocalFile()); } if (service && service->isApplication()) { KRun::runApplication(*service, urls, nullptr, 0, {}, KStartupInfo::createNewStartupIdForTimestamp(timeStamp)); KActivities::ResourceInstance::notifyAccessed(QUrl(QStringLiteral("applications:") + service->storageId()), QStringLiteral("org.kde.libtaskmanager")); } else { new KRun(appData.url, 0, false, KStartupInfo::createNewStartupIdForTimestamp(timeStamp)); if (!appData.id.isEmpty()) { KActivities::ResourceInstance::notifyAccessed(QUrl(QStringLiteral("applications:") + appData.id), QStringLiteral("org.kde.libtaskmanager")); } } } } }