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 §ion) { 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 §ion : 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 }