File indexing completed on 2024-12-01 03:41:14
0001 /* 0002 This file is part of the KDE project 0003 SPDX-FileCopyrightText: 1998-2009 David Faure <faure@kde.org> 0004 SPDX-FileCopyrightText: 2021 Alexander Lohnau <alexander.lohnau@gmx.de> 0005 0006 SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 0007 */ 0008 0009 #include "kfileitemactions.h" 0010 #include "kfileitemactions_p.h" 0011 #include <KAbstractFileItemActionPlugin> 0012 #include <KApplicationTrader> 0013 #include <KAuthorized> 0014 #include <KConfigGroup> 0015 #include <KDesktopFile> 0016 #include <KDesktopFileAction> 0017 #include <KFileUtils> 0018 #include <KIO/ApplicationLauncherJob> 0019 #include <KIO/JobUiDelegate> 0020 #include <KLocalizedString> 0021 #include <KPluginFactory> 0022 #include <KPluginMetaData> 0023 #include <KSandbox> 0024 #include <jobuidelegatefactory.h> 0025 #include <kapplicationtrader.h> 0026 #include <kdirnotify.h> 0027 #include <kurlauthorized.h> 0028 0029 #include <QFile> 0030 #include <QMenu> 0031 #include <QMimeDatabase> 0032 #include <QtAlgorithms> 0033 0034 #ifndef KIO_ANDROID_STUB 0035 #include <QDBusConnection> 0036 #include <QDBusConnectionInterface> 0037 #include <QDBusInterface> 0038 #include <QDBusMessage> 0039 #endif 0040 #include <algorithm> 0041 #include <kio_widgets_debug.h> 0042 #include <set> 0043 0044 static bool KIOSKAuthorizedAction(const KConfigGroup &cfg) 0045 { 0046 const QStringList list = cfg.readEntry("X-KDE-AuthorizeAction", QStringList()); 0047 return std::all_of(list.constBegin(), list.constEnd(), [](const QString &action) { 0048 return KAuthorized::authorize(action.trimmed()); 0049 }); 0050 } 0051 0052 static bool mimeTypeListContains(const QStringList &list, const KFileItem &item) 0053 { 0054 const QString itemMimeType = item.mimetype(); 0055 return std::any_of(list.cbegin(), list.cend(), [&](const QString &mt) { 0056 if (mt == itemMimeType || mt == QLatin1String("all/all")) { 0057 return true; 0058 } 0059 0060 if (item.isFile() // 0061 && (mt == QLatin1String("allfiles") || mt == QLatin1String("all/allfiles") || mt == QLatin1String("application/octet-stream"))) { 0062 return true; 0063 } 0064 0065 if (item.currentMimeType().inherits(mt)) { 0066 return true; 0067 } 0068 0069 if (mt.endsWith(QLatin1String("/*"))) { 0070 const int slashPos = mt.indexOf(QLatin1Char('/')); 0071 const auto topLevelType = QStringView(mt).mid(0, slashPos); 0072 return itemMimeType.startsWith(topLevelType); 0073 } 0074 return false; 0075 }); 0076 } 0077 0078 // This helper class stores the .desktop-file actions and the servicemenus 0079 // in order to support X-KDE-Priority and X-KDE-Submenu. 0080 namespace KIO 0081 { 0082 class PopupServices 0083 { 0084 public: 0085 ServiceList &selectList(const QString &priority, const QString &submenuName); 0086 0087 ServiceList user; 0088 ServiceList userToplevel; 0089 ServiceList userPriority; 0090 0091 QMap<QString, ServiceList> userSubmenus; 0092 QMap<QString, ServiceList> userToplevelSubmenus; 0093 QMap<QString, ServiceList> userPrioritySubmenus; 0094 }; 0095 0096 ServiceList &PopupServices::selectList(const QString &priority, const QString &submenuName) 0097 { 0098 // we use the categories .desktop entry to define submenus 0099 // if none is defined, we just pop it in the main menu 0100 if (submenuName.isEmpty()) { 0101 if (priority == QLatin1String("TopLevel")) { 0102 return userToplevel; 0103 } else if (priority == QLatin1String("Important")) { 0104 return userPriority; 0105 } 0106 } else if (priority == QLatin1String("TopLevel")) { 0107 return userToplevelSubmenus[submenuName]; 0108 } else if (priority == QLatin1String("Important")) { 0109 return userPrioritySubmenus[submenuName]; 0110 } else { 0111 return userSubmenus[submenuName]; 0112 } 0113 return user; 0114 } 0115 } // namespace 0116 0117 //// 0118 0119 KFileItemActionsPrivate::KFileItemActionsPrivate(KFileItemActions *qq) 0120 : QObject() 0121 , q(qq) 0122 , m_executeServiceActionGroup(static_cast<QWidget *>(nullptr)) 0123 , m_runApplicationActionGroup(static_cast<QWidget *>(nullptr)) 0124 , m_parentWidget(nullptr) 0125 , m_config(QStringLiteral("kservicemenurc"), KConfig::NoGlobals) 0126 { 0127 QObject::connect(&m_executeServiceActionGroup, &QActionGroup::triggered, this, &KFileItemActionsPrivate::slotExecuteService); 0128 QObject::connect(&m_runApplicationActionGroup, &QActionGroup::triggered, this, &KFileItemActionsPrivate::slotRunApplication); 0129 } 0130 0131 KFileItemActionsPrivate::~KFileItemActionsPrivate() 0132 { 0133 } 0134 0135 int KFileItemActionsPrivate::insertServicesSubmenus(const QMap<QString, ServiceList> &submenus, QMenu *menu) 0136 { 0137 int count = 0; 0138 QMap<QString, ServiceList>::ConstIterator it; 0139 for (it = submenus.begin(); it != submenus.end(); ++it) { 0140 if (it.value().isEmpty()) { 0141 // avoid empty sub-menus 0142 continue; 0143 } 0144 0145 QMenu *actionSubmenu = new QMenu(menu); 0146 const int servicesAddedCount = insertServices(it.value(), actionSubmenu); 0147 0148 if (servicesAddedCount > 0) { 0149 count += servicesAddedCount; 0150 actionSubmenu->setTitle(it.key()); 0151 actionSubmenu->setIcon(QIcon::fromTheme(it.value().first().icon())); 0152 actionSubmenu->menuAction()->setObjectName(QStringLiteral("services_submenu")); // for the unittest 0153 menu->addMenu(actionSubmenu); 0154 } else { 0155 // avoid empty sub-menus 0156 delete actionSubmenu; 0157 } 0158 } 0159 0160 return count; 0161 } 0162 0163 int KFileItemActionsPrivate::insertServices(const ServiceList &list, QMenu *menu) 0164 { 0165 ServiceList sortedList = list; 0166 std::sort(sortedList.begin(), sortedList.end(), [](const KDesktopFileAction &a1, const KDesktopFileAction &a2) { 0167 return a1.name() < a2.name(); 0168 }); 0169 int count = 0; 0170 for (const KDesktopFileAction &serviceAction : std::as_const(sortedList)) { 0171 if (serviceAction.isSeparator()) { 0172 const QList<QAction *> actions = menu->actions(); 0173 if (!actions.isEmpty() && !actions.last()->isSeparator()) { 0174 menu->addSeparator(); 0175 } 0176 continue; 0177 } 0178 0179 QAction *act = new QAction(q); 0180 act->setObjectName(QStringLiteral("menuaction")); // for the unittest 0181 QString text = serviceAction.name(); 0182 text.replace(QLatin1Char('&'), QLatin1String("&&")); 0183 act->setText(text); 0184 if (!serviceAction.icon().isEmpty()) { 0185 act->setIcon(QIcon::fromTheme(serviceAction.icon())); 0186 } 0187 act->setData(QVariant::fromValue(serviceAction)); 0188 m_executeServiceActionGroup.addAction(act); 0189 0190 menu->addAction(act); // Add to toplevel menu 0191 ++count; 0192 } 0193 0194 return count; 0195 } 0196 0197 void KFileItemActionsPrivate::slotExecuteService(QAction *act) 0198 { 0199 const KDesktopFileAction serviceAction = act->data().value<KDesktopFileAction>(); 0200 if (KAuthorized::authorizeAction(serviceAction.name())) { 0201 auto *job = new KIO::ApplicationLauncherJob(serviceAction); 0202 job->setUrls(m_props.urlList()); 0203 job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget)); 0204 job->start(); 0205 } 0206 } 0207 0208 KFileItemActions::KFileItemActions(QObject *parent) 0209 : QObject(parent) 0210 , d(new KFileItemActionsPrivate(this)) 0211 { 0212 } 0213 0214 KFileItemActions::~KFileItemActions() = default; 0215 0216 void KFileItemActions::setItemListProperties(const KFileItemListProperties &itemListProperties) 0217 { 0218 d->m_props = itemListProperties; 0219 0220 d->m_mimeTypeList.clear(); 0221 const KFileItemList items = d->m_props.items(); 0222 for (const KFileItem &item : items) { 0223 if (!d->m_mimeTypeList.contains(item.mimetype())) { 0224 d->m_mimeTypeList << item.mimetype(); 0225 } 0226 } 0227 } 0228 0229 void KFileItemActions::addActionsTo(QMenu *menu, MenuActionSources sources, const QList<QAction *> &additionalActions, const QStringList &excludeList) 0230 { 0231 QMenu *actionsMenu = menu; 0232 if (sources & MenuActionSource::Services) { 0233 actionsMenu = d->addServiceActionsTo(menu, additionalActions, excludeList).menu; 0234 } else { 0235 // Since we didn't call addServiceActionsTo(), we have to add additional actions manually 0236 for (QAction *action : additionalActions) { 0237 actionsMenu->addAction(action); 0238 } 0239 } 0240 if (sources & MenuActionSource::Plugins) { 0241 d->addPluginActionsTo(menu, actionsMenu, excludeList); 0242 } 0243 } 0244 0245 // static 0246 KService::List KFileItemActions::associatedApplications(const QStringList &mimeTypeList) 0247 { 0248 return KFileItemActionsPrivate::associatedApplications(mimeTypeList, QString(), QStringList{}); 0249 } 0250 0251 static KService::Ptr preferredService(const QString &mimeType, const QStringList &excludedDesktopEntryNames, const QString &constraint) 0252 { 0253 KService::List services; 0254 if (constraint.isEmpty()) { 0255 services = KApplicationTrader::queryByMimeType(mimeType, [&](const KService::Ptr &serv) { 0256 return !excludedDesktopEntryNames.contains(serv->desktopEntryName()); 0257 }); 0258 } 0259 return services.isEmpty() ? KService::Ptr() : services.first(); 0260 } 0261 0262 void KFileItemActions::insertOpenWithActionsTo(QAction *before, QMenu *topMenu, const QStringList &excludedDesktopEntryNames) 0263 { 0264 d->insertOpenWithActionsTo(before, topMenu, excludedDesktopEntryNames, QString()); 0265 } 0266 0267 void KFileItemActionsPrivate::slotRunPreferredApplications() 0268 { 0269 const KFileItemList fileItems = m_fileOpenList; 0270 const QStringList mimeTypeList = listMimeTypes(fileItems); 0271 const QStringList serviceIdList = listPreferredServiceIds(mimeTypeList, QStringList(), m_traderConstraint); 0272 0273 for (const QString &serviceId : serviceIdList) { 0274 KFileItemList serviceItems; 0275 for (const KFileItem &item : fileItems) { 0276 const KService::Ptr serv = preferredService(item.mimetype(), QStringList(), m_traderConstraint); 0277 const QString preferredServiceId = serv ? serv->storageId() : QString(); 0278 if (preferredServiceId == serviceId) { 0279 serviceItems << item; 0280 } 0281 } 0282 0283 if (serviceId.isEmpty()) { // empty means: no associated app for this MIME type 0284 openWithByMime(serviceItems); 0285 continue; 0286 } 0287 0288 const KService::Ptr servicePtr = KService::serviceByStorageId(serviceId); // can be nullptr 0289 auto *job = new KIO::ApplicationLauncherJob(servicePtr); 0290 job->setUrls(serviceItems.urlList()); 0291 job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget)); 0292 job->start(); 0293 } 0294 } 0295 0296 void KFileItemActions::runPreferredApplications(const KFileItemList &fileOpenList) 0297 { 0298 d->m_fileOpenList = fileOpenList; 0299 d->m_traderConstraint = QString(); 0300 d->slotRunPreferredApplications(); 0301 } 0302 0303 void KFileItemActionsPrivate::openWithByMime(const KFileItemList &fileItems) 0304 { 0305 const QStringList mimeTypeList = listMimeTypes(fileItems); 0306 for (const QString &mimeType : mimeTypeList) { 0307 KFileItemList mimeItems; 0308 for (const KFileItem &item : fileItems) { 0309 if (item.mimetype() == mimeType) { 0310 mimeItems << item; 0311 } 0312 } 0313 // Show Open With dialog 0314 auto *job = new KIO::ApplicationLauncherJob(); 0315 job->setUrls(mimeItems.urlList()); 0316 job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget)); 0317 job->start(); 0318 } 0319 } 0320 0321 void KFileItemActionsPrivate::slotRunApplication(QAction *act) 0322 { 0323 // Is it an application, from one of the "Open With" actions? 0324 KService::Ptr app = act->data().value<KService::Ptr>(); 0325 Q_ASSERT(app); 0326 auto *job = new KIO::ApplicationLauncherJob(app); 0327 job->setUrls(m_props.urlList()); 0328 job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget)); 0329 job->start(); 0330 } 0331 0332 void KFileItemActionsPrivate::slotOpenWithDialog() 0333 { 0334 // The item 'Other...' or 'Open With...' has been selected 0335 Q_EMIT q->openWithDialogAboutToBeShown(); 0336 auto *job = new KIO::ApplicationLauncherJob(); 0337 job->setUrls(m_props.urlList()); 0338 job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget)); 0339 job->start(); 0340 } 0341 0342 QStringList KFileItemActionsPrivate::listMimeTypes(const KFileItemList &items) 0343 { 0344 QStringList mimeTypeList; 0345 for (const KFileItem &item : items) { 0346 if (!mimeTypeList.contains(item.mimetype())) { 0347 mimeTypeList << item.mimetype(); 0348 } 0349 } 0350 return mimeTypeList; 0351 } 0352 0353 QStringList 0354 KFileItemActionsPrivate::listPreferredServiceIds(const QStringList &mimeTypeList, const QStringList &excludedDesktopEntryNames, const QString &traderConstraint) 0355 { 0356 QStringList serviceIdList; 0357 serviceIdList.reserve(mimeTypeList.size()); 0358 for (const QString &mimeType : mimeTypeList) { 0359 const KService::Ptr serv = preferredService(mimeType, excludedDesktopEntryNames, traderConstraint); 0360 serviceIdList << (serv ? serv->storageId() : QString()); // empty string means mimetype has no associated apps 0361 } 0362 serviceIdList.removeDuplicates(); 0363 return serviceIdList; 0364 } 0365 0366 QAction *KFileItemActionsPrivate::createAppAction(const KService::Ptr &service, bool singleOffer) 0367 { 0368 QString actionName(service->name().replace(QLatin1Char('&'), QLatin1String("&&"))); 0369 if (singleOffer) { 0370 actionName = i18n("Open &with %1", actionName); 0371 } else { 0372 actionName = i18nc("@item:inmenu Open With, %1 is application name", "%1", actionName); 0373 } 0374 0375 QAction *act = new QAction(q); 0376 act->setObjectName(QStringLiteral("openwith")); // for the unittest 0377 act->setIcon(QIcon::fromTheme(service->icon())); 0378 act->setText(actionName); 0379 act->setData(QVariant::fromValue(service)); 0380 m_runApplicationActionGroup.addAction(act); 0381 return act; 0382 } 0383 0384 bool KFileItemActionsPrivate::shouldDisplayServiceMenu(const KConfigGroup &cfg, const QString &protocol) const 0385 { 0386 const QList<QUrl> urlList = m_props.urlList(); 0387 if (!KIOSKAuthorizedAction(cfg)) { 0388 return false; 0389 } 0390 if (cfg.hasKey("X-KDE-Protocol")) { 0391 const QString theProtocol = cfg.readEntry("X-KDE-Protocol"); 0392 if (theProtocol.startsWith(QLatin1Char('!'))) { // Is it excluded? 0393 if (QStringView(theProtocol).mid(1) == protocol) { 0394 return false; 0395 } 0396 } else if (protocol != theProtocol) { 0397 return false; 0398 } 0399 } else if (cfg.hasKey("X-KDE-Protocols")) { 0400 const QStringList protocols = cfg.readEntry("X-KDE-Protocols", QStringList()); 0401 if (!protocols.contains(protocol)) { 0402 return false; 0403 } 0404 } else if (protocol == QLatin1String("trash")) { 0405 // Require servicemenus for the trash to ask for protocol=trash explicitly. 0406 // Trashed files aren't supposed to be available for actions. 0407 // One might want a servicemenu for trash.desktop itself though. 0408 return false; 0409 } 0410 0411 const auto requiredNumbers = cfg.readEntry("X-KDE-RequiredNumberOfUrls", QList<int>()); 0412 if (!requiredNumbers.isEmpty() && !requiredNumbers.contains(urlList.count())) { 0413 return false; 0414 } 0415 if (cfg.hasKey("X-KDE-MinNumberOfUrls")) { 0416 const int minNumber = cfg.readEntry("X-KDE-MinNumberOfUrls").toInt(); 0417 if (urlList.count() < minNumber) { 0418 return false; 0419 } 0420 } 0421 if (cfg.hasKey("X-KDE-MaxNumberOfUrls")) { 0422 const int maxNumber = cfg.readEntry("X-KDE-MaxNumberOfUrls").toInt(); 0423 if (urlList.count() > maxNumber) { 0424 return false; 0425 } 0426 } 0427 return true; 0428 } 0429 0430 bool KFileItemActionsPrivate::checkTypesMatch(const KConfigGroup &cfg) const 0431 { 0432 const QStringList types = cfg.readXdgListEntry("MimeType"); 0433 if (types.isEmpty()) { 0434 return false; 0435 } 0436 0437 const QStringList excludeTypes = cfg.readEntry("ExcludeServiceTypes", QStringList()); 0438 const KFileItemList items = m_props.items(); 0439 return std::all_of(items.constBegin(), items.constEnd(), [&types, &excludeTypes](const KFileItem &i) { 0440 return mimeTypeListContains(types, i) && !mimeTypeListContains(excludeTypes, i); 0441 }); 0442 } 0443 0444 KFileItemActionsPrivate::ServiceActionInfo 0445 KFileItemActionsPrivate::addServiceActionsTo(QMenu *mainMenu, const QList<QAction *> &additionalActions, const QStringList &excludeList) 0446 { 0447 const KFileItemList items = m_props.items(); 0448 const KFileItem &firstItem = items.first(); 0449 const QString protocol = firstItem.url().scheme(); // assumed to be the same for all items 0450 const bool isLocal = !firstItem.localPath().isEmpty(); 0451 0452 KIO::PopupServices s; 0453 0454 // 2 - Look for "servicemenus" bindings (user-defined services) 0455 0456 // first check the .directory if this is a directory 0457 const bool isSingleLocal = items.count() == 1 && isLocal; 0458 if (m_props.isDirectory() && isSingleLocal) { 0459 const QString dotDirectoryFile = QUrl::fromLocalFile(firstItem.localPath()).path().append(QLatin1String("/.directory")); 0460 if (QFile::exists(dotDirectoryFile)) { 0461 const KDesktopFile desktopFile(dotDirectoryFile); 0462 const KConfigGroup cfg = desktopFile.desktopGroup(); 0463 0464 if (KIOSKAuthorizedAction(cfg)) { 0465 const QString priority = cfg.readEntry("X-KDE-Priority"); 0466 const QString submenuName = cfg.readEntry("X-KDE-Submenu"); 0467 ServiceList &list = s.selectList(priority, submenuName); 0468 list += desktopFile.actions(); 0469 } 0470 } 0471 } 0472 0473 const KConfigGroup showGroup = m_config.group(QStringLiteral("Show")); 0474 0475 const QMimeDatabase db; 0476 const QStringList files = serviceMenuFilePaths(); 0477 for (const QString &file : files) { 0478 const KDesktopFile desktopFile(file); 0479 const KConfigGroup cfg = desktopFile.desktopGroup(); 0480 if (!shouldDisplayServiceMenu(cfg, protocol)) { 0481 continue; 0482 } 0483 0484 const QList<KDesktopFileAction> actions = desktopFile.actions(); 0485 if (!actions.isEmpty()) { 0486 if (!checkTypesMatch(cfg)) { 0487 continue; 0488 } 0489 0490 const QString priority = cfg.readEntry("X-KDE-Priority"); 0491 const QString submenuName = cfg.readEntry("X-KDE-Submenu"); 0492 0493 ServiceList &list = s.selectList(priority, submenuName); 0494 std::copy_if(actions.cbegin(), actions.cend(), std::back_inserter(list), [&excludeList, &showGroup](const KDesktopFileAction &srvAction) { 0495 return showGroup.readEntry(srvAction.name(), true) && !excludeList.contains(srvAction.name()); 0496 }); 0497 } 0498 } 0499 0500 QMenu *actionMenu = mainMenu; 0501 int userItemCount = 0; 0502 if (s.user.count() + s.userSubmenus.count() + s.userPriority.count() + s.userPrioritySubmenus.count() + additionalActions.count() > 3) { 0503 // we have more than three items, so let's make a submenu 0504 actionMenu = new QMenu(i18nc("@title:menu", "&Actions"), mainMenu); 0505 actionMenu->setIcon(QIcon::fromTheme(QStringLiteral("view-more-symbolic"))); 0506 actionMenu->menuAction()->setObjectName(QStringLiteral("actions_submenu")); // for the unittest 0507 mainMenu->addMenu(actionMenu); 0508 } 0509 0510 userItemCount += additionalActions.count(); 0511 for (QAction *action : additionalActions) { 0512 actionMenu->addAction(action); 0513 } 0514 userItemCount += insertServicesSubmenus(s.userPrioritySubmenus, actionMenu); 0515 userItemCount += insertServices(s.userPriority, actionMenu); 0516 userItemCount += insertServicesSubmenus(s.userSubmenus, actionMenu); 0517 userItemCount += insertServices(s.user, actionMenu); 0518 0519 userItemCount += insertServicesSubmenus(s.userToplevelSubmenus, mainMenu); 0520 userItemCount += insertServices(s.userToplevel, mainMenu); 0521 0522 return {userItemCount, actionMenu}; 0523 } 0524 0525 int KFileItemActionsPrivate::addPluginActionsTo(QMenu *mainMenu, QMenu *actionsMenu, const QStringList &excludeList) 0526 { 0527 QString commonMimeType = m_props.mimeType(); 0528 if (commonMimeType.isEmpty() && m_props.isFile()) { 0529 commonMimeType = QStringLiteral("application/octet-stream"); 0530 } 0531 0532 int itemCount = 0; 0533 0534 const KConfigGroup showGroup = m_config.group(QStringLiteral("Show")); 0535 0536 const QMimeDatabase db; 0537 const auto jsonPlugins = KPluginMetaData::findPlugins(QStringLiteral("kf6/kfileitemaction"), [&db, commonMimeType](const KPluginMetaData &metaData) { 0538 auto mimeType = db.mimeTypeForName(commonMimeType); 0539 const QStringList list = metaData.mimeTypes(); 0540 return std::any_of(list.constBegin(), list.constEnd(), [mimeType](const QString &supportedMimeType) { 0541 return mimeType.inherits(supportedMimeType); 0542 }); 0543 }); 0544 0545 for (const auto &jsonMetadata : jsonPlugins) { 0546 // The plugin has been disabled 0547 const QString pluginId = jsonMetadata.pluginId(); 0548 if (!showGroup.readEntry(pluginId, true) || excludeList.contains(pluginId)) { 0549 continue; 0550 } 0551 0552 KAbstractFileItemActionPlugin *abstractPlugin = m_loadedPlugins.value(pluginId); 0553 if (!abstractPlugin) { 0554 abstractPlugin = KPluginFactory::instantiatePlugin<KAbstractFileItemActionPlugin>(jsonMetadata, this).plugin; 0555 m_loadedPlugins.insert(pluginId, abstractPlugin); 0556 } 0557 if (abstractPlugin) { 0558 connect(abstractPlugin, &KAbstractFileItemActionPlugin::error, q, &KFileItemActions::error); 0559 const QList<QAction *> actions = abstractPlugin->actions(m_props, m_parentWidget); 0560 itemCount += actions.count(); 0561 const QString showInSubmenu = jsonMetadata.value(QStringLiteral("X-KDE-Show-In-Submenu")); 0562 if (showInSubmenu == QLatin1String("true")) { 0563 actionsMenu->addActions(actions); 0564 } else { 0565 mainMenu->addActions(actions); 0566 } 0567 } 0568 } 0569 0570 return itemCount; 0571 } 0572 0573 KService::List 0574 KFileItemActionsPrivate::associatedApplications(const QStringList &mimeTypeList, const QString &traderConstraint, const QStringList &excludedDesktopEntryNames) 0575 { 0576 if (!KAuthorized::authorizeAction(QStringLiteral("openwith")) || mimeTypeList.isEmpty()) { 0577 return KService::List(); 0578 } 0579 0580 KService::List firstOffers; 0581 if (traderConstraint.isEmpty()) { 0582 firstOffers = KApplicationTrader::queryByMimeType(mimeTypeList.first(), [excludedDesktopEntryNames](const KService::Ptr &service) { 0583 return !excludedDesktopEntryNames.contains(service->desktopEntryName()); 0584 }); 0585 } 0586 0587 QList<KFileItemActionsPrivate::ServiceRank> rankings; 0588 QStringList serviceList; 0589 0590 // This section does two things. First, it determines which services are common to all the given MIME types. 0591 // Second, it ranks them based on their preference level in the associated applications list. 0592 // The more often a service appear near the front of the list, the LOWER its score. 0593 0594 rankings.reserve(firstOffers.count()); 0595 serviceList.reserve(firstOffers.count()); 0596 for (int i = 0; i < firstOffers.count(); ++i) { 0597 KFileItemActionsPrivate::ServiceRank tempRank; 0598 tempRank.service = firstOffers[i]; 0599 tempRank.score = i; 0600 rankings << tempRank; 0601 serviceList << tempRank.service->storageId(); 0602 } 0603 0604 for (int j = 1; j < mimeTypeList.count(); ++j) { 0605 KService::List offers; 0606 QStringList subservice; // list of services that support this MIME type 0607 if (traderConstraint.isEmpty()) { 0608 offers = KApplicationTrader::queryByMimeType(mimeTypeList[j], [excludedDesktopEntryNames](const KService::Ptr &service) { 0609 return !excludedDesktopEntryNames.contains(service->desktopEntryName()); 0610 }); 0611 } 0612 0613 subservice.reserve(offers.count()); 0614 for (int i = 0; i != offers.count(); ++i) { 0615 const QString serviceId = offers[i]->storageId(); 0616 subservice << serviceId; 0617 const int idPos = serviceList.indexOf(serviceId); 0618 if (idPos != -1) { 0619 rankings[idPos].score += i; 0620 } // else: we ignore the services that didn't support the previous MIME types 0621 } 0622 0623 // Remove services which supported the previous MIME types but don't support this one 0624 for (int i = 0; i < serviceList.count(); ++i) { 0625 if (!subservice.contains(serviceList[i])) { 0626 serviceList.removeAt(i); 0627 rankings.removeAt(i); 0628 --i; 0629 } 0630 } 0631 // Nothing left -> there is no common application for these MIME types 0632 if (rankings.isEmpty()) { 0633 return KService::List(); 0634 } 0635 } 0636 0637 std::sort(rankings.begin(), rankings.end(), KFileItemActionsPrivate::lessRank); 0638 0639 KService::List result; 0640 result.reserve(rankings.size()); 0641 for (const KFileItemActionsPrivate::ServiceRank &tempRank : std::as_const(rankings)) { 0642 result << tempRank.service; 0643 } 0644 0645 return result; 0646 } 0647 0648 void KFileItemActionsPrivate::insertOpenWithActionsTo(QAction *before, 0649 QMenu *topMenu, 0650 const QStringList &excludedDesktopEntryNames, 0651 const QString &traderConstraint) 0652 { 0653 if (!KAuthorized::authorizeAction(QStringLiteral("openwith"))) { 0654 return; 0655 } 0656 0657 m_traderConstraint = traderConstraint; 0658 // TODO Overload with excludedDesktopEntryNames, but this method in public API and will be handled in a new MR 0659 KService::List offers = associatedApplications(m_mimeTypeList, traderConstraint, excludedDesktopEntryNames); 0660 0661 //// Ok, we have everything, now insert 0662 0663 const KFileItemList items = m_props.items(); 0664 const KFileItem &firstItem = items.first(); 0665 const bool isLocal = firstItem.url().isLocalFile(); 0666 const bool isDir = m_props.isDirectory(); 0667 // "Open With..." for folders is really not very useful, especially for remote folders. 0668 // (media:/something, or trash:/, or ftp://...). 0669 // Don't show "open with" actions for remote dirs only 0670 if (isDir && !isLocal) { 0671 return; 0672 } 0673 0674 const auto makeOpenWithAction = [this, isDir] { 0675 auto action = new QAction(this); 0676 action->setText(isDir ? i18nc("@title:menu", "&Open Folder With...") : i18nc("@title:menu", "&Open With...")); 0677 action->setIcon(QIcon::fromTheme(QStringLiteral("system-run"))); 0678 action->setObjectName(QStringLiteral("openwith_browse")); // For the unittest 0679 return action; 0680 }; 0681 0682 #ifndef KIO_ANDROID_STUB 0683 if (KSandbox::isInside() && !m_fileOpenList.isEmpty()) { 0684 auto openWithAction = makeOpenWithAction(); 0685 QObject::connect(openWithAction, &QAction::triggered, this, [this] { 0686 const auto &items = m_fileOpenList; 0687 for (const auto &fileItem : items) { 0688 QDBusMessage message = QDBusMessage::createMethodCall(QLatin1String("org.freedesktop.portal.Desktop"), 0689 QLatin1String("/org/freedesktop/portal/desktop"), 0690 QLatin1String("org.freedesktop.portal.OpenURI"), 0691 QLatin1String("OpenURI")); 0692 message << QString() << fileItem.url() << QVariantMap{}; 0693 QDBusConnection::sessionBus().asyncCall(message); 0694 } 0695 }); 0696 topMenu->insertAction(before, openWithAction); 0697 return; 0698 } 0699 if (KSandbox::isInside()) { 0700 return; 0701 } 0702 #endif 0703 0704 QStringList serviceIdList = listPreferredServiceIds(m_mimeTypeList, excludedDesktopEntryNames, traderConstraint); 0705 0706 // When selecting files with multiple MIME types, offer either "open with <app for all>" 0707 // or a generic <open> (if there are any apps associated). 0708 if (m_mimeTypeList.count() > 1 && !serviceIdList.isEmpty() 0709 && !(serviceIdList.count() == 1 && serviceIdList.first().isEmpty())) { // empty means "no apps associated" 0710 0711 QAction *runAct = new QAction(this); 0712 if (serviceIdList.count() == 1) { 0713 const KService::Ptr app = preferredService(m_mimeTypeList.first(), excludedDesktopEntryNames, traderConstraint); 0714 runAct->setText(isDir ? i18n("&Open folder with %1", app->name()) : i18n("&Open with %1", app->name())); 0715 runAct->setIcon(QIcon::fromTheme(app->icon())); 0716 0717 // Remove that app from the offers list (#242731) 0718 for (int i = 0; i < offers.count(); ++i) { 0719 if (offers[i]->storageId() == app->storageId()) { 0720 offers.removeAt(i); 0721 break; 0722 } 0723 } 0724 } else { 0725 runAct->setText(i18n("&Open")); 0726 } 0727 0728 QObject::connect(runAct, &QAction::triggered, this, &KFileItemActionsPrivate::slotRunPreferredApplications); 0729 topMenu->insertAction(before, runAct); 0730 0731 m_traderConstraint = traderConstraint; 0732 m_fileOpenList = m_props.items(); 0733 } 0734 0735 auto openWithAct = makeOpenWithAction(); 0736 QObject::connect(openWithAct, &QAction::triggered, this, &KFileItemActionsPrivate::slotOpenWithDialog); 0737 0738 if (!offers.isEmpty()) { 0739 // Show the top app inline for files, but not folders 0740 if (!isDir) { 0741 QAction *act = createAppAction(offers.takeFirst(), true); 0742 topMenu->insertAction(before, act); 0743 } 0744 0745 // If there are still more apps, show them in a sub-menu 0746 if (!offers.isEmpty()) { // submenu 'open with' 0747 QMenu *subMenu = new QMenu(isDir ? i18nc("@title:menu", "&Open Folder With") : i18nc("@title:menu", "&Open With"), topMenu); 0748 subMenu->setIcon(QIcon::fromTheme(QStringLiteral("system-run"))); 0749 subMenu->menuAction()->setObjectName(QStringLiteral("openWith_submenu")); // For the unittest 0750 // Add other apps to the sub-menu 0751 for (const KServicePtr &service : std::as_const(offers)) { 0752 QAction *act = createAppAction(service, false); 0753 subMenu->addAction(act); 0754 } 0755 0756 subMenu->addSeparator(); 0757 0758 openWithAct->setText(i18nc("@action:inmenu Open With", "&Other Application...")); 0759 subMenu->addAction(openWithAct); 0760 0761 topMenu->insertMenu(before, subMenu); 0762 } else { // No other apps 0763 topMenu->insertAction(before, openWithAct); 0764 } 0765 } else { // no app offers -> Open With... 0766 openWithAct->setIcon(QIcon::fromTheme(QStringLiteral("system-run"))); 0767 openWithAct->setObjectName(QStringLiteral("openwith")); // For the unittest 0768 topMenu->insertAction(before, openWithAct); 0769 } 0770 0771 if (m_props.mimeType() == QLatin1String("application/x-desktop")) { 0772 const QString path = firstItem.localPath(); 0773 const ServiceList services = KDesktopFile(path).actions(); 0774 for (const KDesktopFileAction &serviceAction : services) { 0775 QAction *action = new QAction(this); 0776 action->setText(serviceAction.name()); 0777 action->setIcon(QIcon::fromTheme(serviceAction.icon())); 0778 0779 connect(action, &QAction::triggered, this, [serviceAction] { 0780 if (KAuthorized::authorizeAction(serviceAction.name())) { 0781 auto *job = new KIO::ApplicationLauncherJob(serviceAction); 0782 job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, nullptr)); 0783 job->start(); 0784 } 0785 }); 0786 0787 topMenu->addAction(action); 0788 } 0789 } 0790 0791 topMenu->insertSeparator(before); 0792 } 0793 0794 QStringList KFileItemActionsPrivate::serviceMenuFilePaths() 0795 { 0796 QStringList filePaths; 0797 0798 std::set<QString> uniqueFileNames; 0799 0800 // Load servicemenus from new install location 0801 const QStringList paths = 0802 QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kio/servicemenus"), QStandardPaths::LocateDirectory); 0803 const QStringList fromDisk = KFileUtils::findAllUniqueFiles(paths, QStringList(QStringLiteral("*.desktop"))); 0804 for (const QString &fileFromDisk : fromDisk) { 0805 if (auto [_, inserted] = uniqueFileNames.insert(fileFromDisk.split(QLatin1Char('/')).last()); inserted) { 0806 filePaths << fileFromDisk; 0807 } 0808 } 0809 return filePaths; 0810 } 0811 0812 void KFileItemActions::setParentWidget(QWidget *widget) 0813 { 0814 d->m_parentWidget = widget; 0815 } 0816 0817 #include "moc_kfileitemactions.cpp" 0818 #include "moc_kfileitemactions_p.cpp"