From 9cd6665a300c3225370b42e88704211981544403 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Wed, 29 Jul 2026 02:24:48 -0400 Subject: [PATCH] A bit of code cleanup. distro-packages created. --- CLAUDE.md | 51 +++++++++ .../Arch-Linux/thunderpad-git/PKGBUILD | 41 +++++++ icons/CMakeLists.txt | 4 +- src/axis.cpp | 40 +++++-- src/axis.h | 4 + src/axis_edit.cpp | 102 +++++++++++++++++- src/axis_edit.h | 12 +++ src/button.cpp | 7 +- src/joyslider.h | 3 + src/quickset.cpp | 7 +- 10 files changed, 256 insertions(+), 15 deletions(-) create mode 100644 distro-packages/Arch-Linux/thunderpad-git/PKGBUILD diff --git a/CLAUDE.md b/CLAUDE.md index 9788333..bcc7735 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -367,3 +367,54 @@ If Qt6 migration proves unstable: - **Qt6 Native Interface**: Future versions might want to revisit using Qt6's native interface once it's more stable **✅ SUCCESS: Qt6 migration complete and functional. Ready for accessibility testing and production use.** + +## Accessible Deadzone Configuration + +**Implementation Date**: 2025-01-07 +**Status**: ✅ **COMPLETE** - Fully accessible deadzone configuration implemented + +### Enhancement Summary +Replaced the visual-only JoySlider deadzone configuration with a fully accessible interface that supports both quick presets and precise manual adjustment. This addresses a major accessibility gap for blind users who previously could not configure joystick deadzones. + +### Changes Made + +1. **Accessible UI Components Added**: + - **Quick Preset Buttons**: 5 buttons for common deadzone values (None, Small, Medium, Large, Extra Large) + - **Precise Spin Boxes**: Separate controls for Deadzone Size and Extreme Zone Size (0-32767 range) + - **Real-time Status Display**: Shows current joystick position and which zone it's in + - **Proper Labels**: Screen reader compatible labels with keyboard mnemonics + +2. **Code Changes**: + - **`axis_edit.h`**: Added new UI component declarations and slot methods + - **`axis_edit.cpp`**: Implemented accessible deadzone configuration interface + - **`joyslider.h`**: Added `setDeadZone()` and `setXZone()` setter methods + - **Integration**: Keeps visual JoySlider synchronized with accessible controls + +3. **Accessibility Features**: + - **Full keyboard navigation**: All controls accessible via Tab/Shift+Tab + - **Screen reader compatibility**: Proper `&` mnemonics and `setBuddy()` associations + - **Real-time feedback**: Live updates when joystick position changes + - **Logical tab order**: Follows accessibility best practices + - **No mouse dependency**: All functionality available via keyboard + +### Preset Values +- **None**: 0 (no deadzone) +- **Small**: 1500 (light deadzone) +- **Medium**: 3000 (current default) +- **Large**: 6000 (heavy deadzone) +- **Extra Large**: 10000 (very heavy deadzone) + +### User Benefits +- **Blind users**: Can now configure deadzones without visual feedback +- **Quick setup**: Most users can click a preset and finish configuration +- **Precise control**: Advanced users can fine-tune exact values +- **Discoverability**: Presets help users understand appropriate deadzone ranges +- **Immediate feedback**: Real-time status shows joystick position vs. configured zones + +### Technical Implementation +- **Maintains compatibility**: Visual JoySlider remains functional for existing users +- **Synchronized updates**: All controls stay in sync when values change +- **Proper encapsulation**: Added public setter methods to JoySlider class +- **Signal/slot architecture**: Follows Qt patterns for responsive UI updates + +This enhancement represents a significant accessibility improvement, making deadzone configuration fully accessible to screen reader users while maintaining all existing functionality. diff --git a/distro-packages/Arch-Linux/thunderpad-git/PKGBUILD b/distro-packages/Arch-Linux/thunderpad-git/PKGBUILD new file mode 100644 index 0000000..979d2de --- /dev/null +++ b/distro-packages/Arch-Linux/thunderpad-git/PKGBUILD @@ -0,0 +1,41 @@ +# Maintainer: Storm Dragon +# shellcheck disable=SC2034,SC2154 + +pkgname=thunderpad-git +_pkgname=thunderpad +pkgver=r159.a0f6f34 +pkgrel=1 +pkgdesc="Accessible joypad to keyboard and mouse mapper" +arch=('aarch64' 'x86_64') +url="https://git.stormux.org/storm/thunderpad" +license=('GPL-2.0-only') +depends=('hicolor-icon-theme' 'libx11' 'libxtst' 'qt6-base' 'systemd-libs') +makedepends=('cmake' 'git') +provides=('thunderpad') +conflicts=('thunderpad') +source=("${_pkgname}::git+https://git.stormux.org/storm/thunderpad.git") +sha256sums=('SKIP') + +pkgver() { + cd "${_pkgname}" || return 1 + + printf 'r%s.%s' \ + "$(git rev-list --count HEAD)" \ + "$(git rev-parse --short=7 HEAD)" +} + +build() { + cmake \ + -B build \ + -S "${_pkgname}" \ + -DCMAKE_BUILD_TYPE=None \ + -DCMAKE_INSTALL_PREFIX=/usr \ + -DWITH_LIBUDEV=ON + cmake --build build +} + +package() { + DESTDIR="${pkgdir}" cmake --install build + install -Dm644 "${_pkgname}/LICENSE.txt" \ + "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" +} diff --git a/icons/CMakeLists.txt b/icons/CMakeLists.txt index 2733874..a8e904f 100644 --- a/icons/CMakeLists.txt +++ b/icons/CMakeLists.txt @@ -1,2 +1,2 @@ -install(FILES gamepad4-24x24.png DESTINATION "share/icons/hicolor/24x24/apps" RENAME qjoypad.png) -install(FILES gamepad4-64x64.png DESTINATION "share/icons/hicolor/64x64/apps" RENAME qjoypad.png) +install(FILES gamepad4-24x24.png DESTINATION "share/icons/hicolor/24x24/apps" RENAME thunderpad.png) +install(FILES gamepad4-64x64.png DESTINATION "share/icons/hicolor/64x64/apps" RENAME thunderpad.png) diff --git a/src/axis.cpp b/src/axis.cpp index 2d810be..d618c9e 100644 --- a/src/axis.cpp +++ b/src/axis.cpp @@ -2,6 +2,7 @@ #include "event.h" #include "time.h" #include +#include #define sqr(a) ((a)*(a)) #define cub(a) ((a)*(a)*(a)) @@ -18,6 +19,8 @@ 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; } @@ -125,6 +128,10 @@ bool Axis::read( QTextStream &stream ) { interpretation = ZeroOne; gradient = false; absolute = false; + // Stop any running timer when switching to ZeroOne mode + timer.stop(); + disconnect(&timer, SIGNAL(timeout()), 0, 0); + tick = 0; } else if (*it == "absolute") { interpretation = AbsolutePos; @@ -220,9 +227,12 @@ void Axis::jsevent( int value ) { else state = (value + JOYMAX) / 2; //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 (gradient) { duration = 0; release(); @@ -234,33 +244,41 @@ void Axis::jsevent( int value ) { //if was off but now should be on: else if (!isOn && abs(state) >= dZone) { isOn = true; + stateChanged = true; if (gradient) { duration = (abs(state) * FREQ) / JOYMAX; connect(&timer, SIGNAL(timeout()), this, SLOT(timerCalled())); timer.start(MSEC); } } - //otherwise, state doesn't change! Don't touch it. - else return; + //if in gradient mode and state changed, update duration + else if (gradient && 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. - if (!gradient) { + //non-gradient needs to be told to move only when state changes. + if (!gradient && stateChanged) { move(isOn); } } void Axis::toDefault() { release(); + timer.stop(); + disconnect(&timer, SIGNAL(timeout()), 0, 0); + isOn = false; + tick = 0; + duration = 0; + lastKeyPressTime = 0; interpretation = ZeroOne; gradient = false; absolute = false; throttle = 0; maxSpeed = 100; transferCurve = Quadratic; - sensitivity = 1.0F; + sensitivity = 1.0F; dZone = DZONE; - tick = 0; xZone = XZONE; mode = Keyboard; pkeycode = 0; @@ -379,6 +397,16 @@ void Axis::move( bool press ) { //this would only happen in odd circumstances involving the setup //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 (state != 0) { useMouse = (state > 0) ? puseMouse : nuseMouse; diff --git a/src/axis.h b/src/axis.h index 4f8f640..ea47d52 100644 --- a/src/axis.h +++ b/src/axis.h @@ -105,6 +105,10 @@ class Axis : public QObject { //this just decides how long it stays down each cycle. 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/axis_edit.cpp b/src/axis_edit.cpp index 4d5ef3b..df2091b 100644 --- a/src/axis_edit.cpp +++ b/src/axis_edit.cpp @@ -80,6 +80,62 @@ AxisEdit::AxisEdit( Axis* ax ) slider = new JoySlider(axis->dZone, axis->xZone, axis->state, this); v->addWidget(slider); + // Accessible deadzone configuration + deadzoneBox = new QGroupBox(tr("Deadzone Configuration"), this); + QGridLayout *dzLayout = new QGridLayout(deadzoneBox); + + // Preset buttons + QLabel *presetLabel = new QLabel(tr("&Quick Presets:"), deadzoneBox); + dzLayout->addWidget(presetLabel, 0, 0, 1, 2); + + presetNone = new QPushButton(tr("None (0)"), deadzoneBox); + presetSmall = new QPushButton(tr("Small (1500)"), deadzoneBox); + presetMedium = new QPushButton(tr("Medium (3000)"), deadzoneBox); + presetLarge = new QPushButton(tr("Large (6000)"), deadzoneBox); + presetXLarge = new QPushButton(tr("Extra Large (10000)"), deadzoneBox); + + connect(presetNone, &QPushButton::clicked, this, &AxisEdit::deadzonePresetClicked); + connect(presetSmall, &QPushButton::clicked, this, &AxisEdit::deadzonePresetClicked); + connect(presetMedium, &QPushButton::clicked, this, &AxisEdit::deadzonePresetClicked); + connect(presetLarge, &QPushButton::clicked, this, &AxisEdit::deadzonePresetClicked); + connect(presetXLarge, &QPushButton::clicked, this, &AxisEdit::deadzonePresetClicked); + + dzLayout->addWidget(presetNone, 1, 0); + dzLayout->addWidget(presetSmall, 1, 1); + dzLayout->addWidget(presetMedium, 1, 2); + dzLayout->addWidget(presetLarge, 1, 3); + dzLayout->addWidget(presetXLarge, 1, 4); + + // Spin boxes for precise control + QLabel *deadzoneLabel = new QLabel(tr("&Deadzone Size:"), deadzoneBox); + spinDeadzone = new QSpinBox(deadzoneBox); + spinDeadzone->setRange(0, 32767); + spinDeadzone->setValue(axis->dZone); + spinDeadzone->setSuffix(tr(" (0-32767)")); + connect(spinDeadzone, QOverload::of(&QSpinBox::valueChanged), this, &AxisEdit::deadzoneValueChanged); + deadzoneLabel->setBuddy(spinDeadzone); + + QLabel *extremeZoneLabel = new QLabel(tr("&Extreme Zone Size:"), deadzoneBox); + spinExtremeZone = new QSpinBox(deadzoneBox); + spinExtremeZone->setRange(0, 32767); + spinExtremeZone->setValue(axis->xZone); + spinExtremeZone->setSuffix(tr(" (0-32767)")); + connect(spinExtremeZone, QOverload::of(&QSpinBox::valueChanged), this, &AxisEdit::extremeZoneValueChanged); + extremeZoneLabel->setBuddy(spinExtremeZone); + + dzLayout->addWidget(deadzoneLabel, 2, 0); + dzLayout->addWidget(spinDeadzone, 2, 1, 1, 2); + dzLayout->addWidget(extremeZoneLabel, 3, 0); + dzLayout->addWidget(spinExtremeZone, 3, 1, 1, 2); + + // Current position and status display + lblCurrentPosition = new QLabel(tr("Current Position: 0"), deadzoneBox); + lblZoneStatus = new QLabel(tr("Status: In Deadzone"), deadzoneBox); + dzLayout->addWidget(lblCurrentPosition, 4, 0, 1, 2); + dzLayout->addWidget(lblZoneStatus, 4, 2, 1, 3); + + v->addWidget(deadzoneBox); + keyBox = new QFrame(this); keyBox->setFrameStyle( QFrame::Box | QFrame::Sunken ); h = new QHBoxLayout(keyBox); @@ -119,6 +175,19 @@ void AxisEdit::show() { void AxisEdit::setState( int val ) { slider->setValue( val ); + + // Update accessible deadzone status display + lblCurrentPosition->setText(tr("Current Position: %1").arg(val)); + + QString status; + if (abs(val) < spinDeadzone->value()) { + status = tr("Status: In Deadzone"); + } else if (abs(val) < spinExtremeZone->value()) { + status = tr("Status: In Active Zone"); + } else { + status = tr("Status: In Extreme Zone"); + } + lblZoneStatus->setText(status); } void AxisEdit::gradientChanged( int index ) { bool gradient = index != Axis::ZeroOne; @@ -176,6 +245,35 @@ void AxisEdit::throttleChanged( int index ) { slider->setThrottle( index - 1 ); } +void AxisEdit::deadzonePresetClicked() { + QPushButton *button = qobject_cast(sender()); + if (!button) return; + + int deadzoneValue = 0; + if (button == presetNone) deadzoneValue = 0; + else if (button == presetSmall) deadzoneValue = 1500; + else if (button == presetMedium) deadzoneValue = 3000; + else if (button == presetLarge) deadzoneValue = 6000; + else if (button == presetXLarge) deadzoneValue = 10000; + + spinDeadzone->setValue(deadzoneValue); + slider->setDeadZone(deadzoneValue); +} + +void AxisEdit::deadzoneValueChanged(int value) { + slider->setDeadZone(value); + + // Update status display with current joystick position + setState(axis->state); +} + +void AxisEdit::extremeZoneValueChanged(int value) { + slider->setXZone(value); + + // Update status display with current joystick position + setState(axis->state); +} + void AxisEdit::accept() { axis->interpretation = (Axis::Interpretation)chkGradient->currentIndex(); axis->gradient = axis->interpretation != Axis::ZeroOne; @@ -184,8 +282,8 @@ void AxisEdit::accept() { axis->transferCurve = (Axis::TransferCurve)cmbTransferCurve->currentIndex(); axis->sensitivity = spinSensitivity->value(); axis->throttle = cmbThrottle->currentIndex() - 1; - axis->dZone = slider->deadZone(); - axis->xZone = slider->xZone(); + axis->dZone = spinDeadzone->value(); + axis->xZone = spinExtremeZone->value(); axis->mode = (Axis::Mode) cmbMode->currentIndex(); axis->pkeycode = btnPos->getValue(); axis->nkeycode = btnNeg->getValue(); diff --git a/src/axis_edit.h b/src/axis_edit.h index bf50e8a..e067d1a 100644 --- a/src/axis_edit.h +++ b/src/axis_edit.h @@ -11,6 +11,9 @@ #include //for my home-brewed widgets #include "joyslider.h" +#include +#include +#include #include "keycode.h" class AxisEdit : public QDialog { @@ -28,6 +31,9 @@ class AxisEdit : public QDialog { void modeChanged( int index ); void transferCurveChanged( int index ); void throttleChanged( int index ); + void deadzonePresetClicked(); + void deadzoneValueChanged(int value); + void extremeZoneValueChanged(int value); void accept(); protected: //the associated Axis that needs to be set. @@ -40,6 +46,12 @@ class AxisEdit : public QDialog { QDoubleSpinBox *spinSensitivity; KeyButton *btnNeg, *btnPos; JoySlider *slider; + + // Accessible deadzone controls + QGroupBox *deadzoneBox; + QPushButton *presetNone, *presetSmall, *presetMedium, *presetLarge, *presetXLarge; + QSpinBox *spinDeadzone, *spinExtremeZone; + QLabel *lblCurrentPosition, *lblZoneStatus; }; #endif diff --git a/src/button.cpp b/src/button.cpp index 4e91b6a..ec2d8ec 100644 --- a/src/button.cpp +++ b/src/button.cpp @@ -79,7 +79,6 @@ void Button::write( QTextStream &stream ) { void Button::release() { if (isDown) { click(false); - isDown = true; } } @@ -131,12 +130,16 @@ void Button::jsevent( int value ) { } void Button::toDefault() { + release(); + timer.stop(); + disconnect(&timer, SIGNAL(timeout()), 0, 0); + isButtonPressed = false; + tick = 0; rapidfire = false; sticky = false; useMouse = false; keycode = 0; hasLayout = false; - timer.stop(); } bool Button::isDefault() { diff --git a/src/joyslider.h b/src/joyslider.h index 823f95f..d12caef 100644 --- a/src/joyslider.h +++ b/src/joyslider.h @@ -38,6 +38,9 @@ class JoySlider : public QWidget //get the current settings int deadZone() { return deadzone; } int xZone() { return xzone; } + //set the current settings + void setDeadZone(int dz) { deadzone = dz; update(); } + void setXZone(int xz) { xzone = xz; update(); } protected: //all for getting the widget to look right: void drawBox( int x, int width ); diff --git a/src/quickset.cpp b/src/quickset.cpp index 041efda..0ceda2d 100644 --- a/src/quickset.cpp +++ b/src/quickset.cpp @@ -39,12 +39,13 @@ void QuickSet::jsevent(const js_event &msg ) { } } else if (type == JS_EVENT_AXIS) { - //require a signal strength of at least 5000 to consider an axis moved. - if (abs(msg.value) < 5000) return; - //capture the axis that moved if (msg.number < joypad->axes.size()) { Axis* axis = joypad->axes[msg.number]; + + //use the axis's configured deadzone instead of hardcoded 5000 + //this prevents phantom input from triggering quickset + if (axis->inDeadZone(msg.value)) return; //grab a keycode for that axis and that direction setting = true;