File indexing completed on 2024-05-12 05:37:25
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 <KWindowInfo> 0026 #include <KWindowSystem> 0027 #include <KX11Extras> 0028 0029 #include <private/qtx11extras_p.h> 0030 0031 #include <xcb/xcb.h> 0032 0033 #include "../c_ptr.h" 0034 #include "window.h" 0035 #include <chrono> 0036 0037 using namespace std::chrono_literals; 0038 0039 static const QString s_ourServiceName = QStringLiteral("org.kde.plasma.gmenu_dbusmenu_proxy"); 0040 0041 static const QString s_dbusMenuRegistrar = QStringLiteral("com.canonical.AppMenu.Registrar"); 0042 0043 static const QByteArray s_gtkUniqueBusName = QByteArrayLiteral("_GTK_UNIQUE_BUS_NAME"); 0044 0045 static const QByteArray s_gtkApplicationObjectPath = QByteArrayLiteral("_GTK_APPLICATION_OBJECT_PATH"); 0046 static const QByteArray s_unityObjectPath = QByteArrayLiteral("_UNITY_OBJECT_PATH"); 0047 static const QByteArray s_gtkWindowObjectPath = QByteArrayLiteral("_GTK_WINDOW_OBJECT_PATH"); 0048 static const QByteArray s_gtkMenuBarObjectPath = QByteArrayLiteral("_GTK_MENUBAR_OBJECT_PATH"); 0049 // that's the generic app menu with Help and Options and will be used if window doesn't have a fully-blown menu bar 0050 static const QByteArray s_gtkAppMenuObjectPath = QByteArrayLiteral("_GTK_APP_MENU_OBJECT_PATH"); 0051 0052 static const QByteArray s_kdeNetWmAppMenuServiceName = QByteArrayLiteral("_KDE_NET_WM_APPMENU_SERVICE_NAME"); 0053 static const QByteArray s_kdeNetWmAppMenuObjectPath = QByteArrayLiteral("_KDE_NET_WM_APPMENU_OBJECT_PATH"); 0054 0055 static const QString s_gtkModules = QStringLiteral("gtk-modules"); 0056 static const QString s_appMenuGtkModule = QStringLiteral("appmenu-gtk-module"); 0057 0058 MenuProxy::MenuProxy() 0059 : QObject() 0060 , m_xConnection(QX11Info::connection()) 0061 , m_serviceWatcher(new QDBusServiceWatcher(this)) 0062 , m_gtk2RcWatch(new KDirWatch(this)) 0063 , m_writeGtk2SettingsTimer(new QTimer(this)) 0064 { 0065 m_serviceWatcher->setConnection(QDBusConnection::sessionBus()); 0066 m_serviceWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration | QDBusServiceWatcher::WatchForRegistration); 0067 m_serviceWatcher->addWatchedService(s_dbusMenuRegistrar); 0068 0069 connect(m_serviceWatcher, &QDBusServiceWatcher::serviceRegistered, this, [this](const QString &service) { 0070 Q_UNUSED(service); 0071 qCDebug(DBUSMENUPROXY) << "Global menu service became available, starting"; 0072 init(); 0073 }); 0074 connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this](const QString &service) { 0075 Q_UNUSED(service); 0076 qCDebug(DBUSMENUPROXY) << "Global menu service disappeared, cleaning up"; 0077 teardown(); 0078 }); 0079 0080 // It's fine to do a blocking call here as we're a separate binary with no UI 0081 if (QDBusConnection::sessionBus().interface()->isServiceRegistered(s_dbusMenuRegistrar)) { 0082 qCDebug(DBUSMENUPROXY) << "Global menu service is running, starting right away"; 0083 init(); 0084 } else { 0085 qCDebug(DBUSMENUPROXY) << "No global menu service available, waiting for it to start before doing anything"; 0086 0087 // be sure when started to restore gtk menus when there's no dbus menu around in case we crashed 0088 enableGtkSettings(false); 0089 } 0090 0091 // kde-gtk-config just deletes and re-creates the gtkrc-2.0, watch this and add our config to it again 0092 m_writeGtk2SettingsTimer->setSingleShot(true); 0093 m_writeGtk2SettingsTimer->setInterval(1s); 0094 connect(m_writeGtk2SettingsTimer, &QTimer::timeout, this, &MenuProxy::writeGtk2Settings); 0095 0096 auto startGtk2SettingsTimer = [this] { 0097 if (!m_writeGtk2SettingsTimer->isActive()) { 0098 m_writeGtk2SettingsTimer->start(); 0099 } 0100 }; 0101 0102 connect(m_gtk2RcWatch, &KDirWatch::created, this, startGtk2SettingsTimer); 0103 connect(m_gtk2RcWatch, &KDirWatch::dirty, this, startGtk2SettingsTimer); 0104 m_gtk2RcWatch->addFile(gtkRc2Path()); 0105 } 0106 0107 MenuProxy::~MenuProxy() 0108 { 0109 teardown(); 0110 } 0111 0112 bool MenuProxy::init() 0113 { 0114 if (!QDBusConnection::sessionBus().registerService(s_ourServiceName)) { 0115 qCWarning(DBUSMENUPROXY) << "Failed to register DBus service" << s_ourServiceName; 0116 return false; 0117 } 0118 0119 enableGtkSettings(true); 0120 0121 connect(KX11Extras::self(), &KX11Extras::windowAdded, this, &MenuProxy::onWindowAdded); 0122 connect(KX11Extras::self(), &KX11Extras::windowRemoved, this, &MenuProxy::onWindowRemoved); 0123 0124 const auto windows = KX11Extras::windows(); 0125 for (WId id : windows) { 0126 onWindowAdded(id); 0127 } 0128 0129 if (m_windows.isEmpty()) { 0130 qCDebug(DBUSMENUPROXY) << "Up and running but no windows with menus in sight"; 0131 } 0132 0133 return true; 0134 } 0135 0136 void MenuProxy::teardown() 0137 { 0138 enableGtkSettings(false); 0139 0140 QDBusConnection::sessionBus().unregisterService(s_ourServiceName); 0141 0142 disconnect(KX11Extras::self(), &KX11Extras::windowAdded, this, &MenuProxy::onWindowAdded); 0143 disconnect(KX11Extras::self(), &KX11Extras::windowRemoved, this, &MenuProxy::onWindowRemoved); 0144 0145 qDeleteAll(m_windows); 0146 m_windows.clear(); 0147 } 0148 0149 void MenuProxy::enableGtkSettings(bool enable) 0150 { 0151 m_enabled = enable; 0152 0153 writeGtk2Settings(); 0154 writeGtk3Settings(); 0155 0156 // TODO use gconf/dconf directly or at least signal a change somehow? 0157 } 0158 0159 QString MenuProxy::gtkRc2Path() 0160 { 0161 return QDir::homePath() + QLatin1String("/.gtkrc-2.0"); 0162 } 0163 0164 QString MenuProxy::gtk3SettingsIniPath() 0165 { 0166 return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1String("/gtk-3.0/settings.ini"); 0167 } 0168 0169 void MenuProxy::writeGtk2Settings() 0170 { 0171 QFile rcFile(gtkRc2Path()); 0172 if (!rcFile.exists()) { 0173 // Don't create it here, that would break writing default GTK-2.0 settings on first login, 0174 // as the gtkbreeze kconf_update script only does so if it does not exist 0175 return; 0176 } 0177 0178 qCDebug(DBUSMENUPROXY) << "Writing gtkrc-2.0 to" << (m_enabled ? "enable" : "disable") << "global menu support"; 0179 0180 if (!rcFile.open(QIODevice::ReadWrite | QIODevice::Text)) { 0181 return; 0182 } 0183 0184 QByteArray content; 0185 0186 QStringList gtkModules; 0187 0188 while (!rcFile.atEnd()) { 0189 const QByteArray rawLine = rcFile.readLine(); 0190 0191 const QString line = QString::fromUtf8(rawLine.trimmed()); 0192 0193 if (!line.startsWith(s_gtkModules)) { 0194 // keep line as-is 0195 content += rawLine; 0196 continue; 0197 } 0198 0199 const int equalSignIdx = line.indexOf(QLatin1Char('=')); 0200 if (equalSignIdx < 1) { 0201 continue; 0202 } 0203 0204 gtkModules = line.mid(equalSignIdx + 1).split(QLatin1Char(':'), Qt::SkipEmptyParts); 0205 0206 break; 0207 } 0208 0209 addOrRemoveAppMenuGtkModule(gtkModules); 0210 0211 if (!gtkModules.isEmpty()) { 0212 content += QStringLiteral("%1=%2").arg(s_gtkModules, gtkModules.join(QLatin1Char(':'))).toUtf8(); 0213 } 0214 0215 qCDebug(DBUSMENUPROXY) << " gtk-modules:" << gtkModules; 0216 0217 m_gtk2RcWatch->stopScan(); 0218 0219 // now write the new contents of the file 0220 rcFile.resize(0); 0221 rcFile.write(content); 0222 rcFile.close(); 0223 0224 m_gtk2RcWatch->startScan(); 0225 } 0226 0227 void MenuProxy::writeGtk3Settings() 0228 { 0229 qCDebug(DBUSMENUPROXY) << "Writing gtk-3.0/settings.ini" << (m_enabled ? "enable" : "disable") << "global menu support"; 0230 0231 // mostly taken from kde-gtk-config 0232 auto cfg = KSharedConfig::openConfig(gtk3SettingsIniPath(), KConfig::NoGlobals); 0233 KConfigGroup group(cfg, QStringLiteral("Settings")); 0234 0235 QStringList gtkModules = group.readEntry("gtk-modules", QString()).split(QLatin1Char(':'), Qt::SkipEmptyParts); 0236 addOrRemoveAppMenuGtkModule(gtkModules); 0237 0238 if (!gtkModules.isEmpty()) { 0239 group.writeEntry("gtk-modules", gtkModules.join(QLatin1Char(':'))); 0240 } else { 0241 group.deleteEntry("gtk-modules"); 0242 } 0243 0244 qCDebug(DBUSMENUPROXY) << " gtk-modules:" << gtkModules; 0245 0246 if (m_enabled) { 0247 group.writeEntry("gtk-shell-shows-menubar", 1); 0248 } else { 0249 group.deleteEntry("gtk-shell-shows-menubar"); 0250 } 0251 0252 qCDebug(DBUSMENUPROXY) << " gtk-shell-shows-menubar:" << (m_enabled ? 1 : 0); 0253 0254 group.sync(); 0255 } 0256 0257 void MenuProxy::addOrRemoveAppMenuGtkModule(QStringList &list) 0258 { 0259 if (m_enabled && !list.contains(s_appMenuGtkModule)) { 0260 list.append(s_appMenuGtkModule); 0261 } else if (!m_enabled) { 0262 list.removeAll(s_appMenuGtkModule); 0263 } 0264 } 0265 0266 void MenuProxy::onWindowAdded(WId id) 0267 { 0268 if (m_windows.contains(id)) { 0269 return; 0270 } 0271 0272 if (KWindowSystem::isPlatformX11()) { 0273 KWindowInfo info(id, NET::WMWindowType); 0274 0275 NET::WindowType wType = info.windowType(NET::NormalMask | NET::DesktopMask | NET::DockMask | NET::ToolbarMask | NET::MenuMask | NET::DialogMask 0276 | NET::OverrideMask | NET::TopMenuMask | NET::UtilityMask | NET::SplashMask); 0277 0278 // Only top level windows typically have a menu bar, dialogs, such as settings don't 0279 if (wType != NET::Normal) { 0280 qCDebug(DBUSMENUPROXY) << "Ignoring window" << id << "of type" << wType; 0281 return; 0282 } 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 }