OSDN Git Service

remove the deprecated DBusMenuImporter::menuReadyToBeShown() signal
[kde/libdbusmenu-qt.git] / src / dbusmenuimporter.cpp
1 /* This file is part of the dbusmenu-qt library
2    Copyright 2009 Canonical
3    Author: Aurelien Gateau <aurelien.gateau@canonical.com>
4
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
9    version.
10
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.
15
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.
20 */
21 #include "dbusmenuimporter.h"
22
23 // Qt
24 #include <QCoreApplication>
25 #include <QDBusConnection>
26 #include <QDBusInterface>
27 #include <QDBusReply>
28 #include <QDBusVariant>
29 #include <QFont>
30 #include <QMenu>
31 #include <QPointer>
32 #include <QSignalMapper>
33 #include <QTime>
34 #include <QTimer>
35 #include <QToolButton>
36 #include <QWidgetAction>
37
38 // Local
39 #include "dbusmenutypes_p.h"
40 #include "dbusmenushortcut_p.h"
41 #include "debug_p.h"
42 #include "utils_p.h"
43
44 //#define BENCHMARK
45 #ifdef BENCHMARK
46 #include <QTime>
47 static QTime sChrono;
48 #endif
49
50 static const char *DBUSMENU_INTERFACE = "com.canonical.dbusmenu";
51
52 static const int ABOUT_TO_SHOW_TIMEOUT = 3000;
53 static const int REFRESH_TIMEOUT = 4000;
54
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";
58
59 static QAction *createKdeTitle(QAction *action, QWidget *parent)
60 {
61     QToolButton *titleWidget = new QToolButton(0);
62     QFont font = titleWidget->font();
63     font.setBold(true);
64     titleWidget->setFont(font);
65     titleWidget->setIcon(action->icon());
66     titleWidget->setText(action->text());
67     titleWidget->setDown(true);
68     titleWidget->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
69
70     QWidgetAction *titleAction = new QWidgetAction(parent);
71     titleAction->setDefaultWidget(titleWidget);
72     return titleAction;
73 }
74
75 class DBusMenuImporterPrivate
76 {
77 public:
78     DBusMenuImporter *q;
79
80     QDBusAbstractInterface *m_interface;
81     QMenu *m_menu;
82     typedef QMap<int, QPointer<QAction> > ActionForId;
83     ActionForId m_actionForId;
84     QSignalMapper m_mapper;
85     QTimer *m_pendingLayoutUpdateTimer;
86
87     QSet<int> m_idsRefreshedByAboutToShow;
88     QSet<int> m_pendingLayoutUpdates;
89
90     bool m_mustEmitMenuUpdated;
91
92     DBusMenuImporterType m_type;
93
94     QDBusPendingCallWatcher *refresh(int id)
95     {
96         #ifdef BENCHMARK
97         DMDEBUG << "Starting refresh chrono for id" << id;
98         sChrono.start();
99         #endif
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*)));
105
106         return watcher;
107     }
108
109     QMenu *createMenu(QWidget *parent)
110     {
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()));
116         return menu;
117     }
118
119     /**
120      * Init all the immutable action properties here
121      * TODO: Document immutable properties?
122      *
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
125      * updateAction()
126      */
127     QAction *createAction(int id, const QVariantMap &_map, QWidget *parent)
128     {
129         QVariantMap map = _map;
130         QAction *action = new QAction(parent);
131         action->setProperty(DBUSMENU_PROPERTY_ID, id);
132
133         QString type = map.take("type").toString();
134         if (type == "separator") {
135             action->setSeparator(true);
136         }
137
138         if (map.take("children-display").toString() == "submenu") {
139             QMenu *menu = createMenu(parent);
140             action->setMenu(menu);
141         }
142
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);
149             }
150         }
151
152         bool isKdeTitle = map.take("x-kde-title").toBool();
153         updateAction(action, map, map.keys());
154
155         if (isKdeTitle) {
156             action = createKdeTitle(action, parent);
157         }
158
159         return action;
160     }
161
162     /**
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
165      * for this property.
166      *
167      * @param action the action to update
168      * @param map holds the property values
169      * @param requestedProperties which properties has been requested
170      */
171     void updateAction(QAction *action, const QVariantMap &map, const QStringList &requestedProperties)
172     {
173         Q_FOREACH(const QString &key, requestedProperties) {
174             updateActionProperty(action, key, map.value(key));
175         }
176     }
177
178     void updateActionProperty(QAction *action, const QString &key, const QVariant &value)
179     {
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") {
195         } else {
196             DMWARNING << "Unhandled property update" << key;
197         }
198     }
199
200     void updateActionLabel(QAction *action, const QVariant &value)
201     {
202         QString text = swapMnemonicChar(value.toString(), '_', '&');
203         action->setText(text);
204     }
205
206     void updateActionEnabled(QAction *action, const QVariant &value)
207     {
208         action->setEnabled(value.isValid() ? value.toBool(): true);
209     }
210
211     void updateActionChecked(QAction *action, const QVariant &value)
212     {
213         if (action->isCheckable() && value.isValid()) {
214             action->setChecked(value.toInt() == 1);
215         }
216     }
217
218     void updateActionIconByName(QAction *action, const QVariant &value)
219     {
220         QString iconName = value.toString();
221         QString previous = action->property(DBUSMENU_PROPERTY_ICON_NAME).toString();
222         if (previous == iconName) {
223             return;
224         }
225         action->setProperty(DBUSMENU_PROPERTY_ICON_NAME, iconName);
226         if (iconName.isEmpty()) {
227             action->setIcon(QIcon());
228             return;
229         }
230         action->setIcon(q->iconForName(iconName));
231     }
232
233     void updateActionIconByData(QAction *action, const QVariant &value)
234     {
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) {
239             return;
240         }
241         action->setProperty(DBUSMENU_PROPERTY_ICON_DATA_HASH, dataHash);
242         QPixmap pix;
243         if (!pix.loadFromData(data)) {
244             DMWARNING << "Failed to decode icon-data property for action" << action->text();
245             action->setIcon(QIcon());
246             return;
247         }
248         action->setIcon(QIcon(pix));
249     }
250
251     void updateActionVisible(QAction *action, const QVariant &value)
252     {
253         action->setVisible(value.isValid() ? value.toBool() : true);
254     }
255
256     void updateActionShortcut(QAction *action, const QVariant &value)
257     {
258         QDBusArgument arg = value.value<QDBusArgument>();
259         DBusMenuShortcut dmShortcut;
260         arg >> dmShortcut;
261         QKeySequence keySequence = dmShortcut.toKeySequence();
262         action->setShortcut(keySequence);
263     }
264
265     QMenu *menuForId(int id) const
266     {
267         if (id == 0) {
268             return q->menu();
269         }
270         QAction *action = m_actionForId.value(id);
271         if (!action) {
272             return 0;
273         }
274         return action->menu();
275     }
276
277     void slotItemsPropertiesUpdated(const DBusMenuItemList &updatedList, const DBusMenuItemKeysList &removedList);
278
279     void sendEvent(int id, const QString &eventId)
280     {
281         QVariant empty = QVariant::fromValue(QDBusVariant(QString()));
282         m_interface->asyncCall("Event", id, eventId, empty, 0u);
283     }
284
285     bool waitForWatcher(QDBusPendingCallWatcher * _watcher, int maxWait)
286     {
287         QPointer<QDBusPendingCallWatcher> watcher(_watcher);
288
289         if(m_type == ASYNCHRONOUS) {
290             QTimer timer;
291             timer.setSingleShot(true);
292             QEventLoop loop;
293             loop.connect(&timer, SIGNAL(timeout()), SLOT(quit()));
294             loop.connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher *)), SLOT(quit()));
295             timer.start(maxWait);
296             loop.exec();
297             timer.stop();
298
299             if (!watcher) {
300                 // Watcher died. This can happen if importer got deleted while we were
301                 // waiting. See:
302                 // https://bugs.kde.org/show_bug.cgi?id=237156
303                 return false;
304             }
305
306             if(!watcher->isFinished()) {
307                 // Timed out
308                 return false;
309             }
310         } else {
311             watcher->waitForFinished();
312         }
313
314         if (watcher->isError()) {
315             DMWARNING << watcher->error().message();
316             return false;
317         }
318
319         return true;
320     }
321 };
322
323 DBusMenuImporter::DBusMenuImporter(const QString &service, const QString &path, QObject *parent)
324 : DBusMenuImporter(service, path, ASYNCHRONOUS, parent)
325 {
326 }
327
328 DBusMenuImporter::DBusMenuImporter(const QString &service, const QString &path, DBusMenuImporterType type, QObject *parent)
329 : QObject(parent)
330 , d(new DBusMenuImporterPrivate)
331 {
332     d->q = this;
333     d->m_interface = new QDBusInterface(service, path, DBUSMENU_INTERFACE, QDBusConnection::sessionBus(), this);
334     d->m_menu = 0;
335     d->m_mustEmitMenuUpdated = false;
336
337     d->m_type = type;
338
339     connect(&d->m_mapper, SIGNAL(mapped(int)), SLOT(sendClickedEvent(int)));
340
341     d->m_pendingLayoutUpdateTimer = new QTimer(this);
342     d->m_pendingLayoutUpdateTimer->setSingleShot(true);
343     connect(d->m_pendingLayoutUpdateTimer, SIGNAL(timeout()), SLOT(processPendingLayoutUpdates()));
344
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)));
353
354     d->refresh(0);
355 }
356
357 DBusMenuImporter::~DBusMenuImporter()
358 {
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();
363     delete d;
364 }
365
366 void DBusMenuImporter::slotLayoutUpdated(uint revision, int parentId)
367 {
368     if (d->m_idsRefreshedByAboutToShow.remove(parentId)) {
369         return;
370     }
371     d->m_pendingLayoutUpdates << parentId;
372     if (!d->m_pendingLayoutUpdateTimer->isActive()) {
373         d->m_pendingLayoutUpdateTimer->start();
374     }
375 }
376
377 void DBusMenuImporter::processPendingLayoutUpdates()
378 {
379     QSet<int> ids = d->m_pendingLayoutUpdates;
380     d->m_pendingLayoutUpdates.clear();
381     Q_FOREACH(int id, ids) {
382         d->refresh(id);
383     }
384 }
385
386 QMenu *DBusMenuImporter::menu() const
387 {
388     if (!d->m_menu) {
389         d->m_menu = d->createMenu(0);
390     }
391     return d->m_menu;
392 }
393
394 void DBusMenuImporterPrivate::slotItemsPropertiesUpdated(const DBusMenuItemList &updatedList, const DBusMenuItemKeysList &removedList)
395 {
396     Q_FOREACH(const DBusMenuItem &item, updatedList) {
397         QAction *action = m_actionForId.value(item.id);
398         if (!action) {
399             // We don't know this action. It probably is in a menu we haven't fetched yet.
400             continue;
401         }
402
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());
408         }
409     }
410
411     Q_FOREACH(const DBusMenuItemKeys &item, removedList) {
412         QAction *action = m_actionForId.value(item.id);
413         if (!action) {
414             // We don't know this action. It probably is in a menu we haven't fetched yet.
415             continue;
416         }
417
418         Q_FOREACH(const QString &key, item.properties) {
419             updateActionProperty(action, key, QVariant());
420         }
421     }
422 }
423
424 void DBusMenuImporter::slotItemActivationRequested(int id, uint /*timestamp*/)
425 {
426     QAction *action = d->m_actionForId.value(id);
427     DMRETURN_IF_FAIL(action);
428     actionActivationRequested(action);
429 }
430
431 void DBusMenuImporter::slotGetLayoutFinished(QDBusPendingCallWatcher *watcher)
432 {
433     int parentId = watcher->property(DBUSMENU_PROPERTY_ID).toInt();
434     watcher->deleteLater();
435
436     QDBusPendingReply<uint, DBusMenuLayoutItem> reply = *watcher;
437     if (!reply.isValid()) {
438         DMWARNING << reply.error().message();
439         return;
440     }
441
442     #ifdef BENCHMARK
443     DMDEBUG << "- items received:" << sChrono.elapsed() << "ms";
444     #endif
445     DBusMenuLayoutItem rootItem = reply.argumentAt<1>();
446
447     QMenu *menu = d->menuForId(parentId);
448     if (!menu) {
449         DMWARNING << "No menu for id" << parentId;
450         return;
451     }
452
453     menu->clear();
454
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);
460         } else {
461             delete *it;
462             *it = action;
463         }
464         menu->addAction(action);
465
466         connect(action, SIGNAL(triggered()),
467             &d->m_mapper, SLOT(map()));
468         d->m_mapper.setMapping(action, dbusMenuItem.id);
469
470         if( action->menu() )
471         {
472           d->refresh( dbusMenuItem.id )->waitForFinished();
473         }
474     }
475     #ifdef BENCHMARK
476     DMDEBUG << "- Menu filled:" << sChrono.elapsed() << "ms";
477     #endif
478 }
479
480 void DBusMenuImporter::sendClickedEvent(int id)
481 {
482     d->sendEvent(id, QString("clicked"));
483 }
484
485 void DBusMenuImporter::updateMenu()
486 {
487     d->m_mustEmitMenuUpdated = true;
488     QMetaObject::invokeMethod(menu(), "aboutToShow");
489 }
490
491 void DBusMenuImporter::slotMenuAboutToShow()
492 {
493     QMenu *menu = qobject_cast<QMenu*>(sender());
494     Q_ASSERT(menu);
495
496     QAction *action = menu->menuAction();
497     Q_ASSERT(action);
498
499     int id = action->property(DBUSMENU_PROPERTY_ID).toInt();
500
501     #ifdef BENCHMARK
502     QTime time;
503     time.start();
504     #endif
505
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*)));
511
512     QPointer<QObject> guard(this);
513
514     if (!d->waitForWatcher(watcher, ABOUT_TO_SHOW_TIMEOUT)) {
515         DMWARNING << "Application did not answer to AboutToShow() before timeout";
516     }
517
518     #ifdef BENCHMARK
519     DMVAR(time.elapsed());
520     #endif
521     // "this" got deleted during the call to waitForWatcher(), get out
522     if (!guard) {
523         return;
524     }
525
526     if (menu == d->m_menu && d->m_mustEmitMenuUpdated) {
527         d->m_mustEmitMenuUpdated = false;
528         menuUpdated();
529     }
530
531     d->sendEvent(id, QString("opened"));
532 }
533
534 void DBusMenuImporter::slotAboutToShowDBusCallFinished(QDBusPendingCallWatcher *watcher)
535 {
536     int id = watcher->property(DBUSMENU_PROPERTY_ID).toInt();
537     watcher->deleteLater();
538
539     QDBusPendingReply<bool> reply = *watcher;
540     if (reply.isError()) {
541         DMWARNING << "Call to AboutToShow() failed:" << reply.error().message();
542         return;
543     }
544     bool needRefresh = reply.argumentAt<0>();
545
546     QMenu *menu = d->menuForId(id);
547     DMRETURN_IF_FAIL(menu);
548
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";
554         }
555     }
556 }
557
558 void DBusMenuImporter::slotMenuAboutToHide()
559 {
560     QMenu *menu = qobject_cast<QMenu*>(sender());
561     Q_ASSERT(menu);
562
563     QAction *action = menu->menuAction();
564     Q_ASSERT(action);
565
566     int id = action->property(DBUSMENU_PROPERTY_ID).toInt();
567     d->sendEvent(id, QString("closed"));
568 }
569
570 QMenu *DBusMenuImporter::createMenu(QWidget *parent)
571 {
572     return new QMenu(parent);
573 }
574
575 QIcon DBusMenuImporter::iconForName(const QString &/*name*/)
576 {
577     return QIcon();
578 }
579
580 #include "moc_dbusmenuimporter.cpp"