From d40fd23fa0f85f22d55fcff16f4e4920073e26d6 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Mon, 7 Sep 2026 00:59:35 -0400 Subject: [PATCH] Try to fix keys getting stuck when they aren't supposed to. --- .../Arch-Linux/thunderpad-git/PKGBUILD | 2 +- src/axis.cpp | 92 ++++++++----------- src/axis.h | 3 - src/event.cpp | 57 +++++++++++- src/event.h | 19 +++- src/joypad.cpp | 46 +++++----- src/layout_edit.cpp | 7 +- src/main.cpp | 53 +++++++---- tests/test_bindings.cpp | 61 ++++++++++-- 9 files changed, 220 insertions(+), 120 deletions(-) diff --git a/distro-packages/Arch-Linux/thunderpad-git/PKGBUILD b/distro-packages/Arch-Linux/thunderpad-git/PKGBUILD index 53424d9..7bce1d5 100644 --- a/distro-packages/Arch-Linux/thunderpad-git/PKGBUILD +++ b/distro-packages/Arch-Linux/thunderpad-git/PKGBUILD @@ -3,7 +3,7 @@ pkgname=thunderpad-git _pkgname=thunderpad -pkgver=r163.47f39ca +pkgver=r164.66862ea pkgrel=1 pkgdesc="Accessible joypad to keyboard and mouse mapper" arch=('aarch64' 'x86_64') diff --git a/src/axis.cpp b/src/axis.cpp index b244759..7092ce6 100644 --- a/src/axis.cpp +++ b/src/axis.cpp @@ -3,7 +3,6 @@ #include "event.h" #include "time.h" #include -#include #define sqr(a) ((a)*(a)) #define cub(a) ((a)*(a)*(a)) @@ -22,8 +21,6 @@ Axis::Axis( int i, QObject *parent ) : QObject(parent) { interpretation = ZeroOne; gradient = false; absolute = false; - lastKeyPressTime = 0; - minKeyInterval = 100; // Default: 100ms minimum between key presses toDefault(); tick = 0; connect(&sequenceRunner, &SequenceRunner::keyEvent, this, &Axis::compositeKeyEvent); @@ -301,52 +298,45 @@ void Axis::jsevent( int value ) { state = (value + JOYMIN) / 2; else state = (value + JOYMAX) / 2; - const InputBinding &directionBinding = state >= 0 ? positiveBinding : negativeBinding; - const bool compositeDirection = isOn ? compositeEngaged : directionBinding.isComposite(); - //set isOn, deal with state changing. - bool stateChanged = false; - - //if was on but now should be off: - if (isOn && abs(state) <= dZone) { - isOn = false; - stateChanged = true; - if (compositeEngaged) { - compositeEngaged = false; - activeDirection = 0; - } - else if (gradient) { - duration = 0; - release(); - timer.stop(); - disconnect(&timer, SIGNAL(timeout()), 0, 0); - tick = 0; - } - } - //if was off but now should be on: - else if (!isOn && abs(state) > dZone) { - isOn = true; - activeDirection = state >= 0 ? 1 : -1; - stateChanged = true; - if (compositeDirection) { - compositeEngaged = true; - sequenceRunner.start(directionBinding); - } - else if (gradient) { + const int newDirection = state > dZone ? 1 : state < -dZone ? -1 : 0; + if (isOn && newDirection == activeDirection) { + if (gradient && !compositeEngaged) duration = (abs(state) * FREQ) / JOYMAX; - connect(&timer, SIGNAL(timeout()), this, SLOT(timerCalled())); - timer.start(MSEC); - } - } - //if in gradient mode and state changed, update duration - else if (gradient && !compositeDirection && abs(state) > dZone) { - duration = (abs(state) * FREQ) / JOYMAX; + return; } - //gradient will trigger movement on its own via timer(). - //non-gradient needs to be told to move only when state changes. - if (!compositeDirection && !gradient && stateChanged) { - move(isOn); - if (!isOn) activeDirection = 0; + if (isOn) { + if (compositeEngaged) { + if (newDirection != 0) sequenceRunner.cancel(); + } + else if (isDown) { + move(false); + } + timer.stop(); + disconnect(&timer, SIGNAL(timeout()), 0, 0); + isOn = false; + compositeEngaged = false; + activeDirection = 0; + tick = 0; + duration = 0; + } + + if (newDirection == 0) return; + + isOn = true; + activeDirection = newDirection; + const InputBinding &directionBinding = newDirection > 0 ? positiveBinding : negativeBinding; + if (directionBinding.isComposite()) { + compositeEngaged = true; + sequenceRunner.start(directionBinding); + } + else if (gradient) { + duration = (abs(state) * FREQ) / JOYMAX; + connect(&timer, SIGNAL(timeout()), this, SLOT(timerCalled()), Qt::UniqueConnection); + timer.start(MSEC); + } + else { + move(true); } } @@ -359,7 +349,6 @@ void Axis::toDefault() { activeDirection = 0; tick = 0; duration = 0; - lastKeyPressTime = 0; interpretation = ZeroOne; gradient = false; absolute = false; @@ -505,15 +494,6 @@ void Axis::move( bool press ) { //dialog being open and blocking events from happening. if (isDown == press) return; - // Key repeat filtering: prevent rapid-fire from controller hardware - if (press && minKeyInterval > 0) { - qint64 currentTime = QDateTime::currentMSecsSinceEpoch(); - if (currentTime - lastKeyPressTime < minKeyInterval) { - return; // Too soon, ignore this key press - } - lastKeyPressTime = currentTime; - } - isDown = press; if (press) { const int direction = activeDirection != 0 diff --git a/src/axis.h b/src/axis.h index 53b7e09..a503c68 100644 --- a/src/axis.h +++ b/src/axis.h @@ -117,9 +117,6 @@ class Axis : public QObject { int duration; QTimer timer; - // Key repeat filtering to prevent controller spam - qint64 lastKeyPressTime; - int minKeyInterval; // milliseconds between key presses public slots: void timerCalled(); }; diff --git a/src/event.cpp b/src/event.cpp index 17ca9d7..6e8289b 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -5,6 +5,45 @@ //persistent X11 display connection to avoid opening/closing for each event static Display* g_display = nullptr; +static SyntheticInputState g_inputState; + +bool SyntheticInputState::updateCount(QHash &counts, int code, bool press) { + if (code <= 0) return false; + if (press) { + const int oldCount = counts.value(code); + counts.insert(code, oldCount + 1); + return oldCount == 0; + } + + const int oldCount = counts.value(code); + if (oldCount <= 0) return false; + if (oldCount == 1) { + counts.remove(code); + return true; + } + counts.insert(code, oldCount - 1); + return false; +} + +bool SyntheticInputState::keyEvent(int keycode, bool press) { + return updateCount(keyCounts, keycode, press); +} + +bool SyntheticInputState::mouseButtonEvent(int button, bool press) { + return updateCount(mouseButtonCounts, button, press); +} + +QVector SyntheticInputState::takePressedKeys() { + const QVector keys = keyCounts.keys().toVector(); + keyCounts.clear(); + return keys; +} + +QVector SyntheticInputState::takePressedMouseButtons() { + const QVector buttons = mouseButtonCounts.keys().toVector(); + mouseButtonCounts.clear(); + return buttons; +} static Display* getDisplay() { if (!g_display) { @@ -21,6 +60,16 @@ void cleanupDisplay() { } } +void releaseAllSyntheticInputs() { + Display* display = getDisplay(); + const QVector keys = g_inputState.takePressedKeys(); + const QVector buttons = g_inputState.takePressedMouseButtons(); + if (!display) return; + for (int keycode : keys) XTestFakeKeyEvent(display, keycode, false, 0); + for (int button : buttons) XTestFakeButtonEvent(display, button, false, 0); + if (!keys.isEmpty() || !buttons.isEmpty()) XFlush(display); +} + //actually creates an XWindows event :) void sendevent(const FakeEvent &e) { Display* display = getDisplay(); @@ -46,22 +95,22 @@ void sendevent(const FakeEvent &e) { break; } case FakeEvent::KeyUp: - if (e.keycode == 0) return; + if (!g_inputState.keyEvent(e.keycode, false)) return; XTestFakeKeyEvent(display, e.keycode, false, 0); break; case FakeEvent::KeyDown: - if (e.keycode == 0) return; + if (!g_inputState.keyEvent(e.keycode, true)) return; XTestFakeKeyEvent(display, e.keycode, true, 0); break; case FakeEvent::MouseUp: - if (e.keycode == 0) return; + if (!g_inputState.mouseButtonEvent(e.keycode, false)) return; XTestFakeButtonEvent(display, e.keycode, false, 0); break; case FakeEvent::MouseDown: - if (e.keycode == 0) return; + if (!g_inputState.mouseButtonEvent(e.keycode, true)) return; XTestFakeButtonEvent(display, e.keycode, true, 0); break; } diff --git a/src/event.h b/src/event.h index 0228519..5fa8e5a 100644 --- a/src/event.h +++ b/src/event.h @@ -1,8 +1,8 @@ #ifndef THUNDERPAD_EVENT_H #define THUNDERPAD_EVENT_H -//for the functions we need to generate keypresses / mouse actions -#include +#include +#include //a simplified event structure that can handle buttons and mouse movements struct FakeEvent { @@ -21,7 +21,22 @@ struct FakeEvent { }; }; +class SyntheticInputState { +public: + bool keyEvent(int keycode, bool press); + bool mouseButtonEvent(int button, bool press); + QVector takePressedKeys(); + QVector takePressedMouseButtons(); + +private: + bool updateCount(QHash &counts, int code, bool press); + + QHash keyCounts; + QHash mouseButtonCounts; +}; + void sendevent(const FakeEvent& e); +void releaseAllSyntheticInputs(); void cleanupDisplay(); #endif diff --git a/src/joypad.cpp b/src/joypad.cpp index 298f970..8925126 100644 --- a/src/joypad.cpp +++ b/src/joypad.cpp @@ -13,7 +13,7 @@ #include JoyPad::JoyPad( int i, int dev, QObject *parent ) - : QObject(parent), joydev(-1), axisCount(0), buttonCount(0), jpw(0), readNotifier(0), errorNotifier(0) { + : QObject(parent), joydev(-1), axisCount(0), buttonCount(0), jpw(0), readNotifier(0), errorNotifier(0), hasFocus(false) { debug_mesg("Constructing the joypad device with index %d and fd %d\n", i, dev); //remember the index, index = i; @@ -232,13 +232,19 @@ void JoyPad::jsevent(const js_event &msg) { jpw->jsevent(msg); return; } - //if the dialog is open, stop here. We don't want to signal ourselves with - //the input we generate. - if (qApp->activeWindow() != 0 && qApp->activeModalWidget() != 0) return; + unsigned int type = msg.type & ~JS_EVENT_INIT; + //While a modal dialog is open, suppress new presses but continue to + //process releases so an already held synthetic input cannot get stranded. + if (qApp->activeWindow() != 0 && qApp->activeModalWidget() != 0) { + if (type == JS_EVENT_AXIS && msg.number < axes.size() && axes[msg.number]->inDeadZone(msg.value)) + axes[msg.number]->jsevent(msg.value); + else if (type == JS_EVENT_BUTTON && msg.number < buttons.size() && msg.value == 0) + buttons[msg.number]->jsevent(msg.value); + return; + } //otherwise, lets create us a fake event! Pass on the event to whichever //Button or Axis was pressed and let them decide what to do with it. - unsigned int type = msg.type & ~JS_EVENT_INIT; if (type == JS_EVENT_AXIS) { debug_mesg("DEBUG: passing on an axis event\n"); debug_mesg("DEBUG: %d %d\n", msg.number, msg.value); @@ -260,24 +266,22 @@ JoyPadWidget* JoyPad::widget( QWidget* parent, int i) { } void JoyPad::handleJoyEvents() { - js_event msg; - ssize_t len = read(joydev, &msg, sizeof(js_event)); - //if there was a real event waiting, - if (len == sizeof(js_event)) { - //pass that event on to the joypad! - jsevent(msg); - } else if (len < 0) { - //handle read errors - if (errno == EAGAIN || errno == EWOULDBLOCK) { - //normal for non-blocking read when no data available - return; + for (;;) { + js_event msg; + const ssize_t len = read(joydev, &msg, sizeof(js_event)); + if (len == sizeof(js_event)) { + jsevent(msg); + continue; + } + if (len < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return; + if (len < 0) { + debug_mesg("Read error on joystick device fd %d: %s\n", joydev, strerror(errno)); + } + else { + debug_mesg("Unexpected read length %zd on joystick device fd %d\n", len, joydev); } - //device error - trigger error handling - debug_mesg("Read error on joystick device fd %d: %s\n", joydev, strerror(errno)); errorRead(); - } else if (len > 0) { - //partial read - should not happen with joystick events - debug_mesg("Warning: partial read (%zd bytes) from joystick device fd %d\n", len, joydev); + return; } } diff --git a/src/layout_edit.cpp b/src/layout_edit.cpp index 1aa2115..6a92db4 100644 --- a/src/layout_edit.cpp +++ b/src/layout_edit.cpp @@ -189,13 +189,10 @@ void LayoutEdit::updateJoypadWidgets() { void LayoutEdit::appFocusChanged(QWidget *old, QWidget *now) { if (now != NULL && old == NULL) { emit focusStateChanged(false); + lm->release(); } else if(old != NULL && now == NULL) { emit focusStateChanged(true); - foreach (JoyPad *joypad, lm->available) { - debug_mesg("iterating and releasing\n"); - joypad->release(); - } - debug_mesg("done releasing!\n"); + lm->release(); } } diff --git a/src/main.cpp b/src/main.cpp index f2eea12..45c3c70 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,12 +4,15 @@ //to create and handle signals for various events #include #include +#include +#include //to create a qapplication #include #include #include #include +#include //to load layouts #include "layout.h" @@ -21,25 +24,13 @@ //variables needed in various functions in this file QPointer layoutManagerPtr; +static int signalSockets[2] = {-1, -1}; -//signal handler for SIGUSR2 -//SIGUSR2 means that a new layout should be loaded. It is saved in -// ~/.config/thunderpad/layout, where the last used layout is put. -void catchSIGUSR2( int sig ) { - if (layoutManagerPtr) layoutManagerPtr->load(); - //remember to catch this signal again next time. - signal( sig, catchSIGUSR2 ); -} - - - -//signal handler for SIGUSR1 -//SIGUSR1 means that we should update the available joystick device list. -void catchSIGUSR1( int sig ) { - //buildJoyDevices(); - if (layoutManagerPtr) layoutManagerPtr->updateJoyDevs(); - //remember to catch this signal again next time. - signal( sig, catchSIGUSR1 ); +void catchSignal(int sig) { + const int savedErrno = errno; + const unsigned char signalNumber = static_cast(sig); + if (signalSockets[0] >= 0) write(signalSockets[0], &signalNumber, sizeof(signalNumber)); + errno = savedErrno; } @@ -245,6 +236,20 @@ int main( int argc, char **argv ) LayoutManager layoutManager(useTrayIcon,devdir,settingsDir,headless); layoutManagerPtr = &layoutManager; + if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, signalSockets) != 0) { + perror("socketpair"); + return 1; + } + QSocketNotifier signalNotifier(signalSockets[1], QSocketNotifier::Read); + QObject::connect(&signalNotifier, &QSocketNotifier::activated, &app, [&]() { + unsigned char signalNumber; + while (read(signalSockets[1], &signalNumber, sizeof(signalNumber)) == sizeof(signalNumber)) { + if (signalNumber == SIGUSR1) layoutManager.updateJoyDevs(); + else if (signalNumber == SIGUSR2) layoutManager.load(); + else if (signalNumber == SIGINT || signalNumber == SIGTERM) app.quit(); + } + }); + //build the joystick device list for the first time, //buildJoyDevices(); layoutManager.updateJoyDevs(); @@ -253,8 +258,10 @@ int main( int argc, char **argv ) layoutManager.load(); //prepare the signal handlers - signal( SIGUSR1, catchSIGUSR1 ); - signal( SIGUSR2, catchSIGUSR2 ); + signal(SIGUSR1, catchSignal); + signal(SIGUSR2, catchSignal); + signal(SIGINT, catchSignal); + signal(SIGTERM, catchSignal); //and run the program! int result = app.exec(); @@ -267,6 +274,12 @@ int main( int argc, char **argv ) //Release every synthetic input and stop binding timers before closing X11. layoutManager.release(); + releaseAllSyntheticInputs(); + + close(signalSockets[0]); + close(signalSockets[1]); + signalSockets[0] = -1; + signalSockets[1] = -1; //cleanup X11 display connection cleanupDisplay(); diff --git a/tests/test_bindings.cpp b/tests/test_bindings.cpp index 75e89ef..cbdeefc 100644 --- a/tests/test_bindings.cpp +++ b/tests/test_bindings.cpp @@ -2,6 +2,7 @@ #include "bindingdialog.h" #include "button.h" #include "inputbinding.h" +#include "event.h" #include #include @@ -10,6 +11,8 @@ #include #include +#include + class RecorderProbe : public BindingDialog { public: RecorderProbe() : BindingDialog("Test control", true) {} @@ -68,6 +71,7 @@ public: QVector> compositeEvents; bool currentBindingUsesMouse() const { return useMouse; } bool bindingIsDown() const { return isDown; } + int pressedKey() const { return downkey; } protected: void compositeKeyEvent(int keycode, bool press) override { compositeEvents.append(qMakePair(keycode, press)); @@ -85,9 +89,11 @@ private slots: void recorderModifiersRepeatLimitAndFinish(); void runnerOrderTimingRetriggerAndCancel(); void preservePlainButtonModes(); - void axisCompositeRearmsWithoutRepeating(); + void axisCompositeChangesDirectionWithoutRepeating(); void axisCompositeRearmsWithZeroDeadzone(); void axisGradientReleaseKeepsActiveDirection(); + void axisChangesDirectionWithoutNeutral(); + void syntheticInputReferenceCounts(); }; void BindingTests::parseAndSerializeSequences() { @@ -324,7 +330,7 @@ void BindingTests::preservePlainButtonModes() { QCOMPARE(cancelling.compositeEvents, cancelled); } -void BindingTests::axisCompositeRearmsWithoutRepeating() { +void BindingTests::axisCompositeChangesDirectionWithoutRepeating() { AxisProbe axis; QString input("Gradient, +sequence 50 10+11, -sequence 50 12+13"); QTextStream reader(&input); @@ -335,17 +341,18 @@ void BindingTests::axisCompositeRearmsWithoutRepeating() { QTest::qWait(80); QCOMPARE(axis.compositeEvents.size(), 4); axis.jsevent(-25000); - QTest::qWait(80); - QCOMPARE(axis.compositeEvents.size(), 4); + QTRY_COMPARE_WITH_TIMEOUT(axis.compositeEvents.size(), 8, 150); + QCOMPARE(axis.compositeEvents.at(4), qMakePair(12, true)); + QCOMPARE(axis.compositeEvents.at(5), qMakePair(13, true)); axis.jsevent(0); axis.jsevent(20000); - QTRY_COMPARE_WITH_TIMEOUT(axis.compositeEvents.size(), 8, 150); + QTRY_COMPARE_WITH_TIMEOUT(axis.compositeEvents.size(), 12, 150); axis.jsevent(0); axis.jsevent(-20000); - QTRY_COMPARE_WITH_TIMEOUT(axis.compositeEvents.size(), 12, 150); - QCOMPARE(axis.compositeEvents.at(8), qMakePair(12, true)); - QCOMPARE(axis.compositeEvents.at(9), qMakePair(13, true)); + QTRY_COMPARE_WITH_TIMEOUT(axis.compositeEvents.size(), 16, 150); + QCOMPARE(axis.compositeEvents.at(12), qMakePair(12, true)); + QCOMPARE(axis.compositeEvents.at(13), qMakePair(13, true)); } void BindingTests::axisCompositeRearmsWithZeroDeadzone() { @@ -380,5 +387,43 @@ void BindingTests::axisGradientReleaseKeepsActiveDirection() { QVERIFY(axis.currentBindingUsesMouse()); } +void BindingTests::axisChangesDirectionWithoutNeutral() { + AxisProbe axis; + QString input("ZeroOne, +key 116, -key 111"); + QTextStream reader(&input); + QVERIFY(axis.read(reader)); + + axis.jsevent(-32767); + QVERIFY(axis.bindingIsDown()); + QCOMPARE(axis.pressedKey(), 111); + + axis.jsevent(32767); + QVERIFY(axis.bindingIsDown()); + QCOMPARE(axis.pressedKey(), 116); + + axis.jsevent(0); + QVERIFY(!axis.bindingIsDown()); +} + +void BindingTests::syntheticInputReferenceCounts() { + SyntheticInputState state; + QVERIFY(state.keyEvent(111, true)); + QVERIFY(!state.keyEvent(111, true)); + QVERIFY(!state.keyEvent(111, false)); + QVERIFY(state.keyEvent(111, false)); + QVERIFY(!state.keyEvent(111, false)); + + QVERIFY(state.mouseButtonEvent(1, true)); + QVERIFY(!state.mouseButtonEvent(1, true)); + QCOMPARE(state.takePressedMouseButtons(), QVector({1})); + QVERIFY(!state.mouseButtonEvent(1, false)); + + QVERIFY(state.keyEvent(38, true)); + QVERIFY(state.keyEvent(39, true)); + QVector keys = state.takePressedKeys(); + std::sort(keys.begin(), keys.end()); + QCOMPARE(keys, QVector({38, 39})); +} + QTEST_MAIN(BindingTests) #include "test_bindings.moc"