Skip to content

Commit 5db153f

Browse files
committed
add gtk gmenu dbus proxy
1 parent a4d2e17 commit 5db153f

19 files changed

+2782
-1
lines changed

CMakeLists.txt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,27 @@ find_package(Qt5DBus REQUIRED)
2222
find_package(KF5WindowSystem REQUIRED)
2323
find_package(Qt5X11Extras REQUIRED)
2424

25-
2625
set(SRCS main.cpp
2726
menuimporter.h menuimporter.cpp
2827
dbusmenutypes_p.h dbusmenutypes_p.cpp
2928
dbusmenushortcut_p.h dbusmenushortcut_p.cpp
29+
30+
# gmenu dbus proxy
31+
utils.h utils.cpp
32+
window.h window.cpp
33+
menuproxy.h menuproxy.cpp
34+
menu.h menu.cpp
35+
icons.h icons.cpp
36+
gdbusmenutypes_p.h gdbusmenutypes_p.cpp
37+
actions.h actions.cpp
38+
config-X11.h
3039
)
3140

3241
qt5_add_dbus_adaptor(SRCS com.canonical.AppMenu.Registrar.xml
3342
menuimporter.h MenuImporter menuimporteradaptor MenuImporterAdaptor)
3443

44+
qt5_add_dbus_adaptor(SRCS com.canonical.dbusmenu.xml window.h Window)
45+
3546

3647
add_executable(${PROJECT} ${SRCS})
3748

