Gentoo Websites Logo
Go to: Gentoo Home Documentation Forums Lists Bugs Planet Store Wiki Get Gentoo!
View | Details | Raw Unified | Return to bug 657236
Collapse All | Expand All

(-)a/CMakeLists.txt (+2 lines)
Lines 166-171 add_subdirectory(solidautoeject) Link Here
166
166
167
ecm_optional_add_subdirectory(xembed-sni-proxy)
167
ecm_optional_add_subdirectory(xembed-sni-proxy)
168
168
169
ecm_optional_add_subdirectory(gmenu-dbusmenu-proxy)
170
169
add_subdirectory(soliduiserver)
171
add_subdirectory(soliduiserver)
170
172
171
if(KF5Holidays_FOUND)
173
if(KF5Holidays_FOUND)
(-)a/gmenu-dbusmenu-proxy/CMakeLists.txt (+45 lines)
Line 0 Link Here
1
add_definitions(-DQT_NO_CAST_TO_ASCII
2
-DQT_NO_CAST_FROM_ASCII
3
-DQT_NO_URL_CAST_FROM_STRING
4
-DQT_NO_CAST_FROM_BYTEARRAY)
5
6
find_package(XCB
7
    REQUIRED COMPONENTS
8
        XCB
9
)
10
11
set(GMENU_DBUSMENU_PROXY_SRCS
12
    main.cpp
13
    menuproxy.cpp
14
    window.cpp
15
    menu.cpp
16
    actions.cpp
17
    gdbusmenutypes_p.cpp
18
    icons.cpp
19
    utils.cpp
20
    ../libdbusmenuqt/dbusmenutypes_p.cpp
21
 )
22
23
qt5_add_dbus_adaptor(GMENU_DBUSMENU_PROXY_SRCS ../libdbusmenuqt/com.canonical.dbusmenu.xml window.h Window)
24
25
ecm_qt_declare_logging_category(GMENU_DBUSMENU_PROXY_SRCS HEADER debug.h
26
                                               IDENTIFIER DBUSMENUPROXY
27
                                               CATEGORY_NAME kde.dbusmenuproxy
28
                                               DEFAULT_SEVERITY Info)
29
30
add_executable(gmenudbusmenuproxy ${GMENU_DBUSMENU_PROXY_SRCS})
31
32
set_package_properties(XCB PROPERTIES TYPE REQUIRED)
33
34
target_link_libraries(gmenudbusmenuproxy
35
    Qt5::Core
36
    Qt5::X11Extras
37
    Qt5::DBus
38
    KF5::ConfigCore
39
    KF5::WindowSystem
40
    XCB::XCB
41
)
42
43
install(TARGETS gmenudbusmenuproxy ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
44
install(FILES gmenudbusmenuproxy.desktop DESTINATION ${KDE_INSTALL_AUTOSTARTDIR})
45
(-)a/gmenu-dbusmenu-proxy/actions.cpp (+216 lines)
Line 0 Link Here
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 "debug.h"
23
24
#include <QDBusConnection>
25
#include <QDBusMessage>
26
#include <QDBusPendingCallWatcher>
27
#include <QDBusPendingReply>
28
#include <QDebug>
29
#include <QStringList>
30
#include <QVariantList>
31
32
static const QString s_orgGtkActions = QStringLiteral("org.gtk.Actions");
33
34
Actions::Actions(const QString &serviceName, const QString &objectPath, QObject *parent)
35
    : QObject(parent)
36
    , m_serviceName(serviceName)
37
    , m_objectPath(objectPath)
38
{
39
    Q_ASSERT(!serviceName.isEmpty());
40
    Q_ASSERT(!m_objectPath.isEmpty());
41
42
    if (!QDBusConnection::sessionBus().connect(serviceName,
43
                                               objectPath,
44
                                               s_orgGtkActions,
45
                                               QStringLiteral("Changed"),
46
                                               this,
47
                                               SLOT(onActionsChanged(QStringList,StringBoolMap,QVariantMap,GMenuActionMap)))) {
48
        qCWarning(DBUSMENUPROXY) << "Failed to subscribe to action changes for" << parent << "on" << serviceName << "at" << objectPath;
49
    }
50
}
51
52
Actions::~Actions() = default;
53
54
void Actions::load()
55
{
56
    QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName,
57
                                                      m_objectPath,
58
                                                      s_orgGtkActions,
59
                                                      QStringLiteral("DescribeAll"));
60
61
    QDBusPendingReply<GMenuActionMap> reply = QDBusConnection::sessionBus().asyncCall(msg);
62
    QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
63
    connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) {
64
        QDBusPendingReply<GMenuActionMap> reply = *watcher;
65
        if (reply.isError()) {
66
            qCWarning(DBUSMENUPROXY) << "Failed to get actions from" << m_serviceName << "at" << m_objectPath << reply.error();
67
            emit failedToLoad();
68
        } else {
69
            m_actions = reply.value();
70
            emit loaded();
71
        }
72
        watcher->deleteLater();
73
    });
74
}
75
76
bool Actions::get(const QString &name, GMenuAction &action) const
77
{
78
    auto it = m_actions.find(name);
79
    if (it == m_actions.constEnd()) {
80
        return false;
81
    }
82
83
    action = *it;
84
    return true;
85
}
86
87
GMenuActionMap Actions::getAll() const
88
{
89
    return m_actions;
90
}
91
92
void Actions::trigger(const QString &name, uint timestamp)
93
{
94
    if (!m_actions.contains(name)) {
95
        qCWarning(DBUSMENUPROXY) << "Cannot invoke action" << name << "which doesn't exist";
96
        return;
97
    }
98
99
    QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName,
100
                                                      m_objectPath,
101
                                                      s_orgGtkActions,
102
                                                      QStringLiteral("Activate"));
103
    msg << name;
104
    // TODO use the arguments provided by "target" in the menu item
105
    msg << QVariant::fromValue(QVariantList());
106
107
    QVariantMap platformData;
108
109
    if (timestamp) {
110
        // From documentation:
111
        // If the startup notification id is not available, this can be just "_TIMEtime", where
112
        // time is the time stamp from the event triggering the call.
113
        // see also gtkwindow.c extract_time_from_startup_id and startup_id_is_fake
114
        platformData.insert(QStringLiteral("desktop-startup-id"), QStringLiteral("_TIME") + QString::number(timestamp));
115
    }
116
117
    msg << platformData;
118
119
    QDBusPendingReply<void> reply = QDBusConnection::sessionBus().asyncCall(msg);
120
    QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
121
    connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, name](QDBusPendingCallWatcher *watcher) {
122
        QDBusPendingReply<void> reply = *watcher;
123
        if (reply.isError()) {
124
            qCWarning(DBUSMENUPROXY) << "Failed to invoke action" << name << "on" << m_serviceName << "at" << m_objectPath << reply.error();
125
        }
126
        watcher->deleteLater();
127
    });
128
}
129
130
bool Actions::isValid() const
131
{
132
    return !m_actions.isEmpty();
133
}
134
135
void Actions::onActionsChanged(const QStringList &removed,
136
                               const StringBoolMap &enabledChanges,
137
                               const QVariantMap &stateChanges,
138
                               const GMenuActionMap &added)
139
{
140
    // Collect the actions that we removed, altered, or added, so we can eventually signal changes for all menus that contain one of those actions
141
    QStringList dirtyActions;
142
143
    // TODO I bet for most of the loops below we could use a nice short std algorithm
144
145
    for (const QString &removedAction : removed) {
146
        if (m_actions.remove(removedAction)) {
147
            dirtyActions.append(removedAction);
148
        }
149
    }
150
151
    for (auto it = enabledChanges.constBegin(), end = enabledChanges.constEnd(); it != end; ++it) {
152
        const QString &actionName = it.key();
153
        const bool enabled = it.value();
154
155
        auto actionIt = m_actions.find(actionName);
156
        if (actionIt == m_actions.end()) {
157
            qCInfo(DBUSMENUPROXY) << "Got enabled changed for action" << actionName << "which we don't know";
158
            continue;
159
        }
160
161
        GMenuAction &action = *actionIt;
162
        if (action.enabled != enabled) {
163
            action.enabled = enabled;
164
            dirtyActions.append(actionName);
165
        } else {
166
            qCInfo(DBUSMENUPROXY) << "Got enabled change for action" << actionName << "which didn't change it";
167
        }
168
    }
169
170
    for (auto it = stateChanges.constBegin(), end = stateChanges.constEnd(); it != end; ++it) {
171
        const QString &actionName = it.key();
172
        const QVariant &state = it.value();
173
174
        auto actionIt = m_actions.find(actionName);
175
        if (actionIt == m_actions.end()) {
176
            qCInfo(DBUSMENUPROXY) << "Got state changed for action" << actionName << "which we don't know";
177
            continue;
178
        }
179
180
        GMenuAction &action = *actionIt;
181
182
        if (action.state.isEmpty()) {
183
            qCDebug(DBUSMENUPROXY) << "Got new state for action" << actionName << "that didn't have any state before";
184
            action.state.append(state);
185
            dirtyActions.append(actionName);
186
        } else {
187
            // Action state is a list but the state change only sends us a single variant, so just overwrite the first one
188
            QVariant &firstState = action.state.first();
189
            if (firstState != state) {
190
                firstState = state;
191
                dirtyActions.append(actionName);
192
            } else {
193
                qCInfo(DBUSMENUPROXY) << "Got state change for action" << actionName << "which didn't change it";
194
            }
195
        }
196
    }
197
198
    // unite() will result in keys being present multiple times, do it manually and overwrite existing ones
199
    for (auto it = added.constBegin(), end = added.constEnd(); it != end; ++it) {
200
        const QString &actionName = it.key();
201
202
        if (DBUSMENUPROXY().isInfoEnabled()) {
203
            if (m_actions.contains(actionName)) {
204
                qCInfo(DBUSMENUPROXY) << "Got new action" << actionName << "that we already have, overwriting existing one";
205
            }
206
        }
207
208
        m_actions.insert(actionName, it.value());
209
210
        dirtyActions.append(actionName);
211
    }
212
213
    if (!dirtyActions.isEmpty()) {
214
        emit actionsChanged(dirtyActions);
215
    }
216
}
(-)a/gmenu-dbusmenu-proxy/actions.h (+62 lines)
Line 0 Link Here
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();
36
37
    void load();
38
39
    bool get(const QString &name, GMenuAction &action) const;
40
    GMenuActionMap getAll() const;
