File indexing completed on 2024-05-05 17:43:31
0001 /* 0002 SPDX-FileCopyrightText: 2011 Lionel Chauvin <megabigbug@yahoo.fr> 0003 SPDX-FileCopyrightText: 2011, 2012 Cédric Bellegarde <gnumdk@gmail.com> 0004 SPDX-FileCopyrightText: 2016 Kai Uwe Broulik <kde@privat.broulik.de> 0005 0006 SPDX-License-Identifier: MIT 0007 */ 0008 0009 #include "appmenu.h" 0010 #include "../c_ptr.h" 0011 #include "appmenu_dbus.h" 0012 #include "appmenu_debug.h" 0013 #include "appmenuadaptor.h" 0014 #include "kdbusimporter.h" 0015 #include "menuimporteradaptor.h" 0016 #include "verticalmenu.h" 0017 0018 #include <QApplication> 0019 #include <QDBusInterface> 0020 #include <QMenu> 0021 #include <private/qwaylanddisplay_p.h> 0022 #include <private/qwaylandinputdevice_p.h> 0023 #include <private/qwaylandwindow_p.h> 0024 0025 #include <KWayland/Client/connection_thread.h> 0026 #include <KWayland/Client/plasmashell.h> 0027 #include <KWayland/Client/registry.h> 0028 #include <KWayland/Client/surface.h> 0029 #include <kpluginfactory.h> 0030 0031 #if HAVE_X11 0032 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 0033 #include <private/qtx11extras_p.h> 0034 #else 0035 #include <QX11Info> 0036 #endif 0037 #endif 0038 0039 static const QByteArray s_x11AppMenuServiceNamePropertyName = QByteArrayLiteral("_KDE_NET_WM_APPMENU_SERVICE_NAME"); 0040 static const QByteArray s_x11AppMenuObjectPathPropertyName = QByteArrayLiteral("_KDE_NET_WM_APPMENU_OBJECT_PATH"); 0041 0042 K_PLUGIN_FACTORY_WITH_JSON(AppMenuFactory, "appmenu.json", registerPlugin<AppMenuModule>();) 0043 0044 AppMenuModule::AppMenuModule(QObject *parent, const QList<QVariant> &) 0045 : KDEDModule(parent) 0046 , m_appmenuDBus(new AppmenuDBus(this)) 0047 { 0048 reconfigure(); 0049 0050 m_appmenuDBus->connectToBus(); 0051 0052 connect(m_appmenuDBus, &AppmenuDBus::appShowMenu, this, &AppMenuModule::slotShowMenu); 0053 connect(m_appmenuDBus, &AppmenuDBus::reconfigured, this, &AppMenuModule::reconfigure); 0054 0055 // transfer our signals to dbus 0056 connect(this, &AppMenuModule::showRequest, m_appmenuDBus, &AppmenuDBus::showRequest); 0057 connect(this, &AppMenuModule::menuHidden, m_appmenuDBus, &AppmenuDBus::menuHidden); 0058 connect(this, &AppMenuModule::menuShown, m_appmenuDBus, &AppmenuDBus::menuShown); 0059 0060 m_menuViewWatcher = new QDBusServiceWatcher(QStringLiteral("org.kde.kappmenuview"), 0061 QDBusConnection::sessionBus(), 0062 QDBusServiceWatcher::WatchForRegistration | QDBusServiceWatcher::WatchForUnregistration, 0063 this); 0064 0065 auto setupMenuImporter = [this]() { 0066 QDBusConnection::sessionBus().connect({}, 0067 {}, 0068 QStringLiteral("com.canonical.dbusmenu"), 0069 QStringLiteral("ItemActivationRequested"), 0070 this, 0071 SLOT(itemActivationRequested(int, uint))); 0072 0073 // Setup a menu importer if needed 0074 if (!m_menuImporter) { 0075 m_menuImporter = new MenuImporter(this); 0076 connect(m_menuImporter, &MenuImporter::WindowRegistered, this, &AppMenuModule::slotWindowRegistered); 0077 m_menuImporter->connectToBus(); 0078 } 0079 }; 0080 connect(m_menuViewWatcher, &QDBusServiceWatcher::serviceRegistered, this, setupMenuImporter); 0081 connect(m_menuViewWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this](const QString &service) { 0082 Q_UNUSED(service) 0083 QDBusConnection::sessionBus().disconnect({}, 0084 {}, 0085 QStringLiteral("com.canonical.dbusmenu"), 0086 QStringLiteral("ItemActivationRequested"), 0087 this, 0088 SLOT(itemActivationRequested(int, uint))); 0089 delete m_menuImporter; 0090 m_menuImporter = nullptr; 0091 }); 0092 0093 if (QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.kappmenuview"))) { 0094 setupMenuImporter(); 0095 } 0096 0097 #if HAVE_X11 0098 if (!QX11Info::connection()) { 0099 m_xcbConn = xcb_connect(nullptr, nullptr); 0100 } 0101 #endif 0102 if (qGuiApp->platformName() == QLatin1String("wayland")) { 0103 auto connection = KWayland::Client::ConnectionThread::fromApplication(); 0104 KWayland::Client::Registry registry; 0105 registry.create(connection); 0106 connect(®istry, &KWayland::Client::Registry::plasmaShellAnnounced, this, [this, ®istry](quint32 name, quint32 version) { 0107 m_plasmashell = registry.createPlasmaShell(name, version, this); 0108 }); 0109 registry.setup(); 0110 connection->roundtrip(); 0111 } 0112 } 0113 0114 AppMenuModule::~AppMenuModule() 0115 { 0116 #if HAVE_X11 0117 if (m_xcbConn) { 0118 xcb_disconnect(m_xcbConn); 0119 } 0120 #endif 0121 } 0122 0123 void AppMenuModule::slotWindowRegistered(WId id, const QString &serviceName, const QDBusObjectPath &menuObjectPath) 0124 { 0125 #if HAVE_X11 0126 auto *c = QX11Info::connection(); 0127 if (!c) { 0128 c = m_xcbConn; 0129 } 0130 0131 if (c) { 0132 static xcb_atom_t s_serviceNameAtom = XCB_ATOM_NONE; 0133 static xcb_atom_t s_objectPathAtom = XCB_ATOM_NONE; 0134 0135 auto setWindowProperty = [c](WId id, xcb_atom_t &atom, const QByteArray &name, const QByteArray &value) { 0136 if (atom == XCB_ATOM_NONE) { 0137 const xcb_intern_atom_cookie_t cookie = xcb_intern_atom(c, false, name.length(), name.constData()); 0138 UniqueCPointer<xcb_intern_atom_reply_t> reply{xcb_intern_atom_reply(c, cookie, nullptr)}; 0139 if (!reply) { 0140 return; 0141 } 0142 atom = reply->atom; 0143 if (atom == XCB_ATOM_NONE) { 0144 return; 0145 } 0146 } 0147 0148 auto cookie = xcb_change_property_checked(c, XCB_PROP_MODE_REPLACE, id, atom, XCB_ATOM_STRING, 8, value.length(), value.constData()); 0149 xcb_generic_error_t *error; 0150 if ((error = xcb_request_check(c, cookie))) { 0151 qCWarning(APPMENU_DEBUG) << "Got an error"; 0152 free(error); 0153 return; 0154 } 0155 }; 0156 0157 // TODO only set the property if it doesn't already exist 0158 0159 setWindowProperty(id, s_serviceNameAtom, s_x11AppMenuServiceNamePropertyName, serviceName.toUtf8()); 0160 setWindowProperty(id, s_objectPathAtom, s_x11AppMenuObjectPathPropertyName, menuObjectPath.path().toUtf8()); 0161 } 0162 #endif 0163 } 0164 0165 void AppMenuModule::slotShowMenu(int x, int y, const QString &serviceName, const QDBusObjectPath &menuObjectPath, int actionId) 0166 { 0167 if (!m_menuImporter) { 0168 return; 0169 } 0170 0171 // If menu visible, hide it 0172 if (m_menu && m_menu.data()->isVisible()) { 0173 m_menu.data()->hide(); 0174 return; 0175 } 0176 0177 // dbus call by user (for khotkey shortcut) 0178 if (x == -1 || y == -1) { 0179 // We do not know kwin button position, so tell kwin to show menu 0180 Q_EMIT showRequest(serviceName, menuObjectPath, actionId); 0181 return; 0182 } 0183 0184 auto *importer = new KDBusMenuImporter(serviceName, menuObjectPath.path(), this); 0185 QMetaObject::invokeMethod(importer, "updateMenu", Qt::QueuedConnection); 0186 disconnect(importer, nullptr, this, nullptr); // ensure we don't popup multiple times in case the menu updates again later 0187 0188 connect(importer, &KDBusMenuImporter::menuUpdated, this, [=](QMenu *m) { 0189 QMenu *menu = importer->menu(); 0190 if (!menu || menu != m) { 0191 return; 0192 } 0193 m_menu = qobject_cast<VerticalMenu *>(menu); 0194 0195 m_menu.data()->setServiceName(serviceName); 0196 m_menu.data()->setMenuObjectPath(menuObjectPath); 0197 0198 connect(m_menu.data(), &QMenu::aboutToHide, this, [this, importer] { 0199 hideMenu(); 0200 importer->deleteLater(); 0201 }); 0202 0203 if (m_plasmashell) { 0204 connect(m_menu.data(), &QMenu::aboutToShow, this, &AppMenuModule::initMenuWayland, Qt::UniqueConnection); 0205 m_menu.data()->popup(QPoint(x, y)); 0206 } else { 0207 m_menu.data()->popup(QPoint(x, y) / qApp->devicePixelRatio()); 0208 } 0209 0210 QAction *actiontoActivate = importer->actionForId(actionId); 0211 0212 Q_EMIT menuShown(serviceName, menuObjectPath); 0213 0214 if (actiontoActivate) { 0215 m_menu.data()->setActiveAction(actiontoActivate); 0216 } 0217 }); 0218 } 0219 0220 void AppMenuModule::hideMenu() 0221 { 0222 if (m_menu) { 0223 Q_EMIT menuHidden(m_menu.data()->serviceName(), m_menu->menuObjectPath()); 0224 } 0225 } 0226 0227 void AppMenuModule::itemActivationRequested(int actionId, uint timeStamp) 0228 { 0229 Q_UNUSED(timeStamp); 0230 Q_EMIT showRequest(message().service(), QDBusObjectPath(message().path()), actionId); 0231 } 0232 0233 // this method is not really used anymore but has to be kept for DBus compatibility 0234 void AppMenuModule::reconfigure() 0235 { 0236 } 0237 0238 void AppMenuModule::initMenuWayland() 0239 { 0240 auto window = m_menu->windowHandle(); 0241 if (window && m_plasmashell) { 0242 window->setFlag(Qt::FramelessWindowHint); 0243 window->requestActivate(); 0244 auto plasmaSurface = m_plasmashell->createSurface(KWayland::Client::Surface::fromWindow(window), m_menu.data()); 0245 plasmaSurface->setPosition(window->position()); 0246 plasmaSurface->setSkipSwitcher(true); 0247 plasmaSurface->setSkipTaskbar(true); 0248 m_menu->installEventFilter(this); 0249 } 0250 } 0251 0252 bool AppMenuModule::eventFilter(QObject *object, QEvent *event) 0253 { 0254 // HACK we need an input serial to create popups but Qt only sets them on click 0255 if (object == m_menu && event->type() == QEvent::Enter && m_plasmashell) { 0256 auto waylandWindow = dynamic_cast<QtWaylandClient::QWaylandWindow *>(m_menu->windowHandle()->handle()); 0257 if (waylandWindow) { 0258 const auto device = waylandWindow->display()->currentInputDevice(); 0259 waylandWindow->display()->setLastInputDevice(device, device->pointer()->mEnterSerial, waylandWindow); 0260 } 0261 } 0262 return KDEDModule::eventFilter(object, event); 0263 } 0264 0265 #include "appmenu.moc"