actions.cpp

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
/*
2+
* Copyright (C) 2018 Kai Uwe Broulik <kde@privat.broulik.de>
3+
*
4+
* This library is free software; you can redistribute it and/or
5+
* modify it under the terms of the GNU Lesser General Public
6+
* License as published by the Free Software Foundation; either
7+
* version 2.1 of the License, or (at your option) any later version.
8+
*
9+
* This library is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12+
* Lesser General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Lesser General Public
15+
* License along with this library; if not, write to the Free Software
16+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17+
*
18+
*/
19+
20+
#include "actions.h"
21+
22+
#include <QDBusConnection>
23+
#include <QDBusMessage>
24+
#include <QDBusPendingCallWatcher>
25+
#include <QDBusPendingReply>
26+
#include <QDebug>
27+
#include <QStringList>
28+
#include <QVariantList>
29+
30+
static const QString s_orgGtkActions = QStringLiteral("org.gtk.Actions");
31+
32+
Actions::Actions(const QString &serviceName, const QString &objectPath, QObject *parent)
33+
: QObject(parent)
34+
, m_serviceName(serviceName)
35+
, m_objectPath(objectPath)
36+
{
37+
Q_ASSERT(!serviceName.isEmpty());
38+
Q_ASSERT(!m_objectPath.isEmpty());
39+
40+
if (!QDBusConnection::sessionBus().connect(serviceName,
41+
objectPath,
42+
s_orgGtkActions,
43+
QStringLiteral("Changed"),
44+
this,
45+
SLOT(onActionsChanged(QStringList,StringBoolMap,QVariantMap,GMenuActionMap)))) {
46+
qDebug() << "Failed to subscribe to action changes for" << parent << "on" << serviceName << "at" << objectPath;
47+
}
48+
}
49+
50+
Actions::~Actions() = default;
51+
52+
void Actions::load()
53+
{
54+
QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName,
55+
m_objectPath,
56+
s_orgGtkActions,
57+
QStringLiteral("DescribeAll"));
58+
59+
QDBusPendingReply<GMenuActionMap> reply = QDBusConnection::sessionBus().asyncCall(msg);
60+
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
61+
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) {
62+
QDBusPendingReply<GMenuActionMap> reply = *watcher;
63+
if (reply.isError()) {
64+
qDebug() << "Failed to get actions from" << m_serviceName << "at" << m_objectPath << reply.error();
65+
emit failedToLoad();
66+
} else {
67+
m_actions = reply.value();
68+
emit loaded();
69+
}
70+
watcher->deleteLater();
71+
});
72+
}
73+
74+
bool Actions::get(const QString &name, GMenuAction &action) const
75+
{
76+
auto it = m_actions.find(name);
77+
if (it == m_actions.constEnd()) {
78+
return false;
79+
}
80+
81+
action = *it;
82+
return true;
83+
}
84+
85+
GMenuActionMap Actions::getAll() const
86+
{
87+
return m_actions;
88+
}
89+
90+
void Actions::trigger(const QString &name, const QVariant &target, uint timestamp)
91+
{
92+
if (!m_actions.contains(name)) {
93+
qDebug() << "Cannot invoke action" << name << "which doesn't exist";
94+
return;
95+
}
96+
97+
QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName,
98+
m_objectPath,
99+
s_orgGtkActions,
100+
QStringLiteral("Activate"));
101+
msg << name;
102+
103+
QVariantList args;
104+
if (target.isValid()) {
105+
args << target;
106+
}
107+
msg << QVariant::fromValue(args);
108+
109+
QVariantMap platformData;
110+
111+
if (timestamp) {
112+
// From documentation:
113+
// If the startup notification id is not available, this can be just "_TIMEtime", where
114+
// time is the time stamp from the event triggering the call.
115+
// see also gtkwindow.c extract_time_from_startup_id and startup_id_is_fake
116+
platformData.insert(QStringLiteral("desktop-startup-id"), QStringLiteral("_TIME") + QString::number(timestamp));
117+
}
118+
119+
msg << platformData;
120+
121+
QDBusPendingReply<void> reply = QDBusConnection::sessionBus().asyncCall(msg);
122+
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
123+
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, name](QDBusPendingCallWatcher *watcher) {
124+
QDBusPendingReply<void> reply = *watcher;
125+
if (reply.isError()) {
126+
qDebug() << "Failed to invoke action" << name << "on" << m_serviceName << "at" << m_objectPath << reply.error();
127+
}
128+
watcher->deleteLater();
129+
});
130+
}
131+
132+
bool Actions::isValid() const
133+
{
134+
return !m_actions.isEmpty();
135+
}
136+
137+
void Actions::onActionsChanged(const QStringList &removed,
138+
const StringBoolMap &enabledChanges,
139+
const QVariantMap &stateChanges,
140+
const GMenuActionMap &added)
141+
{
142+
// Collect the actions that we removed, altered, or added, so we can eventually signal changes for all menus that contain one of those actions
143+
QStringList dirtyActions;
144+
145+
// TODO I bet for most of the loops below we could use a nice short std algorithm
146+
147+
for (const QString &removedAction : removed) {
148+
if (m_actions.remove(removedAction)) {
149+
dirtyActions.append(removedAction);
150+
}
151+
}
152+
153+
for (auto it = enabledChanges.constBegin(), end = enabledChanges.constEnd(); it != end; ++it) {
154+
const QString &actionName = it.key();
155+
const bool enabled = it.value();
156+
157+
auto actionIt = m_actions.find(actionName);
158+
if (actionIt == m_actions.end()) {
159+
qDebug() << "Got enabled changed for action" << actionName << "which we don't know";
160+
continue;
161+
}
162+
163+
GMenuAction &action = *actionIt;
164+
if (action.enabled != enabled) {
165+
action.enabled = enabled;
166+
dirtyActions.append(actionName);
167+
} else {
168+
qDebug() << "Got enabled change for action" << actionName << "which didn't change it";
169+
}
170+
}
171+
172+
for (auto it = stateChanges.constBegin(), end = stateChanges.constEnd(); it != end; ++it) {
173+
const QString &actionName = it.key();
174+
const QVariant &state = it.value();
175+
176+
auto actionIt = m_actions.find(actionName);
177+
if (actionIt == m_actions.end()) {
178+
qDebug() << "Got state changed for action" << actionName << "which we don't know";
179+
continue;
180+
}
181+
182+
GMenuAction &action = *actionIt;
183+
184+
if (action.state.isEmpty()) {
185+
qDebug() << "Got new state for action" << actionName << "that didn't have any state before";
186+
action.state.append(state);
187+
dirtyActions.append(actionName);
188+
} else {
189+
// Action state is a list but the state change only sends us a single variant, so just overwrite the first one
190+
QVariant &firstState = action.state.first();
191+
if (firstState != state) {
192+
firstState = state;
193+
dirtyActions.append(actionName);
194+
} else {
195+
qDebug() << "Got state change for action" << actionName << "which didn't change it";
196+
}
197+
}
198+
}
199+
200+
// unite() will result in keys being present multiple times, do it manually and overwrite existing ones
201+
for (auto it = added.constBegin(), end = added.constEnd(); it != end; ++it) {
202+
const QString &actionName = it.key();
203+
204+
// if (DBUSMENUPROXY().isInfoEnabled()) {
205+
// if (m_actions.contains(actionName)) {
206+
// qDebug() << "Got new action" << actionName << "that we already have, overwriting existing one";
207+
// }
208+
// }
209+
210+
m_actions.insert(actionName, it.value());
211+
212+
dirtyActions.append(actionName);
213+
}
214+
215+
if (!dirtyActions.isEmpty()) {
216+
emit actionsChanged(dirtyActions);
217+
}
218+
}