41
    void trigger(const QString &name, 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
};
(-)a/gmenu-dbusmenu-proxy/gdbusmenutypes_p.cpp (+132 lines)
Line 0 Link Here
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 "gdbusmenutypes_p.h"
21
22
#include <QDBusArgument>
23
#include <QDBusMetaType>
24
25
// GMenuItem
26
QDBusArgument &operator<<(QDBusArgument &argument, const GMenuItem &item)
27
{
28
    argument.beginStructure();
29
    argument << item.id << item.section << item.items;
30
    argument.endStructure();
31
    return argument;
32
}
33
34
const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuItem &item)
35
{
36
    argument.beginStructure();
37
    argument >> item.id >> item.section >> item.items;
38
    argument.endStructure();
39
    return argument;
40
}
41
42
// GMenuSection
43
QDBusArgument &operator<<(QDBusArgument &argument, const GMenuSection &item)
44
{
45
    argument.beginStructure();
46
    argument << item.subscription << item.menu;
47
    argument.endStructure();
48
    return argument;
49
}
50
51
const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuSection &item)
52
{
53
    argument.beginStructure();
54
    argument >> item.subscription >> item.menu;
55
    argument.endStructure();
56
    return argument;
57
}
58
59
// GMenuChange
60
QDBusArgument &operator<<(QDBusArgument &argument, const GMenuChange &item)
61
{
62
    argument.beginStructure();
63
    argument << item.subscription << item.menu << item.changePosition << item.itemsToRemoveCount << item.itemsToInsert;
64
    argument.endStructure();
65
    return argument;
66
}
67
68
const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuChange &item)
69
{
70
    argument.beginStructure();
71
    argument >> item.subscription >> item.menu >> item.changePosition >> item.itemsToRemoveCount >> item.itemsToInsert;
72
    argument.endStructure();
73
    return argument;
74
}
75
76
// GMenuActionProperty
77
QDBusArgument &operator<<(QDBusArgument &argument, const GMenuAction &item)
78
{
79
    argument.beginStructure();
80
    argument << item.enabled << item.signature << item.state;
81
    argument.endStructure();
82
    return argument;
83
}
84
85
const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuAction &item)
86
{
87
    argument.beginStructure();
88
    argument >> item.enabled >> item.signature >> item.state;
89
    argument.endStructure();
90
    return argument;
91
}
92
93
// GMenuActionsChange
94
QDBusArgument &operator<<(QDBusArgument &argument, const GMenuActionsChange &item)
95
{
96
    argument.beginStructure();
97
    argument << item.removed << item.enabledChanged << item.stateChanged << item.added;
98
    argument.endStructure();
99
    return argument;
100
}
101
102
const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuActionsChange &item)
103
{
104
    argument.beginStructure();
105
    argument >> item.removed >> item.enabledChanged >> item.stateChanged >> item.added;
106
    argument.endStructure();
107
    return argument;
108
}
109
110
void GDBusMenuTypes_register()
111
{
112
    static bool registered = false;
113
    if (registered) {
114
        return;
115
    }
116
117
    qDBusRegisterMetaType<GMenuItem>();
118
    qDBusRegisterMetaType<GMenuItemList>();
119
120
    qDBusRegisterMetaType<GMenuSection>();
121
122
    qDBusRegisterMetaType<GMenuChange>();
123
    qDBusRegisterMetaType<GMenuChangeList>();
124
125
    qDBusRegisterMetaType<GMenuAction>();
126
    qDBusRegisterMetaType<GMenuActionMap>();
127
128
    qDBusRegisterMetaType<GMenuActionsChange>();
129
    qDBusRegisterMetaType<StringBoolMap>();
130
131
    registered = true;
132
}
(-)a/gmenu-dbusmenu-proxy/gdbusmenutypes_p.h (+107 lines)
Line 0 Link Here
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 <QDBusSignature>
23
#include <QList>
24
#include <QMap>
25
#include <QVariant>
26
27
class QDBusArgument;
28
29
// Various
30
using VariantMapList = QList<QVariantMap>;
31
Q_DECLARE_METATYPE(VariantMapList);
32
33
using StringBoolMap = QMap<QString, bool>;
34
Q_DECLARE_METATYPE(StringBoolMap);
35
36
// Menu item itself (Start method)
37
struct GMenuItem
38
{
39
    uint id;
40
    uint section;
41
    VariantMapList items;
42
};
43
Q_DECLARE_METATYPE(GMenuItem);
44
45
QDBusArgument &operator<<(QDBusArgument &argument, const GMenuItem &item);
46
const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuItem &item);
47
48
using GMenuItemList = QList<GMenuItem>;
49
Q_DECLARE_METATYPE(GMenuItemList);
50
51
// Information about what section or submenu to use for a particular entry
52
struct GMenuSection
53
{
54
    uint subscription;
55
    uint menu;
56
};
57
Q_DECLARE_METATYPE(GMenuSection);
58
59
QDBusArgument &operator<<(QDBusArgument &argument, const GMenuSection &item);
60
const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuSection &item);
61
62
// Changes of a menu item (Changed signal)
63
struct GMenuChange
64
{
65
    uint subscription;
66
    uint menu;
67
68
    uint changePosition;
69
    uint itemsToRemoveCount;
70
    VariantMapList itemsToInsert;
71
};
72
Q_DECLARE_METATYPE(GMenuChange);
73
74
QDBusArgument &operator<<(QDBusArgument &argument, const GMenuChange &item);
75
const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuChange &item);
76
77
using GMenuChangeList = QList<GMenuChange>;
78
Q_DECLARE_METATYPE(GMenuChangeList);
79
80
// An application action
81
struct GMenuAction
82
{
83
    bool enabled;
84
    QDBusSignature signature;
85
    QVariantList state;
86
};
87
Q_DECLARE_METATYPE(GMenuAction);
88
89
QDBusArgument &operator<<(QDBusArgument &argument, const GMenuAction &item);
90
const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuAction &item);
91
92
using GMenuActionMap = QMap<QString, GMenuAction>;
93
Q_DECLARE_METATYPE(GMenuActionMap);
94
95
struct GMenuActionsChange
96
{
97
    QStringList removed;
98
    QMap<QString, bool> enabledChanged;
99
    QVariantMap stateChanged;
100
    GMenuActionMap added;
101
};
102
Q_DECLARE_METATYPE(GMenuActionsChange);
103
104
QDBusArgument &operator<<(QDBusArgument &argument, const GMenuActionsChange &item);
105
const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuActionsChange &item);
106
107
void GDBusMenuTypes_register();
(-)a/gmenu-dbusmenu-proxy/gmenudbusmenuproxy.desktop (+12 lines)
Line 0 Link Here
1
[Desktop Entry]
2
Exec=gmenudbusmenuproxy
3
Name=GMenuDBusMenuProxy
4
Name[ca]=GMenuDBusMenuProxy
5
Name[en_GB]=GMenuDBusMenuProxy
6
Name[nl]=GMenuDBusMenuProxy
7
Name[uk]=Проксі-меню GMenu D-Bus
8
Name[x-test]=xxGMenuDBusMenuProxyxx
9
Type=Application
10
X-KDE-StartupNotify=false
11
OnlyShowIn=KDE;
12
X-KDE-autostart-phase=1
(-)a/gmenu-dbusmenu-proxy/icons.cpp (+284 lines)
Line 0 Link Here
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 "icons.h"
21
22
#include <QHash>
23
24
QString Icons::actionIcon(const QString &actionName)
25
{
26
    QString icon;
27
28
    QString action = actionName;
29
30
    if (action.isEmpty()) {
31
        return icon;
32
    }
33
34
    static const QHash<QString, QString> s_icons {
35
        {QStringLiteral("image-new"), QStringLiteral("document-new")}, // Gimp "New" item
36
        {QStringLiteral("adddirect"), QStringLiteral("document-new")}, // LibreOffice "New" item
37
        {QStringLiteral("filenew"), QStringLiteral("document-new")}, // Pluma "New" item
38
        {QStringLiteral("new-window"), QStringLiteral("window-new")},
39
        {QStringLiteral("newwindow"), QStringLiteral("window-new")},
40
        {QStringLiteral("yelp-window-new"), QStringLiteral("window-new")}, // Gnome help
41
        {QStringLiteral("new-tab"), QStringLiteral("tab-new")},
42
        {QStringLiteral("open"), QStringLiteral("document-open")},
43
        {QStringLiteral("open-location"), QStringLiteral("document-open-remote")},
44
        {QStringLiteral("openremote"), QStringLiteral("document-open-remote")},
45
        {QStringLiteral("save"), QStringLiteral("document-save")},
46
        {QStringLiteral("save-as"), QStringLiteral("document-save-as")},
47
        {QStringLiteral("saveas"), QStringLiteral("document-save-as")},
48
        {QStringLiteral("save-all"), QStringLiteral("document-save-all")},
49
        {QStringLiteral("saveall"), QStringLiteral("document-save-all")},
50
        {QStringLiteral("export"), QStringLiteral("document-export")},
51
        {QStringLiteral("exportto"), QStringLiteral("document-export")}, // LibreOffice
52
        {QStringLiteral("exporttopdf"), QStringLiteral("viewpdf")}, // LibreOffice, the icon it uses but the name is quite random
53
        {QStringLiteral("webhtml"), QStringLiteral("text-html")}, // LibreOffice
54
        {QStringLiteral("printpreview"), QStringLiteral("document-print-preview")},
55
        {QStringLiteral("print"), QStringLiteral("document-print")},
56
        {QStringLiteral("print-gtk"), QStringLiteral("document-print")}, // Gimp
57
        {QStringLiteral("mail-image"), QStringLiteral("mail-message-new")}, // Gimp
58
        {QStringLiteral("sendmail"), QStringLiteral("mail-message-new")}, // LibreOffice
59
        {QStringLiteral("sendviabluetooth"), QStringLiteral("preferences-system-bluetooth")}, // LibreOffice
60
        {QStringLiteral("close"), QStringLiteral("document-close")},
61
        {QStringLiteral("closedoc"), QStringLiteral("document-close")},
62
        {QStringLiteral("close-all"), QStringLiteral("document-close")},
63
        {QStringLiteral("closeall"), QStringLiteral("document-close")},
64
        {QStringLiteral("closewin"), QStringLiteral("window-close")}, // LibreOffice
65
        {QStringLiteral("quit"), QStringLiteral("application-exit")},
66
67
        {QStringLiteral("undo"), QStringLiteral("edit-undo")},
68
        {QStringLiteral("redo"), QStringLiteral("edit-redo")},
69
        {QStringLiteral("revert"), QStringLiteral("document-revert")},
70
        {QStringLiteral("cut"), QStringLiteral("edit-cut")},
71
        {QStringLiteral("copy"), QStringLiteral("edit-copy")},
72
        {QStringLiteral("paste"), QStringLiteral("edit-paste")},
73
        {QStringLiteral("duplicate"), QStringLiteral("edit-duplicate")},
74
75
        {QStringLiteral("preferences"), QStringLiteral("settings-configure")},
76
        {QStringLiteral("optionstreedialog"), QStringLiteral("settings-configure")}, // LibreOffice
77
        {QStringLiteral("keyboard-shortcuts"), QStringLiteral("configure-shortcuts")},
78
79
        {QStringLiteral("fullscreen"), QStringLiteral("view-fullscreen")},
80
81
        {QStringLiteral("find"), QStringLiteral("edit-find")},
82
        {QStringLiteral("searchfind"), QStringLiteral("edit-find")},
83
        {QStringLiteral("replace"), QStringLiteral("edit-find-replace")},
84
        {QStringLiteral("searchreplace"), QStringLiteral("edit-find-replace")}, // LibreOffice
85
        {QStringLiteral("searchdialog"), QStringLiteral("edit-find-replace")}, // LibreOffice
86
        {QStringLiteral("select-all"), QStringLiteral("edit-select-all")},
87
        {QStringLiteral("selectall"), QStringLiteral("edit-select-all")},
88
        {QStringLiteral("select-none"), QStringLiteral("edit-select-invert")},
89
        {QStringLiteral("select-invert"), QStringLiteral("edit-select-invert")},
90
91
        {QStringLiteral("increasesize"), QStringLiteral("zoom-in")},
92
        {QStringLiteral("decreasesize"), QStringLiteral("zoom-out")},
93
        {QStringLiteral("zoom-in"), QStringLiteral("zoom-in")},
94
        {QStringLiteral("zoom-out"), QStringLiteral("zoom-out")},
95
        {QStringLiteral("zoomfit"), QStringLiteral("zoom-fit-best")},
96
        {QStringLiteral("zoom-fit-in"), QStringLiteral("zoom-fit-best")},
97
        {QStringLiteral("show-guides"), QStringLiteral("show-guides")},
98
        {QStringLiteral("show-grid"), QStringLiteral("show-grid")},
99
100
        {QStringLiteral("rotateclockwise"), QStringLiteral("object-rotate-right")},
101
        {QStringLiteral("rotatecounterclockwise"), QStringLiteral("object-rotate-left")},
102
        {QStringLiteral("fliphorizontally"), QStringLiteral("object-flip-horizontal")},
103
        {QStringLiteral("image-flip-horizontal"), QStringLiteral("object-flip-horizontal")},
104
        {QStringLiteral("flipvertically"), QStringLiteral("object-flip-vertical")},
105
        {QStringLiteral("image-flip-vertical"), QStringLiteral("object-flip-vertical")},
106
        {QStringLiteral("image-scale"), QStringLiteral("transform-scale")},
107
108
        {QStringLiteral("bold"), QStringLiteral("format-text-bold")},
109
        {QStringLiteral("italic"), QStringLiteral("format-text-italic")},
110
        {QStringLiteral("underline"), QStringLiteral("format-text-underline")},
111
        {QStringLiteral("strikeout"), QStringLiteral("format-text-strikethrough")},
112
        {QStringLiteral("superscript"), QStringLiteral("format-text-superscript")},
113
        {QStringLiteral("subscript"), QStringLiteral("format-text-subscript")},
114
        // "grow" is a bit unspecific to always set it to "grow font", so use the exact ID here
115
        {QStringLiteral(".uno:Grow"), QStringLiteral("format-font-size-more")}, // LibreOffice
116
        {QStringLiteral(".uno:Shrink"), QStringLiteral("format-font-size-less")}, // LibreOffice
117
        // also a bit unspecific?
118
        {QStringLiteral("alignleft"), QStringLiteral("format-justify-left")},
119
        {QStringLiteral("alignhorizontalcenter"), QStringLiteral("format-justify-center")},
120
        {QStringLiteral("alignright"), QStringLiteral("format-justify-right")},
121
        {QStringLiteral("alignjustified"), QStringLiteral("format-justify-fill")},
122
        {QStringLiteral("incrementindent"), QStringLiteral("format-indent-more")},
123
        {QStringLiteral("decrementindent"), QStringLiteral("format-indent-less")},
124
        {QStringLiteral("defaultbullet"), QStringLiteral("format-list-unordered")}, // LibreOffice
125
        {QStringLiteral("defaultnumbering"), QStringLiteral("format-list-ordered")}, // LibreOffice
126
127
        {QStringLiteral("sortascending"), QStringLiteral("view-sort-ascending")},
128
        {QStringLiteral("sortdescending"), QStringLiteral("view-sort-descending")},
129
130
        {QStringLiteral("autopilotmenu"), QStringLiteral("tools-wizard")}, // LibreOffice
131
132
        {QStringLiteral("layers-new"), QStringLiteral("layer-new")},
133
        {QStringLiteral("layers-duplicate"), QStringLiteral("layer-duplicate")},
134
        {QStringLiteral("layers-delete"), QStringLiteral("layer-delete")},
135
        {QStringLiteral("layers-anchor"), QStringLiteral("anchor")},
136
137
        {QStringLiteral("slideshow"), QStringLiteral("media-playback-start")}, // Gwenview uses this icon for that
138
        {QStringLiteral("playvideo"), QStringLiteral("media-playback-start")},
139
140
        {QStringLiteral("addtags"), QStringLiteral("tag-new")},
141
        {QStringLiteral("newevent"), QStringLiteral("appointment-new")},
142
143
        {QStringLiteral("previous-document"), QStringLiteral("go-previous")},
144
        {QStringLiteral("prevphoto"), QStringLiteral("go-previous")},
145
        {QStringLiteral("next-document"), QStringLiteral("go-next")},
146
        {QStringLiteral("nextphoto"), QStringLiteral("go-next")},
147
148
        {QStringLiteral("redeye"), QStringLiteral("redeyes")},
149
        {QStringLiteral("crop"), QStringLiteral("transform-crop")},
150
        {QStringLiteral("move"), QStringLiteral("transform-move")},
151
        {QStringLiteral("rotate"), QStringLiteral("transform-rotate")},
152
        {QStringLiteral("scale"), QStringLiteral("transform-scale")},
153
        {QStringLiteral("shear"), QStringLiteral("transform-shear")},
154
        {QStringLiteral("flip"), QStringLiteral("object-flip-horizontal")},
155
        {QStringLiteral("flag"), QStringLiteral("flag-red")}, // is there a "mark" or "important" icon that isn't email?
156
157
        {QStringLiteral("tools-measure"), QStringLiteral("measure")},
158
        {QStringLiteral("tools-text"), QStringLiteral("draw-text")},
159
        {QStringLiteral("tools-color-picker"), QStringLiteral("color-picker")},
160
        {QStringLiteral("tools-paintbrush"), QStringLiteral("draw-brush")},
161
        {QStringLiteral("tools-eraser"), QStringLiteral("draw-eraser")},
162
        {QStringLiteral("tools-paintbrush"), QStringLiteral("draw-brush")},
163
164
        {QStringLiteral("help"), QStringLiteral("help-contents")},
165
        {QStringLiteral("helpindex"), QStringLiteral("help-contents")},
166
        {QStringLiteral("helpcontents"), QStringLiteral("help-contents")},
167
        {QStringLiteral("context-help"), QStringLiteral("help-whatsthis")},
168
        {QStringLiteral("extendedhelp"), QStringLiteral("help-whatsthis")}, // LibreOffice
169
        {QStringLiteral("helpreportproblem"), QStringLiteral("tools-report-bug")},
170
        {QStringLiteral("sendfeedback"), QStringLiteral("tools-report-bug")}, // LibreOffice
171
        {QStringLiteral("about"), QStringLiteral("help-about")},
172
173
        {QStringLiteral("emptytrash"), QStringLiteral("trash-empty")},
174
        {QStringLiteral("movetotrash"), QStringLiteral("user-trash-symbolic")},
175
176
        // Gnome help
177
        {QStringLiteral("yelp-application-larger-text"), QStringLiteral("format-font-size-more")},
178
        {QStringLiteral("yelp-application-smaller-text"), QStringLiteral("format-font-size-less")}, // LibreOffice
179
180
        // LibreOffice documents in its New menu
181
        {QStringLiteral("private:factory/swriter"), QStringLiteral("application-vnd.oasis.opendocument.text")},
182
        {QStringLiteral("private:factory/scalc"), QStringLiteral("application-vnd.oasis.opendocument.spreadsheet")},
183
        {QStringLiteral("private:factory/simpress"), QStringLiteral("application-vnd.oasis.opendocument.presentation")},
184
        {QStringLiteral("private:factory/sdraw"), QStringLiteral("application-vnd.oasis.opendocument.graphics")},
185
        {QStringLiteral("private:factory/swriter/web"), QStringLiteral("text-html")},
186
        {QStringLiteral("private:factory/smath"), QStringLiteral("application-vnd.oasis.opendocument.formula")},
187
    };
188
189
    // Sometimes we get additional arguments (?slot=123) we don't care about
190
    const int questionMarkIndex = action.indexOf(QLatin1Char('?'));
191
    if (questionMarkIndex > -1) {
192
        action.truncate(questionMarkIndex);
193
    }
194
195
    icon = s_icons.value(action);
196
197
    if (icon.isEmpty()) {
198
        const int dotIndex = action.indexOf(QLatin1Char('.')); // app., win., or unity. prefix
199
        if (dotIndex > -1) {
200
            action = action.mid(dotIndex + 1);
201
        }
202
203
        icon = s_icons.value(action);
204
    }
205
206
    if (icon.isEmpty()) {
207
        static const auto s_dup1Prefix = QStringLiteral("dup:1:"); // can it be dup2 also?
208
        if (action.startsWith(s_dup1Prefix)) {
209
            action = action.mid(s_dup1Prefix.length());
210
        }
211
212
        static const auto s_unoPrefix = QStringLiteral(".uno:"); // LibreOffice with appmenu-gtk
213
        if (action.startsWith(s_unoPrefix)) {
214
            action = action.mid(s_unoPrefix.length());
215
        }
216
217
        // LibreOffice's "Open" entry is always "OpenFromAppname" so we just chop that off
218
        if (action.startsWith(QLatin1String("OpenFrom"))) {
219
            action = action.left(4); // basically "Open"
220
        }
221
222
        icon = s_icons.value(action);
223
    }
224
225
    if (icon.isEmpty()) {
226
        static const auto s_commonPrefix = QStringLiteral("Common");
227
        if (action.startsWith(s_commonPrefix)) {
228
            action = action.mid(s_commonPrefix.length());
229
        }
230
231
        icon = s_icons.value(action);
232
    }
233
234
    if (icon.isEmpty()) {
235
        static const auto s_prefixes = QStringList{ // Gimp with appmenu-gtk
236
            QStringLiteral("file-"),
237
            QStringLiteral("edit-"),
238
            QStringLiteral("view-"),
239
            QStringLiteral("image-"),
240
            QStringLiteral("layers-"),
241
            QStringLiteral("colors-"),
242
            QStringLiteral("tools-"),
243
            QStringLiteral("plug-in-"),
244
            QStringLiteral("windows-"),
245
            QStringLiteral("dialogs-"),
246
            QStringLiteral("help-"),
247
        };
248
249
        for (const QString &prefix : s_prefixes) {
250
            if (action.startsWith(prefix)) {
251
                action = action.mid(prefix.length());
252
                break;
253
            }
254
        }
255
256
        icon = s_icons.value(action);
257
    }
258
259
    if (icon.isEmpty()) {
260
        action = action.toLower();
261
        icon = s_icons.value(action);
262
    }
263
264
    if (icon.isEmpty()) {
265
        static const auto s_prefixes = QStringList{ // Pluma with appmenu-gtk
266
            QStringLiteral("file"),
267
            QStringLiteral("edit"),
268
            QStringLiteral("view"),
269
            QStringLiteral("help"),
270
        };
271
272
273
        for (const QString &prefix : s_prefixes) {
274
            if (action.startsWith(prefix)) {
275
                action = action.mid(prefix.length());
276
                break;
277
            }
278
        }
279
280
        icon = s_icons.value(action);
281
    }
282
283
    return icon;
284
}
(-)a/gmenu-dbusmenu-proxy/icons.h (+29 lines)
Line 0 Link Here
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 <QString>
23
24
namespace Icons
25
{
26
27
QString actionIcon(const QString &actionName);
28
29
}
(-)a/gmenu-dbusmenu-proxy/main.cpp (+50 lines)
Line 0 Link Here
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 <QGuiApplication>
21
#include <QSessionManager>
22
23
#include <KWindowSystem>
24
25
#include "menuproxy.h"
26
27
int main(int argc, char ** argv)
28
{
29
    qputenv("QT_QPA_PLATFORM", "xcb");
30
31
    QGuiApplication::setDesktopSettingsAware(false);
32
33
    QGuiApplication app(argc, argv);
34
35
    if (!KWindowSystem::isPlatformX11()) {
36
        qFatal("qdbusmenuproxy is only useful XCB. Aborting");
37
    }
38
39
    auto disableSessionManagement = [](QSessionManager &sm) {
40
        sm.setRestartHint(QSessionManager::RestartNever);
41
    };
42
    QObject::connect(&app, &QGuiApplication::commitDataRequest, disableSessionManagement);
43
    QObject::connect(&app, &QGuiApplication::saveStateRequest, disableSessionManagement);
44
45
    app.setQuitOnLastWindowClosed(false);
46
47
    MenuProxy proxy;
48
49
    return app.exec();
50
}
(-)a/gmenu-dbusmenu-proxy/menu.cpp (+354 lines)
Line 0 Link Here
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 "menu.h"
21
22
#include "debug.h"
23
24
#include <QDBusConnection>
25
#include <QDBusMessage>
26
#include <QDBusPendingCallWatcher>
27
#include <QDBusPendingReply>
28
#include <QDebug>
29
#include <QVariantList>
30
31
#include <algorithm>
32
33
#include "utils.h"
34
35
static const QString s_orgGtkMenus = QStringLiteral("org.gtk.Menus");
36
37
Menu::Menu(const QString &serviceName, const QString &objectPath, QObject *parent)
38
    : QObject(parent)
