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.
392 lines
13 KiB
392 lines
13 KiB
/* |
|
SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <kde@privat.broulik.de> |
|
|
|
SPDX-License-Identifier: LGPL-2.1-or-later |
|
*/ |
|
|
|
#include "menuproxy.h" |
|
|
|
#include <config-X11.h> |
|
|
|
#include "debug.h" |
|
|
|
#include <QCoreApplication> |
|
#include <QDBusConnection> |
|
#include <QDBusConnectionInterface> |
|
#include <QDBusServiceWatcher> |
|
#include <QDir> |
|
#include <QFileInfo> |
|
#include <QStandardPaths> |
|
#include <QTimer> |
|
|
|
#include <KConfigGroup> |
|
#include <KDirWatch> |
|
#include <KSharedConfig> |
|
#include <KWindowSystem> |
|
|
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) |
|
#include <private/qtx11extras_p.h> |
|
#else |
|
#include <QX11Info> |
|
#endif |
|
#include <xcb/xcb.h> |
|
|
|
#include "../c_ptr.h" |
|
#include "window.h" |
|
#include <chrono> |
|
|
|
using namespace std::chrono_literals; |
|
|
|
static const QString s_ourServiceName = QStringLiteral("org.kde.plasma.gmenu_dbusmenu_proxy"); |
|
|
|
static const QString s_dbusMenuRegistrar = QStringLiteral("com.canonical.AppMenu.Registrar"); |
|
|
|
static const QByteArray s_gtkUniqueBusName = QByteArrayLiteral("_GTK_UNIQUE_BUS_NAME"); |
|
|
|
static const QByteArray s_gtkApplicationObjectPath = QByteArrayLiteral("_GTK_APPLICATION_OBJECT_PATH"); |
|
static const QByteArray s_unityObjectPath = QByteArrayLiteral("_UNITY_OBJECT_PATH"); |
|
static const QByteArray s_gtkWindowObjectPath = QByteArrayLiteral("_GTK_WINDOW_OBJECT_PATH"); |
|
static const QByteArray s_gtkMenuBarObjectPath = QByteArrayLiteral("_GTK_MENUBAR_OBJECT_PATH"); |
|
// that's the generic app menu with Help and Options and will be used if window doesn't have a fully-blown menu bar |
|
static const QByteArray s_gtkAppMenuObjectPath = QByteArrayLiteral("_GTK_APP_MENU_OBJECT_PATH"); |
|
|
|
static const QByteArray s_kdeNetWmAppMenuServiceName = QByteArrayLiteral("_KDE_NET_WM_APPMENU_SERVICE_NAME"); |
|
static const QByteArray s_kdeNetWmAppMenuObjectPath = QByteArrayLiteral("_KDE_NET_WM_APPMENU_OBJECT_PATH"); |
|
|
|
static const QString s_gtkModules = QStringLiteral("gtk-modules"); |
|
static const QString s_appMenuGtkModule = QStringLiteral("appmenu-gtk-module"); |
|
|
|
MenuProxy::MenuProxy() |
|
: QObject() |
|
, m_xConnection(QX11Info::connection()) |
|
, m_serviceWatcher(new QDBusServiceWatcher(this)) |
|
, m_gtk2RcWatch(new KDirWatch(this)) |
|
, m_writeGtk2SettingsTimer(new QTimer(this)) |
|
{ |
|
m_serviceWatcher->setConnection(QDBusConnection::sessionBus()); |
|
m_serviceWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration | QDBusServiceWatcher::WatchForRegistration); |
|
m_serviceWatcher->addWatchedService(s_dbusMenuRegistrar); |
|
|
|
connect(m_serviceWatcher, &QDBusServiceWatcher::serviceRegistered, this, [this](const QString &service) { |
|
Q_UNUSED(service); |
|
qCDebug(DBUSMENUPROXY) << "Global menu service became available, starting"; |
|
init(); |
|
}); |
|
connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this](const QString &service) { |
|
Q_UNUSED(service); |
|
qCDebug(DBUSMENUPROXY) << "Global menu service disappeared, cleaning up"; |
|
teardown(); |
|
}); |
|
|
|
// It's fine to do a blocking call here as we're a separate binary with no UI |
|
if (QDBusConnection::sessionBus().interface()->isServiceRegistered(s_dbusMenuRegistrar)) { |
|
qCDebug(DBUSMENUPROXY) << "Global menu service is running, starting right away"; |
|
init(); |
|
} else { |
|
qCDebug(DBUSMENUPROXY) << "No global menu service available, waiting for it to start before doing anything"; |
|
|
|
// be sure when started to restore gtk menus when there's no dbus menu around in case we crashed |
|
enableGtkSettings(false); |
|
} |
|
|
|
// kde-gtk-config just deletes and re-creates the gtkrc-2.0, watch this and add our config to it again |
|
m_writeGtk2SettingsTimer->setSingleShot(true); |
|
m_writeGtk2SettingsTimer->setInterval(1s); |
|
connect(m_writeGtk2SettingsTimer, &QTimer::timeout, this, &MenuProxy::writeGtk2Settings); |
|
|
|
auto startGtk2SettingsTimer = [this] { |
|
if (!m_writeGtk2SettingsTimer->isActive()) { |
|
m_writeGtk2SettingsTimer->start(); |
|
} |
|
}; |
|
|
|
connect(m_gtk2RcWatch, &KDirWatch::created, this, startGtk2SettingsTimer); |
|
connect(m_gtk2RcWatch, &KDirWatch::dirty, this, startGtk2SettingsTimer); |
|
m_gtk2RcWatch->addFile(gtkRc2Path()); |
|
} |
|
|
|
MenuProxy::~MenuProxy() |
|
{ |
|
teardown(); |
|
} |
|
|
|
bool MenuProxy::init() |
|
{ |
|
if (!QDBusConnection::sessionBus().registerService(s_ourServiceName)) { |
|
qCWarning(DBUSMENUPROXY) << "Failed to register DBus service" << s_ourServiceName; |
|
return false; |
|
} |
|
|
|
enableGtkSettings(true); |
|
|
|
connect(KWindowSystem::self(), &KWindowSystem::windowAdded, this, &MenuProxy::onWindowAdded); |
|
connect(KWindowSystem::self(), &KWindowSystem::windowRemoved, this, &MenuProxy::onWindowRemoved); |
|
|
|
const auto windows = KWindowSystem::windows(); |
|
for (WId id : windows) { |
|
onWindowAdded(id); |
|
} |
|
|
|
if (m_windows.isEmpty()) { |
|
qCDebug(DBUSMENUPROXY) << "Up and running but no windows with menus in sight"; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
void MenuProxy::teardown() |
|
{ |
|
enableGtkSettings(false); |
|
|
|
QDBusConnection::sessionBus().unregisterService(s_ourServiceName); |
|
|
|
disconnect(KWindowSystem::self(), &KWindowSystem::windowAdded, this, &MenuProxy::onWindowAdded); |
|
disconnect(KWindowSystem::self(), &KWindowSystem::windowRemoved, this, &MenuProxy::onWindowRemoved); |
|
|
|
qDeleteAll(m_windows); |
|
m_windows.clear(); |
|
} |
|
|
|
void MenuProxy::enableGtkSettings(bool enable) |
|
{ |
|
m_enabled = enable; |
|
|
|
writeGtk2Settings(); |
|
writeGtk3Settings(); |
|
|
|
// TODO use gconf/dconf directly or at least signal a change somehow? |
|
} |
|
|
|
QString MenuProxy::gtkRc2Path() |
|
{ |
|
return QDir::homePath() + QLatin1String("/.gtkrc-2.0"); |
|
} |
|
|
|
QString MenuProxy::gtk3SettingsIniPath() |
|
{ |
|
return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1String("/gtk-3.0/settings.ini"); |
|
} |
|
|
|
void MenuProxy::writeGtk2Settings() |
|
{ |
|
QFile rcFile(gtkRc2Path()); |
|
if (!rcFile.exists()) { |
|
// Don't create it here, that would break writing default GTK-2.0 settings on first login, |
|
// as the gtkbreeze kconf_update script only does so if it does not exist |
|
return; |
|
} |
|
|
|
qCDebug(DBUSMENUPROXY) << "Writing gtkrc-2.0 to" << (m_enabled ? "enable" : "disable") << "global menu support"; |
|
|
|
if (!rcFile.open(QIODevice::ReadWrite | QIODevice::Text)) { |
|
return; |
|
} |
|
|
|
QByteArray content; |
|
|
|
QStringList gtkModules; |
|
|
|
while (!rcFile.atEnd()) { |
|
const QByteArray rawLine = rcFile.readLine(); |
|
|
|
const QString line = QString::fromUtf8(rawLine.trimmed()); |
|
|
|
if (!line.startsWith(s_gtkModules)) { |
|
// keep line as-is |
|
content += rawLine; |
|
continue; |
|
} |
|
|
|
const int equalSignIdx = line.indexOf(QLatin1Char('=')); |
|
if (equalSignIdx < 1) { |
|
continue; |
|
} |
|
|
|
gtkModules = line.mid(equalSignIdx + 1).split(QLatin1Char(':'), Qt::SkipEmptyParts); |
|
|
|
break; |
|
} |
|
|
|
addOrRemoveAppMenuGtkModule(gtkModules); |
|
|
|
if (!gtkModules.isEmpty()) { |
|
content += QStringLiteral("%1=%2").arg(s_gtkModules, gtkModules.join(QLatin1Char(':'))).toUtf8(); |
|
} |
|
|
|
qCDebug(DBUSMENUPROXY) << " gtk-modules:" << gtkModules; |
|
|
|
m_gtk2RcWatch->stopScan(); |
|
|
|
// now write the new contents of the file |
|
rcFile.resize(0); |
|
rcFile.write(content); |
|
rcFile.close(); |
|
|
|
m_gtk2RcWatch->startScan(); |
|
} |
|
|
|
void MenuProxy::writeGtk3Settings() |
|
{ |
|
qCDebug(DBUSMENUPROXY) << "Writing gtk-3.0/settings.ini" << (m_enabled ? "enable" : "disable") << "global menu support"; |
|
|
|
// mostly taken from kde-gtk-config |
|
auto cfg = KSharedConfig::openConfig(gtk3SettingsIniPath(), KConfig::NoGlobals); |
|
KConfigGroup group(cfg, "Settings"); |
|
|
|
QStringList gtkModules = group.readEntry("gtk-modules", QString()).split(QLatin1Char(':'), Qt::SkipEmptyParts); |
|
addOrRemoveAppMenuGtkModule(gtkModules); |
|
|
|
if (!gtkModules.isEmpty()) { |
|
group.writeEntry("gtk-modules", gtkModules.join(QLatin1Char(':'))); |
|
} else { |
|
group.deleteEntry("gtk-modules"); |
|
} |
|
|
|
qCDebug(DBUSMENUPROXY) << " gtk-modules:" << gtkModules; |
|
|
|
if (m_enabled) { |
|
group.writeEntry("gtk-shell-shows-menubar", 1); |
|
} else { |
|
group.deleteEntry("gtk-shell-shows-menubar"); |
|
} |
|
|
|
qCDebug(DBUSMENUPROXY) << " gtk-shell-shows-menubar:" << (m_enabled ? 1 : 0); |
|
|
|
group.sync(); |
|
} |
|
|
|
void MenuProxy::addOrRemoveAppMenuGtkModule(QStringList &list) |
|
{ |
|
if (m_enabled && !list.contains(s_appMenuGtkModule)) { |
|
list.append(s_appMenuGtkModule); |
|
} else if (!m_enabled) { |
|
list.removeAll(s_appMenuGtkModule); |
|
} |
|
} |
|
|
|
void MenuProxy::onWindowAdded(WId id) |
|
{ |
|
if (m_windows.contains(id)) { |
|
return; |
|
} |
|
|
|
KWindowInfo info(id, NET::WMWindowType); |
|
|
|
NET::WindowType wType = info.windowType(NET::NormalMask | NET::DesktopMask | NET::DockMask | NET::ToolbarMask | NET::MenuMask | NET::DialogMask |
|
| NET::OverrideMask | NET::TopMenuMask | NET::UtilityMask | NET::SplashMask); |
|
|
|
// Only top level windows typically have a menu bar, dialogs, such as settings don't |
|
if (wType != NET::Normal) { |
|
qCDebug(DBUSMENUPROXY) << "Ignoring window" << id << "of type" << wType; |
|
return; |
|
} |
|
|
|
const QString serviceName = QString::fromUtf8(getWindowPropertyString(id, s_gtkUniqueBusName)); |
|
|
|
if (serviceName.isEmpty()) { |
|
return; |
|
} |
|
|
|
const QString applicationObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkApplicationObjectPath)); |
|
const QString unityObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_unityObjectPath)); |
|
const QString windowObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkWindowObjectPath)); |
|
|
|
const QString applicationMenuObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkAppMenuObjectPath)); |
|
const QString menuBarObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkMenuBarObjectPath)); |
|
|
|
if (applicationMenuObjectPath.isEmpty() && menuBarObjectPath.isEmpty()) { |
|
return; |
|
} |
|
|
|
Window *window = new Window(serviceName); |
|
window->setWinId(id); |
|
window->setApplicationObjectPath(applicationObjectPath); |
|
window->setUnityObjectPath(unityObjectPath); |
|
window->setWindowObjectPath(windowObjectPath); |
|
window->setApplicationMenuObjectPath(applicationMenuObjectPath); |
|
window->setMenuBarObjectPath(menuBarObjectPath); |
|
m_windows.insert(id, window); |
|
|
|
connect(window, &Window::requestWriteWindowProperties, this, [this, window] { |
|
Q_ASSERT(!window->proxyObjectPath().isEmpty()); |
|
|
|
writeWindowProperty(window->winId(), s_kdeNetWmAppMenuServiceName, s_ourServiceName.toUtf8()); |
|
writeWindowProperty(window->winId(), s_kdeNetWmAppMenuObjectPath, window->proxyObjectPath().toUtf8()); |
|
}); |
|
connect(window, &Window::requestRemoveWindowProperties, this, [this, window] { |
|
writeWindowProperty(window->winId(), s_kdeNetWmAppMenuServiceName, QByteArray()); |
|
writeWindowProperty(window->winId(), s_kdeNetWmAppMenuObjectPath, QByteArray()); |
|
}); |
|
|
|
window->init(); |
|
} |
|
|
|
void MenuProxy::onWindowRemoved(WId id) |
|
{ |
|
// no need to cleanup() (which removes window properties) when the window is gone, delete right away |
|
delete m_windows.take(id); |
|
} |
|
|
|
QByteArray MenuProxy::getWindowPropertyString(WId id, const QByteArray &name) |
|
{ |
|
QByteArray value; |
|
|
|
auto atom = getAtom(name); |
|
if (atom == XCB_ATOM_NONE) { |
|
return value; |
|
} |
|
|
|
// GTK properties aren't XCB_ATOM_STRING but a custom one |
|
auto utf8StringAtom = getAtom(QByteArrayLiteral("UTF8_STRING")); |
|
|
|
static const long MAX_PROP_SIZE = 10000; |
|
auto propertyCookie = xcb_get_property(m_xConnection, false, id, atom, utf8StringAtom, 0, MAX_PROP_SIZE); |
|
UniqueCPointer<xcb_get_property_reply_t> propertyReply(xcb_get_property_reply(m_xConnection, propertyCookie, nullptr)); |
|
if (!propertyReply) { |
|
qCWarning(DBUSMENUPROXY) << "XCB property reply for atom" << name << "on" << id << "was null"; |
|
return value; |
|
} |
|
|
|
if (propertyReply->type == utf8StringAtom && propertyReply->format == 8 && propertyReply->value_len > 0) { |
|
const char *data = (const char *)xcb_get_property_value(propertyReply.get()); |
|
int len = propertyReply->value_len; |
|
if (data) { |
|
value = QByteArray(data, data[len - 1] ? len : len - 1); |
|
} |
|
} |
|
|
|
return value; |
|
} |
|
|
|
void MenuProxy::writeWindowProperty(WId id, const QByteArray &name, const QByteArray &value) |
|
{ |
|
auto atom = getAtom(name); |
|
if (atom == XCB_ATOM_NONE) { |
|
return; |
|
} |
|
|
|
if (value.isEmpty()) { |
|
xcb_delete_property(m_xConnection, id, atom); |
|
} else { |
|
xcb_change_property(m_xConnection, XCB_PROP_MODE_REPLACE, id, atom, XCB_ATOM_STRING, 8, value.length(), value.constData()); |
|
} |
|
} |
|
|
|
xcb_atom_t MenuProxy::getAtom(const QByteArray &name) |
|
{ |
|
static QHash<QByteArray, xcb_atom_t> s_atoms; |
|
|
|
auto atom = s_atoms.value(name, XCB_ATOM_NONE); |
|
if (atom == XCB_ATOM_NONE) { |
|
const xcb_intern_atom_cookie_t atomCookie = xcb_intern_atom(m_xConnection, false, name.length(), name.constData()); |
|
UniqueCPointer<xcb_intern_atom_reply_t> atomReply(xcb_intern_atom_reply(m_xConnection, atomCookie, nullptr)); |
|
if (atomReply) { |
|
atom = atomReply->atom; |
|
if (atom != XCB_ATOM_NONE) { |
|
s_atoms.insert(name, atom); |
|
} |
|
} |
|
} |
|
|
|
return atom; |
|
}
|
|
|