File indexing completed on 2025-01-12 03:40:51

0001 /* This file is part of the dbusmenu-qt library
0002    SPDX-FileCopyrightText: 2009 Canonical
0003    Author: Aurelien Gateau <aurelien.gateau@canonical.com>
0004 
0005    SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 #include "dbusmenuexporter.h"
0008 
0009 // Qt
0010 #include <QActionGroup>
0011 #include <QBuffer>
0012 #include <QDateTime>
0013 #include <QMap>
0014 #include <QMenu>
0015 #include <QSet>
0016 #include <QTimer>
0017 #include <QToolButton>
0018 #include <QWidgetAction>
0019 
0020 // Local
0021 #include "dbusmenu_p.h"
0022 #include "dbusmenuexporterdbus_p.h"
0023 #include "dbusmenuexporterprivate_p.h"
0024 #include "dbusmenushortcut_p.h"
0025 #include "dbusmenutypes_p.h"
0026 #include "debug_p.h"
0027 #include "utils_p.h"
0028 
0029 static const char *KMENU_TITLE = "kmenu_title";
0030 
0031 //-------------------------------------------------
0032 //
0033 // DBusMenuExporterPrivate
0034 //
0035 //-------------------------------------------------
0036 int DBusMenuExporterPrivate::idForAction(QAction *action) const
0037 {
0038     DMRETURN_VALUE_IF_FAIL(action, -1);
0039     return m_idForAction.value(action, -2);
0040 }
0041 
0042 void DBusMenuExporterPrivate::addMenu(QMenu *menu, int parentId)
0043 {
0044     if (menu->findChild<DBusMenu *>()) {
0045         // This can happen if a menu is removed from its parent and added back
0046         // See KDE bug 254066
0047         return;
0048     }
0049     new DBusMenu(menu, q, parentId);
0050     const auto actions = menu->actions();
0051     for (QAction *action : actions) {
0052         addAction(action, parentId);
0053     }
0054 }
0055 
0056 QVariantMap DBusMenuExporterPrivate::propertiesForAction(QAction *action) const
0057 {
0058     DMRETURN_VALUE_IF_FAIL(action, QVariantMap());
0059 
0060     if (action->objectName() == QString::fromLatin1(KMENU_TITLE)) {
0061         // Hack: Support for KDE menu titles in a Qt-only library...
0062         return propertiesForKMenuTitleAction(action);
0063     } else if (action->isSeparator()) {
0064         return propertiesForSeparatorAction(action);
0065     } else {
0066         return propertiesForStandardAction(action);
0067     }
0068 }
0069 
0070 QVariantMap DBusMenuExporterPrivate::propertiesForKMenuTitleAction(QAction *action_) const
0071 {
0072     QVariantMap map;
0073     // In case the other side does not know about x-kde-title, show a disabled item
0074     map.insert(QStringLiteral("enabled"), false);
0075     map.insert(QStringLiteral("x-kde-title"), true);
0076 
0077     const QWidgetAction *widgetAction = qobject_cast<const QWidgetAction *>(action_);
0078     DMRETURN_VALUE_IF_FAIL(widgetAction, map);
0079     QToolButton *button = qobject_cast<QToolButton *>(widgetAction->defaultWidget());
0080     DMRETURN_VALUE_IF_FAIL(button, map);
0081     QAction *action = button->defaultAction();
0082     DMRETURN_VALUE_IF_FAIL(action, map);
0083 
0084     map.insert(QStringLiteral("label"), swapMnemonicChar(action->text(), QLatin1Char('&'), QLatin1Char('_')));
0085     insertIconProperty(&map, action);
0086     if (!action->isVisible()) {
0087         map.insert(QStringLiteral("visible"), false);
0088     }
0089     return map;
0090 }
0091 
0092 QVariantMap DBusMenuExporterPrivate::propertiesForSeparatorAction(QAction *action) const
0093 {
0094     QVariantMap map;
0095     map.insert(QStringLiteral("type"), QStringLiteral("separator"));
0096     if (!action->isVisible()) {
0097         map.insert(QStringLiteral("visible"), false);
0098     }
0099     return map;
0100 }
0101 
0102 QVariantMap DBusMenuExporterPrivate::propertiesForStandardAction(QAction *action) const
0103 {
0104     QVariantMap map;
0105     map.insert(QStringLiteral("label"), swapMnemonicChar(action->text(), QLatin1Char('&'), QLatin1Char('_')));
0106     if (!action->isEnabled()) {
0107         map.insert(QStringLiteral("enabled"), false);
0108     }
0109     if (!action->isVisible()) {
0110         map.insert(QStringLiteral("visible"), false);
0111     }
0112     if (action->menu()) {
0113         map.insert(QStringLiteral("children-display"), QStringLiteral("submenu"));
0114     }
0115     if (action->isCheckable()) {
0116         bool exclusive = action->actionGroup() && action->actionGroup()->isExclusive();
0117         map.insert(QStringLiteral("toggle-type"), exclusive ? QStringLiteral("radio") : QStringLiteral("checkmark"));
0118         map.insert(QStringLiteral("toggle-state"), action->isChecked() ? 1 : 0);
0119     }
0120     insertIconProperty(&map, action);
0121     QKeySequence keySequence = action->shortcut();
0122     if (!keySequence.isEmpty()) {
0123         DBusMenuShortcut shortcut = DBusMenuShortcut::fromKeySequence(keySequence);
0124         map.insert(QStringLiteral("shortcut"), QVariant::fromValue(shortcut));
0125     }
0126     return map;
0127 }
0128 
0129 QMenu *DBusMenuExporterPrivate::menuForId(int id) const
0130 {
0131     if (id == 0) {
0132         return m_rootMenu;
0133     }
0134     QAction *action = m_actionForId.value(id);
0135     // Action may not be in m_actionForId if it has been deleted between the
0136     // time it was announced by the exporter and the time the importer asks for
0137     // it.
0138     return action ? action->menu() : nullptr;
0139 }
0140 
0141 void DBusMenuExporterPrivate::fillLayoutItem(DBusMenuLayoutItem *item, QMenu *menu, int id, int depth, const QStringList &propertyNames)
0142 {
0143     item->id = id;
0144     item->properties = m_dbusObject->getProperties(id, propertyNames);
0145 
0146     if (depth != 0 && menu) {
0147         const auto actions = menu->actions();
0148         for (QAction *action : actions) {
0149             int actionId = m_idForAction.value(action, -1);
0150             if (actionId == -1) {
0151                 DMWARNING << "No id for action";
0152                 continue;
0153             }
0154 
0155             DBusMenuLayoutItem child;
0156             fillLayoutItem(&child, action->menu(), actionId, depth - 1, propertyNames);
0157             item->children << child;
0158         }
0159     }
0160 }
0161 
0162 void DBusMenuExporterPrivate::updateAction(QAction *action)
0163 {
0164     int id = idForAction(action);
0165     if (m_itemUpdatedIds.contains(id)) {
0166         return;
0167     }
0168     m_itemUpdatedIds << id;
0169     m_itemUpdatedTimer->start();
0170 }
0171 
0172 void DBusMenuExporterPrivate::addAction(QAction *action, int parentId)
0173 {
0174     int id = m_idForAction.value(action, -1);
0175     if (id != -1) {
0176         DMWARNING << "Already tracking action" << action->text() << "under id" << id;
0177         return;
0178     }
0179     QVariantMap map = propertiesForAction(action);
0180     id = m_nextId++;
0181     QObject::connect(action, SIGNAL(destroyed(QObject *)), q, SLOT(slotActionDestroyed(QObject *)));
0182     m_actionForId.insert(id, action);
0183     m_idForAction.insert(action, id);
0184     m_actionProperties.insert(action, map);
0185     if (action->menu()) {
0186         addMenu(action->menu(), id);
0187     }
0188     ++m_revision;
0189     emitLayoutUpdated(parentId);
0190 }
0191 
0192 /**
0193  * IMPORTANT: action might have already been destroyed when this method is
0194  * called, so don't dereference the pointer (it is a QObject to avoid being
0195  * tempted to dereference)
0196  */
0197 void DBusMenuExporterPrivate::removeActionInternal(QObject *object)
0198 {
0199     QAction *action = static_cast<QAction *>(object);
0200     m_actionProperties.remove(action);
0201     int id = m_idForAction.take(action);
0202     m_actionForId.remove(id);
0203 }
0204 
0205 void DBusMenuExporterPrivate::removeAction(QAction *action, int parentId)
0206 {
0207     removeActionInternal(action);
0208     QObject::disconnect(action, SIGNAL(destroyed(QObject *)), q, SLOT(slotActionDestroyed(QObject *)));
0209     ++m_revision;
0210     emitLayoutUpdated(parentId);
0211 }
0212 
0213 void DBusMenuExporterPrivate::emitLayoutUpdated(int id)
0214 {
0215     if (m_layoutUpdatedIds.contains(id)) {
0216         return;
0217     }
0218     m_layoutUpdatedIds << id;
0219     m_layoutUpdatedTimer->start();
0220 }
0221 
0222 void DBusMenuExporterPrivate::insertIconProperty(QVariantMap *map, QAction *action) const
0223 {
0224     // provide the icon name for per-theme lookups
0225     const QString iconName = q->iconNameForAction(action);
0226     if (!iconName.isEmpty()) {
0227         map->insert(QStringLiteral("icon-name"), iconName);
0228     }
0229 
0230     // provide the serialized icon data in case the icon
0231     // is unnamed or the name isn't supported by the theme
0232     const QIcon icon = action->icon();
0233     if (!icon.isNull()) {
0234         QBuffer buffer;
0235         icon.pixmap(16).save(&buffer, "PNG");
0236         map->insert(QStringLiteral("icon-data"), buffer.data());
0237     }
0238 }
0239 
0240 static void collapseSeparator(QAction *action)
0241 {
0242     action->setVisible(false);
0243 }
0244 
0245 // Unless the separatorsCollapsible property is set to false, Qt will get rid
0246 // of separators at the beginning and at the end of menus as well as collapse
0247 // multiple separators in the middle. For example, a menu like this:
0248 //
0249 // ---
0250 // Open
0251 // ---
0252 // ---
0253 // Quit
0254 // ---
0255 //
0256 // is displayed like this:
0257 //
0258 // Open
0259 // ---
0260 // Quit
0261 //
0262 // We fake this by setting separators invisible before exporting them.
0263 //
0264 // cf. https://bugs.launchpad.net/libdbusmenu-qt/+bug/793339
0265 void DBusMenuExporterPrivate::collapseSeparators(QMenu *menu)
0266 {
0267     QList<QAction *> actions = menu->actions();
0268     if (actions.isEmpty()) {
0269         return;
0270     }
0271 
0272     QList<QAction *>::Iterator it, begin = actions.begin(), end = actions.end();
0273 
0274     // Get rid of separators at end
0275     it = end - 1;
0276     for (; it != begin; --it) {
0277         if ((*it)->isSeparator()) {
0278             collapseSeparator(*it);
0279         } else {
0280             break;
0281         }
0282     }
0283     // end now points after the last visible entry
0284     end = it + 1;
0285     it = begin;
0286 
0287     // Get rid of separators at beginnning
0288     for (; it != end; ++it) {
0289         if ((*it)->isSeparator()) {
0290             collapseSeparator(*it);
0291         } else {
0292             break;
0293         }
0294     }
0295 
0296     // Collapse separators in between
0297     bool previousWasSeparator = false;
0298     for (; it != end; ++it) {
0299         QAction *action = *it;
0300         if (action->isSeparator()) {
0301             if (previousWasSeparator) {
0302                 collapseSeparator(action);
0303             } else {
0304                 previousWasSeparator = true;
0305             }
0306         } else {
0307             previousWasSeparator = false;
0308         }
0309     }
0310 }
0311 
0312 //-------------------------------------------------
0313 //
0314 // DBusMenuExporter
0315 //
0316 //-------------------------------------------------
0317 DBusMenuExporter::DBusMenuExporter(const QString &objectPath, QMenu *menu, const QDBusConnection &_connection)
0318     : QObject(menu)
0319     , d(new DBusMenuExporterPrivate)
0320 {
0321     d->q = this;
0322     d->m_objectPath = objectPath;
0323     d->m_rootMenu = menu;
0324     d->m_nextId = 1;
0325     d->m_revision = 1;
0326     d->m_emittedLayoutUpdatedOnce = false;
0327     d->m_itemUpdatedTimer = new QTimer(this);
0328     d->m_layoutUpdatedTimer = new QTimer(this);
0329     d->m_dbusObject = new DBusMenuExporterDBus(this);
0330 
0331     d->addMenu(d->m_rootMenu, 0);
0332 
0333     d->m_itemUpdatedTimer->setInterval(0);
0334     d->m_itemUpdatedTimer->setSingleShot(true);
0335     connect(d->m_itemUpdatedTimer, SIGNAL(timeout()), SLOT(doUpdateActions()));
0336 
0337     d->m_layoutUpdatedTimer->setInterval(0);
0338     d->m_layoutUpdatedTimer->setSingleShot(true);
0339     connect(d->m_layoutUpdatedTimer, SIGNAL(timeout()), SLOT(doEmitLayoutUpdated()));
0340 
0341     QDBusConnection connection(_connection);
0342     connection.registerObject(objectPath, d->m_dbusObject, QDBusConnection::ExportAllContents);
0343 }
0344 
0345 DBusMenuExporter::~DBusMenuExporter()
0346 {
0347     delete d;
0348 }
0349 
0350 void DBusMenuExporter::doUpdateActions()
0351 {
0352     if (d->m_itemUpdatedIds.isEmpty()) {
0353         return;
0354     }
0355     DBusMenuItemList updatedList;
0356     DBusMenuItemKeysList removedList;
0357 
0358     for (int id : d->m_itemUpdatedIds) {
0359         QAction *action = d->m_actionForId.value(id);
0360         if (!action) {
0361             // Action does not exist anymore
0362             continue;
0363         }
0364 
0365         QVariantMap &oldProperties = d->m_actionProperties[action];
0366         QVariantMap newProperties = d->propertiesForAction(action);
0367         QVariantMap updatedProperties;
0368         QStringList removedProperties;
0369 
0370         // Find updated and removed properties
0371         QVariantMap::ConstIterator newEnd = newProperties.constEnd();
0372 
0373         QVariantMap::ConstIterator oldIt = oldProperties.constBegin(), oldEnd = oldProperties.constEnd();
0374         for (; oldIt != oldEnd; ++oldIt) {
0375             QString key = oldIt.key();
0376             QVariantMap::ConstIterator newIt = newProperties.constFind(key);
0377             if (newIt != newEnd) {
0378                 if (newIt.value() != oldIt.value()) {
0379                     updatedProperties.insert(key, newIt.value());
0380                 }
0381             } else {
0382                 removedProperties << key;
0383             }
0384         }
0385 
0386         // Find new properties (treat them as updated properties)
0387         QVariantMap::ConstIterator newIt = newProperties.constBegin();
0388         for (; newIt != newEnd; ++newIt) {
0389             QString key = newIt.key();
0390             oldIt = oldProperties.constFind(key);
0391             if (oldIt == oldEnd) {
0392                 updatedProperties.insert(key, newIt.value());
0393             }
0394         }
0395 
0396         // Update our data (oldProperties is a reference)
0397         oldProperties = newProperties;
0398         QMenu *menu = action->menu();
0399         if (menu) {
0400             d->addMenu(menu, id);
0401         }
0402 
0403         if (!updatedProperties.isEmpty()) {
0404             DBusMenuItem item;
0405             item.id = id;
0406             item.properties = updatedProperties;
0407             updatedList << item;
0408         }
0409         if (!removedProperties.isEmpty()) {
0410             DBusMenuItemKeys itemKeys;
0411             itemKeys.id = id;
0412             itemKeys.properties = removedProperties;
0413             removedList << itemKeys;
0414         }
0415     }
0416     d->m_itemUpdatedIds.clear();
0417     if (!d->m_emittedLayoutUpdatedOnce) {
0418         // No need to tell the world about action changes: nobody knows the
0419         // menu layout so nobody knows about the actions.
0420         // Note: We can't stop in DBusMenuExporterPrivate::addAction(), we
0421         // still need to reach this method because we want our properties to be
0422         // updated, even if we don't announce changes.
0423         return;
0424     }
0425     if (!updatedList.isEmpty() || !removedList.isEmpty()) {
0426         d->m_dbusObject->ItemsPropertiesUpdated(updatedList, removedList);
0427     }
0428 }
0429 
0430 void DBusMenuExporter::doEmitLayoutUpdated()
0431 {
0432     // Collapse separators for all updated menus
0433     for (int id : d->m_layoutUpdatedIds) {
0434         QMenu *menu = d->menuForId(id);
0435         if (menu && menu->separatorsCollapsible()) {
0436             d->collapseSeparators(menu);
0437         }
0438     }
0439 
0440     // Tell the world about the update
0441     if (d->m_emittedLayoutUpdatedOnce) {
0442         for (int id : std::as_const(d->m_layoutUpdatedIds)) {
0443             d->m_dbusObject->LayoutUpdated(d->m_revision, id);
0444         }
0445     } else {
0446         // First time we emit LayoutUpdated, no need to emit several layout
0447         // updates, signals the whole layout (id==0) has been updated
0448         d->m_dbusObject->LayoutUpdated(d->m_revision, 0);
0449         d->m_emittedLayoutUpdatedOnce = true;
0450     }
0451     d->m_layoutUpdatedIds.clear();
0452 }
0453 
0454 QString DBusMenuExporter::iconNameForAction(QAction *action)
0455 {
0456     DMRETURN_VALUE_IF_FAIL(action, QString());
0457     QIcon icon = action->icon();
0458     if (action->isIconVisibleInMenu() && !icon.isNull()) {
0459         return icon.name();
0460     } else {
0461         return QString();
0462     }
0463 }
0464 
0465 void DBusMenuExporter::activateAction(QAction *action)
0466 {
0467     int id = d->idForAction(action);
0468     DMRETURN_IF_FAIL(id >= 0);
0469     const uint timeStamp = QDateTime::currentDateTime().toMSecsSinceEpoch();
0470     d->m_dbusObject->ItemActivationRequested(id, timeStamp);
0471 }
0472 
0473 void DBusMenuExporter::slotActionDestroyed(QObject *object)
0474 {
0475     d->removeActionInternal(object);
0476 }
0477 
0478 void DBusMenuExporter::setStatus(const QString &status)
0479 {
0480     d->m_dbusObject->setStatus(status);
0481 }
0482 
0483 QString DBusMenuExporter::status() const
0484 {
0485     return d->m_dbusObject->status();
0486 }
0487 
0488 #include "moc_dbusmenuexporter.cpp"