mirror of
https://github.com/ddeverill/SteamlessController.git
synced 2026-10-21 22:22:16 -05:00
Add per-game control profiles
Everything about how the controller behaves — paddle bindings, both trackpads, and which kind of pad the game sees — is now configured in one window and saved per game, rather than spread across tray checkboxes that applied everywhere at once. Installed games come from the Start Menu and the packaged-app list. Steam does not shortcut its library with .lnk files, so its games are read from the .url shortcuts it does write; Xbox and Store titles register no shortcut file at all and come from the shell's app list instead. None of these sources can tell a game from any other program, so the picker searches rather than scrolls — a saved profile is pinned, everything else is reached by typing. Trackpads are configured per pad. The previous pair of toggles could only ever describe one pad doing one thing, because a single TrackpadMouse instance was switched between pads by a bool; there are two now. A pad can move the pointer, scroll, feed the DS4 touchpad, or do nothing, and its click is an ordinary binding dispatched down the same path as the paddles — it used to be hardwired to a left click that only fired while that pad drove the mouse. Key bindings can carry Ctrl, Alt, Shift and Win. The modifiers ride in the free top byte of the packed binding, so values written by earlier builds decode unchanged. Windows is chosen from a toggle rather than pressed: the Start menu opens before the page is told anything, which is what made that key unbindable before. Choosing modifiers rather than typing them also keeps combinations like Alt+Tab from being claimed by the system mid-capture. Ctrl+Alt+Delete is refused outright, being the Secure Attention Sequence that no ordinary process can send. Profiles follow the foreground application, and nothing polls to do it. An unelevated process cannot subscribe to process-start events — the WMI class that reports them requires administrator rights, as does an ETW kernel session — but SetWinEventHook reports foreground changes for the cost of one hook and no thread. Focus is also the better question than "is it running": a launcher that starts a game and stays resident answers yes long after the game has exited. Steam profiles are keyed by app id, which a foreground window cannot supply, so Steam's manifests are read to map that id to an install directory; a game's launcher, its anti-cheat service and the game itself all live under it and all resolve to the same profile. Auto Mode still decides whether the controller is ours at all, so a profile only loads while we hold the pad. A balloon names the profile when one loads, under the notification setting, because a profile that quietly fails to match is otherwise invisible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
838cd7ae68
commit
a087c236b2
27 files changed
+2780
-290
No files matched your search
@@ -44,6 +44,10 @@ const JsCodeToVk kCodeMap[] = {
|
||||
{"ShiftLeft", VK_LSHIFT}, {"ShiftRight", VK_RSHIFT},
|
||||
{"ControlLeft", VK_LCONTROL}, {"ControlRight", VK_RCONTROL},
|
||||
{"AltLeft", VK_LMENU}, {"AltRight", VK_RMENU},
|
||||
// Present for completeness, not because they can be captured: Windows
|
||||
// opens the Start menu on the way down, so the page is never told. A
|
||||
// binding that wants Windows carries it as a modifier flag instead.
|
||||
{"MetaLeft", VK_LWIN}, {"MetaRight", VK_RWIN},
|
||||
|
||||
// Function row
|
||||
{"F1",VK_F1},{"F2",VK_F2},{"F3",VK_F3},{"F4",VK_F4},{"F5",VK_F5},{"F6",VK_F6},
|
||||
@@ -121,6 +125,89 @@ uint16_t VkFromJsCode(const std::string& jsCode) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// The physical key each modifier flag sends. Left-hand keys throughout:
|
||||
// applications test the merged state, and a left modifier is what a user
|
||||
// pressing the shortcut themselves would almost always produce.
|
||||
struct ModifierKey { BackButtonBinding::Modifier flag; uint16_t vk; const wchar_t* name; };
|
||||
|
||||
// Order matters twice over: it is the order modifiers are pressed in, and the
|
||||
// order they read in a label. Ctrl, Alt, Shift, Win is the convention Windows
|
||||
// itself uses when it spells out a shortcut.
|
||||
const ModifierKey kModifierKeys[] = {
|
||||
{ BackButtonBinding::ModCtrl, VK_LCONTROL, L"Ctrl" },
|
||||
{ BackButtonBinding::ModAlt, VK_LMENU, L"Alt" },
|
||||
{ BackButtonBinding::ModShift, VK_LSHIFT, L"Shift" },
|
||||
{ BackButtonBinding::ModWin, VK_LWIN, L"Win" },
|
||||
};
|
||||
|
||||
// How many live bindings are currently holding each modifier down, indexed
|
||||
// alongside kModifierKeys. Only ever touched from the read thread that
|
||||
// dispatches bindings, which is the same thread for every slot.
|
||||
int g_modifierHolds[std::size(kModifierKeys)] = {};
|
||||
|
||||
// Whether this app is the one holding the key down, as opposed to having
|
||||
// found it already held. Recorded rather than re-tested on release, because
|
||||
// the test cannot tell the two apart: an injected key reads as pressed to
|
||||
// GetAsyncKeyState exactly like a real one, so asking again on the way out
|
||||
// would see our own press and decline to undo it.
|
||||
bool g_modifierSentByUs[std::size(kModifierKeys)] = {};
|
||||
|
||||
// Whether the key is down right now, by anyone's hand. Checked only before a
|
||||
// press, where the answer means "the user is holding it" — nothing of ours is
|
||||
// down at that point, or the hold count would have caught it first.
|
||||
bool PhysicallyHeld(uint16_t vk) {
|
||||
// The merged virtual key, not the left/right one: the user may be holding
|
||||
// the right-hand key while this sends the left, and either satisfies the
|
||||
// shortcut.
|
||||
uint16_t merged = vk;
|
||||
switch (vk) {
|
||||
case VK_LCONTROL: merged = VK_CONTROL; break;
|
||||
case VK_LMENU: merged = VK_MENU; break;
|
||||
case VK_LSHIFT: merged = VK_SHIFT; break;
|
||||
default: break;
|
||||
}
|
||||
return (GetAsyncKeyState(merged) & 0x8000) != 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void SendModifiers(uint8_t mods, bool down) {
|
||||
for (size_t i = 0; i < std::size(kModifierKeys); ++i) {
|
||||
const auto& m = kModifierKeys[i];
|
||||
if (!(mods & m.flag)) continue;
|
||||
|
||||
if (down) {
|
||||
// Only the first binding to want it does anything; the rest just
|
||||
// join the count so the key outlives their individual releases.
|
||||
if (g_modifierHolds[i]++ > 0) continue;
|
||||
// Already down under the user's own finger — leave it theirs, and
|
||||
// remember not to release what we never pressed.
|
||||
g_modifierSentByUs[i] = !PhysicallyHeld(m.vk);
|
||||
if (g_modifierSentByUs[i]) SendKeyInput(m.vk, true);
|
||||
} else {
|
||||
if (g_modifierHolds[i] == 0) continue; // never pressed, or already undone
|
||||
if (--g_modifierHolds[i] > 0) continue; // another binding still wants it
|
||||
if (!g_modifierSentByUs[i]) continue;
|
||||
g_modifierSentByUs[i] = false;
|
||||
SendKeyInput(m.vk, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring KeyComboDisplayName(const BackButtonBinding& binding) {
|
||||
if (binding.kind != BackButtonBinding::Kind::Key) return L"Key";
|
||||
|
||||
std::wstring name;
|
||||
for (const auto& m : kModifierKeys) {
|
||||
if (!(binding.mods & m.flag)) continue;
|
||||
name += m.name;
|
||||
name += L" + ";
|
||||
}
|
||||
return name + KeyDisplayName(binding.code);
|
||||
}
|
||||
|
||||
std::wstring KeyDisplayName(uint16_t vk) {
|
||||
if (vk == 0) return L"Key";
|
||||
|
||||
|
||||
Reference in new issue
Block a user