diff --git a/.gitignore b/.gitignore index 5a53fc3..54b2a22 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ Results/ HIDInfo HIDReportDescriptor.rtf +# Claude Design files +ClaudeDesign/ + # CMake user files CMakeSettings.json CMakeUserPresets.json diff --git a/CMakeLists.txt b/CMakeLists.txt index 0aeb572..f1c42c2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,8 @@ cmake_minimum_required(VERSION 3.20) project(SteamlessController CXX) +include(FetchContent) + set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -46,6 +48,20 @@ if (MSVC) target_compile_options(RawControllerProbe PRIVATE /W4) endif() +# WebView2 — downloaded from NuGet (.nupkg is a zip; FetchContent extracts it) +FetchContent_Declare(WebView2 + URL https://www.nuget.org/api/v2/package/Microsoft.Web.WebView2/1.0.2849.39 + DOWNLOAD_EXTRACT_TIMESTAMP ON +) +FetchContent_MakeAvailable(WebView2) + +# Map CMake architecture to the NuGet lib subdirectory name +if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(WV2_ARCH "x64") +else() + set(WV2_ARCH "x86") +endif() + # System tray app (no console window) add_executable(SteamlessController WIN32 src/app/main.cpp @@ -53,10 +69,21 @@ add_executable(SteamlessController WIN32 src/app/ControllerManager.cpp src/app/VirtualController.cpp src/app/TrackpadMouse.cpp + src/app/RemapWindow.cpp resources/app.rc ) -target_include_directories(SteamlessController PRIVATE resources) -target_link_libraries(SteamlessController PRIVATE SteamController ViGEmClient) +target_include_directories(SteamlessController PRIVATE + resources + ${webview2_SOURCE_DIR}/build/native/include +) +target_link_libraries(SteamlessController PRIVATE + SteamController + ViGEmClient + ${webview2_SOURCE_DIR}/build/native/${WV2_ARCH}/WebView2LoaderStatic.lib + version + ole32 + shlwapi +) if (MSVC) target_compile_options(SteamlessController PRIVATE /W4 /WX) endif() diff --git a/src/app/BackButtonConfig.h b/src/app/BackButtonConfig.h new file mode 100644 index 0000000..7459a79 --- /dev/null +++ b/src/app/BackButtonConfig.h @@ -0,0 +1,64 @@ +#pragma once +#include +#include + +// 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 { + // Row 1 — face buttons + A = 0, B, X, Y, + // Row 2 — bumpers / triggers (Steam nomenclature: L1, L2, R1, R2) + LB, RB, LT, RT, + // Row 3 — d-pad + DPadUp, DPadDown, DPadLeft, DPadRight, + // Row 4 — stick clicks, menu buttons, mouse buttons, and "none" (natural behavior) + LeftMouseButton, RightMouseButton, + None, + Menu, View, + L3, R3, + COUNT +}; + +struct BackButtonConfig { + // Default: each back paddle uses its natural behavior (mouse click when that mode is on). + BackButtonAction l4 = BackButtonAction::None; + BackButtonAction l5 = BackButtonAction::None; + BackButtonAction r4 = BackButtonAction::None; + 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[] = { + "A","B","X","Y", + "LB","RB","LT","RT", + "Up","Down","Left","Right", + "leftMouse","rightMouse", + "none","menu","view","L3","R3" + }; + auto i = static_cast(a); + return i < static_cast(BackButtonAction::COUNT) ? ids[i] : "none"; +} + +inline BackButtonAction BackButtonActionFromId(const std::string& id) { + if (id == "A") return BackButtonAction::A; + if (id == "B") return BackButtonAction::B; + if (id == "X") return BackButtonAction::X; + if (id == "Y") return BackButtonAction::Y; + if (id == "LB") return BackButtonAction::LB; + if (id == "RB") return BackButtonAction::RB; + if (id == "LT") return BackButtonAction::LT; + if (id == "RT") return BackButtonAction::RT; + if (id == "Up") return BackButtonAction::DPadUp; + if (id == "Down") return BackButtonAction::DPadDown; + if (id == "Left") return BackButtonAction::DPadLeft; + if (id == "Right") return BackButtonAction::DPadRight; + if (id == "leftMouse") return BackButtonAction::LeftMouseButton; + if (id == "rightMouse") return BackButtonAction::RightMouseButton; + if (id == "menu") return BackButtonAction::Menu; + if (id == "view") return BackButtonAction::View; + if (id == "L3") return BackButtonAction::L3; + if (id == "R3") return BackButtonAction::R3; + return BackButtonAction::None; +} diff --git a/src/app/ControllerManager.cpp b/src/app/ControllerManager.cpp index db04f1c..d62696c 100644 --- a/src/app/ControllerManager.cpp +++ b/src/app/ControllerManager.cpp @@ -2,15 +2,15 @@ #include "VirtualController.h" #include "TrackpadMouse.h" #include "steam/SteamController.h" +#include #include #include +#include #include #include // --------------------------------------------------------------------------- -// Slot — owns one physical controller and its virtual counterpart. -// All access is from the UI thread except ReadLoop, which only touches -// its own slot's sc/vc/trackpad while readRunning is true. +// Slot // --------------------------------------------------------------------------- struct ControllerManager::Slot { @@ -27,6 +27,53 @@ struct ControllerManager::Slot { Slot& operator=(const Slot&) = delete; }; +// --------------------------------------------------------------------------- +// Rising-edge capture: returns true (and sets 'out') when any of the 16 +// mappable XInput buttons transitions from not-pressed to pressed. +// Back paddles (L4/L5/R4/R5) are excluded — they are what we are remapping, +// not valid capture targets. +// --------------------------------------------------------------------------- + +static bool DetectCapture(const uint8_t* cur, const uint8_t* prev, + BackButtonAction& out) +{ + using BA = BackButtonAction; + + // Helper: rising edge on a single bit in a byte. + auto rose = [&](int byteIdx, uint8_t mask) -> bool { + return (cur[byteIdx] & mask) && !(prev[byteIdx] & mask); + }; + + // Triggers use 16-bit analog — fire on crossing 25% (0x2000). + auto trigRose = [&](int byteIdx) -> bool { + int16_t c, p; + memcpy(&c, cur + byteIdx, 2); + memcpy(&p, prev + byteIdx, 2); + return c > 0x2000 && p <= 0x2000; + }; + + // Check each mappable button in priority order. + // Byte offsets match SteamController.h for the 0x45 report. + if (rose(2, SteamController::BTN_A)) { out = BA::A; return true; } + if (rose(2, SteamController::BTN_B)) { out = BA::B; return true; } + if (rose(2, SteamController::BTN_X)) { out = BA::X; return true; } + if (rose(2, SteamController::BTN_Y)) { out = BA::Y; return true; } + if (rose(4, SteamController::BTN_LB)) { out = BA::LB; return true; } + if (rose(3, SteamController::BTN_RB)) { out = BA::RB; return true; } + if (trigRose(6)) { out = BA::LT; return true; } + if (trigRose(8)) { out = BA::RT; return true; } + if (rose(3, SteamController::BTN_DPAD_UP)) { out = BA::DPadUp; return true; } + if (rose(3, SteamController::BTN_DPAD_DN)) { out = BA::DPadDown; return true; } + if (rose(3, SteamController::BTN_DPAD_LT)) { out = BA::DPadLeft; return true; } + if (rose(3, SteamController::BTN_DPAD_RT)) { out = BA::DPadRight; return true; } + if (rose(2, SteamController::BTN_MENU)) { out = BA::Menu; return true; } + if (rose(3, SteamController::BTN_VIEW)) { out = BA::View; return true; } + if (rose(3, SteamController::BTN_LS)) { out = BA::L3; return true; } + if (rose(2, SteamController::BTN_RS)) { out = BA::R3; return true; } + + return false; +} + // --------------------------------------------------------------------------- // Lifecycle // --------------------------------------------------------------------------- @@ -38,8 +85,17 @@ ControllerManager::ControllerManager(StateChangedFn onStateChanged) } ControllerManager::~ControllerManager() { - for (auto& slot : m_slots) - DisableGameModeSlot(*slot); + for (auto& slot : m_slots) { + // Stop the read loop and virtual controller first (same as DisableGameModeSlot + // but without the gameModeActive guard — we always want to restore lizard mode). + if (slot->gameModeActive) { + StopReadLoop(*slot); + slot->trackpad.Reset(); + slot->vc.reset(); + slot->gameModeActive = false; + } + slot->sc->EnableLizardMode(); // always restore, even if game mode was never active + } } // --------------------------------------------------------------------------- @@ -74,16 +130,34 @@ void ControllerManager::SetTrackpadMouseEnabled(bool enabled) { slot->trackpad.SetTrackpadEnabled(enabled); } +void ControllerManager::SetUseLeftTrackpad(bool enabled) { + m_useLeftTrackpad = enabled; + for (auto& slot : m_slots) + slot->trackpad.SetUseLeftTrackpad(enabled); +} + +void ControllerManager::SetBackButtonConfig(const BackButtonConfig& cfg) { + m_backConfig = cfg; + // Live update: the ReadLoop reads m_backConfig on every frame, so the + // change takes effect on the very next report. +} + void ControllerManager::SetBackButtonsEnabled(bool enabled) { m_backButtonsEnabled = enabled; for (auto& slot : m_slots) slot->trackpad.SetBackButtonsEnabled(enabled); } -void ControllerManager::SetUseLeftTrackpad(bool enabled) { - m_useLeftTrackpad = enabled; - for (auto& slot : m_slots) - slot->trackpad.SetUseLeftTrackpad(enabled); +void ControllerManager::StartButtonCapture(std::function callback) { + std::lock_guard lk(m_captureMutex); + m_captureCallback = std::move(callback); + m_capturing = true; +} + +void ControllerManager::StopButtonCapture() { + m_capturing = false; + std::lock_guard lk(m_captureMutex); + m_captureCallback = nullptr; } // --------------------------------------------------------------------------- @@ -93,7 +167,6 @@ void ControllerManager::SetUseLeftTrackpad(bool enabled) { void ControllerManager::SyncDevices() { auto livePaths = SteamController::EnumerateAll(); - // Remove slots whose device is no longer live. auto it = m_slots.begin(); while (it != m_slots.end()) { bool alive = std::any_of(livePaths.begin(), livePaths.end(), @@ -106,7 +179,6 @@ void ControllerManager::SyncDevices() { } } - // Add slots for newly connected devices. for (auto const& path : livePaths) { bool already = std::any_of(m_slots.begin(), m_slots.end(), [&](const auto& s) { return s->path == path; }); @@ -125,10 +197,12 @@ void ControllerManager::OpenSlot(const std::wstring& path) { m_slots.push_back(std::move(slot)); - // If game mode is already active, bring the new controller in immediately. if (IsGameModeActive()) { bool dummy = false; EnableGameModeSlot(*m_slots.back(), dummy); + } else { + // Restore lizard mode in case a previous session crashed without cleaning up. + m_slots.back()->sc->EnableLizardMode(); } } @@ -151,8 +225,8 @@ void ControllerManager::EnableGameModeSlot(Slot& slot, bool& vigemMissingOut) { slot.gameModeActive = true; slot.trackpad.Reset(); slot.trackpad.SetTrackpadEnabled(m_trackpadMouseEnabled); - slot.trackpad.SetBackButtonsEnabled(m_backButtonsEnabled); slot.trackpad.SetUseLeftTrackpad(m_useLeftTrackpad); + slot.trackpad.SetBackButtonsEnabled(m_backButtonsEnabled); StartReadLoop(slot); } @@ -182,12 +256,50 @@ void ControllerManager::StopReadLoop(Slot& slot) { void ControllerManager::ReadLoop(Slot* slot) { uint8_t buf[64]; + uint8_t prevBuf[64] = {}; + bool hasPrev = false; + while (slot->readRunning) { size_t n = slot->sc->ReadReport(buf, sizeof(buf), /*timeoutMs=*/32); if (n == 0) continue; if (buf[0] != SteamController::REPORT_STATE) continue; - if (slot->vc) slot->vc->Update(buf, n); + + if (slot->vc) slot->vc->Update(buf, n, m_backConfig, m_backButtonsEnabled); slot->trackpad.Update(buf, n); + + // 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. + // Skipped when backButtonsEnabled — TrackpadMouse already handles L4/R4 in that mode. + if (!m_backButtonsEnabled && hasPrev) { + struct PaddleEdge { bool cur; bool prev; BackButtonAction action; }; + 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 }, + { n>2 && (buf[2]&SteamController::BTN_R4)!=0, (prevBuf[2]&SteamController::BTN_R4)!=0, m_backConfig.r4 }, + { n>3 && (buf[3]&SteamController::BTN_R5)!=0, (prevBuf[3]&SteamController::BTN_R5)!=0, m_backConfig.r5 }, + }; + for (const auto& e : edges) { + if (e.cur == e.prev) continue; + DWORD flag = 0; + if (e.action == BackButtonAction::LeftMouseButton) flag = e.cur ? MOUSEEVENTF_LEFTDOWN : MOUSEEVENTF_LEFTUP; + else if (e.action == BackButtonAction::RightMouseButton) flag = e.cur ? MOUSEEVENTF_RIGHTDOWN : MOUSEEVENTF_RIGHTUP; + if (flag) { INPUT inp{}; inp.type = INPUT_MOUSE; inp.mi.dwFlags = flag; SendInput(1, &inp, sizeof(INPUT)); } + } + } + + // Button capture for the remap window (press-to-bind). + // Only fires on a rising edge to avoid repeat-triggering on hold. + if (m_capturing.load() && hasPrev) { + BackButtonAction captured; + if (DetectCapture(buf, prevBuf, captured)) { + m_capturing = false; + std::lock_guard lk(m_captureMutex); + if (m_captureCallback) m_captureCallback(captured); + } + } + + memcpy(prevBuf, buf, n); + hasPrev = true; } } diff --git a/src/app/ControllerManager.h b/src/app/ControllerManager.h index 5ddc499..9ac21e0 100644 --- a/src/app/ControllerManager.h +++ b/src/app/ControllerManager.h @@ -1,6 +1,9 @@ #pragma once +#include "BackButtonConfig.h" +#include #include #include +#include #include #include @@ -13,27 +16,31 @@ public: ControllerManager(const ControllerManager&) = delete; ControllerManager& operator=(const ControllerManager&) = delete; - // Called when Windows reports a device arrival or removal (WM_DEVICECHANGE). void OnDeviceChange(); - // Toggle game mode on/off. Applied to all connected controllers. void EnableGameMode(); void DisableGameMode(); void SetTrackpadMouseEnabled(bool enabled); - void SetBackButtonsEnabled(bool enabled); void SetUseLeftTrackpad(bool enabled); + void SetBackButtonConfig(const BackButtonConfig& cfg); + void SetBackButtonsEnabled(bool enabled); - bool IsConnected() const { return !m_slots.empty(); } - bool IsGameModeActive() const; - bool IsTrackpadMouseEnabled() const { return m_trackpadMouseEnabled; } - bool IsBackButtonsEnabled() const { return m_backButtonsEnabled; } - bool IsUseLeftTrackpad() const { return m_useLeftTrackpad; } + bool IsConnected() const { return !m_slots.empty(); } + bool IsGameModeActive() const; + bool IsTrackpadMouseEnabled() const { return m_trackpadMouseEnabled; } + bool IsUseLeftTrackpad() const { return m_useLeftTrackpad; } + bool IsBackButtonsEnabled() const { return m_backButtonsEnabled; } + const BackButtonConfig& GetBackButtonConfig() const { return m_backConfig; } + + // 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 StopButtonCapture(); private: struct Slot; - // Enumerates live devices, adds new slots, removes disconnected ones. void SyncDevices(); void OpenSlot(const std::wstring& path); void EnableGameModeSlot(Slot& slot, bool& vigemMissingOut); @@ -46,6 +53,11 @@ private: StateChangedFn m_onStateChanged; std::vector> m_slots; bool m_trackpadMouseEnabled = false; - bool m_backButtonsEnabled = false; bool m_useLeftTrackpad = false; + bool m_backButtonsEnabled = false; + BackButtonConfig m_backConfig; + + 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 new file mode 100644 index 0000000..7f6b07a --- /dev/null +++ b/src/app/RemapWindow.cpp @@ -0,0 +1,648 @@ +#include "RemapWindow.h" +#include "ControllerManager.h" +#include +#include +#include +#include +#include + +RemapWindow* RemapWindow::s_instance = nullptr; + +// --------------------------------------------------------------------------- +// Embedded UI — self-contained HTML/CSS/JS matching the design handoff. +// Google Fonts are loaded at runtime; if offline, system-ui fallback applies. +// --------------------------------------------------------------------------- + +static const std::wstring& GetHtml() { + // Narrow literal (65 535 byte limit — much more than needed) avoids MSVC + // C2026 "string too big" that fires at ~16 K for wide literals. + // HTML is ASCII-only (JS glyphs use \uXXXX escapes); each byte widens 1:1. + static const std::wstring html = []() -> std::wstring { + static const char kHtml[] = R"HTML( + + + + + + + + + + + + +
+
+
+ SteamlessController + — Back Button Mapping +
+
+ + +
+
+ +
+
+

