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