File indexing completed on 2024-05-12 05:37:26
0001 /* 0002 SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <kde@privat.broulik.de> 0003 0004 SPDX-License-Identifier: LGPL-2.1-or-later 0005 */ 0006 0007 #include "window.h" 0008 0009 #include "debug.h" 0010 0011 #include <QDBusConnection> 0012 #include <QDBusMessage> 0013 #include <QDBusPendingCallWatcher> 0014 #include <QDBusPendingReply> 0015 #include <QDebug> 0016 #include <QList> 0017 #include <QMutableListIterator> 0018 #include <QVariantList> 0019 0020 #include <algorithm> 0021 0022 #include "actions.h" 0023 #include "dbusmenuadaptor.h" 0024 #include "icons.h" 0025 #include "menu.h" 0026 #include "utils.h" 0027 0028 #include "../libdbusmenuqt/dbusmenushortcut_p.h" 0029 0030 static const QString s_applicationActionsPrefix = QStringLiteral("app."); 0031 static const QString s_unityActionsPrefix = QStringLiteral("unity."); 0032 static const QString s_windowActionsPrefix = QStringLiteral("win."); 0033 0034 Window::Window(const QString &serviceName) 0035 : QObject() 0036 , m_serviceName(serviceName) 0037 { 0038 qCDebug(DBUSMENUPROXY) << "Created menu on" << serviceName; 0039 0040 Q_ASSERT(!serviceName.isEmpty()); 0041 0042 GDBusMenuTypes_register(); 0043 DBusMenuTypes_register(); 0044 } 0045 0046 Window::~Window() = default; 0047 0048 void Window::init() 0049 { 0050 qCDebug(DBUSMENUPROXY) << "Inited window with menu for" << m_winId << "on" << m_serviceName << "at app" << m_applicationObjectPath << "win" 0051 << m_windowObjectPath << "unity" << m_unityObjectPath; 0052 0053 if (!m_applicationMenuObjectPath.isEmpty()) { 0054 m_applicationMenu = new Menu(m_serviceName, m_applicationMenuObjectPath, this); 0055 connect(m_applicationMenu, &Menu::menuAppeared, this, &Window::updateWindowProperties); 0056 connect(m_applicationMenu, &Menu::menuDisappeared, this, &Window::updateWindowProperties); 0057 connect(m_applicationMenu, &Menu::subscribed, this, &Window::onMenuSubscribed); 0058 // basically so it replies on DBus no matter what 0059 connect(m_applicationMenu, &Menu::failedToSubscribe, this, &Window::onMenuSubscribed); 0060 connect(m_applicationMenu, &Menu::itemsChanged, this, &Window::menuItemsChanged); 0061 connect(m_applicationMenu, &Menu::menusChanged, this, &Window::menuChanged); 0062 } 0063 0064 if (!m_menuBarObjectPath.isEmpty()) { 0065 m_menuBar = new Menu(m_serviceName, m_menuBarObjectPath, this); 0066 connect(m_menuBar, &Menu::menuAppeared, this, &Window::updateWindowProperties); 0067 connect(m_menuBar, &Menu::menuDisappeared, this, &Window::updateWindowProperties); 0068 connect(m_menuBar, &Menu::subscribed, this, &Window::onMenuSubscribed); 0069 connect(m_menuBar, &Menu::failedToSubscribe, this, &Window::onMenuSubscribed); 0070 connect(m_menuBar, &Menu::itemsChanged, this, &Window::menuItemsChanged); 0071 connect(m_menuBar, &Menu::menusChanged, this, &Window::menuChanged); 0072 } 0073 0074 if (!m_applicationObjectPath.isEmpty()) { 0075 m_applicationActions = new Actions(m_serviceName, m_applicationObjectPath, this); 0076 connect(m_applicationActions, &Actions::actionsChanged, this, [this](const QStringList &dirtyActions) { 0077 onActionsChanged(dirtyActions, s_applicationActionsPrefix); 0078 }); 0079 connect(m_applicationActions, &Actions::loaded, this, [this] { 0080 if (m_menuInited) { 0081 onActionsChanged(m_applicationActions->getAll().keys(), s_applicationActionsPrefix); 0082 } else { 0083 initMenu(); 0084 } 0085 }); 0086 m_applicationActions->load(); 0087 } 0088 0089 if (!m_unityObjectPath.isEmpty()) { 0090 m_unityActions = new Actions(m_serviceName, m_unityObjectPath, this); 0091 connect(m_unityActions, &Actions::actionsChanged, this, [this](const QStringList &dirtyActions) { 0092 onActionsChanged(dirtyActions, s_unityActionsPrefix); 0093 }); 0094 connect(m_unityActions, &Actions::loaded, this, [this] { 0095 if (m_menuInited) { 0096 onActionsChanged(m_unityActions->getAll().keys(), s_unityActionsPrefix); 0097 } else { 0098 initMenu(); 0099 } 0100 }); 0101 m_unityActions->load(); 0102 } 0103 0104 if (!m_windowObjectPath.isEmpty()) { 0105 m_windowActions = new Actions(m_serviceName, m_windowObjectPath, this); 0106 connect(m_windowActions, &Actions::actionsChanged, this, [this](const QStringList &dirtyActions) { 0107 onActionsChanged(dirtyActions, s_windowActionsPrefix); 0108 }); 0109 connect(m_windowActions, &Actions::loaded, this, [this] { 0110 if (m_menuInited) { 0111 onActionsChanged(m_windowActions->getAll().keys(), s_windowActionsPrefix); 0112 } else { 0113 initMenu(); 0114 } 0115 }); 0116 m_windowActions->load(); 0117 } 0118 } 0119 0120 WId Window::winId() const 0121 { 0122 return m_winId; 0123 } 0124 0125 void Window::setWinId(WId winId) 0126 { 0127 m_winId = winId; 0128 } 0129 0130 QString Window::serviceName() const 0131 { 0132 return m_serviceName; 0133 } 0134 0135 QString Window::applicationObjectPath() const 0136 { 0137 return m_applicationObjectPath; 0138 } 0139 0140 void Window::setApplicationObjectPath(const QString &applicationObjectPath) 0141 { 0142 m_applicationObjectPath = applicationObjectPath; 0143 } 0144 0145 QString Window::unityObjectPath() const 0146 { 0147 return m_unityObjectPath; 0148 } 0149 0150 void Window::setUnityObjectPath(const QString &unityObjectPath) 0151 { 0152 m_unityObjectPath = unityObjectPath; 0153 } 0154 0155 QString Window::applicationMenuObjectPath() const 0156 { 0157 return m_applicationMenuObjectPath; 0158 } 0159 0160 void Window::setApplicationMenuObjectPath(const QString &applicationMenuObjectPath) 0161 { 0162 m_applicationMenuObjectPath = applicationMenuObjectPath; 0163 } 0164 0165 QString Window::menuBarObjectPath() const 0166 { 0167 return m_menuBarObjectPath; 0168 } 0169 0170 void Window::setMenuBarObjectPath(const QString &menuBarObjectPath) 0171 { 0172 m_menuBarObjectPath = menuBarObjectPath; 0173 } 0174 0175 QString Window::windowObjectPath() const 0176 { 0177 return m_windowObjectPath; 0178 } 0179 0180 void Window::setWindowObjectPath(const QString &windowObjectPath) 0181 { 0182 m_windowObjectPath = windowObjectPath; 0183 } 0184 0185 QString Window::currentMenuObjectPath() const 0186 { 0187 return m_currentMenuObjectPath; 0188 } 0189 0190 QString Window::proxyObjectPath() const 0191 { 0192 return m_proxyObjectPath; 0193 } 0194 0195 void Window::initMenu() 0196 { 0197 if (m_menuInited) { 0198 return; 0199 } 0200 0201 if (!registerDBusObject()) { 0202 return; 0203 } 0204 0205 // appmenu-gtk-module always announces a menu bar on every GTK window even if there is none 0206 // so we subscribe to the menu bar as soon as it shows up so we can figure out 0207 // if we have a menu bar, an app menu, or just nothing 0208 if (m_applicationMenu) { 0209 m_applicationMenu->start(0); 0210 } 0211 0212 if (m_menuBar) { 0213 m_menuBar->start(0); 0214 } 0215 0216 m_menuInited = true; 0217 } 0218 0219 void Window::menuItemsChanged(const QList<uint> &itemIds) 0220 { 0221 if (qobject_cast<Menu *>(sender()) != m_currentMenu) { 0222 return; 0223 } 0224 0225 DBusMenuItemList items; 0226 0227 for (uint id : itemIds) { 0228 const auto newItem = m_currentMenu->getItem(id); 0229 0230 DBusMenuItem dBusItem{// 0 is menu, items start at 1 0231 static_cast<int>(id), 0232 gMenuToDBusMenuProperties(newItem)}; 0233 items.append(dBusItem); 0234 } 0235 0236 Q_EMIT ItemsPropertiesUpdated(items, {}); 0237 } 0238 0239 void Window::menuChanged(const QList<uint> &menuIds) 0240 { 0241 if (qobject_cast<Menu *>(sender()) != m_currentMenu) { 0242 return; 0243 } 0244 0245 for (uint menu : menuIds) { 0246 Q_EMIT LayoutUpdated(3 /*revision*/, menu); 0247 } 0248 } 0249 0250 void Window::onMenuSubscribed(uint id) 0251 { 0252 // When it was a delayed GetLayout request, send the reply now 0253 const auto pendingReplies = m_pendingGetLayouts.values(id); 0254 if (!pendingReplies.isEmpty()) { 0255 for (const auto &pendingReply : pendingReplies) { 0256 if (pendingReply.type() != QDBusMessage::InvalidMessage) { 0257 auto reply = pendingReply.createReply(); 0258 0259 DBusMenuLayoutItem item; 0260 uint revision = GetLayout(Utils::treeStructureToInt(id, 0, 0), 0, {}, item); 0261 0262 reply << revision << QVariant::fromValue(std::move(item)); 0263 0264 QDBusConnection::sessionBus().send(reply); 0265 } 0266 } 0267 m_pendingGetLayouts.remove(id); 0268 } else { 0269 Q_EMIT LayoutUpdated(2 /*revision*/, id); 0270 } 0271 } 0272 0273 bool Window::getAction(const QString &name, GMenuAction &action) const 0274 { 0275 QString lookupName; 0276 Actions *actions = getActionsForAction(name, lookupName); 0277 0278 if (!actions) { 0279 return false; 0280 } 0281 0282 return actions->get(lookupName, action); 0283 } 0284 0285 void Window::triggerAction(const QString &name, const QVariant &target, uint timestamp) 0286 { 0287 QString lookupName; 0288 Actions *actions = getActionsForAction(name, lookupName); 0289 if (!actions) { 0290 return; 0291 } 0292 0293 actions->trigger(lookupName, target, timestamp); 0294 } 0295 0296 Actions *Window::getActionsForAction(const QString &name, QString &lookupName) const 0297 { 0298 if (name.startsWith(QLatin1String("app."))) { 0299 lookupName = name.mid(4); 0300 return m_applicationActions; 0301 } else if (name.startsWith(QLatin1String("unity."))) { 0302 lookupName = name.mid(6); 0303 return m_unityActions; 0304 } else if (name.startsWith(QLatin1String("win."))) { 0305 lookupName = name.mid(4); 0306 return m_windowActions; 0307 } 0308 0309 return nullptr; 0310 } 0311 0312 void Window::onActionsChanged(const QStringList &dirty, const QString &prefix) 0313 { 0314 if (m_applicationMenu) { 0315 m_applicationMenu->actionsChanged(dirty, prefix); 0316 } 0317 if (m_menuBar) { 0318 m_menuBar->actionsChanged(dirty, prefix); 0319 } 0320 } 0321 0322 bool Window::registerDBusObject() 0323 { 0324 Q_ASSERT(m_proxyObjectPath.isEmpty()); 0325 0326 static int menus = 0; 0327 ++menus; 0328 0329 new DbusmenuAdaptor(this); 0330 0331 const QString objectPath = QStringLiteral("/MenuBar/%1").arg(QString::number(menus)); 0332 qCDebug(DBUSMENUPROXY) << "Registering DBus object path" << objectPath; 0333 0334 if (!QDBusConnection::sessionBus().registerObject(objectPath, this)) { 0335 qCWarning(DBUSMENUPROXY) << "Failed to register object"; 0336 return false; 0337 } 0338 0339 m_proxyObjectPath = objectPath; 0340 0341 return true; 0342 } 0343 0344 void Window::updateWindowProperties() 0345 { 0346 const bool hasMenu = ((m_applicationMenu && m_applicationMenu->hasMenu()) || (m_menuBar && m_menuBar->hasMenu())); 0347 0348 if (!hasMenu) { 0349 Q_EMIT requestRemoveWindowProperties(); 0350 return; 0351 } 0352 0353 Menu *oldMenu = m_currentMenu; 0354 Menu *newMenu = qobject_cast<Menu *>(sender()); 0355 // set current menu as needed 0356 if (!m_currentMenu) { 0357 m_currentMenu = newMenu; 0358 // Menu Bar takes precedence over application menu 0359 } else if (m_currentMenu == m_applicationMenu && newMenu == m_menuBar) { 0360 qCDebug(DBUSMENUPROXY) << "Switching from application menu to menu bar"; 0361 m_currentMenu = newMenu; 0362 // TODO update layout 0363 } 0364 0365 if (m_currentMenu != oldMenu) { 0366 // update entire menu now 0367 Q_EMIT LayoutUpdated(4 /*revision*/, 0); 0368 } 0369 0370 Q_EMIT requestWriteWindowProperties(); 0371 } 0372 0373 // DBus 0374 bool Window::AboutToShow(int id) 0375 { 0376 // We always request the first time GetLayout is called and keep up-to-date internally 0377 // No need to have us prepare anything here 0378 Q_UNUSED(id); 0379 return false; 0380 } 0381 0382 void Window::Event(int id, const QString &eventId, const QDBusVariant &data, uint timestamp) 0383 { 0384 Q_UNUSED(data); 0385 0386 if (!m_currentMenu) { 0387 return; 0388 } 0389 0390 // GMenu dbus doesn't have any "opened" or "closed" signals, we'll only handle "clicked" 0391 0392 if (eventId == QLatin1String("clicked")) { 0393 const QVariantMap item = m_currentMenu->getItem(id); 0394 const QString action = item.value(QStringLiteral("action")).toString(); 0395 const QVariant target = item.value(QStringLiteral("target")); 0396 if (!action.isEmpty()) { 0397 triggerAction(action, target, timestamp); 0398 } 0399 } 0400 } 0401 0402 DBusMenuItemList Window::GetGroupProperties(const QList<int> &ids, const QStringList &propertyNames) 0403 { 0404 Q_UNUSED(ids); 0405 Q_UNUSED(propertyNames); 0406 return DBusMenuItemList(); 0407 } 0408 0409 uint Window::GetLayout(int parentId, int recursionDepth, const QStringList &propertyNames, DBusMenuLayoutItem &dbusItem) 0410 { 0411 Q_UNUSED(recursionDepth); // TODO 0412 Q_UNUSED(propertyNames); 0413 0414 int subscription; 0415 int sectionId; 0416 int index; 0417 0418 Utils::intToTreeStructure(parentId, subscription, sectionId, index); 0419 0420 if (!m_currentMenu) { 0421 return 1; 0422 } 0423 0424 if (!m_currentMenu->hasSubscription(subscription)) { 0425 // let's serve multiple similar requests in one go once we've processed them 0426 m_pendingGetLayouts.insert(subscription, message()); 0427 setDelayedReply(true); 0428 0429 m_currentMenu->start(subscription); 0430 return 1; 0431 } 0432 0433 bool ok; 0434 const GMenuItem section = m_currentMenu->getSection(subscription, sectionId, &ok); 0435 0436 if (!ok) { 0437 qCDebug(DBUSMENUPROXY) << "There is no section on" << subscription << "at" << sectionId << "with" << parentId; 0438 return 1; 0439 } 0440 0441 // If a particular entry is requested, see what it is and resolve as necessary 0442 // for example the "File" entry on root is 0,0,1 but is a menu reference to e.g. 1,0,0 0443 // so resolve that and return the correct menu 0444 if (index > 0) { 0445 // non-zero index indicates item within a menu but the index in the list still starts at zero 0446 if (section.items.count() < index) { 0447 qCDebug(DBUSMENUPROXY) << "Requested index" << index << "on" << subscription << "at" << sectionId << "with" << parentId << "is out of bounds"; 0448 return 0; 0449 } 0450 0451 const auto &requestedItem = section.items.at(index - 1); 0452 0453 auto it = requestedItem.constFind(QStringLiteral(":submenu")); 0454 if (it != requestedItem.constEnd()) { 0455 const GMenuSection gmenuSection = qdbus_cast<GMenuSection>(it->value<QDBusArgument>()); 0456 return GetLayout(Utils::treeStructureToInt(gmenuSection.subscription, gmenuSection.menu, 0), recursionDepth, propertyNames, dbusItem); 0457 } else { 0458 // TODO 0459 return 0; 0460 } 0461 } 0462 0463 dbusItem.id = parentId; // TODO 0464 dbusItem.properties = {{QStringLiteral("children-display"), QStringLiteral("submenu")}}; 0465 0466 int count = 0; 0467 0468 const auto itemsToBeAdded = section.items; 0469 for (const auto &item : itemsToBeAdded) { 0470 DBusMenuLayoutItem child{ 0471 Utils::treeStructureToInt(section.id, sectionId, ++count), 0472 gMenuToDBusMenuProperties(item), 0473 {} // children 0474 }; 0475 dbusItem.children.append(child); 0476 0477 // Now resolve section aliases 0478 auto it = item.constFind(QStringLiteral(":section")); 0479 if (it != item.constEnd()) { 0480 // references another place, add it instead 0481 GMenuSection gmenuSection = qdbus_cast<GMenuSection>(it->value<QDBusArgument>()); 0482 0483 // remember where the item came from and give it an appropriate ID 0484 // so updates signalled by the app will map to the right place 0485 int originalSubscription = gmenuSection.subscription; 0486 int originalMenu = gmenuSection.menu; 0487 0488 // TODO start subscription if we don't have it 0489 auto items = m_currentMenu->getSection(gmenuSection.subscription, gmenuSection.menu).items; 0490 0491 // Check whether it's an alias to an alias 0492 // FIXME make generic/recursive 0493 if (items.count() == 1) { 0494 const auto &aliasedItem = items.constFirst(); 0495 auto findIt = aliasedItem.constFind(QStringLiteral(":section")); 0496 if (findIt != aliasedItem.constEnd()) { 0497 GMenuSection gmenuSection2 = qdbus_cast<GMenuSection>(findIt->value<QDBusArgument>()); 0498 items = m_currentMenu->getSection(gmenuSection2.subscription, gmenuSection2.menu).items; 0499 0500 originalSubscription = gmenuSection2.subscription; 0501 originalMenu = gmenuSection2.menu; 0502 } 0503 } 0504 0505 int aliasedCount = 0; 0506 for (const auto &aliasedItem : std::as_const(items)) { 0507 DBusMenuLayoutItem aliasedChild{ 0508 Utils::treeStructureToInt(originalSubscription, originalMenu, ++aliasedCount), 0509 gMenuToDBusMenuProperties(aliasedItem), 0510 {} // children 0511 }; 0512 dbusItem.children.append(aliasedChild); 0513 } 0514 } 0515 } 0516 0517 // revision, unused in libdbusmenuqt 0518 return 1; 0519 } 0520 0521 QDBusVariant Window::GetProperty(int id, const QString &property) 0522 { 0523 Q_UNUSED(id); 0524 Q_UNUSED(property); 0525 QDBusVariant value; 0526 return value; 0527 } 0528 0529 QString Window::status() const 0530 { 0531 return QStringLiteral("normal"); 0532 } 0533 0534 uint Window::version() const 0535 { 0536 return 4; 0537 } 0538 0539 QVariantMap Window::gMenuToDBusMenuProperties(const QVariantMap &source) const 0540 { 0541 QVariantMap result; 0542 0543 result.insert(QStringLiteral("label"), source.value(QStringLiteral("label")).toString()); 0544 0545 if (source.contains(QLatin1String(":section"))) { 0546 result.insert(QStringLiteral("type"), QStringLiteral("separator")); 0547 } 0548 0549 const bool isMenu = source.contains(QLatin1String(":submenu")); 0550 if (isMenu) { 0551 result.insert(QStringLiteral("children-display"), QStringLiteral("submenu")); 0552 } 0553 0554 QString accel = source.value(QStringLiteral("accel")).toString(); 0555 if (!accel.isEmpty()) { 0556 QStringList shortcut; 0557 0558 // TODO use regexp or something 0559 if (accel.contains(QLatin1String("<Primary>")) || accel.contains(QLatin1String("<Control>"))) { 0560 shortcut.append(QStringLiteral("Control")); 0561 accel.remove(QLatin1String("<Primary>")); 0562 accel.remove(QLatin1String("<Control>")); 0563 } 0564 0565 if (accel.contains(QLatin1String("<Shift>"))) { 0566 shortcut.append(QStringLiteral("Shift")); 0567 accel.remove(QLatin1String("<Shift>")); 0568 } 0569 0570 if (accel.contains(QLatin1String("<Alt>"))) { 0571 shortcut.append(QStringLiteral("Alt")); 0572 accel.remove(QLatin1String("<Alt>")); 0573 } 0574 0575 if (accel.contains(QLatin1String("<Super>"))) { 0576 shortcut.append(QStringLiteral("Super")); 0577 accel.remove(QLatin1String("<Super>")); 0578 } 0579 0580 if (!accel.isEmpty()) { 0581 // TODO replace "+" by "plus" and "-" by "minus" 0582 shortcut.append(accel); 0583 0584 // TODO does gmenu support multiple? 0585 DBusMenuShortcut dbusShortcut; 0586 dbusShortcut.append(shortcut); // don't let it unwrap the list we append 0587 0588 result.insert(QStringLiteral("shortcut"), QVariant::fromValue(std::move(dbusShortcut))); 0589 } 0590 } 0591 0592 bool enabled = true; 0593 0594 const QString actionName = Utils::itemActionName(source); 0595 0596 GMenuAction action; 0597 // if no action is specified this is fine but if there is an action we don't have 0598 // disable the menu entry 0599 bool actionOk = true; 0600 if (!actionName.isEmpty()) { 0601 actionOk = getAction(actionName, action); 0602 enabled = actionOk && action.enabled; 0603 } 0604 0605 // we used to only send this if not enabled but then dbusmenuimporter does not 0606 // update the enabled state when it changes from disabled to enabled 0607 result.insert(QStringLiteral("enabled"), enabled); 0608 0609 bool visible = true; 0610 const QString hiddenWhen = source.value(QStringLiteral("hidden-when")).toString(); 0611 if (hiddenWhen == QLatin1String("action-disabled") && (!actionOk || !enabled)) { 0612 visible = false; 0613 } else if (hiddenWhen == QLatin1String("action-missing") && !actionOk) { 0614 visible = false; 0615 // While we have Global Menu we don't have macOS menu (where Quit, Help, etc is separate) 0616 } else if (hiddenWhen == QLatin1String("macos-menubar")) { 0617 visible = true; 0618 } 0619 0620 result.insert(QStringLiteral("visible"), visible); 0621 0622 QString icon = source.value(QStringLiteral("icon")).toString(); 0623 if (icon.isEmpty()) { 0624 icon = source.value(QStringLiteral("verb-icon")).toString(); 0625 } 0626 0627 icon = Icons::actionIcon(actionName); 0628 if (!icon.isEmpty()) { 0629 result.insert(QStringLiteral("icon-name"), icon); 0630 } 0631 0632 const QVariant target = source.value(QStringLiteral("target")); 0633 0634 if (actionOk) { 0635 const auto actionStates = action.state; 0636 if (actionStates.count() == 1) { 0637 const auto &actionState = actionStates.first(); 0638 // assume this is a checkbox 0639 if (!isMenu) { 0640 if (actionState.typeId() == QMetaType::Bool) { 0641 result.insert(QStringLiteral("toggle-type"), QStringLiteral("checkbox")); 0642 result.insert(QStringLiteral("toggle-state"), actionState.toBool() ? 1 : 0); 0643 } else if (actionState.typeId() == QMetaType::QString) { 0644 result.insert(QStringLiteral("toggle-type"), QStringLiteral("radio")); 0645 result.insert(QStringLiteral("toggle-state"), actionState == target ? 1 : 0); 0646 } 0647 } 0648 } 0649 } 0650 0651 return result; 0652 }