File indexing completed on 2024-05-19 05:35:56

0001 /*
0002     SPDX-FileCopyrightText: 2012-2016 Eike Hein <hein@kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "backend.h"
0008 
0009 #include "log_settings.h"
0010 #include <KConfigGroup>
0011 #include <KDesktopFile>
0012 #include <KFileItem>
0013 #include <KFilePlacesModel>
0014 #include <KLocalizedString>
0015 #include <KNotificationJobUiDelegate>
0016 #include <KProtocolInfo>
0017 #include <KService>
0018 #include <KServiceAction>
0019 #include <KWindowEffects>
0020 #include <KWindowSystem>
0021 
0022 #include <KApplicationTrader>
0023 #include <KIO/ApplicationLauncherJob>
0024 
0025 #include <QAction>
0026 #include <QActionGroup>
0027 #include <QApplication>
0028 #include <QDBusConnection>
0029 #include <QDBusConnectionInterface>
0030 #include <QDBusMessage>
0031 #include <QDBusMetaType>
0032 #include <QDBusPendingCall>
0033 #include <QDBusReply>
0034 #include <QDBusServiceWatcher>
0035 #include <QJsonArray>
0036 #include <QMenu>
0037 #include <QQuickItem>
0038 #include <QQuickWindow>
0039 #include <QStandardPaths>
0040 #include <QTimer>
0041 #include <QVersionNumber>
0042 
0043 #include <PlasmaActivities/Consumer>
0044 #include <PlasmaActivities/Stats/Cleaning>
0045 #include <PlasmaActivities/Stats/ResultSet>
0046 #include <PlasmaActivities/Stats/Terms>
0047 
0048 #include <processcore/process.h>
0049 #include <processcore/processes.h>
0050 
0051 namespace KAStats = KActivities::Stats;
0052 
0053 using namespace KAStats;
0054 using namespace KAStats::Terms;
0055 
0056 static const QString highlightWindowName = QStringLiteral("org.kde.KWin.HighlightWindow");
0057 static const QString highlightWindowPath = QStringLiteral("/org/kde/KWin/HighlightWindow");
0058 static const QString &highlightWindowInterface = highlightWindowName;
0059 
0060 static const QString appViewName = QStringLiteral("org.kde.KWin.Effect.WindowView1");
0061 static const QString appViewPath = QStringLiteral("/org/kde/KWin/Effect/WindowView1");
0062 static const QString &appViewInterface = appViewName;
0063 
0064 Backend::Backend(QObject *parent)
0065     : QObject(parent)
0066     , m_highlightWindows(false)
0067     , m_actionGroup(new QActionGroup(this))
0068 {
0069     m_windowViewAvailable = QDBusConnection::sessionBus().interface()->isServiceRegistered(appViewName);
0070     auto watcher = new QDBusServiceWatcher(appViewName,
0071                                            QDBusConnection::sessionBus(),
0072                                            QDBusServiceWatcher::WatchForRegistration | QDBusServiceWatcher::WatchForUnregistration,
0073                                            this);
0074     connect(watcher, &QDBusServiceWatcher::serviceRegistered, this, [this] {
0075         m_windowViewAvailable = true;
0076         Q_EMIT windowViewAvailableChanged();
0077     });
0078     connect(watcher, &QDBusServiceWatcher::serviceUnregistered, this, [this] {
0079         m_windowViewAvailable = false;
0080         Q_EMIT windowViewAvailableChanged();
0081     });
0082 }
0083 
0084 Backend::~Backend()
0085 {
0086 }
0087 
0088 bool Backend::highlightWindows() const
0089 {
0090     return m_highlightWindows;
0091 }
0092 
0093 void Backend::setHighlightWindows(bool highlight)
0094 {
0095     if (highlight != m_highlightWindows) {
0096         m_highlightWindows = highlight;
0097 
0098         updateWindowHighlight();
0099 
0100         Q_EMIT highlightWindowsChanged();
0101     }
0102 }
0103 
0104 QUrl Backend::tryDecodeApplicationsUrl(const QUrl &launcherUrl)
0105 {
0106     if (launcherUrl.isValid() && launcherUrl.scheme() == QLatin1String("applications")) {
0107         const KService::Ptr service = KService::serviceByMenuId(launcherUrl.path());
0108 
0109         if (service) {
0110             return QUrl::fromLocalFile(service->entryPath());
0111         }
0112     }
0113 
0114     return launcherUrl;
0115 }
0116 
0117 QStringList Backend::applicationCategories(const QUrl &launcherUrl)
0118 {
0119     const QUrl desktopEntryUrl = tryDecodeApplicationsUrl(launcherUrl);
0120 
0121     if (!desktopEntryUrl.isValid() || !desktopEntryUrl.isLocalFile() || !KDesktopFile::isDesktopFile(desktopEntryUrl.toLocalFile())) {
0122         return QStringList();
0123     }
0124 
0125     KDesktopFile desktopFile(desktopEntryUrl.toLocalFile());
0126 
0127     // Since we can't have dynamic jump list actions, at least add the user's "Places" for file managers.
0128     return desktopFile.desktopGroup().readXdgListEntry(QStringLiteral("Categories"));
0129 }
0130 
0131 QVariantList Backend::jumpListActions(const QUrl &launcherUrl, QObject *parent)
0132 {
0133     QVariantList actions;
0134 
0135     if (!parent) {
0136         return actions;
0137     }
0138 
0139     QUrl desktopEntryUrl = tryDecodeApplicationsUrl(launcherUrl);
0140 
0141     if (!desktopEntryUrl.isValid() || !desktopEntryUrl.isLocalFile() || !KDesktopFile::isDesktopFile(desktopEntryUrl.toLocalFile())) {
0142         return actions;
0143     }
0144 
0145     const KService::Ptr service = KService::serviceByDesktopPath(desktopEntryUrl.toLocalFile());
0146     if (!service) {
0147         return actions;
0148     }
0149 
0150     if (service->storageId() == QLatin1String("systemsettings.desktop")) {
0151         actions = systemSettingsActions(parent);
0152         if (!actions.isEmpty()) {
0153             return actions;
0154         }
0155     }
0156 
0157     const auto jumpListActions = service->actions();
0158 
0159     for (const KServiceAction &serviceAction : jumpListActions) {
0160         if (serviceAction.noDisplay()) {
0161             continue;
0162         }
0163 
0164         QAction *action = new QAction(parent);
0165         action->setText(serviceAction.text());
0166         action->setIcon(QIcon::fromTheme(serviceAction.icon()));
0167         if (serviceAction.isSeparator()) {
0168             action->setSeparator(true);
0169         }
0170 
0171         connect(action, &QAction::triggered, this, [serviceAction]() {
0172             auto *job = new KIO::ApplicationLauncherJob(serviceAction);
0173             auto *delegate = new KNotificationJobUiDelegate;
0174             delegate->setAutoErrorHandlingEnabled(true);
0175             job->setUiDelegate(delegate);
0176             job->start();
0177         });
0178 
0179         actions << QVariant::fromValue<QAction *>(action);
0180     }
0181 
0182     return actions;
0183 }
0184 
0185 QVariantList Backend::systemSettingsActions(QObject *parent) const
0186 {
0187     QVariantList actions;
0188 
0189     auto query = AllResources | Agent(QStringLiteral("org.kde.systemsettings")) | HighScoredFirst | Limit(5);
0190 
0191     ResultSet results(query);
0192 
0193     QStringList ids;
0194     for (const ResultSet::Result &result : results) {
0195         ids << QUrl(result.resource()).path();
0196     }
0197 
0198     if (ids.count() < 5) {
0199         // We'll load the default set of settings from its jump list actions.
0200         return actions;
0201     }
0202 
0203     for (const QString &id : std::as_const(ids)) {
0204         KService::Ptr service = KService::serviceByStorageId(id);
0205         if (!service || !service->isValid()) {
0206             continue;
0207         }
0208 
0209         QAction *action = new QAction(parent);
0210         action->setText(service->name());
0211         action->setIcon(QIcon::fromTheme(service->icon()));
0212 
0213         connect(action, &QAction::triggered, this, [service]() {
0214             auto *job = new KIO::ApplicationLauncherJob(service);
0215             auto *delegate = new KNotificationJobUiDelegate;
0216             delegate->setAutoErrorHandlingEnabled(true);
0217             job->setUiDelegate(delegate);
0218             job->start();
0219         });
0220 
0221         actions << QVariant::fromValue<QAction *>(action);
0222     }
0223     return actions;
0224 }
0225 
0226 QVariantList Backend::placesActions(const QUrl &launcherUrl, bool showAllPlaces, QObject *parent)
0227 {
0228     if (!parent) {
0229         return QVariantList();
0230     }
0231 
0232     QUrl desktopEntryUrl = tryDecodeApplicationsUrl(launcherUrl);
0233 
0234     if (!desktopEntryUrl.isValid() || !desktopEntryUrl.isLocalFile() || !KDesktopFile::isDesktopFile(desktopEntryUrl.toLocalFile())) {
0235         return QVariantList();
0236     }
0237 
0238     QVariantList actions;
0239 
0240     // Since we can't have dynamic jump list actions, at least add the user's "Places" for file managers.
0241     if (!applicationCategories(launcherUrl).contains(QLatin1String("FileManager"))) {
0242         return actions;
0243     }
0244 
0245     QString previousGroup;
0246     QMenu *subMenu = nullptr;
0247 
0248     std::unique_ptr<KFilePlacesModel> placesModel(new KFilePlacesModel());
0249     for (int i = 0; i < placesModel->rowCount(); ++i) {
0250         QModelIndex idx = placesModel->index(i, 0);
0251 
0252         if (placesModel->isHidden(idx)) {
0253             continue;
0254         }
0255 
0256         const QString &title = idx.data(Qt::DisplayRole).toString();
0257         const QIcon &icon = idx.data(Qt::DecorationRole).value<QIcon>();
0258         const QUrl &url = idx.data(KFilePlacesModel::UrlRole).toUrl();
0259 
0260         QAction *placeAction = new QAction(icon, title, parent);
0261 
0262         connect(placeAction, &QAction::triggered, this, [url, desktopEntryUrl] {
0263             KService::Ptr service = KService::serviceByDesktopPath(desktopEntryUrl.toLocalFile());
0264             if (!service) {
0265                 return;
0266             }
0267 
0268             auto *job = new KIO::ApplicationLauncherJob(service);
0269             auto *delegate = new KNotificationJobUiDelegate;
0270             delegate->setAutoErrorHandlingEnabled(true);
0271             job->setUiDelegate(delegate);
0272 
0273             job->setUrls({url});
0274             job->start();
0275         });
0276 
0277         const QString &groupName = idx.data(KFilePlacesModel::GroupRole).toString();
0278         if (previousGroup.isEmpty()) { // Skip first group heading.
0279             previousGroup = groupName;
0280         }
0281 
0282         // Put all subsequent categories into a submenu.
0283         if (previousGroup != groupName) {
0284             QAction *subMenuAction = new QAction(groupName, parent);
0285             subMenu = new QMenu();
0286             // Cannot parent a QMenu to a QAction, need to delete it manually.
0287             connect(parent, &QObject::destroyed, subMenu, &QObject::deleteLater);
0288             subMenuAction->setMenu(subMenu);
0289 
0290             actions << QVariant::fromValue(subMenuAction);
0291 
0292             previousGroup = groupName;
0293         }
0294 
0295         if (subMenu) {
0296             subMenu->addAction(placeAction);
0297         } else {
0298             actions << QVariant::fromValue(placeAction);
0299         }
0300     }
0301 
0302     // There is nothing more frustrating than having a "More" entry that ends up showing just one or two
0303     // additional entries. Therefore we truncate to max. 5 entries only if there are more than 7 in total.
0304     if (!showAllPlaces && actions.count() > 7) {
0305         const int totalActionCount = actions.count();
0306 
0307         while (actions.count() > 5) {
0308             actions.removeLast();
0309         }
0310 
0311         QAction *action = new QAction(parent);
0312         action->setText(i18ncp("Show all user Places", "%1 more Place", "%1 more Places", totalActionCount - actions.count()));
0313         connect(action, &QAction::triggered, this, &Backend::showAllPlaces);
0314         actions << QVariant::fromValue(action);
0315     }
0316 
0317     return actions;
0318 }
0319 
0320 QVariantList Backend::recentDocumentActions(const QUrl &launcherUrl, QObject *parent)
0321 {
0322     QVariantList actions;
0323     if (!parent) {
0324         return actions;
0325     }
0326 
0327     QUrl desktopEntryUrl = tryDecodeApplicationsUrl(launcherUrl);
0328 
0329     if (!desktopEntryUrl.isValid() || !desktopEntryUrl.isLocalFile() || !KDesktopFile::isDesktopFile(desktopEntryUrl.toLocalFile())) {
0330         return QVariantList();
0331     }
0332 
0333     QString desktopName = desktopEntryUrl.fileName();
0334     QString storageId = desktopName;
0335 
0336     if (storageId.endsWith(QLatin1String(".desktop"))) {
0337         storageId = storageId.left(storageId.length() - 8);
0338     }
0339 
0340     auto query = UsedResources | RecentlyUsedFirst | Agent(storageId) | Type::any() | Activity::current();
0341 
0342     ResultSet results(query);
0343 
0344     ResultSet::const_iterator resultIt = results.begin();
0345 
0346     int actionCount = 0;
0347 
0348     bool allFolders = true;
0349     bool allDownloads = true;
0350     bool allRemoteWithoutFileName = true;
0351     const QString downloadsPath = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
0352 
0353     while (actionCount < 5 && resultIt != results.end()) {
0354         const QString resource = (*resultIt).resource();
0355         const QString mimetype = (*resultIt).mimetype();
0356         const QUrl url = (*resultIt).url();
0357         ++resultIt;
0358 
0359         if (!url.isValid()) {
0360             continue;
0361         }
0362 
0363         allFolders = allFolders && mimetype == QLatin1String("inode/directory");
0364         allDownloads = allDownloads && url.toLocalFile().startsWith(downloadsPath);
0365         allRemoteWithoutFileName = allRemoteWithoutFileName && !url.isLocalFile() && url.fileName().isEmpty();
0366 
0367         QString name = url.fileName();
0368         if (name.isEmpty()) {
0369             name = url.toDisplayString();
0370         }
0371 
0372         QString iconName;
0373 
0374         const QString protocol = url.scheme();
0375         if (!KProtocolInfo::isKnownProtocol(protocol) || KProtocolInfo::isHelperProtocol(protocol)) {
0376             const KService::Ptr service = KApplicationTrader::preferredService(QLatin1String("x-scheme-handler/") + protocol);
0377             if (service) {
0378                 iconName = service->icon();
0379             } else if (KProtocolInfo::isKnownProtocol(protocol)) {
0380                 Q_ASSERT(KProtocolInfo::isHelperProtocol(protocol));
0381                 iconName = KProtocolInfo::icon(protocol);
0382             } else {
0383                 // Should not happen?
0384                 continue;
0385             }
0386         } else {
0387             const KFileItem fileItem(url, mimetype);
0388             iconName = fileItem.iconName();
0389         }
0390 
0391         QAction *action = new QAction(parent);
0392         action->setText(name);
0393         action->setIcon(QIcon::fromTheme(iconName, QIcon::fromTheme(QStringLiteral("unknown"))));
0394         action->setProperty("agent", storageId);
0395         action->setProperty("entryPath", desktopEntryUrl);
0396         action->setProperty("mimeType", mimetype);
0397         action->setData(url);
0398         connect(action, &QAction::triggered, this, &Backend::handleRecentDocumentAction);
0399 
0400         actions << QVariant::fromValue<QAction *>(action);
0401 
0402         ++actionCount;
0403     }
0404 
0405     if (actionCount > 0) {
0406         // Overrides section heading on QML side
0407         if (allDownloads) {
0408             actions.prepend(i18n("Recent Downloads"));
0409         } else if (allRemoteWithoutFileName) {
0410             actions.prepend(i18n("Recent Connections"));
0411         } else if (allFolders) {
0412             actions.prepend(i18n("Recent Places"));
0413         }
0414 
0415         QAction *separatorAction = new QAction(parent);
0416         separatorAction->setSeparator(true);
0417         actions << QVariant::fromValue<QAction *>(separatorAction);
0418 
0419         QAction *action = new QAction(parent);
0420         if (allDownloads) {
0421             action->setText(i18nc("@action:inmenu", "Forget Recent Downloads"));
0422         } else if (allRemoteWithoutFileName) {
0423             action->setText(i18nc("@action:inmenu", "Forget Recent Connections"));
0424         } else if (allFolders) {
0425             action->setText(i18nc("@action:inmenu", "Forget Recent Places"));
0426         } else {
0427             action->setText(i18nc("@action:inmenu", "Forget Recent Files"));
0428         }
0429         action->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-history")));
0430         action->setProperty("agent", storageId);
0431         connect(action, &QAction::triggered, this, &Backend::handleRecentDocumentAction);
0432         actions << QVariant::fromValue<QAction *>(action);
0433     }
0434 
0435     return actions;
0436 }
0437 
0438 void Backend::handleRecentDocumentAction() const
0439 {
0440     const QAction *action = qobject_cast<QAction *>(sender());
0441 
0442     if (!action) {
0443         return;
0444     }
0445 
0446     const QString agent = action->property("agent").toString();
0447 
0448     if (agent.isEmpty()) {
0449         return;
0450     }
0451 
0452     const QString desktopPath = action->property("entryPath").toUrl().toLocalFile();
0453     const QUrl url = action->data().toUrl();
0454 
0455     if (desktopPath.isEmpty() || url.isEmpty()) {
0456         auto query = UsedResources | Agent(agent) | Type::any() | Activity::current() | Url::file();
0457 
0458         KAStats::forgetResources(query);
0459 
0460         return;
0461     }
0462 
0463     KService::Ptr service = KService::serviceByDesktopPath(desktopPath);
0464 
0465     if (!service) {
0466         return;
0467     }
0468 
0469     // prevents using a service file that does not support opening a mime type for a file it created
0470     // for instance spectacle
0471     const auto mimetype = action->property("mimeType").toString();
0472     if (!mimetype.isEmpty()) {
0473         if (!service->hasMimeType(mimetype)) {
0474             // needs to find the application that supports this mimetype
0475             service = KApplicationTrader::preferredService(mimetype);
0476 
0477             if (!service) {
0478                 // no service found to handle the mimetype
0479                 return;
0480             } else {
0481                 qCWarning(TASKMANAGER_DEBUG) << "Preventing the file to open with " << service->desktopEntryName() << "no alternative found";
0482             }
0483         }
0484     }
0485 
0486     auto *job = new KIO::ApplicationLauncherJob(service);
0487     auto *delegate = new KNotificationJobUiDelegate;
0488     delegate->setAutoErrorHandlingEnabled(true);
0489     job->setUiDelegate(delegate);
0490     job->setUrls({url});
0491     job->start();
0492 }
0493 
0494 void Backend::setActionGroup(QAction *action) const
0495 {
0496     if (action) {
0497         action->setActionGroup(m_actionGroup);
0498     }
0499 }
0500 
0501 QRect Backend::globalRect(QQuickItem *item) const
0502 {
0503     if (!item || !item->window()) {
0504         return QRect();
0505     }
0506 
0507     QRect iconRect(item->x(), item->y(), item->width(), item->height());
0508     iconRect.moveTopLeft(item->parentItem()->mapToScene(iconRect.topLeft()).toPoint());
0509     iconRect.moveTopLeft(item->window()->mapToGlobal(iconRect.topLeft()));
0510 
0511     return iconRect;
0512 }
0513 
0514 bool Backend::windowViewAvailable() const
0515 {
0516     return m_windowViewAvailable;
0517 }
0518 
0519 void Backend::activateWindowView(const QVariant &_winIds)
0520 {
0521     if (m_windowsToHighlight.count()) {
0522         m_windowsToHighlight.clear();
0523         updateWindowHighlight();
0524     }
0525 
0526     auto message = QDBusMessage::createMethodCall(appViewName, appViewPath, appViewInterface, QStringLiteral("activate"));
0527     message << _winIds.toStringList();
0528     QDBusConnection::sessionBus().asyncCall(message);
0529 }
0530 
0531 bool Backend::isApplication(const QUrl &url) const
0532 {
0533     if (!url.isValid() || !url.isLocalFile()) {
0534         return false;
0535     }
0536 
0537     const QString &localPath = url.toLocalFile();
0538 
0539     if (!KDesktopFile::isDesktopFile(localPath)) {
0540         return false;
0541     }
0542 
0543     KDesktopFile desktopFile(localPath);
0544     return desktopFile.hasApplicationType();
0545 }
0546 
0547 void Backend::cancelHighlightWindows()
0548 {
0549     m_windowsToHighlight.clear();
0550     updateWindowHighlight();
0551 }
0552 
0553 qint64 Backend::parentPid(qint64 pid) const
0554 {
0555     KSysGuard::Processes procs;
0556     procs.updateOrAddProcess(pid);
0557 
0558     KSysGuard::Process *proc = procs.getProcess(pid);
0559     if (!proc) {
0560         return -1;
0561     }
0562 
0563     int parentPid = proc->parentPid();
0564     if (parentPid != -1) {
0565         procs.updateOrAddProcess(parentPid);
0566 
0567         KSysGuard::Process *parentProc = procs.getProcess(parentPid);
0568         if (!parentProc) {
0569             return -1;
0570         }
0571 
0572         if (!proc->cGroup().isEmpty() && parentProc->cGroup() == proc->cGroup()) {
0573             return parentProc->pid();
0574         }
0575     }
0576 
0577     return -1;
0578 }
0579 
0580 void Backend::windowsHovered(const QVariant &_winIds, bool hovered)
0581 {
0582     m_windowsToHighlight.clear();
0583 
0584     if (hovered) {
0585         m_windowsToHighlight = _winIds.toStringList();
0586     }
0587 
0588     // Avoid flickering when scrolling in the tooltip
0589     QTimer::singleShot(0, this, &Backend::updateWindowHighlight);
0590 }
0591 
0592 void Backend::updateWindowHighlight()
0593 {
0594     if (!m_highlightWindows) {
0595         return;
0596     }
0597     auto message = QDBusMessage::createMethodCall(highlightWindowName, highlightWindowPath, highlightWindowInterface, QStringLiteral("highlightWindows"));
0598     message << m_windowsToHighlight;
0599     QDBusConnection::sessionBus().asyncCall(message);
0600 }
0601 
0602 #include "moc_backend.cpp"