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 }