Skip to content

Commit 58176b3

Browse files
committed
Auto-delay modifier+mouse combos so Alt+scroll, Ctrl+click etc. work
When a single event batch contains both a keyboard modifier press (Ctrl/Shift/Alt/Meta) and a mouse event (REL_WHEEL/REL_HWHEEL/BTN_LEFT/ BTN_RIGHT/BTN_MIDDLE), insert a small built-in delay between the modifier and the mouse event so the compositor has time to latch the modifier into keystate before the pointer event arrives. Without this, bindings like KEY_LEFTALT+REL_WHEEL:1 were delivered as plain scrolls because Alt was never registered as held when the wheel event reached the application. The existing modifier_delay setting still applies to pure-keyboard combos and is unchanged. If the user has set modifier_delay larger than the auto floor, their value still wins (max(user, auto)). Pure keyboard combos, plain mouse events, and single-key bindings are all unaffected. Bumps version to 3.1.1.
1 parent 6e18888 commit 58176b3

7 files changed

Lines changed: 81 additions & 44 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# TuxBox — Linux Driver for TourBox Controllers
22

3-
[![Version](https://img.shields.io/badge/version-3.1.0-green.svg)](https://github.com/AndyCappDev/tuxbox/releases)
3+
[![Version](https://img.shields.io/badge/version-3.1.1-green.svg)](https://github.com/AndyCappDev/tuxbox/releases)
44
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
55
[![Platform: Linux](https://img.shields.io/badge/platform-linux-lightgrey.svg)](https://www.linux.org/)
66
[![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)

docs/CONFIG_GUIDE.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ Configures your TourBox device connection.
117117

118118
**Available settings:**
119119
- `mac_address` - Your TourBox's Bluetooth MAC address (XX:XX:XX:XX:XX:XX) - Elite/Elite Plus only
120-
- `modifier_delay` - Milliseconds to wait between modifier keys (Ctrl/Shift/Alt/Meta) and other keys when sending key combinations. Default: `0` (disabled). Set to `20`-`50` if apps like GIMP don't recognize key combos. Can be overridden per-profile in `.profile` files (see below).
120+
- `modifier_delay` - Milliseconds to wait between modifier keys (Ctrl/Shift/Alt/Meta) and other **keyboard** keys when sending key combinations. Default: `0` (disabled). Set to `20`-`50` if apps like GIMP don't recognize keyboard combos. Can be overridden per-profile in `.profile` files (see below). **Note:** modifier+mouse combos (e.g. Alt+scroll, Ctrl+click) get a small built-in delay automatically and do not require this setting.
121121

122122
**How to find your MAC address:**
123123
```bash
@@ -454,7 +454,9 @@ scroll_down = KEY_LEFTCTRL+KEY_MINUS
454454

455455
## Modifier Key Delay
456456

457-
Some applications (notably GIMP) don't recognize key combinations when the modifier key and main key arrive at nearly the same time. The `modifier_delay` setting adds a small pause between them.
457+
Some applications (notably GIMP) don't recognize **keyboard** combinations when the modifier key and the main key arrive at nearly the same time. The `modifier_delay` setting adds a small pause between them.
458+
459+
> **Mouse combos are handled automatically.** When a binding mixes a modifier (Ctrl/Shift/Alt/Meta) with a mouse event (`REL_WHEEL`, `REL_HWHEEL`, `BTN_LEFT`, `BTN_RIGHT`, `BTN_MIDDLE`), TuxBox always inserts a small delay between the modifier press and the mouse event so the compositor has time to latch the modifier into keystate. You do **not** need to set `modifier_delay` for things like `KEY_LEFTALT+REL_WHEEL:1` (Alt+scroll) or `KEY_LEFTCTRL+BTN_LEFT` (Ctrl+click). If you have set `modifier_delay` to a value larger than the built-in floor, your value wins.
458460
459461
### Global Setting
460462

docs/GUI_USER_GUIDE.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# TuxBox Configuration GUI - User Guide
22

3-
**Version:** 3.1.0
4-
**Last Updated:** 2026-03-13
3+
**Version:** 3.1.1
4+
**Last Updated:** 2026-04-07
55

66
## Table of Contents
77

@@ -1205,11 +1205,13 @@ When the TourBox sends a key combination like Ctrl+Z, it sends the modifier key
12051205

12061206
**Modifier key delay** adds a small pause (in milliseconds) between sending the modifier keys (Ctrl, Shift, Alt, Super) and the remaining keys in a combination. This gives the application time to register the modifier before the main key arrives.
12071207

1208+
> **Mouse combos are handled automatically.** Bindings that mix a modifier with a mouse event -- e.g. `Alt+Scroll Wheel`, `Ctrl+Left Click` -- always get a small built-in delay so the compositor has time to register the modifier before the click or scroll. You do **not** need to enable Modifier Key Delay for these. The setting documented here is specifically for **keyboard-only** combos in stubborn applications like GIMP. If you set a value larger than the built-in floor, your value still wins for mouse combos too.
1209+
12081210
### When Do You Need It?
12091211

12101212
You likely need modifier key delay if:
12111213

1212-
- **Key combinations don't work in specific applications** -- e.g., pressing a button mapped to Ctrl+Z does nothing in GIMP, but works fine in other apps
1214+
- **Keyboard combinations don't work in specific applications** -- e.g., pressing a button mapped to Ctrl+Z does nothing in GIMP, but works fine in other apps
12131215
- **Only the main key registers** -- e.g., you get "z" instead of "Ctrl+Z"
12141216
- **Combinations work intermittently** -- sometimes the combo registers, sometimes it doesn't
12151217

tuxbox/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = '3.1.0'
1+
VERSION = '3.1.1'

tuxbox/default_mappings.conf

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,15 @@
2727
#
2828
# KEY COMBO TIMING:
2929
# modifier_delay: Milliseconds to wait between modifier keys (Ctrl/Shift/Alt/Meta)
30-
# and other keys when sending key combinations. Default: 0 (disabled)
31-
# Set to 20-50 if apps like GIMP don't recognize key combos.
32-
# This adds a small delay so apps register the modifier first.
33-
# Can also be set per-profile in the [profile] section of .profile files
34-
# (per-profile value overrides this global setting).
30+
# and other KEYBOARD keys when sending key combinations.
31+
# Default: 0 (disabled). Set to 20-50 if apps like GIMP don't
32+
# recognize key combos. This adds a small delay so apps register
33+
# the modifier first. Can also be set per-profile in the [profile]
34+
# section of .profile files (per-profile value overrides this
35+
# global setting).
36+
# Note: modifier+mouse combos (Alt+scroll, Ctrl+click, etc.)
37+
# get a small built-in delay automatically and do not require
38+
# this setting.
3539

3640
[device]
3741
# usb_port = /dev/ttyACM0

tuxbox/device_base.py

Lines changed: 60 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@
2727
e.KEY_LEFTMETA, e.KEY_RIGHTMETA,
2828
}
2929

30+
# Mouse button keys - paired with KEYBOARD_MODIFIER_KEYS to detect modifier+mouse
31+
# combos that need a forced delay (see AUTO_MOUSE_MODIFIER_DELAY_MS).
32+
MOUSE_EVENT_KEYS = {e.BTN_LEFT, e.BTN_RIGHT, e.BTN_MIDDLE}
33+
34+
# When a single batch contains both a modifier press and a mouse event (REL_* or
35+
# BTN_LEFT/RIGHT/MIDDLE), we force at least this much delay between the modifier
36+
# and the mouse event so the compositor has time to latch the modifier into
37+
# keystate before the pointer event arrives. Without this, apps like DaVinci
38+
# Resolve see Alt+scroll as a plain scroll. Pure-keyboard combos are not
39+
# affected and still honor the user's `modifier_delay` setting verbatim.
40+
AUTO_MOUSE_MODIFIER_DELAY_MS = 15
41+
3042
logger = logging.getLogger(__name__)
3143

3244
# Controls that can be held (buttons) vs momentary (rotary)
@@ -147,47 +159,64 @@ def is_modifier_button(self, control_name: str) -> bool:
147159
def _send_events(self, events: List[Tuple[int, int, int]]):
148160
"""Send events to the virtual input device with optional modifier delay
149161
150-
If modifier_delay is configured and the events contain both keyboard
151-
modifier keys (Ctrl, Shift, Alt, Meta) and non-modifier keys, sends
152-
the modifier keys first, syncs, waits for the delay, then sends the
153-
remaining keys. This helps applications recognize key combinations.
162+
If the batch contains a modifier key press (Ctrl/Shift/Alt/Meta) along
163+
with other events, the modifier is sent first, syned, then the rest of
164+
the events are sent after a delay. This helps applications recognize
165+
key combinations.
166+
167+
The delay used is `max(self.modifier_delay, AUTO_MOUSE_MODIFIER_DELAY_MS)`
168+
when the batch mixes a modifier with a mouse event (REL_* or BTN_LEFT/
169+
RIGHT/MIDDLE), and `self.modifier_delay` otherwise. The auto-delay
170+
exists because compositors need a moment to latch a modifier into
171+
keystate before processing pointer events; without it, things like
172+
Alt+scroll get delivered as plain scrolls. Pure keyboard combos still
173+
honor `self.modifier_delay` verbatim (default 0).
154174
155175
Args:
156176
events: List of (event_type, event_code, value) tuples
157177
"""
158178
if not events or not self.controller:
159179
return
160180

161-
# Check if we need to apply modifier delay
162-
if self.modifier_delay > 0:
163-
# Separate modifier key presses from other events
164-
modifier_presses = []
165-
other_events = []
166-
167-
for event in events:
168-
event_type, event_code, value = event
169-
if (event_type == e.EV_KEY and
170-
value == 1 and
171-
event_code in KEYBOARD_MODIFIER_KEYS):
172-
modifier_presses.append(event)
173-
else:
174-
other_events.append(event)
181+
# Single pass: partition events and detect mouse-mixing.
182+
modifier_presses = []
183+
other_events = []
184+
has_mouse_event = False
175185

176-
# If we have both modifiers and other events, send with delay
177-
if modifier_presses and other_events:
178-
# Send modifier keys first
179-
for event in modifier_presses:
180-
self.controller.write(*event)
181-
self.controller.syn()
186+
for event in events:
187+
event_type, event_code, value = event
188+
if (event_type == e.EV_KEY and
189+
value == 1 and
190+
event_code in KEYBOARD_MODIFIER_KEYS):
191+
modifier_presses.append(event)
192+
else:
193+
other_events.append(event)
194+
if event_type == e.EV_REL:
195+
has_mouse_event = True
196+
elif (event_type == e.EV_KEY and
197+
value == 1 and
198+
event_code in MOUSE_EVENT_KEYS):
199+
has_mouse_event = True
200+
201+
# Compute the delay to use for this batch. Mouse+modifier combos get
202+
# the auto floor; everything else just uses the configured value.
203+
effective_delay = self.modifier_delay
204+
if modifier_presses and has_mouse_event:
205+
effective_delay = max(effective_delay, AUTO_MOUSE_MODIFIER_DELAY_MS)
206+
207+
# If we have both modifiers and other events and a non-zero delay,
208+
# send the modifier first, wait, then send the rest.
209+
if effective_delay > 0 and modifier_presses and other_events:
210+
for event in modifier_presses:
211+
self.controller.write(*event)
212+
self.controller.syn()
182213

183-
# Wait for modifier delay
184-
time.sleep(self.modifier_delay / 1000.0)
214+
time.sleep(effective_delay / 1000.0)
185215

186-
# Send remaining events
187-
for event in other_events:
188-
self.controller.write(*event)
189-
self.controller.syn()
190-
return
216+
for event in other_events:
217+
self.controller.write(*event)
218+
self.controller.syn()
219+
return
191220

192221
# No delay needed - send all events normally
193222
for event in events:

tuxbox/gui/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
Graphical interface for configuring TourBox button mappings and profiles.
44
"""
55

6-
__version__ = "3.1.0"
6+
__version__ = "3.1.1"

0 commit comments

Comments
 (0)