Capture global hotkeys on Windows through raw input
A message-only window registers the keyboard and mouse with RIDEV_INPUTSINK, so every key and button arrives as WM_INPUT whether or not the client is in front. Raw input only observes, where a WH_KEYBOARD_LL hook sits in the path of the system input queue and can swallow a keystroke; it also reports the side buttons and both edges of every key, which push-to-talk needs. Keyboard codes stay in each platform's own numbering — X keycodes on X11, set-1 scan codes with the E0/E1 escape folded into the high byte on Windows — since that is what the input API reports and the key-naming call expects. Bindings are per-machine either way, as TeamSpeak's own per-OS keydefs are. Mouse buttons are unified on the X numbering, so "Mouse 4" means the same thing on both. The RAWINPUT decoding lives in its own class so it can be tested off Windows; the window and its pump have not been run on Windows yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
package com.ts3client.hotkey.desktop;
|
||||
|
||||
import com.ts3client.hotkey.HotkeyKey;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/** Decoding of Windows raw input, which needs no Windows to check. */
|
||||
class RawInputTest {
|
||||
|
||||
@Test
|
||||
void plainKeyKeepsItsScanCode() {
|
||||
// 'A' is scan code 0x1E, no escape.
|
||||
assertEquals(0x1E, RawInput.scanCode(0x1E, 0, 0x41));
|
||||
}
|
||||
|
||||
@Test
|
||||
void escapedKeysStayDistinctFromTheirUnescapedTwins() {
|
||||
// Left Ctrl is 1D; right Ctrl is E0 1D, and the two must not collide.
|
||||
int left = RawInput.scanCode(0x1D, 0, 0xA2);
|
||||
int right = RawInput.scanCode(0x1D, RawInput.RI_KEY_E0, 0xA3);
|
||||
assertEquals(0x1D, left);
|
||||
assertEquals(0xE01D, right);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pauseUsesTheOtherEscape() {
|
||||
assertEquals(0xE11D, RawInput.scanCode(0x1D, RawInput.RI_KEY_E1, 0x13));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fillerHalfOfAnEscapedSequenceIsIgnored() {
|
||||
assertEquals(-1, RawInput.scanCode(0x2A, RawInput.RI_KEY_E0, 0xFF));
|
||||
assertEquals(-1, RawInput.scanCode(0, 0, 0x41));
|
||||
}
|
||||
|
||||
@Test
|
||||
void breakFlagMarksTheRelease() {
|
||||
assertFalse(RawInput.isRelease(0));
|
||||
assertTrue(RawInput.isRelease(RawInput.RI_KEY_BREAK));
|
||||
assertTrue(RawInput.isRelease(RawInput.RI_KEY_BREAK | RawInput.RI_KEY_E0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sideButtonsMapToTheNumbersUsersKnow() {
|
||||
// X1 down: "Mouse 4" everywhere else in the client, X button 8.
|
||||
List<RawInput.ButtonEvent> down = RawInput.buttons(0x0040, (short) 0);
|
||||
assertEquals(List.of(new RawInput.ButtonEvent(HotkeyKey.mouse(8), true)), down);
|
||||
assertEquals("Mouse 4", HotkeyKey.mouse(8).fallbackName());
|
||||
|
||||
List<RawInput.ButtonEvent> up = RawInput.buttons(0x0200, (short) 0);
|
||||
assertEquals(List.of(new RawInput.ButtonEvent(HotkeyKey.mouse(9), false)), up);
|
||||
assertEquals("Mouse 5", HotkeyKey.mouse(9).fallbackName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void primaryButtonsUseTheXNumbering() {
|
||||
assertEquals(List.of(new RawInput.ButtonEvent(HotkeyKey.mouse(1), true)),
|
||||
RawInput.buttons(0x0001, (short) 0));
|
||||
assertEquals(List.of(new RawInput.ButtonEvent(HotkeyKey.mouse(2), true)),
|
||||
RawInput.buttons(0x0010, (short) 0));
|
||||
assertEquals(List.of(new RawInput.ButtonEvent(HotkeyKey.mouse(3), true)),
|
||||
RawInput.buttons(0x0004, (short) 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void oneEventCanCarrySeveralTransitions() {
|
||||
// Left up and right down in the same report.
|
||||
assertEquals(List.of(new RawInput.ButtonEvent(HotkeyKey.mouse(1), false),
|
||||
new RawInput.ButtonEvent(HotkeyKey.mouse(3), true)),
|
||||
RawInput.buttons(0x0002 | 0x0004, (short) 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void wheelBecomesATapSoTheEngineSeesBothEdges() {
|
||||
assertEquals(List.of(new RawInput.ButtonEvent(HotkeyKey.mouse(4), true),
|
||||
new RawInput.ButtonEvent(HotkeyKey.mouse(4), false)),
|
||||
RawInput.buttons(0x0400, (short) 120));
|
||||
assertEquals(List.of(new RawInput.ButtonEvent(HotkeyKey.mouse(5), true),
|
||||
new RawInput.ButtonEvent(HotkeyKey.mouse(5), false)),
|
||||
RawInput.buttons(0x0400, (short) -120));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mouseMovementAloneReportsNothing() {
|
||||
assertEquals(List.of(), RawInput.buttons(0, (short) 0));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user