QortalOS Brooklyn for Raspberry Pi 4
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.
 
 
 
 
 
 

328 lines
11 KiB

/*
SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <[email protected]>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "menu.h"
#include "debug.h"
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusPendingCallWatcher>
#include <QDBusPendingReply>
#include <QDebug>
#include <QVariantList>
#include <algorithm>
#include "utils.h"
static const QString s_orgGtkMenus = QStringLiteral("org.gtk.Menus");
Menu::Menu(const QString &serviceName, const QString &objectPath, QObject *parent)
: QObject(parent)
, m_serviceName(serviceName)
, m_objectPath(objectPath)
{
Q_ASSERT(!serviceName.isEmpty());
Q_ASSERT(!m_objectPath.isEmpty());
if (!QDBusConnection::sessionBus()
.connect(m_serviceName, m_objectPath, s_orgGtkMenus, QStringLiteral("Changed"), this, SLOT(onMenuChanged(GMenuChangeList)))) {
qCWarning(DBUSMENUPROXY) << "Failed to subscribe to menu changes for" << parent << "on" << serviceName << "at" << objectPath;
}
}
Menu::~Menu() = default;
void Menu::cleanup()
{
stop(m_subscriptions);
}
void Menu::start(uint id)
{
if (m_subscriptions.contains(id)) {
return;
}
// TODO watch service disappearing?
// dbus-send --print-reply --session --dest=:1.103 /org/libreoffice/window/104857641/menus/menubar org.gtk.Menus.Start array:uint32:0
QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName, m_objectPath, s_orgGtkMenus, QStringLiteral("Start"));
msg.setArguments({QVariant::fromValue(QList<uint>{id})});
QDBusPendingReply<GMenuItemList> reply = QDBusConnection::sessionBus().asyncCall(msg);
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id](QDBusPendingCallWatcher *watcher) {
QScopedPointer<QDBusPendingCallWatcher, QScopedPointerDeleteLater> watcherPtr(watcher);
QDBusPendingReply<GMenuItemList> reply = *watcherPtr;
if (reply.isError()) {
qCWarning(DBUSMENUPROXY) << "Failed to start subscription to" << id << "on" << m_serviceName << "at" << m_objectPath << reply.error();
Q_EMIT failedToSubscribe(id);
} else {
const bool hadMenu = !m_menus.isEmpty();
const auto menus = reply.value();
for (const auto &menu : menus) {
m_menus[menu.id].append(menus);
}
// LibreOffice on startup fails to give us some menus right away, we'll also subscribe in onMenuChanged() if necessary
if (menus.isEmpty()) {
qCWarning(DBUSMENUPROXY) << "Got an empty menu for" << id << "on" << m_serviceName << "at" << m_objectPath;
return;
}
// TODO are we subscribed to all it returns or just to the ones we requested?
m_subscriptions.append(id);
// do we have a menu now? let's tell everyone
if (!hadMenu && !m_menus.isEmpty()) {
Q_EMIT menuAppeared();
}
Q_EMIT subscribed(id);
}
});
}
void Menu::stop(const QList<uint> &ids)
{
QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName, m_objectPath, s_orgGtkMenus, QStringLiteral("End"));
msg.setArguments({
QVariant::fromValue(ids) // don't let it unwrap it, hence in a variant
});
QDBusPendingReply<void> reply = QDBusConnection::sessionBus().asyncCall(msg);
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, ids](QDBusPendingCallWatcher *watcher) {
QDBusPendingReply<void> reply = *watcher;
if (reply.isError()) {
qCWarning(DBUSMENUPROXY) << "Failed to stop subscription to" << ids << "on" << m_serviceName << "at" << m_objectPath << reply.error();
} else {
// remove all subscriptions that we unsubscribed from
// TODO is there a nicer algorithm for that?
// TODO remove all m_menus also?
m_subscriptions.erase(
std::remove_if(m_subscriptions.begin(), m_subscriptions.end(), std::bind(&QList<uint>::contains, m_subscriptions, std::placeholders::_1)),
m_subscriptions.end());
if (m_subscriptions.isEmpty()) {
Q_EMIT menuDisappeared();
}
}
watcher->deleteLater();
});
}
bool Menu::hasMenu() const
{
return !m_menus.isEmpty();
}
bool Menu::hasSubscription(uint subscription) const
{
return m_subscriptions.contains(subscription);
}
GMenuItem Menu::getSection(int id, bool *ok) const
{
int subscription;
int section;
int index;
Utils::intToTreeStructure(id, subscription, section, index);
return getSection(subscription, section, ok);
}
GMenuItem Menu::getSection(int subscription, int section, bool *ok) const
{
const auto menu = m_menus.value(subscription);
auto it = std::find_if(menu.begin(), menu.end(), [section](const GMenuItem &item) {
return item.section == section;
});
if (it == menu.end()) {
if (ok) {
*ok = false;
}
return GMenuItem();
}
if (ok) {
*ok = true;
}
return *it;
}
QVariantMap Menu::getItem(int id) const
{
int subscription;
int section;
int index;
Utils::intToTreeStructure(id, subscription, section, index);
return getItem(subscription, section, index);
}
QVariantMap Menu::getItem(int subscription, int sectionId, int index) const
{
bool ok;
const GMenuItem section = getSection(subscription, sectionId, &ok);
if (!ok) {
return QVariantMap();
}
const auto items = section.items;
if (items.count() < index) {
qCWarning(DBUSMENUPROXY) << "Cannot get action" << subscription << sectionId << index << "which is out of bounds";
return QVariantMap();
}
// 0 is the menu itself, items start at 1
return items.at(index - 1);
}
void Menu::onMenuChanged(const GMenuChangeList &changes)
{
const bool hadMenu = !m_menus.isEmpty();
QVector<uint> dirtyMenus;
QVector<uint> dirtyItems;
for (const auto &change : changes) {
auto updateSection = [&](GMenuItem &section) {
// Check if the amount of inserted items is identical to the items to be removed,
// just update the existing items and signal a change for that.
// LibreOffice tends to do that e.g. to update its Undo menu entry
if (change.itemsToRemoveCount == change.itemsToInsert.count()) {
for (int i = 0; i < change.itemsToInsert.count(); ++i) {
const auto &newItem = change.itemsToInsert.at(i);
section.items[change.changePosition + i] = newItem;
// 0 is the menu itself, items start at 1
dirtyItems.append(Utils::treeStructureToInt(change.subscription, change.menu, change.changePosition + i + 1));
}
} else {
for (int i = 0; i < change.itemsToRemoveCount; ++i) {
section.items.removeAt(change.changePosition); // TODO bounds check
}
for (int i = 0; i < change.itemsToInsert.count(); ++i) {
section.items.insert(change.changePosition + i, change.itemsToInsert.at(i));
}
dirtyMenus.append(Utils::treeStructureToInt(change.subscription, change.menu, 0));
}
};
// shouldn't happen, it says only Start() subscribes to changes
if (!m_subscriptions.contains(change.subscription)) {
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];
bool sectionFound = false;
// TODO findSectionRef
for (GMenuItem &section : menu) {
if (section.section != change.menu) {
continue;
}
qCInfo(DBUSMENUPROXY) << "Updating existing section" << change.menu << "in subscription" << change.subscription;
sectionFound = true;
updateSection(section);
break;
}
// Insert new section
if (!sectionFound) {
qCInfo(DBUSMENUPROXY) << "Creating new section" << change.menu << "in subscription" << change.subscription;
if (change.itemsToRemoveCount > 0) {
qCWarning(DBUSMENUPROXY) << "Menu change requested to remove items from a new (and as such empty) section";
}
GMenuItem newSection;
newSection.id = change.subscription;
newSection.section = change.menu;
updateSection(newSection);
menu.append(newSection);
}
}
// do we have a menu now? let's tell everyone
if (!hadMenu && !m_menus.isEmpty()) {
Q_EMIT menuAppeared();
} else if (hadMenu && m_menus.isEmpty()) {
Q_EMIT menuDisappeared();
}
if (!dirtyItems.isEmpty()) {
Q_EMIT itemsChanged(dirtyItems);
}
Q_EMIT menusChanged(dirtyMenus);
}
void Menu::actionsChanged(const QStringList &dirtyActions, const QString &prefix)
{
auto forEachMenuItem = [this](const std::function<bool(int subscription, int section, int index, const QVariantMap &item)> &cb) {
for (auto it = m_menus.constBegin(), end = m_menus.constEnd(); it != end; ++it) {
const int subscription = it.key();
for (const auto &menu : it.value()) {
const int section = menu.section;
int count = 0;
const auto items = menu.items;
for (const auto &item : items) {
++count; // 0 is a menu, entries start at 1
if (!cb(subscription, section, count, item)) {
goto loopExit; // hell yeah
break;
}
}
}
}
loopExit: // loop exit
return;
};
// now find in which menus these actions are and Q_EMIT a change accordingly
QVector<uint> dirtyItems;
for (const QString &action : dirtyActions) {
const QString prefixedAction = prefix + action;
forEachMenuItem([&prefixedAction, &dirtyItems](int subscription, int section, int index, const QVariantMap &item) {
const QString actionName = Utils::itemActionName(item);
if (actionName == prefixedAction) {
dirtyItems.append(Utils::treeStructureToInt(subscription, section, index));
return false; // break
}
return true; // continue
});
}
if (!dirtyItems.isEmpty()) {
Q_EMIT itemsChanged(dirtyItems);
}
}