File indexing completed on 2024-04-28 16:54:30
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; 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 d->m_menu = nullptr; 0271 0272 d->m_pendingLayoutUpdateTimer = new QTimer(this); 0273 d->m_pendingLayoutUpdateTimer->setSingleShot(true); 0274 connect(d->m_pendingLayoutUpdateTimer, &QTimer::timeout, this, &DBusMenuImporter::processPendingLayoutUpdates); 0275 0276 connect(d->m_interface, &DBusMenuInterface::LayoutUpdated, this, &DBusMenuImporter::slotLayoutUpdated); 0277 connect(d->m_interface, &DBusMenuInterface::ItemActivationRequested, this, &DBusMenuImporter::slotItemActivationRequested); 0278 connect(d->m_interface, 0279 &DBusMenuInterface::ItemsPropertiesUpdated, 0280 this, 0281 [this](const DBusMenuItemList &updatedList, const DBusMenuItemKeysList &removedList) { 0282 d->slotItemsPropertiesUpdated(updatedList, removedList); 0283 }); 0284 0285 d->refresh(0); 0286 } 0287 0288 DBusMenuImporter::~DBusMenuImporter() 0289 { 0290 // Do not use "delete d->m_menu": even if we are being deleted we should 0291 // leave enough time for the menu to finish what it was doing, for example 0292 // if it was being displayed. 0293 d->m_menu->deleteLater(); 0294 delete d; 0295 } 0296 0297 void DBusMenuImporter::slotLayoutUpdated(uint revision, int parentId) 0298 { 0299 Q_UNUSED(revision) 0300 if (d->m_idsRefreshedByAboutToShow.remove(parentId)) { 0301 return; 0302 } 0303 d->m_pendingLayoutUpdates << parentId; 0304 if (!d->m_pendingLayoutUpdateTimer->isActive()) { 0305 d->m_pendingLayoutUpdateTimer->start(); 0306 } 0307 } 0308 0309 void DBusMenuImporter::processPendingLayoutUpdates() 0310 { 0311 const QSet<int> ids = d->m_pendingLayoutUpdates; 0312 d->m_pendingLayoutUpdates.clear(); 0313 for (int id : ids) { 0314 d->refresh(id); 0315 } 0316 } 0317 0318 QMenu *DBusMenuImporter::menu() const 0319 { 0320 if (!d->m_menu) { 0321 d->m_menu = d->createMenu(nullptr); 0322 } 0323 return d->m_menu; 0324 } 0325 0326 void DBusMenuImporterPrivate::slotItemsPropertiesUpdated(const DBusMenuItemList &updatedList, const DBusMenuItemKeysList &removedList) 0327 { 0328 for (const DBusMenuItem &item : updatedList) { 0329 QAction *action = m_actionForId.value(item.id); 0330 if (!action) { 0331 // We don't know this action. It probably is in a menu we haven't fetched yet. 0332 continue; 0333 } 0334 0335 QVariantMap::ConstIterator it = item.properties.constBegin(), end = item.properties.constEnd(); 0336 for (; it != end; ++it) { 0337 updateActionProperty(action, it.key(), it.value()); 0338 } 0339 } 0340 0341 for (const DBusMenuItemKeys &item : removedList) { 0342 QAction *action = m_actionForId.value(item.id); 0343 if (!action) { 0344 // We don't know this action. It probably is in a menu we haven't fetched yet. 0345 continue; 0346 } 0347 0348 const auto properties{item.properties}; 0349 for (const QString &key : properties) { 0350 updateActionProperty(action, key, QVariant()); 0351 } 0352 } 0353 } 0354 0355 QAction *DBusMenuImporter::actionForId(int id) const 0356 { 0357 return d->m_actionForId.value(id); 0358 } 0359 0360 void DBusMenuImporter::slotItemActivationRequested(int id, uint /*timestamp*/) 0361 { 0362 QAction *action = d->m_actionForId.value(id); 0363 DMRETURN_IF_FAIL(action); 0364 actionActivationRequested(action); 0365 } 0366 0367 void DBusMenuImporter::slotGetLayoutFinished(QDBusPendingCallWatcher *watcher) 0368 { 0369 int parentId = watcher->property(DBUSMENU_PROPERTY_ID).toInt(); 0370 watcher->deleteLater(); 0371 0372 QMenu *menu = d->menuForId(parentId); 0373 0374 QDBusPendingReply<uint, DBusMenuLayoutItem> reply = *watcher; 0375 if (!reply.isValid()) { 0376 qDebug(DBUSMENUQT) << reply.error().message(); 0377 if (menu) { 0378 Q_EMIT menuUpdated(menu); 0379 } 0380 return; 0381 } 0382 0383 #ifdef BENCHMARK 0384 DMDEBUG << "- items received:" << sChrono.elapsed() << "ms"; 0385 #endif 0386 DBusMenuLayoutItem rootItem = reply.argumentAt<1>(); 0387 0388 if (!menu) { 0389 qDebug(DBUSMENUQT) << "No menu for id" << parentId; 0390 return; 0391 } 0392 0393 // remove outdated actions 0394 QSet<int> newDBusMenuItemIds; 0395 newDBusMenuItemIds.reserve(rootItem.children.count()); 0396 for (const DBusMenuLayoutItem &item : qAsConst(rootItem.children)) { 0397 newDBusMenuItemIds << item.id; 0398 } 0399 for (QAction *action : menu->actions()) { 0400 int id = action->property(DBUSMENU_PROPERTY_ID).toInt(); 0401 if (!newDBusMenuItemIds.contains(id)) { 0402 // Not calling removeAction() as QMenu will immediately close when it becomes empty, 0403 // which can happen when an application completely reloads this menu. 0404 // When the action is deleted deferred, it is removed from the menu. 0405 action->deleteLater(); 0406 if (action->menu()) { 0407 action->menu()->deleteLater(); 0408 } 0409 d->m_actionForId.remove(id); 0410 } 0411 } 0412 0413 // insert or update new actions into our menu 0414 for (const DBusMenuLayoutItem &dbusMenuItem : qAsConst(rootItem.children)) { 0415 DBusMenuImporterPrivate::ActionForId::Iterator it = d->m_actionForId.find(dbusMenuItem.id); 0416 QAction *action = nullptr; 0417 if (it == d->m_actionForId.end()) { 0418 int id = dbusMenuItem.id; 0419 action = d->createAction(id, dbusMenuItem.properties, menu); 0420 d->m_actionForId.insert(id, action); 0421 0422 connect(action, &QObject::destroyed, this, [this, id]() { 0423 d->m_actionForId.remove(id); 0424 }); 0425 0426 connect(action, &QAction::triggered, this, [id, this]() { 0427 sendClickedEvent(id); 0428 }); 0429 0430 if (QMenu *menuAction = action->menu()) { 0431 connect(menuAction, &QMenu::aboutToShow, this, &DBusMenuImporter::slotMenuAboutToShow, Qt::UniqueConnection); 0432 } 0433 connect(menu, &QMenu::aboutToHide, this, &DBusMenuImporter::slotMenuAboutToHide, Qt::UniqueConnection); 0434 0435 menu->addAction(action); 0436 } else { 0437 action = *it; 0438 QStringList filteredKeys = dbusMenuItem.properties.keys(); 0439 filteredKeys.removeOne(QStringLiteral("type")); 0440 filteredKeys.removeOne(QStringLiteral("toggle-type")); 0441 filteredKeys.removeOne(QStringLiteral("children-display")); 0442 d->updateAction(*it, dbusMenuItem.properties, filteredKeys); 0443 // Move the action to the tail so we can keep the order same as the dbus request. 0444 menu->removeAction(action); 0445 menu->addAction(action); 0446 } 0447 } 0448 0449 Q_EMIT menuUpdated(menu); 0450 } 0451 0452 void DBusMenuImporter::sendClickedEvent(int id) 0453 { 0454 d->sendEvent(id, QStringLiteral("clicked")); 0455 } 0456 0457 void DBusMenuImporter::updateMenu() 0458 { 0459 updateMenu(DBusMenuImporter::menu()); 0460 } 0461 0462 void DBusMenuImporter::updateMenu(QMenu *menu) 0463 { 0464 Q_ASSERT(menu); 0465 0466 QAction *action = menu->menuAction(); 0467 Q_ASSERT(action); 0468 0469 int id = action->property(DBUSMENU_PROPERTY_ID).toInt(); 0470 0471 auto call = d->m_interface->AboutToShow(id); 0472 QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this); 0473 watcher->setProperty(DBUSMENU_PROPERTY_ID, id); 0474 connect(watcher, &QDBusPendingCallWatcher::finished, this, &DBusMenuImporter::slotAboutToShowDBusCallFinished); 0475 0476 // Firefox deliberately ignores "aboutToShow" whereas Qt ignores" opened", so we'll just send both all the time... 0477 d->sendEvent(id, QStringLiteral("opened")); 0478 } 0479 0480 void DBusMenuImporter::slotAboutToShowDBusCallFinished(QDBusPendingCallWatcher *watcher) 0481 { 0482 int id = watcher->property(DBUSMENU_PROPERTY_ID).toInt(); 0483 watcher->deleteLater(); 0484 0485 QMenu *menu = d->menuForId(id); 0486 if (!menu) { 0487 return; 0488 } 0489 0490 QDBusPendingReply<bool> reply = *watcher; 0491 if (reply.isError()) { 0492 qDebug(DBUSMENUQT) << "Call to AboutToShow() failed:" << reply.error().message(); 0493 Q_EMIT menuUpdated(menu); 0494 return; 0495 } 0496 // Note, this isn't used by Qt's QPT - but we get a LayoutChanged emitted before 0497 // this returns, which equates to the same thing 0498 bool needRefresh = reply.argumentAt<0>(); 0499 0500 if (needRefresh || menu->actions().isEmpty()) { 0501 d->m_idsRefreshedByAboutToShow << id; 0502 d->refresh(id); 0503 } else if (menu) { 0504 Q_EMIT menuUpdated(menu); 0505 } 0506 } 0507 0508 void DBusMenuImporter::slotMenuAboutToHide() 0509 { 0510 QMenu *menu = qobject_cast<QMenu *>(sender()); 0511 Q_ASSERT(menu); 0512 0513 QAction *action = menu->menuAction(); 0514 Q_ASSERT(action); 0515 0516 int id = action->property(DBUSMENU_PROPERTY_ID).toInt(); 0517 d->sendEvent(id, QStringLiteral("closed")); 0518 } 0519 0520 void DBusMenuImporter::slotMenuAboutToShow() 0521 { 0522 QMenu *menu = qobject_cast<QMenu *>(sender()); 0523 Q_ASSERT(menu); 0524 0525 updateMenu(menu); 0526 } 0527 0528 QMenu *DBusMenuImporter::createMenu(QWidget *parent) 0529 { 0530 return new QMenu(parent); 0531 } 0532 0533 QIcon DBusMenuImporter::iconForName(const QString &name) 0534 { 0535 return QIcon::fromTheme(name); 0536 } 0537 0538 #include "moc_dbusmenuimporter.cpp"