File indexing completed on 2024-06-09 05:30:55

0001 /*
0002     SPDX-FileCopyrightText: 2013 Aurélien Gâteau <agateau@kde.org>
0003     SPDX-FileCopyrightText: 2014 Eike Hein <hein@kde.org>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "actionlist.h"
0009 #include "menuentryeditor.h"
0010 
0011 #include <config-appstream.h>
0012 
0013 #include <QApplication>
0014 #include <QDesktopServices>
0015 #include <QDir>
0016 #include <QStandardPaths>
0017 
0018 #include <KApplicationTrader>
0019 #include <KFileUtils>
0020 #include <KIO/ApplicationLauncherJob>
0021 #include <KLocalizedString>
0022 #include <KNotificationJobUiDelegate>
0023 #include <KPropertiesDialog>
0024 #include <KProtocolInfo>
0025 
0026 #include <KIO/DesktopExecParser>
0027 #include <PlasmaActivities/Stats/Cleaning>
0028 #include <PlasmaActivities/Stats/ResultSet>
0029 #include <PlasmaActivities/Stats/Terms>
0030 
0031 #include "containmentinterface.h"
0032 
0033 #ifdef HAVE_APPSTREAMQT
0034 #include <AppStreamQt/pool.h>
0035 #endif
0036 
0037 namespace KAStats = KActivities::Stats;
0038 
0039 using namespace KAStats;
0040 using namespace KAStats::Terms;
0041 
0042 namespace Kicker
0043 {
0044 QVariantMap createActionItem(const QString &label, const QString &icon, const QString &actionId, const QVariant &argument)
0045 {
0046     QVariantMap map;
0047 
0048     map[QStringLiteral("text")] = label;
0049     map[QStringLiteral("icon")] = icon;
0050     map[QStringLiteral("actionId")] = actionId;
0051 
0052     if (argument.isValid()) {
0053         map[QStringLiteral("actionArgument")] = argument;
0054     }
0055 
0056     return map;
0057 }
0058 
0059 QVariantMap createTitleActionItem(const QString &label)
0060 {
0061     QVariantMap map;
0062 
0063     map[QStringLiteral("text")] = label;
0064     map[QStringLiteral("type")] = QStringLiteral("title");
0065 
0066     return map;
0067 }
0068 
0069 QVariantMap createSeparatorActionItem()
0070 {
0071     QVariantMap map;
0072 
0073     map[QStringLiteral("type")] = QStringLiteral("separator");
0074 
0075     return map;
0076 }
0077 
0078 QVariantList createActionListForFileItem(const KFileItem &fileItem)
0079 {
0080     QVariantList list;
0081 
0082     const KService::List services = KApplicationTrader::queryByMimeType(fileItem.mimetype());
0083 
0084     if (!services.isEmpty()) {
0085         list << createTitleActionItem(i18n("Open with:"));
0086 
0087         for (const KService::Ptr &service : services) {
0088             const QString text = service->name().replace(QLatin1Char('&'), QStringLiteral("&&"));
0089             const QVariantMap item = createActionItem(text, service->icon(), QStringLiteral("_kicker_fileItem_openWith"), service->entryPath());
0090 
0091             list << item;
0092         }
0093 
0094         list << createSeparatorActionItem();
0095     }
0096 
0097     const QVariantMap &propertiesItem =
0098         createActionItem(i18n("Properties"), QStringLiteral("document-properties"), QStringLiteral("_kicker_fileItem_properties"));
0099     list << propertiesItem;
0100 
0101     return list;
0102 }
0103 
0104 bool handleFileItemAction(const KFileItem &fileItem, const QString &actionId, const QVariant &argument, bool *close)
0105 {
0106     if (actionId == QLatin1String("_kicker_fileItem_properties")) {
0107         KPropertiesDialog *dlg = new KPropertiesDialog(fileItem, QApplication::activeWindow());
0108         dlg->setAttribute(Qt::WA_DeleteOnClose);
0109         dlg->show();
0110 
0111         *close = false;
0112 
0113         return true;
0114     }
0115 
0116     if (actionId == QLatin1String("_kicker_fileItem_openWith")) {
0117         const QString path = argument.toString();
0118         const KService::Ptr service = KService::serviceByDesktopPath(path);
0119 
0120         if (!service) {
0121             return false;
0122         }
0123 
0124         auto *job = new KIO::ApplicationLauncherJob(service);
0125         job->setUrls({fileItem.url()});
0126         job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled));
0127         job->start();
0128 
0129         *close = true;
0130 
0131         return true;
0132     }
0133 
0134     return false;
0135 }
0136 
0137 QVariantList createAddLauncherActionList(QObject *appletInterface, const KService::Ptr &service)
0138 {
0139     QVariantList actionList;
0140     if (!service) {
0141         return actionList;
0142     }
0143 
0144     if (ContainmentInterface::mayAddLauncher(appletInterface, ContainmentInterface::Desktop)) {
0145         QVariantMap addToDesktopAction = Kicker::createActionItem(i18n("Add to Desktop"), QStringLiteral("list-add"), QStringLiteral("addToDesktop"));
0146         actionList << addToDesktopAction;
0147     }
0148 
0149     if (service && ContainmentInterface::mayAddLauncher(appletInterface, ContainmentInterface::TaskManager, service)) {
0150         if (!ContainmentInterface::hasLauncher(appletInterface, ContainmentInterface::TaskManager, service)) {
0151             QVariantMap addToTaskManagerAction =
0152                 Kicker::createActionItem(i18n("Pin to Task Manager"), QStringLiteral("pin"), QStringLiteral("addToTaskManager"));
0153             actionList << addToTaskManagerAction;
0154         }
0155     } else if (ContainmentInterface::mayAddLauncher(appletInterface, ContainmentInterface::Panel)) {
0156         QVariantMap addToPanelAction = Kicker::createActionItem(i18n("Add to Panel (Widget)"), QStringLiteral("list-add"), QStringLiteral("addToPanel"));
0157         actionList << addToPanelAction;
0158     }
0159 
0160     return actionList;
0161 }
0162 
0163 bool handleAddLauncherAction(const QString &actionId, QObject *appletInterface, const KService::Ptr &service)
0164 {
0165     if (!service) {
0166         return false;
0167     }
0168 
0169     if (actionId == QLatin1String("addToDesktop")) {
0170         if (ContainmentInterface::mayAddLauncher(appletInterface, ContainmentInterface::Desktop)) {
0171             ContainmentInterface::addLauncher(appletInterface, ContainmentInterface::Desktop, service->entryPath());
0172         }
0173         return true;
0174     } else if (actionId == QLatin1String("addToPanel")) {
0175         if (ContainmentInterface::mayAddLauncher(appletInterface, ContainmentInterface::Panel)) {
0176             ContainmentInterface::addLauncher(appletInterface, ContainmentInterface::Panel, service->entryPath());
0177         }
0178         return true;
0179     } else if (actionId == QLatin1String("addToTaskManager")) {
0180         if (ContainmentInterface::mayAddLauncher(appletInterface, ContainmentInterface::TaskManager, service)) {
0181             ContainmentInterface::addLauncher(appletInterface, ContainmentInterface::TaskManager, service->entryPath());
0182         }
0183         return true;
0184     }
0185 
0186     return false;
0187 }
0188 
0189 QString storageIdFromService(KService::Ptr service)
0190 {
0191     QString storageId = service->storageId();
0192 
0193     if (storageId.endsWith(QLatin1String(".desktop"))) {
0194         storageId = storageId.left(storageId.length() - 8);
0195     }
0196 
0197     return storageId;
0198 }
0199 
0200 QVariantList jumpListActions(KService::Ptr service)
0201 {
0202     QVariantList list;
0203 
0204     if (!service) {
0205         return list;
0206     }
0207 
0208     // Add frequently used settings modules similar to SystemSetting's overview page.
0209     if (service->storageId() == QLatin1String("systemsettings.desktop")) {
0210         list = systemSettingsActions();
0211 
0212         if (!list.isEmpty()) {
0213             return list;
0214         }
0215     }
0216 
0217     const auto &actions = service->actions();
0218     for (const KServiceAction &action : actions) {
0219         if (action.text().isEmpty() || action.exec().isEmpty()) {
0220             continue;
0221         }
0222 
0223         QVariantMap item = createActionItem(action.text(), action.icon(), QStringLiteral("_kicker_jumpListAction"), QVariant::fromValue(action));
0224 
0225         list << item;
0226     }
0227 
0228     return list;
0229 }
0230 
0231 QVariantList systemSettingsActions()
0232 {
0233     QVariantList list;
0234 
0235     auto query = AllResources | Agent(QStringLiteral("org.kde.systemsettings")) | HighScoredFirst | Limit(5);
0236 
0237     ResultSet results(query);
0238 
0239     QStringList ids;
0240     for (const ResultSet::Result &result : results) {
0241         ids << QUrl(result.resource()).path();
0242     }
0243 
0244     if (ids.count() < 5) {
0245         // We'll load the default set of settings from its jump list actions.
0246         return list;
0247     }
0248 
0249     for (const QString &id : std::as_const(ids)) {
0250         KService::Ptr service = KService::serviceByStorageId(id);
0251         if (!service || !service->isValid()) {
0252             continue;
0253         }
0254 
0255         KServiceAction action(service->name(), service->desktopEntryName(), service->icon(), service->exec(), false, service);
0256         list << createActionItem(service->name(), service->icon(), QStringLiteral("_kicker_jumpListAction"), QVariant::fromValue(action));
0257     }
0258 
0259     return list;
0260 }
0261 
0262 QVariantList recentDocumentActions(const KService::Ptr &service)
0263 {
0264     QVariantList list;
0265 
0266     if (!service) {
0267         return list;
0268     }
0269 
0270     const QString storageId = storageIdFromService(service);
0271 
0272     if (storageId.isEmpty()) {
0273         return list;
0274     }
0275 
0276     // clang-format off
0277     auto query = UsedResources
0278         | RecentlyUsedFirst
0279         | Agent(storageId)
0280         | Type::any()
0281         | Activity::current()
0282         | Url::file();
0283     // clang-format on
0284 
0285     ResultSet results(query);
0286 
0287     ResultSet::const_iterator resultIt;
0288     resultIt = results.begin();
0289 
0290     while (list.count() < 6 && resultIt != results.end()) {
0291         const QString resource = (*resultIt).resource();
0292         const QString mimeType = (*resultIt).mimetype();
0293         const QUrl url = (*resultIt).url();
0294         ++resultIt;
0295 
0296         if (!url.isValid()) {
0297             continue;
0298         }
0299 
0300         const KFileItem fileItem(url, mimeType);
0301 
0302         if (!fileItem.isFile()) {
0303             continue;
0304         }
0305 
0306         if (list.isEmpty()) {
0307             list << createTitleActionItem(i18n("Recent Files"));
0308         }
0309 
0310         QVariantMap item = createActionItem(url.fileName(), fileItem.iconName(), QStringLiteral("_kicker_recentDocument"), QStringList{resource, mimeType});
0311 
0312         list << item;
0313     }
0314 
0315     if (!list.isEmpty()) {
0316         QVariantMap forgetAction =
0317             createActionItem(i18n("Forget Recent Files"), QStringLiteral("edit-clear-history"), QStringLiteral("_kicker_forgetRecentDocuments"));
0318         list << forgetAction;
0319     }
0320 
0321     return list;
0322 }
0323 
0324 bool handleRecentDocumentAction(KService::Ptr service, const QString &actionId, const QVariant &_argument)
0325 {
0326     if (!service) {
0327         return false;
0328     }
0329 
0330     if (actionId == QLatin1String("_kicker_forgetRecentDocuments")) {
0331         const QString storageId = storageIdFromService(service);
0332 
0333         if (storageId.isEmpty()) {
0334             return false;
0335         }
0336 
0337         // clang-format off
0338         auto query = UsedResources
0339             | Agent(storageId)
0340             | Type::any()
0341             | Activity::current()
0342             | Url::file();
0343         // clang-format on
0344 
0345         KAStats::forgetResources(query);
0346 
0347         return false;
0348     }
0349 
0350     const QStringList argument = _argument.toStringList();
0351     if (argument.isEmpty()) {
0352         return false;
0353     }
0354     const auto resource = argument.at(0);
0355     const auto mimeType = argument.at(1);
0356 
0357     // prevents using a service file that does not support opening a mime type for a file it created
0358     // for instance a screenshot tool
0359     if (!mimeType.isEmpty()) {
0360         if (!service->hasMimeType(mimeType)) {
0361             // needs to find the application that supports this mimetype
0362             service = KApplicationTrader::preferredService(mimeType);
0363 
0364             if (!service) {
0365                 // no service found to handle the mimetype
0366                 return false;
0367             }
0368         }
0369     }
0370 
0371     auto *job = new KIO::ApplicationLauncherJob(service);
0372     job->setUrls({QUrl::fromUserInput(resource)});
0373     job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled));
0374     return job->exec();
0375 }
0376 
0377 Q_GLOBAL_STATIC(MenuEntryEditor, menuEntryEditor)
0378 
0379 bool canEditApplication(const KService::Ptr &service)
0380 {
0381     return (service->isApplication() && menuEntryEditor->canEdit(service->entryPath()));
0382 }
0383 
0384 void editApplication(const QString &entryPath, const QString &menuId)
0385 {
0386     menuEntryEditor->edit(entryPath, menuId);
0387 }
0388 
0389 QVariantList editApplicationAction(const KService::Ptr &service)
0390 {
0391     QVariantList actionList;
0392 
0393     if (canEditApplication(service)) {
0394         // TODO: Using the KMenuEdit icon might be misleading.
0395         QVariantMap editAction = Kicker::createActionItem(i18n("Edit Application…"), QStringLiteral("kmenuedit"), QStringLiteral("editApplication"));
0396         actionList << editAction;
0397     }
0398 
0399     return actionList;
0400 }
0401 
0402 bool handleEditApplicationAction(const QString &actionId, const KService::Ptr &service)
0403 {
0404     if (service && actionId == QLatin1String("editApplication") && canEditApplication(service)) {
0405         Kicker::editApplication(service->entryPath(), service->menuId());
0406 
0407         return true;
0408     }
0409 
0410     return false;
0411 }
0412 
0413 #ifdef HAVE_APPSTREAMQT
0414 Q_GLOBAL_STATIC(AppStream::Pool, appstreamPool)
0415 #endif
0416 
0417 QVariantList appstreamActions(const KService::Ptr &service)
0418 {
0419     Q_UNUSED(service)
0420 #ifdef HAVE_APPSTREAMQT
0421     const KService::Ptr appStreamHandler = KApplicationTrader::preferredService(QStringLiteral("x-scheme-handler/appstream"));
0422 
0423     // Don't show action if we can't find any app to handle appstream:// URLs.
0424     if (!appStreamHandler) {
0425         return {};
0426     }
0427 
0428     QVariantMap appstreamAction = Kicker::createActionItem(i18nc("@action opens a software center with the application", "Uninstall or Manage Add-Ons…"),
0429                                                            appStreamHandler->icon(),
0430                                                            QStringLiteral("manageApplication"));
0431     return {appstreamAction};
0432 #else
0433     return {};
0434 #endif
0435 }
0436 
0437 bool handleAppstreamActions(const QString &actionId, const KService::Ptr &service)
0438 {
0439     if (actionId != QLatin1String("manageApplication")) {
0440         return false;
0441     }
0442 #ifdef HAVE_APPSTREAMQT
0443     if (!appstreamPool.exists()) {
0444         appstreamPool->load();
0445     }
0446 
0447     const auto components =
0448         appstreamPool->componentsByLaunchable(AppStream::Launchable::KindDesktopId, service->desktopEntryName() + QLatin1String(".desktop")).toList();
0449     if (components.empty()) {
0450         return false;
0451     }
0452     return QDesktopServices::openUrl(QUrl(QLatin1String("appstream://") + components[0].id()));
0453 #else
0454     return false;
0455 #endif
0456 }
0457 
0458 static QList<KServiceAction> additionalActions(const KService::Ptr &service)
0459 {
0460     QList<KServiceAction> actions;
0461     const static auto locations =
0462         QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("plasma/kickeractions"), QStandardPaths::LocateDirectory);
0463     const auto files = KFileUtils::findAllUniqueFiles(locations);
0464     for (const auto &file : files) {
0465         KService actionsService(file);
0466         const auto filter = actionsService.property<QStringList>(QStringLiteral("X-KDE-OnlyForAppIds"));
0467         if (filter.empty() || filter.contains(storageIdFromService(service))) {
0468             actions.append(actionsService.actions());
0469         }
0470     }
0471     return actions;
0472 }
0473 
0474 QVariantList additionalAppActions(const KService::Ptr &service)
0475 {
0476     QVariantList list;
0477     const auto actions = additionalActions(service);
0478     list.reserve(actions.size());
0479     for (const auto &action : actions) {
0480         list << createActionItem(action.text(), action.icon(), action.name(), action.service()->entryPath());
0481     }
0482     return list;
0483 }
0484 
0485 bool handleAdditionalAppActions(const QString &actionId, const KService::Ptr &service, const QVariant &argument)
0486 {
0487     const KService actionProvider(argument.toString());
0488     if (!actionProvider.isValid()) {
0489         return false;
0490     }
0491     const auto actions = actionProvider.actions();
0492     auto action = std::find_if(actions.begin(), actions.end(), [&actionId](const KServiceAction &action) {
0493         return action.name() == actionId;
0494     });
0495     if (action == actions.end()) {
0496         return false;
0497     }
0498     auto *job = new KIO::ApplicationLauncherJob(*action);
0499     job->setUrls({QUrl::fromLocalFile(service->entryPath())});
0500     job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled));
0501     job->start();
0502     return true;
0503 }
0504 }