File indexing completed on 2024-05-05 17:43:53

0001 /*
0002     SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <kde@privat.broulik.de>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-or-later
0005 */
0006 
0007 #include "menuproxy.h"
0008 
0009 #include <config-X11.h>
0010 
0011 #include "debug.h"
0012 
0013 #include <QCoreApplication>
0014 #include <QDBusConnection>
0015 #include <QDBusConnectionInterface>
0016 #include <QDBusServiceWatcher>
0017 #include <QDir>
0018 #include <QFileInfo>
0019 #include <QStandardPaths>
0020 #include <QTimer>
0021 
0022 #include <KConfigGroup>
0023 #include <KDirWatch>
0024 #include <KSharedConfig>
0025 #include <KWindowSystem>
0026 #include <KX11Extras>
0027 
0028 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0029 #include <private/qtx11extras_p.h>
0030 #else
0031 #include <QX11Info>
0032 #endif
0033 #include <xcb/xcb.h>
0034 
0035 #include "../c_ptr.h"
0036 #include "window.h"
0037 #include <chrono>
0038 
0039 using namespace std::chrono_literals;
0040 
0041 static const QString s_ourServiceName = QStringLiteral("org.kde.plasma.gmenu_dbusmenu_proxy");
0042 
0043 static const QString s_dbusMenuRegistrar = QStringLiteral("com.canonical.AppMenu.Registrar");
0044 
0045 static const QByteArray s_gtkUniqueBusName = QByteArrayLiteral("_GTK_UNIQUE_BUS_NAME");
0046 
0047 static const QByteArray s_gtkApplicationObjectPath = QByteArrayLiteral("_GTK_APPLICATION_OBJECT_PATH");
0048 static const QByteArray s_unityObjectPath = QByteArrayLiteral("_UNITY_OBJECT_PATH");
0049 static const QByteArray s_gtkWindowObjectPath = QByteArrayLiteral("_GTK_WINDOW_OBJECT_PATH");
0050 static const QByteArray s_gtkMenuBarObjectPath = QByteArrayLiteral("_GTK_MENUBAR_OBJECT_PATH");
0051 // that's the generic app menu with Help and Options and will be used if window doesn't have a fully-blown menu bar
0052 static const QByteArray s_gtkAppMenuObjectPath = QByteArrayLiteral("_GTK_APP_MENU_OBJECT_PATH");
0053 
0054 static const QByteArray s_kdeNetWmAppMenuServiceName = QByteArrayLiteral("_KDE_NET_WM_APPMENU_SERVICE_NAME");
0055 static const QByteArray s_kdeNetWmAppMenuObjectPath = QByteArrayLiteral("_KDE_NET_WM_APPMENU_OBJECT_PATH");
0056 
0057 static const QString s_gtkModules = QStringLiteral("gtk-modules");
0058 static const QString s_appMenuGtkModule = QStringLiteral("appmenu-gtk-module");
0059 
0060 MenuProxy::MenuProxy()
0061     : QObject()
0062     , m_xConnection(QX11Info::connection())
0063     , m_serviceWatcher(new QDBusServiceWatcher(this))
0064     , m_gtk2RcWatch(new KDirWatch(this))
0065     , m_writeGtk2SettingsTimer(new QTimer(this))
0066 {
0067     m_serviceWatcher->setConnection(QDBusConnection::sessionBus());
0068     m_serviceWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration | QDBusServiceWatcher::WatchForRegistration);
0069     m_serviceWatcher->addWatchedService(s_dbusMenuRegistrar);
0070 
0071     connect(m_serviceWatcher, &QDBusServiceWatcher::serviceRegistered, this, [this](const QString &service) {
0072         Q_UNUSED(service);
0073         qCDebug(DBUSMENUPROXY) << "Global menu service became available, starting";
0074         init();
0075     });
0076     connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this](const QString &service) {
0077         Q_UNUSED(service);
0078         qCDebug(DBUSMENUPROXY) << "Global menu service disappeared, cleaning up";
0079         teardown();
0080     });
0081 
0082     // It's fine to do a blocking call here as we're a separate binary with no UI
0083     if (QDBusConnection::sessionBus().interface()->isServiceRegistered(s_dbusMenuRegistrar)) {
0084         qCDebug(DBUSMENUPROXY) << "Global menu service is running, starting right away";
0085         init();
0086     } else {
0087         qCDebug(DBUSMENUPROXY) << "No global menu service available, waiting for it to start before doing anything";
0088 
0089         // be sure when started to restore gtk menus when there's no dbus menu around in case we crashed
0090         enableGtkSettings(false);
0091     }
0092 
0093     // kde-gtk-config just deletes and re-creates the gtkrc-2.0, watch this and add our config to it again
0094     m_writeGtk2SettingsTimer->setSingleShot(true);
0095     m_writeGtk2SettingsTimer->setInterval(1s);
0096     connect(m_writeGtk2SettingsTimer, &QTimer::timeout, this, &MenuProxy::writeGtk2Settings);
0097 
0098     auto startGtk2SettingsTimer = [this] {
0099         if (!m_writeGtk2SettingsTimer->isActive()) {
0100             m_writeGtk2SettingsTimer->start();
0101         }
0102     };
0103 
0104     connect(m_gtk2RcWatch, &KDirWatch::created, this, startGtk2SettingsTimer);
0105     connect(m_gtk2RcWatch, &KDirWatch::dirty, this, startGtk2SettingsTimer);
0106     m_gtk2RcWatch->addFile(gtkRc2Path());
0107 }
0108 
0109 MenuProxy::~MenuProxy()
0110 {
0111     teardown();
0112 }
0113 
0114 bool MenuProxy::init()
0115 {
0116     if (!QDBusConnection::sessionBus().registerService(s_ourServiceName)) {
0117         qCWarning(DBUSMENUPROXY) << "Failed to register DBus service" << s_ourServiceName;
0118         return false;
0119     }
0120 
0121     enableGtkSettings(true);
0122 
0123     connect(KX11Extras::self(), &KX11Extras::windowAdded, this, &MenuProxy::onWindowAdded);
0124     connect(KX11Extras::self(), &KX11Extras::windowRemoved, this, &MenuProxy::onWindowRemoved);
0125 
0126     const auto windows = KX11Extras::windows();
0127     for (WId id : windows) {
0128         onWindowAdded(id);
0129     }
0130 
0131     if (m_windows.isEmpty()) {
0132         qCDebug(DBUSMENUPROXY) << "Up and running but no windows with menus in sight";
0133     }
0134 
0135     return true;
0136 }
0137 
0138 void MenuProxy::teardown()
0139 {
0140     enableGtkSettings(false);
0141 
0142     QDBusConnection::sessionBus().unregisterService(s_ourServiceName);
0143 
0144     disconnect(KX11Extras::self(), &KX11Extras::windowAdded, this, &MenuProxy::onWindowAdded);
0145     disconnect(KX11Extras::self(), &KX11Extras::windowRemoved, this, &MenuProxy::onWindowRemoved);
0146 
0147     qDeleteAll(m_windows);
0148     m_windows.clear();
0149 }
0150 
0151 void MenuProxy::enableGtkSettings(bool enable)
0152 {
0153     m_enabled = enable;
0154 
0155     writeGtk2Settings();
0156     writeGtk3Settings();
0157 
0158     // TODO use gconf/dconf directly or at least signal a change somehow?
0159 }
0160 
0161 QString MenuProxy::gtkRc2Path()
0162 {
0163     return QDir::homePath() + QLatin1String("/.gtkrc-2.0");
0164 }
0165 
0166 QString MenuProxy::gtk3SettingsIniPath()
0167 {
0168     return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1String("/gtk-3.0/settings.ini");
0169 }
0170 
0171 void MenuProxy::writeGtk2Settings()
0172 {
0173     QFile rcFile(gtkRc2Path());
0174     if (!rcFile.exists()) {
0175         // Don't create it here, that would break writing default GTK-2.0 settings on first login,
0176         // as the gtkbreeze kconf_update script only does so if it does not exist
0177         return;
0178     }
0179 
0180     qCDebug(DBUSMENUPROXY) << "Writing gtkrc-2.0 to" << (m_enabled ? "enable" : "disable") << "global menu support";
0181 
0182     if (!rcFile.open(QIODevice::ReadWrite | QIODevice::Text)) {
0183         return;
0184     }
0185 
0186     QByteArray content;
0187 
0188     QStringList gtkModules;
0189 
0190     while (!rcFile.atEnd()) {
0191         const QByteArray rawLine = rcFile.readLine();
0192 
0193         const QString line = QString::fromUtf8(rawLine.trimmed());
0194 
0195         if (!line.startsWith(s_gtkModules)) {
0196             // keep line as-is
0197             content += rawLine;
0198             continue;
0199         }
0200 
0201         const int equalSignIdx = line.indexOf(QLatin1Char('='));
0202         if (equalSignIdx < 1) {
0203             continue;
0204         }
0205 
0206         gtkModules = line.mid(equalSignIdx + 1).split(QLatin1Char(':'), Qt::SkipEmptyParts);
0207 
0208         break;
0209     }
0210 
0211     addOrRemoveAppMenuGtkModule(gtkModules);
0212 
0213     if (!gtkModules.isEmpty()) {
0214         content += QStringLiteral("%1=%2").arg(s_gtkModules, gtkModules.join(QLatin1Char(':'))).toUtf8();
0215     }
0216 
0217     qCDebug(DBUSMENUPROXY) << "  gtk-modules:" << gtkModules;
0218 
0219     m_gtk2RcWatch->stopScan();
0220 
0221     // now write the new contents of the file
0222     rcFile.resize(0);
0223     rcFile.write(content);
0224     rcFile.close();
0225 
0226     m_gtk2RcWatch->startScan();
0227 }
0228 
0229 void MenuProxy::writeGtk3Settings()
0230 {
0231     qCDebug(DBUSMENUPROXY) << "Writing gtk-3.0/settings.ini" << (m_enabled ? "enable" : "disable") << "global menu support";
0232 
0233     // mostly taken from kde-gtk-config
0234     auto cfg = KSharedConfig::openConfig(gtk3SettingsIniPath(), KConfig::NoGlobals);
0235     KConfigGroup group(cfg, "Settings");
0236 
0237     QStringList gtkModules = group.readEntry("gtk-modules", QString()).split(QLatin1Char(':'), Qt::SkipEmptyParts);
0238     addOrRemoveAppMenuGtkModule(gtkModules);
0239 
0240     if (!gtkModules.isEmpty()) {
0241         group.writeEntry("gtk-modules", gtkModules.join(QLatin1Char(':')));
0242     } else {
0243         group.deleteEntry("gtk-modules");
0244     }
0245 
0246     qCDebug(DBUSMENUPROXY) << "  gtk-modules:" << gtkModules;
0247 
0248     if (m_enabled) {
0249         group.writeEntry("gtk-shell-shows-menubar", 1);
0250     } else {
0251         group.deleteEntry("gtk-shell-shows-menubar");
0252     }
0253 
0254     qCDebug(DBUSMENUPROXY) << "  gtk-shell-shows-menubar:" << (m_enabled ? 1 : 0);
0255 
0256     group.sync();
0257 }
0258 
0259 void MenuProxy::addOrRemoveAppMenuGtkModule(QStringList &list)
0260 {
0261     if (m_enabled && !list.contains(s_appMenuGtkModule)) {
0262         list.append(s_appMenuGtkModule);
0263     } else if (!m_enabled) {
0264         list.removeAll(s_appMenuGtkModule);
0265     }
0266 }
0267 
0268 void MenuProxy::onWindowAdded(WId id)
0269 {
0270     if (m_windows.contains(id)) {
0271         return;
0272     }
0273 
0274     KWindowInfo info(id, NET::WMWindowType);
0275 
0276     NET::WindowType wType = info.windowType(NET::NormalMask | NET::DesktopMask | NET::DockMask | NET::ToolbarMask | NET::MenuMask | NET::DialogMask
0277                                             | NET::OverrideMask | NET::TopMenuMask | NET::UtilityMask | NET::SplashMask);
0278 
0279     // Only top level windows typically have a menu bar, dialogs, such as settings don't
0280     if (wType != NET::Normal) {
0281         qCDebug(DBUSMENUPROXY) << "Ignoring window" << id << "of type" << wType;
0282         return;
0283     }
0284 
0285     const QString serviceName = QString::fromUtf8(getWindowPropertyString(id, s_gtkUniqueBusName));
0286 
0287     if (serviceName.isEmpty()) {
0288         return;
0289     }
0290 
0291     const QString applicationObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkApplicationObjectPath));
0292     const QString unityObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_unityObjectPath));
0293     const QString windowObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkWindowObjectPath));
0294 
0295     const QString applicationMenuObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkAppMenuObjectPath));
0296     const QString menuBarObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkMenuBarObjectPath));
0297 
0298     if (applicationMenuObjectPath.isEmpty() && menuBarObjectPath.isEmpty()) {
0299         return;
0300     }
0301 
0302     Window *window = new Window(serviceName);
0303     window->setWinId(id);
0304     window->setApplicationObjectPath(applicationObjectPath);
0305     window->setUnityObjectPath(unityObjectPath);
0306     window->setWindowObjectPath(windowObjectPath);
0307     window->setApplicationMenuObjectPath(applicationMenuObjectPath);
0308     window->setMenuBarObjectPath(menuBarObjectPath);
0309     m_windows.insert(id, window);
0310 
0311     connect(window, &Window::requestWriteWindowProperties, this, [this, window] {
0312         Q_ASSERT(!window->proxyObjectPath().isEmpty());
0313 
0314         writeWindowProperty(window->winId(), s_kdeNetWmAppMenuServiceName, s_ourServiceName.toUtf8());
0315         writeWindowProperty(window->winId(), s_kdeNetWmAppMenuObjectPath, window->proxyObjectPath().toUtf8());
0316     });
0317     connect(window, &Window::requestRemoveWindowProperties, this, [this, window] {
0318         writeWindowProperty(window->winId(), s_kdeNetWmAppMenuServiceName, QByteArray());
0319         writeWindowProperty(window->winId(), s_kdeNetWmAppMenuObjectPath, QByteArray());
0320     });
0321 
0322     window->init();
0323 }
0324 
0325 void MenuProxy::onWindowRemoved(WId id)
0326 {
0327     // no need to cleanup() (which removes window properties) when the window is gone, delete right away
0328     delete m_windows.take(id);
0329 }
0330 
0331 QByteArray MenuProxy::getWindowPropertyString(WId id, const QByteArray &name)
0332 {
0333     QByteArray value;
0334 
0335     auto atom = getAtom(name);
0336     if (atom == XCB_ATOM_NONE) {
0337         return value;
0338     }
0339 
0340     // GTK properties aren't XCB_ATOM_STRING but a custom one
0341     auto utf8StringAtom = getAtom(QByteArrayLiteral("UTF8_STRING"));
0342 
0343     static const long MAX_PROP_SIZE = 10000;
0344     auto propertyCookie = xcb_get_property(m_xConnection, false, id, atom, utf8StringAtom, 0, MAX_PROP_SIZE);
0345     UniqueCPointer<xcb_get_property_reply_t> propertyReply(xcb_get_property_reply(m_xConnection, propertyCookie, nullptr));
0346     if (!propertyReply) {
0347         qCWarning(DBUSMENUPROXY) << "XCB property reply for atom" << name << "on" << id << "was null";
0348         return value;
0349     }
0350 
0351     if (propertyReply->type == utf8StringAtom && propertyReply->format == 8 && propertyReply->value_len > 0) {
0352         const char *data = (const char *)xcb_get_property_value(propertyReply.get());
0353         int len = propertyReply->value_len;
0354         if (data) {
0355             value = QByteArray(data, data[len - 1] ? len : len - 1);
0356         }
0357     }
0358 
0359     return value;
0360 }
0361 
0362 void MenuProxy::writeWindowProperty(WId id, const QByteArray &name, const QByteArray &value)
0363 {
0364     auto atom = getAtom(name);
0365     if (atom == XCB_ATOM_NONE) {
0366         return;
0367     }
0368 
0369     if (value.isEmpty()) {
0370         xcb_delete_property(m_xConnection, id, atom);
0371     } else {
0372         xcb_change_property(m_xConnection, XCB_PROP_MODE_REPLACE, id, atom, XCB_ATOM_STRING, 8, value.length(), value.constData());
0373     }
0374 }
0375 
0376 xcb_atom_t MenuProxy::getAtom(const QByteArray &name)
0377 {
0378     static QHash<QByteArray, xcb_atom_t> s_atoms;
0379 
0380     auto atom = s_atoms.value(name, XCB_ATOM_NONE);
0381     if (atom == XCB_ATOM_NONE) {
0382         const xcb_intern_atom_cookie_t atomCookie = xcb_intern_atom(m_xConnection, false, name.length(), name.constData());
0383         UniqueCPointer<xcb_intern_atom_reply_t> atomReply(xcb_intern_atom_reply(m_xConnection, atomCookie, nullptr));
0384         if (atomReply) {
0385             atom = atomReply->atom;
0386             if (atom != XCB_ATOM_NONE) {
0387                 s_atoms.insert(name, atom);
0388             }
0389         }
0390     }
0391 
0392     return atom;
0393 }