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