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"