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 "dbusmenuexporter.h"
22 
23 // Qt
24 #include <QBuffer>
25 #include <QDateTime>
26 #include <QMap>
27 #include <QMenu>
28 #include <QSet>
29 #include <QTimer>
30 #include <QToolButton>
31 #include <QWidgetAction>
32 #include <QActionGroup>
33 
34 // Local
35 #include "dbusmenu_config.h"
36 #include "dbusmenu_p.h"
37 #include "dbusmenuexporterdbus_p.h"
38 #include "dbusmenuexporterprivate_p.h"
39 #include "dbusmenutypes_p.h"
40 #include "dbusmenushortcut_p.h"
41 #include "debug_p.h"
42 #include "utils_p.h"
43 
44 static const char *KMENU_TITLE = "kmenu_title";
45 
46 //-------------------------------------------------
47 //
48 // DBusMenuExporterPrivate
49 //
50 //-------------------------------------------------
idForAction(QAction * action) const51 int DBusMenuExporterPrivate::idForAction(QAction *action) const
52 {
53     DMRETURN_VALUE_IF_FAIL(action, -1);
54     return m_idForAction.value(action, -2);
55 }
56 
addMenu(QMenu * menu,int parentId)57 void DBusMenuExporterPrivate::addMenu(QMenu *menu, int parentId)
58 {
59     if (menu->findChild<DBusMenu *>()) {
60         // This can happen if a menu is removed from its parent and added back
61         // See KDE bug 254066
62         return;
63     }
64     new DBusMenu(menu, q, parentId);
65     Q_FOREACH(QAction *action, menu->actions()) {
66         addAction(action, parentId);
67     }
68 }
69 
propertiesForAction(QAction * action) const70 QVariantMap DBusMenuExporterPrivate::propertiesForAction(QAction *action) const
71 {
72     DMRETURN_VALUE_IF_FAIL(action, QVariantMap());
73 
74     if (action->objectName() == KMENU_TITLE) {
75         // Hack: Support for KDE menu titles in a Qt-only library...
76         return propertiesForKMenuTitleAction(action);
77     } else if (action->isSeparator()) {
78         return propertiesForSeparatorAction(action);
79     } else {
80         return propertiesForStandardAction(action);
81     }
82 }
83 
propertiesForKMenuTitleAction(QAction * action_) const84 QVariantMap DBusMenuExporterPrivate::propertiesForKMenuTitleAction(QAction *action_) const
85 {
86     QVariantMap map;
87     // In case the other side does not know about x-kde-title, show a disabled item
88     map.insert("enabled", false);
89     map.insert("x-kde-title", true);
90 
91     const QWidgetAction *widgetAction = qobject_cast<const QWidgetAction *>(action_);
92     DMRETURN_VALUE_IF_FAIL(widgetAction, map);
93     QToolButton *button = qobject_cast<QToolButton *>(widgetAction->defaultWidget());
94     DMRETURN_VALUE_IF_FAIL(button, map);
95     QAction *action = button->defaultAction();
96     DMRETURN_VALUE_IF_FAIL(action, map);
97 
98     map.insert("label", swapMnemonicChar(action->text(), '&', '_'));
99     insertIconProperty(&map, action);
100     if (!action->isVisible()) {
101         map.insert("visible", false);
102     }
103     return map;
104 }
105 
propertiesForSeparatorAction(QAction * action) const106 QVariantMap DBusMenuExporterPrivate::propertiesForSeparatorAction(QAction *action) const
107 {
108     QVariantMap map;
109     map.insert("type", "separator");
110     if (!action->isVisible()) {
111         map.insert("visible", false);
112     }
113     return map;
114 }
115 
propertiesForStandardAction(QAction * action) const116 QVariantMap DBusMenuExporterPrivate::propertiesForStandardAction(QAction *action) const
117 {
118     QVariantMap map;
119     map.insert("label", swapMnemonicChar(action->text(), '&', '_'));
120     if (!action->isEnabled()) {
121         map.insert("enabled", false);
122     }
123     if (!action->isVisible()) {
124         map.insert("visible", false);
125     }
126     if (action->menu()) {
127         map.insert("children-display", "submenu");
128     }
129     if (action->isCheckable()) {
130         bool exclusive = action->actionGroup() && action->actionGroup()->isExclusive();
131         map.insert("toggle-type", exclusive ? "radio" : "checkmark");
132         map.insert("toggle-state", action->isChecked() ? 1 : 0);
133     }
134     insertIconProperty(&map, action);
135     QKeySequence keySequence = action->shortcut();
136     if (!keySequence.isEmpty()) {
137         DBusMenuShortcut shortcut = DBusMenuShortcut::fromKeySequence(keySequence);
138         map.insert("shortcut", QVariant::fromValue(shortcut));
139     }
140     return map;
141 }
142 
menuForId(int id) const143 QMenu *DBusMenuExporterPrivate::menuForId(int id) const
144 {
145     if (id == 0) {
146         return m_rootMenu;
147     }
148     QAction *action = m_actionForId.value(id);
149     // Action may not be in m_actionForId if it has been deleted between the
150     // time it was announced by the exporter and the time the importer asks for
151     // it.
152     return action ? action->menu() : 0;
153 }
154 
fillLayoutItem(DBusMenuLayoutItem * item,QMenu * menu,int id,int depth,const QStringList & propertyNames)155 void DBusMenuExporterPrivate::fillLayoutItem(DBusMenuLayoutItem *item, QMenu *menu, int id, int depth, const QStringList &propertyNames)
156 {
157     item->id = id;
158     item->properties = m_dbusObject->getProperties(id, propertyNames);
159 
160     if (depth != 0 && menu) {
161         Q_FOREACH(QAction *action, menu->actions()) {
162             int actionId = m_idForAction.value(action, -1);
163             if (actionId == -1) {
164                 DMWARNING << "No id for action";
165                 continue;
166             }
167 
168             DBusMenuLayoutItem child;
169             fillLayoutItem(&child, action->menu(), actionId, depth - 1, propertyNames);
170             item->children << child;
171         }
172     }
173 }
174 
updateAction(QAction * action)175 void DBusMenuExporterPrivate::updateAction(QAction *action)
176 {
177     int id = idForAction(action);
178     if (m_itemUpdatedIds.contains(id)) {
179         return;
180     }
181     m_itemUpdatedIds << id;
182     m_itemUpdatedTimer->start();
183 }
184 
addAction(QAction * action,int parentId)185 void DBusMenuExporterPrivate::addAction(QAction *action, int parentId)
186 {
187     int id = m_idForAction.value(action, -1);
188     if (id != -1) {
189         DMWARNING << "Already tracking action" << action->text() << "under id" << id;
190         return;
191     }
192     QVariantMap map = propertiesForAction(action);
193     id = m_nextId++;
194     QObject::connect(action, SIGNAL(destroyed(QObject*)), q, SLOT(slotActionDestroyed(QObject*)));
195     m_actionForId.insert(id, action);
196     m_idForAction.insert(action, id);
197     m_actionProperties.insert(action, map);
198     if (action->menu()) {
199         addMenu(action->menu(), id);
200     }
201     ++m_revision;
202     emitLayoutUpdated(parentId);
203 }
204 
205 /**
206  * IMPORTANT: action might have already been destroyed when this method is
207  * called, so don't dereference the pointer (it is a QObject to avoid being
208  * tempted to dereference)
209  */
removeActionInternal(QObject * object)210 void DBusMenuExporterPrivate::removeActionInternal(QObject *object)
211 {
212     QAction* action = static_cast<QAction*>(object);
213     m_actionProperties.remove(action);
214     int id = m_idForAction.take(action);
215     m_actionForId.remove(id);
216 }
217 
removeAction(QAction * action,int parentId)218 void DBusMenuExporterPrivate::removeAction(QAction *action, int parentId)
219 {
220     removeActionInternal(action);
221     QObject::disconnect(action, SIGNAL(destroyed(QObject*)), q, SLOT(slotActionDestroyed(QObject*)));
222     ++m_revision;
223     emitLayoutUpdated(parentId);
224 }
225 
emitLayoutUpdated(int id)226 void DBusMenuExporterPrivate::emitLayoutUpdated(int id)
227 {
228     if (m_layoutUpdatedIds.contains(id)) {
229         return;
230     }
231     m_layoutUpdatedIds << id;
232     m_layoutUpdatedTimer->start();
233 }
234 
insertIconProperty(QVariantMap * map,QAction * action) const235 void DBusMenuExporterPrivate::insertIconProperty(QVariantMap *map, QAction *action) const
236 {
237     // provide the icon name for per-theme lookups
238     const QString iconName = q->iconNameForAction(action);
239     if (!iconName.isEmpty()) {
240         map->insert("icon-name", iconName);
241     }
242 
243     // provide the serialized icon data in case the icon
244     // is unnamed or the name isn't supported by the theme
245     const QIcon icon = action->icon();
246     if (!icon.isNull()) {
247         QBuffer buffer;
248         icon.pixmap(16).save(&buffer, "PNG");
249         map->insert("icon-data", buffer.data());
250     }
251 }
252 
collapseSeparator(QAction * action)253 static void collapseSeparator(QAction* action)
254 {
255     action->setVisible(false);
256 }
257 
258 // Unless the separatorsCollapsible property is set to false, Qt will get rid
259 // of separators at the beginning and at the end of menus as well as collapse
260 // multiple separators in the middle. For example, a menu like this:
261 //
262 // ---
263 // Open
264 // ---
265 // ---
266 // Quit
267 // ---
268 //
269 // is displayed like this:
270 //
271 // Open
272 // ---
273 // Quit
274 //
275 // We fake this by setting separators invisible before exporting them.
276 //
277 // cf. https://bugs.launchpad.net/libdbusmenu-qt/+bug/793339
collapseSeparators(QMenu * menu)278 void DBusMenuExporterPrivate::collapseSeparators(QMenu* menu)
279 {
280     QList<QAction*> actions = menu->actions();
281     if (actions.isEmpty()) {
282         return;
283     }
284 
285     QList<QAction*>::Iterator it, begin = actions.begin(), end = actions.end();
286 
287     // Get rid of separators at end
288     it = end - 1;
289     for (; it != begin; --it) {
290         if ((*it)->isSeparator()) {
291             collapseSeparator(*it);
292         } else {
293             break;
294         }
295     }
296     // end now points after the last visible entry
297     end = it + 1;
298     it = begin;
299 
300     // Get rid of separators at beginnning
301     for (; it != end; ++it) {
302         if ((*it)->isSeparator()) {
303             collapseSeparator(*it);
304         } else {
305             break;
306         }
307     }
308 
309     // Collapse separators in between
310     bool previousWasSeparator = false;
311     for (; it != end; ++it) {
312         QAction* action = *it;
313         if (action->isSeparator()) {
314             if (previousWasSeparator) {
315                 collapseSeparator(action);
316             } else {
317                 previousWasSeparator = true;
318             }
319         } else {
320             previousWasSeparator = false;
321         }
322     }
323 }
324 
325 //-------------------------------------------------
326 //
327 // DBusMenuExporter
328 //
329 //-------------------------------------------------
DBusMenuExporter(const QString & objectPath,QMenu * menu,const QDBusConnection & _connection)330 DBusMenuExporter::DBusMenuExporter(const QString &objectPath, QMenu *menu, const QDBusConnection &_connection)
331 : QObject(menu)
332 , d(new DBusMenuExporterPrivate)
333 {
334     d->q = this;
335     d->m_objectPath = objectPath;
336     d->m_rootMenu = menu;
337     d->m_nextId = 1;
338     d->m_revision = 1;
339     d->m_emittedLayoutUpdatedOnce = false;
340     d->m_itemUpdatedTimer = new QTimer(this);
341     d->m_layoutUpdatedTimer = new QTimer(this);
342     d->m_dbusObject = new DBusMenuExporterDBus(this);
343 
344     d->addMenu(d->m_rootMenu, 0);
345 
346     d->m_itemUpdatedTimer->setInterval(0);
347     d->m_itemUpdatedTimer->setSingleShot(true);
348     connect(d->m_itemUpdatedTimer, SIGNAL(timeout()), SLOT(doUpdateActions()));
349 
350     d->m_layoutUpdatedTimer->setInterval(0);
351     d->m_layoutUpdatedTimer->setSingleShot(true);
352     connect(d->m_layoutUpdatedTimer, SIGNAL(timeout()), SLOT(doEmitLayoutUpdated()));
353 
354     QDBusConnection connection(_connection);
355     connection.registerObject(objectPath, d->m_dbusObject, QDBusConnection::ExportAllContents);
356 }
357 
~DBusMenuExporter()358 DBusMenuExporter::~DBusMenuExporter()
359 {
360     delete d;
361 }
362 
doUpdateActions()363 void DBusMenuExporter::doUpdateActions()
364 {
365     if (d->m_itemUpdatedIds.isEmpty()) {
366         return;
367     }
368     DBusMenuItemList updatedList;
369     DBusMenuItemKeysList removedList;
370 
371     Q_FOREACH(int id, d->m_itemUpdatedIds) {
372         QAction *action = d->m_actionForId.value(id);
373         if (!action) {
374             // Action does not exist anymore
375             continue;
376         }
377 
378         QVariantMap& oldProperties = d->m_actionProperties[action];
379         QVariantMap  newProperties = d->propertiesForAction(action);
380         QVariantMap  updatedProperties;
381         QStringList  removedProperties;
382 
383         // Find updated and removed properties
384         QVariantMap::ConstIterator newEnd = newProperties.constEnd();
385 
386         QVariantMap::ConstIterator
387             oldIt = oldProperties.constBegin(),
388             oldEnd = oldProperties.constEnd();
389         for(; oldIt != oldEnd; ++oldIt) {
390             QString key = oldIt.key();
391             QVariantMap::ConstIterator newIt = newProperties.constFind(key);
392             if (newIt != newEnd) {
393                 if (newIt.value() != oldIt.value()) {
394                     updatedProperties.insert(key, newIt.value());
395                 }
396             } else {
397                 removedProperties << key;
398             }
399         }
400 
401         // Find new properties (treat them as updated properties)
402         QVariantMap::ConstIterator newIt = newProperties.constBegin();
403         for (; newIt != newEnd; ++newIt) {
404             QString key = newIt.key();
405             oldIt = oldProperties.constFind(key);
406             if (oldIt == oldEnd) {
407                 updatedProperties.insert(key, newIt.value());
408             }
409         }
410 
411         // Update our data (oldProperties is a reference)
412         oldProperties = newProperties;
413         QMenu *menu = action->menu();
414         if (menu) {
415             d->addMenu(menu, id);
416         }
417 
418         if (!updatedProperties.isEmpty()) {
419             DBusMenuItem item;
420             item.id = id;
421             item.properties = updatedProperties;
422             updatedList << item;
423         }
424         if (!removedProperties.isEmpty()) {
425             DBusMenuItemKeys itemKeys;
426             itemKeys.id = id;
427             itemKeys.properties = removedProperties;
428             removedList << itemKeys;
429         }
430     }
431     d->m_itemUpdatedIds.clear();
432     if (!d->m_emittedLayoutUpdatedOnce) {
433         // No need to tell the world about action changes: nobody knows the
434         // menu layout so nobody knows about the actions.
435         // Note: We can't stop in DBusMenuExporterPrivate::addAction(), we
436         // still need to reach this method because we want our properties to be
437         // updated, even if we don't announce changes.
438         return;
439     }
440     if (!updatedList.isEmpty() || !removedList.isEmpty()) {
441         d->m_dbusObject->ItemsPropertiesUpdated(updatedList, removedList);
442     }
443 }
444 
doEmitLayoutUpdated()445 void DBusMenuExporter::doEmitLayoutUpdated()
446 {
447     // Collapse separators for all updated menus
448     Q_FOREACH(int id, d->m_layoutUpdatedIds) {
449         QMenu* menu = d->menuForId(id);
450         if (menu && menu->separatorsCollapsible()) {
451             d->collapseSeparators(menu);
452         }
453     }
454 
455     // Tell the world about the update
456     if (d->m_emittedLayoutUpdatedOnce) {
457         Q_FOREACH(int id, d->m_layoutUpdatedIds) {
458             d->m_dbusObject->LayoutUpdated(d->m_revision, id);
459         }
460     } else {
461         // First time we emit LayoutUpdated, no need to emit several layout
462         // updates, signals the whole layout (id==0) has been updated
463         d->m_dbusObject->LayoutUpdated(d->m_revision, 0);
464         d->m_emittedLayoutUpdatedOnce = true;
465     }
466     d->m_layoutUpdatedIds.clear();
467 }
468 
iconNameForAction(QAction * action)469 QString DBusMenuExporter::iconNameForAction(QAction *action)
470 {
471     DMRETURN_VALUE_IF_FAIL(action, QString());
472 #ifdef HAVE_QICON_NAME
473     QIcon icon = action->icon();
474     if (action->isIconVisibleInMenu() && !icon.isNull()) {
475         return icon.name();
476     } else {
477         return QString();
478     }
479 #else
480     return QString();
481 #endif
482 }
483 
activateAction(QAction * action)484 void DBusMenuExporter::activateAction(QAction *action)
485 {
486     int id = d->idForAction(action);
487     DMRETURN_IF_FAIL(id >= 0);
488     uint timeStamp = QDateTime::currentDateTime().toSecsSinceEpoch();
489     d->m_dbusObject->ItemActivationRequested(id, timeStamp);
490 }
491 
slotActionDestroyed(QObject * object)492 void DBusMenuExporter::slotActionDestroyed(QObject* object)
493 {
494     d->removeActionInternal(object);
495 }
496 
setStatus(const QString & status)497 void DBusMenuExporter::setStatus(const QString& status)
498 {
499     d->m_dbusObject->setStatus(status);
500 }
501 
status() const502 QString DBusMenuExporter::status() const
503 {
504     return d->m_dbusObject->status();
505 }
506 
507 #include "moc_dbusmenuexporter.cpp"
508