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 const QStringList types = cfg.readXdgListEntry("MimeType");
457 if (types.isEmpty()) {
458 return false;
459 }
460
461 const QStringList excludeTypes = cfg.readEntry("ExcludeServiceTypes", QStringList());
462 const KFileItemList items = m_props.items();
463 return std::all_of(items.constBegin(), items.constEnd(), [&types, &excludeTypes](const KFileItem &i) {
464 return mimeTypeListContains(types, i) && !mimeTypeListContains(excludeTypes, i);
465 });
466}
467
468KFileItemActionsPrivate::ServiceActionInfo
469KFileItemActionsPrivate::addServiceActionsTo(QMenu *mainMenu, const QList<QAction *> &additionalActions, const QStringList &excludeList)
470{
471 const KFileItemList items = m_props.items();
472 const KFileItem &firstItem = items.first();
473 const QString protocol = firstItem.url().scheme(); // assumed to be the same for all items
474 const bool isLocal = !firstItem.localPath().isEmpty();
475
476 KIO::PopupServices s;
477
478 // 2 - Look for "servicemenus" bindings (user-defined services)
479
480 // first check the .directory if this is a directory
481 const bool isSingleLocal = items.count() == 1 && isLocal;
482 if (m_props.isDirectory() && isSingleLocal) {
483 const QString dotDirectoryFile = QUrl::fromLocalFile(firstItem.localPath()).path().append(QLatin1String("/.directory"));
484 if (QFile::exists(dotDirectoryFile)) {
485 const KDesktopFile desktopFile(dotDirectoryFile);
486 const KConfigGroup cfg = desktopFile.desktopGroup();
487
488 if (KIOSKAuthorizedAction(cfg)) {
489 const QString priority = cfg.readEntry("X-KDE-Priority");
490 const QString submenuName = cfg.readEntry("X-KDE-Submenu");
491 ServiceList &list = s.selectList(priority, submenuName);
492 list += desktopFile.actions();
493 }
494 }
495 }
496
497 const KConfigGroup showGroup = m_config.group(QStringLiteral("Show"));
498
499 const QMimeDatabase db;
500 const QStringList files = serviceMenuFilePaths();
501 for (const QString &file : files) {
502 const KDesktopFile desktopFile(file);
503 const KConfigGroup cfg = desktopFile.desktopGroup();
504 if (!shouldDisplayServiceMenu(cfg, protocol)) {
505 continue;
506 }
507
508 const QList<KDesktopFileAction> actions = desktopFile.actions();
509 if (!actions.isEmpty()) {
510 if (!checkTypesMatch(cfg)) {
511 continue;
512 }
513
514 const QString priority = cfg.readEntry("X-KDE-Priority");
515 const QString submenuName = cfg.readEntry("X-KDE-Submenu");
516
517 ServiceList &list = s.selectList(priority, submenuName);
518 std::copy_if(actions.cbegin(), actions.cend(), std::back_inserter(list), [&excludeList, &showGroup](const KDesktopFileAction &srvAction) {
519 return showGroup.readEntry(srvAction.actionsKey(), true) && !excludeList.contains(srvAction.actionsKey());
520 });
521 }
522 }
523
524 QMenu *actionMenu = mainMenu;
525 int userItemCount = 0;
526 if (s.user.count() + s.userSubmenus.count() + s.userPriority.count() + s.userPrioritySubmenus.count() + additionalActions.count() > 3) {
527 // we have more than three items, so let's make a submenu
528 actionMenu = new QMenu(i18nc("@title:menu", "&Actions"), mainMenu);
529 actionMenu->setIcon(QIcon::fromTheme(QStringLiteral("view-more-symbolic")));
530 actionMenu->menuAction()->setObjectName(QStringLiteral("actions_submenu")); // for the unittest
531 mainMenu->addMenu(actionMenu);
532 }
533
534 userItemCount += additionalActions.count();
535 for (QAction *action : additionalActions) {
536 actionMenu->addAction(action);
537 }
538 userItemCount += insertServicesSubmenus(s.userPrioritySubmenus, actionMenu);
539 userItemCount += insertServices(s.userPriority, actionMenu);
540 userItemCount += insertServicesSubmenus(s.userSubmenus, actionMenu);
541 userItemCount += insertServices(s.user, actionMenu);
542
543 userItemCount += insertServicesSubmenus(s.userToplevelSubmenus, mainMenu);
544 userItemCount += insertServices(s.userToplevel, mainMenu);
545
546 return {userItemCount, actionMenu};
547}
548
549int KFileItemActionsPrivate::addPluginActionsTo(QMenu *mainMenu, QMenu *actionsMenu, const QStringList &excludeList)
550{
551 QString commonMimeType = m_props.mimeType();
552 if (commonMimeType.isEmpty() && m_props.isFile()) {
553 commonMimeType = QStringLiteral("application/octet-stream");
554 }
555
556 int itemCount = 0;
557
558 const KConfigGroup showGroup = m_config.group(QStringLiteral("Show"));
559
560 const QMimeDatabase db;
561 const auto jsonPlugins = KPluginMetaData::findPlugins(QStringLiteral("kf6/kfileitemaction"), [&db, commonMimeType](const KPluginMetaData &metaData) {
562 auto mimeType = db.mimeTypeForName(commonMimeType);
563 const QStringList list = metaData.mimeTypes();
564 return std::any_of(list.constBegin(), list.constEnd(), [mimeType](const QString &supportedMimeType) {
565 return mimeType.inherits(supportedMimeType);
566 });
567 });
568
569 for (const auto &jsonMetadata : jsonPlugins) {
570 // The plugin has been disabled
571 const QString pluginId = jsonMetadata.pluginId();
572 if (!showGroup.readEntry(pluginId, true) || excludeList.contains(pluginId)) {
573 continue;
574 }
575
576 KAbstractFileItemActionPlugin *abstractPlugin = m_loadedPlugins.value(pluginId);
577 if (!abstractPlugin) {
578 abstractPlugin = KPluginFactory::instantiatePlugin<KAbstractFileItemActionPlugin>(jsonMetadata, this).plugin;
579 m_loadedPlugins.insert(pluginId, abstractPlugin);
580 }
581 if (abstractPlugin) {
583 const QList<QAction *> actions = abstractPlugin->actions(m_props, m_parentWidget);
584 itemCount += actions.count();
585 const QString showInSubmenu = jsonMetadata.value(QStringLiteral("X-KDE-Show-In-Submenu"));
586 if (showInSubmenu == QLatin1String("true")) {
587 actionsMenu->addActions(actions);
588 } else {
589 mainMenu->addActions(actions);
590 }
591 }
592 }
593
594 return itemCount;
595}
596
597KService::List KFileItemActionsPrivate::associatedApplications(const QStringList &mimeTypeList, const QStringList &excludedDesktopEntryNames)
598{
599 if (!KAuthorized::authorizeAction(QStringLiteral("openwith")) || mimeTypeList.isEmpty()) {
600 return KService::List();
601 }
602
603 KService::List firstOffers = KApplicationTrader::queryByMimeType(mimeTypeList.first(), [excludedDesktopEntryNames](const KService::Ptr &service) {
604 return !excludedDesktopEntryNames.contains(service->desktopEntryName());
605 });
606
608 QStringList serviceList;
609
610 // This section does two things. First, it determines which services are common to all the given MIME types.
611 // Second, it ranks them based on their preference level in the associated applications list.
612 // The more often a service appear near the front of the list, the LOWER its score.
613
614 rankings.reserve(firstOffers.count());
615 serviceList.reserve(firstOffers.count());
616 for (int i = 0; i < firstOffers.count(); ++i) {
617 KFileItemActionsPrivate::ServiceRank tempRank;
618 tempRank.service = firstOffers[i];
619 tempRank.score = i;
620 rankings << tempRank;
621 serviceList << tempRank.service->storageId();
622 }
623
624 for (int j = 1; j < mimeTypeList.count(); ++j) {
625 QStringList subservice; // list of services that support this MIME type
626 KService::List offers = KApplicationTrader::queryByMimeType(mimeTypeList[j], [excludedDesktopEntryNames](const KService::Ptr &service) {
627 return !excludedDesktopEntryNames.contains(service->desktopEntryName());
628 });
629
630 subservice.reserve(offers.count());
631 for (int i = 0; i != offers.count(); ++i) {
632 const QString serviceId = offers[i]->storageId();
633 subservice << serviceId;
634 const int idPos = serviceList.indexOf(serviceId);
635 if (idPos != -1) {
636 rankings[idPos].score += i;
637 } // else: we ignore the services that didn't support the previous MIME types
638 }
639
640 // Remove services which supported the previous MIME types but don't support this one
641 for (int i = 0; i < serviceList.count(); ++i) {
642 if (!subservice.contains(serviceList[i])) {
643 serviceList.removeAt(i);
644 rankings.removeAt(i);
645 --i;
646 }
647 }
648 // Nothing left -> there is no common application for these MIME types
649 if (rankings.isEmpty()) {
650 return KService::List();
651 }
652 }
653
654 std::sort(rankings.begin(), rankings.end(), KFileItemActionsPrivate::lessRank);
655
656 KService::List result;
657 result.reserve(rankings.size());
658 for (const KFileItemActionsPrivate::ServiceRank &tempRank : std::as_const(rankings)) {
659 result << tempRank.service;
660 }
661
662 return result;
663}
664
665void KFileItemActionsPrivate::insertOpenWithActionsTo(QAction *before, QMenu *topMenu, const QStringList &excludedDesktopEntryNames)
666{
667 if (!KAuthorized::authorizeAction(QStringLiteral("openwith"))) {
668 return;
669 }
670
671 // TODO Overload with excludedDesktopEntryNames, but this method in public API and will be handled in a new MR
672 KService::List offers = associatedApplications(m_mimeTypeList, excludedDesktopEntryNames);
673
674 //// Ok, we have everything, now insert
675
676 const KFileItemList items = m_props.items();
677 const KFileItem &firstItem = items.first();
678 const bool isLocal = firstItem.url().isLocalFile();
679 const bool isDir = m_props.isDirectory();
680 // "Open With..." for folders is really not very useful, especially for remote folders.
681 // (media:/something, or trash:/, or ftp://...).
682 // Don't show "open with" actions for remote dirs only
683 if (isDir && !isLocal) {
684 return;
685 }
686
687 const auto makeOpenWithAction = [this, isDir] {
688 auto action = new QAction(this);
689 action->setText(isDir ? i18nc("@action:inmenu", "&Open Folder With…") : i18nc("@action:inmenu", "&Open With…"));
690 action->setIcon(QIcon::fromTheme(QStringLiteral("system-run")));
691 action->setObjectName(QStringLiteral("openwith_browse")); // For the unittest
692 return action;
693 };
694
695#ifdef WITH_QTDBUS
696 if (KSandbox::isInside() && !m_fileOpenList.isEmpty()) {
697 auto openWithAction = makeOpenWithAction();
698 QObject::connect(openWithAction, &QAction::triggered, this, [this] {
699 const auto &items = m_fileOpenList;
700 for (const auto &fileItem : items) {
701 QDBusMessage message = QDBusMessage::createMethodCall(QLatin1String("org.freedesktop.portal.Desktop"),
702 QLatin1String("/org/freedesktop/portal/desktop"),
703 QLatin1String("org.freedesktop.portal.OpenURI"),
704 QLatin1String("OpenURI"));
705 message << QString() << fileItem.url() << QVariantMap{};
707 }
708 });
709 topMenu->insertAction(before, openWithAction);
710 return;
711 }
712 if (KSandbox::isInside()) {
713 return;
714 }
715#endif
716
717 QStringList serviceIdList = listPreferredServiceIds(m_mimeTypeList, excludedDesktopEntryNames);
718
719 // When selecting files with multiple MIME types, offer either "open with <app for all>"
720 // or a generic <open> (if there are any apps associated).
721 if (m_mimeTypeList.count() > 1 && !serviceIdList.isEmpty()
722 && !(serviceIdList.count() == 1 && serviceIdList.first().isEmpty())) { // empty means "no apps associated"
723
724 QAction *runAct = new QAction(this);
725 if (serviceIdList.count() == 1) {
726 const KService::Ptr app = preferredService(m_mimeTypeList.first(), excludedDesktopEntryNames);
727 runAct->setText(isDir ? i18n("&Open folder with %1", app->name()) : i18n("&Open with %1", app->name()));
728 runAct->setIcon(QIcon::fromTheme(app->icon()));
729
730 // Remove that app from the offers list (#242731)
731 for (int i = 0; i < offers.count(); ++i) {
732 if (offers[i]->storageId() == app->storageId()) {
733 offers.removeAt(i);
734 break;
735 }
736 }
737 } else {
738 runAct->setText(i18n("&Open"));
739 }
740
741 QObject::connect(runAct, &QAction::triggered, this, &KFileItemActionsPrivate::slotRunPreferredApplications);
742 topMenu->insertAction(before, runAct);
743
744 m_fileOpenList = m_props.items();
745 }
746
747 auto openWithAct = makeOpenWithAction();
748 QObject::connect(openWithAct, &QAction::triggered, this, &KFileItemActionsPrivate::slotOpenWithDialog);
749
750 if (!offers.isEmpty()) {
751 // Show the top app inline for files, but not folders
752 if (!isDir) {
753 QAction *act = createAppAction(offers.takeFirst(), true);
754 topMenu->insertAction(before, act);
755 }
756
757 // If there are still more apps, show them in a sub-menu
758 if (!offers.isEmpty()) { // submenu 'open with'
759 QMenu *subMenu = new QMenu(isDir ? i18nc("@title:menu", "&Open Folder With") : i18nc("@title:menu", "&Open With"), topMenu);
760 subMenu->setIcon(QIcon::fromTheme(QStringLiteral("system-run")));
761 subMenu->menuAction()->setObjectName(QStringLiteral("openWith_submenu")); // For the unittest
762 // Add other apps to the sub-menu
763 for (const KServicePtr &service : std::as_const(offers)) {
764 QAction *act = createAppAction(service, false);
765 subMenu->addAction(act);
766 }
767
768 subMenu->addSeparator();
769
770 openWithAct->setText(i18nc("@action:inmenu Open With", "&Other Application…"));
771 subMenu->addAction(openWithAct);
772
773 topMenu->insertMenu(before, subMenu);
774 } else { // No other apps
775 topMenu->insertAction(before, openWithAct);
776 }
777 } else { // no app offers -> Open With...
778 openWithAct->setIcon(QIcon::fromTheme(QStringLiteral("system-run")));
779 openWithAct->setObjectName(QStringLiteral("openwith")); // For the unittest
780 topMenu->insertAction(before, openWithAct);
781 }
782
783 if (m_props.mimeType() == QLatin1String("application/x-desktop")) {
784 const QString path = firstItem.localPath();
785 const ServiceList services = KDesktopFile(path).actions();
786 for (const KDesktopFileAction &serviceAction : services) {
787 QAction *action = new QAction(this);
788 action->setText(serviceAction.name());
789 action->setIcon(QIcon::fromTheme(serviceAction.icon()));
790
791 connect(action, &QAction::triggered, this, [serviceAction] {
792 if (KAuthorized::authorizeAction(serviceAction.name())) {
793 auto *job = new KIO::ApplicationLauncherJob(serviceAction);
794 job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, nullptr));
795 job->start();
796 }
797 });
798
799 topMenu->addAction(action);
800 }
801 }
802
803 topMenu->insertSeparator(before);
804}
805
806QStringList KFileItemActionsPrivate::serviceMenuFilePaths()
807{
808 QStringList filePaths;
809
810 std::set<QString> uniqueFileNames;
811
812 // Load servicemenus from new install location
813 const QStringList paths =
815 const QStringList fromDisk = KFileUtils::findAllUniqueFiles(paths, QStringList(QStringLiteral("*.desktop")));
816 for (const QString &fileFromDisk : fromDisk) {
817 if (auto [_, inserted] = uniqueFileNames.insert(fileFromDisk.split(QLatin1Char('/')).last()); inserted) {
818 filePaths << fileFromDisk;
819 }
820 }
821 return filePaths;
822}
823
825{
826 d->m_parentWidget = widget;
827}
828
829#include "moc_kfileitemactions.cpp"
830#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:630
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
QString icon() const
QExplicitlySharedDataPointer< KService > Ptr
QList< Ptr > List
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={})
QString name(GameStandardAction id)
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()
void removeAt(qsizetype i)
void reserve(qsizetype size)
qsizetype size() const const
value_type takeFirst()
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 Fri Jul 26 2024 11:54:08 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.