File indexing completed on 2024-05-19 09:30:38

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 "menu.h"
0008 
0009 #include "debug.h"
0010 
0011 #include <QDBusConnection>
0012 #include <QDBusPendingCallWatcher>
0013 #include <QDBusPendingReply>
0014 #include <QDebug>
0015 #include <QVariantList>
0016 
0017 #include <algorithm>
0018 
0019 #include "utils.h"
0020 
0021 Menu::Menu(const QString &serviceName, const QString &objectPath, QObject *parent)
0022     : QObject(parent)
0023     , m_interface(new OrgGtkMenusInterface(serviceName, objectPath, QDBusConnection::sessionBus(), this))
0024 {
0025     connect(m_interface.get(), &OrgGtkMenusInterface::Changed, this, &Menu::onMenuChanged);
0026 }
0027 
0028 Menu::~Menu() = default;
0029 
0030 void Menu::cleanup()
0031 {
0032     stop(m_subscriptions);
0033 }
0034 
0035 void Menu::start(uint id)
0036 {
0037     if (m_subscriptions.contains(id)) {
0038         return;
0039     }
0040 
0041     // TODO watch service disappearing?
0042 
0043     // dbus-send --print-reply --session --dest=:1.103 /org/libreoffice/window/104857641/menus/menubar org.gtk.Menus.Start array:uint32:0
0044 
0045     QDBusPendingReply<GMenuItemList> reply = m_interface->Start(QList<uint>{id});
0046     auto *watcher = new QDBusPendingCallWatcher(reply, this);
0047     connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id](QDBusPendingCallWatcher *watcher) {
0048         QScopedPointer<QDBusPendingCallWatcher, QScopedPointerDeleteLater> watcherPtr(watcher);
0049 
0050         QDBusPendingReply<GMenuItemList> reply = *watcherPtr;
0051         if (reply.isError()) {
0052             qCWarning(DBUSMENUPROXY) << "Failed to start subscription to" << id << "on" << m_interface->service() << "at" << m_interface->path()
0053                                      << reply.error();
0054             Q_EMIT failedToSubscribe(id);
0055         } else {
0056             const bool hadMenu = !m_menus.isEmpty();
0057 
0058             const auto menus = reply.value();
0059             for (const auto &menu : menus) {
0060                 m_menus[menu.id].append(menus);
0061             }
0062 
0063             // LibreOffice on startup fails to give us some menus right away, we'll also subscribe in onMenuChanged() if necessary
0064             if (menus.isEmpty()) {
0065                 qCWarning(DBUSMENUPROXY) << "Got an empty menu for" << id << "on" << m_interface->service() << "at" << m_interface->path();
0066                 return;
0067             }
0068 
0069             // TODO are we subscribed to all it returns or just to the ones we requested?
0070             m_subscriptions.append(id);
0071 
0072             // do we have a menu now? let's tell everyone
0073             if (!hadMenu && !m_menus.isEmpty()) {
0074                 Q_EMIT menuAppeared();
0075             }
0076 
0077             Q_EMIT subscribed(id);
0078         }
0079     });
0080 }
0081 
0082 void Menu::stop(const QList<uint> &ids)
0083 {
0084     QDBusPendingReply<void> reply = m_interface->End(ids);
0085     auto *watcher = new QDBusPendingCallWatcher(reply, this);
0086     connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, ids](QDBusPendingCallWatcher *watcher) {
0087         QDBusPendingReply<void> reply = *watcher;
0088         if (reply.isError()) {
0089             qCWarning(DBUSMENUPROXY) << "Failed to stop subscription to" << ids << "on" << m_interface->service() << "at" << m_interface->path()
0090                                      << reply.error();
0091         } else {
0092             // remove all subscriptions that we unsubscribed from
0093             // TODO is there a nicer algorithm for that?
0094             // TODO remove all m_menus also?
0095             m_subscriptions.erase(
0096                 std::remove_if(m_subscriptions.begin(), m_subscriptions.end(), std::bind(&QList<uint>::contains<uint>, m_subscriptions, std::placeholders::_1)),
0097                 m_subscriptions.end());
0098 
0099             if (m_subscriptions.isEmpty()) {
0100                 Q_EMIT menuDisappeared();
0101             }
0102         }
0103         watcher->deleteLater();
0104     });
0105 }
0106 
0107 bool Menu::hasMenu() const
0108 {
0109     return !m_menus.isEmpty();
0110 }
0111 
0112 bool Menu::hasSubscription(uint subscription) const
0113 {
0114     return m_subscriptions.contains(subscription);
0115 }
0116 
0117 GMenuItem Menu::getSection(int id, bool *ok) const
0118 {
0119     int subscription;
0120     int section;
0121     int index;
0122     Utils::intToTreeStructure(id, subscription, section, index);
0123     return getSection(subscription, section, ok);
0124 }
0125 
0126 GMenuItem Menu::getSection(int subscription, int section, bool *ok) const
0127 {
0128     const auto menu = m_menus.value(subscription);
0129 
0130     auto it = std::find_if(menu.begin(), menu.end(), [section](const GMenuItem &item) {
0131         return static_cast<int>(item.section) == section;
0132     });
0133 
0134     if (it == menu.end()) {
0135         if (ok) {
0136             *ok = false;
0137         }
0138         return GMenuItem();
0139     }
0140 
0141     if (ok) {
0142         *ok = true;
0143     }
0144     return *it;
0145 }
0146 
0147 QVariantMap Menu::getItem(int id) const
0148 {
0149     int subscription;
0150     int section;
0151     int index;
0152     Utils::intToTreeStructure(id, subscription, section, index);
0153     return getItem(subscription, section, index);
0154 }
0155 
0156 QVariantMap Menu::getItem(int subscription, int sectionId, int index) const
0157 {
0158     bool ok;
0159     const GMenuItem section = getSection(subscription, sectionId, &ok);
0160 
0161     if (!ok) {
0162         return QVariantMap();
0163     }
0164 
0165     const auto items = section.items;
0166 
0167     if (items.count() < index) {
0168         qCWarning(DBUSMENUPROXY) << "Cannot get action" << subscription << sectionId << index << "which is out of bounds";
0169         return QVariantMap();
0170     }
0171 
0172     // 0 is the menu itself, items start at 1
0173     return items.at(index - 1);
0174 }
0175 
0176 void Menu::onMenuChanged(const GMenuChangeList &changes)
0177 {
0178     const bool hadMenu = !m_menus.isEmpty();
0179 
0180     QList<uint> dirtyMenus;
0181     QList<uint> dirtyItems;
0182 
0183     for (const auto &change : changes) {
0184         auto updateSection = [&](GMenuItem &section) {
0185             // Check if the amount of inserted items is identical to the items to be removed,
0186             // just update the existing items and signal a change for that.
0187             // LibreOffice tends to do that e.g. to update its Undo menu entry
0188             if (change.itemsToRemoveCount == static_cast<uint>(change.itemsToInsert.count())) {
0189                 for (int i = 0; i < change.itemsToInsert.count(); ++i) {
0190                     const auto &newItem = change.itemsToInsert.at(i);
0191 
0192                     section.items[change.changePosition + i] = newItem;
0193 
0194                     // 0 is the menu itself, items start at 1
0195                     dirtyItems.append(Utils::treeStructureToInt(change.subscription, change.menu, change.changePosition + i + 1));
0196                 }
0197             } else {
0198                 for (uint i = 0; i < change.itemsToRemoveCount; ++i) {
0199                     section.items.removeAt(change.changePosition); // TODO bounds check
0200                 }
0201 
0202                 for (int i = 0; i < change.itemsToInsert.count(); ++i) {
0203                     section.items.insert(change.changePosition + i, change.itemsToInsert.at(i));
0204                 }
0205 
0206                 dirtyMenus.append(Utils::treeStructureToInt(change.subscription, change.menu, 0));
0207             }
0208         };
0209 
0210         // shouldn't happen, it says only Start() subscribes to changes
0211         if (!m_subscriptions.contains(change.subscription)) {
0212             qCDebug(DBUSMENUPROXY) << "Got menu change for menu" << change.subscription << "that we are not subscribed to, subscribing now";
0213             // LibreOffice doesn't give us a menu right away but takes a while and then signals us a change
0214             start(change.subscription);
0215             continue;
0216         }
0217 
0218         auto &menu = m_menus[change.subscription];
0219 
0220         bool sectionFound = false;
0221         // TODO findSectionRef
0222         for (GMenuItem &section : menu) {
0223             if (section.section != change.menu) {
0224                 continue;
0225             }
0226 
0227             qCInfo(DBUSMENUPROXY) << "Updating existing section" << change.menu << "in subscription" << change.subscription;
0228 
0229             sectionFound = true;
0230             updateSection(section);
0231             break;
0232         }
0233 
0234         // Insert new section
0235         if (!sectionFound) {
0236             qCInfo(DBUSMENUPROXY) << "Creating new section" << change.menu << "in subscription" << change.subscription;
0237 
0238             if (change.itemsToRemoveCount > 0) {
0239                 qCWarning(DBUSMENUPROXY) << "Menu change requested to remove items from a new (and as such empty) section";
0240             }
0241 
0242             GMenuItem newSection;
0243             newSection.id = change.subscription;
0244             newSection.section = change.menu;
0245             updateSection(newSection);
0246             menu.append(newSection);
0247         }
0248     }
0249 
0250     // do we have a menu now? let's tell everyone
0251     if (!hadMenu && !m_menus.isEmpty()) {
0252         Q_EMIT menuAppeared();
0253     } else if (hadMenu && m_menus.isEmpty()) {
0254         Q_EMIT menuDisappeared();
0255     }
0256 
0257     if (!dirtyItems.isEmpty()) {
0258         Q_EMIT itemsChanged(dirtyItems);
0259     }
0260 
0261     Q_EMIT menusChanged(dirtyMenus);
0262 }
0263 
0264 void Menu::actionsChanged(const QStringList &dirtyActions, const QString &prefix)
0265 {
0266     auto forEachMenuItem = [this](const std::function<bool(int subscription, int section, int index, const QVariantMap &item)> &cb) {
0267         for (auto it = m_menus.constBegin(), end = m_menus.constEnd(); it != end; ++it) {
0268             const int subscription = it.key();
0269 
0270             for (const auto &menu : it.value()) {
0271                 const int section = menu.section;
0272 
0273                 int count = 0;
0274 
0275                 const auto items = menu.items;
0276                 for (const auto &item : items) {
0277                     ++count; // 0 is a menu, entries start at 1
0278 
0279                     if (!cb(subscription, section, count, item)) {
0280                         goto loopExit; // hell yeah
0281                         break;
0282                     }
0283                 }
0284             }
0285         }
0286 
0287     loopExit: // loop exit
0288         return;
0289     };
0290 
0291     // now find in which menus these actions are and Q_EMIT a change accordingly
0292     QList<uint> dirtyItems;
0293 
0294     for (const QString &action : dirtyActions) {
0295         const QString prefixedAction = prefix + action;
0296 
0297         forEachMenuItem([&prefixedAction, &dirtyItems](int subscription, int section, int index, const QVariantMap &item) {
0298             const QString actionName = Utils::itemActionName(item);
0299 
0300             if (actionName == prefixedAction) {
0301                 dirtyItems.append(Utils::treeStructureToInt(subscription, section, index));
0302                 return false; // break
0303             }
0304 
0305             return true; // continue
0306         });
0307     }
0308 
0309     if (!dirtyItems.isEmpty()) {
0310         Q_EMIT itemsChanged(dirtyItems);
0311     }
0312 }