Add gmenuproxy

pull/15/head
reionwong 5 years ago
parent 1798bda745
commit b36ef8a90f

@ -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)

@ -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 <kde@broulik.de>
#
# 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"
)

@ -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)

@ -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/)

@ -0,0 +1,196 @@
/*
SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <kde@privat.broulik.de>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "actions.h"
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusPendingCallWatcher>
#include <QDBusPendingReply>
#include <QDebug>
#include <QStringList>
#include <QVariantList>
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<GMenuActionMap> reply = QDBusConnection::sessionBus().asyncCall(msg);
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) {
QDBusPendingReply<GMenuActionMap> 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<void> reply = QDBusConnection::sessionBus().asyncCall(msg);
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, name](QDBusPendingCallWatcher *watcher) {
QDBusPendingReply<void> 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);
}
}

@ -0,0 +1,45 @@
/*
SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <kde@privat.broulik.de>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#pragma once
#include <QObject>
#include <QString>
#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;
};

@ -0,0 +1,49 @@
<interface name="com.canonical.dbusmenu">
<property name="Version" type="u" access="read"/>
<property name="Status" type="s" access="read"/>
<signal name="ItemsPropertiesUpdated">
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="DBusMenuItemList"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="DBusMenuItemKeysList"/>
<arg type="a(ia{sv})" direction="out"/>
<arg type="a(ias)" direction="out"/>
</signal>
<signal name="LayoutUpdated">
<arg name="revision" type="u" direction="out"/>
<arg name="parentId" type="i" direction="out"/>
</signal>
<signal name="ItemActivationRequested">
<arg name="id" type="i" direction="out"/>
<arg name="timeStamp" type="u" direction="out"/>
</signal>
<method name="Event">
<arg name="id" type="i" direction="in"/>
<arg name="eventId" type="s" direction="in"/>
<arg name="data" type="v" direction="in"/>
<arg name="timestamp" type="u" direction="in"/>
<annotation name="org.freedesktop.DBus.Method.NoReply" value="true"/>
</method>
<method name="GetProperty">
<arg type="v" direction="out"/>
<arg name="id" type="i" direction="in"/>
<arg name="property" type="s" direction="in"/>
</method>
<method name="GetLayout">
<arg type="u" direction="out"/>
<arg name="parentId" type="i" direction="in"/>
<arg name="recursionDepth" type="i" direction="in"/>
<arg name="propertyNames" type="as" direction="in"/>
<arg name="item" type="(ia{sv}av)" direction="out"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="DBusMenuLayoutItem"/>
</method>
<method name="GetGroupProperties">
<arg type="a(ia{sv})" direction="out"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="DBusMenuItemList"/>
<arg name="ids" type="ai" direction="in"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="QList&lt;int&gt;"/>
<arg name="propertyNames" type="as" direction="in"/>
</method>
<method name="AboutToShow">
<arg type="b" direction="out"/>
<arg name="id" type="i" direction="in"/>
</method>
</interface>

@ -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

@ -0,0 +1,69 @@
/* This file is part of the dbusmenu-qt library
SPDX-FileCopyrightText: 2009 Canonical
SPDX-FileContributor: Aurelien Gateau <aurelien.gateau@canonical.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "dbusmenushortcut_p.h"
// Qt
#include <QKeySequence>
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);
}

@ -0,0 +1,22 @@
/* This file is part of the dbusmenu-qt library
SPDX-FileCopyrightText: 2009 Canonical
SPDX-FileContributor: Aurelien Gateau <aurelien.gateau@canonical.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#pragma once
// Qt
#include <QMetaType>
#include <QStringList>
class QKeySequence;
class DBusMenuShortcut : public QList<QStringList>
{
public:
QKeySequence toKeySequence() const;
static DBusMenuShortcut fromKeySequence(const QKeySequence &);
};
Q_DECLARE_METATYPE(DBusMenuShortcut)

@ -0,0 +1,122 @@
/* This file is part of the dbusmenu-qt library
SPDX-FileCopyrightText: 2009 Canonical
SPDX-FileContributor: Aurelien Gateau <aurelien.gateau@canonical.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "dbusmenutypes_p.h"
// Local
#include "dbusmenushortcut_p.h"
// Qt
#include <QDBusArgument>
#include <QDBusMetaType>
//// 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<QDBusVariant>());
Q_FOREACH (const DBusMenuLayoutItem &child, obj.children) {
argument << QDBusVariant(QVariant::fromValue<DBusMenuLayoutItem>(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<QDBusArgument>();
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<QStringList>());
typename QList<QStringList>::ConstIterator it = obj.constBegin();
typename QList<QStringList>::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<DBusMenuItem>();
qDBusRegisterMetaType<DBusMenuItemList>();
qDBusRegisterMetaType<DBusMenuItemKeys>();
qDBusRegisterMetaType<DBusMenuItemKeysList>();
qDBusRegisterMetaType<DBusMenuLayoutItem>();
qDBusRegisterMetaType<DBusMenuLayoutItemList>();
qDBusRegisterMetaType<DBusMenuShortcut>();
registered = true;
}

@ -0,0 +1,80 @@
/* This file is part of the dbusmenu-qt library
SPDX-FileCopyrightText: 2009 Canonical
SPDX-FileContributor: Aurelien Gateau <aurelien.gateau@canonical.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#pragma once
// Qt
#include <QList>
#include <QStringList>
#include <QVariant>
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<DBusMenuItem> 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<DBusMenuItemKeys> 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<DBusMenuLayoutItem> children;
};
Q_DECLARE_METATYPE(DBusMenuLayoutItem)
QDBusArgument &operator<<(QDBusArgument &argument, const DBusMenuLayoutItem &);
const QDBusArgument &operator>>(const QDBusArgument &argument, DBusMenuLayoutItem &);
typedef QList<DBusMenuLayoutItem> DBusMenuLayoutItemList;
Q_DECLARE_METATYPE(DBusMenuLayoutItemList)
//// DBusMenuShortcut
class DBusMenuShortcut;
QDBusArgument &operator<<(QDBusArgument &argument, const DBusMenuShortcut &);
const QDBusArgument &operator>>(const QDBusArgument &argument, DBusMenuShortcut &);
void DBusMenuTypes_register();

@ -0,0 +1,119 @@
/*
SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <kde@privat.broulik.de>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "gdbusmenutypes_p.h"
#include <QDBusArgument>
#include <QDBusMetaType>
// 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<GMenuItem>();
qDBusRegisterMetaType<GMenuItemList>();
qDBusRegisterMetaType<GMenuSection>();
qDBusRegisterMetaType<GMenuChange>();
qDBusRegisterMetaType<GMenuChangeList>();
qDBusRegisterMetaType<GMenuAction>();
qDBusRegisterMetaType<GMenuActionMap>();
qDBusRegisterMetaType<GMenuActionsChange>();
qDBusRegisterMetaType<StringBoolMap>();
registered = true;
}

@ -0,0 +1,89 @@
/*
SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <kde@privat.broulik.de>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#pragma once
#include <QDBusSignature>
#include <QList>
#include <QMap>
#include <QVariant>
class QDBusArgument;
// Various
using VariantMapList = QList<QVariantMap>;
Q_DECLARE_METATYPE(VariantMapList);
using StringBoolMap = QMap<QString, bool>;
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<GMenuItem>;
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<GMenuChange>;
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<QString, GMenuAction>;
Q_DECLARE_METATYPE(GMenuActionMap);
struct GMenuActionsChange {
QStringList removed;
QMap<QString, bool> 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();

@ -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

@ -0,0 +1,308 @@
/*
SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <kde@privat.broulik.de>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "icons.h"
#include <QHash>
#include <QRegularExpression>
QString Icons::actionIcon(const QString &actionName)
{
QString icon;
QString action = actionName;
if (action.isEmpty()) {
return icon;
}
static const QHash<QString, QString> 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;
}

@ -0,0 +1,15 @@
/*
SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <kde@privat.broulik.de>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#pragma once
#include <QString>
namespace Icons
{
QString actionIcon(const QString &actionName);
}

@ -0,0 +1,37 @@
/*
SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <kde@privat.broulik.de>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include <QGuiApplication>
#include <QSessionManager>
#include <KWindowSystem>
#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();
}

@ -0,0 +1,326 @@
/*
SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <kde@privat.broulik.de>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "menu.h"
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusPendingCallWatcher>
#include <QDBusPendingReply>
#include <QDebug>
#include <QVariantList>
#include <algorithm>
#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<uint>{id})});
QDBusPendingReply<GMenuItemList> reply = QDBusConnection::sessionBus().asyncCall(msg);
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id](QDBusPendingCallWatcher *watcher) {
QScopedPointer<QDBusPendingCallWatcher, QScopedPointerDeleteLater> watcherPtr(watcher);
QDBusPendingReply<GMenuItemList> 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<uint> &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<void> reply = QDBusConnection::sessionBus().asyncCall(msg);
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, ids](QDBusPendingCallWatcher *watcher) {
QDBusPendingReply<void> 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<uint>::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<uint> dirtyMenus;
QVector<uint> dirtyItems;
for (const auto &change : changes) {
auto updateSection = [&](GMenuItem &section) {
// 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 &section : 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<bool(int subscription, int section, int index, const QVariantMap &item)> &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<uint> 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);
}
}

@ -0,0 +1,67 @@
/*
SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <kde@privat.broulik.de>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#pragma once
#include <QObject>
#include <QString>
#include <QVector>
#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<uint> &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<uint> &itemIds);
void menusChanged(const QVector<uint> &menuIds);
private slots:
void onMenuChanged(const GMenuChangeList &changes);
private:
void initMenu();
void menuChanged(const GMenuChangeList &changes);
// QSet?
QList<uint> m_subscriptions; // keeps track of which menu trees we're subscribed to
QHash<uint, GMenuItemList> m_menus;
QString m_serviceName;
QString m_objectPath;
};

@ -0,0 +1,384 @@
/*
SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <kde@privat.broulik.de>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "menuproxy.h"
#include <QCoreApplication>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusServiceWatcher>
#include <QDir>
#include <QFileInfo>
#include <QStandardPaths>
#include <QTimer>
#include <QSettings>
#include <QDebug>
// #include <KConfigGroup>
#include <KDirWatch>
// #include <KSharedConfig>
#include <KWindowSystem>
#include <QX11Info>
#include <xcb/xcb.h>
#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<xcb_get_property_reply_t, QScopedPointerPodDeleter> 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<QByteArray, xcb_atom_t> 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<xcb_intern_atom_reply_t, QScopedPointerPodDeleter> 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;
}

@ -0,0 +1,63 @@
/*
SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <kde@privat.broulik.de>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#pragma once
#include <QByteArray>
#include <QHash>
#include <QObject>
#include <QWindow> // for WId
#include <xcb/xcb_atom.h>
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<WId, Window *> m_windows;
QDBusServiceWatcher *m_serviceWatcher;
KDirWatch *m_gtk2RcWatch;
QTimer *m_writeGtk2SettingsTimer;
bool m_enabled = false;
};

@ -0,0 +1,29 @@
/*
SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <kde@privat.broulik.de>
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 &section, 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;
}

@ -0,0 +1,19 @@
/*
SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <kde@privat.broulik.de>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#pragma once
#include <QString>
#include <QVariantMap>
namespace Utils
{
int treeStructureToInt(int subscription, int section, int index);
void intToTreeStructure(int source, int &subscription, int &section, int &index);
QString itemActionName(const QVariantMap &item);
}

@ -0,0 +1,653 @@
/*
SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <kde@privat.broulik.de>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "window.h"
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusPendingCallWatcher>
#include <QDBusPendingReply>
#include <QDebug>
#include <QList>
#include <QMutableListIterator>
#include <QVariantList>
#include <algorithm>
#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<uint> &itemIds)
{
if (qobject_cast<Menu *>(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<int>(id),
gMenuToDBusMenuProperties(newItem)};
items.append(dBusItem);
}
emit ItemsPropertiesUpdated(items, {});
}
void Window::menuChanged(const QVector<uint> &menuIds)
{
if (qobject_cast<Menu *>(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<Menu *>(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<int> &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<GMenuSection>(it->value<QDBusArgument>());
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<GMenuSection>(it->value<QDBusArgument>());
// 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<GMenuSection>(findIt->value<QDBusArgument>());
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("<Primary>")) || accel.contains(QLatin1String("<Control>"))) {
shortcut.append(QStringLiteral("Control"));
accel.remove(QLatin1String("<Primary>"));
accel.remove(QLatin1String("<Control>"));
}
if (accel.contains(QLatin1String("<Shift>"))) {
shortcut.append(QStringLiteral("Shift"));
accel.remove(QLatin1String("<Shift>"));
}
if (accel.contains(QLatin1String("<Alt>"))) {
shortcut.append(QStringLiteral("Alt"));
accel.remove(QLatin1String("<Alt>"));
}
if (accel.contains(QLatin1String("<Super>"))) {
shortcut.append(QStringLiteral("Super"));
accel.remove(QLatin1String("<Super>"));
}
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;
}

@ -0,0 +1,126 @@
/*
SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <kde@privat.broulik.de>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#pragma once
#include <QDBusContext>
#include <QObject>
#include <QString>
#include <QVector>
#include <QWindow> // for WId
#include <functional>
#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<int> &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<uint> &menuIds);
void menuItemsChanged(const QVector<uint> &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<int, QDBusMessage> 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;
};

@ -21,6 +21,7 @@
#include "sessionadaptor.h"
// Qt
#include <QCommandLineOption>
#include <QDBusConnection>
#include <QStandardPaths>
#include <QSettings>
@ -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");

@ -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

@ -18,6 +18,7 @@
*/
#include "processmanager.h"
#include "application.h"
#include <QCoreApplication>
#include <QStandardPaths>
@ -35,8 +36,9 @@
#include <KWindowSystem>
#include <KWindowSystem/NETWM>
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<QString, QStringList> pair : list) {

@ -26,12 +26,13 @@
#include <QEventLoop>
#include <QMap>
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<QString, QProcess *> m_systemProcess;
QMap<QString, QProcess *> m_autoStartProcess;

@ -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 {

Loading…
Cancel
Save