/* SPDX-FileCopyrightText: 2024 David Redondo SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ #include "input.h" #include "kwin_wayland_test.h" #include "main.h" #include "pluginmanager.h" #include "wayland_server.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace KWin; static const QString s_socketName = QStringLiteral("wayland_test_kwin_input_capture=0"); static QString kwinInputCapturePath = QStringLiteral("/org/kde/KWin/EIS/InputCapture"); static QString kwinInputCaptureManagerInterface = QStringLiteral("org.kde.KWin.EIS.InputCaptureManager"); static QString kwinInputCaptureInterface = QStringLiteral("org.kde.KWin.EIS.InputCapture"); enum class PortalCapabilities { Keyboard = 1, Pointer = 2, Touch = 4, All = Keyboard | Pointer | Touch, }; class InputCapture { public: InputCapture() { auto msg = QDBusMessage::createMethodCall(QDBusConnection::sessionBus().baseService(), kwinInputCapturePath, kwinInputCaptureManagerInterface, QStringLiteral("addInputCapture")); msg << static_cast(PortalCapabilities::All); QDBusReply captureReply = QDBusConnection::sessionBus().call(msg); QVERIFY2(captureReply.isValid(), QTest::toString(captureReply.error())); dbusPath = captureReply.value().path(); msg = QDBusMessage::createMethodCall(QDBusConnection::sessionBus().baseService(), dbusPath, kwinInputCaptureInterface, QStringLiteral("connectToEIS")); QDBusReply eisReply = QDBusConnection::sessionBus().call(msg); QVERIFY2(eisReply.isValid(), QTest::toString(eisReply.error())); eifd = eisReply.value().takeFileDescriptor(); const QList> barriers = { {{0, 0}, {0, 1023}}, {{0, 0}, {1279, 0}}, {{1279, 0}, {1279, 1023}}, {{0, 1023}, {1279, 1023}}, }; msg = QDBusMessage::createMethodCall(QDBusConnection::sessionBus().baseService(), dbusPath, kwinInputCaptureInterface, QStringLiteral("enable")); msg << QVariant::fromValue(barriers); QDBusReply enableReply = QDBusConnection::sessionBus().call(msg); QVERIFY2(enableReply.isValid(), QTest::toString(enableReply.error())); } bool setupSuccessfully() const { return eifd != 1 && !dbusPath.isEmpty(); } int eifd = -1; QString dbusPath; }; class TestInputCapture : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testInputCapture_data(); void testInputCapture(); void disconnectingEiRemovesCapture(); }; void TestInputCapture::init() { QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat)); } void TestInputCapture::cleanup() { if (Test::waylandConnection()) { Test::destroyWaylandConnection(); } } void TestInputCapture::initTestCase() { QVERIFY(waylandServer()->init(s_socketName)); Test::setOutputConfig({QRect(0, 0, 1280, 1024)}); kwinApp()->start(); QVERIFY(kwinApp()->pluginManager()->loadedPlugins().contains("eis")); } void TestInputCapture::testInputCapture_data() { QTest::addColumn("mousePos"); QTest::addColumn("delta"); QTest::addRow("left") << QPoint(0, 512) << QPointF(-1, 0); QTest::addRow("up") << QPoint(640, 0) << QPointF(0, -1); QTest::addRow("right") << QPoint(1279, 512) << QPointF(1, 0); QTest::addRow("down") << QPoint(640, 1023) << QPointF(0, 1); } void TestInputCapture::testInputCapture() { QFETCH(QPoint, mousePos); QFETCH(QPointF, delta); int timestamp = 0; QVERIFY(Test::waitForWaylandPointer()); std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); std::unique_ptr pointer(Test::waylandSeat()->createPointer()); auto surface = Test::createSurface(); auto shellSurface = Test::createXdgToplevelSurface(surface.get()); Test::renderAndWaitForShown(surface.get(), {1280, 1024}, Qt::cyan); InputCapture capture; if (!capture.setupSuccessfully()) { return; } Test::pointerMotion(mousePos, ++timestamp); Test::waylandSync(); QCOMPARE(keyboard->enteredSurface(), surface.get()); QCOMPARE(pointer->enteredSurface(), surface.get()); QSignalSpy motionSpy(pointer.get(), &KWayland::Client::Pointer::motion); QSignalSpy buttonSpy(pointer.get(), &KWayland::Client::Pointer::buttonStateChanged); QSignalSpy axisSpy(pointer.get(), &KWayland::Client::Pointer::axisChanged); QSignalSpy keySpy(keyboard.get(), &KWayland::Client::Keyboard::keyChanged); QVERIFY(motionSpy.isValid()); QVERIFY(buttonSpy.isValid()); QVERIFY(axisSpy.isValid()); QVERIFY(keySpy.isValid()); auto ei = ei_new_receiver(nullptr); ei_setup_backend_fd(ei, capture.eifd); QSocketNotifier eiNotifier(ei_get_fd(ei), QSocketNotifier::Read); QSignalSpy eiReadableSpy(&eiNotifier, &QSocketNotifier::activated); int numDevices = 0; while (numDevices != 3 && eiReadableSpy.wait()) { ei_dispatch(ei); while (auto event = ei_get_event(ei)) { switch (const auto type = ei_event_get_type(event)) { case EI_EVENT_CONNECT: #if HAVE_EI_EVENT_SYNC case EI_EVENT_SYNC: #endif break; case EI_EVENT_SEAT_ADDED: ei_seat_bind_capabilities(ei_event_get_seat(event), EI_DEVICE_CAP_POINTER, EI_DEVICE_CAP_POINTER_ABSOLUTE, EI_DEVICE_CAP_KEYBOARD, EI_DEVICE_CAP_TOUCH, EI_DEVICE_CAP_SCROLL, EI_DEVICE_CAP_BUTTON, nullptr); case EI_EVENT_DEVICE_ADDED: break; case EI_EVENT_DEVICE_RESUMED: ++numDevices; break; default: qFatal() << "unexpected event:" << type; } ei_event_unref(event); } } { Test::pointerMotionRelative(delta, ++timestamp); Test::waylandSync(); QVERIFY(motionSpy.empty()); ei_dispatch(ei); for (int i = 0; i < numDevices; ++i) { auto event = ei_get_event(ei); QCOMPARE(ei_event_get_type(event), EI_EVENT_DEVICE_START_EMULATING); ei_event_unref(event); } auto event = ei_get_event(ei); QCOMPARE(ei_event_get_type(event), EI_EVENT_POINTER_MOTION); QCOMPARE(ei_event_pointer_get_dx(event), delta.x()); QCOMPARE(ei_event_pointer_get_dy(event), delta.y()); ei_event_unref(event); event = ei_get_event(ei); QCOMPARE(ei_event_get_type(event), EI_EVENT_FRAME); ei_event_unref(event); } { Test::pointerButtonPressed(BTN_LEFT, ++timestamp); Test::pointerButtonReleased(BTN_LEFT, ++timestamp); Test::waylandSync(); QVERIFY(buttonSpy.empty()); ei_dispatch(ei); auto event = ei_get_event(ei); QCOMPARE(ei_event_get_type(event), EI_EVENT_BUTTON_BUTTON); QCOMPARE(ei_event_button_get_button(event), BTN_LEFT); QCOMPARE(ei_event_button_get_is_press(event), true); ei_event_unref(event); event = ei_get_event(ei); QCOMPARE(ei_event_get_type(event), EI_EVENT_FRAME); ei_event_unref(event); event = ei_get_event(ei); QCOMPARE(ei_event_get_type(event), EI_EVENT_BUTTON_BUTTON); QCOMPARE(ei_event_button_get_button(event), BTN_LEFT); QCOMPARE(ei_event_button_get_is_press(event), false); ei_event_unref(event); event = ei_get_event(ei); QCOMPARE(ei_event_get_type(event), EI_EVENT_FRAME); ei_event_unref(event); event = ei_get_event(ei); } { Test::pointerAxisHorizontal(1, ++timestamp); Test::waylandSync(); QVERIFY(axisSpy.empty()); ei_dispatch(ei); auto event = ei_get_event(ei); QCOMPARE(ei_event_get_type(event), EI_EVENT_SCROLL_DELTA); QCOMPARE(ei_event_scroll_get_dx(event), 1); QCOMPARE(ei_event_scroll_get_dy(event), 0); ei_event_unref(event); event = ei_get_event(ei); QCOMPARE(ei_event_get_type(event), EI_EVENT_FRAME); ei_event_unref(event); } { Test::keyboardKeyPressed(KEY_A, ++timestamp); Test::keyboardKeyReleased(KEY_A, ++timestamp); Test::waylandSync(); QVERIFY(buttonSpy.empty()); ei_dispatch(ei); auto event = ei_get_event(ei); QCOMPARE(ei_event_get_type(event), EI_EVENT_KEYBOARD_KEY); QCOMPARE(ei_event_keyboard_get_key(event), KEY_A); QCOMPARE(ei_event_keyboard_get_key_is_press(event), true); ei_event_unref(event); event = ei_get_event(ei); QCOMPARE(ei_event_get_type(event), EI_EVENT_FRAME); ei_event_unref(event); event = ei_get_event(ei); QCOMPARE(ei_event_get_type(event), EI_EVENT_KEYBOARD_KEY); QCOMPARE(ei_event_keyboard_get_key(event), KEY_A); QCOMPARE(ei_event_keyboard_get_key_is_press(event), false); ei_event_unref(event); event = ei_get_event(ei); QCOMPARE(ei_event_get_type(event), EI_EVENT_FRAME); ei_event_unref(event); } auto msg = QDBusMessage::createMethodCall(QDBusConnection::sessionBus().baseService(), capture.dbusPath, kwinInputCaptureInterface, QStringLiteral("release")); msg << QVariant::fromValue(QPointF(1, 1)) << true; QDBusReply releaseReply = QDBusConnection::sessionBus().call(msg); QVERIFY2(releaseReply.isValid(), QTest::toString(releaseReply.error())); QCOMPARE(input()->globalPointer(), QPoint(1, 1)); QVERIFY(eiReadableSpy.wait()); ei_dispatch(ei); eiReadableSpy.clear(); Test::pointerMotion({2, 2}, ++timestamp); Test::pointerButtonPressed(BTN_LEFT, ++timestamp); Test::pointerButtonReleased(BTN_LEFT, ++timestamp); Test::pointerAxisHorizontal(1, ++timestamp); Test::keyboardKeyPressed(KEY_A, ++timestamp); Test::keyboardKeyReleased(KEY_A, ++timestamp); QVERIFY(motionSpy.wait()); QVERIFY(buttonSpy.count()); QVERIFY(axisSpy.count()); QVERIFY(keySpy.count()); QVERIFY(eiReadableSpy.empty()); msg = QDBusMessage::createMethodCall(QDBusConnection::sessionBus().baseService(), kwinInputCapturePath, kwinInputCaptureManagerInterface, QStringLiteral("removeInputCapture")); msg << QDBusObjectPath(capture.dbusPath); QDBusReply removeReply = QDBusConnection::sessionBus().call(msg); QVERIFY2(removeReply.isValid(), QTest::toString(removeReply.error())); ei_unref(ei); } void TestInputCapture::disconnectingEiRemovesCapture() { InputCapture capture; if (!capture.setupSuccessfully()) { return; } auto ei = ei_new_receiver(nullptr); ei_setup_backend_fd(ei, capture.eifd); QSocketNotifier eiNotifier(ei_get_fd(ei), QSocketNotifier::Read); QSignalSpy eiReadableSpy(&eiNotifier, &QSocketNotifier::activated); bool connected = false; while (!connected && eiReadableSpy.wait()) { ei_dispatch(ei); if (auto event = ei_get_event(ei)) { if (ei_event_get_type(event) == EI_EVENT_CONNECT) { connected = true; } ei_event_unref(event); } } ei_unref(ei); QTRY_COMPARE(QDBusConnection::sessionBus().objectRegisteredAt(capture.dbusPath), nullptr); } WAYLANDTEST_MAIN(TestInputCapture) #include "input_capture_test.moc"