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