1 /* This file is part of the KDE project
2 Copyright (C) 1998-2007 David Faure <faure@kde.org>
4 This library is free software; you can redistribute it and/or
5 modify it under the terms of the GNU Library General Public
6 License as published by the Free Software Foundation; either
7 version 2 of the License, or (at your option) any later version.
9 This library is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 Library General Public License for more details.
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to
16 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 Boston, MA 02110-1301, USA.
20 #include "konq_menuactions.h"
21 #include "konq_menuactions_p.h"
24 #include <kmimetypetrader.h>
26 #include <kdesktopfileactions.h>
29 #include <kauthorized.h>
30 #include <kconfiggroup.h>
31 #include <kdesktopfile.h>
34 #include <kstandarddirs.h>
36 #include <KServiceTypeTrader>
39 #include <QtDBus/QtDBus>
41 static bool KIOSKAuthorizedAction(const KConfigGroup
& cfg
)
43 if ( !cfg
.hasKey( "X-KDE-AuthorizeAction") ) {
46 const QStringList list
= cfg
.readEntry("X-KDE-AuthorizeAction", QStringList() );
47 for(QStringList::ConstIterator it
= list
.constBegin();
48 it
!= list
.constEnd(); ++it
) {
49 if (!KAuthorized::authorize((*it
).trimmed())) {
56 // This helper class stores the .desktop-file actions and the servicemenus
57 // in order to support X-KDE-Priority and X-KDE-Submenu.
61 ServiceList
& selectList( const QString
& priority
, const QString
& submenuName
);
64 ServiceList user
, userToplevel
, userPriority
;
65 QMap
<QString
, ServiceList
> userSubmenus
, userToplevelSubmenus
, userPrioritySubmenus
;
68 ServiceList
& PopupServices::selectList( const QString
& priority
, const QString
& submenuName
)
70 // we use the categories .desktop entry to define submenus
71 // if none is defined, we just pop it in the main menu
72 if (submenuName
.isEmpty()) {
73 if (priority
== "TopLevel") {
75 } else if (priority
== "Important") {
78 } else if (priority
== "TopLevel") {
79 return userToplevelSubmenus
[submenuName
];
80 } else if (priority
== "Important") {
81 return userPrioritySubmenus
[submenuName
];
83 return userSubmenus
[submenuName
];
90 KonqMenuActionsPrivate::KonqMenuActionsPrivate()
92 m_executeServiceActionGroup(static_cast<QWidget
*>(0)),
93 m_runApplicationActionGroup(static_cast<QWidget
*>(0)),
94 m_ownActions(static_cast<QWidget
*>(0))
96 QObject::connect(&m_executeServiceActionGroup
, SIGNAL(triggered(QAction
*)),
97 this, SLOT(slotExecuteService(QAction
*)));
98 QObject::connect(&m_runApplicationActionGroup
, SIGNAL(triggered(QAction
*)),
99 this, SLOT(slotRunApplication(QAction
*)));
102 int KonqMenuActionsPrivate::insertServicesSubmenus(const QMap
<QString
, ServiceList
>& submenus
,
107 QMap
<QString
, ServiceList
>::ConstIterator it
;
108 for (it
= submenus
.begin(); it
!= submenus
.end(); ++it
) {
109 if (it
.value().isEmpty()) {
110 //avoid empty sub-menus
114 QMenu
* actionSubmenu
= new KMenu(menu
);
115 actionSubmenu
->setTitle( it
.key() );
116 actionSubmenu
->menuAction()->setObjectName("services_submenu"); // for the unittest
117 menu
->addMenu(actionSubmenu
);
118 count
+= insertServices(it
.value(), actionSubmenu
, isBuiltin
);
124 int KonqMenuActionsPrivate::insertServices(const ServiceList
& list
,
129 ServiceList::const_iterator it
= list
.begin();
130 for( ; it
!= list
.end(); ++it
) {
131 if ((*it
).isSeparator()) {
132 const QList
<QAction
*> actions
= menu
->actions();
133 if (!actions
.isEmpty() && !actions
.last()->isSeparator()) {
134 menu
->addSeparator();
139 if (isBuiltin
|| !(*it
).noDisplay()) {
140 QAction
* act
= new QAction(&m_ownActions
);
141 act
->setObjectName("menuaction"); // for the unittest
142 QString text
= (*it
).text();
143 text
.replace('&',"&&");
144 act
->setText( text
);
145 if ( !(*it
).icon().isEmpty() ) {
146 act
->setIcon( KIcon((*it
).icon()) );
148 act
->setData(QVariant::fromValue(*it
));
149 m_executeServiceActionGroup
.addAction(act
);
151 menu
->addAction(act
); // Add to toplevel menu
159 void KonqMenuActionsPrivate::slotExecuteService(QAction
* act
)
161 KServiceAction serviceAction
= act
->data().value
<KServiceAction
>();
162 KDesktopFileActions::executeService(m_info
.urlList(), serviceAction
);
167 KonqMenuActions::KonqMenuActions()
168 : d(new KonqMenuActionsPrivate
)
173 KonqMenuActions::~KonqMenuActions()
178 void KonqMenuActions::setPopupMenuInfo(const KonqPopupMenuInformation
& info
)
183 int KonqMenuActions::addActionsTo(QMenu
* mainMenu
)
185 const KFileItemList items
= d
->m_info
.items();
186 const KFileItem firstItem
= items
.first();
187 const QString protocol
= firstItem
.url().protocol(); // assumed to be the same for all items
188 const bool isLocal
= firstItem
.url().isLocalFile();
189 const bool isSingleLocal
= items
.count() == 1 && isLocal
;
190 const KUrl::List urlList
= d
->m_info
.urlList();
194 // 1 - Look for builtin and user-defined services
195 if (isSingleLocal
&& d
->m_info
.mimeType() == "application/x-desktop") // .desktop file
197 // get builtin services, like mount/unmount
198 s
.builtin
= KDesktopFileActions::builtinServices(firstItem
.url());
199 const QString path
= firstItem
.url().path();
200 KDesktopFile
desktopFile(path
);
201 KConfigGroup cfg
= desktopFile
.desktopGroup();
202 const QString priority
= cfg
.readEntry("X-KDE-Priority");
203 const QString submenuName
= cfg
.readEntry( "X-KDE-Submenu" );
205 if ( cfg
.readEntry("Type") == "Link" ) {
206 d
->m_url
= cfg
.readEntry("URL");
207 // TODO: Do we want to make all the actions apply on the target
208 // of the .desktop file instead of the .desktop file itself?
211 ServiceList
& list
= s
.selectList(priority
, submenuName
);
212 list
= KDesktopFileActions::userDefinedServices(path
, desktopFile
, true /*isLocal*/);
215 // 2 - Look for "servicesmenus" bindings (konqueror-specific user-defined services)
217 // first check the .directory if this is a directory
218 if (d
->m_info
.isDirectory() && isSingleLocal
) {
219 QString dotDirectoryFile
= firstItem
.url().path(KUrl::AddTrailingSlash
).append(".directory");
220 if (QFile::exists(dotDirectoryFile
)) {
221 const KDesktopFile
desktopFile( dotDirectoryFile
);
222 const KConfigGroup cfg
= desktopFile
.desktopGroup();
224 if (KIOSKAuthorizedAction(cfg
)) {
225 const QString priority
= cfg
.readEntry("X-KDE-Priority");
226 const QString submenuName
= cfg
.readEntry( "X-KDE-Submenu" );
227 ServiceList
& list
= s
.selectList( priority
, submenuName
);
228 list
+= KDesktopFileActions::userDefinedServices( dotDirectoryFile
, desktopFile
, true );
233 const QString commonMimeType
= d
->m_info
.mimeType();
234 const QString commonMimeGroup
= d
->m_info
.mimeGroup();
235 const KMimeType::Ptr mimeTypePtr
= commonMimeType
.isEmpty() ? KMimeType::Ptr() : KMimeType::mimeType(commonMimeType
);
236 const KService::List entries
= KServiceTypeTrader::self()->query( "KonqPopupMenu/Plugin");
237 KService::List::const_iterator eEnd
= entries
.end();
238 for (KService::List::const_iterator it2
= entries
.begin(); it2
!= eEnd
; ++it2
) {
239 QString file
= KStandardDirs::locate("services", (*it2
)->entryPath());
240 KDesktopFile
desktopFile( file
);
241 const KConfigGroup cfg
= desktopFile
.desktopGroup();
243 if (!KIOSKAuthorizedAction(cfg
)) {
247 if ( cfg
.hasKey( "X-KDE-ShowIfRunning" ) ) {
248 const QString app
= cfg
.readEntry( "X-KDE-ShowIfRunning" );
249 if ( QDBusConnection::sessionBus().interface()->isServiceRegistered( app
) )
252 if ( cfg
.hasKey( "X-KDE-ShowIfDBusCall" ) ) {
253 QString calldata
= cfg
.readEntry( "X-KDE-ShowIfDBusCall" );
254 QStringList parts
= calldata
.split(' ');
255 const QString
&app
= parts
.at(0);
256 const QString
&obj
= parts
.at(1);
257 QString interface
= parts
.at(2);
259 int pos
= interface
.lastIndexOf( QLatin1Char( '.' ) );
261 method
= interface
.mid(pos
+ 1);
262 interface
.truncate(pos
);
265 //if ( !QDBus::sessionBus().busService()->nameHasOwner( app ) )
266 // continue; //app does not exist so cannot send call
268 QDBusMessage reply
= QDBusInterface( app
, obj
, interface
).
269 call( method
, urlList
.toStringList() );
270 if ( reply
.arguments().count() < 1 || reply
.arguments().at(0).type() != QVariant::Bool
|| !reply
.arguments().at(0).toBool() )
274 if ( cfg
.hasKey( "X-KDE-Protocol" ) ) {
275 const QString protocol
= cfg
.readEntry( "X-KDE-Protocol" );
276 if (protocol
.startsWith('!')) {
277 const QString excludedProtocol
= protocol
.mid(1);
278 if (excludedProtocol
== protocol
)
280 } else if (protocol
!= protocol
)
283 else if ( cfg
.hasKey( "X-KDE-Protocols" ) ) {
284 const QStringList protocols
= cfg
.readEntry("X-KDE-Protocols", QStringList());
285 if (!protocols
.contains(protocol
))
288 else if (protocol
== "trash") {
289 // Require servicemenus for the trash to ask for protocol=trash explicitly.
290 // Trashed files aren't supposed to be available for actions.
291 // One might want a servicemenu for trash.desktop itself though.
295 if ( cfg
.hasKey( "X-KDE-Require" ) ) {
296 const QStringList capabilities
= cfg
.readEntry( "X-KDE-Require" , QStringList() );
297 if (capabilities
.contains("Write") && !d
->m_info
.capabilities().supportsWriting())
300 if ( cfg
.hasKey( "Actions" ) || cfg
.hasKey( "X-KDE-GetActionMenu") ) {
301 // Like KService, we support ServiceTypes, X-KDE-ServiceTypes, and MimeType.
302 QStringList types
= cfg
.readEntry("ServiceTypes", QStringList());
303 types
+= cfg
.readEntry("X-KDE-ServiceTypes", QStringList());
304 types
+= cfg
.readXdgListEntry("MimeType");
305 //kDebug() << file << types;
309 const QStringList excludeTypes
= cfg
.readEntry( "ExcludeServiceTypes" , QStringList() );
312 // check for exact matches or a typeglob'd mimetype if we have a mimetype
313 for (QStringList::ConstIterator it
= types
.constBegin();
314 it
!= types
.constEnd() && !ok
;
317 // first check if we have an all mimetype
318 bool checkTheMimetypes
= false;
319 if (*it
== "all/all" ||
320 *it
== "allfiles" /*compat with KDE up to 3.0.3*/) {
321 checkTheMimetypes
= true;
324 // next, do we match all files?
326 !d
->m_info
.isDirectory() &&
327 *it
== "all/allfiles") {
328 checkTheMimetypes
= true;
331 // if we have a mimetype, see if we have an exact or a type globbed match
333 (mimeTypePtr
&& mimeTypePtr
->is(*it
)) ||
334 (!commonMimeGroup
.isEmpty() &&
335 ((*it
).right(1) == "*" &&
336 (*it
).left((*it
).indexOf('/')) == commonMimeGroup
)))) {
337 checkTheMimetypes
= true;
340 if (checkTheMimetypes
) {
342 for (QStringList::ConstIterator itex
= excludeTypes
.constBegin(); itex
!= excludeTypes
.constEnd(); ++itex
)
344 if( ((*itex
).endsWith('*') && (*itex
).left((*itex
).indexOf('/')) == commonMimeGroup
) ||
345 ((*itex
) == commonMimeType
) ) {
354 const QString priority
= cfg
.readEntry("X-KDE-Priority");
355 const QString submenuName
= cfg
.readEntry( "X-KDE-Submenu" );
357 ServiceList
& list
= s
.selectList( priority
, submenuName
);
358 list
+= KDesktopFileActions::userDefinedServices(*(*it2
), isLocal
, urlList
);
365 QMenu
* actionMenu
= mainMenu
;
366 int userItemCount
= 0;
367 if (s
.user
.count() + s
.userSubmenus
.count() +
368 s
.userPriority
.count() + s
.userPrioritySubmenus
.count() > 1)
370 // we have more than one item, so let's make a submenu
371 actionMenu
= new KMenu(i18nc("@title:menu", "&Actions"), mainMenu
);
372 actionMenu
->menuAction()->setObjectName("actions_submenu"); // for the unittest
373 mainMenu
->addMenu(actionMenu
);
376 userItemCount
+= d
->insertServicesSubmenus(s
.userPrioritySubmenus
, actionMenu
, false);
377 userItemCount
+= d
->insertServices(s
.userPriority
, actionMenu
, false);
379 // see if we need to put a separator between our priority items and our regular items
380 if (userItemCount
> 0 &&
381 (s
.user
.count() > 0 ||
382 s
.userSubmenus
.count() > 0 ||
383 s
.builtin
.count() > 0) &&
384 !actionMenu
->actions().last()->isSeparator()) {
385 actionMenu
->addSeparator();
387 userItemCount
+= d
->insertServicesSubmenus(s
.userSubmenus
, actionMenu
, false);
388 userItemCount
+= d
->insertServices(s
.user
, actionMenu
, false);
389 userItemCount
+= d
->insertServices(s
.builtin
, mainMenu
, true);
390 userItemCount
+= d
->insertServicesSubmenus(s
.userToplevelSubmenus
, mainMenu
, false);
391 userItemCount
+= d
->insertServices(s
.userToplevel
, mainMenu
, false);
392 return userItemCount
;
395 void KonqMenuActions::addOpenWithActionsTo(QMenu
* topMenu
, const QString
& traderConstraint
)
397 if (!KAuthorized::authorizeKAction("openwith"))
400 const KFileItemList items
= d
->m_info
.items();
401 QStringList mimeTypeList
;
402 KFileItemList::const_iterator kit
= items
.constBegin();
403 const KFileItemList::const_iterator kend
= items
.constEnd();
404 for ( ; kit
!= kend
; ++kit
) {
405 if (!mimeTypeList
.contains((*kit
).mimetype()))
406 mimeTypeList
<< (*kit
).mimetype();
409 QString constraint
= traderConstraint
;
410 const QString subConstraint
= " and '%1' in ServiceTypes";
412 QStringList::ConstIterator it
= mimeTypeList
.constBegin();
413 const QStringList::ConstIterator end
= mimeTypeList
.constEnd();
414 Q_ASSERT( it
!= end
);
415 QString firstMimeType
= *it
;
417 for (; it
!= end
; ++it
) {
418 constraint
+= subConstraint
.arg(*it
);
421 KService::List offers
= KMimeTypeTrader::self()->query(firstMimeType
, "Application", constraint
);
423 QSet
<QString
> seenTexts
;
424 for (KService::List::iterator it
= offers
.begin(); it
!= offers
.end(); ) {
425 // The offer list from the KTrader returns duplicate
426 // application entries (kde3 and kde4). Although this is a configuration
427 // problem, duplicated entries just will be skipped here.
428 const QString
appName((*it
)->name());
429 if (!seenTexts
.contains(appName
)) {
430 seenTexts
.insert(appName
);
433 it
= offers
.erase(it
);
438 //// Ok, we have everything, now insert
440 const KFileItem firstItem
= items
.first();
441 const bool isLocal
= firstItem
.url().isLocalFile();
442 // "Open With..." for folders is really not very useful, especially for remote folders.
443 // (media:/something, or trash:/, or ftp://...)
444 if ( !d
->m_info
.isDirectory() || isLocal
) {
445 if ( !topMenu
->actions().isEmpty() )
446 topMenu
->addSeparator();
448 if ( !offers
.isEmpty() ) {
449 QMenu
* menu
= topMenu
;
451 if ( offers
.count() > 1 ) { // submenu 'open with'
452 // TODO i18nc("@title:menu", "Open With")
453 menu
= new QMenu(i18n("&Open With"), topMenu
);
454 menu
->menuAction()->setObjectName("openWith_submenu"); // for the unittest
455 topMenu
->addMenu(menu
);
457 //kDebug() << offers.count() << "offers" << topMenu << menu;
459 KService::List::ConstIterator it
= offers
.constBegin();
460 for( ; it
!= offers
.constEnd(); it
++ ) {
461 KService::Ptr service
= (*it
);
463 // Skip OnlyShowIn=Foo and NotShowIn=KDE entries,
464 // but still offer NoDisplay=true entries, that's the
465 // whole point of such desktop files. This is why we don't
466 // use service->noDisplay() here.
467 const QString onlyShowIn
= service
->property("OnlyShowIn", QVariant::String
).toString();
468 if ( !onlyShowIn
.isEmpty() ) {
469 const QStringList aList
= onlyShowIn
.split(';', QString::SkipEmptyParts
);
470 if (!aList
.contains("KDE"))
473 const QString notShowIn
= service
->property("NotShowIn", QVariant::String
).toString();
474 if ( !notShowIn
.isEmpty() ) {
475 const QStringList aList
= notShowIn
.split(';', QString::SkipEmptyParts
);
476 if (aList
.contains("KDE"))
480 QString
actionName(service
->name().replace('&', "&&"));
481 if (menu
== topMenu
) // no submenu -> prefix single offer
482 actionName
= i18n("Open &with %1", actionName
);
484 KAction
* act
= d
->m_ownActions
.addAction("openwith");
485 act
->setIcon(KIcon(service
->icon()));
486 act
->setText(actionName
);
487 act
->setData(QVariant::fromValue(service
));
488 d
->m_runApplicationActionGroup
.addAction(act
);
489 menu
->addAction(act
);
492 QString openWithActionName
;
493 if ( menu
!= topMenu
) { // submenu
494 menu
->addSeparator();
495 // TODO i18nc("@action:inmenu Open With", "&Other...")
496 openWithActionName
= i18n("&Other...");
498 // TODO i18nc("@title:menu", "Open With...")
499 openWithActionName
= i18n("&Open With...");
501 QAction
*openWithAct
= d
->m_ownActions
.addAction( "openwith_browse" );
502 openWithAct
->setText( openWithActionName
);
503 QObject::connect(openWithAct
, SIGNAL(triggered()), d
, SLOT(slotOpenWithDialog()));
504 menu
->addAction(openWithAct
);
506 else // no app offers -> Open With...
508 KAction
* act
= d
->m_ownActions
.addAction( "openwith_browse" );
509 // TODO i18nc("@title:menu", "Open With...")
510 act
->setText( i18n( "&Open With..." ) );
511 QObject::connect(act
, SIGNAL(triggered()), d
, SLOT(slotOpenWithDialog()));
512 topMenu
->addAction(act
);
518 void KonqMenuActionsPrivate::slotRunApplication(QAction
* act
)
520 // Is it an application, from one of the "Open With" actions
521 KService::Ptr app
= act
->data().value
<KService::Ptr
>();
524 KRun::run(*app
, m_info
.urlList(), m_info
.parentWidget());
528 void KonqMenuActionsPrivate::slotOpenWithDialog()
530 // The item 'Other...' or 'Open With...' has been selected
531 KRun::displayOpenWithDialog(m_info
.urlList(), m_info
.parentWidget());