/* KWin - the KDE window manager This file is part of the KDE project. SPDX-FileCopyrightText: 2024 David Edmundson SPDX-License-Identifier: GPL-2.0-or-later */ #include "KWayland/Client/keyboard.h" #include "KWayland/Client/seat.h" #include "kwin_wayland_test.h" #include "input.h" #include "options.h" #include "pointer_input.h" #include "qabstracteventdispatcher.h" #include "qsocketnotifier.h" #include "wayland/keyboard.h" #include "wayland/seat.h" #include "wayland_server.h" #include "workspace.h" #include #include #define explicit xcb_explicit #include #include #include #include #undef explicit using namespace KWin; static const QString s_socketName = QStringLiteral("wayland_test_kwin_x11-key-read-0"); enum class State { Press, Release } state; typedef QPair KeyAction; Q_DECLARE_METATYPE(KeyAction); /* * This tests the "Legacy App Support" feature of allowing X11 apps to get notified of some key press events */ class X11KeyReadTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase_data(); void initTestCase(); void init(); void cleanup(); void testSimpleLetter(); void onlyModifier(); void letterWithModifier(); void testWaylandWindowHasFocus(); void tabBox(); private: QList recievedX11EventsForInput(const QList &keyEventsIn); }; void X11KeyReadTest::initTestCase_data() { QTest::addColumn("operatingMode"); QTest::newRow("all") << XwaylandEavesdropsMode::All; QTest::newRow("allWithModifier") << XwaylandEavesdropsMode::AllKeysWithModifier; QTest::newRow("nonCharacter") << XwaylandEavesdropsMode::NonCharacterKeys; QTest::newRow("none") << XwaylandEavesdropsMode::None; } void X11KeyReadTest::initTestCase() { QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); QVERIFY(waylandServer()->init(s_socketName)); Test::setOutputConfig({ QRect(0, 0, 1280, 1024), }); qputenv("KWIN_XKB_DEFAULT_KEYMAP", "1"); qputenv("XKB_DEFAULT_RULES", "evdev"); kwinApp()->start(); QVERIFY(applicationStartedSpy.wait()); Test::XcbConnectionPtr c = Test::createX11Connection(); QVERIFY(!xcb_connection_has_error(c.get())); } void X11KeyReadTest::init() { QFETCH_GLOBAL(XwaylandEavesdropsMode, operatingMode); options->setXwaylandEavesdrops(operatingMode); workspace()->setActiveOutput(QPoint(640, 512)); KWin::input()->pointer()->warp(QPoint(640, 512)); QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat)); QVERIFY(Test::waitForWaylandKeyboard()); } void X11KeyReadTest::cleanup() { Test::destroyWaylandConnection(); } void X11KeyReadTest::testSimpleLetter() { // press a QList keyEvents = { {State::Press, KEY_A}, {State::Release, KEY_A}, }; auto received = recievedX11EventsForInput(keyEvents); QList expected; QFETCH_GLOBAL(XwaylandEavesdropsMode, operatingMode); switch (operatingMode) { case XwaylandEavesdropsMode::None: case XwaylandEavesdropsMode::NonCharacterKeys: case XwaylandEavesdropsMode::AllKeysWithModifier: expected = {}; break; case XwaylandEavesdropsMode::All: expected = keyEvents; break; } QCOMPARE(received, expected); } void X11KeyReadTest::onlyModifier() { QList keyEvents = { {State::Press, KEY_LEFTALT}, {State::Release, KEY_LEFTALT}, }; auto received = recievedX11EventsForInput(keyEvents); QList expected; QFETCH_GLOBAL(XwaylandEavesdropsMode, operatingMode); switch (operatingMode) { case XwaylandEavesdropsMode::None: expected = {}; break; case XwaylandEavesdropsMode::NonCharacterKeys: case XwaylandEavesdropsMode::AllKeysWithModifier: case XwaylandEavesdropsMode::All: expected = keyEvents; break; } QCOMPARE(received, expected); } void X11KeyReadTest::letterWithModifier() { QList keyEvents = { {State::Press, KEY_LEFTALT}, {State::Press, KEY_F}, {State::Release, KEY_F}, {State::Release, KEY_LEFTALT}, }; auto received = recievedX11EventsForInput(keyEvents); QList expected; QFETCH_GLOBAL(XwaylandEavesdropsMode, operatingMode); switch (operatingMode) { case XwaylandEavesdropsMode::None: expected = {}; break; case XwaylandEavesdropsMode::NonCharacterKeys: expected = { {State::Press, KEY_LEFTALT}, {State::Release, KEY_LEFTALT}, }; break; case XwaylandEavesdropsMode::AllKeysWithModifier: case XwaylandEavesdropsMode::All: expected = keyEvents; break; } QCOMPARE(received, expected); } void X11KeyReadTest::testWaylandWindowHasFocus() { // A wayland window should be unaffected int timestamp = 0; std::unique_ptr surface(Test::createSurface()); std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); QSignalSpy modifierSpy(keyboard.get(), &KWayland::Client::Keyboard::modifiersChanged); QSignalSpy keyChangedSpy(keyboard.get(), &KWayland::Client::Keyboard::keyChanged); Window *waylandWindow = Test::renderAndWaitForShown(surface.get(), QSize(10, 10), Qt::blue); QVERIFY(waylandWindow->isActive()); modifierSpy.wait(); // initial modifiers modifierSpy.clear(); Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++); QVERIFY(keyChangedSpy.wait()); QCOMPARE(keyChangedSpy.last()[0], KEY_LEFTALT); QCOMPARE(keyChangedSpy.last()[1].value(), KWayland::Client::Keyboard::KeyState::Pressed); QCOMPARE(modifierSpy.count(), 1); QCOMPARE(modifierSpy.last()[0], 8); QCOMPARE(modifierSpy.last()[1], 0); QCOMPARE(modifierSpy.last()[2], 0); QCOMPARE(modifierSpy.last()[3], 0); Test::keyboardKeyPressed(KEY_G, timestamp++); QVERIFY(keyChangedSpy.wait()); QCOMPARE(keyChangedSpy.last()[0], KEY_G); QCOMPARE(keyChangedSpy.last()[1].value(), KWayland::Client::Keyboard::KeyState::Pressed); Test::keyboardKeyReleased(KEY_G, timestamp++); QVERIFY(keyChangedSpy.wait()); QCOMPARE(keyChangedSpy.last()[0], KEY_G); QCOMPARE(keyChangedSpy.last()[1].value(), KWayland::Client::Keyboard::KeyState::Released); Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++); QVERIFY(keyChangedSpy.wait()); QCOMPARE(keyChangedSpy.last()[0], KEY_LEFTALT); QCOMPARE(keyChangedSpy.last()[1].value(), KWayland::Client::Keyboard::KeyState::Released); QCOMPARE(modifierSpy.count(), 2); QCOMPARE(modifierSpy.last()[0], 0); QCOMPARE(modifierSpy.last()[1], 0); QCOMPARE(modifierSpy.last()[2], 0); QCOMPARE(modifierSpy.last()[3], 0); } void X11KeyReadTest::tabBox() { // Note this test will fail if you forget to use dbus-run-session! #if KWIN_BUILD_TABBOX QList keyEvents = { {State::Press, KEY_LEFTALT}, {State::Press, KEY_TAB}, {State::Release, KEY_TAB}, {State::Press, KEY_TAB}, {State::Release, KEY_TAB}, {State::Release, KEY_LEFTALT}, }; auto received = recievedX11EventsForInput(keyEvents); QList expected; QFETCH_GLOBAL(XwaylandEavesdropsMode, operatingMode); switch (operatingMode) { case XwaylandEavesdropsMode::None: expected = {}; break; // even though tab is a regular key whilst holding alt, the tab switcher should be grabbing case XwaylandEavesdropsMode::AllKeysWithModifier: case XwaylandEavesdropsMode::NonCharacterKeys: case XwaylandEavesdropsMode::All: expected = { {State::Press, KEY_LEFTALT}, {State::Release, KEY_LEFTALT}, }; break; } QCOMPARE(received, expected); #endif } class X11EventRecorder : public QObject { Q_OBJECT public: X11EventRecorder(xcb_connection_t * c); QList keyEvents() const { return m_keyEvents; } Q_SIGNALS: void fenceReceived(); private: void processXcbEvents(); QList m_keyEvents; xcb_connection_t *m_connection; QSocketNotifier *m_notifier; }; X11EventRecorder::X11EventRecorder(xcb_connection_t * c) : QObject() , m_connection(c) , m_notifier(new QSocketNotifier(xcb_get_file_descriptor(m_connection), QSocketNotifier::Read, this)) { struct { xcb_input_event_mask_t head; xcb_input_xi_event_mask_t mask; } mask; mask.head.deviceid = XCB_INPUT_DEVICE_ALL; mask.head.mask_len = sizeof(mask.mask) / sizeof(uint32_t); mask.mask = static_cast( XCB_INPUT_XI_EVENT_MASK_KEY_PRESS | XCB_INPUT_XI_EVENT_MASK_KEY_RELEASE | XCB_INPUT_RAW_KEY_PRESS | XCB_INPUT_RAW_KEY_RELEASE); xcb_input_xi_select_events(c, kwinApp()->x11RootWindow(), 1, &mask.head); xcb_flush(c); connect(m_notifier, &QSocketNotifier::activated, this, &X11EventRecorder::processXcbEvents); connect(QCoreApplication::eventDispatcher(), &QAbstractEventDispatcher::aboutToBlock, this, &X11EventRecorder::processXcbEvents); connect(QCoreApplication::eventDispatcher(), &QAbstractEventDispatcher::awake, this, &X11EventRecorder::processXcbEvents); } void X11EventRecorder::processXcbEvents() { xcb_generic_event_t *event; while ((event = xcb_poll_for_event(m_connection))) { u_int8_t responseType = event->response_type & ~0x80; if (responseType == XCB_GE_GENERIC) { auto *geEvent = reinterpret_cast(event); if (geEvent->event_type == XCB_INPUT_KEY_PRESS || geEvent->event_type == XCB_INPUT_KEY_RELEASE) { auto keyEvent = reinterpret_cast(geEvent); int nativeKeyCode = keyEvent->detail - 0x08; if (nativeKeyCode == 0) { Q_EMIT fenceReceived(); } else { KeyAction action({geEvent->event_type == XCB_INPUT_KEY_PRESS ? State::Press : State::Release, nativeKeyCode}); m_keyEvents << action; } } } std::free(event); } xcb_flush(m_connection); } QList X11KeyReadTest::recievedX11EventsForInput(const QList &keysIn) { quint32 timestamp = 1; Test::XcbConnectionPtr c = Test::createX11Connection(); X11EventRecorder eventReader(c.get()); QSignalSpy fenceEventSpy(&eventReader, &X11EventRecorder::fenceReceived); for (const KeyAction &action : keysIn) { if (action.first == State::Press) { Test::keyboardKeyPressed(action.second, timestamp++); } else { Test::keyboardKeyReleased(action.second, timestamp++); } Test::flushWaylandConnection(); QTest::qWait(5); } // special case, explicitly send key 0, to use as a fence ClientConnection *xwaylandClient = waylandServer()->xWaylandConnection(); waylandServer()->seat()->keyboard()->sendKey(0, KeyboardKeyState::Pressed, xwaylandClient); Test::flushWaylandConnection(); bool fenceComplete = fenceEventSpy.wait(); Q_ASSERT(fenceComplete); return eventReader.keyEvents(); } WAYLANDTEST_MAIN(X11KeyReadTest) #include "x11keyread.moc"