diff --git a/CMakeLists.txt b/CMakeLists.txt index a130c7f..9f34c59 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,6 +9,7 @@ set(CMAKE_AUTORCC ON) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake;${CMAKE_CURRENT_SOURCE_DIR}/cmake") set(QT Core Gui Widgets Quick QuickControls2 DBus X11Extras LinguistTools) find_package(Qt5 REQUIRED ${QT}) @@ -30,3 +31,4 @@ add_subdirectory(powerman) add_subdirectory(cpufreq) add_subdirectory(chotkeys) add_subdirectory(cupdatecursor) +add_subdirectory(gmenuproxy) diff --git a/cmake/FindAppMenuGtkModule.cmake b/cmake/FindAppMenuGtkModule.cmake new file mode 100644 index 0000000..f42b6f2 --- /dev/null +++ b/cmake/FindAppMenuGtkModule.cmake @@ -0,0 +1,41 @@ +#.rst: +# FindAppmenuGtkModule +# ----------- +# +# Try to find appmenu-gtk2-module and appmenu-gtk3-module. +# Once done this will define: +# +# ``AppMenuGtkModule_FOUND`` +# System has both appmenu-gtk2-module and appmenu-gtk3-module + +#============================================================================= +# SPDX-FileCopyrightText: 2018 Kai Uwe Broulik +# +# SPDX-License-Identifier: BSD-3-Clause + +find_library(AppMenuGtk2Module_LIBRARY libappmenu-gtk-module.so + PATH_SUFFIXES + gtk-2.0/modules +) + +find_library(AppMenuGtk3Module_LIBRARY libappmenu-gtk-module.so + PATH_SUFFIXES + gtk-3.0/modules +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(AppMenuGtkModule + FOUND_VAR + AppMenuGtkModule_FOUND + REQUIRED_VARS + AppMenuGtk3Module_LIBRARY + AppMenuGtk2Module_LIBRARY +) + +mark_as_advanced(AppMenuGtk3Module_LIBRARY AppMenuGtk2Module_LIBRARY) + +include(FeatureSummary) +set_package_properties(AppMenuGtkModule PROPERTIES + URL "https://github.com/rilian-la-te/vala-panel-appmenu/tree/master/subprojects/appmenu-gtk-module" + DESCRIPTION "Application Menu GTK+ Module" +) \ No newline at end of file diff --git a/cmake/FindXCB.cmake b/cmake/FindXCB.cmake deleted file mode 100644 index 78fcb7b..0000000 --- a/cmake/FindXCB.cmake +++ /dev/null @@ -1,53 +0,0 @@ -#.rst: -# FindXCB -# ------- -# -# Find XCB libraries -# -# Tries to find xcb libraries on unix systems. -# -# - Be sure to set the COMPONENTS to the components you want to link to -# - The XCB_LIBRARIES variable is set ONLY to your COMPONENTS list -# - To use only a specific component check the XCB_LIBRARIES_${COMPONENT} variable -# -# The following values are defined -# -# :: -# -# XCB_FOUND - True if xcb is available -# XCB_INCLUDE_DIRS - Include directories for xcb -# XCB_LIBRARIES - List of libraries for xcb -# XCB_DEFINITIONS - List of definitions for xcb -# -#============================================================================= -# Copyright (c) 2015 Jari Vetoniemi -# -# Distributed under the OSI-approved BSD License (the "License"); -# -# This software is distributed WITHOUT ANY WARRANTY; without even the -# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# See the License for more information. -#============================================================================= - -include(FeatureSummary) -set_package_properties(XCB PROPERTIES - URL "https://xcb.freedesktop.org/" - DESCRIPTION "X protocol C-language Binding") - -find_package(PkgConfig) -pkg_check_modules(PC_XCB xcb ${XCB_FIND_COMPONENTS}) - -find_library(XCB_LIBRARIES xcb HINTS ${PC_XCB_LIBRARY_DIRS}) -find_path(XCB_INCLUDE_DIRS xcb/xcb.h PATH_SUFFIXES xcb HINTS ${PC_XCB_INCLUDE_DIRS}) - -foreach(COMPONENT ${XCB_FIND_COMPONENTS}) - find_library(XCB_LIBRARIES_${COMPONENT} ${COMPONENT} HINTS ${PC_XCB_LIBRARY_DIRS}) - list(APPEND XCB_LIBRARIES ${XCB_LIBRARIES_${COMPONENT}}) - mark_as_advanced(XCB_LIBRARIES_${COMPONENT}) -endforeach(COMPONENT ${XCB_FIND_COMPONENTS}) - -set(XCB_DEFINITIONS ${PC_XCB_CFLAGS_OTHER}) - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(XCB DEFAULT_MSG XCB_LIBRARIES XCB_INCLUDE_DIRS) -mark_as_advanced(XCB_INCLUDE_DIRS XCB_LIBRARIES XCB_DEFINITIONS) diff --git a/gmenuproxy/CMakeLists.txt b/gmenuproxy/CMakeLists.txt new file mode 100644 index 0000000..4abb7ee --- /dev/null +++ b/gmenuproxy/CMakeLists.txt @@ -0,0 +1,52 @@ +find_package(AppMenuGtkModule) +find_package(KF5WindowSystem) +find_package(KF5CoreAddons) +set_package_properties(AppMenuGtkModule PROPERTIES TYPE RUNTIME) + +add_definitions(-DQT_NO_CAST_TO_ASCII +-DQT_NO_CAST_FROM_ASCII +-DQT_NO_CAST_FROM_BYTEARRAY) + +find_package(XCB + REQUIRED COMPONENTS + XCB +) + +set(GMENU_DBUSMENU_PROXY_SRCS + extend/dbusmenutypes_p.cpp +# extend/dbusmenushortcut_p.cpp + + main.cpp + menuproxy.cpp + window.cpp + menu.cpp + actions.cpp + gdbusmenutypes_p.cpp + icons.cpp + utils.cpp + ) + +qt_add_dbus_adaptor(GMENU_DBUSMENU_PROXY_SRCS ./com.canonical.dbusmenu.xml window.h Window) + +add_executable(cutefish-gmenuproxy ${GMENU_DBUSMENU_PROXY_SRCS}) + +set_package_properties(XCB PROPERTIES TYPE REQUIRED) + +target_link_libraries(cutefish-gmenuproxy + Qt::Core + Qt::X11Extras + Qt::DBus + Qt::Widgets + KF5::CoreAddons + KF5::WindowSystem + XCB::XCB +) + +configure_file( + cutefish-gmenuproxy.service.in + cutefish-gmenuproxy.service + @ONLY +) + +install(TARGETS cutefish-gmenuproxy DESTINATION ${CMAKE_INSTALL_BINDIR}) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/cutefish-gmenuproxy.service DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/systemd/user/) diff --git a/gmenuproxy/actions.cpp b/gmenuproxy/actions.cpp new file mode 100644 index 0000000..e831d2a --- /dev/null +++ b/gmenuproxy/actions.cpp @@ -0,0 +1,196 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "actions.h" + +#include +#include +#include +#include +#include +#include +#include + +static const QString s_orgGtkActions = QStringLiteral("org.gtk.Actions"); + +Actions::Actions(const QString &serviceName, const QString &objectPath, QObject *parent) + : QObject(parent) + , m_serviceName(serviceName) + , m_objectPath(objectPath) +{ + Q_ASSERT(!serviceName.isEmpty()); + Q_ASSERT(!m_objectPath.isEmpty()); + + if (!QDBusConnection::sessionBus().connect(serviceName, + objectPath, + s_orgGtkActions, + QStringLiteral("Changed"), + this, + SLOT(onActionsChanged(QStringList, StringBoolMap, QVariantMap, GMenuActionMap)))) { + qDebug() << "Failed to subscribe to action changes for" << parent << "on" << serviceName << "at" << objectPath; + } +} + +Actions::~Actions() = default; + +void Actions::load() +{ + QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName, m_objectPath, s_orgGtkActions, QStringLiteral("DescribeAll")); + + QDBusPendingReply reply = QDBusConnection::sessionBus().asyncCall(msg); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply reply = *watcher; + if (reply.isError()) { + qDebug() << "Failed to get actions from" << m_serviceName << "at" << m_objectPath << reply.error(); + emit failedToLoad(); + } else { + m_actions = reply.value(); + emit loaded(); + } + watcher->deleteLater(); + }); +} + +bool Actions::get(const QString &name, GMenuAction &action) const +{ + auto it = m_actions.find(name); + if (it == m_actions.constEnd()) { + return false; + } + + action = *it; + return true; +} + +GMenuActionMap Actions::getAll() const +{ + return m_actions; +} + +void Actions::trigger(const QString &name, const QVariant &target, uint timestamp) +{ + if (!m_actions.contains(name)) { + qDebug() << "Cannot invoke action" << name << "which doesn't exist"; + return; + } + + QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName, m_objectPath, s_orgGtkActions, QStringLiteral("Activate")); + msg << name; + + QVariantList args; + if (target.isValid()) { + args << target; + } + msg << QVariant::fromValue(args); + + 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, name](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply reply = *watcher; + if (reply.isError()) { + qDebug() << "Failed to invoke action" << name << "on" << m_serviceName << "at" << m_objectPath << reply.error(); + } + watcher->deleteLater(); + }); +} + +bool Actions::isValid() const +{ + return !m_actions.isEmpty(); +} + +void Actions::onActionsChanged(const QStringList &removed, const StringBoolMap &enabledChanges, const QVariantMap &stateChanges, const GMenuActionMap &added) +{ + // Collect the actions that we removed, altered, or added, so we can eventually signal changes for all menus that contain one of those actions + QStringList dirtyActions; + + // TODO I bet for most of the loops below we could use a nice short std algorithm + + for (const QString &removedAction : removed) { + if (m_actions.remove(removedAction)) { + dirtyActions.append(removedAction); + } + } + + for (auto it = enabledChanges.constBegin(), end = enabledChanges.constEnd(); it != end; ++it) { + const QString &actionName = it.key(); + const bool enabled = it.value(); + + auto actionIt = m_actions.find(actionName); + if (actionIt == m_actions.end()) { + qDebug() << "Got enabled changed for action" << actionName << "which we don't know"; + continue; + } + + GMenuAction &action = *actionIt; + if (action.enabled != enabled) { + action.enabled = enabled; + dirtyActions.append(actionName); + } else { + qDebug() << "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 = m_actions.find(actionName); + if (actionIt == m_actions.end()) { + qDebug() << "Got state changed for action" << actionName << "which we don't know"; + continue; + } + + GMenuAction &action = *actionIt; + + if (action.state.isEmpty()) { + qDebug() << "Got new state for action" << actionName << "that didn't have any state before"; + action.state.append(state); + dirtyActions.append(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.append(actionName); + } else { + qDebug() << "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 ((DBUSMENUPROXY).isInfoEnabled()) { + // if (m_actions.contains(actionName)) { + // qDebug() << "Got new action" << actionName << "that we already have, overwriting existing one"; + // } + // } + + m_actions.insert(actionName, it.value()); + + dirtyActions.append(actionName); + } + + if (!dirtyActions.isEmpty()) { + emit actionsChanged(dirtyActions); + } +} diff --git a/gmenuproxy/actions.h b/gmenuproxy/actions.h new file mode 100644 index 0000000..a0de1c4 --- /dev/null +++ b/gmenuproxy/actions.h @@ -0,0 +1,45 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include +#include + +#include "gdbusmenutypes_p.h" + +class QStringList; + +class Actions : public QObject +{ + Q_OBJECT + +public: + Actions(const QString &serviceName, const QString &objectPath, QObject *parent = nullptr); + ~Actions() override; + + void load(); + + bool get(const QString &name, GMenuAction &action) const; + GMenuActionMap getAll() const; + void trigger(const QString &name, const QVariant &target, uint timestamp = 0); + + bool isValid() const; // basically "has actions" + +Q_SIGNALS: + void loaded(); + void failedToLoad(); // expose error? + void actionsChanged(const QStringList &dirtyActions); + +private slots: + void onActionsChanged(const QStringList &removed, const StringBoolMap &enabledChanges, const QVariantMap &stateChanges, const GMenuActionMap &added); + +private: + GMenuActionMap m_actions; + + QString m_serviceName; + QString m_objectPath; +}; diff --git a/gmenuproxy/com.canonical.dbusmenu.xml b/gmenuproxy/com.canonical.dbusmenu.xml new file mode 100644 index 0000000..c90776a --- /dev/null +++ b/gmenuproxy/com.canonical.dbusmenu.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gmenuproxy/cutefish-gmenuproxy.service.in b/gmenuproxy/cutefish-gmenuproxy.service.in new file mode 100644 index 0000000..ad095b1 --- /dev/null +++ b/gmenuproxy/cutefish-gmenuproxy.service.in @@ -0,0 +1,10 @@ +[Unit] +Description=Proxies GTK DBus menus to a Cutefish readable format +PartOf=graphical-session.target + +[Service] +ExecStart=@CMAKE_INSTALL_FULL_BINDIR@/cutefish-gmenuproxy +Restart=on-failure +Type=simple +Slice=background.slice +TimeoutSec=5sec diff --git a/gmenuproxy/extend/dbusmenushortcut_p.cpp b/gmenuproxy/extend/dbusmenushortcut_p.cpp new file mode 100644 index 0000000..0257f2b --- /dev/null +++ b/gmenuproxy/extend/dbusmenushortcut_p.cpp @@ -0,0 +1,69 @@ +/* This file is part of the dbusmenu-qt library + SPDX-FileCopyrightText: 2009 Canonical + SPDX-FileContributor: Aurelien Gateau + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#include "dbusmenushortcut_p.h" + +// Qt +#include + +static const int QT_COLUMN = 0; +static const int DM_COLUMN = 1; + +static void processKeyTokens(QStringList *tokens, int srcCol, int dstCol) +{ + struct Row { + const char *zero; + const char *one; + const char *operator[](int col) const + { + return col == 0 ? zero : one; + } + }; + static const Row table[] = {{"Meta", "Super"}, + {"Ctrl", "Control"}, + // Special cases for compatibility with libdbusmenu-glib which uses + // "plus" for "+" and "minus" for "-". + // cf https://bugs.launchpad.net/libdbusmenu-qt/+bug/712565 + {"+", "plus"}, + {"-", "minus"}, + {nullptr, nullptr}}; + + const Row *ptr = table; + for (; ptr->zero != nullptr; ++ptr) { + const char *from = (*ptr)[srcCol]; + const char *to = (*ptr)[dstCol]; + tokens->replaceInStrings(from, to); + } +} + +DBusMenuShortcut DBusMenuShortcut::fromKeySequence(const QKeySequence &sequence) +{ + QString string = sequence.toString(); + DBusMenuShortcut shortcut; + QStringList tokens = string.split(QStringLiteral(", ")); + Q_FOREACH (QString token, tokens) { + // Hack: Qt::CTRL | Qt::Key_Plus is turned into the string "Ctrl++", + // but we don't want the call to token.split() to consider the + // second '+' as a separator so we replace it with its final value. + token.replace(QLatin1String("++"), QLatin1String("+plus")); + QStringList keyTokens = token.split('+'); + processKeyTokens(&keyTokens, QT_COLUMN, DM_COLUMN); + shortcut << keyTokens; + } + return shortcut; +} + +QKeySequence DBusMenuShortcut::toKeySequence() const +{ + QStringList tmp; + Q_FOREACH (const QStringList &keyTokens_, *this) { + QStringList keyTokens = keyTokens_; + processKeyTokens(&keyTokens, DM_COLUMN, QT_COLUMN); + tmp << keyTokens.join(QLatin1String("+")); + } + QString string = tmp.join(QLatin1String(", ")); + return QKeySequence::fromString(string); +} diff --git a/gmenuproxy/extend/dbusmenushortcut_p.h b/gmenuproxy/extend/dbusmenushortcut_p.h new file mode 100644 index 0000000..1d5d7a2 --- /dev/null +++ b/gmenuproxy/extend/dbusmenushortcut_p.h @@ -0,0 +1,22 @@ +/* This file is part of the dbusmenu-qt library + SPDX-FileCopyrightText: 2009 Canonical + SPDX-FileContributor: Aurelien Gateau + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#pragma once + +// Qt +#include +#include + +class QKeySequence; + +class DBusMenuShortcut : public QList +{ +public: + QKeySequence toKeySequence() const; + static DBusMenuShortcut fromKeySequence(const QKeySequence &); +}; + +Q_DECLARE_METATYPE(DBusMenuShortcut) diff --git a/gmenuproxy/extend/dbusmenutypes_p.cpp b/gmenuproxy/extend/dbusmenutypes_p.cpp new file mode 100644 index 0000000..b0125fb --- /dev/null +++ b/gmenuproxy/extend/dbusmenutypes_p.cpp @@ -0,0 +1,122 @@ +/* This file is part of the dbusmenu-qt library + SPDX-FileCopyrightText: 2009 Canonical + SPDX-FileContributor: Aurelien Gateau + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#include "dbusmenutypes_p.h" + +// Local +#include "dbusmenushortcut_p.h" + +// Qt +#include +#include + +//// DBusMenuItem +QDBusArgument &operator<<(QDBusArgument &argument, const DBusMenuItem &obj) +{ + argument.beginStructure(); + argument << obj.id << obj.properties; + argument.endStructure(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &argument, DBusMenuItem &obj) +{ + argument.beginStructure(); + argument >> obj.id >> obj.properties; + argument.endStructure(); + return argument; +} + +//// DBusMenuItemKeys +QDBusArgument &operator<<(QDBusArgument &argument, const DBusMenuItemKeys &obj) +{ + argument.beginStructure(); + argument << obj.id << obj.properties; + argument.endStructure(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &argument, DBusMenuItemKeys &obj) +{ + argument.beginStructure(); + argument >> obj.id >> obj.properties; + argument.endStructure(); + return argument; +} + +//// DBusMenuLayoutItem +QDBusArgument &operator<<(QDBusArgument &argument, const DBusMenuLayoutItem &obj) +{ + argument.beginStructure(); + argument << obj.id << obj.properties; + argument.beginArray(qMetaTypeId()); + Q_FOREACH (const DBusMenuLayoutItem &child, obj.children) { + argument << QDBusVariant(QVariant::fromValue(child)); + } + argument.endArray(); + argument.endStructure(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &argument, DBusMenuLayoutItem &obj) +{ + argument.beginStructure(); + argument >> obj.id >> obj.properties; + argument.beginArray(); + while (!argument.atEnd()) { + QDBusVariant dbusVariant; + argument >> dbusVariant; + QDBusArgument childArgument = dbusVariant.variant().value(); + + DBusMenuLayoutItem child; + childArgument >> child; + obj.children.append(child); + } + argument.endArray(); + argument.endStructure(); + return argument; +} + +//// DBusMenuShortcut +QDBusArgument &operator<<(QDBusArgument &argument, const DBusMenuShortcut &obj) +{ + argument.beginArray(qMetaTypeId()); + typename QList::ConstIterator it = obj.constBegin(); + typename QList::ConstIterator end = obj.constEnd(); + for (; it != end; ++it) + argument << *it; + argument.endArray(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &argument, DBusMenuShortcut &obj) +{ + argument.beginArray(); + obj.clear(); + while (!argument.atEnd()) { + QStringList item; + argument >> item; + obj.push_back(item); + } + argument.endArray(); + return argument; +} + +void DBusMenuTypes_register() +{ + static bool registered = false; + if (registered) { + return; + } + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + registered = true; +} diff --git a/gmenuproxy/extend/dbusmenutypes_p.h b/gmenuproxy/extend/dbusmenutypes_p.h new file mode 100644 index 0000000..f6dc79f --- /dev/null +++ b/gmenuproxy/extend/dbusmenutypes_p.h @@ -0,0 +1,80 @@ +/* This file is part of the dbusmenu-qt library + SPDX-FileCopyrightText: 2009 Canonical + SPDX-FileContributor: Aurelien Gateau + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#pragma once + +// Qt +#include +#include +#include + +class QDBusArgument; + +//// DBusMenuItem +/** + * Internal struct used to communicate on DBus + */ +struct DBusMenuItem { + int id; + QVariantMap properties; +}; + +Q_DECLARE_METATYPE(DBusMenuItem) + +QDBusArgument &operator<<(QDBusArgument &argument, const DBusMenuItem &item); +const QDBusArgument &operator>>(const QDBusArgument &argument, DBusMenuItem &item); + +typedef QList DBusMenuItemList; + +Q_DECLARE_METATYPE(DBusMenuItemList) + +//// DBusMenuItemKeys +/** + * Represents a list of keys for a menu item + */ +struct DBusMenuItemKeys { + int id; + QStringList properties; +}; + +Q_DECLARE_METATYPE(DBusMenuItemKeys) + +QDBusArgument &operator<<(QDBusArgument &argument, const DBusMenuItemKeys &); +const QDBusArgument &operator>>(const QDBusArgument &argument, DBusMenuItemKeys &); + +typedef QList DBusMenuItemKeysList; + +Q_DECLARE_METATYPE(DBusMenuItemKeysList) + +//// DBusMenuLayoutItem +/** + * Represents an item with its children. GetLayout() returns a + * DBusMenuLayoutItemList. + */ +struct DBusMenuLayoutItem; +struct DBusMenuLayoutItem { + int id; + QVariantMap properties; + QList children; +}; + +Q_DECLARE_METATYPE(DBusMenuLayoutItem) + +QDBusArgument &operator<<(QDBusArgument &argument, const DBusMenuLayoutItem &); +const QDBusArgument &operator>>(const QDBusArgument &argument, DBusMenuLayoutItem &); + +typedef QList DBusMenuLayoutItemList; + +Q_DECLARE_METATYPE(DBusMenuLayoutItemList) + +//// DBusMenuShortcut + +class DBusMenuShortcut; + +QDBusArgument &operator<<(QDBusArgument &argument, const DBusMenuShortcut &); +const QDBusArgument &operator>>(const QDBusArgument &argument, DBusMenuShortcut &); + +void DBusMenuTypes_register(); \ No newline at end of file diff --git a/gmenuproxy/gdbusmenutypes_p.cpp b/gmenuproxy/gdbusmenutypes_p.cpp new file mode 100644 index 0000000..10f248c --- /dev/null +++ b/gmenuproxy/gdbusmenutypes_p.cpp @@ -0,0 +1,119 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#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/gmenuproxy/gdbusmenutypes_p.h b/gmenuproxy/gdbusmenutypes_p.h new file mode 100644 index 0000000..e8c1333 --- /dev/null +++ b/gmenuproxy/gdbusmenutypes_p.h @@ -0,0 +1,89 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#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/gmenuproxy/gmenudbusmenuproxy.desktop b/gmenuproxy/gmenudbusmenuproxy.desktop new file mode 100644 index 0000000..ab65a32 --- /dev/null +++ b/gmenuproxy/gmenudbusmenuproxy.desktop @@ -0,0 +1,50 @@ +[Desktop Entry] +Exec=gmenudbusmenuproxy +Name=GMenuDBusMenuProxy +Name[ar]=وكيل قائمة D-Bus لـ GMenu. +Name[ast]=GMenuDBusMenuProxy +Name[az]=GMenuDBusMenuProxy +Name[ca]=GMenuDBusMenuProxy +Name[ca@valencia]=GMenuDBusMenuProxy +Name[da]=GMenuDBusMenuProxy +Name[de]=GMenuDBusMenuProxy +Name[el]=GMenuDBusMenuProxy +Name[en_GB]=GMenuDBusMenuProxy +Name[es]=GMenuDBusMenuProxy +Name[et]=GMenuDBusMenuProxy +Name[eu]=GMenuDBusMenuProxy +Name[fi]=GMenuDBusMenuProxy +Name[fr]=GMenuDBusMenuProxy +Name[gl]=Proxy de menú por D-Bus para GMenu. +Name[hi]=जीमेन्यूडीबसमेन्यूप्रॉक्सी +Name[hu]=GMenuDBusMenuProxy +Name[ia]=GMenuDBusMenuProxy +Name[id]=GMenuDBusMenuProxy +Name[it]=GMenuDBusMenuProxy +Name[ko]=GMenuDBusMenuProxy +Name[lt]=GMenuDBusMenuProxy +Name[ml]=ജിമെനുഡിബസ്‍മെനുപ്രോക്സി +Name[nl]=GMenuDBusMenuProxy +Name[nn]=GMenuDBusMenuProxy +Name[pa]=GMenuDBusMenuProxy +Name[pl]=GMenuDBusMenuProxy +Name[pt]=GMenuDBusMenuProxy +Name[pt_BR]=GMenuDBusMenuProxy +Name[ro]=GMenuDBusMenuProxy +Name[ru]=GMenuDBusMenuProxy +Name[sk]=GMenuDBusMenuProxy +Name[sl]=GMenuDBusMenuProxy +Name[sv]=GMenuDBusMenuProxy +Name[ta]=GMenuDBusMenuProxy +Name[tr]=GMenuDBusMenuProxy +Name[uk]=Проксі-меню GMenu D-Bus +Name[vi]=GMenuDBusMenuProxy +Name[x-test]=xxGMenuDBusMenuProxyxx +Name[zh_CN]=GMenuDBusMenuProxy +Name[zh_TW]=GMenuDBusMenuProxy +Type=Application +X-KDE-StartupNotify=false +NoDisplay=true +OnlyShowIn=KDE; +X-KDE-autostart-phase=1 +X-systemd-skip=true diff --git a/gmenuproxy/icons.cpp b/gmenuproxy/icons.cpp new file mode 100644 index 0000000..3cca6c3 --- /dev/null +++ b/gmenuproxy/icons.cpp @@ -0,0 +1,308 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "icons.h" + +#include +#include + +QString Icons::actionIcon(const QString &actionName) +{ + QString icon; + + QString action = actionName; + + if (action.isEmpty()) { + return icon; + } + + static const QHash s_icons{ + {QStringLiteral("new"), QStringLiteral("document-new")}, // appmenu-gtk-module "New" + {QStringLiteral("image-new"), QStringLiteral("document-new")}, // Gimp "New" item + {QStringLiteral("adddirect"), QStringLiteral("document-new")}, // LibreOffice "New" item + {QStringLiteral("filenew"), QStringLiteral("document-new")}, // Pluma "New" item + {QStringLiteral("new-window"), QStringLiteral("window-new")}, + {QStringLiteral("newwindow"), QStringLiteral("window-new")}, + {QStringLiteral("yelp-window-new"), QStringLiteral("window-new")}, // Gnome help + {QStringLiteral("new-tab"), QStringLiteral("tab-new")}, + {QStringLiteral("open"), QStringLiteral("document-open")}, + {QStringLiteral("open-location"), QStringLiteral("document-open-remote")}, + {QStringLiteral("openremote"), QStringLiteral("document-open-remote")}, + {QStringLiteral("save"), QStringLiteral("document-save")}, + {QStringLiteral("save-as"), QStringLiteral("document-save-as")}, + {QStringLiteral("saveas"), QStringLiteral("document-save-as")}, + {QStringLiteral("save-all"), QStringLiteral("document-save-all")}, + {QStringLiteral("saveall"), QStringLiteral("document-save-all")}, + {QStringLiteral("import"), QStringLiteral("document-import")}, + {QStringLiteral("export"), QStringLiteral("document-export")}, + {QStringLiteral("exportto"), QStringLiteral("document-export")}, // LibreOffice + {QStringLiteral("exporttopdf"), QStringLiteral("viewpdf")}, // LibreOffice, the icon it uses but the name is quite random + {QStringLiteral("webhtml"), QStringLiteral("text-html")}, // LibreOffice + {QStringLiteral("printpreview"), QStringLiteral("document-print-preview")}, + {QStringLiteral("print-preview"), QStringLiteral("document-print-preview")}, + {QStringLiteral("print"), QStringLiteral("document-print")}, + {QStringLiteral("print-gtk"), QStringLiteral("document-print")}, // Gimp + {QStringLiteral("mail-image"), QStringLiteral("mail-message-new")}, // Gimp + {QStringLiteral("sendmail"), QStringLiteral("mail-message-new")}, // LibreOffice + {QStringLiteral("sendviabluetooth"), QStringLiteral("preferences-system-bluetooth")}, // LibreOffice + {QStringLiteral("sendviabluetooth"), QStringLiteral("preferences-system-bluetooth")}, // LibreOffice + {QStringLiteral("document-properties"), QStringLiteral("document-properties")}, + {QStringLiteral("close"), QStringLiteral("document-close")}, // appmenu-gtk-module "Close" + {QStringLiteral("closedoc"), QStringLiteral("document-close")}, + {QStringLiteral("close-all"), QStringLiteral("document-close")}, + {QStringLiteral("closeall"), QStringLiteral("document-close")}, + {QStringLiteral("closewin"), QStringLiteral("window-close")}, // LibreOffice + {QStringLiteral("quit"), QStringLiteral("application-exit")}, + + {QStringLiteral("undo"), QStringLiteral("edit-undo")}, + {QStringLiteral("redo"), QStringLiteral("edit-redo")}, + {QStringLiteral("revert"), QStringLiteral("document-revert")}, + {QStringLiteral("cut"), QStringLiteral("edit-cut")}, + {QStringLiteral("copy"), QStringLiteral("edit-copy")}, + {QStringLiteral("paste"), QStringLiteral("edit-paste")}, + {QStringLiteral("duplicate"), QStringLiteral("edit-duplicate")}, + + {QStringLiteral("preferences"), QStringLiteral("settings-configure")}, + {QStringLiteral("optionstreedialog"), QStringLiteral("settings-configure")}, // LibreOffice + {QStringLiteral("keyboard-shortcuts"), QStringLiteral("configure-shortcuts")}, + + {QStringLiteral("fullscreen"), QStringLiteral("view-fullscreen")}, + + {QStringLiteral("find"), QStringLiteral("edit-find")}, + {QStringLiteral("searchfind"), QStringLiteral("edit-find")}, + {QStringLiteral("replace"), QStringLiteral("edit-find-replace")}, + {QStringLiteral("searchreplace"), QStringLiteral("edit-find-replace")}, // LibreOffice + {QStringLiteral("searchdialog"), QStringLiteral("edit-find-replace")}, // LibreOffice + {QStringLiteral("find-replace"), QStringLiteral("edit-find-replace")}, // Inkscape + {QStringLiteral("select-all"), QStringLiteral("edit-select-all")}, + {QStringLiteral("selectall"), QStringLiteral("edit-select-all")}, + {QStringLiteral("select-none"), QStringLiteral("edit-select-invert")}, + {QStringLiteral("select-invert"), QStringLiteral("edit-select-invert")}, + {QStringLiteral("invert-selection"), QStringLiteral("edit-select-invert")}, // Inkscape + {QStringLiteral("check-spelling"), QStringLiteral("tools-check-spelling")}, + {QStringLiteral("set-language"), QStringLiteral("set-language")}, + + {QStringLiteral("increasesize"), QStringLiteral("zoom-in")}, + {QStringLiteral("decreasesize"), QStringLiteral("zoom-out")}, + {QStringLiteral("zoom-in"), QStringLiteral("zoom-in")}, + {QStringLiteral("zoom-out"), QStringLiteral("zoom-out")}, + {QStringLiteral("zoomfit"), QStringLiteral("zoom-fit-best")}, + {QStringLiteral("zoom-fit-in"), QStringLiteral("zoom-fit-best")}, + {QStringLiteral("show-guides"), QStringLiteral("show-guides")}, + {QStringLiteral("show-grid"), QStringLiteral("show-grid")}, + + {QStringLiteral("rotateclockwise"), QStringLiteral("object-rotate-right")}, + {QStringLiteral("rotatecounterclockwise"), QStringLiteral("object-rotate-left")}, + {QStringLiteral("fliphorizontally"), QStringLiteral("object-flip-horizontal")}, + {QStringLiteral("image-flip-horizontal"), QStringLiteral("object-flip-horizontal")}, + {QStringLiteral("flipvertically"), QStringLiteral("object-flip-vertical")}, + {QStringLiteral("image-flip-vertical"), QStringLiteral("object-flip-vertical")}, + {QStringLiteral("image-scale"), QStringLiteral("transform-scale")}, + + {QStringLiteral("bold"), QStringLiteral("format-text-bold")}, + {QStringLiteral("italic"), QStringLiteral("format-text-italic")}, + {QStringLiteral("underline"), QStringLiteral("format-text-underline")}, + {QStringLiteral("strikeout"), QStringLiteral("format-text-strikethrough")}, + {QStringLiteral("superscript"), QStringLiteral("format-text-superscript")}, + {QStringLiteral("subscript"), QStringLiteral("format-text-subscript")}, + // "grow" is a bit unspecific to always set it to "grow font", so use the exact ID here + {QStringLiteral(".uno:Grow"), QStringLiteral("format-font-size-more")}, // LibreOffice + {QStringLiteral(".uno:Shrink"), QStringLiteral("format-font-size-less")}, // LibreOffice + // also a bit unspecific? + {QStringLiteral("alignleft"), QStringLiteral("format-justify-left")}, + {QStringLiteral("alignhorizontalcenter"), QStringLiteral("format-justify-center")}, + {QStringLiteral("alignright"), QStringLiteral("format-justify-right")}, + {QStringLiteral("alignjustified"), QStringLiteral("format-justify-fill")}, + {QStringLiteral("incrementindent"), QStringLiteral("format-indent-more")}, + {QStringLiteral("decrementindent"), QStringLiteral("format-indent-less")}, + {QStringLiteral("defaultbullet"), QStringLiteral("format-list-unordered")}, // LibreOffice + {QStringLiteral("defaultnumbering"), QStringLiteral("format-list-ordered")}, // LibreOffice + + {QStringLiteral("sortascending"), QStringLiteral("view-sort-ascending")}, + {QStringLiteral("sortdescending"), QStringLiteral("view-sort-descending")}, + + {QStringLiteral("autopilotmenu"), QStringLiteral("tools-wizard")}, // LibreOffice + + {QStringLiteral("layers-new"), QStringLiteral("layer-new")}, + {QStringLiteral("layers-duplicate"), QStringLiteral("layer-duplicate")}, + {QStringLiteral("layers-delete"), QStringLiteral("layer-delete")}, + {QStringLiteral("layers-anchor"), QStringLiteral("anchor")}, + + {QStringLiteral("slideshow"), QStringLiteral("media-playback-start")}, // Gwenview uses this icon for that + {QStringLiteral("playvideo"), QStringLiteral("media-playback-start")}, + + {QStringLiteral("addtags"), QStringLiteral("tag-new")}, + {QStringLiteral("newevent"), QStringLiteral("appointment-new")}, + + {QStringLiteral("previous-document"), QStringLiteral("go-previous")}, + {QStringLiteral("prevphoto"), QStringLiteral("go-previous")}, + {QStringLiteral("next-document"), QStringLiteral("go-next")}, + {QStringLiteral("nextphoto"), QStringLiteral("go-next")}, + + {QStringLiteral("redeye"), QStringLiteral("redeyes")}, + {QStringLiteral("crop"), QStringLiteral("transform-crop")}, + {QStringLiteral("move"), QStringLiteral("transform-move")}, + {QStringLiteral("rotate"), QStringLiteral("transform-rotate")}, + {QStringLiteral("scale"), QStringLiteral("transform-scale")}, + {QStringLiteral("shear"), QStringLiteral("transform-shear")}, + {QStringLiteral("flip"), QStringLiteral("object-flip-horizontal")}, + {QStringLiteral("flag"), QStringLiteral("flag-red")}, // is there a "mark" or "important" icon that isn't email? + + {QStringLiteral("tools-measure"), QStringLiteral("measure")}, + {QStringLiteral("tools-text"), QStringLiteral("draw-text")}, + {QStringLiteral("tools-color-picker"), QStringLiteral("color-picker")}, + {QStringLiteral("tools-paintbrush"), QStringLiteral("draw-brush")}, + {QStringLiteral("tools-eraser"), QStringLiteral("draw-eraser")}, + {QStringLiteral("tools-paintbrush"), QStringLiteral("draw-brush")}, + + {QStringLiteral("help"), QStringLiteral("help-contents")}, + {QStringLiteral("helpindex"), QStringLiteral("help-contents")}, + {QStringLiteral("contents"), QStringLiteral("help-contents")}, + {QStringLiteral("helpcontents"), QStringLiteral("help-contents")}, + {QStringLiteral("context-help"), QStringLiteral("help-whatsthis")}, + {QStringLiteral("extendedhelp"), QStringLiteral("help-whatsthis")}, // LibreOffice + {QStringLiteral("helpreportproblem"), QStringLiteral("tools-report-bug")}, + {QStringLiteral("sendfeedback"), QStringLiteral("tools-report-bug")}, // LibreOffice + {QStringLiteral("about"), QStringLiteral("help-about")}, + + {QStringLiteral("emptytrash"), QStringLiteral("trash-empty")}, + {QStringLiteral("movetotrash"), QStringLiteral("user-trash-symbolic")}, + + // Gnome help + {QStringLiteral("yelp-application-larger-text"), QStringLiteral("format-font-size-more")}, + {QStringLiteral("yelp-application-smaller-text"), QStringLiteral("format-font-size-less")}, // LibreOffice + + // LibreOffice documents in its New menu + {QStringLiteral("private:factory/swriter"), QStringLiteral("application-vnd.oasis.opendocument.text")}, + {QStringLiteral("private:factory/scalc"), QStringLiteral("application-vnd.oasis.opendocument.spreadsheet")}, + {QStringLiteral("private:factory/simpress"), QStringLiteral("application-vnd.oasis.opendocument.presentation")}, + {QStringLiteral("private:factory/sdraw"), QStringLiteral("application-vnd.oasis.opendocument.graphics")}, + {QStringLiteral("private:factory/swriter/web"), QStringLiteral("text-html")}, + {QStringLiteral("private:factory/smath"), QStringLiteral("application-vnd.oasis.opendocument.formula")}, + }; + + // Sometimes we get additional arguments (?slot=123) we don't care about + const int questionMarkIndex = action.indexOf(QLatin1Char('?')); + if (questionMarkIndex > -1) { + action.truncate(questionMarkIndex); + } + + icon = s_icons.value(action); + + if (icon.isEmpty()) { + const int dotIndex = action.indexOf(QLatin1Char('.')); // app., win., or unity. prefix + + QString prefix; + if (dotIndex > -1) { + prefix = action.left(dotIndex); + + action = action.mid(dotIndex + 1); + } + + // appmenu-gtk-module + if (prefix == QLatin1String("unity")) { + // Remove superfluous hyphens added by appmenu-gtk-module + // First remove multiple subsequent ones + static QRegularExpression subsequentHyphenRegExp(QStringLiteral("-{2,}")); + action.replace(subsequentHyphenRegExp, QStringLiteral("-")); + + // now we can be sure we only have a single hyphen at the start or end, remove it if needed + if (action.startsWith(QLatin1Char('-'))) { + action.remove(0, 1); + } + if (action.endsWith(QLatin1Char('-'))) { + action.chop(1); + } + + // It also turns accelerators (&) into hyphens, so remove any hyphen that comes before + // a lower-case letter ("mid sentence"), e.g. "P-references" + static QRegularExpression strayHyphenRegExp(QStringLiteral("-(?=[a-z]+)")); + action.remove(strayHyphenRegExp); + } + + icon = s_icons.value(action); + } + + if (icon.isEmpty()) { + static const auto s_dup1Prefix = QStringLiteral("dup:1:"); // can it be dup2 also? + if (action.startsWith(s_dup1Prefix)) { + action = action.mid(s_dup1Prefix.length()); + } + + static const auto s_unoPrefix = QStringLiteral(".uno:"); // LibreOffice with appmenu-gtk + if (action.startsWith(s_unoPrefix)) { + action = action.mid(s_unoPrefix.length()); + } + + // LibreOffice's "Open" entry is always "OpenFromAppname" so we just chop that off + if (action.startsWith(QLatin1String("OpenFrom"))) { + action.truncate(4); // basically "Open" + } + + icon = s_icons.value(action); + } + + if (icon.isEmpty()) { + static const auto s_commonPrefix = QStringLiteral("Common"); + if (action.startsWith(s_commonPrefix)) { + action = action.mid(s_commonPrefix.length()); + } + + icon = s_icons.value(action); + } + + if (icon.isEmpty()) { + static const auto s_prefixes = QStringList{ + // Gimp with appmenu-gtk + QStringLiteral("file-"), + QStringLiteral("edit-"), + QStringLiteral("view-"), + QStringLiteral("image-"), + QStringLiteral("layers-"), + QStringLiteral("colors-"), + QStringLiteral("tools-"), + QStringLiteral("plug-in-"), + QStringLiteral("windows-"), + QStringLiteral("dialogs-"), + QStringLiteral("help-"), + }; + + for (const QString &prefix : s_prefixes) { + if (action.startsWith(prefix)) { + action = action.mid(prefix.length()); + break; + } + } + + icon = s_icons.value(action); + } + + if (icon.isEmpty()) { + action = action.toLower(); + icon = s_icons.value(action); + } + + if (icon.isEmpty()) { + static const auto s_prefixes = QStringList{ + // Pluma with appmenu-gtk + QStringLiteral("file"), + QStringLiteral("edit"), + QStringLiteral("view"), + QStringLiteral("help"), + }; + + for (const QString &prefix : s_prefixes) { + if (action.startsWith(prefix)) { + action = action.mid(prefix.length()); + break; + } + } + + icon = s_icons.value(action); + } + + return icon; +} diff --git a/gmenuproxy/icons.h b/gmenuproxy/icons.h new file mode 100644 index 0000000..46dd1db --- /dev/null +++ b/gmenuproxy/icons.h @@ -0,0 +1,15 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include + +namespace Icons +{ +QString actionIcon(const QString &actionName); + +} diff --git a/gmenuproxy/main.cpp b/gmenuproxy/main.cpp new file mode 100644 index 0000000..72e510c --- /dev/null +++ b/gmenuproxy/main.cpp @@ -0,0 +1,37 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#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/gmenuproxy/menu.cpp b/gmenuproxy/menu.cpp new file mode 100644 index 0000000..ab20049 --- /dev/null +++ b/gmenuproxy/menu.cpp @@ -0,0 +1,326 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "menu.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include "utils.h" + +static const QString s_orgGtkMenus = QStringLiteral("org.gtk.Menus"); + +Menu::Menu(const QString &serviceName, const QString &objectPath, QObject *parent) + : QObject(parent) + , m_serviceName(serviceName) + , m_objectPath(objectPath) +{ + Q_ASSERT(!serviceName.isEmpty()); + Q_ASSERT(!m_objectPath.isEmpty()); + + if (!QDBusConnection::sessionBus() + .connect(m_serviceName, m_objectPath, s_orgGtkMenus, QStringLiteral("Changed"), this, SLOT(onMenuChanged(GMenuChangeList)))) { + qDebug() << "Failed to subscribe to menu changes for" << parent << "on" << serviceName << "at" << objectPath; + } +} + +Menu::~Menu() = default; + +void Menu::cleanup() +{ + stop(m_subscriptions); +} + +void Menu::start(uint id) +{ + if (m_subscriptions.contains(id)) { + return; + } + + // TODO watch service disappearing? + + // dbus-send --print-reply --session --dest=:1.103 /org/libreoffice/window/104857641/menus/menubar org.gtk.Menus.Start array:uint32:0 + + QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName, m_objectPath, 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) { + QScopedPointer watcherPtr(watcher); + + QDBusPendingReply reply = *watcherPtr; + if (reply.isError()) { + qDebug() << "Failed to start subscription to" << id << "on" << m_serviceName << "at" << m_objectPath << reply.error(); + emit failedToSubscribe(id); + } else { + const bool hadMenu = !m_menus.isEmpty(); + + const auto menus = reply.value(); + for (const auto &menu : menus) { + m_menus[menu.id].append(menus); + } + + // LibreOffice on startup fails to give us some menus right away, we'll also subscribe in onMenuChanged() if necessary + if (menus.isEmpty()) { + qDebug() << "Got an empty menu for" << id << "on" << m_serviceName << "at" << m_objectPath; + return; + } + + // TODO are we subscribed to all it returns or just to the ones we requested? + m_subscriptions.append(id); + + // do we have a menu now? let's tell everyone + if (!hadMenu && !m_menus.isEmpty()) { + emit menuAppeared(); + } + + emit subscribed(id); + } + }); +} + +void Menu::stop(const QList &ids) +{ + QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName, m_objectPath, 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()) { + qDebug() << "Failed to stop subscription to" << ids << "on" << m_serviceName << "at" << m_objectPath << reply.error(); + } else { + // remove all subscriptions that we unsubscribed from + // TODO is there a nicer algorithm for that? + // TODO remove all m_menus also? + m_subscriptions.erase( + std::remove_if(m_subscriptions.begin(), m_subscriptions.end(), std::bind(&QList::contains, m_subscriptions, std::placeholders::_1)), + m_subscriptions.end()); + + if (m_subscriptions.isEmpty()) { + emit menuDisappeared(); + } + } + watcher->deleteLater(); + }); +} + +bool Menu::hasMenu() const +{ + return !m_menus.isEmpty(); +} + +bool Menu::hasSubscription(uint subscription) const +{ + return m_subscriptions.contains(subscription); +} + +GMenuItem Menu::getSection(int id, bool *ok) const +{ + int subscription; + int section; + int index; + Utils::intToTreeStructure(id, subscription, section, index); + return getSection(subscription, section, ok); +} + +GMenuItem Menu::getSection(int subscription, int section, bool *ok) const +{ + const auto menu = m_menus.value(subscription); + + auto it = std::find_if(menu.begin(), menu.end(), [section](const GMenuItem &item) { + return item.section == section; + }); + + if (it == menu.end()) { + if (ok) { + *ok = false; + } + return GMenuItem(); + } + + if (ok) { + *ok = true; + } + return *it; +} + +QVariantMap Menu::getItem(int id) const +{ + int subscription; + int section; + int index; + Utils::intToTreeStructure(id, subscription, section, index); + return getItem(subscription, section, index); +} + +QVariantMap Menu::getItem(int subscription, int sectionId, int index) const +{ + bool ok; + const GMenuItem section = getSection(subscription, sectionId, &ok); + + if (!ok) { + return QVariantMap(); + } + + const auto items = section.items; + + if (items.count() < index) { + qDebug() << "Cannot get action" << subscription << sectionId << index << "which is out of bounds"; + return QVariantMap(); + } + + // 0 is the menu itself, items start at 1 + return items.at(index - 1); +} + +void Menu::onMenuChanged(const GMenuChangeList &changes) +{ + const bool hadMenu = !m_menus.isEmpty(); + + QVector dirtyMenus; + QVector dirtyItems; + + for (const auto &change : changes) { + auto updateSection = [&](GMenuItem §ion) { + // Check if the amount of inserted items is identical to the items to be removed, + // just update the existing items and signal a change for that. + // LibreOffice tends to do that e.g. to update its Undo menu entry + if (change.itemsToRemoveCount == change.itemsToInsert.count()) { + for (int i = 0; i < change.itemsToInsert.count(); ++i) { + const auto &newItem = change.itemsToInsert.at(i); + + section.items[change.changePosition + i] = newItem; + + // 0 is the menu itself, items start at 1 + dirtyItems.append(Utils::treeStructureToInt(change.subscription, change.menu, change.changePosition + i + 1)); + } + } else { + for (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.append(Utils::treeStructureToInt(change.subscription, change.menu, 0)); + } + }; + + // shouldn't happen, it says only Start() subscribes to changes + if (!m_subscriptions.contains(change.subscription)) { + qDebug() << "Got menu change for menu" << change.subscription << "that we are not subscribed to, subscribing now"; + // LibreOffice doesn't give us a menu right away but takes a while and then signals us a change + start(change.subscription); + continue; + } + + auto &menu = m_menus[change.subscription]; + + bool sectionFound = false; + // TODO findSectionRef + for (GMenuItem §ion : menu) { + if (section.section != change.menu) { + continue; + } + + qDebug() << "Updating existing section" << change.menu << "in subscription" << change.subscription; + + sectionFound = true; + updateSection(section); + break; + } + + // Insert new section + if (!sectionFound) { + qDebug() << "Creating new section" << change.menu << "in subscription" << change.subscription; + + if (change.itemsToRemoveCount > 0) { + qDebug() << "Menu change requested to remove items from a new (and as such empty) section"; + } + + GMenuItem newSection; + newSection.id = change.subscription; + newSection.section = change.menu; + updateSection(newSection); + menu.append(newSection); + } + } + + // do we have a menu now? let's tell everyone + if (!hadMenu && !m_menus.isEmpty()) { + emit menuAppeared(); + } else if (hadMenu && m_menus.isEmpty()) { + emit menuDisappeared(); + } + + if (!dirtyItems.isEmpty()) { + emit itemsChanged(dirtyItems); + } + + emit menusChanged(dirtyMenus); +} + +void Menu::actionsChanged(const QStringList &dirtyActions, const QString &prefix) +{ + auto forEachMenuItem = [this](const std::function &cb) { + for (auto it = m_menus.constBegin(), end = m_menus.constEnd(); it != end; ++it) { + const int subscription = it.key(); + + for (const auto &menu : it.value()) { + const int section = menu.section; + + int count = 0; + + const auto items = menu.items; + for (const auto &item : items) { + ++count; // 0 is a menu, entries start at 1 + + if (!cb(subscription, section, count, item)) { + goto loopExit; // hell yeah + break; + } + } + } + } + + loopExit: // loop exit + return; + }; + + // now find in which menus these actions are and emit a change accordingly + QVector dirtyItems; + + for (const QString &action : dirtyActions) { + const QString prefixedAction = prefix + action; + + forEachMenuItem([&prefixedAction, &dirtyItems](int subscription, int section, int index, const QVariantMap &item) { + const QString actionName = Utils::itemActionName(item); + + if (actionName == prefixedAction) { + dirtyItems.append(Utils::treeStructureToInt(subscription, section, index)); + return false; // break + } + + return true; // continue + }); + } + + if (!dirtyItems.isEmpty()) { + emit itemsChanged(dirtyItems); + } +} diff --git a/gmenuproxy/menu.h b/gmenuproxy/menu.h new file mode 100644 index 0000000..d19b666 --- /dev/null +++ b/gmenuproxy/menu.h @@ -0,0 +1,67 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include +#include +#include + +#include "./extend/dbusmenutypes_p.h" +#include "gdbusmenutypes_p.h" + +class Menu : public QObject +{ + Q_OBJECT + +public: + Menu(const QString &serviceName, const QString &objectPath, QObject *parent = nullptr); + ~Menu() override; + + void init(); + void cleanup(); + + void start(uint id); + void stop(const QList &ids); + + bool hasMenu() const; + bool hasSubscription(uint subscription) const; + + GMenuItem getSection(int id, bool *ok = nullptr) const; + GMenuItem getSection(int subscription, int sectionId, bool *ok = nullptr) const; + + QVariantMap getItem(int id) const; // bool ok argument? + QVariantMap getItem(int subscription, int sectionId, int id) const; + +public slots: + void actionsChanged(const QStringList &dirtyActions, const QString &prefix); + +Q_SIGNALS: + void menuAppeared(); // emitted the first time a menu was successfully loaded + void menuDisappeared(); + + void subscribed(uint id); + void failedToSubscribe(uint id); + + void itemsChanged(const QVector &itemIds); + void menusChanged(const QVector &menuIds); + +private slots: + void onMenuChanged(const GMenuChangeList &changes); + +private: + void initMenu(); + + void menuChanged(const GMenuChangeList &changes); + + // QSet? + QList m_subscriptions; // keeps track of which menu trees we're subscribed to + + QHash m_menus; + + QString m_serviceName; + QString m_objectPath; +}; diff --git a/gmenuproxy/menuproxy.cpp b/gmenuproxy/menuproxy.cpp new file mode 100644 index 0000000..09ceee6 --- /dev/null +++ b/gmenuproxy/menuproxy.cpp @@ -0,0 +1,384 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "menuproxy.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// #include +#include +// #include +#include + +#include +#include + +#include "window.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_unityObjectPath = QByteArrayLiteral("_UNITY_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"); + +static const QString s_gtkModules = QStringLiteral("gtk-modules"); +static const QString s_appMenuGtkModule = QStringLiteral("appmenu-gtk-module"); + +MenuProxy::MenuProxy() + : QObject() + , m_xConnection(QX11Info::connection()) + , m_serviceWatcher(new QDBusServiceWatcher(this)) + , m_gtk2RcWatch(new KDirWatch(this)) + , m_writeGtk2SettingsTimer(new QTimer(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); + qDebug() << "Global menu service became available, starting"; + init(); + }); + connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this](const QString &service) { + Q_UNUSED(service); + qDebug() << "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)) { + qDebug() << "Global menu service is running, starting right away"; + init(); + } else { + qDebug() << "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 + enableGtkSettings(false); + } + + // kde-gtk-config just deletes and re-creates the gtkrc-2.0, watch this and add our config to it again + m_writeGtk2SettingsTimer->setSingleShot(true); + m_writeGtk2SettingsTimer->setInterval(1000); + connect(m_writeGtk2SettingsTimer, &QTimer::timeout, this, &MenuProxy::writeGtk2Settings); + + auto startGtk2SettingsTimer = [this] { + if (!m_writeGtk2SettingsTimer->isActive()) { + m_writeGtk2SettingsTimer->start(); + } + }; + + connect(m_gtk2RcWatch, &KDirWatch::created, this, startGtk2SettingsTimer); + connect(m_gtk2RcWatch, &KDirWatch::dirty, this, startGtk2SettingsTimer); + m_gtk2RcWatch->addFile(gtkRc2Path()); +} + +MenuProxy::~MenuProxy() +{ + teardown(); +} + +bool MenuProxy::init() +{ + if (!QDBusConnection::sessionBus().registerService(s_ourServiceName)) { + qDebug() << "Failed to register DBus service" << s_ourServiceName; + return false; + } + + enableGtkSettings(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_windows.isEmpty()) { + qDebug() << "Up and running but no windows with menus in sight"; + } + + return true; +} + +void MenuProxy::teardown() +{ + enableGtkSettings(false); + + QDBusConnection::sessionBus().unregisterService(s_ourServiceName); + + disconnect(KWindowSystem::self(), &KWindowSystem::windowAdded, this, &MenuProxy::onWindowAdded); + disconnect(KWindowSystem::self(), &KWindowSystem::windowRemoved, this, &MenuProxy::onWindowRemoved); + + qDeleteAll(m_windows); + m_windows.clear(); +} + +void MenuProxy::enableGtkSettings(bool enable) +{ + m_enabled = enable; + + writeGtk2Settings(); + writeGtk3Settings(); + + // TODO use gconf/dconf directly or at least signal a change somehow? +} + +QString MenuProxy::gtkRc2Path() +{ + return QDir::homePath() + QLatin1String("/.gtkrc-2.0"); +} + +QString MenuProxy::gtk3SettingsIniPath() +{ + return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1String("/gtk-3.0/settings.ini"); +} + +void MenuProxy::writeGtk2Settings() +{ + QFile rcFile(gtkRc2Path()); + if (!rcFile.exists()) { + // Don't create it here, that would break writing default GTK-2.0 settings on first login, + // as the gtkbreeze kconf_update script only does so if it does not exist + return; + } + + qDebug() << "Writing gtkrc-2.0 to" << (m_enabled ? "enable" : "disable") << "global menu support"; + + if (!rcFile.open(QIODevice::ReadWrite | QIODevice::Text)) { + return; + } + + QByteArray content; + + QStringList gtkModules; + + while (!rcFile.atEnd()) { + const QByteArray rawLine = rcFile.readLine(); + + const QString line = QString::fromUtf8(rawLine.trimmed()); + + if (!line.startsWith(s_gtkModules)) { + // keep line as-is + content += rawLine; + continue; + } + + const int equalSignIdx = line.indexOf(QLatin1Char('=')); + if (equalSignIdx < 1) { + continue; + } + + gtkModules = line.mid(equalSignIdx + 1).split(QLatin1Char(':'), Qt::SkipEmptyParts); + + break; + } + + addOrRemoveAppMenuGtkModule(gtkModules); + + if (!gtkModules.isEmpty()) { + content += QStringLiteral("%1=%2").arg(s_gtkModules, gtkModules.join(QLatin1Char(':'))).toUtf8(); + } + + qDebug() << " gtk-modules:" << gtkModules; + + m_gtk2RcWatch->stopScan(); + + // now write the new contents of the file + rcFile.resize(0); + rcFile.write(content); + rcFile.close(); + + m_gtk2RcWatch->startScan(); +} + +void MenuProxy::writeGtk3Settings() +{ + qDebug() << "Writing gtk-3.0/settings.ini" << (m_enabled ? "enable" : "disable") << "global menu support"; + + // mostly taken from kde-gtk-config + QSettings cfg(gtk3SettingsIniPath(), QSettings::IniFormat); + // cfg.beginGroup("Settings"); + // auto cfg = KSharedConfig::openConfig(gtk3SettingsIniPath(), KConfig::NoGlobals); + // KConfigGroup group(cfg, "Settings"); + + // QStringList gtkModules = group.readEntry("gtk-modules", QString()).split(QLatin1Char(':'), Qt::SkipEmptyParts); + // addOrRemoveAppMenuGtkModule(gtkModules); + + // if (!gtkModules.isEmpty()) { + // group.writeEntry("gtk-modules", gtkModules.join(QLatin1Char(':'))); + // } else { + // group.deleteEntry("gtk-modules"); + // } + + // qDebug() << " gtk-modules:" << gtkModules; + + // if (m_enabled) { + // group.writeEntry("gtk-shell-shows-menubar", 1); + // } else { + // group.deleteEntry("gtk-shell-shows-menubar"); + // } + + qDebug() << " gtk-shell-shows-menubar:" << (m_enabled ? 1 : 0); + + cfg.sync(); +} + +void MenuProxy::addOrRemoveAppMenuGtkModule(QStringList &list) +{ + if (m_enabled && !list.contains(s_appMenuGtkModule)) { + list.append(s_appMenuGtkModule); + } else if (!m_enabled) { + list.removeAll(s_appMenuGtkModule); + } +} + +void MenuProxy::onWindowAdded(WId id) +{ + if (m_windows.contains(id)) { + return; + } + + KWindowInfo info(id, NET::WMWindowType); + + NET::WindowType wType = info.windowType(NET::NormalMask | NET::DesktopMask | NET::DockMask | NET::ToolbarMask | NET::MenuMask | NET::DialogMask + | NET::OverrideMask | NET::TopMenuMask | NET::UtilityMask | NET::SplashMask); + + // Only top level windows typically have a menu bar, dialogs, such as settings don't + if (wType != NET::Normal) { + qDebug() << "Ignoring window" << id << "of type" << wType; + return; + } + + const QString serviceName = QString::fromUtf8(getWindowPropertyString(id, s_gtkUniqueBusName)); + + if (serviceName.isEmpty()) { + return; + } + + const QString applicationObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkApplicationObjectPath)); + const QString unityObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_unityObjectPath)); + const QString windowObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkWindowObjectPath)); + + const QString applicationMenuObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkAppMenuObjectPath)); + const QString menuBarObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkMenuBarObjectPath)); + + if (applicationMenuObjectPath.isEmpty() && menuBarObjectPath.isEmpty()) { + return; + } + + Window *window = new Window(serviceName); + window->setWinId(id); + window->setApplicationObjectPath(applicationObjectPath); + window->setUnityObjectPath(unityObjectPath); + window->setWindowObjectPath(windowObjectPath); + window->setApplicationMenuObjectPath(applicationMenuObjectPath); + window->setMenuBarObjectPath(menuBarObjectPath); + m_windows.insert(id, window); + + connect(window, &Window::requestWriteWindowProperties, this, [this, window] { + Q_ASSERT(!window->proxyObjectPath().isEmpty()); + + writeWindowProperty(window->winId(), s_kdeNetWmAppMenuServiceName, s_ourServiceName.toUtf8()); + writeWindowProperty(window->winId(), s_kdeNetWmAppMenuObjectPath, window->proxyObjectPath().toUtf8()); + }); + connect(window, &Window::requestRemoveWindowProperties, this, [this, window] { + writeWindowProperty(window->winId(), s_kdeNetWmAppMenuServiceName, QByteArray()); + writeWindowProperty(window->winId(), s_kdeNetWmAppMenuObjectPath, QByteArray()); + }); + + window->init(); +} + +void MenuProxy::onWindowRemoved(WId id) +{ + // no need to cleanup() (which removes window properties) when the window is gone, delete right away + delete m_windows.take(id); +} + +QByteArray MenuProxy::getWindowPropertyString(WId id, const QByteArray &name) +{ + QByteArray value; + + auto atom = getAtom(name); + if (atom == XCB_ATOM_NONE) { + return value; + } + + // GTK properties aren't XCB_ATOM_STRING but a custom one + auto utf8StringAtom = getAtom(QByteArrayLiteral("UTF8_STRING")); + + static const long MAX_PROP_SIZE = 10000; + auto propertyCookie = xcb_get_property(m_xConnection, false, id, atom, utf8StringAtom, 0, MAX_PROP_SIZE); + QScopedPointer propertyReply(xcb_get_property_reply(m_xConnection, propertyCookie, nullptr)); + if (propertyReply.isNull()) { + qDebug() << "XCB property reply for atom" << name << "on" << id << "was null"; + return value; + } + + if (propertyReply->type == utf8StringAtom && 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, nullptr)); + if (!atomReply.isNull()) { + atom = atomReply->atom; + if (atom != XCB_ATOM_NONE) { + s_atoms.insert(name, atom); + } + } + } + + return atom; +} diff --git a/gmenuproxy/menuproxy.h b/gmenuproxy/menuproxy.h new file mode 100644 index 0000000..7ca4cb1 --- /dev/null +++ b/gmenuproxy/menuproxy.h @@ -0,0 +1,63 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include +#include +#include +#include // for WId + +#include + +class QDBusServiceWatcher; +class QTimer; + +class KDirWatch; + +class Window; + +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(); + + static QString gtkRc2Path(); + static QString gtk3SettingsIniPath(); + + void enableGtkSettings(bool enabled); + + void writeGtk2Settings(); + void writeGtk3Settings(); + + void addOrRemoveAppMenuGtkModule(QStringList &list); + + xcb_connection_t *m_xConnection; + + 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_windows; + + QDBusServiceWatcher *m_serviceWatcher; + + KDirWatch *m_gtk2RcWatch; + QTimer *m_writeGtk2SettingsTimer; + + bool m_enabled = false; +}; diff --git a/gmenuproxy/utils.cpp b/gmenuproxy/utils.cpp new file mode 100644 index 0000000..9517e6f --- /dev/null +++ b/gmenuproxy/utils.cpp @@ -0,0 +1,29 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "utils.h" + +int Utils::treeStructureToInt(int subscription, int section, int index) +{ + return subscription * 1000000 + section * 1000 + index; +} + +void Utils::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; +} + +QString Utils::itemActionName(const QVariantMap &item) +{ + QString actionName = item.value(QStringLiteral("action")).toString(); + if (actionName.isEmpty()) { + actionName = item.value(QStringLiteral("submenu-action")).toString(); + } + return actionName; +} diff --git a/gmenuproxy/utils.h b/gmenuproxy/utils.h new file mode 100644 index 0000000..513783d --- /dev/null +++ b/gmenuproxy/utils.h @@ -0,0 +1,19 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include +#include + +namespace Utils +{ +int treeStructureToInt(int subscription, int section, int index); +void intToTreeStructure(int source, int &subscription, int §ion, int &index); + +QString itemActionName(const QVariantMap &item); + +} diff --git a/gmenuproxy/window.cpp b/gmenuproxy/window.cpp new file mode 100644 index 0000000..ca5762e --- /dev/null +++ b/gmenuproxy/window.cpp @@ -0,0 +1,653 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "window.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "actions.h" +#include "dbusmenuadaptor.h" +#include "icons.h" +#include "menu.h" +#include "utils.h" + +#include "./extend/dbusmenushortcut_p.h" + +static const QString s_orgGtkActions = QStringLiteral("org.gtk.Actions"); +static const QString s_orgGtkMenus = QStringLiteral("org.gtk.Menus"); + +static const QString s_applicationActionsPrefix = QStringLiteral("app."); +static const QString s_unityActionsPrefix = QStringLiteral("unity."); +static const QString s_windowActionsPrefix = QStringLiteral("win."); + +Window::Window(const QString &serviceName) + : QObject() + , m_serviceName(serviceName) +{ + qDebug() << "Created menu on" << serviceName; + + Q_ASSERT(!serviceName.isEmpty()); + + GDBusMenuTypes_register(); + DBusMenuTypes_register(); +} + +Window::~Window() = default; + +void Window::init() +{ + qDebug() << "Inited window with menu for" << m_winId << "on" << m_serviceName << "at app" << m_applicationObjectPath << "win" + << m_windowObjectPath << "unity" << m_unityObjectPath; + + if (!m_applicationMenuObjectPath.isEmpty()) { + m_applicationMenu = new Menu(m_serviceName, m_applicationMenuObjectPath, this); + connect(m_applicationMenu, &Menu::menuAppeared, this, &Window::updateWindowProperties); + connect(m_applicationMenu, &Menu::menuDisappeared, this, &Window::updateWindowProperties); + connect(m_applicationMenu, &Menu::subscribed, this, &Window::onMenuSubscribed); + // basically so it replies on DBus no matter what + connect(m_applicationMenu, &Menu::failedToSubscribe, this, &Window::onMenuSubscribed); + connect(m_applicationMenu, &Menu::itemsChanged, this, &Window::menuItemsChanged); + connect(m_applicationMenu, &Menu::menusChanged, this, &Window::menuChanged); + } + + if (!m_menuBarObjectPath.isEmpty()) { + m_menuBar = new Menu(m_serviceName, m_menuBarObjectPath, this); + connect(m_menuBar, &Menu::menuAppeared, this, &Window::updateWindowProperties); + connect(m_menuBar, &Menu::menuDisappeared, this, &Window::updateWindowProperties); + connect(m_menuBar, &Menu::subscribed, this, &Window::onMenuSubscribed); + connect(m_menuBar, &Menu::failedToSubscribe, this, &Window::onMenuSubscribed); + connect(m_menuBar, &Menu::itemsChanged, this, &Window::menuItemsChanged); + connect(m_menuBar, &Menu::menusChanged, this, &Window::menuChanged); + } + + if (!m_applicationObjectPath.isEmpty()) { + m_applicationActions = new Actions(m_serviceName, m_applicationObjectPath, this); + connect(m_applicationActions, &Actions::actionsChanged, this, [this](const QStringList &dirtyActions) { + onActionsChanged(dirtyActions, s_applicationActionsPrefix); + }); + connect(m_applicationActions, &Actions::loaded, this, [this] { + if (m_menuInited) { + onActionsChanged(m_applicationActions->getAll().keys(), s_applicationActionsPrefix); + } else { + initMenu(); + } + }); + m_applicationActions->load(); + } + + if (!m_unityObjectPath.isEmpty()) { + m_unityActions = new Actions(m_serviceName, m_unityObjectPath, this); + connect(m_unityActions, &Actions::actionsChanged, this, [this](const QStringList &dirtyActions) { + onActionsChanged(dirtyActions, s_unityActionsPrefix); + }); + connect(m_unityActions, &Actions::loaded, this, [this] { + if (m_menuInited) { + onActionsChanged(m_unityActions->getAll().keys(), s_unityActionsPrefix); + } else { + initMenu(); + } + }); + m_unityActions->load(); + } + + if (!m_windowObjectPath.isEmpty()) { + m_windowActions = new Actions(m_serviceName, m_windowObjectPath, this); + connect(m_windowActions, &Actions::actionsChanged, this, [this](const QStringList &dirtyActions) { + onActionsChanged(dirtyActions, s_windowActionsPrefix); + }); + connect(m_windowActions, &Actions::loaded, this, [this] { + if (m_menuInited) { + onActionsChanged(m_windowActions->getAll().keys(), s_windowActionsPrefix); + } else { + initMenu(); + } + }); + m_windowActions->load(); + } +} + +WId Window::winId() const +{ + return m_winId; +} + +void Window::setWinId(WId winId) +{ + m_winId = winId; +} + +QString Window::serviceName() const +{ + return m_serviceName; +} + +QString Window::applicationObjectPath() const +{ + return m_applicationObjectPath; +} + +void Window::setApplicationObjectPath(const QString &applicationObjectPath) +{ + m_applicationObjectPath = applicationObjectPath; +} + +QString Window::unityObjectPath() const +{ + return m_unityObjectPath; +} + +void Window::setUnityObjectPath(const QString &unityObjectPath) +{ + m_unityObjectPath = unityObjectPath; +} + +QString Window::applicationMenuObjectPath() const +{ + return m_applicationMenuObjectPath; +} + +void Window::setApplicationMenuObjectPath(const QString &applicationMenuObjectPath) +{ + m_applicationMenuObjectPath = applicationMenuObjectPath; +} + +QString Window::menuBarObjectPath() const +{ + return m_menuBarObjectPath; +} + +void Window::setMenuBarObjectPath(const QString &menuBarObjectPath) +{ + m_menuBarObjectPath = menuBarObjectPath; +} + +QString Window::windowObjectPath() const +{ + return m_windowObjectPath; +} + +void Window::setWindowObjectPath(const QString &windowObjectPath) +{ + m_windowObjectPath = windowObjectPath; +} + +QString Window::currentMenuObjectPath() const +{ + return m_currentMenuObjectPath; +} + +QString Window::proxyObjectPath() const +{ + return m_proxyObjectPath; +} + +void Window::initMenu() +{ + if (m_menuInited) { + return; + } + + if (!registerDBusObject()) { + return; + } + + // appmenu-gtk-module always announces a menu bar on every GTK window even if there is none + // so we subscribe to the menu bar as soon as it shows up so we can figure out + // if we have a menu bar, an app menu, or just nothing + if (m_applicationMenu) { + m_applicationMenu->start(0); + } + + if (m_menuBar) { + m_menuBar->start(0); + } + + m_menuInited = true; +} + +void Window::menuItemsChanged(const QVector &itemIds) +{ + if (qobject_cast(sender()) != m_currentMenu) { + return; + } + + DBusMenuItemList items; + + for (uint id : itemIds) { + const auto newItem = m_currentMenu->getItem(id); + + DBusMenuItem dBusItem{// 0 is menu, items start at 1 + static_cast(id), + gMenuToDBusMenuProperties(newItem)}; + items.append(dBusItem); + } + + emit ItemsPropertiesUpdated(items, {}); +} + +void Window::menuChanged(const QVector &menuIds) +{ + if (qobject_cast(sender()) != m_currentMenu) { + return; + } + + for (uint menu : menuIds) { + emit LayoutUpdated(3 /*revision*/, menu); + } +} + +void Window::onMenuSubscribed(uint id) +{ + // When it was a delayed GetLayout request, send the reply now + const auto pendingReplies = m_pendingGetLayouts.values(id); + if (!pendingReplies.isEmpty()) { + for (const auto &pendingReply : pendingReplies) { + if (pendingReply.type() != QDBusMessage::InvalidMessage) { + auto reply = pendingReply.createReply(); + + DBusMenuLayoutItem item; + uint revision = GetLayout(Utils::treeStructureToInt(id, 0, 0), 0, {}, item); + + reply << revision << QVariant::fromValue(item); + + QDBusConnection::sessionBus().send(reply); + } + } + m_pendingGetLayouts.remove(id); + } else { + emit LayoutUpdated(2 /*revision*/, id); + } +} + +bool Window::getAction(const QString &name, GMenuAction &action) const +{ + QString lookupName; + Actions *actions = getActionsForAction(name, lookupName); + + if (!actions) { + return false; + } + + return actions->get(lookupName, action); +} + +void Window::triggerAction(const QString &name, const QVariant &target, uint timestamp) +{ + QString lookupName; + Actions *actions = getActionsForAction(name, lookupName); + if (!actions) { + return; + } + + actions->trigger(lookupName, target, timestamp); +} + +Actions *Window::getActionsForAction(const QString &name, QString &lookupName) const +{ + if (name.startsWith(QLatin1String("app."))) { + lookupName = name.mid(4); + return m_applicationActions; + } else if (name.startsWith(QLatin1String("unity."))) { + lookupName = name.mid(6); + return m_unityActions; + } else if (name.startsWith(QLatin1String("win."))) { + lookupName = name.mid(4); + return m_windowActions; + } + + return nullptr; +} + +void Window::onActionsChanged(const QStringList &dirty, const QString &prefix) +{ + if (m_applicationMenu) { + m_applicationMenu->actionsChanged(dirty, prefix); + } + if (m_menuBar) { + m_menuBar->actionsChanged(dirty, prefix); + } +} + +bool Window::registerDBusObject() +{ + Q_ASSERT(m_proxyObjectPath.isEmpty()); + + static int menus = 0; + ++menus; + + new DbusmenuAdaptor(this); + + const QString objectPath = QStringLiteral("/MenuBar/%1").arg(QString::number(menus)); + qDebug() << "Registering DBus object path" << objectPath; + + if (!QDBusConnection::sessionBus().registerObject(objectPath, this)) { + qDebug() << "Failed to register object"; + return false; + } + + m_proxyObjectPath = objectPath; + + return true; +} + +void Window::updateWindowProperties() +{ + const bool hasMenu = ((m_applicationMenu && m_applicationMenu->hasMenu()) || (m_menuBar && m_menuBar->hasMenu())); + + if (!hasMenu) { + emit requestRemoveWindowProperties(); + return; + } + + Menu *oldMenu = m_currentMenu; + Menu *newMenu = qobject_cast(sender()); + // set current menu as needed + if (!m_currentMenu) { + m_currentMenu = newMenu; + // Menu Bar takes precedence over application menu + } else if (m_currentMenu == m_applicationMenu && newMenu == m_menuBar) { + qDebug() << "Switching from application menu to menu bar"; + m_currentMenu = newMenu; + // TODO update layout + } + + if (m_currentMenu != oldMenu) { + // update entire menu now + emit LayoutUpdated(4 /*revision*/, 0); + } + + emit requestWriteWindowProperties(); +} + +// DBus +bool Window::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 Window::Event(int id, const QString &eventId, const QDBusVariant &data, uint timestamp) +{ + Q_UNUSED(data); + + if (!m_currentMenu) { + return; + } + + // GMenu dbus doesn't have any "opened" or "closed" signals, we'll only handle "clicked" + + if (eventId == QLatin1String("clicked")) { + const QVariantMap item = m_currentMenu->getItem(id); + const QString action = item.value(QStringLiteral("action")).toString(); + const QVariant target = item.value(QStringLiteral("target")); + if (!action.isEmpty()) { + triggerAction(action, target, timestamp); + } + } +} + +DBusMenuItemList Window::GetGroupProperties(const QList &ids, const QStringList &propertyNames) +{ + Q_UNUSED(ids); + Q_UNUSED(propertyNames); + return DBusMenuItemList(); +} + +uint Window::GetLayout(int parentId, int recursionDepth, const QStringList &propertyNames, DBusMenuLayoutItem &dbusItem) +{ + Q_UNUSED(recursionDepth); // TODO + Q_UNUSED(propertyNames); + + int subscription; + int sectionId; + int index; + + Utils::intToTreeStructure(parentId, subscription, sectionId, index); + + if (!m_currentMenu) { + return 1; + } + + if (!m_currentMenu->hasSubscription(subscription)) { + // let's serve multiple similar requests in one go once we've processed them + m_pendingGetLayouts.insertMulti(subscription, message()); + setDelayedReply(true); + + m_currentMenu->start(subscription); + return 1; + } + + bool ok; + const GMenuItem section = m_currentMenu->getSection(subscription, sectionId, &ok); + + if (!ok) { + qDebug() << "There is no section on" << subscription << "at" << sectionId << "with" << parentId; + return 1; + } + + // If a particular entry is requested, see what it is and resolve as necessary + // 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 + if (section.items.count() < index) { + qDebug() << "Requested index" << index << "on" << subscription << "at" << sectionId << "with" << parentId << "is out of bounds"; + return 0; + } + + const auto &requestedItem = section.items.at(index - 1); + + auto it = requestedItem.constFind(QStringLiteral(":submenu")); + if (it != requestedItem.constEnd()) { + const GMenuSection gmenuSection = qdbus_cast(it->value()); + return GetLayout(Utils::treeStructureToInt(gmenuSection.subscription, gmenuSection.menu, 0), recursionDepth, propertyNames, dbusItem); + } else { + // TODO + return 0; + } + } + + dbusItem.id = parentId; // TODO + dbusItem.properties = {{QStringLiteral("children-display"), QStringLiteral("submenu")}}; + + int count = 0; + + const auto itemsToBeAdded = section.items; + for (const auto &item : itemsToBeAdded) { + DBusMenuLayoutItem child{ + Utils::treeStructureToInt(section.id, sectionId, ++count), + gMenuToDBusMenuProperties(item), + {} // children + }; + dbusItem.children.append(child); + + // Now resolve section aliases + auto it = item.constFind(QStringLiteral(":section")); + if (it != item.constEnd()) { + // references another place, add it instead + GMenuSection gmenuSection = qdbus_cast(it->value()); + + // remember where the item came from and give it an appropriate ID + // so updates signalled by the app will map to the right place + int originalSubscription = gmenuSection.subscription; + int originalMenu = gmenuSection.menu; + + // TODO start subscription if we don't have it + auto items = m_currentMenu->getSection(gmenuSection.subscription, gmenuSection.menu).items; + + // 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 = m_currentMenu->getSection(gmenuSection2.subscription, gmenuSection2.menu).items; + + originalSubscription = gmenuSection2.subscription; + originalMenu = gmenuSection2.menu; + } + } + + int aliasedCount = 0; + for (const auto &aliasedItem : qAsConst(items)) { + DBusMenuLayoutItem aliasedChild{ + Utils::treeStructureToInt(originalSubscription, originalMenu, ++aliasedCount), + gMenuToDBusMenuProperties(aliasedItem), + {} // children + }; + dbusItem.children.append(aliasedChild); + } + } + } + + // revision, unused in libdbusmenuqt + return 1; +} + +QDBusVariant Window::GetProperty(int id, const QString &property) +{ + Q_UNUSED(id); + Q_UNUSED(property); + QDBusVariant value; + return value; +} + +QString Window::status() const +{ + return QStringLiteral("normal"); +} + +uint Window::version() const +{ + return 4; +} + +QVariantMap Window::gMenuToDBusMenuProperties(const QVariantMap &source) const +{ + QVariantMap result; + + result.insert(QStringLiteral("label"), source.value(QStringLiteral("label")).toString()); + + if (source.contains(QLatin1String(":section"))) { + result.insert(QStringLiteral("type"), QStringLiteral("separator")); + } + + const bool isMenu = source.contains(QLatin1String(":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 = Utils::itemActionName(source); + + GMenuAction action; + // if no action is specified this is fine but if there is an action we don't have + // disable the menu entry + bool actionOk = true; + if (!actionName.isEmpty()) { + actionOk = getAction(actionName, action); + enabled = actionOk && action.enabled; + } + + // we used to only send this if not enabled but then dbusmenuimporter does not + // update the enabled state when it changes from disabled to enabled + result.insert(QStringLiteral("enabled"), enabled); + + 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; + } + + result.insert(QStringLiteral("visible"), visible); + + QString icon = source.value(QStringLiteral("icon")).toString(); + if (icon.isEmpty()) { + icon = source.value(QStringLiteral("verb-icon")).toString(); + } + + icon = Icons::actionIcon(actionName); + if (!icon.isEmpty()) { + result.insert(QStringLiteral("icon-name"), icon); + } + + const QVariant target = source.value(QStringLiteral("target")); + + if (actionOk) { + const auto actionStates = action.state; + if (actionStates.count() == 1) { + const auto &actionState = actionStates.first(); + // assume this is a checkbox + if (!isMenu) { + if (actionState.type() == QVariant::Bool) { + result.insert(QStringLiteral("toggle-type"), QStringLiteral("checkbox")); + result.insert(QStringLiteral("toggle-state"), actionState.toBool() ? 1 : 0); + } else if (actionState.type() == QVariant::String) { + result.insert(QStringLiteral("toggle-type"), QStringLiteral("radio")); + result.insert(QStringLiteral("toggle-state"), actionState == target ? 1 : 0); + } + } + } + } + + return result; +} diff --git a/gmenuproxy/window.h b/gmenuproxy/window.h new file mode 100644 index 0000000..d4cadca --- /dev/null +++ b/gmenuproxy/window.h @@ -0,0 +1,126 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include +#include +#include +#include +#include // for WId + +#include + +#include "./extend/dbusmenutypes_p.h" +#include "gdbusmenutypes_p.h" + +class QDBusVariant; + +class Actions; +class Menu; + +class Window : public QObject, protected QDBusContext +{ + Q_OBJECT + + // DBus + Q_PROPERTY(QString Status READ status) + Q_PROPERTY(uint Version READ version) + +public: + Window(const QString &serviceName); + ~Window() override; + + void init(); + + WId winId() const; + void setWinId(WId winId); + + QString serviceName() const; + + QString applicationObjectPath() const; + void setApplicationObjectPath(const QString &applicationObjectPath); + + QString unityObjectPath() const; + void setUnityObjectPath(const QString &unityObjectPath); + + QString windowObjectPath() const; + void setWindowObjectPath(const QString &windowObjectPath); + + QString applicationMenuObjectPath() const; + void setApplicationMenuObjectPath(const QString &applicationMenuObjectPath); + + QString menuBarObjectPath() const; + void setMenuBarObjectPath(const QString &menuBarObjectPath); + + QString currentMenuObjectPath() 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; + +Q_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: + void initMenu(); + + bool registerDBusObject(); + void updateWindowProperties(); + + bool getAction(const QString &name, GMenuAction &action) const; + void triggerAction(const QString &name, const QVariant &target, uint timestamp = 0); + Actions *getActionsForAction(const QString &name, QString &lookupName) const; + + void menuChanged(const QVector &menuIds); + void menuItemsChanged(const QVector &itemIds); + + void onActionsChanged(const QStringList &dirty, const QString &prefix); + void onMenuSubscribed(uint id); + + QVariantMap gMenuToDBusMenuProperties(const QVariantMap &source) const; + + WId m_winId = 0; + QString m_serviceName; // original GMenu service (the gtk app) + + QString m_applicationObjectPath; + QString m_unityObjectPath; + QString m_windowObjectPath; + QString m_applicationMenuObjectPath; + QString m_menuBarObjectPath; + + QString m_currentMenuObjectPath; + + QString m_proxyObjectPath; // our object path on this proxy app + + QHash m_pendingGetLayouts; + + Menu *m_applicationMenu = nullptr; + Menu *m_menuBar = nullptr; + + Menu *m_currentMenu = nullptr; + + Actions *m_applicationActions = nullptr; + Actions *m_unityActions = nullptr; + Actions *m_windowActions = nullptr; + + bool m_menuInited = false; +}; diff --git a/session/application.cpp b/session/application.cpp index 2821fb4..e0a2cb0 100644 --- a/session/application.cpp +++ b/session/application.cpp @@ -21,6 +21,7 @@ #include "sessionadaptor.h" // Qt +#include #include #include #include @@ -82,7 +83,8 @@ void setEnvironmentVariable(const QByteArray &name, const QByteArray &value) Application::Application(int &argc, char **argv) : QApplication(argc, argv) - , m_processManager(new ProcessManager) + , m_processManager(new ProcessManager(this)) + , m_wayland(false) { new SessionAdaptor(this); @@ -90,6 +92,16 @@ Application::Application(int &argc, char **argv) QDBusConnection::sessionBus().registerService(QStringLiteral("org.cutefish.Session")); QDBusConnection::sessionBus().registerObject(QStringLiteral("/Session"), this); + QCommandLineParser parser; + parser.setApplicationDescription(QStringLiteral("Cutefish Session")); + parser.addHelpOption(); + + QCommandLineOption waylandOption(QStringList() << "w" << "wayland" << "Wayland Mode"); + parser.addOption(waylandOption); + parser.process(*this); + + m_wayland = parser.isSet(waylandOption); + createConfigDirectory(); initKWinConfig(); initLanguage(); @@ -116,6 +128,11 @@ Application::Application(int &argc, char **argv) QTimer::singleShot(100, m_processManager, &ProcessManager::start); } +bool Application::wayland() const +{ + return m_wayland; +} + void Application::initEnvironments() { // Set defaults @@ -232,7 +249,7 @@ void Application::initKWinConfig() settings.beginGroup("Effect-Blur"); settings.setValue("BlurStrength", 7); - settings.setValue("NoiseStrength", 1); + settings.setValue("NoiseStrength", 0); settings.endGroup(); settings.beginGroup("Windows"); diff --git a/session/application.h b/session/application.h index f1a1e72..6a2f83f 100644 --- a/session/application.h +++ b/session/application.h @@ -32,6 +32,7 @@ class Application : public QApplication public: explicit Application(int &argc, char **argv); + bool wayland() const; public slots: void logout() @@ -75,6 +76,8 @@ private: private: ProcessManager *m_processManager; Power m_power; + + bool m_wayland; }; #endif // APPLICATION_H diff --git a/session/processmanager.cpp b/session/processmanager.cpp index 8907874..bf779ce 100644 --- a/session/processmanager.cpp +++ b/session/processmanager.cpp @@ -18,6 +18,7 @@ */ #include "processmanager.h" +#include "application.h" #include #include @@ -35,8 +36,9 @@ #include #include -ProcessManager::ProcessManager(QObject *parent) +ProcessManager::ProcessManager(Application *app, QObject *parent) : QObject(parent) + , m_app(app) , m_wmStarted(false) , m_waitLoop(nullptr) { @@ -96,14 +98,17 @@ void ProcessManager::logout() void ProcessManager::startWindowManager() { QProcess *wmProcess = new QProcess; - wmProcess->start("kwin_x11", QStringList()); - - QEventLoop waitLoop; - m_waitLoop = &waitLoop; - // add a timeout to avoid infinite blocking if a WM fail to execute. - QTimer::singleShot(30 * 1000, &waitLoop, SLOT(quit())); - waitLoop.exec(); - m_waitLoop = nullptr; + + wmProcess->start(m_app->wayland() ? "kwin_wayland" : "kwin_x11", QStringList()); + + if (!m_app->wayland()) { + QEventLoop waitLoop; + m_waitLoop = &waitLoop; + // add a timeout to avoid infinite blocking if a WM fail to execute. + QTimer::singleShot(30 * 1000, &waitLoop, SLOT(quit())); + waitLoop.exec(); + m_waitLoop = nullptr; + } } void ProcessManager::startDesktopProcess() @@ -143,6 +148,7 @@ void ProcessManager::startDaemonProcess() list << qMakePair(QString("cutefish-settings-daemon"), QStringList()); list << qMakePair(QString("cutefish-powerman"), QStringList()); list << qMakePair(QString("cutefish-xembedsniproxy"), QStringList()); + list << qMakePair(QString("cutefish-gmenuproxy"), QStringList()); list << qMakePair(QString("chotkeys"), QStringList()); for (QPair pair : list) { diff --git a/session/processmanager.h b/session/processmanager.h index 8054c3c..e78974e 100644 --- a/session/processmanager.h +++ b/session/processmanager.h @@ -26,12 +26,13 @@ #include #include +class Application; class ProcessManager : public QObject, public QAbstractNativeEventFilter { Q_OBJECT public: - explicit ProcessManager(QObject *parent = nullptr); + explicit ProcessManager(Application *app, QObject *parent = nullptr); ~ProcessManager(); void start(); @@ -45,6 +46,7 @@ public: bool nativeEventFilter(const QByteArray & eventType, void * message, long * result) override; private: + Application *m_app; QMap m_systemProcess; QMap m_autoStartProcess; diff --git a/shutdown-ui/IconButton.qml b/shutdown-ui/IconButton.qml index b571e6a..1469e16 100644 --- a/shutdown-ui/IconButton.qml +++ b/shutdown-ui/IconButton.qml @@ -62,12 +62,6 @@ Item { color: "white" opacity: mouseArea.pressed ? 0.1 : mouseArea.containsMouse || control.checked ? 0.2 : 0 radius: height / 2 - - Behavior on opacity { - NumberAnimation { - duration: 100 - } - } } Image {