39
    , m_serviceName(serviceName)
40
    , m_objectPath(objectPath)
41
{
42
    Q_ASSERT(!serviceName.isEmpty());
43
    Q_ASSERT(!m_objectPath.isEmpty());
44
45
    if (!QDBusConnection::sessionBus().connect(m_serviceName,
46
                                               m_objectPath,
47
                                               s_orgGtkMenus,
48
                                               QStringLiteral("Changed"),
49
                                               this,
50
                                               SLOT(onMenuChanged(GMenuChangeList)))) {
51
        qCWarning(DBUSMENUPROXY) << "Failed to subscribe to menu changes for" << parent << "on" << serviceName << "at" << objectPath;
52
    }
53
}
54
55
Menu::~Menu() = default;
56
57
void Menu::cleanup()
58
{
59
    stop(m_subscriptions);
60
}
61
62
void Menu::start(uint id)
63
{
64
    if (m_subscriptions.contains(id)) {
65
        return;
66
    }
67
68
    // TODO watch service disappearing?
69
70
    // dbus-send --print-reply --session --dest=:1.103 /org/libreoffice/window/104857641/menus/menubar org.gtk.Menus.Start array:uint32:0
71
72
    QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName,
73
                                                      m_objectPath,
74
                                                      s_orgGtkMenus,
75
                                                      QStringLiteral("Start"));
