SPDX-FileCopyrightText: 2012-2016 Eike Hein <hein@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "backend.h"
#include "log_settings.h"
#include <KActionCollection>
#include <KConfigGroup>
#include <KDesktopFile>
#include <KFileItem>
#include <KFilePlacesModel>
#include <KLocalizedString>
#include <KNotificationJobUiDelegate>
#include <KProtocolInfo>
#include <KService>
#include <KServiceAction>
#include <KWindowEffects>
#include <KWindowSystem>
#include <KApplicationTrader>
#include <KIO/ApplicationLauncherJob>
#include <QAction>
#include <QActionGroup>
#include <QApplication>
#include <QJsonArray>
#include <QMenu>
#include <QQuickItem>
#include <QQuickWindow>
#include <QStandardPaths>
#include <QTimer>
#include <QVersionNumber>
#include <KGlobalAccel>
#include <mutex>
#include <PlasmaActivities/Consumer>
#include <PlasmaActivities/Stats/Cleaning>
#include <PlasmaActivities/Stats/ResultSet>
#include <PlasmaActivities/Stats/Terms>
#include <processcore/process.h>
#include <processcore/processes.h>
namespace KAStats = KActivities::Stats;
using namespace KAStats;
using namespace KAStats::Terms;
static constexpr int NoApplications = 2;
QVector<Backend *> Backend::s_instances;
Backend::Backend(QObject *parent)
: QObject(parent)
, m_actionGroup(new QActionGroup(this))
, m_activityManagerPluginsSettingsWatcher(KConfigWatcher::create(m_activityManagerPluginsSettings.sharedConfig()))
{
connect(m_activityManagerPluginsSettingsWatcher.get(),
&KConfigWatcher::configChanged,
this,
[this](const KConfigGroup &group, const QByteArrayList &names) {
if (group.name() == QLatin1String("Plugin-org.kde.ActivityManager.Resources.Scoring")
&& names.contains(QByteArrayLiteral("what-to-remember"))) {
m_activityManagerPluginsSettings.load();
}
});
s_instances.append(this);
setupShortcuts();
}
Backend::~Backend()
{
s_instances.removeAll(this);
}
int Backend::screen() const
{
return m_screen;
}
void Backend::setScreen(int screen)
{
if (m_screen != screen) {
m_screen = screen;
Q_EMIT screenChanged();
}
}
bool Backend::hasActiveTask() const
{
return m_hasActiveTask;
}
void Backend::setHasActiveTask(bool active)
{
if (m_hasActiveTask != active) {
m_hasActiveTask = active;
Q_EMIT hasActiveTaskChanged();
}
}
bool Backend::showsOnlyCurrentScreen() const
{
return m_showsOnlyCurrentScreen;
}
void Backend::setShowsOnlyCurrentScreen(bool shows)
{
if (m_showsOnlyCurrentScreen != shows) {
m_showsOnlyCurrentScreen = shows;
Q_EMIT showsOnlyCurrentScreenChanged();
}
}
QUrl Backend::tryDecodeApplicationsUrl(const QUrl &launcherUrl)
{
if (launcherUrl.isValid() && launcherUrl.scheme() == QLatin1String("applications")) {
const KService::Ptr service = KService::serviceByMenuId(launcherUrl.path());
if (service) {
return QUrl::fromLocalFile(service->entryPath());
}
}
return launcherUrl;
}
QStringList Backend::applicationCategories(const QUrl &launcherUrl)
{
const QUrl desktopEntryUrl = tryDecodeApplicationsUrl(launcherUrl);
if (!desktopEntryUrl.isValid() || !desktopEntryUrl.isLocalFile() || !KDesktopFile::isDesktopFile(desktopEntryUrl.toLocalFile())) {
return QStringList();
}
KDesktopFile desktopFile(desktopEntryUrl.toLocalFile());
return desktopFile.desktopGroup().readXdgListEntry(QStringLiteral("Categories"));
}
QVariantList Backend::jumpListActions(const QUrl &launcherUrl, QObject *parent)
{
QVariantList actions;
if (!parent) {
return actions;
}
QUrl desktopEntryUrl = tryDecodeApplicationsUrl(launcherUrl);
if (!desktopEntryUrl.isValid() || !desktopEntryUrl.isLocalFile() || !KDesktopFile::isDesktopFile(desktopEntryUrl.toLocalFile())) {
return actions;
}
const KService::Ptr service = KService::serviceByDesktopPath(desktopEntryUrl.toLocalFile());
if (!service) {
return actions;
}
if (service->storageId() == QLatin1String("systemsettings.desktop")) {
actions = systemSettingsActions(parent);
if (!actions.isEmpty()) {
return actions;
}
}
const auto jumpListActions = service->actions();
for (const KServiceAction &serviceAction : jumpListActions) {
if (serviceAction.noDisplay()) {
continue;
}
QAction *action = new QAction(parent);
action->setText(serviceAction.text());
action->setIcon(QIcon::fromTheme(serviceAction.icon()));
if (serviceAction.isSeparator()) {
action->setSeparator(true);
}
connect(action, &QAction::triggered, this, [serviceAction]() {
auto *job = new KIO::ApplicationLauncherJob(serviceAction);
auto *delegate = new KNotificationJobUiDelegate;
delegate->setAutoErrorHandlingEnabled(true);
job->setUiDelegate(delegate);
job->start();
});
actions << QVariant::fromValue<QAction *>(action);
}
return actions;
}
QVariantList Backend::systemSettingsActions(QObject *parent) const
{
QVariantList actions;
if (m_activityManagerPluginsSettings.whatToRemember() == NoApplications) {
return actions;
}
auto query = AllResources | Agent(QStringLiteral("org.kde.systemsettings")) | HighScoredFirst | Limit(5);
ResultSet results(query);
QStringList ids;
for (const ResultSet::Result &result : results) {
ids << QUrl(result.resource()).path();
}
if (ids.count() < 5) {
return actions;
}
for (const QString &id : std::as_const(ids)) {
KService::Ptr service = KService::serviceByStorageId(id);
if (!service || !service->isValid()) {
continue;
}
QAction *action = new QAction(parent);
action->setText(service->name());
action->setIcon(QIcon::fromTheme(service->icon()));
connect(action, &QAction::triggered, this, [service]() {
auto *job = new KIO::ApplicationLauncherJob(service);
auto *delegate = new KNotificationJobUiDelegate;
delegate->setAutoErrorHandlingEnabled(true);
job->setUiDelegate(delegate);
job->start();
});
actions << QVariant::fromValue<QAction *>(action);
}
return actions;
}
QVariantList Backend::placesActions(const QUrl &launcherUrl, bool showAllPlaces, QObject *parent)
{
if (!parent) {
return QVariantList();
}
QUrl desktopEntryUrl = tryDecodeApplicationsUrl(launcherUrl);
if (!desktopEntryUrl.isValid() || !desktopEntryUrl.isLocalFile() || !KDesktopFile::isDesktopFile(desktopEntryUrl.toLocalFile())) {
return QVariantList();
}
QVariantList actions;
if (!applicationCategories(launcherUrl).contains(QLatin1String("FileManager"))) {
return actions;
}
QString previousGroup;
QMenu *subMenu = nullptr;
std::unique_ptr<KFilePlacesModel> placesModel(new KFilePlacesModel());
for (int i = 0; i < placesModel->rowCount(); ++i) {
QModelIndex idx = placesModel->index(i, 0);
if (placesModel->isHidden(idx)) {
continue;
}
const QString &title = idx.data(Qt::DisplayRole).toString();
const QIcon &icon = idx.data(Qt::DecorationRole).value<QIcon>();
const QUrl &url = idx.data(KFilePlacesModel::UrlRole).toUrl();
QAction *placeAction = new QAction(icon, title, parent);
connect(placeAction, &QAction::triggered, this, [url, desktopEntryUrl] {
KService::Ptr service = KService::serviceByDesktopPath(desktopEntryUrl.toLocalFile());
if (!service) {
return;
}
auto *job = new KIO::ApplicationLauncherJob(service);
auto *delegate = new KNotificationJobUiDelegate;
delegate->setAutoErrorHandlingEnabled(true);
job->setUiDelegate(delegate);
job->setUrls({url});
job->start();
});
const QString &groupName = idx.data(KFilePlacesModel::GroupRole).toString();
if (previousGroup.isEmpty()) {
previousGroup = groupName;
}
if (previousGroup != groupName) {
QAction *subMenuAction = new QAction(groupName, parent);
subMenu = new QMenu();
subMenu->setAttribute(Qt::WA_TranslucentBackground);
connect(parent, &QObject::destroyed, subMenu, &QObject::deleteLater);
subMenuAction->setMenu(subMenu);
actions << QVariant::fromValue(subMenuAction);
previousGroup = groupName;
}
if (subMenu) {
subMenu->addAction(placeAction);
} else {
actions << QVariant::fromValue(placeAction);
}
}
if (!showAllPlaces && actions.count() > 7) {
const int totalActionCount = actions.count();
while (actions.count() > 5) {
actions.removeLast();
}
QAction *action = new QAction(parent);
action->setIcon(QIcon::fromTheme(QStringLiteral("view-more-symbolic")));
action->setText(i18ncp("Show all user Places", "%1 more Place…", "%1 more Places…", totalActionCount - actions.count()));
connect(action, &QAction::triggered, this, &Backend::showAllPlaces);
actions << QVariant::fromValue(action);
}
return actions;
}
QVariantList Backend::recentDocumentActions(const QUrl &launcherUrl, QObject *parent)
{
QVariantList actions;
if (!parent) {
return actions;
}
if (m_activityManagerPluginsSettings.whatToRemember() == NoApplications) {
return actions;
}
QUrl desktopEntryUrl = tryDecodeApplicationsUrl(launcherUrl);
if (!desktopEntryUrl.isValid() || !desktopEntryUrl.isLocalFile() || !KDesktopFile::isDesktopFile(desktopEntryUrl.toLocalFile())) {
return QVariantList();
}
QString desktopName = desktopEntryUrl.fileName();
QString storageId = desktopName;
if (storageId.endsWith(QLatin1String(".desktop"))) {
storageId = storageId.left(storageId.length() - 8);
}
auto query = UsedResources | RecentlyUsedFirst | Agent(storageId) | Type::any() | Activity::current();
ResultSet results(query);
ResultSet::const_iterator resultIt = results.begin();
int actionCount = 0;
bool allFolders = true;
bool allDownloads = true;
bool allRemoteWithoutFileName = true;
const QString downloadsPath = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
while (actionCount < 5 && resultIt != results.end()) {
const QString mimetype = (*resultIt).mimetype();
const QUrl url = (*resultIt).url();
++resultIt;
if (!url.isValid()) {
continue;
}
allFolders = allFolders && mimetype == QLatin1String("inode/directory");
allDownloads = allDownloads && url.toLocalFile().startsWith(downloadsPath);
allRemoteWithoutFileName = allRemoteWithoutFileName && !url.isLocalFile() && url.fileName().isEmpty();
QString name;
if (url.isLocalFile() && !url.fileName().isEmpty()) {
name = url.fileName();
} else {
name = url.toDisplayString();
}
QAction *action = new QAction(parent);
action->setText(name);
action->setIcon(QIcon::fromTheme(KIO::iconNameForUrl(url)));
action->setProperty("agent", storageId);
action->setProperty("entryPath", desktopEntryUrl);
action->setProperty("mimeType", mimetype);
action->setData(url);
connect(action, &QAction::triggered, this, &Backend::handleRecentDocumentAction);
actions << QVariant::fromValue<QAction *>(action);
++actionCount;
}
if (actionCount > 0) {
if (allDownloads) {
actions.prepend(i18n("Recent Downloads"));
} else if (allRemoteWithoutFileName) {
actions.prepend(i18n("Recent Connections"));
} else if (allFolders) {
actions.prepend(i18n("Recent Places"));
}
QAction *separatorAction = new QAction(parent);
separatorAction->setSeparator(true);
actions << QVariant::fromValue<QAction *>(separatorAction);
QAction *action = new QAction(parent);
if (allDownloads) {
action->setText(i18nc("@action:inmenu", "Forget Recent Downloads"));
} else if (allRemoteWithoutFileName) {
action->setText(i18nc("@action:inmenu", "Forget Recent Connections"));
} else if (allFolders) {
action->setText(i18nc("@action:inmenu", "Forget Recent Places"));
} else {
action->setText(i18nc("@action:inmenu", "Forget Recent Files"));
}
action->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-history")));
action->setProperty("agent", storageId);
connect(action, &QAction::triggered, this, &Backend::handleRecentDocumentAction);
actions << QVariant::fromValue<QAction *>(action);
}
return actions;
}
void Backend::handleRecentDocumentAction() const
{
const QAction *action = qobject_cast<QAction *>(sender());
if (!action) {
return;
}
const QString agent = action->property("agent").toString();
if (agent.isEmpty()) {
return;
}
const QString desktopPath = action->property("entryPath").toUrl().toLocalFile();
const QUrl url = action->data().toUrl();
if (desktopPath.isEmpty() || url.isEmpty()) {
auto query = UsedResources | Agent(agent) | Type::any() | Activity::current();
KAStats::forgetResources(query);
return;
}
KService::Ptr service = KService::serviceByDesktopPath(desktopPath);
if (!service) {
return;
}
const auto mimetype = action->property("mimeType").toString();
if (!mimetype.isEmpty() && mimetype != QLatin1String("application/octet-stream")) {
if (!service->hasMimeType(mimetype)) {
service = KApplicationTrader::preferredService(mimetype);
if (!service) {
return;
} else {
qCWarning(TASKMANAGER_DEBUG) << "Preventing the file to open with " << service->desktopEntryName() << "no alternative found";
}
}
}
auto *job = new KIO::ApplicationLauncherJob(service);
auto *delegate = new KNotificationJobUiDelegate;
delegate->setAutoErrorHandlingEnabled(true);
job->setUiDelegate(delegate);
job->setUrls({url});
job->start();
}
void Backend::setActionGroup(QAction *action) const
{
if (action) {
action->setActionGroup(m_actionGroup);
}
}
QRect Backend::globalRect(QQuickItem *item) const
{
if (!item || !item->window()) {
return QRect();
}
QRect iconRect(item->x(), item->y(), item->width(), item->height());
iconRect.moveTopLeft(item->parentItem()->mapToScene(iconRect.topLeft()).toPoint());
iconRect.moveTopLeft(item->window()->mapToGlobal(iconRect.topLeft()));
return iconRect;
}
bool Backend::isApplication(const QUrl &url) const
{
if (!url.isValid() || !url.isLocalFile()) {
return false;
}
const QString &localPath = url.toLocalFile();
if (!KDesktopFile::isDesktopFile(localPath)) {
return false;
}
KDesktopFile desktopFile(localPath);
return desktopFile.hasApplicationType();
}
qint64 Backend::parentPid(qint64 pid) const
{
KSysGuard::Processes procs;
procs.updateOrAddProcess(pid);
KSysGuard::Process *proc = procs.getProcess(pid);
if (!proc) {
return -1;
}
int parentPid = proc->parentPid();
if (parentPid != -1) {
procs.updateOrAddProcess(parentPid);
KSysGuard::Process *parentProc = procs.getProcess(parentPid);
if (!parentProc) {
return -1;
}
if (!proc->cGroup().isEmpty() && parentProc->cGroup() == proc->cGroup()) {
return parentProc->pid();
}
}
return -1;
}
void Backend::setupShortcuts()
{
static std::once_flag once;
std::call_once(once, [] {
auto *collection = new KActionCollection(QCoreApplication::instance());
auto *activatePreviousTaskAction = collection->addAction(QStringLiteral("activate previous task manager entry"));
QObject::connect(activatePreviousTaskAction, &QAction::triggered, collection, [](bool) {
dispatchActivatePreviousTask();
});
activatePreviousTaskAction->setText(i18n("Activate Previous Task Manager Entry"));
KGlobalAccel::self()->setGlobalShortcut(activatePreviousTaskAction, QList<QKeySequence>() << QKeySequence(Qt::META | Qt::CTRL | Qt::Key_PageUp));
auto *activateNextTaskAction = collection->addAction(QStringLiteral("activate next task manager entry"));
QObject::connect(activateNextTaskAction, &QAction::triggered, collection, [](bool) {
dispatchActivateNextTask();
});
activateNextTaskAction->setText(i18n("Activate Next Task Manager Entry"));
KGlobalAccel::self()->setGlobalShortcut(activateNextTaskAction, QList<QKeySequence>() << QKeySequence(Qt::META | Qt::CTRL | Qt::Key_PageDown));
auto *moveTaskBackwardAction = collection->addAction(QStringLiteral("move active task to left"));
QObject::connect(moveTaskBackwardAction, &QAction::triggered, collection, [](bool) {
dispatchMoveActiveTaskBackward();
});
moveTaskBackwardAction->setText(i18n("Move Active Task Manager Entry Backward"));
KGlobalAccel::self()->setGlobalShortcut(moveTaskBackwardAction,
QList<QKeySequence>() << QKeySequence(Qt::META | Qt::CTRL | Qt::SHIFT | Qt::Key_PageUp));
auto *moveTaskForwardAction = collection->addAction(QStringLiteral("move active task to right"));
QObject::connect(moveTaskForwardAction, &QAction::triggered, collection, [](bool) {
dispatchMoveActiveTaskForward();
});
moveTaskForwardAction->setText(i18n("Move Active Task Manager Entry Forward"));
KGlobalAccel::self()->setGlobalShortcut(moveTaskForwardAction,
QList<QKeySequence>() << QKeySequence(Qt::META | Qt::CTRL | Qt::SHIFT | Qt::Key_PageDown));
for (int i = 0; i < 10; ++i) {
const int entryNumber = i + 1;
const auto key = static_cast<Qt::Key>(Qt::Key_0 + (entryNumber % 10));
QAction *action = collection->addAction(QStringLiteral("activate task manager entry %1").arg(QString::number(entryNumber)));
action->setText(i18n("Activate Task Manager Entry %1", entryNumber));
KGlobalAccel::setGlobalShortcut(action, entryNumber < 10 ? QKeySequence(Qt::META | key) : QKeySequence());
QObject::connect(action, &QAction::triggered, collection, [i](bool) {
dispatchActivateTaskAtIndex(i);
});
}
});
}
Backend *Backend::findTargetBackend()
{
Backend *fallback = nullptr;
for (auto *instance : std::as_const(s_instances)) {
if (!instance->m_hasActiveTask) {
continue;
}
if (!instance->m_showsOnlyCurrentScreen) {
return instance;
}
if (!fallback) {
fallback = instance;
}
}
return fallback;
}
void Backend::dispatchActivateTaskAtIndex(int index)
{
if (auto *target = findTargetBackend()) {
Q_EMIT target->activateTaskAtIndexRequested(index);
return;
}
Backend *fallback = nullptr;
for (auto *instance : std::as_const(s_instances)) {
if (!instance->m_showsOnlyCurrentScreen) {
Q_EMIT instance->activateTaskAtIndexRequested(index);
return;
}
if (!fallback) {
fallback = instance;
}
}
if (fallback) {
Q_EMIT fallback->activateTaskAtIndexRequested(index);
}
}
void Backend::dispatchActivatePreviousTask()
{
if (auto *target = findTargetBackend()) {
Q_EMIT target->activatePreviousTaskRequested();
}
}
void Backend::dispatchActivateNextTask()
{
if (auto *target = findTargetBackend()) {
Q_EMIT target->activateNextTaskRequested();
}
}
void Backend::dispatchMoveActiveTaskBackward()
{
if (auto *target = findTargetBackend()) {
Q_EMIT target->moveActiveTaskBackwardRequested();
}
}
void Backend::dispatchMoveActiveTaskForward()
{
if (auto *target = findTargetBackend()) {
Q_EMIT target->moveActiveTaskForwardRequested();
}
}
#include "moc_backend.cpp"