A bit of code cleanup. distro-packages created.

This commit is contained in:
Storm Dragon
2026-07-29 02:24:48 -04:00
parent a0f6f34701
commit 9cd6665a30
10 changed files with 256 additions and 15 deletions
+51
View File
@@ -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 - **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.** **✅ 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.
@@ -0,0 +1,41 @@
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
# 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"
}
+2 -2
View File
@@ -1,2 +1,2 @@
install(FILES gamepad4-24x24.png DESTINATION "share/icons/hicolor/24x24/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 qjoypad.png) install(FILES gamepad4-64x64.png DESTINATION "share/icons/hicolor/64x64/apps" RENAME thunderpad.png)
+33 -5
View File
@@ -2,6 +2,7 @@
#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))
@@ -18,6 +19,8 @@ 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;
} }
@@ -125,6 +128,10 @@ bool Axis::read( QTextStream &stream ) {
interpretation = ZeroOne; interpretation = ZeroOne;
gradient = false; gradient = false;
absolute = 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") { else if (*it == "absolute") {
interpretation = AbsolutePos; interpretation = AbsolutePos;
@@ -220,9 +227,12 @@ void Axis::jsevent( int value ) {
else else
state = (value + JOYMAX) / 2; state = (value + JOYMAX) / 2;
//set isOn, deal with state changing. //set isOn, deal with state changing.
bool stateChanged = false;
//if was on but now should be off: //if was on but now should be off:
if (isOn && abs(state) <= dZone) { if (isOn && abs(state) <= dZone) {
isOn = false; isOn = false;
stateChanged = true;
if (gradient) { if (gradient) {
duration = 0; duration = 0;
release(); release();
@@ -234,24 +244,33 @@ void Axis::jsevent( int value ) {
//if was off but now should be on: //if was off but now should be on:
else if (!isOn && abs(state) >= dZone) { else if (!isOn && abs(state) >= dZone) {
isOn = true; isOn = true;
stateChanged = true;
if (gradient) { 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()));
timer.start(MSEC); timer.start(MSEC);
} }
} }
//otherwise, state doesn't change! Don't touch it. //if in gradient mode and state changed, update duration
else return; else if (gradient && abs(state) > dZone) {
duration = (abs(state) * FREQ) / JOYMAX;
}
//gradient will trigger movement on its own via timer(). //gradient will trigger movement on its own via timer().
//non-gradient needs to be told to move. //non-gradient needs to be told to move only when state changes.
if (!gradient) { if (!gradient && stateChanged) {
move(isOn); move(isOn);
} }
} }
void Axis::toDefault() { void Axis::toDefault() {
release(); release();
timer.stop();
disconnect(&timer, SIGNAL(timeout()), 0, 0);
isOn = false;
tick = 0;
duration = 0;
lastKeyPressTime = 0;
interpretation = ZeroOne; interpretation = ZeroOne;
gradient = false; gradient = false;
absolute = false; absolute = false;
@@ -260,7 +279,6 @@ void Axis::toDefault() {
transferCurve = Quadratic; transferCurve = Quadratic;
sensitivity = 1.0F; sensitivity = 1.0F;
dZone = DZONE; dZone = DZONE;
tick = 0;
xZone = XZONE; xZone = XZONE;
mode = Keyboard; mode = Keyboard;
pkeycode = 0; pkeycode = 0;
@@ -379,6 +397,16 @@ void Axis::move( bool press ) {
//this would only happen in odd circumstances involving the setup //this would only happen in odd circumstances involving the setup
//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 (state != 0) { if (state != 0) {
useMouse = (state > 0) ? puseMouse : nuseMouse; useMouse = (state > 0) ? puseMouse : nuseMouse;
+4
View File
@@ -105,6 +105,10 @@ class Axis : public QObject {
//this just decides how long it stays down each cycle. //this just decides how long it stays down each cycle.
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();
}; };
+100 -2
View File
@@ -80,6 +80,62 @@ AxisEdit::AxisEdit( Axis* ax )
slider = new JoySlider(axis->dZone, axis->xZone, axis->state, this); slider = new JoySlider(axis->dZone, axis->xZone, axis->state, this);
v->addWidget(slider); 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<int>::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<int>::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 = new QFrame(this);
keyBox->setFrameStyle( QFrame::Box | QFrame::Sunken ); keyBox->setFrameStyle( QFrame::Box | QFrame::Sunken );
h = new QHBoxLayout(keyBox); h = new QHBoxLayout(keyBox);
@@ -119,6 +175,19 @@ void AxisEdit::show() {
void AxisEdit::setState( int val ) { void AxisEdit::setState( int val ) {
slider->setValue( 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 ) { void AxisEdit::gradientChanged( int index ) {
bool gradient = index != Axis::ZeroOne; bool gradient = index != Axis::ZeroOne;
@@ -176,6 +245,35 @@ void AxisEdit::throttleChanged( int index ) {
slider->setThrottle( index - 1 ); slider->setThrottle( index - 1 );
} }
void AxisEdit::deadzonePresetClicked() {
QPushButton *button = qobject_cast<QPushButton*>(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() { void AxisEdit::accept() {
axis->interpretation = (Axis::Interpretation)chkGradient->currentIndex(); axis->interpretation = (Axis::Interpretation)chkGradient->currentIndex();
axis->gradient = axis->interpretation != Axis::ZeroOne; axis->gradient = axis->interpretation != Axis::ZeroOne;
@@ -184,8 +282,8 @@ void AxisEdit::accept() {
axis->transferCurve = (Axis::TransferCurve)cmbTransferCurve->currentIndex(); axis->transferCurve = (Axis::TransferCurve)cmbTransferCurve->currentIndex();
axis->sensitivity = spinSensitivity->value(); axis->sensitivity = spinSensitivity->value();
axis->throttle = cmbThrottle->currentIndex() - 1; axis->throttle = cmbThrottle->currentIndex() - 1;
axis->dZone = slider->deadZone(); axis->dZone = spinDeadzone->value();
axis->xZone = slider->xZone(); axis->xZone = spinExtremeZone->value();
axis->mode = (Axis::Mode) cmbMode->currentIndex(); axis->mode = (Axis::Mode) cmbMode->currentIndex();
axis->pkeycode = btnPos->getValue(); axis->pkeycode = btnPos->getValue();
axis->nkeycode = btnNeg->getValue(); axis->nkeycode = btnNeg->getValue();
+12
View File
@@ -11,6 +11,9 @@
#include <QDialogButtonBox> #include <QDialogButtonBox>
//for my home-brewed widgets //for my home-brewed widgets
#include "joyslider.h" #include "joyslider.h"
#include <QPushButton>
#include <QGroupBox>
#include <QGridLayout>
#include "keycode.h" #include "keycode.h"
class AxisEdit : public QDialog { class AxisEdit : public QDialog {
@@ -28,6 +31,9 @@ class AxisEdit : public QDialog {
void modeChanged( int index ); void modeChanged( int index );
void transferCurveChanged( int index ); void transferCurveChanged( int index );
void throttleChanged( int index ); void throttleChanged( int index );
void deadzonePresetClicked();
void deadzoneValueChanged(int value);
void extremeZoneValueChanged(int value);
void accept(); void accept();
protected: protected:
//the associated Axis that needs to be set. //the associated Axis that needs to be set.
@@ -40,6 +46,12 @@ class AxisEdit : public QDialog {
QDoubleSpinBox *spinSensitivity; QDoubleSpinBox *spinSensitivity;
KeyButton *btnNeg, *btnPos; KeyButton *btnNeg, *btnPos;
JoySlider *slider; JoySlider *slider;
// Accessible deadzone controls
QGroupBox *deadzoneBox;
QPushButton *presetNone, *presetSmall, *presetMedium, *presetLarge, *presetXLarge;
QSpinBox *spinDeadzone, *spinExtremeZone;
QLabel *lblCurrentPosition, *lblZoneStatus;
}; };
#endif #endif
+5 -2
View File
@@ -79,7 +79,6 @@ void Button::write( QTextStream &stream ) {
void Button::release() { void Button::release() {
if (isDown) { if (isDown) {
click(false); click(false);
isDown = true;
} }
} }
@@ -131,12 +130,16 @@ void Button::jsevent( int value ) {
} }
void Button::toDefault() { void Button::toDefault() {
release();
timer.stop();
disconnect(&timer, SIGNAL(timeout()), 0, 0);
isButtonPressed = false;
tick = 0;
rapidfire = false; rapidfire = false;
sticky = false; sticky = false;
useMouse = false; useMouse = false;
keycode = 0; keycode = 0;
hasLayout = false; hasLayout = false;
timer.stop();
} }
bool Button::isDefault() { bool Button::isDefault() {
+3
View File
@@ -38,6 +38,9 @@ class JoySlider : public QWidget
//get the current settings //get the current settings
int deadZone() { return deadzone; } int deadZone() { return deadzone; }
int xZone() { return xzone; } int xZone() { return xzone; }
//set the current settings
void setDeadZone(int dz) { deadzone = dz; update(); }
void setXZone(int xz) { xzone = xz; update(); }
protected: protected:
//all for getting the widget to look right: //all for getting the widget to look right:
void drawBox( int x, int width ); void drawBox( int x, int width );
+4 -3
View File
@@ -39,13 +39,14 @@ void QuickSet::jsevent(const js_event &msg ) {
} }
} }
else if (type == JS_EVENT_AXIS) { 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 //capture the axis that moved
if (msg.number < joypad->axes.size()) { if (msg.number < joypad->axes.size()) {
Axis* axis = joypad->axes[msg.number]; 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 //grab a keycode for that axis and that direction
setting = true; setting = true;
bool isMouse = false; bool isMouse = false;