From 801d31a926a4ad7ceeeecc64b273cd514550d613 Mon Sep 17 00:00:00 2001
From: Nicolas Fella <nicolas.fella@gmx.de>
Date: Tue, 9 Jul 2024 12:41:14 +0200
Subject: [PATCH] plugins/stickykeys: Unlatch keys after mouse click

This is how X11 behaves and it allows things like Ctrl+Click to work as expected with sticky keys
---
 autotests/integration/sticky_keys_test.cpp | 57 ++++++++++++++++++++++
 src/plugins/stickykeys/stickykeys.cpp      | 27 ++++++++++
 src/plugins/stickykeys/stickykeys.h        |  1 +
 3 files changed, 85 insertions(+)

--- a/autotests/integration/sticky_keys_test.cpp
+++ b/autotests/integration/sticky_keys_test.cpp
@@ -37,6 +37,8 @@ private Q_SLOTS:
     void testStick_data();
     void testLock();
     void testLock_data();
+    void testMouse();
+    void testMouse_data();
     void testDisableTwoKeys();
 };
 
@@ -280,6 +282,61 @@ void StickyKeysTest::testDisableTwoKeys(
     Test::keyboardKeyReleased(KEY_A, ++timestamp);
     QVERIFY(!modifierSpy.wait(10));
 }
+
+void StickyKeysTest::testMouse_data()
+{
+    QTest::addColumn<int>("modifierKey");
+    QTest::addColumn<int>("expectedMods");
+
+    QTest::addRow("Shift") << KEY_LEFTSHIFT << 1;
+    QTest::addRow("Ctrl") << KEY_LEFTCTRL << 4;
+    QTest::addRow("Alt") << KEY_LEFTALT << 8;
+    QTest::addRow("AltGr") << KEY_RIGHTALT << 128;
+}
+
+void StickyKeysTest::testMouse()
+{
+    QFETCH(int, modifierKey);
+    QFETCH(int, expectedMods);
+
+    std::unique_ptr<KWayland::Client::Keyboard> keyboard(Test::waylandSeat()->createKeyboard());
+
+    std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
+    QVERIFY(surface != nullptr);
+    std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
+    QVERIFY(shellSurface != nullptr);
+    Window *waylandWindow = Test::renderAndWaitForShown(surface.get(), QSize(10, 10), Qt::blue);
+    QVERIFY(waylandWindow);
+
+    QSignalSpy modifierSpy(keyboard.get(), &KWayland::Client::Keyboard::modifiersChanged);
+    QVERIFY(modifierSpy.wait());
+    modifierSpy.clear();
+
+    quint32 timestamp = 0;
+
+    // press mod to latch it
+    Test::keyboardKeyPressed(modifierKey, ++timestamp);
+    QVERIFY(modifierSpy.wait());
+    // arguments are: quint32 depressed, quint32 latched, quint32 locked, quint32 group
+    QCOMPARE(modifierSpy.first()[0], expectedMods); // verify that mod is depressed
+    QCOMPARE(modifierSpy.first()[1], expectedMods); // verify that mod is latched
+
+    modifierSpy.clear();
+    // release mod, the modifier should still be latched
+    Test::keyboardKeyReleased(modifierKey, ++timestamp);
+    QVERIFY(modifierSpy.wait());
+    QCOMPARE(modifierSpy.first()[0], 0); // verify that mod is not depressed
+    QCOMPARE(modifierSpy.first()[1], expectedMods); // verify that mod is still latched
+
+    // press and release a mouse button, this unlatches the modifier
+    modifierSpy.clear();
+    Test::pointerButtonPressed(BTN_LEFT, ++timestamp);
+    QVERIFY(!modifierSpy.wait(10));
+    Test::pointerButtonReleased(BTN_LEFT, ++timestamp);
+    QVERIFY(modifierSpy.wait());
+    QCOMPARE(modifierSpy.first()[0], 0); // verify that mod is not depressed
+    QCOMPARE(modifierSpy.first()[1], 0); // verify that mod is not latched any more
+}
 }
 
 WAYLANDTEST_MAIN(KWin::StickyKeysTest)
--- a/src/plugins/stickykeys/stickykeys.cpp
+++ b/src/plugins/stickykeys/stickykeys.cpp
@@ -9,6 +9,8 @@
 #include "keyboard_input.h"
 #include "xkb.h"
 
+#include <QTimer>
+
 #include <KLazyLocalizedString>
 #if KWIN_BUILD_NOTIFICATIONS
 #include <KNotification>
@@ -183,4 +185,29 @@ void StickyKeysFilter::disableStickyKeys
     KWin::input()->uninstallInputEventFilter(this);
 }
 
+bool StickyKeysFilter::pointerButton(KWin::PointerButtonEvent *event)
+{
+    if (event->state == KWin::PointerButtonState::Released) {
+        // unlatch all unlocked modifiers
+        for (auto it = m_keyStates.keyValueBegin(); it != m_keyStates.keyValueEnd(); ++it) {
+
+            if (it->second == Locked) {
+                continue;
+            }
+
+            it->second = KeyState::None;
+
+            KWin::input()->keyboard()->xkb()->setModifierLatched(keyToModifier(static_cast<Qt::Key>(it->first)), false);
+
+            // We need to delay the modifier update until the client received the mouse event, otherwise
+            // the updated modifiers arrive before the mouse event and e.g. Ctrl+Click won't work
+            QTimer::singleShot(0, this, [] {
+                KWin::input()->keyboard()->xkb()->forwardModifiers();
+            });
+        }
+    }
+
+    return false;
+}
+
 #include "moc_stickykeys.cpp"
--- a/src/plugins/stickykeys/stickykeys.h
+++ b/src/plugins/stickykeys/stickykeys.h
@@ -18,6 +18,7 @@ public:
     explicit StickyKeysFilter();
 
     bool keyboardKey(KWin::KeyboardKeyEvent *event) override;
+    bool pointerButton(KWin::PointerButtonEvent *event) override;
 
     enum KeyState {
         None,
