1 /* This file is part of the dbusmenu-qt library
2 Copyright 2009 Canonical
3 Author: Aurelien Gateau <aurelien.gateau@canonical.com>
5 This library is free software; you can redistribute it and/or
6 modify it under the terms of the GNU Library General Public
7 License (LGPL) as published by the Free Software Foundation;
8 either version 2 of the License, or (at your option) any later
11 This library is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 Library General Public License for more details.
16 You should have received a copy of the GNU Library General Public License
17 along with this library; see the file COPYING.LIB. If not, write to
18 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19 Boston, MA 02110-1301, USA.
21 #include "dbusmenuimporter.h"
24 #include <QCoreApplication>
25 #include <QDBusConnection>
26 #include <QDBusInterface>
28 #include <QDBusVariant>
32 #include <QSignalMapper>
35 #include <QToolButton>
36 #include <QWidgetAction>
39 #include "dbusmenutypes_p.h"
40 #include "dbusmenushortcut_p.h"
50 static const char *DBUSMENU_INTERFACE = "com.canonical.dbusmenu";
52 static const int ABOUT_TO_SHOW_TIMEOUT = 3000;
53 static const int REFRESH_TIMEOUT = 4000;
55 static const char *DBUSMENU_PROPERTY_ID = "_dbusmenu_id";
56 static const char *DBUSMENU_PROPERTY_ICON_NAME = "_dbusmenu_icon_name";
57 static const char *DBUSMENU_PROPERTY_ICON_DATA_HASH = "_dbusmenu_icon_data_hash";
59 static QAction *createKdeTitle(QAction *action, QWidget *parent)
61 QToolButton *titleWidget = new QToolButton(0);
62 QFont font = titleWidget->font();
64 titleWidget->setFont(font);
65 titleWidget->setIcon(action->icon());
66 titleWidget->setText(action->text());
67 titleWidget->setDown(true);
68 titleWidget->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
70 QWidgetAction *titleAction = new QWidgetAction(parent);
71 titleAction->setDefaultWidget(titleWidget);
75 class DBusMenuImporterPrivate
80 QDBusAbstractInterface *m_interface;
82 typedef QMap<int, QPointer<QAction> > ActionForId;
83 ActionForId m_actionForId;
84 QSignalMapper m_mapper;
85 QTimer *m_pendingLayoutUpdateTimer;
87 QSet<int> m_idsRefreshedByAboutToShow;
88 QSet<int> m_pendingLayoutUpdates;
90 bool m_mustEmitMenuUpdated;
92 DBusMenuImporterType m_type;
94 QDBusPendingCallWatcher *refresh(int id)
97 DMDEBUG << "Starting refresh chrono for id" << id;
100 QDBusPendingCall call = m_interface->asyncCall("GetLayout", id, 1, QStringList());
101 QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, q);
102 watcher->setProperty(DBUSMENU_PROPERTY_ID, id);
103 QObject::connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)),
104 q, SLOT(slotGetLayoutFinished(QDBusPendingCallWatcher*)));
109 QMenu *createMenu(QWidget *parent)
111 QMenu *menu = q->createMenu(parent);
112 QObject::connect(menu, SIGNAL(aboutToShow()),
113 q, SLOT(slotMenuAboutToShow()));
114 QObject::connect(menu, SIGNAL(aboutToHide()),
115 q, SLOT(slotMenuAboutToHide()));
120 * Init all the immutable action properties here
121 * TODO: Document immutable properties?
123 * Note: we remove properties we handle from the map (using QMap::take()
124 * instead of QMap::value()) to avoid warnings about these properties in
127 QAction *createAction(int id, const QVariantMap &_map, QWidget *parent)
129 QVariantMap map = _map;
130 QAction *action = new QAction(parent);
131 action->setProperty(DBUSMENU_PROPERTY_ID, id);
133 QString type = map.take("type").toString();
134 if (type == "separator") {
135 action->setSeparator(true);
138 if (map.take("children-display").toString() == "submenu") {
139 QMenu *menu = createMenu(parent);
140 action->setMenu(menu);
143 QString toggleType = map.take("toggle-type").toString();
144 if (!toggleType.isEmpty()) {
145 action->setCheckable(true);
146 if (toggleType == "radio") {
147 QActionGroup *group = new QActionGroup(action);
148 group->addAction(action);
152 bool isKdeTitle = map.take("x-kde-title").toBool();
153 updateAction(action, map, map.keys());
156 action = createKdeTitle(action, parent);
163 * Update mutable properties of an action. A property may be listed in
164 * requestedProperties but not in map, this means we should use the default value
167 * @param action the action to update
168 * @param map holds the property values
169 * @param requestedProperties which properties has been requested
171 void updateAction(QAction *action, const QVariantMap &map, const QStringList &requestedProperties)
173 Q_FOREACH(const QString &key, requestedProperties) {
174 updateActionProperty(action, key, map.value(key));
178 void updateActionProperty(QAction *action, const QString &key, const QVariant &value)
180 if (key == "label") {
181 updateActionLabel(action, value);
182 } else if (key == "enabled") {
183 updateActionEnabled(action, value);
184 } else if (key == "toggle-state") {
185 updateActionChecked(action, value);
186 } else if (key == "icon-name") {
187 updateActionIconByName(action, value);
188 } else if (key == "icon-data") {
189 updateActionIconByData(action, value);
190 } else if (key == "visible") {
191 updateActionVisible(action, value);
192 } else if (key == "shortcut") {
193 updateActionShortcut(action, value);
194 } else if (key == "children-display") {
196 DMWARNING << "Unhandled property update" << key;
200 void updateActionLabel(QAction *action, const QVariant &value)
202 QString text = swapMnemonicChar(value.toString(), '_', '&');
203 action->setText(text);
206 void updateActionEnabled(QAction *action, const QVariant &value)
208 action->setEnabled(value.isValid() ? value.toBool(): true);
211 void updateActionChecked(QAction *action, const QVariant &value)
213 if (action->isCheckable() && value.isValid()) {
214 action->setChecked(value.toInt() == 1);
218 void updateActionIconByName(QAction *action, const QVariant &value)
220 QString iconName = value.toString();
221 QString previous = action->property(DBUSMENU_PROPERTY_ICON_NAME).toString();
222 if (previous == iconName) {
225 action->setProperty(DBUSMENU_PROPERTY_ICON_NAME, iconName);
226 if (iconName.isEmpty()) {
227 action->setIcon(QIcon());
230 action->setIcon(q->iconForName(iconName));
233 void updateActionIconByData(QAction *action, const QVariant &value)
235 QByteArray data = value.toByteArray();
236 uint dataHash = qHash(data);
237 uint previousDataHash = action->property(DBUSMENU_PROPERTY_ICON_DATA_HASH).toUInt();
238 if (previousDataHash == dataHash) {
241 action->setProperty(DBUSMENU_PROPERTY_ICON_DATA_HASH, dataHash);
243 if (!pix.loadFromData(data)) {
244 DMWARNING << "Failed to decode icon-data property for action" << action->text();
245 action->setIcon(QIcon());
248 action->setIcon(QIcon(pix));
251 void updateActionVisible(QAction *action, const QVariant &value)
253 action->setVisible(value.isValid() ? value.toBool() : true);
256 void updateActionShortcut(QAction *action, const QVariant &value)
258 QDBusArgument arg = value.value<QDBusArgument>();
259 DBusMenuShortcut dmShortcut;
261 QKeySequence keySequence = dmShortcut.toKeySequence();
262 action->setShortcut(keySequence);
265 QMenu *menuForId(int id) const
270 QAction *action = m_actionForId.value(id);
274 return action->menu();
277 void slotItemsPropertiesUpdated(const DBusMenuItemList &updatedList, const DBusMenuItemKeysList &removedList);
279 void sendEvent(int id, const QString &eventId)
281 QVariant empty = QVariant::fromValue(QDBusVariant(QString()));
282 m_interface->asyncCall("Event", id, eventId, empty, 0u);
285 bool waitForWatcher(QDBusPendingCallWatcher * _watcher, int maxWait)
287 QPointer<QDBusPendingCallWatcher> watcher(_watcher);
289 if(m_type == ASYNCHRONOUS) {
291 timer.setSingleShot(true);
293 loop.connect(&timer, SIGNAL(timeout()), SLOT(quit()));
294 loop.connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher *)), SLOT(quit()));
295 timer.start(maxWait);
300 // Watcher died. This can happen if importer got deleted while we were
302 // https://bugs.kde.org/show_bug.cgi?id=237156
306 if(!watcher->isFinished()) {
311 watcher->waitForFinished();
314 if (watcher->isError()) {
315 DMWARNING << watcher->error().message();
323 DBusMenuImporter::DBusMenuImporter(const QString &service, const QString &path, QObject *parent)
324 : DBusMenuImporter(service, path, ASYNCHRONOUS, parent)
328 DBusMenuImporter::DBusMenuImporter(const QString &service, const QString &path, DBusMenuImporterType type, QObject *parent)
330 , d(new DBusMenuImporterPrivate)
333 d->m_interface = new QDBusInterface(service, path, DBUSMENU_INTERFACE, QDBusConnection::sessionBus(), this);
335 d->m_mustEmitMenuUpdated = false;
339 connect(&d->m_mapper, SIGNAL(mapped(int)), SLOT(sendClickedEvent(int)));
341 d->m_pendingLayoutUpdateTimer = new QTimer(this);
342 d->m_pendingLayoutUpdateTimer->setSingleShot(true);
343 connect(d->m_pendingLayoutUpdateTimer, SIGNAL(timeout()), SLOT(processPendingLayoutUpdates()));
345 // For some reason, using QObject::connect() does not work but
346 // QDBusConnect::connect() does
347 QDBusConnection::sessionBus().connect(service, path, DBUSMENU_INTERFACE, "LayoutUpdated", "ui",
348 this, SLOT(slotLayoutUpdated(uint, int)));
349 QDBusConnection::sessionBus().connect(service, path, DBUSMENU_INTERFACE, "ItemsPropertiesUpdated", "a(ia{sv})a(ias)",
350 this, SLOT(slotItemsPropertiesUpdated(DBusMenuItemList, DBusMenuItemKeysList)));
351 QDBusConnection::sessionBus().connect(service, path, DBUSMENU_INTERFACE, "ItemActivationRequested", "iu",
352 this, SLOT(slotItemActivationRequested(int, uint)));
357 DBusMenuImporter::~DBusMenuImporter()
359 // Do not use "delete d->m_menu": even if we are being deleted we should
360 // leave enough time for the menu to finish what it was doing, for example
361 // if it was being displayed.
362 d->m_menu->deleteLater();
366 void DBusMenuImporter::slotLayoutUpdated(uint revision, int parentId)
368 if (d->m_idsRefreshedByAboutToShow.remove(parentId)) {
371 d->m_pendingLayoutUpdates << parentId;
372 if (!d->m_pendingLayoutUpdateTimer->isActive()) {
373 d->m_pendingLayoutUpdateTimer->start();
377 void DBusMenuImporter::processPendingLayoutUpdates()
379 QSet<int> ids = d->m_pendingLayoutUpdates;
380 d->m_pendingLayoutUpdates.clear();
381 Q_FOREACH(int id, ids) {
386 QMenu *DBusMenuImporter::menu() const
389 d->m_menu = d->createMenu(0);
394 void DBusMenuImporterPrivate::slotItemsPropertiesUpdated(const DBusMenuItemList &updatedList, const DBusMenuItemKeysList &removedList)
396 Q_FOREACH(const DBusMenuItem &item, updatedList) {
397 QAction *action = m_actionForId.value(item.id);
399 // We don't know this action. It probably is in a menu we haven't fetched yet.
403 QVariantMap::ConstIterator
404 it = item.properties.constBegin(),
405 end = item.properties.constEnd();
406 for(; it != end; ++it) {
407 updateActionProperty(action, it.key(), it.value());
411 Q_FOREACH(const DBusMenuItemKeys &item, removedList) {
412 QAction *action = m_actionForId.value(item.id);
414 // We don't know this action. It probably is in a menu we haven't fetched yet.
418 Q_FOREACH(const QString &key, item.properties) {
419 updateActionProperty(action, key, QVariant());
424 void DBusMenuImporter::slotItemActivationRequested(int id, uint /*timestamp*/)
426 QAction *action = d->m_actionForId.value(id);
427 DMRETURN_IF_FAIL(action);
428 actionActivationRequested(action);
431 void DBusMenuImporter::slotGetLayoutFinished(QDBusPendingCallWatcher *watcher)
433 int parentId = watcher->property(DBUSMENU_PROPERTY_ID).toInt();
434 watcher->deleteLater();
436 QDBusPendingReply<uint, DBusMenuLayoutItem> reply = *watcher;
437 if (!reply.isValid()) {
438 DMWARNING << reply.error().message();
443 DMDEBUG << "- items received:" << sChrono.elapsed() << "ms";
445 DBusMenuLayoutItem rootItem = reply.argumentAt<1>();
447 QMenu *menu = d->menuForId(parentId);
449 DMWARNING << "No menu for id" << parentId;
455 Q_FOREACH(const DBusMenuLayoutItem &dbusMenuItem, rootItem.children) {
456 QAction *action = d->createAction(dbusMenuItem.id, dbusMenuItem.properties, menu);
457 DBusMenuImporterPrivate::ActionForId::Iterator it = d->m_actionForId.find(dbusMenuItem.id);
458 if (it == d->m_actionForId.end()) {
459 d->m_actionForId.insert(dbusMenuItem.id, action);
464 menu->addAction(action);
466 connect(action, SIGNAL(triggered()),
467 &d->m_mapper, SLOT(map()));
468 d->m_mapper.setMapping(action, dbusMenuItem.id);
472 d->refresh( dbusMenuItem.id )->waitForFinished();
476 DMDEBUG << "- Menu filled:" << sChrono.elapsed() << "ms";
480 void DBusMenuImporter::sendClickedEvent(int id)
482 d->sendEvent(id, QString("clicked"));
485 void DBusMenuImporter::updateMenu()
487 d->m_mustEmitMenuUpdated = true;
488 QMetaObject::invokeMethod(menu(), "aboutToShow");
491 void DBusMenuImporter::slotMenuAboutToShow()
493 QMenu *menu = qobject_cast<QMenu*>(sender());
496 QAction *action = menu->menuAction();
499 int id = action->property(DBUSMENU_PROPERTY_ID).toInt();
506 QDBusPendingCall call = d->m_interface->asyncCall("AboutToShow", id);
507 QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this);
508 watcher->setProperty(DBUSMENU_PROPERTY_ID, id);
509 connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)),
510 SLOT(slotAboutToShowDBusCallFinished(QDBusPendingCallWatcher*)));
512 QPointer<QObject> guard(this);
514 if (!d->waitForWatcher(watcher, ABOUT_TO_SHOW_TIMEOUT)) {
515 DMWARNING << "Application did not answer to AboutToShow() before timeout";
519 DMVAR(time.elapsed());
521 // "this" got deleted during the call to waitForWatcher(), get out
526 if (menu == d->m_menu && d->m_mustEmitMenuUpdated) {
527 d->m_mustEmitMenuUpdated = false;
531 d->sendEvent(id, QString("opened"));
534 void DBusMenuImporter::slotAboutToShowDBusCallFinished(QDBusPendingCallWatcher *watcher)
536 int id = watcher->property(DBUSMENU_PROPERTY_ID).toInt();
537 watcher->deleteLater();
539 QDBusPendingReply<bool> reply = *watcher;
540 if (reply.isError()) {
541 DMWARNING << "Call to AboutToShow() failed:" << reply.error().message();
544 bool needRefresh = reply.argumentAt<0>();
546 QMenu *menu = d->menuForId(id);
547 DMRETURN_IF_FAIL(menu);
549 if (needRefresh || menu->actions().isEmpty()) {
550 d->m_idsRefreshedByAboutToShow << id;
551 QDBusPendingCallWatcher *watcher2 = d->refresh(id);
552 if (!d->waitForWatcher(watcher2, REFRESH_TIMEOUT)) {
553 DMWARNING << "Application did not refresh before timeout";
558 void DBusMenuImporter::slotMenuAboutToHide()
560 QMenu *menu = qobject_cast<QMenu*>(sender());
563 QAction *action = menu->menuAction();
566 int id = action->property(DBUSMENU_PROPERTY_ID).toInt();
567 d->sendEvent(id, QString("closed"));
570 QMenu *DBusMenuImporter::createMenu(QWidget *parent)
572 return new QMenu(parent);
575 QIcon DBusMenuImporter::iconForName(const QString &/*name*/)
580 #include "moc_dbusmenuimporter.cpp"