/* KWin - the KDE window manager This file is part of the KDE project. SPDX-FileCopyrightText: 2024 Aleix Pol Gonzalez SPDX-License-Identifier: GPL-2.0-or-later */ #include "kwin_wayland_test.h" #include "pointer_input.h" #include "tablet_input.h" #include "wayland_server.h" #include "workspace.h" #include #include #include #include using namespace KWin; static const QString s_socketName = QStringLiteral("wayland_test_kwin_buttonrebind-0"); class TestButtonRebind : public QObject { Q_OBJECT private Q_SLOTS: void init(); void cleanup(); void initTestCase(); void testKey_data(); void testKey(); void testMouse_data(); void testMouse(); void testMouseKeyboardMod_data(); void testMouseKeyboardMod(); void testDisabled(); // NOTE: Mouse buttons are not tested because those are used in the other tests void testBindingTabletPad(); void testBindingTabletTool(); void testMouseTabletCursorSync(); private: quint32 timestamp = 1; }; void TestButtonRebind::init() { QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat)); QVERIFY(Test::waitForWaylandPointer()); } void TestButtonRebind::cleanup() { Test::destroyWaylandConnection(); QVERIFY(QFile::remove(QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("kcminputrc")))); } void TestButtonRebind::initTestCase() { qRegisterMetaType(); QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); QVERIFY(waylandServer()->init(s_socketName)); Test::setOutputConfig({ QRect(0, 0, 1280, 1024), QRect(1280, 0, 1280, 1024), }); kwinApp()->start(); QVERIFY(applicationStartedSpy.wait()); } void TestButtonRebind::testKey_data() { QTest::addColumn("boundKeys"); QTest::addColumn>("expectedKeys"); QTest::newRow("single key") << QKeySequence(Qt::Key_A) << QList{KEY_A}; QTest::newRow("single modifier") << QKeySequence(Qt::Key_Control) << QList{KEY_LEFTCTRL}; QTest::newRow("single modifier plus key") << QKeySequence(Qt::ControlModifier | Qt::Key_N) << QList{KEY_LEFTCTRL, KEY_N}; QTest::newRow("multiple modifiers plus key") << QKeySequence(Qt::ControlModifier | Qt::MetaModifier | Qt::Key_Y) << QList{KEY_LEFTCTRL, KEY_LEFTMETA, KEY_Y}; QTest::newRow("delete") << QKeySequence(Qt::Key_Delete) << QList{KEY_DELETE}; QTest::newRow("keypad delete") << QKeySequence(Qt::KeypadModifier | Qt::Key_Delete) << QList{KEY_KPDOT}; QTest::newRow("keypad enter") << QKeySequence(Qt::KeypadModifier | Qt::Key_Enter) << QList{KEY_KPENTER}; QTest::newRow("exclamation mark") << QKeySequence(Qt::Key_Exclam) << QList{KEY_LEFTSHIFT, KEY_1}; } void TestButtonRebind::testKey() { KConfigGroup buttonGroup = KSharedConfig::openConfig(QStringLiteral("kcminputrc"))->group(QStringLiteral("ButtonRebinds")).group(QStringLiteral("Mouse")); QFETCH(QKeySequence, boundKeys); buttonGroup.writeEntry("ExtraButton7", QStringList{"Key", boundKeys.toString(QKeySequence::PortableText)}, KConfig::Notify); buttonGroup.sync(); std::unique_ptr surface = Test::createSurface(); std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); QSignalSpy enteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered); QSignalSpy keyChangedSpy(keyboard.get(), &KWayland::Client::Keyboard::keyChanged); QVERIFY(enteredSpy.wait()); // 0x119 is Qt::ExtraButton7 Test::pointerButtonPressed(0x119, timestamp++); QVERIFY(keyChangedSpy.wait()); QFETCH(QList, expectedKeys); QCOMPARE(keyChangedSpy.count(), expectedKeys.count()); for (int i = 0; i < keyChangedSpy.count(); i++) { QCOMPARE(keyChangedSpy.at(i).at(0).value(), expectedKeys.at(i)); QCOMPARE(keyChangedSpy.at(i).at(1).value(), KWayland::Client::Keyboard::KeyState::Pressed); } Test::pointerButtonReleased(0x119, timestamp++); } void TestButtonRebind::testMouse_data() { QTest::addColumn("mouseButton"); QTest::newRow("left button") << BTN_LEFT; QTest::newRow("middle button") << BTN_MIDDLE; QTest::newRow("right button") << BTN_RIGHT; } void TestButtonRebind::testMouse() { KConfigGroup buttonGroup = KSharedConfig::openConfig(QStringLiteral("kcminputrc"))->group(QStringLiteral("ButtonRebinds")).group(QStringLiteral("Mouse")); QFETCH(int, mouseButton); buttonGroup.writeEntry("ExtraButton7", QStringList{"MouseButton", QString::number(mouseButton)}, KConfig::Notify); buttonGroup.sync(); std::unique_ptr surface = Test::createSurface(); std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); std::unique_ptr pointer(Test::waylandSeat()->createPointer()); QSignalSpy enteredSpy(pointer.get(), &KWayland::Client::Pointer::entered); QSignalSpy buttonChangedSpy(pointer.get(), &KWayland::Client::Pointer::buttonStateChanged); const QRectF startGeometry = window->frameGeometry(); input()->pointer()->warp(startGeometry.center()); QVERIFY(enteredSpy.wait()); // 0x119 is Qt::ExtraButton7 Test::pointerButtonPressed(0x119, timestamp++); QVERIFY(buttonChangedSpy.wait()); QCOMPARE(buttonChangedSpy.count(), 1); QCOMPARE(buttonChangedSpy.at(0).at(2).value(), mouseButton); Test::pointerButtonReleased(0x119, timestamp++); } void TestButtonRebind::testMouseKeyboardMod_data() { QTest::addColumn("modifiers"); QTest::addColumn>("expectedKeys"); QTest::newRow("single ctrl") << Qt::KeyboardModifiers(Qt::ControlModifier) << QList{KEY_LEFTCTRL}; QTest::newRow("single alt") << Qt::KeyboardModifiers(Qt::AltModifier) << QList{KEY_LEFTALT}; QTest::newRow("single shift") << Qt::KeyboardModifiers(Qt::ShiftModifier) << QList{KEY_LEFTSHIFT}; // We have to test Meta with another key, because it will most likely trigger KWin to do some window operation. QTest::newRow("meta + alt") << Qt::KeyboardModifiers(Qt::MetaModifier | Qt::AltModifier) << QList{KEY_LEFTALT, KEY_LEFTMETA}; QTest::newRow("ctrl + alt + shift + meta") << Qt::KeyboardModifiers(Qt::ControlModifier | Qt::AltModifier | Qt::ShiftModifier | Qt::MetaModifier) << QList{KEY_LEFTSHIFT, KEY_LEFTCTRL, KEY_LEFTALT, KEY_LEFTMETA}; } void TestButtonRebind::testMouseKeyboardMod() { QFETCH(Qt::KeyboardModifiers, modifiers); KConfigGroup buttonGroup = KSharedConfig::openConfig(QStringLiteral("kcminputrc"))->group(QStringLiteral("ButtonRebinds")).group(QStringLiteral("TabletTool")).group(QStringLiteral("Virtual Tablet Tool 1")); buttonGroup.writeEntry(QString::number(BTN_STYLUS), QStringList{"MouseButton", QString::number(BTN_LEFT), QString::number(modifiers.toInt())}, KConfig::Notify); buttonGroup.sync(); std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); QSignalSpy keyboardEnteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered); QSignalSpy keyboardKeyChangedSpy(keyboard.get(), &KWayland::Client::Keyboard::keyChanged); std::unique_ptr surface = Test::createSurface(); std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); QVERIFY(keyboardEnteredSpy.wait()); std::unique_ptr pointer(Test::waylandSeat()->createPointer()); QSignalSpy pointerEnteredSpy(pointer.get(), &KWayland::Client::Pointer::entered); QSignalSpy pointerButtonChangedSpy(pointer.get(), &KWayland::Client::Pointer::buttonStateChanged); const QRectF startGeometry = window->frameGeometry(); input()->pointer()->warp(startGeometry.center()); QVERIFY(pointerEnteredSpy.wait()); // Send the tablet button event so it can be processed by the filter Test::tabletToolButtonPressed(BTN_STYLUS, timestamp++); // The keyboard modifier is sent first QVERIFY(keyboardKeyChangedSpy.wait()); QFETCH(QList, expectedKeys); QCOMPARE(keyboardKeyChangedSpy.count(), expectedKeys.count()); for (int i = 0; i < keyboardKeyChangedSpy.count(); i++) { QCOMPARE(keyboardKeyChangedSpy.at(i).at(0).value(), expectedKeys.at(i)); QCOMPARE(keyboardKeyChangedSpy.at(i).at(1).value(), KWayland::Client::Keyboard::KeyState::Pressed); } // Then the mouse button is QCOMPARE(pointerButtonChangedSpy.count(), 1); QCOMPARE(pointerButtonChangedSpy.at(0).at(2).value(), BTN_LEFT); Test::tabletToolButtonReleased(BTN_STYLUS, timestamp++); } void TestButtonRebind::testDisabled() { KConfigGroup buttonGroup = KSharedConfig::openConfig(QStringLiteral("kcminputrc"))->group(QStringLiteral("ButtonRebinds")).group(QStringLiteral("Mouse")); buttonGroup.writeEntry("ExtraButton7", QStringList{"Disabled"}, KConfig::Notify); buttonGroup.sync(); std::unique_ptr surface = Test::createSurface(); std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); std::unique_ptr pointer(Test::waylandSeat()->createPointer()); QSignalSpy enteredSpy(pointer.get(), &KWayland::Client::Pointer::entered); QSignalSpy buttonChangedSpy(pointer.get(), &KWayland::Client::Pointer::buttonStateChanged); const QRectF startGeometry = window->frameGeometry(); input()->pointer()->warp(startGeometry.center()); QVERIFY(enteredSpy.wait()); // 0x119 is Qt::ExtraButton7 Test::pointerButtonPressed(0x119, timestamp++); // Qt::ExtraButton7 should not have been emitted if this button is disabled QVERIFY(!buttonChangedSpy.wait(std::chrono::milliseconds(100))); QCOMPARE(buttonChangedSpy.count(), 0); Test::pointerButtonReleased(0x119, timestamp++); } void TestButtonRebind::testBindingTabletPad() { const QKeySequence sequence(Qt::Key_A); KConfigGroup buttonGroup = KSharedConfig::openConfig(QStringLiteral("kcminputrc"))->group(QStringLiteral("ButtonRebinds")).group(QStringLiteral("Tablet")).group(QStringLiteral("Virtual Tablet Pad 1")); buttonGroup.writeEntry("1", QStringList{"Key", sequence.toString(QKeySequence::PortableText)}, KConfig::Notify); buttonGroup.sync(); std::unique_ptr surface = Test::createSurface(); std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); QSignalSpy enteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered); QSignalSpy keyChangedSpy(keyboard.get(), &KWayland::Client::Keyboard::keyChanged); QVERIFY(enteredSpy.wait()); Test::tabletPadButtonPressed(1, timestamp++); QVERIFY(keyChangedSpy.wait()); QCOMPARE(keyChangedSpy.count(), 1); QCOMPARE(keyChangedSpy.at(0).at(0), KEY_A); Test::tabletPadButtonReleased(1, timestamp++); } void TestButtonRebind::testBindingTabletTool() { const QKeySequence sequence(Qt::Key_A); KConfigGroup buttonGroup = KSharedConfig::openConfig(QStringLiteral("kcminputrc"))->group(QStringLiteral("ButtonRebinds")).group(QStringLiteral("TabletTool")).group(QStringLiteral("Virtual Tablet Tool 1")); buttonGroup.writeEntry(QString::number(BTN_STYLUS), QStringList{"Key", sequence.toString(QKeySequence::PortableText)}, KConfig::Notify); buttonGroup.sync(); std::unique_ptr surface = Test::createSurface(); std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); QSignalSpy enteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered); QSignalSpy keyChangedSpy(keyboard.get(), &KWayland::Client::Keyboard::keyChanged); QVERIFY(enteredSpy.wait()); const QRectF startGeometry = window->frameGeometry(); Test::tabletToolEvent(InputRedirection::Proximity, startGeometry.center(), 1.0, 0, 0, 0, false, false, timestamp++); Test::tabletToolButtonPressed(BTN_STYLUS, timestamp++); QVERIFY(keyChangedSpy.wait()); QCOMPARE(keyChangedSpy.count(), 1); QCOMPARE(keyChangedSpy.at(0).at(0), KEY_A); Test::tabletToolButtonReleased(BTN_STYLUS, timestamp++); } void TestButtonRebind::testMouseTabletCursorSync() { KConfigGroup buttonGroup = KSharedConfig::openConfig(QStringLiteral("kcminputrc"))->group(QStringLiteral("ButtonRebinds")).group(QStringLiteral("TabletTool")).group(QStringLiteral("Virtual Tablet Tool 1")); buttonGroup.writeEntry(QString::number(BTN_STYLUS), QStringList{"MouseButton", QString::number(BTN_LEFT)}, KConfig::Notify); buttonGroup.sync(); std::unique_ptr surface = Test::createSurface(); std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); std::unique_ptr pointer(Test::waylandSeat()->createPointer()); QSignalSpy enteredSpy(pointer.get(), &KWayland::Client::Pointer::entered); QSignalSpy buttonChangedSpy(pointer.get(), &KWayland::Client::Pointer::buttonStateChanged); const QRectF startGeometry = window->frameGeometry(); // Move the mouse cursor to (25, 25) input()->pointer()->warp(startGeometry.topLeft() + QPointF{25.f, 25.5f}); QVERIFY(enteredSpy.wait()); // Move the tablet cursor to (10,10) Test::tabletToolEvent(InputRedirection::Proximity, startGeometry.topLeft() + QPointF{10.f, 10.f}, 1.0, 0, 0, 0, false, false, timestamp++); // Verify they are not starting in the same place QVERIFY(input()->pointer()->pos() != input()->tablet()->position()); // Send the tablet button event so it can be processed by the filter Test::tabletToolButtonPressed(BTN_STYLUS, timestamp++); QVERIFY(buttonChangedSpy.wait()); QCOMPARE(buttonChangedSpy.count(), 1); QCOMPARE(buttonChangedSpy.at(0).at(2).value(), BTN_LEFT); Test::tabletToolButtonReleased(BTN_STYLUS, timestamp++); // Verify that by using the mouse button binding, the mouse cursor was moved to the tablet cursor position QVERIFY(input()->pointer()->pos() == input()->tablet()->position()); } WAYLANDTEST_MAIN(TestButtonRebind) #include "buttonrebind_test.moc"