KStatusNotifierItem

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

KDE's Doxygen guidelines are available online.