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 }