allow all apps to execute keyboard key in background

This commit is contained in:
jonas.bark@gmx.de
2026-01-17 11:58:15 +01:00
parent 586b148ed3
commit 6b541e1d14

View File

@@ -1,10 +1,10 @@
#include "keypress_simulator_windows_plugin.h" #include "keypress_simulator_windows_plugin.h"
// This must be included before many other Windows headers. // This must be included before many other Windows headers.
#include <windows.h> #include <flutter_windows.h>
#include <psapi.h> #include <psapi.h>
#include <string.h> #include <string.h>
#include <flutter_windows.h> #include <windows.h>
#include <flutter/method_channel.h> #include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h> #include <flutter/plugin_registrar_windows.h>
@@ -30,7 +30,8 @@ struct FindWindowData {
}; };
BOOL CALLBACK EnumWindowsCallback(HWND hwnd, LPARAM lParam); BOOL CALLBACK EnumWindowsCallback(HWND hwnd, LPARAM lParam);
HWND FindTargetWindow(const std::string& processName, const std::string& windowTitle); HWND FindTargetWindow(const std::string& processName,
const std::string& windowTitle);
// static // static
void KeypressSimulatorWindowsPlugin::RegisterWithRegistrar( void KeypressSimulatorWindowsPlugin::RegisterWithRegistrar(
@@ -71,54 +72,50 @@ void KeypressSimulatorWindowsPlugin::SimulateKeyPress(
} }
// List of compatible training apps to look for // List of compatible training apps to look for
std::vector<std::string> compatibleApps = { std::vector<std::string> compatibleApps = {"MyWhooshHD.exe", "MyWhoosh.exe",
"MyWhooshHD.exe", "indieVelo.exe", "biketerra.exe",
"indieVelo.exe", "Rouvy.exe"};
"biketerra.exe",
"ROUVY.exe"
};
// Apps that should receive key events directly without taking focus (allows media/apps to stay on top)
const std::vector<std::string> backgroundInputApps = {
"ROUVY.exe"
};
// Try to find and focus (or directly target) a compatible app // Try to find and focus (or directly target) a compatible app
std::string foundProcessName; std::string foundProcessName;
bool supportsBackgroundInput = false; bool supportsBackgroundInput = true;
HWND targetWindow = NULL; HWND targetWindow = NULL;
for (const std::string& processName : compatibleApps) { for (const std::string& processName : compatibleApps) {
targetWindow = FindTargetWindow(processName, ""); targetWindow = FindTargetWindow(processName, "");
if (targetWindow != NULL) { if (targetWindow != NULL) {
foundProcessName = processName; foundProcessName = processName;
// For background-capable apps, prefer sending keys directly to their window to avoid stealing focus
supportsBackgroundInput = std::find(backgroundInputApps.begin(), backgroundInputApps.end(), processName) != backgroundInputApps.end();
if (!supportsBackgroundInput && GetForegroundWindow() != targetWindow) { if (!supportsBackgroundInput && GetForegroundWindow() != targetWindow) {
SetForegroundWindow(targetWindow); SetForegroundWindow(targetWindow);
Sleep(50); // Brief delay to ensure window is focused Sleep(50); // Brief delay to ensure window is focused
} }
break; break;
} }
} }
// If we found a target window that supports background input and it's not focused, send messages directly // If we found a target window that supports background input and it's not
// focused, send messages directly
auto postKeyMessage = [](HWND hwnd, UINT vkCode, bool down) { auto postKeyMessage = [](HWND hwnd, UINT vkCode, bool down) {
const WORD scanCode = static_cast<WORD>(MapVirtualKey(vkCode, MAPVK_VK_TO_VSC)); const WORD scanCode =
// Build lParam with repeat count 1 and scan code; set transition states for key up static_cast<WORD>(MapVirtualKey(vkCode, MAPVK_VK_TO_VSC));
// Build lParam with repeat count 1 and scan code; set transition states for
// key up
LPARAM lParam = 1 | (static_cast<LPARAM>(scanCode) << 16); LPARAM lParam = 1 | (static_cast<LPARAM>(scanCode) << 16);
if (vkCode == VK_LEFT || vkCode == VK_RIGHT || vkCode == VK_UP || vkCode == VK_DOWN || if (vkCode == VK_LEFT || vkCode == VK_RIGHT || vkCode == VK_UP ||
vkCode == VK_INSERT || vkCode == VK_DELETE || vkCode == VK_HOME || vkCode == VK_END || vkCode == VK_DOWN || vkCode == VK_INSERT || vkCode == VK_DELETE ||
vkCode == VK_PRIOR || vkCode == VK_NEXT) { vkCode == VK_HOME || vkCode == VK_END || vkCode == VK_PRIOR ||
lParam |= (1 << 24); // extended key vkCode == VK_NEXT) {
lParam |= (1 << 24); // extended key
} }
if (!down) { if (!down) {
lParam |= (1 << 30); // previous key state lParam |= (1 << 30); // previous key state
lParam |= (1 << 31); // transition state lParam |= (1 << 31); // transition state
} }
PostMessage(hwnd, down ? WM_KEYDOWN : WM_KEYUP, vkCode, lParam); PostMessage(hwnd, down ? WM_KEYDOWN : WM_KEYUP, vkCode, lParam);
}; };
auto sendKeyToWindow = [&postKeyMessage](HWND hwnd, const std::vector<std::string>& mods, UINT keyCode, bool down) { auto sendKeyToWindow = [&postKeyMessage](HWND hwnd,
const std::vector<std::string>& mods,
UINT keyCode, bool down) {
auto handleModifier = [&postKeyMessage, hwnd](UINT vk, bool press) { auto handleModifier = [&postKeyMessage, hwnd](UINT vk, bool press) {
postKeyMessage(hwnd, vk, press); postKeyMessage(hwnd, vk, press);
}; };
@@ -153,10 +150,8 @@ void KeypressSimulatorWindowsPlugin::SimulateKeyPress(
} }
}; };
if (targetWindow != NULL && if (targetWindow != NULL && !foundProcessName.empty() &&
!foundProcessName.empty() && supportsBackgroundInput && GetForegroundWindow() != targetWindow) {
supportsBackgroundInput &&
GetForegroundWindow() != targetWindow) {
sendKeyToWindow(targetWindow, modifiers, keyCode, keyDown); sendKeyToWindow(targetWindow, modifiers, keyCode, keyDown);
result->Success(flutter::EncodableValue(true)); result->Success(flutter::EncodableValue(true));
return; return;
@@ -174,7 +169,8 @@ void KeypressSimulatorWindowsPlugin::SimulateKeyPress(
}; };
// Helper function to process modifiers // Helper function to process modifiers
auto processModifiers = [&sendModifierKey](const std::vector<std::string>& mods, bool down) { auto processModifiers = [&sendModifierKey](
const std::vector<std::string>& mods, bool down) {
for (const std::string& modifier : mods) { for (const std::string& modifier : mods) {
if (modifier == "shiftModifier") { if (modifier == "shiftModifier") {
sendModifierKey(VK_SHIFT, down); sendModifierKey(VK_SHIFT, down);
@@ -198,12 +194,13 @@ void KeypressSimulatorWindowsPlugin::SimulateKeyPress(
INPUT in = {0}; INPUT in = {0};
in.type = INPUT_KEYBOARD; in.type = INPUT_KEYBOARD;
in.ki.wVk = 0; // when using SCANCODE, set VK=0 in.ki.wVk = 0; // when using SCANCODE, set VK=0
in.ki.wScan = sc; in.ki.wScan = sc;
in.ki.dwFlags = KEYEVENTF_SCANCODE | (keyDown ? 0 : KEYEVENTF_KEYUP); in.ki.dwFlags = KEYEVENTF_SCANCODE | (keyDown ? 0 : KEYEVENTF_KEYUP);
if (keyCode == VK_LEFT || keyCode == VK_RIGHT || keyCode == VK_UP || keyCode == VK_DOWN || if (keyCode == VK_LEFT || keyCode == VK_RIGHT || keyCode == VK_UP ||
keyCode == VK_INSERT || keyCode == VK_DELETE || keyCode == VK_HOME || keyCode == VK_END || keyCode == VK_DOWN || keyCode == VK_INSERT || keyCode == VK_DELETE ||
keyCode == VK_PRIOR || keyCode == VK_NEXT) { keyCode == VK_HOME || keyCode == VK_END || keyCode == VK_PRIOR ||
keyCode == VK_NEXT) {
in.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY; in.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
} }
SendInput(1, &in, sizeof(INPUT)); SendInput(1, &in, sizeof(INPUT));
@@ -222,7 +219,6 @@ void KeypressSimulatorWindowsPlugin::SimulateKeyPress(
void KeypressSimulatorWindowsPlugin::SimulateMouseClick( void KeypressSimulatorWindowsPlugin::SimulateMouseClick(
const flutter::MethodCall<flutter::EncodableValue>& method_call, const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) { std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
const EncodableMap& args = std::get<EncodableMap>(*method_call.arguments()); const EncodableMap& args = std::get<EncodableMap>(*method_call.arguments());
double x = 0; double x = 0;
double y = 0; double y = 0;
@@ -230,12 +226,12 @@ void KeypressSimulatorWindowsPlugin::SimulateMouseClick(
bool keyDown = std::get<bool>(args.at(EncodableValue("keyDown"))); bool keyDown = std::get<bool>(args.at(EncodableValue("keyDown")));
auto it_x = args.find(EncodableValue("x")); auto it_x = args.find(EncodableValue("x"));
if (it_x != args.end() && std::holds_alternative<double>(it_x->second)) { if (it_x != args.end() && std::holds_alternative<double>(it_x->second)) {
x = std::get<double>(it_x->second); x = std::get<double>(it_x->second);
} }
auto it_y = args.find(EncodableValue("y")); auto it_y = args.find(EncodableValue("y"));
if (it_y != args.end() && std::holds_alternative<double>(it_y->second)) { if (it_y != args.end() && std::holds_alternative<double>(it_y->second)) {
y = std::get<double>(it_y->second); y = std::get<double>(it_y->second);
} }
// Get the monitor containing the target point and its DPI // Get the monitor containing the target point and its DPI
@@ -243,7 +239,7 @@ void KeypressSimulatorWindowsPlugin::SimulateMouseClick(
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
double scale_factor = dpi / 96.0; double scale_factor = dpi / 96.0;
// Scale the coordinates according to the DPI scaling // Scale the coordinates according to the DPI scaling
int scaled_x = static_cast<int>(x * scale_factor); int scaled_x = static_cast<int>(x * scale_factor);
int scaled_y = static_cast<int>(y * scale_factor); int scaled_y = static_cast<int>(y * scale_factor);
@@ -256,14 +252,14 @@ void KeypressSimulatorWindowsPlugin::SimulateMouseClick(
input.type = INPUT_MOUSE; input.type = INPUT_MOUSE;
if (keyDown) { if (keyDown) {
// Mouse left button down // Mouse left button down
input.mi.dwFlags = MOUSEEVENTF_LEFTDOWN; input.mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
SendInput(1, &input, sizeof(INPUT)); SendInput(1, &input, sizeof(INPUT));
} else { } else {
// Mouse left button up // Mouse left button up
input.mi.dwFlags = MOUSEEVENTF_LEFTUP; input.mi.dwFlags = MOUSEEVENTF_LEFTUP;
SendInput(1, &input, sizeof(INPUT)); SendInput(1, &input, sizeof(INPUT));
} }
result->Success(flutter::EncodableValue(true)); result->Success(flutter::EncodableValue(true));
@@ -274,7 +270,7 @@ BOOL CALLBACK EnumWindowsCallback(HWND hwnd, LPARAM lParam) {
// Check if window is visible and not minimized // Check if window is visible and not minimized
if (!IsWindowVisible(hwnd) || IsIconic(hwnd)) { if (!IsWindowVisible(hwnd) || IsIconic(hwnd)) {
return TRUE; // Continue enumeration return TRUE; // Continue enumeration
} }
// Get window title // Get window title
@@ -284,7 +280,8 @@ BOOL CALLBACK EnumWindowsCallback(HWND hwnd, LPARAM lParam) {
// Get process name // Get process name
DWORD processId; DWORD processId;
GetWindowThreadProcessId(hwnd, &processId); GetWindowThreadProcessId(hwnd, &processId);
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processId); HANDLE hProcess =
OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processId);
char processName[MAX_PATH]; char processName[MAX_PATH];
if (hProcess) { if (hProcess) {
DWORD size = sizeof(processName); DWORD size = sizeof(processName);
@@ -292,7 +289,7 @@ BOOL CALLBACK EnumWindowsCallback(HWND hwnd, LPARAM lParam) {
// Extract just the filename from the full path // Extract just the filename from the full path
char* filename = strrchr(processName, '\\'); char* filename = strrchr(processName, '\\');
if (filename) { if (filename) {
filename++; // Skip the backslash filename++; // Skip the backslash
} else { } else {
filename = processName; filename = processName;
} }
@@ -301,7 +298,7 @@ BOOL CALLBACK EnumWindowsCallback(HWND hwnd, LPARAM lParam) {
if (!data->targetProcessName.empty() && if (!data->targetProcessName.empty() &&
_stricmp(filename, data->targetProcessName.c_str()) == 0) { _stricmp(filename, data->targetProcessName.c_str()) == 0) {
data->foundWindow = hwnd; data->foundWindow = hwnd;
return FALSE; // Stop enumeration return FALSE; // Stop enumeration
} }
} }
CloseHandle(hProcess); CloseHandle(hProcess);
@@ -311,13 +308,14 @@ BOOL CALLBACK EnumWindowsCallback(HWND hwnd, LPARAM lParam) {
if (!data->targetWindowTitle.empty() && if (!data->targetWindowTitle.empty() &&
_stricmp(windowTitle, data->targetWindowTitle.c_str()) == 0) { _stricmp(windowTitle, data->targetWindowTitle.c_str()) == 0) {
data->foundWindow = hwnd; data->foundWindow = hwnd;
return FALSE; // Stop enumeration return FALSE; // Stop enumeration
} }
return TRUE; // Continue enumeration return TRUE; // Continue enumeration
} }
HWND FindTargetWindow(const std::string& processName, const std::string& windowTitle) { HWND FindTargetWindow(const std::string& processName,
const std::string& windowTitle) {
FindWindowData data; FindWindowData data;
data.targetProcessName = processName; data.targetProcessName = processName;
data.targetWindowTitle = windowTitle; data.targetWindowTitle = windowTitle;
@@ -330,19 +328,15 @@ HWND FindTargetWindow(const std::string& processName, const std::string& windowT
void KeypressSimulatorWindowsPlugin::SimulateMediaKey( void KeypressSimulatorWindowsPlugin::SimulateMediaKey(
const flutter::MethodCall<flutter::EncodableValue>& method_call, const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) { std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
const EncodableMap& args = std::get<EncodableMap>(*method_call.arguments()); const EncodableMap& args = std::get<EncodableMap>(*method_call.arguments());
std::string keyIdentifier = std::get<std::string>(args.at(EncodableValue("key"))); std::string keyIdentifier =
std::get<std::string>(args.at(EncodableValue("key")));
// Map string identifier to Windows virtual key codes // Map string identifier to Windows virtual key codes
static const std::unordered_map<std::string, UINT> keyMap = { static const std::unordered_map<std::string, UINT> keyMap = {
{"playPause", VK_MEDIA_PLAY_PAUSE}, {"playPause", VK_MEDIA_PLAY_PAUSE}, {"stop", VK_MEDIA_STOP},
{"stop", VK_MEDIA_STOP}, {"next", VK_MEDIA_NEXT_TRACK}, {"previous", VK_MEDIA_PREV_TRACK},
{"next", VK_MEDIA_NEXT_TRACK}, {"volumeUp", VK_VOLUME_UP}, {"volumeDown", VK_VOLUME_DOWN}};
{"previous", VK_MEDIA_PREV_TRACK},
{"volumeUp", VK_VOLUME_UP},
{"volumeDown", VK_VOLUME_DOWN}
};
auto it = keyMap.find(keyIdentifier); auto it = keyMap.find(keyIdentifier);
if (it == keyMap.end()) { if (it == keyMap.end()) {
@@ -355,7 +349,7 @@ void KeypressSimulatorWindowsPlugin::SimulateMediaKey(
INPUT inputs[2] = {}; INPUT inputs[2] = {};
inputs[0].type = INPUT_KEYBOARD; inputs[0].type = INPUT_KEYBOARD;
inputs[0].ki.wVk = static_cast<WORD>(vkCode); inputs[0].ki.wVk = static_cast<WORD>(vkCode);
inputs[0].ki.dwFlags = 0; // Key down inputs[0].ki.dwFlags = 0; // Key down
// Send key up event // Send key up event
inputs[1].type = INPUT_KEYBOARD; inputs[1].type = INPUT_KEYBOARD;
@@ -371,7 +365,6 @@ void KeypressSimulatorWindowsPlugin::SimulateMediaKey(
result->Success(flutter::EncodableValue(true)); result->Success(flutter::EncodableValue(true));
} }
void KeypressSimulatorWindowsPlugin::HandleMethodCall( void KeypressSimulatorWindowsPlugin::HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue>& method_call, const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) { std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {