#include "TrayApp.h" #include "ControllerManager.h" #include "ControllerPlatform.h" #include "DeviceRestart.h" #include "EventLog.h" #include "steam/SteamController.h" #include "resource.h" #include #include #include #include static TrayApp* g_app = nullptr; static constexpr wchar_t WNDCLASS_NAME[] = L"SteamlessControllerTray"; static constexpr wchar_t CYCLE_TASK_NAME[] = L"SteamlessControllerDeviceCycle"; static constexpr wchar_t LEGACY_STARTUP_TASK_NAME[] = L"SteamlessController"; static constexpr wchar_t HELPER_EXE_NAME[] = L"SteamlessDeviceCycle.exe"; // Runs a console tool with no visible window; true when it exits 0. static bool RunToolHidden(std::wstring cmdline) { STARTUPINFOW si{}; si.cb = sizeof(si); si.dwFlags = STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE; PROCESS_INFORMATION pi{}; if (!CreateProcessW(nullptr, cmdline.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) return false; WaitForSingleObject(pi.hProcess, 15000); DWORD code = 1; GetExitCodeProcess(pi.hProcess, &code); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return code == 0; } // Path of the helper exe, expected beside our own executable. static std::wstring HelperPath() { wchar_t exe[MAX_PATH]; GetModuleFileNameW(nullptr, exe, MAX_PATH); std::wstring path(exe); const size_t slash = path.find_last_of(L'\\'); if (slash != std::wstring::npos) path.resize(slash + 1); path += HELPER_EXE_NAME; return path; } TrayApp::TrayApp() { g_app = this; } TrayApp::~TrayApp() { // Stop the watcher before anything else so its callback can't post to a // window (or reference a controller) that is being torn down. m_steamWatcher.Stop(); RemoveTrayIcon(); g_app = nullptr; } bool TrayApp::Init(HINSTANCE hInstance) { m_hInstance = hInstance; m_iconOff = LoadIconW(hInstance, MAKEINTRESOURCEW(IDI_ICON_OFF)); m_iconOn = LoadIconW(hInstance, MAKEINTRESOURCEW(IDI_ICON_ON)); m_wmTaskbar = RegisterWindowMessageW(L"TaskbarCreated"); WNDCLASSEXW wc{}; wc.cbSize = sizeof(wc); wc.lpfnWndProc = WndProc; wc.hInstance = hInstance; wc.lpszClassName = WNDCLASS_NAME; if (!RegisterClassExW(&wc)) return false; // Message-only window — invisible, never shown. m_hwnd = CreateWindowExW(0, WNDCLASS_NAME, L"SteamlessController", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, hInstance, nullptr); if (!m_hwnd) return false; // Register for HID device arrival/removal notifications. DEV_BROADCAST_DEVICEINTERFACE_W filter{}; filter.dbcc_size = sizeof(filter); filter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE; filter.dbcc_classguid = {0x4D1E55B2, 0xF16F, 0x11CF, {0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30}}; RegisterDeviceNotificationW(m_hwnd, &filter, DEVICE_NOTIFY_WINDOW_HANDLE); EventLog::Write("=== SteamlessController started ==="); m_controller = std::make_unique( [this](bool connected, bool gameModeActive, bool vigemMissing) { UpdateTrayIcon(connected, gameModeActive, vigemMissing); }); m_controller->SetAlertCallback( [this](const std::wstring& title, const std::wstring& text) { { std::lock_guard lk(m_alertMutex); m_alertTitle = title; m_alertText = text; } PostMessageW(m_hwnd, WM_ALERT, 0, 0); }); LoadSettings(); // Converge the startup mechanism with the loaded mode — e.g. the first // elevated run after enabling in-game mode migrates the Run key to the // highest-privileges logon task (and back when the mode changes). UpdateStartupRegistration(); AddTrayIcon(); UpdateTrayIcon(m_controller->IsConnected(), m_controller->IsGameModeActive(), false); // Start watching for Steam regardless of auto mode — the state is applied // only when auto mode is on, and having it warm makes toggling auto mode // take effect instantly. The initial callback asserts the correct state // at startup (which also self-heals after a previous crash). m_steamWatcher.Start([this](SteamState state) { PostMessageW(m_hwnd, WM_STEAMSTATE, static_cast(state), 0); }); return true; } int TrayApp::Run() { MSG msg; BOOL ret; while ((ret = GetMessageW(&msg, nullptr, 0, 0)) != 0) { if (ret == -1) return -1; TranslateMessage(&msg); DispatchMessageW(&msg); } return static_cast(msg.wParam); } LRESULT CALLBACK TrayApp::WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { if (g_app) return g_app->HandleMessage(hwnd, msg, wp, lp); return DefWindowProcW(hwnd, msg, wp, lp); } LRESULT TrayApp::HandleMessage(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { if (msg == m_wmTaskbar) { AddTrayIcon(); return 0; } switch (msg) { case WM_TRAY: 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; case WM_COMMAND: switch (LOWORD(wp)) { case IDM_TOGGLE: if (m_controller->IsGameModeActive()) m_controller->DisableGameMode(); else m_controller->EnableGameMode(); break; case IDM_TRACKPAD: m_controller->SetTrackpadMouseEnabled(!m_controller->IsTrackpadMouseEnabled()); SaveSettings(); break; case IDM_REMAP_BACK: OpenRemapWindow(); break; case IDM_BACKBUTTONS: m_controller->SetBackButtonsEnabled(!m_controller->IsBackButtonsEnabled()); SaveSettings(); break; case IDM_LEFT_TRACKPAD: m_controller->SetUseLeftTrackpad(!m_controller->IsUseLeftTrackpad()); SaveSettings(); break; case IDM_PLATFORM_XBOX: m_controller->SetControllerPlatform(ControllerPlatform::Xbox); SaveSettings(); break; case IDM_PLATFORM_PS: m_controller->SetControllerPlatform(ControllerPlatform::PlayStation); SaveSettings(); break; case IDM_MODE_MANUAL: SetAutoMode(AutoMode::Manual); break; case IDM_MODE_STEAM: SetAutoMode(AutoMode::OffWhileSteam); break; case IDM_MODE_GAME: // Make sure the elevated helper task exists (one-time UAC when // the installer didn't register it) before the mode needs it. if (!IsProcessElevated()) EnsureCycleTaskRegistered(); SetAutoMode(AutoMode::OffOnlyInGame); break; case IDM_STARTUP: m_startupEnabled = !m_startupEnabled; UpdateStartupRegistration(); break; case IDM_OPENLOG: OpenEventLog(); break; case IDM_NOTIFICATIONS: m_notificationsEnabled = !m_notificationsEnabled; EventLog::Write("USER: disconnect notifications %s", m_notificationsEnabled ? "enabled" : "disabled"); SaveSettings(); break; case IDM_EXIT: EventLog::Write("=== SteamlessController exiting (user request) ==="); m_controller->DisableGameMode(); PostQuitMessage(0); break; } return 0; case WM_STEAMSTATE: ApplySteamState(static_cast(wp)); return 0; case WM_ALERT: { if (!m_notificationsEnabled) return 0; // the cause is still in the event log std::wstring title, text; { std::lock_guard lk(m_alertMutex); title = m_alertTitle; text = m_alertText; } if (!title.empty()) ShowAlertBalloon(title, text); return 0; } case WM_TIMER: if (wp == IDT_ACQUIRE) { KillTimer(m_hwnd, IDT_ACQUIRE); if (m_autoMode != AutoMode::Manual && WantControl(m_steamWatcher.GetState())) TryAcquireController(); } return 0; case WM_DEVICECHANGE: if (wp == DBT_DEVICEARRIVAL || wp == DBT_DEVICEREMOVECOMPLETE) { m_controller->OnDeviceChange(); // A controller arriving (plugged in, or re-arriving after a device // cycle) should be acquired without a manual toggle when the // active auto mode wants control right now. if (wp == DBT_DEVICEARRIVAL && m_autoMode != AutoMode::Manual && WantControl(m_steamWatcher.GetState())) TryAcquireController(); } return TRUE; case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProcW(hwnd, msg, wp, lp); } void TrayApp::SetAutoMode(AutoMode mode) { EventLog::Write("MODE: control mode set to %d (0=manual 1=offWhileSteam 2=offOnlyInGame)", static_cast(mode)); m_autoMode = mode; m_acquireRetries = 0; SaveSettings(); UpdateStartupRegistration(); if (mode != AutoMode::Manual) ApplySteamState(m_steamWatcher.GetState()); } bool TrayApp::WantControl(SteamState state) const { switch (m_autoMode) { case AutoMode::OffWhileSteam: return state == SteamState::NoSteam; case AutoMode::OffOnlyInGame: return state != SteamState::InGame; default: return false; // Manual: tray toggle decides } } void TrayApp::ApplySteamState(SteamState state) { if (m_autoMode == AutoMode::Manual) return; EventLog::Write("AUTO: steam state %d (0=none 1=idle 2=inGame) -> %s", static_cast(state), WantControl(state) ? "take control" : "yield"); if (WantControl(state)) { m_acquireRetries = 0; TryAcquireController(); } else { KillTimer(m_hwnd, IDT_ACQUIRE); m_acquireRetries = 0; const bool hadControl = m_controller->IsGameModeActive(); // Good citizen: restore lizard mode and close our HID handles so // Steam can claim the controller without contention. m_controller->ReleaseDevices(); // Steam only (re)opens controllers on device-arrival events. If it is // already running and we held the device, it never saw one — cycle // the device so Steam adopts it immediately. if (hadControl && state != SteamState::NoSteam) RestartControllerDevices(); } } void TrayApp::TryAcquireController() { m_controller->OnDeviceChange(); m_controller->EnableGameMode(); // A short burst of rapid retries: right after a device cycle we race // Steam's re-enumeration for the exclusive open, and starting the claim // the instant the device arrives is what wins that race. for (int i = 0; i < 10 && !m_controller->IsGameModeActive(); ++i) { Sleep(50); m_controller->OnDeviceChange(); m_controller->EnableGameMode(); } if (m_controller->IsGameModeActive()) { m_acquireRetries = 0; return; } if (!m_controller->IsConnected()) return; // nothing plugged in — arrival will retrigger // Steam holds a write handle, so our exclusive claim can't succeed while // its handle lives. Cycle the device to invalidate it and retry on // re-arrival (the timer is a fallback in case the arrival event is missed). if (m_acquireRetries >= MAX_ACQUIRE_CYCLES) { EventLog::Write("AUTO: giving up acquiring after %d device cycles", MAX_ACQUIRE_CYCLES); return; } ++m_acquireRetries; EventLog::Write("AUTO: exclusive claim blocked — cycling device (attempt %d)", m_acquireRetries); m_controller->ReleaseDevices(); if (!RestartControllerDevices()) return; // helper unavailable — balloon shown // The cycle runs asynchronously via the helper task; the device-arrival // notification drives the claim, with this timer as the fallback. SetTimer(m_hwnd, IDT_ACQUIRE, 2500, nullptr); } bool TrayApp::RestartControllerDevices() { // On the rare elevated run, cycle directly. if (IsProcessElevated()) { bool anyRestarted = false; for (const auto& path : SteamController::EnumerateAll()) if (DeviceRestart::RestartInterfaceDevice(path)) anyRestarted = true; return anyRestarted; } // Normal path: fire the elevated helper via its scheduled task. Starting // a task the user authored needs no elevation, so no UAC prompt here. std::wstring run = L"schtasks.exe /Run /TN \""; run += CYCLE_TASK_NAME; run += L"\""; if (RunToolHidden(run)) return true; // Task missing (copied exes without installing?) — register it with a // one-time UAC prompt, then retry. if (!EnsureCycleTaskRegistered()) return false; return RunToolHidden(std::move(run)); } void TrayApp::ShowElevationBalloon() { if (m_elevationBalloonShown) return; m_elevationBalloonShown = true; NOTIFYICONDATAW nid{}; nid.cbSize = sizeof(nid); nid.hWnd = m_hwnd; nid.uID = TRAY_UID; nid.uFlags = NIF_INFO; nid.dwInfoFlags = NIIF_WARNING; wcscpy_s(nid.szInfoTitle, L"One-time setup needed"); wcscpy_s(nid.szInfo, L"Approve the administrator prompt to enable in-game controller " L"handoff (select the mode again to retry)."); Shell_NotifyIconW(NIM_MODIFY, &nid); } 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); nid.hWnd = m_hwnd; nid.uID = TRAY_UID; nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; nid.uCallbackMessage = WM_TRAY; nid.hIcon = m_iconOff; wcscpy_s(nid.szTip, L"Steamless Controller"); Shell_NotifyIconW(NIM_ADD, &nid); nid.uVersion = NOTIFYICON_VERSION_4; Shell_NotifyIconW(NIM_SETVERSION, &nid); } void TrayApp::RemoveTrayIcon() { NOTIFYICONDATAW nid{}; nid.cbSize = sizeof(nid); nid.hWnd = m_hwnd; nid.uID = TRAY_UID; Shell_NotifyIconW(NIM_DELETE, &nid); } void TrayApp::UpdateTrayIcon(bool connected, bool gameModeActive, bool vigemMissing) { if (vigemMissing) { ShowViGEmBalloon(); return; } 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 = gameModeActive ? m_iconOn : m_iconOff; wcscpy_s(nid.szTip, tip); Shell_NotifyIconW(NIM_MODIFY, &nid); } void TrayApp::ShowAlertBalloon(const std::wstring& title, const std::wstring& text) { NOTIFYICONDATAW nid{}; nid.cbSize = sizeof(nid); nid.hWnd = m_hwnd; nid.uID = TRAY_UID; nid.uFlags = NIF_INFO; nid.dwInfoFlags = NIIF_WARNING; wcsncpy_s(nid.szInfoTitle, title.c_str(), _TRUNCATE); wcsncpy_s(nid.szInfo, text.c_str(), _TRUNCATE); Shell_NotifyIconW(NIM_MODIFY, &nid); } void TrayApp::OpenEventLog() { // Touch the log so the file exists even on a fresh install. EventLog::Write("USER: opened event log"); // Open with Notepad explicitly — .log has no guaranteed file association. ShellExecuteW(nullptr, L"open", L"notepad.exe", EventLog::FilePath().c_str(), nullptr, SW_SHOWNORMAL); } void TrayApp::ShowViGEmBalloon() { NOTIFYICONDATAW nid{}; nid.cbSize = sizeof(nid); nid.hWnd = m_hwnd; nid.uID = TRAY_UID; nid.uFlags = NIF_INFO; nid.dwInfoFlags = NIIF_WARNING; wcscpy_s(nid.szInfoTitle, L"Driver required"); wcscpy_s(nid.szInfo, L"ViGEmBus is not installed. Click here to download it."); Shell_NotifyIconW(NIM_MODIFY, &nid); } static constexpr wchar_t REG_KEY[] = L"Software\\SteamlessController"; static constexpr wchar_t REG_RUN_KEY[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Run"; static constexpr wchar_t APP_NAME[] = L"SteamlessController"; bool TrayApp::RunKeyExists() const { HKEY key; if (RegOpenKeyExW(HKEY_CURRENT_USER, REG_RUN_KEY, 0, KEY_READ, &key) != ERROR_SUCCESS) return false; bool exists = RegQueryValueExW(key, APP_NAME, nullptr, nullptr, nullptr, nullptr) == ERROR_SUCCESS; RegCloseKey(key); return exists; } void TrayApp::SetRunKey(bool enabled) { HKEY key; if (RegOpenKeyExW(HKEY_CURRENT_USER, REG_RUN_KEY, 0, KEY_WRITE, &key) != ERROR_SUCCESS) return; if (enabled) { wchar_t path[MAX_PATH]; GetModuleFileNameW(nullptr, path, MAX_PATH); RegSetValueExW(key, APP_NAME, 0, REG_SZ, reinterpret_cast(path), static_cast((wcslen(path) + 1) * sizeof(wchar_t))); } else { RegDeleteValueW(key, APP_NAME); } RegCloseKey(key); } bool TrayApp::IsProcessElevated() { HANDLE token = nullptr; if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) return false; TOKEN_ELEVATION elev{}; DWORD size = sizeof(elev); const bool ok = GetTokenInformation(token, TokenElevation, &elev, sizeof(elev), &size) != 0; CloseHandle(token); return ok && elev.TokenIsElevated != 0; } bool TrayApp::EnsureCycleTaskRegistered() { // Already registered? (The installer normally does this.) { std::wstring query = L"schtasks.exe /Query /TN \""; query += CYCLE_TASK_NAME; query += L"\""; if (RunToolHidden(std::move(query))) return true; } // One-time UAC prompt: the helper registers its own on-demand task. SHELLEXECUTEINFOW sei{}; sei.cbSize = sizeof(sei); sei.fMask = SEE_MASK_NOCLOSEPROCESS; const std::wstring helper = HelperPath(); sei.lpVerb = L"runas"; sei.lpFile = helper.c_str(); sei.lpParameters = L"--register"; sei.nShow = SW_HIDE; if (!ShellExecuteExW(&sei) || !sei.hProcess) { // UAC declined — in-game mode degrades: we can't cycle the device // away from a running Steam, but takeover when Steam exits still works. ShowElevationBalloon(); return false; } WaitForSingleObject(sei.hProcess, 30000); DWORD code = 1; GetExitCodeProcess(sei.hProcess, &code); CloseHandle(sei.hProcess); if (code != 0) ShowElevationBalloon(); return code == 0; } void TrayApp::DeleteLegacyStartupTask() { // A pre-release build registered an elevated logon task for the tray app // itself; running elevated at logon resurrects the UIPI cursor-freeze // bug, so remove it best-effort (also done by the helper's --register). std::wstring cmd = L"schtasks.exe /Delete /F /TN \""; cmd += LEGACY_STARTUP_TASK_NAME; cmd += L"\""; RunToolHidden(std::move(cmd)); } void TrayApp::UpdateStartupRegistration() { // Startup always uses the HKCU Run key — the tray app never runs // elevated, so the Run key works unconditionally. if (m_startupMechanism == 2) DeleteLegacyStartupTask(); const int desired = m_startupEnabled ? 1 : 0; SetRunKey(desired == 1); m_startupMechanism = desired; SaveSettings(); } void TrayApp::LoadSettings() { HKEY key; if (RegOpenKeyExW(HKEY_CURRENT_USER, REG_KEY, 0, KEY_READ, &key) != ERROR_SUCCESS) return; 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; return def; }; // 0 = Manual, 1 = OffWhileSteam, 2 = OffOnlyInGame (old builds stored 0/1). const DWORD mode = readDw(L"AutoSteamMode", 0); m_autoMode = mode == 2 ? AutoMode::OffOnlyInGame : mode == 1 ? AutoMode::OffWhileSteam : AutoMode::Manual; m_controller->SetTrackpadMouseEnabled(readDw(L"TrackpadMouse", 0) != 0); m_controller->SetBackButtonsEnabled (readDw(L"BackButtons", 0) != 0); m_controller->SetUseLeftTrackpad (readDw(L"UseLeftTrackpad", 0) != 0); m_controller->SetControllerPlatform (readDw(L"ControllerPlatform", 0) != 0 ? ControllerPlatform::PlayStation : ControllerPlatform::Xbox); 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); m_notificationsEnabled = readDw(L"ShowNotifications", 1) != 0; // Startup mechanism: 0 none, 1 Run key, 2 elevated task. Migrate installs // that predate the setting by probing the Run key they would have used. m_startupMechanism = static_cast(readDw(L"StartupMechanism", RunKeyExists() ? 1 : 0)); m_startupEnabled = m_startupMechanism != 0; RegCloseKey(key); } void TrayApp::SaveSettings() { HKEY key; if (RegCreateKeyExW(HKEY_CURRENT_USER, REG_KEY, 0, nullptr, REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, &key, nullptr) != ERROR_SUCCESS) return; auto writeDw = [&](const wchar_t* name, DWORD val) { RegSetValueExW(key, name, 0, REG_DWORD, reinterpret_cast(&val), sizeof(val)); }; writeDw(L"AutoSteamMode", static_cast(m_autoMode)); writeDw(L"StartupMechanism", static_cast(m_startupMechanism)); writeDw(L"ShowNotifications", m_notificationsEnabled ? 1 : 0); writeDw(L"TrackpadMouse", m_controller->IsTrackpadMouseEnabled() ? 1 : 0); writeDw(L"BackButtons", m_controller->IsBackButtonsEnabled() ? 1 : 0); writeDw(L"UseLeftTrackpad", m_controller->IsUseLeftTrackpad() ? 1 : 0); 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)); RegCloseKey(key); } void TrayApp::ShowContextMenu() { 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 = m_startupEnabled; HMENU menu = CreatePopupMenu(); // Manual toggle is disabled while an auto mode owns the decision — when // grayed, the label itself says why. const bool manual = (m_autoMode == AutoMode::Manual); std::wstring toggleLabel = gameModeOn ? L"Disable Steamless Mode" : L"Enable Steamless Mode"; if (!connected) toggleLabel += L" (no controller detected)"; else if (!manual) toggleLabel += L" (managed by Auto Mode)"; UINT toggleFlags = MF_STRING | ((connected && manual) ? MF_ENABLED : MF_GRAYED); AppendMenuW(menu, toggleFlags, IDM_TOGGLE, toggleLabel.c_str()); HMENU modeMenu = CreatePopupMenu(); AppendMenuW(modeMenu, MF_STRING | (manual ? MF_CHECKED : MF_UNCHECKED), IDM_MODE_MANUAL, L"Manual"); AppendMenuW(modeMenu, MF_STRING | (m_autoMode == AutoMode::OffWhileSteam ? MF_CHECKED : MF_UNCHECKED), IDM_MODE_STEAM, L"Auto - Off while Steam running"); AppendMenuW(modeMenu, MF_STRING | (m_autoMode == AutoMode::OffOnlyInGame ? MF_CHECKED : MF_UNCHECKED), IDM_MODE_GAME, L"Auto - Off ONLY while in Steam game"); AppendMenuW(menu, MF_STRING | MF_POPUP, reinterpret_cast(modeMenu), L"Control Mode"); AppendMenuW(menu, MF_SEPARATOR, 0, nullptr); AppendMenuW(menu, MF_STRING | (trackpadOn ? MF_CHECKED : MF_UNCHECKED), IDM_TRACKPAD, L"Enable Trackpad Mouse"); AppendMenuW(menu, MF_STRING | (backMouseOn ? MF_CHECKED : MF_UNCHECKED), IDM_BACKBUTTONS, L"Back Buttons as Mouse Click"); 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); bool isPS = (m_controller->GetControllerPlatform() == ControllerPlatform::PlayStation); HMENU platformMenu = CreatePopupMenu(); AppendMenuW(platformMenu, MF_STRING | (!isPS ? MF_CHECKED : MF_UNCHECKED), IDM_PLATFORM_XBOX, L"Xbox"); AppendMenuW(platformMenu, MF_STRING | (isPS ? MF_CHECKED : MF_UNCHECKED), IDM_PLATFORM_PS, L"PlayStation"); AppendMenuW(menu, MF_STRING | MF_POPUP, reinterpret_cast(platformMenu), L"Controller Platform"); AppendMenuW(menu, MF_SEPARATOR, 0, nullptr); 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_OPENLOG, L"Open Event Log"); AppendMenuW(menu, MF_STRING | (m_notificationsEnabled ? MF_CHECKED : MF_UNCHECKED), IDM_NOTIFICATIONS, L"Disconnect Notifications"); AppendMenuW(menu, MF_STRING, IDM_EXIT, L"Exit"); // SetForegroundWindow is required for the menu to dismiss on click-away. SetForegroundWindow(m_hwnd); POINT pt; GetCursorPos(&pt); TrackPopupMenu(menu, TPM_RIGHTALIGN | TPM_BOTTOMALIGN | TPM_RIGHTBUTTON, pt.x, pt.y, 0, m_hwnd, nullptr); DestroyMenu(menu); }