You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1009 lines
36 KiB

/*
* Copyright (C) 2018 Kai Uwe Broulik <kde@privat.broulik.de>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
#include "menu.h"
#include "debug.h"
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusPendingCallWatcher>
#include <QDBusPendingReply>
#include <QDebug>
#include <QList>
#include <QMutableListIterator>
#include <QVariantList>
#include <algorithm>
#include "dbusmenuadaptor.h"
#include "../libdbusmenuqt/dbusmenushortcut_p.h"
static const QString s_orgGtkActions = QStringLiteral("org.gtk.Actions");
static const QString s_orgGtkMenus = QStringLiteral("org.gtk.Menus");
Menu::Menu(const QString &serviceName)
: QObject()
, m_serviceName(serviceName)
{
qCDebug(DBUSMENUPROXY) << "Created menu on" << serviceName;
Q_ASSERT(!serviceName.isEmpty());
GDBusMenuTypes_register();
DBusMenuTypes_register();
}
Menu::~Menu() = default;
void Menu::init()
{
qCDebug(DBUSMENUPROXY) << "Inited menu for" << m_winId << "on" << m_serviceName << "at app" << m_applicationObjectPath << "win" << m_windowObjectPath << "unity" << m_unityObjectPath << "menu" << m_menuObjectPath;
if (!QDBusConnection::sessionBus().connect(m_serviceName,
m_menuObjectPath,
s_orgGtkMenus,
QStringLiteral("Changed"),
this,
SLOT(onMenuChanged(GMenuChangeList)))) {
qCWarning(DBUSMENUPROXY) << "Failed to subscribe to menu changes on" << m_serviceName << "at" << m_menuObjectPath;
}
if (!m_applicationObjectPath.isEmpty() && !QDBusConnection::sessionBus().connect(m_serviceName,
m_applicationObjectPath,
s_orgGtkActions,
QStringLiteral("Changed"),
this,
SLOT(onApplicationActionsChanged(QStringList,StringBoolMap,QVariantMap,GMenuActionMap)))) {
qCWarning(DBUSMENUPROXY) << "Failed to subscribe to application action changes on" << m_serviceName << "at" << m_applicationObjectPath;
}
if (!m_unityObjectPath.isEmpty() && !QDBusConnection::sessionBus().connect(m_serviceName,
m_unityObjectPath,
s_orgGtkActions,
QStringLiteral("Changed"),
this,
SLOT(onUnityActionsChanged(QStringList,StringBoolMap,QVariantMap,GMenuActionMap)))) {
qCWarning(DBUSMENUPROXY) << "Failed to subscribe to Unity action changes on" << m_serviceName << "at" << m_applicationObjectPath;
}
if (!m_windowObjectPath.isEmpty() && !QDBusConnection::sessionBus().connect(m_serviceName,
m_windowObjectPath,
s_orgGtkActions,
QStringLiteral("Changed"),
this,
SLOT(onWindowActionsChanged(QStringList,StringBoolMap,QVariantMap,GMenuActionMap)))) {
qCWarning(DBUSMENUPROXY) << "Failed to subscribe to window action changes on" << m_serviceName << "at" << m_windowObjectPath;
}
// TODO share application actions between menus of the same app?
if (!m_applicationObjectPath.isEmpty()) {
getActions(m_applicationObjectPath, [this](const GMenuActionMap &actions, bool ok) {
if (ok) {
// TODO just do all of this in getActions instead of copying it thrice
if (m_menuInited) {
onApplicationActionsChanged({}, {}, {}, actions);
} else {
m_applicationActions = actions;
initMenu();
}
}
});
}
if (!m_unityObjectPath.isEmpty()) {
getActions(m_unityObjectPath, [this](const GMenuActionMap &actions, bool ok) {
if (ok) {
if (m_menuInited) {
onUnityActionsChanged({}, {}, {}, actions);
} else {
m_unityActions = actions;
initMenu();
}
}
});
}
if (!m_windowObjectPath.isEmpty()) {
getActions(m_windowObjectPath, [this](const GMenuActionMap &actions, bool ok) {
if (ok) {
if (m_menuInited) {
onWindowActionsChanged({}, {}, {}, actions);
} else {
m_windowActions = actions;
initMenu();
}
}
});
}
}
void Menu::cleanup()
{
stop(m_subscriptions);
emit requestRemoveWindowProperties();
}
WId Menu::winId() const
{
return m_winId;
}
void Menu::setWinId(WId winId)
{
m_winId = winId;
}
QString Menu::serviceName() const
{
return m_serviceName;
}
QString Menu::applicationObjectPath() const
{
return m_applicationObjectPath;
}
void Menu::setApplicationObjectPath(const QString &applicationObjectPath)
{
m_applicationObjectPath = applicationObjectPath;
}
QString Menu::unityObjectPath() const
{
return m_unityObjectPath;
}
void Menu::setUnityObjectPath(const QString &unityObjectPath)
{
m_unityObjectPath = unityObjectPath;
}
QString Menu::windowObjectPath() const
{
return m_windowObjectPath;
}
void Menu::setWindowObjectPath(const QString &windowObjectPath)
{
m_windowObjectPath = windowObjectPath;
}
QString Menu::menuObjectPath() const
{
return m_menuObjectPath;
}
void Menu::setMenuObjectPath(const QString &menuObjectPath)
{
m_menuObjectPath = menuObjectPath;
}
QString Menu::proxyObjectPath() const
{
return m_proxyObjectPath;
}
void Menu::initMenu()
{
if (m_menuInited) {
return;
}
if (!registerDBusObject()) {
return;
}
emit requestWriteWindowProperties();
m_menuInited = true;
}
void Menu::start(uint id)
{
if (m_subscriptions.contains(id)) {
return;
}
// TODO watch service disappearing?
// dbus-send --print-reply --session --dest=:1.103 /org/libreoffice/window/104857641/menus/menubar org.gtk.Menus.Start array:uint32:0
QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName,
m_menuObjectPath,
s_orgGtkMenus,
QStringLiteral("Start"));
msg.setArguments({
QVariant::fromValue(QList<uint>{id})
});
QDBusPendingReply<GMenuItemList> reply = QDBusConnection::sessionBus().asyncCall(msg);
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id](QDBusPendingCallWatcher *watcher) {
QDBusPendingReply<GMenuItemList> reply = *watcher;
if (reply.isError()) {
qCWarning(DBUSMENUPROXY) << "Failed to start subscription to" << id << "on" << m_serviceName << "at" << m_menuObjectPath << reply.error();
} else {
const auto menus = reply.value();
for (auto menu : menus) {
m_menus[menu.id].append(menus);
}
// LibreOffice on startup fails to give us some menus right away, we'll also subscribe in onMenuChanged() if neccessary
if (menus.isEmpty()) {
qCWarning(DBUSMENUPROXY) << "Got an empty menu for" << id << "on" << m_serviceName << "at" << m_menuObjectPath;
return;
}
// TODO are we subscribed to all it returns or just to the ones we requested?
m_subscriptions.append(id);
}
// When it was a delayed GetLayout request, send the reply now
const auto pendingReplies = m_pendingGetLayouts.values(id);
if (!pendingReplies.isEmpty()) {
for (const auto &pendingReply : pendingReplies) {
if (pendingReply.type() != QDBusMessage::InvalidMessage) {
auto reply = pendingReply.createReply();
DBusMenuLayoutItem item;
uint revision = GetLayout(treeStructureToInt(id, 0, 0), 0, {}, item);
reply << revision << QVariant::fromValue(item);
qDebug() << "Send get layout reply for" << id;
QDBusConnection::sessionBus().send(reply);
}
}
m_pendingGetLayouts.remove(id);
} else {
emit LayoutUpdated(2 /*revision*/, id);
}
watcher->deleteLater();
});
}
void Menu::stop(const QList<uint> &ids)
{
QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName,
m_menuObjectPath,
s_orgGtkMenus,
QStringLiteral("End"));
msg.setArguments({
QVariant::fromValue(ids) // don't let it unwrap it, hence in a variant
});
QDBusPendingReply<void> reply = QDBusConnection::sessionBus().asyncCall(msg);
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, ids](QDBusPendingCallWatcher *watcher) {
QDBusPendingReply<void> reply = *watcher;
if (reply.isError()) {
qCWarning(DBUSMENUPROXY) << "Failed to stop subscription to" << ids << "on" << m_serviceName << "at" << m_menuObjectPath << reply.error();
} else {
// remove all subscriptions that we unsubscribed from
// TODO is there a nicer algorithm for that?
m_subscriptions.erase(std::remove_if(m_subscriptions.begin(), m_subscriptions.end(),
std::bind(&QList<uint>::contains, m_subscriptions, std::placeholders::_1)),
m_subscriptions.end());
}
});
}
void Menu::onMenuChanged(const GMenuChangeList &changes)
{
QSet<uint> dirtyMenus;
DBusMenuItemList dirtyItems;
for (const auto &change : changes) {
// shouldn't happen, it says only Start() subscribes to changes
if (!m_subscriptions.contains(change.subscription)) {
qCDebug(DBUSMENUPROXY) << "Got menu change for menu" << change.subscription << "that we are not subscribed to, subscribing now";
// LibreOffice doesn't give us a menu right away but takes a while and then signals us a change
start(change.subscription);
continue;
}
auto &menu = m_menus[change.subscription];
// TODO findSectionRef
for (GMenuItem &section : menu) {
if (section.section != change.menu) {
continue;
}
qDebug() << "change at" << change.changePosition << "remove" << change.itemsToRemoveCount << "INSERT" << change.itemsToInsert.count();
// Check if the amount of inserted items is identical to the items to be removed,
// just update the existing items and signal a change for that.
// LibreOffice tends to do that e.g. to update its Undo menu entry
if (change.itemsToRemoveCount == change.itemsToInsert.count()) {
qDebug() << "is the same, let's just update";
for (int i = 0; i < change.itemsToInsert.count(); ++i) {
const auto &newItem = change.itemsToInsert.at(i);
section.items[change.changePosition + i] = newItem;
DBusMenuItem dBusItem{
// 0 is menu, items start at 1
treeStructureToInt(change.subscription, change.menu, change.changePosition + i + 1),
gMenuToDBusMenuProperties(newItem)
};
dirtyItems.append(dBusItem);
}
} else {
for (int i = 0; i < change.itemsToRemoveCount; ++i) {
section.items.removeAt(change.changePosition); // TODO bounds check
}
for (int i = 0; i < change.itemsToInsert.count(); ++i) {
section.items.insert(change.changePosition + i, change.itemsToInsert.at(i));
}
dirtyMenus.insert(treeStructureToInt(change.subscription, change.menu, 0));
}
break;
}
}
if (!dirtyItems.isEmpty()) {
qDebug() << "Emit item properties changed for" << dirtyItems.count() << "after menu changed";
emit ItemsPropertiesUpdated(dirtyItems, {});
}
for (uint menu : dirtyMenus) {
emit LayoutUpdated(3 /*revision*/, menu);
}
}
void Menu::onApplicationActionsChanged(const QStringList &removed, const StringBoolMap &enabledChanges, const QVariantMap &stateChanges, const GMenuActionMap &added)
{
if (!m_menuInited) {
return;
}
actionsChanged(removed, enabledChanges, stateChanges, added, m_applicationActions, QStringLiteral("app."));
}
void Menu::onUnityActionsChanged(const QStringList &removed, const StringBoolMap &enabledChanges, const QVariantMap &stateChanges, const GMenuActionMap &added)
{
if (!m_menuInited) {
return;
}
actionsChanged(removed, enabledChanges, stateChanges, added, m_unityActions, QStringLiteral("unity."));
}
void Menu::onWindowActionsChanged(const QStringList &removed, const StringBoolMap &enabledChanges, const QVariantMap &stateChanges, const GMenuActionMap &added)
{
if (!m_menuInited) {
return;
}
actionsChanged(removed, enabledChanges, stateChanges, added, m_windowActions, QStringLiteral("win."));
}
void Menu::getActions(const QString &path, const std::function<void(GMenuActionMap,bool)> &cb)
{
QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName,
path,
s_orgGtkActions,
QStringLiteral("DescribeAll"));
QDBusPendingReply<GMenuActionMap> reply = QDBusConnection::sessionBus().asyncCall(msg);
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, path, cb](QDBusPendingCallWatcher *watcher) {
QDBusPendingReply<GMenuActionMap> reply = *watcher;
if (reply.isError()) {
qCWarning(DBUSMENUPROXY) << "Failed to get actions from" << m_serviceName << "at" << path << reply.error();
cb({}, false);
} else {
cb(reply.value(), true);
}
});
}
bool Menu::getAction(const QString &name, GMenuAction &action) const
{
QString lookupName;
const GMenuActionMap *actionMap = nullptr;
if (name.startsWith(QLatin1String("app."))) {
lookupName = name.mid(4);
actionMap = &m_applicationActions;
} else if (name.startsWith(QLatin1String("unity."))) {
lookupName = name.mid(6);
actionMap = &m_unityActions;
} else if (name.startsWith(QLatin1String("win."))) {
lookupName = name.mid(4);
actionMap = &m_windowActions;
}
if (!actionMap) {
return false;
}
auto it = actionMap->constFind(lookupName);
if (it == actionMap->constEnd()) {
return false;
}
action = *it;
return true;
}
void Menu::triggerAction(const QString &name, uint timestamp)
{
QString lookupName;
QString path;
// TODO avoid code duplication with getAction
if (name.startsWith(QLatin1String("app."))) {
lookupName = name.mid(4);
path = m_applicationObjectPath;
} else if (name.startsWith(QLatin1String("unity."))) {
lookupName = name.mid(6);
path = m_unityObjectPath;
} else if (name.startsWith(QLatin1String("win."))) {
lookupName = name.mid(4);
path = m_windowObjectPath;
}
if (path.isEmpty()) {
return;
}
GMenuAction action;
if (!getAction(name, action)) {
return;
}
QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName,
path,
s_orgGtkActions,
QStringLiteral("Activate"));
msg << lookupName;
// TODO use the arguments provided by "target" in the menu item
msg << QVariant::fromValue(QVariantList());
QVariantMap platformData;
if (timestamp) {
// From documentation:
// If the startup notification id is not available, this can be just "_TIMEtime", where
// time is the time stamp from the event triggering the call.
// see also gtkwindow.c extract_time_from_startup_id and startup_id_is_fake
platformData.insert(QStringLiteral("desktop-startup-id"), QStringLiteral("_TIME") + QString::number(timestamp));
}
msg << platformData;
QDBusPendingReply<void> reply = QDBusConnection::sessionBus().asyncCall(msg);
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, path, name](QDBusPendingCallWatcher *watcher) {
QDBusPendingReply<void> reply = *watcher;
if (reply.isError()) {
qCWarning(DBUSMENUPROXY) << "Failed to invoke action" << name << "on" << m_serviceName << "at" << path << reply.error();
}
});
}
void Menu::actionsChanged(const QStringList &removed, const StringBoolMap &enabledChanges, const QVariantMap &stateChanges, const GMenuActionMap &added, GMenuActionMap &actions, const QString &prefix)
{
// Collect the actions that we removed, altered, or added, so we can eventually signal changes for all menus that contain one of those actions
QSet<QString> dirtyActions;
// TODO I bet for most of the loops below we could use a nice short std algorithm
for (const QString &removedAction : removed) {
if (actions.remove(removedAction)) {
dirtyActions.insert(removedAction);
}
}
for (auto it = enabledChanges.constBegin(), end = enabledChanges.constEnd(); it != end; ++it) {
const QString &actionName = it.key();
const bool enabled = it.value();
auto actionIt = actions.find(actionName);
if (actionIt == actions.end()) {
qCInfo(DBUSMENUPROXY) << "Got enabled changed for action" << actionName << "which we don't know";
continue;
}
GMenuAction &action = *actionIt;
if (action.enabled != enabled) {
action.enabled = enabled;
dirtyActions.insert(actionName);
} else {
qCInfo(DBUSMENUPROXY) << "Got enabled change for action" << actionName << "which didn't change it";
}
}
for (auto it = stateChanges.constBegin(), end = stateChanges.constEnd(); it != end; ++it) {
const QString &actionName = it.key();
const QVariant &state = it.value();
auto actionIt = actions.find(actionName);
if (actionIt == actions.end()) {
qCInfo(DBUSMENUPROXY) << "Got state changed for action" << actionName << "which we don't know";
continue;
}
GMenuAction &action = *actionIt;
if (action.state.isEmpty()) {
qCDebug(DBUSMENUPROXY) << "Got new state for action" << actionName << "that didn't have any state before";
action.state.append(state);
dirtyActions.insert(actionName);
} else {
// Action state is a list but the state change only sends us a single variant, so just overwrite the first one
QVariant &firstState = action.state.first();
if (firstState != state) {
firstState = state;
dirtyActions.insert(actionName);
} else {
qCInfo(DBUSMENUPROXY) << "Got state change for action" << actionName << "which didn't change it";
}
}
}
// unite() will result in keys being present multiple times, do it manually and overwrite existing ones
for (auto it = added.constBegin(), end = added.constEnd(); it != end; ++it) {
const QString &actionName = it.key();
if (actions.contains(actionName)) { // TODO check isInfoEnabled
qCInfo(DBUSMENUPROXY) << "Got new action" << actionName << "that we already have, overwriting existing one";
}
actions.insert(actionName, it.value());
dirtyActions.insert(actionName);
}
auto forEachMenuItem = [this](const std::function<bool(int subscription, int section, int index, const QVariantMap &item)> &cb) {
for (auto it = m_menus.constBegin(), end = m_menus.constEnd(); it != end; ++it) {
const int subscription = it.key();
for (const auto &menu : it.value()) {
const int section = menu.section;
int count = 0;
const auto items = menu.items;
for (const auto &item : items) {
++count; // 0 is a menu, entries start at 1
if (!cb(subscription, section, count, item)) {
goto loopExit; // hell yeah
break;
}
}
}
}
loopExit: // loop exit
return;
};
qDebug() << "The following actions changed" << dirtyActions;
// now find in which menus these actions are and emit a change accordingly
DBusMenuItemList dirtyItems;
for (const QString &action : dirtyActions) {
const QString prefixedAction = prefix + action;
forEachMenuItem([this, &prefixedAction, &dirtyItems](int subscription, int section, int index, const QVariantMap &item) {
const QString actionName = actionNameOfItem(item);
if (actionName == prefixedAction) {
DBusMenuItem dBusItem{
treeStructureToInt(subscription, section, index),
gMenuToDBusMenuProperties(item)
};
dirtyItems.append(dBusItem);
return false; // break
}
return true; // continue
});
}
if (!dirtyItems.isEmpty()) {
emit ItemsPropertiesUpdated(dirtyItems, {});
}
}
bool Menu::registerDBusObject()
{
Q_ASSERT(m_proxyObjectPath.isEmpty());
static int menus = 0;
++menus;
const QString objectPath = QStringLiteral("/MenuBar/%1").arg(QString::number(menus));
qCDebug(DBUSMENUPROXY) << "Registering DBus object path" << objectPath;
if (!QDBusConnection::sessionBus().registerObject(objectPath, this)) {
qCWarning(DBUSMENUPROXY) << "Failed to register object";
return false;
}
new DbusmenuAdaptor(this); // do this before registering the object?
m_proxyObjectPath = objectPath;
return true;
}
// DBus
bool Menu::AboutToShow(int id)
{
// We always request the first time GetLayout is called and keep up-to-date internally
// No need to have us prepare anything here
Q_UNUSED(id);
return false;
}
void Menu::Event(int id, const QString &eventId, const QDBusVariant &data, uint timestamp)
{
Q_UNUSED(data);
// GMenu dbus doesn't have any "opened" or "closed" signals, we'll only handle "clicked"
if (eventId == QLatin1String("clicked")) {
int subscription;
int sectionId;
int index;
intToTreeStructure(id, subscription, sectionId, index);
if (index < 1) { // cannot "click" a menu
return;
}
// TODO check bounds
const auto items = findSection(m_menus.value(subscription), sectionId).items;
if (items.count() < index) {
qCWarning(DBUSMENUPROXY) << "Cannot trigger action" << id << subscription << sectionId << index << "as it is out of bounds";
return;
}
const QString action = items.at(index - 1).value(QStringLiteral("action")).toString();
if (!action.isEmpty()) {
triggerAction(action, timestamp);
}
}
}
DBusMenuItemList Menu::GetGroupProperties(const QList<int> &ids, const QStringList &propertyNames)
{
Q_UNUSED(ids);
Q_UNUSED(propertyNames);
return DBusMenuItemList();
}
uint Menu::GetLayout(int parentId, int recursionDepth, const QStringList &propertyNames, DBusMenuLayoutItem &dbusItem)
{
Q_UNUSED(recursionDepth); // TODO
Q_UNUSED(propertyNames);
int subscription;
int sectionId;
int index;
intToTreeStructure(parentId, subscription, sectionId, index);
if (!m_subscriptions.contains(subscription)) {
// let's serve multiple similar requests in one go once we've processed them
m_pendingGetLayouts.insertMulti(subscription, message());
setDelayedReply(true);
start(subscription);
return 1;
}
const auto sections = m_menus.value(subscription);
if (sections.isEmpty()) {
qCDebug(DBUSMENUPROXY) << "There are no sections for requested subscription" << subscription << "with" << parentId;
return 1;
}
// which sections to add to the menu
const GMenuItem &section = findSection(sections, sectionId);
// If a particular entry is requested, see what it is and resolve as neccessary
// for example the "File" entry on root is 0,0,1 but is a menu reference to e.g. 1,0,0
// so resolve that and return the correct menu
if (index > 0) {
// non-zero index indicates item within a menu but the index in the list still starts at zero
if (section.items.count() < index) {
qCDebug(DBUSMENUPROXY) << "Requested index" << index << "on" << subscription << "at" << sectionId << "with" << parentId << "is out of bounds";
return 0;
}
const auto &requestedItem = section.items.at(index - 1);
auto it = requestedItem.constFind(QStringLiteral(":submenu"));
if (it != requestedItem.constEnd()) {
const GMenuSection gmenuSection = qdbus_cast<GMenuSection>(it->value<QDBusArgument>());
return GetLayout(treeStructureToInt(gmenuSection.subscription, gmenuSection.menu, 0), recursionDepth, propertyNames, dbusItem);
} else {
// TODO
return 0;
}
}
dbusItem.id = parentId; // TODO
// TODO use gMenuToDBusMenuProperties?
dbusItem.properties = {
{QStringLiteral("children-display"), QStringLiteral("submenu")}
};
int count = 0;
const auto itemsToBeAdded = section.items;
for (const auto &item : itemsToBeAdded) {
DBusMenuLayoutItem child{
treeStructureToInt(section.id, sectionId, ++count),
gMenuToDBusMenuProperties(item),
{} // children
};
dbusItem.children.append(child);
// Now resolve section aliases
auto it = item.constFind(QStringLiteral(":section"));
if (it != item.constEnd()) {
// references another place, add it instead
GMenuSection gmenuSection = qdbus_cast<GMenuSection>(it->value<QDBusArgument>());
// remember where the item came from and give it an appropriate ID
// so updates signalled by the app will map to the right place
int originalSubscription = gmenuSection.subscription;
int originalMenu = gmenuSection.menu;
// TODO start subscription if we don't have it
auto items = findSection(m_menus.value(gmenuSection.subscription), gmenuSection.menu).items;
// Check whether it's an alias to an alias
// FIXME make generic/recursive
if (items.count() == 1) {
const auto &aliasedItem = items.constFirst();
auto findIt = aliasedItem.constFind(QStringLiteral(":section"));
if (findIt != aliasedItem.constEnd()) {
GMenuSection gmenuSection2 = qdbus_cast<GMenuSection>(findIt->value<QDBusArgument>());
items = findSection(m_menus.value(gmenuSection2.subscription), gmenuSection2.menu).items;
originalSubscription = gmenuSection2.subscription;
originalMenu = gmenuSection2.menu;
}
}
int aliasedCount = 0;
for (const auto &aliasedItem : qAsConst(items)) {
DBusMenuLayoutItem aliasedChild{
treeStructureToInt(originalSubscription, originalMenu, ++aliasedCount),
gMenuToDBusMenuProperties(aliasedItem),
{} // children
};
dbusItem.children.append(aliasedChild);
}
}
}
// revision, unused in libdbusmenuqt
return 1;
}
QDBusVariant Menu::GetProperty(int id, const QString &property)
{
Q_UNUSED(id);
Q_UNUSED(property);
QDBusVariant value;
return value;
}
QString Menu::status() const
{
return QStringLiteral("normal");
}
uint Menu::version() const
{
return 4;
}
int Menu::treeStructureToInt(int subscription, int section, int index)
{
return subscription * 1000000 + section * 1000 + index;
}
void Menu::intToTreeStructure(int source, int &subscription, int &section, int &index)
{
// TODO some better math :) or bit shifting or something
index = source % 1000;
section = (source / 1000) % 1000;
subscription = source / 1000000;
}
GMenuItem Menu::findSection(const QList<GMenuItem> &list, int section)
{
// TODO algorithm?
for (const GMenuItem &item : list) {
if (item.section == section) {
return item;
}
}
return GMenuItem();
}
QString Menu::actionNameOfItem(const QVariantMap &item)
{
QString actionName = item.value(QStringLiteral("action")).toString();
if (actionName.isEmpty()) {
actionName = item.value(QStringLiteral("submenu-action")).toString();
}
return actionName;
}
QVariantMap Menu::gMenuToDBusMenuProperties(const QVariantMap &source) const
{
QVariantMap result;
result.insert(QStringLiteral("label"), source.value(QStringLiteral("label")).toString());
if (source.contains(QStringLiteral(":section"))) {
result.insert(QStringLiteral("type"), QStringLiteral("separator"));
}
const bool isMenu = source.contains(QStringLiteral(":submenu"));
if (isMenu) {
result.insert(QStringLiteral("children-display"), QStringLiteral("submenu"));
}
QString accel = source.value(QStringLiteral("accel")).toString();
if (!accel.isEmpty()) {
QStringList shortcut;
// TODO use regexp or something
if (accel.contains(QLatin1String("<Primary>")) || accel.contains(QLatin1String("<Control>"))) {
shortcut.append(QStringLiteral("Control"));
accel.remove(QLatin1String("<Primary>"));
accel.remove(QLatin1String("<Control>"));
}
if (accel.contains(QLatin1String("<Shift>"))) {
shortcut.append(QStringLiteral("Shift"));
accel.remove(QLatin1String("<Shift>"));
}
if (accel.contains(QLatin1String("<Alt>"))) {
shortcut.append(QStringLiteral("Alt"));
accel.remove(QLatin1String("<Alt>"));
}
if (accel.contains(QLatin1String("<Super>"))) {
shortcut.append(QStringLiteral("Super"));
accel.remove(QLatin1String("<Super>"));
}
if (!accel.isEmpty()) {
// TODO replace "+" by "plus" and "-" by "minus"
shortcut.append(accel);
// TODO does gmenu support multiple?
DBusMenuShortcut dbusShortcut;
dbusShortcut.append(shortcut); // don't let it unwrap the list we append
result.insert(QStringLiteral("shortcut"), QVariant::fromValue(dbusShortcut));
}
}
bool enabled = true;
const QString actionName = actionNameOfItem(source);
GMenuAction action;
// if no action is specified this is fine but if there is an action we don't have
// disable the menu entry
bool actionOk = true;
if (!actionName.isEmpty()) {
actionOk = getAction(actionName, action);
enabled = actionOk && action.enabled;
}
// we used to only send this if not enabled but then dbusmenuimporter does not
// update the enabled state when it changes from disabled to enabled
result.insert(QStringLiteral("enabled"), enabled);
bool visible = true;
const QString hiddenWhen = source.value(QStringLiteral("hidden-when")).toString();
if (hiddenWhen == QLatin1String("action-disabled") && (!actionOk || !enabled)) {
visible = false;
} else if (hiddenWhen == QLatin1String("action-missing") && !actionOk) {
visible = false;
// While we have Global Menu we don't have macOS menu (where Quit, Help, etc is separate)
} else if (hiddenWhen == QLatin1String("macos-menubar")) {
visible = true;
}
result.insert(QStringLiteral("visible"), visible);
QString icon = source.value(QStringLiteral("icon")).toString();
if (icon.isEmpty()) {
icon = source.value(QStringLiteral("verb-icon")).toString();
}
if (icon.isEmpty()) {
QString lookupName = actionName.mid(4); // FIXME also FIXME unity.
// FIXME do properly
static QHash<QString, QString> s_icons {
{QStringLiteral("new-window"), QStringLiteral("window-new")},
{QStringLiteral("new-tab"), QStringLiteral("tab-new")},
{QStringLiteral("open"), QStringLiteral("document-open")},
{QStringLiteral("save"), QStringLiteral("document-save")},
{QStringLiteral("save-as"), QStringLiteral("document-save-as")},
{QStringLiteral("save-all"), QStringLiteral("document-save-all")},
{QStringLiteral("print"), QStringLiteral("document-print")},
{QStringLiteral("close"), QStringLiteral("document-close")},
{QStringLiteral("close-all"), QStringLiteral("document-close")},
{QStringLiteral("quit"), QStringLiteral("application-exit")},
{QStringLiteral("undo"), QStringLiteral("edit-undo")},
{QStringLiteral("redo"), QStringLiteral("edit-redo")},
{QStringLiteral("cut"), QStringLiteral("edit-cut")},
{QStringLiteral("copy"), QStringLiteral("edit-copy")},
{QStringLiteral("paste"), QStringLiteral("edit-paste")},
{QStringLiteral("preferences"), QStringLiteral("settings-configure")},
{QStringLiteral("fullscreen"), QStringLiteral("view-fullscreen")},
{QStringLiteral("find"), QStringLiteral("edit-find")},
{QStringLiteral("replace"), QStringLiteral("edit-find-replace")},
{QStringLiteral("select-all"), QStringLiteral("edit-select-all")},
{QStringLiteral("previous-document"), QStringLiteral("go-previous")},
{QStringLiteral("next-document"), QStringLiteral("go-next")},
{QStringLiteral("help"), QStringLiteral("help-contents")},
{QStringLiteral("about"), QStringLiteral("help-about")},
// TODO some more
};
icon = s_icons.value(lookupName);
}
if (!icon.isEmpty()) {
result.insert(QStringLiteral("icon-name"), icon);
}
if (actionOk) {
const auto args = action.state;
if (args.count() == 1) {
const auto &firstArg = args.first();
// assume this is a checkbox
if (firstArg.canConvert<bool>() && !isMenu) {
result.insert(QStringLiteral("toggle-type"), QStringLiteral("checkbox"));
if (firstArg.toBool()) {
result.insert(QStringLiteral("toggle-state"), 1);
}
}
}
}
return result;
}