76
    msg.setArguments({
77
        QVariant::fromValue(QList<uint>{id})
78
    });
79
80
    QDBusPendingReply<GMenuItemList> reply = QDBusConnection::sessionBus().asyncCall(msg);
81
    QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
82
    connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id](QDBusPendingCallWatcher *watcher) {
83
        QScopedPointer<QDBusPendingCallWatcher, QScopedPointerDeleteLater> watcherPtr(watcher);
84
85
        QDBusPendingReply<GMenuItemList> reply = *watcherPtr;
86
        if (reply.isError()) {
87
            qCWarning(DBUSMENUPROXY) << "Failed to start subscription to" << id << "on" << m_serviceName << "at" << m_objectPath << reply.error();
88
            emit failedToSubscribe(id);
89
        } else {
90
            const bool hadMenu = !m_menus.isEmpty();
91
92
            const auto menus = reply.value();
93
            for (auto menu : menus) {
94
                m_menus[menu.id].append(menus);
95
            }
96
97
            // LibreOffice on startup fails to give us some menus right away, we'll also subscribe in onMenuChanged() if neccessary
98
            if (menus.isEmpty()) {
99
                qCWarning(DBUSMENUPROXY) << "Got an empty menu for" << id << "on" << m_serviceName << "at" << m_objectPath;
100
                return;
101
            }
102
103
            // TODO are we subscribed to all it returns or just to the ones we requested?
104
            m_subscriptions.append(id);
105
106
            // do we have a menu now? let's tell everyone
107
            if (!hadMenu && !m_menus.isEmpty()) {
108
                emit menuAppeared();
109
            }
110
111
            emit subscribed(id);
112
        }
113
    });
114
}
115
116
void Menu::stop(const QList<uint> &ids)
117
{
118
    QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName,
119
                                                      m_objectPath,
120
                                                      s_orgGtkMenus,
121
                                                      QStringLiteral("End"));
122
    msg.setArguments({
123
        QVariant::fromValue(ids) // don't let it unwrap it, hence in a variant
124
    });
125
126
    QDBusPendingReply<void> reply = QDBusConnection::sessionBus().asyncCall(msg);
127
    QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
128
    connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, ids](QDBusPendingCallWatcher *watcher) {
129
        QDBusPendingReply<void> reply = *watcher;
130
        if (reply.isError()) {
131
            qCWarning(DBUSMENUPROXY) << "Failed to stop subscription to" << ids << "on" << m_serviceName << "at" << m_objectPath << reply.error();
132
        } else {
133
            // remove all subscriptions that we unsubscribed from
134
            // TODO is there a nicer algorithm for that?
135
            // TODO remove all m_menus also?
136
            m_subscriptions.erase(std::remove_if(m_subscriptions.begin(), m_subscriptions.end(),
137
                                      std::bind(&QList<uint>::contains, m_subscriptions, std::placeholders::_1)),
138
                                  m_subscriptions.end());
139
140
            if (m_subscriptions.isEmpty()) {
141
                emit menuDisappeared();
142
            }
143
        }
144
        watcher->deleteLater();
145
    });
146
}
147
148
bool Menu::hasMenu() const
149
{
150
    return !m_menus.isEmpty();
151
}
152
153
bool Menu::hasSubscription(uint subscription) const
154
{
155
    return m_subscriptions.contains(subscription);
156
}
157
158
GMenuItem Menu::getSection(int id, bool *ok) const
159
{
160
    int subscription;
161
    int section;
162
    int index;
163
    Utils::intToTreeStructure(id, subscription, section, index);
164
    return getSection(subscription, section, ok);
165
}
166
167
GMenuItem Menu::getSection(int subscription, int section, bool *ok) const
168
{
169
    const auto menu = m_menus.value(subscription);
170
171
    auto it = std::find_if(menu.begin(), menu.end(), [section](const GMenuItem &item) {
172
        return item.section == section;
173
    });
174
175
    if (it == menu.end()) {
176
        if (ok) {
177
            *ok = false;
178
        }
179
        return GMenuItem();
180
    }
181
182
    if (ok) {
183
        *ok = true;
184
    }
185
    return *it;
186
}
187
188
QVariantMap Menu::getItem(int id) const
189
{
190
    int subscription;
191
    int section;
192
    int index;
193
    Utils::intToTreeStructure(id, subscription, section, index);
194
    return getItem(subscription, section, index);
195
}
196
197
QVariantMap Menu::getItem(int subscription, int sectionId, int index) const
198
{
199
    bool ok;
200
    const GMenuItem section = getSection(subscription, sectionId, &ok);
201
202
    if (!ok) {
203
        return QVariantMap();
204
    }
205
206
    const auto items = section.items;
207
208
    if (items.count() < index) {
209
        qCWarning(DBUSMENUPROXY) << "Cannot get action" << subscription << sectionId << index << "which is out of bounds";
210
        return QVariantMap();
211
    }
212
213
    // 0 is the menu itself, items start at 1
214
    return items.at(index - 1);
215
}
216
217
void Menu::onMenuChanged(const GMenuChangeList &changes)
218
{
219
    const bool hadMenu = !m_menus.isEmpty();
220
221
    QVector<uint> dirtyMenus;
222
    QVector<uint> dirtyItems;
223
224
    for (const auto &change : changes) {
225
        auto updateSection = [&](GMenuItem &section) {
226
            // Check if the amount of inserted items is identical to the items to be removed,
227
            // just update the existing items and signal a change for that.
228
            // LibreOffice tends to do that e.g. to update its Undo menu entry
229
            if (change.itemsToRemoveCount == change.itemsToInsert.count()) {
230
                for (int i = 0; i < change.itemsToInsert.count(); ++i) {
231
                    const auto &newItem = change.itemsToInsert.at(i);
232
233
                    section.items[change.changePosition + i] = newItem;
234
235
                    // 0 is the menu itself, items start at 1
236
                    dirtyItems.append(Utils::treeStructureToInt(change.subscription, change.menu, change.changePosition + i + 1));
237
                }
238
            } else {
239
                for (int i = 0; i < change.itemsToRemoveCount; ++i) {
240
                    section.items.removeAt(change.changePosition); // TODO bounds check
241
                }
242
243
                for (int i = 0; i < change.itemsToInsert.count(); ++i) {
244
                    section.items.insert(change.changePosition + i, change.itemsToInsert.at(i));
245
                }
246
247
                dirtyMenus.append(Utils::treeStructureToInt(change.subscription, change.menu, 0));
248
            }
249
        };
250
251
        // shouldn't happen, it says only Start() subscribes to changes
252
        if (!m_subscriptions.contains(change.subscription)) {
253
            qCDebug(DBUSMENUPROXY) << "Got menu change for menu" << change.subscription << "that we are not subscribed to, subscribing now";
254
            // LibreOffice doesn't give us a menu right away but takes a while and then signals us a change
255
            start(change.subscription);
256
            continue;
257
        }
258
259
        auto &menu = m_menus[change.subscription];
260
261
        bool sectionFound = false;
262
        // TODO findSectionRef
263
        for (GMenuItem &section : menu) {
264
            if (section.section != change.menu) {
265
                continue;
266
            }
267
268
            qCInfo(DBUSMENUPROXY) << "Updating existing section" << change.menu << "in subscription" << change.subscription;
269
270
            sectionFound = true;
271
            updateSection(section);
272
            break;
273
        }
274
275
        // Insert new section
276
        if (!sectionFound) {
277
            qCInfo(DBUSMENUPROXY) << "Creating new section" << change.menu << "in subscription" << change.subscription;
278
279
            if (change.itemsToRemoveCount > 0) {
280
                qCWarning(DBUSMENUPROXY) << "Menu change requested to remove items from a new (and as such empty) section";
281
            }
282
283
            GMenuItem newSection;
284
            newSection.id = change.subscription;
285
            newSection.section = change.menu;
286
            updateSection(newSection);
287
            menu.append(newSection);
288
        }
289
    }
290
291
    // do we have a menu now? let's tell everyone
292
    if (!hadMenu && !m_menus.isEmpty()) {
293
        emit menuAppeared();
294
    } else if (hadMenu && m_menus.isEmpty()) {
295
        emit menuDisappeared();
296
    }
297
298
    if (!dirtyItems.isEmpty()) {
299
        emit itemsChanged(dirtyItems);
300
    }
301
302
    emit menusChanged(dirtyMenus);
303
}
304
305
void Menu::actionsChanged(const QStringList &dirtyActions, const QString &prefix)
306
{
307
    auto forEachMenuItem = [this](const std::function<bool(int subscription, int section, int index, const QVariantMap &item)> &cb) {
308
        for (auto it = m_menus.constBegin(), end = m_menus.constEnd(); it != end; ++it) {
309
            const int subscription = it.key();
310
311
            for (const auto &menu : it.value()) {
312
                const int section = menu.section;
313
314
                int count = 0;
315
316
                const auto items = menu.items;
317
                for (const auto &item : items) {
318
                    ++count; // 0 is a menu, entries start at 1
319
320
                    if (!cb(subscription, section, count, item)) {
321
                        goto loopExit; // hell yeah
322
                        break;
323
                    }
324
                }
325
            }
326
        }
327
328
        loopExit: // loop exit
329
        return;
330
    };
331
332
    // now find in which menus these actions are and emit a change accordingly
333
    QVector<uint> dirtyItems;
334
335
    for (const QString &action : dirtyActions) {
336
        const QString prefixedAction = prefix + action;
337
338
        forEachMenuItem([this, &prefixedAction, &dirtyItems](int subscription, int section, int index, const QVariantMap &item) {
339
            const QString actionName = Utils::itemActionName(item);
340
341
            if (actionName == prefixedAction) {
342
                dirtyItems.append(Utils::treeStructureToInt(subscription, section, index));
343
                return false; // break
344
            }
345
346
            return true; // continue
347
        });
348
    }
349
350
    if (!dirtyItems.isEmpty()) {
351
        emit itemsChanged(dirtyItems);
352
    }
353
}
354
(-)a/gmenu-dbusmenu-proxy/menu.h (+81 lines)
Line 0 Link Here
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
#include <QVector>
25
26
#include "gdbusmenutypes_p.h"
27
#include "../libdbusmenuqt/dbusmenutypes_p.h"
28
29
class Menu : public QObject
30
{
31
    Q_OBJECT
32
33
public:
34
    Menu(const QString &serviceName, const QString &objectPath, QObject *parent = nullptr);
35
    ~Menu();
36
37
    void init();
38
    void cleanup();
39
40
    void start(uint id);
41
    void stop(const QList<uint> &ids);
42
43
    bool hasMenu() const;
44
    bool hasSubscription(uint subscription) const;
45
46
    GMenuItem getSection(int id, bool *ok = nullptr) const;
47
    GMenuItem getSection(int subscription, int sectionId, bool *ok = nullptr) const;
48
49
    QVariantMap getItem(int id) const; // bool ok argument?
50
    QVariantMap getItem(int subscription, int sectionId, int id) const;
51
52
public slots:
53
    void actionsChanged(const QStringList &dirtyActions, const QString &prefix);
54
55
signals:
56
    void menuAppeared(); // emitted the first time a menu was successfully loaded
57
    void menuDisappeared();
58
59
    void subscribed(uint id);
60
    void failedToSubscribe(uint id);
61
62
    void itemsChanged(const QVector<uint> &itemIds);
63
    void menusChanged(const QVector<uint> &menuIds);
64
65
private slots:
66
    void onMenuChanged(const GMenuChangeList &changes);
67
68
private:
69
    void initMenu();
70
71
    void menuChanged(const GMenuChangeList &changes);
72
73
    // QSet?
74
    QList<uint> m_subscriptions; // keeps track of which menu trees we're subscribed to
75
76
    QHash<uint, GMenuItemList> m_menus;
77
78
    QString m_serviceName;
79
    QString m_objectPath;
80
81
};
(-)a/gmenu-dbusmenu-proxy/menuproxy.cpp (+291 lines)
Line 0 Link Here
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 "menuproxy.h"
21
22
#include <config-X11.h>
23
24
#include "debug.h"
25
26
#include <QByteArray>
27
#include <QCoreApplication>
28
#include <QDBusConnection>
29
#include <QDBusConnectionInterface>
30
#include <QDBusServiceWatcher>
31
#include <QDir>
32
#include <QFileInfo>
33
#include <QHash>
34
#include <QStandardPaths>
35
36
#include <KConfigGroup>
37
#include <KSharedConfig>
38
#include <KWindowSystem>
39
40
#include <QX11Info>
41
#include <xcb/xcb.h>
42
#include <xcb/xcb_atom.h>
43
44
#include "window.h"
45
46
static const QString s_ourServiceName = QStringLiteral("org.kde.plasma.gmenu_dbusmenu_proxy");
47
48
static const QString s_dbusMenuRegistrar = QStringLiteral("com.canonical.AppMenu.Registrar");
49
50
static const QByteArray s_gtkUniqueBusName = QByteArrayLiteral("_GTK_UNIQUE_BUS_NAME");
51
52
static const QByteArray s_gtkApplicationObjectPath = QByteArrayLiteral("_GTK_APPLICATION_OBJECT_PATH");
53
static const QByteArray s_unityObjectPath = QByteArrayLiteral("_UNITY_OBJECT_PATH");
54
static const QByteArray s_gtkWindowObjectPath = QByteArrayLiteral("_GTK_WINDOW_OBJECT_PATH");
55
static const QByteArray s_gtkMenuBarObjectPath = QByteArrayLiteral("_GTK_MENUBAR_OBJECT_PATH");
56
// that's the generic app menu with Help and Options and will be used if window doesn't have a fully-blown menu bar
57
static const QByteArray s_gtkAppMenuObjectPath = QByteArrayLiteral("_GTK_APP_MENU_OBJECT_PATH");
58
59
static const QByteArray s_kdeNetWmAppMenuServiceName = QByteArrayLiteral("_KDE_NET_WM_APPMENU_SERVICE_NAME");
60
static const QByteArray s_kdeNetWmAppMenuObjectPath = QByteArrayLiteral("_KDE_NET_WM_APPMENU_OBJECT_PATH");
61
62
MenuProxy::MenuProxy()
63
    : QObject()
