KIO

kfileitemactions.cpp
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#ifdef WITH_QTDBUS
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("X-KDE-AuthorizeAction", QStringList());
47 return std::all_of(list.constBegin(), list.constEnd(), [](const QString &action) {
48 return KAuthorized::authorize(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(list.cbegin(), list.cend(), [&](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(mt)) {
66 return true;
67 }
68
69 if (mt.endsWith(QLatin1String("/*"))) {
70 const int slashPos = mt.indexOf(QLatin1Char('/'));
71 const auto topLevelType = QStringView(mt).mid(0, slashPos);
72 return itemMimeType.startsWith(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(&m_executeServiceActionGroup, &QActionGroup::triggered, this, &KFileItemActionsPrivate::slotExecuteService);
128 QObject::connect(&m_runApplicationActionGroup, &QActionGroup::triggered, this, &KFileItemActionsPrivate::slotRunApplication);
129}
130
131KFileItemActionsPrivate::~KFileItemActionsPrivate()
132{
133}
134
135int KFileItemActionsPrivate::insertServicesSubmenus(const QMap<QString, ServiceList> &submenus, QMenu *menu)
136{
137 int count = 0;
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(it.value(), actionSubmenu);
147
148 if (servicesAddedCount > 0) {
149 count += servicesAddedCount;
150 actionSubmenu->setTitle(it.key());
151 actionSubmenu->setIcon(QIcon::fromTheme(it.value().first().icon()));
152 actionSubmenu->menuAction()->setObjectName(QStringLiteral("services_submenu")); // for the unittest
153 menu->addMenu(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 // Temporary storage for current group and all groups
166 ServiceList currentGroup;
167 std::vector<ServiceList> allGroups;
168
169 // Grouping
170 for (const KDesktopFileAction &serviceAction : std::as_const(list)) {
171 if (serviceAction.isSeparator()) {
172 if (!currentGroup.empty()) {
173 allGroups.push_back(currentGroup);
174 currentGroup.clear();
175 }
176 // Push back a dummy list to represent a separator for later
177 allGroups.push_back(ServiceList());
178 } else {
179 currentGroup.push_back(serviceAction);
180 }
181 }
182 // Don't forget to add the last group if it exists
183 if (!currentGroup.empty()) {
184 allGroups.push_back(currentGroup);
185 }
186
187 // Sort each group
188 for (ServiceList &group : allGroups) {
189 std::sort(group.begin(), group.end(), [](const KDesktopFileAction &a1, const KDesktopFileAction &a2) {
190 return a1.name() < a2.name();
191 });
192 }
193
194 int count = 0;
195 for (const ServiceList &group : allGroups) {
196 // Check if the group is a separator
197 if (group.empty()) {
198 const QList<QAction *> actions = menu->actions();
199 if (!actions.isEmpty() && !actions.last()->isSeparator()) {
200 menu->addSeparator();
201 }
202 continue;
203 }
204
205 // Insert sorted actions for current group
206 for (const KDesktopFileAction &serviceAction : group) {
207 QAction *act = new QAction(q);
208 act->setObjectName(QStringLiteral("menuaction")); // for the unittest
209 QString text = serviceAction.name();
210 text.replace(QLatin1Char('&'), QLatin1String("&&"));
211 act->setText(text);
212 if (!serviceAction.icon().isEmpty()) {
213 act->setIcon(QIcon::fromTheme(serviceAction.icon()));
214 }
215 act->setData(QVariant::fromValue(serviceAction));
216 m_executeServiceActionGroup.addAction(act);
217
218 menu->addAction(act); // Add to toplevel menu
219 ++count;
220 }
221 }
222
223 return count;
224}
225
226void KFileItemActionsPrivate::slotExecuteService(QAction *act)
227{
228 const KDesktopFileAction serviceAction = act->data().value<KDesktopFileAction>();
229 if (KAuthorized::authorizeAction(serviceAction.name())) {
230 auto *job = new KIO::ApplicationLauncherJob(serviceAction);
231 job->setUrls(m_props.urlList());
232 job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget));
233 job->start();
234 }
235}
236
238 : QObject(parent)
239 , d(new KFileItemActionsPrivate(this))
240{
241}
242
244
246{
247 d->m_props = itemListProperties;
248
249 d->m_mimeTypeList.clear();
250 const KFileItemList items = d->m_props.items();
251 for (const KFileItem &item : items) {
252 if (!d->m_mimeTypeList.contains(item.mimetype())) {
253 d->m_mimeTypeList << item.mimetype();
254 }
255 }
256}
257
258void KFileItemActions::addActionsTo(QMenu *menu, MenuActionSources sources, const QList<QAction *> &additionalActions, const QStringList &excludeList)
259{
260 QMenu *actionsMenu = menu;
261 if (sources & MenuActionSource::Services) {
262 actionsMenu = d->addServiceActionsTo(menu, additionalActions, excludeList).menu;
263 } else {
264 // Since we didn't call addServiceActionsTo(), we have to add additional actions manually
265 for (QAction *action : additionalActions) {
266 actionsMenu->addAction(action);
267 }
268 }
269 if (sources & MenuActionSource::Plugins) {
270 d->addPluginActionsTo(menu, actionsMenu, excludeList);
271 }
272}
273
274// static
276{
277 return KFileItemActionsPrivate::associatedApplications(mimeTypeList, QStringList{});
278}
279
280static KService::Ptr preferredService(const QString &mimeType, const QStringList &excludedDesktopEntryNames)
281{
282 KService::List services = KApplicationTrader::queryByMimeType(mimeType, [&](const KService::Ptr &serv) {
283 return !excludedDesktopEntryNames.contains(serv->desktopEntryName());
284 });
285 return services.isEmpty() ? KService::Ptr() : services.first();
286}
287
288void KFileItemActions::insertOpenWithActionsTo(QAction *before, QMenu *topMenu, const QStringList &excludedDesktopEntryNames)
289{
290 d->insertOpenWithActionsTo(before, topMenu, excludedDesktopEntryNames);
291}
292
293void KFileItemActionsPrivate::slotRunPreferredApplications()
294{
295 const KFileItemList fileItems = m_fileOpenList;
296 const QStringList mimeTypeList = listMimeTypes(fileItems);
297 const QStringList serviceIdList = listPreferredServiceIds(mimeTypeList, QStringList());
298
299 for (const QString &serviceId : serviceIdList) {
300 KFileItemList serviceItems;
301 for (const KFileItem &item : fileItems) {
302 const KService::Ptr serv = preferredService(item.mimetype(), QStringList());
303 const QString preferredServiceId = serv ? serv->storageId() : QString();
304 if (preferredServiceId == serviceId) {
305 serviceItems << item;
306 }
307 }
308
309 if (serviceId.isEmpty()) { // empty means: no associated app for this MIME type
310 openWithByMime(serviceItems);
311 continue;
312 }
313
314 const KService::Ptr servicePtr = KService::serviceByStorageId(serviceId); // can be nullptr
315 auto *job = new KIO::ApplicationLauncherJob(servicePtr);
316 job->setUrls(serviceItems.urlList());
317 job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget));
318 job->start();
319 }
320}
321
323{
324 d->m_fileOpenList = fileOpenList;
325 d->slotRunPreferredApplications();
326}
327
328void KFileItemActionsPrivate::openWithByMime(const KFileItemList &fileItems)
329{
330 const QStringList mimeTypeList = listMimeTypes(fileItems);
331 for (const QString &mimeType : mimeTypeList) {
332 KFileItemList mimeItems;
333 for (const KFileItem &item : fileItems) {
334 if (item.mimetype() == mimeType) {
335 mimeItems << item;
336 }
337 }
338 // Show Open With dialog
339 auto *job = new KIO::ApplicationLauncherJob();
340 job->setUrls(mimeItems.urlList());
341 job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget));
342 job->start();
343 }
344}
345
346void KFileItemActionsPrivate::slotRunApplication(QAction *act)
347{
348 // Is it an application, from one of the "Open With" actions?
349 KService::Ptr app = act->data().value<KService::Ptr>();
350 Q_ASSERT(app);
351 auto *job = new KIO::ApplicationLauncherJob(app);
352 job->setUrls(m_props.urlList());
353 job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget));
354 job->start();
355}
356
357void KFileItemActionsPrivate::slotOpenWithDialog()
358{
359 // The item 'Other...' or 'Open With...' has been selected
360 Q_EMIT q->openWithDialogAboutToBeShown();
361 auto *job = new KIO::ApplicationLauncherJob();
362 job->setUrls(m_props.urlList());
363 job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget));
364 job->start();
365}
366
367QStringList KFileItemActionsPrivate::listMimeTypes(const KFileItemList &items)
368{
369 QStringList mimeTypeList;
370 for (const KFileItem &item : items) {
371 if (!mimeTypeList.contains(item.mimetype())) {
372 mimeTypeList << item.mimetype();
373 }
374 }
375 return mimeTypeList;
376}
377
378QStringList KFileItemActionsPrivate::listPreferredServiceIds(const QStringList &mimeTypeList, const QStringList &excludedDesktopEntryNames)
379{
380 QStringList serviceIdList;
381 serviceIdList.reserve(mimeTypeList.size());
382 for (const QString &mimeType : mimeTypeList) {
383 const KService::Ptr serv = preferredService(mimeType, excludedDesktopEntryNames);
384 serviceIdList << (serv ? serv->storageId() : QString()); // empty string means mimetype has no associated apps
385 }
386 serviceIdList.removeDuplicates();
387 return serviceIdList;
388}
389
390QAction *KFileItemActionsPrivate::createAppAction(const KService::Ptr &service, bool singleOffer)
391{
392 QString actionName(service->name().replace(QLatin1Char('&'), QLatin1String("&&")));
393 if (singleOffer) {
394 actionName = i18n("Open &with %1", actionName);
395 } else {
396 actionName = i18nc("@item:inmenu Open With, %1 is application name", "%1", actionName);
397 }
398
399 QAction *act = new QAction(q);
400 act->setObjectName(QStringLiteral("openwith")); // for the unittest
401 act->setIcon(QIcon::fromTheme(service->icon()));
402 act->setText(actionName);
403 act->setData(QVariant::fromValue(service));
404 m_runApplicationActionGroup.addAction(act);
405 return act;
406}
407
408bool KFileItemActionsPrivate::shouldDisplayServiceMenu(const KConfigGroup &cfg, const QString &protocol) const
409{
410 const QList<QUrl> urlList = m_props.urlList();
411 if (!KIOSKAuthorizedAction(cfg)) {
412 return false;
413 }
414 if (cfg.hasKey("X-KDE-Protocol")) {
415 const QString theProtocol = cfg.readEntry("X-KDE-Protocol");
416 if (theProtocol.startsWith(QLatin1Char('!'))) { // Is it excluded?
417 if (QStringView(theProtocol).mid(1) == protocol) {
418 return false;
419 }
420 } else if (protocol != theProtocol) {
421 return false;
422 }
423 } else if (cfg.hasKey("X-KDE-Protocols")) {
424 const QStringList protocols = cfg.readEntry("X-KDE-Protocols", QStringList());
425 if (!protocols.contains(protocol)) {
426 return false;
427 }
428 } else if (protocol == QLatin1String("trash")) {
429 // Require servicemenus for the trash to ask for protocol=trash explicitly.
430 // Trashed files aren't supposed to be available for actions.
431 // One might want a servicemenu for trash.desktop itself though.
432 return false;
433 }
434
435 const auto requiredNumbers = cfg.readEntry("X-KDE-RequiredNumberOfUrls", QList<int>());
436 if (!requiredNumbers.isEmpty() && !requiredNumbers.contains(urlList.count())) {
437 return false;
438 }
439 if (cfg.hasKey("X-KDE-MinNumberOfUrls")) {
440 const int minNumber = cfg.readEntry("X-KDE-MinNumberOfUrls").toInt();
441 if (urlList.count() < minNumber) {
442 return false;
443 }
444 }
445 if (cfg.hasKey("X-KDE-MaxNumberOfUrls")) {
446 const int maxNumber = cfg.readEntry("X-KDE-MaxNumberOfUrls").toInt();
447 if (urlList.count() > maxNumber) {
448 return false;
449 }
450 }
451 return true;
452}
453
454bool KFileItemActionsPrivate::checkTypesMatch(const KConfigGroup &cfg) const
455{
456 QStringList types = cfg.readXdgListEntry("MimeType");
457 if (types.isEmpty()) {
458 types = cfg.readEntry("ServiceTypes", QStringList());
459 types.removeAll(QStringLiteral("KonqPopupMenu/Plugin"));
460
461 if (types.isEmpty()) {
462 return false;
463 }
464 }
465
466 const QStringList excludeTypes = cfg.readEntry("ExcludeServiceTypes", QStringList());
467 const KFileItemList items = m_props.items();
468 return std::all_of(items.constBegin(), items.constEnd(), [&types, &excludeTypes](const KFileItem &i) {
469 return mimeTypeListContains(types, i) && !mimeTypeListContains(excludeTypes, i);
470 });
471}
472
473KFileItemActionsPrivate::ServiceActionInfo
474KFileItemActionsPrivate::addServiceActionsTo(QMenu *mainMenu, const QList<QAction *> &additionalActions, const QStringList &excludeList)
475{
476 const KFileItemList items = m_props.items();
477 const KFileItem &firstItem = items.first();
478 const QString protocol = firstItem.url().scheme(); // assumed to be the same for all items
479 const bool isLocal = !firstItem.localPath().isEmpty();
480
481 KIO::PopupServices s;
482
483 // 2 - Look for "servicemenus" bindings (user-defined services)
484
485 // first check the .directory if this is a directory
486 const bool isSingleLocal = items.count() == 1 && isLocal;
487 if (m_props.isDirectory() && isSingleLocal) {
488 const QString dotDirectoryFile = QUrl::fromLocalFile(firstItem.localPath()).path().append(QLatin1String("/.directory"));
489 if (QFile::exists(dotDirectoryFile)) {
490 const KDesktopFile desktopFile(dotDirectoryFile);
491 const KConfigGroup cfg = desktopFile.desktopGroup();
492
493 if (KIOSKAuthorizedAction(cfg)) {
494 const QString priority = cfg.readEntry("X-KDE-Priority");
495 const QString submenuName = cfg.readEntry("X-KDE-Submenu");
496 ServiceList &list = s.selectList(priority, submenuName);
497 list += desktopFile.actions();
498 }
499 }
500 }
501
502 const KConfigGroup showGroup = m_config.group(QStringLiteral("Show"));
503
504 const QMimeDatabase db;
505 const QStringList files = serviceMenuFilePaths();
506 for (const QString &file : files) {
507 const KDesktopFile desktopFile(file);
508 const KConfigGroup cfg = desktopFile.desktopGroup();
509 if (!shouldDisplayServiceMenu(cfg, protocol)) {
510 continue;
511 }
512
513 const QList<KDesktopFileAction> actions = desktopFile.actions();
514 if (!actions.isEmpty()) {
515 if (!checkTypesMatch(cfg)) {
516 continue;
517 }
518
519 const QString priority = cfg.readEntry("X-KDE-Priority");
520 const QString submenuName = cfg.readEntry("X-KDE-Submenu");
521
522 ServiceList &list = s.selectList(priority, submenuName);
523 std::copy_if(actions.cbegin(), actions.cend(), std::back_inserter(list), [&excludeList, &showGroup](const KDesktopFileAction &srvAction) {
524 return showGroup.readEntry(srvAction.actionsKey(), true) && !excludeList.contains(srvAction.actionsKey());
525 });
526 }
527 }
528
529 QMenu *actionMenu = mainMenu;
530 int userItemCount = 0;
531 if (s.user.count() + s.userSubmenus.count() + s.userPriority.count() + s.userPrioritySubmenus.count() + additionalActions.count() > 3) {
532 // we have more than three items, so let's make a submenu
533 actionMenu = new QMenu(i18nc("@title:menu", "&Actions"), mainMenu);
534 actionMenu->setIcon(QIcon::fromTheme(QStringLiteral("view-more-symbolic")));
535 actionMenu->menuAction()->setObjectName(QStringLiteral("actions_submenu")); // for the unittest
536 mainMenu->addMenu(actionMenu);
537 }
538
539 userItemCount += additionalActions.count();
540 for (QAction *action : additionalActions) {
541 actionMenu->addAction(action);
542 }
543 userItemCount += insertServicesSubmenus(s.userPrioritySubmenus, actionMenu);
544 userItemCount += insertServices(s.userPriority, actionMenu);
545 userItemCount += insertServicesSubmenus(s.userSubmenus, actionMenu);
546 userItemCount += insertServices(s.user, actionMenu);
547
548 userItemCount += insertServicesSubmenus(s.userToplevelSubmenus, mainMenu);
549 userItemCount += insertServices(s.userToplevel, mainMenu);
550
551 return {userItemCount, actionMenu};
552}
553
554int KFileItemActionsPrivate::addPluginActionsTo(QMenu *mainMenu, QMenu *actionsMenu, const QStringList &excludeList)
555{
556 QString commonMimeType = m_props.mimeType();
557 if (commonMimeType.isEmpty() && m_props.isFile()) {
558 commonMimeType = QStringLiteral("application/octet-stream");
559 }
560
561 int itemCount = 0;
562
563 const KConfigGroup showGroup = m_config.group(QStringLiteral("Show"));
564
565 const QMimeDatabase db;
566 const auto jsonPlugins = KPluginMetaData::findPlugins(QStringLiteral("kf6/kfileitemaction"), [&db, commonMimeType](const KPluginMetaData &metaData) {
567 auto mimeType = db.mimeTypeForName(commonMimeType);
568 const QStringList list = metaData.mimeTypes();
569 return std::any_of(list.constBegin(), list.constEnd(), [mimeType](const QString &supportedMimeType) {
570 return mimeType.inherits(supportedMimeType);
571 });
572 });
573
574 for (const auto &jsonMetadata : jsonPlugins) {
575 // The plugin has been disabled
576 const QString pluginId = jsonMetadata.pluginId();
577 if (!showGroup.readEntry(pluginId, true) || excludeList.contains(pluginId)) {
578 continue;
579 }
580
581 KAbstractFileItemActionPlugin *abstractPlugin = m_loadedPlugins.value(pluginId);
582 if (!abstractPlugin) {
583 abstractPlugin = KPluginFactory::instantiatePlugin<KAbstractFileItemActionPlugin>(jsonMetadata, this).plugin;
584 m_loadedPlugins.insert(pluginId, abstractPlugin);
585 }
586 if (abstractPlugin) {
588 const QList<QAction *> actions = abstractPlugin->actions(m_props, m_parentWidget);
589 itemCount += actions.count();
590 if (jsonMetadata.value(QStringLiteral("X-KDE-Show-In-Submenu"), false)) {
591 actionsMenu->addActions(actions);
592 } else {
593 mainMenu->addActions(actions);
594 }
595 }
596 }
597
598 return itemCount;
599}
600
601KService::List KFileItemActionsPrivate::associatedApplications(const QStringList &mimeTypeList, const QStringList &excludedDesktopEntryNames)
602{
603 if (!KAuthorized::authorizeAction(QStringLiteral("openwith")) || mimeTypeList.isEmpty()) {
604 return KService::List();
605 }
606
607 KService::List firstOffers = KApplicationTrader::queryByMimeType(mimeTypeList.first(), [excludedDesktopEntryNames](const KService::Ptr &service) {
608 return !excludedDesktopEntryNames.contains(service->desktopEntryName());
609 });
610
612 QStringList serviceList;
613
614 // This section does two things. First, it determines which services are common to all the given MIME types.
615 // Second, it ranks them based on their preference level in the associated applications list.
616 // The more often a service appear near the front of the list, the LOWER its score.
617
618 rankings.reserve(firstOffers.count());
619 serviceList.reserve(firstOffers.count());
620 for (int i = 0; i < firstOffers.count(); ++i) {
621 KFileItemActionsPrivate::ServiceRank tempRank;
622 tempRank.service = firstOffers[i];
623 tempRank.score = i;
624 rankings << tempRank;
625 serviceList << tempRank.service->storageId();
626 }
627
628 for (int j = 1; j < mimeTypeList.count(); ++j) {
629 QStringList subservice; // list of services that support this MIME type
630 KService::List offers = KApplicationTrader::queryByMimeType(mimeTypeList[j], [excludedDesktopEntryNames](const KService::Ptr &service) {
631 return !excludedDesktopEntryNames.contains(service->desktopEntryName());
632 });
633
634 subservice.reserve(offers.count());
635 for (int i = 0; i != offers.count(); ++i) {
636 const QString serviceId = offers[i]->storageId();
637 subservice << serviceId;
638 const int idPos = serviceList.indexOf(serviceId);
639 if (idPos != -1) {
640 rankings[idPos].score += i;
641 } // else: we ignore the services that didn't support the previous MIME types
642 }
643
644 // Remove services which supported the previous MIME types but don't support this one
645 for (int i = 0; i < serviceList.count(); ++i) {
646 if (!subservice.contains(serviceList[i])) {
647 serviceList.removeAt(i);
648 rankings.removeAt(i);
649 --i;
650 }
651 }
652 // Nothing left -> there is no common application for these MIME types
653 if (rankings.isEmpty()) {
654 return KService::List();
655 }
656 }
657
658 std::sort(rankings.begin(), rankings.end(), KFileItemActionsPrivate::lessRank);
659
660 KService::List result;
661 result.reserve(rankings.size());
662 for (const KFileItemActionsPrivate::ServiceRank &tempRank : std::as_const(rankings)) {
663 result << tempRank.service;
664 }
665
666 return result;
667}
668
669void KFileItemActionsPrivate::insertOpenWithActionsTo(QAction *before, QMenu *topMenu, const QStringList &excludedDesktopEntryNames)
670{
671 if (!KAuthorized::authorizeAction(QStringLiteral("openwith"))) {
672 return;
673 }
674
675 // TODO Overload with excludedDesktopEntryNames, but this method in public API and will be handled in a new MR
676 KService::List offers = associatedApplications(m_mimeTypeList, excludedDesktopEntryNames);
677
678 //// Ok, we have everything, now insert
679
680 const KFileItemList items = m_props.items();
681 const KFileItem &firstItem = items.first();
682 const bool isLocal = firstItem.url().isLocalFile();
683 const bool isDir = m_props.isDirectory();
684 // "Open With..." for folders is really not very useful, especially for remote folders.
685 // (media:/something, or trash:/, or ftp://...).
686 // Don't show "open with" actions for remote dirs only
687 if (isDir && !isLocal) {
688 return;
689 }
690
691 const auto makeOpenWithAction = [this, isDir] {
692 auto action = new QAction(this);
693 action->setText(isDir ? i18nc("@action:inmenu", "&Open Folder With…") : i18nc("@action:inmenu", "&Open With…"));
694 action->setIcon(QIcon::fromTheme(QStringLiteral("system-run")));
695 action->setObjectName(QStringLiteral("openwith_browse")); // For the unittest
696 return action;
697 };
698
699#ifdef WITH_QTDBUS
700 if (KSandbox::isInside() && !m_fileOpenList.isEmpty()) {
701 auto openWithAction = makeOpenWithAction();
702 QObject::connect(openWithAction, &QAction::triggered, this, [this] {
703 const auto &items = m_fileOpenList;
704 for (const auto &fileItem : items) {
705 QDBusMessage message = QDBusMessage::createMethodCall(QLatin1String("org.freedesktop.portal.Desktop"),
706 QLatin1String("/org/freedesktop/portal/desktop"),
707 QLatin1String("org.freedesktop.portal.OpenURI"),
708 QLatin1String("OpenURI"));
709 message << QString() << fileItem.url() << QVariantMap{};
711 }
712 });
713 topMenu->insertAction(before, openWithAction);
714 return;
715 }
716 if (KSandbox::isInside()) {
717 return;
718 }
719#endif
720
721 QStringList serviceIdList = listPreferredServiceIds(m_mimeTypeList, excludedDesktopEntryNames);
722
723 // When selecting files with multiple MIME types, offer either "open with <app for all>"
724 // or a generic <open> (if there are any apps associated).
725 if (m_mimeTypeList.count() > 1 && !serviceIdList.isEmpty()
726 && !(serviceIdList.count() == 1 && serviceIdList.first().isEmpty())) { // empty means "no apps associated"
727
728 QAction *runAct = new QAction(this);
729 if (serviceIdList.count() == 1) {
730 const KService::Ptr app = preferredService(m_mimeTypeList.first(), excludedDesktopEntryNames);
731 runAct->setText(isDir ? i18n("&Open folder with %1", app->name()) : i18n("&Open with %1", app->name()));
732 runAct->setIcon(QIcon::fromTheme(app->icon()));
733
734 // Remove that app from the offers list (#242731)
735 for (int i = 0; i < offers.count(); ++i) {
736 if (offers[i]->storageId() == app->storageId()) {
737 offers.removeAt(i);
738 break;
739 }
740 }
741 } else {
742 runAct->setText(i18n("&Open"));
743 }
744
745 QObject::connect(runAct, &QAction::triggered, this, &KFileItemActionsPrivate::slotRunPreferredApplications);
746 topMenu->insertAction(before, runAct);
747
748 m_fileOpenList = m_props.items();
749 }
750
751 auto openWithAct = makeOpenWithAction();
752 QObject::connect(openWithAct, &QAction::triggered, this, &KFileItemActionsPrivate::slotOpenWithDialog);
753
754 if (!offers.isEmpty()) {
755 // Show the top app inline for files, but not folders
756 if (!isDir) {
757 QAction *act = createAppAction(offers.takeFirst(), true);
758 topMenu->insertAction(before, act);
759 }
760
761 // If there are still more apps, show them in a sub-menu
762 if (!offers.isEmpty()) { // submenu 'open with'
763 QMenu *subMenu = new QMenu(isDir ? i18nc("@title:menu", "&Open Folder With") : i18nc("@title:menu", "&Open With"), topMenu);
764 subMenu->setIcon(QIcon::fromTheme(QStringLiteral("system-run")));
765 subMenu->menuAction()->setObjectName(QStringLiteral("openWith_submenu")); // For the unittest
766 // Add other apps to the sub-menu
767 for (const KServicePtr &service : std::as_const(offers)) {
768 QAction *act = createAppAction(service, false);
769 subMenu->addAction(act);
770 }
771
772 subMenu->addSeparator();
773
774 openWithAct->setText(i18nc("@action:inmenu Open With", "&Other Application…"));
775 subMenu->addAction(openWithAct);
776
777 topMenu->insertMenu(before, subMenu);
778 } else { // No other apps
779 topMenu->insertAction(before, openWithAct);
780 }
781 } else { // no app offers -> Open With...
782 openWithAct->setIcon(QIcon::fromTheme(QStringLiteral("system-run")));
783 openWithAct->setObjectName(QStringLiteral("openwith")); // For the unittest
784 topMenu->insertAction(before, openWithAct);
785 }
786
787 if (m_props.mimeType() == QLatin1String("application/x-desktop")) {
788 const QString path = firstItem.localPath();
789 const ServiceList services = KDesktopFile(path).actions();
790 for (const KDesktopFileAction &serviceAction : services) {
791 QAction *action = new QAction(this);
792 action->setText(serviceAction.name());
793 action->setIcon(QIcon::fromTheme(serviceAction.icon()));
794
795 connect(action, &QAction::triggered, this, [serviceAction] {
796 if (KAuthorized::authorizeAction(serviceAction.name())) {
797 auto *job = new KIO::ApplicationLauncherJob(serviceAction);
798 job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, nullptr));
799 job->start();
800 }
801 });
802
803 topMenu->addAction(action);
804 }
805 }
806
807 topMenu->insertSeparator(before);
808}
809
810QStringList KFileItemActionsPrivate::serviceMenuFilePaths()
811{
812 QStringList filePaths;
813
814 std::set<QString> uniqueFileNames;
815
816 // Load servicemenus from new install location
817 const QStringList paths =
819 QStringList fromDisk = KFileUtils::findAllUniqueFiles(paths, QStringList(QStringLiteral("*.desktop")));
820
821 // Also search in kservices5 for compatibility with older existing files
822 const QStringList legacyPaths =
824 const QStringList legacyFiles = KFileUtils::findAllUniqueFiles(legacyPaths, QStringList(QStringLiteral("*.desktop")));
825
826 for (const QString &path : legacyFiles) {
827 KDesktopFile file(path);
828
829 const QStringList serviceTypes = file.desktopGroup().readEntry("ServiceTypes", QStringList());
830 if (serviceTypes.contains(QStringLiteral("KonqPopupMenu/Plugin"))) {
831 fromDisk << path;
832 }
833 }
834
835 for (const QString &fileFromDisk : std::as_const(fromDisk)) {
836 if (auto [_, inserted] = uniqueFileNames.insert(fileFromDisk.split(QLatin1Char('/')).last()); inserted) {
837 filePaths << fileFromDisk;
838 }
839 }
840 return filePaths;
841}
842
844{
845 d->m_parentWidget = widget;
846}
847
848#include "moc_kfileitemactions.cpp"
849#include "moc_kfileitemactions_p.cpp"
Base class for KFileItemAction plugins.
void error(const QString &errorMessage)
Emits an error which will be displayed to the user.
virtual QList< QAction * > actions(const KFileItemListProperties &fileItemInfos, QWidget *parentWidget)=0
Implement the actions method in the plugin in order to create actions.
static Q_INVOKABLE bool authorizeAction(const QString &action)
KConfigGroup group(const QString &group)
bool hasKey(const char *key) const
QStringList readXdgListEntry(const char *key, const QStringList &aDefault=QStringList()) const
QString readEntry(const char *key, const char *aDefault=nullptr) const
QString icon() const
QString name() const
QList< KDesktopFileAction > actions() const
This class creates and handles the actions for a url (or urls) in a popupmenu.
void error(const QString &errorMessage)
Forwards the errors from the KAbstractFileItemActionPlugin instances.
static KService::List associatedApplications(const QStringList &mimeTypeList)
Returns the applications associated with all the given MIME types.
void insertOpenWithActionsTo(QAction *before, QMenu *topMenu, const QStringList &excludedDesktopEntryNames)
Generates the "Open With <Application>" actions, and inserts them in menu, before action before.
void setItemListProperties(const KFileItemListProperties &itemList)
Sets all the data for the next instance of the popupmenu.
~KFileItemActions() override
Destructor.
void addActionsTo(QMenu *menu, MenuActionSources sources=MenuActionSource::All, const QList< QAction * > &additionalActions={}, const QStringList &excludeList={})
This methods adds additional actions to the menu.
@ Services
Add user defined actions and servicemenu actions (this used to include builtin actions,...
@ Plugins
Add actions implemented by plugins. See KAbstractFileItemActionPlugin base class.
void setParentWidget(QWidget *widget)
Set the parent widget for any dialogs being shown.
void runPreferredApplications(const KFileItemList &fileOpenList)
Slot used to execute a list of files in their respective preferred application.
KFileItemActions(QObject *parent=nullptr)
Creates a KFileItemActions instance.
Provides information about the common properties of a group of KFileItem objects.
List of KFileItems, which adds a few helper methods to QList<KFileItem>.
Definition kfileitem.h:632
QList< QUrl > urlList() const
A KFileItem is a generic class to handle a file, local or remote.
Definition kfileitem.h:36
ApplicationLauncherJob runs an application and watches it while running.
QStringList mimeTypes() const
static QList< KPluginMetaData > findPlugins(const QString &directory, std::function< bool(const KPluginMetaData &)> filter={}, KPluginMetaDataOptions options={})
static Ptr serviceByStorageId(const QString &_storageId)
QString storageId() const
QString desktopEntryName() const
QList< Ptr > List
QString icon() const
QExplicitlySharedDataPointer< KService > Ptr
QString name() const
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
KSERVICE_EXPORT KService::List queryByMimeType(const QString &mimeType, FilterFunc filterFunc={})
KSERVICE_EXPORT KService::Ptr preferredService(const QString &mimeType)
KCALUTILS_EXPORT QString mimeType()
KCOREADDONS_EXPORT QStringList findAllUniqueFiles(const QStringList &dirs, const QStringList &nameFilters={})
A namespace for KIO globals.
KIOCORE_EXPORT KJobUiDelegate * createDefaultJobUiDelegate()
Convenience method: use default factory, if there's one, to create a delegate and return it.
QString path(const QString &relativePath)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
Returns a list of directories associated with this file-class.
KCOREADDONS_EXPORT bool isInside()
QVariant data() const const
void setIcon(const QIcon &icon)
void setData(const QVariant &data)
void setText(const QString &text)
void triggered(bool checked)
QDBusPendingCall asyncCall(const QDBusMessage &message, int timeout) const const
QDBusConnection sessionBus()
QDBusMessage createMethodCall(const QString &service, const QString &path, const QString &interface, const QString &method)
bool exists() const const
QIcon fromTheme(const QString &name)
iterator begin()
const_iterator cbegin() const const
const_iterator cend() const const
const_iterator constBegin() const const
const_iterator constEnd() const const
qsizetype count() const const
iterator end()
T & first()
bool isEmpty() const const
T & last()
qsizetype removeAll(const AT &t)
void removeAt(qsizetype i)
void reserve(qsizetype size)
qsizetype size() const const
value_type takeFirst()
ConstIterator
iterator begin()
size_type count() const const
iterator end()
Key key(const T &value, const Key &defaultKey) const const
T value(const Key &key, const T &defaultValue) const const
QAction * addAction(const QIcon &icon, const QString &text, Functor functor, const QKeySequence &shortcut)
QAction * addMenu(QMenu *menu)
QAction * addSeparator()
void setIcon(const QIcon &icon)
QAction * insertMenu(QAction *before, QMenu *menu)
QAction * insertSeparator(QAction *before)
QAction * menuAction() const const
void setTitle(const QString &title)
QMimeType mimeTypeForName(const QString &nameOrAlias) const const
bool inherits(const QString &mimeTypeName) const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void setObjectName(QAnyStringView name)
QStringList locateAll(StandardLocation type, const QString &fileName, LocateOptions options)
QString & append(QChar ch)
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
int toInt(bool *ok, int base) const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
qsizetype indexOf(const QRegularExpression &re, qsizetype from) const const
qsizetype removeDuplicates()
QStringView mid(qsizetype start, qsizetype length) const const
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QUrl fromLocalFile(const QString &localFile)
bool isLocalFile() const const
QString path(ComponentFormattingOptions options) const const
QString scheme() const const
QVariant fromValue(T &&value)
T value() const const
QList< QAction * > actions() const const
void addActions(const QList< QAction * > &actions)
void insertAction(QAction *before, QAction *action)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:16:28 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.