Back Button Mapping

+

Click Rebind on a button, then press any gamepad button on your controller. The new binding shows up here instantly.

+
+
+
LEFT GRIP
+
+
+
+
+
RIGHT GRIP
+
+
+
+
+ + + + + + +)HTML"; + std::wstring w; + w.reserve(sizeof(kHtml)); + for (const char* p = kHtml; *p; ++p) + w += static_cast(static_cast(*p)); + return w; + }(); + return html; +} + +// --------------------------------------------------------------------------- +// Window procedure +// --------------------------------------------------------------------------- + +LRESULT CALLBACK RemapWindow::WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { + if (s_instance) return s_instance->HandleMessage(hwnd, msg, wp, lp); + return DefWindowProcW(hwnd, msg, wp, lp); +} + +LRESULT RemapWindow::HandleMessage(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { + switch (msg) { + case WM_SIZE: + if (m_controller) { + RECT bounds; + GetClientRect(hwnd, &bounds); + m_controller->put_Bounds(bounds); + } + return 0; + + case WM_NCHITTEST: { + // Let Windows handle non-client areas (resize border, etc.) first. + LRESULT hit = DefWindowProcW(hwnd, msg, wp, lp); + if (hit == HTCLIENT) { + POINT pt = { GET_X_LPARAM(lp), GET_Y_LPARAM(lp) }; + ScreenToClient(hwnd, &pt); + RECT rc; + GetClientRect(hwnd, &rc); + // The right ~96px of the title bar hosts our close/min buttons — + // leave those as HTCLIENT so WebView2 can receive the clicks. + UINT dpi = GetDpiForWindow(hwnd); + int tbPx = MulDiv(46, dpi, 96); // CSS 46px → physical pixels + int ctlPx = MulDiv(96, dpi, 96); + if (pt.y < tbPx && pt.x < rc.right - ctlPx) + return HTCAPTION; + } + return hit; + } + + case WM_BUTTON_CAPTURED: { + // Marshalled from the read thread via PostMessage. + auto action = static_cast(static_cast(wp)); + const char* id = BackButtonActionId(action); + if (m_webview) { + std::wstring json = L"{\"type\":\"buttonCaptured\",\"button\":\""; + json += std::wstring(id, id + strlen(id)); + json += L"\"}"; + m_webview->PostWebMessageAsString(json.c_str()); + } + return 0; + } + + case WM_CLOSE: + ShowWindow(hwnd, SW_HIDE); + return 0; + + case WM_DESTROY: + if (m_mgr) m_mgr->StopButtonCapture(); + m_webview.Reset(); + m_controller.Reset(); + m_env.Reset(); + m_hwnd = nullptr; + return 0; + } + return DefWindowProcW(hwnd, msg, wp, lp); +} + +// --------------------------------------------------------------------------- +// Public interface +// --------------------------------------------------------------------------- + +RemapWindow::~RemapWindow() { + if (m_hwnd) DestroyWindow(m_hwnd); + s_instance = nullptr; +} + +void RemapWindow::Open(HINSTANCE hInst, ControllerManager* mgr, + const BackButtonConfig& cfg, + std::function applyCallback) +{ + // If already open, just bring it to front. + if (m_hwnd) { + if (IsWindowVisible(m_hwnd)) { BringToFront(); return; } + // Was hidden. Refresh config and show. + m_config = cfg; + m_applyCallback = std::move(applyCallback); + ShowWindow(m_hwnd, SW_SHOW); + BringToFront(); + SendInitState(); + return; + } + + // --- Check WebView2 runtime --- + WCHAR* versionStr = nullptr; + if (FAILED(GetAvailableCoreWebView2BrowserVersionString(nullptr, &versionStr)) || !versionStr) { + MessageBoxW(nullptr, + L"Back Button Remapping requires the Microsoft Edge WebView2 Runtime.\n\n" + L"Please update Windows or download the runtime from microsoft.com/edge/webview2.", + L"WebView2 Runtime Not Found", MB_OK | MB_ICONINFORMATION); + return; + } + CoTaskMemFree(versionStr); + + m_hInst = hInst; + m_mgr = mgr; + m_config = cfg; + m_applyCallback = std::move(applyCallback); + s_instance = this; + + // --- Register window class (once) --- + WNDCLASSEXW wc{}; + wc.cbSize = sizeof(wc); + wc.lpfnWndProc = WndProc; + wc.hInstance = hInst; + wc.hCursor = LoadCursor(nullptr, IDC_ARROW); + wc.hbrBackground = reinterpret_cast(COLOR_WINDOW + 1); + wc.lpszClassName = CLASS_NAME; + wc.style = CS_DROPSHADOW; + RegisterClassExW(&wc); // OK if already registered + + // --- Create the frameless popup window, centered on screen --- + int screenW = GetSystemMetrics(SM_CXSCREEN); + int screenH = GetSystemMetrics(SM_CYSCREEN); + int x = (screenW - WINDOW_W) / 2; + int y = (screenH - WINDOW_H) / 2; + + m_hwnd = CreateWindowExW( + 0, CLASS_NAME, L"Back Button Mapping", + WS_POPUP | WS_SYSMENU | WS_MINIMIZEBOX, + x, y, WINDOW_W, WINDOW_H, + nullptr, nullptr, hInst, nullptr); + + if (!m_hwnd) return; + + ShowWindow(m_hwnd, SW_SHOW); + UpdateWindow(m_hwnd); + + // WebView2 initialization is async; callbacks run on the UI thread + // via the app's existing GetMessage/DispatchMessage loop in TrayApp::Run(). + CreateWebViewAsync(m_hwnd); +} + +void RemapWindow::BringToFront() const { + if (!m_hwnd) return; + SetForegroundWindow(m_hwnd); + BringWindowToTop(m_hwnd); +} + +// --------------------------------------------------------------------------- +// WebView2 async init chain +// --------------------------------------------------------------------------- + +void RemapWindow::CreateWebViewAsync(HWND hwnd) { + // Use %LOCALAPPDATA%\SteamlessController\WebView2 for the browser data dir. + wchar_t localApp[MAX_PATH] = {}; + SHGetFolderPathW(nullptr, CSIDL_LOCAL_APPDATA, nullptr, 0, localApp); + std::wstring dataDir = std::wstring(localApp) + L"\\SteamlessController\\WebView2"; + + using namespace Microsoft::WRL; + + HRESULT hr = CreateCoreWebView2EnvironmentWithOptions( + nullptr, dataDir.c_str(), nullptr, + Callback( + [this, hwnd](HRESULT result, ICoreWebView2Environment* env) -> HRESULT { + if (FAILED(result) || !env) return S_OK; + m_env = env; + + m_env->CreateCoreWebView2Controller(hwnd, + Callback( + [this](HRESULT result, ICoreWebView2Controller* ctrl) -> HRESULT { + if (FAILED(result) || !ctrl) return S_OK; + OnControllerReady(ctrl); + return S_OK; + } + ).Get()); + + return S_OK; + } + ).Get()); + + if (FAILED(hr)) { + // WebView2 creation failed unexpectedly after version check — bail. + DestroyWindow(hwnd); + } +} + +void RemapWindow::OnControllerReady(ICoreWebView2Controller* ctrl) { + m_controller = ctrl; + m_controller->get_CoreWebView2(&m_webview); + + // Settings: disable context menu and dev tools in production. + Microsoft::WRL::ComPtr settings; + m_webview->get_Settings(&settings); + if (settings) { + settings->put_AreDefaultContextMenusEnabled(FALSE); + settings->put_IsStatusBarEnabled(FALSE); +#ifndef _DEBUG + settings->put_AreDevToolsEnabled(FALSE); +#endif + } + + // Size the WebView2 to fill the window. + RECT bounds; + GetClientRect(m_hwnd, &bounds); + m_controller->put_Bounds(bounds); + m_controller->put_IsVisible(TRUE); + + // Register the web message handler. Tokens are not stored since the handlers + // persist for the lifetime of the WebView2 object (destroyed in WM_DESTROY). + EventRegistrationToken token{}; + (void)m_webview->add_WebMessageReceived( + Microsoft::WRL::Callback( + [this](ICoreWebView2*, ICoreWebView2WebMessageReceivedEventArgs* args) -> HRESULT { + LPWSTR raw = nullptr; + args->TryGetWebMessageAsString(&raw); + if (raw) { + OnWebMessage(std::wstring(raw)); + CoTaskMemFree(raw); + } + return S_OK; + } + ).Get(), &token); + + // Send initial state to the page once navigation completes. + EventRegistrationToken navToken{}; + (void)m_webview->add_NavigationCompleted( + Microsoft::WRL::Callback( + [this](ICoreWebView2*, ICoreWebView2NavigationCompletedEventArgs*) -> HRESULT { + SendInitState(); + return S_OK; + } + ).Get(), &navToken); + + // Navigate to the embedded HTML. + m_webview->NavigateToString(GetHtml().c_str()); +} + +// --------------------------------------------------------------------------- +// Message routing: JS → C++ +// --------------------------------------------------------------------------- + +// Minimal JSON string extractor — finds "key":"value" in a flat JSON object. +static std::string JsonStr(const std::string& json, const std::string& key) { + std::string needle = "\"" + key + "\":\""; + size_t pos = json.find(needle); + if (pos == std::string::npos) return {}; + pos += needle.size(); + size_t end = json.find('"', pos); + return end != std::string::npos ? json.substr(pos, end - pos) : std::string{}; +} + +void RemapWindow::OnWebMessage(const std::wstring& raw) { + // All message content is ASCII; narrow explicitly to suppress C4244. + std::string msg; + msg.reserve(raw.size()); + for (wchar_t c : raw) msg += static_cast(c); + + std::string type = JsonStr(msg, "type"); + + if (type == "startDrag") { + // Safe to call from WndProc context (we're on the UI thread). + ReleaseCapture(); + PostMessageW(m_hwnd, WM_SYSCOMMAND, SC_MOVE | HTCAPTION, 0); + + } else if (type == "minimize") { + ShowWindow(m_hwnd, SW_MINIMIZE); + + } else if (type == "close") { + PostMessageW(m_hwnd, WM_CLOSE, 0, 0); + + } else if (type == "startListening") { + std::string row = JsonStr(msg, "row"); + if (!row.empty() && m_mgr) { + HWND hwnd = m_hwnd; + m_mgr->StartButtonCapture([hwnd](BackButtonAction action) { + // Called from the read thread — use PostMessage to marshal to UI. + PostMessageW(hwnd, WM_BUTTON_CAPTURED, + static_cast(static_cast(action)), 0); + }); + } + + } else if (type == "stopListening") { + if (m_mgr) m_mgr->StopButtonCapture(); + + } else if (type == "apply") { + // Extract the four bindings from the nested "bindings" object. + size_t bStart = msg.find("\"bindings\":"); + 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")); + m_config = cfg; + if (m_applyCallback) m_applyCallback(cfg); + } + } +} + +void RemapWindow::PostToWebView(const std::wstring& jsonStr) { + if (m_webview) m_webview->PostWebMessageAsString(jsonStr.c_str()); +} + +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; + }; + + std::wstring json = + L"{\"type\":\"init\",\"bindings\":{" + L"\"L4\":\"" + wid(m_config.l4) + L"\"," + L"\"L5\":\"" + wid(m_config.l5) + L"\"," + L"\"R4\":\"" + wid(m_config.r4) + L"\"," + L"\"R5\":\"" + wid(m_config.r5) + L"\"" + L"}}"; + PostToWebView(json); +} diff --git a/src/app/RemapWindow.h b/src/app/RemapWindow.h new file mode 100644 index 0000000..328422b --- /dev/null +++ b/src/app/RemapWindow.h @@ -0,0 +1,56 @@ +#pragma once +#include +#include +#include +#include +#include "BackButtonConfig.h" + +class ControllerManager; + +class RemapWindow { +public: + RemapWindow() = default; + ~RemapWindow(); + RemapWindow(const RemapWindow&) = delete; + RemapWindow& operator=(const RemapWindow&) = delete; + + // Opens (or re-focuses) the remap window. + // Shows a "not supported" message box and returns without opening if WebView2 + // is unavailable (Windows 7/8 or no runtime installed). + // applyCallback fires on the UI thread when the user clicks Apply. + void Open(HINSTANCE hInst, ControllerManager* mgr, const BackButtonConfig& cfg, + std::function applyCallback); + + void BringToFront() const; + bool IsOpen() const { return m_hwnd != nullptr; } + +private: + static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp); + LRESULT HandleMessage(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp); + + void CreateWebViewAsync(HWND hwnd); + void OnControllerReady(ICoreWebView2Controller* ctrl); + void OnWebMessage(const std::wstring& raw); + void PostToWebView(const std::wstring& jsonStr); + void SendInitState(); + + // Called from the read thread via PostMessage — marshals a captured button + // back to the UI thread so we can call PostWebMessageAsString safely. + static constexpr UINT WM_BUTTON_CAPTURED = WM_APP + 1; + + HWND m_hwnd = nullptr; + HINSTANCE m_hInst = nullptr; + ControllerManager* m_mgr = nullptr; + BackButtonConfig m_config; + std::function m_applyCallback; + + Microsoft::WRL::ComPtr m_env; + Microsoft::WRL::ComPtr m_controller; + Microsoft::WRL::ComPtr m_webview; + + static RemapWindow* s_instance; + + static constexpr wchar_t CLASS_NAME[] = L"SteamlessRemapWindow"; + static constexpr int WINDOW_W = 760; + static constexpr int WINDOW_H = 668; // 46 titlebar + ~560 body + 62 footer +}; diff --git a/src/app/TrackpadMouse.cpp b/src/app/TrackpadMouse.cpp index 9fa9448..56a9740 100644 --- a/src/app/TrackpadMouse.cpp +++ b/src/app/TrackpadMouse.cpp @@ -2,6 +2,7 @@ #include "steam/SteamController.h" #include #include +#include static constexpr uint8_t BTN_TP_RT_CLICK = 0x40; // buf[4] bit 6 — hard press @@ -14,80 +15,71 @@ static void SendMouseButton(DWORD flags) { void TrackpadMouse::Reset() { if (m_prevClick) SendMouseButton(MOUSEEVENTF_LEFTUP); + if (m_prevL4) SendMouseButton(MOUSEEVENTF_LEFTUP); if (m_prevR4) SendMouseButton(MOUSEEVENTF_LEFTUP); - if (m_prevR5) SendMouseButton(MOUSEEVENTF_RIGHTUP); m_touching = false; m_prevClick = false; + m_prevL4 = false; m_prevR4 = false; - m_prevR5 = false; m_prevX = 0; m_prevY = 0; } void TrackpadMouse::Update(const uint8_t* buf, size_t n) { - if (n < 30) return; + // L4 (upper-left paddle) and R4 (upper-right paddle) → left mouse click. + // This works independently of trackpad mouse mode. + if (m_backButtonsEnabled && n > 4) { + bool l4 = (buf[4] & SteamController::BTN_L4) != 0; + bool r4 = (n > 2) && (buf[2] & SteamController::BTN_R4) != 0; + if (l4 != m_prevL4) { + SendMouseButton(l4 ? MOUSEEVENTF_LEFTDOWN : MOUSEEVENTF_LEFTUP); + m_prevL4 = l4; + } + if (r4 != m_prevR4) { + SendMouseButton(r4 ? MOUSEEVENTF_LEFTDOWN : MOUSEEVENTF_LEFTUP); + m_prevR4 = r4; + } + } + + if (n < 30 || !m_trackpadEnabled) return; - const uint8_t b0 = buf[2]; - const uint8_t b1 = buf[3]; const uint8_t b2 = buf[4]; const uint8_t b3 = buf[5]; - // --- Trackpad mouse movement and click --- - if (m_trackpadEnabled) { - const bool touching = m_useLeftTrackpad - ? (b3 & SteamController::BTN_TP_LT) != 0 - : (b2 & SteamController::BTN_TP_RT) != 0; - const bool clicking = m_useLeftTrackpad - ? (b3 & SteamController::BTN_TP_LT_CLICK) != 0 - : (b2 & BTN_TP_RT_CLICK) != 0; + const bool touching = m_useLeftTrackpad + ? (b3 & SteamController::BTN_TP_LT) != 0 + : (b2 & SteamController::BTN_TP_RT) != 0; + const bool clicking = m_useLeftTrackpad + ? (b3 & SteamController::BTN_TP_LT_CLICK) != 0 + : (b2 & BTN_TP_RT_CLICK) != 0; - int16_t x = 0, y = 0; - if (m_useLeftTrackpad) { - memcpy(&x, buf + 18, 2); - memcpy(&y, buf + 20, 2); - } else { - memcpy(&x, buf + 24, 2); - memcpy(&y, buf + 26, 2); - } + int16_t x = 0, y = 0; + if (m_useLeftTrackpad) { + memcpy(&x, buf + 18, 2); + memcpy(&y, buf + 20, 2); + } else { + memcpy(&x, buf + 24, 2); + memcpy(&y, buf + 26, 2); + } - if (touching && m_touching) { - const int dx = static_cast(x - m_prevX); - const int dy = -static_cast(y - m_prevY); // negate: up = up - if (dx != 0 || dy != 0) { - INPUT input{}; - input.type = INPUT_MOUSE; - input.mi.dwFlags = MOUSEEVENTF_MOVE; - input.mi.dx = static_cast(dx * SENSITIVITY); - input.mi.dy = static_cast(dy * SENSITIVITY); - SendInput(1, &input, sizeof(INPUT)); - } - } - - if (touching) { m_prevX = x; m_prevY = y; } - m_touching = touching; - - if (clicking != m_prevClick) { - SendMouseButton(clicking ? MOUSEEVENTF_LEFTDOWN : MOUSEEVENTF_LEFTUP); - m_prevClick = clicking; + if (touching && m_touching) { + const int dx = static_cast(x - m_prevX); + const int dy = -static_cast(y - m_prevY); + if (dx != 0 || dy != 0) { + INPUT input{}; + input.type = INPUT_MOUSE; + input.mi.dwFlags = MOUSEEVENTF_MOVE; + input.mi.dx = static_cast(dx * SENSITIVITY); + input.mi.dy = static_cast(dy * SENSITIVITY); + SendInput(1, &input, sizeof(INPUT)); } } - // --- Back buttons: left side uses L4/L5, right side uses R4/R5 --- - if (m_backButtonsEnabled) { - const bool btn1 = m_useLeftTrackpad - ? (b2 & SteamController::BTN_L4) != 0 // L4 = left click - : (b0 & SteamController::BTN_R4) != 0; // R4 = left click - const bool btn2 = m_useLeftTrackpad - ? (b2 & SteamController::BTN_L5) != 0 // L5 = right click - : (b1 & SteamController::BTN_R5) != 0; // R5 = right click + if (touching) { m_prevX = x; m_prevY = y; } + m_touching = touching; - if (btn1 != m_prevR4) { - SendMouseButton(btn1 ? MOUSEEVENTF_LEFTDOWN : MOUSEEVENTF_LEFTUP); - m_prevR4 = btn1; - } - if (btn2 != m_prevR5) { - SendMouseButton(btn2 ? MOUSEEVENTF_RIGHTDOWN : MOUSEEVENTF_RIGHTUP); - m_prevR5 = btn2; - } + if (clicking != m_prevClick) { + SendMouseButton(clicking ? MOUSEEVENTF_LEFTDOWN : MOUSEEVENTF_LEFTUP); + m_prevClick = clicking; } } diff --git a/src/app/TrackpadMouse.h b/src/app/TrackpadMouse.h index e0eb903..745ce8e 100644 --- a/src/app/TrackpadMouse.h +++ b/src/app/TrackpadMouse.h @@ -4,24 +4,25 @@ class TrackpadMouse { public: - void SetTrackpadEnabled(bool enabled) { m_trackpadEnabled = enabled; } - void SetBackButtonsEnabled(bool enabled) { m_backButtonsEnabled = enabled; } - void SetUseLeftTrackpad(bool enabled) { m_useLeftTrackpad = enabled; } + void SetTrackpadEnabled(bool enabled) { m_trackpadEnabled = enabled; } + void SetUseLeftTrackpad(bool enabled) { m_useLeftTrackpad = enabled; } + void SetBackButtonsEnabled(bool enabled){ m_backButtonsEnabled = enabled; } + bool IsBackButtonsEnabled() const { return m_backButtonsEnabled; } void Update(const uint8_t* buf, size_t n); void Reset(); private: - bool m_trackpadEnabled = false; + bool m_trackpadEnabled = false; + bool m_useLeftTrackpad = false; bool m_backButtonsEnabled = false; - bool m_useLeftTrackpad = false; - bool m_touching = false; - bool m_prevClick = false; - bool m_prevR4 = false; - bool m_prevR5 = false; - int16_t m_prevX = 0; - int16_t m_prevY = 0; + bool m_touching = false; + bool m_prevClick = false; + bool m_prevL4 = false; + bool m_prevR4 = false; + int16_t m_prevX = 0; + int16_t m_prevY = 0; static constexpr float SENSITIVITY = 0.015f; }; diff --git a/src/app/TrayApp.cpp b/src/app/TrayApp.cpp index ec99600..8e55d82 100644 --- a/src/app/TrayApp.cpp +++ b/src/app/TrayApp.cpp @@ -40,7 +40,6 @@ bool TrayApp::Init(HINSTANCE hInstance) { DEV_BROADCAST_DEVICEINTERFACE_W filter{}; filter.dbcc_size = sizeof(filter); filter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE; - // HID device interface GUID filter.dbcc_classguid = {0x4D1E55B2, 0xF16F, 0x11CF, {0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30}}; RegisterDeviceNotificationW(m_hwnd, &filter, DEVICE_NOTIFY_WINDOW_HANDLE); @@ -82,6 +81,8 @@ LRESULT TrayApp::HandleMessage(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { if (LOWORD(lp) == NIN_BALLOONUSERCLICK) ShellExecuteW(nullptr, L"open", L"https://github.com/nefarius/ViGEmBus/releases/latest", nullptr, nullptr, SW_SHOWNORMAL); + else if (LOWORD(lp) == WM_LBUTTONDBLCLK) + OpenRemapWindow(); else if (LOWORD(lp) == WM_RBUTTONUP || LOWORD(lp) == WM_LBUTTONUP) ShowContextMenu(); return 0; @@ -98,6 +99,9 @@ LRESULT TrayApp::HandleMessage(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { m_controller->SetTrackpadMouseEnabled(!m_controller->IsTrackpadMouseEnabled()); SaveSettings(); break; + case IDM_REMAP_BACK: + OpenRemapWindow(); + break; case IDM_BACKBUTTONS: m_controller->SetBackButtonsEnabled(!m_controller->IsBackButtonsEnabled()); SaveSettings(); @@ -128,6 +132,17 @@ LRESULT TrayApp::HandleMessage(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { return DefWindowProcW(hwnd, msg, wp, lp); } +void TrayApp::OpenRemapWindow() { + m_remapWindow.Open( + m_hInstance, + m_controller.get(), + m_controller->GetBackButtonConfig(), + [this](const BackButtonConfig& cfg) { + m_controller->SetBackButtonConfig(cfg); + SaveSettings(); + }); +} + void TrayApp::AddTrayIcon() { NOTIFYICONDATAW nid{}; nid.cbSize = sizeof(nid); @@ -152,18 +167,17 @@ void TrayApp::RemoveTrayIcon() { void TrayApp::UpdateTrayIcon(bool connected, bool gameModeActive, bool vigemMissing) { if (vigemMissing) { ShowViGEmBalloon(); return; } - bool gameModeOn = gameModeActive; - const wchar_t* tip = gameModeOn ? L"Steamless Controller — Steamless Mode ON" - : connected ? L"Steamless Controller — Connected (Steamless Mode OFF)" - : L"Steamless Controller — No controller found"; + const wchar_t* tip = gameModeActive ? L"Steamless Controller — Steamless Mode ON" + : connected ? L"Steamless Controller — Connected (Steamless Mode OFF)" + : L"Steamless Controller — No controller found"; NOTIFYICONDATAW nid{}; nid.cbSize = sizeof(nid); nid.hWnd = m_hwnd; nid.uID = TRAY_UID; nid.uFlags = NIF_TIP | NIF_ICON; - nid.hIcon = gameModeOn ? m_iconOn : m_iconOff; + nid.hIcon = gameModeActive ? m_iconOn : m_iconOff; wcscpy_s(nid.szTip, tip); Shell_NotifyIconW(NIM_MODIFY, &nid); } @@ -216,17 +230,24 @@ void TrayApp::LoadSettings() { if (RegOpenKeyExW(HKEY_CURRENT_USER, REG_KEY, 0, KEY_READ, &key) != ERROR_SUCCESS) return; - auto readBool = [&](const wchar_t* name, bool def) -> bool { + auto readDw = [&](const wchar_t* name, DWORD def) -> DWORD { DWORD val = 0, size = sizeof(val); if (RegQueryValueExW(key, name, nullptr, nullptr, reinterpret_cast(&val), &size) == ERROR_SUCCESS) - return val != 0; + return val; return def; }; - m_controller->SetTrackpadMouseEnabled(readBool(L"TrackpadMouse", false)); - m_controller->SetBackButtonsEnabled (readBool(L"BackButtons", false)); - m_controller->SetUseLeftTrackpad (readBool(L"UseLeftTrackpad", false)); + m_controller->SetTrackpadMouseEnabled(readDw(L"TrackpadMouse", 0) != 0); + m_controller->SetBackButtonsEnabled (readDw(L"BackButtons", 0) != 0); + m_controller->SetUseLeftTrackpad (readDw(L"UseLeftTrackpad", 0) != 0); + + 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))); + m_controller->SetBackButtonConfig(cfg); RegCloseKey(key); } @@ -238,26 +259,31 @@ void TrayApp::SaveSettings() { &key, nullptr) != ERROR_SUCCESS) return; - auto writeBool = [&](const wchar_t* name, bool val) { - DWORD dw = val ? 1 : 0; + auto writeDw = [&](const wchar_t* name, DWORD val) { RegSetValueExW(key, name, 0, REG_DWORD, - reinterpret_cast(&dw), sizeof(dw)); + reinterpret_cast(&val), sizeof(val)); }; - writeBool(L"TrackpadMouse", m_controller->IsTrackpadMouseEnabled()); - writeBool(L"BackButtons", m_controller->IsBackButtonsEnabled()); - writeBool(L"UseLeftTrackpad", m_controller->IsUseLeftTrackpad()); + writeDw(L"TrackpadMouse", m_controller->IsTrackpadMouseEnabled() ? 1 : 0); + writeDw(L"BackButtons", m_controller->IsBackButtonsEnabled() ? 1 : 0); + writeDw(L"UseLeftTrackpad", m_controller->IsUseLeftTrackpad() ? 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)); RegCloseKey(key); } void TrayApp::ShowContextMenu() { - bool connected = m_controller->IsConnected(); - bool gameModeOn = m_controller->IsGameModeActive(); - bool trackpadOn = m_controller->IsTrackpadMouseEnabled(); - bool backButtonsOn = m_controller->IsBackButtonsEnabled(); - bool leftTrackpad = m_controller->IsUseLeftTrackpad(); - bool startupOn = IsStartupEnabled(); + bool connected = m_controller->IsConnected(); + bool gameModeOn = m_controller->IsGameModeActive(); + bool trackpadOn = m_controller->IsTrackpadMouseEnabled(); + bool backMouseOn = m_controller->IsBackButtonsEnabled(); + bool leftPad = m_controller->IsUseLeftTrackpad(); + bool startupOn = IsStartupEnabled(); HMENU menu = CreatePopupMenu(); @@ -267,19 +293,21 @@ void TrayApp::ShowContextMenu() { AppendMenuW(menu, MF_SEPARATOR, 0, nullptr); - UINT trackpadFlags = MF_STRING | (trackpadOn ? MF_CHECKED : MF_UNCHECKED); - AppendMenuW(menu, trackpadFlags, IDM_TRACKPAD, L"Enable Trackpad Mouse"); + AppendMenuW(menu, MF_STRING | (trackpadOn ? MF_CHECKED : MF_UNCHECKED), + IDM_TRACKPAD, L"Enable Trackpad Mouse"); - UINT backFlags = MF_STRING | (backButtonsOn ? MF_CHECKED : MF_UNCHECKED); - AppendMenuW(menu, backFlags, IDM_BACKBUTTONS, L"Enable Back Buttons for Clicking"); + AppendMenuW(menu, MF_STRING | (backMouseOn ? MF_CHECKED : MF_UNCHECKED), + IDM_BACKBUTTONS, L"Back Buttons as Mouse Click"); - UINT leftFlags = MF_STRING | (leftTrackpad ? MF_CHECKED : MF_UNCHECKED); - AppendMenuW(menu, leftFlags, IDM_LEFT_TRACKPAD, L"Use Left Trackpad Instead"); + AppendMenuW(menu, MF_STRING | (leftPad ? MF_CHECKED : MF_UNCHECKED), + IDM_LEFT_TRACKPAD, L"Use Left Trackpad Instead"); + + AppendMenuW(menu, MF_STRING, IDM_REMAP_BACK, L"Remap Back Buttons..."); AppendMenuW(menu, MF_SEPARATOR, 0, nullptr); - UINT startupFlags = MF_STRING | (startupOn ? MF_CHECKED : MF_UNCHECKED); - AppendMenuW(menu, startupFlags, IDM_STARTUP, L"Start with Windows"); + AppendMenuW(menu, MF_STRING | (startupOn ? MF_CHECKED : MF_UNCHECKED), + IDM_STARTUP, L"Start with Windows"); AppendMenuW(menu, MF_SEPARATOR, 0, nullptr); AppendMenuW(menu, MF_STRING, IDM_EXIT, L"Exit"); diff --git a/src/app/TrayApp.h b/src/app/TrayApp.h index 8914583..0f46600 100644 --- a/src/app/TrayApp.h +++ b/src/app/TrayApp.h @@ -1,6 +1,7 @@ #pragma once #include #include +#include "RemapWindow.h" class ControllerManager; @@ -23,22 +24,25 @@ private: void ShowContextMenu(); void LoadSettings(); void SaveSettings(); + void OpenRemapWindow(); bool IsStartupEnabled() const; void SetStartupEnabled(bool enabled); - HWND m_hwnd = nullptr; - HINSTANCE m_hInstance = nullptr; - UINT m_wmTaskbar = 0; - HICON m_iconOff = nullptr; - HICON m_iconOn = nullptr; + HWND m_hwnd = nullptr; + HINSTANCE m_hInstance = nullptr; + UINT m_wmTaskbar = 0; + HICON m_iconOff = nullptr; + HICON m_iconOn = nullptr; std::unique_ptr m_controller; + RemapWindow m_remapWindow; static constexpr UINT IDM_TOGGLE = 1001; static constexpr UINT IDM_EXIT = 1002; static constexpr UINT IDM_TRACKPAD = 1003; - static constexpr UINT IDM_BACKBUTTONS = 1004; + static constexpr UINT IDM_REMAP_BACK = 1004; static constexpr UINT IDM_LEFT_TRACKPAD = 1005; static constexpr UINT IDM_STARTUP = 1006; - static constexpr UINT WM_TRAY = WM_APP + 1; - static constexpr UINT TRAY_UID = 1; + static constexpr UINT IDM_BACKBUTTONS = 1007; + static constexpr UINT WM_TRAY = WM_APP + 1; + static constexpr UINT TRAY_UID = 1; }; diff --git a/src/app/VirtualController.cpp b/src/app/VirtualController.cpp index 83568b2..3c03d9f 100644 --- a/src/app/VirtualController.cpp +++ b/src/app/VirtualController.cpp @@ -8,6 +8,33 @@ // Report translation — 0x45 → XUSB_REPORT // --------------------------------------------------------------------------- +static void ApplyBackAction(BackButtonAction action, XUSB_REPORT& r) { + switch (action) { + 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; + case BackButtonAction::Y: r.wButtons |= XUSB_GAMEPAD_Y; break; + case BackButtonAction::LB: r.wButtons |= XUSB_GAMEPAD_LEFT_SHOULDER; break; + case BackButtonAction::RB: r.wButtons |= XUSB_GAMEPAD_RIGHT_SHOULDER; break; + case BackButtonAction::LT: r.bLeftTrigger = 255; break; + case BackButtonAction::RT: r.bRightTrigger = 255; break; + case BackButtonAction::DPadUp: r.wButtons |= XUSB_GAMEPAD_DPAD_UP; break; + case BackButtonAction::DPadDown: r.wButtons |= XUSB_GAMEPAD_DPAD_DOWN; break; + case BackButtonAction::DPadLeft: r.wButtons |= XUSB_GAMEPAD_DPAD_LEFT; break; + case BackButtonAction::DPadRight: r.wButtons |= XUSB_GAMEPAD_DPAD_RIGHT; break; + case BackButtonAction::Menu: r.wButtons |= XUSB_GAMEPAD_START; break; + case BackButtonAction::View: r.wButtons |= XUSB_GAMEPAD_BACK; break; + case BackButtonAction::L3: r.wButtons |= XUSB_GAMEPAD_LEFT_THUMB; break; + case BackButtonAction::R3: r.wButtons |= XUSB_GAMEPAD_RIGHT_THUMB; break; + // Mouse button actions are handled via SendInput in the ReadLoop (edge-detected). + // None/mouse actions produce no XInput output here. + case BackButtonAction::None: + case BackButtonAction::LeftMouseButton: + case BackButtonAction::RightMouseButton: + default: break; + } +} + static XUSB_REPORT Translate(const uint8_t* buf, size_t n) { XUSB_REPORT r{}; if (n < 18) return r; @@ -104,9 +131,24 @@ VirtualController::~VirtualController() { } } -void VirtualController::Update(const uint8_t* buf, size_t n) { +void VirtualController::Update(const uint8_t* buf, size_t n, const BackButtonConfig& backCfg, bool backMouseEnabled) { if (!m_valid) return; XUSB_REPORT report = Translate(buf, n); + + // Inject back paddle buttons as their mapped XInput actions. + // L4 and R4 are skipped when "back buttons as mouse click" is active — + // TrackpadMouse handles them as left-mouse-button presses instead. + if (n > 4) { + if (!backMouseEnabled && (buf[4] & SteamController::BTN_L4)) ApplyBackAction(backCfg.l4, report); + if (buf[4] & SteamController::BTN_L5) ApplyBackAction(backCfg.l5, report); + } + if (n > 2) { + if (!backMouseEnabled && (buf[2] & SteamController::BTN_R4)) ApplyBackAction(backCfg.r4, report); + } + if (n > 3) { + if (buf[3] & SteamController::BTN_R5) ApplyBackAction(backCfg.r5, report); + } + vigem_target_x360_update(static_cast(m_client), static_cast(m_target), report); diff --git a/src/app/VirtualController.h b/src/app/VirtualController.h index 04840fc..ca80e63 100644 --- a/src/app/VirtualController.h +++ b/src/app/VirtualController.h @@ -1,6 +1,7 @@ #pragma once #include #include +#include "BackButtonConfig.h" class VirtualController { public: @@ -12,7 +13,7 @@ public: bool IsValid() const { return m_valid; } bool IsDriverMissing() const { return m_driverMissing; } - void Update(const uint8_t* buf, size_t n); + void Update(const uint8_t* buf, size_t n, const BackButtonConfig& backCfg, bool backMouseEnabled); private: void* m_client = nullptr; diff --git a/src/app/main.cpp b/src/app/main.cpp index d37d521..0a03c06 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -2,6 +2,10 @@ #include int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int) { + // Enable per-monitor v2 DPI awareness so the WebView2 window renders crisp + // on high-DPI displays and the WM_NCHITTEST pixel math stays correct. + SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + // Prevent multiple instances. HANDLE mutex = CreateMutexW(nullptr, TRUE, L"SteamlessController_SingleInstance"); if (!mutex || GetLastError() == ERROR_ALREADY_EXISTS) {