64
    , m_xConnection(QX11Info::connection())
65
    , m_serviceWatcher(new QDBusServiceWatcher(this))
66
{
67
    m_serviceWatcher->setConnection(QDBusConnection::sessionBus());
68
    m_serviceWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration |
69
                                     QDBusServiceWatcher::WatchForRegistration);
70
    m_serviceWatcher->addWatchedService(s_dbusMenuRegistrar);
71
72
    connect(m_serviceWatcher, &QDBusServiceWatcher::serviceRegistered, this, [this](const QString &service) {
73
        Q_UNUSED(service);
74
        qCDebug(DBUSMENUPROXY) << "Global menu service became available, starting";
75
        init();
76
    });
77
    connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this](const QString &service) {
78
        Q_UNUSED(service);
79
        qCDebug(DBUSMENUPROXY) << "Global menu service disappeared, cleaning up";
80
        teardown();
81
    });
82
83
    // It's fine to do a blocking call here as we're a separate binary with no UI
84
    if (QDBusConnection::sessionBus().interface()->isServiceRegistered(s_dbusMenuRegistrar)) {
85
        qCDebug(DBUSMENUPROXY) << "Global menu service is running, starting right away";
86
        init();
87
    } else {
88
        qCDebug(DBUSMENUPROXY) << "No global menu service available, waiting for it to start before doing anything";
89
90
        // be sure when started to restore gtk menus when there's no dbus menu around in case we crashed
91
        setGtkShellShowsMenuBar(false);
92
    }
93
}
94
95
MenuProxy::~MenuProxy()
96
{
97
    teardown();
98
}
99
100
bool MenuProxy::init()
101
{
102
    if (!QDBusConnection::sessionBus().registerService(s_ourServiceName)) {
103
        qCWarning(DBUSMENUPROXY) << "Failed to register DBus service" << s_ourServiceName;
104
        return false;
105
    }
106
107
    setGtkShellShowsMenuBar(true);
108
109
    connect(KWindowSystem::self(), &KWindowSystem::windowAdded, this, &MenuProxy::onWindowAdded);
110
    connect(KWindowSystem::self(), &KWindowSystem::windowRemoved, this, &MenuProxy::onWindowRemoved);
111
112
    const auto windows = KWindowSystem::windows();
113
    for (WId id : windows) {
114
        onWindowAdded(id);
115
    }
116
117
    if (m_windows.isEmpty()) {
118
        qCDebug(DBUSMENUPROXY) << "Up and running but no windows with menus in sight";
119
    }
120
121
    return true;
122
}
123
124
void MenuProxy::teardown()
125
{
126
    setGtkShellShowsMenuBar(false);
127
128
    QDBusConnection::sessionBus().unregisterService(s_ourServiceName);
129
130
    disconnect(KWindowSystem::self(), &KWindowSystem::windowAdded, this, &MenuProxy::onWindowAdded);
131
    disconnect(KWindowSystem::self(), &KWindowSystem::windowRemoved, this, &MenuProxy::onWindowRemoved);
132
133
    qDeleteAll(m_windows);
134
    m_windows.clear();
135
}
136
137
void MenuProxy::setGtkShellShowsMenuBar(bool show)
138
{
139
    qCDebug(DBUSMENUPROXY) << "Setting gtk-shell-shows-menu-bar to" << show << "which will" << (show ? "hide" : "show") << "menu bars in applications";
140
141
    // mostly taken from kde-gtk-config
142
    QString root = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation);
143
    if (root.isEmpty()) {
144
        root = QFileInfo(QDir::home(), QStringLiteral(".config")).absoluteFilePath();
145
    }
146
147
    const QString settingsFilePath = root + QStringLiteral("/gtk-3.0/settings.ini");
148
149
    auto cfg = KSharedConfig::openConfig(settingsFilePath, KConfig::NoGlobals);
150
    KConfigGroup group(cfg, "Settings");
151
152
    if (show) {
153
        group.writeEntry("gtk-shell-shows-menubar", true);
154
    } else {
155
        group.deleteEntry("gtk-shell-shows-menubar");
156
    }
157
158
    group.sync();
159
160
    // TODO use gconf/dconf directly or at least signal a change somehow?
161
}
162
163
void MenuProxy::onWindowAdded(WId id)
164
{
165
    if (m_windows.contains(id)) {
166
        return;
167
    }
168
169
    KWindowInfo info(id, NET::WMWindowType);
170
171
    NET::WindowType wType = info.windowType(NET::NormalMask | NET::DesktopMask | NET::DockMask |
172
                                            NET::ToolbarMask | NET::MenuMask | NET::DialogMask |
173
                                            NET::OverrideMask | NET::TopMenuMask |
174
                                            NET::UtilityMask | NET::SplashMask);
175
176
    // Only top level windows typically have a menu bar, dialogs, such as settings don't
177
    if (wType != NET::Normal) {
178
        qCInfo(DBUSMENUPROXY) << "Ignoring window" << id << "of type" << wType;
179
        return;
180
    }
181
182
    const QString serviceName = QString::fromUtf8(getWindowPropertyString(id, s_gtkUniqueBusName));
183
184
    if (serviceName.isEmpty()) {
185
        return;
186
    }
187
188
    const QString applicationObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkApplicationObjectPath));
189
    const QString unityObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_unityObjectPath));
190
    const QString windowObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkWindowObjectPath));
191
192
    const QString applicationMenuObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkAppMenuObjectPath));
193
    const QString menuBarObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkMenuBarObjectPath));
194
195
    if (applicationMenuObjectPath.isEmpty() && menuBarObjectPath.isEmpty()) {
196
        return;
197
    }
198
199
    Window *window = new Window(serviceName);
200
    window->setWinId(id);
201
    window->setApplicationObjectPath(applicationObjectPath);
202
    window->setUnityObjectPath(unityObjectPath);
203
    window->setWindowObjectPath(windowObjectPath);
204
    window->setApplicationMenuObjectPath(applicationMenuObjectPath);
205
    window->setMenuBarObjectPath(menuBarObjectPath);
206
    m_windows.insert(id, window);
207
208
    connect(window, &Window::requestWriteWindowProperties, this, [this, window] {
209
       Q_ASSERT(!window->proxyObjectPath().isEmpty());
210
211
       writeWindowProperty(window->winId(), s_kdeNetWmAppMenuServiceName, s_ourServiceName.toUtf8());
212
       writeWindowProperty(window->winId(), s_kdeNetWmAppMenuObjectPath, window->proxyObjectPath().toUtf8());
213
    });
