Commit 968238ba authored by Kai Uwe Broulik's avatar Kai Uwe Broulik 🍇
Browse files

Split Menu handling into its own class

This way we can monitor both appmenu and window menu simultaneously and (theoretically)
switch between one and the other on the fly.
The Menu class is now Window as it encapsulates an entire window with everything that belongs to it.
All DBusMenu-specific code is in Window whereas GMenu-stuff is in Menu and so it's less entangled
parent 3df45867
......@@ -13,14 +13,16 @@ find_package(XCB
set(GMENU_DBUSMENU_PROXY_SRCS
main.cpp
menuproxy.cpp
window.cpp
menu.cpp
actions.cpp
gdbusmenutypes_p.cpp
icons.cpp
utils.cpp
../libdbusmenuqt/dbusmenutypes_p.cpp
)
qt5_add_dbus_adaptor(GMENU_DBUSMENU_PROXY_SRCS ../libdbusmenuqt/com.canonical.dbusmenu.xml menu.h Menu)
qt5_add_dbus_adaptor(GMENU_DBUSMENU_PROXY_SRCS ../libdbusmenuqt/com.canonical.dbusmenu.xml window.h Window)
ecm_qt_declare_logging_category(GMENU_DBUSMENU_PROXY_SRCS HEADER debug.h
IDENTIFIER DBUSMENUPROXY
......
/*
* Copyright (C) 2018 Kai Uwe Broulik <kde@privat.broulik.de>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
#include "actions.h"
#include "debug.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)))) {
qCWarning(DBUSMENUPROXY) << "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()) {
qCWarning(DBUSMENUPROXY) << "Failed to get actions from" << m_serviceName << "at" << m_objectPath << reply.error();
emit failedToLoad();
} else {
m_actions = reply.value();
emit loaded();
}
});
}
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, uint timestamp)
{
if (!m_actions.contains(name)) {
qCWarning(DBUSMENUPROXY) << "Cannot invoke action" << name << "which doesn't exist";
return;
}
QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName,
m_objectPath,
s_orgGtkActions,
QStringLiteral("Activate"));
msg << name;
// TODO use the arguments provided by "target" in the menu item
msg << QVariant::fromValue(QVariantList());
QVariantMap platformData;
if (timestamp) {
// From documentation:
// If the startup notification id is not available, this can be just "_TIMEtime", where
// time is the time stamp from the event triggering the call.
// see also gtkwindow.c extract_time_from_startup_id and startup_id_is_fake
platformData.insert(QStringLiteral("desktop-startup-id"), QStringLiteral("_TIME") + QString::number(timestamp));
}
msg << platformData;
QDBusPendingReply<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()) {
qCWarning(DBUSMENUPROXY) << "Failed to invoke action" << name << "on" << m_serviceName << "at" << m_objectPath << reply.error();
}
});
}
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()) {
qCInfo(DBUSMENUPROXY) << "Got enabled changed for action" << actionName << "which we don't know";
continue;
}
GMenuAction &action = *actionIt;
if (action.enabled != enabled) {
action.enabled = enabled;
dirtyActions.append(actionName);
} else {
qCInfo(DBUSMENUPROXY) << "Got enabled change for action" << actionName << "which didn't change it";
}
}
for (auto it = stateChanges.constBegin(), end = stateChanges.constEnd(); it != end; ++it) {
const QString &actionName = it.key();
const QVariant &state = it.value();
auto actionIt = m_actions.find(actionName);
if (actionIt == m_actions.end()) {
qCInfo(DBUSMENUPROXY) << "Got state changed for action" << actionName << "which we don't know";
continue;
}
GMenuAction &action = *actionIt;
if (action.state.isEmpty()) {
qCDebug(DBUSMENUPROXY) << "Got new state for action" << actionName << "that didn't have any state before";
action.state.append(state);
dirtyActions.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 {
qCInfo(DBUSMENUPROXY) << "Got state change for action" << actionName << "which didn't change it";
}
}
}
// unite() will result in keys being present multiple times, do it manually and overwrite existing ones
for (auto it = added.constBegin(), end = added.constEnd(); it != end; ++it) {
const QString &actionName = it.key();
if (DBUSMENUPROXY().isInfoEnabled()) {
if (m_actions.contains(actionName)) {
qCInfo(DBUSMENUPROXY) << "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);
}
}
/*
* Copyright (C) 2018 Kai Uwe Broulik <kde@privat.broulik.de>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
#pragma once
#include <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();
void load();
bool get(const QString &name, GMenuAction &action) const;
GMenuActionMap getAll() const;
void trigger(const QString &name, uint timestamp = 0);
bool isValid() const; // basically "has actions"
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;
};
This diff is collapsed.
......@@ -20,130 +20,62 @@
#pragma once
#include <QObject>
#include <QDBusContext>
#include <QString>
#include <QVector>
#include <QWindow> // for WId
#include <functional>
#include "gdbusmenutypes_p.h"
#include "../libdbusmenuqt/dbusmenutypes_p.h"
class QDBusVariant;
class Actions;
class Menu : public QObject, protected QDBusContext
class Menu : public QObject
{
Q_OBJECT
// DBus
Q_PROPERTY(QString Status READ status)
Q_PROPERTY(uint Version READ version)
public:
Menu(const QString &serviceName);
Menu(const QString &serviceName, const QString &objectPath, QObject *parent = nullptr);
~Menu();
void init();
void cleanup();
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);
void start(uint id);
void stop(const QList<uint> &ids);
QString currentMenuObjectPath() const;
bool hasMenu() const;
bool hasSubscription(uint subscription) const;
QString proxyObjectPath() const;
GMenuItem getSection(int id, bool *ok = nullptr) const;
GMenuItem getSection(int subscription, int sectionId, bool *ok = nullptr) 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);
QVariantMap getItem(int id) const; // bool ok argument?
QVariantMap getItem(int subscription, int sectionId, int id) const;
QString status() const;
uint version() const;
public slots:
void actionsChanged(const QStringList &dirtyActions, const QString &prefix);
signals:
// don't want to pollute X stuff into Menu, let all of that be in MenuProxy
void requestWriteWindowProperties();
void requestRemoveWindowProperties();
void menuAppeared(); // emitted the first time a menu was successfully loaded
void menuDisappeared();
void subscribed(uint id);
void failedToSubscribe(uint id);
// DBus
void ItemActivationRequested(int id, uint timestamp);
void ItemsPropertiesUpdated(const DBusMenuItemList &updatedProps, const DBusMenuItemKeysList &removedProps);
void LayoutUpdated(uint revision, int parent);
void itemsChanged(const QVector<uint> &itemIds);
void menusChanged(const QVector<uint> &menuIds);
private slots:
void onApplicationMenuChanged(const GMenuChangeList &changes);
void onMenuBarChanged(const GMenuChangeList &changes);
void onMenuChanged(const GMenuChangeList &changes);
private:
void initMenu();
void start(uint id);
void stop(const QList<uint> &id);
bool registerDBusObject();
bool getAction(const QString &name, GMenuAction &action) const;
void triggerAction(const QString &name, uint timestamp = 0);
Actions *getActionsForAction(const QString &name, QString &lookupName) const;
void menuChanged(const GMenuChangeList &changes);
void actionsChanged(const QStringList &dirty, const QString &prefix);
static int treeStructureToInt(int subscription, int section, int index);
static void intToTreeStructure(int source, int &subscription, int &section, int &index);
static QString actionNameOfItem(const QVariantMap &item);
static GMenuItem findSection(const QList<GMenuItem> &list, int section);
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
// QSet?
QList<uint> m_subscriptions; // keeps track of which menu trees we're subscribed to
QHash<uint, GMenuItemList> m_menus;
QHash<int, QDBusMessage> m_pendingGetLayouts;
Actions *m_applicationActions = nullptr;
Actions *m_unityActions = nullptr;
Actions *m_windowActions = nullptr;
bool m_menuInited = false;
QString m_serviceName;
QString m_objectPath;
};
......@@ -41,7 +41,7 @@
#include <xcb/xcb.h>
#include <xcb/xcb_atom.h>
#include "menu.h"
#include "window.h"
static const QString s_ourServiceName = QStringLiteral("org.kde.plasma.gmenu_dbusmenu_proxy");
......@@ -114,8 +114,8 @@ bool MenuProxy::init()
onWindowAdded(id);
}
if (m_menus.isEmpty()) {
qCDebug(DBUSMENUPROXY) << "Up and running but no menus in sight";
if (m_windows.isEmpty()) {
qCDebug(DBUSMENUPROXY) << "Up and running but no windows with menus in sight";
}
return true;
......@@ -130,8 +130,8 @@ void MenuProxy::teardown()
disconnect(KWindowSystem::self(), &KWindowSystem::windowAdded, this, &MenuProxy::onWindowAdded);
disconnect(KWindowSystem::self(), &KWindowSystem::windowRemoved, this, &MenuProxy::onWindowRemoved);
qDeleteAll(m_menus);
m_menus.clear();
qDeleteAll(m_windows);
m_windows.clear();
}
void MenuProxy::setGtkShellShowsMenuBar(bool show)
......@@ -162,7 +162,20 @@ void MenuProxy::setGtkShellShowsMenuBar(bool show)
void MenuProxy::onWindowAdded(WId id)
{
if (m_menus.contains(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) {
qCInfo(DBUSMENUPROXY) << "Ignoring window" << id << "of type" << wType;
return;
}
......@@ -183,33 +196,33 @@ void MenuProxy::onWindowAdded(WId id)
return;
}
Menu *menu = new Menu(serviceName);
menu->setWinId(id);
menu->setApplicationObjectPath(applicationObjectPath);
menu->setUnityObjectPath(unityObjectPath);
menu->setWindowObjectPath(windowObjectPath);
menu->setApplicationMenuObjectPath(applicationMenuObjectPath);
menu->setMenuBarObjectPath(menuBarObjectPath);
m_menus.insert(id, menu);
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(menu, &Menu::requestWriteWindowProperties, this, [this, menu] {
Q_ASSERT(!menu->proxyObjectPath().isEmpty());
connect(window, &Window::requestWriteWindowProperties, this, [this, window] {
Q_ASSERT(!window->proxyObjectPath().isEmpty());
writeWindowProperty(menu->winId(), s_kdeNetWmAppMenuServiceName, s_ourServiceName.toUtf8());
writeWindowProperty(menu->winId(), s_kdeNetWmAppMenuObjectPath, menu->proxyObjectPath().toUtf8());
writeWindowProperty(window->winId(), s_kdeNetWmAppMenuServiceName, s_ourServiceName.toUtf8());
writeWindowProperty(window->winId(), s_kdeNetWmAppMenuObjectPath, window->proxyObjectPath().toUtf8());
});
connect(menu, &Menu::requestRemoveWindowProperties, this, [this, menu] {
writeWindowProperty(menu->winId(), s_kdeNetWmAppMenuServiceName, QByteArray());
writeWindowProperty(menu->winId(), s_kdeNetWmAppMenuObjectPath, QByteArray());
connect(window, &Window::requestRemoveWindowProperties, this, [this, window] {
writeWindowProperty(window->winId(), s_kdeNetWmAppMenuServiceName, QByteArray());
writeWindowProperty(window->winId(), s_kdeNetWmAppMenuObjectPath, QByteArray());
});
menu->init();
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_menus.take(id);
delete m_windows.take(id);
}
QByteArray MenuProxy::getWindowPropertyString(WId id, const QByteArray &name)
......
......@@ -28,7 +28,7 @@
class QDBusServiceWatcher;
class Menu;
class Window;
class MenuProxy : public QObject
{
......@@ -55,7 +55,7 @@ private:
void writeWindowProperty(WId id, const QByteArray &name, const QByteArray &value);
xcb_atom_t getAtom(const QByteArray &name);
QHash<WId, Menu *> m_menus;
QHash<WId, Window *> m_windows;
QDBusServiceWatcher *m_serviceWatcher;
......
This diff is collapsed.
/*
* Copyright (C) 2018 Kai Uwe Broulik <kde@privat.broulik.de>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
#pragma once
#include <QObject>
#include <QDBusContext>
#include <QString>
#include <QVector>
#include <QWindow> // for WId
#include <functional>
#include "gdbusmenutypes_p.h"
#include "../libdbusmenuqt/dbusmenutypes_p.h"