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