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-git
_pkgname=thunderpad _pkgname=thunderpad
pkgver=r163.47f39ca pkgver=r164.66862ea
pkgrel=1 pkgrel=1
pkgdesc="Accessible joypad to keyboard and mouse mapper" pkgdesc="Accessible joypad to keyboard and mouse mapper"
arch=('aarch64' 'x86_64') arch=('aarch64' 'x86_64')
+24 -44
View File
@@ -3,7 +3,6 @@
#include "event.h" #include "event.h"
#include "time.h" #include "time.h"
#include <QRegularExpression> #include <QRegularExpression>
#include <QDateTime>
#define sqr(a) ((a)*(a)) #define sqr(a) ((a)*(a))
#define cub(a) ((a)*(a)*(a)) #define cub(a) ((a)*(a)*(a))
@@ -22,8 +21,6 @@ Axis::Axis( int i, QObject *parent ) : QObject(parent) {
interpretation = ZeroOne; interpretation = ZeroOne;
gradient = false; gradient = false;
absolute = false; absolute = false;
lastKeyPressTime = 0;
minKeyInterval = 100; // Default: 100ms minimum between key presses
toDefault(); toDefault();
tick = 0; tick = 0;
connect(&sequenceRunner, &SequenceRunner::keyEvent, this, &Axis::compositeKeyEvent); connect(&sequenceRunner, &SequenceRunner::keyEvent, this, &Axis::compositeKeyEvent);
@@ -301,52 +298,45 @@ void Axis::jsevent( int value ) {
state = (value + JOYMIN) / 2; state = (value + JOYMIN) / 2;
else else
state = (value + JOYMAX) / 2; state = (value + JOYMAX) / 2;
const InputBinding &directionBinding = state >= 0 ? positiveBinding : negativeBinding; const int newDirection = state > dZone ? 1 : state < -dZone ? -1 : 0;
const bool compositeDirection = isOn ? compositeEngaged : directionBinding.isComposite(); if (isOn && newDirection == activeDirection) {
//set isOn, deal with state changing. if (gradient && !compositeEngaged)
bool stateChanged = false; duration = (abs(state) * FREQ) / JOYMAX;
return;
}
//if was on but now should be off: if (isOn) {
if (isOn && abs(state) <= dZone) {
isOn = false;
stateChanged = true;
if (compositeEngaged) { if (compositeEngaged) {
compositeEngaged = false; if (newDirection != 0) sequenceRunner.cancel();
activeDirection = 0; }
else if (isDown) {
move(false);
} }
else if (gradient) {
duration = 0;
release();
timer.stop(); timer.stop();
disconnect(&timer, SIGNAL(timeout()), 0, 0); disconnect(&timer, SIGNAL(timeout()), 0, 0);
isOn = false;
compositeEngaged = false;
activeDirection = 0;
tick = 0; tick = 0;
duration = 0;
} }
}
//if was off but now should be on: if (newDirection == 0) return;
else if (!isOn && abs(state) > dZone) {
isOn = true; isOn = true;
activeDirection = state >= 0 ? 1 : -1; activeDirection = newDirection;
stateChanged = true; const InputBinding &directionBinding = newDirection > 0 ? positiveBinding : negativeBinding;
if (compositeDirection) { if (directionBinding.isComposite()) {
compositeEngaged = true; compositeEngaged = true;
sequenceRunner.start(directionBinding); sequenceRunner.start(directionBinding);
} }
else if (gradient) { else if (gradient) {
duration = (abs(state) * FREQ) / JOYMAX; duration = (abs(state) * FREQ) / JOYMAX;
connect(&timer, SIGNAL(timeout()), this, SLOT(timerCalled())); connect(&timer, SIGNAL(timeout()), this, SLOT(timerCalled()), Qt::UniqueConnection);
timer.start(MSEC); timer.start(MSEC);
} }
} else {
//if in gradient mode and state changed, update duration move(true);
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;
} }
} }
@@ -359,7 +349,6 @@ void Axis::toDefault() {
activeDirection = 0; activeDirection = 0;
tick = 0; tick = 0;
duration = 0; duration = 0;
lastKeyPressTime = 0;
interpretation = ZeroOne; interpretation = ZeroOne;
gradient = false; gradient = false;
absolute = false; absolute = false;
@@ -505,15 +494,6 @@ void Axis::move( bool press ) {
//dialog being open and blocking events from happening. //dialog being open and blocking events from happening.
if (isDown == press) return; 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; isDown = press;
if (press) { if (press) {
const int direction = activeDirection != 0 const int direction = activeDirection != 0
-3
View File
@@ -117,9 +117,6 @@ class Axis : public QObject {
int duration; int duration;
QTimer timer; QTimer timer;
// Key repeat filtering to prevent controller spam
qint64 lastKeyPressTime;
int minKeyInterval; // milliseconds between key presses
public slots: public slots:
void timerCalled(); void timerCalled();
}; };
+53 -4
View File
@@ -5,6 +5,45 @@
//persistent X11 display connection to avoid opening/closing for each event //persistent X11 display connection to avoid opening/closing for each event
static Display* g_display = nullptr; 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() { static Display* getDisplay() {
if (!g_display) { 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 :) //actually creates an XWindows event :)
void sendevent(const FakeEvent &e) { void sendevent(const FakeEvent &e) {
Display* display = getDisplay(); Display* display = getDisplay();
@@ -46,22 +95,22 @@ void sendevent(const FakeEvent &e) {
break; break;
} }
case FakeEvent::KeyUp: case FakeEvent::KeyUp:
if (e.keycode == 0) return; if (!g_inputState.keyEvent(e.keycode, false)) return;
XTestFakeKeyEvent(display, e.keycode, false, 0); XTestFakeKeyEvent(display, e.keycode, false, 0);
break; break;
case FakeEvent::KeyDown: case FakeEvent::KeyDown:
if (e.keycode == 0) return; if (!g_inputState.keyEvent(e.keycode, true)) return;
XTestFakeKeyEvent(display, e.keycode, true, 0); XTestFakeKeyEvent(display, e.keycode, true, 0);
break; break;
case FakeEvent::MouseUp: case FakeEvent::MouseUp:
if (e.keycode == 0) return; if (!g_inputState.mouseButtonEvent(e.keycode, false)) return;
XTestFakeButtonEvent(display, e.keycode, false, 0); XTestFakeButtonEvent(display, e.keycode, false, 0);
break; break;
case FakeEvent::MouseDown: case FakeEvent::MouseDown:
if (e.keycode == 0) return; if (!g_inputState.mouseButtonEvent(e.keycode, true)) return;
XTestFakeButtonEvent(display, e.keycode, true, 0); XTestFakeButtonEvent(display, e.keycode, true, 0);
break; break;
} }
+17 -2
View File
@@ -1,8 +1,8 @@
#ifndef THUNDERPAD_EVENT_H #ifndef THUNDERPAD_EVENT_H
#define THUNDERPAD_EVENT_H #define THUNDERPAD_EVENT_H
//for the functions we need to generate keypresses / mouse actions #include <QHash>
#include <X11/extensions/XTest.h> #include <QVector>
//a simplified event structure that can handle buttons and mouse movements //a simplified event structure that can handle buttons and mouse movements
struct FakeEvent { 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 sendevent(const FakeEvent& e);
void releaseAllSyntheticInputs();
void cleanupDisplay(); void cleanupDisplay();
#endif #endif
+21 -17
View File
@@ -13,7 +13,7 @@
#include <stdint.h> #include <stdint.h>
JoyPad::JoyPad( int i, int dev, QObject *parent ) 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); debug_mesg("Constructing the joypad device with index %d and fd %d\n", i, dev);
//remember the index, //remember the index,
index = i; index = i;
@@ -232,13 +232,19 @@ void JoyPad::jsevent(const js_event &msg) {
jpw->jsevent(msg); jpw->jsevent(msg);
return; return;
} }
//if the dialog is open, stop here. We don't want to signal ourselves with unsigned int type = msg.type & ~JS_EVENT_INIT;
//the input we generate. //While a modal dialog is open, suppress new presses but continue to
if (qApp->activeWindow() != 0 && qApp->activeModalWidget() != 0) return; //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 //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. //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) { if (type == JS_EVENT_AXIS) {
debug_mesg("DEBUG: passing on an axis event\n"); debug_mesg("DEBUG: passing on an axis event\n");
debug_mesg("DEBUG: %d %d\n", msg.number, msg.value); debug_mesg("DEBUG: %d %d\n", msg.number, msg.value);
@@ -260,24 +266,22 @@ JoyPadWidget* JoyPad::widget( QWidget* parent, int i) {
} }
void JoyPad::handleJoyEvents() { void JoyPad::handleJoyEvents() {
for (;;) {
js_event msg; js_event msg;
ssize_t len = read(joydev, &msg, sizeof(js_event)); const ssize_t len = read(joydev, &msg, sizeof(js_event));
//if there was a real event waiting,
if (len == sizeof(js_event)) { if (len == sizeof(js_event)) {
//pass that event on to the joypad!
jsevent(msg); jsevent(msg);
} else if (len < 0) { continue;
//handle read errors
if (errno == EAGAIN || errno == EWOULDBLOCK) {
//normal for non-blocking read when no data available
return;
} }
//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)); 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(); errorRead();
} else if (len > 0) { return;
//partial read - should not happen with joystick events
debug_mesg("Warning: partial read (%zd bytes) from joystick device fd %d\n", len, joydev);
} }
} }
+2 -5
View File
@@ -189,13 +189,10 @@ void LayoutEdit::updateJoypadWidgets() {
void LayoutEdit::appFocusChanged(QWidget *old, QWidget *now) { void LayoutEdit::appFocusChanged(QWidget *old, QWidget *now) {
if (now != NULL && old == NULL) { if (now != NULL && old == NULL) {
emit focusStateChanged(false); emit focusStateChanged(false);
lm->release();
} else if(old != NULL && now == NULL) { } else if(old != NULL && now == NULL) {
emit focusStateChanged(true); emit focusStateChanged(true);
foreach (JoyPad *joypad, lm->available) { lm->release();
debug_mesg("iterating and releasing\n");
joypad->release();
}
debug_mesg("done releasing!\n");
} }
} }
+33 -20
View File
@@ -4,12 +4,15 @@
//to create and handle signals for various events //to create and handle signals for various events
#include <signal.h> #include <signal.h>
#include <getopt.h> #include <getopt.h>
#include <errno.h>
#include <sys/socket.h>
//to create a qapplication //to create a qapplication
#include <QFile> #include <QFile>
#include <QSystemTrayIcon> #include <QSystemTrayIcon>
#include <QPointer> #include <QPointer>
#include <QFileInfo> #include <QFileInfo>
#include <QSocketNotifier>
//to load layouts //to load layouts
#include "layout.h" #include "layout.h"
@@ -21,25 +24,13 @@
//variables needed in various functions in this file //variables needed in various functions in this file
QPointer<LayoutManager> layoutManagerPtr; QPointer<LayoutManager> layoutManagerPtr;
static int signalSockets[2] = {-1, -1};
//signal handler for SIGUSR2 void catchSignal(int sig) {
//SIGUSR2 means that a new layout should be loaded. It is saved in const int savedErrno = errno;
// ~/.config/thunderpad/layout, where the last used layout is put. const unsigned char signalNumber = static_cast<unsigned char>(sig);
void catchSIGUSR2( int sig ) { if (signalSockets[0] >= 0) write(signalSockets[0], &signalNumber, sizeof(signalNumber));
if (layoutManagerPtr) layoutManagerPtr->load(); errno = savedErrno;
//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 );
} }
@@ -245,6 +236,20 @@ int main( int argc, char **argv )
LayoutManager layoutManager(useTrayIcon,devdir,settingsDir,headless); LayoutManager layoutManager(useTrayIcon,devdir,settingsDir,headless);
layoutManagerPtr = &layoutManager; 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, //build the joystick device list for the first time,
//buildJoyDevices(); //buildJoyDevices();
layoutManager.updateJoyDevs(); layoutManager.updateJoyDevs();
@@ -253,8 +258,10 @@ int main( int argc, char **argv )
layoutManager.load(); layoutManager.load();
//prepare the signal handlers //prepare the signal handlers
signal( SIGUSR1, catchSIGUSR1 ); signal(SIGUSR1, catchSignal);
signal( SIGUSR2, catchSIGUSR2 ); signal(SIGUSR2, catchSignal);
signal(SIGINT, catchSignal);
signal(SIGTERM, catchSignal);
//and run the program! //and run the program!
int result = app.exec(); 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. //Release every synthetic input and stop binding timers before closing X11.
layoutManager.release(); layoutManager.release();
releaseAllSyntheticInputs();
close(signalSockets[0]);
close(signalSockets[1]);
signalSockets[0] = -1;
signalSockets[1] = -1;
//cleanup X11 display connection //cleanup X11 display connection
cleanupDisplay(); cleanupDisplay();
+53 -8
View File
@@ -2,6 +2,7 @@
#include "bindingdialog.h" #include "bindingdialog.h"
#include "button.h" #include "button.h"
#include "inputbinding.h" #include "inputbinding.h"
#include "event.h"
#include <QElapsedTimer> #include <QElapsedTimer>
#include <QApplication> #include <QApplication>
@@ -10,6 +11,8 @@
#include <QTest> #include <QTest>
#include <QTextStream> #include <QTextStream>
#include <algorithm>
class RecorderProbe : public BindingDialog { class RecorderProbe : public BindingDialog {
public: public:
RecorderProbe() : BindingDialog("Test control", true) {} RecorderProbe() : BindingDialog("Test control", true) {}
@@ -68,6 +71,7 @@ public:
QVector<QPair<int, bool>> compositeEvents; QVector<QPair<int, bool>> compositeEvents;
bool currentBindingUsesMouse() const { return useMouse; } bool currentBindingUsesMouse() const { return useMouse; }
bool bindingIsDown() const { return isDown; } bool bindingIsDown() const { return isDown; }
int pressedKey() const { return downkey; }
protected: protected:
void compositeKeyEvent(int keycode, bool press) override { void compositeKeyEvent(int keycode, bool press) override {
compositeEvents.append(qMakePair(keycode, press)); compositeEvents.append(qMakePair(keycode, press));
@@ -85,9 +89,11 @@ private slots:
void recorderModifiersRepeatLimitAndFinish(); void recorderModifiersRepeatLimitAndFinish();
void runnerOrderTimingRetriggerAndCancel(); void runnerOrderTimingRetriggerAndCancel();
void preservePlainButtonModes(); void preservePlainButtonModes();
void axisCompositeRearmsWithoutRepeating(); void axisCompositeChangesDirectionWithoutRepeating();
void axisCompositeRearmsWithZeroDeadzone(); void axisCompositeRearmsWithZeroDeadzone();
void axisGradientReleaseKeepsActiveDirection(); void axisGradientReleaseKeepsActiveDirection();
void axisChangesDirectionWithoutNeutral();
void syntheticInputReferenceCounts();
}; };
void BindingTests::parseAndSerializeSequences() { void BindingTests::parseAndSerializeSequences() {
@@ -324,7 +330,7 @@ void BindingTests::preservePlainButtonModes() {
QCOMPARE(cancelling.compositeEvents, cancelled); QCOMPARE(cancelling.compositeEvents, cancelled);
} }
void BindingTests::axisCompositeRearmsWithoutRepeating() { void BindingTests::axisCompositeChangesDirectionWithoutRepeating() {
AxisProbe axis; AxisProbe axis;
QString input("Gradient, +sequence 50 10+11, -sequence 50 12+13"); QString input("Gradient, +sequence 50 10+11, -sequence 50 12+13");
QTextStream reader(&input); QTextStream reader(&input);
@@ -335,17 +341,18 @@ void BindingTests::axisCompositeRearmsWithoutRepeating() {
QTest::qWait(80); QTest::qWait(80);
QCOMPARE(axis.compositeEvents.size(), 4); QCOMPARE(axis.compositeEvents.size(), 4);
axis.jsevent(-25000); axis.jsevent(-25000);
QTest::qWait(80); QTRY_COMPARE_WITH_TIMEOUT(axis.compositeEvents.size(), 8, 150);
QCOMPARE(axis.compositeEvents.size(), 4); QCOMPARE(axis.compositeEvents.at(4), qMakePair(12, true));
QCOMPARE(axis.compositeEvents.at(5), qMakePair(13, true));
axis.jsevent(0); axis.jsevent(0);
axis.jsevent(20000); 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(0);
axis.jsevent(-20000); axis.jsevent(-20000);
QTRY_COMPARE_WITH_TIMEOUT(axis.compositeEvents.size(), 12, 150); QTRY_COMPARE_WITH_TIMEOUT(axis.compositeEvents.size(), 16, 150);
QCOMPARE(axis.compositeEvents.at(8), qMakePair(12, true)); QCOMPARE(axis.compositeEvents.at(12), qMakePair(12, true));
QCOMPARE(axis.compositeEvents.at(9), qMakePair(13, true)); QCOMPARE(axis.compositeEvents.at(13), qMakePair(13, true));
} }
void BindingTests::axisCompositeRearmsWithZeroDeadzone() { void BindingTests::axisCompositeRearmsWithZeroDeadzone() {
@@ -380,5 +387,43 @@ void BindingTests::axisGradientReleaseKeepsActiveDirection() {
QVERIFY(axis.currentBindingUsesMouse()); 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) QTEST_MAIN(BindingTests)
#include "test_bindings.moc" #include "test_bindings.moc"