kwin/autotests/integration/x11keyread.cpp
David Edmundson 8fd4476ff1 wayland: Move XWayland key forwarding into a filter
We optionally send some keys to xwayland through the filter when no x11
client has focus. This allows shortcut handling in X11 apps to work.

When kwin is grabbing keys we don't necessarily want X11 to sniff these
keys as things can get out of sync. A key place is the tabbox. The X11
client sill has focus, but xwayland is not active. This means we pass
tab keys to X which then go to application incorrectly.

Part of this patch changes the tabbox filter to not intercept the alt
key release event. This ensures xwayland's concept of pressed modifiers
stays in sync.

BUG: 484992
2024-08-02 09:31:40 +00:00

375 lines
12 KiB
C++

/*
KWin - the KDE window manager
This file is part of the KDE project.
SPDX-FileCopyrightText: 2024 David Edmundson <davidedmundson@kde.org>
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 <KConfigGroup>
#include <linux/input.h>
#define explicit xcb_explicit
#include <xcb/xcb.h>
#include <xcb/xcbext.h>
#include <xcb/xinput.h>
#include <xcb/xkb.h>
#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<State, int> 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<KeyAction> recievedX11EventsForInput(const QList<KeyAction> &keyEventsIn);
};
void X11KeyReadTest::initTestCase_data()
{
QTest::addColumn<XwaylandEavesdropsMode>("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<KeyAction> keyEvents = {
{State::Press, KEY_A},
{State::Release, KEY_A},
};
auto received = recievedX11EventsForInput(keyEvents);
QList<KeyAction> 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<KeyAction> keyEvents = {
{State::Press, KEY_LEFTALT},
{State::Release, KEY_LEFTALT},
};
auto received = recievedX11EventsForInput(keyEvents);
QList<KeyAction> 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<KeyAction> keyEvents = {
{State::Press, KEY_LEFTALT},
{State::Press, KEY_F},
{State::Release, KEY_F},
{State::Release, KEY_LEFTALT},
};
auto received = recievedX11EventsForInput(keyEvents);
QList<KeyAction> 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<KWayland::Client::Surface> surface(Test::createSurface());
std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
std::unique_ptr<KWayland::Client::Keyboard> 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>(), 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>(), 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>(), 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>(), 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<KeyAction> 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<KeyAction> 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<KeyAction> keyEvents() const
{
return m_keyEvents;
}
Q_SIGNALS:
void fenceReceived();
private:
void processXcbEvents();
QList<KeyAction> 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_t>(
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<xcb_ge_generic_event_t *>(event);
if (geEvent->event_type == XCB_INPUT_KEY_PRESS || geEvent->event_type == XCB_INPUT_KEY_RELEASE) {
auto keyEvent = reinterpret_cast<xcb_input_key_press_event_t *>(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<KeyAction> X11KeyReadTest::recievedX11EventsForInput(const QList<KeyAction> &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"