/* SPDX-FileCopyrightText: 2018 Kai Uwe Broulik SPDX-License-Identifier: LGPL-2.1-or-later */ #include "menu.h" #include "debug.h" #include #include #include #include #include #include #include "utils.h" Menu::Menu(const QString &serviceName, const QString &objectPath, QObject *parent) : QObject(parent) , m_interface(new OrgGtkMenusInterface(serviceName, objectPath, QDBusConnection::sessionBus(), this)) { connect(m_interface.get(), &OrgGtkMenusInterface::Changed, this, &Menu::onMenuChanged); } Menu::~Menu() = default; void Menu::cleanup() { stop(m_subscriptions); } void Menu::start(uint id) { if (m_subscriptions.contains(id)) { return; } // TODO watch service disappearing? // dbus-send --print-reply --session --dest=:1.103 /org/libreoffice/window/104857641/menus/menubar org.gtk.Menus.Start array:uint32:0 QDBusPendingReply reply = m_interface->Start(QList{id}); auto *watcher = new QDBusPendingCallWatcher(reply, this); connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id](QDBusPendingCallWatcher *watcher) { QScopedPointer watcherPtr(watcher); QDBusPendingReply reply = *watcherPtr; if (reply.isError()) { qCWarning(DBUSMENUPROXY) << "Failed to start subscription to" << id << "on" << m_interface->service() << "at" << m_interface->path() << reply.error(); Q_EMIT failedToSubscribe(id); } else { const bool hadMenu = !m_menus.isEmpty(); const auto menus = reply.value(); for (const auto &menu : menus) { m_menus[menu.id].append(menus); } // LibreOffice on startup fails to give us some menus right away, we'll also subscribe in onMenuChanged() if necessary if (menus.isEmpty()) { qCWarning(DBUSMENUPROXY) << "Got an empty menu for" << id << "on" << m_interface->service() << "at" << m_interface->path(); return; } // TODO are we subscribed to all it returns or just to the ones we requested? m_subscriptions.append(id); // do we have a menu now? let's tell everyone if (!hadMenu && !m_menus.isEmpty()) { Q_EMIT menuAppeared(); } Q_EMIT subscribed(id); } }); } void Menu::stop(const QList &ids) { QDBusPendingReply reply = m_interface->End(ids); auto *watcher = new QDBusPendingCallWatcher(reply, this); connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, ids](QDBusPendingCallWatcher *watcher) { QDBusPendingReply reply = *watcher; if (reply.isError()) { qCWarning(DBUSMENUPROXY) << "Failed to stop subscription to" << ids << "on" << m_interface->service() << "at" << m_interface->path() << reply.error(); } else { // remove all subscriptions that we unsubscribed from // TODO is there a nicer algorithm for that? // TODO remove all m_menus also? m_subscriptions.erase( std::remove_if(m_subscriptions.begin(), m_subscriptions.end(), std::bind(&QList::contains, m_subscriptions, std::placeholders::_1)), m_subscriptions.end()); if (m_subscriptions.isEmpty()) { Q_EMIT menuDisappeared(); } } watcher->deleteLater(); }); } bool Menu::hasMenu() const { return !m_menus.isEmpty(); } bool Menu::hasSubscription(uint subscription) const { return m_subscriptions.contains(subscription); } GMenuItem Menu::getSection(int id, bool *ok) const { int subscription; int section; int index; Utils::intToTreeStructure(id, subscription, section, index); return getSection(subscription, section, ok); } GMenuItem Menu::getSection(int subscription, int section, bool *ok) const { const auto menu = m_menus.value(subscription); auto it = std::find_if(menu.begin(), menu.end(), [section](const GMenuItem &item) { return static_cast(item.section) == section; }); if (it == menu.end()) { if (ok) { *ok = false; } return GMenuItem(); } if (ok) { *ok = true; } return *it; } QVariantMap Menu::getItem(int id) const { int subscription; int section; int index; Utils::intToTreeStructure(id, subscription, section, index); return getItem(subscription, section, index); } QVariantMap Menu::getItem(int subscription, int sectionId, int index) const { bool ok; const GMenuItem section = getSection(subscription, sectionId, &ok); if (!ok) { return QVariantMap(); } const auto items = section.items; if (items.count() < index) { qCWarning(DBUSMENUPROXY) << "Cannot get action" << subscription << sectionId << index << "which is out of bounds"; return QVariantMap(); } // 0 is the menu itself, items start at 1 return items.at(index - 1); } void Menu::onMenuChanged(const GMenuChangeList &changes) { const bool hadMenu = !m_menus.isEmpty(); QList dirtyMenus; QList dirtyItems; for (const auto &change : changes) { auto updateSection = [&](GMenuItem §ion) { // Check if the amount of inserted items is identical to the items to be removed, // just update the existing items and signal a change for that. // LibreOffice tends to do that e.g. to update its Undo menu entry if (change.itemsToRemoveCount == static_cast(change.itemsToInsert.count())) { for (int i = 0; i < change.itemsToInsert.count(); ++i) { const auto &newItem = change.itemsToInsert.at(i); section.items[change.changePosition + i] = newItem; // 0 is the menu itself, items start at 1 dirtyItems.append(Utils::treeStructureToInt(change.subscription, change.menu, change.changePosition + i + 1)); } } else { for (uint i = 0; i < change.itemsToRemoveCount; ++i) { section.items.removeAt(change.changePosition); // TODO bounds check } for (int i = 0; i < change.itemsToInsert.count(); ++i) { section.items.insert(change.changePosition + i, change.itemsToInsert.at(i)); } dirtyMenus.append(Utils::treeStructureToInt(change.subscription, change.menu, 0)); } }; // shouldn't happen, it says only Start() subscribes to changes if (!m_subscriptions.contains(change.subscription)) { qCDebug(DBUSMENUPROXY) << "Got menu change for menu" << change.subscription << "that we are not subscribed to, subscribing now"; // LibreOffice doesn't give us a menu right away but takes a while and then signals us a change start(change.subscription); continue; } auto &menu = m_menus[change.subscription]; bool sectionFound = false; // TODO findSectionRef for (GMenuItem §ion : menu) { if (section.section != change.menu) { continue; } qCInfo(DBUSMENUPROXY) << "Updating existing section" << change.menu << "in subscription" << change.subscription; sectionFound = true; updateSection(section); break; } // Insert new section if (!sectionFound) { qCInfo(DBUSMENUPROXY) << "Creating new section" << change.menu << "in subscription" << change.subscription; if (change.itemsToRemoveCount > 0) { qCWarning(DBUSMENUPROXY) << "Menu change requested to remove items from a new (and as such empty) section"; } GMenuItem newSection; newSection.id = change.subscription; newSection.section = change.menu; updateSection(newSection); menu.append(newSection); } } // do we have a menu now? let's tell everyone if (!hadMenu && !m_menus.isEmpty()) { Q_EMIT menuAppeared(); } else if (hadMenu && m_menus.isEmpty()) { Q_EMIT menuDisappeared(); } if (!dirtyItems.isEmpty()) { Q_EMIT itemsChanged(dirtyItems); } Q_EMIT menusChanged(dirtyMenus); } void Menu::actionsChanged(const QStringList &dirtyActions, const QString &prefix) { auto forEachMenuItem = [this](const std::function &cb) { for (auto it = m_menus.constBegin(), end = m_menus.constEnd(); it != end; ++it) { const int subscription = it.key(); for (const auto &menu : it.value()) { const int section = menu.section; int count = 0; const auto items = menu.items; for (const auto &item : items) { ++count; // 0 is a menu, entries start at 1 if (!cb(subscription, section, count, item)) { goto loopExit; // hell yeah break; } } } } loopExit: // loop exit return; }; // now find in which menus these actions are and Q_EMIT a change accordingly QList dirtyItems; for (const QString &action : dirtyActions) { const QString prefixedAction = prefix + action; forEachMenuItem([&prefixedAction, &dirtyItems](int subscription, int section, int index, const QVariantMap &item) { const QString actionName = Utils::itemActionName(item); if (actionName == prefixedAction) { dirtyItems.append(Utils::treeStructureToInt(subscription, section, index)); return false; // break } return true; // continue }); } if (!dirtyItems.isEmpty()) { Q_EMIT itemsChanged(dirtyItems); } }