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 }