diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -166,6 +166,8 @@ ecm_optional_add_subdirectory(xembed-sni-proxy) +ecm_optional_add_subdirectory(gmenu-dbusmenu-proxy) + add_subdirectory(soliduiserver) if(KF5Holidays_FOUND) diff --git a/gmenu-dbusmenu-proxy/CMakeLists.txt b/gmenu-dbusmenu-proxy/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/gmenu-dbusmenu-proxy/CMakeLists.txt @@ -0,0 +1,41 @@ +add_definitions(-DQT_NO_CAST_TO_ASCII +-DQT_NO_CAST_FROM_ASCII +-DQT_NO_URL_CAST_FROM_STRING +-DQT_NO_CAST_FROM_BYTEARRAY) + +find_package(XCB + REQUIRED COMPONENTS + XCB +) + +set(GMENU_DBUSMENU_PROXY_SRCS + main.cpp + menuproxy.cpp + menu.cpp + gdbusmenutypes_p.cpp + ../libdbusmenuqt/dbusmenutypes_p.cpp + ) + +qt5_add_dbus_adaptor(GMENU_DBUSMENU_PROXY_SRCS ../libdbusmenuqt/com.canonical.dbusmenu.xml menu.h Menu) + +ecm_qt_declare_logging_category(GMENU_DBUSMENU_PROXY_SRCS HEADER debug.h + IDENTIFIER DBUSMENUPROXY + CATEGORY_NAME kde.dbusmenuproxy + DEFAULT_SEVERITY Info) + +add_executable(gmenudbusmenuproxy ${GMENU_DBUSMENU_PROXY_SRCS}) + +set_package_properties(XCB PROPERTIES TYPE REQUIRED) + +target_link_libraries(gmenudbusmenuproxy + Qt5::Core + Qt5::X11Extras + Qt5::DBus + KF5::ConfigCore + KF5::WindowSystem + XCB::XCB +) + +install(TARGETS gmenudbusmenuproxy ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) +install(FILES gmenudbusmenuproxy.desktop DESTINATION ${KDE_INSTALL_AUTOSTARTDIR}) + diff --git a/gmenu-dbusmenu-proxy/gdbusmenutypes_p.h b/gmenu-dbusmenu-proxy/gdbusmenutypes_p.h new file mode 100644 --- /dev/null +++ b/gmenu-dbusmenu-proxy/gdbusmenutypes_p.h @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2018 Kai Uwe Broulik + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#pragma once + +#include +#include +#include +#include + +class QDBusArgument; + +// Various +using VariantMapList = QList; +Q_DECLARE_METATYPE(VariantMapList); + +using StringBoolMap = QMap; +Q_DECLARE_METATYPE(StringBoolMap); + +// Menu item itself (Start method) +struct GMenuItem +{ + uint id; + uint section; + VariantMapList items; +}; +Q_DECLARE_METATYPE(GMenuItem); + +QDBusArgument &operator<<(QDBusArgument &argument, const GMenuItem &item); +const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuItem &item); + +using GMenuItemList = QList; +Q_DECLARE_METATYPE(GMenuItemList); + +// Information about what section or submenu to use for a particular entry +struct GMenuSection +{ + uint subscription; + uint menu; +}; +Q_DECLARE_METATYPE(GMenuSection); + +QDBusArgument &operator<<(QDBusArgument &argument, const GMenuSection &item); +const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuSection &item); + +// Changes of a menu item (Changed signal) +struct GMenuChange +{ + uint subscription; + uint menu; + + uint changePosition; + uint itemsToRemoveCount; + VariantMapList itemsToInsert; +}; +Q_DECLARE_METATYPE(GMenuChange); + +QDBusArgument &operator<<(QDBusArgument &argument, const GMenuChange &item); +const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuChange &item); + +using GMenuChangeList = QList; +Q_DECLARE_METATYPE(GMenuChangeList); + +// An application action +struct GMenuAction +{ + bool enabled; + QDBusSignature signature; + QVariantList state; +}; +Q_DECLARE_METATYPE(GMenuAction); + +QDBusArgument &operator<<(QDBusArgument &argument, const GMenuAction &item); +const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuAction &item); + +using GMenuActionMap = QMap; +Q_DECLARE_METATYPE(GMenuActionMap); + +struct GMenuActionsChange +{ + QStringList removed; + QMap enabledChanged; + QVariantMap stateChanged; + GMenuActionMap added; +}; +Q_DECLARE_METATYPE(GMenuActionsChange); + +QDBusArgument &operator<<(QDBusArgument &argument, const GMenuActionsChange &item); +const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuActionsChange &item); + +void GDBusMenuTypes_register(); diff --git a/gmenu-dbusmenu-proxy/gdbusmenutypes_p.cpp b/gmenu-dbusmenu-proxy/gdbusmenutypes_p.cpp new file mode 100644 --- /dev/null +++ b/gmenu-dbusmenu-proxy/gdbusmenutypes_p.cpp @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2018 Kai Uwe Broulik + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "gdbusmenutypes_p.h" + +#include +#include + +// GMenuItem +QDBusArgument &operator<<(QDBusArgument &argument, const GMenuItem &item) +{ + argument.beginStructure(); + argument << item.id << item.section << item.items; + argument.endStructure(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuItem &item) +{ + argument.beginStructure(); + argument >> item.id >> item.section >> item.items; + argument.endStructure(); + return argument; +} + +// GMenuSection +QDBusArgument &operator<<(QDBusArgument &argument, const GMenuSection &item) +{ + argument.beginStructure(); + argument << item.subscription << item.menu; + argument.endStructure(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuSection &item) +{ + argument.beginStructure(); + argument >> item.subscription >> item.menu; + argument.endStructure(); + return argument; +} + +// GMenuChange +QDBusArgument &operator<<(QDBusArgument &argument, const GMenuChange &item) +{ + argument.beginStructure(); + argument << item.subscription << item.menu << item.changePosition << item.itemsToRemoveCount << item.itemsToInsert; + argument.endStructure(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuChange &item) +{ + argument.beginStructure(); + argument >> item.subscription >> item.menu >> item.changePosition >> item.itemsToRemoveCount >> item.itemsToInsert; + argument.endStructure(); + return argument; +} + +// GMenuActionProperty +QDBusArgument &operator<<(QDBusArgument &argument, const GMenuAction &item) +{ + argument.beginStructure(); + argument << item.enabled << item.signature << item.state; + argument.endStructure(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuAction &item) +{ + argument.beginStructure(); + argument >> item.enabled >> item.signature >> item.state; + argument.endStructure(); + return argument; +} + +// GMenuActionsChange +QDBusArgument &operator<<(QDBusArgument &argument, const GMenuActionsChange &item) +{ + argument.beginStructure(); + argument << item.removed << item.enabledChanged << item.stateChanged << item.added; + argument.endStructure(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuActionsChange &item) +{ + argument.beginStructure(); + argument >> item.removed >> item.enabledChanged >> item.stateChanged >> item.added; + argument.endStructure(); + return argument; +} + +void GDBusMenuTypes_register() +{ + static bool registered = false; + if (registered) { + return; + } + + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + qDBusRegisterMetaType(); + + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + registered = true; +} diff --git a/gmenu-dbusmenu-proxy/gmenudbusmenuproxy.desktop b/gmenu-dbusmenu-proxy/gmenudbusmenuproxy.desktop new file mode 100644 --- /dev/null +++ b/gmenu-dbusmenu-proxy/gmenudbusmenuproxy.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Exec=gmenudbusmenuproxy +Name=GMenuDBusMenuProxy +Type=Application +X-KDE-StartupNotify=false +OnlyShowIn=KDE; +X-KDE-autostart-phase=0 diff --git a/gmenu-dbusmenu-proxy/main.cpp b/gmenu-dbusmenu-proxy/main.cpp new file mode 100644 --- /dev/null +++ b/gmenu-dbusmenu-proxy/main.cpp @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2018 Kai Uwe Broulik + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include +#include + +#include + +#include "menuproxy.h" + +int main(int argc, char ** argv) +{ + qputenv("QT_QPA_PLATFORM", "xcb"); + + QGuiApplication::setDesktopSettingsAware(false); + + QGuiApplication app(argc, argv); + + if (!KWindowSystem::isPlatformX11()) { + qFatal("qdbusmenuproxy is only useful XCB. Aborting"); + } + + auto disableSessionManagement = [](QSessionManager &sm) { + sm.setRestartHint(QSessionManager::RestartNever); + }; + QObject::connect(&app, &QGuiApplication::commitDataRequest, disableSessionManagement); + QObject::connect(&app, &QGuiApplication::saveStateRequest, disableSessionManagement); + + app.setQuitOnLastWindowClosed(false); + + MenuProxy proxy; + + return app.exec(); +} diff --git a/gmenu-dbusmenu-proxy/menu.h b/gmenu-dbusmenu-proxy/menu.h new file mode 100644 --- /dev/null +++ b/gmenu-dbusmenu-proxy/menu.h @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2018 Kai Uwe Broulik + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#pragma once + +#include +#include +#include +#include +#include // for WId + +#include + +#include "gdbusmenutypes_p.h" +#include "../libdbusmenuqt/dbusmenutypes_p.h" + +class QDBusVariant; + +class Menu : public QObject, protected QDBusContext +{ + Q_OBJECT + + // DBus + Q_PROPERTY(QString Status READ status) + Q_PROPERTY(uint Version READ version) + +public: + Menu(WId winId, + const QString &serviceName, + const QString &applicationObjectPath, + const QString &windowObjectPath, + const QString &menuObjectPath); + ~Menu(); + + void cleanup(); + + WId winId() const; + QString serviceName() const; + + QString applicationObjectPath() const; + QString windowObjectPath() const; + QString menuObjectPath() const; + + QString proxyObjectPath() const; + + // DBus + bool AboutToShow(int id); + void Event(int id, const QString &eventId, const QDBusVariant &data, uint timestamp); + DBusMenuItemList GetGroupProperties(const QList &ids, const QStringList &propertyNames); + uint GetLayout(int parentId, int recursionDepth, const QStringList &propertyNames, DBusMenuLayoutItem &dbusItem); + QDBusVariant GetProperty(int id, const QString &property); + + QString status() const; + uint version() const; + +signals: + // don't want to pollute X stuff into Menu, let all of that be in MenuProxy + void requestWriteWindowProperties(); + void requestRemoveWindowProperties(); + + // DBus + void ItemActivationRequested(int id, uint timestamp); + void ItemsPropertiesUpdated(const DBusMenuItemList &updatedProps, const DBusMenuItemKeysList &removedProps); + void LayoutUpdated(uint revision, int parent); + +private slots: + void onMenuChanged(const GMenuChangeList &changes); + void onApplicationActionsChanged(const QStringList &removed, const StringBoolMap &enabledChanges, const QVariantMap &stateChanges, const GMenuActionMap &added); + void onWindowActionsChanged(const QStringList &removed, const StringBoolMap &enabledChanges, const QVariantMap &stateChanges, const GMenuActionMap &added); + +private: + void init(); + void start(uint id); + void stop(const QList &id); + + bool registerDBusObject(); + + void getActions(const QString &path, const std::function &cb); + bool getAction(const QString &name, GMenuAction &action) const; + void triggerAction(const QString &name, uint timestamp = 0); + + void actionsChanged(const QStringList &removed, const StringBoolMap &enabledChanges, const QVariantMap &stateChanges, const GMenuActionMap &added, + GMenuActionMap &actions, const QString &prefix); + + static int treeStructureToInt(int subscription, int section, int index); + static void intToTreeStructure(int source, int &subscription, int §ion, int &index); + + static QString actionNameOfItem(const QVariantMap &item); + + static GMenuItem findSection(const QList &list, int section); + + QVariantMap gMenuToDBusMenuProperties(const QVariantMap &source) const; + + WId m_winId; + QString m_serviceName; // original GMenu service (the gtk app) + + QString m_applicationObjectPath; + QString m_windowObjectPath; + QString m_menuObjectPath; + + QString m_proxyObjectPath; // our object path on this proxy app + + // QSet? + QList m_subscriptions; // keeps track of which menu trees we're subscribed to + + QHash m_menus; + + QHash m_pendingGetLayouts; + + bool m_queriedApplicationActions = false; + GMenuActionMap m_applicationActions; + bool m_queriedWindowActions = false; + GMenuActionMap m_windowActions; + +}; diff --git a/gmenu-dbusmenu-proxy/menu.cpp b/gmenu-dbusmenu-proxy/menu.cpp new file mode 100644 --- /dev/null +++ b/gmenu-dbusmenu-proxy/menu.cpp @@ -0,0 +1,894 @@ +/* + * Copyright (C) 2018 Kai Uwe Broulik + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "menu.h" + +#include "debug.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "dbusmenuadaptor.h" + +#include "../libdbusmenuqt/dbusmenushortcut_p.h" + +static const QString s_orgGtkActions = QStringLiteral("org.gtk.Actions"); +static const QString s_orgGtkMenus = QStringLiteral("org.gtk.Menus"); + +Menu::Menu(WId winId, + const QString &serviceName, + const QString &applicationObjectPath, + const QString &windowObjectPath, + const QString &menuObjectPath) + : QObject() + , m_winId(winId) + , m_serviceName(serviceName) + , m_applicationObjectPath(applicationObjectPath) + , m_windowObjectPath(windowObjectPath) + , m_menuObjectPath(menuObjectPath) +{ + qCDebug(DBUSMENUPROXY) << "Created menu for" << m_winId << "on" << m_serviceName << "at app" << m_applicationObjectPath << "win" << m_windowObjectPath << "menu" << m_menuObjectPath; + + GDBusMenuTypes_register(); + DBusMenuTypes_register(); + + if (!QDBusConnection::sessionBus().connect(m_serviceName, + m_menuObjectPath, + s_orgGtkMenus, + QStringLiteral("Changed"), + this, + SLOT(onMenuChanged(GMenuChangeList)))) { + qCWarning(DBUSMENUPROXY) << "Failed to subscribe to menu changes on" << m_serviceName << "at" << m_menuObjectPath; + } + + if (!QDBusConnection::sessionBus().connect(m_serviceName, + m_applicationObjectPath, + s_orgGtkActions, + QStringLiteral("Changed"), + this, + SLOT(onApplicationActionsChanged(QStringList,StringBoolMap,QVariantMap,GMenuActionMap)))) { + qCWarning(DBUSMENUPROXY) << "Failed to subscribe to application action changes on" << m_serviceName << "at" << m_applicationObjectPath; + } + + if (!QDBusConnection::sessionBus().connect(m_serviceName, + m_windowObjectPath, + s_orgGtkActions, + QStringLiteral("Changed"), + this, + SLOT(onWindowActionsChanged(QStringList,StringBoolMap,QVariantMap,GMenuActionMap)))) { + qCWarning(DBUSMENUPROXY) << "Failed to subscribe to window action changes on" << m_serviceName << "at" << m_windowObjectPath; + } + + // TODO share application actions between menus of the same app? + getActions(m_applicationObjectPath, [this](const GMenuActionMap &actions, bool ok) { + if (ok) { + m_applicationActions = actions; + m_queriedApplicationActions = true; + init(); + } + }); + + getActions(m_windowObjectPath, [this](const GMenuActionMap &actions, bool ok) { + if (ok) { + m_windowActions = actions; + m_queriedWindowActions = true; + init(); + } + }); +} + +Menu::~Menu() = default; + +void Menu::cleanup() +{ + stop(m_subscriptions); + + emit requestRemoveWindowProperties(); +} + +WId Menu::winId() const +{ + return m_winId; +} + +QString Menu::serviceName() const +{ + return m_serviceName; +} + +QString Menu::applicationObjectPath() const +{ + return m_applicationObjectPath; +} + +QString Menu::windowObjectPath() const +{ + return m_windowObjectPath; +} + +QString Menu::menuObjectPath() const +{ + return m_menuObjectPath; +} + +QString Menu::proxyObjectPath() const +{ + return m_proxyObjectPath; +} + +void Menu::init() +{ + if (!m_queriedApplicationActions || m_queriedWindowActions) { + return; + } + + if (!registerDBusObject()) { + return; + } + + emit requestWriteWindowProperties(); +} + +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 + + QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName, + m_menuObjectPath, + s_orgGtkMenus, + QStringLiteral("Start")); + msg.setArguments({ + QVariant::fromValue(QList{id}) + }); + + QDBusPendingReply reply = QDBusConnection::sessionBus().asyncCall(msg); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply reply = *watcher; + if (reply.isError()) { + qCWarning(DBUSMENUPROXY) << "Failed to start subscription to" << id << "on" << m_serviceName << "at" << m_menuObjectPath << reply.error(); + } else { + const auto menus = reply.value(); + for (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 neccessary + if (menus.isEmpty()) { + qCWarning(DBUSMENUPROXY) << "Got an empty menu for" << id << "on" << m_serviceName << "at" << m_menuObjectPath; + return; + } + + // Now resolve any aliases we might have + // TODO Don't do this every time? ugly.. + for (auto &menu : m_menus) { + for (auto §ion : menu) { + QMutableListIterator it(section.items); + while (it.hasNext()) { + auto &item = it.next(); + + auto findIt = item.constFind(QStringLiteral(":section")); + if (findIt != item.constEnd()) { + // references another place, add it instead + GMenuSection gmenuSection = qdbus_cast(findIt->value()); + + // TODO figure out what to do when menu changed since we'd end up shifting the indices around here + if (item.value(QStringLiteral("section-expanded")).toBool()) { + continue; + } + + // TODO start subscription if we don't have it + auto items = findSection(m_menus.value(gmenuSection.subscription), gmenuSection.menu).items; + + // remember that this section was already expanded, we'll keep the reference around + // so we can show a separator line in the menu + item.insert(QStringLiteral("section-expanded"), true); + + // Check whether it's an alias to an alias + // FIXME make generic/recursive + if (items.count() == 1) { + const auto &aliasedItem = items.constFirst(); + auto findIt = aliasedItem.constFind(QStringLiteral(":section")); + if (findIt != aliasedItem.constEnd()) { + GMenuSection gmenuSection2 = qdbus_cast(findIt->value()); + items = findSection(m_menus.value(gmenuSection2.subscription), gmenuSection2.menu).items; + } + } + + for (const auto &aliasedItem : qAsConst(items)) { + it.insert(aliasedItem); + } + } + } + } + } + + // TODO are we subscribed to all it returns or just to the ones we requested? + m_subscriptions.append(id); + } + + // When it was a delayed GetLayout request, send the reply now + auto pendingReply = m_pendingGetLayouts.value(id); + if (pendingReply.type() != QDBusMessage::InvalidMessage) { + auto reply = pendingReply.createReply(); + + DBusMenuLayoutItem item; + uint revision = GetLayout(treeStructureToInt(id, 0, 0), 0, {}, item); + + reply << revision << QVariant::fromValue(item); + + QDBusConnection::sessionBus().send(reply); + } else { + emit LayoutUpdated(2 /*revision*/, id); + } + + watcher->deleteLater(); + }); +} + +void Menu::stop(const QList &ids) +{ + QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName, + m_menuObjectPath, + s_orgGtkMenus, + QStringLiteral("End")); + msg.setArguments({ + QVariant::fromValue(ids) // don't let it unwrap it, hence in a variant + }); + + QDBusPendingReply reply = QDBusConnection::sessionBus().asyncCall(msg); + QDBusPendingCallWatcher *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_serviceName << "at" << m_menuObjectPath << reply.error(); + } else { + // remove all subscriptions that we unsubscribed from + // TODO is there a nicer algorithm for that? + m_subscriptions.erase(std::remove_if(m_subscriptions.begin(), m_subscriptions.end(), + std::bind(&QList::contains, m_subscriptions, std::placeholders::_1)), + m_subscriptions.end()); + } + }); +} + +void Menu::onMenuChanged(const GMenuChangeList &changes) +{ + QSet dirtyMenus; + + for (const auto &change : changes) { + // 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]; + + // TODO findSectionRef + for (GMenuItem §ion : menu) { + if (section.section != change.menu) { + continue; + } + + for (int 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.insert(treeStructureToInt(change.subscription, change.menu, 0)); + break; + } + } + + for (uint menu : dirtyMenus) { + emit LayoutUpdated(2 /*revision*/, menu); + } +} + +void Menu::onApplicationActionsChanged(const QStringList &removed, const StringBoolMap &enabledChanges, const QVariantMap &stateChanges, const GMenuActionMap &added) +{ + if (!m_queriedApplicationActions) { + return; + } + actionsChanged(removed, enabledChanges, stateChanges, added, m_applicationActions, QStringLiteral("app.")); +} + +void Menu::onWindowActionsChanged(const QStringList &removed, const StringBoolMap &enabledChanges, const QVariantMap &stateChanges, const GMenuActionMap &added) +{ + if (!m_queriedWindowActions) { + return; + } + actionsChanged(removed, enabledChanges, stateChanges, added, m_windowActions, QStringLiteral("win.")); +} + +void Menu::getActions(const QString &path, const std::function &cb) +{ + QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName, + path, + s_orgGtkActions, + QStringLiteral("DescribeAll")); + + QDBusPendingReply reply = QDBusConnection::sessionBus().asyncCall(msg); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, path, cb](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply reply = *watcher; + if (reply.isError()) { + qCWarning(DBUSMENUPROXY) << "Failed to get actions from" << m_serviceName << "at" << path << reply.error(); + cb({}, false); + } else { + cb(reply.value(), true); + } + }); +} + +bool Menu::getAction(const QString &name, GMenuAction &action) const +{ + QString lookupName; + const GMenuActionMap *actionMap = nullptr; + + if (name.startsWith(QLatin1String("app."))) { + lookupName = name.mid(4); + actionMap = &m_applicationActions; + } else if (name.startsWith(QLatin1String("win."))) { + lookupName = name.mid(4); + actionMap = &m_windowActions; + } + + if (!actionMap) { + return false; + } + + auto it = actionMap->constFind(lookupName); + if (it == actionMap->constEnd()) { + return false; + } + + action = *it; + return true; +} + +void Menu::triggerAction(const QString &name, uint timestamp) +{ + QString lookupName; + QString path; + + // TODO avoid code duplication with getAction + if (name.startsWith(QLatin1String("app."))) { + lookupName = name.mid(4); + path = m_applicationObjectPath; + } else if (name.startsWith(QLatin1String("win."))) { + lookupName = name.mid(4); + path = m_windowObjectPath; + } + + if (path.isEmpty()) { + return; + } + + GMenuAction action; + if (!getAction(name, action)) { + return; + } + + QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName, + path, + s_orgGtkActions, + QStringLiteral("Activate")); + msg << lookupName; + // TODO use the arguments provided by "target" in the menu item + msg << QVariant::fromValue(QVariantList()); + + QVariantMap platformData; + + if (timestamp) { + // From documentation: + // If the startup notification id is not available, this can be just "_TIMEtime", where + // time is the time stamp from the event triggering the call. + // see also gtkwindow.c extract_time_from_startup_id and startup_id_is_fake + platformData.insert(QStringLiteral("desktop-startup-id"), QStringLiteral("_TIME") + QString::number(timestamp)); + } + + msg << platformData; + + QDBusPendingReply reply = QDBusConnection::sessionBus().asyncCall(msg); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, path, name](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply reply = *watcher; + if (reply.isError()) { + qCWarning(DBUSMENUPROXY) << "Failed to invoke action" << name << "on" << m_serviceName << "at" << path << reply.error(); + } + }); +} + +void Menu::actionsChanged(const QStringList &removed, const StringBoolMap &enabledChanges, const QVariantMap &stateChanges, const GMenuActionMap &added, GMenuActionMap &actions, const QString &prefix) +{ + // Collect the actions that we removed, altered, or added, so we can eventually signal changes for all menus that contain one of those actions + QSet dirtyActions; + + // TODO I bet for most of the loops below we could use a nice short std algorithm + + for (const QString &removedAction : removed) { + if (actions.remove(removedAction)) { + dirtyActions.insert(removedAction); + } + } + + for (auto it = enabledChanges.constBegin(), end = enabledChanges.constEnd(); it != end; ++it) { + const QString &actionName = it.key(); + const bool enabled = it.value(); + + auto actionIt = actions.find(actionName); + if (actionIt == actions.end()) { + qCInfo(DBUSMENUPROXY) << "Got enabled changed for action" << actionName << "which we don't know"; + continue; + } + + GMenuAction &action = *actionIt; + if (action.enabled != enabled) { + action.enabled = enabled; + dirtyActions.insert(actionName); + } else { + qCInfo(DBUSMENUPROXY) << "Got enabled change for action" << actionName << "which didn't change it"; + } + } + + for (auto it = stateChanges.constBegin(), end = stateChanges.constEnd(); it != end; ++it) { + const QString &actionName = it.key(); + const QVariant &state = it.value(); + + auto actionIt = actions.find(actionName); + if (actionIt == actions.end()) { + qCInfo(DBUSMENUPROXY) << "Got state changed for action" << actionName << "which we don't know"; + continue; + } + + GMenuAction &action = *actionIt; + + if (action.state.isEmpty()) { + qCDebug(DBUSMENUPROXY) << "Got new state for action" << actionName << "that didn't have any state before"; + action.state.append(state); + dirtyActions.insert(actionName); + } else { + // Action state is a list but the state change only sends us a single variant, so just overwrite the first one + QVariant &firstState = action.state.first(); + if (firstState != state) { + firstState = state; + dirtyActions.insert(actionName); + } else { + qCInfo(DBUSMENUPROXY) << "Got state change for action" << actionName << "which didn't change it"; + } + } + } + + // unite() will result in keys being present multiple times, do it manually and overwrite existing ones + for (auto it = added.constBegin(), end = added.constEnd(); it != end; ++it) { + const QString &actionName = it.key(); + + if (actions.contains(actionName)) { // TODO check isInfoEnabled + qCInfo(DBUSMENUPROXY) << "Got new action" << actionName << "that we already have, overwriting existing one"; + } + + actions.insert(actionName, it.value()); + + dirtyActions.insert(actionName); + } + + 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 emit a change accordingly + QSet dirtyMenus; + for (const QString &action : dirtyActions) { + const QString prefixedAction = prefix + action; + + forEachMenuItem([&prefixedAction, &dirtyMenus](int subscription, int section, int index, const QVariantMap &item) { + Q_UNUSED(index); // we only signal menu changes + + const QString actionName = actionNameOfItem(item); + if (actionName == prefixedAction) { + dirtyMenus.insert(treeStructureToInt(subscription, section, 0)); + return false; // break + } + + return true; // continue + }); + } + + for (uint menu : dirtyMenus) { + emit LayoutUpdated(2 /*revision*/, menu); + } +} + +bool Menu::registerDBusObject() +{ + Q_ASSERT(m_proxyObjectPath.isEmpty()); + Q_ASSERT(!m_menus.isEmpty()); + + static int menus = 0; + ++menus; + + const QString objectPath = QStringLiteral("/MenuBar/%1").arg(QString::number(menus)); + qCDebug(DBUSMENUPROXY) << "Registering DBus object path" << objectPath; + + if (!QDBusConnection::sessionBus().registerObject(objectPath, this)) { + qCWarning(DBUSMENUPROXY) << "Failed to register object"; + return false; + } + + new DbusmenuAdaptor(this); // do this before registering the object? + + m_proxyObjectPath = objectPath; + + return true; +} + +// DBus +bool Menu::AboutToShow(int id) +{ + // We always request the first time GetLayout is called and keep up-to-date internally + // No need to have us prepare anything here + Q_UNUSED(id); + return false; +} + +void Menu::Event(int id, const QString &eventId, const QDBusVariant &data, uint timestamp) +{ + Q_UNUSED(data); + + // GMenu dbus doesn't have any "opened" or "closed" signals, we'll only handle "clicked" + + if (eventId == QLatin1String("clicked")) { + int subscription; + int sectionId; + int index; + + intToTreeStructure(id, subscription, sectionId, index); + + if (index < 1) { // cannot "click" a menu + return; + } + + // TODO check bounds + const auto &item = findSection(m_menus.value(subscription), sectionId).items.at(index - 1); + + const QString action = item.value(QStringLiteral("action")).toString(); + if (!action.isEmpty()) { + triggerAction(action, timestamp); + } + } + +} + +DBusMenuItemList Menu::GetGroupProperties(const QList &ids, const QStringList &propertyNames) +{ + Q_UNUSED(ids); + Q_UNUSED(propertyNames); + return DBusMenuItemList(); +} + +uint Menu::GetLayout(int parentId, int recursionDepth, const QStringList &propertyNames, DBusMenuLayoutItem &dbusItem) +{ + Q_UNUSED(recursionDepth); // TODO + Q_UNUSED(propertyNames); + + int subscription; + int sectionId; + int index; + + intToTreeStructure(parentId, subscription, sectionId, index); + + if (!m_subscriptions.contains(subscription)) { + auto oldPending = m_pendingGetLayouts.value(subscription); + if (oldPending.type() != QDBusMessage::InvalidMessage) { + QDBusConnection::sessionBus().send( + oldPending.createErrorReply( + QStringLiteral("org.kde.plasma.gmenu_dbusmenu_proxy.Error.GetLayoutCalledRepeatedly"), + QStringLiteral("GetLayout got cancelled because a new similar request came in") + ) + ); + } + + m_pendingGetLayouts.insert(subscription, message()); + setDelayedReply(true); + + start(subscription); + return 1; + } + + const auto sections = m_menus.value(subscription); + if (sections.isEmpty()) { + qCDebug(DBUSMENUPROXY) << "There are no sections for requested subscription" << subscription << "with" << parentId; + return 1; + } + + // which sections to add to the menu + const GMenuItem §ion = findSection(sections, sectionId); + + // If a particular entry is requested, see what it is and resolve as neccessary + // for example the "File" entry on root is 0,0,1 but is a menu reference to e.g. 1,0,0 + // so resolve that and return the correct menu + if (index > 0) { + // non-zero index indicates item within a menu but the index in the list still starts at zero + const auto &requestedItem = section.items.at(index - 1); // TODO bounds check + + auto it = requestedItem.constFind(QStringLiteral(":submenu")); + if (it != requestedItem.constEnd()) { + const GMenuSection gmenuSection = qdbus_cast(it->value()); + return GetLayout(treeStructureToInt(gmenuSection.subscription, gmenuSection.menu, 0), recursionDepth, propertyNames, dbusItem); + } else { + // TODO + return 0; + } + } + + dbusItem.id = parentId; // TODO + // TODO use gMenuToDBusMenuProperties? + dbusItem.properties = { + {QStringLiteral("children-display"), QStringLiteral("submenu")} + }; + + auto itemsToBeAdded = section.items; + + int count = 0; + + for (const auto &item : itemsToBeAdded) { + DBusMenuLayoutItem child{ + treeStructureToInt(section.id, sectionId, ++count), + gMenuToDBusMenuProperties(item) + }; + + dbusItem.children.append(child); + } + + // revision, unused in libdbusmenuqt + return 1; +} + +QDBusVariant Menu::GetProperty(int id, const QString &property) +{ + Q_UNUSED(id); + Q_UNUSED(property); + QDBusVariant value; + return value; +} + +QString Menu::status() const +{ + return QStringLiteral("normal"); +} + +uint Menu::version() const +{ + return 4; +} + +int Menu::treeStructureToInt(int subscription, int section, int index) +{ + return subscription * 1000000 + section * 1000 + index; +} + +void Menu::intToTreeStructure(int source, int &subscription, int §ion, int &index) +{ + // TODO some better math :) or bit shifting or something + index = source % 1000; + section = (source / 1000) % 1000; + subscription = source / 1000000; +} + +GMenuItem Menu::findSection(const QList &list, int section) +{ + // TODO algorithm? + for (const GMenuItem &item : list) { + if (item.section == section) { + return item; + } + } + return GMenuItem(); +} + +QString Menu::actionNameOfItem(const QVariantMap &item) +{ + QString actionName = item.value(QStringLiteral("action")).toString(); + if (actionName.isEmpty()) { + actionName = item.value(QStringLiteral("submenu-action")).toString(); + } + return actionName; +} + +QVariantMap Menu::gMenuToDBusMenuProperties(const QVariantMap &source) const +{ + QVariantMap result; + + result.insert(QStringLiteral("label"), source.value(QStringLiteral("label")).toString()); + + if (source.contains(QStringLiteral(":section"))) { + result.insert(QStringLiteral("type"), QStringLiteral("separator")); + } + + const bool isMenu = source.contains(QStringLiteral(":submenu")); + if (isMenu) { + result.insert(QStringLiteral("children-display"), QStringLiteral("submenu")); + } + + QString accel = source.value(QStringLiteral("accel")).toString(); + if (!accel.isEmpty()) { + QStringList shortcut; + + // TODO use regexp or something + if (accel.contains(QLatin1String("")) || accel.contains(QLatin1String(""))) { + shortcut.append(QStringLiteral("Control")); + accel.remove(QLatin1String("")); + accel.remove(QLatin1String("")); + } + + if (accel.contains(QLatin1String(""))) { + shortcut.append(QStringLiteral("Shift")); + accel.remove(QLatin1String("")); + } + + if (accel.contains(QLatin1String(""))) { + shortcut.append(QStringLiteral("Alt")); + accel.remove(QLatin1String("")); + } + + if (accel.contains(QLatin1String(""))) { + shortcut.append(QStringLiteral("Super")); + accel.remove(QLatin1String("")); + } + + if (!accel.isEmpty()) { + // TODO replace "+" by "plus" and "-" by "minus" + shortcut.append(accel); + + // TODO does gmenu support multiple? + DBusMenuShortcut dbusShortcut; + dbusShortcut.append(shortcut); // don't let it unwrap the list we append + + result.insert(QStringLiteral("shortcut"), QVariant::fromValue(dbusShortcut)); + } + } + + bool enabled = true; + const QString actionName = actionNameOfItem(source); + + GMenuAction action; + bool actionOk = getAction(actionName, action); + if (actionOk) { + enabled = action.enabled; + } + + if (!enabled) { + result.insert(QStringLiteral("enabled"), false); + } + + bool visible = true; + const QString hiddenWhen = source.value(QStringLiteral("hidden-when")).toString(); + if (hiddenWhen == QLatin1String("action-disabled") && (!actionOk || !enabled)) { + visible = false; + } else if (hiddenWhen == QLatin1String("action-missing") && !actionOk) { + visible = false; + // While we have Global Menu we don't have macOS menu (where Quit, Help, etc is separate) + } else if (hiddenWhen == QLatin1String("macos-menubar")) { + visible = true; + } + + if (!visible) { + result.insert(QStringLiteral("visible"), false); + } + + QString icon = source.value(QStringLiteral("icon")).toString(); + if (icon.isEmpty()) { + icon = source.value(QStringLiteral("verb-icon")).toString(); + } + if (icon.isEmpty()) { + QString lookupName = actionName.mid(4); // FIXME + // FIXME do properly + static QHash s_icons { + {QStringLiteral("new-window"), QStringLiteral("window-new")}, + {QStringLiteral("new-tab"), QStringLiteral("tab-new")}, + {QStringLiteral("open"), QStringLiteral("document-open")}, + {QStringLiteral("save"), QStringLiteral("document-save")}, + {QStringLiteral("save-as"), QStringLiteral("document-save-as")}, + {QStringLiteral("save-all"), QStringLiteral("document-save-all")}, + {QStringLiteral("print"), QStringLiteral("document-print")}, + {QStringLiteral("close"), QStringLiteral("document-close")}, + {QStringLiteral("close-all"), QStringLiteral("document-close")}, + {QStringLiteral("quit"), QStringLiteral("application-exit")}, + + {QStringLiteral("undo"), QStringLiteral("edit-undo")}, + {QStringLiteral("redo"), QStringLiteral("edit-redo")}, + {QStringLiteral("cut"), QStringLiteral("edit-cut")}, + {QStringLiteral("copy"), QStringLiteral("edit-copy")}, + {QStringLiteral("paste"), QStringLiteral("edit-paste")}, + {QStringLiteral("preferences"), QStringLiteral("settings-configure")}, + + {QStringLiteral("fullscreen"), QStringLiteral("view-fullscreen")}, + + {QStringLiteral("find"), QStringLiteral("edit-find")}, + + {QStringLiteral("previous-document"), QStringLiteral("go-previous")}, + {QStringLiteral("next-document"), QStringLiteral("go-next")}, + + {QStringLiteral("help"), QStringLiteral("help-contents")}, + {QStringLiteral("about"), QStringLiteral("help-about")}, + // TODO some more + }; + icon = s_icons.value(lookupName); + } + if (!icon.isEmpty()) { + result.insert(QStringLiteral("icon-name"), icon); + } + + if (actionOk) { + const auto args = action.state; + if (args.count() == 1) { + const auto &firstArg = args.first(); + // assume this is a checkbox + if (firstArg.canConvert() && !isMenu) { + result.insert(QStringLiteral("toggle-type"), QStringLiteral("checkbox")); + if (firstArg.toBool()) { + result.insert(QStringLiteral("toggle-state"), 1); + } + } + } + } + + return result; +} diff --git a/gmenu-dbusmenu-proxy/menuproxy.h b/gmenu-dbusmenu-proxy/menuproxy.h new file mode 100644 --- /dev/null +++ b/gmenu-dbusmenu-proxy/menuproxy.h @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2018 Kai Uwe Broulik + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#pragma once + +#include +#include +#include +#include // for WId + +#include + +class QDBusServiceWatcher; + +class Menu; + +class MenuProxy : public QObject +{ + Q_OBJECT + +public: + MenuProxy(); + ~MenuProxy() override; + +private Q_SLOTS: + void onWindowAdded(WId id); + void onWindowRemoved(WId id); + +private: + bool init(); + void teardown(); + + void setGtkShellShowsMenuBar(bool show); + + xcb_connection_t *m_xConnection; + + // FIXME the get one reads "UTF8String" (reads gnome) the write thing writes "String" (writes kde) + QByteArray getWindowPropertyString(WId id, const QByteArray &name); + void writeWindowProperty(WId id, const QByteArray &name, const QByteArray &value); + xcb_atom_t getAtom(const QByteArray &name); + + QHash m_menus; + + QDBusServiceWatcher *m_serviceWatcher; + +}; diff --git a/gmenu-dbusmenu-proxy/menuproxy.cpp b/gmenu-dbusmenu-proxy/menuproxy.cpp new file mode 100644 --- /dev/null +++ b/gmenu-dbusmenu-proxy/menuproxy.cpp @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2018 Kai Uwe Broulik + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "menuproxy.h" + +#include + +#include "debug.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include "menu.h" + +static const QString s_ourServiceName = QStringLiteral("org.kde.plasma.gmenu_dbusmenu_proxy"); + +static const QString s_dbusMenuRegistrar = QStringLiteral("com.canonical.AppMenu.Registrar"); + +static const QByteArray s_gtkUniqueBusName = QByteArrayLiteral("_GTK_UNIQUE_BUS_NAME"); + +static const QByteArray s_gtkApplicationObjectPath = QByteArrayLiteral("_GTK_APPLICATION_OBJECT_PATH"); +static const QByteArray s_gtkWindowObjectPath = QByteArrayLiteral("_GTK_WINDOW_OBJECT_PATH"); +static const QByteArray s_gtkMenuBarObjectPath = QByteArrayLiteral("_GTK_MENUBAR_OBJECT_PATH"); +// that's the generic app menu with Help and Options and will be used if window doesn't have a fully-blown menu bar +static const QByteArray s_gtkAppMenuObjectPath = QByteArrayLiteral("_GTK_APP_MENU_OBJECT_PATH"); + +static const QByteArray s_kdeNetWmAppMenuServiceName = QByteArrayLiteral("_KDE_NET_WM_APPMENU_SERVICE_NAME"); +static const QByteArray s_kdeNetWmAppMenuObjectPath = QByteArrayLiteral("_KDE_NET_WM_APPMENU_OBJECT_PATH"); + +MenuProxy::MenuProxy() + : QObject() + , m_xConnection(QX11Info::connection()) + , m_serviceWatcher(new QDBusServiceWatcher(this)) +{ + m_serviceWatcher->setConnection(QDBusConnection::sessionBus()); + m_serviceWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration | + QDBusServiceWatcher::WatchForRegistration); + m_serviceWatcher->addWatchedService(s_dbusMenuRegistrar); + + connect(m_serviceWatcher, &QDBusServiceWatcher::serviceRegistered, this, [this](const QString &service) { + Q_UNUSED(service); + qCDebug(DBUSMENUPROXY) << "Global menu service became available, starting"; + init(); + }); + connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this](const QString &service) { + Q_UNUSED(service); + qCDebug(DBUSMENUPROXY) << "Global menu service disappeared, cleaning up"; + teardown(); + }); + + // It's fine to do a blocking call here as we're a separate binary with no UI + if (QDBusConnection::sessionBus().interface()->isServiceRegistered(s_dbusMenuRegistrar)) { + qCDebug(DBUSMENUPROXY) << "Global menu service is running, starting right away"; + init(); + } else { + qCDebug(DBUSMENUPROXY) << "No global menu service available, waiting for it to start before doing anything"; + + // be sure when started to restore gtk menus when there's no dbus menu around in case we crashed + setGtkShellShowsMenuBar(false); + } +} + +MenuProxy::~MenuProxy() +{ + teardown(); +} + +bool MenuProxy::init() +{ + if (!QDBusConnection::sessionBus().registerService(s_ourServiceName)) { + qCWarning(DBUSMENUPROXY) << "Failed to register DBus service" << s_ourServiceName; + return false; + } + + setGtkShellShowsMenuBar(true); + + connect(KWindowSystem::self(), &KWindowSystem::windowAdded, this, &MenuProxy::onWindowAdded); + connect(KWindowSystem::self(), &KWindowSystem::windowRemoved, this, &MenuProxy::onWindowRemoved); + + const auto windows = KWindowSystem::windows(); + for (WId id : windows) { + onWindowAdded(id); + } + + if (m_menus.isEmpty()) { + qCDebug(DBUSMENUPROXY) << "Up and running but no menus in sight"; + } + + return true; +} + +void MenuProxy::teardown() +{ + setGtkShellShowsMenuBar(false); + + QDBusConnection::sessionBus().unregisterService(s_ourServiceName); + + disconnect(KWindowSystem::self(), &KWindowSystem::windowAdded, this, &MenuProxy::onWindowAdded); + disconnect(KWindowSystem::self(), &KWindowSystem::windowRemoved, this, &MenuProxy::onWindowRemoved); + + qDeleteAll(m_menus); + m_menus.clear(); +} + +void MenuProxy::setGtkShellShowsMenuBar(bool show) +{ + qCDebug(DBUSMENUPROXY) << "Setting gtk-shell-shows-menu-bar to" << show << "which will" << (show ? "hide" : "show") << "menu bars in applications"; + + // mostly taken from kde-gtk-config + QString root = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation); + if (root.isEmpty()) { + root = QFileInfo(QDir::home(), QStringLiteral(".config")).absoluteFilePath(); + } + + const QString settingsFilePath = root + QStringLiteral("/gtk-3.0/settings.ini"); + + auto cfg = KSharedConfig::openConfig(settingsFilePath, KConfig::NoGlobals); + KConfigGroup group(cfg, "Settings"); + + if (show) { + group.writeEntry("gtk-shell-shows-menubar", true); + } else { + group.deleteEntry("gtk-shell-shows-menubar"); + } + + group.sync(); + + // TODO use gconf/dconf directly or at least signal a change somehow? +} + +void MenuProxy::onWindowAdded(WId id) +{ + if (m_menus.contains(id)) { + return; + } + + const QString serviceName = QString::fromUtf8(getWindowPropertyString(id, s_gtkUniqueBusName)); + const QString applicationObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkApplicationObjectPath)); + const QString windowObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkWindowObjectPath)); + + if (serviceName.isEmpty() || applicationObjectPath.isEmpty() || windowObjectPath.isEmpty()) { + return; + } + + QString menuObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkMenuBarObjectPath)); + if (menuObjectPath.isEmpty()) { + // try generic app menu + menuObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkAppMenuObjectPath)); + } + + if (menuObjectPath.isEmpty()) { + return; + } + + Menu *menu = new Menu(id, serviceName, applicationObjectPath, windowObjectPath, menuObjectPath); + m_menus.insert(id, menu); + + connect(menu, &Menu::requestWriteWindowProperties, this, [this, menu] { + Q_ASSERT(!menu->proxyObjectPath().isEmpty()); + + writeWindowProperty(menu->winId(), s_kdeNetWmAppMenuServiceName, s_ourServiceName.toUtf8()); + writeWindowProperty(menu->winId(), s_kdeNetWmAppMenuObjectPath, menu->proxyObjectPath().toUtf8()); + }); + connect(menu, &Menu::requestRemoveWindowProperties, this, [this, menu] { + writeWindowProperty(menu->winId(), s_kdeNetWmAppMenuServiceName, QByteArray()); + writeWindowProperty(menu->winId(), s_kdeNetWmAppMenuObjectPath, QByteArray()); + }); +} + +void MenuProxy::onWindowRemoved(WId id) +{ + // no need to cleanup() (which removes window properties) when the window is gone, delete right away + delete m_menus.take(id); +} + +QByteArray MenuProxy::getWindowPropertyString(WId id, const QByteArray &name) +{ + auto *c = QX11Info::connection(); // FIXME cache + + QByteArray value; + + auto atom = getAtom(name); + if (atom == XCB_ATOM_NONE) { + return value; + } + + static const long MAX_PROP_SIZE = 10000; + // FIXME figure out what "UT8String" is as atom type, it's 392 or 378 but I don't find that enum + auto propertyCookie = xcb_get_property(c, false, id, atom, XCB_ATOM_ANY, 0, MAX_PROP_SIZE); + QScopedPointer propertyReply(xcb_get_property_reply(c, propertyCookie, NULL)); + if (propertyReply.isNull()) { + qCWarning(DBUSMENUPROXY) << "XCB property reply for atom" << name << "on" << id << "was null"; + return value; + } + + // FIXME Check type + if (/*propertyReply->type == 392 && */propertyReply->format == 8 && propertyReply->value_len > 0) { + const char *data = (const char *) xcb_get_property_value(propertyReply.data()); + int len = propertyReply->value_len; + if (data) { + value = QByteArray(data, data[len - 1] ? len : len - 1); + } + } + + return value; +} + +void MenuProxy::writeWindowProperty(WId id, const QByteArray &name, const QByteArray &value) +{ + auto atom = getAtom(name); + if (atom == XCB_ATOM_NONE) { + return; + } + + if (value.isEmpty()) { + xcb_delete_property(m_xConnection, id, atom); + } else { + xcb_change_property(m_xConnection, XCB_PROP_MODE_REPLACE, id, atom, XCB_ATOM_STRING, + 8, value.length(), value.constData()); + } +} + +xcb_atom_t MenuProxy::getAtom(const QByteArray &name) +{ + static QHash s_atoms; + + auto atom = s_atoms.value(name, XCB_ATOM_NONE); + if (atom == XCB_ATOM_NONE) { + const xcb_intern_atom_cookie_t atomCookie = xcb_intern_atom(m_xConnection, false, name.length(), name.constData()); + QScopedPointer atomReply(xcb_intern_atom_reply(m_xConnection, atomCookie, Q_NULLPTR)); + if (!atomReply.isNull()) { + atom = atomReply->atom; + if (atom != XCB_ATOM_NONE) { + s_atoms.insert(name, atom); + } + } + } + + return atom; +}