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.

656 lines
20 KiB

/*
SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <kde@privat.broulik.de>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "window.h"
#include "debug.h"
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusPendingCallWatcher>
#include <QDBusPendingReply>
#include <QDebug>
#include <QList>
#include <QMutableListIterator>
#include <QVariantList>
#include <algorithm>
#include "actions.h"
#include "dbusmenuadaptor.h"
#include "icons.h"
#include "menu.h"
#include "utils.h"
#include "../libdbusmenuqt/dbusmenushortcut_p.h"
static const QString s_orgGtkActions = QStringLiteral("org.gtk.Actions");
static const QString s_orgGtkMenus = QStringLiteral("org.gtk.Menus");
static const QString s_applicationActionsPrefix = QStringLiteral("app.");
static const QString s_unityActionsPrefix = QStringLiteral("unity.");
static const QString s_windowActionsPrefix = QStringLiteral("win.");
Window::Window(const QString &serviceName)
: QObject()
, m_serviceName(serviceName)
{
qCDebug(DBUSMENUPROXY) << "Created menu on" << serviceName;
Q_ASSERT(!serviceName.isEmpty());
GDBusMenuTypes_register();
DBusMenuTypes_register();
}
Window::~Window() = default;
void Window::init()
{
qCDebug(DBUSMENUPROXY) << "Inited window with menu for" << m_winId << "on" << m_serviceName << "at app" << m_applicationObjectPath << "win"
<< m_windowObjectPath << "unity" << m_unityObjectPath;
if (!m_applicationMenuObjectPath.isEmpty()) {
m_applicationMenu = new Menu(m_serviceName, m_applicationMenuObjectPath, this);
connect(m_applicationMenu, &Menu::menuAppeared, this, &Window::updateWindowProperties);
connect(m_applicationMenu, &Menu::menuDisappeared, this, &Window::updateWindowProperties);
connect(m_applicationMenu, &Menu::subscribed, this, &Window::onMenuSubscribed);
// basically so it replies on DBus no matter what
connect(m_applicationMenu, &Menu::failedToSubscribe, this, &Window::onMenuSubscribed);
connect(m_applicationMenu, &Menu::itemsChanged, this, &Window::menuItemsChanged);
connect(m_applicationMenu, &Menu::menusChanged, this, &Window::menuChanged);
}
if (!m_menuBarObjectPath.isEmpty()) {
m_menuBar = new Menu(m_serviceName, m_menuBarObjectPath, this);
connect(m_menuBar, &Menu::menuAppeared, this, &Window::updateWindowProperties);
connect(m_menuBar, &Menu::menuDisappeared, this, &Window::updateWindowProperties);
connect(m_menuBar, &Menu::subscribed, this, &Window::onMenuSubscribed);
connect(m_menuBar, &Menu::failedToSubscribe, this, &Window::onMenuSubscribed);
connect(m_menuBar, &Menu::itemsChanged, this, &Window::menuItemsChanged);
connect(m_menuBar, &Menu::menusChanged, this, &Window::menuChanged);
}
if (!m_applicationObjectPath.isEmpty()) {
m_applicationActions = new Actions(m_serviceName, m_applicationObjectPath, this);
connect(m_applicationActions, &Actions::actionsChanged, this, [this](const QStringList &dirtyActions) {
onActionsChanged(dirtyActions, s_applicationActionsPrefix);
});
connect(m_applicationActions, &Actions::loaded, this, [this] {
if (m_menuInited) {
onActionsChanged(m_applicationActions->getAll().keys(), s_applicationActionsPrefix);
} else {
initMenu();
}
});
m_applicationActions->load();
}
if (!m_unityObjectPath.isEmpty()) {
m_unityActions = new Actions(m_serviceName, m_unityObjectPath, this);
connect(m_unityActions, &Actions::actionsChanged, this, [this](const QStringList &dirtyActions) {
onActionsChanged(dirtyActions, s_unityActionsPrefix);
});
connect(m_unityActions, &Actions::loaded, this, [this] {
if (m_menuInited) {
onActionsChanged(m_unityActions->getAll().keys(), s_unityActionsPrefix);
} else {
initMenu();
}
});
m_unityActions->load();
}
if (!m_windowObjectPath.isEmpty()) {
m_windowActions = new Actions(m_serviceName, m_windowObjectPath, this);
connect(m_windowActions, &Actions::actionsChanged, this, [this](const QStringList &dirtyActions) {
onActionsChanged(dirtyActions, s_windowActionsPrefix);
});
connect(m_windowActions, &Actions::loaded, this, [this] {
if (m_menuInited) {
onActionsChanged(m_windowActions->getAll().keys(), s_windowActionsPrefix);
} else {
initMenu();
}
});
m_windowActions->load();
}
}
WId Window::winId() const
{
return m_winId;
}
void Window::setWinId(WId winId)
{
m_winId = winId;
}
QString Window::serviceName() const
{
return m_serviceName;
}
QString Window::applicationObjectPath() const
{
return m_applicationObjectPath;
}
void Window::setApplicationObjectPath(const QString &applicationObjectPath)
{
m_applicationObjectPath = applicationObjectPath;
}
QString Window::unityObjectPath() const
{
return m_unityObjectPath;
}
void Window::setUnityObjectPath(const QString &unityObjectPath)
{
m_unityObjectPath = unityObjectPath;
}
QString Window::applicationMenuObjectPath() const
{
return m_applicationMenuObjectPath;
}
void Window::setApplicationMenuObjectPath(const QString &applicationMenuObjectPath)
{
m_applicationMenuObjectPath = applicationMenuObjectPath;
}
QString Window::menuBarObjectPath() const
{
return m_menuBarObjectPath;
}
void Window::setMenuBarObjectPath(const QString &menuBarObjectPath)
{
m_menuBarObjectPath = menuBarObjectPath;
}
QString Window::windowObjectPath() const
{
return m_windowObjectPath;
}
void Window::setWindowObjectPath(const QString &windowObjectPath)
{
m_windowObjectPath = windowObjectPath;
}
QString Window::currentMenuObjectPath() const
{
return m_currentMenuObjectPath;
}
QString Window::proxyObjectPath() const
{
return m_proxyObjectPath;
}
void Window::initMenu()
{
if (m_menuInited) {
return;
}
if (!registerDBusObject()) {
return;
}
// appmenu-gtk-module always announces a menu bar on every GTK window even if there is none
// so we subscribe to the menu bar as soon as it shows up so we can figure out
// if we have a menu bar, an app menu, or just nothing
if (m_applicationMenu) {
m_applicationMenu->start(0);
}
if (m_menuBar) {
m_menuBar->start(0);
}
m_menuInited = true;
}
void Window::menuItemsChanged(const QVector<uint> &itemIds)
{
if (qobject_cast<Menu *>(sender()) != m_currentMenu) {
return;
}
DBusMenuItemList items;
for (uint id : itemIds) {
const auto newItem = m_currentMenu->getItem(id);
DBusMenuItem dBusItem{// 0 is menu, items start at 1
static_cast<int>(id),
gMenuToDBusMenuProperties(newItem)};
items.append(dBusItem);
}
Q_EMIT ItemsPropertiesUpdated(items, {});
}
void Window::menuChanged(const QVector<uint> &menuIds)
{
if (qobject_cast<Menu *>(sender()) != m_currentMenu) {
return;
}
for (uint menu : menuIds) {
Q_EMIT LayoutUpdated(3 /*revision*/, menu);
}
}
void Window::onMenuSubscribed(uint 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(Utils::treeStructureToInt(id, 0, 0), 0, {}, item);
reply << revision << QVariant::fromValue(item);
QDBusConnection::sessionBus().send(reply);
}
}
m_pendingGetLayouts.remove(id);
} else {
Q_EMIT LayoutUpdated(2 /*revision*/, id);
}
}
bool Window::getAction(const QString &name, GMenuAction &action) const
{
QString lookupName;
Actions *actions = getActionsForAction(name, lookupName);
if (!actions) {
return false;
}
return actions->get(lookupName, action);
}
void Window::triggerAction(const QString &name, const QVariant &target, uint timestamp)
{
QString lookupName;
Actions *actions = getActionsForAction(name, lookupName);
if (!actions) {
return;
}
actions->trigger(lookupName, target, timestamp);
}
Actions *Window::getActionsForAction(const QString &name, QString &lookupName) const
{
if (name.startsWith(QLatin1String("app."))) {
lookupName = name.mid(4);
return m_applicationActions;
} else if (name.startsWith(QLatin1String("unity."))) {
lookupName = name.mid(6);
return m_unityActions;
} else if (name.startsWith(QLatin1String("win."))) {
lookupName = name.mid(4);
return m_windowActions;
}
return nullptr;
}
void Window::onActionsChanged(const QStringList &dirty, const QString &prefix)
{
if (m_applicationMenu) {
m_applicationMenu->actionsChanged(dirty, prefix);
}
if (m_menuBar) {
m_menuBar->actionsChanged(dirty, prefix);
}
}
bool Window::registerDBusObject()
{
Q_ASSERT(m_proxyObjectPath.isEmpty());
static int menus = 0;
++menus;
new DbusmenuAdaptor(this);
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;
}
m_proxyObjectPath = objectPath;
return true;
}
void Window::updateWindowProperties()
{
const bool hasMenu = ((m_applicationMenu && m_applicationMenu->hasMenu()) || (m_menuBar && m_menuBar->hasMenu()));
if (!hasMenu) {
Q_EMIT requestRemoveWindowProperties();
return;
}
Menu *oldMenu = m_currentMenu;
Menu *newMenu = qobject_cast<Menu *>(sender());
// set current menu as needed
if (!m_currentMenu) {
m_currentMenu = newMenu;
// Menu Bar takes precedence over application menu
} else if (m_currentMenu == m_applicationMenu && newMenu == m_menuBar) {
qCDebug(DBUSMENUPROXY) << "Switching from application menu to menu bar";
m_currentMenu = newMenu;
// TODO update layout
}
if (m_currentMenu != oldMenu) {
// update entire menu now
Q_EMIT LayoutUpdated(4 /*revision*/, 0);
}
Q_EMIT requestWriteWindowProperties();
}
// DBus
bool Window::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 Window::Event(int id, const QString &eventId, const QDBusVariant &data, uint timestamp)
{
Q_UNUSED(data);
if (!m_currentMenu) {
return;
}
// GMenu dbus doesn't have any "opened" or "closed" signals, we'll only handle "clicked"
if (eventId == QLatin1String("clicked")) {
const QVariantMap item = m_currentMenu->getItem(id);
const QString action = item.value(QStringLiteral("action")).toString();
const QVariant target = item.value(QStringLiteral("target"));
if (!action.isEmpty()) {
triggerAction(action, target, timestamp);
}
}
}
DBusMenuItemList Window::GetGroupProperties(const QList<int> &ids, const QStringList &propertyNames)
{
Q_UNUSED(ids);
Q_UNUSED(propertyNames);
return DBusMenuItemList();
}
uint Window::GetLayout(int parentId, int recursionDepth, const QStringList &propertyNames, DBusMenuLayoutItem &dbusItem)
{
Q_UNUSED(recursionDepth); // TODO
Q_UNUSED(propertyNames);
int subscription;
int sectionId;
int index;
Utils::intToTreeStructure(parentId, subscription, sectionId, index);
if (!m_currentMenu) {
return 1;
}
if (!m_currentMenu->hasSubscription(subscription)) {
// let's serve multiple similar requests in one go once we've processed them
m_pendingGetLayouts.insert(subscription, message());
setDelayedReply(true);
m_currentMenu->start(subscription);
return 1;
}
bool ok;
const GMenuItem section = m_currentMenu->getSection(subscription, sectionId, &ok);
if (!ok) {
qCDebug(DBUSMENUPROXY) << "There is no section on" << subscription << "at" << sectionId << "with" << parentId;
return 1;
}
// If a particular entry is requested, see what it is and resolve as necessary
// 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(Utils::treeStructureToInt(gmenuSection.subscription, gmenuSection.menu, 0), recursionDepth, propertyNames, dbusItem);
} else {
// TODO
return 0;
}
}
dbusItem.id = parentId; // TODO
dbusItem.properties = {{QStringLiteral("children-display"), QStringLiteral("submenu")}};
int count = 0;
const auto itemsToBeAdded = section.items;
for (const auto &item : itemsToBeAdded) {
DBusMenuLayoutItem child{
Utils::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 = m_currentMenu->getSection(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 = m_currentMenu->getSection(gmenuSection2.subscription, gmenuSection2.menu).items;
originalSubscription = gmenuSection2.subscription;
originalMenu = gmenuSection2.menu;
}
}
int aliasedCount = 0;
for (const auto &aliasedItem : qAsConst(items)) {
DBusMenuLayoutItem aliasedChild{
Utils::treeStructureToInt(originalSubscription, originalMenu, ++aliasedCount),
gMenuToDBusMenuProperties(aliasedItem),
{} // children
};
dbusItem.children.append(aliasedChild);
}
}
}
// revision, unused in libdbusmenuqt
return 1;
}
QDBusVariant Window::GetProperty(int id, const QString &property)
{
Q_UNUSED(id);
Q_UNUSED(property);
QDBusVariant value;
return value;
}
QString Window::status() const
{
return QStringLiteral("normal");
}
uint Window::version() const
{
return 4;
}
QVariantMap Window::gMenuToDBusMenuProperties(const QVariantMap &source) const
{
QVariantMap result;
result.insert(QStringLiteral("label"), source.value(QStringLiteral("label")).toString());
if (source.contains(QLatin1String(":section"))) {
result.insert(QStringLiteral("type"), QStringLiteral("separator"));
}
const bool isMenu = source.contains(QLatin1String(":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 = Utils::itemActionName(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();
}
icon = Icons::actionIcon(actionName);
if (!icon.isEmpty()) {
result.insert(QStringLiteral("icon-name"), icon);
}
const QVariant target = source.value(QStringLiteral("target"));
if (actionOk) {
const auto actionStates = action.state;
if (actionStates.count() == 1) {
const auto &actionState = actionStates.first();
// assume this is a checkbox
if (!isMenu) {
if (actionState.type() == QVariant::Bool) {
result.insert(QStringLiteral("toggle-type"), QStringLiteral("checkbox"));
result.insert(QStringLiteral("toggle-state"), actionState.toBool() ? 1 : 0);
} else if (actionState.type() == QVariant::String) {
result.insert(QStringLiteral("toggle-type"), QStringLiteral("radio"));
result.insert(QStringLiteral("toggle-state"), actionState == target ? 1 : 0);
}
}
}
}
return result;
}