/* SPDX-FileCopyrightText: 2018 Kai Uwe Broulik SPDX-License-Identifier: LGPL-2.1-or-later */ #include "menuproxy.h" #include #include "debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../c_ptr.h" #include "window.h" #include 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(KX11Extras::self(), &KX11Extras::windowAdded, this, &MenuProxy::onWindowAdded); connect(KX11Extras::self(), &KX11Extras::windowRemoved, this, &MenuProxy::onWindowRemoved); const auto windows = KX11Extras::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(KX11Extras::self(), &KX11Extras::windowAdded, this, &MenuProxy::onWindowAdded); disconnect(KX11Extras::self(), &KX11Extras::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, QStringLiteral("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; } if (KWindowSystem::isPlatformX11()) { 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 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 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 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; }