214
    connect(window, &Window::requestRemoveWindowProperties, this, [this, window] {
215
        writeWindowProperty(window->winId(), s_kdeNetWmAppMenuServiceName, QByteArray());
216
        writeWindowProperty(window->winId(), s_kdeNetWmAppMenuObjectPath, QByteArray());
217
    });
218
219
    window->init();
220
}
221
222
void MenuProxy::onWindowRemoved(WId id)
223
{
224
    // no need to cleanup() (which removes window properties) when the window is gone, delete right away
225
    delete m_windows.take(id);
226
}
227
228
QByteArray MenuProxy::getWindowPropertyString(WId id, const QByteArray &name)
229
{
230
    QByteArray value;
231
232
    auto atom = getAtom(name);
233
    if (atom == XCB_ATOM_NONE) {
234
        return value;
235
    }
236
237
    // GTK properties aren't XCB_ATOM_STRING but a custom one
238
    auto utf8StringAtom = getAtom(QByteArrayLiteral("UTF8_STRING"));
239
240
    static const long MAX_PROP_SIZE = 10000;
241
    auto propertyCookie = xcb_get_property(m_xConnection, false, id, atom, utf8StringAtom, 0, MAX_PROP_SIZE);
242
    QScopedPointer<xcb_get_property_reply_t, QScopedPointerPodDeleter> propertyReply(xcb_get_property_reply(m_xConnection, propertyCookie, NULL));
243
    if (propertyReply.isNull()) {
244
        qCWarning(DBUSMENUPROXY) << "XCB property reply for atom" << name << "on" << id << "was null";
245
        return value;
246
    }
247
248
    if (propertyReply->type == utf8StringAtom && propertyReply->format == 8 && propertyReply->value_len > 0) {
249
        const char *data = (const char *) xcb_get_property_value(propertyReply.data());
250
        int len = propertyReply->value_len;
251
        if (data) {
252
            value = QByteArray(data, data[len - 1] ? len : len - 1);
253
        }
254
    }
255
256
    return value;
257
}
258
259
void MenuProxy::writeWindowProperty(WId id, const QByteArray &name, const QByteArray &value)
260
{
261
    auto atom = getAtom(name);
262
    if (atom == XCB_ATOM_NONE) {
263
        return;
264
    }
265
266
    if (value.isEmpty()) {
267
        xcb_delete_property(m_xConnection, id, atom);
268
    } else {
269
        xcb_change_property(m_xConnection, XCB_PROP_MODE_REPLACE, id, atom, XCB_ATOM_STRING,
270
                            8, value.length(), value.constData());
271
    }
272
}
273
274
xcb_atom_t MenuProxy::getAtom(const QByteArray &name)
275
{
276
    static QHash<QByteArray, xcb_atom_t> s_atoms;
277
278
    auto atom = s_atoms.value(name, XCB_ATOM_NONE);
279
    if (atom == XCB_ATOM_NONE) {
280
        const xcb_intern_atom_cookie_t atomCookie = xcb_intern_atom(m_xConnection, false, name.length(), name.constData());
281
        QScopedPointer<xcb_intern_atom_reply_t, QScopedPointerPodDeleter> atomReply(xcb_intern_atom_reply(m_xConnection, atomCookie, Q_NULLPTR));
282
        if (!atomReply.isNull()) {
283
            atom = atomReply->atom;
284
            if (atom != XCB_ATOM_NONE) {
285
                s_atoms.insert(name, atom);
286
            }
287
        }
288
    }
289
290
    return atom;
291
}
(-)a/gmenu-dbusmenu-proxy/menuproxy.h (+61 lines)
Line 0 Link Here
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 <QByteArray>
24
#include <QHash>
25
#include <QWindow> // for WId
26
27
#include <xcb/xcb_atom.h>
28
29
class QDBusServiceWatcher;
30
31
class Window;
32
33
class MenuProxy : public QObject
34
{
35
    Q_OBJECT
36
37
public:
38
    MenuProxy();
39
    ~MenuProxy() override;
40
41
private Q_SLOTS:
42
    void onWindowAdded(WId id);
43
    void onWindowRemoved(WId id);
44
45
private:
46
    bool init();
47
    void teardown();
48
49
    void setGtkShellShowsMenuBar(bool show);
50
51
    xcb_connection_t *m_xConnection;
52
53
    QByteArray getWindowPropertyString(WId id, const QByteArray &name);
54
    void writeWindowProperty(WId id, const QByteArray &name, const QByteArray &value);
55
    xcb_atom_t getAtom(const QByteArray &name);
56
57
    QHash<WId, Window *> m_windows;
58
59
    QDBusServiceWatcher *m_serviceWatcher;
60
61
};
(-)a/gmenu-dbusmenu-proxy/utils.cpp (+42 lines)
Line 0 Link Here
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 "utils.h"
21
22
int Utils::treeStructureToInt(int subscription, int section, int index)
23
{
24
    return subscription * 1000000 + section * 1000 + index;
25
}
26
27
void Utils::intToTreeStructure(int source, int &subscription, int &section, int &index)
28
{
29
    // TODO some better math :) or bit shifting or something
30
    index = source % 1000;
31
    section = (source / 1000) % 1000;
32
    subscription = source / 1000000;
33
}
34
35
QString Utils::itemActionName(const QVariantMap &item)
36
{
37
    QString actionName = item.value(QStringLiteral("action")).toString();
38
    if (actionName.isEmpty()) {
39
        actionName = item.value(QStringLiteral("submenu-action")).toString();
40
    }
41
    return actionName;
42
}
(-)a/gmenu-dbusmenu-proxy/utils.h (+33 lines)
Line 0 Link Here
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 <QString>
23
#include <QVariantMap>
24
25
namespace Utils
26
{
27
28
int treeStructureToInt(int subscription, int section, int index);
29
void intToTreeStructure(int source, int &subscription, int &section, int &index);
30
31
QString itemActionName(const QVariantMap &item);
32
33
}
(-)a/gmenu-dbusmenu-proxy/window.cpp (+677 lines)
Line 0 Link Here
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 "window.h"
21
22
#include "debug.h"
23
24
#include <QDBusConnection>
25
#include <QDBusMessage>
26
#include <QDBusPendingCallWatcher>
27
#include <QDBusPendingReply>
28
#include <QDebug>
29
#include <QList>
30
#include <QMutableListIterator>
31
#include <QVariantList>
32
33
#include <algorithm>
34
35
#include "actions.h"
36
#include "dbusmenuadaptor.h"
37
#include "icons.h"
38
#include "menu.h"
39
#include "utils.h"
40
41
#include "../libdbusmenuqt/dbusmenushortcut_p.h"
42
43
static const QString s_orgGtkActions = QStringLiteral("org.gtk.Actions");
44
static const QString s_orgGtkMenus = QStringLiteral("org.gtk.Menus");
45
46
static const QString s_applicationActionsPrefix = QStringLiteral("app.");
47
static const QString s_unityActionsPrefix = QStringLiteral("unity.");
48
static const QString s_windowActionsPrefix = QStringLiteral("win.");
49
50
Window::Window(const QString &serviceName)
51
    : QObject()
52
    , m_serviceName(serviceName)
