Try to fix keys getting stuck when they aren't supposed to.

This commit is contained in:
Storm Dragon
2026-09-07 00:59:35 -04:00
parent 66862ea965
commit d40fd23fa0
9 changed files with 220 additions and 120 deletions
@@ -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')
+24 -44
View File
@@ -3,7 +3,6 @@
#include "event.h"
#include "time.h"
#include <QRegularExpression>
#include <QDateTime>
#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;
const int newDirection = state > dZone ? 1 : state < -dZone ? -1 : 0;
if (isOn && newDirection == activeDirection) {
if (gradient && !compositeEngaged)
duration = (abs(state) * FREQ) / JOYMAX;
return;
}
//if was on but now should be off:
if (isOn && abs(state) <= dZone) {
isOn = false;
stateChanged = true;
if (isOn) {
if (compositeEngaged) {
compositeEngaged = false;
activeDirection = 0;
if (newDirection != 0) sequenceRunner.cancel();
}
else if (isDown) {
move(false);
}
else if (gradient) {
duration = 0;
release();
timer.stop();
disconnect(&timer, SIGNAL(timeout()), 0, 0);
isOn = false;
compositeEngaged = false;
activeDirection = 0;
tick = 0;
duration = 0;
}
}
//if was off but now should be on:
else if (!isOn && abs(state) > dZone) {
if (newDirection == 0) return;
isOn = true;
activeDirection = state >= 0 ? 1 : -1;
stateChanged = true;
if (compositeDirection) {
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()));
connect(&timer, SIGNAL(timeout()), this, SLOT(timerCalled()), Qt::UniqueConnection);
timer.start(MSEC);
}
}
//if in gradient mode and state changed, update duration
else if (gradient && !compositeDirection && abs(state) > dZone) {
duration = (abs(state) * FREQ) / JOYMAX;
}
//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;
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
-3
View File
@@ -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();
};
+53 -4
View File
@@ -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<int, int> &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<int> SyntheticInputState::takePressedKeys() {
const QVector<int> keys = keyCounts.keys().toVector();
keyCounts.clear();
return keys;
}
QVector<int> SyntheticInputState::takePressedMouseButtons() {
const QVector<int> 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<int> keys = g_inputState.takePressedKeys();
const QVector<int> 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;
}
+17 -2
View File
@@ -1,8 +1,8 @@
#ifndef THUNDERPAD_EVENT_H
#define THUNDERPAD_EVENT_H
//for the functions we need to generate keypresses / mouse actions
#include <X11/extensions/XTest.h>
#include <QHash>
#include <QVector>
//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<int> takePressedKeys();
QVector<int> takePressedMouseButtons();
private:
bool updateCount(QHash<int, int> &counts, int code, bool press);
QHash<int, int> keyCounts;
QHash<int, int> mouseButtonCounts;
};
void sendevent(const FakeEvent& e);
void releaseAllSyntheticInputs();
void cleanupDisplay();
#endif
+21 -17
View File
@@ -13,7 +13,7 @@
#include <stdint.h>
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() {
for (;;) {
js_event msg;
ssize_t len = read(joydev, &msg, sizeof(js_event));
//if there was a real event waiting,
const ssize_t len = read(joydev, &msg, sizeof(js_event));
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;
continue;
}
//device error - trigger error handling
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);
}
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;
}
}
+2 -5
View File
@@ -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();
}
}
+33 -20
View File
@@ -4,12 +4,15 @@
//to create and handle signals for various events
#include <signal.h>
#include <getopt.h>
#include <errno.h>
#include <sys/socket.h>
//to create a qapplication
#include <QFile>
#include <QSystemTrayIcon>
#include <QPointer>
#include <QFileInfo>
#include <QSocketNotifier>
//to load layouts
#include "layout.h"
@@ -21,25 +24,13 @@
//variables needed in various functions in this file
QPointer<LayoutManager> 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<unsigned char>(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();
+53 -8
View File
@@ -2,6 +2,7 @@
#include "bindingdialog.h"
#include "button.h"
#include "inputbinding.h"
#include "event.h"
#include <QElapsedTimer>
#include <QApplication>
@@ -10,6 +11,8 @@
#include <QTest>
#include <QTextStream>
#include <algorithm>
class RecorderProbe : public BindingDialog {
public:
RecorderProbe() : BindingDialog("Test control", true) {}
@@ -68,6 +71,7 @@ public:
QVector<QPair<int, bool>> 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<int>({1}));
QVERIFY(!state.mouseButtonEvent(1, false));
QVERIFY(state.keyEvent(38, true));
QVERIFY(state.keyEvent(39, true));
QVector<int> keys = state.takePressedKeys();
std::sort(keys.begin(), keys.end());
QCOMPARE(keys, QVector<int>({38, 39}));
}
QTEST_MAIN(BindingTests)
#include "test_bindings.moc"