File indexing completed on 2024-04-28 05:35:32

0001 /* This file is part of the dbusmenu-qt library
0002     SPDX-FileCopyrightText: 2009 Canonical
0003     SPDX-FileContributor: Aurelien Gateau <aurelien.gateau@canonical.com>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 #include "dbusmenuimporter.h"
0008 
0009 #include "debug.h"
0010 
0011 // Qt
0012 #include <QActionGroup>
0013 #include <QCoreApplication>
0014 #include <QDBusConnection>
0015 #include <QDBusInterface>
0016 #include <QDBusReply>
0017 #include <QDBusVariant>
0018 #include <QDebug>
0019 #include <QFont>
0020 #include <QMenu>
0021 #include <QPointer>
0022 #include <QSet>
0023 #include <QTime>
0024 #include <QTimer>
0025 #include <QToolButton>
0026 #include <QWidgetAction>
0027 
0028 // Local
0029 #include "dbusmenushortcut_p.h"
0030 #include "dbusmenutypes_p.h"
0031 #include "utils_p.h"
0032 
0033 // Generated
0034 #include "dbusmenu_interface.h"
0035 
0036 // #define BENCHMARK
0037 #ifdef BENCHMARK
0038 static QTime sChrono;
0039 #endif
0040 
0041 #define DMRETURN_IF_FAIL(cond)                                                                                                                                 \
0042     if (!(cond)) {                                                                                                                                             \
0043         qCWarning(DBUSMENUQT) << "Condition failed: " #cond;                                                                                                   \
0044         return;                                                                                                                                                \
0045     }
0046 
0047 static const char *DBUSMENU_PROPERTY_ID = "_dbusmenu_id";
0048 static const char *DBUSMENU_PROPERTY_ICON_NAME = "_dbusmenu_icon_name";
0049 static const char *DBUSMENU_PROPERTY_ICON_DATA_HASH = "_dbusmenu_icon_data_hash";
0050 
0051 static QAction *createKdeTitle(QAction *action, QWidget *parent)
0052 {
0053     QToolButton *titleWidget = new QToolButton(nullptr);
0054     QFont font = titleWidget->font();
0055     font.setBold(true);
0056     titleWidget->setFont(font);
0057     titleWidget->setIcon(action->icon());
0058     titleWidget->setText(action->text());
0059     titleWidget->setDown(true);
0060     titleWidget->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
0061 
0062     QWidgetAction *titleAction = new QWidgetAction(parent);
0063     titleAction->setDefaultWidget(titleWidget);
0064     return titleAction;
0065 }
0066 
0067 class DBusMenuImporterPrivate
0068 {
0069 public:
0070     DBusMenuImporter *q;
0071 
0072     DBusMenuInterface *m_interface;
0073     QMenu *m_menu = nullptr;
0074     using ActionForId = QMap<int, QAction *>;
0075     ActionForId m_actionForId;
0076     QTimer m_pendingLayoutUpdateTimer;
0077 
0078     QSet<int> m_idsRefreshedByAboutToShow;
0079     QSet<int> m_pendingLayoutUpdates;
0080 
0081     QDBusPendingCallWatcher *refresh(int id)
0082     {
0083         auto call = m_interface->GetLayout(id, 1, QStringList());
0084         QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, q);
0085         watcher->setProperty(DBUSMENU_PROPERTY_ID, id);
0086         QObject::connect(watcher, &QDBusPendingCallWatcher::finished, q, &DBusMenuImporter::slotGetLayoutFinished);
0087 
0088         return watcher;
0089     }
0090 
0091     QMenu *createMenu(QWidget *parent)
0092     {
0093         QMenu *menu = q->createMenu(parent);
0094         return menu;
0095     }
0096 
0097     /**
0098      * Init all the immutable action properties here
0099      * TODO: Document immutable properties?
0100      *
0101      * Note: we remove properties we handle from the map (using QMap::take()
0102      * instead of QMap::value()) to avoid warnings about these properties in
0103      * updateAction()
0104      */
0105     QAction *createAction(int id, const QVariantMap &_map, QWidget *parent)
0106     {
0107         QVariantMap map = _map;
0108         QAction *action = new QAction(parent);
0109         action->setProperty(DBUSMENU_PROPERTY_ID, id);
0110 
0111         QString type = map.take(QStringLiteral("type")).toString();
0112         if (type == QLatin1String("separator")) {
0113             action->setSeparator(true);
0114         }
0115 
0116         if (map.take(QStringLiteral("children-display")).toString() == QLatin1String("submenu")) {
0117             QMenu *menu = createMenu(parent);
0118             action->setMenu(menu);
0119         }
0120 
0121         QString toggleType = map.take(QStringLiteral("toggle-type")).toString();
0122         if (!toggleType.isEmpty()) {
0123             action->setCheckable(true);
0124             if (toggleType == QLatin1String("radio")) {
0125                 QActionGroup *group = new QActionGroup(action);
0126                 group->addAction(action);
0127             }
0128         }
0129 
0130         bool isKdeTitle = map.take(QStringLiteral("x-kde-title")).toBool();
0131         updateAction(action, map, map.keys());
0132 
0133         if (isKdeTitle) {
0134             action = createKdeTitle(action, parent);
0135         }
0136 
0137         return action;
0138     }
0139 
0140     /**
0141      * Update mutable properties of an action. A property may be listed in
0142      * requestedProperties but not in map, this means we should use the default value
0143      * for this property.
0144      *
0145      * @param action the action to update
0146      * @param map holds the property values
0147      * @param requestedProperties which properties has been requested
0148      */
0149     void updateAction(QAction *action, const QVariantMap &map, const QStringList &requestedProperties)
0150     {
0151         for (const QString &key : requestedProperties) {
0152             updateActionProperty(action, key, map.value(key));
0153         }
0154     }
0155 
0156     void updateActionProperty(QAction *action, const QString &key, const QVariant &value)
0157     {
0158         if (key == QLatin1String("label")) {
0159             updateActionLabel(action, value);
0160         } else if (key == QLatin1String("enabled")) {
0161             updateActionEnabled(action, value);
0162         } else if (key == QLatin1String("toggle-state")) {
0163             updateActionChecked(action, value);
0164         } else if (key == QLatin1String("icon-name")) {
0165             updateActionIconByName(action, value);
0166         } else if (key == QLatin1String("icon-data")) {
0167             updateActionIconByData(action, value);
0168         } else if (key == QLatin1String("visible")) {
0169             updateActionVisible(action, value);
0170         } else if (key == QLatin1String("shortcut")) {
0171             updateActionShortcut(action, value);
0172         } else {
0173             qDebug(DBUSMENUQT) << "Unhandled property update" << key;
0174         }
0175     }
0176 
0177     void updateActionLabel(QAction *action, const QVariant &value)
0178     {
0179         QString text = swapMnemonicChar(value.toString(), '_', '&');
0180         action->setText(text);
0181     }
0182 
0183     void updateActionEnabled(QAction *action, const QVariant &value)
0184     {
0185         action->setEnabled(value.isValid() ? value.toBool() : true);
0186     }
0187 
0188     void updateActionChecked(QAction *action, const QVariant &value)
0189     {
0190         if (action->isCheckable() && value.isValid()) {
0191             action->setChecked(value.toInt() == 1);
0192         }
0193     }
0194 
0195     void updateActionIconByName(QAction *action, const QVariant &value)
0196     {
0197         const QString iconName = value.toString();
0198         const QString previous = action->property(DBUSMENU_PROPERTY_ICON_NAME).toString();
0199         if (previous == iconName) {
0200             return;
0201         }
0202         action->setProperty(DBUSMENU_PROPERTY_ICON_NAME, iconName);
0203         if (iconName.isEmpty()) {
0204             action->setIcon(QIcon());
0205             return;
0206         }
0207         action->setIcon(q->iconForName(iconName));
0208     }
0209 
0210     void updateActionIconByData(QAction *action, const QVariant &value)
0211     {
0212         const QByteArray data = value.toByteArray();
0213         uint dataHash = qHash(data);
0214         uint previousDataHash = action->property(DBUSMENU_PROPERTY_ICON_DATA_HASH).toUInt();
0215         if (previousDataHash == dataHash) {
0216             return;
0217         }
0218         action->setProperty(DBUSMENU_PROPERTY_ICON_DATA_HASH, dataHash);
0219         QPixmap pix;
0220         if (!pix.loadFromData(data)) {
0221             qDebug(DBUSMENUQT) << "Failed to decode icon-data property for action" << action->text();
0222             action->setIcon(QIcon());
0223             return;
0224         }
0225         action->setIcon(QIcon(pix));
0226     }
0227 
0228     void updateActionVisible(QAction *action, const QVariant &value)
0229     {
0230         action->setVisible(value.isValid() ? value.toBool() : true);
0231     }
0232 
0233     void updateActionShortcut(QAction *action, const QVariant &value)
0234     {
0235         QDBusArgument arg = value.value<QDBusArgument>();
0236         DBusMenuShortcut dmShortcut;
0237         arg >> dmShortcut;
0238         QKeySequence keySequence = dmShortcut.toKeySequence();
0239         action->setShortcut(keySequence);
0240     }
0241 
0242     QMenu *menuForId(int id) const
0243     {
0244         if (id == 0) {
0245             return q->menu();
0246         }
0247         QAction *action = m_actionForId.value(id);
0248         if (!action) {
0249             return nullptr;
0250         }
0251         return action->menu();
0252     }
0253 
0254     void slotItemsPropertiesUpdated(const DBusMenuItemList &updatedList, const DBusMenuItemKeysList &removedList);
0255 
0256     void sendEvent(int id, const QString &eventId)
0257     {
0258         m_interface->Event(id, eventId, QDBusVariant(QString()), 0u);
0259     }
0260 };
0261 
0262 DBusMenuImporter::DBusMenuImporter(const QString &service, const QString &path, QObject *parent)
0263     : QObject(parent)
0264     , d(new DBusMenuImporterPrivate)
0265 {
0266     DBusMenuTypes_register();
0267 
0268     d->q = this;
0269     d->m_interface = new DBusMenuInterface(service, path, QDBusConnection::sessionBus(), this);
0270 
0271     d->m_pendingLayoutUpdateTimer.setSingleShot(true);
0272     connect(&d->m_pendingLayoutUpdateTimer, &QTimer::timeout, this, &DBusMenuImporter::processPendingLayoutUpdates);
0273 
0274     connect(d->m_interface, &DBusMenuInterface::LayoutUpdated, this, &DBusMenuImporter::slotLayoutUpdated);
0275     connect(d->m_interface, &DBusMenuInterface::ItemActivationRequested, this, &DBusMenuImporter::slotItemActivationRequested);
0276     connect(d->m_interface,
0277             &DBusMenuInterface::ItemsPropertiesUpdated,
0278             this,
0279             [this](const DBusMenuItemList &updatedList, const DBusMenuItemKeysList &removedList) {
0280                 d->slotItemsPropertiesUpdated(updatedList, removedList);
0281             });
0282 
0283     d->refresh(0);
0284 }
0285 
0286 DBusMenuImporter::~DBusMenuImporter()
0287 {
0288     // Do not use "delete d->m_menu": even if we are being deleted we should
0289     // leave enough time for the menu to finish what it was doing, for example
0290     // if it was being displayed.
0291     d->m_menu->deleteLater();
0292     delete d;
0293 }
0294 
0295 void DBusMenuImporter::slotLayoutUpdated(uint revision, int parentId)
0296 {
0297     Q_UNUSED(revision)
0298     if (d->m_idsRefreshedByAboutToShow.remove(parentId)) {
0299         return;
0300     }
0301     d->m_pendingLayoutUpdates << parentId;
0302     if (!d->m_pendingLayoutUpdateTimer.isActive()) {
0303         d->m_pendingLayoutUpdateTimer.start();
0304     }
0305 }
0306 
0307 void DBusMenuImporter::processPendingLayoutUpdates()
0308 {
0309     const QSet<int> ids = d->m_pendingLayoutUpdates;
0310     d->m_pendingLayoutUpdates.clear();
0311     for (int id : ids) {
0312         d->refresh(id);
0313     }
0314 }
0315 
0316 QMenu *DBusMenuImporter::menu() const
0317 {
0318     if (!d->m_menu) {
0319         d->m_menu = d->createMenu(nullptr);
0320     }
0321     return d->m_menu;
0322 }
0323 
0324 void DBusMenuImporterPrivate::slotItemsPropertiesUpdated(const DBusMenuItemList &updatedList, const DBusMenuItemKeysList &removedList)
0325 {
0326     for (const DBusMenuItem &item : updatedList) {
0327         QAction *action = m_actionForId.value(item.id);
0328         if (!action) {
0329             // We don't know this action. It probably is in a menu we haven't fetched yet.
0330             continue;
0331         }
0332 
0333         QVariantMap::ConstIterator it = item.properties.constBegin(), end = item.properties.constEnd();
0334         for (; it != end; ++it) {
0335             updateActionProperty(action, it.key(), it.value());
0336         }
0337     }
0338 
0339     for (const DBusMenuItemKeys &item : removedList) {
0340         QAction *action = m_actionForId.value(item.id);
0341         if (!action) {
0342             // We don't know this action. It probably is in a menu we haven't fetched yet.
0343             continue;
0344         }
0345 
0346         const auto properties{item.properties};
0347         for (const QString &key : properties) {
0348             updateActionProperty(action, key, QVariant());
0349         }
0350     }
0351 }
0352 
0353 QAction *DBusMenuImporter::actionForId(int id) const
0354 {
0355     return d->m_actionForId.value(id);
0356 }
0357 
0358 void DBusMenuImporter::slotItemActivationRequested(int id, uint /*timestamp*/)
0359 {
0360     QAction *action = d->m_actionForId.value(id);
0361     DMRETURN_IF_FAIL(action);
0362     actionActivationRequested(action);
0363 }
0364 
0365 void DBusMenuImporter::slotGetLayoutFinished(QDBusPendingCallWatcher *watcher)
0366 {
0367     int parentId = watcher->property(DBUSMENU_PROPERTY_ID).toInt();
0368     watcher->deleteLater();
0369 
0370     QMenu *menu = d->menuForId(parentId);
0371 
0372     QDBusPendingReply<uint, DBusMenuLayoutItem> reply = *watcher;
0373     if (!reply.isValid()) {
0374         qDebug(DBUSMENUQT) << reply.error().message();
0375         if (menu) {
0376             Q_EMIT menuUpdated(menu);
0377         }
0378         return;
0379     }
0380 
0381 #ifdef BENCHMARK
0382     DMDEBUG << "- items received:" << sChrono.elapsed() << "ms";
0383 #endif
0384     DBusMenuLayoutItem rootItem = reply.argumentAt<1>();
0385 
0386     if (!menu) {
0387         qDebug(DBUSMENUQT) << "No menu for id" << parentId;
0388         return;
0389     }
0390 
0391     // remove outdated actions
0392     QSet<int> newDBusMenuItemIds;
0393     newDBusMenuItemIds.reserve(rootItem.children.count());
0394     for (const DBusMenuLayoutItem &item : std::as_const(rootItem.children)) {
0395         newDBusMenuItemIds << item.id;
0396     }
0397     for (QAction *action : menu->actions()) {
0398         int id = action->property(DBUSMENU_PROPERTY_ID).toInt();
0399         if (!newDBusMenuItemIds.contains(id)) {
0400             // Not calling removeAction() as QMenu will immediately close when it becomes empty,
0401             // which can happen when an application completely reloads this menu.
0402             // When the action is deleted deferred, it is removed from the menu.
0403             action->deleteLater();
0404             if (action->menu()) {
0405                 action->menu()->deleteLater();
0406             }
0407             d->m_actionForId.remove(id);
0408         }
0409     }
0410 
0411     // insert or update new actions into our menu
0412     for (const DBusMenuLayoutItem &dbusMenuItem : std::as_const(rootItem.children)) {
0413         DBusMenuImporterPrivate::ActionForId::Iterator it = d->m_actionForId.find(dbusMenuItem.id);
0414         QAction *action = nullptr;
0415         if (it == d->m_actionForId.end()) {
0416             int id = dbusMenuItem.id;
0417             action = d->createAction(id, dbusMenuItem.properties, menu);
0418             d->m_actionForId.insert(id, action);
0419 
0420             connect(action, &QObject::destroyed, this, [this, id]() {
0421                 d->m_actionForId.remove(id);
0422             });
0423 
0424             connect(action, &QAction::triggered, this, [id, this]() {
0425                 sendClickedEvent(id);
0426             });
0427 
0428             if (QMenu *menuAction = action->menu()) {
0429                 connect(menuAction, &QMenu::aboutToShow, this, &DBusMenuImporter::slotMenuAboutToShow, Qt::UniqueConnection);
0430             }
0431             connect(menu, &QMenu::aboutToHide, this, &DBusMenuImporter::slotMenuAboutToHide, Qt::UniqueConnection);
0432 
0433             menu->addAction(action);
0434         } else {
0435             action = *it;
0436             QStringList filteredKeys = dbusMenuItem.properties.keys();
0437             filteredKeys.removeOne(QStringLiteral("type"));
0438             filteredKeys.removeOne(QStringLiteral("toggle-type"));
0439             filteredKeys.removeOne(QStringLiteral("children-display"));
0440             d->updateAction(*it, dbusMenuItem.properties, filteredKeys);
0441             // Move the action to the tail so we can keep the order same as the dbus request.
0442             menu->removeAction(action);
0443             menu->addAction(action);
0444         }
0445     }
0446 
0447     Q_EMIT menuUpdated(menu);
0448 }
0449 
0450 void DBusMenuImporter::sendClickedEvent(int id)
0451 {
0452     d->sendEvent(id, QStringLiteral("clicked"));
0453 }
0454 
0455 void DBusMenuImporter::updateMenu()
0456 {
0457     updateMenu(DBusMenuImporter::menu());
0458 }
0459 
0460 void DBusMenuImporter::updateMenu(QMenu *menu)
0461 {
0462     Q_ASSERT(menu);
0463 
0464     QAction *action = menu->menuAction();
0465     Q_ASSERT(action);
0466 
0467     int id = action->property(DBUSMENU_PROPERTY_ID).toInt();
0468 
0469     auto call = d->m_interface->AboutToShow(id);
0470     QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this);
0471     watcher->setProperty(DBUSMENU_PROPERTY_ID, id);
0472     connect(watcher, &QDBusPendingCallWatcher::finished, this, &DBusMenuImporter::slotAboutToShowDBusCallFinished);
0473 
0474     // Firefox deliberately ignores "aboutToShow" whereas Qt ignores" opened", so we'll just send both all the time...
0475     d->sendEvent(id, QStringLiteral("opened"));
0476 }
0477 
0478 void DBusMenuImporter::slotAboutToShowDBusCallFinished(QDBusPendingCallWatcher *watcher)
0479 {
0480     int id = watcher->property(DBUSMENU_PROPERTY_ID).toInt();
0481     watcher->deleteLater();
0482 
0483     QMenu *menu = d->menuForId(id);
0484     if (!menu) {
0485         return;
0486     }
0487 
0488     QDBusPendingReply<bool> reply = *watcher;
0489     if (reply.isError()) {
0490         qDebug(DBUSMENUQT) << "Call to AboutToShow() failed:" << reply.error().message();
0491         Q_EMIT menuUpdated(menu);
0492         return;
0493     }
0494     // Note, this isn't used by Qt's QPT - but we get a LayoutChanged emitted before
0495     // this returns, which equates to the same thing
0496     bool needRefresh = reply.argumentAt<0>();
0497 
0498     if (needRefresh || menu->actions().isEmpty()) {
0499         d->m_idsRefreshedByAboutToShow << id;
0500         d->refresh(id);
0501     } else if (menu) {
0502         Q_EMIT menuUpdated(menu);
0503     }
0504 }
0505 
0506 void DBusMenuImporter::slotMenuAboutToHide()
0507 {
0508     QMenu *menu = qobject_cast<QMenu *>(sender());
0509     Q_ASSERT(menu);
0510 
0511     QAction *action = menu->menuAction();
0512     Q_ASSERT(action);
0513 
0514     int id = action->property(DBUSMENU_PROPERTY_ID).toInt();
0515     d->sendEvent(id, QStringLiteral("closed"));
0516 }
0517 
0518 void DBusMenuImporter::slotMenuAboutToShow()
0519 {
0520     QMenu *menu = qobject_cast<QMenu *>(sender());
0521     Q_ASSERT(menu);
0522 
0523     updateMenu(menu);
0524 }
0525 
0526 QMenu *DBusMenuImporter::createMenu(QWidget *parent)
0527 {
0528     return new QMenu(parent);
0529 }
0530 
0531 QIcon DBusMenuImporter::iconForName(const QString &name)
0532 {
0533     return QIcon::fromTheme(name);
0534 }
0535 
0536 #include "moc_dbusmenuimporter.cpp"