A bit of code cleanup. distro-packages created.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
+34
-6
@@ -2,6 +2,7 @@
|
||||
#include "event.h"
|
||||
#include "time.h"
|
||||
#include <QRegularExpression>
|
||||
#include <QDateTime>
|
||||
|
||||
#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;
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
+100
-2
@@ -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<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->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<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() {
|
||||
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();
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
#include <QDialogButtonBox>
|
||||
//for my home-brewed widgets
|
||||
#include "joyslider.h"
|
||||
#include <QPushButton>
|
||||
#include <QGroupBox>
|
||||
#include <QGridLayout>
|
||||
#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
|
||||
|
||||
+5
-2
@@ -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() {
|
||||
|
||||
@@ -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 );
|
||||
|
||||
+4
-3
@@ -39,13 +39,14 @@ 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;
|
||||
bool isMouse = false;
|
||||
|
||||
Reference in New Issue
Block a user