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"