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(&registry, &KWayland::Client::Registry::plasmaShellAnnounced, this, [this, &registry](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"