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 | |
44 | static 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 | |
52 | static 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. |
80 | namespace KIO |
81 | { |
82 | class |
83 | { |
84 | public: |
85 | ServiceList &selectList(const QString &priority, const QString &); |
86 | |
87 | ServiceList ; |
88 | ServiceList ; |
89 | ServiceList ; |
90 | |
91 | QMap<QString, ServiceList> ; |
92 | QMap<QString, ServiceList> ; |
93 | QMap<QString, ServiceList> ; |
94 | }; |
95 | |
96 | ServiceList &PopupServices::(const QString &priority, const QString &) |
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 | |
119 | KFileItemActionsPrivate::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 | |
131 | KFileItemActionsPrivate::~KFileItemActionsPrivate() |
132 | { |
133 | } |
134 | |
135 | int KFileItemActionsPrivate::(const QMap<QString, ServiceList> &, QMenu *) |
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 * = 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 | |
163 | int KFileItemActionsPrivate::(const ServiceList &list, QMenu *) |
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 | |
197 | void 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 | |
208 | KFileItemActions::KFileItemActions(QObject *parent) |
209 | : QObject(parent) |
210 | , d(new KFileItemActionsPrivate(this)) |
211 | { |
212 | } |
213 | |
214 | KFileItemActions::~KFileItemActions() = default; |
215 | |
216 | void 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 | |
229 | void KFileItemActions::(QMenu *, MenuActionSources sources, const QList<QAction *> &additionalActions, const QStringList &excludeList) |
230 | { |
231 | QMenu * = 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 |
246 | KService::List KFileItemActions::associatedApplications(const QStringList &mimeTypeList) |
247 | { |
248 | return KFileItemActionsPrivate::associatedApplications(mimeTypeList, excludedDesktopEntryNames: QStringList{}); |
249 | } |
250 | |
251 | static 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 | |
259 | void KFileItemActions::(QAction *before, QMenu *, const QStringList &excludedDesktopEntryNames) |
260 | { |
261 | d->insertOpenWithActionsTo(before, topMenu, excludedDesktopEntryNames); |
262 | } |
263 | |
264 | void 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 | |
293 | void KFileItemActions::runPreferredApplications(const KFileItemList &fileOpenList) |
294 | { |
295 | d->m_fileOpenList = fileOpenList; |
296 | d->slotRunPreferredApplications(); |
297 | } |
298 | |
299 | void 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 | |
317 | void 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 | |
328 | void 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 | |
338 | QStringList 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 | |
349 | QStringList 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 | |
361 | QAction *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 | |
379 | bool KFileItemActionsPrivate::(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 | |
425 | bool 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 | |
439 | KFileItemActionsPrivate::ServiceActionInfo |
440 | KFileItemActionsPrivate::(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 = 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 = 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 * = 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 | |
520 | int KFileItemActionsPrivate::(QMenu *mainMenu, QMenu *, 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 = 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 | |
568 | KService::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 | |
636 | void KFileItemActionsPrivate::(QAction *before, QMenu *, 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 * = 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 | |
777 | QStringList KFileItemActionsPrivate::() |
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 | |
795 | void KFileItemActions::setParentWidget(QWidget *widget) |
796 | { |
797 | d->m_parentWidget = widget; |
798 | } |
799 | |
800 | #include "moc_kfileitemactions.cpp" |
801 | #include "moc_kfileitemactions_p.cpp" |
802 | |