actions.h

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright (C) 2018 Kai Uwe Broulik <kde@privat.broulik.de>
3+
*
4+
* This library is free software; you can redistribute it and/or
5+
* modify it under the terms of the GNU Lesser General Public
6+
* License as published by the Free Software Foundation; either
7+
* version 2.1 of the License, or (at your option) any later version.
8+
*
9+
* This library is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12+
* Lesser General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Lesser General Public
15+
* License along with this library; if not, write to the Free Software
16+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17+
*
18+
*/
19+
20+
#pragma once
21+
22+
#include <QObject>
23+
#include <QString>
24+
25+
#include "gdbusmenutypes_p.h"
26+
27+
class QStringList;
28+
29+
class Actions : public QObject
30+
{
31+
Q_OBJECT
32+
33+
public:
34+
Actions(const QString &serviceName, const QString &objectPath, QObject *parent = nullptr);
35+
~Actions() override;
36+
37+
void load();
38+
39+
bool get(const QString &name, GMenuAction &action) const;
40+
GMenuActionMap getAll() const;
41+
void trigger(const QString &name, const QVariant &target, uint timestamp = 0);
42+
43+
bool isValid() const; // basically "has actions"
44+
45+
signals:
46+
void loaded();
47+
void failedToLoad(); // expose error?
48+
void actionsChanged(const QStringList &dirtyActions);
49+
50+
private slots:
51+
void onActionsChanged(const QStringList &removed,
52+
const StringBoolMap &enabledChanges,
53+
const QVariantMap &stateChanges,
54+
const GMenuActionMap &added);
55+
56+
private:
57+
GMenuActionMap m_actions;
58+
59+
QString m_serviceName;
60+
QString m_objectPath;
61+
62+
};

com.canonical.dbusmenu.xml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<interface name="com.canonical.dbusmenu">
2+
<property name="Version" type="u" access="read"/>
3+
<property name="Status" type="s" access="read"/>
4+
<signal name="ItemsPropertiesUpdated">
5+
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="DBusMenuItemList"/>
6+
<annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="DBusMenuItemKeysList"/>
7+
<arg type="a(ia{sv})" direction="out"/>
8+
<arg type="a(ias)" direction="out"/>
9+
</signal>
10+
<signal name="LayoutUpdated">
11+
<arg name="revision" type="u" direction="out"/>
12+
<arg name="parentId" type="i" direction="out"/>
13+
</signal>
14+
<signal name="ItemActivationRequested">
15+
<arg name="id" type="i" direction="out"/>
16+
<arg name="timeStamp" type="u" direction="out"/>
17+
</signal>
18+
<method name="Event">
19+
<arg name="id" type="i" direction="in"/>
20+
<arg name="eventId" type="s" direction="in"/>
21+
<arg name="data" type="v" direction="in"/>
22+
<arg name="timestamp" type="u" direction="in"/>
23+
<annotation name="org.freedesktop.DBus.Method.NoReply" value="true"/>
24+
</method>
25+
<method name="GetProperty">
26+
<arg type="v" direction="out"/>
27+
<arg name="id" type="i" direction="in"/>
28+
<arg name="property" type="s" direction="in"/>
29+
</method>
30+
<method name="GetLayout">
31+
<arg type="u" direction="out"/>
32+
<arg name="parentId" type="i" direction="in"/>
33+
<arg name="recursionDepth" type="i" direction="in"/>
34+
<arg name="propertyNames" type="as" direction="in"/>
35+
<arg name="item" type="(ia{sv}av)" direction="out"/>
36+
<annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="DBusMenuLayoutItem"/>
37+
</method>
38+
<method name="GetGroupProperties">
39+
<arg type="a(ia{sv})" direction="out"/>
40+
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="DBusMenuItemList"/>
41+
<arg name="ids" type="ai" direction="in"/>
42+
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="QList&lt;int&gt;"/>
43+
<arg name="propertyNames" type="as" direction="in"/>
44+
</method>
45+
<method name="AboutToShow">
46+
<arg type="b" direction="out"/>
47+
<arg name="id" type="i" direction="in"/>
48+
</method>
49+
</interface>

0 commit comments

Comments
 (0)