diff --git a/src/app/BackButtonConfig.h b/src/app/BackButtonConfig.h index b3f9297..103c5bd 100644 --- a/src/app/BackButtonConfig.h +++ b/src/app/BackButtonConfig.h @@ -2,7 +2,7 @@ #include #include -// Actions a back paddle can be mapped to. +// Built-in actions a back paddle can be mapped to. // Values are stable — persisted as DWORDs in the registry. // Entries 0-11 match the original ordering so existing settings are preserved. enum class BackButtonAction : uint8_t { @@ -20,15 +20,6 @@ enum class BackButtonAction : uint8_t { COUNT }; -struct BackButtonConfig { - // Default: the upper paddles left-click the mouse (this used to be the - // "Back Buttons as Mouse Click" tray toggle), the lower paddles are unbound. - BackButtonAction l4 = BackButtonAction::LeftMouseButton; - BackButtonAction l5 = BackButtonAction::None; - BackButtonAction r4 = BackButtonAction::LeftMouseButton; - BackButtonAction r5 = BackButtonAction::None; -}; - // String IDs match the JS input catalog (used in JSON messages to/from WebView2). inline const char* BackButtonActionId(BackButtonAction a) { static const char* ids[] = { @@ -63,3 +54,124 @@ inline BackButtonAction BackButtonActionFromId(const std::string& id) { if (id == "R3") return BackButtonAction::R3; return BackButtonAction::None; } + +// A paddle binding: either one of the built-in actions above, or a value drawn +// from a space too large to enumerate (a keyboard virtual-key code, an extended +// mouse button). +// +// Persisted as a single DWORD, (kind << 16) | code. Every value written by +// builds that stored a bare BackButtonAction is 0-18, so its kind bits are +// already zero and it decodes as Kind::Action — old settings load unchanged +// with no migration step. +struct BackButtonBinding { + enum class Kind : uint8_t { + Action = 0, // code is a BackButtonAction + Key = 1, // code is a Windows virtual-key code + MouseButton = 2, // code is a MouseButtonCode + }; + + // Extended mouse buttons. Left and right live in BackButtonAction instead, + // where they were first persisted. + enum class MouseButtonCode : uint16_t { Middle = 0, X1 = 1, X2 = 2, COUNT }; + + Kind kind = Kind::Action; + uint16_t code = static_cast(BackButtonAction::None); + + static BackButtonBinding FromAction(BackButtonAction a) { + return { Kind::Action, static_cast(a) }; + } + static BackButtonBinding FromKey(uint16_t vk) { + return { Kind::Key, vk }; + } + static BackButtonBinding FromMouseButton(MouseButtonCode b) { + return { Kind::MouseButton, static_cast(b) }; + } + + bool IsAction() const { return kind == Kind::Action; } + + // The built-in action this binding names, or None when it is a key or an + // extended mouse button. Callers that only understand actions can switch on + // this directly — anything they cannot represent reads as "do nothing". + BackButtonAction AsAction() const { + return kind == Kind::Action ? static_cast(code) + : BackButtonAction::None; + } + bool IsAction(BackButtonAction a) const { + return kind == Kind::Action && code == static_cast(a); + } + + bool operator==(const BackButtonBinding& o) const { + return kind == o.kind && code == o.code; + } + bool operator!=(const BackButtonBinding& o) const { return !(*this == o); } + + uint32_t Pack() const { + return (static_cast(kind) << 16) | code; + } + + // Anything unrecognised — a corrupt value, or one written by a future build + // that knows kinds we do not — degrades to None rather than being applied. + static BackButtonBinding Unpack(uint32_t packed) { + const auto kind = static_cast((packed >> 16) & 0xFF); + const auto code = static_cast(packed & 0xFFFF); + switch (kind) { + case Kind::Action: + return code < static_cast(BackButtonAction::COUNT) + ? BackButtonBinding{ Kind::Action, code } + : BackButtonBinding{}; + case Kind::Key: + return code != 0 ? BackButtonBinding{ Kind::Key, code } + : BackButtonBinding{}; + case Kind::MouseButton: + return code < static_cast(MouseButtonCode::COUNT) + ? BackButtonBinding{ Kind::MouseButton, code } + : BackButtonBinding{}; + } + return {}; + } + + // Wire format for the WebView2 UI. Built-in actions keep their bare ids + // ("A", "leftMouse", "none"); the open-ended kinds are prefixed. Staying + // with flat strings keeps the hand-rolled JSON reader in RemapWindow usable. + std::string Id() const { + switch (kind) { + case Kind::Key: + return "key:" + std::to_string(code); + case Kind::MouseButton: + switch (static_cast(code)) { + case MouseButtonCode::Middle: return "mouse:middle"; + case MouseButtonCode::X1: return "mouse:x1"; + case MouseButtonCode::X2: return "mouse:x2"; + default: return "none"; + } + case Kind::Action: + default: + return BackButtonActionId(AsAction()); + } + } + + static BackButtonBinding FromId(const std::string& id) { + if (id.rfind("key:", 0) == 0) { + uint32_t vk = 0; + for (size_t i = 4; i < id.size(); ++i) { + if (id[i] < '0' || id[i] > '9') return {}; + vk = vk * 10 + static_cast(id[i] - '0'); + if (vk > 0xFFFF) return {}; + } + return vk != 0 ? FromKey(static_cast(vk)) : BackButtonBinding{}; + } + if (id == "mouse:middle") return FromMouseButton(MouseButtonCode::Middle); + if (id == "mouse:x1") return FromMouseButton(MouseButtonCode::X1); + if (id == "mouse:x2") return FromMouseButton(MouseButtonCode::X2); + return FromAction(BackButtonActionFromId(id)); + } +}; + +struct BackButtonConfig { + // Default: the upper paddles left-click the mouse (this used to be the + // "Back Buttons as Mouse Click" tray toggle), the lower paddles are unbound. + BackButtonBinding l4 = BackButtonBinding::FromAction(BackButtonAction::LeftMouseButton); + BackButtonBinding l5 = BackButtonBinding::FromAction(BackButtonAction::None); + BackButtonBinding r4 = BackButtonBinding::FromAction(BackButtonAction::LeftMouseButton); + BackButtonBinding r5 = BackButtonBinding::FromAction(BackButtonAction::None); +}; diff --git a/src/app/ControllerManager.cpp b/src/app/ControllerManager.cpp index 3ea6516..9156e55 100644 --- a/src/app/ControllerManager.cpp +++ b/src/app/ControllerManager.cpp @@ -116,6 +116,8 @@ struct ControllerManager::Slot { static bool DetectCapture(const uint8_t* cur, const uint8_t* prev, BackButtonAction& out) { + // Controller input only ever yields a built-in action; the caller wraps the + // result. Keyboard and mouse capture arrive through the UI, not this loop. using BA = BackButtonAction; // Helper: rising edge on a single bit in a byte. @@ -304,7 +306,7 @@ void ControllerManager::SetControllerPlatform(ControllerPlatform platform) { } } -void ControllerManager::StartButtonCapture(std::function callback) { +void ControllerManager::StartButtonCapture(std::function callback) { std::lock_guard lk(m_captureMutex); m_captureCallback = std::move(callback); m_capturing = true; @@ -779,7 +781,7 @@ void ControllerManager::ReadLoop(Slot* slot) { // Fire mouse button events for back paddles mapped to LeftMouseButton/RightMouseButton. // Uses edge detection so we only send DOWN on press and UP on release. if (hasPrev) { - struct PaddleEdge { bool cur; bool prev; BackButtonAction action; }; + struct PaddleEdge { bool cur; bool prev; const BackButtonBinding& binding; }; const PaddleEdge edges[] = { { n>4 && (buf[4]&SteamController::BTN_L4)!=0, (prevBuf[4]&SteamController::BTN_L4)!=0, m_backConfig.l4 }, { n>4 && (buf[4]&SteamController::BTN_L5)!=0, (prevBuf[4]&SteamController::BTN_L5)!=0, m_backConfig.l5 }, @@ -789,10 +791,10 @@ void ControllerManager::ReadLoop(Slot* slot) { for (const auto& e : edges) { if (e.cur == e.prev) continue; DWORD flag = 0; - if (e.action == BackButtonAction::LeftMouseButton) { + if (e.binding.IsAction(BackButtonAction::LeftMouseButton)) { flag = e.cur ? MOUSEEVENTF_LEFTDOWN : MOUSEEVENTF_LEFTUP; slot->paddleMouseLeftDown = e.cur; - } else if (e.action == BackButtonAction::RightMouseButton) { + } else if (e.binding.IsAction(BackButtonAction::RightMouseButton)) { flag = e.cur ? MOUSEEVENTF_RIGHTDOWN : MOUSEEVENTF_RIGHTUP; slot->paddleMouseRightDown = e.cur; } @@ -807,7 +809,8 @@ void ControllerManager::ReadLoop(Slot* slot) { if (DetectCapture(buf, prevBuf, captured)) { m_capturing = false; std::lock_guard lk(m_captureMutex); - if (m_captureCallback) m_captureCallback(captured); + if (m_captureCallback) + m_captureCallback(BackButtonBinding::FromAction(captured)); } } diff --git a/src/app/ControllerManager.h b/src/app/ControllerManager.h index 3fe2666..c68e2d5 100644 --- a/src/app/ControllerManager.h +++ b/src/app/ControllerManager.h @@ -68,7 +68,7 @@ public: // Called by RemapWindow when a row enters/exits listening state. // Callback fires on the read thread — use PostMessage to marshal to the UI thread. - void StartButtonCapture(std::function callback); + void StartButtonCapture(std::function callback); void StopButtonCapture(); private: @@ -94,7 +94,7 @@ private: ControllerPlatform m_controllerPlatform = ControllerPlatform::Xbox; BackButtonConfig m_backConfig; - std::atomic m_capturing{false}; - std::mutex m_captureMutex; - std::function m_captureCallback; + std::atomic m_capturing{false}; + std::mutex m_captureMutex; + std::function m_captureCallback; }; diff --git a/src/app/RemapWindow.cpp b/src/app/RemapWindow.cpp index 5d18b53..1d0157c 100644 --- a/src/app/RemapWindow.cpp +++ b/src/app/RemapWindow.cpp @@ -419,12 +419,12 @@ LRESULT RemapWindow::HandleMessage(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { } case WM_BUTTON_CAPTURED: { - // Marshalled from the read thread via PostMessage. - auto action = static_cast(static_cast(wp)); - const char* id = BackButtonActionId(action); + // Marshalled from the read thread via PostMessage, packed into WPARAM. + const std::string id = + BackButtonBinding::Unpack(static_cast(wp)).Id(); if (m_webview) { std::wstring json = L"{\"type\":\"buttonCaptured\",\"button\":\""; - json += std::wstring(id, id + strlen(id)); + json += std::wstring(id.begin(), id.end()); json += L"\"}"; m_webview->PostWebMessageAsString(json.c_str()); } @@ -665,10 +665,10 @@ void RemapWindow::OnWebMessage(const std::wstring& raw) { std::string row = JsonStr(msg, "row"); if (!row.empty() && m_mgr) { HWND hwnd = m_hwnd; - m_mgr->StartButtonCapture([hwnd](BackButtonAction action) { + m_mgr->StartButtonCapture([hwnd](const BackButtonBinding& binding) { // Called from the read thread — use PostMessage to marshal to UI. PostMessageW(hwnd, WM_BUTTON_CAPTURED, - static_cast(static_cast(action)), 0); + static_cast(binding.Pack()), 0); }); } @@ -681,10 +681,10 @@ void RemapWindow::OnWebMessage(const std::wstring& raw) { if (bStart != std::string::npos) { std::string sub = msg.substr(bStart); BackButtonConfig cfg; - cfg.l4 = BackButtonActionFromId(JsonStr(sub, "L4")); - cfg.l5 = BackButtonActionFromId(JsonStr(sub, "L5")); - cfg.r4 = BackButtonActionFromId(JsonStr(sub, "R4")); - cfg.r5 = BackButtonActionFromId(JsonStr(sub, "R5")); + cfg.l4 = BackButtonBinding::FromId(JsonStr(sub, "L4")); + cfg.l5 = BackButtonBinding::FromId(JsonStr(sub, "L5")); + cfg.r4 = BackButtonBinding::FromId(JsonStr(sub, "R4")); + cfg.r5 = BackButtonBinding::FromId(JsonStr(sub, "R5")); m_config = cfg; if (m_applyCallback) m_applyCallback(cfg); } @@ -699,11 +699,9 @@ void RemapWindow::SendInitState() { if (!m_webview) return; // Convert ASCII action ID string to wstring without char→wchar_t narrowing warnings. - auto wid = [](BackButtonAction a) -> std::wstring { - const char* s = BackButtonActionId(a); - std::wstring w; - for (; *s; ++s) w += static_cast(*s); - return w; + auto wid = [](const BackButtonBinding& b) -> std::wstring { + const std::string s = b.Id(); + return std::wstring(s.begin(), s.end()); }; std::wstring json = diff --git a/src/app/TrayApp.cpp b/src/app/TrayApp.cpp index 3698b6f..6e61970 100644 --- a/src/app/TrayApp.cpp +++ b/src/app/TrayApp.cpp @@ -753,11 +753,12 @@ void TrayApp::LoadSettings() { // Existing installs keep exactly the bindings they had — the new // click-by-default only applies to fresh installs, which never reach here // and so take the BackButtonConfig defaults. + const DWORD unbound = BackButtonBinding::FromAction(BackButtonAction::None).Pack(); BackButtonConfig cfg; - cfg.l4 = static_cast(readDw(L"BackBtnL4", static_cast(BackButtonAction::None))); - cfg.l5 = static_cast(readDw(L"BackBtnL5", static_cast(BackButtonAction::None))); - cfg.r4 = static_cast(readDw(L"BackBtnR4", static_cast(BackButtonAction::None))); - cfg.r5 = static_cast(readDw(L"BackBtnR5", static_cast(BackButtonAction::None))); + cfg.l4 = BackButtonBinding::Unpack(readDw(L"BackBtnL4", unbound)); + cfg.l5 = BackButtonBinding::Unpack(readDw(L"BackBtnL5", unbound)); + cfg.r4 = BackButtonBinding::Unpack(readDw(L"BackBtnR4", unbound)); + cfg.r5 = BackButtonBinding::Unpack(readDw(L"BackBtnR5", unbound)); // Migrate the retired "Back Buttons as Mouse Click" toggle into real // bindings. The toggle used to hijack L4/R4 for a left click and silently @@ -766,8 +767,9 @@ void TrayApp::LoadSettings() { // all along and starts working again now that nothing overrides it. const bool migrateBackMouse = readDw(L"BackButtons", 0) != 0; if (migrateBackMouse) { - if (cfg.l4 == BackButtonAction::None) cfg.l4 = BackButtonAction::LeftMouseButton; - if (cfg.r4 == BackButtonAction::None) cfg.r4 = BackButtonAction::LeftMouseButton; + const auto leftClick = BackButtonBinding::FromAction(BackButtonAction::LeftMouseButton); + if (cfg.l4.IsAction(BackButtonAction::None)) cfg.l4 = leftClick; + if (cfg.r4.IsAction(BackButtonAction::None)) cfg.r4 = leftClick; } m_controller->SetBackButtonConfig(cfg); @@ -813,10 +815,10 @@ void TrayApp::SaveSettings() { writeDw(L"ControllerPlatform", m_controller->GetControllerPlatform() == ControllerPlatform::PlayStation ? 1 : 0); const auto& cfg = m_controller->GetBackButtonConfig(); - writeDw(L"BackBtnL4", static_cast(cfg.l4)); - writeDw(L"BackBtnL5", static_cast(cfg.l5)); - writeDw(L"BackBtnR4", static_cast(cfg.r4)); - writeDw(L"BackBtnR5", static_cast(cfg.r5)); + writeDw(L"BackBtnL4", cfg.l4.Pack()); + writeDw(L"BackBtnL5", cfg.l5.Pack()); + writeDw(L"BackBtnR4", cfg.r4.Pack()); + writeDw(L"BackBtnR5", cfg.r5.Pack()); RegCloseKey(key); } diff --git a/src/app/VirtualController.cpp b/src/app/VirtualController.cpp index 95d3083..bcce32d 100644 --- a/src/app/VirtualController.cpp +++ b/src/app/VirtualController.cpp @@ -27,8 +27,10 @@ static VOID CALLBACK X360Notification( static_cast(userData)->OnRumble(largeMotor, smallMotor); } -static void ApplyBackActionX360(BackButtonAction action, XUSB_REPORT& r) { - switch (action) { +// Keys and mouse buttons are delivered by ControllerManager via SendInput, not +// through the virtual pad, so they fall through as "no gamepad input". +static void ApplyBackActionX360(const BackButtonBinding& binding, XUSB_REPORT& r) { + switch (binding.AsAction()) { case BackButtonAction::A: r.wButtons |= XUSB_GAMEPAD_A; break; case BackButtonAction::B: r.wButtons |= XUSB_GAMEPAD_B; break; case BackButtonAction::X: r.wButtons |= XUSB_GAMEPAD_X; break; @@ -333,9 +335,9 @@ void VirtualController::Update(const uint8_t* buf, size_t n, r.bThumbRX = s16ToU8(rx); r.bThumbRY = static_cast(255u - s16ToU8(ry)); - // Back paddles - auto applyBack = [&](BackButtonAction action) { - switch (action) { + // Back paddles. As above, non-gamepad bindings fall through. + auto applyBack = [&](const BackButtonBinding& binding) { + switch (binding.AsAction()) { case BackButtonAction::A: r.wButtons |= DS4_BUTTON_CROSS; break; case BackButtonAction::B: r.wButtons |= DS4_BUTTON_CIRCLE; break; case BackButtonAction::X: r.wButtons |= DS4_BUTTON_SQUARE; break;