mirror of
https://github.com/ddeverill/SteamlessController.git
synced 2026-10-21 22:22:16 -05:00
Allow back paddles to be bound to keyboard keys
Press-to-bind only listened to the controller, so the remap window could offer gamepad buttons and mouse clicks but nothing from the keyboard. Capture a key press while a row is listening and bind that instead. Capture happens in the remap page's keydown handler rather than through a keyboard hook. The page is already focused while listening, and a low-level hook is a heavy, AV-bait mechanism for a convenience feature. The page reports the physical KeyboardEvent.code, which KeyInput turns into a virtual-key code, so a binding made on one layout means the same physical key on another. Escape stays the cancel affordance and is deliberately not bindable. Keys are delivered with SendInput as scan codes rather than virtual keys, because software that reads scan codes directly sees nothing useful otherwise. Note that injected input does not reach every game — anti-cheat and some input paths ignore it — so this is realistically a desktop, emulator, and browser feature. Gamepad bindings remain the answer in game. Extended keys are flagged from an explicit set. MapVirtualKey is documented to report the 0xE0 prefix under MAPVK_VK_TO_VSC_EX but does not do so for the navigation cluster, which shares scan codes with the numpad — a test caught Up arrow being delivered as numpad 8. Labels come from GetKeyNameText against the active layout, so a non-US keyboard shows its own key names, and are JSON-escaped on the way to the page: the backslash key really is named "\". Paddle release tracking now records what the press actually sent instead of re-reading the binding on release, so rebinding a paddle mid-hold releases the input that was pressed rather than stranding it down. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0839f79f16
commit
b5b7e28cf1
7 files changed
+328
-41
No files matched your search
@@ -0,0 +1,153 @@
|
||||
#include "KeyInput.h"
|
||||
#include <Windows.h>
|
||||
#include <iterator>
|
||||
|
||||
namespace {
|
||||
|
||||
struct JsCodeToVk { const char* code; uint16_t vk; };
|
||||
|
||||
// KeyboardEvent.code values are physical positions, so a binding made on one
|
||||
// keyboard layout means the same physical key on another. Escape is absent on
|
||||
// purpose — the remap window uses it to cancel listening.
|
||||
const JsCodeToVk kCodeMap[] = {
|
||||
// Letters
|
||||
{"KeyA",'A'},{"KeyB",'B'},{"KeyC",'C'},{"KeyD",'D'},{"KeyE",'E'},{"KeyF",'F'},
|
||||
{"KeyG",'G'},{"KeyH",'H'},{"KeyI",'I'},{"KeyJ",'J'},{"KeyK",'K'},{"KeyL",'L'},
|
||||
{"KeyM",'M'},{"KeyN",'N'},{"KeyO",'O'},{"KeyP",'P'},{"KeyQ",'Q'},{"KeyR",'R'},
|
||||
{"KeyS",'S'},{"KeyT",'T'},{"KeyU",'U'},{"KeyV",'V'},{"KeyW",'W'},{"KeyX",'X'},
|
||||
{"KeyY",'Y'},{"KeyZ",'Z'},
|
||||
|
||||
// Digit row
|
||||
{"Digit0",'0'},{"Digit1",'1'},{"Digit2",'2'},{"Digit3",'3'},{"Digit4",'4'},
|
||||
{"Digit5",'5'},{"Digit6",'6'},{"Digit7",'7'},{"Digit8",'8'},{"Digit9",'9'},
|
||||
|
||||
// Whitespace and editing
|
||||
{"Space", VK_SPACE},
|
||||
{"Enter", VK_RETURN},
|
||||
{"Tab", VK_TAB},
|
||||
{"Backspace", VK_BACK},
|
||||
{"Delete", VK_DELETE},
|
||||
{"Insert", VK_INSERT},
|
||||
|
||||
// Navigation
|
||||
{"ArrowUp", VK_UP},
|
||||
{"ArrowDown", VK_DOWN},
|
||||
{"ArrowLeft", VK_LEFT},
|
||||
{"ArrowRight", VK_RIGHT},
|
||||
{"Home", VK_HOME},
|
||||
{"End", VK_END},
|
||||
{"PageUp", VK_PRIOR},
|
||||
{"PageDown", VK_NEXT},
|
||||
|
||||
// Modifiers — bound to the specific left/right key, not the merged VK_SHIFT
|
||||
{"ShiftLeft", VK_LSHIFT}, {"ShiftRight", VK_RSHIFT},
|
||||
{"ControlLeft", VK_LCONTROL}, {"ControlRight", VK_RCONTROL},
|
||||
{"AltLeft", VK_LMENU}, {"AltRight", VK_RMENU},
|
||||
|
||||
// Function row
|
||||
{"F1",VK_F1},{"F2",VK_F2},{"F3",VK_F3},{"F4",VK_F4},{"F5",VK_F5},{"F6",VK_F6},
|
||||
{"F7",VK_F7},{"F8",VK_F8},{"F9",VK_F9},{"F10",VK_F10},{"F11",VK_F11},{"F12",VK_F12},
|
||||
|
||||
// Punctuation (OEM codes are positional, which suits a physical binding)
|
||||
{"Minus", VK_OEM_MINUS},
|
||||
{"Equal", VK_OEM_PLUS},
|
||||
{"BracketLeft", VK_OEM_4},
|
||||
{"BracketRight", VK_OEM_6},
|
||||
{"Backslash", VK_OEM_5},
|
||||
{"Semicolon", VK_OEM_1},
|
||||
{"Quote", VK_OEM_7},
|
||||
{"Backquote", VK_OEM_3},
|
||||
{"Comma", VK_OEM_COMMA},
|
||||
{"Period", VK_OEM_PERIOD},
|
||||
{"Slash", VK_OEM_2},
|
||||
|
||||
// Numpad
|
||||
{"Numpad0",VK_NUMPAD0},{"Numpad1",VK_NUMPAD1},{"Numpad2",VK_NUMPAD2},
|
||||
{"Numpad3",VK_NUMPAD3},{"Numpad4",VK_NUMPAD4},{"Numpad5",VK_NUMPAD5},
|
||||
{"Numpad6",VK_NUMPAD6},{"Numpad7",VK_NUMPAD7},{"Numpad8",VK_NUMPAD8},
|
||||
{"Numpad9",VK_NUMPAD9},
|
||||
{"NumpadAdd", VK_ADD},
|
||||
{"NumpadSubtract", VK_SUBTRACT},
|
||||
{"NumpadMultiply", VK_MULTIPLY},
|
||||
{"NumpadDivide", VK_DIVIDE},
|
||||
{"NumpadDecimal", VK_DECIMAL},
|
||||
// Windows gives numpad Enter the same virtual key as the main one and
|
||||
// separates them by scan code alone. A binding stores only the VK, so this
|
||||
// one behaves as a plain Enter — which is what nearly all software wants.
|
||||
{"NumpadEnter", VK_RETURN},
|
||||
|
||||
// Locks
|
||||
{"CapsLock", VK_CAPITAL},
|
||||
{"NumLock", VK_NUMLOCK},
|
||||
{"ScrollLock", VK_SCROLL},
|
||||
};
|
||||
|
||||
// The navigation cluster shares scan codes with the numpad and is told apart
|
||||
// only by the 0xE0 extended prefix — drop it and an Up arrow arrives as numpad
|
||||
// 8. MapVirtualKey is documented to report that prefix under MAPVK_VK_TO_VSC_EX
|
||||
// but does not actually do so for these keys, so the set is spelled out here.
|
||||
bool IsExtendedKey(uint16_t vk) {
|
||||
switch (vk) {
|
||||
case VK_RCONTROL: case VK_RMENU:
|
||||
case VK_INSERT: case VK_DELETE:
|
||||
case VK_HOME: case VK_END:
|
||||
case VK_PRIOR: case VK_NEXT:
|
||||
case VK_UP: case VK_DOWN:
|
||||
case VK_LEFT: case VK_RIGHT:
|
||||
case VK_NUMLOCK: case VK_DIVIDE:
|
||||
case VK_SNAPSHOT:
|
||||
case VK_LWIN: case VK_RWIN: case VK_APPS:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
struct ScanCode { WORD scan; bool extended; };
|
||||
|
||||
ScanCode ScanCodeFor(uint16_t vk) {
|
||||
// _EX is still the right lookup for the scan code itself: it distinguishes
|
||||
// left from right modifiers, which the plain mapping collapses.
|
||||
const UINT mapped = MapVirtualKeyW(vk, MAPVK_VK_TO_VSC_EX);
|
||||
return { static_cast<WORD>(mapped & 0xFF), IsExtendedKey(vk) };
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint16_t VkFromJsCode(const std::string& jsCode) {
|
||||
for (const auto& e : kCodeMap)
|
||||
if (jsCode == e.code) return e.vk;
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::wstring KeyDisplayName(uint16_t vk) {
|
||||
if (vk == 0) return L"Key";
|
||||
|
||||
const ScanCode sc = ScanCodeFor(vk);
|
||||
if (sc.scan == 0) return L"Key";
|
||||
|
||||
// GetKeyNameText takes the scan code and extended flag positioned as they
|
||||
// appear in a WM_KEYDOWN lParam.
|
||||
LONG lParam = static_cast<LONG>(sc.scan) << 16;
|
||||
if (sc.extended) lParam |= (1L << 24);
|
||||
|
||||
wchar_t buf[64] = {};
|
||||
const int n = GetKeyNameTextW(lParam, buf, static_cast<int>(std::size(buf)));
|
||||
return n > 0 ? std::wstring(buf, static_cast<size_t>(n)) : std::wstring(L"Key");
|
||||
}
|
||||
|
||||
void SendKeyInput(uint16_t vk, bool down) {
|
||||
if (vk == 0) return;
|
||||
|
||||
const ScanCode sc = ScanCodeFor(vk);
|
||||
if (sc.scan == 0) return;
|
||||
|
||||
INPUT input = {};
|
||||
input.type = INPUT_KEYBOARD;
|
||||
input.ki.wVk = 0; // ignored, and must be zero, when sending a scan code
|
||||
input.ki.wScan = sc.scan;
|
||||
input.ki.dwFlags = KEYEVENTF_SCANCODE;
|
||||
if (sc.extended) input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
|
||||
if (!down) input.ki.dwFlags |= KEYEVENTF_KEYUP;
|
||||
SendInput(1, &input, sizeof(INPUT));
|
||||
}
|
||||
Reference in new issue
Block a user