1/*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 1998-2009 David Faure <faure@kde.org>
4 SPDX-FileCopyrightText: 2021 Alexander Lohnau <alexander.lohnau@gmx.de>
5
6 SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
7*/
8
9#include "kfileitemactions.h"
10#include "kfileitemactions_p.h"
11#include <KAbstractFileItemActionPlugin>
12#include <KApplicationTrader>
13#include <KAuthorized>
14#include <KConfigGroup>
15#include <KDesktopFile>
16#include <KDesktopFileAction>
17#include <KFileUtils>
18#include <KIO/ApplicationLauncherJob>
19#include <KIO/JobUiDelegate>
20#include <KLocalizedString>
21#include <KPluginFactory>
22#include <KPluginMetaData>
23#include <KSandbox>
24#include <jobuidelegatefactory.h>
25#include <kapplicationtrader.h>
26#include <kdirnotify.h>
27#include <kurlauthorized.h>
28
29#include <QFile>
30#include <QMenu>
31#include <QMimeDatabase>
32#include <QtAlgorithms>
33
34#ifndef KIO_ANDROID_STUB
35#include <QDBusConnection>
36#include <QDBusConnectionInterface>
37#include <QDBusInterface>
38#include <QDBusMessage>
39#endif
40#include <algorithm>
41#include <kio_widgets_debug.h>
42#include <set>
43
44static bool KIOSKAuthorizedAction(const KConfigGroup &cfg)
45{
46 const QStringList list = cfg.readEntry(key: "X-KDE-AuthorizeAction", aDefault: QStringList());
47 return std::all_of(first: list.constBegin(), last: list.constEnd(), pred: [](const QString &action) {
48 return KAuthorized::authorize(action: action.trimmed());
49 });
50}
51
52static bool mimeTypeListContains(const QStringList &list, const KFileItem &item)
53{
54 const QString itemMimeType = item.mimetype();
55 return std::any_of(first: list.cbegin(), last: list.cend(), pred: [&](const QString &mt) {
56 if (mt == itemMimeType || mt == QLatin1String("all/all")) {
57 return true;
58 }
59
60 if (item.isFile() //
61 && (mt == QLatin1String("allfiles") || mt == QLatin1String("all/allfiles") || mt == QLatin1String("application/octet-stream"))) {
62 return true;
63 }
64
65 if (item.currentMimeType().inherits(mimeTypeName: mt)) {
66 return true;
67 }
68
69 if (mt.endsWith(s: QLatin1String("/*"))) {
70 const int slashPos = mt.indexOf(c: QLatin1Char('/'));
71 const auto topLevelType = QStringView(mt).mid(pos: 0, n: slashPos);
72 return itemMimeType.startsWith(s: topLevelType);
73 }
74 return false;
75 });
76}
77
78// This helper class stores the .desktop-file actions and the servicemenus
79// in order to support X-KDE-Priority and X-KDE-Submenu.
80namespace KIO
81{
82class PopupServices
83{
84public:
85 ServiceList &selectList(const QString &priority, const QString &submenuName);
86
87 ServiceList user;
88 ServiceList userToplevel;
89 ServiceList userPriority;
90
91 QMap<QString, ServiceList> userSubmenus;
92 QMap<QString, ServiceList> userToplevelSubmenus;
93 QMap<QString, ServiceList> userPrioritySubmenus;
94};
95
96ServiceList &PopupServices::selectList(const QString &priority, const QString &submenuName)
97{
98 // we use the categories .desktop entry to define submenus
99 // if none is defined, we just pop it in the main menu
100 if (submenuName.isEmpty()) {
101 if (priority == QLatin1String("TopLevel")) {
102 return userToplevel;
103 } else if (priority == QLatin1String("Important")) {
104 return userPriority;
105 }
106 } else if (priority == QLatin1String("TopLevel")) {
107 return userToplevelSubmenus[submenuName];
108 } else if (priority == QLatin1String("Important")) {
109 return userPrioritySubmenus[submenuName];
110 } else {
111 return userSubmenus[submenuName];
112 }
113 return user;
114}
115} // namespace
116
117////
118
119KFileItemActionsPrivate::KFileItemActionsPrivate(KFileItemActions *qq)
120 : QObject()
121 , q(qq)
122 , m_executeServiceActionGroup(static_cast<QWidget *>(nullptr))
123 , m_runApplicationActionGroup(static_cast<QWidget *>(nullptr))
124 , m_parentWidget(nullptr)
125 , m_config(QStringLiteral("kservicemenurc"), KConfig::NoGlobals)
126{
127 QObject::connect(sender: &m_executeServiceActionGroup, signal: &QActionGroup::triggered, context: this, slot: &KFileItemActionsPrivate::slotExecuteService);
128 QObject::connect(sender: &m_runApplicationActionGroup, signal: &QActionGroup::triggered, context: this, slot: &KFileItemActionsPrivate::slotRunApplication);
129}
130
131KFileItemActionsPrivate::~KFileItemActionsPrivate()
132{
133}
134
135int KFileItemActionsPrivate::insertServicesSubmenus(const QMap<QString, ServiceList> &submenus, QMenu *menu)
136{
137 int count = 0;
138 QMap<QString, ServiceList>::ConstIterator it;
139 for (it = submenus.begin(); it != submenus.end(); ++it) {
140 if (it.value().isEmpty()) {
141 // avoid empty sub-menus
142 continue;
143 }
144
145 QMenu *actionSubmenu = new QMenu(menu);
146 const int servicesAddedCount = insertServices(list: it.value(), menu: actionSubmenu);
147
148 if (servicesAddedCount > 0) {
149 count += servicesAddedCount;
150 actionSubmenu->setTitle(it.key());
151 actionSubmenu->setIcon(QIcon::fromTheme(name: it.value().first().icon()));
152 actionSubmenu->menuAction()->setObjectName(QStringLiteral("services_submenu")); // for the unittest
153 menu->addMenu(menu: actionSubmenu);
154 } else {
155 // avoid empty sub-menus
156 delete actionSubmenu;
157 }
158 }
159
160 return count;
161}
162
163int KFileItemActionsPrivate::insertServices(const ServiceList &list, QMenu *menu)
164{
165 ServiceList sortedList = list;
166 std::sort(first: sortedList.begin(), last: sortedList.end(), comp: [](const KDesktopFileAction &a1, const KDesktopFileAction &a2) {
167 return a1.name() < a2.name();
168 });
169 int count = 0;
170 for (const KDesktopFileAction &serviceAction : std::as_const(t&: sortedList)) {
171 if (serviceAction.isSeparator()) {
172 const QList<QAction *> actions = menu->actions();
173 if (!actions.isEmpty() && !actions.last()->isSeparator()) {
174 menu->addSeparator();
175 }
176 continue;
177 }
178
179 QAction *act = new QAction(q);
180 act->setObjectName(QStringLiteral("menuaction")); // for the unittest
181 QString text = serviceAction.name();
182 text.replace(c: QLatin1Char('&'), after: QLatin1String("&&"));
183 act->setText(text);
184 if (!serviceAction.icon().isEmpty()) {
185 act->setIcon(QIcon::fromTheme(name: serviceAction.icon()));
186 }
187 act->setData(QVariant::fromValue(value: serviceAction));
188 m_executeServiceActionGroup.addAction(a: act);
189
190 menu->addAction(action: act); // Add to toplevel menu
191 ++count;
192 }
193
194 return count;
195}
196
197void KFileItemActionsPrivate::slotExecuteService(QAction *act)
198{
199 const KDesktopFileAction serviceAction = act->data().value<KDesktopFileAction>();
200 if (KAuthorized::authorizeAction(action: serviceAction.name())) {
201 auto *job = new KIO::ApplicationLauncherJob(serviceAction);
202 job->setUrls(m_props.urlList());
203 job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget));
204 job->start();
205 }
206}
207
208KFileItemActions::KFileItemActions(QObject *parent)
209 : QObject(parent)
210 , d(new KFileItemActionsPrivate(this))
211{
212}
213
214KFileItemActions::~KFileItemActions() = default;
215
216void KFileItemActions::setItemListProperties(const KFileItemListProperties &itemListProperties)
217{
218 d->m_props = itemListProperties;
219
220 d->m_mimeTypeList.clear();
221 const KFileItemList items = d->m_props.items();
222 for (const KFileItem &item : items) {
223 if (!d->m_mimeTypeList.contains(str: item.mimetype())) {
224 d->m_mimeTypeList << item.mimetype();
225 }
226 }
227}
228
229void KFileItemActions::addActionsTo(QMenu *menu, MenuActionSources sources, const QList<QAction *> &additionalActions, const QStringList &excludeList)
230{
231 QMenu *actionsMenu = menu;
232 if (sources & MenuActionSource::Services) {
233 actionsMenu = d->addServiceActionsTo(mainMenu: menu, additionalActions, excludeList).menu;
234 } else {
235 // Since we didn't call addServiceActionsTo(), we have to add additional actions manually
236 for (QAction *action : additionalActions) {
237 actionsMenu->addAction(action);
238 }
239 }
240 if (sources & MenuActionSource::Plugins) {
241 d->addPluginActionsTo(mainMenu: menu, actionsMenu, excludeList);
242 }
243}
244
245// static
246KService::List KFileItemActions::associatedApplications(const QStringList &mimeTypeList)
247{
248 return KFileItemActionsPrivate::associatedApplications(mimeTypeList, excludedDesktopEntryNames: QStringList{});
249}
250
251static KService::Ptr preferredService(const QString &mimeType, const QStringList &excludedDesktopEntryNames)
252{
253 KService::List services = KApplicationTrader::queryByMimeType(mimeType, filterFunc: [&](const KService::Ptr &serv) {
254 return !excludedDesktopEntryNames.contains(str: serv->desktopEntryName());
255 });
256 return services.isEmpty() ? KService::Ptr() : services.first();
257}
258
259void KFileItemActions::insertOpenWithActionsTo(QAction *before, QMenu *topMenu, const QStringList &excludedDesktopEntryNames)
260{
261 d->insertOpenWithActionsTo(before, topMenu, excludedDesktopEntryNames);
262}
263
264void KFileItemActionsPrivate::slotRunPreferredApplications()
265{
266 const KFileItemList fileItems = m_fileOpenList;
267 const QStringList mimeTypeList = listMimeTypes(items: fileItems);
268 const QStringList serviceIdList = listPreferredServiceIds(mimeTypeList, excludedDesktopEntryNames: QStringList());
269
270 for (const QString &serviceId : serviceIdList) {
271 KFileItemList serviceItems;
272 for (const KFileItem &item : fileItems) {
273 const KService::Ptr serv = preferredService(mimeType: item.mimetype(), excludedDesktopEntryNames: QStringList());
274 const QString preferredServiceId = serv ? serv->storageId() : QString();
275 if (preferredServiceId == serviceId) {
276 serviceItems << item;
277 }
278 }
279
280 if (serviceId.isEmpty()) { // empty means: no associated app for this MIME type
281 openWithByMime(fileItems: serviceItems);
282 continue;
283 }
284
285 const KService::Ptr servicePtr = KService::serviceByStorageId(storageId: serviceId); // can be nullptr
286 auto *job = new KIO::ApplicationLauncherJob(servicePtr);
287 job->setUrls(serviceItems.urlList());
288 job->setUiDelegate(KIO::createDefaultJobUiDelegate(flags: KJobUiDelegate::AutoHandlingEnabled, window: m_parentWidget));
289 job->start();
290 }
291}
292
293void KFileItemActions::runPreferredApplications(const KFileItemList &fileOpenList)
294{
295 d->m_fileOpenList = fileOpenList;
296 d->slotRunPreferredApplications();
297}
298
299void KFileItemActionsPrivate::openWithByMime(const KFileItemList &fileItems)
300{
301 const QStringList mimeTypeList = listMimeTypes(items: fileItems);
302 for (const QString &mimeType : mimeTypeList) {
303 KFileItemList mimeItems;
304 for (const KFileItem &item : fileItems) {
305 if (item.mimetype() == mimeType) {
306 mimeItems << item;
307 }
308 }
309 // Show Open With dialog
310 auto *job = new KIO::ApplicationLauncherJob();
311 job->setUrls(mimeItems.urlList());
312 job->setUiDelegate(KIO::createDefaultJobUiDelegate(flags: KJobUiDelegate::AutoHandlingEnabled, window: m_parentWidget));
313 job->start();
314 }
315}
316
317void KFileItemActionsPrivate::slotRunApplication(QAction *act)
318{
319 // Is it an application, from one of the "Open With" actions?
320 KService::Ptr app = act->data().value<KService::Ptr>();
321 Q_ASSERT(app);
322 auto *job = new KIO::ApplicationLauncherJob(app);
323 job->setUrls(m_props.urlList());
324 job->setUiDelegate(KIO::createDefaultJobUiDelegate(flags: KJobUiDelegate::AutoHandlingEnabled, window: m_parentWidget));
325 job->start();
326}
327
328void KFileItemActionsPrivate::slotOpenWithDialog()
329{
330 // The item 'Other...' or 'Open With...' has been selected
331 Q_EMIT q->openWithDialogAboutToBeShown();
332 auto *job = new KIO::ApplicationLauncherJob();
333 job->setUrls(m_props.urlList());
334 job->setUiDelegate(KIO::createDefaultJobUiDelegate(flags: KJobUiDelegate::AutoHandlingEnabled, window: m_parentWidget));
335 job->start();
336}
337
338QStringList KFileItemActionsPrivate::listMimeTypes(const KFileItemList &items)
339{
340 QStringList mimeTypeList;
341 for (const KFileItem &item : items) {
342 if (!mimeTypeList.contains(str: item.mimetype())) {
343 mimeTypeList << item.mimetype();
344 }
345 }
346 return mimeTypeList;
347}
348
349QStringList KFileItemActionsPrivate::listPreferredServiceIds(const QStringList &mimeTypeList, const QStringList &excludedDesktopEntryNames)
350{
351 QStringList serviceIdList;
352 serviceIdList.reserve(asize: mimeTypeList.size());
353 for (const QString &mimeType : mimeTypeList) {
354 const KService::Ptr serv = preferredService(mimeType, excludedDesktopEntryNames);
355 serviceIdList << (serv ? serv->storageId() : QString()); // empty string means mimetype has no associated apps
356 }
357 serviceIdList.removeDuplicates();
358 return serviceIdList;
359}
360
361QAction *KFileItemActionsPrivate::createAppAction(const KService::Ptr &service, bool singleOffer)
362{
363 QString actionName(service->name().replace(c: QLatin1Char('&'), after: QLatin1String("&&")));
364 if (singleOffer) {
365 actionName = i18n("Open &with %1", actionName);
366 } else {
367 actionName = i18nc("@item:inmenu Open With, %1 is application name", "%1", actionName);
368 }
369
370 QAction *act = new QAction(q);
371 act->setObjectName(QStringLiteral("openwith")); // for the unittest
372 act->setIcon(QIcon::fromTheme(name: service->icon()));
373 act->setText(actionName);
374 act->setData(QVariant::fromValue(value: service));
375 m_runApplicationActionGroup.addAction(a: act);
376 return act;
377}
378
379bool KFileItemActionsPrivate::shouldDisplayServiceMenu(const KConfigGroup &cfg, const QString &protocol) const
380{
381 const QList<QUrl> urlList = m_props.urlList();
382 if (!KIOSKAuthorizedAction(cfg)) {
383 return false;
384 }
385 if (cfg.hasKey(key: "X-KDE-Protocol")) {
386 const QString theProtocol = cfg.readEntry(key: "X-KDE-Protocol");
387 if (theProtocol.startsWith(c: QLatin1Char('!'))) { // Is it excluded?
388 if (QStringView(theProtocol).mid(pos: 1) == protocol) {
389 return false;
390 }
391 } else if (protocol != theProtocol) {
392 return false;
393 }
394 } else if (cfg.hasKey(key: "X-KDE-Protocols")) {
395 const QStringList protocols = cfg.readEntry(key: "X-KDE-Protocols", aDefault: QStringList());
396 if (!protocols.contains(str: protocol)) {
397 return false;
398 }
399 } else if (protocol == QLatin1String("trash")) {
400 // Require servicemenus for the trash to ask for protocol=trash explicitly.
401 // Trashed files aren't supposed to be available for actions.
402 // One might want a servicemenu for trash.desktop itself though.
403 return false;
404 }
405
406 const auto requiredNumbers = cfg.readEntry(key: "X-KDE-RequiredNumberOfUrls", defaultValue: QList<int>());
407 if (!requiredNumbers.isEmpty() && !requiredNumbers.contains(t: urlList.count())) {
408 return false;
409 }
410 if (cfg.hasKey(key: "X-KDE-MinNumberOfUrls")) {
411 const int minNumber = cfg.readEntry(key: "X-KDE-MinNumberOfUrls").toInt();
412 if (urlList.count() < minNumber) {
413 return false;
414 }
415 }
416 if (cfg.hasKey(key: "X-KDE-MaxNumberOfUrls")) {
417 const int maxNumber = cfg.readEntry(key: "X-KDE-MaxNumberOfUrls").toInt();
418 if (urlList.count() > maxNumber) {
419 return false;
420 }
421 }
422 return true;
423}
424
425bool KFileItemActionsPrivate::checkTypesMatch(const KConfigGroup &cfg) const
426{
427 const QStringList types = cfg.readXdgListEntry(key: "MimeType");
428 if (types.isEmpty()) {
429 return false;
430 }
431
432 const QStringList excludeTypes = cfg.readEntry(key: "ExcludeServiceTypes", aDefault: QStringList());
433 const KFileItemList items = m_props.items();
434 return std::all_of(first: items.constBegin(), last: items.constEnd(), pred: [&types, &excludeTypes](const KFileItem &i) {
435 return mimeTypeListContains(list: types, item: i) && !mimeTypeListContains(list: excludeTypes, item: i);
436 });
437}
438
439KFileItemActionsPrivate::ServiceActionInfo
440KFileItemActionsPrivate::addServiceActionsTo(QMenu *mainMenu, const QList<QAction *> &additionalActions, const QStringList &excludeList)
441{
442 const KFileItemList items = m_props.items();
443 const KFileItem &firstItem = items.first();
444 const QString protocol = firstItem.url().scheme(); // assumed to be the same for all items
445 const bool isLocal = !firstItem.localPath().isEmpty();
446
447 KIO::PopupServices s;
448
449 // 2 - Look for "servicemenus" bindings (user-defined services)
450
451 // first check the .directory if this is a directory
452 const bool isSingleLocal = items.count() == 1 && isLocal;
453 if (m_props.isDirectory() && isSingleLocal) {
454 const QString dotDirectoryFile = QUrl::fromLocalFile(localfile: firstItem.localPath()).path().append(s: QLatin1String("/.directory"));
455 if (QFile::exists(fileName: dotDirectoryFile)) {
456 const KDesktopFile desktopFile(dotDirectoryFile);
457 const KConfigGroup cfg = desktopFile.desktopGroup();
458
459 if (KIOSKAuthorizedAction(cfg)) {
460 const QString priority = cfg.readEntry(key: "X-KDE-Priority");
461 const QString submenuName = cfg.readEntry(key: "X-KDE-Submenu");
462 ServiceList &list = s.selectList(priority, submenuName);
463 list += desktopFile.actions();
464 }
465 }
466 }
467
468 const KConfigGroup showGroup = m_config.group(QStringLiteral("Show"));
469
470 const QMimeDatabase db;
471 const QStringList files = serviceMenuFilePaths();
472 for (const QString &file : files) {
473 const KDesktopFile desktopFile(file);
474 const KConfigGroup cfg = desktopFile.desktopGroup();
475 if (!shouldDisplayServiceMenu(cfg, protocol)) {
476 continue;
477 }
478
479 const QList<KDesktopFileAction> actions = desktopFile.actions();
480 if (!actions.isEmpty()) {
481 if (!checkTypesMatch(cfg)) {
482 continue;
483 }
484
485 const QString priority = cfg.readEntry(key: "X-KDE-Priority");
486 const QString submenuName = cfg.readEntry(key: "X-KDE-Submenu");
487
488 ServiceList &list = s.selectList(priority, submenuName);
489 std::copy_if(first: actions.cbegin(), last: actions.cend(), result: std::back_inserter(x&: list), pred: [&excludeList, &showGroup](const KDesktopFileAction &srvAction) {
490 return showGroup.readEntry(key: srvAction.actionsKey(), aDefault: true) && !excludeList.contains(str: srvAction.actionsKey());
491 });
492 }
493 }
494
495 QMenu *actionMenu = mainMenu;
496 int userItemCount = 0;
497 if (s.user.count() + s.userSubmenus.count() + s.userPriority.count() + s.userPrioritySubmenus.count() + additionalActions.count() > 3) {
498 // we have more than three items, so let's make a submenu
499 actionMenu = new QMenu(i18nc("@title:menu", "&Actions"), mainMenu);
500 actionMenu->setIcon(QIcon::fromTheme(QStringLiteral("view-more-symbolic")));
501 actionMenu->menuAction()->setObjectName(QStringLiteral("actions_submenu")); // for the unittest
502 mainMenu->addMenu(menu: actionMenu);
503 }
504
505 userItemCount += additionalActions.count();
506 for (QAction *action : additionalActions) {
507 actionMenu->addAction(action);
508 }
509 userItemCount += insertServicesSubmenus(submenus: s.userPrioritySubmenus, menu: actionMenu);
510 userItemCount += insertServices(list: s.userPriority, menu: actionMenu);
511 userItemCount += insertServicesSubmenus(submenus: s.userSubmenus, menu: actionMenu);
512 userItemCount += insertServices(list: s.user, menu: actionMenu);
513
514 userItemCount += insertServicesSubmenus(submenus: s.userToplevelSubmenus, menu: mainMenu);
515 userItemCount += insertServices(list: s.userToplevel, menu: mainMenu);
516
517 return {.userItemCount: userItemCount, .menu: actionMenu};
518}
519
520int KFileItemActionsPrivate::addPluginActionsTo(QMenu *mainMenu, QMenu *actionsMenu, const QStringList &excludeList)
521{
522 QString commonMimeType = m_props.mimeType();
523 if (commonMimeType.isEmpty() && m_props.isFile()) {
524 commonMimeType = QStringLiteral("application/octet-stream");
525 }
526
527 int itemCount = 0;
528
529 const KConfigGroup showGroup = m_config.group(QStringLiteral("Show"));
530
531 const QMimeDatabase db;
532 const auto jsonPlugins = KPluginMetaData::findPlugins(QStringLiteral("kf6/kfileitemaction"), filter: [&db, commonMimeType](const KPluginMetaData &metaData) {
533 auto mimeType = db.mimeTypeForName(nameOrAlias: commonMimeType);
534 const QStringList list = metaData.mimeTypes();
535 return std::any_of(first: list.constBegin(), last: list.constEnd(), pred: [mimeType](const QString &supportedMimeType) {
536 return mimeType.inherits(mimeTypeName: supportedMimeType);
537 });
538 });
539
540 for (const auto &jsonMetadata : jsonPlugins) {
541 // The plugin has been disabled
542 const QString pluginId = jsonMetadata.pluginId();
543 if (!showGroup.readEntry(key: pluginId, aDefault: true) || excludeList.contains(str: pluginId)) {
544 continue;
545 }
546
547 KAbstractFileItemActionPlugin *abstractPlugin = m_loadedPlugins.value(key: pluginId);
548 if (!abstractPlugin) {
549 abstractPlugin = KPluginFactory::instantiatePlugin<KAbstractFileItemActionPlugin>(data: jsonMetadata, parent: this).plugin;
550 m_loadedPlugins.insert(key: pluginId, value: abstractPlugin);
551 }
552 if (abstractPlugin) {
553 connect(sender: abstractPlugin, signal: &KAbstractFileItemActionPlugin::error, context: q, slot: &KFileItemActions::error);
554 const QList<QAction *> actions = abstractPlugin->actions(fileItemInfos: m_props, parentWidget: m_parentWidget);
555 itemCount += actions.count();
556 const QString showInSubmenu = jsonMetadata.value(QStringLiteral("X-KDE-Show-In-Submenu"));
557 if (showInSubmenu == QLatin1String("true")) {
558 actionsMenu->addActions(actions);
559 } else {
560 mainMenu->addActions(actions);
561 }
562 }
563 }
564
565 return itemCount;
566}
567
568KService::List KFileItemActionsPrivate::associatedApplications(const QStringList &mimeTypeList, const QStringList &excludedDesktopEntryNames)
569{
570 if (!KAuthorized::authorizeAction(QStringLiteral("openwith")) || mimeTypeList.isEmpty()) {
571 return KService::List();
572 }
573
574 KService::List firstOffers = KApplicationTrader::queryByMimeType(mimeType: mimeTypeList.first(), filterFunc: [excludedDesktopEntryNames](const KService::Ptr &service) {
575 return !excludedDesktopEntryNames.contains(str: service->desktopEntryName());
576 });
577
578 QList<KFileItemActionsPrivate::ServiceRank> rankings;
579 QStringList serviceList;
580
581 // This section does two things. First, it determines which services are common to all the given MIME types.
582 // Second, it ranks them based on their preference level in the associated applications list.
583 // The more often a service appear near the front of the list, the LOWER its score.
584
585 rankings.reserve(asize: firstOffers.count());
586 serviceList.reserve(asize: firstOffers.count());
587 for (int i = 0; i < firstOffers.count(); ++i) {
588 KFileItemActionsPrivate::ServiceRank tempRank;
589 tempRank.service = firstOffers[i];
590 tempRank.score = i;
591 rankings << tempRank;
592 serviceList << tempRank.service->storageId();
593 }
594
595 for (int j = 1; j < mimeTypeList.count(); ++j) {
596 QStringList subservice; // list of services that support this MIME type
597 KService::List offers = KApplicationTrader::queryByMimeType(mimeType: mimeTypeList[j], filterFunc: [excludedDesktopEntryNames](const KService::Ptr &service) {
598 return !excludedDesktopEntryNames.contains(str: service->desktopEntryName());
599 });
600
601 subservice.reserve(asize: offers.count());
602 for (int i = 0; i != offers.count(); ++i) {
603 const QString serviceId = offers[i]->storageId();
604 subservice << serviceId;
605 const int idPos = serviceList.indexOf(str: serviceId);
606 if (idPos != -1) {
607 rankings[idPos].score += i;
608 } // else: we ignore the services that didn't support the previous MIME types
609 }
610
611 // Remove services which supported the previous MIME types but don't support this one
612 for (int i = 0; i < serviceList.count(); ++i) {
613 if (!subservice.contains(str: serviceList[i])) {
614 serviceList.removeAt(i);
615 rankings.removeAt(i);
616 --i;
617 }
618 }
619 // Nothing left -> there is no common application for these MIME types
620 if (rankings.isEmpty()) {
621 return KService::List();
622 }
623 }
624
625 std::sort(first: rankings.begin(), last: rankings.end(), comp: KFileItemActionsPrivate::lessRank);
626
627 KService::List result;
628 result.reserve(asize: rankings.size());
629 for (const KFileItemActionsPrivate::ServiceRank &tempRank : std::as_const(t&: rankings)) {
630 result << tempRank.service;
631 }
632
633 return result;
634}
635
636void KFileItemActionsPrivate::insertOpenWithActionsTo(QAction *before, QMenu *topMenu, const QStringList &excludedDesktopEntryNames)
637{
638 if (!KAuthorized::authorizeAction(QStringLiteral("openwith"))) {
639 return;
640 }
641
642 // TODO Overload with excludedDesktopEntryNames, but this method in public API and will be handled in a new MR
643 KService::List offers = associatedApplications(mimeTypeList: m_mimeTypeList, excludedDesktopEntryNames);
644
645 //// Ok, we have everything, now insert
646
647 const KFileItemList items = m_props.items();
648 const KFileItem &firstItem = items.first();
649 const bool isLocal = firstItem.url().isLocalFile();
650 const bool isDir = m_props.isDirectory();
651 // "Open With..." for folders is really not very useful, especially for remote folders.
652 // (media:/something, or trash:/, or ftp://...).
653 // Don't show "open with" actions for remote dirs only
654 if (isDir && !isLocal) {
655 return;
656 }
657
658 const auto makeOpenWithAction = [this, isDir] {
659 auto action = new QAction(this);
660 action->setText(isDir ? i18nc("@title:menu", "&Open Folder With...") : i18nc("@title:menu", "&Open With..."));
661 action->setIcon(QIcon::fromTheme(QStringLiteral("system-run")));
662 action->setObjectName(QStringLiteral("openwith_browse")); // For the unittest
663 return action;
664 };
665
666#ifndef KIO_ANDROID_STUB
667 if (KSandbox::isInside() && !m_fileOpenList.isEmpty()) {
668 auto openWithAction = makeOpenWithAction();
669 QObject::connect(sender: openWithAction, signal: &QAction::triggered, context: this, slot: [this] {
670 const auto &items = m_fileOpenList;
671 for (const auto &fileItem : items) {
672 QDBusMessage message = QDBusMessage::createMethodCall(destination: QLatin1String("org.freedesktop.portal.Desktop"),
673 path: QLatin1String("/org/freedesktop/portal/desktop"),
674 interface: QLatin1String("org.freedesktop.portal.OpenURI"),
675 method: QLatin1String("OpenURI"));
676 message << QString() << fileItem.url() << QVariantMap{};
677 QDBusConnection::sessionBus().asyncCall(message);
678 }
679 });
680 topMenu->insertAction(before, action: openWithAction);
681 return;
682 }
683 if (KSandbox::isInside()) {
684 return;
685 }
686#endif
687
688 QStringList serviceIdList = listPreferredServiceIds(mimeTypeList: m_mimeTypeList, excludedDesktopEntryNames);
689
690 // When selecting files with multiple MIME types, offer either "open with <app for all>"
691 // or a generic <open> (if there are any apps associated).
692 if (m_mimeTypeList.count() > 1 && !serviceIdList.isEmpty()
693 && !(serviceIdList.count() == 1 && serviceIdList.first().isEmpty())) { // empty means "no apps associated"
694
695 QAction *runAct = new QAction(this);
696 if (serviceIdList.count() == 1) {
697 const KService::Ptr app = preferredService(mimeType: m_mimeTypeList.first(), excludedDesktopEntryNames);
698 runAct->setText(isDir ? i18n("&Open folder with %1", app->name()) : i18n("&Open with %1", app->name()));
699 runAct->setIcon(QIcon::fromTheme(name: app->icon()));
700
701 // Remove that app from the offers list (#242731)
702 for (int i = 0; i < offers.count(); ++i) {
703 if (offers[i]->storageId() == app->storageId()) {
704 offers.removeAt(i);
705 break;
706 }
707 }
708 } else {
709 runAct->setText(i18n("&Open"));
710 }
711
712 QObject::connect(sender: runAct, signal: &QAction::triggered, context: this, slot: &KFileItemActionsPrivate::slotRunPreferredApplications);
713 topMenu->insertAction(before, action: runAct);
714
715 m_fileOpenList = m_props.items();
716 }
717
718 auto openWithAct = makeOpenWithAction();
719 QObject::connect(sender: openWithAct, signal: &QAction::triggered, context: this, slot: &KFileItemActionsPrivate::slotOpenWithDialog);
720
721 if (!offers.isEmpty()) {
722 // Show the top app inline for files, but not folders
723 if (!isDir) {
724 QAction *act = createAppAction(service: offers.takeFirst(), singleOffer: true);
725 topMenu->insertAction(before, action: act);
726 }
727
728 // If there are still more apps, show them in a sub-menu
729 if (!offers.isEmpty()) { // submenu 'open with'
730 QMenu *subMenu = new QMenu(isDir ? i18nc("@title:menu", "&Open Folder With") : i18nc("@title:menu", "&Open With"), topMenu);
731 subMenu->setIcon(QIcon::fromTheme(QStringLiteral("system-run")));
732 subMenu->menuAction()->setObjectName(QStringLiteral("openWith_submenu")); // For the unittest
733 // Add other apps to the sub-menu
734 for (const KServicePtr &service : std::as_const(t&: offers)) {
735 QAction *act = createAppAction(service, singleOffer: false);
736 subMenu->addAction(action: act);
737 }
738
739 subMenu->addSeparator();
740
741 openWithAct->setText(i18nc("@action:inmenu Open With", "&Other Application..."));
742 subMenu->addAction(action: openWithAct);
743
744 topMenu->insertMenu(before, menu: subMenu);
745 } else { // No other apps
746 topMenu->insertAction(before, action: openWithAct);
747 }
748 } else { // no app offers -> Open With...
749 openWithAct->setIcon(QIcon::fromTheme(QStringLiteral("system-run")));
750 openWithAct->setObjectName(QStringLiteral("openwith")); // For the unittest
751 topMenu->insertAction(before, action: openWithAct);
752 }
753
754 if (m_props.mimeType() == QLatin1String("application/x-desktop")) {
755 const QString path = firstItem.localPath();
756 const ServiceList services = KDesktopFile(path).actions();
757 for (const KDesktopFileAction &serviceAction : services) {
758 QAction *action = new QAction(this);
759 action->setText(serviceAction.name());
760 action->setIcon(QIcon::fromTheme(name: serviceAction.icon()));
761
762 connect(sender: action, signal: &QAction::triggered, context: this, slot: [serviceAction] {
763 if (KAuthorized::authorizeAction(action: serviceAction.name())) {
764 auto *job = new KIO::ApplicationLauncherJob(serviceAction);
765 job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, nullptr));
766 job->start();
767 }
768 });
769
770 topMenu->addAction(action);
771 }
772 }
773
774 topMenu->insertSeparator(before);
775}
776
777QStringList KFileItemActionsPrivate::serviceMenuFilePaths()
778{
779 QStringList filePaths;
780
781 std::set<QString> uniqueFileNames;
782
783 // Load servicemenus from new install location
784 const QStringList paths =
785 QStandardPaths::locateAll(type: QStandardPaths::GenericDataLocation, QStringLiteral("kio/servicemenus"), options: QStandardPaths::LocateDirectory);
786 const QStringList fromDisk = KFileUtils::findAllUniqueFiles(dirs: paths, nameFilters: QStringList(QStringLiteral("*.desktop")));
787 for (const QString &fileFromDisk : fromDisk) {
788 if (auto [_, inserted] = uniqueFileNames.insert(x: fileFromDisk.split(sep: QLatin1Char('/')).last()); inserted) {
789 filePaths << fileFromDisk;
790 }
791 }
792 return filePaths;
793}
794
795void KFileItemActions::setParentWidget(QWidget *widget)
796{
797 d->m_parentWidget = widget;
798}
799
800#include "moc_kfileitemactions.cpp"
801#include "moc_kfileitemactions_p.cpp"
802

source code of kio/src/widgets/kfileitemactions.cpp