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