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,40 @@ +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::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,99 @@ +/* + * 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; + +// 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; + +// 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 id; + uint count; + 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; + +// 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; + +struct GMenuActionsChange +{ + QStringList removed; + QMap enabledChanged; + QMap stateChanged; + QList 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.id << item.count << item.changePosition << item.itemsToRemoveCount << item.itemsToInsert; + argument.endStructure(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuChange &item) +{ + argument.beginStructure(); + argument >> item.id >> item.count >> 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(); + + 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,123 @@ +/* + * 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(); + + 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 onWindowActionsChanged(const GMenuActionsChange &changes); + +private: + void start(); + 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); + + static int treeStructureToInt(int subscription, int section, int index); + static void intToTreeStructure(int source, int &subscription, int §ion, int &index); + + 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,727 @@ +/* + * 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 + +#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) +{ + qDebug() << "Created menu on" << m_serviceName << "at" << m_applicationObjectPath << m_windowObjectPath << m_menuObjectPath; + + + GDBusMenuTypes_register(); + DBusMenuTypes_register(); + + // FIXME doesn't work work + if (!QDBusConnection::sessionBus().connect(m_serviceName, + m_menuObjectPath, + s_orgGtkMenus, + QStringLiteral("Changed"), + this, + SLOT(onMenuChanged(GMenuChangeList)))) { + qWarning() << "Failed to subscribe to menu changes in" << m_serviceName << "at" << m_menuObjectPath; + } + + if (!QDBusConnection::sessionBus().connect(m_serviceName, + m_windowObjectPath, + s_orgGtkActions, + QStringLiteral("Changed"), + this, + SLOT(onWindowActionsChanged(GMenuActionsChange)))) { + qWarning() << "Failed to subsribe to window action changes in" << m_serviceName << "at" << m_windowObjectPath; + } + + // TODO connect to application action changes + + // 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; + start(); + } + }); + + getActions(m_windowObjectPath, [this](const GMenuActionMap &actions, bool ok) { + if (ok) { + m_windowActions = actions; + m_queriedWindowActions = true; + start(); + } + }); +} + +Menu::~Menu() +{ + 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::start() +{ + if (!m_queriedApplicationActions || m_queriedWindowActions) { + return; + } + + qDebug() << "START!"; + start(0); +} + + +void Menu::start(uint id) +{ + qDebug() << "start" << id; + + if (m_subscriptions.contains(id)) { + qDebug() << "Already subscribed to" << 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()) { + qWarning() << "Failed to start subscription to" << id << "from" << m_serviceName << "at" << m_menuObjectPath << reply.error(); + } else { + const bool wasSubscribed = !m_subscriptions.isEmpty(); + + const auto menus = reply.value(); + for (auto menu : menus) { + m_menus[menu.id].append(menus); + + // TODO? + emit LayoutUpdated(1 /*revision*/, treeStructureToInt(menu.id, 0, 0)); + + // TODO are we subscribed to all it returns or just to the ones we requested? + m_subscriptions.append(menu.id); + } + + // 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()) { + //qDebug() << "Already know section here, skip"; + continue; + } + + //qDebug() << "pls add" << gmenuSection.subscription << "at" << gmenuSection.menu << "for" << item; + + // 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()); + qDebug() << "Resolved alias from" << gmenuSection.subscription << gmenuSection.menu << "to" << gmenuSection2.subscription << gmenuSection2.menu; + items = findSection(m_menus.value(gmenuSection2.subscription), gmenuSection2.menu).items; + } + } + + for (const auto &aliasedItem : qAsConst(items)) { + it.insert(aliasedItem); + } + } + } + } + } + + + // first time we successfully requested a menu, announce that this window supports DBusMenu + if (!m_subscriptions.isEmpty() && !wasSubscribed) { + if (registerDBusObject()) { + emit requestWriteWindowProperties(); + } + } + } + + // When it was a delayed GetLayout request, send the reply now + auto pendingReply = m_pendingGetLayouts.value(id); + if (pendingReply.type() != QDBusMessage::InvalidMessage) { + qDebug() << "replying that we now have" << id; + + auto reply = pendingReply.createReply(); + + DBusMenuLayoutItem item; + uint revision = GetLayout(treeStructureToInt(id, 0, 0), 0, {}, item); + + reply << revision << QVariant::fromValue(item); + + QDBusConnection::sessionBus().send(reply); + } + + watcher->deleteLater(); + }); +} + +void Menu::stop(const QList &ids) +{ + // TODO + Q_UNUSED(ids); +} + +void Menu::onMenuChanged(const GMenuChangeList &changes) +{ + // TODO + Q_UNUSED(changes); +} + +void Menu::onWindowActionsChanged(const GMenuActionsChange &changes) +{ + qDebug() << "window actions changed"; + qDebug() << changes.removed << changes.enabledChanged << changes.stateChanged << changes.added.count(); +} + +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()) { + qWarning() << "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) +{ + 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; + 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()) { + qWarning() << "Failed to invoke action" << name << "on" << m_serviceName << "at" << path << reply.error(); + } + }); +} + +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)); + qDebug() << "register as object path" << objectPath; + + if (!QDBusConnection::sessionBus().registerObject(objectPath, this)) { + qWarning() << "Failed to register object" << this << "at" << objectPath; + return false; + } + + new DbusmenuAdaptor(this); // do this before registering the object? + + m_proxyObjectPath = objectPath; + + return true; +} + +// DBus +bool Menu::AboutToShow(int id) +{ + qDebug() << "about to show" << id << calledFromDBus(); + + int subscription; + int sectionId; + int index; + intToTreeStructure(id, subscription, sectionId, index); + + // This doesn't seem to happen, apps seem to just blatantly run GetLayout for unknown stuff + if (!m_subscriptions.contains(subscription)) { + qDebug() << "NEED TO QUERY FIRST subscription" << subscription; + start(subscription); + return false; + } + + return true; +} + +void Menu::Event(int id, const QString &eventId, const QDBusVariant &data, uint timestamp) +{ + qDebug() << "event" << id << eventId << data.variant() << timestamp; + + if (eventId == QLatin1String("opened")) { + + } else if (eventId == QLatin1String("closed")) { + + } else 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); + } + } + +} + +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); + + //qDebug() << "GIMME" << parentId << "which is" << subscription << sectionId << index; + + if (!m_subscriptions.contains(subscription)) { + qDebug() << "not subscribed to" << subscription << ", requesting it now"; + + 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()) { + // TODO start? + qWarning() << "dont have sections for" << subscription; + 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 { + qDebug() << "Requested a particular item" << parentId << requestedItem; + // 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); + } + + return 1; // revision +} + +QDBusVariant Menu::GetProperty(int id, const QString &property) +{ + qDebug() << "get property" << id << 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(); +} + +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; + QString actionName = source.value(QStringLiteral("action")).toString(); + if (actionName.isEmpty()) { + actionName = source.value(QStringLiteral("submenu-action")).toString(); + } + + 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); + } + } + } + } + + auto foo = source.value(QStringLiteral("target")); + if (foo.isValid()) { + qDebug() << "ACTION" << actionName << "HAS" << foo; + } + + /*const QString targetString = source.value(QStringLiteral("target")).toString(); + if (isCheckBox && !targetString.isEmpty()) { + result.insert(QStringLiteral("toggle-type"), QStringLiteral("radio")); + }*/ + + //qDebug() << result; + + 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,54 @@ +/* + * 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 Menu; + +class MenuProxy : public QObject//, public QAbstractNativeEventFilter +{ + Q_OBJECT + +public: + MenuProxy(); + ~MenuProxy() override; + +protected: + //bool nativeEventFilter(const QByteArray & eventType, void * message, long * result) override; + +private Q_SLOTS: + void onWindowAdded(WId id); + void onWindowRemoved(WId id); + +private: + // 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; + +}; 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,197 @@ +/* + * 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 "menu.h" + +static const QString s_ourServiceName = QStringLiteral("org.kde.plasma.gmenu_dbusmenu_proxy"); + +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() +{ + // TODO do nothing if no dbusmenu service is unavailable and wait for it to come + + if (!QDBusConnection::sessionBus().registerService(s_ourServiceName)) { + qWarning() << "Failed to register DBus service" << s_ourServiceName; + //qApp->exit(1); // doensn't work? + ::exit(1); + return; + } + + connect(KWindowSystem::self(), &KWindowSystem::windowAdded, this, &MenuProxy::onWindowAdded); + connect(KWindowSystem::self(), &KWindowSystem::windowRemoved, this, &MenuProxy::onWindowRemoved); + + for (WId id : KWindowSystem::windows()) { // detaches? + onWindowAdded(id); + } + + if (m_menus.isEmpty()) { + qDebug() << "Up and running but no menus in sight"; + } +} + +void MenuProxy::onWindowAdded(WId id) +{ + if (m_menus.contains(id)) { + qDebug() << "Already know window" << id; + return; + } + +//#if HAVE_X11 + if (KWindowSystem::isPlatformX11()) { + // TODO split that stuff out so we can do early returns and not a kilometer of indentation + + 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()); + }); + } +//#endif // HAVE_X11 +} + +void MenuProxy::onWindowRemoved(WId id) +{ + delete m_menus.take(id); // destructor of Menu cleans up everything +} + +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()) { + qDebug() << "property reply 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 *c = QX11Info::connection(); // FIXME cache + + auto atom = getAtom(name); + if (atom == XCB_ATOM_NONE) { + return; + } + + if (value.isEmpty()) { + xcb_delete_property(c,id, atom); + } else { + xcb_change_property(c, XCB_PROP_MODE_REPLACE, id, atom, XCB_ATOM_STRING, + 8, value.length(), value.constData()); + } +} + +xcb_atom_t MenuProxy::getAtom(const QByteArray &name) +{ + auto *c = QX11Info::connection(); // FIXME cache + + 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(c, false, name.length(), name.constData()); + QScopedPointer atomReply(xcb_intern_atom_reply(c, atomCookie, Q_NULLPTR)); + if (!atomReply.isNull()) { + atom = atomReply->atom; + if (atom != XCB_ATOM_NONE) { + s_atoms.insert(name, atom); + } + } + } + + return atom; +} + +MenuProxy::~MenuProxy() = default;