Fix crash if key bindings specified in profile cannot be found. Improve
[personal-kdebase.git] / apps / lib / konq / konq_menuactions.cpp
blobeb92f342be04efa1b1a56fd849065cf916ee0921
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"
22 #include <kaction.h>
23 #include <krun.h>
24 #include <kmimetypetrader.h>
25 #include <kdebug.h>
26 #include <kdesktopfileactions.h>
27 #include <kmenu.h>
28 #include <klocale.h>
29 #include <kauthorized.h>
30 #include <kconfiggroup.h>
31 #include <kdesktopfile.h>
32 #include <kglobal.h>
33 #include <kicon.h>
34 #include <kstandarddirs.h>
35 #include <KService>
36 #include <KServiceTypeTrader>
37 #include <QFile>
39 #include <QtDBus/QtDBus>
41 static bool KIOSKAuthorizedAction(const KConfigGroup& cfg)
43 if ( !cfg.hasKey( "X-KDE-AuthorizeAction") ) {
44 return true;
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())) {
50 return false;
53 return true;
56 // This helper class stores the .desktop-file actions and the servicemenus
57 // in order to support X-KDE-Priority and X-KDE-Submenu.
58 class PopupServices
60 public:
61 ServiceList& selectList( const QString& priority, const QString& submenuName );
63 ServiceList builtin;
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") {
74 return userToplevel;
75 } else if (priority == "Important") {
76 return userPriority;
78 } else if (priority == "TopLevel") {
79 return userToplevelSubmenus[submenuName];
80 } else if (priority == "Important") {
81 return userPrioritySubmenus[submenuName];
82 } else {
83 return userSubmenus[submenuName];
85 return user;
88 ////
90 KonqMenuActionsPrivate::KonqMenuActionsPrivate()
91 : QObject(),
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,
103 QMenu* menu,
104 bool isBuiltin)
106 int count = 0;
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
111 continue;
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);
121 return count;
124 int KonqMenuActionsPrivate::insertServices(const ServiceList& list,
125 QMenu* menu,
126 bool isBuiltin)
128 int count = 0;
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();
136 continue;
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
152 ++count;
156 return count;
159 void KonqMenuActionsPrivate::slotExecuteService(QAction* act)
161 KServiceAction serviceAction = act->data().value<KServiceAction>();
162 KDesktopFileActions::executeService(m_info.urlList(), serviceAction);
165 ////
167 KonqMenuActions::KonqMenuActions()
168 : d(new KonqMenuActionsPrivate)
173 KonqMenuActions::~KonqMenuActions()
175 delete d;
178 void KonqMenuActions::setPopupMenuInfo(const KonqPopupMenuInformation& info)
180 d->m_info = 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();
192 PopupServices s;
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" );
204 #if 0
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?
210 #endif
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)) {
244 continue;
247 if ( cfg.hasKey( "X-KDE-ShowIfRunning" ) ) {
248 const QString app = cfg.readEntry( "X-KDE-ShowIfRunning" );
249 if ( QDBusConnection::sessionBus().interface()->isServiceRegistered( app ) )
250 continue;
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);
258 QString method;
259 int pos = interface.lastIndexOf( QLatin1Char( '.' ) );
260 if ( pos != -1 ) {
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() )
271 continue;
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)
279 continue;
280 } else if (protocol != protocol)
281 continue;
283 else if ( cfg.hasKey( "X-KDE-Protocols" ) ) {
284 const QStringList protocols = cfg.readEntry("X-KDE-Protocols", QStringList());
285 if (!protocols.contains(protocol))
286 continue;
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.
292 continue;
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())
298 continue;
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;
307 if (types.isEmpty())
308 continue;
309 const QStringList excludeTypes = cfg.readEntry( "ExcludeServiceTypes" , QStringList() );
310 bool ok = false;
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;
315 ++it)
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?
325 if (!ok &&
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
332 if (!ok && (
333 (mimeTypePtr && mimeTypePtr->is(*it)) ||
334 (!commonMimeGroup.isEmpty() &&
335 ((*it).right(1) == "*" &&
336 (*it).left((*it).indexOf('/')) == commonMimeGroup)))) {
337 checkTheMimetypes = true;
340 if (checkTheMimetypes) {
341 ok = true;
342 for (QStringList::ConstIterator itex = excludeTypes.constBegin(); itex != excludeTypes.constEnd(); ++itex)
344 if( ((*itex).endsWith('*') && (*itex).left((*itex).indexOf('/')) == commonMimeGroup) ||
345 ((*itex) == commonMimeType) ) {
346 ok = false;
347 break;
353 if ( ok ) {
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"))
398 return;
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;
416 ++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);
431 ++it;
432 } else {
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"))
471 continue;
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"))
477 continue;
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...");
497 } else {
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>();
522 Q_ASSERT(app);
523 if (app) {
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());