53
{
54
    qCDebug(DBUSMENUPROXY) << "Created menu on" << serviceName;
55
56
    Q_ASSERT(!serviceName.isEmpty());
57
58
    GDBusMenuTypes_register();
59
    DBusMenuTypes_register();
60
}
61
62
Window::~Window() = default;
63
64
void Window::init()
65
{
66
     qCDebug(DBUSMENUPROXY) << "Inited window with menu for" << m_winId << "on" << m_serviceName << "at app" << m_applicationObjectPath << "win" << m_windowObjectPath << "unity" << m_unityObjectPath;
67
68
     if (!m_applicationMenuObjectPath.isEmpty()) {
69
         m_applicationMenu = new Menu(m_serviceName, m_applicationMenuObjectPath, this);
70
         connect(m_applicationMenu, &Menu::menuAppeared, this, &Window::updateWindowProperties);
71
         connect(m_applicationMenu, &Menu::menuDisappeared, this, &Window::updateWindowProperties);
72
         connect(m_applicationMenu, &Menu::subscribed, this, &Window::onMenuSubscribed);
73
         // basically so it replies on DBus no matter what
74
         connect(m_applicationMenu, &Menu::failedToSubscribe, this, &Window::onMenuSubscribed);
75
         connect(m_applicationMenu, &Menu::itemsChanged, this, &Window::menuItemsChanged);
76
         connect(m_applicationMenu, &Menu::menusChanged, this, &Window::menuChanged);
77
     }
78
79
     if (!m_menuBarObjectPath.isEmpty()) {
80
         m_menuBar = new Menu(m_serviceName, m_menuBarObjectPath, this);
81
         connect(m_menuBar, &Menu::menuAppeared, this, &Window::updateWindowProperties);
82
         connect(m_menuBar, &Menu::menuDisappeared, this, &Window::updateWindowProperties);
83
         connect(m_menuBar, &Menu::subscribed, this, &Window::onMenuSubscribed);
84
         connect(m_menuBar, &Menu::failedToSubscribe, this, &Window::onMenuSubscribed);
85
         connect(m_menuBar, &Menu::itemsChanged, this, &Window::menuItemsChanged);
86
         connect(m_menuBar, &Menu::menusChanged, this, &Window::menuChanged);
87
     }
88
89
    if (!m_applicationObjectPath.isEmpty()) {
90
        m_applicationActions = new Actions(m_serviceName, m_applicationObjectPath, this);
91
        connect(m_applicationActions, &Actions::actionsChanged, this, [this](const QStringList &dirtyActions) {
92
            onActionsChanged(dirtyActions, s_applicationActionsPrefix);
93
        });
94
        connect(m_applicationActions, &Actions::loaded, this, [this] {
95
            if (m_menuInited) {
96
                onActionsChanged(m_applicationActions->getAll().keys(), s_applicationActionsPrefix);
97
            } else {
98
                initMenu();
99
            }
100
        });
101
        m_applicationActions->load();
102
    }
103
104
    if (!m_unityObjectPath.isEmpty()) {
105
        m_unityActions = new Actions(m_serviceName, m_unityObjectPath, this);
106
        connect(m_unityActions, &Actions::actionsChanged, this, [this](const QStringList &dirtyActions) {
107
            onActionsChanged(dirtyActions, s_unityActionsPrefix);
108
        });
109
        connect(m_unityActions, &Actions::loaded, this, [this] {
110
            if (m_menuInited) {
111
                onActionsChanged(m_unityActions->getAll().keys(), s_unityActionsPrefix);
112
            } else {
113
                initMenu();
114
            }
115
        });
116
        m_unityActions->load();
117
    }
118
119
    if (!m_windowObjectPath.isEmpty()) {
120
        m_windowActions = new Actions(m_serviceName, m_windowObjectPath, this);
121
        connect(m_windowActions, &Actions::actionsChanged, this, [this](const QStringList &dirtyActions) {
122
            onActionsChanged(dirtyActions, s_windowActionsPrefix);
123
        });
124
        connect(m_windowActions, &Actions::loaded, this, [this] {
125
            if (m_menuInited) {
126
                onActionsChanged(m_windowActions->getAll().keys(), s_windowActionsPrefix);
127
            } else {
128
                initMenu();
129
            }
130
        });
131
        m_windowActions->load();
132
    }
133
}
134
135
WId Window::winId() const
136
{
137
    return m_winId;
138
}
139
140
void Window::setWinId(WId winId)
141
{
142
    m_winId = winId;
143
}
144
145
QString Window::serviceName() const
146
{
147
    return m_serviceName;
148
}
149
150
QString Window::applicationObjectPath() const
151
{
152
    return m_applicationObjectPath;
153
}
154
155
void Window::setApplicationObjectPath(const QString &applicationObjectPath)
156
{
157
    m_applicationObjectPath = applicationObjectPath;
158
}
159
160
QString Window::unityObjectPath() const
161
{
162
    return m_unityObjectPath;
163
}
164
165
void Window::setUnityObjectPath(const QString &unityObjectPath)
166
{
167
    m_unityObjectPath = unityObjectPath;
168
}
169
170
QString Window::applicationMenuObjectPath() const
171
{
172
    return m_applicationMenuObjectPath;
173
}
174
175
void Window::setApplicationMenuObjectPath(const QString &applicationMenuObjectPath)
176
{
177
    m_applicationMenuObjectPath = applicationMenuObjectPath;
178
}
179
180
QString Window::menuBarObjectPath() const
181
{
182
    return m_menuBarObjectPath;
183
}
184
185
void Window::setMenuBarObjectPath(const QString &menuBarObjectPath)
186
{
187
    m_menuBarObjectPath = menuBarObjectPath;
188
}
189
190
QString Window::windowObjectPath() const
191
{
192
    return m_windowObjectPath;
193
}
194
195
void Window::setWindowObjectPath(const QString &windowObjectPath)
196
{
197
    m_windowObjectPath = windowObjectPath;
198
}
199
200
QString Window::currentMenuObjectPath() const
201
{
202
    return m_currentMenuObjectPath;
203
}
204
205
QString Window::proxyObjectPath() const
206
{
207
    return m_proxyObjectPath;
208
}
209
210
void Window::initMenu()
211
{
212
    if (m_menuInited) {
213
        return;
214
    }
215
216
    if (!registerDBusObject()) {
217
        return;
218
    }
219
220
    // appmenu-gtk-module always announces a menu bar on every GTK window even if there is none
221
    // so we subscribe to the menu bar as soon as it shows up so we can figure out
222
    // if we have a menu bar, an app menu, or just nothing
223
    if (m_applicationMenu) {
224
        m_applicationMenu->start(0);
225
    }
226
227
    if (m_menuBar) {
228
        m_menuBar->start(0);
229
    }
230
231
    m_menuInited = true;
232
}
233
234
void Window::menuItemsChanged(const QVector<uint> &itemIds)
235
{
236
    if (qobject_cast<Menu*>(sender()) != m_currentMenu) {
237
        return;
238
    }
239
240
    DBusMenuItemList items;
241
242
    for (uint id : itemIds) {
243
        const auto newItem = m_currentMenu->getItem(id);
244
245
        DBusMenuItem dBusItem{
246
            // 0 is menu, items start at 1
247
            static_cast<int>(id),
248
            gMenuToDBusMenuProperties(newItem)
249
        };
250
        items.append(dBusItem);
251
    }
252
253
    emit ItemsPropertiesUpdated(items, {});
254
}
255
256
void Window::menuChanged(const QVector<uint> &menuIds)
257
{
258
    if (qobject_cast<Menu*>(sender()) != m_currentMenu) {
259
        return;
260
    }
261
262
    for (uint menu : menuIds) {
263
        emit LayoutUpdated(3 /*revision*/, menu);
264
    }
265
}
266
267
void Window::onMenuSubscribed(uint id)
268
{
269
    // When it was a delayed GetLayout request, send the reply now
270
    const auto pendingReplies = m_pendingGetLayouts.values(id);
271
    if (!pendingReplies.isEmpty()) {
272
        for (const auto &pendingReply : pendingReplies) {
273
            if (pendingReply.type() != QDBusMessage::InvalidMessage) {
274
                auto reply = pendingReply.createReply();
275
276
                DBusMenuLayoutItem item;
277
                uint revision = GetLayout(Utils::treeStructureToInt(id, 0, 0), 0, {}, item);
278
279
                reply << revision << QVariant::fromValue(item);
280
281
                QDBusConnection::sessionBus().send(reply);
282
            }
283
        }
284
        m_pendingGetLayouts.remove(id);
285
    } else {
286
        emit LayoutUpdated(2 /*revision*/, id);
287
    }
288
}
289
290
bool Window::getAction(const QString &name, GMenuAction &action) const
291
{
292
    QString lookupName;
293
    Actions *actions = getActionsForAction(name, lookupName);
294
295
    if (!actions) {
296
        return false;
297
    }
298
299
    return actions->get(lookupName, action);
300
}
301
302
void Window::triggerAction(const QString &name, uint timestamp)
303
{
304
    QString lookupName;
305
    Actions *actions = getActionsForAction(name, lookupName);
306
307
    if (!actions) {
308
        return;
309
    }
310
311
    actions->trigger(lookupName, timestamp);
312
}
313
314
Actions *Window::getActionsForAction(const QString &name, QString &lookupName) const
315
{
316
    if (name.startsWith(QLatin1String("app."))) {
317
        lookupName = name.mid(4);
318
        return m_applicationActions;
319
    } else if (name.startsWith(QLatin1String("unity."))) {
320
        lookupName = name.mid(6);
321
        return m_unityActions;
322
    } else if (name.startsWith(QLatin1String("win."))) {
323
        lookupName = name.mid(4);
324
        return m_windowActions;
325
    }
326
327
    return nullptr;
328
}
329
330
void Window::onActionsChanged(const QStringList &dirty, const QString &prefix)
331
{
332
    if (m_applicationMenu) {
333
        m_applicationMenu->actionsChanged(dirty, prefix);
334
    }
335
    if (m_menuBar) {
336
        m_menuBar->actionsChanged(dirty, prefix);
337
    }
338
}
339
340
bool Window::registerDBusObject()
341
{
342
    Q_ASSERT(m_proxyObjectPath.isEmpty());
343
344
    static int menus = 0;
345
    ++menus;
346
347
    new DbusmenuAdaptor(this);
348
349
    const QString objectPath = QStringLiteral("/MenuBar/%1").arg(QString::number(menus));
350
    qCDebug(DBUSMENUPROXY) << "Registering DBus object path" << objectPath;
351
352
    if (!QDBusConnection::sessionBus().registerObject(objectPath, this)) {
353
        qCWarning(DBUSMENUPROXY) << "Failed to register object";
354
        return false;
355
    }
356
357
    m_proxyObjectPath = objectPath;
358
359
    return true;
360
}
361
362
void Window::updateWindowProperties()
363
{
364
    const bool hasMenu = ((m_applicationMenu && m_applicationMenu->hasMenu())
365
                           || (m_menuBar && m_menuBar->hasMenu()));
366
367
    if (!hasMenu) {
368
        emit requestRemoveWindowProperties();
369
        return;
370
    }
371
372
    Menu *oldMenu = m_currentMenu;
373
    Menu *newMenu = qobject_cast<Menu*>(sender());
374
    // set current menu as needed
375
    if (!m_currentMenu) {
376
        m_currentMenu = newMenu;
377
    // Menu Bar takes precedence over application menu
378
    } else if (m_currentMenu == m_applicationMenu && newMenu == m_menuBar) {
379
        qCDebug(DBUSMENUPROXY) << "Switching from application menu to menu bar";
380
        m_currentMenu = newMenu;
381
        // TODO update layout
382
    }
383
384
    if (m_currentMenu != oldMenu) {
385
        // update entire menu now
386
        emit LayoutUpdated(4 /*revision*/, 0);
387
    }
388
389
    emit requestWriteWindowProperties();
390
}
391
392
// DBus
393
bool Window::AboutToShow(int id)
394
{
395
    // We always request the first time GetLayout is called and keep up-to-date internally
396
    // No need to have us prepare anything here
397
    Q_UNUSED(id);
398
    return false;
399
}
400
401
void Window::Event(int id, const QString &eventId, const QDBusVariant &data, uint timestamp)
402
{
403
    Q_UNUSED(data);
404
405
    if (!m_currentMenu) {
406
        return;
407
    }
408
409
    // GMenu dbus doesn't have any "opened" or "closed" signals, we'll only handle "clicked"
410
411
    if (eventId == QLatin1String("clicked")) {
412
        const QString action = m_currentMenu->getItem(id).value(QStringLiteral("action")).toString();
413
        if (!action.isEmpty()) {
414
            triggerAction(action, timestamp);
415
        }
416
    }
417
418
}
419
420
DBusMenuItemList Window::GetGroupProperties(const QList<int> &ids, const QStringList &propertyNames)
421
{
422
    Q_UNUSED(ids);
423
    Q_UNUSED(propertyNames);
424
    return DBusMenuItemList();
425
}
426
427
uint Window::GetLayout(int parentId, int recursionDepth, const QStringList &propertyNames, DBusMenuLayoutItem &dbusItem)
428
{
429
    Q_UNUSED(recursionDepth); // TODO
430
    Q_UNUSED(propertyNames);
431
432
    int subscription;
433
    int sectionId;
434
    int index;
435
436
    Utils::intToTreeStructure(parentId, subscription, sectionId, index);
437
438
    if (!m_currentMenu) {
439
        return 1;
440
    }
441
442
    if (!m_currentMenu->hasSubscription(subscription)) {
443
        // let's serve multiple similar requests in one go once we've processed them
444
        m_pendingGetLayouts.insertMulti(subscription, message());
445
        setDelayedReply(true);
446
447
        m_currentMenu->start(subscription);
448
        return 1;
449
    }
450
451
    bool ok;
452
    const GMenuItem section = m_currentMenu->getSection(subscription, sectionId, &ok);
453
454
    if (!ok) {
455
        qCDebug(DBUSMENUPROXY) << "There is no section on" << subscription << "at" << sectionId << "with" << parentId;
456
        return 1;
457
    }
458
459
    // If a particular entry is requested, see what it is and resolve as neccessary
460
    // for example the "File" entry on root is 0,0,1 but is a menu reference to e.g. 1,0,0
461
    // so resolve that and return the correct menu
462
    if (index > 0) {
463
        // non-zero index indicates item within a menu but the index in the list still starts at zero
464
        if (section.items.count() < index) {
465
            qCDebug(DBUSMENUPROXY) << "Requested index" << index << "on" << subscription << "at" << sectionId << "with" << parentId << "is out of bounds";
466
            return 0;
467
        }
468
469
        const auto &requestedItem = section.items.at(index - 1);
470
471
        auto it = requestedItem.constFind(QStringLiteral(":submenu"));
472
        if (it != requestedItem.constEnd()) {
473
            const GMenuSection gmenuSection = qdbus_cast<GMenuSection>(it->value<QDBusArgument>());
474
            return GetLayout(Utils::treeStructureToInt(gmenuSection.subscription, gmenuSection.menu, 0), recursionDepth, propertyNames, dbusItem);
475
        } else {
476
            // TODO
477
            return 0;
478
        }
479
    }
480
481
    dbusItem.id = parentId; // TODO
482
    dbusItem.properties = {
483
        {QStringLiteral("children-display"), QStringLiteral("submenu")}
484
    };
485
486
    int count = 0;
487
488
    const auto itemsToBeAdded = section.items;
489
    for (const auto &item : itemsToBeAdded) {
490
491
        DBusMenuLayoutItem child{
492
            Utils::treeStructureToInt(section.id, sectionId, ++count),
493
            gMenuToDBusMenuProperties(item),
494
            {} // children
495
        };
496
        dbusItem.children.append(child);
497
498
        // Now resolve section aliases
499
        auto it = item.constFind(QStringLiteral(":section"));
500
        if (it != item.constEnd()) {
501
502
            // references another place, add it instead
503
            GMenuSection gmenuSection = qdbus_cast<GMenuSection>(it->value<QDBusArgument>());
504
505
            // remember where the item came from and give it an appropriate ID
506
            // so updates signalled by the app will map to the right place
507
            int originalSubscription = gmenuSection.subscription;
508
            int originalMenu = gmenuSection.menu;
509
510
            // TODO start subscription if we don't have it
511
            auto items = m_currentMenu->getSection(gmenuSection.subscription, gmenuSection.menu).items;
512
513
            // Check whether it's an alias to an alias
514
            // FIXME make generic/recursive
515
            if (items.count() == 1) {
516
                const auto &aliasedItem = items.constFirst();
517
                auto findIt = aliasedItem.constFind(QStringLiteral(":section"));
518
                if (findIt != aliasedItem.constEnd()) {
519
                    GMenuSection gmenuSection2 = qdbus_cast<GMenuSection>(findIt->value<QDBusArgument>());
520
                    items = m_currentMenu->getSection(gmenuSection2.subscription, gmenuSection2.menu).items;
521
522
                    originalSubscription = gmenuSection2.subscription;
523
                    originalMenu = gmenuSection2.menu;
524
                }
525
            }
526
527
            int aliasedCount = 0;
528
            for (const auto &aliasedItem : qAsConst(items)) {
529
                DBusMenuLayoutItem aliasedChild{
530
                    Utils::treeStructureToInt(originalSubscription, originalMenu, ++aliasedCount),
531
                    gMenuToDBusMenuProperties(aliasedItem),
532
                    {} // children
533
                };
534
                dbusItem.children.append(aliasedChild);
535
            }
536
        }
537
    }
538
539
    // revision, unused in libdbusmenuqt
540
    return 1;
541
}
542
543
QDBusVariant Window::GetProperty(int id, const QString &property)
544
{
545
    Q_UNUSED(id);
546
    Q_UNUSED(property);
547
    QDBusVariant value;
548
    return value;
549
}
550
551
QString Window::status() const
552
{
553
    return QStringLiteral("normal");
554
}
555
556
uint Window::version() const
557
{
558
    return 4;
559
}
560
561
QVariantMap Window::gMenuToDBusMenuProperties(const QVariantMap &source) const
562
{
563
    QVariantMap result;
564
565
    result.insert(QStringLiteral("label"), source.value(QStringLiteral("label")).toString());
566
567
    if (source.contains(QStringLiteral(":section"))) {
568
        result.insert(QStringLiteral("type"), QStringLiteral("separator"));
569
    }
570
571
    const bool isMenu = source.contains(QStringLiteral(":submenu"));
572
    if (isMenu) {
573
        result.insert(QStringLiteral("children-display"), QStringLiteral("submenu"));
574
    }
575
576
    QString accel = source.value(QStringLiteral("accel")).toString();
577
    if (!accel.isEmpty()) {
578
        QStringList shortcut;
579
580
        // TODO use regexp or something
581
        if (accel.contains(QLatin1String("<Primary>")) || accel.contains(QLatin1String("<Control>"))) {
582
            shortcut.append(QStringLiteral("Control"));
583
            accel.remove(QLatin1String("<Primary>"));
584
            accel.remove(QLatin1String("<Control>"));
585
        }
586
587
        if (accel.contains(QLatin1String("<Shift>"))) {
588
            shortcut.append(QStringLiteral("Shift"));
589
            accel.remove(QLatin1String("<Shift>"));
590
        }
591
592
        if (accel.contains(QLatin1String("<Alt>"))) {
593
            shortcut.append(QStringLiteral("Alt"));
594
            accel.remove(QLatin1String("<Alt>"));
595
        }
596
597
        if (accel.contains(QLatin1String("<Super>"))) {
598
            shortcut.append(QStringLiteral("Super"));
599
            accel.remove(QLatin1String("<Super>"));
600
        }
601
602
        if (!accel.isEmpty()) {
603
            // TODO replace "+" by "plus" and "-" by "minus"
604
            shortcut.append(accel);
605
606
            // TODO does gmenu support multiple?
607
            DBusMenuShortcut dbusShortcut;
608
            dbusShortcut.append(shortcut); // don't let it unwrap the list we append
609
610
            result.insert(QStringLiteral("shortcut"), QVariant::fromValue(dbusShortcut));
611
        }
612
    }
613
614
    bool enabled = true;
615
616
    const QString actionName = Utils::itemActionName(source);
617
618
    GMenuAction action;
619
    // if no action is specified this is fine but if there is an action we don't have
620
    // disable the menu entry
621
    bool actionOk = true;
622
    if (!actionName.isEmpty()) {
623
        actionOk = getAction(actionName, action);
624
        enabled = actionOk && action.enabled;
625
    }
626
627
    // we used to only send this if not enabled but then dbusmenuimporter does not
628
    // update the enabled state when it changes from disabled to enabled
629
    result.insert(QStringLiteral("enabled"), enabled);
630
631
    bool visible = true;
632
    const QString hiddenWhen = source.value(QStringLiteral("hidden-when")).toString();
633
    if (hiddenWhen == QLatin1String("action-disabled") && (!actionOk || !enabled)) {
634
        visible = false;
635
    } else if (hiddenWhen == QLatin1String("action-missing") && !actionOk) {
636
        visible = false;
637
    // While we have Global Menu we don't have macOS menu (where Quit, Help, etc is separate)
638
    } else if (hiddenWhen == QLatin1String("macos-menubar")) {
639
        visible = true;
640
    }
641
642
    result.insert(QStringLiteral("visible"), visible);
643
644
    QString icon = source.value(QStringLiteral("icon")).toString();
645
    if (icon.isEmpty()) {
646
        icon = source.value(QStringLiteral("verb-icon")).toString();
647
    }
648
649
    icon = Icons::actionIcon(actionName);
650
    if (!icon.isEmpty()) {
651
        result.insert(QStringLiteral("icon-name"), icon);
652
    }
653
654
    if (actionOk) {
655
        const auto args = action.state;
656
        if (args.count() == 1) {
657
            const auto &firstArg = args.first();
658
            // assume this is a checkbox
659
            if (!isMenu) {
660
                if (firstArg.type() == QVariant::Bool) {
661
                    result.insert(QStringLiteral("toggle-type"), QStringLiteral("checkbox"));
662
                    result.insert(QStringLiteral("toggle-state"), firstArg.toBool() ? 1 : 0);
663
                } else if (firstArg.type() == QVariant::String) {
664
                    result.insert(QStringLiteral("toggle-type"), QStringLiteral("radio"));
665
                    const QString checkedAction = firstArg.toString();
666
                    if (!checkedAction.isEmpty() && actionName.endsWith(checkedAction)) {
667
                        result.insert(QStringLiteral("toggle-state"), 1);
668
                    } else {
669
                        result.insert(QStringLiteral("toggle-state"), 0);
670
                    }
671
                }
672
            }
673
        }
674
    }
675
676
    return result;
677
}
(-)a/gmenu-dbusmenu-proxy/window.h (+140 lines)
Line 0 Link Here
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 <QDBusContext>
24
#include <QString>
25
#include <QVector>
26
#include <QWindow> // for WId
27
28
#include <functional>
29
30
#include "gdbusmenutypes_p.h"
31
#include "../libdbusmenuqt/dbusmenutypes_p.h"
32
33
class QDBusVariant;
34
35
class Actions;
36
class Menu;
37
38
class Window : public QObject, protected QDBusContext
39
{
40
    Q_OBJECT
41
42
    // DBus
43
    Q_PROPERTY(QString Status READ status)
44
    Q_PROPERTY(uint Version READ version)
45
46
public:
47
    Window(const QString &serviceName);
48
    ~Window();
49
50
    void init();
51
52
    WId winId() const;
53
    void setWinId(WId winId);
54
55
    QString serviceName() const;
56
57
    QString applicationObjectPath() const;
58
    void setApplicationObjectPath(const QString &applicationObjectPath);
59
60
    QString unityObjectPath() const;
61
    void setUnityObjectPath(const QString &unityObjectPath);
62
63
    QString windowObjectPath() const;
64
    void setWindowObjectPath(const QString &windowObjectPath);
65
66
    QString applicationMenuObjectPath() const;
67
    void setApplicationMenuObjectPath(const QString &applicationMenuObjectPath);
68
69
    QString menuBarObjectPath() const;
70
    void setMenuBarObjectPath(const QString &menuBarObjectPath);
71
72
    QString currentMenuObjectPath() const;
73
74
    QString proxyObjectPath() const;
75
76
    // DBus
77
    bool AboutToShow(int id);
78
    void Event(int id, const QString &eventId, const QDBusVariant &data, uint timestamp);
79
    DBusMenuItemList GetGroupProperties(const QList<int> &ids, const QStringList &propertyNames);
80
    uint GetLayout(int parentId, int recursionDepth, const QStringList &propertyNames, DBusMenuLayoutItem &dbusItem);
81
    QDBusVariant GetProperty(int id, const QString &property);
82
83
    QString status() const;
84
    uint version() const;
85
86
signals:
87
    // don't want to pollute X stuff into Menu, let all of that be in MenuProxy
88
    void requestWriteWindowProperties();
89
    void requestRemoveWindowProperties();
90
91
    // DBus
92
    void ItemActivationRequested(int id, uint timestamp);
93
    void ItemsPropertiesUpdated(const DBusMenuItemList &updatedProps, const DBusMenuItemKeysList &removedProps);
94
    void LayoutUpdated(uint revision, int parent);
95
96
private:
97
    void initMenu();
98
99
    bool registerDBusObject();
100
    void updateWindowProperties();
101
102
    bool getAction(const QString &name, GMenuAction &action) const;
103
    void triggerAction(const QString &name, uint timestamp = 0);
104
    Actions *getActionsForAction(const QString &name, QString &lookupName) const;
105
106
    void menuChanged(const QVector<uint> &menuIds);
107
    void menuItemsChanged(const QVector<uint> &itemIds);
108
109
    void onActionsChanged(const QStringList &dirty, const QString &prefix);
110
    void onMenuSubscribed(uint id);
111
112
    QVariantMap gMenuToDBusMenuProperties(const QVariantMap &source) const;
113
114
    WId m_winId = 0;
115
    QString m_serviceName; // original GMenu service (the gtk app)
116
117
    QString m_applicationObjectPath;
118
    QString m_unityObjectPath;
119
    QString m_windowObjectPath;
120
    QString m_applicationMenuObjectPath;
121
    QString m_menuBarObjectPath;
122
123
    QString m_currentMenuObjectPath;
124
125
    QString m_proxyObjectPath; // our object path on this proxy app
126
127
    QHash<int, QDBusMessage> m_pendingGetLayouts;
128
129
    Menu *m_applicationMenu = nullptr;
130
    Menu *m_menuBar = nullptr;
131
132
    Menu *m_currentMenu = nullptr;
133
134
    Actions *m_applicationActions = nullptr;
135
    Actions *m_unityActions = nullptr;
136
    Actions *m_windowActions = nullptr;
137
138
    bool m_menuInited = false;
139
140
};

Return to bug 657236