Fix up a few things.
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
ThunderPad is a Qt 6/C++ joystick-to-keyboard and mouse mapper for X11. Application code and headers live in `src/`; keep related widget, model, and input-handling files together there. `icons/` contains installed PNG assets, while `thunderpad.desktop` defines the desktop launcher. CMake helpers are in `cmake/`, Arch packaging is under `distro-packages/Arch-Linux/`, and Qt Test coverage lives in `tests/`. Treat `build/` and other `build-*` directories as generated output.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
- `cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug` configures a development build.
|
||||
- `cmake --build build -j"$(nproc)"` compiles ThunderPad with the repository's warning flags.
|
||||
- `./build/src/thunderpad` runs the built GUI in an active X11 session.
|
||||
- `cmake -S . -B build -DWITH_LIBUDEV=OFF` configures systems without libudev development files.
|
||||
- `cmake --install build` installs to the configured prefix and may require elevated privileges outside a user-local prefix.
|
||||
|
||||
The `tests/` directory contains `binding_tests`, but the top-level CMake configuration does not currently register that directory. If changing test-covered behavior, wire it into the root build with CTest, then run `ctest --test-dir build --output-on-failure`. Do not report tests as run until that integration exists.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
|
||||
Match nearby Qt/C++ code: four-space indentation, opening braces on the same line, and Qt containers and strings where already used. Use PascalCase for classes, camelCase for methods and local variables, and lowercase snake_case filenames such as `inputbinding.cpp` or `axis_edit.h`. Keep declarations in the matching header and avoid unrelated modernization. GCC builds use `-Wall -Wextra -Werror -pedantic`, so new warnings fail the build.
|
||||
|
||||
## Testing & Accessibility
|
||||
|
||||
Add focused Qt Test slots to `tests/test_bindings.cpp`; name them after observable behavior and add `_data()` companions for table-driven cases. GUI changes must also be checked with keyboard-only Tab/Shift+Tab navigation and a screen reader. Verify labels, focus order, and dialog escape paths; screenshots alone are insufficient accessibility evidence.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
Recent commits use short, sentence-case summaries such as `Fixed quickbinding bug.` Keep each commit focused and describe the user-visible outcome. Pull requests should explain the problem and solution, list build/test commands and results, link relevant issues, and include manual accessibility results for UI changes. Note required devices or X11 conditions and attach screenshots only when they add useful visual context.
|
||||
@@ -1,420 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**ThunderPad** is an accessibility-focused fork of QJoyPad - a Qt-based Linux application that maps joystick/gamepad inputs to keyboard keys and mouse actions. ThunderPad was completely redesigned for full screen reader compatibility and keyboard navigation support. It's developed for the Stormux project with accessibility as the top priority.
|
||||
|
||||
### Key Differentiators from QJoyPad
|
||||
- **Full keyboard navigation** - No mouse required for any configuration task
|
||||
- **Screen reader compatible** - Works seamlessly with Orca and other assistive technologies
|
||||
- **Window mode by default** - Opens configuration window directly (use `--tray` for system tray)
|
||||
- **English-only interface** - Translation system removed for simplicity
|
||||
- **Separate config directory** - Uses `~/.config/thunderpad/` to coexist with QJoyPad
|
||||
|
||||
## Critical Development Principle
|
||||
|
||||
**🔴 ACCESSIBILITY FIRST: Screen reader accessibility is the top priority for ANY code changes.**
|
||||
|
||||
All modifications must maintain or improve:
|
||||
- Complete keyboard navigation (Tab/Shift+Tab/Arrow keys)
|
||||
- Screen reader compatibility (proper labels, focus management)
|
||||
- Logical tab order and focus flow
|
||||
- Accessible widget choices (lists vs. clickable grids)
|
||||
|
||||
## Build System
|
||||
|
||||
This project uses CMake with Qt6 dependencies:
|
||||
|
||||
### Build Commands
|
||||
```bash
|
||||
# Standard build process
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
make -j$(nproc)
|
||||
|
||||
# For installation
|
||||
make install
|
||||
|
||||
# For development with debug symbols
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Debug
|
||||
```
|
||||
|
||||
### CMake Configuration Options
|
||||
- `-DDEVICE_DIR=/dev/input` - Set joystick device directory (default: /dev/input)
|
||||
- `-DWITH_LIBUDEV=OFF` - Disable libudev support for automatic device detection
|
||||
- `-DPLAIN_KEYS=ON` - Use standard X11 key names instead of readable names
|
||||
- `-DCMAKE_INSTALL_PREFIX=/usr` - Set installation prefix
|
||||
|
||||
### Dependencies
|
||||
- Qt6Widgets, Qt6Gui
|
||||
- libudev (optional, for automatic device detection)
|
||||
- Linux joystick driver support
|
||||
- X11 libraries (libX11, libXtst) for event generation
|
||||
- For accessibility testing: Orca screen reader or Cthulhu screen reader
|
||||
|
||||
## Code Architecture
|
||||
|
||||
### Project Rebranding (QJoyPad → ThunderPad)
|
||||
**Version**: 2025.06.30
|
||||
**Executable**: `thunderpad`
|
||||
**Config Directory**: `~/.config/thunderpad/`
|
||||
**PID File**: `/tmp/thunderpad.pid`
|
||||
**Desktop Entry**: `thunderpad.desktop`
|
||||
|
||||
All source code has been systematically updated:
|
||||
- Header guards: `QJOYPAD_*` → `THUNDERPAD_*`
|
||||
- Macro definitions: `QJOYPAD_NAME` → `THUNDERPAD_NAME`
|
||||
- Icon paths: `QJOYPAD_ICON24/64` → `THUNDERPAD_ICON24/64`
|
||||
- Comments and references updated throughout
|
||||
|
||||
### Core Components
|
||||
|
||||
1. **LayoutManager** (`layout.h/cpp`) - Central coordinator that manages:
|
||||
- Layout loading/saving to `~/.config/thunderpad/`
|
||||
- Optional system tray icon and menu (when `--tray` used)
|
||||
- Configuration window display (default behavior)
|
||||
- Joystick device detection and management
|
||||
- Inter-process communication via signals (SIGUSR1/SIGUSR2)
|
||||
|
||||
2. **JoyPad** (`joypad.h/cpp`) - Represents individual joystick devices:
|
||||
- File descriptor management for `/dev/input/jsX` devices
|
||||
- Contains collections of Button and Axis objects
|
||||
- Handles joystick events via QSocketNotifier
|
||||
|
||||
3. **Button** (`button.h/cpp`) - Individual button configuration:
|
||||
- Key/mouse button mapping
|
||||
- Sticky mode (toggle on/off)
|
||||
- Rapid fire functionality
|
||||
- Layout switching capability
|
||||
|
||||
4. **Axis** (`axis.h/cpp`) - Joystick axis configuration:
|
||||
- Keyboard key mapping for positive/negative directions
|
||||
- Mouse movement control (relative/absolute)
|
||||
- Sensitivity and dead zone settings
|
||||
- Gradient mode for variable input strength
|
||||
|
||||
5. **Event System** (`event.h/cpp`) - X11 integration for:
|
||||
- Key press/release simulation
|
||||
- Mouse button clicks
|
||||
- Mouse movement
|
||||
|
||||
### Accessibility-Focused UI Components
|
||||
|
||||
**CRITICAL**: All UI components prioritize keyboard navigation and screen reader compatibility.
|
||||
|
||||
- **LayoutEdit** (`layout_edit.h/cpp`) - Main accessible configuration dialog:
|
||||
- QTabWidget for joystick navigation (replaces clickable FlashRadioArray)
|
||||
- Layout management controls at top (dropdown, add/remove/rename buttons)
|
||||
- Import/export functionality
|
||||
- Save/revert controls
|
||||
|
||||
- **JoyPadWidget** (`joypadw.h/cpp`) - Accessible per-device configuration:
|
||||
- **Axes list** - QListWidget showing all axes with current assignments
|
||||
- **Configure Axis button** - Opens configuration for selected axis
|
||||
- **Buttons list** - QListWidget showing all buttons with current assignments
|
||||
- **Configure Button button** - Opens configuration for selected button
|
||||
- **Clear/Quick Set buttons** - Bulk operations
|
||||
- Explicit tab order with `setTabOrder()` calls
|
||||
|
||||
- **ButtonEdit/AxisEdit** - Component-specific configuration dialogs
|
||||
- **Icon** - Floating icon alternative (rarely used in accessibility mode)
|
||||
|
||||
### Accessibility Design Patterns
|
||||
- **QListWidget over clickable grids** - Screen reader navigable
|
||||
- **Dedicated configure buttons** - Instead of click-to-configure interfaces
|
||||
- **QTabWidget for device switching** - Keyboard accessible tabs
|
||||
- **Explicit tab order** - `setTabOrder()` calls ensure logical navigation
|
||||
- **Descriptive widget names** - Proper accessibility labels
|
||||
- **Qt::StrongFocus policy** - Ensures all interactive elements are keyboard reachable
|
||||
|
||||
## Development Workflows
|
||||
|
||||
### Testing Changes
|
||||
```bash
|
||||
# Build and run locally (default: window mode)
|
||||
make
|
||||
DISPLAY=:0 ./thunderpad
|
||||
|
||||
# Test with system tray mode
|
||||
DISPLAY=:0 ./thunderpad --tray
|
||||
|
||||
# Test with specific layout
|
||||
DISPLAY=:0 ./thunderpad "layout-name"
|
||||
|
||||
# Update device list in running instance
|
||||
DISPLAY=:0 ./thunderpad --update
|
||||
|
||||
# Test accessibility with screen reader
|
||||
# Start Orca or Cthulhu first, then launch thunderpad
|
||||
```
|
||||
|
||||
### Accessibility Testing Requirements
|
||||
**Before any code changes are considered complete:**
|
||||
|
||||
1. **Keyboard Navigation Test**:
|
||||
- Tab through entire interface without mouse
|
||||
- Verify all controls are reachable via keyboard
|
||||
- Test arrow key navigation within lists and tabs
|
||||
- Ensure Escape key behavior is consistent
|
||||
|
||||
2. **Screen Reader Test**:
|
||||
- Launch with Orca or Cthulhu running
|
||||
- Verify all interface elements are announced properly
|
||||
- Check that focus changes are announced
|
||||
- Ensure list items have descriptive labels
|
||||
|
||||
3. **Focus Management Test**:
|
||||
- Verify logical tab order (top to bottom, left to right)
|
||||
- Check that focus is visible (highlight indicators)
|
||||
- Test that dialogs return focus appropriately when closed
|
||||
|
||||
### Configuration Files and Compatibility
|
||||
- **ThunderPad layouts**: `~/.config/thunderpad/*.lyt`
|
||||
- **Last used layout**: `~/.config/thunderpad/layout`
|
||||
- **Layout compatibility**: Same `.lyt` format as QJoyPad - files can be manually copied between directories
|
||||
- **Coexistence**: ThunderPad and QJoyPad can run simultaneously (different config dirs, different PID files)
|
||||
|
||||
### Device Detection
|
||||
- Scans `/dev/input/js*` by default
|
||||
- Uses libudev for automatic device hotplug detection when available
|
||||
- Supports manual device directory specification via `--device` flag
|
||||
|
||||
## Interface Changes from QJoyPad
|
||||
|
||||
### Major Accessibility Improvements
|
||||
1. **Tab-based interface**: Replaced `FlashRadioArray` with `QTabWidget`
|
||||
2. **List-based controls**: Axes and buttons shown in `QListWidget` instead of clickable grid
|
||||
3. **Configure buttons**: Dedicated buttons for each control type instead of click-only interfaces
|
||||
4. **Window mode default**: No system tray dependency for basic operation
|
||||
5. **Keyboard-only operation**: Every function accessible via keyboard
|
||||
|
||||
### Removed Features (For Accessibility)
|
||||
- **FlashRadioArray widgets**: Replaced with standard Qt accessible widgets
|
||||
- **Click-to-configure interfaces**: Replaced with explicit configure buttons
|
||||
- **System tray requirement**: Optional with `--tray` flag
|
||||
|
||||
### Command Line Interface
|
||||
```bash
|
||||
thunderpad [OPTIONS] [LAYOUT_NAME]
|
||||
|
||||
Options:
|
||||
-h, --help Show help message
|
||||
-d, --device=PATH Specify joystick device directory
|
||||
-t, --tray Use system tray icon (accessibility: not recommended)
|
||||
-T, --notray Use window mode (default, accessibility: recommended)
|
||||
-u, --update Update device list in running instance
|
||||
```
|
||||
|
||||
## Code Quality Standards
|
||||
|
||||
### Accessibility Requirements
|
||||
- **All interactive elements** must be keyboard accessible
|
||||
- **Proper widget labeling** for screen readers
|
||||
- **Logical tab order** with explicit `setTabOrder()` when needed
|
||||
- **Focus indicators** must be visible and announced
|
||||
- **No mouse-only functionality** - everything must work with keyboard alone
|
||||
|
||||
### Code Style
|
||||
- **No comments unless necessary** - Keep code self-documenting
|
||||
- **Follow existing Qt patterns** - Use existing signal/slot architecture
|
||||
- **Maintain security** - Never log or expose secrets/keys
|
||||
- **Consistent naming** - Use THUNDERPAD_ prefix for all new macros
|
||||
|
||||
### File Organization
|
||||
```
|
||||
src/
|
||||
├── config.h.in # Build-time configuration (THUNDERPAD_* macros)
|
||||
├── main.cpp # Application entry point
|
||||
├── layout.h/cpp # Central layout management
|
||||
├── layout_edit.h/cpp # Main configuration dialog
|
||||
├── joypad.h/cpp # Individual joystick representation
|
||||
├── joypadw.h/cpp # Accessible joystick widget
|
||||
├── button.h/cpp # Button configuration
|
||||
├── buttonw.h/cpp # Accessible button widget
|
||||
├── axis.h/cpp # Axis configuration
|
||||
├── axisw.h/cpp # Accessible axis widget
|
||||
├── event.h/cpp # X11 event generation
|
||||
├── error.h # Error handling utilities
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Recent Major Changes
|
||||
|
||||
### Complete Rebranding (QJoyPad → ThunderPad)
|
||||
- **Project name**: qjoypad → thunderpad
|
||||
- **Version**: Updated to 2025.06.30
|
||||
- **All source files**: Updated header guards, macros, comments
|
||||
- **Configuration**: New directory `~/.config/thunderpad/`
|
||||
- **Desktop entry**: New `thunderpad.desktop` file
|
||||
- **Build system**: CMakeLists.txt updated for new project name
|
||||
|
||||
### Accessibility Transformation
|
||||
- **Interface redesign**: Complete overhaul for keyboard navigation
|
||||
- **Widget replacement**: Accessible alternatives to custom widgets
|
||||
- **Default behavior**: Window mode instead of system tray
|
||||
- **Focus management**: Explicit tab order and focus policies
|
||||
|
||||
### Development Impact
|
||||
- **Building**: Same CMake process, now produces `thunderpad` executable
|
||||
- **Testing**: Must include accessibility testing with screen reader
|
||||
- **Configuration**: New config directory allows coexistence with QJoyPad
|
||||
- **Installation**: Desktop entry and icons updated for ThunderPad branding
|
||||
|
||||
## Future Development Guidelines
|
||||
|
||||
### Accessibility-First Development
|
||||
1. **Before implementing any UI change**:
|
||||
- Consider keyboard navigation impact
|
||||
- Test with screen reader if possible
|
||||
- Prefer standard Qt widgets over custom implementations
|
||||
- Ensure all functionality works without mouse
|
||||
|
||||
2. **Widget selection priority**:
|
||||
- QListWidget > clickable grids
|
||||
- QPushButton > mouse-only interactions
|
||||
- QTabWidget > custom tab implementations
|
||||
- Standard Qt dialogs > custom dialogs
|
||||
|
||||
3. **Testing requirements**:
|
||||
- Manual keyboard navigation test
|
||||
- Screen reader compatibility verification
|
||||
- Focus flow validation
|
||||
- No mouse required for any operation
|
||||
|
||||
### Compatibility Considerations
|
||||
- Maintain `.lyt` file format compatibility with QJoyPad
|
||||
- Keep command-line interface consistent where possible
|
||||
- Preserve core joystick mapping functionality
|
||||
- Ensure coexistence capability with QJoyPad installations
|
||||
|
||||
Remember: **Accessibility is not optional** - it's the core mission of ThunderPad.
|
||||
|
||||
## Qt6 Migration Notes
|
||||
|
||||
**Migration Date**: 2025-06-30
|
||||
**Status**: ⚠️ **PARTIALLY COMPLETE - NEEDS TESTING**
|
||||
|
||||
### Migration Summary
|
||||
ThunderPad has been migrated from Qt5 to Qt6 for future-proofing and improved performance. The migration involved several key changes:
|
||||
|
||||
### Changes Made
|
||||
|
||||
1. **Build System Updates**:
|
||||
- `CMakeLists.txt`: Updated `find_package(Qt5Widgets)` → `find_package(Qt6Widgets)`
|
||||
- `CMakeLists.txt`: Replaced `find_package(Qt5X11Extras)` → `find_package(Qt6Gui)`
|
||||
|
||||
2. **Qt5X11Extras Replacement**:
|
||||
- **Previous Qt5 approach**: Used `QX11Info::display()` from Qt5X11Extras
|
||||
- **New Qt6 approach**: Direct X11 library usage with `XOpenDisplay(nullptr)`
|
||||
- **File**: `src/event.cpp` - Complete rewrite of X11 display access
|
||||
- **Dependencies**: Now requires libX11 and libXtst directly
|
||||
|
||||
3. **QRegExp → QRegularExpression Migration**:
|
||||
- Updated 4 files: `layout.cpp`, `keycode.cpp`, `button.cpp`, `axis.cpp`
|
||||
- Replaced deprecated `QRegExp` with `QRegularExpression`
|
||||
- Updated API calls: `exactMatch()` → `match().hasMatch()`, `cap(n)` → `captured(n)`
|
||||
|
||||
### Current Status: ✅ **MIGRATION COMPLETE**
|
||||
|
||||
**Build Status**: ✅ **SUCCESS** - Project compiles without errors
|
||||
**Runtime Status**: ✅ **SUCCESS** - Application runs correctly with valid display
|
||||
|
||||
### Issues Resolved
|
||||
1. **"Startup Crash"**: Resolved - Qt6 requires valid X11 display connection
|
||||
2. **Root Cause**: Missing DISPLAY environment variable for GUI initialization
|
||||
3. **Solution**: Use `DISPLAY=:0 thunderpad` or run in X11 environment
|
||||
|
||||
### Testing Required
|
||||
⚠️ **TESTING NEEDED BEFORE PRODUCTION USE**:
|
||||
|
||||
1. **Debug the crash**:
|
||||
```bash
|
||||
gdb ./src/thunderpad
|
||||
(gdb) run --help
|
||||
(gdb) bt # Get backtrace
|
||||
```
|
||||
|
||||
2. **Accessibility Testing**:
|
||||
- Test with Orca screen reader once crash is fixed
|
||||
- Verify all keyboard navigation still works
|
||||
- Check focus management and tab order
|
||||
|
||||
3. **Functional Testing**:
|
||||
- Joystick detection and mapping
|
||||
- Layout loading/saving
|
||||
- Configuration interface
|
||||
- System tray functionality (--tray mode)
|
||||
|
||||
### Rollback Plan
|
||||
If Qt6 migration proves unstable:
|
||||
1. Revert `CMakeLists.txt` changes to Qt5
|
||||
2. Restore original `event.cpp` with Qt5X11Extras
|
||||
3. Revert QRegExp changes (though QRegularExpression works fine)
|
||||
|
||||
### Next Steps
|
||||
1. **PRIORITY 1**: Debug and fix startup crash
|
||||
2. **PRIORITY 2**: Complete accessibility testing
|
||||
3. **PRIORITY 3**: Performance testing and validation
|
||||
4. **PRIORITY 4**: Update documentation once stable
|
||||
|
||||
### Technical Notes for Future Development
|
||||
- **X11 Event Generation**: Now uses direct X11 calls instead of Qt's X11 integration
|
||||
- **Display Management**: Each `sendevent()` call now opens/closes X11 display (potential performance impact)
|
||||
- **Memory Management**: Ensure proper X11 display cleanup to prevent leaks
|
||||
- **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.
|
||||
@@ -8,6 +8,11 @@ set(THUNDERPAD_PATCH 30)
|
||||
|
||||
find_package(Qt6Widgets REQUIRED)
|
||||
find_package(Qt6Gui REQUIRED)
|
||||
include(CTest)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
find_package(Qt6Test REQUIRED)
|
||||
endif()
|
||||
|
||||
option(WITH_LIBUDEV "Use libudev for automatically updating joypad devices." ON)
|
||||
|
||||
@@ -50,6 +55,10 @@ include_directories("${PROJECT_BINARY_DIR}/src")
|
||||
add_subdirectory(icons)
|
||||
add_subdirectory(src)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
|
||||
install(PROGRAMS thunderpad.desktop DESTINATION "share/applications")
|
||||
|
||||
# uninstall target
|
||||
|
||||
@@ -189,9 +189,14 @@ This will minimize ThunderPad to the system tray. Right-click the tray icon to a
|
||||
The Quick Set feature provides rapid configuration:
|
||||
1. Click the "Quick Set" button
|
||||
2. Press any button or move any axis on your controller
|
||||
3. ThunderPad will prompt you to assign a keyboard key or mouse action
|
||||
4. Repeat for all controls you want to configure
|
||||
5. Click "Done" when finished
|
||||
3. ThunderPad opens the binding recorder. Tap keys for separate steps, or hold keys together for a chord.
|
||||
4. Hold Ctrl and press Enter to finish recording, then review the sequence and its step delay before choosing Use.
|
||||
|
||||
The recorder accepts up to 32 keyboard chords. A chord presses its keys in the recorded order and releases them in reverse order. The delay, from 50 through 1000 milliseconds, is measured between the starts of consecutive chords and defaults to 100 milliseconds. A single plain key keeps the existing held, Sticky, and Rapid Fire behavior. Chords and multi-step sequences run once per controller activation, so Sticky and Rapid Fire are unavailable for them.
|
||||
|
||||
When the first recorded action is a mouse button or wheel action, it remains a standalone mouse binding. Mouse actions cannot be mixed with keyboard chords or sequences.
|
||||
5. Repeat for all controls you want to configure
|
||||
6. Click "Done" when finished
|
||||
|
||||
### Command-line use and scripting
|
||||
|
||||
@@ -216,10 +221,10 @@ ThunderPad stores its configuration files in `~/.config/thunderpad/`, separate f
|
||||
|
||||
### Sharing Layouts with QJoyPad
|
||||
|
||||
ThunderPad uses the same `.lyt` file format as QJoyPad, so layouts can be shared:
|
||||
ThunderPad uses the same legacy `.lyt` syntax as QJoyPad, so layouts without sequences can be shared:
|
||||
|
||||
1. **From QJoyPad to ThunderPad**: Copy `.lyt` files from `~/.config/qjoypad4/` to `~/.config/thunderpad/`
|
||||
2. **From ThunderPad to QJoyPad**: Copy `.lyt` files from `~/.config/thunderpad/` to `~/.config/qjoypad4/`
|
||||
2. **From ThunderPad to QJoyPad**: Copy sequence-free `.lyt` files from `~/.config/thunderpad/` to `~/.config/qjoypad4/`
|
||||
|
||||
After copying files, restart the application or use the "Update Layout List" option to see the new layouts.
|
||||
|
||||
@@ -231,6 +236,15 @@ Layout files use standard X11 keycodes and are human-readable. You can edit them
|
||||
- Mouse movement and click assignments
|
||||
- Special properties like rapid fire and sticky buttons
|
||||
|
||||
Existing single-key and mouse entries retain the QJoyPad layout syntax. ThunderPad adds these sequence forms:
|
||||
|
||||
```text
|
||||
Button 1: sequence 100 64+28/49
|
||||
Axis 1: ZeroOne, +sequence 100 64+28/49, -sequence 250 10/11+12
|
||||
```
|
||||
|
||||
The number after `sequence` is the step delay in milliseconds. Slash separates consecutive chords, and plus joins keys held together within one chord. Native X11 keycodes are used, as in legacy layouts. Layouts without sequence tokens remain QJoyPad-compatible. Layouts containing `sequence`, `+sequence`, or `-sequence` are ThunderPad-specific and should not be expected to work in QJoyPad.
|
||||
|
||||
## Differences from QJoyPad
|
||||
|
||||
### Accessibility Improvements
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
pkgname=thunderpad-git
|
||||
_pkgname=thunderpad
|
||||
pkgver=r159.a0f6f34
|
||||
pkgver=r162.32c9924
|
||||
pkgrel=1
|
||||
pkgdesc="Accessible joypad to keyboard and mouse mapper"
|
||||
arch=('aarch64' 'x86_64')
|
||||
@@ -30,10 +30,15 @@ build() {
|
||||
-S "${_pkgname}" \
|
||||
-DCMAKE_BUILD_TYPE=None \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DBUILD_TESTING=ON \
|
||||
-DWITH_LIBUDEV=ON
|
||||
cmake --build build
|
||||
}
|
||||
|
||||
check() {
|
||||
ctest --test-dir build --output-on-failure
|
||||
}
|
||||
|
||||
package() {
|
||||
DESTDIR="${pkgdir}" cmake --install build
|
||||
install -Dm644 "${_pkgname}/LICENSE.txt" \
|
||||
|
||||
@@ -4,6 +4,7 @@ configure_file(config.h.in "${CMAKE_CURRENT_BINARY_DIR}/config.h" @ONLY)
|
||||
|
||||
set(thunderpad_SOURCES
|
||||
axis.cpp
|
||||
bindingdialog.cpp
|
||||
axis_edit.cpp
|
||||
axisw.cpp
|
||||
button.cpp
|
||||
@@ -17,6 +18,7 @@ set(thunderpad_SOURCES
|
||||
joyslider.cpp
|
||||
keycode.cpp
|
||||
keydialog.cpp
|
||||
inputbinding.cpp
|
||||
layout.cpp
|
||||
layout_edit.cpp
|
||||
main.cpp
|
||||
@@ -25,6 +27,7 @@ set(thunderpad_SOURCES
|
||||
set(thunderpad_QOBJECT_HEADERS
|
||||
axis_edit.h
|
||||
axis.h
|
||||
bindingdialog.h
|
||||
axisw.h
|
||||
button_edit.h
|
||||
button.h
|
||||
@@ -36,6 +39,7 @@ set(thunderpad_QOBJECT_HEADERS
|
||||
joyslider.h
|
||||
keycode.h
|
||||
keydialog.hpp
|
||||
inputbinding.h
|
||||
layout_edit.h
|
||||
layout.h
|
||||
quickset.h)
|
||||
|
||||
+134
-20
@@ -1,4 +1,5 @@
|
||||
#include "axis.h"
|
||||
#include "keycode.h"
|
||||
#include "event.h"
|
||||
#include "time.h"
|
||||
#include <QRegularExpression>
|
||||
@@ -13,6 +14,8 @@
|
||||
Axis::Axis( int i, QObject *parent ) : QObject(parent) {
|
||||
index = i;
|
||||
isOn = false;
|
||||
compositeEngaged = false;
|
||||
activeDirection = 0;
|
||||
isDown = false;
|
||||
useMouse = false;
|
||||
state = 0;
|
||||
@@ -23,6 +26,7 @@ Axis::Axis( int i, QObject *parent ) : QObject(parent) {
|
||||
minKeyInterval = 100; // Default: 100ms minimum between key presses
|
||||
toDefault();
|
||||
tick = 0;
|
||||
connect(&sequenceRunner, &SequenceRunner::keyEvent, this, &Axis::compositeKeyEvent);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +41,10 @@ bool Axis::read( QTextStream &stream ) {
|
||||
QString input = stream.readLine().toLower();
|
||||
QRegularExpression regex("[\\s,]+");
|
||||
QStringList words = input.split(regex);
|
||||
parseError.clear();
|
||||
bool positiveSeen = false;
|
||||
bool negativeSeen = false;
|
||||
bool sequenceSeen = false;
|
||||
|
||||
//used to assure QString->int conversions worked
|
||||
bool ok;
|
||||
@@ -89,40 +97,84 @@ bool Axis::read( QTextStream &stream ) {
|
||||
}
|
||||
//and for the positive keycode,
|
||||
else if (*it == "+key") {
|
||||
if (positiveSeen) { parseError = tr("Positive axis direction has more than one binding."); return false; }
|
||||
positiveSeen = true;
|
||||
++it;
|
||||
if (it == words.end()) return false;
|
||||
val = (*it).toInt(&ok);
|
||||
if (ok && val >= 0 && val <= MAXKEY) pkeycode = val;
|
||||
if (ok && val >= 0 && val <= MAXKEY) {
|
||||
pkeycode = val;
|
||||
positiveBinding = InputBinding::key(val);
|
||||
}
|
||||
else return false;
|
||||
}
|
||||
//and finally for the negative keycode.
|
||||
else if (*it == "-key") {
|
||||
if (negativeSeen) { parseError = tr("Negative axis direction has more than one binding."); return false; }
|
||||
negativeSeen = true;
|
||||
++it;
|
||||
if (it == words.end()) return false;
|
||||
val = (*it).toInt(&ok);
|
||||
if (ok && val >= 0 && val <= MAXKEY) nkeycode = val;
|
||||
if (ok && val >= 0 && val <= MAXKEY) {
|
||||
nkeycode = val;
|
||||
negativeBinding = InputBinding::key(val);
|
||||
}
|
||||
else return false;
|
||||
}
|
||||
else if (*it == "+mouse") {
|
||||
if (positiveSeen) { parseError = tr("Positive axis direction has more than one binding."); return false; }
|
||||
positiveSeen = true;
|
||||
++it;
|
||||
if (it == words.end()) return false;
|
||||
val = (*it).toInt(&ok);
|
||||
if (ok && val >= 0 && val <= MAXKEY) {
|
||||
puseMouse = true;
|
||||
pkeycode = val;
|
||||
positiveBinding = InputBinding::mouse(val);
|
||||
}
|
||||
else return false;
|
||||
}
|
||||
else if (*it == "-mouse") {
|
||||
if (negativeSeen) { parseError = tr("Negative axis direction has more than one binding."); return false; }
|
||||
negativeSeen = true;
|
||||
++it;
|
||||
if (it == words.end()) return false;
|
||||
val = (*it).toInt(&ok);
|
||||
if (ok && val >= 0 && val <= MAXKEY) {
|
||||
nuseMouse = true;
|
||||
nkeycode = val;
|
||||
negativeBinding = InputBinding::mouse(val);
|
||||
}
|
||||
else return false;
|
||||
}
|
||||
else if (*it == "+sequence" || *it == "-sequence") {
|
||||
const bool positive = *it == "+sequence";
|
||||
if ((positive && positiveSeen) || (!positive && negativeSeen)) {
|
||||
parseError = positive ? tr("Positive axis direction has more than one binding.")
|
||||
: tr("Negative axis direction has more than one binding.");
|
||||
return false;
|
||||
}
|
||||
if (positive) positiveSeen = true;
|
||||
else negativeSeen = true;
|
||||
sequenceSeen = true;
|
||||
++it;
|
||||
if (it == words.end()) { parseError = tr("Sequence is missing its delay."); return false; }
|
||||
const QString delayText = *it;
|
||||
++it;
|
||||
if (it == words.end()) { parseError = tr("Sequence is missing its chords."); return false; }
|
||||
InputBinding parsed;
|
||||
if (!InputBinding::parseSequence(delayText, *it, &parsed, &parseError)) return false;
|
||||
if (positive) {
|
||||
positiveBinding = parsed;
|
||||
pkeycode = parsed.isPlainKey() ? parsed.chords.first().keycodes.first() : 0;
|
||||
puseMouse = false;
|
||||
}
|
||||
else {
|
||||
negativeBinding = parsed;
|
||||
nkeycode = parsed.isPlainKey() ? parsed.chords.first().keycodes.first() : 0;
|
||||
nuseMouse = false;
|
||||
}
|
||||
}
|
||||
//the rest of the options are keywords without integers
|
||||
else if (*it == "zeroone") {
|
||||
interpretation = ZeroOne;
|
||||
@@ -161,11 +213,20 @@ bool Axis::read( QTextStream &stream ) {
|
||||
else if (*it == "mouse-h") {
|
||||
mode = MouseNegHor;
|
||||
}
|
||||
else if (sequenceSeen && !it->isEmpty()) {
|
||||
parseError = tr("Unexpected token '%1' after axis sequence.").arg(*it);
|
||||
return false;
|
||||
}
|
||||
//we ignore unrecognized words to be friendly and allow for additions to
|
||||
//the format in later versions. Note, this means that typos will not get
|
||||
//the desired effect OR produce an error message.
|
||||
}
|
||||
|
||||
if (sequenceSeen && mode != Keyboard) {
|
||||
parseError = tr("Axis sequences cannot be combined with a mouse movement mode.");
|
||||
return false;
|
||||
}
|
||||
|
||||
//assume that xZone, dZone, or maxSpeed has changed, for simplicity.
|
||||
//do a few floating point calculations.
|
||||
adjustGradient();
|
||||
@@ -187,9 +248,15 @@ void Axis::write( QTextStream &stream ) {
|
||||
if (dZone != DZONE) stream << "dZone " << dZone << ", ";
|
||||
if (xZone != XZONE) stream << "xZone " << xZone << ", ";
|
||||
if (mode == Keyboard) {
|
||||
stream
|
||||
<< (puseMouse ? "+mouse " : "+key ") << pkeycode << ", "
|
||||
<< (nuseMouse ? "-mouse " : "-key ") << nkeycode << "\n";
|
||||
if (positiveBinding.isComposite())
|
||||
stream << "+sequence " << positiveBinding.stepDelay << " " << positiveBinding.sequenceToken();
|
||||
else
|
||||
stream << (puseMouse ? "+mouse " : "+key ") << pkeycode;
|
||||
stream << ", ";
|
||||
if (negativeBinding.isComposite())
|
||||
stream << "-sequence " << negativeBinding.stepDelay << " " << negativeBinding.sequenceToken() << "\n";
|
||||
else
|
||||
stream << (nuseMouse ? "-mouse " : "-key ") << nkeycode << "\n";
|
||||
}
|
||||
else {
|
||||
if (gradient) stream << "maxSpeed " << maxSpeed << ", ";
|
||||
@@ -211,11 +278,19 @@ void Axis::write( QTextStream &stream ) {
|
||||
}
|
||||
|
||||
void Axis::release() {
|
||||
sequenceRunner.cancel();
|
||||
timer.stop();
|
||||
disconnect(&timer, SIGNAL(timeout()), 0, 0);
|
||||
//if we're pressing a key, let it go.
|
||||
if (isDown) {
|
||||
move(false);
|
||||
isDown = false;
|
||||
}
|
||||
isOn = false;
|
||||
compositeEngaged = false;
|
||||
activeDirection = 0;
|
||||
tick = 0;
|
||||
duration = 0;
|
||||
}
|
||||
|
||||
void Axis::jsevent( int value ) {
|
||||
@@ -226,6 +301,8 @@ 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;
|
||||
|
||||
@@ -233,7 +310,11 @@ void Axis::jsevent( int value ) {
|
||||
if (isOn && abs(state) <= dZone) {
|
||||
isOn = false;
|
||||
stateChanged = true;
|
||||
if (gradient) {
|
||||
if (compositeEngaged) {
|
||||
compositeEngaged = false;
|
||||
activeDirection = 0;
|
||||
}
|
||||
else if (gradient) {
|
||||
duration = 0;
|
||||
release();
|
||||
timer.stop();
|
||||
@@ -242,24 +323,30 @@ void Axis::jsevent( int value ) {
|
||||
}
|
||||
}
|
||||
//if was off but now should be on:
|
||||
else if (!isOn && abs(state) >= dZone) {
|
||||
else if (!isOn && abs(state) > dZone) {
|
||||
isOn = true;
|
||||
activeDirection = state >= 0 ? 1 : -1;
|
||||
stateChanged = true;
|
||||
if (gradient) {
|
||||
if (compositeDirection) {
|
||||
compositeEngaged = true;
|
||||
sequenceRunner.start(directionBinding);
|
||||
}
|
||||
else if (gradient) {
|
||||
duration = (abs(state) * FREQ) / JOYMAX;
|
||||
connect(&timer, SIGNAL(timeout()), this, SLOT(timerCalled()));
|
||||
timer.start(MSEC);
|
||||
}
|
||||
}
|
||||
//if in gradient mode and state changed, update duration
|
||||
else if (gradient && abs(state) > dZone) {
|
||||
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 (!gradient && stateChanged) {
|
||||
if (!compositeDirection && !gradient && stateChanged) {
|
||||
move(isOn);
|
||||
if (!isOn) activeDirection = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,6 +355,8 @@ void Axis::toDefault() {
|
||||
timer.stop();
|
||||
disconnect(&timer, SIGNAL(timeout()), 0, 0);
|
||||
isOn = false;
|
||||
compositeEngaged = false;
|
||||
activeDirection = 0;
|
||||
tick = 0;
|
||||
duration = 0;
|
||||
lastKeyPressTime = 0;
|
||||
@@ -285,6 +374,8 @@ void Axis::toDefault() {
|
||||
nkeycode = 0;
|
||||
puseMouse = false;
|
||||
nuseMouse = false;
|
||||
positiveBinding = InputBinding();
|
||||
negativeBinding = InputBinding();
|
||||
downkey = 0;
|
||||
state = 0;
|
||||
adjustGradient();
|
||||
@@ -301,6 +392,8 @@ bool Axis::isDefault() {
|
||||
(mode == Keyboard) &&
|
||||
(pkeycode == 0) &&
|
||||
(nkeycode == 0) &&
|
||||
positiveBinding.isEmpty() &&
|
||||
negativeBinding.isEmpty() &&
|
||||
(puseMouse == false) &&
|
||||
(nuseMouse == false) ;
|
||||
}
|
||||
@@ -317,7 +410,7 @@ bool Axis::inDeadZone( int val ) {
|
||||
value = (val + JOYMIN) / 2;
|
||||
else
|
||||
value = (val + JOYMAX) / 2;
|
||||
return (abs(value) < dZone);
|
||||
return (abs(value) <= dZone);
|
||||
}
|
||||
|
||||
QString Axis::status() {
|
||||
@@ -341,6 +434,11 @@ QString Axis::status() {
|
||||
else {
|
||||
label = tr("MOUSE");
|
||||
}
|
||||
if (mode == Keyboard) {
|
||||
const QString negative = negativeBinding.isEmpty() ? ktos(nkeycode) : negativeBinding.summary();
|
||||
const QString positive = positiveBinding.isEmpty() ? ktos(pkeycode) : positiveBinding.summary();
|
||||
return tr("%1 : negative %2; positive %3").arg(getName(), negative, positive);
|
||||
}
|
||||
return QString("%1 : [%2]").arg(getName(), label);
|
||||
}
|
||||
|
||||
@@ -349,13 +447,22 @@ void Axis::setKey(bool positive, int value) {
|
||||
}
|
||||
|
||||
void Axis::setKey(bool useMouse, bool positive, int value) {
|
||||
setBinding(positive, useMouse ? InputBinding::mouse(value) : InputBinding::key(value));
|
||||
}
|
||||
|
||||
void Axis::setBinding(bool positive, const InputBinding &binding) {
|
||||
release();
|
||||
if (positive) {
|
||||
pkeycode = value;
|
||||
puseMouse = useMouse;
|
||||
positiveBinding = binding;
|
||||
pkeycode = binding.isPlainKey() ? binding.chords.first().keycodes.first()
|
||||
: binding.isMouse() ? binding.mouseButton : 0;
|
||||
puseMouse = binding.isMouse();
|
||||
}
|
||||
else {
|
||||
nkeycode = value;
|
||||
nuseMouse = useMouse;
|
||||
negativeBinding = binding;
|
||||
nkeycode = binding.isPlainKey() ? binding.chords.first().keycodes.first()
|
||||
: binding.isMouse() ? binding.mouseButton : 0;
|
||||
nuseMouse = binding.isMouse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,12 +515,12 @@ void Axis::move( bool press ) {
|
||||
}
|
||||
|
||||
isDown = press;
|
||||
if (state != 0) {
|
||||
useMouse = (state > 0) ? puseMouse : nuseMouse;
|
||||
}
|
||||
if (press) {
|
||||
const int direction = activeDirection != 0
|
||||
? activeDirection : (state >= 0 ? 1 : -1);
|
||||
useMouse = direction > 0 ? puseMouse : nuseMouse;
|
||||
e.type = useMouse ? FakeEvent::MouseDown : FakeEvent::KeyDown;
|
||||
downkey = (state > 0) ? pkeycode : nkeycode;
|
||||
downkey = direction > 0 ? pkeycode : nkeycode;
|
||||
}
|
||||
else {
|
||||
e.type = useMouse ? FakeEvent::MouseUp : FakeEvent::KeyUp;
|
||||
@@ -483,6 +590,13 @@ void Axis::move( bool press ) {
|
||||
e.move.y = 0;
|
||||
}
|
||||
}
|
||||
//actually create the event
|
||||
//actually create the event
|
||||
sendevent(e);
|
||||
}
|
||||
|
||||
void Axis::compositeKeyEvent(int keycode, bool press) {
|
||||
FakeEvent event;
|
||||
event.type = press ? FakeEvent::KeyDown : FakeEvent::KeyUp;
|
||||
event.keycode = keycode;
|
||||
sendevent(event);
|
||||
}
|
||||
|
||||
+12
-1
@@ -11,6 +11,7 @@
|
||||
#include <QStringList>
|
||||
#include "constant.h"
|
||||
#include "error.h"
|
||||
#include "inputbinding.h"
|
||||
|
||||
//default and arbitrary values for dZone and xZone
|
||||
#define DZONE 3000
|
||||
@@ -52,6 +53,9 @@ class Axis : public QObject {
|
||||
//set the key code for this axis. Used by quickset.
|
||||
void setKey(bool positive, int value);
|
||||
void setKey(bool useMouse, bool positive, int value);
|
||||
void setBinding(bool positive, const InputBinding &binding);
|
||||
InputBinding getBinding(bool positive) const { return positive ? positiveBinding : negativeBinding; }
|
||||
QString readError() const { return parseError; }
|
||||
//happens every MSEC milliseconds (constant.h)
|
||||
//uses tick to decide if key events should be generated
|
||||
void timerTick( int tick );
|
||||
@@ -64,12 +68,15 @@ class Axis : public QObject {
|
||||
//This axis is logically depressed (positive or negative)
|
||||
//if the axis is gradient, this is true even if it is not
|
||||
//currently generating a keypress at the instant.
|
||||
bool isOn;
|
||||
bool isOn;
|
||||
bool compositeEngaged;
|
||||
int activeDirection;
|
||||
//the index of this axis on the joystick
|
||||
int index;
|
||||
//actually sends key events. Press is true iff the key
|
||||
//is to be depressed as opposed to released.
|
||||
virtual void move( bool press );
|
||||
virtual void compositeKeyEvent(int keycode, bool press);
|
||||
//is a key currently depressed?
|
||||
bool isDown;
|
||||
//is a mouse button currently in use?
|
||||
@@ -96,6 +103,10 @@ class Axis : public QObject {
|
||||
int nkeycode;
|
||||
bool puseMouse;
|
||||
bool nuseMouse;
|
||||
InputBinding positiveBinding;
|
||||
InputBinding negativeBinding;
|
||||
SequenceRunner sequenceRunner;
|
||||
QString parseError;
|
||||
//the key that is currently pressed
|
||||
int downkey;
|
||||
//the position of the axis, as from jsevent
|
||||
|
||||
+41
-7
@@ -1,5 +1,6 @@
|
||||
#include "config.h"
|
||||
#include "axis_edit.h"
|
||||
#include "error.h"
|
||||
|
||||
|
||||
AxisEdit::AxisEdit( Axis* ax )
|
||||
@@ -28,7 +29,11 @@ AxisEdit::AxisEdit( Axis* ax )
|
||||
chkGradient->insertItem((int) Axis::Gradient, tr("Relative movement (previously gradient)"), Qt::DisplayRole);
|
||||
chkGradient->insertItem((int) Axis::AbsolutePos, tr("Absolute movement (direct position)"), Qt::DisplayRole);
|
||||
chkGradient->setCurrentIndex( axis->interpretation );
|
||||
QLabel *movementLabel = new QLabel(tr("&Movement behavior:"), this);
|
||||
movementLabel->setBuddy(chkGradient);
|
||||
chkGradient->setAccessibleName(tr("Movement behavior"));
|
||||
connect(chkGradient, SIGNAL(activated(int)), this, SLOT( gradientChanged( int )));
|
||||
v2->addWidget(movementLabel);
|
||||
v2->addWidget(chkGradient);
|
||||
|
||||
cmbMode = new QComboBox(this);
|
||||
@@ -38,7 +43,11 @@ AxisEdit::AxisEdit( Axis* ax )
|
||||
cmbMode->insertItem((int) Axis::MousePosHor, tr("Mouse (Hor.)"), Qt::DisplayRole);
|
||||
cmbMode->insertItem((int) Axis::MouseNegHor, tr("Mouse (Hor. Rev.)"), Qt::DisplayRole);
|
||||
cmbMode->setCurrentIndex( axis->mode );
|
||||
QLabel *modeLabel = new QLabel(tr("Input &mode:"), this);
|
||||
modeLabel->setBuddy(cmbMode);
|
||||
cmbMode->setAccessibleName(tr("Input mode"));
|
||||
connect(cmbMode, SIGNAL(activated(int)), this, SLOT( modeChanged( int )));
|
||||
v2->addWidget(modeLabel);
|
||||
v2->addWidget(cmbMode);
|
||||
cmbTransferCurve = new QComboBox(this);
|
||||
cmbTransferCurve->insertItem(Axis::Linear, tr("Linear"), Qt::DisplayRole);
|
||||
@@ -47,8 +56,12 @@ AxisEdit::AxisEdit( Axis* ax )
|
||||
cmbTransferCurve->insertItem(Axis::QuadraticExtreme, tr("Quadratic Extreme"), Qt::DisplayRole);
|
||||
cmbTransferCurve->insertItem(Axis::PowerFunction, tr("Power Function"), Qt::DisplayRole);
|
||||
cmbTransferCurve->setCurrentIndex( axis->transferCurve );
|
||||
QLabel *curveLabel = new QLabel(tr("Transfer &curve:"), this);
|
||||
curveLabel->setBuddy(cmbTransferCurve);
|
||||
cmbTransferCurve->setAccessibleName(tr("Transfer curve"));
|
||||
cmbTransferCurve->setEnabled(axis->gradient);
|
||||
connect(cmbTransferCurve, SIGNAL(activated(int)), this, SLOT( transferCurveChanged( int )));
|
||||
v2->addWidget(curveLabel);
|
||||
v2->addWidget(cmbTransferCurve);
|
||||
h->addLayout(v2);
|
||||
|
||||
@@ -78,6 +91,8 @@ AxisEdit::AxisEdit( Axis* ax )
|
||||
v->addLayout(h);
|
||||
|
||||
slider = new JoySlider(axis->dZone, axis->xZone, axis->state, this);
|
||||
slider->setAccessibleName(tr("Axis position preview"));
|
||||
slider->setAccessibleDescription(tr("Shows the current position and configured deadzone and extreme zone."));
|
||||
v->addWidget(slider);
|
||||
|
||||
// Accessible deadzone configuration
|
||||
@@ -142,18 +157,28 @@ AxisEdit::AxisEdit( Axis* ax )
|
||||
h->setSpacing(5);
|
||||
h->setContentsMargins(5, 5, 5, 5);
|
||||
//h->setAutoAdd(true);
|
||||
btnNeg = new KeyButton(axis->getName(),axis->nkeycode,keyBox,true,axis->nuseMouse);
|
||||
QLabel *negativeLabel = new QLabel(tr("&Negative binding:"), keyBox);
|
||||
btnNeg = new BindingButton(tr("%1, negative").arg(axis->getName()), axis->getBinding(false), keyBox, true);
|
||||
negativeLabel->setBuddy(btnNeg);
|
||||
|
||||
cmbThrottle = new QComboBox(keyBox);
|
||||
cmbThrottle->insertItem(0, tr("Neg. Throttle"), Qt::DisplayRole);
|
||||
cmbThrottle->insertItem(1, tr("No Throttle"), Qt::DisplayRole);
|
||||
cmbThrottle->insertItem(2, tr("Pos. Throttle"), Qt::DisplayRole);
|
||||
cmbThrottle->setCurrentIndex(axis->throttle + 1);
|
||||
QLabel *throttleLabel = new QLabel(tr("&Throttle:"), keyBox);
|
||||
throttleLabel->setBuddy(cmbThrottle);
|
||||
cmbThrottle->setAccessibleName(tr("Throttle direction"));
|
||||
connect( cmbThrottle, SIGNAL( activated( int )), this, SLOT( throttleChanged( int )));
|
||||
|
||||
btnPos = new KeyButton(axis->getName(),axis->pkeycode,keyBox,true,axis->puseMouse);
|
||||
QLabel *positiveLabel = new QLabel(tr("&Positive binding:"), keyBox);
|
||||
btnPos = new BindingButton(tr("%1, positive").arg(axis->getName()), axis->getBinding(true), keyBox, true);
|
||||
positiveLabel->setBuddy(btnPos);
|
||||
h->addWidget(negativeLabel);
|
||||
h->addWidget(btnNeg);
|
||||
h->addWidget(throttleLabel);
|
||||
h->addWidget(cmbThrottle);
|
||||
h->addWidget(positiveLabel);
|
||||
h->addWidget(btnPos);
|
||||
v->addWidget( keyBox );
|
||||
|
||||
@@ -275,6 +300,17 @@ void AxisEdit::extremeZoneValueChanged(int value) {
|
||||
}
|
||||
|
||||
void AxisEdit::accept() {
|
||||
const Axis::Mode selectedMode = static_cast<Axis::Mode>(cmbMode->currentIndex());
|
||||
if (selectedMode != Axis::Keyboard
|
||||
&& (btnPos->binding().isComposite() || btnNeg->binding().isComposite())) {
|
||||
errorBox(tr("Invalid axis binding"),
|
||||
tr("Keyboard chords and sequences cannot be combined with a mouse movement mode. "
|
||||
"Choose Keyboard/Mouse Button, or replace both bindings with single keys or mouse buttons."),
|
||||
this);
|
||||
cmbMode->setFocus(Qt::OtherFocusReason);
|
||||
return;
|
||||
}
|
||||
|
||||
axis->interpretation = (Axis::Interpretation)chkGradient->currentIndex();
|
||||
axis->gradient = axis->interpretation != Axis::ZeroOne;
|
||||
axis->absolute = axis->interpretation == Axis::AbsolutePos;
|
||||
@@ -284,11 +320,9 @@ void AxisEdit::accept() {
|
||||
axis->throttle = cmbThrottle->currentIndex() - 1;
|
||||
axis->dZone = spinDeadzone->value();
|
||||
axis->xZone = spinExtremeZone->value();
|
||||
axis->mode = (Axis::Mode) cmbMode->currentIndex();
|
||||
axis->pkeycode = btnPos->getValue();
|
||||
axis->nkeycode = btnNeg->getValue();
|
||||
axis->puseMouse = btnPos->choseMouse();
|
||||
axis->nuseMouse = btnNeg->choseMouse();
|
||||
axis->mode = selectedMode;
|
||||
axis->setBinding(true, btnPos->binding());
|
||||
axis->setBinding(false, btnNeg->binding());
|
||||
axis->adjustGradient();
|
||||
|
||||
QDialog::accept();
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@
|
||||
#include <QPushButton>
|
||||
#include <QGroupBox>
|
||||
#include <QGridLayout>
|
||||
#include "keycode.h"
|
||||
#include "bindingdialog.h"
|
||||
|
||||
class AxisEdit : public QDialog {
|
||||
Q_OBJECT
|
||||
@@ -44,7 +44,7 @@ class AxisEdit : public QDialog {
|
||||
QSpinBox *spinSpeed;
|
||||
QLabel *lblSensitivity;
|
||||
QDoubleSpinBox *spinSensitivity;
|
||||
KeyButton *btnNeg, *btnPos;
|
||||
BindingButton *btnNeg, *btnPos;
|
||||
JoySlider *slider;
|
||||
|
||||
// Accessible deadzone controls
|
||||
|
||||
+76
-2
@@ -10,6 +10,7 @@ Button::Button( int i, QObject *parent ) : QObject(parent) {
|
||||
hasLayout = false;
|
||||
toDefault();
|
||||
tick = 0;
|
||||
connect(&sequenceRunner, &SequenceRunner::keyEvent, this, &Button::compositeKeyEvent);
|
||||
}
|
||||
|
||||
Button::~Button() {
|
||||
@@ -23,6 +24,9 @@ bool Button::read( QTextStream &stream ) {
|
||||
QString input = stream.readLine();
|
||||
QRegularExpression regex("[\\s,]+");
|
||||
QStringList words = input.split(regex);
|
||||
parseError.clear();
|
||||
bool foundBinding = false;
|
||||
bool sequenceSeen = false;
|
||||
|
||||
//used to assure correct conversion of QStrings -> ints
|
||||
bool ok;
|
||||
@@ -32,22 +36,28 @@ bool Button::read( QTextStream &stream ) {
|
||||
//go through every word on the line describing this button.
|
||||
for ( QStringList::Iterator it = words.begin(); it != words.end(); ++it ) {
|
||||
if (QString::compare(*it, "mouse", Qt::CaseInsensitive) == 0) {
|
||||
if (sequenceSeen) { parseError = tr("Mouse and keyboard bindings cannot be mixed with a sequence."); return false; }
|
||||
++it;
|
||||
if (it == words.end()) return false;
|
||||
val = (*it).toInt(&ok);
|
||||
if (ok && val >= 0 && val <= MAXKEY) {
|
||||
useMouse = true;
|
||||
keycode = val;
|
||||
binding = InputBinding::mouse(val);
|
||||
foundBinding = true;
|
||||
}
|
||||
else return false;
|
||||
}
|
||||
else if (QString::compare(*it, "key", Qt::CaseInsensitive) == 0) {
|
||||
if (sequenceSeen) { parseError = tr("Mouse and keyboard bindings cannot be mixed with a sequence."); return false; }
|
||||
++it;
|
||||
if (it == words.end()) return false;
|
||||
val = (*it).toInt(&ok);
|
||||
if (ok && val >= 0 && val <= MAXKEY) {
|
||||
useMouse = false;
|
||||
keycode = val;
|
||||
binding = InputBinding::key(val);
|
||||
foundBinding = true;
|
||||
}
|
||||
else return false;
|
||||
}
|
||||
@@ -63,6 +73,30 @@ bool Button::read( QTextStream &stream ) {
|
||||
else if (QString::compare(*it, "sticky", Qt::CaseInsensitive) == 0) {
|
||||
sticky = true;
|
||||
}
|
||||
else if (QString::compare(*it, "sequence", Qt::CaseInsensitive) == 0) {
|
||||
if (foundBinding) { parseError = tr("A button may contain only one input binding."); return false; }
|
||||
++it;
|
||||
if (it == words.end()) { parseError = tr("Sequence is missing its delay."); return false; }
|
||||
const QString delayText = *it;
|
||||
++it;
|
||||
if (it == words.end()) { parseError = tr("Sequence is missing its chords."); return false; }
|
||||
InputBinding parsed;
|
||||
if (!InputBinding::parseSequence(delayText, *it, &parsed, &parseError)) return false;
|
||||
binding = parsed;
|
||||
sequenceSeen = true;
|
||||
useMouse = false;
|
||||
keycode = binding.isPlainKey() ? binding.chords.first().keycodes.first() : 0;
|
||||
foundBinding = true;
|
||||
}
|
||||
else if (sequenceSeen && !it->isEmpty()) {
|
||||
parseError = tr("Unexpected token '%1' after button binding.").arg(*it);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!foundBinding) binding = InputBinding();
|
||||
if (binding.isComposite()) {
|
||||
rapidfire = false;
|
||||
sticky = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -71,15 +105,25 @@ void Button::write( QTextStream &stream ) {
|
||||
stream << "\tButton " << (index+1) << ": ";
|
||||
if (rapidfire) stream << "rapidfire, ";
|
||||
if (sticky) stream << "sticky, ";
|
||||
stream << (useMouse ? "mouse " : "key ") << keycode;
|
||||
if (binding.isComposite()) {
|
||||
stream << "sequence " << binding.stepDelay << " " << binding.sequenceToken();
|
||||
}
|
||||
else {
|
||||
stream << (useMouse ? "mouse " : "key ") << keycode;
|
||||
}
|
||||
if (hasLayout) stream << " layout " << layout.replace(" ", "\\s");
|
||||
stream << "\n";
|
||||
}
|
||||
|
||||
void Button::release() {
|
||||
sequenceRunner.cancel();
|
||||
timer.stop();
|
||||
disconnect(&timer, SIGNAL(timeout()), 0, 0);
|
||||
if (isDown) {
|
||||
click(false);
|
||||
}
|
||||
isButtonPressed = false;
|
||||
tick = 0;
|
||||
}
|
||||
|
||||
void Button::jsevent( int value ) {
|
||||
@@ -95,6 +139,13 @@ void Button::jsevent( int value ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (binding.isComposite()) {
|
||||
const bool newval = value == 1;
|
||||
if (newval && !isButtonPressed) sequenceRunner.start(binding);
|
||||
isButtonPressed = newval;
|
||||
return;
|
||||
}
|
||||
|
||||
bool newval = (value == 1);
|
||||
if (sticky) {
|
||||
//the state of a sticky key only changes on button press, not button release.
|
||||
@@ -139,6 +190,7 @@ void Button::toDefault() {
|
||||
sticky = false;
|
||||
useMouse = false;
|
||||
keycode = 0;
|
||||
binding = InputBinding();
|
||||
hasLayout = false;
|
||||
}
|
||||
|
||||
@@ -147,6 +199,7 @@ bool Button::isDefault() {
|
||||
(sticky == false) &&
|
||||
(useMouse == false) &&
|
||||
(keycode == 0) &&
|
||||
binding.isEmpty() &&
|
||||
(hasLayout == false);
|
||||
}
|
||||
|
||||
@@ -162,13 +215,34 @@ QString Button::status() {
|
||||
return tr("%1 : Mouse %2").arg(getName()).arg(keycode);
|
||||
}
|
||||
else {
|
||||
return tr("%1 : %2").arg(getName(), ktos(keycode));
|
||||
return tr("%1 : %2").arg(getName(), binding.isEmpty() ? ktos(keycode) : binding.summary());
|
||||
}
|
||||
}
|
||||
|
||||
void Button::setKey( bool mouse, int value ) {
|
||||
release();
|
||||
useMouse = mouse;
|
||||
keycode = value;
|
||||
binding = mouse ? InputBinding::mouse(value) : InputBinding::key(value);
|
||||
}
|
||||
|
||||
void Button::setBinding(const InputBinding &newBinding) {
|
||||
release();
|
||||
binding = newBinding;
|
||||
useMouse = binding.isMouse();
|
||||
keycode = binding.isPlainKey() ? binding.chords.first().keycodes.first()
|
||||
: binding.isMouse() ? binding.mouseButton : 0;
|
||||
if (binding.isComposite()) {
|
||||
sticky = false;
|
||||
rapidfire = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Button::compositeKeyEvent(int keycode, bool press) {
|
||||
FakeEvent event;
|
||||
event.type = press ? FakeEvent::KeyDown : FakeEvent::KeyUp;
|
||||
event.keycode = keycode;
|
||||
sendevent(event);
|
||||
}
|
||||
|
||||
void Button::timerTick( int tick ) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
//for getting a key name in status()
|
||||
#include "keycode.h"
|
||||
#include "inputbinding.h"
|
||||
|
||||
//note that the Button class, unlike the axis class, does not need a release
|
||||
//function because it releases the key as soon as it is pressed.
|
||||
@@ -34,6 +35,9 @@ class Button : public QObject {
|
||||
QString status();
|
||||
//set the key code for this axis. Used by quickset.
|
||||
void setKey(bool mouse, int value);
|
||||
void setBinding(const InputBinding &newBinding);
|
||||
InputBinding getBinding() const { return binding; }
|
||||
QString readError() const { return parseError; }
|
||||
//happens every MSEC (constant.h) milliseconds
|
||||
void timerTick( int tick );
|
||||
int buttonIndex() const { return index; }
|
||||
@@ -44,6 +48,7 @@ class Button : public QObject {
|
||||
int index;
|
||||
//actually sends a key press/release
|
||||
virtual void click( bool press );
|
||||
virtual void compositeKeyEvent(int keycode, bool press);
|
||||
//is a simulated key currently depressed?
|
||||
bool isDown;
|
||||
int tick;
|
||||
@@ -53,6 +58,9 @@ class Button : public QObject {
|
||||
bool useMouse;
|
||||
int keycode;
|
||||
QTimer timer;
|
||||
InputBinding binding;
|
||||
SequenceRunner sequenceRunner;
|
||||
QString parseError;
|
||||
//Layout settings
|
||||
bool hasLayout;
|
||||
QString layout;
|
||||
|
||||
+30
-6
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QVBoxLayout>
|
||||
#include <QLabel>
|
||||
|
||||
ButtonEdit::ButtonEdit(Button* butt, const QStringList *layoutNames)
|
||||
: QDialog(0) {
|
||||
@@ -16,7 +17,10 @@ ButtonEdit::ButtonEdit(Button* butt, const QStringList *layoutNames)
|
||||
v->setContentsMargins(5, 5, 5, 5);
|
||||
v->setSpacing(5);
|
||||
|
||||
btnKey = new KeyButton( button->getName(), button->keycode, this, true, button->useMouse);
|
||||
QLabel *bindingLabel = new QLabel(tr("&Binding:"), this);
|
||||
btnKey = new BindingButton(button->getName(), button->getBinding(), this, true);
|
||||
bindingLabel->setBuddy(btnKey);
|
||||
v->addWidget(bindingLabel);
|
||||
v->addWidget(btnKey);
|
||||
|
||||
QHBoxLayout* h = new QHBoxLayout();
|
||||
@@ -28,7 +32,10 @@ ButtonEdit::ButtonEdit(Button* butt, const QStringList *layoutNames)
|
||||
h->addWidget(chkRapid);
|
||||
v->addLayout(h);
|
||||
|
||||
QLabel *layoutLabel = new QLabel(tr("Target &layout:"), this);
|
||||
cmbLayout = new QComboBox(this);
|
||||
layoutLabel->setBuddy(cmbLayout);
|
||||
cmbLayout->setAccessibleName(tr("Target layout"));
|
||||
cmbLayout->addItem(tr("[UNSET]"), QVariant(QString()));
|
||||
foreach (const QString& layout, *layoutNames) {
|
||||
cmbLayout->addItem(layout, layout);
|
||||
@@ -40,8 +47,12 @@ ButtonEdit::ButtonEdit(Button* butt, const QStringList *layoutNames)
|
||||
else {
|
||||
cmbLayout->setCurrentIndex(0);
|
||||
}
|
||||
v->addWidget(layoutLabel);
|
||||
v->addWidget(cmbLayout);
|
||||
|
||||
connect(btnKey, &BindingButton::bindingChanged, this, &ButtonEdit::updateBindingControls);
|
||||
updateBindingControls(btnKey->binding());
|
||||
|
||||
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, this);
|
||||
connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept()));
|
||||
connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject()));
|
||||
@@ -61,11 +72,9 @@ void ButtonEdit::accept() {
|
||||
else {
|
||||
if (CRapid->isChecked()) takeTimer(button);
|
||||
}*/
|
||||
button->rapidfire = chkRapid->isChecked();
|
||||
button->sticky = chkSticky->isChecked();
|
||||
//if the user chose a mouse button...
|
||||
button->useMouse = btnKey->choseMouse();
|
||||
button->keycode = btnKey->getValue();
|
||||
button->setBinding(btnKey->binding());
|
||||
button->rapidfire = chkRapid->isEnabled() && chkRapid->isChecked();
|
||||
button->sticky = chkSticky->isEnabled() && chkSticky->isChecked();
|
||||
if (cmbLayout->currentIndex() != 0) {
|
||||
button->hasLayout = true;
|
||||
button->layout = cmbLayout->currentText();
|
||||
@@ -76,3 +85,18 @@ void ButtonEdit::accept() {
|
||||
|
||||
QDialog::accept();
|
||||
}
|
||||
|
||||
void ButtonEdit::updateBindingControls(const InputBinding &binding) {
|
||||
const bool enabled = !binding.isComposite();
|
||||
const QString explanation = enabled
|
||||
? tr("Available for a single key or mouse action.")
|
||||
: tr("Unavailable because chords and sequences run once as a complete binding.");
|
||||
chkSticky->setEnabled(enabled);
|
||||
chkRapid->setEnabled(enabled);
|
||||
chkSticky->setAccessibleDescription(explanation);
|
||||
chkRapid->setAccessibleDescription(explanation);
|
||||
if (!enabled) {
|
||||
chkSticky->setChecked(false);
|
||||
chkRapid->setChecked(false);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -11,7 +11,7 @@
|
||||
|
||||
|
||||
//to get a new key for the button.
|
||||
#include "keycode.h"
|
||||
#include "bindingdialog.h"
|
||||
|
||||
class ButtonEdit : public QDialog {
|
||||
Q_OBJECT
|
||||
@@ -20,9 +20,10 @@ class ButtonEdit : public QDialog {
|
||||
void show();
|
||||
protected slots:
|
||||
void accept();
|
||||
void updateBindingControls(const InputBinding &binding);
|
||||
protected:
|
||||
Button *button;
|
||||
KeyButton *btnKey;
|
||||
BindingButton *btnKey;
|
||||
QCheckBox *chkSticky, *chkRapid;
|
||||
QComboBox *cmbLayout;
|
||||
};
|
||||
|
||||
+9
-2
@@ -34,6 +34,7 @@ JoyPad::~JoyPad() {
|
||||
}
|
||||
|
||||
void JoyPad::close() {
|
||||
release();
|
||||
if (readNotifier) {
|
||||
disconnect(readNotifier, 0, 0, 0);
|
||||
|
||||
@@ -156,7 +157,10 @@ bool JoyPad::readConfig( QTextStream &stream ) {
|
||||
buttons.append(new Button(i, this));
|
||||
}
|
||||
if (!buttons[num-1]->read( stream )) {
|
||||
errorBox(tr("Layout file error"), tr("Error reading Button %1").arg(num));
|
||||
const QString detail = buttons[num-1]->readError();
|
||||
errorBox(tr("Layout file error"), detail.isEmpty()
|
||||
? tr("Error reading Button %1").arg(num)
|
||||
: tr("Error reading Button %1: %2").arg(num).arg(detail));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -176,7 +180,10 @@ bool JoyPad::readConfig( QTextStream &stream ) {
|
||||
axes.append(new Axis(i, this));
|
||||
}
|
||||
if (!axes[num-1]->read(stream)) {
|
||||
errorBox(tr("Layout file error"), tr("Error reading Axis %1").arg(num));
|
||||
const QString detail = axes[num-1]->readError();
|
||||
errorBox(tr("Layout file error"), detail.isEmpty()
|
||||
? tr("Error reading Axis %1").arg(num)
|
||||
: tr("Error reading Axis %1: %2").arg(num).arg(detail));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,49 @@
|
||||
#include <QRegularExpression>
|
||||
#include <QHash>
|
||||
#include "keycode.h"
|
||||
#include "keydialog.hpp"
|
||||
#include <X11/XKBlib.h>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/keysym.h>
|
||||
|
||||
static KeySym keycodeKeysym(int keycode, int level) {
|
||||
Display *display = XOpenDisplay(nullptr);
|
||||
if (!display) return NoSymbol;
|
||||
const KeySym result = XkbKeycodeToKeysym(display, keycode, 0, level);
|
||||
XCloseDisplay(display);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool keycodeIsShift(int keycode) {
|
||||
const KeySym symbol = keycodeKeysym(keycode, 0);
|
||||
return symbol == XK_Shift_L || symbol == XK_Shift_R;
|
||||
}
|
||||
|
||||
QString modifierKeyName(int keycode) {
|
||||
const KeySym symbol = keycodeKeysym(keycode, 0);
|
||||
if (symbol == XK_Shift_L || symbol == XK_Shift_R) return "Shift";
|
||||
if (symbol == XK_Control_L || symbol == XK_Control_R) return "Ctrl";
|
||||
if (symbol == XK_Alt_L || symbol == XK_Alt_R || symbol == XK_Meta_L || symbol == XK_Meta_R) return "Alt";
|
||||
if (symbol == XK_Super_L || symbol == XK_Super_R) return "Super";
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString shiftedKeyName(int keycode) {
|
||||
const KeySym shifted = keycodeKeysym(keycode, 1);
|
||||
const KeySym plain = keycodeKeysym(keycode, 0);
|
||||
if (shifted == NoSymbol || shifted == plain) return QString();
|
||||
const char *name = XKeysymToString(shifted);
|
||||
if (!name) return QString();
|
||||
const QString symbolName = QString::fromLatin1(name);
|
||||
const QHash<QString, QString> symbols = {
|
||||
{"exclam", "!"}, {"at", "@"}, {"numbersign", "#"}, {"dollar", "$"},
|
||||
{"percent", "%"}, {"asciicircum", "^"}, {"ampersand", "&"}, {"asterisk", "*"},
|
||||
{"parenleft", "("}, {"parenright", ")"}, {"underscore", "_"}, {"plus", "+"},
|
||||
{"braceleft", "{"}, {"braceright", "}"}, {"colon", ":"}, {"quotedbl", "\""},
|
||||
{"asciitilde", "~"}, {"bar", "|"}, {"less", "<"}, {"greater", ">"}, {"question", "?"}
|
||||
};
|
||||
return symbols.value(symbolName, symbolName.size() == 1 ? symbolName : QString());
|
||||
}
|
||||
|
||||
const QString ktos( int keycode )
|
||||
{
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
|
||||
//Produce a string for any keycode
|
||||
const QString ktos( int keycode );
|
||||
bool keycodeIsShift(int keycode);
|
||||
QString modifierKeyName(int keycode);
|
||||
QString shiftedKeyName(int keycode);
|
||||
|
||||
|
||||
//a button that requests a keycode from the user when clicked.
|
||||
|
||||
@@ -61,6 +61,7 @@ LayoutManager::LayoutManager( bool useTrayIcon, const QString &devdir, const QSt
|
||||
}
|
||||
|
||||
LayoutManager::~LayoutManager() {
|
||||
release();
|
||||
if (le) {
|
||||
le->close();
|
||||
le = 0;
|
||||
@@ -80,6 +81,12 @@ LayoutManager::~LayoutManager() {
|
||||
#endif
|
||||
}
|
||||
|
||||
void LayoutManager::release() {
|
||||
foreach (JoyPad *joypad, joypads) {
|
||||
joypad->release();
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef WITH_LIBUDEV
|
||||
bool LayoutManager::initUDev() {
|
||||
udev = udev_new();
|
||||
|
||||
@@ -43,6 +43,7 @@ class LayoutManager : public QObject {
|
||||
|
||||
//produces a list of the names of all the available layout.
|
||||
QStringList getLayoutNames() const;
|
||||
void release();
|
||||
public slots:
|
||||
//This is necessary to prevent issues with the overloaded load() function
|
||||
void loadLayoutFromButton(QString name);
|
||||
|
||||
@@ -265,6 +265,9 @@ int main( int argc, char **argv )
|
||||
//remove the lock file...
|
||||
pidFile.remove();
|
||||
|
||||
//Release every synthetic input and stop binding timers before closing X11.
|
||||
layoutManager.release();
|
||||
|
||||
//cleanup X11 display connection
|
||||
cleanupDisplay();
|
||||
|
||||
|
||||
+12
-11
@@ -1,5 +1,5 @@
|
||||
#include "quickset.h"
|
||||
#include "keydialog.hpp"
|
||||
#include "bindingdialog.h"
|
||||
|
||||
//build the dialog
|
||||
QuickSet::QuickSet( JoyPad* jp, QWidget *parent)
|
||||
@@ -11,8 +11,12 @@ QuickSet::QuickSet( JoyPad* jp, QWidget *parent)
|
||||
//LMain->setAutoAdd(true);
|
||||
|
||||
QLabel *temp = new QLabel(tr("Press any button or axis and\nyou will be prompted for a key."),this);
|
||||
temp->setWordWrap(true);
|
||||
temp->setAccessibleName(tr("Quick Set instructions"));
|
||||
temp->setText(tr("Press any controller button or move an axis outside its deadzone. Then record a key, chord, sequence, mouse button, or wheel action."));
|
||||
LMain->addWidget(temp);
|
||||
QPushButton* button = new QPushButton(tr("Done"),this);
|
||||
button->setAccessibleName(tr("Finish Quick Set"));
|
||||
LMain->addWidget(button);
|
||||
connect( button, SIGNAL(clicked()), this, SLOT(accept()));
|
||||
}
|
||||
@@ -29,13 +33,11 @@ void QuickSet::jsevent(const js_event &msg ) {
|
||||
Button* button = joypad->buttons[msg.number];
|
||||
//go into setting mode and request a key/mousebutton
|
||||
setting = true;
|
||||
bool isMouse = false;
|
||||
int code = KeyDialog::getKey(button->getName(), true, &isMouse, this);
|
||||
InputBinding binding;
|
||||
const bool accepted = BindingDialog::getBinding(button->getName(), button->getBinding(), &binding, true, this);
|
||||
setting = false;
|
||||
|
||||
if (code >= 0) {
|
||||
button->setKey(isMouse, code);
|
||||
}
|
||||
if (accepted) button->setBinding(binding);
|
||||
}
|
||||
}
|
||||
else if (type == JS_EVENT_AXIS) {
|
||||
@@ -49,14 +51,13 @@ void QuickSet::jsevent(const js_event &msg ) {
|
||||
|
||||
//grab a keycode for that axis and that direction
|
||||
setting = true;
|
||||
bool isMouse = false;
|
||||
int code = KeyDialog::getKey((msg.value >= 0 ? tr("%1, positive") : tr("%1, negative")).arg(axis->getName()), true, &isMouse, this);
|
||||
InputBinding binding;
|
||||
const bool positive = msg.value > 0;
|
||||
const bool accepted = BindingDialog::getBinding((positive ? tr("%1, positive") : tr("%1, negative")).arg(axis->getName()), axis->getBinding(positive), &binding, true, this);
|
||||
setting = false;
|
||||
|
||||
//assign the key to the axis.
|
||||
if (code >= 0) {
|
||||
axis->setKey(isMouse, (msg.value > 0), code);
|
||||
}
|
||||
if (accepted) axis->setBinding(positive, binding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,8 @@ class AxisProbe : public Axis {
|
||||
public:
|
||||
AxisProbe() : Axis(0) {}
|
||||
QVector<QPair<int, bool>> compositeEvents;
|
||||
bool currentBindingUsesMouse() const { return useMouse; }
|
||||
bool bindingIsDown() const { return isDown; }
|
||||
protected:
|
||||
void compositeKeyEvent(int keycode, bool press) override {
|
||||
compositeEvents.append(qMakePair(keycode, press));
|
||||
@@ -84,6 +86,8 @@ private slots:
|
||||
void runnerOrderTimingRetriggerAndCancel();
|
||||
void preservePlainButtonModes();
|
||||
void axisCompositeRearmsWithoutRepeating();
|
||||
void axisCompositeRearmsWithZeroDeadzone();
|
||||
void axisGradientReleaseKeepsActiveDirection();
|
||||
};
|
||||
|
||||
void BindingTests::parseAndSerializeSequences() {
|
||||
@@ -344,5 +348,37 @@ void BindingTests::axisCompositeRearmsWithoutRepeating() {
|
||||
QCOMPARE(axis.compositeEvents.at(9), qMakePair(13, true));
|
||||
}
|
||||
|
||||
void BindingTests::axisCompositeRearmsWithZeroDeadzone() {
|
||||
AxisProbe axis;
|
||||
QString input("Gradient, dZone 0, +sequence 50 10+11, -key 12");
|
||||
QTextStream reader(&input);
|
||||
QVERIFY(axis.read(reader));
|
||||
QVERIFY(axis.inDeadZone(0));
|
||||
QVERIFY(!axis.inDeadZone(1));
|
||||
|
||||
axis.jsevent(20000);
|
||||
QTRY_COMPARE_WITH_TIMEOUT(axis.compositeEvents.size(), 4, 150);
|
||||
axis.jsevent(0);
|
||||
axis.jsevent(20000);
|
||||
QTRY_COMPARE_WITH_TIMEOUT(axis.compositeEvents.size(), 8, 150);
|
||||
}
|
||||
|
||||
void BindingTests::axisGradientReleaseKeepsActiveDirection() {
|
||||
AxisProbe axis;
|
||||
QString input("Gradient, +mouse 1, -key 12");
|
||||
QTextStream reader(&input);
|
||||
QVERIFY(axis.read(reader));
|
||||
|
||||
axis.jsevent(20000);
|
||||
axis.timerTick(FREQ);
|
||||
QVERIFY(axis.bindingIsDown());
|
||||
QVERIFY(axis.currentBindingUsesMouse());
|
||||
|
||||
axis.jsevent(-20000);
|
||||
axis.timerTick((20000 * FREQ) / JOYMAX);
|
||||
QVERIFY(!axis.bindingIsDown());
|
||||
QVERIFY(axis.currentBindingUsesMouse());
|
||||
}
|
||||
|
||||
QTEST_MAIN(BindingTests)
|
||||
#include "test_bindings.moc"
|
||||
|
||||
Reference in New Issue
Block a user